fastify-openid-auth
Advanced tools
@@ -0,1 +1,20 @@ | ||
# [3.0.0](https://github.com/mikaelkaron/fastify-openid-auth/compare/v2.1.0...v3.0.0) (2022-03-01) | ||
### Code Refactoring | ||
* simplify login code ([5b5e1f8](https://github.com/mikaelkaron/fastify-openid-auth/commit/5b5e1f8502382b362efab04e74b24e05963b2c23)) | ||
### Features | ||
* remove dynamic factories ([b30653b](https://github.com/mikaelkaron/fastify-openid-auth/commit/b30653b00d2c33745fc8f51201711575f99310ee)) | ||
* use `fastify-error` for plugin errors ([df1ccf9](https://github.com/mikaelkaron/fastify-openid-auth/commit/df1ccf930a6be499105d98d40236c2854da31b6a)) | ||
### BREAKING CHANGES | ||
* `OpenIDLoginHandlerOptions.params` is now `OpenIDLoginHandlerOptions.parameters` | ||
* Removed outer factories, resulting in `handlerFactory() => handler` is now just `handler`. | ||
# [2.1.0](https://github.com/mikaelkaron/fastify-openid-auth/compare/v2.0.0...v2.1.0) (2022-02-22) | ||
@@ -2,0 +21,0 @@ |
"use strict"; | ||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { | ||
if (k2 === undefined) k2 = k; | ||
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); | ||
var desc = Object.getOwnPropertyDescriptor(m, k); | ||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { | ||
desc = { enumerable: true, get: function() { return m[k]; } }; | ||
} | ||
Object.defineProperty(o, k2, desc); | ||
}) : (function(o, m, k, k2) { | ||
@@ -6,0 +10,0 @@ if (k2 === undefined) k2 = k; |
import { RouteHandlerMethod } from 'fastify'; | ||
import createError from 'fastify-error'; | ||
import { AuthorizationParameters, CallbackExtras, Client } from 'openid-client'; | ||
@@ -7,9 +8,9 @@ import { OpenIDWriteTokens } from './types'; | ||
session: { | ||
get(key: string): any; | ||
set(key: string, value: any): void; | ||
get: <T>(key: string) => T; | ||
set: (key: string, value: unknown) => void; | ||
}; | ||
} | ||
} | ||
export interface OpenIDLoginOptions { | ||
params?: AuthorizationParameters; | ||
export interface OpenIDLoginHandlerOptions { | ||
parameters?: AuthorizationParameters; | ||
extras?: CallbackExtras; | ||
@@ -20,3 +21,5 @@ usePKCE?: boolean | 'plain' | 'S256'; | ||
} | ||
export declare type OpenIDLoginHandlerFactory = (options?: OpenIDLoginOptions) => RouteHandlerMethod; | ||
export declare const openIDAuthLoginFactory: (client: Client, defaults?: OpenIDLoginOptions | undefined) => OpenIDLoginHandlerFactory; | ||
export declare const SessionKeyError: createError.FastifyErrorConstructor; | ||
export declare const SessionValueError: createError.FastifyErrorConstructor; | ||
export declare const SupportedMethodError: createError.FastifyErrorConstructor; | ||
export declare const openIDLoginHandlerFactory: (client: Client, options?: OpenIDLoginHandlerOptions | undefined) => RouteHandlerMethod; |
"use strict"; | ||
var __importDefault = (this && this.__importDefault) || function (mod) { | ||
return (mod && mod.__esModule) ? mod : { "default": mod }; | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.openIDAuthLoginFactory = void 0; | ||
exports.openIDLoginHandlerFactory = exports.SupportedMethodError = exports.SessionValueError = exports.SessionKeyError = void 0; | ||
const fastify_error_1 = __importDefault(require("fastify-error")); | ||
const openid_client_1 = require("openid-client"); | ||
const util_1 = require("util"); | ||
const pick = (obj, ...keys) => { | ||
const ret = {}; | ||
keys.forEach((key) => { | ||
ret[key] = obj[key]; | ||
}); | ||
return ret; | ||
}; | ||
exports.SessionKeyError = (0, fastify_error_1.default)('FST_SESSION_KEY', 'client must have an issuer with an identifier', 500); | ||
exports.SessionValueError = (0, fastify_error_1.default)('FST_SESSION_VALUE', 'did not find expected authorization request details in req.session["%s"]', 500); | ||
exports.SupportedMethodError = (0, fastify_error_1.default)('FST_SUPPORTED_METHOD', 'neither code_challenge_method supported by the client is supported by the issuer', 500); | ||
const resolveResponseType = (client) => { | ||
@@ -27,3 +26,3 @@ const { length, 0: value } = client.metadata.response_types ?? []; | ||
}; | ||
const resolveSupportedMethods = (issuer) => { | ||
const resolveSupportedMethod = (issuer) => { | ||
const supportedMethods = Array.isArray(issuer.code_challenge_methods_supported) | ||
@@ -39,3 +38,3 @@ ? issuer.code_challenge_methods_supported | ||
else { | ||
throw new TypeError('neither code_challenge_method supported by the client is supported by the issuer'); | ||
throw new exports.SupportedMethodError(); | ||
} | ||
@@ -45,82 +44,68 @@ }; | ||
if (issuer.metadata.issuer === undefined) { | ||
throw new TypeError('client must have an issuer with an identifier'); | ||
throw new exports.SessionKeyError(); | ||
} | ||
return `oidc:${new URL(issuer.metadata.issuer).hostname}`; | ||
}; | ||
const openIDAuthLoginFactory = (client, defaults) => { | ||
const _params = { | ||
scope: 'openid', | ||
response_type: resolveResponseType(client), | ||
redirect_uri: resolveRedirectUri(client), | ||
...defaults?.params, | ||
}; | ||
const _sessionKey = defaults?.sessionKey !== undefined | ||
? defaults.sessionKey | ||
const openIDLoginHandlerFactory = (client, options) => { | ||
const redirect_uri = options?.parameters?.redirect_uri !== undefined | ||
? options.parameters.redirect_uri | ||
: resolveRedirectUri(client); | ||
const sessionKey = options?.sessionKey !== undefined | ||
? options.sessionKey | ||
: resolveSessionKey(client.issuer); | ||
const _usePKCE = defaults?.usePKCE !== undefined | ||
? defaults.usePKCE === true | ||
? resolveSupportedMethods(client.issuer) | ||
: defaults.usePKCE | ||
const usePKCE = options?.usePKCE !== undefined | ||
? options.usePKCE === true | ||
? resolveSupportedMethod(client.issuer) | ||
: options.usePKCE | ||
: false; | ||
const openIDLoginHandlerFactory = (options) => { | ||
const { sessionKey = _sessionKey, usePKCE = _usePKCE, write, } = { ...defaults, ...options }; | ||
return async function openIDLoginHandler(request, reply) { | ||
const parameters = client.callbackParams(request.raw); | ||
// #region authentication request | ||
if (Object.keys(parameters).length === 0) { | ||
const params = { | ||
state: openid_client_1.generators.random(), | ||
..._params, | ||
...options?.params, | ||
}; | ||
if (params.nonce === undefined && params.response_type === 'code') { | ||
params.nonce = openid_client_1.generators.random(); | ||
const { write } = { ...options }; | ||
return async function openIDLoginHandler(request, reply) { | ||
const callbackParams = client.callbackParams(request.raw); | ||
// #region authentication request | ||
if (Object.keys(callbackParams).length === 0) { | ||
const response_type = options?.parameters?.response_type !== undefined | ||
? options.parameters.response_type | ||
: resolveResponseType(client); | ||
const parameters = { | ||
scope: 'openid', | ||
state: openid_client_1.generators.random(), | ||
redirect_uri, | ||
response_type, | ||
...options?.parameters, | ||
}; | ||
if (parameters.nonce === undefined && | ||
parameters.response_type === 'code') { | ||
parameters.nonce = openid_client_1.generators.random(); | ||
} | ||
const callbackChecks = (({ nonce, state, max_age, response_type, }) => ({ nonce, state, max_age, response_type }))(parameters); | ||
if (usePKCE !== false && parameters.response_type === 'code') { | ||
const verifier = openid_client_1.generators.random(); | ||
callbackChecks.code_verifier = verifier; | ||
switch (usePKCE) { | ||
case 'S256': | ||
parameters.code_challenge = openid_client_1.generators.codeChallenge(verifier); | ||
parameters.code_challenge_method = 'S256'; | ||
break; | ||
case 'plain': | ||
parameters.code_challenge = verifier; | ||
break; | ||
} | ||
const sessionValue = pick(params, 'nonce', 'state', 'max_age', 'response_type'); | ||
if (usePKCE !== false && params.response_type === 'code') { | ||
const verifier = openid_client_1.generators.random(); | ||
sessionValue.code_verifier = verifier; | ||
switch (usePKCE) { | ||
case 'S256': | ||
params.code_challenge = openid_client_1.generators.codeChallenge(verifier); | ||
params.code_challenge_method = 'S256'; | ||
break; | ||
case 'plain': | ||
params.code_challenge = verifier; | ||
break; | ||
} | ||
} | ||
request.session.set(sessionKey, sessionValue); | ||
return await reply.redirect(client.authorizationUrl(params)); | ||
} | ||
// #endregion | ||
// #region authentication response | ||
const sessionValue = request.session.get(sessionKey); | ||
if (sessionValue === undefined || | ||
Object.keys(sessionValue).length === 0) { | ||
throw new Error((0, util_1.format)('did not find expected authorization request details in session, req.session["%s"] is %j', sessionKey, sessionValue)); | ||
} | ||
request.session.set(sessionKey, undefined); | ||
const params = { | ||
..._params, | ||
...options?.params, | ||
}; | ||
const extras = { ...defaults?.extras, ...options?.extras }; | ||
// eslint-disable-next-line @typescript-eslint/naming-convention | ||
const { state, nonce, max_age, code_verifier, response_type } = sessionValue; | ||
const checks = { | ||
state, | ||
nonce, | ||
max_age, | ||
code_verifier, | ||
response_type, | ||
}; | ||
const tokenset = await client.callback(params.redirect_uri, parameters, checks, extras); | ||
return await write?.call(this, request, reply, tokenset); | ||
// #endregion | ||
}; | ||
request.session.set(sessionKey, callbackChecks); | ||
return await reply.redirect(client.authorizationUrl(parameters)); | ||
} | ||
// #endregion | ||
// #region authentication response | ||
const callbackChecks = request.session.get(sessionKey); | ||
if (callbackChecks === undefined || | ||
Object.keys(callbackChecks).length === 0) { | ||
throw new exports.SessionValueError(sessionKey); | ||
} | ||
request.session.set(sessionKey, undefined); | ||
const tokenset = await client.callback(redirect_uri, callbackParams, callbackChecks, options?.extras); | ||
return await write?.call(this, request, reply, tokenset); | ||
// #endregion | ||
}; | ||
return openIDLoginHandlerFactory; | ||
}; | ||
exports.openIDAuthLoginFactory = openIDAuthLoginFactory; | ||
exports.openIDLoginHandlerFactory = openIDLoginHandlerFactory; | ||
//# sourceMappingURL=login.js.map |
import { RouteHandlerMethod } from 'fastify'; | ||
import { Client, EndSessionParameters } from 'openid-client'; | ||
import { OpenIDReadTokens, OpenIDWriteTokens } from './types'; | ||
export interface OpenIDLogoutOptions { | ||
export interface OpenIDLogoutHandlerOptions { | ||
parameters?: EndSessionParameters; | ||
@@ -9,3 +9,2 @@ read: OpenIDReadTokens; | ||
} | ||
export declare type OpenIDLogoutHandlerFactory = (options?: OpenIDLogoutOptions) => RouteHandlerMethod; | ||
export declare const openIDAuthLogoutFactory: (client: Client, defaults: OpenIDLogoutOptions) => OpenIDLogoutHandlerFactory; | ||
export declare const openIDLogoutHandlerFactory: (client: Client, options: OpenIDLogoutHandlerOptions) => RouteHandlerMethod; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.openIDAuthLogoutFactory = void 0; | ||
const openIDAuthLogoutFactory = (client, defaults) => { | ||
const openIDLogoutHandlerFactory = (options) => { | ||
const { parameters, read, write } = { ...defaults, ...options }; | ||
return async function openIDLogoutHandler(request, reply) { | ||
const tokenset = await read.call(this, request, reply); | ||
// #region authentication request | ||
if (Object.keys(request.query).length === 0) { | ||
// eslint-disable-next-line @typescript-eslint/naming-convention | ||
const { id_token, session_state } = tokenset; | ||
if (id_token !== undefined) { | ||
return await reply.redirect(client.endSessionUrl({ | ||
id_token_hint: id_token, | ||
state: session_state, | ||
...parameters, | ||
})); | ||
} | ||
exports.openIDLogoutHandlerFactory = void 0; | ||
const openIDLogoutHandlerFactory = (client, options) => { | ||
const { parameters, read, write } = options; | ||
return async function openIDLogoutHandler(request, reply) { | ||
const tokenset = await read.call(this, request, reply); | ||
// #region authentication request | ||
if (Object.keys(request.query).length === 0) { | ||
// eslint-disable-next-line @typescript-eslint/naming-convention | ||
const { id_token, session_state } = tokenset; | ||
if (id_token !== undefined) { | ||
return await reply.redirect(client.endSessionUrl({ | ||
id_token_hint: id_token, | ||
state: session_state, | ||
...parameters, | ||
})); | ||
} | ||
// #endregion | ||
// #region authentication response | ||
return await write?.call(this, request, reply, tokenset); | ||
// #endregion | ||
}; | ||
} | ||
// #endregion | ||
// #region authentication response | ||
return await write?.call(this, request, reply, tokenset); | ||
// #endregion | ||
}; | ||
return openIDLogoutHandlerFactory; | ||
}; | ||
exports.openIDAuthLogoutFactory = openIDAuthLogoutFactory; | ||
exports.openIDLogoutHandlerFactory = openIDLogoutHandlerFactory; | ||
//# sourceMappingURL=logout.js.map |
@@ -1,21 +0,21 @@ | ||
import { FastifyPluginAsync } from 'fastify'; | ||
import { FastifyPluginAsync, RouteHandlerMethod } from 'fastify'; | ||
import { Client } from 'openid-client'; | ||
import { OpenIDLoginHandlerFactory, OpenIDLoginOptions } from './login'; | ||
import { OpenIDLogoutHandlerFactory, OpenIDLogoutOptions } from './logout'; | ||
import { OpenIDRefreshHandlerFactory, OpenIDRefreshOptions } from './refresh'; | ||
import { OpenIDVerifyHandlerFactory, OpenIDVerifyOptions } from './verify'; | ||
import { OpenIDLoginHandlerOptions } from './login'; | ||
import { OpenIDLogoutHandlerOptions } from './logout'; | ||
import { OpenIDRefreshHandlerOptions } from './refresh'; | ||
import { OpenIDVerifyHandlerOptions } from './verify'; | ||
export interface FastifyOpenIDAuthPluginOptions { | ||
name: string; | ||
client: Client; | ||
login?: OpenIDLoginOptions; | ||
verify: OpenIDVerifyOptions; | ||
refresh: OpenIDRefreshOptions; | ||
logout: OpenIDLogoutOptions; | ||
login?: OpenIDLoginHandlerOptions; | ||
verify: OpenIDVerifyHandlerOptions; | ||
refresh: OpenIDRefreshHandlerOptions; | ||
logout: OpenIDLogoutHandlerOptions; | ||
} | ||
export interface OpenIDAuthNamespace { | ||
login: OpenIDLoginHandlerFactory; | ||
verify: OpenIDVerifyHandlerFactory; | ||
refresh: OpenIDRefreshHandlerFactory; | ||
logout: OpenIDLogoutHandlerFactory; | ||
login: RouteHandlerMethod; | ||
verify: RouteHandlerMethod; | ||
refresh: RouteHandlerMethod; | ||
logout: RouteHandlerMethod; | ||
} | ||
export declare const openIDAuthPlugin: FastifyPluginAsync<FastifyOpenIDAuthPluginOptions>; |
@@ -15,6 +15,6 @@ "use strict"; | ||
const openIDAuthNamespace = { | ||
login: (0, login_1.openIDAuthLoginFactory)(client, login), | ||
refresh: (0, refresh_1.openIDAuthRefreshFactory)(client, refresh), | ||
verify: (0, verify_1.openIDAuthVerifyFactory)(verify), | ||
logout: (0, logout_1.openIDAuthLogoutFactory)(client, logout), | ||
login: (0, login_1.openIDLoginHandlerFactory)(client, login), | ||
refresh: (0, refresh_1.openIDRefreshHandlerFactory)(client, refresh), | ||
verify: (0, verify_1.openIDVerifyHandlerFactory)(verify), | ||
logout: (0, logout_1.openIDLogoutHandlerFactory)(client, logout), | ||
}; | ||
@@ -21,0 +21,0 @@ fastify.log.trace(`decorating \`fastify[${name}]\` with OpenIDAuthNamespace`); |
import { RouteHandlerMethod } from 'fastify'; | ||
import { Client, RefreshExtras } from 'openid-client'; | ||
import { OpenIDReadTokens, OpenIDWriteTokens } from './types'; | ||
export interface OpenIDRefreshOptions { | ||
export interface OpenIDRefreshHandlerOptions { | ||
extras?: RefreshExtras; | ||
@@ -9,3 +9,2 @@ read: OpenIDReadTokens; | ||
} | ||
export declare type OpenIDRefreshHandlerFactory = (options?: OpenIDRefreshOptions) => RouteHandlerMethod; | ||
export declare const openIDAuthRefreshFactory: (client: Client, defaults: OpenIDRefreshOptions) => OpenIDRefreshHandlerFactory; | ||
export declare const openIDRefreshHandlerFactory: (client: Client, options: OpenIDRefreshHandlerOptions) => RouteHandlerMethod; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.openIDAuthRefreshFactory = void 0; | ||
const openIDAuthRefreshFactory = (client, defaults) => { | ||
const openIDRefreshHandlerFactory = (options) => { | ||
const { extras, read, write } = { ...defaults, ...options }; | ||
return async function openIDRefreshHandler(request, reply) { | ||
const oldTokenset = await read.call(this, request, reply); | ||
if (oldTokenset.expired()) { | ||
request.log.trace(`OpenID token expired ${oldTokenset.expires_at !== undefined | ||
? new Date(oldTokenset.expires_at * 1000).toUTCString() | ||
: 'recently'}, refreshing`); | ||
const newTokenset = await client.refresh(oldTokenset, extras); | ||
request.log.trace('OpenID tokens refreshed'); | ||
return await write?.call(this, request, reply, newTokenset); | ||
} | ||
}; | ||
exports.openIDRefreshHandlerFactory = void 0; | ||
const openIDRefreshHandlerFactory = (client, options) => { | ||
const { extras, read, write } = options; | ||
return async function openIDRefreshHandler(request, reply) { | ||
const oldTokenset = await read.call(this, request, reply); | ||
if (oldTokenset.expired()) { | ||
request.log.trace(`OpenID token expired ${oldTokenset.expires_at !== undefined | ||
? new Date(oldTokenset.expires_at * 1000).toUTCString() | ||
: 'recently'}, refreshing`); | ||
const newTokenset = await client.refresh(oldTokenset, extras); | ||
request.log.trace('OpenID tokens refreshed'); | ||
return await write?.call(this, request, reply, newTokenset); | ||
} | ||
}; | ||
return openIDRefreshHandlerFactory; | ||
}; | ||
exports.openIDAuthRefreshFactory = openIDAuthRefreshFactory; | ||
exports.openIDRefreshHandlerFactory = openIDRefreshHandlerFactory; | ||
//# sourceMappingURL=refresh.js.map |
@@ -6,3 +6,3 @@ import { RouteHandlerMethod } from 'fastify'; | ||
export declare type OpenIDVerifyTokens = keyof Pick<TokenSet, 'id_token' | 'access_token' | 'refresh_token'>; | ||
export interface OpenIDVerifyOptions { | ||
export interface OpenIDVerifyHandlerOptions { | ||
options?: JWTVerifyOptions; | ||
@@ -14,3 +14,2 @@ key: JWTVerifyGetKey | KeyLike | Uint8Array; | ||
} | ||
export declare type OpenIDVerifyHandlerFactory = (options?: OpenIDVerifyOptions) => RouteHandlerMethod; | ||
export declare const openIDAuthVerifyFactory: (defaults: OpenIDVerifyOptions) => OpenIDVerifyHandlerFactory; | ||
export declare const openIDVerifyHandlerFactory: (options: OpenIDVerifyHandlerOptions) => RouteHandlerMethod; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.openIDAuthVerifyFactory = void 0; | ||
exports.openIDVerifyHandlerFactory = void 0; | ||
const jose_1 = require("jose"); | ||
const openIDAuthVerifyFactory = (defaults) => { | ||
const openIDVerifyHandlerFactory = (options) => { | ||
const { options: jwtVerifyOptions, key, tokens = ['id_token'], read, write, } = { ...defaults, ...options }; | ||
return async function openIDVerify(request, reply) { | ||
const tokenset = await read.call(this, request, reply); | ||
// eslint-disable-next-line @typescript-eslint/naming-convention | ||
for (const token of tokens) { | ||
const jwt = tokenset[token]; | ||
if (jwt !== undefined) { | ||
key instanceof Function | ||
? await (0, jose_1.jwtVerify)(jwt, key, jwtVerifyOptions) | ||
: await (0, jose_1.jwtVerify)(jwt, key, jwtVerifyOptions); | ||
} | ||
const openIDVerifyHandlerFactory = (options) => { | ||
const { options: jwtVerifyOptions, key, tokens = ['id_token'], read, write, } = options; | ||
return async function openIDVerify(request, reply) { | ||
const tokenset = await read.call(this, request, reply); | ||
// eslint-disable-next-line @typescript-eslint/naming-convention | ||
for (const token of tokens) { | ||
const jwt = tokenset[token]; | ||
if (jwt !== undefined) { | ||
key instanceof Function | ||
? await (0, jose_1.jwtVerify)(jwt, key, jwtVerifyOptions) | ||
: await (0, jose_1.jwtVerify)(jwt, key, jwtVerifyOptions); | ||
} | ||
return await write?.call(this, request, reply, tokenset); | ||
}; | ||
} | ||
return await write?.call(this, request, reply, tokenset); | ||
}; | ||
return openIDVerifyHandlerFactory; | ||
}; | ||
exports.openIDAuthVerifyFactory = openIDAuthVerifyFactory; | ||
exports.openIDVerifyHandlerFactory = openIDVerifyHandlerFactory; | ||
//# sourceMappingURL=verify.js.map |
{ | ||
"name": "fastify-openid-auth", | ||
"version": "2.1.0", | ||
"version": "3.0.0", | ||
"description": "Fastify auth plugin for openid-client", | ||
@@ -33,17 +33,18 @@ "main": "dist/index.js", | ||
"@semantic-release/github": "^8.0.2", | ||
"@semantic-release/npm": "^9.0.0", | ||
"@semantic-release/npm": "^9.0.1", | ||
"@semantic-release/release-notes-generator": "^10.0.3", | ||
"@tsconfig/node16": "^1.0.2", | ||
"@types/node": "^17.0.17", | ||
"@types/node": "^17.0.21", | ||
"@wtf/eslint-config": "^1.0.0", | ||
"semantic-release": "^19.0.2", | ||
"shx": "^0.3.4", | ||
"typescript": "^4.5.5" | ||
"typescript": "^4.6.2" | ||
}, | ||
"dependencies": { | ||
"fastify": "^3.27.1", | ||
"fastify": "^3.27.2", | ||
"fastify-error": "^1.0.0", | ||
"fastify-plugin": "^3.0.1", | ||
"jose": "^4.5.0", | ||
"jose": "^4.5.1", | ||
"openid-client": "^5.1.3" | ||
} | ||
} |
223
src/login.ts
@@ -1,3 +0,4 @@ | ||
/* eslint-disable @typescript-eslint/method-signature-style */ | ||
/* eslint-disable @typescript-eslint/naming-convention */ | ||
import { RouteHandlerMethod } from 'fastify'; | ||
import createError from 'fastify-error'; | ||
import { | ||
@@ -11,3 +12,2 @@ AuthorizationParameters, | ||
} from 'openid-client'; | ||
import { format } from 'util'; | ||
import { OpenIDWriteTokens } from './types'; | ||
@@ -18,9 +18,10 @@ | ||
session: { | ||
get(key: string): any; | ||
set(key: string, value: any): void; | ||
get: <T>(key: string) => T; | ||
set: (key: string, value: unknown) => void; | ||
}; | ||
} | ||
} | ||
export interface OpenIDLoginOptions { | ||
params?: AuthorizationParameters; | ||
export interface OpenIDLoginHandlerOptions { | ||
parameters?: AuthorizationParameters; | ||
extras?: CallbackExtras; | ||
@@ -32,14 +33,20 @@ usePKCE?: boolean | 'plain' | 'S256'; | ||
export type OpenIDLoginHandlerFactory = ( | ||
options?: OpenIDLoginOptions | ||
) => RouteHandlerMethod; | ||
export const SessionKeyError = createError( | ||
'FST_SESSION_KEY', | ||
'client must have an issuer with an identifier', | ||
500 | ||
); | ||
const pick = <T, K extends keyof T>(obj: T, ...keys: K[]): Pick<T, K> => { | ||
const ret: any = {}; | ||
keys.forEach((key) => { | ||
ret[key] = obj[key]; | ||
}); | ||
return ret; | ||
}; | ||
export const SessionValueError = createError( | ||
'FST_SESSION_VALUE', | ||
'did not find expected authorization request details in req.session["%s"]', | ||
500 | ||
); | ||
export const SupportedMethodError = createError( | ||
'FST_SUPPORTED_METHOD', | ||
'neither code_challenge_method supported by the client is supported by the issuer', | ||
500 | ||
); | ||
const resolveResponseType = (client: Client): string | undefined => { | ||
@@ -65,3 +72,3 @@ const { length, 0: value } = client.metadata.response_types ?? []; | ||
const resolveSupportedMethods = (issuer: Issuer): string => { | ||
const resolveSupportedMethod = (issuer: Issuer): string => { | ||
const supportedMethods = Array.isArray( | ||
@@ -78,5 +85,3 @@ issuer.code_challenge_methods_supported | ||
} else { | ||
throw new TypeError( | ||
'neither code_challenge_method supported by the client is supported by the issuer' | ||
); | ||
throw new SupportedMethodError(); | ||
} | ||
@@ -87,3 +92,3 @@ }; | ||
if (issuer.metadata.issuer === undefined) { | ||
throw new TypeError('client must have an issuer with an identifier'); | ||
throw new SessionKeyError(); | ||
} | ||
@@ -93,116 +98,94 @@ return `oidc:${new URL(issuer.metadata.issuer).hostname}`; | ||
export const openIDAuthLoginFactory = ( | ||
export const openIDLoginHandlerFactory = ( | ||
client: Client, | ||
defaults?: OpenIDLoginOptions | ||
): OpenIDLoginHandlerFactory => { | ||
const _params: AuthorizationParameters = { | ||
scope: 'openid', | ||
response_type: resolveResponseType(client), | ||
redirect_uri: resolveRedirectUri(client), | ||
...defaults?.params, | ||
}; | ||
const _sessionKey = | ||
defaults?.sessionKey !== undefined | ||
? defaults.sessionKey | ||
options?: OpenIDLoginHandlerOptions | ||
): RouteHandlerMethod => { | ||
const redirect_uri = | ||
options?.parameters?.redirect_uri !== undefined | ||
? options.parameters.redirect_uri | ||
: resolveRedirectUri(client); | ||
const sessionKey = | ||
options?.sessionKey !== undefined | ||
? options.sessionKey | ||
: resolveSessionKey(client.issuer); | ||
const _usePKCE = | ||
defaults?.usePKCE !== undefined | ||
? defaults.usePKCE === true | ||
? resolveSupportedMethods(client.issuer) | ||
: defaults.usePKCE | ||
const usePKCE = | ||
options?.usePKCE !== undefined | ||
? options.usePKCE === true | ||
? resolveSupportedMethod(client.issuer) | ||
: options.usePKCE | ||
: false; | ||
const openIDLoginHandlerFactory: OpenIDLoginHandlerFactory = (options?) => { | ||
const { | ||
sessionKey = _sessionKey, | ||
usePKCE = _usePKCE, | ||
write, | ||
} = { ...defaults, ...options }; | ||
const { write } = { ...options }; | ||
return async function openIDLoginHandler(request, reply) { | ||
const parameters = client.callbackParams(request.raw); | ||
return async function openIDLoginHandler(request, reply) { | ||
const callbackParams = client.callbackParams(request.raw); | ||
// #region authentication request | ||
if (Object.keys(parameters).length === 0) { | ||
const params = { | ||
state: generators.random(), | ||
..._params, | ||
...options?.params, | ||
}; | ||
if (params.nonce === undefined && params.response_type === 'code') { | ||
params.nonce = generators.random(); | ||
} | ||
const sessionValue: Record<string, unknown> = pick( | ||
params, | ||
'nonce', | ||
'state', | ||
'max_age', | ||
'response_type' | ||
); | ||
if (usePKCE !== false && params.response_type === 'code') { | ||
const verifier = generators.random(); | ||
// #region authentication request | ||
if (Object.keys(callbackParams).length === 0) { | ||
const response_type = | ||
options?.parameters?.response_type !== undefined | ||
? options.parameters.response_type | ||
: resolveResponseType(client); | ||
const parameters = { | ||
scope: 'openid', | ||
state: generators.random(), | ||
redirect_uri, | ||
response_type, | ||
...options?.parameters, | ||
}; | ||
if ( | ||
parameters.nonce === undefined && | ||
parameters.response_type === 'code' | ||
) { | ||
parameters.nonce = generators.random(); | ||
} | ||
const callbackChecks: OpenIDCallbackChecks = (({ | ||
nonce, | ||
state, | ||
max_age, | ||
response_type, | ||
}) => ({ nonce, state, max_age, response_type }))(parameters); | ||
if (usePKCE !== false && parameters.response_type === 'code') { | ||
const verifier = generators.random(); | ||
sessionValue.code_verifier = verifier; | ||
callbackChecks.code_verifier = verifier; | ||
switch (usePKCE) { | ||
case 'S256': | ||
params.code_challenge = generators.codeChallenge(verifier); | ||
params.code_challenge_method = 'S256'; | ||
break; | ||
case 'plain': | ||
params.code_challenge = verifier; | ||
break; | ||
} | ||
switch (usePKCE) { | ||
case 'S256': | ||
parameters.code_challenge = generators.codeChallenge(verifier); | ||
parameters.code_challenge_method = 'S256'; | ||
break; | ||
case 'plain': | ||
parameters.code_challenge = verifier; | ||
break; | ||
} | ||
} | ||
request.session.set(sessionKey, sessionValue); | ||
request.session.set(sessionKey, callbackChecks); | ||
return await reply.redirect(client.authorizationUrl(params)); | ||
} | ||
// #endregion | ||
return await reply.redirect(client.authorizationUrl(parameters)); | ||
} | ||
// #endregion | ||
// #region authentication response | ||
const sessionValue = request.session.get(sessionKey); | ||
if ( | ||
sessionValue === undefined || | ||
Object.keys(sessionValue).length === 0 | ||
) { | ||
throw new Error( | ||
format( | ||
'did not find expected authorization request details in session, req.session["%s"] is %j', | ||
sessionKey, | ||
sessionValue | ||
) | ||
); | ||
} | ||
// #region authentication response | ||
const callbackChecks: OpenIDCallbackChecks = | ||
request.session.get(sessionKey); | ||
if ( | ||
callbackChecks === undefined || | ||
Object.keys(callbackChecks).length === 0 | ||
) { | ||
throw new SessionValueError(sessionKey); | ||
} | ||
request.session.set(sessionKey, undefined); | ||
request.session.set(sessionKey, undefined); | ||
const params = { | ||
..._params, | ||
...options?.params, | ||
}; | ||
const extras = { ...defaults?.extras, ...options?.extras }; | ||
// eslint-disable-next-line @typescript-eslint/naming-convention | ||
const { state, nonce, max_age, code_verifier, response_type } = | ||
sessionValue; | ||
const checks: OpenIDCallbackChecks = { | ||
state, | ||
nonce, | ||
max_age, | ||
code_verifier, | ||
response_type, | ||
}; | ||
const tokenset = await client.callback( | ||
params.redirect_uri, | ||
parameters, | ||
checks, | ||
extras | ||
); | ||
return await write?.call(this, request, reply, tokenset); | ||
// #endregion | ||
}; | ||
const tokenset = await client.callback( | ||
redirect_uri, | ||
callbackParams, | ||
callbackChecks, | ||
options?.extras | ||
); | ||
return await write?.call(this, request, reply, tokenset); | ||
// #endregion | ||
}; | ||
return openIDLoginHandlerFactory; | ||
}; |
@@ -5,3 +5,3 @@ import { RouteHandlerMethod } from 'fastify'; | ||
export interface OpenIDLogoutOptions { | ||
export interface OpenIDLogoutHandlerOptions { | ||
parameters?: EndSessionParameters; | ||
@@ -12,39 +12,31 @@ read: OpenIDReadTokens; | ||
export type OpenIDLogoutHandlerFactory = ( | ||
options?: OpenIDLogoutOptions | ||
) => RouteHandlerMethod; | ||
export const openIDAuthLogoutFactory = ( | ||
export const openIDLogoutHandlerFactory = ( | ||
client: Client, | ||
defaults: OpenIDLogoutOptions | ||
): OpenIDLogoutHandlerFactory => { | ||
const openIDLogoutHandlerFactory: OpenIDLogoutHandlerFactory = (options?) => { | ||
const { parameters, read, write } = { ...defaults, ...options }; | ||
options: OpenIDLogoutHandlerOptions | ||
): RouteHandlerMethod => { | ||
const { parameters, read, write } = options; | ||
return async function openIDLogoutHandler(request, reply) { | ||
const tokenset = await read.call(this, request, reply); | ||
return async function openIDLogoutHandler(request, reply) { | ||
const tokenset = await read.call(this, request, reply); | ||
// #region authentication request | ||
if (Object.keys(request.query as object).length === 0) { | ||
// eslint-disable-next-line @typescript-eslint/naming-convention | ||
const { id_token, session_state } = tokenset; | ||
if (id_token !== undefined) { | ||
return await reply.redirect( | ||
client.endSessionUrl({ | ||
id_token_hint: id_token, | ||
state: session_state, | ||
...parameters, | ||
}) | ||
); | ||
} | ||
// #region authentication request | ||
if (Object.keys(request.query as object).length === 0) { | ||
// eslint-disable-next-line @typescript-eslint/naming-convention | ||
const { id_token, session_state } = tokenset; | ||
if (id_token !== undefined) { | ||
return await reply.redirect( | ||
client.endSessionUrl({ | ||
id_token_hint: id_token, | ||
state: session_state, | ||
...parameters, | ||
}) | ||
); | ||
} | ||
// #endregion | ||
} | ||
// #endregion | ||
// #region authentication response | ||
return await write?.call(this, request, reply, tokenset); | ||
// #endregion | ||
}; | ||
// #region authentication response | ||
return await write?.call(this, request, reply, tokenset); | ||
// #endregion | ||
}; | ||
return openIDLogoutHandlerFactory; | ||
}; |
@@ -1,23 +0,16 @@ | ||
import { FastifyPluginAsync } from 'fastify'; | ||
import { FastifyPluginAsync, RouteHandlerMethod } from 'fastify'; | ||
import fp from 'fastify-plugin'; | ||
import { Client } from 'openid-client'; | ||
import { openIDLoginHandlerFactory, OpenIDLoginHandlerOptions } from './login'; | ||
import { | ||
openIDAuthLoginFactory, | ||
OpenIDLoginHandlerFactory, | ||
OpenIDLoginOptions, | ||
} from './login'; | ||
import { | ||
openIDAuthLogoutFactory, | ||
OpenIDLogoutHandlerFactory, | ||
OpenIDLogoutOptions, | ||
openIDLogoutHandlerFactory, | ||
OpenIDLogoutHandlerOptions, | ||
} from './logout'; | ||
import { | ||
openIDAuthRefreshFactory, | ||
OpenIDRefreshHandlerFactory, | ||
OpenIDRefreshOptions, | ||
openIDRefreshHandlerFactory, | ||
OpenIDRefreshHandlerOptions, | ||
} from './refresh'; | ||
import { | ||
openIDAuthVerifyFactory, | ||
OpenIDVerifyHandlerFactory, | ||
OpenIDVerifyOptions, | ||
openIDVerifyHandlerFactory, | ||
OpenIDVerifyHandlerOptions, | ||
} from './verify'; | ||
@@ -28,13 +21,13 @@ | ||
client: Client; | ||
login?: OpenIDLoginOptions; | ||
verify: OpenIDVerifyOptions; | ||
refresh: OpenIDRefreshOptions; | ||
logout: OpenIDLogoutOptions; | ||
login?: OpenIDLoginHandlerOptions; | ||
verify: OpenIDVerifyHandlerOptions; | ||
refresh: OpenIDRefreshHandlerOptions; | ||
logout: OpenIDLogoutHandlerOptions; | ||
} | ||
export interface OpenIDAuthNamespace { | ||
login: OpenIDLoginHandlerFactory; | ||
verify: OpenIDVerifyHandlerFactory; | ||
refresh: OpenIDRefreshHandlerFactory; | ||
logout: OpenIDLogoutHandlerFactory; | ||
login: RouteHandlerMethod; | ||
verify: RouteHandlerMethod; | ||
refresh: RouteHandlerMethod; | ||
logout: RouteHandlerMethod; | ||
} | ||
@@ -48,6 +41,6 @@ | ||
const openIDAuthNamespace: OpenIDAuthNamespace = { | ||
login: openIDAuthLoginFactory(client, login), | ||
refresh: openIDAuthRefreshFactory(client, refresh), | ||
verify: openIDAuthVerifyFactory(verify), | ||
logout: openIDAuthLogoutFactory(client, logout), | ||
login: openIDLoginHandlerFactory(client, login), | ||
refresh: openIDRefreshHandlerFactory(client, refresh), | ||
verify: openIDVerifyHandlerFactory(verify), | ||
logout: openIDLogoutHandlerFactory(client, logout), | ||
}; | ||
@@ -54,0 +47,0 @@ |
@@ -5,3 +5,3 @@ import { RouteHandlerMethod } from 'fastify'; | ||
export interface OpenIDRefreshOptions { | ||
export interface OpenIDRefreshHandlerOptions { | ||
extras?: RefreshExtras; | ||
@@ -12,33 +12,23 @@ read: OpenIDReadTokens; | ||
export type OpenIDRefreshHandlerFactory = ( | ||
options?: OpenIDRefreshOptions | ||
) => RouteHandlerMethod; | ||
export const openIDAuthRefreshFactory = ( | ||
export const openIDRefreshHandlerFactory = ( | ||
client: Client, | ||
defaults: OpenIDRefreshOptions | ||
): OpenIDRefreshHandlerFactory => { | ||
const openIDRefreshHandlerFactory: OpenIDRefreshHandlerFactory = ( | ||
options? | ||
) => { | ||
const { extras, read, write } = { ...defaults, ...options }; | ||
options: OpenIDRefreshHandlerOptions | ||
): RouteHandlerMethod => { | ||
const { extras, read, write } = options; | ||
return async function openIDRefreshHandler(request, reply) { | ||
const oldTokenset = await read.call(this, request, reply); | ||
if (oldTokenset.expired()) { | ||
request.log.trace( | ||
`OpenID token expired ${ | ||
oldTokenset.expires_at !== undefined | ||
? new Date(oldTokenset.expires_at * 1000).toUTCString() | ||
: 'recently' | ||
}, refreshing` | ||
); | ||
const newTokenset = await client.refresh(oldTokenset, extras); | ||
request.log.trace('OpenID tokens refreshed'); | ||
return await write?.call(this, request, reply, newTokenset); | ||
} | ||
}; | ||
return async function openIDRefreshHandler(request, reply) { | ||
const oldTokenset = await read.call(this, request, reply); | ||
if (oldTokenset.expired()) { | ||
request.log.trace( | ||
`OpenID token expired ${ | ||
oldTokenset.expires_at !== undefined | ||
? new Date(oldTokenset.expires_at * 1000).toUTCString() | ||
: 'recently' | ||
}, refreshing` | ||
); | ||
const newTokenset = await client.refresh(oldTokenset, extras); | ||
request.log.trace('OpenID tokens refreshed'); | ||
return await write?.call(this, request, reply, newTokenset); | ||
} | ||
}; | ||
return openIDRefreshHandlerFactory; | ||
}; |
@@ -11,3 +11,3 @@ import { RouteHandlerMethod } from 'fastify'; | ||
export interface OpenIDVerifyOptions { | ||
export interface OpenIDVerifyHandlerOptions { | ||
options?: JWTVerifyOptions; | ||
@@ -20,34 +20,26 @@ key: JWTVerifyGetKey | KeyLike | Uint8Array; | ||
export type OpenIDVerifyHandlerFactory = ( | ||
options?: OpenIDVerifyOptions | ||
) => RouteHandlerMethod; | ||
export const openIDVerifyHandlerFactory = ( | ||
options: OpenIDVerifyHandlerOptions | ||
): RouteHandlerMethod => { | ||
const { | ||
options: jwtVerifyOptions, | ||
key, | ||
tokens = ['id_token'], | ||
read, | ||
write, | ||
} = options; | ||
export const openIDAuthVerifyFactory = ( | ||
defaults: OpenIDVerifyOptions | ||
): OpenIDVerifyHandlerFactory => { | ||
const openIDVerifyHandlerFactory: OpenIDVerifyHandlerFactory = (options?) => { | ||
const { | ||
options: jwtVerifyOptions, | ||
key, | ||
tokens = ['id_token'], | ||
read, | ||
write, | ||
} = { ...defaults, ...options }; | ||
return async function openIDVerify(request, reply) { | ||
const tokenset = await read.call(this, request, reply); | ||
// eslint-disable-next-line @typescript-eslint/naming-convention | ||
for (const token of tokens) { | ||
const jwt = tokenset[token]; | ||
if (jwt !== undefined) { | ||
key instanceof Function | ||
? await jwtVerify(jwt, key, jwtVerifyOptions) | ||
: await jwtVerify(jwt, key, jwtVerifyOptions); | ||
} | ||
return async function openIDVerify(request, reply) { | ||
const tokenset = await read.call(this, request, reply); | ||
// eslint-disable-next-line @typescript-eslint/naming-convention | ||
for (const token of tokens) { | ||
const jwt = tokenset[token]; | ||
if (jwt !== undefined) { | ||
key instanceof Function | ||
? await jwtVerify(jwt, key, jwtVerifyOptions) | ||
: await jwtVerify(jwt, key, jwtVerifyOptions); | ||
} | ||
return await write?.call(this, request, reply, tokenset); | ||
}; | ||
} | ||
return await write?.call(this, request, reply, tokenset); | ||
}; | ||
return openIDVerifyHandlerFactory; | ||
}; |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
49139
-4.11%5
25%694
-8.56%