Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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 @@ -160,6 +160,7 @@ It can also be set using environment variables:
- Microsoft
- Spotify
- Twitch
- OpenID Connect

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

Expand Down
1 change: 1 addition & 0 deletions playground/server/routes/auth/auth0.get.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export default oauth.auth0EventHandler({
config: {
emailRequired: true,
checks: ['state']
},
async onSuccess(event, { user }) {
await setUserSession(event, {
Expand Down
10 changes: 10 additions & 0 deletions src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,16 @@ export default defineNuxtModule<ModuleOptions>({
sameSite: 'lax'
}
})
// Security settings
runtimeConfig.nuxtAuthUtils = defu(runtimeConfig.nuxtAuthUtils, {})
runtimeConfig.nuxtAuthUtils.security = defu(runtimeConfig.nuxtAuthUtils.security, {
cookie: {
secure: true,
httpOnly: true,
sameSite: 'lax',
maxAge: 60 * 15
}
})
// OAuth settings
runtimeConfig.oauth = defu(runtimeConfig.oauth, {})
// GitHub OAuth
Expand Down
27 changes: 25 additions & 2 deletions src/runtime/server/lib/oauth/auth0.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import type { H3Event } from 'h3'
import type { H3Event, H3Error } from 'h3'
import { eventHandler, createError, getQuery, getRequestURL, sendRedirect } from 'h3'
import { withQuery, parsePath } from 'ufo'
import { ofetch } from 'ofetch'
import { defu } from 'defu'
import { useRuntimeConfig } from '#imports'
import type { OAuthConfig } from '#auth-utils'
import { type OAuthChecks, checks } from '../../utils/security'

export interface OAuthAuth0Config {
/**
Expand All @@ -24,7 +25,7 @@ export interface OAuthAuth0Config {
domain?: string
/**
* Auth0 OAuth Audience
* @default process.env.NUXT_OAUTH_AUTH0_AUDIENCE
* @default ''
*/
audience?: string
/**
Expand All @@ -45,6 +46,13 @@ export interface OAuthAuth0Config {
* @see https://auth0.com/docs/authenticate/login/max-age-reauthentication
*/
maxAge?: number
/**
* checks
* @default []
* @see https://auth0.com/docs/flows/authorization-code-flow-with-proof-key-for-code-exchange-pkce
* @see https://auth0.com/docs/protocols/oauth2/oauth-state
*/
checks?: OAuthChecks[]
/**
* Login connection. If no connection is specified, it will redirect to the standard Auth0 login page and show the Login Widget.
* @default ''
Expand Down Expand Up @@ -73,6 +81,7 @@ export function auth0EventHandler({ config, onSuccess, onError }: OAuthConfig<OA

const redirectUrl = getRequestURL(event).href
if (!code) {
const authParam = await checks.create(event, config.checks) // Initialize checks
config.scope = config.scope || ['openid', 'offline_access']
if (config.emailRequired && !config.scope.includes('email')) {
config.scope.push('email')
Expand All @@ -87,11 +96,24 @@ export function auth0EventHandler({ config, onSuccess, onError }: OAuthConfig<OA
scope: config.scope.join(' '),
audience: config.audience || '',
max_age: config.maxAge || 0,
<<<<<<< HEAD
...authParam
=======
connection: config.connection || ''
>>>>>>> main

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a remainder of a merge conflict here

})
)
}

// Verify checks
let checkResult
try {
checkResult = await checks.use(event, config.checks)
} catch (error) {
if (!onError) throw error
return onError(event, error as H3Error)
}

const tokens: any = await ofetch(
tokenURL as string,
{
Expand All @@ -105,6 +127,7 @@ export function auth0EventHandler({ config, onSuccess, onError }: OAuthConfig<OA
client_secret: config.clientSecret,
redirect_uri: parsePath(redirectUrl).pathname,
code,
...checkResult
}
}
).catch(error => {
Expand Down
172 changes: 172 additions & 0 deletions src/runtime/server/lib/oauth/oidc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import type { H3Event, H3Error } from 'h3'
import { eventHandler, createError, getQuery, sendRedirect } from 'h3'
import { withQuery } from 'ufo'
import { ofetch } from 'ofetch'
import { defu } from 'defu'
import { useRuntimeConfig } from '#imports'
import type { OAuthConfig } from '#auth-utils'
import { type OAuthChecks, checks } from '../../utils/security'
import { validateConfig } from '../../utils/config'

export interface OAuthOidcConfig {
/**
* OIDC Client ID
* @default process.env.NUXT_OAUTH_OIDC_CLIENT_ID
*/
clientId?: string
/**
* OIDC Client Secret
* @default process.env.NUXT_OAUTH_OIDC_CLIENT_SECRET
*/
clientSecret?: string
/**
* OIDC Response Type
* @default process.env.NUXT_OAUTH_OIDC_RESPONSE_TYPE
*/
responseType?: string
/**
* OIDC Authorization Endpoint URL
* @default process.env.NUXT_OAUTH_OIDC_AUTHORIZATION_URL
*/
authorizationUrl?: string
/**
* OIDC Token Endpoint URL
* @default process.env.NUXT_OAUTH_OIDC_TOKEN_URL
*/
tokenUrl?: string
/**
* OIDC Userino Endpoint URL
* @default process.env.NUXT_OAUTH_OIDC_USERINFO_URL
*/
userinfoUrl?: string
/**
* OIDC Redirect URI
* @default process.env.NUXT_OAUTH_OIDC_USERINFO_URL
*/
redirectUri?: string
/**
* OIDC Code challenge method
* @default process.env.NUXT_OAUTH_OIDC_CODE_CHALLENGE_METHOD
*/
codeChallengeMethod?: string
/**
* OIDC Grant Type
* @default process.env.NUXT_OAUTH_OIDC_GRANT_TYPE
*/
grantType?: string
/**
* OIDC Claims
* @default process.env.NUXT_OAUTH_OIDC_AUDIENCE
*/
audience?: string
/**
* OIDC Claims
* @default {}
*/
claims?: {}
/**
* OIDC Scope
* @default []
* @example ['openid']
*/
scope?: string[]
/**
* A list of checks to add to the OIDC Flow (eg. 'state' or 'pkce')
* @default []
* @see https://auth0.com/docs/flows/authorization-code-flow-with-proof-key-for-code-exchange-pkce
* @see https://auth0.com/docs/protocols/oauth2/oauth-state
*/
checks?: OAuthChecks[]
}

export function oidcEventHandler({ config, onSuccess, onError }: OAuthConfig<OAuthOidcConfig>) {
return eventHandler(async (event: H3Event) => {
// @ts-ignore
config = defu(config, useRuntimeConfig(event).oauth?.oidc) as OAuthOidcConfig
const { code } = getQuery(event)

const validationResult = validateConfig(config, ['clientId', 'clientSecret', 'authorizationUrl', 'tokenUrl', 'userinfoUrl', 'redirectUri', 'responseType'])

if (!validationResult.valid && validationResult.error) {
if (!onError) throw validationResult.error
return onError(event, validationResult.error)
}

if (!code) {
const authParams = await checks.create(event, config.checks) // Initialize checks
// Redirect to OIDC login page
return sendRedirect(
event,
withQuery(config.authorizationUrl as string, {
response_type: config.responseType,
client_id: config.clientId,
redirect_uri: config.redirectUri,
scope: config?.scope?.join(' ') || 'openid',
claims: config?.claims || {},
grant_type: config.grantType || 'authorization_code',
audience: config.audience || null,
...authParams
})
)
}

// Verify checks
let checkResult
try {
checkResult = await checks.use(event, config.checks)
} catch (error) {
if (!onError) throw error
return onError(event, error as H3Error)
}

// @ts-ignore
const queryString = new URLSearchParams({
code,
client_id: config.clientId,
client_secret: config.clientSecret,
redirect_uri: config.redirectUri,
response_type: config.responseType,
grant_type: config.grantType || 'authorization_code',
...checkResult
})

// Request tokens.
const tokens: any = await ofetch(
config.tokenUrl as string,
{
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: queryString.toString(),
}
).catch(error => {
return { error }
})
if (tokens.error) {
const error = createError({
statusCode: 401,
message: `OIDC login failed: ${tokens.error?.data?.error_description || 'Unknown error'}`,
data: tokens
})
if (!onError) throw error
return onError(event, error)
}

const tokenType = tokens.token_type
const accessToken = tokens.access_token
const userInfoUrl = config.userinfoUrl || ''

// Request userinfo.
const user: any = await ofetch(userInfoUrl, {
headers: {
Authorization: `${tokenType} ${accessToken}`
}
})

return onSuccess(event, {
tokens,
user
})
})
}
27 changes: 27 additions & 0 deletions src/runtime/server/utils/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { H3Error } from 'h3'

export type configValidationResult = {
valid: boolean,
error?: H3Error
}

export function validateConfig(config: any, requiredKeys: string[]): configValidationResult {
const missingKeys: string[] = []
requiredKeys.forEach(key => {
if (!config[key]) {
missingKeys.push(key)
}
})
if (missingKeys.length) {
const error = createError({
statusCode: 500,
message: `Missing config keys: ${missingKeys.join(', ')}. Please pass the required parameters either as env variables or as part of the config parameter.`
})

return {
valid: false,
error
}
}
return { valid: true }
}
2 changes: 2 additions & 0 deletions src/runtime/server/utils/oauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { discordEventHandler } from '../lib/oauth/discord'
import { battledotnetEventHandler } from '../lib/oauth/battledotnet'
import { keycloakEventHandler } from '../lib/oauth/keycloak'
import { linkedinEventHandler } from '../lib/oauth/linkedin'
import { oidcEventHandler } from '../lib/oauth/oidc'
import { cognitoEventHandler } from '../lib/oauth/cognito'

export const oauth = {
Expand All @@ -21,5 +22,6 @@ export const oauth = {
battledotnetEventHandler,
keycloakEventHandler,
linkedinEventHandler,
oidcEventHandler,
cognitoEventHandler
}
Loading