1010 * Please see https://github.com/mozilla-iam/mozilla-iam/blob/master/docs/deratcheting-user-flows.md#user-logs-in-with-the-mozilla-iam-system-for-the-first-time
1111 * for detailed explanation of what happens here.
1212 *
13+ * A failure to link users isn't considered an authentication failure. We should
14+ * still allow the authentication process to continue. The errors related to
15+ * unlinked accounts will crop-up else where, such as when trying to log in with
16+ * a non-LDAP identity. (Enforced in ensureLdapUsersUseLdap).
1317 */
1418
1519const auth0Sdk = require ( "auth0" ) ;
1620
21+ // A wrapper around getByEmail to retry any time it fails. The SDK already has
22+ // some code to deal with retrying, though it does not cover issues like
23+ // transient network errors.
24+ //
25+ // https://github.com/auth0/node-auth0/blob/1e0fbf0e9aeafffa680360a7b324575ff6f1830c/src/lib/retry.ts#L56
26+ async function usersByEmail ( mgmtClient , email ) {
27+ console . log ( `Searching for users with email ${ email } ` ) ;
28+ let error ;
29+ for ( var retries = 0 ; retries < 3 ; retries ++ ) {
30+ try {
31+ return await mgmtClient . usersByEmail . getByEmail ( { email } ) ;
32+ } catch ( err ) {
33+ // In the future, we can use `err.errorCode` we should abort or retry on.
34+ // For now, we don't have enough information.
35+ console . error (
36+ "Error getting user by email:" ,
37+ err . errorCode ,
38+ err . statusCode ,
39+ err . error
40+ ) ;
41+ error = err ;
42+ }
43+ }
44+ throw error ;
45+ }
46+
47+ // Links the account in the event with another profile we've found.
48+ async function linkAccount ( api , event , mgmtClient , otherProfile ) {
49+ // sanity check if both accounts have LDAP as primary
50+ // we should NOT link these accounts and simply allow the user to continue logging in.
51+ if (
52+ event . user . user_id . startsWith ( "ad|Mozilla-LDAP" ) &&
53+ otherProfile . user_id . startsWith ( "ad|Mozilla-LDAP" )
54+ ) {
55+ console . error (
56+ `Error: both ${ event . user . user_id } and ${ otherProfile . user_id } are LDAP Primary accounts. Linking will not occur.`
57+ ) ;
58+ return ; // Continue with user login without account linking
59+ }
60+
61+ // LDAP takes priority being the primary identity
62+ // So we need to determine if one or neither are LDAP
63+ // If both are non-primary, linking order doesn't matter
64+ let primaryUser ;
65+ let secondaryUser ;
66+
67+ if ( event . user . user_id . startsWith ( "ad|Mozilla-LDAP" ) ) {
68+ primaryUser = event . user ;
69+ secondaryUser = otherProfile ;
70+ } else {
71+ primaryUser = otherProfile ;
72+ secondaryUser = event . user ;
73+ }
74+
75+ // Link the secondary account into the primary account
76+ console . log (
77+ `Linking secondary identity ${ secondaryUser . user_id } into primary identity ${ primaryUser . user_id } `
78+ ) ;
79+
80+ // We no longer keep the user_metadata nor app_metadata from the secondary account
81+ // that is being linked. If the primary account is LDAP, then its existing
82+ // metadata should prevail. And in the case of both, primary and secondary being
83+ // non-ldap, account priority does not matter and neither does the metadata of
84+ // the secondary account.
85+
86+ // Link the accounts
87+ try {
88+ await mgmtClient . users . link (
89+ { id : String ( primaryUser . user_id ) } ,
90+ {
91+ provider : secondaryUser . identities [ 0 ] . provider ,
92+ user_id : secondaryUser . identities [ 0 ] . user_id ,
93+ }
94+ ) ;
95+ } catch ( err ) {
96+ console . error (
97+ "Error linking accounts:" ,
98+ err . errorCode ,
99+ err . statusCode ,
100+ err . error
101+ ) ;
102+ throw err ;
103+ }
104+
105+ // Auth0 Action api object provides a method for updating the current
106+ // authenticated user to the new user_id after account linking has taken place
107+ return api . authentication . setPrimaryUser ( primaryUser . user_id ) ;
108+ }
109+
110+ // Can probably be inlined, but pulled out for clarity.
111+ function mergeAccountLists ( accumulator , response ) {
112+ return accumulator . concat ( response . data ) ;
113+ }
114+
115+ // Since email addresses within auth0 are allowed to be mixed case and the /user-by-email search endpoint
116+ // is case sensitive, we need to search for both situations. In the first search we search by "this" users email
117+ // which might be mixed case (or not). Our second search is for the lowercase equivalent but only if two searches
118+ // would be different.
119+ async function searchMultipleEmailCases ( mgmtClient , email ) {
120+ let userAccountsFound = [ ] ;
121+
122+ // Push the base case.
123+ userAccountsFound . push ( usersByEmail ( mgmtClient , email ) ) ;
124+ // if this user is mixed case, we need to also search for the lower case equivalent
125+ if ( email !== email . toLowerCase ( ) ) {
126+ userAccountsFound . push ( usersByEmail ( mgmtClient , email . toLowerCase ( ) ) ) ;
127+ }
128+
129+ // await all json responses promises to resolve
130+ const allJSONResponses = await Promise . all ( userAccountsFound ) ;
131+
132+ // flatten the array of arrays to get one array of profiles
133+ return allJSONResponses . reduce ( mergeAccountLists , [ ] ) ;
134+ }
135+
17136exports . onExecutePostLogin = async ( event , api ) => {
18137 console . log ( "Running actions:" , "linkUsersByEmail" ) ;
19138
@@ -37,135 +156,69 @@ exports.onExecutePostLogin = async (event, api) => {
37156 scope : "update:users" ,
38157 } ) ;
39158
40- // Since email addresses within auth0 are allowed to be mixed case and the /user-by-email search endpoint
41- // is case sensitive, we need to search for both situations. In the first search we search by "this" users email
42- // which might be mixed case (or not). Our second search is for the lowercase equivalent but only if two searches
43- // would be different.
44- const searchMultipleEmailCases = async ( ) => {
45- let userAccountsFound = [ ] ;
46-
47- // Push the
48- userAccountsFound . push (
49- mgmtClient . usersByEmail . getByEmail ( { email : event . user . email } )
50- ) ;
51-
52- // if this user is mixed case, we need to also search for the lower case equivalent
53- if ( event . user . email !== event . user . email . toLowerCase ( ) ) {
54- userAccountsFound . push (
55- mgmtClient . usersByEmail . getByEmail ( {
56- email : event . user . email . toLowerCase ( ) ,
57- } )
58- ) ;
59- }
60-
61- // await all json responses promises to resolve
62- const allJSONResponses = await Promise . all ( userAccountsFound ) ;
63-
64- // flatten the array of arrays to get one array of profiles
65- const mergedDataProfiles = allJSONResponses . reduce ( ( acc , response ) => {
66- return acc . concat ( response . data ) ;
67- } , [ ] ) ;
68-
69- return mergedDataProfiles ;
70- } ;
71-
72- const linkAccount = async ( otherProfile ) => {
73- // sanity check if both accounts have LDAP as primary
74- // we should NOT link these accounts and simply allow the user to continue logging in.
75- if (
76- event . user . user_id . startsWith ( "ad|Mozilla-LDAP" ) &&
77- otherProfile . user_id . startsWith ( "ad|Mozilla-LDAP" )
78- ) {
79- console . error (
80- `Error: both ${ event . user . user_id } and ${ otherProfile . user_id } are LDAP Primary accounts. Linking will not occur.`
81- ) ;
82- return ; // Continue with user login without account linking
83- }
84-
85- // LDAP takes priority being the primary identity
86- // So we need to determine if one or neither are LDAP
87- // If both are non-primary, linking order doesn't matter
88- let primaryUser ;
89- let secondaryUser ;
90-
91- if ( event . user . user_id . startsWith ( "ad|Mozilla-LDAP" ) ) {
92- primaryUser = event . user ;
93- secondaryUser = otherProfile ;
94- } else {
95- primaryUser = otherProfile ;
96- secondaryUser = event . user ;
97- }
98-
99- // Link the secondary account into the primary account
100- console . log (
101- `Linking secondary identity ${ secondaryUser . user_id } into primary identity ${ primaryUser . user_id } `
102- ) ;
103-
104- // We no longer keep the user_metadata nor app_metadata from the secondary account
105- // that is being linked. If the primary account is LDAP, then its existing
106- // metadata should prevail. And in the case of both, primary and secondary being
107- // non-ldap, account priority does not matter and neither does the metadata of
108- // the secondary account.
109-
110- // Link the accounts
111- try {
112- await mgmtClient . users . link (
113- { id : String ( primaryUser . user_id ) } ,
114- {
115- provider : secondaryUser . identities [ 0 ] . provider ,
116- user_id : secondaryUser . identities [ 0 ] . user_id ,
117- }
118- ) ;
119-
120- // Auth0 Action api object provides a method for updating the current
121- // authenticated user to the new user_id after account linking has taken place
122- api . authentication . setPrimaryUser ( primaryUser . user_id ) ;
123- } catch ( err ) {
124- console . error ( "An unknown error occurred while linking accounts:" , err ) ;
125- throw err ;
126- }
127-
128- return ;
129- } ;
130-
131159 // Main
160+ let candidateUserAccountList ;
132161 try {
133162 // Search for multiple accounts of the same user to link
134- let userAccountList = await searchMultipleEmailCases ( ) ;
135-
136- // Ignore non-verified users
137- userAccountList = userAccountList . filter ( ( u ) => u . email_verified ) ;
163+ candidateUserAccountList = await searchMultipleEmailCases (
164+ mgmtClient ,
165+ event . user . email
166+ ) ;
167+ } catch ( err ) {
168+ console . error (
169+ "Error searching for users by email:" ,
170+ err . errorCode ,
171+ err . statusCode ,
172+ err . error
173+ ) ;
174+ return ;
175+ }
138176
139- if ( userAccountList . length <= 1 ) {
140- // The user logged in with an identity which is the only one Auth0 knows about
141- // or no data returned
142- // Do not perform any account linking
143- return ;
144- }
177+ // Ignore non-verified users
178+ let userAccountList = candidateUserAccountList . filter (
179+ ( u ) => u . email_verified
180+ ) ;
145181
146- if ( userAccountList . length === 2 ) {
147- // Auth0 is aware of 2 identities with the same email address which means
148- // that the user just logged in with a new identity that hasn't been linked
149- // into the other existing identity. Here we pass the other account to the
150- // linking function
182+ // The user logged in with an identity which is the only one Auth0 knows about
183+ // or no data returned. Do not perform any account linking
184+ if ( userAccountList . length <= 1 ) {
185+ return ;
186+ }
151187
152- await linkAccount (
188+ // Auth0 is aware of 2 identities with the same email address which means
189+ // that the user just logged in with a new identity that hasn't been linked
190+ // into the other existing identity. Here we pass the other account to the
191+ // linking function.
192+ //
193+ // If we fail to link the accounts, the user should still be allowed to log in.
194+ if ( userAccountList . length === 2 ) {
195+ try {
196+ return await linkAccount (
197+ api ,
198+ event ,
199+ mgmtClient ,
153200 userAccountList . filter ( ( u ) => u . user_id !== event . user . user_id ) [ 0 ]
154201 ) ;
155- } else {
156- // data.length is > 2 which, post November 2020 when all identities were
157- // force linked manually, shouldn't be possible
158- var error_message =
159- `Error linking account ${ event . user . user_id } as there are ` +
160- `over 2 identities with the email address ${ event . user . email } ` +
161- userAccountList . map ( ( x ) => x . user_id ) . join ( ) ;
162- console . error ( error_message ) ;
163- throw new Error ( error_message ) ;
202+ } catch ( err ) {
203+ console . error (
204+ "Error linking accounts:" ,
205+ err . errorCode ,
206+ err . statusCode ,
207+ err . error
208+ ) ;
209+ return ;
164210 }
165- } catch ( err ) {
166- console . error ( "An error occurred while linking accounts:" , err ) ;
167- return api . access . deny ( err . message || String ( err ) ) ;
168211 }
169212
170- return ;
213+ // data.length is > 2 which, post November 2020 when all identities were
214+ // force linked manually, shouldn't be possible.
215+ //
216+ // While this is, strictly speaking, not an authentication failure, it is a
217+ // state that we don't expect to be in and should be investigated immediately.
218+ var error_message =
219+ `Error linking account ${ event . user . user_id } as there are ` +
220+ `over 2 identities with the email address ${ event . user . email } ` +
221+ userAccountList . map ( ( x ) => x . user_id ) . join ( ) ;
222+ console . error ( error_message ) ;
223+ return api . access . deny ( error_message ) ;
171224} ;
0 commit comments