Skip to content

Commit b0da81f

Browse files
committed
feat: add ory provider
1 parent f7127f5 commit b0da81f

File tree

5 files changed

+194
-2
lines changed

5 files changed

+194
-2
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@ It can also be set using environment variables:
236236
- LinkedIn
237237
- LiveChat
238238
- Microsoft
239+
- Ory
239240
- PayPal
240241
- Polar
241242
- Salesforce

playground/.env.example

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,4 +136,8 @@ NUXT_OAUTH_SLACK_REDIRECT_URL=
136136
#Heroku
137137
NUXT_OAUTH_HEROKU_CLIENT_ID=
138138
NUXT_OAUTH_HEROKU_CLIENT_SECRET=
139-
NUXT_OAUTH_HEROKU_REDIRECT_URL=
139+
NUXT_OAUTH_HEROKU_REDIRECT_URL=
140+
#Ory
141+
NUXT_OAUTH_ORY_CLIENT_ID=
142+
NUXT_OAUTH_ORY_CLIENT_SECRET=
143+
NUXT_OAUTH_ORY_SERVER_URL=

src/module.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,5 +468,16 @@ export default defineNuxtModule<ModuleOptions>({
468468
redirectURL: '',
469469
scope: '',
470470
})
471+
// Ory OAuth
472+
runtimeConfig.oauth.ory = defu(runtimeConfig.oauth.ory, {
473+
clientId: '',
474+
clientSecret: '',
475+
sdkURL: '',
476+
redirectURL: '',
477+
scope: [],
478+
authorizationURL: '',
479+
tokenURL: '',
480+
userURL: '',
481+
})
471482
},
472483
})

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

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
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 {
6+
getOAuthRedirectURL,
7+
handleAccessTokenErrorResponse,
8+
handleInvalidState,
9+
handleMissingConfiguration,
10+
handlePkceVerifier,
11+
handleState,
12+
requestAccessToken,
13+
} from '../utils'
14+
import { createError, useRuntimeConfig } from '#imports'
15+
import type { OAuthConfig } from '#auth-utils'
16+
17+
/**
18+
* Ory OAuth2
19+
* @see https://www.ory.sh/docs/oauth2-oidc/authorization-code-flow
20+
*/
21+
22+
export interface OAuthOryConfig {
23+
/**
24+
* Ory OAuth Client ID
25+
* @default process.env.NUXT_OAUTH_ORY_CLIENT_ID
26+
*/
27+
clientId?: string
28+
/**
29+
* Ory OAuth Client Secret
30+
* @default process.env.NUXT_OAUTH_ORY_CLIENT_SECRET
31+
*/
32+
clientSecret?: string
33+
/**
34+
* Ory OAuth SDK URL
35+
* @default "https://playground.projects.oryapis.com" || process.env.NUXT_OAUTH_ORY_SDK_URL
36+
*/
37+
sdkURL?: string
38+
/**
39+
* Ory OAuth Scope
40+
* @default ['openid', 'offline']
41+
* @see https://docs.oryhydra.com/en/developers/apps/building-oauth-apps/scopes-for-oauth-apps
42+
* @example ['openid', 'offline', 'email']
43+
*/
44+
scope?: string[] | string
45+
/**
46+
* Ory OAuth Authorization URL
47+
* @default '/oauth2/auth'
48+
*/
49+
authorizationURL?: string
50+
51+
/**
52+
* Ory OAuth Token URL
53+
* @default '/oauth2/token'
54+
*/
55+
tokenURL?: string
56+
57+
/**
58+
* Extra authorization parameters to provide to the authorization URL
59+
* @example { allow_signup: 'true' }
60+
*/
61+
authorizationParams?: Record<string, string>
62+
63+
/**
64+
* Ory OAuth Userinfo URL
65+
* @default '/userinfo'
66+
*/
67+
userURL?: string
68+
}
69+
70+
export function oryHydraEventHandler({ config, onSuccess, onError }: OAuthConfig<OAuthOryConfig>) {
71+
return eventHandler(async (event: H3Event) => {
72+
config = defu(config, useRuntimeConfig(event).oauth?.ory, {
73+
scope: ['openid', 'offline'],
74+
sdkURL: 'https://playground.projects.oryapis.com',
75+
authorizationURL: '/oauth2/auth',
76+
tokenURL: '/oauth2/token',
77+
userURL: '/userinfo',
78+
authorizationParams: {},
79+
}) as OAuthOryConfig
80+
81+
// TODO: improve typing
82+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
83+
const query = getQuery<{ code?: string, state?: string, error: any }>(event)
84+
85+
if (query.error) {
86+
const error = createError({
87+
statusCode: 401,
88+
message: `ory login failed: ${query.error || 'Unknown error'}`,
89+
data: query,
90+
})
91+
if (!onError) throw error
92+
return onError(event, error)
93+
}
94+
95+
if (!config.clientId || !config.sdkURL) {
96+
return handleMissingConfiguration(event, 'ory', ['clientId', 'sdkURL'], onError)
97+
}
98+
99+
const redirectURL = getOAuthRedirectURL(event)
100+
101+
// guarantee uniqueness of the scope and convert to string if it's an array
102+
if (Array.isArray(config.scope)) {
103+
config.scope = Array.from(new Set(config.scope)).join(' ')
104+
}
105+
106+
// Create pkce verifier
107+
const verifier = await handlePkceVerifier(event)
108+
const state = await handleState(event)
109+
110+
if (!query.code) {
111+
const authorizationURL = `${config.sdkURL}${config.authorizationURL}`
112+
return sendRedirect(
113+
event,
114+
withQuery(authorizationURL, {
115+
client_id: config.clientId,
116+
response_type: 'code',
117+
redirect_uri: redirectURL,
118+
scope: config.scope,
119+
state,
120+
code_challenge: verifier.code_challenge,
121+
code_challenge_method: verifier.code_challenge_method,
122+
...config.authorizationParams,
123+
}),
124+
)
125+
}
126+
127+
if (query.state !== state) {
128+
handleInvalidState(event, 'ory', onError)
129+
}
130+
131+
const tokenURL = `${config.sdkURL}${config.tokenURL}`
132+
const tokens = await requestAccessToken(tokenURL, {
133+
body: {
134+
grant_type: 'authorization_code',
135+
client_id: config.clientId,
136+
code: query.code as string,
137+
redirect_uri: redirectURL,
138+
scope: config.scope,
139+
code_verifier: verifier.code_verifier,
140+
},
141+
})
142+
143+
if (tokens.error) {
144+
return handleAccessTokenErrorResponse(event, 'ory', tokens, onError)
145+
}
146+
147+
const tokenType = tokens.token_type
148+
const accessToken = tokens.access_token
149+
150+
const userURL = `${config.sdkURL}${config.userURL}`
151+
// TODO: improve typing
152+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
153+
const user: any = await $fetch(userURL, {
154+
headers: {
155+
'User-Agent': `Ory-${config.clientId}`,
156+
'Authorization': `${tokenType} ${accessToken}`,
157+
},
158+
}).catch((error) => {
159+
return { error }
160+
})
161+
if (user.error) {
162+
const error = createError({
163+
statusCode: 401,
164+
message: `ory login failed: ${user.error || 'Unknown error'}`,
165+
data: user,
166+
})
167+
if (!onError) throw error
168+
return onError(event, error)
169+
}
170+
171+
return onSuccess(event, {
172+
tokens,
173+
user,
174+
})
175+
})
176+
}

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' | (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' | 'ory' | (string & {})
66

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

0 commit comments

Comments
 (0)