From 48378843fdceff75cab222fb1dda22bce1295a38 Mon Sep 17 00:00:00 2001 From: Jakub Frelik Date: Thu, 9 Nov 2023 18:39:24 +0100 Subject: [PATCH 1/9] feat: added Microsoft as oauth provider --- playground/app.vue | 9 +- playground/auth.d.ts | 15 +-- .../server/routes/auth/microsoft.get.ts | 13 ++ src/module.ts | 6 + src/runtime/server/lib/oauth/microsoft.ts | 114 ++++++++++++++++++ src/runtime/server/utils/oauth.ts | 4 +- 6 files changed, 151 insertions(+), 10 deletions(-) create mode 100644 playground/server/routes/auth/microsoft.get.ts create mode 100644 src/runtime/server/lib/oauth/microsoft.ts diff --git a/playground/app.vue b/playground/app.vue index 712412ff..6c8a8845 100644 --- a/playground/app.vue +++ b/playground/app.vue @@ -56,11 +56,16 @@ const { loggedIn, session, clear } = useUserSession() Login with Auth0 + Login with Microsoft + + Logout diff --git a/playground/auth.d.ts b/playground/auth.d.ts index 27100dd6..d030b540 100644 --- a/playground/auth.d.ts +++ b/playground/auth.d.ts @@ -1,12 +1,13 @@ declare module '#auth-utils' { interface UserSession { user: { - spotify?: any - github?: any - google?: any - twitch?: any - auth0?: any - } - loggedInAt: number + spotify?: any; + github?: any; + google?: any; + twitch?: any; + auth0?: any; + microsoft?: any; + }; + loggedInAt: number; } } diff --git a/playground/server/routes/auth/microsoft.get.ts b/playground/server/routes/auth/microsoft.get.ts new file mode 100644 index 00000000..bf071a61 --- /dev/null +++ b/playground/server/routes/auth/microsoft.get.ts @@ -0,0 +1,13 @@ +export default oauth.microsoftEventHandler({ + async onSuccess(event, { user }) { + await setUserSession(event, { + user: { + microsoft: user, + }, + loggedInAt: Date.now() + }) + + return sendRedirect(event, '/') + } + }) + \ No newline at end of file diff --git a/src/module.ts b/src/module.ts index a121ee68..fef93751 100644 --- a/src/module.ts +++ b/src/module.ts @@ -95,5 +95,11 @@ export default defineNuxtModule({ clientSecret: '', domain: '' }) + // Microsoft OAuth + runtimeConfig.oauth.microsoft = defu(runtimeConfig.oauth.microsoft, { + clientId: '', + clientSecret: '', + tenant: '', + }) } }) diff --git a/src/runtime/server/lib/oauth/microsoft.ts b/src/runtime/server/lib/oauth/microsoft.ts new file mode 100644 index 00000000..93d358e3 --- /dev/null +++ b/src/runtime/server/lib/oauth/microsoft.ts @@ -0,0 +1,114 @@ +import type { H3Event, H3Error } from 'h3' +import { eventHandler, createError, getQuery, getRequestURL, sendRedirect } from 'h3' +import { withQuery, parsePath } from 'ufo' +import { ofetch } from 'ofetch' +import { defu } from 'defu' +import { useRuntimeConfig } from '#imports' + +export interface OAuthMicrosoftConfig { + /** + * Microsoft OAuth Client ID + * @default process.env.NUXT_OAUTH_MICROSOFT_CLIENT_ID + */ + clientId?: string + /** + * Microsoft OAuth Client Secret + * @default process.env.NUXT_OAUTH_MICROSOFT_CLIENT_SECRET + */ + clientSecret?: string + /** + * Microsoft OAuth Tenant ID + * @default process.env.NUXT_OAUTH_MICROSOFT_TENANT + */ + tenant?: string + /** + * Microsoft OAuth Scope + * @default [] + * @see https://learn.microsoft.com/en-us/entra/identity-platform/scopes-oidc + * @example ['User.Read'] + */ + scope?: string[] +} + +interface OAuthConfig { + config?: OAuthMicrosoftConfig + onSuccess: (event: H3Event, result: { user: any, tokens: any }) => Promise | void + onError?: (event: H3Event, error: H3Error) => Promise | void +} + +export function microsoftEventHandler({ config, onSuccess, onError }: OAuthConfig) { + return eventHandler(async (event: H3Event) => { + // @ts-ignore + config = defu(config, useRuntimeConfig(event).oauth?.microsoft) as OAuthMicrosoftConfig + const { code } = getQuery(event) + + if (!config.clientId || !config.clientSecret || !config.tenant) { + const error = createError({ + statusCode: 500, + message: 'Missing NUXT_OAUTH_MICROSOFT_CLIENT_ID or NUXT_OAUTH_MICROSOFT_CLIENT_SECRET or NUXT_OAUTH_MICROSOFT_TENANT env variables.' + }) + if (!onError) throw error + return onError(event, error) + } + const authorizationURL = `https://login.microsoftonline.com/${config.tenant}/oauth2/v2.0/authorize` + const tokenURL = `https://login.microsoftonline.com/${config.tenant}/oauth2/v2.0/token` + + const redirectUrl = getRequestURL(event).href + if (!code) { + + config.scope = config.scope || ['User.Read'] + // Redirect to Microsoft Oauth page + return sendRedirect( + event, + withQuery(authorizationURL as string, { + client_id: config.clientId, + response_type: 'code', + redirect_uri: redirectUrl, + scope: config.scope.join('%20'), + }) + ) + } + + const data = new URLSearchParams() + data.append('grant_type', 'authorization_code') + data.append('client_id', config.clientId) + data.append('client_secret', config.clientSecret) + data.append('redirect_uri', parsePath(redirectUrl).pathname) + data.append('code', String(code)) + + const tokens: any = await ofetch( + tokenURL as string, + { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: data, + } + ).catch(error => { + return { error } + }) + if (tokens.error) { + const error = createError({ + statusCode: 401, + message: `Microsoft login failed: ${tokens.error?.data?.error_description || 'Unknown error'}`, + data: tokens + }) + if (!onError) throw error + return onError(event, error) + } + + const tokenType = tokens.token_type + const accessToken = tokens.access_token + const user: any = await ofetch('https://graph.microsoft.com/v1.0/me', { + headers: { + Authorization: `${tokenType} ${accessToken}` + } + }) + + return onSuccess(event, { + tokens, + user + }) + }) +} diff --git a/src/runtime/server/utils/oauth.ts b/src/runtime/server/utils/oauth.ts index 680b9b1f..82045f60 100644 --- a/src/runtime/server/utils/oauth.ts +++ b/src/runtime/server/utils/oauth.ts @@ -3,11 +3,13 @@ import { googleEventHandler } from '../lib/oauth/google' import { spotifyEventHandler } from '../lib/oauth/spotify' import { twitchEventHandler } from '../lib/oauth/twitch' import { auth0EventHandler } from '../lib/oauth/auth0' +import { microsoftEventHandler} from '../lib/oauth/microsoft' export const oauth = { githubEventHandler, spotifyEventHandler, googleEventHandler, twitchEventHandler, - auth0EventHandler + auth0EventHandler, + microsoftEventHandler } From 5d13544df5f858a322f03b581f6b630c46a3f4e0 Mon Sep 17 00:00:00 2001 From: Jakub Frelik Date: Thu, 9 Nov 2023 18:44:33 +0100 Subject: [PATCH 2/9] feat: added Microsoft OAuth configuration to .env.example --- playground/.env.example | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/playground/.env.example b/playground/.env.example index 9de44bf1..00301b56 100644 --- a/playground/.env.example +++ b/playground/.env.example @@ -15,3 +15,7 @@ NUXT_OAUTH_TWITCH_CLIENT_SECRET= NUXT_OAUTH_AUTH0_CLIENT_ID= NUXT_OAUTH_AUTH0_CLIENT_SECRET= NUXT_OAUTH_AUTH0_DOMAIN= +# Microsoft OAuth +NUXT_OAUTH_MICROSOFT_CLIENT_ID= +NUXT_OAUTH_MICROSOFT_CLIENT_SECRET= +NUXT_OAUTH_MICROSOFT_TENANT= \ No newline at end of file From d954c3cbde855ddeb613ad4acd99a55c6501bfdc Mon Sep 17 00:00:00 2001 From: Jakub Frelik Date: Mon, 13 Nov 2023 17:29:50 +0100 Subject: [PATCH 3/9] feat: Added support for Azure Government --- src/runtime/server/lib/oauth/microsoft.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/runtime/server/lib/oauth/microsoft.ts b/src/runtime/server/lib/oauth/microsoft.ts index 93d358e3..f306d276 100644 --- a/src/runtime/server/lib/oauth/microsoft.ts +++ b/src/runtime/server/lib/oauth/microsoft.ts @@ -28,6 +28,12 @@ export interface OAuthMicrosoftConfig { * @example ['User.Read'] */ scope?: string[] + /** + * Microsoft OAuth US Government + * @see https://learn.microsoft.com/en-us/azure/azure-government/documentation-government-aad-auth-qs + * @default false + */ + usGov?: boolean } interface OAuthConfig { @@ -50,8 +56,9 @@ export function microsoftEventHandler({ config, onSuccess, onError }: OAuthConfi if (!onError) throw error return onError(event, error) } - const authorizationURL = `https://login.microsoftonline.com/${config.tenant}/oauth2/v2.0/authorize` - const tokenURL = `https://login.microsoftonline.com/${config.tenant}/oauth2/v2.0/token` + + const authorizationURL = `https://login.microsoftonline.${config.usGov ? 'us' : 'com'}/${config.tenant}/oauth2/v2.0/authorize` + const tokenURL = `https://login.microsoftonline.${config.usGov ? 'us' : 'com'}/${config.tenant}/oauth2/v2.0/token` const redirectUrl = getRequestURL(event).href if (!code) { @@ -100,7 +107,7 @@ export function microsoftEventHandler({ config, onSuccess, onError }: OAuthConfi const tokenType = tokens.token_type const accessToken = tokens.access_token - const user: any = await ofetch('https://graph.microsoft.com/v1.0/me', { + const user: any = await ofetch(`https://graph.microsoft.${config.usGov ? 'us' : 'com'}/v1.0/me`, { headers: { Authorization: `${tokenType} ${accessToken}` } From 99b9469dec32cc69c17b4d9c34d908a169b79f90 Mon Sep 17 00:00:00 2001 From: Jakub Frelik Date: Mon, 13 Nov 2023 21:24:54 +0100 Subject: [PATCH 4/9] feat: Added usGov env to module config --- src/module.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/module.ts b/src/module.ts index fef93751..e0ab9d92 100644 --- a/src/module.ts +++ b/src/module.ts @@ -100,6 +100,7 @@ export default defineNuxtModule({ clientId: '', clientSecret: '', tenant: '', + usGov: false }) } }) From e8415427b03f1a2354c94a71a5dc2ef905800666 Mon Sep 17 00:00:00 2001 From: Jakub Frelik Date: Mon, 13 Nov 2023 21:33:16 +0100 Subject: [PATCH 5/9] fix: discord login button --- playground/app.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/playground/app.vue b/playground/app.vue index a064fa5a..20719081 100644 --- a/playground/app.vue +++ b/playground/app.vue @@ -65,6 +65,7 @@ const { loggedIn, session, clear } = useUserSession() > Login with Microsoft + Date: Mon, 20 Nov 2023 18:09:36 +0100 Subject: [PATCH 6/9] feat: Microsoft login error handling --- src/runtime/server/lib/oauth/microsoft.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/runtime/server/lib/oauth/microsoft.ts b/src/runtime/server/lib/oauth/microsoft.ts index f306d276..9fbeb47d 100644 --- a/src/runtime/server/lib/oauth/microsoft.ts +++ b/src/runtime/server/lib/oauth/microsoft.ts @@ -111,7 +111,19 @@ export function microsoftEventHandler({ config, onSuccess, onError }: OAuthConfi headers: { Authorization: `${tokenType} ${accessToken}` } + }).catch(error => { + return { error } }) + if (user.error) { + console.log(user.error) + const error = createError({ + statusCode: 401, + message: `Microsoft login failed: ${user.error || 'Unknown error'}`, + data: user + }) + if (!onError) throw error + return onError(event, error) + } return onSuccess(event, { tokens, From 5deef5fd6f190d97723484468ae3f47b193d7317 Mon Sep 17 00:00:00 2001 From: Jakub Frelik Date: Mon, 20 Nov 2023 18:11:49 +0100 Subject: [PATCH 7/9] feat: Update Microsoft OAuth configuration --- src/module.ts | 5 +++- src/runtime/server/lib/oauth/microsoft.ts | 34 +++++++++++++++-------- 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/src/module.ts b/src/module.ts index 50b335d6..afb62dc3 100644 --- a/src/module.ts +++ b/src/module.ts @@ -100,7 +100,10 @@ export default defineNuxtModule({ clientId: '', clientSecret: '', tenant: '', - usGov: false + scope: [], + authorizationURL: '', + tokenURL: '', + userURL: '' }) // Discord OAuth runtimeConfig.oauth.discord = defu(runtimeConfig.oauth.discord, { diff --git a/src/runtime/server/lib/oauth/microsoft.ts b/src/runtime/server/lib/oauth/microsoft.ts index 9fbeb47d..d44087d0 100644 --- a/src/runtime/server/lib/oauth/microsoft.ts +++ b/src/runtime/server/lib/oauth/microsoft.ts @@ -23,17 +23,28 @@ export interface OAuthMicrosoftConfig { tenant?: string /** * Microsoft OAuth Scope - * @default [] + * @default ['User.Read'] * @see https://learn.microsoft.com/en-us/entra/identity-platform/scopes-oidc - * @example ['User.Read'] */ scope?: string[] /** - * Microsoft OAuth US Government - * @see https://learn.microsoft.com/en-us/azure/azure-government/documentation-government-aad-auth-qs - * @default false + * Microsoft OAuth Authorization URL + * @default https://login.microsoftonline.com/${tenant}/oauth2/v2.0/authorize + * @see https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow */ - usGov?: boolean + authorizationURL?: string + /** + * Microsoft OAuth Token URL + * @default https://login.microsoftonline.com/${tenant}/oauth2/v2.0/token + * @see https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow + */ + tokenURL?: string + /** + * Microsoft OAuth User URL + * @default https://graph.microsoft.com/v1.0/me + * @see https://docs.microsoft.com/en-us/graph/api/user-get?view=graph-rest-1.0&tabs=http + */ + userURL?: string } interface OAuthConfig { @@ -57,13 +68,13 @@ export function microsoftEventHandler({ config, onSuccess, onError }: OAuthConfi return onError(event, error) } - const authorizationURL = `https://login.microsoftonline.${config.usGov ? 'us' : 'com'}/${config.tenant}/oauth2/v2.0/authorize` - const tokenURL = `https://login.microsoftonline.${config.usGov ? 'us' : 'com'}/${config.tenant}/oauth2/v2.0/token` + const authorizationURL = config.authorizationURL || `https://login.microsoftonline.com/${config.tenant}/oauth2/v2.0/authorize` + const tokenURL = config.tokenURL || `https://login.microsoftonline.com/${config.tenant}/oauth2/v2.0/token` const redirectUrl = getRequestURL(event).href if (!code) { - config.scope = config.scope || ['User.Read'] + const scope = config.scope && config.scope.length > 0 ? config.scope : ['User.Read'] // Redirect to Microsoft Oauth page return sendRedirect( event, @@ -71,7 +82,7 @@ export function microsoftEventHandler({ config, onSuccess, onError }: OAuthConfi client_id: config.clientId, response_type: 'code', redirect_uri: redirectUrl, - scope: config.scope.join('%20'), + scope: scope.join('%20'), }) ) } @@ -107,7 +118,8 @@ export function microsoftEventHandler({ config, onSuccess, onError }: OAuthConfi const tokenType = tokens.token_type const accessToken = tokens.access_token - const user: any = await ofetch(`https://graph.microsoft.${config.usGov ? 'us' : 'com'}/v1.0/me`, { + const userURL = config.userURL || 'https://graph.microsoft.com/v1.0/me' + const user: any = await ofetch(userURL, { headers: { Authorization: `${tokenType} ${accessToken}` } From 2ad17eabe27a64b594a5fe52e7313a7b78c1ac38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Chopin?= Date: Wed, 29 Nov 2023 15:35:04 +0100 Subject: [PATCH 8/9] chore: remove extra logs --- src/runtime/server/lib/oauth/discord.ts | 1 - src/runtime/server/lib/oauth/microsoft.ts | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/runtime/server/lib/oauth/discord.ts b/src/runtime/server/lib/oauth/discord.ts index 868fe925..06d3c4c4 100644 --- a/src/runtime/server/lib/oauth/discord.ts +++ b/src/runtime/server/lib/oauth/discord.ts @@ -114,7 +114,6 @@ export function discordEventHandler({ config, onSuccess, onError }: OAuthConfig) return { error } }) if (tokens.error) { - console.log(tokens) const error = createError({ statusCode: 401, message: `Discord login failed: ${tokens.error?.data?.error_description || 'Unknown error'}`, diff --git a/src/runtime/server/lib/oauth/microsoft.ts b/src/runtime/server/lib/oauth/microsoft.ts index d44087d0..db00305f 100644 --- a/src/runtime/server/lib/oauth/microsoft.ts +++ b/src/runtime/server/lib/oauth/microsoft.ts @@ -73,7 +73,7 @@ export function microsoftEventHandler({ config, onSuccess, onError }: OAuthConfi const redirectUrl = getRequestURL(event).href if (!code) { - + const scope = config.scope && config.scope.length > 0 ? config.scope : ['User.Read'] // Redirect to Microsoft Oauth page return sendRedirect( @@ -127,7 +127,6 @@ export function microsoftEventHandler({ config, onSuccess, onError }: OAuthConfi return { error } }) if (user.error) { - console.log(user.error) const error = createError({ statusCode: 401, message: `Microsoft login failed: ${user.error || 'Unknown error'}`, From 2234d0a789389f51f766f029af17574c2fad841c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Chopin?= Date: Wed, 29 Nov 2023 15:36:31 +0100 Subject: [PATCH 9/9] add microsoft --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 31942531..7f8bc97f 100644 --- a/README.md +++ b/README.md @@ -149,12 +149,13 @@ It can also be set using environment variables: #### Supported OAuth Providers - Auth0 +- Battle.net - Discord - GitHub - Google +- Microsoft - Spotify - Twitch -- Battle.net You can add your favorite provider by creating a new file in [src/runtime/server/lib/oauth/](./src/runtime/server/lib/oauth/).