Add hooks for providing our own login UI
This commit is contained in:
+236
-8
@@ -2,10 +2,11 @@
|
||||
|
||||
exports = module.exports = {
|
||||
getProvider,
|
||||
getMiddleware
|
||||
attachInteractionRoutes
|
||||
};
|
||||
|
||||
const debug = require('debug')('box:oidc'),
|
||||
const assert = require('assert'),
|
||||
debug = require('debug')('box:oidc'),
|
||||
fs = require('fs'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
@@ -167,10 +168,243 @@ class CloudronAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
const store = new Map();
|
||||
const logins = new Map();
|
||||
|
||||
class Account {
|
||||
constructor(id, profile) {
|
||||
this.accountId = id || 'FIXME_someid';
|
||||
this.profile = profile;
|
||||
store.set(this.accountId, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param use - can either be "id_token" or "userinfo", depending on
|
||||
* where the specific claims are intended to be put in.
|
||||
* @param scope - the intended scope, while oidc-provider will mask
|
||||
* claims depending on the scope automatically you might want to skip
|
||||
* loading some claims from external resources etc. based on this detail
|
||||
* or not return them in id tokens but only userinfo and so on.
|
||||
*/
|
||||
async claims(use, scope) { // eslint-disable-line no-unused-vars
|
||||
if (this.profile) {
|
||||
return {
|
||||
sub: this.accountId, // it is essential to always return a sub claim
|
||||
email: this.profile.email,
|
||||
email_verified: this.profile.email_verified,
|
||||
family_name: this.profile.family_name,
|
||||
given_name: this.profile.given_name,
|
||||
locale: this.profile.locale,
|
||||
name: this.profile.name,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
sub: this.accountId, // it is essential to always return a sub claim
|
||||
|
||||
address: {
|
||||
country: '000',
|
||||
formatted: '000',
|
||||
locality: '000',
|
||||
postal_code: '000',
|
||||
region: '000',
|
||||
street_address: '000',
|
||||
},
|
||||
birthdate: '1987-10-16',
|
||||
email: 'johndoe@example.com',
|
||||
email_verified: false,
|
||||
family_name: 'Doe',
|
||||
gender: 'male',
|
||||
given_name: 'John',
|
||||
locale: 'en-US',
|
||||
middle_name: 'Middle',
|
||||
name: 'John Doe',
|
||||
nickname: 'Johny',
|
||||
phone_number: '+49 000 000000',
|
||||
phone_number_verified: false,
|
||||
picture: 'http://lorempixel.com/400/200/',
|
||||
preferred_username: 'johnny',
|
||||
profile: 'https://johnswebsite.com',
|
||||
updated_at: 1454704946,
|
||||
website: 'http://example.com',
|
||||
zoneinfo: 'Europe/Berlin',
|
||||
};
|
||||
}
|
||||
|
||||
static async findByFederated(provider, claims) {
|
||||
const id = `${provider}.${claims.sub}`;
|
||||
if (!logins.get(id)) {
|
||||
logins.set(id, new Account(id, claims));
|
||||
}
|
||||
return logins.get(id);
|
||||
}
|
||||
|
||||
static async findByLogin(login) {
|
||||
if (!logins.get(login)) {
|
||||
logins.set(login, new Account(login));
|
||||
}
|
||||
|
||||
return logins.get(login);
|
||||
}
|
||||
|
||||
static async findAccount(ctx, id, token) { // eslint-disable-line no-unused-vars
|
||||
// token is a reference to the token used for which a given account is being loaded,
|
||||
// it is undefined in scenarios where account claims are returned from authorization endpoint
|
||||
// ctx is the koa request context
|
||||
if (!store.get(id)) new Account(id); // eslint-disable-line no-new
|
||||
return store.get(id);
|
||||
}
|
||||
}
|
||||
|
||||
function attachInteractionRoutes(routePrefix, app, provider) {
|
||||
assert.strictEqual(typeof routePrefix, 'string');
|
||||
assert.strictEqual(typeof app, 'function'); // express app
|
||||
assert.strictEqual(typeof provider, 'object');
|
||||
|
||||
function setNoCache(req, res, next) {
|
||||
res.set('cache-control', 'no-store');
|
||||
next();
|
||||
}
|
||||
|
||||
app.get(routePrefix + '/interaction/:uid', setNoCache, async (req, res, next) => {
|
||||
try {
|
||||
const { uid, prompt, params, session } = await provider.interactionDetails(req, res);
|
||||
|
||||
debug(`interaction get uid:${uid} prompt.name:${prompt.name} client_id:${params.client_id} session:${session}`);
|
||||
|
||||
const client = await provider.Client.find(params.client_id);
|
||||
|
||||
switch (prompt.name) {
|
||||
case 'login': {
|
||||
return res.render('login', {
|
||||
client,
|
||||
uid,
|
||||
details: prompt.details,
|
||||
params,
|
||||
title: 'Sign-in',
|
||||
session: session ? debug(session) : undefined,
|
||||
dbg: {
|
||||
params: debug(params),
|
||||
prompt: debug(prompt),
|
||||
},
|
||||
});
|
||||
}
|
||||
case 'consent': {
|
||||
return res.render('interaction', {
|
||||
client,
|
||||
uid,
|
||||
details: prompt.details,
|
||||
params,
|
||||
title: 'Authorize',
|
||||
session: session ? debug(session) : undefined,
|
||||
dbg: {
|
||||
params: debug(params),
|
||||
prompt: debug(prompt),
|
||||
},
|
||||
});
|
||||
}
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
});
|
||||
|
||||
app.post(routePrefix + '/interaction/:uid/login', setNoCache, async (req, res, next) => {
|
||||
try {
|
||||
const { uid, prompt: { name } } = await provider.interactionDetails(req, res);
|
||||
|
||||
debug(`interaction login post uid:${uid} prompt.name:${name} login:${req.body.login}`);
|
||||
|
||||
assert.equal(name, 'login');
|
||||
const account = await Account.findByLogin(req.body.login);
|
||||
|
||||
const result = {
|
||||
login: {
|
||||
accountId: account.accountId,
|
||||
},
|
||||
};
|
||||
|
||||
await provider.interactionFinished(req, res, result, { mergeWithLastSubmission: false });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
app.post(routePrefix + '/interaction/:uid/confirm', setNoCache, async (req, res, next) => {
|
||||
try {
|
||||
const interactionDetails = await provider.interactionDetails(req, res);
|
||||
const { uid, prompt: { name, details }, params, session: { accountId } } = interactionDetails;
|
||||
|
||||
debug(`interaction confirm post uid:${uid} prompt.name:${name} accountId:${accountId}`);
|
||||
|
||||
assert.equal(name, 'consent');
|
||||
|
||||
let { grantId } = interactionDetails;
|
||||
let grant;
|
||||
|
||||
if (grantId) {
|
||||
// we'll be modifying existing grant in existing session
|
||||
grant = await provider.Grant.find(grantId);
|
||||
} else {
|
||||
// we're establishing a new grant
|
||||
grant = new provider.Grant({
|
||||
accountId,
|
||||
clientId: params.client_id,
|
||||
});
|
||||
}
|
||||
|
||||
if (details.missingOIDCScope) {
|
||||
grant.addOIDCScope(details.missingOIDCScope.join(' '));
|
||||
}
|
||||
if (details.missingOIDCClaims) {
|
||||
grant.addOIDCClaims(details.missingOIDCClaims);
|
||||
}
|
||||
if (details.missingResourceScopes) {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const [indicator, scopes] of Object.entries(details.missingResourceScopes)) {
|
||||
grant.addResourceScope(indicator, scopes.join(' '));
|
||||
}
|
||||
}
|
||||
|
||||
grantId = await grant.save();
|
||||
|
||||
const consent = {};
|
||||
if (!interactionDetails.grantId) {
|
||||
// we don't have to pass grantId to consent, we're just modifying existing one
|
||||
consent.grantId = grantId;
|
||||
}
|
||||
|
||||
const result = { consent };
|
||||
await provider.interactionFinished(req, res, result, { mergeWithLastSubmission: true });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
app.get(routePrefix + '/interaction/:uid/abort', setNoCache, async (req, res, next) => {
|
||||
debug(`interaction abort`);
|
||||
|
||||
try {
|
||||
const result = {
|
||||
error: 'access_denied',
|
||||
error_description: 'End-User aborted interaction',
|
||||
};
|
||||
await provider.interactionFinished(req, res, result, { mergeWithLastSubmission: false });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function getProvider(routePrefix) {
|
||||
assert.strictEqual(typeof routePrefix, 'string');
|
||||
|
||||
const { Provider } = await import('oidc-provider');
|
||||
|
||||
const configuration = {
|
||||
// use the one from Account class I guess?
|
||||
async findAccount(ctx, id) {
|
||||
debug(`findAccount ctx:${ctx} id:${id}`);
|
||||
|
||||
@@ -202,9 +436,3 @@ async function getProvider(routePrefix) {
|
||||
|
||||
return provider;
|
||||
}
|
||||
|
||||
async function getMiddleware(routePrefix) {
|
||||
const provider = await getProvider(routePrefix);
|
||||
|
||||
return provider.callback();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user