diff --git a/README.md b/README.md index 1ad5352e5..9820a3fe0 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,9 @@ +commercebuild install +mvn clean install -DskipTests -Dmaven.javadoc.skip=true -Dcheckstyle.skip=true +commercebuild deploy +mvn deploy -B -DskipTests -Dmaven.javadoc.skip=true -Dcheckstyle.skip=true + + [![Build Status](https://github.com/firebase/firebase-admin-java/workflows/Continuous%20Integration/badge.svg)](https://github.com/firebase/firebase-admin-java/actions) [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.google.firebase/firebase-admin/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.google.firebase/firebase-admin) [![Javadoc](https://javadoc-badge.appspot.com/com.google.firebase/firebase-admin.svg)](https://firebase.google.com/docs/reference/admin/java/reference/packages) diff --git a/pom.xml b/pom.xml index 3763023f4..84b3cb5c5 100644 --- a/pom.xml +++ b/pom.xml @@ -17,7 +17,7 @@ 4.0.0 - com.google.firebase + com.commercebuild.firebase firebase-admin 9.1.1 jar @@ -70,9 +70,15 @@ + + platform-release + platform-release + https://nexus.dev-mysagestore.com/repository/platform-release + - ossrh - https://oss.sonatype.org/content/repositories/snapshots + platform-snapshot + platform-snapshot + https://nexus.dev-mysagestore.com/repository/platform-snapshot @@ -348,18 +354,6 @@ - - - org.sonatype.plugins - nexus-staging-maven-plugin - 1.6.13 - true - - ossrh - https://oss.sonatype.org/ - true - - diff --git a/src/main/java/com/google/firebase/auth/FirebaseAuth.java b/src/main/java/com/google/firebase/auth/FirebaseAuth.java index 27e79960d..2df49c87d 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseAuth.java +++ b/src/main/java/com/google/firebase/auth/FirebaseAuth.java @@ -91,6 +91,24 @@ static Builder builder() { return new Builder(); } + public void addAuthorizedDomain(String domain) throws FirebaseAuthException { + this + .getUserManager() + .addAuthorizedDomain(domain); + } + + public void verifyDomain(String domain) throws FirebaseAuthException { + this + .getUserManager() + .verifyDomain(domain); + } + + public void applyDomain(String domain) throws FirebaseAuthException { + this + .getUserManager() + .applyDomain(domain); + } + static class Builder extends AbstractFirebaseAuth.Builder { private Supplier tenantManager; @@ -107,8 +125,9 @@ public Builder setTenantManager(Supplier tenantManager) { return this; } - public FirebaseAuth build() { - return new FirebaseAuth(this); - } + public FirebaseAuth build() { + return new FirebaseAuth(this); + } } + } diff --git a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java index 195527841..41bd7ac8c 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java +++ b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java @@ -46,7 +46,10 @@ import com.google.firebase.internal.HttpRequestInfo; import com.google.firebase.internal.NonNull; import com.google.firebase.internal.Nullable; + +import java.util.ArrayList; import java.util.Collection; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -73,11 +76,16 @@ final class FirebaseUserManager { private static final String ID_TOOLKIT_URL = "https://identitytoolkit.googleapis.com/%s/projects/%s"; + + private static final String ID_TOOLKIT_ADMIN_URL = + "https://identitytoolkit.googleapis.com/admin/%s/projects/%s"; + private static final String ID_TOOLKIT_URL_EMULATOR = "http://%s/identitytoolkit.googleapis.com/%s/projects/%s"; private final String userMgtBaseUrl; private final String idpConfigMgtBaseUrl; + private final String adminUrl; private final JsonFactory jsonFactory; private final AuthHttpClient httpClient; @@ -99,7 +107,7 @@ private FirebaseUserManager(Builder builder) { this.userMgtBaseUrl = idToolkitUrlV1 + "/tenants/" + tenantId; this.idpConfigMgtBaseUrl = idToolkitUrlV2 + "/tenants/" + tenantId; } - + this.adminUrl = getIdToolkitAdminUrl(projectId, "v2"); this.httpClient = new AuthHttpClient(jsonFactory, builder.requestFactory); } @@ -110,6 +118,10 @@ private String getIdToolkitUrl(String projectId, String version) { return String.format(ID_TOOLKIT_URL, version, projectId); } + private String getIdToolkitAdminUrl(String projectId, String version) { + return String.format(ID_TOOLKIT_ADMIN_URL, version, projectId); + } + @VisibleForTesting void setInterceptor(HttpResponseInterceptor interceptor) { httpClient.setInterceptor(interceptor); @@ -292,6 +304,29 @@ SamlProviderConfig getSamlProviderConfig(String providerId) throws FirebaseAuthE return httpClient.sendRequest(HttpRequestInfo.buildGetRequest(url), SamlProviderConfig.class); } + void addAuthorizedDomain(String domain) throws FirebaseAuthException { + String url = idpConfigMgtBaseUrl + "/config"; + HashMap map = httpClient.sendRequest(HttpRequestInfo.buildGetRequest(url), HashMap.class); + ((ArrayList)map.get("authorizedDomains")).add(domain); + HttpRequestInfo requestInfo = HttpRequestInfo.buildJsonPatchRequest(url, map) + .addParameter("updateMask", Joiner.on(",").join(AuthHttpClient.generateMask(map))); + httpClient.sendRequest(requestInfo, Map.class); + } + + void verifyDomain(String domain) throws FirebaseAuthException { + String url = adminUrl + "/domain:verify"; + HttpRequestInfo requestInfo = HttpRequestInfo.buildJsonPostRequest(url, Map.of("domain", domain, "action", "VERIFY")); + + httpClient.sendRequest(requestInfo, Map.class); + } + + void applyDomain(String domain) throws FirebaseAuthException { + String url = adminUrl + "/domain:verify"; + HttpRequestInfo requestInfo = HttpRequestInfo.buildJsonPostRequest(url, Map.of("domain", domain, "action", "APPLY")); + + httpClient.sendRequest(requestInfo, Map.class); + } + ListOidcProviderConfigsResponse listOidcProviderConfigs(int maxResults, String pageToken) throws FirebaseAuthException { ImmutableMap.Builder builder = 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..207d8ae22 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/MultiFactorInfo.java @@ -0,0 +1,75 @@ +/* + * 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 { + + public enum MfaType { PHONE, TOTP } + + /** + * 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; + } + + public abstract MfaType getMfaType(); +} diff --git a/src/main/java/com/google/firebase/auth/MultiFactorInfoFactory.java b/src/main/java/com/google/firebase/auth/MultiFactorInfoFactory.java new file mode 100644 index 000000000..1eaccfed5 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/MultiFactorInfoFactory.java @@ -0,0 +1,14 @@ +package com.google.firebase.auth; + +import com.google.firebase.auth.internal.GetAccountInfoResponse; + +public class MultiFactorInfoFactory { + + + public MultiFactorInfo create(GetAccountInfoResponse.MultiFactorInfo multiFactorInfo) { + if (multiFactorInfo.getPhoneInfo() == null || multiFactorInfo.getPhoneInfo().isEmpty()){ + return new TotpMultiFactorInfo(multiFactorInfo); + } + return new PhoneMultiFactorInfo(multiFactorInfo); + } +} 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..59ffff56c --- /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 MultiFactorInfo[] enrolledFactors; + + public MultiFactorSettings(MultiFactorInfo[] enrolledFactors) { + this.enrolledFactors = enrolledFactors; + } + + public MultiFactorInfo[] 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..cc6191d69 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/PhoneMultiFactorInfo.java @@ -0,0 +1,52 @@ +/* + * 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(); + } + + @Override + public MfaType getMfaType() { + return MfaType.PHONE; + } + + public String getPhoneNumber() { + return phoneNumber; + } + + public String getUnobfuscatedPhoneNumber() { + return unobfuscatedPhoneNumber; + } +} diff --git a/src/main/java/com/google/firebase/auth/TotpMultiFactorInfo.java b/src/main/java/com/google/firebase/auth/TotpMultiFactorInfo.java new file mode 100644 index 000000000..1f882a195 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/TotpMultiFactorInfo.java @@ -0,0 +1,17 @@ +package com.google.firebase.auth; + +import com.google.firebase.auth.internal.GetAccountInfoResponse; + +public class TotpMultiFactorInfo extends MultiFactorInfo { + + + public TotpMultiFactorInfo(GetAccountInfoResponse.MultiFactorInfo response) { + super(response); + } + + public MfaType getMfaType() { + return MfaType.TOTP; + } + + +} diff --git a/src/main/java/com/google/firebase/auth/UserRecord.java b/src/main/java/com/google/firebase/auth/UserRecord.java index ac962c52d..e62aeb0fa 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 MultiFactorInfo[0]); + } else { + int mfaInfoLength = response.getMfaInfo().length; + MultiFactorInfo[] multiFactorInfos = new MultiFactorInfo[mfaInfoLength]; + for (int i = 0; i < multiFactorInfos.length; i++) { + multiFactorInfos[i] = new MultiFactorInfoFactory().create(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/main/java/com/google/firebase/auth/multitenancy/Tenant.java b/src/main/java/com/google/firebase/auth/multitenancy/Tenant.java index 57d215e96..14e1a2a13 100644 --- a/src/main/java/com/google/firebase/auth/multitenancy/Tenant.java +++ b/src/main/java/com/google/firebase/auth/multitenancy/Tenant.java @@ -22,6 +22,7 @@ import com.google.common.base.Strings; import com.google.common.collect.ImmutableMap; import java.util.HashMap; +import java.util.List; import java.util.Map; /** @@ -119,6 +120,19 @@ public CreateRequest setEmailLinkSignInEnabled(boolean emailLinkSignInEnabled) { return this; } + public CreateRequest setMFAEnabled() { + properties.put("mfaConfig", Map.of( + "state", "ENABLED", + "enabledProviders", List.of("PHONE_SMS"), + "providerConfigs", List.of(Map.of( + "state", "ENABLED", + "totpProviderConfig", Map.of("adjacentIntervals", 5) + )) + + )); + return this; + } + Map getProperties() { return ImmutableMap.copyOf(properties); } diff --git a/src/test/java/com/google/firebase/auth/UserRecordTest.java b/src/test/java/com/google/firebase/auth/UserRecordTest.java index 6d50f075c..2a700f757 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 = (PhoneMultiFactorInfo[]) 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(