@@ -114,13 +114,51 @@ exports.onExecutePostLogin = async (event, api) => {
114114 return A . some ( ( element ) => B . includes ( element ) ) ;
115115 } ;
116116
117- // Process the access cache decision
118- const access_decision = ( access_rules , access_file_conf ) => {
117+ // Return a single identity by connection name, from the user structure
118+ const getProfileData = ( connection ) => {
119+ var i = 0 ;
120+ for ( i = 0 ; i < event . user . identities . length ; i ++ ) {
121+ var cid = event . user . identities [ i ] ;
122+ if ( cid . connection === connection ) {
123+ return cid . profileData ;
124+ }
125+ }
126+ return undefined ;
127+ } ;
128+
129+ // Sometimes we need to add custom claims to the various tokens we hand
130+ // out.
131+ const groupsSetCustomClaims = ( groups ) => {
132+ // If the only scopes requested are neither profile nor any scope beginning with
133+ // https:// then do not overload with custom claims
134+ const scopes_requested = event . transaction . requested_scopes || [ ] ;
135+
136+ let fixup_needed = ( scope ) => {
137+ return scope === "profile" || scope . startsWith ( "https://" ) ;
138+ } ;
139+
140+ if ( scopes_requested . some ( fixup_needed ) ) {
141+ console . log (
142+ `Client ${ event . client . client_id } requested ${ scopes_requested } , therefore adding custom claims`
143+ ) ;
144+ api . idToken . setCustomClaim ( "email_aliases" , undefined ) ;
145+ api . idToken . setCustomClaim ( "dn" , undefined ) ;
146+ api . idToken . setCustomClaim ( "organizationUnits" , undefined ) ;
147+ api . idToken . setCustomClaim ( `${ namespace } /groups` , groups ) ;
148+
149+ const claimMsg =
150+ "Please refer to https://github.com/mozilla-iam/person-api in order to query Mozilla IAM CIS user profile data" ;
151+ api . idToken . setCustomClaim ( `${ namespace } /README_FIRST` , claimMsg ) ;
152+ }
153+ } ;
154+
155+ // Collect all variations of groups and merge them together for access
156+ // evaluation.
157+ const groupsGather = ( ) => {
119158 // Ensure we have the correct group data
120159 const app_metadata_groups = event . user . app_metadata . groups || [ ] ;
121160 const ldap_groups = event . user . ldap_groups || [ ] ;
122161 const user_groups = event . user . groups || [ ] ;
123-
124162 // With account linking its possible that LDAP is not the main account on contributor LDAP accounts
125163 // Here we iterate over all possible user identities and build an array of all groups from them
126164 let _identity ;
@@ -138,54 +176,53 @@ exports.onExecutePostLogin = async (event, api) => {
138176 }
139177 }
140178 }
141-
142- // Collect all variations of groups and merge them together for access evaluation
143- let groups = [
179+ const all_groups = [
144180 ...user_groups ,
145181 ...app_metadata_groups ,
146182 ...ldap_groups ,
147183 ...identityGroups ,
184+ // A default group, added to everyone.
185+ "everyone" ,
148186 ] ;
149-
150- // Inject the everyone group and filter for duplicates
151- groups . push ( "everyone" ) ;
152- groups = groups . filter (
187+ // Filter for duplicates
188+ return all_groups . filter (
153189 ( value , index , array ) => array . indexOf ( value ) === index
154190 ) ;
191+ } ;
155192
156- // If the only scopes requested are neither profile nor any scope beginning with
157- // https:// then do not overload with custom claims
158- const scopes_requested = event . transaction . requested_scopes || [ ] ;
159-
160- let fixup_needed = ( scope ) => {
161- return scope === "profile" || scope . startsWith ( "https://" ) ;
193+ const deny = ( reason ) => {
194+ return {
195+ granted : false ,
196+ denied : {
197+ reason ,
198+ } ,
162199 } ;
200+ } ;
163201
164- if ( scopes_requested . some ( fixup_needed ) ) {
165- console . log (
166- `Client ${ event . client . client_id } requested ${ scopes_requested } , therefore adding custom claims`
167- ) ;
168- api . idToken . setCustomClaim ( "email_aliases" , undefined ) ;
169- api . idToken . setCustomClaim ( "dn" , undefined ) ;
170- api . idToken . setCustomClaim ( "organizationUnits" , undefined ) ;
171- api . idToken . setCustomClaim ( `${ namespace } /groups` , groups ) ;
172-
173- const claimMsg =
174- "Please refer to https://github.com/mozilla-iam/person-api in order to query Mozilla IAM CIS user profile data" ;
175- api . idToken . setCustomClaim ( `${ namespace } /README_FIRST` , claimMsg ) ;
176- }
177-
178- //// === Actions don't allow modifying the event.user
179- //// Update user.groups with new merged values
180- //user.groups = groups;
181-
202+ // Process the access cache decision
203+ const access_decision = ( groups , access_rules , access_file_conf ) => {
182204 // This is used for authorized user/groups
183205 let authorized = false ;
184- // Defaut app requested aal to MEDIUM for all apps which do not have this set in access file
206+
207+ // Defaut app requested aal to MEDIUM for all apps which do not have
208+ // this set in access file
185209 let required_aal = "MEDIUM" ;
186210
187- for ( let i = 0 ; i < access_rules . length ; i ++ ) {
188- let app = access_rules [ i ] . application ;
211+ const apps = access_rules . filter (
212+ ( a ) =>
213+ ( a . application . client_id ?? "" ) . indexOf ( event . client . client_id ) >= 0
214+ ) ;
215+
216+ // Default deny for apps we don't define in
217+ // https://github.com/mozilla-iam/sso-dashboard-configuration/blob/master/apps.yml
218+ if ( apps . length == 0 ) {
219+ console . log ( `No access rules defined for ${ event . client . client_id } ` ) ;
220+ return deny ( "notingroup" ) ;
221+ }
222+
223+ // Check users and groups.
224+ for ( let i = 0 ; i < apps . length ; i ++ ) {
225+ let app = apps [ i ] . application ;
189226
190227 //Handy for quick testing in dev (overrides access rules)
191228 //var app = {'client_id': 'pCGEHXW0VQNrQKURDcGi0tghh7NwWGhW', // This is testrp social-ldap-pwless
@@ -195,19 +232,23 @@ exports.onExecutePostLogin = async (event, api) => {
195232 // };
196233
197234 if ( app . client_id && app . client_id . indexOf ( event . client . client_id ) >= 0 ) {
198- // If there are multiple applications in apps.yml with the same client_id
199- // then this expiration of access check will only run against the first
200- // one encountered. This matters if there are multiple applications, using
201- // the same client_id, and asserting different expire_access_when_unused_after
202- // values.
235+ // If there are multiple applications in apps.yml with the same
236+ // client_id then this expiration of access check will only run
237+ // against the first one encountered. This matters if there are
238+ // multiple applications, using the same client_id, and asserting
239+ // different expire_access_when_unused_after values.
203240
204241 // Set app AAL (AA level) if present
205242 required_aal = app . AAL || required_aal ;
206243
207244 // AUTHORIZED_{GROUPS,USERS}
208- // XXX this authorized_users SHOULD BE REMOVED as it's unsafe (too easy to make mistakes). USE GROUPS.
209- // XXX This needs to be fixed in the dashboard first
210- // Empty users or groups (length == 0) means no access in the dashboard apps.yml world
245+ //
246+ // XXX this authorized_users SHOULD BE REMOVED as it's unsafe (too
247+ // easy to make mistakes). USE GROUPS.
248+ //
249+ // XXX This needs to be fixed in the dashboard first. Empty users
250+ // or groups (length == 0) means no access in the dashboard
251+ // apps.yml world.
211252 if (
212253 app . authorized_users . length === 0 &&
213254 app . authorized_groups . length === 0
@@ -216,7 +257,7 @@ exports.onExecutePostLogin = async (event, api) => {
216257 `Access denied to ${ event . client . client_id } for user ${ event . user . email } (${ event . user . user_id } )` +
217258 ` - this app denies ALL users and ALL groups")` ;
218259 console . log ( msg ) ;
219- return "notingroup" ;
260+ return deny ( "notingroup" ) ;
220261 }
221262
222263 // Check if the user is authorized to access
@@ -241,7 +282,7 @@ exports.onExecutePostLogin = async (event, api) => {
241282 `Access denied to ${ event . client . client_id } for user ${ event . user . email } (${ event . user . user_id } )` +
242283 ` - not in authorized group or not an authorized user` ;
243284 console . log ( msg ) ;
244- return "notingroup" ;
285+ return deny ( "notingroup" ) ;
245286 }
246287 } // correct client id / we matched the current RP
247288 } // for loop / next rule in apps.yml
@@ -252,22 +293,10 @@ exports.onExecutePostLogin = async (event, api) => {
252293 // We go through each possible attribute as auth0 will translate these differently in the main profile
253294 // depending on the connection type
254295
255- const getProfileData = ( connection ) => {
256- // Return a single identity by connection name, from the user structure
257- var i = 0 ;
258- for ( i = 0 ; i < event . user . identities . length ; i ++ ) {
259- var cid = event . user . identities [ i ] ;
260- if ( cid . connection === connection ) {
261- return cid . profileData ;
262- }
263- }
264-
265- return undefined ;
266- } ; // getProfileData func
267-
268296 // Ensure all users have some AAI and AAL attributes, even if its empty
269297 let aai = [ ] ;
270298 let aal = "UNKNOWN" ;
299+ let enableDuo = false ;
271300
272301 // Allow certain LDAP service accounts to fake their MFA. For all other LDAPi accounts, enforce MFA
273302 if ( event . connection . strategy === "ad" ) {
@@ -277,10 +306,7 @@ exports.onExecutePostLogin = async (event, api) => {
277306 ) ;
278307 aai . push ( "2FA" ) ;
279308 } else {
280- api . multifactor . enable ( "duo" , {
281- providerOptions : duoConfig ,
282- allowRememberBrowser : true ,
283- } ) ;
309+ enableDuo = true ;
284310 console . log (
285311 `duosecurity: ${ event . user . email } is in LDAP and requires 2FA check`
286312 ) ;
@@ -391,20 +417,21 @@ exports.onExecutePostLogin = async (event, api) => {
391417 }
392418 }
393419
394- // Set AAI & AAL claims in idToken
395- api . idToken . setCustomClaim ( `${ namespace } /AAI` , aai ) ;
396- api . idToken . setCustomClaim ( `${ namespace } /AAL` , aal ) ;
397-
398420 if ( ! aai_pass ) {
399421 const msg =
400422 `Access denied to ${ event . client . client_id } for user ${ event . user . email } (${ event . user . user_id } ) - due to` +
401423 ` Identity Assurance Level being too low for this RP. Required AAL: ${ required_aal } (${ aai_pass } )` ;
402424 console . log ( msg ) ;
403- return "aai_failed" ;
425+ return deny ( "aai_failed" ) ;
404426 }
405427
406428 // We matched no rule, access is granted
407- return true ;
429+ return {
430+ granted : true ,
431+ enableDuo,
432+ aai,
433+ aal,
434+ } ;
408435 } ;
409436
410437 const access_file_conf = {
@@ -433,15 +460,25 @@ exports.onExecutePostLogin = async (event, api) => {
433460 try {
434461 const cdnUrl = "https://cdn.sso.mozilla.com/apps.yml" ;
435462 const appsYaml = await getAppsYaml ( cdnUrl ) ;
436- const decision = access_decision ( appsYaml , access_file_conf ) ;
463+ const groups = groupsGather ( ) ;
464+ const decision = access_decision ( groups , appsYaml , access_file_conf ) ;
437465
438- if ( decision === true ) {
439- return ; // Allow login to continue
440- } else {
441- // Go back to the shadow. You shall not pass!
442- postError ( decision ) ;
466+ if ( decision . granted ) {
467+ if ( decision . enableDuo ) {
468+ api . multifactor . enable ( "duo" , {
469+ providerOptions : duoConfig ,
470+ allowRememberBrowser : true ,
471+ } ) ;
472+ }
473+ // Set groups, AAI, and AAL claims in idToken
474+ api . idToken . setCustomClaim ( `${ namespace } /AAI` , decision . aai ) ;
475+ api . idToken . setCustomClaim ( `${ namespace } /AAL` , decision . aal ) ;
476+ groupsSetCustomClaims ( groups ) ;
443477 return ;
444478 }
479+
480+ // Go back to the shadow. You shall not pass!
481+ return postError ( decision . denied . reason ) ;
445482 } catch ( err ) {
446483 // All error should be caught here and we return the callback handler with the error
447484 console . log ( "AccessRules:" , err ) ;
0 commit comments