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/). 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 d2494886..894a8a12 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 48bd50aa..1aff8699 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 d480593d..afb62dc3 100644 --- a/src/module.ts +++ b/src/module.ts @@ -95,6 +95,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 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 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 }