-
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 ldap protocol to new ingress s…
…cheme
- Loading branch information
Showing
1 changed file
with
97 additions
and
133 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,149 +1,113 @@ | ||
const LdapStrategy = require('passport-ldapauth') | ||
, log = require('winston') | ||
, User = require('../models/user') | ||
, Role = require('../models/role') | ||
, TokenAssertion = require('./verification').TokenAssertion | ||
, api = require('../api') | ||
, userTransformer = require('../transformers/user') | ||
, { app, passport, tokenService } = require('./index'); | ||
import passport from 'passport' | ||
import LdapStrategy from 'passport-ldapauth' | ||
import { IdentityProvider, IdentityProviderUser } from './ingress.entities' | ||
import { IngressProtocolWebBinding, IngressResponseType } from './ingress.protocol.bindings' | ||
|
||
function configure(strategy) { | ||
log.info('Configuring ' + strategy.title + ' authentication'); | ||
|
||
passport.use(strategy.name, new LdapStrategy({ | ||
server: { | ||
url: strategy.settings.url, | ||
bindDN: strategy.settings.bindDN, | ||
bindCredentials: strategy.settings.bindCredentials, | ||
searchBase: strategy.settings.searchBase, | ||
searchFilter: strategy.settings.searchFilter, | ||
searchScope: strategy.settings.searchScope, | ||
groupSearchBase: strategy.settings.groupSearchBase, | ||
groupSearchFilter: strategy.settings.groupSearchFilter, | ||
groupSearchScope: strategy.settings.groupSearchScope, | ||
bindProperty: strategy.settings.bindProperty, | ||
groupDnProperty: strategy.settings.groupDnProperty | ||
} | ||
}, | ||
function (profile, done) { | ||
const username = profile[strategy.settings.profile.id ]; | ||
// TODO: users-next | ||
User.getUserByAuthenticationStrategy(strategy.type, username, 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); | ||
type LdapProfileKeys = { | ||
id?: string | ||
email?: string | ||
displayName?: string | ||
} | ||
|
||
const user = { | ||
username: username, | ||
displayName: profile[strategy.settings.profile.displayName], | ||
email: profile[strategy.settings.profile.email], | ||
active: false, | ||
roleId: role._id, | ||
authentication: { | ||
type: strategy.name, | ||
id: username, | ||
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, newUser, { message: 'Authentication method is not enabled, please contact a MAGE administrator for assistance.' }); | ||
} | ||
if (newUser.active) { | ||
done(null, newUser); | ||
} else { | ||
done(null, newUser, { status: 403 }); | ||
} | ||
}).catch(err => done(err)); | ||
}); | ||
} 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); | ||
} | ||
}); | ||
}) | ||
); | ||
type LdapProtocolSettings = LdapStrategy.Options['server'] & { | ||
profile?: LdapProfileKeys | ||
} | ||
|
||
function setDefaults(strategy) { | ||
if (!strategy.settings.profile) { | ||
strategy.settings.profile = {}; | ||
type ReadyLdapProtocolSettings = Omit<LdapProtocolSettings, 'profile'> & { profile: Required<LdapProfileKeys> } | ||
|
||
function copyProtocolSettings(from: LdapProtocolSettings): LdapProtocolSettings { | ||
const copy = { ...from } | ||
if (copy.profile) { | ||
copy.profile = { ...from.profile! } | ||
} | ||
if (!strategy.settings.profile.displayName) { | ||
strategy.settings.profile.displayName = 'givenname'; | ||
return copy | ||
} | ||
|
||
function applyDefaultProtocolSettings(idp: IdentityProvider): ReadyLdapProtocolSettings { | ||
const settings = copyProtocolSettings(idp.protocolSettings as LdapProtocolSettings) | ||
const profileKeys = settings.profile || {} | ||
if (!profileKeys.displayName) { | ||
profileKeys.displayName = 'givenname'; | ||
} | ||
if (!strategy.settings.profile.email) { | ||
strategy.settings.profile.email = 'mail'; | ||
if (!profileKeys.email) { | ||
profileKeys.email = 'mail'; | ||
} | ||
if (!strategy.settings.profile.id) { | ||
strategy.settings.profile.id = 'cn'; | ||
if (!profileKeys.id) { | ||
profileKeys.id = 'cn'; | ||
} | ||
settings.profile = profileKeys | ||
return settings as ReadyLdapProtocolSettings | ||
} | ||
|
||
function initialize(strategy) { | ||
setDefaults(strategy); | ||
configure(strategy); | ||
|
||
const authenticationOptions = { | ||
invalidLogonHours: `Not Permitted to login to ${strategy.title} account at this time.`, | ||
invalidWorkstation: `Not permited to logon to ${strategy.title} account at this workstation.`, | ||
passwordExpired: `${strategy.title} password expired.`, | ||
accountDisabled: `${strategy.title} account disabled.`, | ||
accountExpired: `${strategy.title} account expired.`, | ||
passwordMustChange: `User must reset ${strategy.title} password.`, | ||
accountLockedOut: `${strategy.title} user account locked.`, | ||
invalidCredentials: `Invalid ${strategy.title} username/password.` | ||
}; | ||
|
||
app.post(`/auth/${strategy.name}/signin`, | ||
function authenticate(req, res, next) { | ||
passport.authenticate(strategy.name, authenticationOptions, function (err, user, info = {}) { | ||
if (err) return next(err); | ||
|
||
if (!user) { | ||
return res.status(401).send(info.message); | ||
} | ||
|
||
if (!user.active) { | ||
return res.status(info.status || 401).send('User account is not approved, please contact your MAGE administrator to approve your account.'); | ||
} | ||
|
||
if (!user.enabled) { | ||
log.warn('Failed user login attempt: User ' + user.username + ' account is disabled.'); | ||
return res.status(401).send('Your account has been disabled, please contact a MAGE administrator for assistance.') | ||
} | ||
function strategyOptionsFromProtocolSettings(settings: ReadyLdapProtocolSettings): LdapStrategy.Options { | ||
return { | ||
server: { | ||
url: settings.url, | ||
bindDN: settings.bindDN, | ||
bindCredentials: settings.bindCredentials, | ||
searchBase: settings.searchBase, | ||
searchFilter: settings.searchFilter, | ||
searchScope: settings.searchScope, | ||
groupSearchBase: settings.groupSearchBase, | ||
groupSearchFilter: settings.groupSearchFilter, | ||
groupSearchScope: settings.groupSearchScope, | ||
bindProperty: settings.bindProperty, | ||
groupDnProperty: settings.groupDnProperty | ||
} | ||
} | ||
} | ||
|
||
if (!user.authentication.authenticationConfigurationId) { | ||
log.warn('Failed user login attempt: ' + user.authentication.type + ' is not configured'); | ||
return res.status(401).send(user.authentication.type + ' authentication is not configured, please contact a MAGE administrator for assistance.') | ||
export function createWebBinding(idp: IdentityProvider, passport: passport.Authenticator): IngressProtocolWebBinding { | ||
const settings = applyDefaultProtocolSettings(idp) | ||
const profileKeys = settings.profile | ||
const strategyOptions = strategyOptionsFromProtocolSettings(settings) | ||
const verify: LdapStrategy.VerifyCallback = (profile, done) => { | ||
const idpAccount: IdentityProviderUser = { | ||
username: profile[profileKeys.id], | ||
displayName: profile[profileKeys.displayName], | ||
email: profile[profileKeys.email], | ||
phones: [], | ||
idpAccountId: profile[profileKeys.id] | ||
} | ||
const webIngressUser: Express.User = { | ||
admittingFromIdentityProvider: { | ||
account: idpAccount, | ||
idpName: idp.name, | ||
} | ||
} | ||
return done(null, webIngressUser) | ||
} | ||
const title = idp.title | ||
const authOptions: LdapStrategy.AuthenticateOptions = { | ||
invalidLogonHours: `Access to ${title} account is prohibited at this time.`, | ||
invalidWorkstation: `Access to ${title} account is prohibited from this workstation.`, | ||
passwordExpired: `${title} password expired.`, | ||
accountDisabled: `${title} account disabled.`, | ||
accountExpired: `${title} account expired.`, | ||
passwordMustChange: `${title} account requires password reset.`, | ||
accountLockedOut: `${title} account locked.`, | ||
invalidCredentials: `Invalid ${title} credentials.`, | ||
} | ||
const ldapIdp = new LdapStrategy(strategyOptions, verify) | ||
return { | ||
ingressResponseType: IngressResponseType.Direct, | ||
beginIngressFlow(req, res, next, flowState): any { | ||
const completeIngress: passport.AuthenticateCallback = (err, user) => { | ||
if (err) { | ||
return next(err) | ||
} | ||
|
||
if (!user.authentication.authenticationConfiguration.enabled) { | ||
log.warn('Failed user login attempt: Authentication ' + user.authentication.authenticationConfiguration.title + ' is disabled.'); | ||
return res.status(401).send(user.authentication.authenticationConfiguration.title + ' authentication is disabled, please contact a MAGE administrator for assistance.') | ||
if (user && user.admittingFromIdentityProvider) { | ||
user.admittingFromIdentityProvider.flowState = flowState | ||
req.user = user | ||
return next() | ||
} | ||
|
||
tokenService.generateToken(user._id.toString(), TokenAssertion.Authorized, 60 * 5) | ||
.then(token => { | ||
res.json({ | ||
user: userTransformer.transform(req.user, { path: req.getRoot() }), | ||
token: token | ||
}); | ||
}).catch(err => next(err)); | ||
})(req, res, next); | ||
return res.status(500).send('internal server error: invalid ldap ingress state') | ||
} | ||
passport.authenticate(ldapIdp, authOptions, completeIngress)(req, res, next) | ||
}, | ||
handleIngressFlowRequest(req, res): any { | ||
return res.status(400).send('invalid ldap ingress request') | ||
} | ||
); | ||
}; | ||
|
||
module.exports = { | ||
initialize | ||
} | ||
} |