Skip to content

Commit

Permalink
fix: integrate security utils.
Browse files Browse the repository at this point in the history
  • Loading branch information
maximilianmikus committed Nov 30, 2023
1 parent 9354a72 commit ebe364d
Show file tree
Hide file tree
Showing 2 changed files with 32 additions and 31 deletions.
53 changes: 27 additions & 26 deletions src/runtime/server/lib/oauth/oidc.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import type { H3Event } from 'h3'
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 { createHash, randomBytes } from 'node:crypto'
import { type OAuthChecks, checks } from '../../utils/security'

export interface OAuthOidcConfig {
/**
Expand Down Expand Up @@ -69,10 +69,17 @@ export interface OAuthOidcConfig {
* @example ['openid']
*/
scope?: string[]
/**
* 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[]
}

function validateConfig(config: any) {
const requiredConfigKeys = ['clientId', 'clientSecret', 'authorizationUrl', 'tokenUrl', 'userinfoUrl', 'redirectUri']
const requiredConfigKeys = ['clientId', 'clientSecret', 'authorizationUrl', 'tokenUrl', 'userinfoUrl', 'redirectUri', 'responseType']
const missingConfigKeys: string[] = []
requiredConfigKeys.forEach(key => {
if (!config[key]) {
Expand All @@ -82,7 +89,7 @@ function validateConfig(config: any) {
if (missingConfigKeys.length) {
const error = createError({
statusCode: 500,
message: `Missing config keys:${missingConfigKeys.join(', ')}`
message: `Missing config keys: ${missingConfigKeys.join(', ')}`
})

return {
Expand All @@ -93,21 +100,11 @@ function validateConfig(config: any) {
return { valid: true }
}

function createCodeChallenge(verifier: string) {
return createHash('sha256')
.update(verifier)
.digest('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '')
}

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

const validationResult = validateConfig(config)

Expand All @@ -116,12 +113,8 @@ export function oidcEventHandler({ config, onSuccess, onError }: OAuthConfig<OAu
return onError(event, validationResult.error)
}

if (!code && !state) {
const state = randomBytes(10).toString('hex')
const codeVerifier = randomBytes(52).toString('hex')
const challenge = createCodeChallenge(codeVerifier)
await storage.setItem('oidc:verifier:' + state, codeVerifier)
await storage.setItem('oidc:challenge:' + state, challenge)
if (!code) {
const authParams = await checks.create(event, config.checks) // Initialize checks
// Redirect to OIDC login page
return sendRedirect(
event,
Expand All @@ -133,14 +126,19 @@ export function oidcEventHandler({ config, onSuccess, onError }: OAuthConfig<OAu
claims: config?.claims || {},
grant_type: config.grantType || 'authorization_code',
audience: config.audience || null,
state: state,
code_challenge: config.codeChallengeMethod ? challenge : null,
code_challenge_method: config.codeChallengeMethod,
...authParams
})
)
}

const codeVerifier: string = await storage.getItem('oidc:verifier:' + state) || ''
// 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({
Expand All @@ -150,9 +148,10 @@ export function oidcEventHandler({ config, onSuccess, onError }: OAuthConfig<OAu
redirect_uri: config.redirectUri,
response_type: config.responseType,
grant_type: config.grantType || 'authorization_code',
code_verifier: codeVerifier,
...checkResult
})

// Request tokens.
const tokens: any = await ofetch(
config.tokenUrl as string,
{
Expand All @@ -178,6 +177,8 @@ export function oidcEventHandler({ config, onSuccess, onError }: OAuthConfig<OAu
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}`
Expand Down
10 changes: 5 additions & 5 deletions src/runtime/server/utils/security.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { subtle, getRandomValues } from 'uncrypto'

export type OAuthChecks = 'pkce' | 'state'

// From oauth4webapi https://github.com/panva/oauth4webapi/blob/4b46a7b4a4ca77a513774c94b718592fe3ad576f/src/index.ts#L567C1-L579C2
// From oauth4webapi https://github.com/panva/oauth4webapi/blob/4b46a7b4a4ca77a513774c94b718592fe3ad576f/src/index.ts#L567C1-L579C2
const CHUNK_SIZE = 0x8000
export function encodeBase64Url(input: Uint8Array | ArrayBuffer) {
if (input instanceof ArrayBuffer) {
Expand All @@ -22,16 +22,16 @@ function randomBytes() {
return encodeBase64Url(getRandomValues(new Uint8Array(32)))
}

/**
/**
* Generate a random `code_verifier` for use in the PKCE flow
* @see https://tools.ietf.org/html/rfc7636#section-4.1
*/
export function generateCodeVerifier() {
return randomBytes()
}

/**
* Generate a random `state` used to prevent CSRF attacks
/**
* Generate a random `state` used to prevent CSRF attacks
* @see https://www.rfc-editor.org/rfc/rfc6749.html#section-4.1.1
*/
export function generateState() {
Expand Down Expand Up @@ -112,4 +112,4 @@ export const checks = {
}
return res
},
}
}

0 comments on commit ebe364d

Please sign in to comment.