oidc-provider
Version:
OAuth 2.0 Authorization Server implementation for Node.js with OpenID Connect
285 lines (221 loc) • 7.53 kB
JavaScript
/* eslint-disable max-classes-per-file */
import * as events from 'node:events';
import isPlainObject from './_/is_plain_object.js';
import omitBy from './_/omit_by.js';
import { InvalidRequest, InvalidToken } from './errors.js';
import instance from './weak_cache.js';
import resolveResponseMode from './resolve_response_mode.js';
const COOKIES = Symbol();
class NoAccessTokenProvided extends InvalidToken {
constructor() {
super();
// eslint-disable-next-line no-multi-assign
this.error_detail = this.error_description = 'no access token provided';
}
}
export default function getContext(provider) {
const {
acceptQueryParamAccessTokens,
features: {
dPoP: dPoPConfig,
fapi,
},
scopes: oidcScopes,
} = instance(provider).configuration;
class OIDCContext extends events.EventEmitter {
#requestParamClaims = null;
#accessToken = null;
#fapiProfile = null;
constructor(ctx) {
super();
this.ctx = ctx;
this.route = ctx._matchedRouteName;
this.redirectUriCheckPerformed = false;
this.entities = {};
this.claims = {};
this.resourceServers = {};
}
get cookies() {
if (!this[COOKIES]) {
this[COOKIES] = provider.createContext(this.ctx.req, this.ctx.res).cookies;
this[COOKIES].secure = !this[COOKIES].secure && this.ctx.secure
? true : this[COOKIES].secure;
}
return this[COOKIES];
}
get fapiProfile() {
if (this.#fapiProfile === null) {
this.#fapiProfile = fapi.profile(this.ctx, this.client);
}
return this.#fapiProfile;
}
isFapi(...oneOf) {
const i = oneOf.indexOf(this.fapiProfile);
return i !== -1 ? oneOf[i] : undefined;
}
get issuer() { // eslint-disable-line class-methods-use-this
return provider.issuer;
}
get provider() { // eslint-disable-line class-methods-use-this
return provider;
}
entity(key, value) {
this.entities[key] = value;
if (key === 'Client') {
this.emit('assign.client', this.ctx, value);
}
}
urlFor(name, opt) {
const { originalUrl } = this.ctx.req;
const mountPath = originalUrl?.substring(0, originalUrl?.indexOf(this.ctx.request.url))
|| this.ctx.mountPath // koa-mount
|| this.ctx.req.baseUrl // expressApp.use('/op', provider.callback());
|| ''; // no mount
return new URL(provider.pathFor(name, { mountPath, ...opt }), this.ctx.href).href;
}
promptPending(name) {
if (this.ctx.oidc.route.endsWith('resume')) {
const should = new Set([...this.prompts]);
Object.keys(this.result || {}).forEach(Set.prototype.delete.bind(should));
return should.has(name);
}
// first pass
return this.prompts.has(name);
}
get requestParamClaims() {
if (this.#requestParamClaims) {
return this.#requestParamClaims;
}
const requestParamClaims = new Set();
if (this.params.claims) {
const {
userinfo, id_token: idToken,
} = JSON.parse(this.params.claims);
const claims = instance(provider).configuration.claimsSupported;
if (userinfo) {
Object.entries(userinfo).forEach(([claim, value]) => {
if (claims.has(claim) && (value === null || isPlainObject(value))) {
requestParamClaims.add(claim);
}
});
}
if (idToken) {
Object.entries(idToken).forEach(([claim, value]) => {
if (claims.has(claim) && (value === null || isPlainObject(value))) {
requestParamClaims.add(claim);
}
});
}
}
this.#requestParamClaims = requestParamClaims;
return requestParamClaims;
}
clientJwtAuthExpectedAudience() {
return new Set([this.issuer, this.urlFor('token'), this.urlFor(this.route)]);
}
get requestParamScopes() {
return new Set(this.params.scope?.split(' '));
}
get requestParamOIDCScopes() {
return new Set(this.params.scope?.split(' ').filter(Set.prototype.has.bind(oidcScopes)));
}
resolvedClaims() {
const rejected = this.session.rejectedClaimsFor(this.params.client_id);
const claims = structuredClone(this.claims);
claims.rejected = [...rejected];
return claims;
}
get responseMode() {
if (typeof this.params.response_mode === 'string') {
return this.params.response_mode;
}
if (this.params.response_type !== undefined) {
return resolveResponseMode(this.params.response_type);
}
return undefined;
}
get acr() {
return this.session.acr;
}
get amr() {
return this.session.amr;
}
get prompts() {
return new Set(this.params.prompt ? this.params.prompt.split(' ') : []);
}
get registrationAccessToken() {
return this.entities.RegistrationAccessToken;
}
get deviceCode() {
return this.entities.DeviceCode;
}
get authorizationCode() {
return this.entities.AuthorizationCode;
}
get refreshToken() {
return this.entities.RefreshToken;
}
get accessToken() {
return this.entities.AccessToken;
}
get account() {
return this.entities.Account;
}
get client() {
return this.entities.Client;
}
get grant() {
return this.entities.Grant;
}
getAccessToken({
acceptDPoP = false, acceptQueryParam = acceptQueryParamAccessTokens && !fapi.enabled,
} = {}) {
if (this.#accessToken) {
return this.#accessToken;
}
const { ctx } = this;
const mechanisms = omitBy({
body: ctx.is('application/x-www-form-urlencoded') && ctx.oidc.body?.access_token,
header: ctx.headers.authorization,
query: ctx.query.access_token,
}, (value) => typeof value !== 'string' || !value);
let mechanism;
let length;
let token;
try {
({ 0: [mechanism, token], length } = Object.entries(mechanisms));
} catch (err) {}
if (!length) {
throw new NoAccessTokenProvided();
}
if (length > 1) {
throw new InvalidRequest('access token must only be provided using one mechanism');
}
if (!acceptQueryParam && mechanism === 'query') {
throw new InvalidRequest('access tokens must not be provided via query parameter');
}
const dpop = acceptDPoP && dPoPConfig.enabled && ctx.get('DPoP');
if (mechanism === 'header') {
const header = token;
const { 0: scheme, 1: value, length: parts } = header.split(' ');
if (parts !== 2) {
throw new InvalidRequest('invalid authorization header value format');
}
if (dpop && scheme.toLowerCase() !== 'dpop') {
throw new InvalidRequest('authorization header scheme must be `DPoP` when DPoP is used');
} else if (!dpop && scheme.toLowerCase() === 'dpop') {
throw new InvalidRequest('`DPoP` header not provided');
} else if (!dpop && scheme.toLowerCase() !== 'bearer') {
throw new InvalidRequest('authorization header scheme must be `Bearer`');
}
token = value;
}
if (dpop && mechanism !== 'header') {
throw new InvalidRequest('`DPoP` tokens must be provided via an authorization header');
}
this.#accessToken = token;
return token;
}
}
return OIDCContext;
}