Skip to content

Commit b0abad8

Browse files
feat: add generic oidc provider.
1 parent 7113f98 commit b0abad8

File tree

2 files changed

+195
-1
lines changed

2 files changed

+195
-1
lines changed

src/runtime/server/lib/oauth/oidc.ts

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
import type { H3Event } from 'h3'
2+
import { eventHandler, createError, getQuery, sendRedirect } from 'h3'
3+
import { withQuery } from 'ufo'
4+
import { ofetch } from 'ofetch'
5+
import { defu } from 'defu'
6+
import { useRuntimeConfig } from '#imports'
7+
import type { OAuthConfig } from '#auth-utils'
8+
import { createHash, randomBytes } from 'node:crypto'
9+
10+
export interface OAuthOidcConfig {
11+
/**
12+
* OIDC Client ID
13+
* @default process.env.NUXT_OAUTH_OIDC_CLIENT_ID
14+
*/
15+
clientId?: string
16+
/**
17+
* OIDC Client Secret
18+
* @default process.env.NUXT_OAUTH_OIDC_CLIENT_SECRET
19+
*/
20+
clientSecret?: string
21+
/**
22+
* OIDC Response Type
23+
* @default process.env.NUXT_OAUTH_OIDC_RESPONSE_TYPE
24+
*/
25+
responseType?: string
26+
/**
27+
* OIDC Authorization Endpoint URL
28+
* @default process.env.NUXT_OAUTH_OIDC_AUTHORIZATION_URL
29+
*/
30+
authorizationUrl?: string
31+
/**
32+
* OIDC Token Endpoint URL
33+
* @default process.env.NUXT_OAUTH_OIDC_TOKEN_URL
34+
*/
35+
tokenUrl?: string
36+
/**
37+
* OIDC Userino Endpoint URL
38+
* @default process.env.NUXT_OAUTH_OIDC_USERINFO_URL
39+
*/
40+
userinfoUrl?: string
41+
/**
42+
* OIDC Redirect URI
43+
* @default process.env.NUXT_OAUTH_OIDC_TOKEN_URL
44+
*/
45+
redirectUri?: string
46+
/**
47+
* OIDC Code challenge method
48+
* @default process.env.NUXT_OAUTH_OIDC_CODE_CHALLENGE_METHOD
49+
*/
50+
codeChallengeMethod?: string
51+
/**
52+
* OIDC Grant Type
53+
* @default process.env.NUXT_OAUTH_OIDC_GRANT_TYPE
54+
*/
55+
grantType?: string
56+
/**
57+
* OIDC Claims
58+
* @default process.env.NUXT_OAUTH_OIDC_AUDIENCE
59+
*/
60+
audience?: string
61+
/**
62+
* OIDC Claims
63+
* @default {}
64+
*/
65+
claims?: {}
66+
/**
67+
* OIDC Scope
68+
* @default []
69+
* @example ['openid']
70+
*/
71+
scope?: string[]
72+
}
73+
74+
function validateConfig(config: any) {
75+
const requiredConfigKeys = ['clientId', 'clientSecret', 'authorizationUrl', 'tokenUrl', 'userinfoUrl', 'redirectUri']
76+
const missingConfigKeys: string[] = []
77+
requiredConfigKeys.forEach(key => {
78+
if (!config[key]) {
79+
missingConfigKeys.push(key)
80+
}
81+
})
82+
if (missingConfigKeys.length) {
83+
const error = createError({
84+
statusCode: 500,
85+
message: `Missing config keys:${missingConfigKeys.join(', ')}`
86+
})
87+
88+
return {
89+
valid: false,
90+
error
91+
}
92+
}
93+
return { valid: true }
94+
}
95+
96+
function createCodeChallenge(verifier: string) {
97+
return createHash('sha256')
98+
.update(verifier)
99+
.digest('base64')
100+
.replace(/\+/g, '-')
101+
.replace(/\//g, '_')
102+
.replace(/=+$/, '')
103+
}
104+
105+
export function oidcEventHandler({ config, onSuccess, onError }: OAuthConfig<OAuthOidcConfig>) {
106+
return eventHandler(async (event: H3Event) => {
107+
const storage = useStorage('redis')
108+
// @ts-ignore
109+
config = defu(config, useRuntimeConfig(event).oauth?.oidc) as OAuthOidcConfig
110+
const { code, state } = getQuery(event)
111+
112+
const validationResult = validateConfig(config)
113+
114+
if (!validationResult.valid && validationResult.error) {
115+
if (!onError) throw validationResult.error
116+
return onError(event, validationResult.error)
117+
}
118+
119+
if (!code && !state) {
120+
const state = randomBytes(10).toString('hex')
121+
const codeVerifier = randomBytes(52).toString('hex')
122+
const challenge = createCodeChallenge(codeVerifier)
123+
await storage.setItem('oidc:verifier:' + state, codeVerifier)
124+
await storage.setItem('oidc:challenge:' + state, challenge)
125+
// Redirect to OIDC login page
126+
return sendRedirect(
127+
event,
128+
withQuery(config.authorizationUrl as string, {
129+
response_type: config.responseType,
130+
client_id: config.clientId,
131+
redirect_uri: config.redirectUri,
132+
scope: config?.scope?.join(' ') || 'openid',
133+
claims: config?.claims || {},
134+
grant_type: config.grantType || 'authorization_code',
135+
audience: config.audience || null,
136+
state: state,
137+
code_challenge: config.codeChallengeMethod ? challenge : null,
138+
code_challenge_method: config.codeChallengeMethod,
139+
})
140+
)
141+
}
142+
143+
const codeVerifier: string = await storage.getItem('oidc:verifier:' + state) || ''
144+
145+
// @ts-ignore
146+
const queryString = new URLSearchParams({
147+
code,
148+
client_id: config.clientId,
149+
client_secret: config.clientSecret,
150+
redirect_uri: config.redirectUri,
151+
response_type: config.responseType,
152+
grant_type: config.grantType || 'authorization_code',
153+
code_verifier: codeVerifier,
154+
})
155+
156+
const tokens: any = await ofetch(
157+
config.tokenUrl as string,
158+
{
159+
method: 'POST',
160+
headers: {
161+
'Content-Type': 'application/x-www-form-urlencoded'
162+
},
163+
body: queryString.toString(),
164+
}
165+
).catch(error => {
166+
return { error }
167+
})
168+
if (tokens.error) {
169+
const error = createError({
170+
statusCode: 401,
171+
message: `OIDC login failed: ${tokens.error?.data?.error_description || 'Unknown error'}`,
172+
data: tokens
173+
})
174+
if (!onError) throw error
175+
return onError(event, error)
176+
}
177+
178+
const tokenType = tokens.token_type
179+
const accessToken = tokens.access_token
180+
const userInfoUrl = config.userinfoUrl || ''
181+
const user: any = await ofetch(userInfoUrl, {
182+
headers: {
183+
Authorization: `${tokenType} ${accessToken}`
184+
}
185+
})
186+
187+
return onSuccess(event, {
188+
tokens,
189+
user
190+
})
191+
})
192+
}

src/runtime/server/utils/oauth.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { twitchEventHandler } from '../lib/oauth/twitch'
55
import { auth0EventHandler } from '../lib/oauth/auth0'
66
import { discordEventHandler } from '../lib/oauth/discord'
77
import { battledotnetEventHandler } from '../lib/oauth/battledotnet'
8+
import { oidcEventHandler } from '../lib/oauth/oidc'
89

910
export const oauth = {
1011
githubEventHandler,
@@ -13,5 +14,6 @@ export const oauth = {
1314
twitchEventHandler,
1415
auth0EventHandler,
1516
discordEventHandler,
16-
battledotnetEventHandler
17+
battledotnetEventHandler,
18+
oidcEventHandler
1719
}

0 commit comments

Comments
 (0)