Skip to content

Commit

Permalink
refactor(service): users/auth: migrate oidc ingress protocol to trype…
Browse files Browse the repository at this point in the history
…script and new auth scheme
  • Loading branch information
restjohn committed Nov 14, 2024
1 parent 52aa863 commit 1794201
Showing 1 changed file with 109 additions and 155 deletions.
264 changes: 109 additions & 155 deletions service/src/ingress/ingress.protocol.oidc.ts
Original file line number Diff line number Diff line change
@@ -1,168 +1,122 @@
const OpenIdConnectStrategy = require('passport-openidconnect').Strategy
, log = require('winston')
, User = require('../models/user')
, Role = require('../models/role')
, TokenAssertion = require('./verification').TokenAssertion
, api = require('../api')
, { app, passport, tokenService } = require('./index');

function configure(strategy) {
log.info(`Configuring ${strategy.title} authentication`);

passport.use(strategy.name, new OpenIdConnectStrategy({
clientID: strategy.settings.clientID,
clientSecret: strategy.settings.clientSecret,
issuer: strategy.settings.issuer,
authorizationURL: strategy.settings.authorizationURL,
tokenURL: strategy.settings.tokenURL,
userInfoURL: strategy.settings.profileURL,
callbackURL: `/auth/${strategy.name}/callback`,
scope: strategy.settings.scope
}, function (issuer, uiProfile, profile, context, idToken, accessToken, refreshToken, params, done) {
const jsonProfile = uiProfile._json
const profileId = jsonProfile[strategy.settings.profile.id];
if (!profileId) {
log.warn(JSON.stringify(jsonProfile));
return done(`OIDC user profile does not contain id property ${strategy.settings.profile.id}`);
import express from 'express'
import passport from 'passport'
import OpenIdConnectStrategy from 'passport-openidconnect'
import { IdentityProvider, IdentityProviderUser } from './ingress.entities'
import { IdentityProviderAdmissionWebUser, IngressProtocolWebBinding, IngressResponseType } from './ingress.protocol.bindings'


export type OpenIdConnectProtocolSettings =
Pick<
OpenIdConnectStrategy.StrategyOptions,
'clientID' | 'clientSecret' | 'issuer' | 'authorizationURL' | 'tokenURL' | 'scope'
> &
{
profileURL: string,
profile: {
displayName?: string
email?: string
id?: string
}

// TODO: users-next
User.getUserByAuthenticationStrategy(strategy.type, profileId, function (err, user) {
if (err) return done(err);

if (!user) {
// Create an account for the user
Role.getRole('USER_ROLE', function (err, role) {
if (err) return done(err);

const user = {
username: profileId,
displayName: jsonProfile[strategy.settings.profile.displayName] || profileId,
email: jsonProfile[strategy.settings.profile.email],
active: false,
roleId: role._id,
authentication: {
type: strategy.name,
id: profileId,
authenticationConfiguration: {
name: strategy.name
}
}
};
// TODO: users-next
new api.User().create(user).then(newUser => {
if (!newUser.authentication.authenticationConfiguration.enabled) {
log.warn(newUser.authentication.authenticationConfiguration.title + " authentication is not enabled");
return done(null, false, { message: 'Authentication method is not enabled, please contact a MAGE administrator for assistance.' });
}
return done(null, newUser);
}).catch(err => done(err));
});
} else if (!user.active) {
return done(null, user, { message: "User is not approved, please contact your MAGE administrator to approve your account." });
} else if (!user.authentication.authenticationConfiguration.enabled) {
log.warn(user.authentication.authenticationConfiguration.title + " authentication is not enabled");
return done(null, user, { message: 'Authentication method is not enabled, please contact a MAGE administrator for assistance.' });
} else {
return done(null, user);
}
});
}));

function authenticate(req, res, next) {
passport.authenticate(strategy.name, function (err, user, info = {}) {
if (err) return next(err);

// TODO, this is a workaround for openidconnect library killing the app state
req.query.state = info.state

req.user = user;

// For inactive or disabled accounts don't generate an authorization token
if (!user.active || !user.enabled) {
log.warn('Failed user login attempt: User ' + user.username + ' account is inactive or disabled.');
return next();
}

if (!user.authentication.authenticationConfigurationId) {
log.warn('Failed user login attempt: ' + user.authentication.type + ' is not configured');
return next();
}

if (!user.authentication.authenticationConfiguration.enabled) {
log.warn('Failed user login attempt: Authentication ' + user.authentication.authenticationConfiguration.title + ' is disabled.');
return next();
}

tokenService.generateToken(user._id.toString(), TokenAssertion.Authorized, 60 * 5)
.then(token => {
req.token = token;
req.user = user;
req.info = info
next();
}).catch(err => {
next(err);
});
})(req, res, next);
}

app.get(`/auth/${strategy.name}/callback`,
authenticate,
function (req, res) {
if (req.query.state === 'mobile') {
let uri;
if (!req.user.active || !req.user.enabled) {
uri = `mage://app/invalid_account?active=${req.user.active}&enabled=${req.user.enabled}`;
} else {
uri = `mage://app/authentication?token=${req.token}`
}

res.redirect(uri);
} else {
res.render('authentication', { host: req.getRoot(), success: true, login: { token: req.token, user: req.user } });
}
}
);
function copyProtocolSettings(from: OpenIdConnectProtocolSettings): OpenIdConnectProtocolSettings {
const copy = { ...from }
copy.profile = { ...from.profile }
if (Array.isArray(from.scope)) {
copy.scope = [ ...from.scope ]
}
return copy
}

function setDefaults(strategy) {
//openid must be included in scope
if (!strategy.settings.scope) {
strategy.settings.scope = ['openid'];
} else {
if (!strategy.settings.scope.includes('openid')) {
strategy.settings.scope.push('openid');
}
function applyDefaultProtocolSettings(idp: IdentityProvider): OpenIdConnectProtocolSettings {
const settings = copyProtocolSettings(idp.protocolSettings as OpenIdConnectProtocolSettings)
if (!settings.scope) {
settings.scope = [ 'openid' ]
}

if (!strategy.settings.profile) {
strategy.settings.profile = {};
else if (Array.isArray(settings.scope) && !settings.scope.includes('openid')) {
settings.scope = [ ...settings.scope, 'openid' ]
}
else if (typeof settings.scope === 'string' && settings.scope !== 'openid') {
settings.scope = [ settings.scope, 'openid' ]
}
if (!strategy.settings.profile.displayName) {
strategy.settings.profile.displayName = 'name';
const profile = settings.profile
if (!profile.displayName) {
profile.displayName = 'displayName'
}
if (!strategy.settings.profile.email) {
strategy.settings.profile.email = 'email';
if (!profile.email) {
profile.email = 'email'
}
if (!strategy.settings.profile.id) {
strategy.settings.profile.id = 'sub';
if (!profile.id) {
profile.id = 'sub';
}
return settings
}

function initialize(strategy) {
configure(strategy);
setDefaults(strategy);

app.get(`/auth/${strategy.name}/signin`,
function (req, res, next) {
passport.authenticate(strategy.name, {
scope: strategy.settings.scope,
state: req.query.state
})(req, res, next);
export function createWebBinding(idp: IdentityProvider, passport: passport.Authenticator, baseUrl: string): IngressProtocolWebBinding {
const settings = applyDefaultProtocolSettings(idp)
const verify: OpenIdConnectStrategy.VerifyFunction = (
issuer: string,
uiProfile: any,
idProfile: object,
context: object,
idToken: string | object,
accessToken: string | object,
refreshToken: string,
params: any,
done: OpenIdConnectStrategy.VerifyCallback
) => {
const jsonProfile = uiProfile._json
const idpAccountId = jsonProfile[settings.profile.id!]
if (!idpAccountId) {
const message = `user profile from oidc identity provider ${idp.name} does not contain id property ${settings.profile.id}`
console.error(message, JSON.stringify(jsonProfile, null, 2))
return done(new Error(message))
}
);
};

module.exports = {
initialize
}
const idpUser: IdentityProviderUser = {
username: idpAccountId,
displayName: jsonProfile[settings.profile.displayName!] || idpAccountId,
email: jsonProfile[settings.profile.email!],
phones: [],
idpAccountId
}
done(null, { admittingFromIdentityProvider: { idpName: idp.name, account: idpUser } } )
}
const oidcStrategy = new OpenIdConnectStrategy(
{
clientID: settings.clientID,
clientSecret: settings.clientSecret,
issuer: settings.issuer,
authorizationURL: settings.authorizationURL,
tokenURL: settings.tokenURL,
userInfoURL: settings.profileURL,
callbackURL: `${baseUrl}/callback`,
scope: settings.scope
},
verify
)
const handleIngressFlowRequest = express.Router()
.get('/callback', (req, res, next) => {
const finishIngressFlow = passport.authenticate(
oidcStrategy,
(err: Error | null, user: IdentityProviderAdmissionWebUser, info: { state: string | undefined }) => {
if (err) {
return next(err)
}
const idpUserWithState: IdentityProviderAdmissionWebUser = {
...user,
flowState: info.state
}
req.user = { admittingFromIdentityProvider: idpUserWithState }
next()
}
)
finishIngressFlow(req, res, next)
})
return {
ingressResponseType: IngressResponseType.Redirect,
beginIngressFlow(req, res, next, flowState): any {
passport.authenticate(oidcStrategy, { state: flowState })(req, res, next)
},
handleIngressFlowRequest
}
}

0 comments on commit 1794201

Please sign in to comment.