Browse Source

properly allow multiple providers

dev-oauth
Bernd Storath 1 week ago
parent
commit
287c50f5ac
  1. 247
      src/app/components/Icons/Google.vue
  2. 51
      src/app/pages/login.vue
  3. 14
      src/server/api/auth/[provider]/callback.get.ts
  4. 1
      src/server/api/auth/[provider]/index.get.ts
  5. 10
      src/server/api/auth/methods.get.ts
  6. 6
      src/server/database/repositories/user/service.ts
  7. 15
      src/server/utils/config.ts
  8. 34
      src/server/utils/oauth.ts

247
src/app/components/Icons/Google.vue

@ -0,0 +1,247 @@
<template>
<!-- By Original: Google Vector: Designism, Liquinoid, and YeBoy371 - Own work based on: Google updating its G icon for the first time in 10 years, Public Domain, https://commons.wikimedia.org/w/index.php?curid=165142698 -->
<svg
version="1.1"
viewBox="0 0 268.1522 273.8827"
overflow="hidden"
xml:space="preserve"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
>
<defs>
<linearGradient id="a">
<stop offset="0" stop-color="#0fbc5c" />
<stop offset="1" stop-color="#0cba65" />
</linearGradient>
<linearGradient id="g">
<stop offset=".2312727" stop-color="#0fbc5f" />
<stop offset=".3115468" stop-color="#0fbc5f" />
<stop offset=".3660131" stop-color="#0fbc5e" />
<stop offset=".4575163" stop-color="#0fbc5d" />
<stop offset=".540305" stop-color="#12bc58" />
<stop offset=".6993464" stop-color="#28bf3c" />
<stop offset=".7712418" stop-color="#38c02b" />
<stop offset=".8605665" stop-color="#52c218" />
<stop offset=".9150327" stop-color="#67c30f" />
<stop offset="1" stop-color="#86c504" />
</linearGradient>
<linearGradient id="h">
<stop offset=".1416122" stop-color="#1abd4d" />
<stop offset=".2475151" stop-color="#6ec30d" />
<stop offset=".3115468" stop-color="#8ac502" />
<stop offset=".3660131" stop-color="#a2c600" />
<stop offset=".4456735" stop-color="#c8c903" />
<stop offset=".540305" stop-color="#ebcb03" />
<stop offset=".6156363" stop-color="#f7cd07" />
<stop offset=".6993454" stop-color="#fdcd04" />
<stop offset=".7712418" stop-color="#fdce05" />
<stop offset=".8605661" stop-color="#ffce0a" />
</linearGradient>
<linearGradient id="f">
<stop offset=".3159041" stop-color="#ff4c3c" />
<stop offset=".6038179" stop-color="#ff692c" />
<stop offset=".7268366" stop-color="#ff7825" />
<stop offset=".884534" stop-color="#ff8d1b" />
<stop offset="1" stop-color="#ff9f13" />
</linearGradient>
<linearGradient id="b">
<stop offset=".2312727" stop-color="#ff4541" />
<stop offset=".3115468" stop-color="#ff4540" />
<stop offset=".4575163" stop-color="#ff4640" />
<stop offset=".540305" stop-color="#ff473f" />
<stop offset=".6993464" stop-color="#ff5138" />
<stop offset=".7712418" stop-color="#ff5b33" />
<stop offset=".8605665" stop-color="#ff6c29" />
<stop offset="1" stop-color="#ff8c18" />
</linearGradient>
<linearGradient id="d">
<stop offset=".4084578" stop-color="#fb4e5a" />
<stop offset="1" stop-color="#ff4540" />
</linearGradient>
<linearGradient id="c">
<stop offset=".1315461" stop-color="#0cba65" />
<stop offset=".2097843" stop-color="#0bb86d" />
<stop offset=".2972969" stop-color="#09b479" />
<stop offset=".3962575" stop-color="#08ad93" />
<stop offset=".4771242" stop-color="#0aa6a9" />
<stop offset=".5684245" stop-color="#0d9cc6" />
<stop offset=".667385" stop-color="#1893dd" />
<stop offset=".7687273" stop-color="#258bf1" />
<stop offset=".8585063" stop-color="#3086ff" />
</linearGradient>
<linearGradient id="e">
<stop offset=".3660131" stop-color="#ff4e3a" />
<stop offset=".4575163" stop-color="#ff8a1b" />
<stop offset=".540305" stop-color="#ffa312" />
<stop offset=".6156363" stop-color="#ffb60c" />
<stop offset=".7712418" stop-color="#ffcd0a" />
<stop offset=".8605665" stop-color="#fecf0a" />
<stop offset=".9150327" stop-color="#fecf08" />
<stop offset="1" stop-color="#fdcd01" />
</linearGradient>
<linearGradient
id="s"
xlink:href="#a"
x1="219.6997"
y1="329.5351"
x2="254.4673"
y2="329.5351"
gradientUnits="userSpaceOnUse"
/>
<radialGradient
id="m"
xlink:href="#b"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(-1.936885,1.043001,1.455731,2.555422,290.5254,-400.6338)"
cx="109.6267"
cy="135.8619"
fx="109.6267"
fy="135.8619"
r="71.46001"
/>
<radialGradient
id="n"
xlink:href="#c"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(-3.512595,-4.45809,-1.692547,1.260616,870.8006,191.554)"
cx="45.25866"
cy="279.2738"
fx="45.25866"
fy="279.2738"
r="71.46001"
/>
<radialGradient
id="l"
xlink:href="#d"
cx="304.0166"
cy="118.0089"
fx="304.0166"
fy="118.0089"
r="47.85445"
gradientTransform="matrix(2.064353,-4.926832e-6,-2.901531e-6,2.592041,-297.6788,-151.7469)"
gradientUnits="userSpaceOnUse"
/>
<radialGradient
id="o"
xlink:href="#e"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(-0.2485783,2.083138,2.962486,0.3341668,-255.1463,-331.1636)"
cx="181.001"
cy="177.2013"
fx="181.001"
fy="177.2013"
r="71.46001"
/>
<radialGradient
id="p"
xlink:href="#f"
cx="207.6733"
cy="108.0972"
fx="207.6733"
fy="108.0972"
r="41.1025"
gradientTransform="matrix(-1.249206,1.343263,-3.896837,-3.425693,880.5011,194.9051)"
gradientUnits="userSpaceOnUse"
/>
<radialGradient
id="r"
xlink:href="#g"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(-1.936885,-1.043001,1.455731,-2.555422,290.5254,838.6834)"
cx="109.6267"
cy="135.8619"
fx="109.6267"
fy="135.8619"
r="71.46001"
/>
<radialGradient
id="j"
xlink:href="#h"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(-0.081402,-1.93722,2.926737,-0.1162508,-215.1345,632.8606)"
cx="154.8697"
cy="145.9691"
fx="154.8697"
fy="145.9691"
r="71.46001"
/>
<filter
id="q"
x="-.04842873"
y="-.0582241"
width="1.096857"
height="1.116448"
color-interpolation-filters="sRGB"
>
<feGaussianBlur stdDeviation="1.700914" />
</filter>
<filter
id="k"
x="-.01670084"
y="-.01009856"
width="1.033402"
height="1.020197"
color-interpolation-filters="sRGB"
>
<feGaussianBlur stdDeviation=".2419367" />
</filter>
<clipPath id="i" clipPathUnits="userSpaceOnUse">
<path
d="M371.3784 193.2406H237.0825v53.4375h77.167c-1.2405 7.5627-4.0259 15.0024-8.1049 21.7862-4.6734 7.7723-10.4511 13.6895-16.373 18.1957-17.7389 13.4983-38.42 16.2584-52.7828 16.2584-36.2824 0-67.2833-23.2865-79.2844-54.9287-.4843-1.1482-.8059-2.3344-1.1975-3.5068-2.652-8.0533-4.101-16.5825-4.101-25.4474 0-9.226 1.5691-18.0575 4.4301-26.3985 11.2851-32.8967 42.9849-57.4674 80.1789-57.4674 7.4811 0 14.6854.8843 21.5173 2.6481 15.6135 4.0309 26.6578 11.9698 33.4252 18.2494l40.834-39.7111c-24.839-22.616-57.2194-36.3201-95.8444-36.3201-30.8782-.00066-59.3863 9.55308-82.7477 25.6992-18.9454 13.0941-34.4833 30.6254-44.9695 50.9861-9.75366 18.8785-15.09441 39.7994-15.09441 62.2934 0 22.495 5.34891 43.6334 15.10261 62.3374v.126c10.3023 19.8567 25.3678 36.9537 43.6783 49.9878 15.9962 11.3866 44.6789 26.5516 84.0307 26.5516 22.6301 0 42.6867-4.0517 60.3748-11.6447 12.76-5.4775 24.0655-12.6217 34.3012-21.8036 13.5247-12.1323 24.1168-27.1388 31.3465-44.4041 7.2297-17.2654 11.097-36.7895 11.097-57.957 0-9.858-.9971-19.8694-2.6881-28.9684Z"
fill="#000"
/>
</clipPath>
</defs>
<g transform="matrix(0.957922,0,0,0.985255,-90.17436,-78.85577)">
<g clip-path="url(#i)">
<path
d="M92.07563 219.9585c.14844 22.14 6.5014 44.983 16.11767 63.4234v.1269c6.9482 13.3919 16.4444 23.9704 27.2604 34.4518l65.326-23.67c-12.3593-6.2344-14.2452-10.0546-23.1048-17.0253-9.0537-9.0658-15.8015-19.4735-20.0038-31.677h-.1693l.1693-.1269c-2.7646-8.0587-3.0373-16.6129-3.1393-25.5029Z"
fill="url(#j)"
filter="url(#k)"
/>
<path
d="M237.0835 79.02491c-6.4568 22.52569-3.988 44.42139 0 57.16129 7.4561.0055 14.6388.8881 21.4494 2.6464 15.6135 4.0309 26.6566 11.97 33.424 18.2496l41.8794-40.7256c-24.8094-22.58904-54.6663-37.2961-96.7528-37.33169Z"
fill="url(#l)"
filter="url(#k)"
/>
<path
d="M236.9434 78.84678c-31.6709-.00068-60.9107 9.79833-84.8718 26.35902-8.8968 6.149-17.0612 13.2521-24.3311 21.1509-1.9045 17.7429 14.2569 39.5507 46.2615 39.3702 15.5284-17.9373 38.4946-29.5427 64.0561-29.5427.0233 0 .046.0019.0693.002l-1.0439-57.33536c-.0472-.00003-.0929-.00406-.1401-.00406Z"
fill="url(#m)"
filter="url(#k)"
/>
<path
d="m341.4751 226.3788-28.2685 19.2848c-1.2405 7.5627-4.0278 15.0023-8.1068 21.7861-4.6734 7.7723-10.4506 13.6898-16.3725 18.196-17.7022 13.4704-38.3286 16.2439-52.6877 16.2553-14.8415 25.1018-17.4435 37.6749 1.0439 57.9342 22.8762-.0167 43.157-4.1174 61.0458-11.7965 12.9312-5.551 24.3879-12.7913 34.7609-22.0964 13.7061-12.295 24.4421-27.5034 31.7688-45.0003 7.3267-17.497 11.2446-37.2822 11.2446-58.7336Z"
fill="url(#n)"
filter="url(#k)"
/>
<path
d="M234.9956 191.2104v57.4981h136.0062c1.1962-7.8745 5.1523-18.0644 5.1523-26.5001 0-9.858-.9963-21.899-2.6873-30.998Z"
fill="#3086ff"
filter="url(#k)"
/>
<path
d="M128.3894 124.3268c-8.393 9.1191-15.5632 19.326-21.2483 30.3646-9.75351 18.8785-15.09402 41.8295-15.09402 64.3235 0 .317.02642.6271.02855.9436 4.31953 8.2244 59.66647 6.6495 62.45617 0-.0035-.3103-.0387-.6128-.0387-.9238 0-9.226 1.5696-16.0262 4.4306-24.3672 3.5294-10.2885 9.0557-19.7628 16.1223-27.9257 1.6019-2.0309 5.8748-6.3969 7.1214-9.0157.4749-.9975-.8621-1.5574-.9369-1.9085-.0836-.3927-1.8762-.0769-2.2778-.3694-1.2751-.9288-3.8001-1.4138-5.3334-1.8449-3.2772-.9215-8.7085-2.9536-11.7252-5.0601-9.5357-6.6586-24.417-14.6122-33.5047-24.2164Z"
fill="url(#o)"
filter="url(#k)"
/>
<path
d="M162.0989 155.8569c22.1123 13.3013 28.4714-6.7139 43.173-12.9771L179.698 90.21568c-9.4075 3.92642-18.2957 8.80465-26.5426 14.50442-12.316 8.5122-23.192 18.8995-32.1763 30.7204Z"
fill="url(#p)"
filter="url(#q)"
/>
<path
d="M171.0987 290.222c-29.6829 10.6413-34.3299 11.023-37.0622 29.2903 5.2213 5.0597 10.8312 9.74 16.7926 13.9835 15.9962 11.3867 46.766 26.5517 86.1178 26.5517.0462 0 .0904-.004.1366-.004v-59.1574c-.0298.0001-.064.002-.0938.002-14.7359 0-26.5113-3.8435-38.5848-10.5273-2.9768-1.6479-8.3775 2.7772-11.1229.799-3.7865-2.7284-12.8991 2.3508-16.1833-.9378Z"
fill="url(#r)"
filter="url(#k)"
/>
<path
d="M219.6997 299.0227v59.9959c5.506.6402 11.2361 1.0289 17.2472 1.0289 6.0259 0 11.8556-.3073 17.5204-.8723v-59.7481c-6.3482 1.0777-12.3272 1.461-17.4776 1.461-5.9318 0-11.7005-.6858-17.29-1.8654Z"
opacity=".5"
fill="url(#s)"
filter="url(#k)"
/>
</g>
</g>
</svg>
</template>

