-
Notifications
You must be signed in to change notification settings - Fork 46
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor(service): users/auth: migrate oidc ingress protocol to trype…
…script and new auth scheme
- Loading branch information
Showing
1 changed file
with
109 additions
and
155 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |