diff --git a/src/main/java/com/google/firebase/auth/MultiFactorInfo.java b/src/main/java/com/google/firebase/auth/MultiFactorInfo.java new file mode 100644 index 000000000..78fa9d45f --- /dev/null +++ b/src/main/java/com/google/firebase/auth/MultiFactorInfo.java @@ -0,0 +1,71 @@ +/* + * Copyright 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.auth; + +import com.google.firebase.auth.internal.GetAccountInfoResponse; +import com.google.firebase.internal.Nullable; + +/** + * Interface representing the common properties of a user-enrolled second factor. + */ +public abstract class MultiFactorInfo { + + /** + * The ID of the enrolled second factor. This ID is unique to the user. + */ + private final String uid; + + /** + * The optional display name of the enrolled second factor. + */ + private final String displayName; + + /** + * The type identifier of the second factor. For SMS second factors, this is `phone`. + */ + private final String factorId; + + /** + * The optional date the second factor was enrolled, formatted as a UTC string. + */ + private final String enrollmentTime; + + MultiFactorInfo(GetAccountInfoResponse.MultiFactorInfo response) { + this.uid = response.getMfaEnrollmentId(); + this.displayName = response.getDisplayName(); + this.factorId = response.getFactorId(); + this.enrollmentTime = response.getEnrollmentTime(); + } + + public String getUid() { + return uid; + } + + @Nullable + public String getDisplayName() { + return displayName; + } + + public String getFactorId() { + return factorId; + } + + @Nullable + public String getEnrollmentTime() { + return enrollmentTime; + } +} diff --git a/src/main/java/com/google/firebase/auth/MultiFactorSettings.java b/src/main/java/com/google/firebase/auth/MultiFactorSettings.java new file mode 100644 index 000000000..96c06a402 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/MultiFactorSettings.java @@ -0,0 +1,33 @@ +/* + * Copyright 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.auth; + +/** + * The multi-factor related user settings. + */ +public class MultiFactorSettings { + + private final PhoneMultiFactorInfo[] enrolledFactors; + + public MultiFactorSettings(PhoneMultiFactorInfo[] enrolledFactors) { + this.enrolledFactors = enrolledFactors; + } + + public PhoneMultiFactorInfo[] getEnrolledFactors() { + return enrolledFactors; + } +} diff --git a/src/main/java/com/google/firebase/auth/PhoneMultiFactorInfo.java b/src/main/java/com/google/firebase/auth/PhoneMultiFactorInfo.java new file mode 100644 index 000000000..9c4b6b249 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/PhoneMultiFactorInfo.java @@ -0,0 +1,47 @@ +/* + * Copyright 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.auth; + +import com.google.firebase.auth.internal.GetAccountInfoResponse; + +/** + * Interface representing the common properties of a user-enrolled second factor. + */ +public class PhoneMultiFactorInfo extends MultiFactorInfo { + + /** + * The phone number associated with a phone second factor. + */ + private final String phoneNumber; + + private final String unobfuscatedPhoneNumber; + + public PhoneMultiFactorInfo(GetAccountInfoResponse.MultiFactorInfo response) { + super(response); + + this.phoneNumber = response.getPhoneInfo(); + this.unobfuscatedPhoneNumber = response.getUnobfuscatedPhoneInfo(); + } + + public String getPhoneNumber() { + return phoneNumber; + } + + public String getUnobfuscatedPhoneNumber() { + return unobfuscatedPhoneNumber; + } +} diff --git a/src/main/java/com/google/firebase/auth/UserRecord.java b/src/main/java/com/google/firebase/auth/UserRecord.java index ac962c52d..80ecec323 100644 --- a/src/main/java/com/google/firebase/auth/UserRecord.java +++ b/src/main/java/com/google/firebase/auth/UserRecord.java @@ -61,6 +61,7 @@ public class UserRecord implements UserInfo { private final long tokensValidAfterTimestamp; private final UserMetadata userMetadata; private final Map customClaims; + private final MultiFactorSettings multiFactor; UserRecord(User response, JsonFactory jsonFactory) { checkNotNull(response, "response must not be null"); @@ -82,6 +83,18 @@ public class UserRecord implements UserInfo { this.providers[i] = new ProviderUserInfo(response.getProviders()[i]); } } + + if (response.getMfaInfo() == null || response.getMfaInfo().length == 0) { + this.multiFactor = new MultiFactorSettings(new PhoneMultiFactorInfo[0]); + } else { + int mfaInfoLength = response.getMfaInfo().length; + PhoneMultiFactorInfo[] multiFactorInfos = new PhoneMultiFactorInfo[mfaInfoLength]; + for (int i = 0; i < multiFactorInfos.length; i++) { + multiFactorInfos[i] = new PhoneMultiFactorInfo(response.getMfaInfo()[i]); + } + this.multiFactor = new MultiFactorSettings(multiFactorInfos); + } + this.tokensValidAfterTimestamp = response.getValidSince() * 1000; String lastRefreshAtRfc3339 = response.getLastRefreshAt(); @@ -240,6 +253,13 @@ public Map getCustomClaims() { return customClaims; } + /** + * The multi-factor related properties for the current user, if available. + */ + public MultiFactorSettings getMultiFactor() { + return multiFactor; + } + /** * Returns a new {@link UpdateRequest}, which can be used to update the attributes * of this user. diff --git a/src/main/java/com/google/firebase/auth/internal/GetAccountInfoResponse.java b/src/main/java/com/google/firebase/auth/internal/GetAccountInfoResponse.java index 7bde3eb39..581a5582c 100644 --- a/src/main/java/com/google/firebase/auth/internal/GetAccountInfoResponse.java +++ b/src/main/java/com/google/firebase/auth/internal/GetAccountInfoResponse.java @@ -17,6 +17,7 @@ package com.google.firebase.auth.internal; import com.google.api.client.util.Key; + import java.util.List; /** @@ -85,6 +86,9 @@ public static class User { @Key("customAttributes") private String customClaims; + @Key("mfaInfo") + private MultiFactorInfo[] mfaInfo; + public String getUid() { return uid; } @@ -140,6 +144,10 @@ public long getValidSince() { public String getCustomClaims() { return customClaims; } + + public MultiFactorInfo[] getMfaInfo() { + return mfaInfo; + } } /** @@ -189,4 +197,71 @@ public String getProviderId() { return providerId; } } + + /** + * JSON data binding for multi factor info data. + */ + public static final class MultiFactorInfo { + /** + * The ID of the enrolled second factor. This ID is unique to the user. + */ + @Key("mfaEnrollmentId") + private String mfaEnrollmentId; + + /** + * The optional display name of the enrolled second factor. + */ + @Key("displayName") + private String displayName; + + /** + * The type identifier of the second factor. For SMS second factors, this is `phone`. + */ + @Key("factorId") + private String factorId; + + /** + * The optional date the second factor was enrolled, formatted as a UTC string. + */ + @Key("enrollmentTime") + private String enrollmentTime; + + /** + * Normally this will show the phone number associated with this enrollment. + * In some situations, such as after a first factor sign in, + * it will only show the obfuscated version of the associated phone number. + */ + @Key("phoneInfo") + private String phoneInfo; + + /** + * Unobfuscated phoneInfo. + */ + @Key("unobfuscatedPhoneInfo") + private String unobfuscatedPhoneInfo; + + public String getMfaEnrollmentId() { + return mfaEnrollmentId; + } + + public String getDisplayName() { + return displayName; + } + + public String getFactorId() { + return factorId; + } + + public String getEnrollmentTime() { + return enrollmentTime; + } + + public String getPhoneInfo() { + return phoneInfo; + } + + public String getUnobfuscatedPhoneInfo() { + return unobfuscatedPhoneInfo; + } + } } diff --git a/src/test/java/com/google/firebase/auth/UserRecordTest.java b/src/test/java/com/google/firebase/auth/UserRecordTest.java index 6d50f075c..1948ed16b 100644 --- a/src/test/java/com/google/firebase/auth/UserRecordTest.java +++ b/src/test/java/com/google/firebase/auth/UserRecordTest.java @@ -2,6 +2,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.fail; @@ -17,6 +18,7 @@ import java.io.IOException; import java.io.InputStream; import java.nio.charset.Charset; + import org.junit.Test; public class UserRecordTest { @@ -92,7 +94,7 @@ public void testAllProviderInfo() throws IOException { .put("photoUrl", "http://photo.url") .put("providerId", "providerId") .build() - ) + ) ); String json = JSON_FACTORY.toString(resp); UserRecord userRecord = parseUser(json); @@ -108,6 +110,37 @@ public void testAllProviderInfo() throws IOException { } } + @Test + public void testPhoneMultiFactors() throws IOException { + ImmutableMap resp = ImmutableMap.of( + "localId", "user", + "mfaInfo", ImmutableList.of( + ImmutableMap.builder() + .put("mfaEnrollmentId", "53HG4HG45HG8G04GJ40J4G3J") + .put("displayName", "Display Name") + .put("factorId", "phone") + .put("enrollmentTime", "Fri, 22 Sep 2017 01:49:58 GMT") + .put("phoneInfo", "+16505551234") + .build() + ) + ); + String json = JSON_FACTORY.toString(resp); + UserRecord userRecord = parseUser(json); + assertEquals("user", userRecord.getUid()); + + assertNotNull(userRecord.getMultiFactor()); + PhoneMultiFactorInfo[] enrolledFactors = userRecord.getMultiFactor().getEnrolledFactors(); + assertEquals(1, enrolledFactors.length); + for (PhoneMultiFactorInfo multiFactorInfo : enrolledFactors) { + assertEquals("53HG4HG45HG8G04GJ40J4G3J", multiFactorInfo.getUid()); + assertEquals("Display Name", multiFactorInfo.getDisplayName()); + assertEquals("phone", multiFactorInfo.getFactorId()); + assertEquals("Fri, 22 Sep 2017 01:49:58 GMT", multiFactorInfo.getEnrollmentTime()); + assertEquals("+16505551234", multiFactorInfo.getPhoneNumber()); + assertNull(multiFactorInfo.getUnobfuscatedPhoneNumber()); + } + } + @Test public void testUserMetadata() throws IOException { ImmutableMap resp = ImmutableMap.of(