From f97c86f7f1265bea702004ff474a8be3bc6b7510 Mon Sep 17 00:00:00 2001 From: Jake Watkins Date: Thu, 7 Dec 2023 17:28:45 -0500 Subject: [PATCH 01/13] major fixes --- .gitignore | 333 +++++++++++++++++++++ connections/firefoxaccounts.js | 6 + rules/Global-Function-Declarations.js | 10 +- rules/force-ldap-logins-over-ldap.js | 5 +- rules/link-users-by-email-with-metadata.js | 193 ++++++------ tests/modules/global/auth0.js | 2 +- 6 files changed, 459 insertions(+), 90 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3ad6496 --- /dev/null +++ b/.gitignore @@ -0,0 +1,333 @@ +# Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode,node,python +# Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode,node,python + +### Node ### +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +### Node Patch ### +# Serverless Webpack directories +.webpack/ + +# Optional stylelint cache + +# SvelteKit build / generate output +.svelte-kit + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +### Python Patch ### +# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration +poetry.toml + +# ruff +.ruff_cache/ + +# LSP config files +pyrightconfig.json + +### VisualStudioCode ### +.vscode/* +#!.vscode/settings.json +#!.vscode/tasks.json +#!.vscode/launch.json +#!.vscode/extensions.json +#!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +# End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,node,python + diff --git a/connections/firefoxaccounts.js b/connections/firefoxaccounts.js index 19d936f..e7d65d3 100644 --- a/connections/firefoxaccounts.js +++ b/connections/firefoxaccounts.js @@ -60,6 +60,12 @@ function firefoxAccountsConnection(accessToken, ctx, cb) { user_id: id_token.sub, picture: p.avatar, preferredLanguage: p.locale, + // Mozilla accounts (formerly Firefox Accounts), allows mixed case characters in thier email property + // I'm adding this comment here in case we want to actually enforce email case conformity in the future + // Although, it will need extensive testing to ensure we are breaking anything. In the meantime, we will + // need to account for the fact that this and other IdP connectors return email with mixed case. + // So until otherwise decided, we allow mixed case email. + // email: p.email.toLowerCase(), email: p.email, email_verified: true, fxa_sub: id_token.sub, diff --git a/rules/Global-Function-Declarations.js b/rules/Global-Function-Declarations.js index 9a2ed7f..210a03d 100644 --- a/rules/Global-Function-Declarations.js +++ b/rules/Global-Function-Declarations.js @@ -3,6 +3,12 @@ function globalFunctionDeclaration(user, context, callback) { // This rule MUST be at the top of the rule list (FIRST) or other rules WILL FAIL // with a NON RECOVERABLE error, and thus LOGIN WILL FAIL FOR USERS + // Since we do not use the /continue endpoint let's make sure we explictly fail with an ErrorUnauthorized + // otherwise it is possible to continue the session even after a postError redirect is set. + if (context.protocol === "redirect-callback") { + return callback(new UnauthorizedError('The /continue endpoint is not allowed')); + } + // postError(code) // @code string with an error code for the SSO Dashboard to display // @rcontext the current Auth0 rule context (passed from the rule) @@ -29,8 +35,10 @@ function globalFunctionDeclaration(user, context, callback) { ); skey = undefined; // auth0 compiler does not allow 'delete' so we undefine instead + + var domain = context.tenant === "dev" ? "sso.allizom.org" : "sso.mozilla.com"; rcontext.redirect = { - url: `https://sso.mozilla.com/forbidden?error=${token}` + url: `https://${domain}/forbidden?error=${token}` }; return rcontext; diff --git a/rules/force-ldap-logins-over-ldap.js b/rules/force-ldap-logins-over-ldap.js index 758cb0b..d63b3fd 100644 --- a/rules/force-ldap-logins-over-ldap.js +++ b/rules/force-ldap-logins-over-ldap.js @@ -10,6 +10,8 @@ function forceLDAPLoginsOverLDAP(user, context, callback) { 'mozillafoundation.org', // Main org domain 'getpocket.com', // Pocket domain 'thunderbird.net', // MZLA domain + 'readitlater.com', + 'mozilla-japan.org' ]; // Sanity checks @@ -31,7 +33,8 @@ function forceLDAPLoginsOverLDAP(user, context, callback) { // 'ad' is LDAP - Force LDAP users to log with LDAP here if (context.connectionStrategy !== 'ad') { for (let domain of MOZILLA_STAFF_DOMAINS) { - if (user.email.endsWith(domain)) { + // we need to sanitize the email address to lowercase before matching so we can catch users with upper/mixed case email addresses + if (user.email.toLowerCase().endsWith(domain)) { console.log(`Staff or LDAP user attempted to login with the wrong login method. We only allow ad (LDAP) for staff: ${user.email}`); return callback(null, user, global.postError('staffmustuseldap', context)); } diff --git a/rules/link-users-by-email-with-metadata.js b/rules/link-users-by-email-with-metadata.js index 331d9ac..7a18965 100644 --- a/rules/link-users-by-email-with-metadata.js +++ b/rules/link-users-by-email-with-metadata.js @@ -22,58 +22,87 @@ function linkUsersByEmailWithMetadata(user, context, callback) { } const userApiUrl = auth0.baseUrl + '/users'; - const userSearchApiUrl = auth0.baseUrl + '/users-by-email?'; - const params = new URLSearchParams({ - email: user.email, - }); - - fetch(userSearchApiUrl + params.toString(), { + const opts = { headers: { Authorization: 'Bearer ' + auth0.accessToken, }, - }) - .then((response) => { - if (response.status !== 200) - return callback(new Error('API Call failed: ' + response.body)); - return response.json(); - }) - .then((data) => { - // Ignore non-verified users - data = data.filter((u) => u.email_verified); - - if (data.length === 1) { - // The user logged in with an identity which is the only one Auth0 knows about - // Do not perform any account linking - return callback(null, user, context); - } + } + + // Since email addresses within auth0 are allowed to me mixed case and the /user-by-email search endpoint + // is case sensitive, we need to search for both situations. In the first search we search by "this" users email + // which might be mixed case (or not). Our second search is for the lowercase equivalent but only if two searches + // would be identical. + const searchMultipleEmailCases = async () => { + const emailUrl = new URL('/users-by-email', auth0.baseUrl) + emailUrl.searchParams.append('email', user.email); + + const emailUrlToLower = new URL('/users-by-email', auth0.baseUrl); + emailUrlToLower.searchParams.append('email', user.email.toLowerCase()); - if (data.length === 2) { - // Auth0 is aware of 2 identities with the same email address which means - // that the user just logged in with a new identity that hasn't been linked - // into the other existing identity. Here we pass the other account to the - // linking function - return linkAccount(data.filter((u) => u.user_id !== user.user_id)[0]); - } else { - // data.length is > 2 which, post November 2020 when all identities were - // force linked manually, shouldn't be possible - var error_message = - `Error linking account ${user.user_id} as there are ` + - `over 2 identities with the email address ${user.email} ` + - data.map((x) => x.user_id).join(); - console.log(error_message); - publishSNSMessage( - `${error_message}\n\ndata : ${JSON.stringify( - data - )}\nuser : ${JSON.stringify(user)}` - ); - return callback(new Error(error_message)); + let fetchPromiseArray = [fetch(emailUrl.toString(), opts)]; + // if this user is mixed case, we need to also search for the lower case equivalent + if (user.email !== user.email.toLowerCase()) { + fetchPromiseArray.push(...fetch(emailUrlToLower.toString(), opts)); + } + // Call one (or two) api calls to the /user-by-email api endpoint + const responsePromises = await Promise.all(fetchPromiseArray); + + // Map each response to its JSON conversion promise + const jsonPromises = responsePromises.map(response => { + if (!response.ok) { + return callback(new Error('API Call failed: ' + response.body)); } - }) - .catch((err) => { - return callback(err); + return response.json(); }); + // await all json responses promises to resolve + const allResponses = await Promise.all(jsonPromises); + + // flatten the array of arrays to get one array of profiles + const mergedProfiles = allResponses.flat(); + + return mergedProfiles; + } + + const data = searchMultipleEmailCases(); + + try { + // Ignore non-verified users + data = data.filter((u) => u.email_verified); + + if (data.length === 1) { + // The user logged in with an identity which is the only one Auth0 knows about + // Do not perform any account linking + return callback(null, user, context); + } + + if (data.length === 2) { + // Auth0 is aware of 2 identities with the same email address which means + // that the user just logged in with a new identity that hasn't been linked + // into the other existing identity. Here we pass the other account to the + // linking function + linkAccount(data.filter((u) => u.user_id !== user.user_id)[0]); + } else { + // data.length is > 2 which, post November 2020 when all identities were + // force linked manually, shouldn't be possible + var error_message = + `Error linking account ${user.user_id} as there are ` + + `over 2 identities with the email address ${user.email} ` + + data.map((x) => x.user_id).join(); + console.log(error_message); + publishSNSMessage( + `${error_message}\n\ndata : ${JSON.stringify( + data + )}\nuser : ${JSON.stringify(user)}` + ); + return callback(new Error(error_message)); + } + } catch (err) { + console.log('An unknown error occurred while linking accounts: ' + err); + return callback(err); + } + const linkAccount = (otherProfile) => { // sanity check if both accounts have LDAP as primary // we should NOT link these accounts and simply allow the user to continue logging in. @@ -99,50 +128,40 @@ function linkUsersByEmailWithMetadata(user, context, callback) { `Linking secondary identity ${secondaryUser.user_id} into primary identity ${primaryUser.user_id}` ); - // Update app, user metadata as Auth0 won't back this up in user.identities[x].profileData - secondaryUser.app_metadata = secondaryUser.app_metadata || {}; - secondaryUser.user_metadata = secondaryUser.user_metadata || {}; - auth0.users - .updateAppMetadata(primaryUser.user_id, secondaryUser.app_metadata) - .then( - auth0.users.updateUserMetadata( - primaryUser.user_id, - Object.assign( - {}, - secondaryUser.user_metadata, - primaryUser.user_metadata - ) - ) - ) - // Link the accounts - .then(function () { - fetch(userApiUrl + '/' + primaryUser.user_id + '/identities', { - method: 'post', - headers: { - Authorization: 'Bearer ' + auth0.accessToken, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - provider: secondaryUser.identities[0].provider, - user_id: String(secondaryUser.identities[0].user_id), - }), - }).then((response) => { - if (!response.ok && response.status >= 400) { - console.log('Error linking account: ' + response.statusText); - return callback( - new Error('Error linking account: ' + response.statusText) - ); - } - // Finally, swap user_id so that the current login process has the correct data - context.primaryUser = primaryUser.user_id; - context.primaryUserMetadata = primaryUser.user_metadata || {}; - return callback(null, user, context); - }); - }) - .catch((err) => { - console.log('An unknown error occurred while linking accounts: ' + err); - return callback(err); + // We no longer keep the user_metadata nor app_metadata from the secondary account + // that is being linked. If the primary account is LDAP, then it's existing + // metadata should prevail. And in the case of both, primary and secondary being + // non-ldap, account priority does not matter and neither does the metadata of + // the secondary account. + + // Link the accounts + try { + fetch(userApiUrl + '/' + primaryUser.user_id + '/identities', { + method: 'post', + headers: { + Authorization: 'Bearer ' + auth0.accessToken, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + provider: secondaryUser.identities[0].provider, + user_id: String(secondaryUser.identities[0].user_id), + }), + }).then((response) => { + if (!response.ok && response.status >= 400) { + console.log('Error linking account: ' + response.statusText); + return callback( + new Error('Error linking account: ' + response.statusText) + ); + } + // Finally, swap user_id so that the current login process has the correct data + context.primaryUser = primaryUser.user_id; + context.primaryUserMetadata = primaryUser.user_metadata || {}; + return callback(null, user, context); }); + } catch(err) { + console.log('An unknown error occurred while linking accounts: ' + err); + return callback(err); + }; }; const publishSNSMessage = (message) => { if ( diff --git a/tests/modules/global/auth0.js b/tests/modules/global/auth0.js index df64b8c..7e635d6 100644 --- a/tests/modules/global/auth0.js +++ b/tests/modules/global/auth0.js @@ -5,7 +5,7 @@ // functions of the Auth0 object. module.exports = { - baseUrl: "testing", + baseUrl: "https://example.com", accessToken: "testing", users: { updateAppMetadata: (user_id, app_metadata) => { From ec547a590ab53109c55c0cb16dba8277e015d49a Mon Sep 17 00:00:00 2001 From: Jake Watkins Date: Wed, 29 May 2024 11:24:01 -0400 Subject: [PATCH 02/13] Spelling and grammatical fixed --- connections/firefoxaccounts.js | 4 ++-- rules/Global-Function-Declarations.js | 2 +- rules/force-ldap-logins-over-ldap.js | 1 + rules/link-users-by-email-with-metadata.js | 6 +++--- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/connections/firefoxaccounts.js b/connections/firefoxaccounts.js index e7d65d3..d993b90 100644 --- a/connections/firefoxaccounts.js +++ b/connections/firefoxaccounts.js @@ -60,9 +60,9 @@ function firefoxAccountsConnection(accessToken, ctx, cb) { user_id: id_token.sub, picture: p.avatar, preferredLanguage: p.locale, - // Mozilla accounts (formerly Firefox Accounts), allows mixed case characters in thier email property + // Mozilla accounts (formerly Firefox Accounts), allows mixed case characters in their email property // I'm adding this comment here in case we want to actually enforce email case conformity in the future - // Although, it will need extensive testing to ensure we are breaking anything. In the meantime, we will + // Although, it will need extensive testing to ensure we aren't breaking anything. In the meantime, we will // need to account for the fact that this and other IdP connectors return email with mixed case. // So until otherwise decided, we allow mixed case email. // email: p.email.toLowerCase(), diff --git a/rules/Global-Function-Declarations.js b/rules/Global-Function-Declarations.js index 210a03d..37f239b 100644 --- a/rules/Global-Function-Declarations.js +++ b/rules/Global-Function-Declarations.js @@ -6,7 +6,7 @@ function globalFunctionDeclaration(user, context, callback) { // Since we do not use the /continue endpoint let's make sure we explictly fail with an ErrorUnauthorized // otherwise it is possible to continue the session even after a postError redirect is set. if (context.protocol === "redirect-callback") { - return callback(new UnauthorizedError('The /continue endpoint is not allowed')); + return callback(new UnauthorizedError('The /continue endpoint is not allowed'), user, context); } // postError(code) diff --git a/rules/force-ldap-logins-over-ldap.js b/rules/force-ldap-logins-over-ldap.js index d63b3fd..d80f792 100644 --- a/rules/force-ldap-logins-over-ldap.js +++ b/rules/force-ldap-logins-over-ldap.js @@ -5,6 +5,7 @@ function forceLDAPLoginsOverLDAP(user, context, callback) { 'jijaIzcZmFCDRtV74scMb9lI87MtYNTA', // mozillians.org Verification Client ]; + // The domain strings in this array should always be declared here in lowercase const MOZILLA_STAFF_DOMAINS = [ 'mozilla.com', // Main corp domain 'mozillafoundation.org', // Main org domain diff --git a/rules/link-users-by-email-with-metadata.js b/rules/link-users-by-email-with-metadata.js index 7a18965..97a9755 100644 --- a/rules/link-users-by-email-with-metadata.js +++ b/rules/link-users-by-email-with-metadata.js @@ -29,10 +29,10 @@ function linkUsersByEmailWithMetadata(user, context, callback) { }, } - // Since email addresses within auth0 are allowed to me mixed case and the /user-by-email search endpoint + // Since email addresses within auth0 are allowed to be mixed case and the /user-by-email search endpoint // is case sensitive, we need to search for both situations. In the first search we search by "this" users email // which might be mixed case (or not). Our second search is for the lowercase equivalent but only if two searches - // would be identical. + // would be different. const searchMultipleEmailCases = async () => { const emailUrl = new URL('/users-by-email', auth0.baseUrl) emailUrl.searchParams.append('email', user.email); @@ -129,7 +129,7 @@ function linkUsersByEmailWithMetadata(user, context, callback) { ); // We no longer keep the user_metadata nor app_metadata from the secondary account - // that is being linked. If the primary account is LDAP, then it's existing + // that is being linked. If the primary account is LDAP, then its existing // metadata should prevail. And in the case of both, primary and secondary being // non-ldap, account priority does not matter and neither does the metadata of // the secondary account. From e24da424f027718ac1a0bfc2bcab3907b0230e98 Mon Sep 17 00:00:00 2001 From: Jake Watkins Date: Wed, 13 Dec 2023 17:46:10 -0500 Subject: [PATCH 03/13] Stop overwriting primary metadata with secondary while account linking --- rules/link-users-by-email-with-metadata.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/rules/link-users-by-email-with-metadata.js b/rules/link-users-by-email-with-metadata.js index 97a9755..4d11116 100644 --- a/rules/link-users-by-email-with-metadata.js +++ b/rules/link-users-by-email-with-metadata.js @@ -134,7 +134,10 @@ function linkUsersByEmailWithMetadata(user, context, callback) { // non-ldap, account priority does not matter and neither does the metadata of // the secondary account. +<<<<<<< HEAD // Link the accounts +======= +>>>>>>> 4c9e892 (Stop overwriting primary metadata with secondary while account linking) try { fetch(userApiUrl + '/' + primaryUser.user_id + '/identities', { method: 'post', @@ -163,6 +166,7 @@ function linkUsersByEmailWithMetadata(user, context, callback) { return callback(err); }; }; + const publishSNSMessage = (message) => { if ( !('aws_logging_sns_topic_arn' in configuration) || From a9b7c1ae1fd1fc0500684ba702e0c09eb65b933f Mon Sep 17 00:00:00 2001 From: Jake Watkins Date: Mon, 29 Jan 2024 15:16:45 -0500 Subject: [PATCH 04/13] Add mozilliansorg_http-observatory-rds group for awsSaml --- rules/awsSaml.js | 1 + 1 file changed, 1 insertion(+) diff --git a/rules/awsSaml.js b/rules/awsSaml.js index 1005921..5cb3bc2 100644 --- a/rules/awsSaml.js +++ b/rules/awsSaml.js @@ -26,6 +26,7 @@ function awsSaml(user, context, callback) { "mozilliansorg_cia-aws", "mozilliansorg_consolidated-billing-aws", "mozilliansorg_devtools-code-origin-access", + "mozilliansorg_http-observatory-rds", "mozilliansorg_iam-in-transition", "mozilliansorg_iam-in-transition-admin", "mozilliansorg_meao-admins", From f291202948c8dc96b90f91022f53ad71e0d4f427 Mon Sep 17 00:00:00 2001 From: Jake Watkins Date: Mon, 12 Feb 2024 18:02:12 -0500 Subject: [PATCH 05/13] Vectra SAML modifications --- rules/SAML-vectra.js | 28 ++++++++++++++++++++++++++++ rules/SAML-vectra.json | 4 ++++ 2 files changed, 32 insertions(+) create mode 100644 rules/SAML-vectra.js create mode 100644 rules/SAML-vectra.json diff --git a/rules/SAML-vectra.js b/rules/SAML-vectra.js new file mode 100644 index 0000000..3ac7bf1 --- /dev/null +++ b/rules/SAML-vectra.js @@ -0,0 +1,28 @@ +function SAMLVectra(user, context, callback) { + if (!user) { + // If the user is not presented (i.e. a rule deleted it), just go on, since authenticate will always fail. + return callback(null, null, context); + } + + var ALLOWED_CLIENTIDS = [ + 'RmsIEl3T3cZzpKhEmZv1XZDns0OvTzIy', + ]; + + if (ALLOWED_CLIENTIDS.indexOf(context.clientID) >= 0) { + // Vectra expects one and only one group which happens to map to a single role on the Vectra side + // https://support.vectra.ai/s/article/KB-VS-1577 + + user.vectra_group = "mozilliansorg_sec_network_detection"; + context.samlConfiguration.mappings = { + "https://schema.vectra.ai/role": "vectra_group", + "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress": "email", + "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name": "name", + "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname": "given_name", + "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname": "family_name", + "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn": "upn" + }; + + return callback(null, user, context); + } + return callback(null, user, context); +} diff --git a/rules/SAML-vectra.json b/rules/SAML-vectra.json new file mode 100644 index 0000000..ff3a81f --- /dev/null +++ b/rules/SAML-vectra.json @@ -0,0 +1,4 @@ +{ + "enabled": true, + "order": 840 +} From 23cbd5705d1a6981cfe7e1c21b7c8b39000df0b0 Mon Sep 17 00:00:00 2001 From: Jake Watkins Date: Fri, 9 Feb 2024 11:40:15 -0500 Subject: [PATCH 06/13] Fix the mozilla logo image link in the passwordless email --- manual/passwordless.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manual/passwordless.html b/manual/passwordless.html index 51e8f6f..3b1fd18 100644 --- a/manual/passwordless.html +++ b/manual/passwordless.html @@ -11,7 +11,7 @@

