Skip to content

Commit ba78a8b

Browse files
justserdarautofix-ci[bot]atinux
authored
feat: added discord auth provider (#7)
* 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]>
1 parent 86226ad commit ba78a8b

File tree

8 files changed

+178
-3
lines changed

8 files changed

+178
-3
lines changed

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,11 +147,12 @@ It can also be set using environment variables:
147147

148148
#### Supported OAuth Providers
149149

150+
- Auth0
151+
- Discord
150152
- GitHub
151-
- Spotify
152153
- Google
154+
- Spotify
153155
- Twitch
154-
- Auth0
155156

156157
You can add your favorite provider by creating a new file in [src/runtime/server/lib/oauth/](./src/runtime/server/lib/oauth/).
157158

playground/.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,6 @@ NUXT_OAUTH_TWITCH_CLIENT_SECRET=
1515
NUXT_OAUTH_AUTH0_CLIENT_ID=
1616
NUXT_OAUTH_AUTH0_CLIENT_SECRET=
1717
NUXT_OAUTH_AUTH0_DOMAIN=
18+
# Discord
19+
NUXT_OAUTH_DISCORD_CLIENT_ID=
20+
NUXT_OAUTH_DISCORD_CLIENT_SECRET=

playground/app.vue

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,16 @@ const { loggedIn, session, clear } = useUserSession()
5555
>
5656
Login with Auth0
5757
</UButton>
58+
<UButton
59+
v-if="!loggedIn || !session.user.discord"
60+
to="/auth/discord"
61+
icon="i-simple-icons-discord"
62+
external
63+
color="gray"
64+
size="xs"
65+
>
66+
Login with Discord
67+
</UButton>
5868
<UButton
5969
v-if="loggedIn"
6070
color="gray"

playground/auth.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ declare module '#auth-utils' {
66
google?: any
77
twitch?: any
88
auth0?: any
9+
discord?: any
910
}
1011
loggedInAt: number
1112
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export default oauth.discordEventHandler({
2+
async onSuccess(event, { user }) {
3+
await setUserSession(event, {
4+
user: {
5+
discord: user,
6+
},
7+
loggedInAt: Date.now()
8+
})
9+
10+
return sendRedirect(event, '/')
11+
}
12+
})

