mirror of
https://github.com/NginxProxyManager/nginx-proxy-manager.git
synced 2025-05-03 04:22:28 +00:00
* 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
209 lines
5.0 KiB
JavaScript
209 lines
5.0 KiB
JavaScript
const _ = require('lodash');
|
|
const error = require('../lib/error');
|
|
const userModel = require('../models/user');
|
|
const authModel = require('../models/auth');
|
|
const helpers = require('../lib/helpers');
|
|
const TokenModel = require('../models/token');
|
|
|
|
module.exports = {
|
|
|
|
/**
|
|
* @param {Object} data
|
|
* @param {String} data.identity
|
|
* @param {String} data.secret
|
|
* @param {String} [data.scope]
|
|
* @param {String} [data.expiry]
|
|
* @param {String} [issuer]
|
|
* @returns {Promise}
|
|
*/
|
|
getTokenFromEmail: (data, issuer) => {
|
|
let Token = new TokenModel();
|
|
|
|
data.scope = data.scope || 'user';
|
|
data.expiry = data.expiry || '1d';
|
|
|
|
return userModel
|
|
.query()
|
|
.where('email', data.identity)
|
|
.andWhere('is_deleted', 0)
|
|
.andWhere('is_disabled', 0)
|
|
.first()
|
|
.then((user) => {
|
|
if (user) {
|
|
// Get auth
|
|
return authModel
|
|
.query()
|
|
.where('user_id', '=', user.id)
|
|
.where('type', '=', 'password')
|
|
.first()
|
|
.then((auth) => {
|
|
if (auth) {
|
|
return auth.verifyPassword(data.secret)
|
|
.then((valid) => {
|
|
if (valid) {
|
|
|
|
if (data.scope !== 'user' && _.indexOf(user.roles, data.scope) === -1) {
|
|
// The scope requested doesn't exist as a role against the user,
|
|
// you shall not pass.
|
|
throw new error.AuthError('Invalid scope: ' + data.scope);
|
|
}
|
|
|
|
// Create a moment of the expiry expression
|
|
let expiry = helpers.parseDatePeriod(data.expiry);
|
|
if (expiry === null) {
|
|
throw new error.AuthError('Invalid expiry time: ' + data.expiry);
|
|
}
|
|
|
|
return Token.create({
|
|
iss: issuer || 'api',
|
|
attrs: {
|
|
id: user.id
|
|
},
|
|
scope: [data.scope],
|
|
expiresIn: data.expiry
|
|
})
|
|
.then((signed) => {
|
|
return {
|
|
token: signed.token,
|
|
expires: expiry.toISOString()
|
|
};
|
|
});
|
|
} else {
|
|
throw new error.AuthError('Invalid password');
|
|
}
|
|
});
|
|
} else {
|
|
throw new error.AuthError('No password auth for user');
|
|
}
|
|
});
|
|
} else {
|
|
throw new error.AuthError('No relevant user found');
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* @param {Object} data
|
|
* @param {String} data.identity
|
|
* @param {String} [issuer]
|
|
* @returns {Promise}
|
|
*/
|
|
getTokenFromOAuthClaim: (data, issuer) => {
|
|
let Token = new TokenModel();
|
|
|
|
data.scope = 'user';
|
|
data.expiry = '1d';
|
|
|
|
return userModel
|
|
.query()
|
|
.where('email', data.identity)
|
|
.andWhere('is_deleted', 0)
|
|
.andWhere('is_disabled', 0)
|
|
.first()
|
|
.then((user) => {
|
|
if (!user) {
|
|
throw new error.AuthError('No relevant user found');
|
|
}
|
|
|
|
// Create a moment of the expiry expression
|
|
let expiry = helpers.parseDatePeriod(data.expiry);
|
|
if (expiry === null) {
|
|
throw new error.AuthError('Invalid expiry time: ' + data.expiry);
|
|
}
|
|
|
|
let iss = 'api',
|
|
attrs = { id: user.id },
|
|
scope = [ data.scope ],
|
|
expiresIn = data.expiry;
|
|
|
|
return Token.create({ iss, attrs, scope, expiresIn })
|
|
.then((signed) => {
|
|
return {
|
|
token: signed.token,
|
|
expires: expiry.toISOString()
|
|
};
|
|
});
|
|
|
|
}
|
|
);
|
|
},
|
|
|
|
/**
|
|
* @param {Access} access
|
|
* @param {Object} [data]
|
|
* @param {String} [data.expiry]
|
|
* @param {String} [data.scope] Only considered if existing token scope is admin
|
|
* @returns {Promise}
|
|
*/
|
|
getFreshToken: (access, data) => {
|
|
let Token = new TokenModel();
|
|
|
|
data = data || {};
|
|
data.expiry = data.expiry || '1d';
|
|
|
|
if (access && access.token.getUserId(0)) {
|
|
|
|
// Create a moment of the expiry expression
|
|
let expiry = helpers.parseDatePeriod(data.expiry);
|
|
if (expiry === null) {
|
|
throw new error.AuthError('Invalid expiry time: ' + data.expiry);
|
|
}
|
|
|
|
let token_attrs = {
|
|
id: access.token.getUserId(0)
|
|
};
|
|
|
|
// Only admins can request otherwise scoped tokens
|
|
let scope = access.token.get('scope');
|
|
if (data.scope && access.token.hasScope('admin')) {
|
|
scope = [data.scope];
|
|
|
|
if (data.scope === 'job-board' || data.scope === 'worker') {
|
|
token_attrs.id = 0;
|
|
}
|
|
}
|
|
|
|
return Token.create({
|
|
iss: 'api',
|
|
scope: scope,
|
|
attrs: token_attrs,
|
|
expiresIn: data.expiry
|
|
})
|
|
.then((signed) => {
|
|
return {
|
|
token: signed.token,
|
|
expires: expiry.toISOString()
|
|
};
|
|
});
|
|
} else {
|
|
throw new error.AssertionFailedError('Existing token contained invalid user data');
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @param {Object} user
|
|
* @returns {Promise}
|
|
*/
|
|
getTokenFromUser: (user) => {
|
|
const expire = '1d';
|
|
const Token = new TokenModel();
|
|
const expiry = helpers.parseDatePeriod(expire);
|
|
|
|
return Token.create({
|
|
iss: 'api',
|
|
attrs: {
|
|
id: user.id
|
|
},
|
|
scope: ['user'],
|
|
expiresIn: expire
|
|
})
|
|
.then((signed) => {
|
|
return {
|
|
token: signed.token,
|
|
expires: expiry.toISOString(),
|
|
user: user
|
|
};
|
|
});
|
|
}
|
|
};
|