FEAT: Add Open ID Connect authentication method

* add `oidc-config` setting allowing an admin user to configure parameters
* modify login page to show another button when oidc is configured
* add dependency `openid-client` `v5.4.0`
* add backend route to process "OAuth2 Authorization Code" flow
  initialisation
* add backend route to process callback of above flow
* sign in the authenticated user with internal jwt token if internal
  user with email matching the one retrieved from oauth claims exists

Note: Only Open ID Connect Discovery is supported which most modern
Identity Providers offer.

Tested with Authentik 2023.2.2 and Keycloak 18.0.2
This commit is contained in:
Marcell FÜLÖP
2023-02-24 08:39:21 +00:00
committed by Marcell Fülöp
parent 5920b0cf5e
commit caeb2934f0
17 changed files with 441 additions and 7 deletions

View File

@ -27,6 +27,7 @@ router.get('/', (req, res/*, next*/) => {
router.use('/schema', require('./schema'));
router.use('/tokens', require('./tokens'));
router.use('/oidc', require('./oidc'))
router.use('/users', require('./users'));
router.use('/audit-log', require('./audit-log'));
router.use('/reports', require('./reports'));

132
backend/routes/api/oidc.js Normal file
View File

@ -0,0 +1,132 @@
const crypto = require('crypto');
const express = require('express');
const jwtdecode = require('../../lib/express/jwt-decode');
const oidc = require('openid-client');
const settingModel = require('../../models/setting');
const internalToken = require('../../internal/token');
let router = express.Router({
caseSensitive: true,
strict: true,
mergeParams: true
});
/**
* OAuth Authorization Code flow initialisation
*
* /api/oidc
*/
router
.route('/')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/users
*
* Retrieve all users
*/
.get(jwtdecode(), async (req, res, next) => {
console.log("oidc init >>>", res.locals.access, oidc);
settingModel
.query()
.where({id: 'oidc-config'})
.first()
.then( async row => {
console.log('oidc init > config > ', row);
let issuer = await oidc.Issuer.discover(row.meta.issuerURL);
let client = new issuer.Client({
client_id: row.meta.clientID,
client_secret: row.meta.clientSecret,
redirect_uris: [row.meta.redirectURL],
response_types: ['code'],
})
let state = crypto.randomUUID();
let nonce = crypto.randomUUID();
let url = client.authorizationUrl({
scope: 'openid email profile',
resource: 'http://rye.local:2081/api/oidc/callback',
state,
nonce,
})
console.log('oidc init > url > ', state, nonce, url);
res.cookie("npm_oidc", state + '--' + nonce);
res.redirect(url);
});
});
/**
* Oauth Authorization Code flow callback
*
* /api/oidc/callback
*/
router
.route('/callback')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /users/123 or /users/me
*
* Retrieve a specific user
*/
.get(jwtdecode(), async (req, res, next) => {
console.log("oidc callback >>>");
settingModel
.query()
.where({id: 'oidc-config'})
.first()
.then( async row => {
console.log('oidc callback > config > ', row);
let issuer = await oidc.Issuer.discover(row.meta.issuerURL);
let client = new issuer.Client({
client_id: row.meta.clientID,
client_secret: row.meta.clientSecret,
redirect_uris: [row.meta.redirectURL],
response_types: ['code'],
});
let state, nonce;
let cookies = req.headers.cookie.split(';');
for (cookie of cookies) {
if (cookie.split('=')[0].trim() === 'npm_oidc') {
let raw = cookie.split('=')[1];
let val = raw.split('--');
state = val[0].trim();
nonce = val[1].trim();
break;
}
}
const params = client.callbackParams(req);
const tokenSet = await client.callback(row.meta.redirectURL, params, { /*code_verifier: verifier,*/ state, nonce });
let claims = tokenSet.claims();
console.log('validated ID Token claims %j', claims);
return internalToken.getTokenFromOAuthClaim({ identity: claims.email })
})
.then( response => {
console.log('oidc callback > signed token > >', response);
res.cookie('npm_oidc', response.token + '---' + response.expires);
res.redirect('/login');
})
.catch( err => {
console.log('oidc callback ERR > ', err);
res.cookie('npm_oidc_error', err.message);
res.redirect('/login');
});
});
module.exports = router;

View File

@ -69,6 +69,17 @@ router
});
})
.then((row) => {
if (row.id === 'oidc-config') {
// redact oidc configuration via api
let m = row.meta
row.meta = {
name: m.name,
enabled: m.enabled === true && !!(m.clientID && m.clientSecret && m.issuerURL && m.redirectURL && m.name)
};
// remove these temporary cookies used during oidc authentication
res.clearCookie('npm_oidc')
res.clearCookie('npm_oidc_error')
}
res.status(200)
.send(row);
})

View File

@ -28,6 +28,8 @@ router
scope: (typeof req.query.scope !== 'undefined' ? req.query.scope : null)
})
.then((data) => {
// clear this temporary cookie following a successful oidc authentication
res.clearCookie('npm_oidc');
res.status(200)
.send(data);
})