Files
cloudron-box/src/oidc.js

723 lines
26 KiB
JavaScript
Raw Normal View History

'use strict';
exports = module.exports = {
2023-03-21 13:54:40 +01:00
start,
stop,
revokeByUserId,
2023-03-16 15:37:03 +01:00
clients: {
add: clientsAdd,
get: clientsGet,
del: clientsDel,
update: clientsUpdate,
list: clientsList
}
};
const assert = require('assert'),
2023-03-16 15:37:03 +01:00
BoxError = require('./boxerror.js'),
blobs = require('./blobs.js'),
2023-03-21 14:46:09 +01:00
constants = require('./constants.js'),
2023-03-16 15:37:03 +01:00
database = require('./database.js'),
debug = require('debug')('box:oidc'),
2023-03-17 14:45:45 +01:00
ejs = require('ejs'),
2023-03-21 14:46:09 +01:00
express = require('express'),
fs = require('fs'),
middleware = require('./middleware'),
path = require('path'),
paths = require('./paths.js'),
2023-03-21 14:46:09 +01:00
http = require('http'),
HttpError = require('connect-lastmile').HttpError,
jose = require('jose'),
safe = require('safetydance'),
2023-03-21 14:46:09 +01:00
settings = require('./settings.js'),
users = require('./users.js'),
util = require('util');
2023-03-16 15:37:03 +01:00
const OIDC_CLIENTS_TABLE_NAME = 'oidcClients';
const OIDC_CLIENTS_FIELDS = [ 'id', 'secret', 'name', 'appId', 'loginRedirectUri', 'logoutRedirectUri', 'tokenSignatureAlgorithm' ];
2023-03-16 15:37:03 +01:00
const ROUTE_PREFIX = '/openid';
const DEFAULT_TOKEN_SIGNATURE_ALGORITHM='RS256';
let gHttpServer = null;
2023-03-23 09:27:40 +01:00
// -----------------------------
// Database model
// -----------------------------
function postProcess(result) {
assert.strictEqual(typeof result, 'object');
result.tokenSignatureAlgorithm = result.tokenSignatureAlgorithm || DEFAULT_TOKEN_SIGNATURE_ALGORITHM;
return result;
}
2023-03-23 09:27:40 +01:00
async function clientsAdd(id, data) {
2023-03-16 15:37:03 +01:00
assert.strictEqual(typeof id, 'string');
2023-03-23 09:27:40 +01:00
assert.strictEqual(typeof data.secret, 'string');
assert.strictEqual(typeof data.loginRedirectUri, 'string');
assert.strictEqual(typeof data.logoutRedirectUri, 'string');
assert.strictEqual(typeof data.name, 'string');
assert.strictEqual(typeof data.appId, 'string');
assert(data.tokenSignatureAlgorithm === 'RS256' || data.tokenSignatureAlgorithm === 'EdDSA');
2023-03-16 15:37:03 +01:00
debug(`clientsAdd: id:${id} secret:${data.secret} name:${data.name} appId:${data.appId} loginRedirectUri:${data.loginRedirectUri} logoutRedirectUri:${data.logoutRedirectUri} tokenSignatureAlgorithm:${data.tokenSignatureAlgorithm}`);
2023-03-21 19:39:13 +01:00
const query = `INSERT INTO ${OIDC_CLIENTS_TABLE_NAME} (id, secret, name, appId, loginRedirectUri, logoutRedirectUri, tokenSignatureAlgorithm) VALUES (?, ?, ?, ?, ?, ?, ?)`;
const args = [ id, data.secret, data.name, data.appId, data.loginRedirectUri, data.logoutRedirectUri, data.tokenSignatureAlgorithm ];
2023-03-16 15:37:03 +01:00
const [error] = await safe(database.query(query, args));
if (error && error.code === 'ER_DUP_ENTRY') throw new BoxError(BoxError.ALREADY_EXISTS, 'client already exists');
if (error) throw error;
}
async function clientsGet(id) {
assert.strictEqual(typeof id, 'string');
debug(`clientsGet: id:${id}`);
const result = await database.query(`SELECT ${OIDC_CLIENTS_FIELDS} FROM ${OIDC_CLIENTS_TABLE_NAME} WHERE id = ?`, [ id ]);
if (result.length === 0) return null;
return postProcess(result[0]);
2023-03-16 15:37:03 +01:00
}
2023-03-23 09:27:40 +01:00
async function clientsUpdate(id, data) {
2023-03-16 15:37:03 +01:00
assert.strictEqual(typeof id, 'string');
2023-03-23 09:27:40 +01:00
assert.strictEqual(typeof data.secret, 'string');
assert.strictEqual(typeof data.loginRedirectUri, 'string');
assert.strictEqual(typeof data.logoutRedirectUri, 'string');
assert.strictEqual(typeof data.name, 'string');
assert.strictEqual(typeof data.appId, 'string');
assert(data.tokenSignatureAlgorithm === 'RS256' || data.tokenSignatureAlgorithm === 'EdDSA');
2023-03-16 15:37:03 +01:00
debug(`clientsUpdate: id:${id} secret:${data.secret} name:${data.name} appId:${data.appId} loginRedirectUri:${data.loginRedirectUri} logoutRedirectUri:${data.logoutRedirectUri} tokenSignatureAlgorithm:${data.tokenSignatureAlgorithm}`);
2023-03-23 09:27:40 +01:00
const result = await database.query(`UPDATE ${OIDC_CLIENTS_TABLE_NAME} SET secret=?, name=?, appId=?, loginRedirectUri=?, logoutRedirectUri=?, tokenSignatureAlgorithm=? WHERE id = ?`, [ data.secret, data.name, data.appId, data.loginRedirectUri, data.logoutRedirectUri, data.tokenSignatureAlgorithm, id]);
2023-03-16 15:37:03 +01:00
if (result.affectedRows !== 1) throw new BoxError(BoxError.NOT_FOUND, 'client not found');
}
async function clientsDel(id) {
assert.strictEqual(typeof id, 'string');
const result = await database.query(`DELETE FROM ${OIDC_CLIENTS_TABLE_NAME} WHERE id = ?`, [ id ]);
if (result.affectedRows !== 1) throw new BoxError(BoxError.NOT_FOUND, 'client not found');
}
async function clientsList() {
2023-03-21 17:40:06 +01:00
const results = await database.query(`SELECT * FROM ${OIDC_CLIENTS_TABLE_NAME} ORDER BY id ASC`, []);
results.forEach(postProcess);
2023-03-16 15:37:03 +01:00
return results;
}
2023-03-14 14:58:09 +01:00
// -----------------------------
// Basic in-memory json file backed based data store
// -----------------------------
const DATA_STORE = {};
function load(modelName) {
assert.strictEqual(typeof modelName, 'string');
if (DATA_STORE[modelName]) return;
const filePath = path.join(paths.OIDC_STORE_DIR, `${modelName}.json`);
debug(`load: model ${modelName} based on ${filePath}.`);
let data = {};
try {
data = JSON.parse(fs.readFileSync(filePath), 'utf8');
} catch (e) {
debug(`load: failed to read ${filePath}, start with new one.`, e);
}
DATA_STORE[modelName] = data;
}
function save(modelName) {
assert.strictEqual(typeof modelName, 'string');
if (!DATA_STORE[modelName]) return;
const filePath = path.join(paths.OIDC_STORE_DIR, `${modelName}.json`);
debug(`save: model ${modelName} to ${filePath}.`);
try {
fs.writeFileSync(filePath, JSON.stringify(DATA_STORE[modelName]), 'utf8');
} catch (e) {
debug(`revokeByUserId: failed to write ${filePath}`, e);
}
}
// -----------------------------
// Session, Grant and Token management
// -----------------------------
// This is based on the same storage as the below CloudronAdapter
async function revokeByUserId(userId) {
assert.strictEqual(typeof userId, 'string');
debug(`revokeByUserId: userId:${userId}`);
function revokeObjects(modelName) {
load(modelName);
for (let id in DATA_STORE[modelName]) {
if (DATA_STORE[modelName][id].payload?.accountId === userId) delete DATA_STORE[modelName][id];
}
save(modelName);
}
revokeObjects('Session');
revokeObjects('Grant');
revokeObjects('AuthorizationCode');
revokeObjects('AccessToken');
}
2023-03-23 09:27:40 +01:00
// -----------------------------
// Generic oidc node module data store model
// -----------------------------
class CloudronAdapter {
2023-03-21 14:46:09 +01:00
/**
*
* Creates an instance of MyAdapter for an oidc-provider model.
*
* @constructor
* @param {string} name Name of the oidc-provider model. One of "Grant, "Session", "AccessToken",
* "AuthorizationCode", "RefreshToken", "ClientCredentials", "Client", "InitialAccessToken",
* "RegistrationAccessToken", "DeviceCode", "Interaction", "ReplayDetection",
* "BackchannelAuthenticationRequest", or "PushedAuthorizationRequest"
*
*/
constructor(name) {
this.name = name;
debug(`Creating storage adapter for ${name}`);
2023-03-14 14:58:09 +01:00
if (this.name !== 'Client') {
load(name);
2023-03-14 14:58:09 +01:00
}
}
2023-03-21 14:46:09 +01:00
/**
*
* Update or Create an instance of an oidc-provider model.
*
* @return {Promise} Promise fulfilled when the operation succeeded. Rejected with error when
* encountered.
* @param {string} id Identifier that oidc-provider will use to reference this model instance for
* future operations.
* @param {object} payload Object with all properties intended for storage.
* @param {integer} expiresIn Number of seconds intended for this model to be stored.
*
*/
async upsert(id, payload, expiresIn) {
debug(`[${this.name}] upsert id:${id} expiresIn:${expiresIn}`, payload);
2023-03-16 15:37:03 +01:00
if (this.name === 'Client') {
console.log('WARNING!! this should not happen as it is stored in our db');
} else {
DATA_STORE[this.name][id] = { id, expiresIn, payload, consumed: false };
save(this.name);
2023-03-16 15:37:03 +01:00
}
}
2023-03-21 14:46:09 +01:00
/**
*
* Return previously stored instance of an oidc-provider model.
*
* @return {Promise} Promise fulfilled with what was previously stored for the id (when found and
* not dropped yet due to expiration) or falsy value when not found anymore. Rejected with error
* when encountered.
* @param {string} id Identifier of oidc-provider model
*
*/
async find(id) {
debug(`[${this.name}] find id:${id}`);
2023-03-16 15:37:03 +01:00
if (this.name === 'Client') {
const [error, client] = await safe(clientsGet(id));
if (error) {
console.log('Error getting client', error);
return null;
}
if (!client) return null;
2023-03-16 15:37:03 +01:00
debug(`[${this.name}] find id:${id}`, client);
const tmp = {
2023-03-16 15:37:03 +01:00
client_id: id,
client_secret: client.secret,
application_type: 'native', // default is web but we want more flexible redirectUris and this is only used in https://github.com/panva/node-oidc-provider/blob/03c9bc513860e68ee7be84f99bfc9dc930b224e8/lib/helpers/client_schema.js#L536
2023-04-06 12:42:51 +02:00
redirect_uris: client.loginRedirectUri.split(',').map(s => s.trim()),
id_token_signed_response_alg: client.tokenSignatureAlgorithm || 'RS256'
2023-03-16 15:37:03 +01:00
};
if (client.logoutRedirectUri) tmp.post_logout_redirect_uris = [ client.logoutRedirectUri ];
return tmp;
2023-03-16 15:37:03 +01:00
} else {
if (!DATA_STORE[this.name][id]) return null;
debug(`[${this.name}] find id:${id}`, DATA_STORE[this.name][id]);
2023-03-14 14:58:09 +01:00
return DATA_STORE[this.name][id].payload;
2023-03-16 15:37:03 +01:00
}
}
2023-03-21 14:46:09 +01:00
/**
*
* Return previously stored instance of DeviceCode by the end-user entered user code. You only
* need this method for the deviceFlow feature
*
* @return {Promise} Promise fulfilled with the stored device code object (when found and not
* dropped yet due to expiration) or falsy value when not found anymore. Rejected with error
* when encountered.
* @param {string} userCode the user_code value associated with a DeviceCode instance
*
*/
async findByUserCode(userCode) {
debug(`[${this.name}] FIXME findByUserCode userCode:${userCode}`);
}
2023-03-21 14:46:09 +01:00
/**
*
* Return previously stored instance of Session by its uid reference property.
*
* @return {Promise} Promise fulfilled with the stored session object (when found and not
* dropped yet due to expiration) or falsy value when not found anymore. Rejected with error
* when encountered.
* @param {string} uid the uid value associated with a Session instance
*
*/
async findByUid(uid) {
debug(`[${this.name}] findByUid uid:${uid}`);
2023-03-16 15:37:03 +01:00
if (this.name === 'Client') {
console.log('WARNING!! this should not happen as it is stored in our db');
} else {
for (let d in DATA_STORE[this.name]) {
if (DATA_STORE[this.name][d].payload.uid === uid) return DATA_STORE[this.name][d].payload;
2023-03-16 15:37:03 +01:00
}
2023-03-16 15:37:03 +01:00
return false;
}
}
2023-03-21 14:46:09 +01:00
/**
*
* Mark a stored oidc-provider model as consumed (not yet expired though!). Future finds for this
* id should be fulfilled with an object containing additional property named "consumed" with a
* truthy value (timestamp, date, boolean, etc).
*
* @return {Promise} Promise fulfilled when the operation succeeded. Rejected with error when
* encountered.
* @param {string} id Identifier of oidc-provider model
*
*/
async consume(id) {
2023-03-09 18:59:04 +01:00
debug(`[${this.name}] consume id:${id}`);
2023-03-16 15:37:03 +01:00
if (this.name === 'Client') {
console.log('WARNING!! this should not happen as it is stored in our db');
} else {
if (DATA_STORE[this.name][id]) DATA_STORE[this.name][id].consumed = true;
save(this.name);
2023-03-16 15:37:03 +01:00
}
}
2023-03-21 14:46:09 +01:00
/**
*
* Destroy/Drop/Remove a stored oidc-provider model. Future finds for this id should be fulfilled
* with falsy values.
*
* @return {Promise} Promise fulfilled when the operation succeeded. Rejected with error when
* encountered.
* @param {string} id Identifier of oidc-provider model
*
*/
async destroy(id) {
debug(`[${this.name}] destroy id:${id}`);
2023-03-16 15:37:03 +01:00
if (this.name === 'Client') {
console.log('WARNING!! this should not happen as it is stored in our db');
} else {
delete DATA_STORE[this.name][id];
save(this.name);
2023-03-16 15:37:03 +01:00
}
}
2023-03-21 14:46:09 +01:00
/**
*
* Destroy/Drop/Remove a stored oidc-provider model by its grantId property reference. Future
* finds for all tokens having this grantId value should be fulfilled with falsy values.
*
* @return {Promise} Promise fulfilled when the operation succeeded. Rejected with error when
* encountered.
* @param {string} grantId the grantId value associated with a this model's instance
*
*/
async revokeByGrantId(grantId) {
debug(`[${this.name}] revokeByGrantId grantId:${grantId}`);
2023-03-16 15:37:03 +01:00
if (this.name === 'Client') {
console.log('WARNING!! this should not happen as it is stored in our db');
} else {
for (let d in DATA_STORE[this.name]) {
if (DATA_STORE[this.name][d].grantId === grantId) {
delete DATA_STORE[this.name][d];
return save(this.name);
2023-03-16 15:37:03 +01:00
}
}
}
}
}
2023-03-23 09:27:40 +01:00
// -----------------------------
// Route handler
// -----------------------------
function renderInteractionPage(provider) {
assert.strictEqual(typeof provider, 'object');
return async function (req, res, next) {
try {
const { uid, prompt, params, session } = await provider.interactionDetails(req, res);
2023-03-13 19:08:41 +01:00
console.log('details', await provider.interactionDetails(req, res));
2023-03-11 17:22:27 +01:00
debug(`route interaction get uid:${uid} prompt.name:${prompt.name} client_id:${params.client_id} session:${session}`);
2023-03-23 10:00:17 +01:00
const [error, client] = await safe(clientsGet(params.client_id));
if (error) return next(error);
switch (prompt.name) {
2023-03-21 14:46:09 +01:00
case 'login': {
return res.render('login', {
submitUrl: `${ROUTE_PREFIX}/interaction/${uid}/login`,
2023-03-23 10:00:17 +01:00
name: client?.name || 'Cloudron'
2023-03-21 14:46:09 +01:00
});
}
case 'consent': {
return res.render('interaction', {
submitUrl: `${ROUTE_PREFIX}/interaction/${uid}/confirm`,
2023-03-23 10:00:17 +01:00
name: client?.name || 'Cloudron'
2023-03-21 14:46:09 +01:00
});
}
default:
return undefined;
}
} catch (error) {
2023-03-21 14:46:09 +01:00
debug('route interaction get error');
console.log(error);
return next(error);
}
};
}
function interactionLogin(provider) {
assert.strictEqual(typeof provider, 'object');
return async function(req, res, next) {
2023-03-13 19:08:41 +01:00
const [detailsError, details] = await safe(provider.interactionDetails(req, res));
if (detailsError) return next(new HttpError(500, detailsError));
2023-03-13 19:08:41 +01:00
const uid = details.uid;
const prompt = details.prompt;
const name = prompt.name;
2023-03-13 19:08:41 +01:00
debug(`route interaction login post uid:${uid} prompt.name:${name}`, req.body);
2023-03-13 19:08:41 +01:00
assert.equal(name, 'login');
2023-03-13 19:08:41 +01:00
if (!req.body.username || typeof req.body.username !== 'string') return next(new HttpError(400, 'A username must be non-empty string'));
if (!req.body.password || typeof req.body.password !== 'string') return next(new HttpError(400, 'A password must be non-empty string'));
if ('totpToken' in req.body && typeof req.body.totpToken !== 'string') return next(new HttpError(400, 'totpToken must be a string' ));
2023-03-13 19:08:41 +01:00
const { username, password, totpToken } = req.body;
2023-03-13 19:08:41 +01:00
const verifyFunc = username.indexOf('@') === -1 ? users.verifyWithUsername : users.verifyWithEmail;
2023-03-13 19:08:41 +01:00
const [verifyError, user] = await safe(verifyFunc(username, password, users.AP_WEBADMIN, { totpToken }));
if (verifyError && verifyError.reason === BoxError.INVALID_CREDENTIALS) return next(new HttpError(401, verifyError.message));
if (verifyError && verifyError.reason === BoxError.NOT_FOUND) return next(new HttpError(401, 'Unauthorized'));
if (verifyError) return next(new HttpError(500, verifyError));
if (!user) return next(new HttpError(401, 'Unauthorized'));
2023-03-13 19:08:41 +01:00
// TODO we may have to check what else the Account class provides, in which case we have to map those things
const result = {
login: {
accountId: user.id,
},
};
const [interactionFinishError, redirectTo] = await safe(provider.interactionResult(req, res, result));
2023-03-13 19:08:41 +01:00
if (interactionFinishError) return next(new HttpError(500, interactionFinishError));
debug(`route interaction login post result redirectTo:${redirectTo}`);
res.status(200).send({ redirectTo });
};
}
function interactionConfirm(provider) {
assert.strictEqual(typeof provider, 'object');
return async function (req, res, next) {
try {
const interactionDetails = await provider.interactionDetails(req, res);
const { uid, prompt: { name, details }, params, session: { accountId } } = interactionDetails;
2023-03-11 17:22:27 +01:00
debug(`route 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);
}
};
}
function interactionAbort(provider) {
assert.strictEqual(typeof provider, 'object');
return async function (req, res, next) {
2023-03-21 14:46:09 +01:00
debug('route 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);
}
};
}
2023-03-14 12:24:35 +01:00
/**
* @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 function claims(userId, use, scope) {
debug(`claims: userId:${userId} use:${use} scope:${scope}`);
const [error, user] = await safe(users.get(userId));
if (error) return { error: 'user not found' };
const displayName = user.displayName || user.username || ''; // displayName can be empty and username can be null
const nameParts = displayName.split(' ');
const firstName = nameParts[0];
const lastName = nameParts.length > 1 ? nameParts[nameParts.length - 1] : ''; // choose last part, if it exists
2023-03-14 12:52:37 +01:00
const claims = {
2023-03-17 14:20:21 +01:00
sub: user.username, // it is essential to always return a sub claim
2023-03-14 12:24:35 +01:00
email: user.email,
email_verified: true,
family_name: lastName,
given_name: firstName,
locale: 'en-US',
name: user.displayName,
2023-03-14 12:52:37 +01:00
preferred_username: user.username
2023-03-14 12:24:35 +01:00
};
2023-03-14 12:52:37 +01:00
debug(`claims: userId:${userId} result`, claims);
return claims;
2023-03-14 12:24:35 +01:00
}
2023-03-17 14:45:45 +01:00
// @param form - form source (id="op.logoutForm") to be embedded in the page and submitted by the End-User
async function logoutSource(ctx, form) {
2023-03-17 14:45:45 +01:00
const data = {
host: settings.dashboardFqdn(),
form
};
ctx.body = ejs.render(fs.readFileSync(path.join(__dirname, 'oidc_templates/logout.ejs'), 'utf8'), data, {});
}
async function postLogoutSuccessSource(ctx) {
2023-03-21 14:46:09 +01:00
// const client = ctx.oidc.client || {}; // client is defined if the user chose to stay logged in with the OP
2023-03-17 14:45:45 +01:00
const data = {
dashboardOrigin: settings.dashboardOrigin()
};
ctx.body = ejs.render(fs.readFileSync(path.join(__dirname, 'oidc_templates/post_logout.ejs'), 'utf8'), data, {});
}
2023-03-21 15:12:55 +01:00
async function findAccount(ctx, id) {
debug(`findAccount id:${id}`);
return {
accountId: id,
async claims(use, scope) { return await claims(id, use, scope); },
};
}
async function renderError(ctx, out, error) {
const data = {
dashboardOrigin: settings.dashboardOrigin(),
errorMessage: error.error_description || error.error_detail || 'Unknown error'
2023-03-21 15:12:55 +01:00
};
debug('renderError:', error);
ctx.type = 'html';
ctx.body = ejs.render(fs.readFileSync(path.join(__dirname, 'oidc_templates/error.ejs'), 'utf8'), data, {});
}
async function start() {
const app = express();
gHttpServer = http.createServer(app);
const { Provider } = await import('oidc-provider');
// TODO we may want to rotate those in the future
const jwksKeys = [];
let keyEdDsa = await blobs.getString(blobs.OIDC_KEY_EDDSA);
if (!keyEdDsa) {
debug('Generating new OIDC EdDSA key');
const { privateKey } = await jose.generateKeyPair('EdDSA');
keyEdDsa = await jose.exportJWK(privateKey);
await blobs.setString(blobs.OIDC_KEY_EDDSA, JSON.stringify(keyEdDsa));
jwksKeys.push(keyEdDsa);
} else {
debug('Using existing OIDC EdDSA key');
jwksKeys.push(JSON.parse(keyEdDsa));
}
let keyRs256 = await blobs.getString(blobs.OIDC_KEY_RS256);
if (!keyRs256) {
debug('Generating new OIDC RS256 key');
const { privateKey } = await jose.generateKeyPair('RS256');
keyRs256 = await jose.exportJWK(privateKey);
await blobs.setString(blobs.OIDC_KEY_RS256, JSON.stringify(keyRs256));
jwksKeys.push(keyRs256);
} else {
debug('Using existing OIDC RS256 key');
jwksKeys.push(JSON.parse(keyRs256));
}
const configuration = {
2023-03-21 15:12:55 +01:00
findAccount,
renderError,
adapter: CloudronAdapter,
interactions: {
url: async function(ctx, interaction) {
return `${ROUTE_PREFIX}/interaction/${interaction.uid}`;
}
2023-03-10 16:07:45 +01:00
},
jwks: {
jwksKeys
},
2023-03-16 16:42:18 +01:00
claims: {
email: ['email', 'email_verified'],
profile: [ 'family_name', 'given_name', 'locale', 'name', 'preferred_username' ]
},
2023-03-11 17:22:27 +01:00
features: {
devInteractions: { enabled: false },
rpInitiatedLogout: {
enabled: true,
logoutSource,
postLogoutSuccessSource
},
2023-03-11 17:22:27 +01:00
},
2023-03-15 13:37:51 +01:00
// if a client only has one redirect uri specified, the client does not have to provide it in the request
allowOmittingSingleRegisteredRedirectUri: true,
2023-03-14 14:58:09 +01:00
clients: [],
cookies: {
// FIXME https://github.com/panva/node-oidc-provider/blob/b1c1a9318036c2d3793cc9e668f99937c5c36bc6/lib/helpers/defaults.js#L770
keys: [ 'cookiesecret1', 'cookiesecret2' ]
},
pkce: {
required: function pkceRequired(ctx, client) {
return false;
}
},
ttl: {
// in seconds, can also be a function returning the seconds https://github.com/panva/node-oidc-provider/blob/b1c1a9318036c2d3793cc9e668f99937c5c36bc6/docs/README.md#ttl
AccessToken: 3600, // 1 hour
IdToken: 3600, // 1 hour
Grant: 1209600, // 14 days
Session: 1209600, // 14 days
Interaction: 3600 // 1 hour
}
};
debug(`start: create provider for ${settings.dashboardFqdn()} at ${ROUTE_PREFIX}`);
const provider = new Provider(`https://${settings.dashboardFqdn()}${ROUTE_PREFIX}`, configuration);
app.enable('trust proxy');
2023-03-21 14:46:09 +01:00
provider.proxy = true;
app.set('views', path.join(__dirname, 'oidc_templates'));
app.set('view engine', 'ejs');
2023-03-21 13:54:40 +01:00
2023-03-21 14:46:09 +01:00
const json = middleware.json({ strict: true, limit: '2mb' });
function setNoCache(req, res, next) {
res.set('cache-control', 'no-store');
next();
}
2023-03-21 14:46:09 +01:00
app.get (`${ROUTE_PREFIX}/interaction/:uid`, setNoCache, renderInteractionPage(provider));
app.post(`${ROUTE_PREFIX}/interaction/:uid/login`, setNoCache, json, interactionLogin(provider));
app.post(`${ROUTE_PREFIX}/interaction/:uid/confirm`, setNoCache, json, interactionConfirm(provider));
app.get (`${ROUTE_PREFIX}/interaction/:uid/abort`, setNoCache, interactionAbort(provider));
2023-03-21 13:54:40 +01:00
app.use(ROUTE_PREFIX, provider.callback());
await util.promisify(gHttpServer.listen.bind(gHttpServer))(constants.OIDC_PORT, '127.0.0.1');
2023-03-21 13:54:40 +01:00
}
async function stop() {
if (!gHttpServer) return;
await util.promisify(gHttpServer.close.bind(gHttpServer))();
2023-03-21 13:54:40 +01:00
gHttpServer = null;
2023-03-21 13:54:40 +01:00
}