51
src/app/pages/login.vue

@ -12,40 +12,25 @@
<IconsAvatar class="m-5 h-10 w-10 text-white dark:text-white" />
</div>
<!-- Google OAuth Button -->
<a
v-if="authMethods?.google"
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"
>
<svg class="h-4 w-4" viewBox="0 0 24 24">
<path
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
fill="#4285F4"
/>
<path
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
fill="#34A853"
/>
<path
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
fill="#FBBC05"
/>
<path
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
fill="#EA4335"
/>
</svg>
<span>{{ $t('login.signInWithGoogle') }}</span>
</a>
<div v-if="authMethods" class="flex flex-col gap-5">
<!-- Google OAuth Button -->
<a
v-if="authMethods.providers?.google"
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>
</a>
<!-- Divider -->
<div v-if="authMethods?.google" class="flex items-center gap-2">
<div class="h-px flex-1 bg-gray-300 dark:bg-neutral-600"></div>
<span class="text-xs text-gray-500 dark:text-neutral-400">{{
$t('login.or')
}}</span>
<div class="h-px flex-1 bg-gray-300 dark:bg-neutral-600"></div>
<!-- Divider -->
<div v-if="authMethods.oauthEnabled" class="flex items-center gap-2">
<div class="h-px flex-1 bg-gray-300 dark:bg-neutral-600"></div>
<span class="text-xs text-gray-500 dark:text-neutral-400">{{
$t('login.or')
}}</span>
<div class="h-px flex-1 bg-gray-300 dark:bg-neutral-600"></div>
</div>
</div>
<!-- Classic Login Form -->

