From e7a0dbd3dd897b2a62a011df795c89fb9171b965 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?U=C4=A3is?= Date: Wed, 29 Nov 2023 16:13:22 +0200 Subject: [PATCH 1/3] feat: add max_age param for auth0 (#26) Forces reauthentication whenever user logs out, and shows Auth0 Universal Login Can be set when defining auth0EventHandler, same as `emailRequired` --- src/runtime/server/lib/oauth/auth0.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/runtime/server/lib/oauth/auth0.ts b/src/runtime/server/lib/oauth/auth0.ts index 1cd52bab..b1fd967b 100644 --- a/src/runtime/server/lib/oauth/auth0.ts +++ b/src/runtime/server/lib/oauth/auth0.ts @@ -39,6 +39,12 @@ export interface OAuthAuth0Config { * @default false */ emailRequired?: boolean + /** + * Maximum Authentication Age. If the elapsed time is greater than this value, the OP must attempt to actively re-authenticate the end-user. + * @default 0 + * @see https://auth0.com/docs/authenticate/login/max-age-reauthentication + */ + maxAge?: number } export function auth0EventHandler({ config, onSuccess, onError }: OAuthConfig) { @@ -73,6 +79,7 @@ export function auth0EventHandler({ config, onSuccess, onError }: OAuthConfig Date: Wed, 29 Nov 2023 15:36:41 +0100 Subject: [PATCH 2/3] feat: added Microsoft as oauth provider (#8) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: added Microsoft as oauth provider * feat: added Microsoft OAuth configuration to .env.example * feat: Added support for Azure Government * feat: Added usGov env to module config * fix: discord login button * feat: Microsoft login error handling * feat: Update Microsoft OAuth configuration * chore: remove extra logs * add microsoft --------- Co-authored-by: Sébastien Chopin --- README.md | 3 +- playground/.env.example | 6 +- playground/app.vue | 7 + playground/auth.d.ts | 1 + .../server/routes/auth/microsoft.get.ts | 13 ++ src/module.ts | 10 ++ src/runtime/server/lib/oauth/discord.ts | 1 - src/runtime/server/lib/oauth/microsoft.ts | 144 ++++++++++++++++++ src/runtime/server/utils/oauth.ts | 2 + 9 files changed, 184 insertions(+), 3 deletions(-) create mode 100644 playground/server/routes/auth/microsoft.get.ts create mode 100644 src/runtime/server/lib/oauth/microsoft.ts diff --git a/README.md b/README.md index 776639ea..dda07090 100644 --- a/README.md +++ b/README.md @@ -150,12 +150,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/). diff --git a/playground/.env.example b/playground/.env.example index 2dec1b9a..ef228c05 100644 --- a/playground/.env.example +++ b/playground/.env.example @@ -15,9 +15,13 @@ 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= # Discord NUXT_OAUTH_DISCORD_CLIENT_ID= NUXT_OAUTH_DISCORD_CLIENT_SECRET= # Battle.net OAuth NUXT_OAUTH_BATTLEDOTNET_CLIENT_ID= -NUXT_OAUTH_BATTLEDOTNET_CLIENT_SECRET= +NUXT_OAUTH_BATTLEDOTNET_CLIENT_SECRET= \ No newline at end of file diff --git a/playground/app.vue b/playground/app.vue index 3075ffe5..1ec6a360 100644 --- a/playground/app.vue +++ b/playground/app.vue @@ -44,6 +44,13 @@ const providers = computed(() => [ disabled: Boolean(user.value?.battledotnet), icon: 'i-simple-icons-battledotnet', }, + { + label: user.value?.microsoft?.displayName || 'Microsoft', + to: '/auth/microsoft', + disabled: Boolean(user.value?.microsoft), + icon: 'i-simple-icons-microsoft', + } + ].map(p => ({ ...p, prefetch: false, diff --git a/playground/auth.d.ts b/playground/auth.d.ts index 9da7af5f..b86b8224 100644 --- a/playground/auth.d.ts +++ b/playground/auth.d.ts @@ -6,6 +6,7 @@ declare module '#auth-utils' { google?: any twitch?: any auth0?: any + microsoft?: any; discord?: any battledotnet?: any } 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 e73b64e0..7b08149c 100644 --- a/src/module.ts +++ b/src/module.ts @@ -98,6 +98,16 @@ export default defineNuxtModule({ clientSecret: '', domain: '' }) + // Microsoft OAuth + runtimeConfig.oauth.microsoft = defu(runtimeConfig.oauth.microsoft, { + clientId: '', + clientSecret: '', + tenant: '', + scope: [], + authorizationURL: '', + tokenURL: '', + userURL: '' + }) // Discord OAuth runtimeConfig.oauth.discord = defu(runtimeConfig.oauth.discord, { clientId: '', diff --git a/src/runtime/server/lib/oauth/discord.ts b/src/runtime/server/lib/oauth/discord.ts index 20a3f681..325f5003 100644 --- a/src/runtime/server/lib/oauth/discord.ts +++ b/src/runtime/server/lib/oauth/discord.ts @@ -109,7 +109,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 new file mode 100644 index 00000000..db00305f --- /dev/null +++ b/src/runtime/server/lib/oauth/microsoft.ts @@ -0,0 +1,144 @@ +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 ['User.Read'] + * @see https://learn.microsoft.com/en-us/entra/identity-platform/scopes-oidc + */ + scope?: string[] + /** + * 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 + */ + 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 { + 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 = 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) { + + const scope = config.scope && config.scope.length > 0 ? 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: 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 userURL = config.userURL || 'https://graph.microsoft.com/v1.0/me' + const user: any = await ofetch(userURL, { + headers: { + Authorization: `${tokenType} ${accessToken}` + } + }).catch(error => { + return { error } + }) + if (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, + user + }) + }) +} diff --git a/src/runtime/server/utils/oauth.ts b/src/runtime/server/utils/oauth.ts index 95a0c4c9..a242dbdf 100644 --- a/src/runtime/server/utils/oauth.ts +++ b/src/runtime/server/utils/oauth.ts @@ -3,6 +3,7 @@ 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' import { discordEventHandler } from '../lib/oauth/discord' import { battledotnetEventHandler } from '../lib/oauth/battledotnet' @@ -12,6 +13,7 @@ export const oauth = { googleEventHandler, twitchEventHandler, auth0EventHandler, + microsoftEventHandler, discordEventHandler, battledotnetEventHandler } From 36caddac5d40672de4de4113599e64e22b8208a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Chopin?= Date: Wed, 29 Nov 2023 15:40:36 +0100 Subject: [PATCH 3/3] chore(release): v0.0.9 --- CHANGELOG.md | 14 ++++++++++++++ package.json | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 890ad928..187b0a61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,20 @@ # Changelog +## v0.0.9 + +[compare changes](https://github.com/Atinux/nuxt-auth-utils/compare/v0.0.8...v0.0.9) + +### 🚀 Enhancements + +- Add max_age param for auth0 ([#26](https://github.com/Atinux/nuxt-auth-utils/pull/26)) +- Added Microsoft as oauth provider ([#8](https://github.com/Atinux/nuxt-auth-utils/pull/8)) + +### ❤️ Contributors + +- Jakub Frelik +- Uģis ([@BerzinsU](http://github.com/BerzinsU)) + ## v0.0.8 [compare changes](https://github.com/Atinux/nuxt-auth-utils/compare/v0.0.7...v0.0.8) diff --git a/package.json b/package.json index 431a4cb1..b72ee42c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nuxt-auth-utils", - "version": "0.0.8", + "version": "0.0.9", "description": "Minimalist Auth module for Nuxt with SSR", "repository": "Atinux/nuxt-auth-utils", "license": "MIT",