Skip to content

Commit 1f82c17

Browse files
committed
refactor(service): users/auth: migrate saml ingress protocol to trypescript and new auth scheme
1 parent 3acf16b commit 1f82c17

File tree

1 file changed

+106
-234
lines changed

1 file changed

+106
-234
lines changed
Lines changed: 106 additions & 234 deletions
Original file line numberDiff line numberDiff line change
@@ -1,251 +1,123 @@
1-
const SamlStrategy = require('@node-saml/passport-saml').Strategy
2-
, log = require('winston')
3-
, User = require('../models/user')
4-
, Role = require('../models/role')
5-
, TokenAssertion = require('./verification').TokenAssertion
6-
, api = require('../api')
7-
, AuthenticationInitializer = require('./index')
1+
import express from 'express'
2+
import { Authenticator } from 'passport'
3+
import { SamlConfig, Strategy as SamlStrategy, VerifyWithRequest } from '@node-saml/passport-saml'
4+
import { IdentityProvider, IdentityProviderUser } from './ingress.entities'
5+
import { IngressProtocolWebBinding, IngressResponseType } from './ingress.protocol.bindings'
86

9-
function configure(strategy) {
10-
log.info('Configuring ' + strategy.title + ' authentication');
117

12-
const options = {
13-
path: `/auth/${strategy.name}/callback`,
14-
entryPoint: strategy.settings.entryPoint,
15-
cert: strategy.settings.cert,
16-
issuer: strategy.settings.issuer
17-
}
18-
if (strategy.settings.privateKey) {
19-
options.privateKey = strategy.settings.privateKey;
20-
}
21-
if (strategy.settings.decryptionPvk) {
22-
options.decryptionPvk = strategy.settings.decryptionPvk;
23-
}
24-
if (strategy.settings.signatureAlgorithm) {
25-
options.signatureAlgorithm = strategy.settings.signatureAlgorithm;
26-
}
27-
if(strategy.settings.audience) {
28-
options.audience = strategy.settings.audience;
29-
}
30-
if(strategy.settings.identifierFormat) {
31-
options.identifierFormat = strategy.settings.identifierFormat;
32-
}
33-
if(strategy.settings.acceptedClockSkewMs) {
34-
options.acceptedClockSkewMs = strategy.settings.acceptedClockSkewMs;
35-
}
36-
if(strategy.settings.attributeConsumingServiceIndex) {
37-
options.attributeConsumingServiceIndex = strategy.settings.attributeConsumingServiceIndex;
38-
}
39-
if(strategy.settings.disableRequestedAuthnContext) {
40-
options.disableRequestedAuthnContext = strategy.settings.disableRequestedAuthnContext;
41-
}
42-
if(strategy.settings.authnContext) {
43-
options.authnContext = strategy.settings.authnContext;
44-
}
45-
if(strategy.settings.forceAuthn) {
46-
options.forceAuthn = strategy.settings.forceAuthn;
47-
}
48-
if(strategy.settings.skipRequestCompression) {
49-
options.skipRequestCompression = strategy.settings.skipRequestCompression;
50-
}
51-
if(strategy.settings.authnRequestBinding) {
52-
options.authnRequestBinding = strategy.settings.authnRequestBinding;
53-
}
54-
if(strategy.settings.RACComparison) {
55-
options.RACComparison = strategy.settings.RACComparison;
56-
}
57-
if(strategy.settings.providerName) {
58-
options.providerName = strategy.settings.providerName;
59-
}
60-
if(strategy.settings.idpIssuer) {
61-
options.idpIssuer = strategy.settings.idpIssuer;
8+
type SamlProfileKeys = {
9+
id?: string
10+
email?: string
11+
displayName?: string
12+
}
13+
14+
type SamlProtocolSettings =
15+
Pick<
16+
SamlConfig,
17+
| 'path'
18+
| 'entryPoint'
19+
| 'cert'
20+
| 'issuer'
21+
| 'privateKey'
22+
| 'decryptionPvk'
23+
| 'signatureAlgorithm'
24+
| 'audience'
25+
| 'identifierFormat'
26+
| 'acceptedClockSkewMs'
27+
| 'attributeConsumingServiceIndex'
28+
| 'disableRequestedAuthnContext'
29+
| 'authnContext'
30+
| 'forceAuthn'
31+
| 'skipRequestCompression'
32+
| 'authnRequestBinding'
33+
| 'racComparison'
34+
| 'providerName'
35+
| 'idpIssuer'
36+
| 'validateInResponseTo'
37+
| 'requestIdExpirationPeriodMs'
38+
| 'logoutUrl'
39+
>
40+
& {
41+
profile: SamlProfileKeys
42+
}
43+
44+
function copyProtocolSettings(from: SamlProtocolSettings): SamlProtocolSettings {
45+
const copy = { ...from }
46+
copy.profile = { ...from.profile }
47+
return copy
48+
}
49+
50+
function applyDefaultProtocolSettings(idp: IdentityProvider): SamlProtocolSettings {
51+
const settings = copyProtocolSettings(idp.protocolSettings as SamlProtocolSettings)
52+
if (!settings.profile) {
53+
settings.profile = {}
6254
}
63-
if(strategy.settings.validateInResponseTo) {
64-
options.validateInResponseTo = strategy.settings.validateInResponseTo;
55+
if (!settings.profile.displayName) {
56+
settings.profile.displayName = 'email'
6557
}
66-
if(strategy.settings.requestIdExpirationPeriodMs) {
67-
options.requestIdExpirationPeriodMs = strategy.settings.requestIdExpirationPeriodMs;
58+
if (!settings.profile.email) {
59+
settings.profile.email = 'email'
6860
}
69-
if(strategy.settings.logoutUrl) {
70-
options.logoutUrl = strategy.settings.logoutUrl;
61+
if (!settings.profile.id) {
62+
settings.profile.id = 'uid'
7163
}
64+
return settings
65+
}
7266

73-
AuthenticationInitializer.passport.use(new SamlStrategy(options, function (profile, done) {
74-
const uid = profile[strategy.settings.profile.id];
75-
76-
if (!uid) {
77-
log.warn('Failed to find property uid. SAML profile keys ' + Object.keys(profile));
78-
return done('Failed to load user id from SAML profile');
79-
}
80-
81-
// TODO: users-next
82-
User.getUserByAuthenticationStrategy(strategy.type, uid, function (err, user) {
83-
if (err) return done(err);
84-
85-
if (!user) {
86-
// Create an account for the user
87-
Role.getRole('USER_ROLE', function (err, role) {
88-
if (err) return done(err);
89-
90-
const user = {
91-
username: uid,
92-
displayName: profile[strategy.settings.profile.displayName],
93-
email: profile[strategy.settings.profile.email],
94-
active: false,
95-
roleId: role._id,
96-
authentication: {
97-
type: strategy.name,
98-
id: uid,
99-
authenticationConfiguration: {
100-
name: strategy.name
101-
}
102-
}
103-
};
104-
// TODO: users-next
105-
new api.User().create(user).then(newUser => {
106-
if (!newUser.authentication.authenticationConfiguration.enabled) {
107-
log.warn(newUser.authentication.authenticationConfiguration.title + " authentication is not enabled");
108-
return done(null, false, { message: 'Authentication method is not enabled, please contact a MAGE administrator for assistance.' });
109-
}
110-
return done(null, newUser);
111-
}).catch(err => done(err));
112-
});
113-
} else if (!user.active) {
114-
return done(null, user, { message: "User is not approved, please contact your MAGE administrator to approve your account." });
115-
} else if (!user.authentication.authenticationConfiguration.enabled) {
116-
log.warn(user.authentication.authenticationConfiguration.title + " authentication is not enabled");
117-
return done(null, user, { message: 'Authentication method is not enabled, please contact a MAGE administrator for assistance.' });
118-
} else {
119-
return done(null, user);
120-
}
121-
});
122-
}));
123-
124-
function authenticate(req, res, next) {
125-
AuthenticationInitializer.passport.authenticate(strategy.name, function (err, user, info = {}) {
126-
if (err) {
127-
console.error('saml: authentication error', err);
128-
return next(err);
129-
}
130-
131-
req.user = user;
132-
133-
// For inactive or disabled accounts don't generate an authorization token
134-
if (!user.active || !user.enabled) {
135-
log.warn('Failed user login attempt: User ' + user.username + ' account is inactive or disabled.');
136-
return next();
67+
export function createWebBinding(idp: IdentityProvider, passport: Authenticator, baseUrlPath: string): IngressProtocolWebBinding {
68+
const { profile: profileKeys, ...settings } = applyDefaultProtocolSettings(idp)
69+
// TODO: this will need the the saml callback override change
70+
settings.path = `${baseUrlPath}/callback`
71+
const samlStrategy = new SamlStrategy(settings,
72+
(function samlSignIn(req, profile, done) {
73+
if (!profile) {
74+
return done(new Error('missing saml profile'))
13775
}
138-
139-
if (!user.authentication.authenticationConfigurationId) {
140-
log.warn('Failed user login attempt: ' + user.authentication.type + ' is not configured');
141-
return next();
76+
const uid = profile[profileKeys.id!]
77+
if (!uid || typeof uid !== 'string') {
78+
return done(new Error(`saml profile missing id for key ${profileKeys.id}`))
14279
}
143-
144-
if (!user.authentication.authenticationConfiguration.enabled) {
145-
log.warn('Failed user login attempt: Authentication ' + user.authentication.authenticationConfiguration.title + ' is disabled.');
146-
return next();
80+
const idpAccount: IdentityProviderUser = {
81+
username: uid,
82+
displayName: profile[profileKeys.displayName!] as string,
83+
email: profile[profileKeys.email!] as string | undefined,
84+
phones: [],
14785
}
148-
149-
// DEPRECATED session authorization, remove req.login which creates session in next version
150-
req.login(user, function (err) {
151-
if (err) {
152-
return next(err);
86+
const webUser: Pick<Express.User, 'admittingFromIdentityProvider'> = {
87+
admittingFromIdentityProvider: {
88+
idpName: idp.name,
89+
account: idpAccount,
15390
}
154-
AuthenticationInitializer.tokenService.generateToken(user._id.toString(), TokenAssertion.Authorized, 60 * 5)
155-
.then(token => {
156-
req.token = token;
157-
req.user = user;
158-
req.info = info
159-
next();
160-
}).catch(err => {
161-
next(err);
162-
});
163-
});
164-
})(req, res, next);
165-
}
166-
167-
AuthenticationInitializer.app.post(
168-
`/auth/${strategy.name}/callback`,
169-
authenticate,
170-
function (req, res) {
171-
let state = {};
172-
try {
173-
state = JSON.parse(req.body.RelayState)
174-
} catch (ignore) {
175-
console.warn('saml: error parsing RelayState', ignore)
17691
}
177-
178-
if (state.initiator === 'mage') {
179-
if (state.client === 'mobile') {
180-
let uri;
181-
if (!req.user.active || !req.user.enabled) {
182-
uri = `mage://app/invalid_account?active=${req.user.active}&enabled=${req.user.enabled}`;
183-
} else {
184-
uri = `mage://app/authentication?token=${req.token}`
185-
}
186-
187-
res.redirect(uri);
188-
} else {
189-
res.render('authentication', { host: req.getRoot(), login: { token: req.token, user: req.user } });
92+
try {
93+
const relayState = JSON.parse(req.body.RelayState) || {}
94+
if (!relayState) {
95+
return done(new Error('missing saml relay state'))
19096
}
191-
} else {
192-
if (req.user.active && req.user.enabled) {
193-
res.redirect(`/#/signin?strategy=${strategy.name}&action=authorize-device&token=${req.token}`);
194-
} else {
195-
const action = !req.user.active ? 'inactive-account' : 'disabled-account';
196-
res.redirect(`/#/signin?strategy=${strategy.name}&action=${action}`);
97+
if (relayState.initiator !== 'mage') {
98+
return done(new Error(`invalid saml relay state initiator: ${relayState.initiator}`))
19799
}
100+
webUser.admittingFromIdentityProvider!.flowState = relayState.flowState
198101
}
199-
}
200-
);
201-
}
202-
203-
function setDefaults(strategy) {
204-
if (!strategy.settings.profile) {
205-
strategy.settings.profile = {};
206-
}
207-
if (!strategy.settings.profile.displayName) {
208-
strategy.settings.profile.displayName = 'email';
209-
}
210-
if (!strategy.settings.profile.email) {
211-
strategy.settings.profile.email = 'email';
212-
}
213-
if (!strategy.settings.profile.id) {
214-
strategy.settings.profile.id = 'uid';
102+
catch (err) {
103+
return done(err as Error)
104+
}
105+
done(null, webUser)
106+
}) as VerifyWithRequest,
107+
(function samlSignOut() {
108+
console.warn('saml sign out unimplemented')
109+
}) as VerifyWithRequest
110+
)
111+
const handleIngressFlowRequest = express.Router()
112+
.post('/callback',
113+
passport.authenticate(samlStrategy),
114+
)
115+
return {
116+
ingressResponseType: IngressResponseType.Redirect,
117+
beginIngressFlow(req, res, next, flowState): any {
118+
const RelayState = JSON.stringify({ initiator: 'mage', flowState })
119+
passport.authenticate(samlStrategy, { additionalParams: { RelayState } } as any)(req, res, next)
120+
},
121+
handleIngressFlowRequest
215122
}
216-
}
217-
218-
function initialize(strategy) {
219-
const app = AuthenticationInitializer.app;
220-
const passport = AuthenticationInitializer.passport;
221-
// const provision = AuthenticationInitializer.provision;
222-
223-
setDefaults(strategy);
224-
configure(strategy);
225-
226-
// function parseLoginMetadata(req, res, next) {
227-
// req.loginOptions = {
228-
// userAgent: req.headers['user-agent'],
229-
// appVersion: req.param('appVersion')
230-
// };
231-
232-
// next();
233-
// }
234-
app.get(
235-
'/auth/' + strategy.name + '/signin',
236-
function (req, res, next) {
237-
const state = {
238-
initiator: 'mage',
239-
client: req.query.state
240-
};
241-
242-
passport.authenticate(strategy.name, {
243-
additionalParams: { RelayState: JSON.stringify(state) }
244-
})(req, res, next);
245-
}
246-
);
247-
}
248-
249-
module.exports = {
250-
initialize
251123
}

0 commit comments

Comments
 (0)