- Mozilla + Mozilla

From 4237ede3807737070998abc9e16a1f3b88881e57 Mon Sep 17 00:00:00 2001 From: Jake Watkins Date: Fri, 16 Feb 2024 09:23:01 -0500 Subject: [PATCH 07/13] Remove original_connection_user_id namespaced claim --- rules/CIS-Claims-fixups.js | 33 --------------------------------- 1 file changed, 33 deletions(-) diff --git a/rules/CIS-Claims-fixups.js b/rules/CIS-Claims-fixups.js index d3b3906..0fd4f5a 100644 --- a/rules/CIS-Claims-fixups.js +++ b/rules/CIS-Claims-fixups.js @@ -104,39 +104,6 @@ function CISClaimsFixups(user, context, callback) { user.aal = user.aal || "UNKNOWN"; context.idToken[namespace+'AAL'] = user.aal; -/* WARNING this entire block can be removed when mozillians.org / DinoPark uses it's own verification method for -* accounts */ -/* START removable block */ - var WHITELIST = ['HvN5D3R64YNNhvcHKuMKny1O0KJZOOwH', // mozillians.org account verification - 't9bMi4eTCPpMp5Y6E1Lu92iVcqU0r1P1', // https://web-mozillians-staging.production.paas.mozilla.community Verification client - 'jijaIzcZmFCDRtV74scMb9lI87MtYNTA', // mozillians.org Verification Client - ]; - if (WHITELIST.indexOf(context.clientID) >= 0) { - // Original connection method's user_id (useful when the account is a linked account, this lets you know what the actual IdP - // was used to login - // Default to current user_id - var originalConnection_user_id = user.user_id; - var targetIdentity; - // If we have linked account, check if we have a better match - if (user.identities && user.identities.length > 1) { - for (var i = 0; i < user.identities.length; i++) { - targetIdentity = user.identities[i]; - // Find the identity which corresponding to the user logging in - if ((targetIdentity.connection === context.connection) && (targetIdentity.provider === context.connectionStrategy)) { - // If what we find has no `profileData` structure it means the user_id is the same as the one currently - // logging in, so we don't need to do anything. - // If it is, then we need to reconstruct a user_id from the identity data - if (targetIdentity.profileData !== undefined) { - originalConnection_user_id = targetIdentity.provider + '|' + targetIdentity.user_id; - } - break; - } - } - } - context.idToken[namespace+'original_connection_user_id'] = originalConnection_user_id; - } -/* END of removable block */ - // Give info about CIS API context.idToken[namespace+'README_FIRST'] = 'Please refer to https://github.com/mozilla-iam/person-api in order to query Mozilla IAM CIS user profile data'; return callback(null, user, context); From 29016368a3da09b9c8fc9a3d0b6b8e504b00ea9f Mon Sep 17 00:00:00 2001 From: Jake Watkins Date: Wed, 22 May 2024 13:19:07 -0400 Subject: [PATCH 08/13] Update GHE mapping to match current rule in production --- rules/GHE-Groups.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/rules/GHE-Groups.js b/rules/GHE-Groups.js index f31fae1..09bf016 100644 --- a/rules/GHE-Groups.js +++ b/rules/GHE-Groups.js @@ -56,7 +56,9 @@ function GHEGroups(user, context, callback) { 'RLPUxhCQsmmRHyOmDOGkLpu1mArNH3xn': 'mozilliansorg_ghe_firefoxux_users', 'KMcYzqySOFXHteY1zliDlq577ARCb6gi': 'mozilliansorg_ghe_mozillasocial_users', 'IEc83wZvZzcQXMkpUmrnb9P8wztUiokl': 'mozilliansorg_ghe_mozscout_users', - 'vkoDkHlCEUhlHNhVDtewJqRLVLGVsPrZ': 'mozilliansorg_ghe_mozilla-fakespot_users' + 'vkoDkHlCEUhlHNhVDtewJqRLVLGVsPrZ': 'mozilliansorg_ghe_mozilla-fakespot_users', + 'T6mjvGguOB5hkq9Aviaa58tOlwpJG5o6': 'mozilliansorg_ghe_mozilla-necko_users', + 'ZemrAl9S2q9GKJNQUdjZCNsLiVmSEg1P': 'mozilliansorg_ghe_mozilla-privacy_users' }; const fetch = require('node-fetch@2.6.1'); @@ -124,7 +126,7 @@ const getPersonProfile = async () => { return await response.json(); }; - + // We only care about SSO applications that exist in the applicationGroupMapping // If the SSO ID is undefined in applicationGroupMapping, skip processing and return callback() @@ -153,7 +155,7 @@ const getPersonProfile = async () => { } // If somehow dinopark allows a user to store an empty value // Let's set to null to be redirected later - if(githubUsername.length === 0) { + if(githubUsername.length === 0) { console.log("empty HACK#GITHUB"); errorCode = "ghnd"; githubUsername = null; @@ -163,7 +165,7 @@ const getPersonProfile = async () => { console.log("Unable to do the githubUsername lookup: " + e.message); errorCode = "ghul"; } - + // confirm the user has a githubUsername stored in mozillians, otherwise redirect if(githubUsername === null) { context.redirect = { From f3f2699063311349d444632562848a42dfb84314 Mon Sep 17 00:00:00 2001 From: Jake Watkins Date: Thu, 7 Dec 2023 17:28:45 -0500 Subject: [PATCH 09/13] major fixes --- rules/link-users-by-email-with-metadata.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/rules/link-users-by-email-with-metadata.js b/rules/link-users-by-email-with-metadata.js index 4d11116..5385846 100644 --- a/rules/link-users-by-email-with-metadata.js +++ b/rules/link-users-by-email-with-metadata.js @@ -134,10 +134,14 @@ function linkUsersByEmailWithMetadata(user, context, callback) { // non-ldap, account priority does not matter and neither does the metadata of // the secondary account. +<<<<<<< HEAD <<<<<<< HEAD // Link the accounts ======= >>>>>>> 4c9e892 (Stop overwriting primary metadata with secondary while account linking) +======= + // Link the accounts +>>>>>>> 4750e4e (major fixes) try { fetch(userApiUrl + '/' + primaryUser.user_id + '/identities', { method: 'post', From e62e79c423eeb04e1745d94ecd8539bca1f751c3 Mon Sep 17 00:00:00 2001 From: Jake Watkins Date: Wed, 29 May 2024 15:27:18 -0400 Subject: [PATCH 10/13] Fix various typos in comments and missing logic case --- rules/Global-Function-Declarations.js | 2 +- rules/force-ldap-logins-over-ldap.js | 14 ++++++++------ rules/link-users-by-email-with-metadata.js | 10 ++-------- 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/rules/Global-Function-Declarations.js b/rules/Global-Function-Declarations.js index 37f239b..1f247b2 100644 --- a/rules/Global-Function-Declarations.js +++ b/rules/Global-Function-Declarations.js @@ -3,7 +3,7 @@ function globalFunctionDeclaration(user, context, callback) { // This rule MUST be at the top of the rule list (FIRST) or other rules WILL FAIL // with a NON RECOVERABLE error, and thus LOGIN WILL FAIL FOR USERS - // Since we do not use the /continue endpoint let's make sure we explictly fail with an ErrorUnauthorized + // Since we do not use the /continue endpoint let's make sure we explictly fail with an UnauthorizedError // otherwise it is possible to continue the session even after a postError redirect is set. if (context.protocol === "redirect-callback") { return callback(new UnauthorizedError('The /continue endpoint is not allowed'), user, context); diff --git a/rules/force-ldap-logins-over-ldap.js b/rules/force-ldap-logins-over-ldap.js index d80f792..4365ad3 100644 --- a/rules/force-ldap-logins-over-ldap.js +++ b/rules/force-ldap-logins-over-ldap.js @@ -7,12 +7,14 @@ function forceLDAPLoginsOverLDAP(user, context, callback) { // The domain strings in this array should always be declared here in lowercase const MOZILLA_STAFF_DOMAINS = [ - 'mozilla.com', // Main corp domain - 'mozillafoundation.org', // Main org domain - 'getpocket.com', // Pocket domain - 'thunderbird.net', // MZLA domain - 'readitlater.com', - 'mozilla-japan.org' + 'mozilla.com', // Main corp domain + 'mozillafoundation.org', // Main org domain + 'getpocket.com', // Pocket domain + 'thunderbird.net', // MZLA domain + 'readitlater.com', + 'mozilla-japan.org', + 'mozilla.ai', + 'mozilla.vc' ]; // Sanity checks diff --git a/rules/link-users-by-email-with-metadata.js b/rules/link-users-by-email-with-metadata.js index 5385846..a08acd2 100644 --- a/rules/link-users-by-email-with-metadata.js +++ b/rules/link-users-by-email-with-metadata.js @@ -71,8 +71,9 @@ function linkUsersByEmailWithMetadata(user, context, callback) { // Ignore non-verified users data = data.filter((u) => u.email_verified); - if (data.length === 1) { + if (data.length <= 1) { // The user logged in with an identity which is the only one Auth0 knows about + // or no data returned // Do not perform any account linking return callback(null, user, context); } @@ -134,14 +135,7 @@ function linkUsersByEmailWithMetadata(user, context, callback) { // non-ldap, account priority does not matter and neither does the metadata of // the secondary account. -<<<<<<< HEAD -<<<<<<< HEAD // Link the accounts -======= ->>>>>>> 4c9e892 (Stop overwriting primary metadata with secondary while account linking) -======= - // Link the accounts ->>>>>>> 4750e4e (major fixes) try { fetch(userApiUrl + '/' + primaryUser.user_id + '/identities', { method: 'post', From 6a6d645fc9ee49d433e35fe8a3cf57c094a9b2e6 Mon Sep 17 00:00:00 2001 From: Jake Watkins Date: Wed, 29 May 2024 15:34:46 -0400 Subject: [PATCH 11/13] Remove erroneous newline --- rules/Global-Function-Declarations.js | 1 - 1 file changed, 1 deletion(-) diff --git a/rules/Global-Function-Declarations.js b/rules/Global-Function-Declarations.js index 9bc0af7..1f247b2 100644 --- a/rules/Global-Function-Declarations.js +++ b/rules/Global-Function-Declarations.js @@ -4,7 +4,6 @@ function globalFunctionDeclaration(user, context, callback) { // with a NON RECOVERABLE error, and thus LOGIN WILL FAIL FOR USERS // Since we do not use the /continue endpoint let's make sure we explictly fail with an UnauthorizedError - // otherwise it is possible to continue the session even after a postError redirect is set. if (context.protocol === "redirect-callback") { return callback(new UnauthorizedError('The /continue endpoint is not allowed'), user, context); From 843575545fab4d54eb4d2174ce7d4bccadfd7a00 Mon Sep 17 00:00:00 2001 From: Jake Watkins Date: Thu, 30 May 2024 10:56:45 -0400 Subject: [PATCH 12/13] Last minute fixes and cleanup * Added missing semicolons and remove unnecessary one * Removed publishSNSMessage function which was a legacy alert component and not longer used. * Rearranged function declaration and call to proper declare->call order --- rules/link-users-by-email-with-metadata.js | 108 +++++++-------------- 1 file changed, 36 insertions(+), 72 deletions(-) diff --git a/rules/link-users-by-email-with-metadata.js b/rules/link-users-by-email-with-metadata.js index a08acd2..0251b9a 100644 --- a/rules/link-users-by-email-with-metadata.js +++ b/rules/link-users-by-email-with-metadata.js @@ -27,14 +27,14 @@ function linkUsersByEmailWithMetadata(user, context, callback) { headers: { Authorization: 'Bearer ' + auth0.accessToken, }, - } + }; // Since email addresses within auth0 are allowed to be mixed case and the /user-by-email search endpoint // is case sensitive, we need to search for both situations. In the first search we search by "this" users email // which might be mixed case (or not). Our second search is for the lowercase equivalent but only if two searches // would be different. const searchMultipleEmailCases = async () => { - const emailUrl = new URL('/users-by-email', auth0.baseUrl) + const emailUrl = new URL('/users-by-email', auth0.baseUrl); emailUrl.searchParams.append('email', user.email); const emailUrlToLower = new URL('/users-by-email', auth0.baseUrl); @@ -63,46 +63,7 @@ function linkUsersByEmailWithMetadata(user, context, callback) { const mergedProfiles = allResponses.flat(); return mergedProfiles; - } - - const data = searchMultipleEmailCases(); - - try { - // Ignore non-verified users - data = data.filter((u) => u.email_verified); - - if (data.length <= 1) { - // The user logged in with an identity which is the only one Auth0 knows about - // or no data returned - // Do not perform any account linking - return callback(null, user, context); - } - - if (data.length === 2) { - // Auth0 is aware of 2 identities with the same email address which means - // that the user just logged in with a new identity that hasn't been linked - // into the other existing identity. Here we pass the other account to the - // linking function - linkAccount(data.filter((u) => u.user_id !== user.user_id)[0]); - } else { - // data.length is > 2 which, post November 2020 when all identities were - // force linked manually, shouldn't be possible - var error_message = - `Error linking account ${user.user_id} as there are ` + - `over 2 identities with the email address ${user.email} ` + - data.map((x) => x.user_id).join(); - console.log(error_message); - publishSNSMessage( - `${error_message}\n\ndata : ${JSON.stringify( - data - )}\nuser : ${JSON.stringify(user)}` - ); - return callback(new Error(error_message)); - } - } catch (err) { - console.log('An unknown error occurred while linking accounts: ' + err); - return callback(err); - } + }; const linkAccount = (otherProfile) => { // sanity check if both accounts have LDAP as primary @@ -162,38 +123,41 @@ function linkUsersByEmailWithMetadata(user, context, callback) { } catch(err) { console.log('An unknown error occurred while linking accounts: ' + err); return callback(err); - }; + } }; - const publishSNSMessage = (message) => { - if ( - !('aws_logging_sns_topic_arn' in configuration) || - !('aws_logging_access_key_id' in configuration) || - !('aws_logging_secret_key' in configuration) - ) { - console.log('Missing Auth0 AWS SNS logging configuration values'); - return false; + // Search for multiple accounts of the same user to link + let data = searchMultipleEmailCases(); + + try { + // Ignore non-verified users + data = data.filter((u) => u.email_verified); + + if (data.length <= 1) { + // The user logged in with an identity which is the only one Auth0 knows about + // or no data returned + // Do not perform any account linking + return callback(null, user, context); } - const SNS_TOPIC_ARN = configuration.aws_logging_sns_topic_arn; - const ACCESS_KEY_ID = configuration.aws_logging_access_key_id; - const SECRET_KEY = configuration.aws_logging_secret_key; - - let AWS = require('aws-sdk@2.1416.0'); - let sns = new AWS.SNS({ - apiVersion: '2010-03-31', - accessKeyId: ACCESS_KEY_ID, - secretAccessKey: SECRET_KEY, - region: 'us-west-2', - logger: console, - }); - const params = { - Message: message, - TopicArn: SNS_TOPIC_ARN, - }; - sns.publish(params, function (err, data) { - if (err) console.log(err, err.stack); // an error occurred - else console.log(data); // successful response - }); - }; + if (data.length === 2) { + // Auth0 is aware of 2 identities with the same email address which means + // that the user just logged in with a new identity that hasn't been linked + // into the other existing identity. Here we pass the other account to the + // linking function + linkAccount(data.filter((u) => u.user_id !== user.user_id)[0]); + } else { + // data.length is > 2 which, post November 2020 when all identities were + // force linked manually, shouldn't be possible + var error_message = + `Error linking account ${user.user_id} as there are ` + + `over 2 identities with the email address ${user.email} ` + + data.map((x) => x.user_id).join(); + console.log(error_message); + return callback(new Error(error_message)); + } + } catch (err) { + console.log('An unknown error occurred while linking accounts: ' + err); + return callback(err); + } } From 6569b1e98fbd2a84551bb80eef23294727899fd2 Mon Sep 17 00:00:00 2001 From: Jake Watkins Date: Thu, 30 May 2024 14:18:03 -0400 Subject: [PATCH 13/13] More cleanup after testing * Fixed the auth0 API URL path * Changed the searchMultipleEmailCases call to a .then().catch() in order to properly resolve the returned promise --- rules/link-users-by-email-with-metadata.js | 68 +++++++++++----------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/rules/link-users-by-email-with-metadata.js b/rules/link-users-by-email-with-metadata.js index 0251b9a..8d150ea 100644 --- a/rules/link-users-by-email-with-metadata.js +++ b/rules/link-users-by-email-with-metadata.js @@ -34,10 +34,10 @@ function linkUsersByEmailWithMetadata(user, context, callback) { // which might be mixed case (or not). Our second search is for the lowercase equivalent but only if two searches // would be different. const searchMultipleEmailCases = async () => { - const emailUrl = new URL('/users-by-email', auth0.baseUrl); + const emailUrl = new URL('/api/v2/users-by-email', auth0.baseUrl); emailUrl.searchParams.append('email', user.email); - const emailUrlToLower = new URL('/users-by-email', auth0.baseUrl); + const emailUrlToLower = new URL('/api/v2/users-by-email', auth0.baseUrl); emailUrlToLower.searchParams.append('email', user.email.toLowerCase()); let fetchPromiseArray = [fetch(emailUrl.toString(), opts)]; @@ -127,37 +127,37 @@ function linkUsersByEmailWithMetadata(user, context, callback) { }; // Search for multiple accounts of the same user to link - let data = searchMultipleEmailCases(); - - try { - // Ignore non-verified users - data = data.filter((u) => u.email_verified); - - if (data.length <= 1) { - // The user logged in with an identity which is the only one Auth0 knows about - // or no data returned - // Do not perform any account linking - return callback(null, user, context); - } + searchMultipleEmailCases() + .then((data) => { + // Ignore non-verified users + data = data.filter((u) => u.email_verified); + + if (data.length <= 1) { + // The user logged in with an identity which is the only one Auth0 knows about + // or no data returned + // Do not perform any account linking + return callback(null, user, context); + } - if (data.length === 2) { - // Auth0 is aware of 2 identities with the same email address which means - // that the user just logged in with a new identity that hasn't been linked - // into the other existing identity. Here we pass the other account to the - // linking function - linkAccount(data.filter((u) => u.user_id !== user.user_id)[0]); - } else { - // data.length is > 2 which, post November 2020 when all identities were - // force linked manually, shouldn't be possible - var error_message = - `Error linking account ${user.user_id} as there are ` + - `over 2 identities with the email address ${user.email} ` + - data.map((x) => x.user_id).join(); - console.log(error_message); - return callback(new Error(error_message)); - } - } catch (err) { - console.log('An unknown error occurred while linking accounts: ' + err); - return callback(err); - } + if (data.length === 2) { + // Auth0 is aware of 2 identities with the same email address which means + // that the user just logged in with a new identity that hasn't been linked + // into the other existing identity. Here we pass the other account to the + // linking function + linkAccount(data.filter((u) => u.user_id !== user.user_id)[0]); + } else { + // data.length is > 2 which, post November 2020 when all identities were + // force linked manually, shouldn't be possible + var error_message = + `Error linking account ${user.user_id} as there are ` + + `over 2 identities with the email address ${user.email} ` + + data.map((x) => x.user_id).join(); + console.log(error_message); + return callback(new Error(error_message)); + } + }) + .catch((err) => { + console.log('An unknown error occurred while linking accounts: ' + err); + return callback(err); + }); }