Skip to content

feat: add ory provider #417

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ It can also be set using environment variables:
- LinkedIn
- LiveChat
- Microsoft
- Ory
- PayPal
- Polar
- Salesforce
Expand Down
6 changes: 5 additions & 1 deletion playground/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -136,4 +136,8 @@ NUXT_OAUTH_SLACK_REDIRECT_URL=
#Heroku
NUXT_OAUTH_HEROKU_CLIENT_ID=
NUXT_OAUTH_HEROKU_CLIENT_SECRET=
NUXT_OAUTH_HEROKU_REDIRECT_URL=
NUXT_OAUTH_HEROKU_REDIRECT_URL=
#Ory
NUXT_OAUTH_ORY_CLIENT_ID=
NUXT_OAUTH_ORY_CLIENT_SECRET=
NUXT_OAUTH_ORY_SDK_URL=
6 changes: 6 additions & 0 deletions playground/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,12 @@ const providers = computed(() =>
disabled: Boolean(user.value?.heroku),
icon: 'i-simple-icons-heroku',
},
{
label: user.value?.ory || 'Ory',
to: '/auth/ory',
disabled: Boolean(user.value?.ory),
icon: 'i-simple-icons-ory',
},
].map(p => ({
...p,
prefetch: false,
Expand Down
1 change: 1 addition & 0 deletions playground/auth.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ declare module '#auth-utils' {
salesforce?: string
slack?: string
heroku?: string
ory?: string
}

interface UserSession {
Expand Down
11 changes: 11 additions & 0 deletions src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -468,5 +468,16 @@ export default defineNuxtModule<ModuleOptions>({
redirectURL: '',
scope: '',
})
// Ory OAuth
runtimeConfig.oauth.ory = defu(runtimeConfig.oauth.ory, {
clientId: '',
clientSecret: '',
sdkURL: '',
redirectURL: '',
scope: [],
authorizationURL: '',
tokenURL: '',
userURL: '',
})
},
})
176 changes: 176 additions & 0 deletions src/runtime/server/lib/oauth/ory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import type { H3Event } from 'h3'
import { eventHandler, getQuery, sendRedirect } from 'h3'
import { withQuery } from 'ufo'
import { defu } from 'defu'
import {
getOAuthRedirectURL,
handleAccessTokenErrorResponse,
handleInvalidState,
handleMissingConfiguration,
handlePkceVerifier,
handleState,
requestAccessToken,
} from '../utils'
import { createError, useRuntimeConfig } from '#imports'
import type { OAuthConfig } from '#auth-utils'

/**
* Ory OAuth2
* @see https://www.ory.sh/docs/oauth2-oidc/authorization-code-flow
*/

export interface OAuthOryConfig {
/**
* Ory OAuth Client ID
* @default process.env.NUXT_OAUTH_ORY_CLIENT_ID
*/
clientId?: string
/**
* Ory OAuth Client Secret
* @default process.env.NUXT_OAUTH_ORY_CLIENT_SECRET
*/
clientSecret?: string
/**
* Ory OAuth SDK URL
* @default "https://playground.projects.oryapis.com" || process.env.NUXT_OAUTH_ORY_SDK_URL
*/
sdkURL?: string
/**
* Ory OAuth Scope
* @default ['openid', 'offline']
* @see https://docs.oryhydra.com/en/developers/apps/building-oauth-apps/scopes-for-oauth-apps
* @example ['openid', 'offline', 'email']
*/
scope?: string[] | string
/**
* Ory OAuth Authorization URL
* @default '/oauth2/auth'
*/
authorizationURL?: string

/**
* Ory OAuth Token URL
* @default '/oauth2/token'
*/
tokenURL?: string

/**
* Extra authorization parameters to provide to the authorization URL
* @example { allow_signup: 'true' }
*/
authorizationParams?: Record<string, string>

/**
* Ory OAuth Userinfo URL
* @default '/userinfo'
*/
userURL?: string
}

export function oryHydraEventHandler({ config, onSuccess, onError }: OAuthConfig<OAuthOryConfig>) {
return eventHandler(async (event: H3Event) => {
config = defu(config, useRuntimeConfig(event).oauth?.ory, {
scope: ['openid', 'offline'],
sdkURL: 'https://playground.projects.oryapis.com',
authorizationURL: '/oauth2/auth',
tokenURL: '/oauth2/token',
userURL: '/userinfo',
authorizationParams: {},
}) as OAuthOryConfig

// TODO: improve typing
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const query = getQuery<{ code?: string, state?: string, error: any }>(event)

if (query.error) {
const error = createError({
statusCode: 401,
message: `ory login failed: ${query.error || 'Unknown error'}`,
data: query,
})
if (!onError) throw error
return onError(event, error)
}

if (!config.clientId || !config.sdkURL) {
return handleMissingConfiguration(event, 'ory', ['clientId', 'sdkURL'], onError)
}

const redirectURL = getOAuthRedirectURL(event)

// guarantee uniqueness of the scope and convert to string if it's an array
if (Array.isArray(config.scope)) {
config.scope = Array.from(new Set(config.scope)).join(' ')
}

// Create pkce verifier
const verifier = await handlePkceVerifier(event)
const state = await handleState(event)

if (!query.code) {
const authorizationURL = `${config.sdkURL}${config.authorizationURL}`
return sendRedirect(
event,
withQuery(authorizationURL, {
client_id: config.clientId,
response_type: 'code',
redirect_uri: redirectURL,
scope: config.scope,
state,
code_challenge: verifier.code_challenge,
code_challenge_method: verifier.code_challenge_method,
...config.authorizationParams,
}),
)
}

if (query.state !== state) {
handleInvalidState(event, 'ory', onError)
}

const tokenURL = `${config.sdkURL}${config.tokenURL}`
const tokens = await requestAccessToken(tokenURL, {
body: {
grant_type: 'authorization_code',
client_id: config.clientId,
code: query.code as string,
redirect_uri: redirectURL,
scope: config.scope,
code_verifier: verifier.code_verifier,
},
})

if (tokens.error) {
return handleAccessTokenErrorResponse(event, 'ory', tokens, onError)
}

const tokenType = tokens.token_type
const accessToken = tokens.access_token

const userURL = `${config.sdkURL}${config.userURL}`
// TODO: improve typing
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const user: any = await $fetch(userURL, {
headers: {
'User-Agent': `Ory-${config.clientId}`,
'Authorization': `${tokenType} ${accessToken}`,
},
}).catch((error) => {
return { error }
})
if (user.error) {
const error = createError({
statusCode: 401,
message: `ory login failed: ${user.error || 'Unknown error'}`,
data: user,
})
if (!onError) throw error
return onError(event, error)
}

return onSuccess(event, {
tokens,
user,
})
})
}
2 changes: 1 addition & 1 deletion src/runtime/types/oauth-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { H3Event, H3Error } from 'h3'

export type ATProtoProvider = 'bluesky'

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 & {})
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 & {})

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

Expand Down