forked from mozilla-iam/auth0-deploy
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathaccessRules.js
More file actions
501 lines (456 loc) · 17.6 KB
/
accessRules.js
File metadata and controls
501 lines (456 loc) · 17.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
// Required Libraries
const YAML = require("js-yaml");
const jwt = require("jsonwebtoken");
const AWS = require("aws-sdk");
exports.onExecutePostLogin = async (event, api) => {
console.log("Running actions:", "accessRules");
// Retrieve and return a secret from AWS Secrets Manager
const getSecrets = async () => {
try {
if (!event.secrets.accessKeyId || !event.secrets.secretAccessKey) {
throw new Error("AWS access keys are not defined.");
}
// Set up AWS client
AWS.config.update({
region: "us-west-2",
accessKeyId: event.secrets.accessKeyId,
secretAccessKey: event.secrets.secretAccessKey,
});
const secretsManager = new AWS.SecretsManager();
const secretPath =
event.tenant.id === "dev"
? "/iam/auth0/dev/actions"
: "/iam/auth0/prod/actions";
const data = await secretsManager
.getSecretValue({ SecretId: secretPath })
.promise();
// handle string or binary
if ("SecretString" in data) {
return JSON.parse(data.SecretString);
} else {
let buff = Buffer.from(data.SecretBinary, "base64");
return buff.toString("ascii");
}
} catch (err) {
console.log("getSecrets:", err);
throw err;
}
};
// Load secrets
const secrets = await getSecrets();
const jwtMsgsRsaSkey = secrets.jwtMsgsRsaSkey;
// postError(code)
// @code string with an error code for the SSO Dashboard to display
// @rcontext the current Auth0 rule context (passed from the rule)
// Returns rcontext with redirect set to the error
const postError = (code, prefered_connection_arg) => {
try {
const prefered_connection = prefered_connection_arg || ""; // Optional arg
if (!jwtMsgsRsaSkey) {
throw new Error("jwtMsgsRsaSkey is not defined.");
}
// Token is valid from 30s ago, to 1h from now
const skey = Buffer.from(jwtMsgsRsaSkey, "base64").toString("ascii");
const token = jwt.sign(
{
client: event.client.name,
code: code,
connection: event.connection.name,
exp: Math.floor(Date.now() / 1000) + 3600,
iat: Math.floor(Date.now() / 1000) - 30,
preferred_connection_name: prefered_connection,
redirect_uri: event.transaction.redirect_uri,
},
skey,
{ algorithm: "RS256" }
);
const domain =
event.tenant.id === "dev" ? "sso.allizom.org" : "sso.mozilla.com";
const forbiddenUrl = new URL(`https://${domain}/forbidden`);
forbiddenUrl.searchParams.set("error", token);
api.redirect.sendUserTo(forbiddenUrl.href);
return;
} catch (err) {
console.log("postError:", err);
throw err;
}
};
if (!event.user.email_verified) {
console.log(
`User primary email NOT verified, refusing login for ${event.user.email}`
);
// This post error is broken in sso dashboard
postError("primarynotverified", event, api, jwt, jwtMsgsRsaSkey);
return;
}
const namespace = "https://sso.mozilla.com/claim";
// MFA bypass for special service accounts
const mfaBypassAccounts = [
"moc+servicenow@mozilla.com", // MOC see: https://bugzilla.mozilla.org/show_bug.cgi?id=1423903
"moc-sso-monitoring@mozilla.com", // MOC see: https://bugzilla.mozilla.org/show_bug.cgi?id=1423903
"shared-deng-playstore@mozilla.com", // See: https://mozilla-hub.atlassian.net/browse/IAM-1938
];
const duoConfig = {
host: event.secrets.duo_apihost_mozilla,
ikey: event.secrets.duo_ikey_mozilla,
skey: event.secrets.duo_skey_mozilla,
username: event.user.email,
};
// Check if array A has any occurrence from array B
const hasCommonElements = (A, B) => {
return A.some((element) => B.includes(element));
};
// Return a single identity by connection name, from the user structure
const getProfileData = (connection) => {
var i = 0;
for (i = 0; i < event.user.identities.length; i++) {
var cid = event.user.identities[i];
if (cid.connection === connection) {
return cid.profileData;
}
}
return undefined;
};
// Sometimes we need to add custom claims to the various tokens we hand
// out.
const groupsSetCustomClaims = (groups) => {
// If the only scopes requested are neither profile nor any scope beginning with
// https:// then do not overload with custom claims
const scopes_requested = event.transaction.requested_scopes || [];
let fixup_needed = (scope) => {
return scope === "profile" || scope.startsWith("https://");
};
if (scopes_requested.some(fixup_needed)) {
console.log(
`Client ${event.client.client_id} requested ${scopes_requested}, therefore adding custom claims`
);
api.idToken.setCustomClaim("email_aliases", undefined);
api.idToken.setCustomClaim("dn", undefined);
api.idToken.setCustomClaim("organizationUnits", undefined);
api.idToken.setCustomClaim(`${namespace}/groups`, groups);
const claimMsg =
"Please refer to https://github.com/mozilla-iam/person-api in order to query Mozilla IAM CIS user profile data";
api.idToken.setCustomClaim(`${namespace}/README_FIRST`, claimMsg);
}
};
// Collect all variations of groups and merge them together for access
// evaluation.
const groupsGather = () => {
// Ensure we have the correct group data
const app_metadata_groups = event.user.app_metadata.groups || [];
const ldap_groups = event.user.ldap_groups || [];
const user_groups = event.user.groups || [];
// With account linking its possible that LDAP is not the main account on contributor LDAP accounts
// Here we iterate over all possible user identities and build an array of all groups from them
let _identity;
let identityGroups = [];
// Iterate over each identity
for (let x = 0, len = event.user.identities.length; x < len; x++) {
// Get profile for the given identity
_identity = event.user.identities[x];
// If the identity contains profileData
if ("profileData" in _identity) {
// If profileData contains a groups array
if ("groups" in _identity.profileData) {
// Merge the group arry into identityGroups
identityGroups.push(..._identity.profileData.groups);
}
}
}
const all_groups = [
...user_groups,
...app_metadata_groups,
...ldap_groups,
...identityGroups,
// A default group, added to everyone.
"everyone",
];
// Filter for duplicates
return all_groups.filter(
(value, index, array) => array.indexOf(value) === index
);
};
const deny = (reason) => {
return {
granted: false,
denied: {
reason,
},
};
};
// Process the access cache decision.
// Note that applications may be defined multiple times in the access rules.
//
// When access is granted, the AAL from the rule is used, meaning different
// groups can be subjected to different MFA requirements.
//
// The one exception is that: if any apps say _no_ users nor groups should
// have access, then we use bail other checks early.
const access_decision = (groups, access_rules, access_file_conf) => {
// This is used for authorized user/groups
let authorized = false;
// Defaut app requested aal to MEDIUM for all apps which do not have
// this set in access file
const default_aal = "MEDIUM";
// The AAL from the matched authorization rule is used.
let required_aal;
// Only look at rules which match our client_id.
const apps = access_rules.filter(
(a) =>
(a.application.client_id ?? "").indexOf(event.client.client_id) >= 0
);
// Default deny for apps we don't define in
// https://github.com/mozilla-iam/sso-dashboard-configuration/blob/master/apps.yml
if (apps.length == 0) {
console.log(`No access rules defined for ${event.client.client_id}`);
return deny("notingroup");
}
// Check users and groups.
for (let i = 0; i < apps.length; i++) {
let app = apps[i].application;
//Handy for quick testing in dev (overrides access rules)
//var app = {'client_id': 'pCGEHXW0VQNrQKURDcGi0tghh7NwWGhW', // This is testrp social-ldap-pwless
// 'authorized_users': ['gdestuynder@mozilla.com'],
// 'authorized_groups': ['okta_mfa'],
// 'aal': 'LOW'
// };
if (app.client_id && app.client_id.indexOf(event.client.client_id) >= 0) {
// AUTHORIZED_{GROUPS,USERS}
//
// XXX this authorized_users SHOULD BE REMOVED as it's unsafe (too
// easy to make mistakes). USE GROUPS.
//
// XXX This needs to be fixed in the dashboard first. Empty users
// or groups (length == 0) means no access in the dashboard
// apps.yml world.
if (
app.authorized_users.length === 0 &&
app.authorized_groups.length === 0
) {
console.log(
`Access denied to ${event.client.client_id} for user ` +
`${event.user.email} (${event.user.user_id})` +
` - this app denies ALL users and ALL groups")`
);
return deny("notingroup");
}
// Check if the user is authorized to access.
// A user is authorized if they are a member of any authorized_groups
// or if they are one of the authorized_users.
if (
app.authorized_users.length > 0 &&
app.authorized_users.indexOf(event.user.email) >= 0
) {
console.log(`${event.user.user_id} was in authorized_users`);
required_aal = app.AAL || default_aal;
authorized = true;
break;
// Same dance as above, but for groups
} else if (
app.authorized_groups.length > 0 &&
hasCommonElements(app.authorized_groups, groups)
) {
console.log(`${event.user.user_id} was in authorized_groups`);
required_aal = app.AAL || default_aal;
authorized = true;
break;
}
} // correct client id / we matched the current RP
} // for loop / next rule in apps.yml
if (!authorized) {
console.log(
`Access denied to ${event.client.client_id} for user ` +
`${event.user.email} (${event.user.user_id}) - not in ` +
"authorized group or not an authorized user"
);
return deny("notingroup");
}
// AAI (AUTHENTICATOR ASSURANCE INDICATOR)
// Sets the AAI for the user. This is later used by the AccessRules.js rule which also sets the AAL.
// We go through each possible attribute as auth0 will translate these differently in the main profile
// depending on the connection type
// Ensure all users have some AAI and AAL attributes, even if its empty
let aai = [];
let aal = "UNKNOWN";
let enableDuo = false;
// Allow certain LDAP service accounts to fake their MFA. For all other LDAPi accounts, enforce MFA
if (event.connection.strategy === "ad") {
if (mfaBypassAccounts.includes(event.user.email)) {
console.log(
`LDAP service account (${event.user.email}) is allowed to bypass MFA`
);
aai.push("2FA");
} else {
enableDuo = true;
console.log(
`duosecurity: ${event.user.email} is in LDAP and requires 2FA check`
);
}
}
const profileData = getProfileData(event.connection.name);
//GitHub attribute
if (event.connection.name === "github") {
if (
event.user.two_factor_authentication !== undefined &&
event.user.two_factor_authentication === true
) {
aai.push("2FA");
} else if (
profileData !== undefined &&
profileData.two_factor_authentication === true
) {
aai.push("2FA");
}
// Firefox Accounts
} else if (event.connection.name === "firefoxaccounts") {
if (
event.user.fxa_twoFactorAuthentication !== undefined &&
event.user.fxa_twoFactorAuthentication === true
) {
aai.push("2FA");
} else if (
profileData !== undefined &&
profileData.fxa_twoFactorAuthentication === true
) {
aai.push("2FA");
}
// LDAP/DuoSecurity
} else if (
event.user.multifactor !== undefined &&
event.user.multifactor[0] === "duo"
) {
aai.push("2FA");
} else if (event.connection.name === "google-oauth2") {
// We set Google to HIGH_ASSURANCE_IDP which is a special indicator, this is what it represents:
// - has fraud detection
// - will inform users when their account is used or logged through push notifications on their devices
// - will actively block detected fraudulent logins even with correct credentials
// - will fallback to phone 2FA in most cases (old accounts may still bypass that in some cases)
// - will fallback to phone 2FA on all recent accounts
// Note that this is not the same as "2FA" and other indicators, as we simply do not have a technically accurate
// indicator of what the authenticator supports at this time for Google accounts
aai.push("HIGH_ASSURANCE_IDP");
}
// AAI (AUTHENTICATOR ASSURANCE INDICATOR) REQUIREMENTS
//
// Note that user.aai is set in another rule (rules/aai.js)
// This file sets the user.aal (authenticator assurance level) which is the result of a map lookup against user.aai
//
// Mapping logic and verification
// Ex: our mapping says 2FA for MEDIUM AAL and app AAL is MEDIUM as well, and the user has 2FA AAI, looks like:
// access_file_conf.aai_mapping['MEDIUM'] = ['2FA'];
// app.AAL = 'MEDIUM;
// user.aai = ['2FA'];
// Thus user should be allowed for this app (it requires MEDIUM, and MEDIUM requires 2FA, and user has 2FA
// indeed)
//
let aai_pass = false;
if (access_file_conf.aai_mapping !== undefined) {
// 1 Set user.aal
// maps = [ "LOW", "MEDIUM", ...
// aal_nr = position in the maps (aai_mapping[maps[aal_nr=0]] is "LOW" for.ex)
// aai_nr = position in the array of AAIs (aai_mapping[maps[aal_nr=0]] returns ["2FA", .., aai_nr=0 would be the
// position for "2FA")
// Note that the list is ordered so that the highest AAL always wins
const maps = Object.keys(access_file_conf.aai_mapping);
for (let aal_nr = 0; aal_nr < maps.length; aal_nr++) {
for (
let aai_nr = 0;
aai_nr < access_file_conf.aai_mapping[maps[aal_nr]].length;
aai_nr++
) {
let cur_aai = access_file_conf.aai_mapping[maps[aal_nr]][aai_nr];
if (aai.indexOf(cur_aai) >= 0) {
aal = maps[aal_nr];
console.log(`User AAL set to ${aal} because AAI contains ${aai}`);
break;
}
}
}
// 2 Check if user.aal is allowed for this RP
if (access_file_conf.aai_mapping[required_aal].length === 0) {
console.log(
"No required indicator in aai_mapping for this RP (mapping empty for this AAL), access will be granted"
);
aai_pass = true;
} else {
for (let y = 0; y < aai.length; y++) {
let this_aai = aai[y];
if (
access_file_conf.aai_mapping[required_aal].indexOf(this_aai) >= 0
) {
console.log(
"User AAL is included in this RP's AAL requirements, access will be granted"
);
aai_pass = true;
break;
}
}
}
}
if (!aai_pass) {
const msg =
`Access denied to ${event.client.client_id} for user ${event.user.email} (${event.user.user_id}) - due to` +
` Identity Assurance Level being too low for this RP. Required AAL: ${required_aal} (${aai_pass})`;
console.log(msg);
return deny("aai_failed");
}
// We matched no rule, access is granted
return {
granted: true,
enableDuo,
aai,
aal,
};
};
const access_file_conf = {
aai_mapping: {
LOW: [],
MEDIUM: ["2FA", "HIGH_ASSURANCE_IDP"],
HIGH: ["HIGH_NOT_IMPLEMENTED"],
MAXIMUM: ["MAXIMUM_NOT_IMPLEMENTED"],
},
};
// This function pulls the apps.yml and returns a promise to yield the application list
async function getAppsYaml(url) {
try {
const response = await fetch(url);
const data = await response.text();
const yamlContent = YAML.load(data);
return yamlContent.apps;
} catch (error) {
console.error("Error fetching apps.yml:", error);
throw error;
}
}
// Main try
try {
const cdnUrl = "https://cdn.sso.mozilla.com/apps.yml";
const appsYaml = await getAppsYaml(cdnUrl);
const groups = groupsGather();
const decision = access_decision(groups, appsYaml, access_file_conf);
// Refresh token exchanges are non-interactive, and are minted when a user
// has already completed the MFA challenge.
const isRefreshTokenFlow =
event.transaction?.protocol === "oauth2-refresh-token";
if (decision.granted) {
if (decision.enableDuo && !isRefreshTokenFlow) {
api.multifactor.enable("duo", {
providerOptions: duoConfig,
allowRememberBrowser: true,
});
}
// Set groups, AAI, and AAL claims in idToken
api.idToken.setCustomClaim(`${namespace}/AAI`, decision.aai);
api.idToken.setCustomClaim(`${namespace}/AAL`, decision.aal);
groupsSetCustomClaims(groups);
return;
}
// Go back to the shadow. You shall not pass!
return postError(decision.denied.reason);
} catch (err) {
// All error should be caught here and we return the callback handler with the error
console.log("AccessRules:", err);
return api.access.deny(err);
}
};