mirror of
https://github.com/NginxProxyManager/nginx-proxy-manager.git
synced 2025-06-19 02:26:27 +00:00
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:
committed by
Marcell Fülöp
parent
5920b0cf5e
commit
caeb2934f0
@ -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
132
backend/routes/api/oidc.js
Normal 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;
|
@ -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);
|
||||
})
|
||||
|
@ -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);
|
||||
})
|
||||
|
Reference in New Issue
Block a user