Browse Source

github login flow

dev-oauth
Bernd Storath 1 week ago
parent
commit
26ea26b613
  1. 32
      src/app/components/Icons/Brands/GitHub.vue
  2. 0
      src/app/components/Icons/Brands/Google.vue
  3. 13
      src/app/pages/login.vue
  4. 2
      src/i18n/locales/en.json
  5. 1
      src/i18n/locales/pl.json
  6. 37
      src/server/api/auth/[provider]/callback.get.ts
  7. 3
      src/server/database/repositories/user/service.ts
  8. 79
      src/server/utils/oauth.ts

32
src/app/components/Icons/Brands/GitHub.vue

@ -0,0 +1,32 @@
<template>
<!-- By GitHub, CC BY 4.0, https://commons.wikimedia.org/w/index.php?curid=130805002 -->
<svg
version="1.0"
xmlns="http://www.w3.org/2000/svg"
width="240.000000pt"
height="240.000000pt"
viewBox="0 0 240.000000 240.000000"
preserveAspectRatio="xMidYMid meet"
>
<g
transform="translate(0.000000,240.000000) scale(0.100000,-0.100000)"
fill="#000000"
stroke="none"
>
<path
d="M970 2301 c-305 -68 -555 -237 -727 -493 -301 -451 -241 -1056 143
-1442 115 -116 290 -228 422 -271 49 -16 55 -16 77 -1 24 16 25 20 25 135 l0
118 -88 -5 c-103 -5 -183 13 -231 54 -17 14 -50 62 -73 106 -38 74 -66 108
-144 177 -26 23 -27 24 -9 37 43 32 130 1 185 -65 96 -117 133 -148 188 -160
49 -10 94 -6 162 14 9 3 21 24 27 48 6 23 22 58 35 77 l24 35 -81 16 c-170 35
-275 96 -344 200 -64 96 -85 179 -86 334 0 146 16 206 79 288 28 36 31 47 23
68 -15 36 -11 188 5 234 13 34 20 40 47 43 45 5 129 -24 214 -72 l73 -42 64
15 c91 21 364 20 446 0 l62 -16 58 35 c77 46 175 82 224 82 39 0 39 -1 55 -52
17 -59 20 -166 5 -217 -8 -30 -6 -39 16 -68 109 -144 121 -383 29 -579 -62
-129 -193 -219 -369 -252 l-84 -16 31 -55 32 -56 3 -223 4 -223 25 -16 c23
-15 28 -15 76 2 80 27 217 101 292 158 446 334 590 933 343 1431 -145 293
-419 518 -733 602 -137 36 -395 44 -525 15z"
/>
</g>
</svg>
</template>

0
src/app/components/Icons/Google.vue → src/app/components/Icons/Brands/Google.vue

13
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"
>
<IconsGoogle class="h-4 w-4" />
<span>{{ $t('login.signInWithGoogle') }}</span>
<IconsBrandsGoogle class="h-4 w-4" />
<span>{{ $t('login.signInWith', ['Google']) }}</span>
</a>
<!-- GitHub OAuth Button -->
<a
v-if="authMethods.providers?.github"
href="/api/auth/github"
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"
>
<IconsBrandsGitHub class="h-4 w-4" />
<span>{{ $t('login.signInWith', ['GitHub']) }}</span>
</a>
<!-- Divider -->

2
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",

1
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",

37
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) {

3
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,

79
src/server/utils/oauth.ts

@ -1,8 +1,17 @@
import type { H3Event } from 'h3';
import { discovery } from 'openid-client';
export const OAUTH_PROVIDERS = {
google: {
type OAuthConfig = {
server: string;
scope: string;
clientId: string | undefined;
clientSecret: string | undefined;
params: Record<string, string>;
isOIDC?: false;
userInfoFlow?: 'github';
};
const GoogleConfig: OAuthConfig = {
server: 'https://accounts.google.com',
scope: 'openid email profile',
clientId: process.env.OAUTH_GOOGLE_CLIENT_ID,
@ -11,7 +20,23 @@ export const OAUTH_PROVIDERS = {
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_USERINFO>(
OAUTH_GITHUB_FLOW.userinfo_endpoint,
{
headers: {
'User-Agent': 'wg-easy',
Authorization: `Bearer ${accessToken}`,
},
}
);
if (!response.email) {
const emailResponse = await $fetch<OAUTH_GITHUB_EMAIL>(
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,
};
}

Loading…
Cancel
Save