Skip to content

Commit

Permalink
feat: added discord auth provider (#7)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
3 people authored Nov 11, 2023
1 parent 86226ad commit ba78a8b
Show file tree
Hide file tree
Showing 8 changed files with 178 additions and 3 deletions.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/).

Expand Down
3 changes: 3 additions & 0 deletions playground/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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=
10 changes: 10 additions & 0 deletions playground/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,16 @@ const { loggedIn, session, clear } = useUserSession()
>
Login with Auth0
</UButton>
<UButton
v-if="!loggedIn || !session.user.discord"
to="/auth/discord"
icon="i-simple-icons-discord"
external
color="gray"
size="xs"
>
Login with Discord
</UButton>
<UButton
v-if="loggedIn"
color="gray"
Expand Down
1 change: 1 addition & 0 deletions playground/auth.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ declare module '#auth-utils' {
google?: any
twitch?: any
auth0?: any
discord?: any
}
loggedInAt: number
}
Expand Down
12 changes: 12 additions & 0 deletions playground/server/routes/auth/discord.get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export default oauth.discordEventHandler({
async onSuccess(event, { user }) {
await setUserSession(event, {
user: {
discord: user,
},
loggedInAt: Date.now()
})

return sendRedirect(event, '/')
}
})
5 changes: 5 additions & 0 deletions src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,5 +95,10 @@ export default defineNuxtModule<ModuleOptions>({
clientSecret: '',
domain: ''
})
// Discord OAuth
runtimeConfig.oauth.discord = defu(runtimeConfig.oauth.discord, {
clientId: '',
clientSecret: ''
})
}
})
141 changes: 141 additions & 0 deletions src/runtime/server/lib/oauth/discord.ts
Original file line number Diff line number Diff line change
@@ -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> | void
onError?: (event: H3Event, error: H3Error) => Promise<void> | 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
})
})
}
4 changes: 3 additions & 1 deletion src/runtime/server/utils/oauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

0 comments on commit ba78a8b

Please sign in to comment.