Skip to content

Commit ec17cb2

Browse files
ahmedrangelatinux
andauthored
feat: added twitch as supported oauth provider (#5)
* feat: added twitch as supported oauth provider * chore: improvements --------- Co-authored-by: Sébastien Chopin <[email protected]>
1 parent af4e2d1 commit ec17cb2

File tree

9 files changed

+194
-9
lines changed

9 files changed

+194
-9
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ Supported providers:
149149
- GitHub
150150
- Spotify
151151
- Google
152+
- Twitch
152153

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

@@ -158,6 +159,9 @@ Example: `~/server/routes/auth/github.get.ts`
158159

159160
```ts
160161
export default oauth.githubEventHandler({
162+
config: {
163+
emailRequired: true
164+
},
161165
async onSuccess(event, { user, tokens }) {
162166
await setUserSession(event, {
163167
user: {

playground/.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,6 @@ NUXT_OAUTH_SPOTIFY_CLIENT_SECRET=
88
# Google OAuth
99
NUXT_OAUTH_GOOGLE_CLIENT_ID=
1010
NUXT_OAUTH_GOOGLE_CLIENT_SECRET=
11+
# Twitch OAuth
12+
NUXT_OAUTH_TWITCH_CLIENT_ID=
13+
NUXT_OAUTH_TWITCH_CLIENT_SECRET=

playground/app.vue

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,16 @@ const { loggedIn, session, clear } = useUserSession()
4343
>
4444
Logout
4545
</UButton>
46+
<UButton
47+
v-if="!loggedIn || !session.user.twitch"
48+
to="/auth/twitch"
49+
icon="i-simple-icons-twitch"
50+
external
51+
color="gray"
52+
size="xs"
53+
>
54+
Login with Twitch
55+
</UButton>
4656
</template>
4757
</UHeader>
4858
<UMain>

playground/auth.d.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
declare module '#auth-utils' {
22
interface UserSession {
33
user: {
4-
spotify?: any;
5-
github?: any;
6-
google?: any;
7-
};
8-
loggedInAt: number;
4+
spotify?: any
5+
github?: any
6+
google?: any
7+
twitch?: any
8+
}
9+
loggedInAt: number
910
}
1011
}

playground/pages/index.vue

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
<script setup>
1+
<script setup lang="ts">
22
const { session } = useUserSession()
33
</script>
44

55
<template>
6-
<pre>{{ session }}</pre>
6+
<UPageBody>
7+
<pre>{{ session }}</pre>
8+
</UPageBody>
79
</template>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
export default oauth.twitchEventHandler({
2+
config: {
3+
emailRequired: true,
4+
},
5+
async onSuccess(event, { user }) {
6+
await setUserSession(event, {
7+
user: {
8+
twitch: user,
9+
},
10+
loggedInAt: Date.now()
11+
})
12+
13+
return sendRedirect(event, '/')
14+
}
15+
})

src/module.ts

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

src/runtime/server/utils/oauth.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import { githubEventHandler } from '../lib/oauth/github'
22
import { googleEventHandler } from '../lib/oauth/google'
33
import { spotifyEventHandler } from '../lib/oauth/spotify'
4+
import { twitchEventHandler } from '../lib/oauth/twitch'
45

56
export const oauth = {
67
githubEventHandler,
78
spotifyEventHandler,
8-
googleEventHandler
9+
googleEventHandler,
10+
twitchEventHandler,
911
}

0 commit comments

Comments
 (0)