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