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
}