Skip to content

Commit

Permalink
(android) Implement multi-factor utility functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
dpa99c committed Nov 8, 2022
1 parent 0e46983 commit 7dea9bc
Show file tree
Hide file tree
Showing 5 changed files with 214 additions and 18 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Version 15.0.0
* (iOS & Android) BREAKING CHANGE: Changed signature of `verifyPhoneNumber()` to pass optional arguments as an object.
* (iOS & Android) feat: Add support for multi-factor authentication
* See `enrollSecondAuthFactor()`, `verifySecondAuthFactor()`
* Added `enrollSecondAuthFactor()`, `verifySecondAuthFactor()`, `listEnrolledSecondAuthFactors()`, `unenrollSecondAuthFactor()`, `verifyBeforeUpdateEmail()`

# Version 14.2.1
* (iOS) bugfix: remove openURL delegate that was erroneously re-added by merge error.
Expand Down
88 changes: 86 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ To help ensure this plugin is kept updated, new features are added and bugfixes
- [updateUserProfile](#updateuserprofile)
- [updateUserEmail](#updateuseremail)
- [sendUserEmailVerification](#senduseremailverification)
- [verifyBeforeUpdateEmail](#verifybeforeupdateemail)
- [updateUserPassword](#updateuserpassword)
- [sendUserPasswordResetEmail](#senduserpasswordresetemail)
- [deleteUser](#deleteuser)
Expand Down Expand Up @@ -2448,10 +2449,10 @@ Updates the display name and/or photo URL of the current Firebase user signed in
```

### updateUserEmail
Updates/sets the email address of the current Firebase user signed into the app.
Updates/sets the email address of the current Firebase user signed in to the app.

**Parameters**:
- {string} email - email address of user
- {string} email - email address of user to set as current
- {function} success - callback function to call on success
- {function} error - callback function which will be passed a {string} error message as an argument

Expand Down Expand Up @@ -2495,6 +2496,25 @@ When the user opens the contained link, their email address will have been verif
});
```

### verifyBeforeUpdateEmail
First verifies the user's identity, then set/supdates the email address of the current Firebase user signed in to the app.
- This is required when a user with multi-factor authentication enabled on their account wishes to change their registered email address.
- [updateUserEmail](#updateuseremail) cannot be used and will result in an error.
- See [the Firebase documentation](https://cloud.google.com/identity-platform/docs/work-with-mfa-users#updating_a_users_email) regarding updating the email address of a user with multi-factor authentication enabled.

**Parameters**:
- {string} email - email address of user to set as current
- {function} success - callback function to call on success
- {function} error - callback function which will be passed a {string} error message as an argument

```javascript
FirebasePlugin.verifyBeforeUpdateEmail("user@somewhere.com",function() {
console.log("User verified and email successfully updated");
}, function(error) {
console.error("Failed to verify user/update user email: " + error);
});
```

### updateUserPassword
Updates/sets the account password for the current Firebase user signed into the app.

Expand Down Expand Up @@ -2745,6 +2765,7 @@ Enrolls a user-specified phone number as a second factor for multi-factor authen
- In this case, once the user has entered the code, `enrollSecondAuthFactor` will need to be called again with the `credential` option used to specified the `code` and verification `id`.
- This function involves a similar verification flow to [verifyPhoneNumber](#verifyphonenumber) and therefore has the same pre-requisites and requirements.
- See the Firebase MFA documentation for [Android](https://cloud.google.com/identity-platform/docs/android/mfa) and [iOS](https://cloud.google.com/identity-platform/docs/ios/mfa) for more information on MFA-specific setup requirements.
- Calling when no user is signed in will result in error callback being invoked.

**Parameters**:
- {function} success - callback function to invoke either upon:
Expand Down Expand Up @@ -2876,6 +2897,69 @@ FirebasePlugin.signInWithCredential(credential, function() {
});
```

### listEnrolledSecondAuthFactors
Lists the second factors the current user has enrolled for multi-factor authentication (MFA).
- Calling when no user is signed in will result in error callback being invoked.

**Parameters**:
- {function} success - callback function to invoke upon successfully retrieving second factors. Will be passed an {array} of second factor {object} with properties:
- {integer} index - index of the factor in the list
- {string} phoneNumber - masked version of the phone number for this factor.
- {string} displayName - (optional) name of factor specified by the user when this factor was enrolled.
- {function} error - callback function which will be passed a {string} error message as an argument

Example usage:

```javascript
FirebasePlugin.listEnrolledSecondAuthFactors(
function(secondFactors) {
if(secondFactors.length > 0){
for(var secondFactor of secondFactors){
console.log(`${secondFactor.index}: ${secondFactor.phoneNumber}${secondFactor.displayName ? ' ('+secondFactor.displayName+')' : ''}`)
}
}else{
console.log("No second factors are enrolled");
}
}, function(error) {
console.error("Failed to list second factors: " + JSON.stringify(error));
}
)
```

### unenrollSecondAuthFactor
Unenrolls (removes) an enrolled second factor that the current user has enrolled for multi-factor authentication (MFA).
- Calling when no user is signed in will result in error callback being invoked.

**Parameters**:
- {function} success - callback function to invoke upon success
- {function} error - callback function which will be passed a {string} error message as an argument
- {number} selectedIndex - Index of the second factor to unenroll (obtained using [listEnrolledSecondAuthFactors](#listenrolledsecondauthfactors))

Example usage:

```javascript
function unenrollSecondAuthFactor(){
FirebasePlugin.listEnrolledSecondAuthFactors(
function(secondFactors) {
askUserToSelectSecondFactorToUnenroll(secondFactors) // you implement this
.then(function(selectedIndex){
FirebasePlugin.unenrollSecondAuthFactor(
function() {
console.log("Successfully unenrolled selected second factor");
}, function(error) {
console.error("Failed to unenroll second factor: " + JSON.stringify(error));
},
selectedIndex
)
}
);
}, function(error) {
console.error("Failed to list second factors: " + JSON.stringify(error));
}
)
}
```

### setLanguageCode
Sets the user-facing language code for auth operations that can be internationalized, such as sendEmailVerification() or verifyPhoneNumber(). This language code should follow the conventions defined by the IETF in BCP47.

Expand Down
115 changes: 100 additions & 15 deletions src/android/FirebasePlugin.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import com.google.firebase.auth.EmailAuthProvider;
import com.google.firebase.auth.FirebaseAuthMultiFactorException;
import com.google.firebase.auth.MultiFactorAssertion;
import com.google.firebase.auth.MultiFactorInfo;
import com.google.firebase.auth.MultiFactorResolver;
import com.google.firebase.auth.MultiFactorSession;
import com.google.firebase.auth.PhoneAuthOptions;
Expand Down Expand Up @@ -288,7 +289,11 @@ public boolean execute(String action, JSONArray args, CallbackContext callbackCo
this.enrollSecondAuthFactor(callbackContext, args);
} else if (action.equals("verifySecondAuthFactor")) {
this.verifySecondAuthFactor(callbackContext, args);
}else if (action.equals("setLanguageCode")) {
} else if (action.equals("listEnrolledSecondAuthFactors")) {
this.listEnrolledSecondAuthFactors(callbackContext, args);
} else if (action.equals("unenrollSecondAuthFactor")) {
this.unenrollSecondAuthFactor(callbackContext, args);
} else if (action.equals("setLanguageCode")) {
this.setLanguageCode(callbackContext, args);
} else if (action.equals("authenticateUserWithGoogle")) {
this.authenticateUserWithGoogle(callbackContext, args);
Expand Down Expand Up @@ -324,6 +329,8 @@ public boolean execute(String action, JSONArray args, CallbackContext callbackCo
this.updateUserEmail(callbackContext, args);
} else if (action.equals("sendUserEmailVerification")) {
this.sendUserEmailVerification(callbackContext, args);
} else if (action.equals("verifyBeforeUpdateEmail")) {
this.verifyBeforeUpdateEmail(callbackContext, args);
} else if (action.equals("updateUserPassword")) {
this.updateUserPassword(callbackContext, args);
} else if (action.equals("sendUserPasswordResetEmail")) {
Expand Down Expand Up @@ -1299,6 +1306,22 @@ public void run() {
});
}

public void verifyBeforeUpdateEmail(final CallbackContext callbackContext, final JSONArray args){
cordova.getThreadPool().execute(new Runnable() {
public void run() {
try {
if(!userNotSignedInError(callbackContext)) return;
FirebaseUser user = FirebaseAuth.getInstance().getCurrentUser();

String email = args.getString(0);
handleTaskOutcome(user.verifyBeforeUpdateEmail(email), callbackContext);
} catch (Exception e) {
handleExceptionWithContext(e, callbackContext);
}
}
});
}

public void updateUserPassword(final CallbackContext callbackContext, final JSONArray args){
cordova.getThreadPool().execute(new Runnable() {
public void run() {
Expand Down Expand Up @@ -1963,6 +1986,81 @@ public void onCodeSent(String verificationId, PhoneAuthProvider.ForceResendingTo
});
}

public void listEnrolledSecondAuthFactors(
final CallbackContext callbackContext,
final JSONArray args
) {

cordova.getThreadPool().execute(new Runnable() {
public void run() {
try {
if(!userNotSignedInError(callbackContext)) return;
FirebaseUser user = FirebaseAuth.getInstance().getCurrentUser();

JSONArray secondFactors = parseEnrolledSecondFactorsToJson(user.getMultiFactor().getEnrolledFactors());
callbackContext.success(secondFactors);
} catch (Exception e) {
handleExceptionWithContext(e, callbackContext);
}
}
});
}

private JSONArray parseEnrolledSecondFactorsToJson(List<MultiFactorInfo> multiFactorInfoList) throws JSONException {
JSONArray secondFactors = new JSONArray();
for(int i=0; i<multiFactorInfoList.size(); i++){
JSONObject secondFactor = new JSONObject();
secondFactor.put("index", i);

PhoneMultiFactorInfo phoneMultiFactorInfo = (PhoneMultiFactorInfo) multiFactorInfoList.get(i);
secondFactor.put("phoneNumber", phoneMultiFactorInfo.getPhoneNumber());

String displayName = phoneMultiFactorInfo.getDisplayName();
if(displayName != null){
secondFactor.put("displayName", displayName);
}
secondFactors.put(secondFactor);
}
return secondFactors;
}

public void unenrollSecondAuthFactor(
final CallbackContext callbackContext,
final JSONArray args
) {
cordova.getThreadPool().execute(new Runnable() {
public void run() {
try {
if(!userNotSignedInError(callbackContext)) return;
FirebaseUser user = FirebaseAuth.getInstance().getCurrentUser();

int selectedIndex = args.getInt(0);

if(selectedIndex < 0){
callbackContext.error("Selected index value ("+selectedIndex+") must be a positive integer");
return;
}

List<MultiFactorInfo> multiFactorInfos = user.getMultiFactor().getEnrolledFactors();
if(selectedIndex+1 > multiFactorInfos.size()){
callbackContext.error("Selected index value ("+selectedIndex+") exceeds the number of enrolled factors ("+multiFactorInfos.size()+")");
return;
}

user.getMultiFactor().unenroll(multiFactorInfos.get(selectedIndex)).addOnCompleteListener(task -> {
try {
handleTaskOutcome(task, callbackContext);
} catch(Exception e){
handleExceptionWithContext(e, callbackContext);
}
});
} catch (Exception e) {
handleExceptionWithContext(e, callbackContext);
}
}
});
}

public void setLanguageCode(final CallbackContext callbackContext, final JSONArray args){
cordova.getThreadPool().execute(new Runnable() {
public void run() {
Expand Down Expand Up @@ -3450,20 +3548,7 @@ private void handleAuthTaskOutcome(@NonNull Task<AuthResult> task, CallbackConte
// The user is a multi-factor user. Second factor challenge is required.
multiFactorResolver = ((FirebaseAuthMultiFactorException) task.getException()).getResolver();
String errMessage = "Second factor required";
JSONArray secondFactors = new JSONArray();
for(int i=0; i<multiFactorResolver.getHints().size(); i++){
JSONObject secondFactor = new JSONObject();
secondFactor.put("index", i);

PhoneMultiFactorInfo info = (PhoneMultiFactorInfo) multiFactorResolver.getHints().get(i);
secondFactor.put("phoneNumber", info.getPhoneNumber());

String displayName = info.getDisplayName();
if(displayName != null){
secondFactor.put("displayName", displayName);
}
secondFactors.put(secondFactor);
}
JSONArray secondFactors = parseEnrolledSecondFactorsToJson(multiFactorResolver.getHints());

// Invoke error callback with second factors
// App should ask user to choose if more than one
Expand Down
14 changes: 14 additions & 0 deletions types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,15 @@ export interface FirebasePlugin {
requireSmsValidation: boolean
},
): void
unenrollSecondAuthFactor(
success: () => void,
error: (err: string) => void,
selectedIndex: number
): void
listEnrolledSecondAuthFactors(
success: (secondFactors: [object]) => void,
error: (err: string) => void
): void
setLanguageCode(
lang: string,
success?: () => void,
Expand Down Expand Up @@ -285,6 +294,11 @@ export interface FirebasePlugin {
success?: () => void,
error?: (err: string) => void
): void
verifyBeforeUpdateEmail(
email: string,
success?: () => void,
error?: (err: string) => void
): void
sendUserEmailVerification(
actionCodeSettings?: {
handleCodeInApp?: boolean,
Expand Down
13 changes: 13 additions & 0 deletions www/firebase.js
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,14 @@ exports.verifySecondAuthFactor = function (success, error, params, opts) {
exec(success, error, "FirebasePlugin", "verifySecondAuthFactor", [params, opts]);
};

exports.listEnrolledSecondAuthFactors = function (success, error) {
exec(success, error, "FirebasePlugin", "listEnrolledSecondAuthFactors", []);
};

exports.unenrollSecondAuthFactor = function (success, error, selectedIndex) {
exec(success, error, "FirebasePlugin", "unenrollSecondAuthFactor", [selectedIndex]);
};

exports.setLanguageCode = function (lang, success, error) {
exec(success, error, "FirebasePlugin", "setLanguageCode", [lang]);
};
Expand Down Expand Up @@ -376,6 +384,11 @@ exports.sendUserEmailVerification = function (actionCodeSettings, success, error
exec(success, error, "FirebasePlugin", "sendUserEmailVerification", [actionCodeSettings]);
};

exports.verifyBeforeUpdateEmail = function (email, success, error) {
if(typeof email !== 'string' || !email) return error("'email' must be a valid email address");
exec(success, error, "FirebasePlugin", "verifyBeforeUpdateEmail", [email]);
};

exports.updateUserPassword = function (password, success, error) {
if(typeof password !== 'string' || !password) return error("'password' must be a valid string");
exec(success, error, "FirebasePlugin", "updateUserPassword", [password]);
Expand Down

0 comments on commit 7dea9bc

Please sign in to comment.