diff --git a/src/app/components/Icons/Brands/GitHub.vue b/src/app/components/Icons/Brands/GitHub.vue new file mode 100644 index 00000000..4c90a044 --- /dev/null +++ b/src/app/components/Icons/Brands/GitHub.vue @@ -0,0 +1,32 @@ + diff --git a/src/app/components/Icons/Google.vue b/src/app/components/Icons/Brands/Google.vue similarity index 100% rename from src/app/components/Icons/Google.vue rename to src/app/components/Icons/Brands/Google.vue diff --git a/src/app/pages/login.vue b/src/app/pages/login.vue index c701a068..387943c7 100644 --- a/src/app/pages/login.vue +++ b/src/app/pages/login.vue @@ -19,8 +19,17 @@ href="/api/auth/google" class="flex cursor-pointer items-center justify-center gap-2 rounded border border-gray-300 bg-white py-2 text-sm text-gray-700 shadow-sm transition hover:bg-gray-50 dark:border-neutral-600 dark:bg-neutral-800 dark:text-neutral-200 dark:hover:bg-neutral-700" > - - {{ $t('login.signInWithGoogle') }} + + {{ $t('login.signInWith', ['Google']) }} + + + + + {{ $t('login.signInWith', ['GitHub']) }} diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 4c065256..b8f2bc6e 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -72,7 +72,7 @@ }, "login": { "signIn": "Sign In", - "signInWithGoogle": "Sign in with Google", + "signInWith": "Sign In with {0}", "or": "or", "rememberMe": "Remember me", "rememberMeDesc": "Stay logged after closing the browser", diff --git a/src/i18n/locales/pl.json b/src/i18n/locales/pl.json index ebc989c3..be500131 100644 --- a/src/i18n/locales/pl.json +++ b/src/i18n/locales/pl.json @@ -72,7 +72,6 @@ }, "login": { "signIn": "Zaloguj się", - "signInWithGoogle": "Zaloguj się przez Google", "or": "lub", "rememberMe": "Zapamiętaj mnie", "rememberMeDesc": "Pozostań zalogowany po zamknięciu przeglądarki", diff --git a/src/server/api/auth/[provider]/callback.get.ts b/src/server/api/auth/[provider]/callback.get.ts index 5e2a5fe4..b1fd7105 100644 --- a/src/server/api/auth/[provider]/callback.get.ts +++ b/src/server/api/auth/[provider]/callback.get.ts @@ -1,7 +1,7 @@ import * as client from 'openid-client'; export default defineEventHandler(async (event) => { - const { config, provider } = await buildOauthConfig(event); + const { config, provider, providerConfig } = await buildOauthConfig(event); const session = await useWGSession(event); if ( @@ -19,12 +19,18 @@ export default defineEventHandler(async (event) => { const tokens = await client.authorizationCodeGrant(config, currentUrl, { pkceCodeVerifier: session.data.oauth_verifier, - expectedNonce: session.data.oauth_nonce, + expectedNonce: + providerConfig.isOIDC === false ? undefined : session.data.oauth_nonce, expectedState: session.data.oauth_state, - idTokenExpected: true, + idTokenExpected: providerConfig.isOIDC, }); - const subject = tokens.claims()?.sub; + type SubjectType = string | undefined | typeof client.skipSubjectCheck; + let subject: SubjectType = tokens.claims()?.sub; + if (providerConfig.isOIDC === false) { + subject = client.skipSubjectCheck; + } + if (!subject) { throw createError({ statusCode: 400, @@ -32,11 +38,21 @@ export default defineEventHandler(async (event) => { }); } - const userInfo = await client.fetchUserInfo( - config, - tokens.access_token, - subject - ); + let userInfo; + if (providerConfig.userInfoFlow === 'github') { + userInfo = await githubUserInfoFlow(tokens.access_token); + } else { + userInfo = await client.fetchUserInfo(config, tokens.access_token, subject); + } + + console.log(userInfo); + + if (!userInfo.sub) { + throw createError({ + statusCode: 400, + statusMessage: 'No sub set', + }); + } if (!userInfo.email) { throw createError({ @@ -55,8 +71,9 @@ export default defineEventHandler(async (event) => { const result = await Database.users.findOrCreateByProvider( provider, userInfo.sub, + userInfo.preferred_username || userInfo.email, userInfo.email, - userInfo.preferred_username || userInfo.name || userInfo.email + userInfo.name || 'User' ); if (!result.success) { diff --git a/src/server/database/repositories/user/service.ts b/src/server/database/repositories/user/service.ts index ad7e3a6c..37ea9bcc 100644 --- a/src/server/database/repositories/user/service.ts +++ b/src/server/database/repositories/user/service.ts @@ -108,6 +108,7 @@ export class UserService { async findOrCreateByProvider( provider: OAUTH_PROVIDER, oauthId: string, + username: string, email: string, name: string ) { @@ -142,7 +143,7 @@ export class UserService { // Create new user await this.#db.insert(user).values({ - username: email, + username, password: '--- no password ---', email, name, diff --git a/src/server/utils/oauth.ts b/src/server/utils/oauth.ts index 1be1220d..a22ab51e 100644 --- a/src/server/utils/oauth.ts +++ b/src/server/utils/oauth.ts @@ -1,17 +1,42 @@ import type { H3Event } from 'h3'; import { discovery } from 'openid-client'; -export const OAUTH_PROVIDERS = { - google: { - server: 'https://accounts.google.com', - scope: 'openid email profile', - clientId: process.env.OAUTH_GOOGLE_CLIENT_ID, - clientSecret: process.env.OAUTH_GOOGLE_CLIENT_SECRET, - params: { - access_type: 'online', - prompt: 'select_account', - }, +type OAuthConfig = { + server: string; + scope: string; + clientId: string | undefined; + clientSecret: string | undefined; + params: Record; + isOIDC?: false; + userInfoFlow?: 'github'; +}; + +const GoogleConfig: OAuthConfig = { + server: 'https://accounts.google.com', + scope: 'openid email profile', + clientId: process.env.OAUTH_GOOGLE_CLIENT_ID, + clientSecret: process.env.OAUTH_GOOGLE_CLIENT_SECRET, + params: { + access_type: 'online', + prompt: 'select_account', + }, +}; +const GithubConfig: OAuthConfig = { + server: 'https://github.com/login/oauth', + scope: 'read:user user:email', + clientId: process.env.OAUTH_GITHUB_CLIENT_ID, + clientSecret: process.env.OAUTH_GITHUB_CLIENT_SECRET, + params: { + allow_signup: 'false', + prompt: 'select_account', }, + isOIDC: false, + userInfoFlow: 'github', +}; + +export const OAUTH_PROVIDERS = { + google: GoogleConfig, + github: GithubConfig, }; export type OAUTH_PROVIDER = keyof typeof OAUTH_PROVIDERS; @@ -65,3 +90,53 @@ export async function buildOauthConfig(event: H3Event) { return { config, providerConfig: oauthProvider, provider }; } + +export async function githubUserInfoFlow(accessToken: string) { + const OAUTH_GITHUB_FLOW = { + userinfo_endpoint: 'https://api.github.com/user', + email_endpoint: 'https://api.github.com/user/emails', + }; + type OAUTH_GITHUB_USERINFO = { + id: number; + login: string; + avatar_url: string; + email: string | null; + name: string | null; + }; + type OAUTH_GITHUB_EMAIL = { + email: string; + primary: boolean; + verified: boolean; + visibility: string | null; + }[]; + + const response = await $fetch( + OAUTH_GITHUB_FLOW.userinfo_endpoint, + { + headers: { + 'User-Agent': 'wg-easy', + Authorization: `Bearer ${accessToken}`, + }, + } + ); + if (!response.email) { + const emailResponse = await $fetch( + OAUTH_GITHUB_FLOW.email_endpoint, + { + headers: { + 'User-Agent': 'wg-easy', + Authorization: `Bearer ${accessToken}`, + }, + } + ); + const primaryEmail = emailResponse.find((v) => v.primary && v.verified); + response.email = primaryEmail?.email || null; + } + return { + sub: response.id.toString(), + email: response.email, + email_verified: true, + preferred_username: response.login, + name: response.name || response.login, + }; +}