Skip to content

Commit 3fbc43a

Browse files
committed
feat(other): implement TOTP auth for Other platform
1 parent 213ee45 commit 3fbc43a

File tree

5 files changed

+158
-5
lines changed

5 files changed

+158
-5
lines changed

packages/app/lib/internal/NativeFirebaseError.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,18 @@ export default class NativeFirebaseError extends Error {
4949
value: userInfo,
5050
});
5151

52+
// Needed for MFA processing of errors on web
53+
Object.defineProperty(this, 'customData', {
54+
enumerable: false,
55+
value: nativeError.customData || null,
56+
});
57+
58+
// Needed for MFA processing of errors on web
59+
Object.defineProperty(this, 'operationType', {
60+
enumerable: false,
61+
value: nativeError.operationType || null,
62+
});
63+
5264
Object.defineProperty(this, 'nativeErrorCode', {
5365
enumerable: false,
5466
value: userInfo.nativeErrorCode || null,

packages/auth/lib/TotpMultiFactorGenerator.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@
1515
*
1616
*/
1717

18+
import { isOther } from '@react-native-firebase/app/lib/common';
1819
import { TotpSecret } from './TotpSecret';
20+
import { getAuth } from './modular';
1921

2022
export default class TotpMultiFactorGenerator {
2123
static FACTOR_ID = 'totp';
@@ -27,6 +29,11 @@ export default class TotpMultiFactorGenerator {
2729
}
2830

2931
static assertionForSignIn(uid, verificationCode) {
32+
if (isOther) {
33+
// we require the web native assertion when using firebase-js-sdk
34+
// as it has functions used by the SDK, a shim won't do
35+
return getAuth().native.assertionForSignIn(uid, verificationCode);
36+
}
3037
return { uid, verificationCode };
3138
}
3239

packages/auth/lib/getMultiFactorResolver.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { isOther } from '@react-native-firebase/app/lib/common';
12
import MultiFactorResolver from './MultiFactorResolver.js';
23

34
/**
@@ -7,6 +8,9 @@ import MultiFactorResolver from './MultiFactorResolver.js';
78
* Returns null if no resolver object can be found on the error.
89
*/
910
export function getMultiFactorResolver(auth, error) {
11+
if (isOther) {
12+
return auth.native.getMultiFactorResolver(error);
13+
}
1014
if (
1115
error.hasOwnProperty('userInfo') &&
1216
error.userInfo.hasOwnProperty('resolver') &&

packages/auth/lib/multiFactor.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export class MultiFactorUser {
3636
}
3737

3838
// We need to reload the user otherwise the changes are not visible
39+
// TODO reload not working on Other platform
3940
return reload(this._auth.currentUser);
4041
}
4142

packages/auth/lib/web/RNFBAuthModule.js

Lines changed: 134 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import {
88
sendSignInLinkToEmail,
99
getAdditionalUserInfo,
1010
multiFactor,
11+
getMultiFactorResolver,
12+
TotpMultiFactorGenerator,
1113
createUserWithEmailAndPassword,
1214
signInWithEmailAndPassword,
1315
isSignInWithEmailLink,
@@ -70,6 +72,15 @@ function rejectPromiseWithCodeAndMessage(code, message) {
7072
return rejectPromise(getWebError({ code: `auth/${code}`, message }));
7173
}
7274

75+
function rejectWithCodeAndMessage(code, message) {
76+
return Promise.reject(
77+
getWebError({
78+
code,
79+
message,
80+
}),
81+
);
82+
}
83+
7384
/**
7485
* Returns a structured error object.
7586
* @param {error} error The error object.
@@ -102,7 +113,9 @@ function userToObject(user) {
102113
tenantId: user.tenantId !== null && user.tenantId !== '' ? user.tenantId : null,
103114
providerData: user.providerData.map(userInfoToObject),
104115
metadata: userMetadataToObject(user.metadata),
105-
multiFactor: multiFactor(user).enrolledFactors.map(multiFactorInfoToObject),
116+
multiFactor: {
117+
enrolledFactors: multiFactor(user).enrolledFactors.map(multiFactorInfoToObject),
118+
},
106119
};
107120
}
108121

@@ -222,6 +235,7 @@ const instances = {};
222235
const authStateListeners = {};
223236
const idTokenListeners = {};
224237
const sessionMap = new Map();
238+
const totpSecretMap = new Map();
225239
let sessionId = 0;
226240

227241
// Returns a cached Firestore instance.
@@ -441,11 +455,28 @@ export default {
441455
* @returns {Promise<object>} - The result of the sign in.
442456
*/
443457
async signInWithEmailAndPassword(appName, email, password) {
444-
return guard(async () => {
445-
const auth = getCachedAuthInstance(appName);
446-
const credential = await signInWithEmailAndPassword(auth, email, password);
458+
// The default guard / getWebError process doesn't work well here,
459+
// since it creates a new error object that is then passed through
460+
// a native module proxy and gets processed again.
461+
// We need lots of information from the error so that MFA will work
462+
// later if needed. So we handle the error custom here.
463+
// return guard(async () => {
464+
try {
465+
const credential = await signInWithEmailAndPassword(
466+
getCachedAuthInstance(appName),
467+
email,
468+
password,
469+
);
447470
return authResultToObject(credential);
448-
});
471+
} catch (e) {
472+
e.userInfo = {
473+
code: e.code.split('/')[1],
474+
message: e.message,
475+
customData: e.customData,
476+
};
477+
throw e;
478+
}
479+
// });
449480
},
450481

451482
/**
@@ -991,6 +1022,104 @@ export default {
9911022
});
9921023
},
9931024

1025+
/**
1026+
* Get a MultiFactorResolver from the underlying SDK
1027+
* @param {*} _appName the name of the app to get the auth instance for
1028+
* @param {*} uid the uid of the TOTP MFA attempt
1029+
* @param {*} code the code from the user TOTP app
1030+
* @return TotpMultiFactorAssertion to use for resolving
1031+
*/
1032+
assertionForSignIn(_appName, uid, code) {
1033+
return TotpMultiFactorGenerator.assertionForSignIn(uid, code);
1034+
},
1035+
1036+
/**
1037+
* Get a MultiFactorResolver from the underlying SDK
1038+
* @param {*} appName the name of the app to get the auth instance for
1039+
* @param {*} error the MFA error returned from initial factor login attempt
1040+
* @return MultiFactorResolver to use for verifying the second factor
1041+
*/
1042+
getMultiFactorResolver(appName, error) {
1043+
return getMultiFactorResolver(getCachedAuthInstance(appName), error);
1044+
},
1045+
1046+
/**
1047+
* generate a TOTP secret
1048+
* @param {*} _appName - The name of the app to get the auth instance for.
1049+
* @param {*} session - The MultiFactorSession to associate with the secret
1050+
* @returns object with secretKey to associate with TotpSecret
1051+
*/
1052+
async generateTotpSecret(_appName, session) {
1053+
return guard(async () => {
1054+
const totpSecret = await TotpMultiFactorGenerator.generateSecret(sessionMap.get(session));
1055+
totpSecretMap.set(totpSecret.secretKey, totpSecret);
1056+
return { secretKey: totpSecret.secretKey };
1057+
});
1058+
},
1059+
1060+
/**
1061+
* unenroll from TOTP
1062+
* @param {*} appName - The name of the app to get the auth instance for.
1063+
* @param {*} enrollmentId - The ID to associate with the enrollment
1064+
* @returns
1065+
*/
1066+
async unenrollMultiFactor(appName, enrollmentId) {
1067+
return guard(async () => {
1068+
const auth = getCachedAuthInstance(appName);
1069+
if (auth.currentUser === null) {
1070+
return promiseNoUser(true);
1071+
}
1072+
await multiFactor(auth.currentUser).unenroll(enrollmentId);
1073+
});
1074+
},
1075+
1076+
/**
1077+
* finalize a TOTP enrollment
1078+
* @param {*} appName - The name of the app to get the auth instance for.
1079+
* @param {*} secretKey - The secretKey to associate native TotpSecret
1080+
* @param {*} verificationCode - The TOTP to verify
1081+
* @param {*} displayName - The name to associate as a hint
1082+
* @returns
1083+
*/
1084+
async finalizeTotpEnrollment(appName, secretKey, verificationCode, displayName) {
1085+
return guard(async () => {
1086+
const auth = getCachedAuthInstance(appName);
1087+
if (auth.currentUser === null) {
1088+
return promiseNoUser(true);
1089+
}
1090+
const multiFactorAssertion = TotpMultiFactorGenerator.assertionForEnrollment(
1091+
totpSecretMap.get(secretKey),
1092+
verificationCode,
1093+
);
1094+
await multiFactor(auth.currentUser).enroll(multiFactorAssertion, displayName);
1095+
});
1096+
},
1097+
1098+
/**
1099+
* generate a TOTP QR Code URL
1100+
* @param {*} _appName - The name of the app to get the auth instance for.
1101+
* @param {*} secretKey - The secretKey to associate with the TotpSecret
1102+
* @param {*} accountName - The account name to use in auth app
1103+
* @param {*} issuer - The issuer to use in auth app
1104+
* @returns QR Code URL
1105+
*/
1106+
generateQrCodeUrl(_appName, secretKey, accountName, issuer) {
1107+
return totpSecretMap.get(secretKey).generateQrCodeUrl(accountName, issuer);
1108+
},
1109+
1110+
/**
1111+
* open a QR Code URL in an app directly
1112+
* @param {*} appName - The name of the app to get the auth instance for.
1113+
* @param {*} qrCodeUrl the URL to open in the app, from generateQrCodeUrl
1114+
* @throws Error not supported in this environment
1115+
*/
1116+
openInOtpApp() {
1117+
return rejectWithCodeAndMessage(
1118+
'unsupported',
1119+
'This operation is not supported in this environment.',
1120+
);
1121+
},
1122+
9941123
/* ----------------------
9951124
* other methods
9961125
* ---------------------- */

0 commit comments

Comments
 (0)