src/module.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,5 +95,10 @@ export default defineNuxtModule<ModuleOptions>({
9595
clientSecret: '',
9696
domain: ''
9797
})
98+
// Discord OAuth
99+
runtimeConfig.oauth.discord = defu(runtimeConfig.oauth.discord, {
100+
clientId: '',
101+
clientSecret: ''
102+
})
98103
}
99104
})
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import type { H3Event, H3Error } from 'h3'
2+
import { eventHandler, createError, getQuery, getRequestURL, sendRedirect } from 'h3'
3+
import { withQuery, parseURL, stringifyParsedURL } from 'ufo'
4+
import { ofetch } from 'ofetch'
5+
import { defu } from 'defu'
6+
import { useRuntimeConfig } from '#imports'
7+
8+
export interface OAuthDiscordConfig {
9+
/**
10+
* Discord OAuth Client ID
11+
* @default process.env.NUXT_OAUTH_DISCORD_CLIENT_ID
12+
*/
13+
clientId?: string
14+
/**
15+
* Discord OAuth Client Secret
16+
* @default process.env.NUXT_OAUTH_DISCORD_CLIENT_SECRET
17+
*/
18+
clientSecret?: string
19+
/**
20+
* Discord OAuth Scope
21+
* @default []
22+
* @see https://discord.com/developers/docs/topics/oauth2#shared-resources-oauth2-scopes
23+
* @example ['identify', 'email']
24+
* Without the identify scope the user will not be returned.
25+
*/
26+
scope?: string[]
27+
/**
28+
* Require email from user, adds the ['email'] scope if not present.
29+
* @default false
30+
*/
31+
emailRequired?: boolean,
32+
/**
33+
* Require profile from user, adds the ['identify'] scope if not present.
34+
* @default true
35+
*/
36+
profileRequired?: boolean
37+
/**
38+
* Discord OAuth Authorization URL
39+
* @default 'https://discord.com/oauth2/authorize'
40+
*/
41+
authorizationURL?: string
42+
/**
43+
* Discord OAuth Token URL
44+
* @default 'https://discord.com/api/oauth2/token'
45+
*/
46+
tokenURL?: string
47+
}
48+
49+
interface OAuthConfig {
50+
config?: OAuthDiscordConfig
51+
onSuccess: (event: H3Event, result: { user: any, tokens: any }) => Promise<void> | void
52+
onError?: (event: H3Event, error: H3Error) => Promise<void> | void
53+
}
54+
55+
export function discordEventHandler({ config, onSuccess, onError }: OAuthConfig) {
56+
return eventHandler(async (event: H3Event) => {
57+
// @ts-ignore
58+
config = defu(config, useRuntimeConfig(event).oauth?.discord, {
59+
authorizationURL: 'https://discord.com/oauth2/authorize',
60+
tokenURL: 'https://discord.com/api/oauth2/token',
61+
profileRequired: true
62+
}) as OAuthDiscordConfig
63+
const { code } = getQuery(event)
64+
65+
if (!config.clientId || !config.clientSecret) {
66+
const error = createError({
67+
statusCode: 500,
68+
message: 'Missing NUXT_OAUTH_DISCORD_CLIENT_ID or NUXT_OAUTH_DISCORD_CLIENT_SECRET env variables.'
69+
})
70+
if (!onError) throw error
71+
return onError(event, error)
72+
}
73+
74+
const redirectUrl = getRequestURL(event).href
75+
if (!code) {
76+
config.scope = config.scope || []
77+
if (config.emailRequired && !config.scope.includes('email')) {
78+
config.scope.push('email')
79+
}
80+
if (config.profileRequired && !config.scope.includes('identify')) {
81+
config.scope.push('identify')
82+
}
83+
84+
// Redirect to Discord Oauth page
85+
return sendRedirect(
86+
event,
87+
withQuery(config.authorizationURL as string, {
88+
response_type: 'code',
89+
client_id: config.clientId,
90+
redirect_uri: redirectUrl,
91+
scope: config.scope.join(' ')
92+
})
93+
)
94+
}
95+
96+
const parsedRedirectUrl = parseURL(redirectUrl)
97+
parsedRedirectUrl.search = ''
98+
const tokens: any = await ofetch(
99+
config.tokenURL as string,
100+
{
101+
method: 'POST',
102+
headers: {
103+
'Content-Type': 'application/x-www-form-urlencoded'
104+
},
105+
body: new URLSearchParams({
106+
client_id: config.clientId,
107+
client_secret: config.clientSecret,
108+
grant_type: 'authorization_code',
109+
redirect_uri: stringifyParsedURL(parsedRedirectUrl),
110+
code: code as string,
111+
}).toString()
112+
}
113+
).catch(error => {
114+
return { error }
115+
})
116+
if (tokens.error) {
117+
console.log(tokens)
118+
const error = createError({
119+
statusCode: 401,
120+
message: `Discord login failed: ${tokens.error?.data?.error_description || 'Unknown error'}`,
121+
data: tokens
122+
})
123+
124+
if (!onError) throw error
125+
return onError(event, error)
126+
}
127+
128+
const accessToken = tokens.access_token
129+
const user: any = await ofetch('https://discord.com/api/users/@me', {
130+
headers: {
131+
'user-agent': 'Nuxt Auth Utils',
132+
Authorization: `Bearer ${accessToken}`
133+
}
134+
})
135+
136+
return onSuccess(event, {
137+
tokens,
138+
user
139+
})
140+
})
141+
}

src/runtime/server/utils/oauth.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ import { googleEventHandler } from '../lib/oauth/google'
33
import { spotifyEventHandler } from '../lib/oauth/spotify'
44
import { twitchEventHandler } from '../lib/oauth/twitch'
55
import { auth0EventHandler } from '../lib/oauth/auth0'
6+
import { discordEventHandler } from '../lib/oauth/discord'
67

78
export const oauth = {
89
githubEventHandler,
910
spotifyEventHandler,
1011
googleEventHandler,
1112
twitchEventHandler,
12-
auth0EventHandler
13+
auth0EventHandler,
14+
discordEventHandler
1315
}

0 commit comments

Comments
 (0)