@strongnguyen/oidc-provider
Version:
OAuth 2.0 Authorization Server implementation for Node.js with OpenID Connect
287 lines (240 loc) • 8.27 kB
JavaScript
const crypto = require('crypto');
const util = require('util');
const sessionMiddleware = require('../shared/session');
const paramsMiddleware = require('../shared/assemble_params');
const bodyParser = require('../shared/conditional_body');
const rejectDupes = require('../shared/reject_dupes');
const instance = require('../helpers/weak_cache');
const {
InvalidClient, InvalidRequest, InvalidToken, InsufficientScope,
} = require('../helpers/errors');
const {
NoCodeError, NotFoundError, ExpiredError, AlreadyUsedError, AbortedError,
} = require('../helpers/re_render_errors');
const formHtml = require('../helpers/user_code_form');
const formPost = require('../response_modes/form_post');
const { normalize, denormalize } = require('../helpers/user_codes');
const noCache = require('../shared/no_cache');
const presence = require('../helpers/validate_presence');
const parseBody = bodyParser.bind(undefined, 'application/x-www-form-urlencoded');
const randomFill = util.promisify(crypto.randomFill);
async function accessTokenAuth(ctx, next) {
const regexTest = /^Bearer\s+[A-Za-z0-9-._~+/]+$/;
if (!ctx.req.headers.authorization || !regexTest.test(ctx.req.headers.authorization)) {
throw new InvalidToken();
}
const token = await ctx.oidc.provider.AccessToken.find(ctx.req.headers.authorization.split(' ')[1]);
if (!token || !token.isValid) {
throw new InvalidToken('Invalid token');
}
if (token.grantId) {
const grant = await ctx.oidc.provider.Grant.find(token.grantId, {
ignoreExpiration: true,
});
if (!grant) {
throw new InvalidToken('Grant not found');
}
if (grant.isExpired) {
throw new InvalidToken('Grant is expired');
}
if (grant.clientId !== token.clientId) {
throw new InvalidToken('Grant client not match');
}
if (grant.accountId !== token.accountId) {
throw new InvalidToken('Grant account not match');
}
ctx.oidc.entity('Grant', grant);
}
const session = await ctx.oidc.provider.Session.findByUid(token.sessionUid);
if (!session) {
throw new InvalidToken('Session not found');
}
ctx.oidc.entity('Session', session);
ctx.oidc.entity('AccessToken', token);
await next();
}
async function codeVerificationCSRF(ctx, next) {
if (!ctx.oidc.session.state) {
throw new InvalidRequest('could not find device form details');
}
if (ctx.oidc.session.state.secret !== ctx.oidc.params.xsrf) {
throw new InvalidRequest('xsrf token invalid');
}
await next();
}
async function loadDeviceCodeByUserCodeInput(ctx, next) {
const { approvalScopeValidate } = instance(ctx.oidc.provider).configuration('features.deviceFlow');
const { user_code: userCode } = ctx.oidc.params;
const normalized = normalize(userCode);
const code = await ctx.oidc.provider.DeviceCode.findByUserCode(
normalized,
{ ignoreExpiration: true },
);
if (!code) {
throw new NotFoundError(userCode);
}
if (code.isExpired) {
throw new ExpiredError(userCode);
}
if (code.error || code.accountId || code.inFlight) {
throw new AlreadyUsedError(userCode);
}
if (!approvalScopeValidate(ctx, ctx.oidc.entities.AccessToken, code)) {
throw new InsufficientScope();
}
ctx.oidc.entity('DeviceCode', code);
await next();
}
function cleanup(ctx, next) {
ctx.oidc.session.state = undefined;
return next();
}
module.exports = {
get: [
sessionMiddleware,
paramsMiddleware.bind(undefined, new Set(['user_code'])),
async function renderCodeVerification(ctx, next) {
const {
features: { deviceFlow: { charset, userCodeInputSource } },
} = instance(ctx.oidc.provider).configuration();
// TODO: generic xsrf middleware to remove this
let secret = Buffer.allocUnsafe(24);
await randomFill(secret);
secret = secret.toString('hex');
ctx.oidc.session.state = { secret };
const action = ctx.oidc.urlFor('code_verification');
if (ctx.oidc.params.user_code) {
await formPost(ctx, action, {
xsrf: secret,
user_code: ctx.oidc.params.user_code,
});
} else {
await userCodeInputSource(ctx, formHtml.input(action, secret, undefined, charset));
}
await next();
},
],
post: [
sessionMiddleware,
parseBody,
paramsMiddleware.bind(undefined, new Set(['xsrf', 'user_code', 'confirm', 'abort'])),
rejectDupes.bind(undefined, {}),
codeVerificationCSRF,
async function loadDeviceCodeByUserInput(ctx, next) {
const { userCodeConfirmSource, mask } = instance(ctx.oidc.provider).configuration('features.deviceFlow');
const { user_code: userCode, confirm, abort } = ctx.oidc.params;
if (!userCode) {
throw new NoCodeError();
}
const normalized = normalize(userCode);
const code = await ctx.oidc.provider.DeviceCode.findByUserCode(
normalized,
{ ignoreExpiration: true },
);
if (!code) {
throw new NotFoundError(userCode);
}
if (code.isExpired) {
throw new ExpiredError(userCode);
}
if (code.error || code.accountId || code.inFlight) {
throw new AlreadyUsedError(userCode);
}
ctx.oidc.entity('DeviceCode', code);
if (abort) {
Object.assign(code, {
error: 'access_denied',
errorDescription: 'End-User aborted interaction',
});
await code.save();
throw new AbortedError();
}
if (!confirm) {
const client = await ctx.oidc.provider.Client.find(code.clientId);
if (!client) {
throw new InvalidClient('client is invalid', 'client not found');
}
ctx.oidc.entity('Client', client);
const action = ctx.oidc.urlFor('code_verification');
await userCodeConfirmSource(
ctx,
formHtml.confirm(action, ctx.oidc.session.state.secret, userCode),
client,
code.deviceInfo,
denormalize(normalized, mask),
);
return;
}
code.inFlight = true;
await code.save();
await next();
},
cleanup,
],
checkUserCode: [
noCache,
parseBody,
paramsMiddleware.bind(undefined, new Set(['user_code'])),
rejectDupes.bind(undefined, {}),
async function validateUserCodePresence(ctx, next) {
presence(ctx, 'user_code');
await next();
},
accessTokenAuth,
sessionMiddleware,
loadDeviceCodeByUserCodeInput,
async function checkUserCodeResponse(ctx, next) {
const { userCodeConfirmSource } = instance(ctx.oidc.provider)
.configuration('features.deviceFlow');
const { user_code: userCode } = ctx.oidc.params;
const code = ctx.oidc.entities.DeviceCode;
const client = await ctx.oidc.provider.Client.find(code.clientId);
if (!client) {
throw new InvalidClient('client is invalid', 'client not found');
}
ctx.oidc.entity('Client', client);
let secret = Buffer.allocUnsafe(24);
await randomFill(secret);
secret = secret.toString('hex');
ctx.oidc.session.state = { secret };
const action = ctx.oidc.urlFor('code_verification');
await userCodeConfirmSource(
ctx,
formHtml.confirm(action, secret, userCode),
client,
code.deviceInfo,
userCode,
);
await next();
},
],
approvalUserCode: [
noCache,
parseBody,
paramsMiddleware.bind(undefined, new Set(['xsrf', 'user_code', 'confirm'])),
rejectDupes.bind(undefined, {}),
async function validateInputPresence(ctx, next) {
presence(ctx, 'xsrf', 'user_code', 'confirm');
await next();
},
accessTokenAuth,
sessionMiddleware,
codeVerificationCSRF,
loadDeviceCodeByUserCodeInput,
async function confirmUserCode(ctx, next) {
const { confirm } = ctx.oidc.params;
const code = ctx.oidc.entities.DeviceCode;
if (confirm === 'no') {
Object.assign(code, {
error: 'access_denied',
errorDescription: 'End-User aborted interaction',
});
await code.save();
throw new AbortedError();
}
code.inFlight = true;
await code.save();
await next();
},
],
};