Skip to content

Commit 9890998

Browse files
agatemosuatinux
andauthored
feat: add osu! oauth provider (#492)
Co-authored-by: Sébastien Chopin <seb@nuxt.com>
1 parent bbcfae2 commit 9890998

File tree

8 files changed

+160
-1
lines changed

8 files changed

+160
-1
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,7 @@ It can also be set using environment variables:
239239
- OIDC / OpenID Connect (Generic)
240240
- Okta
241241
- Ory
242+
- osu!
242243
- PayPal
243244
- Polar
244245
- Salesforce

playground/.env.example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,3 +154,7 @@ NUXT_OAUTH_OKTA_REDIRECT_URL=
154154
NUXT_OAUTH_ORY_CLIENT_ID=
155155
NUXT_OAUTH_ORY_CLIENT_SECRET=
156156
NUXT_OAUTH_ORY_SDK_URL=
157+
# osu!
158+
NUXT_OAUTH_OSU_CLIENT_ID=
159+
NUXT_OAUTH_OSU_CLIENT_SECRET=
160+
NUXT_OAUTH_OSU_REDIRECT_URL=

playground/app/pages/index.vue

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,12 @@ const providers = computed(() =>
287287
disabled: Boolean(user.value?.oidc),
288288
icon: 'i-simple-icons-openid',
289289
},
290+
{
291+
title: user.value?.osu || 'osu!',
292+
to: '/auth/osu',
293+
disabled: Boolean(user.value?.osu),
294+
icon: 'i-simple-icons-osu',
295+
},
290296
].map(p => ({
291297
...p,
292298
prefetch: false,
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export default defineOAuthOsuEventHandler({
2+
async onSuccess(event, { user }) {
3+
await setUserSession(event, {
4+
user: {
5+
osu: user.username,
6+
},
7+
loggedInAt: Date.now(),
8+
})
9+
10+
return sendRedirect(event, '/')
11+
},
12+
})

playground/shared/types/auth.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ declare module '#auth-utils' {
4848
okta?: string
4949
ory?: string
5050
oidc?: string
51+
osu?: string
5152
}
5253

5354
interface UserSession {

src/module.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -538,5 +538,12 @@ export default defineNuxtModule<ModuleOptions>({
538538
redirectURL: '',
539539
scope: [],
540540
})
541+
// osu! OAuth
542+
runtimeConfig.oauth.osu = defu(runtimeConfig.oauth.osu, {
543+
clientId: '',
544+
clientSecret: '',
545+
redirectURL: '',
546+
scope: [],
547+
})
541548
},
542549
})
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import type { H3Event } from 'h3'
2+
import { eventHandler, getQuery, sendRedirect } from 'h3'
3+
import { withQuery } from 'ufo'
4+
import { defu } from 'defu'
5+
import { handleMissingConfiguration, handleAccessTokenErrorResponse, getOAuthRedirectURL, requestAccessToken, handleState, handleInvalidState } from '../utils'
6+
import { useRuntimeConfig } from '#imports'
7+
import type { OAuthConfig } from '#auth-utils'
8+
9+
export interface OAuthOsuConfig {
10+
/**
11+
* osu! OAuth Client ID
12+
* @default process.env.NUXT_OAUTH_OSU_CLIENT_ID
13+
*/
14+
clientId?: string
15+
16+
/**
17+
* osu! OAuth Client Secret
18+
* @default process.env.NUXT_OAUTH_OSU_CLIENT_SECRET
19+
*/
20+
clientSecret?: string
21+
22+
/**
23+
* osu! OAuth Scope
24+
*
25+
* The identify scope is always implicitly provided.
26+
* @default []
27+
* @see https://osu.ppy.sh/docs/#scopes
28+
*/
29+
scope?: string[]
30+
31+
/**
32+
* osu! OAuth Authorization URL
33+
* @default 'https://osu.ppy.sh/oauth/authorize'
34+
*/
35+
authorizationURL?: string
36+
37+
/**
38+
* osu! OAuth Token URL
39+
* @default 'https://osu.ppy.sh/oauth/token'
40+
*/
41+
tokenURL?: string
42+
43+
/**
44+
* Extra authorization parameters to provide to the authorization URL
45+
* @see 'https://osu.ppy.sh/docs/#authorization-code-grant'
46+
*/
47+
authorizationParams?: Record<string, string>
48+
49+
/**
50+
* Redirect URL to to allow overriding for situations like prod failing to determine public hostname
51+
* @default process.env.NUXT_OAUTH_OSU_REDIRECT_URL or current URL
52+
*/
53+
redirectURL?: string
54+
}
55+
56+
export function defineOAuthOsuEventHandler({ config, onSuccess, onError }: OAuthConfig<OAuthOsuConfig>) {
57+
return eventHandler(async (event: H3Event) => {
58+
config = defu(config, useRuntimeConfig(event).oauth?.osu, {
59+
authorizationURL: 'https://osu.ppy.sh/oauth/authorize',
60+
tokenURL: 'https://osu.ppy.sh/oauth/token',
61+
authorizationParams: {},
62+
}) as OAuthOsuConfig
63+
64+
const query = getQuery<{ code?: string, state?: string, error?: string }>(event)
65+
66+
if (!config.clientId || !config.clientSecret) {
67+
return handleMissingConfiguration(event, 'osu', ['clientId', 'clientSecret'], onError)
68+
}
69+
70+
if (query.error) {
71+
return handleAccessTokenErrorResponse(event, 'osu', query, onError)
72+
}
73+
74+
const redirectURL = config.redirectURL || getOAuthRedirectURL(event)
75+
76+
const state = await handleState(event)
77+
78+
if (!query.code) {
79+
config.scope = config.scope || []
80+
81+
// Redirect to osu! OAuth page
82+
return sendRedirect(
83+
event,
84+
withQuery(config.authorizationURL as string, {
85+
response_type: 'code',
86+
client_id: config.clientId,
87+
redirect_uri: redirectURL,
88+
scope: config.scope.join(' '),
89+
state,
90+
...config.authorizationParams,
91+
}),
92+
)
93+
}
94+
95+
if (query.state !== state) {
96+
return handleInvalidState(event, 'osu', onError)
97+
}
98+
99+
const tokens = await requestAccessToken(config.tokenURL as string, {
100+
body: {
101+
client_id: config.clientId,
102+
client_secret: config.clientSecret,
103+
grant_type: 'authorization_code',
104+
redirect_uri: redirectURL,
105+
code: query.code,
106+
},
107+
})
108+
109+
if (tokens.error) {
110+
return handleAccessTokenErrorResponse(event, 'osu', tokens, onError)
111+
}
112+
113+
const accessToken = tokens.access_token
114+
// TODO: improve typing
115+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
116+
const user: any = await $fetch('https://osu.ppy.sh/api/v2/me', {
117+
headers: {
118+
'user-agent': 'Nuxt Auth Utils',
119+
'Authorization': `Bearer ${accessToken}`,
120+
},
121+
})
122+
123+
return onSuccess(event, {
124+
tokens,
125+
user,
126+
})
127+
})
128+
}

src/runtime/types/oauth-config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { H3Event, H3Error } from 'h3'
22

33
export type ATProtoProvider = 'bluesky'
44

5-
export type OAuthProvider = ATProtoProvider | 'atlassian' | 'auth0' | 'authentik' | 'azureb2c' | 'battledotnet' | 'cognito' | 'discord' | 'dropbox' | 'facebook' | 'gitea' | 'github' | 'gitlab' | 'google' | 'hubspot' | 'instagram' | 'kick' | 'keycloak' | 'line' | 'linear' | 'linkedin' | 'microsoft' | 'paypal' | 'polar' | 'spotify' | 'seznam' | 'steam' | 'strava' | 'tiktok' | 'twitch' | 'vk' | 'workos' | 'x' | 'xsuaa' | 'yandex' | 'zitadel' | 'apple' | 'livechat' | 'salesforce' | 'slack' | 'heroku' | 'roblox' | 'okta' | 'ory' | 'shopifyCustomer' | 'oidc' | (string & {})
5+
export type OAuthProvider = ATProtoProvider | 'atlassian' | 'auth0' | 'authentik' | 'azureb2c' | 'battledotnet' | 'cognito' | 'discord' | 'dropbox' | 'facebook' | 'gitea' | 'github' | 'gitlab' | 'google' | 'hubspot' | 'instagram' | 'kick' | 'keycloak' | 'line' | 'linear' | 'linkedin' | 'microsoft' | 'paypal' | 'polar' | 'spotify' | 'seznam' | 'steam' | 'strava' | 'tiktok' | 'twitch' | 'vk' | 'workos' | 'x' | 'xsuaa' | 'yandex' | 'zitadel' | 'apple' | 'livechat' | 'salesforce' | 'slack' | 'heroku' | 'roblox' | 'okta' | 'ory' | 'shopifyCustomer' | 'oidc' | 'osu' | (string & {})
66

77
export type OnError = (event: H3Event, error: H3Error) => Promise<void> | void
88

0 commit comments

Comments
 (0)