14
src/server/api/auth/[provider]/callback.get.ts

@ -28,7 +28,7 @@ export default defineEventHandler(async (event) => {
if (!subject) {
throw createError({
statusCode: 400,
statusMessage: 'Cant get subject',
statusMessage: "Can't get subject",
});
}
@ -56,7 +56,7 @@ export default defineEventHandler(async (event) => {
provider,
userInfo.sub,
userInfo.email,
userInfo.name || userInfo.email
userInfo.preferred_username || userInfo.name || userInfo.email
);
if (!result.success) {
@ -66,6 +66,12 @@ export default defineEventHandler(async (event) => {
statusMessage: 'User disabled',
});
}
if (result.error === 'USER_ALREADY_LINKED') {
throw createError({
statusCode: 401,
statusMessage: 'User already linked with different account or provider',
});
}
throw createError({
statusCode: 500,
statusMessage: 'Unexpected error',
@ -73,7 +79,7 @@ export default defineEventHandler(async (event) => {
}
// Create session
await session.update({
const data = await session.update({
userId: result.user.id,
oauth_nonce: undefined,
oauth_state: undefined,
@ -81,7 +87,7 @@ export default defineEventHandler(async (event) => {
});
SERVER_DEBUG(
`New OAuth Session for ${provider} ${result.user.id} (${result.user.username})`
`New OAuth Session: ${data.id} for ${result.user.id} (${result.user.username}) with ${provider}`
);
return sendRedirect(event, '/');

1
src/server/api/auth/[provider]/index.get.ts

@ -13,6 +13,7 @@ export default defineEventHandler(async (event) => {
const state = client.randomState();
const parameters: Record<string, string> = {
...providerConfig.params,
redirect_uri: redirectUri,
scope: providerConfig.scope,
code_challenge: codeChallenge,

10
src/server/api/auth/methods.get.ts

@ -1,5 +1,13 @@
export default defineEventHandler(() => {
return {
google: OAUTH_GOOGLE_ENV.ENABLED,
providers: WG_ENV.OAUTH_PROVIDERS?.reduce(
(acc, curr) => {
acc[curr] = true;
return acc;
},
{} as Record<OAUTH_PROVIDER, boolean>
),
oauthEnabled:
WG_ENV.OAUTH_PROVIDERS !== undefined && WG_ENV.OAUTH_PROVIDERS.length > 0,
};
});

6
src/server/database/repositories/user/service.ts

@ -127,6 +127,12 @@ export class UserService {
if (!existingUser.enabled) {
return { success: false as const, error: 'USER_DISABLED' as const };
}
if (existingUser.oauthProvider && existingUser.oauthId) {
return {
success: false as const,
error: 'USER_ALREADY_LINKED' as const,
};
}
await this.#db
.update(user)
.set({ oauthProvider: provider, oauthId: oauthId })

15
src/server/utils/config.ts

@ -39,17 +39,10 @@ export const WG_ENV = {
DISABLE_IPV6: process.env.DISABLE_IPV6 === 'true',
WG_EXECUTABLE: await detectAwg(),
DISABLE_VERSION_CHECK: process.env.DISABLE_VERSION_CHECK === 'true',
};
export const OAUTH_GOOGLE_ENV = {
/** Enable Google OAuth login */
ENABLED: process.env.OAUTH_GOOGLE_ENABLED === 'true',
/** Google OAuth Client ID */
CLIENT_ID: process.env.OAUTH_GOOGLE_CLIENT_ID || '',
/** Google OAuth Client Secret */
CLIENT_SECRET: process.env.OAUTH_GOOGLE_CLIENT_SECRET || '',
/** Allowed email domain (optional, e.g. "example.com") */
ALLOWED_DOMAIN: process.env.OAUTH_GOOGLE_ALLOWED_DOMAIN || '',
OAUTH_PROVIDERS: process.env.OAUTH_PROVIDERS?.split(',')
.map((v) => v.trim())
.filter((v) => isValidOauthProvider(v))
.filter((v) => isConfiguredOauthProvider(OAUTH_PROVIDERS[v])),
};
export const WG_INITIAL_ENV = {

34
src/server/utils/oauth.ts

@ -1,10 +1,16 @@
import type { H3Event } from 'h3';
import { discovery } from 'openid-client';
const OAUTH_PROVIDERS = {
export const OAUTH_PROVIDERS = {
google: {
server: 'https://accounts.google.com',
scope: 'openid email',
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',
},
},
};
@ -19,6 +25,18 @@ export function isValidOauthProvider(
return false;
}
export function isConfiguredOauthProvider(
oauthProvider: (typeof OAUTH_PROVIDERS)[OAUTH_PROVIDER]
): oauthProvider is (typeof OAUTH_PROVIDERS)[OAUTH_PROVIDER] & {
clientId: string;
clientSecret: string;
} {
if (!oauthProvider.clientId || !oauthProvider.clientSecret) {
return false;
}
return true;
}
export async function buildOauthConfig(event: H3Event) {
const provider = getRouterParam(event, 'provider');
if (!provider || !isValidOauthProvider(provider)) {
@ -29,11 +47,19 @@ export async function buildOauthConfig(event: H3Event) {
}
const oauthProvider = OAUTH_PROVIDERS[provider];
if (!isConfiguredOauthProvider(oauthProvider)) {
throw createError({
statusCode: 500,
statusMessage: 'Provider is not configured',
});
}
const config = await discovery(
new URL(oauthProvider.server),
OAUTH_GOOGLE_ENV.CLIENT_ID,
oauthProvider.clientId,
{
client_secret: OAUTH_GOOGLE_ENV.CLIENT_SECRET,
client_secret: oauthProvider.clientSecret,
}
);

Loading…
Cancel
Save