oidc-provider
Version:
OAuth 2.0 Authorization Server implementation for Node.js with OpenID Connect
123 lines (101 loc) • 3.63 kB
JavaScript
import * as crypto from 'node:crypto';
import sessionMiddleware from '../shared/session.js';
import paramsMiddleware from '../shared/assemble_params.js';
import bodyParser from '../shared/conditional_body.js';
import rejectDupes from '../shared/reject_dupes.js';
import instance from '../helpers/weak_cache.js';
import { InvalidClient, InvalidRequest } from '../helpers/errors.js';
import * as formHtml from '../helpers/user_code_form.js';
import formPost from '../response_modes/form_post.js';
import { normalize, denormalize } from '../helpers/user_codes.js';
import {
NoCodeError, NotFoundError, ExpiredError, AlreadyUsedError, AbortedError,
} from '../helpers/re_render_errors.js';
const parseBody = bodyParser.bind(undefined, 'application/x-www-form-urlencoded');
export const get = [
sessionMiddleware,
paramsMiddleware.bind(undefined, new Set(['user_code'])),
async function renderCodeVerification(ctx) {
const { charset, userCodeInputSource } = instance(ctx.oidc.provider).features.deviceFlow;
// TODO: generic xsrf middleware to remove this
const secret = crypto.randomBytes(24).toString('hex');
ctx.oidc.session.state = { secret };
const action = ctx.oidc.urlFor('code_verification');
if (ctx.oidc.params.user_code) {
formPost(ctx, action, {
xsrf: secret,
user_code: ctx.oidc.params.user_code,
});
} else {
await userCodeInputSource(ctx, formHtml.input(action, secret, undefined, charset));
}
},
];
export const post = [
sessionMiddleware,
parseBody,
paramsMiddleware.bind(undefined, new Set(['xsrf', 'user_code', 'confirm', 'abort'])),
rejectDupes.bind(undefined, {}),
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 loadDeviceCodeByUserInput(ctx, next) {
const { userCodeConfirmSource, mask } = instance(ctx.oidc.provider).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();
},
function cleanup(ctx, next) {
ctx.oidc.session.state = undefined;
return next();
},
];