From ba78a8b9fea8077051414f272417d52cd4b76e6d Mon Sep 17 00:00:00 2001 From: h+ Date: Sat, 11 Nov 2023 16:22:54 +0100 Subject: [PATCH] feat: added discord auth provider (#7) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: discord auth provider * Update discord.get.ts Typo Github -> Discord * fix: Update redirectUrl to include server route * refactor: cleanup discord provider * fix: Added discord env vars in example env * feat: Module addition of Discord vars * [autofix.ci] apply automated fixes * refactor: removed domain env usage * chore: update --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Sébastien Chopin --- README.md | 5 +- playground/.env.example | 3 + playground/app.vue | 10 ++ playground/auth.d.ts | 1 + playground/server/routes/auth/discord.get.ts | 12 ++ src/module.ts | 5 + src/runtime/server/lib/oauth/discord.ts | 141 +++++++++++++++++++ src/runtime/server/utils/oauth.ts | 4 +- 8 files changed, 178 insertions(+), 3 deletions(-) create mode 100644 playground/server/routes/auth/discord.get.ts create mode 100644 src/runtime/server/lib/oauth/discord.ts diff --git a/README.md b/README.md index 84bc162e..fe98e400 100644 --- a/README.md +++ b/README.md @@ -147,11 +147,12 @@ It can also be set using environment variables: #### Supported OAuth Providers +- Auth0 +- Discord - GitHub -- Spotify - Google +- Spotify - Twitch -- Auth0 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 9de44bf1..87bac3d2 100644 --- a/playground/.env.example +++ b/playground/.env.example @@ -15,3 +15,6 @@ NUXT_OAUTH_TWITCH_CLIENT_SECRET= NUXT_OAUTH_AUTH0_CLIENT_ID= NUXT_OAUTH_AUTH0_CLIENT_SECRET= NUXT_OAUTH_AUTH0_DOMAIN= +# Discord +NUXT_OAUTH_DISCORD_CLIENT_ID= +NUXT_OAUTH_DISCORD_CLIENT_SECRET= diff --git a/playground/app.vue b/playground/app.vue index 712412ff..bdd0bc46 100644 --- a/playground/app.vue +++ b/playground/app.vue @@ -55,6 +55,16 @@ const { loggedIn, session, clear } = useUserSession() > Login with Auth0 + + Login with Discord + ({ clientSecret: '', domain: '' }) + // Discord OAuth + runtimeConfig.oauth.discord = defu(runtimeConfig.oauth.discord, { + clientId: '', + clientSecret: '' + }) } }) diff --git a/src/runtime/server/lib/oauth/discord.ts b/src/runtime/server/lib/oauth/discord.ts new file mode 100644 index 00000000..868fe925 --- /dev/null +++ b/src/runtime/server/lib/oauth/discord.ts @@ -0,0 +1,141 @@ +import type { H3Event, H3Error } from 'h3' +import { eventHandler, createError, getQuery, getRequestURL, sendRedirect } from 'h3' +import { withQuery, parseURL, stringifyParsedURL } from 'ufo' +import { ofetch } from 'ofetch' +import { defu } from 'defu' +import { useRuntimeConfig } from '#imports' + +export interface OAuthDiscordConfig { + /** + * Discord OAuth Client ID + * @default process.env.NUXT_OAUTH_DISCORD_CLIENT_ID + */ + clientId?: string + /** + * Discord OAuth Client Secret + * @default process.env.NUXT_OAUTH_DISCORD_CLIENT_SECRET + */ + clientSecret?: string + /** + * Discord OAuth Scope + * @default [] + * @see https://discord.com/developers/docs/topics/oauth2#shared-resources-oauth2-scopes + * @example ['identify', 'email'] + * Without the identify scope the user will not be returned. + */ + scope?: string[] + /** + * Require email from user, adds the ['email'] scope if not present. + * @default false + */ + emailRequired?: boolean, + /** + * Require profile from user, adds the ['identify'] scope if not present. + * @default true + */ + profileRequired?: boolean + /** + * Discord OAuth Authorization URL + * @default 'https://discord.com/oauth2/authorize' + */ + authorizationURL?: string + /** + * Discord OAuth Token URL + * @default 'https://discord.com/api/oauth2/token' + */ + tokenURL?: string +} + +interface OAuthConfig { + config?: OAuthDiscordConfig + onSuccess: (event: H3Event, result: { user: any, tokens: any }) => Promise | void + onError?: (event: H3Event, error: H3Error) => Promise | void +} + +export function discordEventHandler({ config, onSuccess, onError }: OAuthConfig) { + return eventHandler(async (event: H3Event) => { + // @ts-ignore + config = defu(config, useRuntimeConfig(event).oauth?.discord, { + authorizationURL: 'https://discord.com/oauth2/authorize', + tokenURL: 'https://discord.com/api/oauth2/token', + profileRequired: true + }) as OAuthDiscordConfig + const { code } = getQuery(event) + + if (!config.clientId || !config.clientSecret) { + const error = createError({ + statusCode: 500, + message: 'Missing NUXT_OAUTH_DISCORD_CLIENT_ID or NUXT_OAUTH_DISCORD_CLIENT_SECRET env variables.' + }) + if (!onError) throw error + return onError(event, error) + } + + const redirectUrl = getRequestURL(event).href + if (!code) { + config.scope = config.scope || [] + if (config.emailRequired && !config.scope.includes('email')) { + config.scope.push('email') + } + if (config.profileRequired && !config.scope.includes('identify')) { + config.scope.push('identify') + } + + // Redirect to Discord Oauth page + return sendRedirect( + event, + withQuery(config.authorizationURL as string, { + response_type: 'code', + client_id: config.clientId, + redirect_uri: redirectUrl, + scope: config.scope.join(' ') + }) + ) + } + + const parsedRedirectUrl = parseURL(redirectUrl) + parsedRedirectUrl.search = '' + const tokens: any = await ofetch( + config.tokenURL as string, + { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: new URLSearchParams({ + client_id: config.clientId, + client_secret: config.clientSecret, + grant_type: 'authorization_code', + redirect_uri: stringifyParsedURL(parsedRedirectUrl), + code: code as string, + }).toString() + } + ).catch(error => { + return { error } + }) + if (tokens.error) { + console.log(tokens) + const error = createError({ + statusCode: 401, + message: `Discord login failed: ${tokens.error?.data?.error_description || 'Unknown error'}`, + data: tokens + }) + + if (!onError) throw error + return onError(event, error) + } + + const accessToken = tokens.access_token + const user: any = await ofetch('https://discord.com/api/users/@me', { + headers: { + 'user-agent': 'Nuxt Auth Utils', + Authorization: `Bearer ${accessToken}` + } + }) + + return onSuccess(event, { + tokens, + user + }) + }) +} diff --git a/src/runtime/server/utils/oauth.ts b/src/runtime/server/utils/oauth.ts index 680b9b1f..b5cf41b5 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 { discordEventHandler } from '../lib/oauth/discord' export const oauth = { githubEventHandler, spotifyEventHandler, googleEventHandler, twitchEventHandler, - auth0EventHandler + auth0EventHandler, + discordEventHandler }