From a039e051c9433eb26eafbde5c152fdcb5a4b80d9 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Mon, 10 Feb 2020 11:16:50 -0800 Subject: [PATCH 001/354] fix: Enabled automatic HTTP retries for FirebaseProjectManagement (#356) * Enabled automatic HTTP retries for FirebaseProjectManagement * Added some test cases * Added helper function for disabling exponential backoff during tests * Added util class for simplifying retry tests * Simplified test cases * Removed unused method --- .../firebase/internal/ApiClientUtils.java | 18 +++- .../FirebaseProjectManagementServiceImpl.java | 25 +++-- .../firebase/internal/ApiClientUtilsTest.java | 13 +++ .../firebase/internal/TestApiClientUtils.java | 95 +++++++++++++++++++ ...ebaseProjectManagementServiceImplTest.java | 59 ++++++++++-- 5 files changed, 191 insertions(+), 19 deletions(-) create mode 100644 src/test/java/com/google/firebase/internal/TestApiClientUtils.java diff --git a/src/main/java/com/google/firebase/internal/ApiClientUtils.java b/src/main/java/com/google/firebase/internal/ApiClientUtils.java index 7506ff8ee..36ccf5cc8 100644 --- a/src/main/java/com/google/firebase/internal/ApiClientUtils.java +++ b/src/main/java/com/google/firebase/internal/ApiClientUtils.java @@ -29,7 +29,7 @@ */ public class ApiClientUtils { - private static final RetryConfig DEFAULT_RETRY_CONFIG = RetryConfig.builder() + static final RetryConfig DEFAULT_RETRY_CONFIG = RetryConfig.builder() .setMaxRetries(4) .setRetryStatusCodes(ImmutableList.of(500, 503)) .setMaxIntervalMillis(60 * 1000) @@ -43,9 +43,21 @@ public class ApiClientUtils { * @return A new {@code HttpRequestFactory} instance. */ public static HttpRequestFactory newAuthorizedRequestFactory(FirebaseApp app) { + return newAuthorizedRequestFactory(app, DEFAULT_RETRY_CONFIG); + } + + /** + * Creates a new {@code HttpRequestFactory} which provides authorization (OAuth2), timeouts and + * automatic retries. + * + * @param app {@link FirebaseApp} from which to obtain authorization credentials. + * @param retryConfig {@link RetryConfig} instance or null to disable retries. + * @return A new {@code HttpRequestFactory} instance. + */ + public static HttpRequestFactory newAuthorizedRequestFactory( + FirebaseApp app, @Nullable RetryConfig retryConfig) { HttpTransport transport = app.getOptions().getHttpTransport(); - return transport.createRequestFactory( - new FirebaseRequestInitializer(app, DEFAULT_RETRY_CONFIG)); + return transport.createRequestFactory(new FirebaseRequestInitializer(app, retryConfig)); } public static HttpRequestFactory newUnauthorizedRequestFactory(FirebaseApp app) { diff --git a/src/main/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImpl.java b/src/main/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImpl.java index 72c570c6d..8abced696 100644 --- a/src/main/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImpl.java +++ b/src/main/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImpl.java @@ -18,6 +18,7 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; +import com.google.api.client.http.HttpRequestFactory; import com.google.api.client.http.HttpResponseInterceptor; import com.google.api.client.util.Base64; import com.google.api.client.util.Key; @@ -32,8 +33,8 @@ import com.google.common.collect.ImmutableMap; import com.google.firebase.FirebaseApp; import com.google.firebase.ImplFirebaseTrampolines; +import com.google.firebase.internal.ApiClientUtils; import com.google.firebase.internal.CallableOperation; -import com.google.firebase.internal.FirebaseRequestInitializer; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; @@ -55,6 +56,7 @@ class FirebaseProjectManagementServiceImpl implements AndroidAppService, IosAppS private final FirebaseApp app; private final Sleeper sleeper; private final Scheduler scheduler; + private final HttpRequestFactory requestFactory; private final HttpHelper httpHelper; private final CreateAndroidAppFromAppIdFunction createAndroidAppFromAppIdFunction = @@ -63,17 +65,26 @@ class FirebaseProjectManagementServiceImpl implements AndroidAppService, IosAppS new CreateIosAppFromAppIdFunction(); FirebaseProjectManagementServiceImpl(FirebaseApp app) { - this(app, Sleeper.DEFAULT, new FirebaseAppScheduler(app)); + this( + app, + Sleeper.DEFAULT, + new FirebaseAppScheduler(app), + ApiClientUtils.newAuthorizedRequestFactory(app)); } - FirebaseProjectManagementServiceImpl(FirebaseApp app, Sleeper sleeper, Scheduler scheduler) { + @VisibleForTesting + FirebaseProjectManagementServiceImpl( + FirebaseApp app, Sleeper sleeper, Scheduler scheduler, HttpRequestFactory requestFactory) { this.app = checkNotNull(app); this.sleeper = checkNotNull(sleeper); this.scheduler = checkNotNull(scheduler); - this.httpHelper = new HttpHelper( - app.getOptions().getJsonFactory(), - app.getOptions().getHttpTransport().createRequestFactory( - new FirebaseRequestInitializer(app))); + this.requestFactory = checkNotNull(requestFactory); + this.httpHelper = new HttpHelper(app.getOptions().getJsonFactory(), requestFactory); + } + + @VisibleForTesting + HttpRequestFactory getRequestFactory() { + return requestFactory; } @VisibleForTesting diff --git a/src/test/java/com/google/firebase/internal/ApiClientUtilsTest.java b/src/test/java/com/google/firebase/internal/ApiClientUtilsTest.java index 78eabaf32..c19f5f567 100644 --- a/src/test/java/com/google/firebase/internal/ApiClientUtilsTest.java +++ b/src/test/java/com/google/firebase/internal/ApiClientUtilsTest.java @@ -69,6 +69,19 @@ public void testAuthorizedHttpClient() throws IOException { assertEquals(retryConfig.getRetryStatusCodes(), ImmutableList.of(500, 503)); } + @Test + public void testAuthorizedHttpClientWithoutRetry() throws IOException { + FirebaseApp app = FirebaseApp.initializeApp(TEST_OPTIONS); + + HttpRequestFactory requestFactory = ApiClientUtils.newAuthorizedRequestFactory(app, null); + + assertTrue(requestFactory.getInitializer() instanceof FirebaseRequestInitializer); + HttpRequest request = requestFactory.buildGetRequest(TEST_URL); + assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); + HttpUnsuccessfulResponseHandler retryHandler = request.getUnsuccessfulResponseHandler(); + assertFalse(retryHandler instanceof RetryHandlerDecorator); + } + @Test public void testUnauthorizedHttpClient() throws IOException { FirebaseApp app = FirebaseApp.initializeApp(TEST_OPTIONS); diff --git a/src/test/java/com/google/firebase/internal/TestApiClientUtils.java b/src/test/java/com/google/firebase/internal/TestApiClientUtils.java new file mode 100644 index 000000000..9f4bc2d09 --- /dev/null +++ b/src/test/java/com/google/firebase/internal/TestApiClientUtils.java @@ -0,0 +1,95 @@ +/* + * Copyright 2020 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.internal; + +import static com.google.firebase.internal.ApiClientUtils.DEFAULT_RETRY_CONFIG; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import com.google.api.client.http.GenericUrl; +import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.HttpRequestFactory; +import com.google.api.client.http.HttpUnsuccessfulResponseHandler; +import com.google.api.client.testing.util.MockSleeper; +import com.google.firebase.FirebaseApp; +import com.google.firebase.internal.RetryInitializer.RetryHandlerDecorator; +import java.io.IOException; + +public class TestApiClientUtils { + + private static final RetryConfig TEST_RETRY_CONFIG = RetryConfig.builder() + .setMaxRetries(DEFAULT_RETRY_CONFIG.getMaxRetries()) + .setRetryStatusCodes(DEFAULT_RETRY_CONFIG.getRetryStatusCodes()) + .setMaxIntervalMillis(DEFAULT_RETRY_CONFIG.getMaxIntervalMillis()) + .setSleeper(new MockSleeper()) + .build(); + + private static final GenericUrl TEST_URL = new GenericUrl("https://firebase.google.com"); + + /** + * Creates a new {@code HttpRequestFactory} which provides authorization (OAuth2), timeouts and + * automatic retries. Bypasses exponential backoff between consecutive retries for faster + * execution during tests. + * + * @param app {@link FirebaseApp} from which to obtain authorization credentials. + * @return A new {@code HttpRequestFactory} instance. + */ + public static HttpRequestFactory delayBypassedRequestFactory(FirebaseApp app) { + return ApiClientUtils.newAuthorizedRequestFactory(app, TEST_RETRY_CONFIG); + } + + /** + * Creates a new {@code HttpRequestFactory} which provides authorization (OAuth2), timeouts but + * no retries. + * + * @param app {@link FirebaseApp} from which to obtain authorization credentials. + * @return A new {@code HttpRequestFactory} instance. + */ + public static HttpRequestFactory retryDisabledRequestFactory(FirebaseApp app) { + return ApiClientUtils.newAuthorizedRequestFactory(app, null); + } + + /** + * Checks whther the given HttpRequestFactory has been configured for authorization and + * automatic retries. + * + * @param requestFactory The HttpRequestFactory to check. + */ + public static void assertAuthAndRetrySupport(HttpRequestFactory requestFactory) { + assertTrue(requestFactory.getInitializer() instanceof FirebaseRequestInitializer); + HttpRequest request; + try { + request = requestFactory.buildGetRequest(TEST_URL); + } catch (IOException e) { + throw new RuntimeException("Failed to initialize request", e); + } + + // Verify authorization + assertTrue(request.getHeaders().getAuthorization().startsWith("Bearer ")); + + // Verify retry support + HttpUnsuccessfulResponseHandler retryHandler = request.getUnsuccessfulResponseHandler(); + assertTrue(retryHandler instanceof RetryHandlerDecorator); + RetryConfig retryConfig = ((RetryHandlerDecorator) retryHandler).getRetryHandler() + .getRetryConfig(); + assertEquals(DEFAULT_RETRY_CONFIG.getMaxRetries(), retryConfig.getMaxRetries()); + assertEquals(DEFAULT_RETRY_CONFIG.getMaxIntervalMillis(), retryConfig.getMaxIntervalMillis()); + assertFalse(retryConfig.isRetryOnIOExceptions()); + assertEquals(DEFAULT_RETRY_CONFIG.getRetryStatusCodes(), retryConfig.getRetryStatusCodes()); + } +} diff --git a/src/test/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImplTest.java b/src/test/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImplTest.java index a2fe8728d..87afe609c 100644 --- a/src/test/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImplTest.java +++ b/src/test/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImplTest.java @@ -29,6 +29,7 @@ import com.google.api.client.googleapis.util.Utils; import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.HttpRequestFactory; import com.google.api.client.http.HttpResponse; import com.google.api.client.http.HttpResponseInterceptor; import com.google.api.client.json.JsonParser; @@ -43,6 +44,7 @@ import com.google.firebase.FirebaseOptions; import com.google.firebase.TestOnlyImplFirebaseTrampolines; import com.google.firebase.auth.MockGoogleCredentials; +import com.google.firebase.internal.TestApiClientUtils; import com.google.firebase.testing.MultiRequestMockHttpTransport; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -370,7 +372,7 @@ public void listIosAppsAsyncMultiplePages() throws Exception { MockLowLevelHttpResponse secondRpcResponse = new MockLowLevelHttpResponse(); secondRpcResponse.setContent(LIST_IOS_APPS_PAGE_2_RESPONSE); serviceImpl = initServiceImpl( - ImmutableList.of(firstRpcResponse, secondRpcResponse), + ImmutableList.of(firstRpcResponse, secondRpcResponse), interceptor); List iosAppList = serviceImpl.listIosAppsAsync(PROJECT_ID).get(); @@ -400,7 +402,7 @@ public void createIosApp() throws Exception { MockLowLevelHttpResponse thirdRpcResponse = new MockLowLevelHttpResponse(); thirdRpcResponse.setContent(CREATE_IOS_GET_OPERATION_ATTEMPT_2_RESPONSE); serviceImpl = initServiceImpl( - ImmutableList.of( + ImmutableList.of( firstRpcResponse, secondRpcResponse, thirdRpcResponse), interceptor); @@ -624,7 +626,7 @@ public void listAndroidAppsMultiplePages() throws Exception { MockLowLevelHttpResponse secondRpcResponse = new MockLowLevelHttpResponse(); secondRpcResponse.setContent(LIST_ANDROID_APPS_PAGE_2_RESPONSE); serviceImpl = initServiceImpl( - ImmutableList.of(firstRpcResponse, secondRpcResponse), + ImmutableList.of(firstRpcResponse, secondRpcResponse), interceptor); List androidAppList = serviceImpl.listAndroidApps(PROJECT_ID); @@ -652,7 +654,7 @@ public void listAndroidAppsAsyncMultiplePages() throws Exception { MockLowLevelHttpResponse secondRpcResponse = new MockLowLevelHttpResponse(); secondRpcResponse.setContent(LIST_ANDROID_APPS_PAGE_2_RESPONSE); serviceImpl = initServiceImpl( - ImmutableList.of(firstRpcResponse, secondRpcResponse), + ImmutableList.of(firstRpcResponse, secondRpcResponse), interceptor); List androidAppList = serviceImpl.listAndroidAppsAsync(PROJECT_ID).get(); @@ -682,7 +684,7 @@ public void createAndroidApp() throws Exception { MockLowLevelHttpResponse thirdRpcResponse = new MockLowLevelHttpResponse(); thirdRpcResponse.setContent(CREATE_ANDROID_GET_OPERATION_ATTEMPT_2_RESPONSE); serviceImpl = initServiceImpl( - ImmutableList.of( + ImmutableList.of( firstRpcResponse, secondRpcResponse, thirdRpcResponse), interceptor); @@ -714,7 +716,7 @@ public void createAndroidAppAsync() throws Exception { MockLowLevelHttpResponse thirdRpcResponse = new MockLowLevelHttpResponse(); thirdRpcResponse.setContent(CREATE_ANDROID_GET_OPERATION_ATTEMPT_2_RESPONSE); serviceImpl = initServiceImpl( - ImmutableList.of( + ImmutableList.of( firstRpcResponse, secondRpcResponse, thirdRpcResponse), interceptor); @@ -915,10 +917,48 @@ public void deleteShaCertificateAsync() throws Exception { checkRequestHeader(expectedUrl, HttpMethod.DELETE); } + @Test + public void testAuthAndRetriesSupport() { + FirebaseOptions options = new FirebaseOptions.Builder() + .setCredentials(new MockGoogleCredentials("test-token")) + .setProjectId(PROJECT_ID) + .build(); + FirebaseApp app = FirebaseApp.initializeApp(options); + + FirebaseProjectManagementServiceImpl serviceImpl = + new FirebaseProjectManagementServiceImpl(app); + + TestApiClientUtils.assertAuthAndRetrySupport(serviceImpl.getRequestFactory()); + } + + @Test + public void testHttpRetries() throws Exception { + List mockResponses = ImmutableList.of( + firstRpcResponse.setStatusCode(503).setContent("{}"), + new MockLowLevelHttpResponse().setContent("{}")); + MockHttpTransport transport = new MultiRequestMockHttpTransport(mockResponses); + FirebaseOptions options = new FirebaseOptions.Builder() + .setCredentials(new MockGoogleCredentials("test-token")) + .setProjectId(PROJECT_ID) + .setHttpTransport(transport) + .build(); + FirebaseApp app = FirebaseApp.initializeApp(options); + HttpRequestFactory requestFactory = TestApiClientUtils.delayBypassedRequestFactory(app); + FirebaseProjectManagementServiceImpl serviceImpl = new FirebaseProjectManagementServiceImpl( + app, new MockSleeper(), new MockScheduler(), requestFactory); + serviceImpl.setInterceptor(interceptor); + + serviceImpl.deleteShaCertificate(SHA1_RESOURCE_NAME); + + String expectedUrl = String.format( + "%s/v1beta1/%s", FIREBASE_PROJECT_MANAGEMENT_URL, SHA1_RESOURCE_NAME); + checkRequestHeader(expectedUrl, HttpMethod.DELETE); + } + private static FirebaseProjectManagementServiceImpl initServiceImpl( MockLowLevelHttpResponse mockResponse, MultiRequestTestResponseInterceptor interceptor) { - return initServiceImpl(ImmutableList.of(mockResponse), interceptor); + return initServiceImpl(ImmutableList.of(mockResponse), interceptor); } private static FirebaseProjectManagementServiceImpl initServiceImpl( @@ -931,8 +971,9 @@ private static FirebaseProjectManagementServiceImpl initServiceImpl( .setHttpTransport(transport) .build(); FirebaseApp app = FirebaseApp.initializeApp(options); - FirebaseProjectManagementServiceImpl serviceImpl = - new FirebaseProjectManagementServiceImpl(app, new MockSleeper(), new MockScheduler()); + HttpRequestFactory requestFactory = TestApiClientUtils.retryDisabledRequestFactory(app); + FirebaseProjectManagementServiceImpl serviceImpl = new FirebaseProjectManagementServiceImpl( + app, new MockSleeper(), new MockScheduler(), requestFactory); serviceImpl.setInterceptor(interceptor); return serviceImpl; } From c9648cfb5a1d6d789ffd2535331677bd6c6502b1 Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Tue, 11 Feb 2020 14:23:02 -0500 Subject: [PATCH 002/354] Staged Release 6.12.2 (#364) * [maven-release-plugin] prepare release v6.12.2 * [maven-release-plugin] prepare for next development iteration --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index e82439e48..665edc8a6 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ com.google.firebase firebase-admin - 6.12.2-SNAPSHOT + 6.12.3-SNAPSHOT jar firebase-admin From c40a69d04c73e158a0ea1e75b1ea7400c3544c92 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Tue, 18 Feb 2020 10:11:27 -0800 Subject: [PATCH 003/354] chore: Implementing GitHub Actions release workflow (#363) * Adding release workflow preliminaries * Fixed a syntax error * Uploading only the jar artifacts * Enabling integration tests * Using Maven lifecycle more effectively * Fixing indentation * Maven deployment configuration * Point the send tweet action to master * Setting all publish config as env variables * Fixed typo in tweet --- .github/resources/firebase.asc.gpg | Bin 0 -> 4056 bytes .../resources/integ-service-account.json.gpg | Bin 0 -> 1734 bytes .github/resources/settings.xml | 29 ++++ .github/scripts/generate_changelog.sh | 79 ++++++++++ .github/scripts/package_artifacts.sh | 36 +++++ .github/scripts/publish_artifacts.sh | 35 +++++ .github/scripts/publish_preflight_check.sh | 146 ++++++++++++++++++ .github/workflows/ci.yml | 24 ++- .github/workflows/release.yml | 129 ++++++++++++++++ pom.xml | 119 +++++++------- 10 files changed, 532 insertions(+), 65 deletions(-) create mode 100644 .github/resources/firebase.asc.gpg create mode 100644 .github/resources/integ-service-account.json.gpg create mode 100644 .github/resources/settings.xml create mode 100755 .github/scripts/generate_changelog.sh create mode 100755 .github/scripts/package_artifacts.sh create mode 100755 .github/scripts/publish_artifacts.sh create mode 100755 .github/scripts/publish_preflight_check.sh create mode 100644 .github/workflows/release.yml diff --git a/.github/resources/firebase.asc.gpg b/.github/resources/firebase.asc.gpg new file mode 100644 index 0000000000000000000000000000000000000000..a946776c7fd1498a0399836a94785dc8fd5293ed GIT binary patch literal 4056 zcmV;}4=3=94Fm}T0y&=z%W?c1O8?U90o%rSLxzz3Cs`6<0^{*c`lR7N#ikp)!>DCQ z2so>P8Xz5P`%krULZ%hwWtW!=gEFCn@5ZoJkLvVsjQj?_gTijiyjQd;s;+?fEMEIl zCW_+-tHe1(pA!HtoTeq|lxrM{iU@)90oap)GEIqUB-CIKkMVwgc7A5iMm|VXlX9c;YSx z3RIqP!LqF6ofr8ell_En&(EMvLLs1qA4|f=kV=jbPQ76z_u0iIsBI((a|_S(VY-`*o&I+#7og2mvd1bVY^0 zA*7F*e;zlI^~iUzzhHPPLG^UG8LnIYHnC=D_HwMyZ8+%J>EEXHy*$CCin!zIgn)r& zeHOTG)WpHuyy3PDf7ogl1~I`lrUe)%qVtb$eDdDeZ@A8iku6Tjet@7P)bfU7#guou zNb?bve9@1LDToX%)Ev253ot{ZD&&1G#`Y`jLmP#YO>Q=2tLod!NV>DwuooOnEEYwd z%wu*~D`sKW5)K2S;}>U+y_=@mAnxRQ6opOoWzm#mRSppcyD!tUjZM|;Px{@dP zBG{0!Sh6P!W{N>7`Iv7y{~m4XVvzfBP=^b<7s5>mK*)n!RHgfXGw_f31aJ&4N^sd} zKi}q$0l3trF9dJx;9_Px#Vj?A_#0mF_z0|j*R$(DO7Xr zZpNqdnM~+ZY>e@$;S(kIV-V^a@J8BL)H&)4@&9HzfXT=Il+j@CSI@?zoDiESCgN>^ z#pHteO6U&MCvWmwSq8|;RRUvm<3^0BE|>K0i~@WuuaYpOz&I*9xh8}|@dg%V&CF|L zjFb)OOOe8-KaVW=c#S`Ss}RlrOZ?#O*e$Zfq;e%ryTPGShAnu~lCvuHZMrihx7xlx zOldQSRd0@j9j--aCzT7^QKEl=V$5g~I@@Wwp>jI3>!f7QuG#g>wSdkhT2p?%dzVVg5laj~V|q>A|JFUL}b98ib12!uE-b?Bau8(pUW}a91l>SVGGx;XOV)6025jq&C z=twVclr4(}0I?N&^aaVxtCBQCR))pC@EqriFb%+-DI5CrhP_PhAa9Y7I)@w zAM_59o0Qvqd3}Z+V6=o`M>WWVXE+&!?nF zLvnv8yA!_8okUw9?3-ONUs2IYTt}$V2|G^fgO<7OMVu!PXI9%Et z5s1`~8iy6(0=AAfa8|uITcKp490(SSVaum>4=Jh>ronEnW>OrY9wJ!mAe9Nte2*on?U zT?a*nuNb5(!8XBRAX&Flbv`2-6JPU)+QZd~>Y47db7Jx#eK<&zabmE98=cxKS(4eH z`~LxwSvU7Ve(gS#jSZH>(QfE86u7?#JTl6+$H~qAfD&Azhw=VtmZGh1f}l5yRriKO zX3M|cmHms6?b7brMTiPh)v#(~tj$p{+VI59Ahkf0I%tVP+Xj?1R=dnULjCiSmB;Cc zzX@;6nt{_4HjG2s=ocb|Z(~geFdCvH!qGK3{H6$Tt;%08lj+{Xb`OMw=DGkjlV{h_ zx9iiwLi3&5l>!iJ$*7<_?7dQnf`W4u`o2_78XQrmKSF^Z!L(q$2Cx*)8^bN}x$4h2 zPc!gA|Ee6=3VN*$WfOlPn2oiNcVVv4l}{Lag$6_g#i{+wc3b{JPE})$0Ls`Ly8NKx zaLzE8Ox77z1_sYV%zeTx%^~OcbNhtxQXGc!?+7{iV6Y1^7Gz;g&;d;w>0lgUe22E~ zroyyWSlW?m69Sv_H8w6(} z+xj5L1e3|mf@0}hxL)4wM&~%rV~a8p(g*_onDrC7%H3ckPbodKsE&y}hUW^iPB0YS zKQVl&_8p>?Iz|#3+QB^u4(nN<>I+5Os+4wTYOmAEP0i5wS%O$RFCHYuBN(C%R6Hs; zOe)l~$Ku1MN0fcI8&0cra9jplsOyUl3?+lOylU)YyUv8f2az|~#pXg{P<%~TSPV&% zyM^15lsNqI$ClcwXIjr;vmG5Alzg23T>^XwXFP=lXA|xipmA^L=c_Yb5tug$9cuS$ z25^~Q8azMB>oq7o#U-|4SIq)>HQp2Iy!uc>s#*_X71mG*Qm32k%8h<9w0}0LhcGd3 zxYyQGK+!}J1_V$Y>EobOVhcOY!xcBR5CA}j2WC-@<=w9j)-`uUaj#DLF!_HznM#Lz zY%1K9k61k8JS!~W!OCJ@!r+fds8CMcc^4VVHOp*umI1e0un{e=lkW@3-8A6{H$63bSEi^yFI;=q3{WtI|mI_}hmX5=8hD3!XCA4jA)ty$VeRhT!cfcTUV+ zK3XpIA(E-?V%<@jAurSB3lw2Ip%BO>p|Tu2=1Eez7S{=0wQmt8Yv{KmyNz(#8^qlq zW7rZ}3i8jb5eukdrQv0!PQOcOhev=~y-T+%1Mv+VJr2tJ7Mu$PMenudb8iF(prngJ zIEuBG;~hkP?>IDcTO@3vUC#;HCv$Sv!VdM#5&0>P-v&(K2e2Hzx{K*a>fXmcR1wqe3Z8{5+CFGDx5l!PvCe#wMNn%eApGB0;`?EY{JZHY`28~L zB=22wfOTv01{hJNXn8(G!PWNTuNq|dB)6xI$2^`NHro7vTmPmbFM<^KefX_TSPTg; zl*h&qo-dZ+$%a5yUNi+tbm(|zu={)Do3<1A4CNiurj6(It`Frh*sGKl=1UQN^^E^I z9FAjNimgI(YM*F+B6aANlQ&8Z9gXhok+j^{6zSo(cN8!i{KS9X+s{!r#x^7@kbth) z$+b5ul8vGL{J!_#OB^uHd&4K3dBk7rB~J$)fo;OFm%@aVAGE(ZPCZ1PrH(KtMo1~t z+Y9X5?o;j~Z5MN=C*A$7-a(UHc|fPyuFh^e!{~wJAJ9AzO;jj^hX#tL2Y@Me2++(m z){lC!c;^egc?B^a)Fla4>M3YTZ#5b=&OciVJCQDF<h~nMeWb9n*u6nmnT!UW{+K-Fpba>kTBsChG!vmLMk7CNGIo3=d-`YOBWZ@wT1{K^ zD0*+k1^Uy=_fL1&o()tA>K3E944{K;J8f6c=M9sXM?|H>?rUP&o0@7=QYB5|+Q{s^ zu4p?+s8d$@zcO6->L*;nGJXL_Y3Sv^_yF z=G;x4p%gDB0zUvhxk#G?iGD8Yk5dy6yGI5VlHX&d@l4ebjr;x8?iHtp&U%4@ZLjf~ zt%J+l3NK6v&&7VEwTV&Ely+pvh~5-Cs&qAo&5S*YA8?Jj?X}*PKE{0@b!=_Sg2^Ux zY{w0(nR)c$p3~SsxwI^&uCV}%z7oL$+ciBkJZ5aR0UA%cw-&;vy$|~_jFK`EQwP+( z4KwsI@xrjC-9h$FmJg-36Om%AG}fRW`H*+hfb)XOloW1Z3L7f{ub%JdpIJUgPA-bq6BG|q_Fs-h;T z>yDbjZIOs91!|4^+n`~dYA4JH+1LBHCNjabPA+o& z9XgjmGBm>#@WC!hDn9~uqaId;^A`Ny^~BNK8nF713ReS%2Grj5^oShCV@jF}dL?^s z*gf`7^qQ2G6bNwg(Sz&

z4r*r|VPWM6hN{&7xv$RQO}Zn7RnRZ!)V9sRB$hC;}* KX);zY-!1|?!O3F) literal 0 HcmV?d00001 diff --git a/.github/resources/integ-service-account.json.gpg b/.github/resources/integ-service-account.json.gpg new file mode 100644 index 0000000000000000000000000000000000000000..da547f44dff331d7c4b1e454ca1a230aef03622c GIT binary patch literal 1734 zcmV;%208hR4Fm}T0_WqJMLyD96#vrd0kSseMv-_5+A>|dXkh)Lxg1WlZ_;HhFTh#x zDXB$vd955)y_i6fJ)Oj)aBCs+sb^rD8ckG^9BEnrFn(oz#yfzS@$+cn3lx!+B3v>8 z+oZYA$+d_~HR_?8Y|47V&47`mfrj&cS28OWOy@;W6RABVdPnTBDiQppT7W_J!vZ~k zJU+n4wlc$u!0!vj)k)conSKAdiLj5M4~3gTnMvFiA3OAaxG9$-Fi4Hz8zrK}UW+wP zy2KkBMs*nJt{;S3op3>^&flbtsElr~I!hsD^U2aEDCp+ScE}*!0)iJIYxI~0rz0!! z$*UJ{b*NbH9yB>mQVu(OxgZzVjskCoA1LLO)@Ob*I`%xmx3pY(UfU8<8kU$joj5L4 zATtgXL$nQ}etB%PN4yHDL~%HN5EDvD5vvXO-mjxu`Jw^zi}3M^AJM(=3}3DOf8t`1 zj@}P~D3y#8&Z~?x@1we($j}oT5e)y|907FiLk@uC)}}$TRV@Z^%|3?~AHf{N=JdS& zmAzu?{0*volC=e78`s^b_7LW?R+;1;+(&V(( zT*de9#KZNX^&6OE$fSpA*AbRQ@qfp9pz}Jz&>w6bXOw_#xf2&v-L&Phl|oHWm@D3} zOys2V$RMQlp$0{v*s34|sV=iP;x;zP=5RpwWJqnMP&QI7t)^63FDPCOen|MjFf6Fy zbH8(4!{wkLz8MP^38`HGosmm`E%%?rxE~2g)rNC_O(^3S*C5=3vYzG?3+y!oJ{s{1 zzUNA>f{FX&_8Zr~`3Aw+iI_(l<`oeGJGikkiq2sFOWR;=B!0NQRSX2%#^zb?1Vrn0 zUAk&@PC~v?RWOgIT?*LH&uK#GKtFXuFkP#zy6ByVhMXCsN=h$QNcx6Fab!8QW6Ur& zu}^KvHWAtV9#3GkF0lWTE~;FldqZH>&o2Z%KX~iA^p+fyiGvth=T-)<@CAc* z?GOwo%qlyt!74iU48M9XWmg)7k`fK)b}RNsiVo+UFE1g{y{tK^E0QF7xiW`>{iE_L z2e)`aCjrF4vaJ(AvkPqelNNjHn&uE@P7P07O+*CceK`8`Ej^O>;!(i7HaCaidZ~kj z$!=OMn@HL1CK6=ew?$zQygRGVm+>k2DR7{CKke27evSnGj z1DV~gv?jynB1yH$8kV3HDk^T{2PYCrSXN1{hh09 z;l&(GhZ7|l=@6E5%7=&;~1HUa3Edum39CeKllCDf5wr&+x@&+?!$Me2rKSZQ|DnUwYV~xd6kbA z^fr&FN-_FU?HQ=YD~>e6BR*{U((QLQmN58HK)d%N*5aFTL7;s!iBET0tq(c}0dVe8m%YTz{Xp6^}&9rC_ zFW>egRI>>@J|qb&;qSrsBP~vN2r{|}xny7Ft7$IqnF5kN{Ynzk**xBl6FNa!ASKZr zjkC^DnaK9M?q75@{PckJ6UMReK^Nr?iNe=Y6Rz(Imio@B{ax^h!#Hx-I^eU|&f@Sk zeh`^4eBkBo*Y~t$e?2{Lb0)LXxo?T=3jk#6n!+oCl3nSeNpa@uI5OjP zK#5K-d&uK-Mex4OqRTzG{P7!FFn+a literal 0 HcmV?d00001 diff --git a/.github/resources/settings.xml b/.github/resources/settings.xml new file mode 100644 index 000000000..708bbcb75 --- /dev/null +++ b/.github/resources/settings.xml @@ -0,0 +1,29 @@ + + + + false + + + + ossrh + ${env.NEXUS_OSSRH_USERNAME} + ${env.NEXUS_OSSRH_PASSWORD} + + + + + + release + + true + + + gpg + B652FFD3865AF7A75830876F5F55C8F6985BB9DD + ${env.GPG_PASSPHRASE} + + + + diff --git a/.github/scripts/generate_changelog.sh b/.github/scripts/generate_changelog.sh new file mode 100755 index 000000000..e393f40e4 --- /dev/null +++ b/.github/scripts/generate_changelog.sh @@ -0,0 +1,79 @@ +#!/bin/bash + +# Copyright 2020 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. + +set -e +set -u + +function printChangelog() { + local TITLE=$1 + shift + # Skip the sentinel value. + local ENTRIES=("${@:2}") + if [ ${#ENTRIES[@]} -ne 0 ]; then + echo "### ${TITLE}" + echo "" + for ((i = 0; i < ${#ENTRIES[@]}; i++)) + do + echo "* ${ENTRIES[$i]}" + done + echo "" + fi +} + +if [[ -z "${GITHUB_SHA}" ]]; then + GITHUB_SHA="HEAD" +fi + +LAST_TAG=`git describe --tags $(git rev-list --tags --max-count=1) 2> /dev/null` || true +if [[ -z "${LAST_TAG}" ]]; then + echo "[INFO] No tags found. Including all commits up to ${GITHUB_SHA}." + VERSION_RANGE="${GITHUB_SHA}" +else + echo "[INFO] Last release tag: ${LAST_TAG}." + COMMIT_SHA=`git show-ref -s ${LAST_TAG}` + echo "[INFO] Last release commit: ${COMMIT_SHA}." + VERSION_RANGE="${COMMIT_SHA}..${GITHUB_SHA}" + echo "[INFO] Including all commits in the range ${VERSION_RANGE}." +fi + +echo "" + +# Older versions of Bash (< 4.4) treat empty arrays as unbound variables, which triggers +# errors when referencing them. Therefore we initialize each of these arrays with an empty +# sentinel value, and later skip them. +CHANGES=("") +FIXES=("") +FEATS=("") +MISC=("") + +while read -r line +do + COMMIT_MSG=`echo ${line} | cut -d ' ' -f 2-` + if [[ $COMMIT_MSG =~ ^change(\(.*\))?: ]]; then + CHANGES+=("$COMMIT_MSG") + elif [[ $COMMIT_MSG =~ ^fix(\(.*\))?: ]]; then + FIXES+=("$COMMIT_MSG") + elif [[ $COMMIT_MSG =~ ^feat(\(.*\))?: ]]; then + FEATS+=("$COMMIT_MSG") + else + MISC+=("${COMMIT_MSG}") + fi +done < <(git log ${VERSION_RANGE} --oneline) + +printChangelog "Breaking Changes" "${CHANGES[@]}" +printChangelog "New Features" "${FEATS[@]}" +printChangelog "Bug Fixes" "${FIXES[@]}" +printChangelog "Miscellaneous" "${MISC[@]}" diff --git a/.github/scripts/package_artifacts.sh b/.github/scripts/package_artifacts.sh new file mode 100755 index 000000000..6e993066e --- /dev/null +++ b/.github/scripts/package_artifacts.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +# Copyright 2020 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. + +set -e +set -u + +gpg --quiet --batch --yes --decrypt --passphrase="${FIREBASE_SERVICE_ACCT_KEY}" \ + --output integration_cert.json .github/resources/integ-service-account.json.gpg + +echo "${FIREBASE_API_KEY}" > integration_apikey.txt + +# Does the following: +# 1. Runs the Checkstyle plugin (validate phase) +# 2. Compiles the source (compile phase) +# 3. Runs the unit tests (test phase) +# 4. Packages the artifacts - src, bin, javadocs (package phase) +# 5. Runs the integration tests (verify phase) +mvn -B clean verify + +# Maven target directory can consist of many files. Just copy the jar artifacts +# into a new directory for upload. +mkdir -p dist +cp target/*.jar dist/ diff --git a/.github/scripts/publish_artifacts.sh b/.github/scripts/publish_artifacts.sh new file mode 100755 index 000000000..f4a2f1734 --- /dev/null +++ b/.github/scripts/publish_artifacts.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +# Copyright 2020 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. + +set -e +set -u + +gpg --quiet --batch --yes --decrypt --passphrase="${GPG_PRIVATE_KEY}" \ + --output firebase.asc .github/resources/firebase.asc.gpg + +gpg --import firebase.asc + +# Does the following: +# 1. Compiles the source (compile phase) +# 2. Packages the artifacts - src, bin, javadocs (package phase) +# 3. Signs the artifacts (verify phase) +# 4. Publishes artifacts via Nexus (deploy phase) +mvn -B clean deploy \ + -Dcheckstyle.skip \ + -DskipTests \ + -Prelease \ + --settings .github/resources/settings.xml + diff --git a/.github/scripts/publish_preflight_check.sh b/.github/scripts/publish_preflight_check.sh new file mode 100755 index 000000000..7a191518d --- /dev/null +++ b/.github/scripts/publish_preflight_check.sh @@ -0,0 +1,146 @@ +#!/bin/bash + +# Copyright 2020 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. + + +###################################### Outputs ##################################### + +# 1. version: The version of this release including the 'v' prefix (e.g. v1.2.3). +# 2. changelog: Formatted changelog text for this release. + +#################################################################################### + +set -e +set -u + +function echo_info() { + local MESSAGE=$1 + echo "[INFO] ${MESSAGE}" +} + +function echo_warn() { + local MESSAGE=$1 + echo "[WARN] ${MESSAGE}" +} + +function terminate() { + echo "" + echo_warn "--------------------------------------------" + echo_warn "PREFLIGHT FAILED" + echo_warn "--------------------------------------------" + exit 1 +} + + +echo_info "Starting publish preflight check..." +echo_info "Git revision : ${GITHUB_SHA}" +echo_info "Workflow triggered by : ${GITHUB_ACTOR}" +echo_info "GitHub event : ${GITHUB_EVENT_NAME}" + + +echo_info "" +echo_info "--------------------------------------------" +echo_info "Extracting release version" +echo_info "--------------------------------------------" +echo_info "" + +echo_info "Loading version from: pom.xml" +readonly RELEASE_VERSION=`mvn help:evaluate -Dexpression=project.version -q -DforceStdout` || true +if [[ -z "${RELEASE_VERSION}" ]]; then + echo_warn "Failed to extract release version from: pom.xml" + terminate +fi + +if [[ ! "${RELEASE_VERSION}" =~ ^([0-9]*)\.([0-9]*)\.([0-9]*)$ ]]; then + echo_warn "Malformed release version string: ${RELEASE_VERSION}. Exiting." + terminate +fi + +echo_info "Extracted release version: ${RELEASE_VERSION}" +echo "::set-output name=version::v${RELEASE_VERSION}" + + +echo_info "" +echo_info "--------------------------------------------" +echo_info "Checking previous releases" +echo_info "--------------------------------------------" +echo_info "" + +readonly MAVEN_CENTRAL_URL="https://repo1.maven.org/maven2/com/google/firebase/firebase-admin/${RELEASE_VERSION}" +readonly MAVEN_STATUS=`curl -s -o /dev/null -L -w "%{http_code}" ${MAVEN_CENTRAL_URL}` +if [[ $MAVEN_STATUS -eq 404 ]]; then + echo_info "Release version ${RELEASE_VERSION} not found in Maven Central." +elif [[ $MAVEN_STATUS -eq 200 ]]; then + echo_warn "Release version ${RELEASE_VERSION} already present in Maven Central." + terminate +else + echo_warn "Unexpected ${MAVEN_STATUS} response from Maven Central. Exiting." + terminate +fi + + +echo_info "" +echo_info "--------------------------------------------" +echo_info "Checking release tag" +echo_info "--------------------------------------------" +echo_info "" + +echo_info "---< git fetch --depth=1 origin +refs/tags/*:refs/tags/* >---" +git fetch --depth=1 origin +refs/tags/*:refs/tags/* +echo "" + +readonly EXISTING_TAG=`git rev-parse -q --verify "refs/tags/v${RELEASE_VERSION}"` || true +if [[ -n "${EXISTING_TAG}" ]]; then + echo_warn "Tag v${RELEASE_VERSION} already exists. Exiting." + echo_warn "If the tag was created in a previous unsuccessful attempt, delete it and try again." + echo_warn " $ git tag -d v${RELEASE_VERSION}" + echo_warn " $ git push --delete origin v${RELEASE_VERSION}" + + readonly RELEASE_URL="https://github.com/firebase/firebase-admin-java/releases/tag/v${RELEASE_VERSION}" + echo_warn "Delete any corresponding releases at ${RELEASE_URL}." + terminate +fi + +echo_info "Tag v${RELEASE_VERSION} does not exist." + + +echo_info "" +echo_info "--------------------------------------------" +echo_info "Generating changelog" +echo_info "--------------------------------------------" +echo_info "" + +echo_info "---< git fetch origin master --prune --unshallow >---" +git fetch origin master --prune --unshallow +echo "" + +echo_info "Generating changelog from history..." +readonly CURRENT_DIR=$(dirname "$0") +readonly CHANGELOG=`${CURRENT_DIR}/generate_changelog.sh` +echo "$CHANGELOG" + +# Parse and preformat the text to handle multi-line output. +# See https://github.amrom.workers.devmunity/t5/GitHub-Actions/set-output-Truncates-Multiline-Strings/td-p/37870 +FILTERED_CHANGELOG=`echo "$CHANGELOG" | grep -v "\\[INFO\\]"` +FILTERED_CHANGELOG="${FILTERED_CHANGELOG//'%'/'%25'}" +FILTERED_CHANGELOG="${FILTERED_CHANGELOG//$'\n'/'%0A'}" +FILTERED_CHANGELOG="${FILTERED_CHANGELOG//$'\r'/'%0D'}" +echo "::set-output name=changelog::${FILTERED_CHANGELOG}" + + +echo "" +echo_info "--------------------------------------------" +echo_info "PREFLIGHT SUCCESSFUL" +echo_info "--------------------------------------------" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 10007c5c6..fbb1aad5e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,15 +1,35 @@ +# Copyright 2020 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. name: Continuous Integration -on: [push, pull_request] +on: push jobs: build: runs-on: ubuntu-latest + steps: - uses: actions/checkout@v1 + - name: Set up JDK 1.7 uses: actions/setup-java@v1 with: java-version: 1.7 + + # Does the following: + # 1. Runs the Checkstyle plugin (validate phase) + # 2. Compiles the source (compile phase) + # 3. Runs the unit tests (test phase) - name: Build with Maven - run: mvn -B package --file pom.xml + run: mvn -B clean test diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..0e1c307bc --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,129 @@ +# Copyright 2020 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. + +name: Release Candidate + +on: + # Only run the workflow when a PR is updated or when a developer explicitly requests + # a build by sending a 'firebase_build' event. + pull_request: + types: [opened, synchronize, closed] + + repository_dispatch: + types: + - firebase_build + +jobs: + stage_release: + # To publish a release, merge the release PR with the label 'release:publish'. + # To stage a release without publishing it, send a 'firebase_build' event or apply + # the 'release:stage' label to a PR. + if: github.event.action == 'firebase_build' || + contains(github.event.pull_request.labels.*.name, 'release:stage') || + (github.event.pull_request.merged && + contains(github.event.pull_request.labels.*.name, 'release:publish')) + + runs-on: ubuntu-latest + + # When manually triggering the build, the requester can specify a target branch or a tag + # via the 'ref' client parameter. + steps: + - name: Checkout source for staging + uses: actions/checkout@v2 + with: + ref: ${{ github.event.client_payload.ref || github.ref }} + + - name: Set up JDK 1.7 + uses: actions/setup-java@v1 + with: + java-version: 1.7 + + - name: Compile, test and package + run: ./.github/scripts/package_artifacts.sh + env: + FIREBASE_SERVICE_ACCT_KEY: ${{ secrets.FIREBASE_SERVICE_ACCT_KEY }} + FIREBASE_API_KEY: ${{ secrets.FIREBASE_API_KEY }} + + # Attach the packaged artifacts to the workflow output. These can be manually + # downloaded for later inspection if necessary. + - name: Archive artifacts + uses: actions/upload-artifact@v1 + with: + name: dist + path: dist + + publish_release: + needs: stage_release + + # Check whether the release should be published. We publish only when the trigger PR is + # 1. merged + # 2. to the master branch + # 3. with the label 'release:publish', and + # 4. the title prefix '[chore] Release '. + if: github.event.pull_request.merged && + github.ref == 'master' && + contains(github.event.pull_request.labels.*.name, 'release:publish') && + startsWith(github.event.pull_request.title, '[chore] Release ') + + runs-on: ubuntu-latest + + steps: + - name: Checkout source for publish + uses: actions/checkout@v2 + + - name: Set up JDK 1.7 + uses: actions/setup-java@v1 + with: + java-version: 1.7 + + - name: Publish preflight check + id: preflight + run: ./.github/scripts/publish_preflight_check.sh + + # We pull this action from a custom fork of a contributor until + # https://github.com/actions/create-release/pull/32 is merged. Also note that v1 of + # this action does not support the "body" parameter. + - name: Create release tag + uses: fleskesvor/create-release@1a72e235c178bf2ae6c51a8ae36febc24568c5fe + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ steps.preflight.outputs.version }} + release_name: Firebase Admin Java SDK ${{ steps.preflight.outputs.version }} + body: ${{ steps.preflight.outputs.changelog }} + draft: false + prerelease: false + + - name: Publish to Maven Central + run: ./.github/scripts/publish_artifacts.sh + env: + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} + NEXUS_OSSRH_USERNAME: ${{ secrets.NEXUS_OSSRH_USERNAME }} + NEXUS_OSSRH_PASSWORD: ${{ secrets.NEXUS_OSSRH_PASSWORD }} + + # Post to Twitter if explicitly opted-in by adding the label 'release:tweet'. + - name: Post to Twitter + if: success() && + contains(github.event.pull_request.labels.*.name, 'release:tweet') + uses: firebase/firebase-admin-node/.github/actions/send-tweet@master + with: + status: > + ${{ steps.preflight.outputs.version }} of @Firebase Admin Java SDK is available. + https://github.com/firebase/firebase-admin-java/releases/tag/${{ steps.preflight.outputs.version }} + consumer-key: ${{ secrets.TWITTER_CONSUMER_KEY }} + consumer-secret: ${{ secrets.TWITTER_CONSUMER_SECRET }} + access-token: ${{ secrets.TWITTER_ACCESS_TOKEN }} + access-token-secret: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }} + continue-on-error: true diff --git a/pom.xml b/pom.xml index 665edc8a6..af25b87d7 100644 --- a/pom.xml +++ b/pom.xml @@ -161,56 +161,8 @@ release - - - true - - - maven-javadoc-plugin - - - package - - jar - - - - - - com.google.doclava - doclava - 1.0.6 - - com.google.doclava.Doclava - ${sun.boot.class.path} - - - com.google.j2objc - j2objc-annotations - 1.3 - - - - -warning 101 - - false - -J-Xmx1024m - - - - maven-source-plugin - 2.2.1 - - - attach-sources - - jar-no-fork - - - - maven-gpg-plugin 1.5 @@ -295,6 +247,7 @@ + maven-checkstyle-plugin 2.17 @@ -315,6 +268,8 @@ + + maven-compiler-plugin 3.6.1 @@ -323,6 +278,8 @@ 1.7 + + maven-surefire-plugin 2.19.1 @@ -330,32 +287,68 @@ ${skipUTs} + + - maven-failsafe-plugin - 2.19.1 + maven-source-plugin + 2.2.1 + attach-sources - integration-test - verify + jar-no-fork - + maven-javadoc-plugin - 2.10.4 - - - maven-release-plugin - 2.5.3 + + + attach-javadocs + + jar + + + - false - release - v@{project.version} - deploy + + com.google.doclava + doclava + 1.0.6 + + com.google.doclava.Doclava + ${sun.boot.class.path} + + + com.google.j2objc + j2objc-annotations + 1.3 + + + + -warning 101 + + false + -J-Xmx1024m + + + + maven-failsafe-plugin + 2.19.1 + + + + integration-test + verify + + + + + + org.sonatype.plugins nexus-staging-maven-plugin @@ -364,7 +357,7 @@ ossrh https://oss.sonatype.org/ - false + true From b18cf69fc92a8a792879632ba296c4ff82a351d2 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Fri, 10 Apr 2020 13:53:47 -0700 Subject: [PATCH 004/354] chore: Updated CI workflow to run on all PRs (#390) --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fbb1aad5e..58ecbfa35 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ # limitations under the License. name: Continuous Integration -on: push +on: pull_request jobs: build: From 2cfbc3d96d0090593828f78b497ea29c09fcaae1 Mon Sep 17 00:00:00 2001 From: Ian Thomas Date: Fri, 10 Apr 2020 17:27:20 -0400 Subject: [PATCH 005/354] Testable BatchResponse (#389) * Testable BatchResponse - Converts batch response to an interface - Moves batch response implementation to BatchResponseImpl - Update FirebaseMessagingClientImpl to use BatchResponseImpl - Updated tests * Make BatchResponseImpl package private --- .../firebase/messaging/BatchResponse.java | 29 ++-------- .../firebase/messaging/BatchResponseImpl.java | 58 +++++++++++++++++++ .../FirebaseMessagingClientImpl.java | 2 +- .../firebase/messaging/BatchResponseTest.java | 8 +-- .../messaging/FirebaseMessagingTest.java | 2 +- 5 files changed, 68 insertions(+), 31 deletions(-) create mode 100644 src/main/java/com/google/firebase/messaging/BatchResponseImpl.java diff --git a/src/main/java/com/google/firebase/messaging/BatchResponse.java b/src/main/java/com/google/firebase/messaging/BatchResponse.java index bd5069f4c..164403be4 100644 --- a/src/main/java/com/google/firebase/messaging/BatchResponse.java +++ b/src/main/java/com/google/firebase/messaging/BatchResponse.java @@ -16,7 +16,6 @@ package com.google.firebase.messaging; -import com.google.common.collect.ImmutableList; import com.google.firebase.internal.NonNull; import java.util.List; @@ -25,32 +24,12 @@ * See {@link FirebaseMessaging#sendAll(List)} and {@link * FirebaseMessaging#sendMulticast(MulticastMessage)}. */ -public final class BatchResponse { - - private final List responses; - private final int successCount; - - BatchResponse(List responses) { - this.responses = ImmutableList.copyOf(responses); - int successCount = 0; - for (SendResponse response : this.responses) { - if (response.isSuccessful()) { - successCount++; - } - } - this.successCount = successCount; - } +public interface BatchResponse { @NonNull - public List getResponses() { - return responses; - } + List getResponses(); - public int getSuccessCount() { - return successCount; - } + int getSuccessCount(); - public int getFailureCount() { - return responses.size() - successCount; - } + int getFailureCount(); } diff --git a/src/main/java/com/google/firebase/messaging/BatchResponseImpl.java b/src/main/java/com/google/firebase/messaging/BatchResponseImpl.java new file mode 100644 index 000000000..99cf63df1 --- /dev/null +++ b/src/main/java/com/google/firebase/messaging/BatchResponseImpl.java @@ -0,0 +1,58 @@ +/* + * Copyright 2020 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.messaging; + +import com.google.common.collect.ImmutableList; +import com.google.firebase.internal.NonNull; + +import java.util.List; + +/** + * Response from an operation that sends FCM messages to multiple recipients. + * See {@link FirebaseMessaging#sendAll(List)} and {@link + * FirebaseMessaging#sendMulticast(MulticastMessage)}. + */ +class BatchResponseImpl implements BatchResponse { + + private final List responses; + private final int successCount; + + BatchResponseImpl(List responses) { + this.responses = ImmutableList.copyOf(responses); + int successCount = 0; + for (SendResponse response : this.responses) { + if (response.isSuccessful()) { + successCount++; + } + } + this.successCount = successCount; + } + + @NonNull + public List getResponses() { + return responses; + } + + public int getSuccessCount() { + return successCount; + } + + public int getFailureCount() { + return responses.size() - successCount; + } + +} diff --git a/src/main/java/com/google/firebase/messaging/FirebaseMessagingClientImpl.java b/src/main/java/com/google/firebase/messaging/FirebaseMessagingClientImpl.java index c2659a270..53d5ab00b 100644 --- a/src/main/java/com/google/firebase/messaging/FirebaseMessagingClientImpl.java +++ b/src/main/java/com/google/firebase/messaging/FirebaseMessagingClientImpl.java @@ -167,7 +167,7 @@ private BatchResponse sendBatchRequest( MessagingBatchCallback callback = new MessagingBatchCallback(); BatchRequest batch = newBatchRequest(messages, dryRun, callback); batch.execute(); - return new BatchResponse(callback.getResponses()); + return new BatchResponseImpl(callback.getResponses()); } private BatchRequest newBatchRequest( diff --git a/src/test/java/com/google/firebase/messaging/BatchResponseTest.java b/src/test/java/com/google/firebase/messaging/BatchResponseTest.java index cea001085..9c174f569 100644 --- a/src/test/java/com/google/firebase/messaging/BatchResponseTest.java +++ b/src/test/java/com/google/firebase/messaging/BatchResponseTest.java @@ -31,7 +31,7 @@ public class BatchResponseTest { public void testEmptyResponses() { List responses = new ArrayList<>(); - BatchResponse batchResponse = new BatchResponse(responses); + BatchResponse batchResponse = new BatchResponseImpl(responses); assertEquals(0, batchResponse.getSuccessCount()); assertEquals(0, batchResponse.getFailureCount()); @@ -47,7 +47,7 @@ public void testSomeResponse() { "error-message", null)) ); - BatchResponse batchResponse = new BatchResponse(responses); + BatchResponse batchResponse = new BatchResponseImpl(responses); assertEquals(2, batchResponse.getSuccessCount()); assertEquals(1, batchResponse.getFailureCount()); @@ -61,7 +61,7 @@ public void testSomeResponse() { public void testResponsesImmutable() { List responses = new ArrayList<>(); responses.add(SendResponse.fromMessageId("message1")); - BatchResponse batchResponse = new BatchResponse(responses); + BatchResponse batchResponse = new BatchResponseImpl(responses); SendResponse sendResponse = SendResponse.fromMessageId("message2"); try { @@ -74,6 +74,6 @@ public void testResponsesImmutable() { @Test(expected = NullPointerException.class) public void testResponsesCannotBeNull() { - new BatchResponse(null); + new BatchResponseImpl(null); } } diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java index bebba0864..496823175 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java @@ -668,7 +668,7 @@ private BatchResponse getBatchResponse(String ...messageIds) { for (String messageId : messageIds) { listBuilder.add(SendResponse.fromMessageId(messageId)); } - return new BatchResponse(listBuilder.build()); + return new BatchResponseImpl(listBuilder.build()); } private static class MockFirebaseMessagingClient implements FirebaseMessagingClient { From ab3af3cda5937b4e335e0a9256e19cb76c9de4bb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 7 May 2020 15:36:27 -0700 Subject: [PATCH 006/354] Bump netty.version from 4.1.34.Final to 4.1.45.Final (#373) Bumps `netty.version` from 4.1.34.Final to 4.1.45.Final. Updates `netty-codec-http` from 4.1.34.Final to 4.1.45.Final - [Release notes](https://github.com/netty/netty/releases) - [Commits](https://github.com/netty/netty/compare/netty-4.1.34.Final...netty-4.1.45.Final) Updates `netty-handler` from 4.1.34.Final to 4.1.45.Final - [Release notes](https://github.com/netty/netty/releases) - [Commits](https://github.com/netty/netty/compare/netty-4.1.34.Final...netty-4.1.45.Final) Updates `netty-transport` from 4.1.34.Final to 4.1.45.Final - [Release notes](https://github.com/netty/netty/releases) - [Commits](https://github.com/netty/netty/compare/netty-4.1.34.Final...netty-4.1.45.Final) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index af25b87d7..b71b4e773 100644 --- a/pom.xml +++ b/pom.xml @@ -59,7 +59,7 @@ UTF-8 UTF-8 ${skipTests} - 4.1.34.Final + 4.1.45.Final From f8052c76330364675b13b2724d6d951269ecd6e3 Mon Sep 17 00:00:00 2001 From: rsgowman Date: Tue, 12 May 2020 16:56:35 -0400 Subject: [PATCH 007/354] feat(auth): Add bulk get/delete methods (#365) This PR allows callers to retrieve a list of users by unique identifier (uid, email, phone, federated provider uid) as well as to delete a list of users. RELEASE NOTE: Added getUsers() and deleteUsers() APIs for retrieving and deleting user accounts in bulk. --- .../firebase/auth/DeleteUsersResult.java | 74 ++++++ .../google/firebase/auth/EmailIdentifier.java | 49 ++++ .../google/firebase/auth/FirebaseAuth.java | 143 ++++++++++- .../firebase/auth/FirebaseUserManager.java | 52 +++- .../google/firebase/auth/GetUsersResult.java | 52 ++++ .../google/firebase/auth/PhoneIdentifier.java | 49 ++++ .../firebase/auth/ProviderIdentifier.java | 56 +++++ .../google/firebase/auth/UidIdentifier.java | 49 ++++ .../google/firebase/auth/UserIdentifier.java | 31 +++ .../google/firebase/auth/UserMetadata.java | 15 +- .../com/google/firebase/auth/UserRecord.java | 16 +- .../auth/internal/BatchDeleteResponse.java | 51 ++++ .../auth/internal/GetAccountInfoRequest.java | 80 ++++++ .../auth/internal/GetAccountInfoResponse.java | 7 + .../google/firebase/auth/FirebaseAuthIT.java | 128 +++++++++- .../auth/FirebaseUserManagerTest.java | 227 ++++++++++++++++++ .../com/google/firebase/auth/GetUsersIT.java | 152 ++++++++++++ .../firebase/auth/ImportUserRecordTest.java | 2 +- 18 files changed, 1220 insertions(+), 13 deletions(-) create mode 100644 src/main/java/com/google/firebase/auth/DeleteUsersResult.java create mode 100644 src/main/java/com/google/firebase/auth/EmailIdentifier.java create mode 100644 src/main/java/com/google/firebase/auth/GetUsersResult.java create mode 100644 src/main/java/com/google/firebase/auth/PhoneIdentifier.java create mode 100644 src/main/java/com/google/firebase/auth/ProviderIdentifier.java create mode 100644 src/main/java/com/google/firebase/auth/UidIdentifier.java create mode 100644 src/main/java/com/google/firebase/auth/UserIdentifier.java create mode 100644 src/main/java/com/google/firebase/auth/internal/BatchDeleteResponse.java create mode 100644 src/main/java/com/google/firebase/auth/internal/GetAccountInfoRequest.java create mode 100644 src/test/java/com/google/firebase/auth/GetUsersIT.java diff --git a/src/main/java/com/google/firebase/auth/DeleteUsersResult.java b/src/main/java/com/google/firebase/auth/DeleteUsersResult.java new file mode 100644 index 000000000..e8ca7dba5 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/DeleteUsersResult.java @@ -0,0 +1,74 @@ +/* + * Copyright 2020 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 static com.google.common.base.Preconditions.checkArgument; + +import com.google.common.collect.ImmutableList; +import com.google.firebase.auth.internal.BatchDeleteResponse; +import com.google.firebase.internal.NonNull; +import java.util.List; + +/** + * Represents the result of the {@link FirebaseAuth#deleteUsersAsync(List)} API. + */ +public final class DeleteUsersResult { + + private final int successCount; + private final List errors; + + DeleteUsersResult(int users, BatchDeleteResponse response) { + ImmutableList.Builder errorsBuilder = ImmutableList.builder(); + List responseErrors = response.getErrors(); + if (responseErrors != null) { + checkArgument(users >= responseErrors.size()); + for (BatchDeleteResponse.ErrorInfo error : responseErrors) { + errorsBuilder.add(new ErrorInfo(error.getIndex(), error.getMessage())); + } + } + errors = errorsBuilder.build(); + successCount = users - errors.size(); + } + + /** + * Returns the number of users that were deleted successfully (possibly zero). Users that did not + * exist prior to calling {@link FirebaseAuth#deleteUsersAsync(List)} are considered to be + * successfully deleted. + */ + public int getSuccessCount() { + return successCount; + } + + /** + * Returns the number of users that failed to be deleted (possibly zero). + */ + public int getFailureCount() { + return errors.size(); + } + + /** + * A list of {@link ErrorInfo} instances describing the errors that were encountered during + * the deletion. Length of this list is equal to the return value of + * {@link #getFailureCount()}. + * + * @return A non-null list (possibly empty). + */ + @NonNull + public List getErrors() { + return errors; + } +} diff --git a/src/main/java/com/google/firebase/auth/EmailIdentifier.java b/src/main/java/com/google/firebase/auth/EmailIdentifier.java new file mode 100644 index 000000000..8e729c220 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/EmailIdentifier.java @@ -0,0 +1,49 @@ +/* + * Copyright 2020 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.GetAccountInfoRequest; +import com.google.firebase.internal.NonNull; + +/** + * Used for looking up an account by email. + * + * @see {FirebaseAuth#getUsers} + */ +public final class EmailIdentifier extends UserIdentifier { + private final String email; + + public EmailIdentifier(@NonNull String email) { + UserRecord.checkEmail(email); + this.email = email; + } + + @Override + public String toString() { + return "EmailIdentifier(" + email + ")"; + } + + @Override + void populate(@NonNull GetAccountInfoRequest payload) { + payload.addEmail(email); + } + + @Override + boolean matches(@NonNull UserRecord userRecord) { + return email.equals(userRecord.getEmail()); + } +} diff --git a/src/main/java/com/google/firebase/auth/FirebaseAuth.java b/src/main/java/com/google/firebase/auth/FirebaseAuth.java index f7f6231ad..923778af4 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseAuth.java +++ b/src/main/java/com/google/firebase/auth/FirebaseAuth.java @@ -42,8 +42,11 @@ import com.google.firebase.internal.Nullable; import java.io.IOException; +import java.util.Collection; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; /** @@ -601,7 +604,82 @@ protected UserRecord execute() throws FirebaseAuthException { } /** - * Gets a page of users starting from the specified {@code pageToken}. Page size will be + * Gets the user data corresponding to the specified identifiers. + * + *

There are no ordering guarantees; in particular, the nth entry in the users result list is + * not guaranteed to correspond to the nth entry in the input parameters list. + * + *

A maximum of 100 identifiers may be specified. If more than 100 identifiers are + * supplied, this method throws an {@link IllegalArgumentException}. + * + * @param identifiers The identifiers used to indicate which user records should be returned. Must + * have 100 or fewer entries. + * @return The corresponding user records. + * @throws IllegalArgumentException If any of the identifiers are invalid or if more than 100 + * identifiers are specified. + * @throws NullPointerException If the identifiers parameter is null. + * @throws FirebaseAuthException If an error occurs while retrieving user data. + */ + public GetUsersResult getUsers(@NonNull Collection identifiers) + throws FirebaseAuthException { + return getUsersOp(identifiers).call(); + } + + /** + * Gets the user data corresponding to the specified identifiers. + * + *

There are no ordering guarantees; in particular, the nth entry in the users result list is + * not guaranteed to correspond to the nth entry in the input parameters list. + * + *

A maximum of 100 identifiers may be specified. If more than 100 identifiers are + * supplied, this method throws an {@link IllegalArgumentException}. + * + * @param identifiers The identifiers used to indicate which user records should be returned. + * Must have 100 or fewer entries. + * @return An {@code ApiFuture} that resolves to the corresponding user records. + * @throws IllegalArgumentException If any of the identifiers are invalid or if more than 100 + * identifiers are specified. + * @throws NullPointerException If the identifiers parameter is null. + */ + public ApiFuture getUsersAsync(@NonNull Collection identifiers) { + return getUsersOp(identifiers).callAsync(firebaseApp); + } + + private CallableOperation getUsersOp( + @NonNull final Collection identifiers) { + checkNotDestroyed(); + checkNotNull(identifiers, "identifiers must not be null"); + checkArgument(identifiers.size() <= FirebaseUserManager.MAX_GET_ACCOUNTS_BATCH_SIZE, + "identifiers parameter must have <= " + FirebaseUserManager.MAX_GET_ACCOUNTS_BATCH_SIZE + + " entries."); + + final FirebaseUserManager userManager = getUserManager(); + return new CallableOperation() { + @Override + protected GetUsersResult execute() throws FirebaseAuthException { + Set users = userManager.getAccountInfo(identifiers); + Set notFound = new HashSet<>(); + for (UserIdentifier id : identifiers) { + if (!isUserFound(id, users)) { + notFound.add(id); + } + } + return new GetUsersResult(users, notFound); + } + }; + } + + private boolean isUserFound(UserIdentifier id, Collection userRecords) { + for (UserRecord userRecord : userRecords) { + if (id.matches(userRecord)) { + return true; + } + } + return false; + } + + /** + * Gets a page of users starting from the specified {@code pageToken}. Page size is * limited to 1000 users. * * @param pageToken A non-empty page token string, or null to retrieve the first page of users. @@ -842,8 +920,67 @@ protected Void execute() throws FirebaseAuthException { } /** - * Imports the provided list of users into Firebase Auth. At most 1000 users can be imported at a - * time. This operation is optimized for bulk imports and will ignore checks on identifier + * Deletes the users specified by the given identifiers. + * + *

Deleting a non-existing user does not generate an error (the method is idempotent). + * Non-existing users are considered to be successfully deleted and are therefore included in the + * DeleteUsersResult.getSuccessCount() value. + * + *

A maximum of 1000 identifiers may be supplied. If more than 1000 identifiers are + * supplied, this method throws an {@link IllegalArgumentException}. + * + *

This API has a rate limit of 1 QPS. Exceeding the limit may result in a quota exceeded + * error. If you want to delete more than 1000 users, we suggest adding a delay to ensure you + * don't exceed this limit. + * + * @param uids The uids of the users to be deleted. Must have <= 1000 entries. + * @return The total number of successful/failed deletions, as well as the array of errors that + * correspond to the failed deletions. + * @throw IllegalArgumentException If any of the identifiers are invalid or if more than 1000 + * identifiers are specified. + * @throws FirebaseAuthException If an error occurs while deleting users. + */ + public DeleteUsersResult deleteUsers(List uids) throws FirebaseAuthException { + return deleteUsersOp(uids).call(); + } + + /** + * Similar to {@link #deleteUsers(List)} but performs the operation asynchronously. + * + * @param uids The uids of the users to be deleted. Must have <= 1000 entries. + * @return An {@code ApiFuture} that resolves to the total number of successful/failed + * deletions, as well as the array of errors that correspond to the failed deletions. If an + * error occurs while deleting the user account, the future throws a + * {@link FirebaseAuthException}. + * @throw IllegalArgumentException If any of the identifiers are invalid or if more than 1000 + * identifiers are specified. + */ + public ApiFuture deleteUsersAsync(List uids) { + return deleteUsersOp(uids).callAsync(firebaseApp); + } + + private CallableOperation deleteUsersOp( + final List uids) { + checkNotDestroyed(); + checkNotNull(uids, "uids must not be null"); + for (String uid : uids) { + UserRecord.checkUid(uid); + } + checkArgument(uids.size() <= FirebaseUserManager.MAX_DELETE_ACCOUNTS_BATCH_SIZE, + "uids parameter must have <= " + FirebaseUserManager.MAX_DELETE_ACCOUNTS_BATCH_SIZE + + " entries."); + final FirebaseUserManager userManager = getUserManager(); + return new CallableOperation() { + @Override + protected DeleteUsersResult execute() throws FirebaseAuthException { + return userManager.deleteUsers(uids); + } + }; + } + + /** + * Imports the provided list of users into Firebase Auth. You can import a maximum of 1000 users + * at a time. This operation is optimized for bulk imports and does not check identifier * uniqueness which could result in duplications. * *

{@link UserImportOptions} is required to import users with passwords. See diff --git a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java index 03c2813bc..ab8759c4f 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java +++ b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java @@ -39,9 +39,10 @@ import com.google.firebase.ImplFirebaseTrampolines; import com.google.firebase.auth.UserRecord.CreateRequest; import com.google.firebase.auth.UserRecord.UpdateRequest; +import com.google.firebase.auth.internal.BatchDeleteResponse; import com.google.firebase.auth.internal.DownloadAccountResponse; +import com.google.firebase.auth.internal.GetAccountInfoRequest; import com.google.firebase.auth.internal.GetAccountInfoResponse; - import com.google.firebase.auth.internal.HttpErrorResponse; import com.google.firebase.auth.internal.UploadAccountResponse; import com.google.firebase.internal.ApiClientUtils; @@ -50,8 +51,11 @@ import com.google.firebase.internal.SdkUtils; import java.io.IOException; +import java.util.Collection; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; /** * FirebaseUserManager provides methods for interacting with the Google Identity Toolkit via its @@ -86,6 +90,8 @@ class FirebaseUserManager { .put("INVALID_DYNAMIC_LINK_DOMAIN", "invalid-dynamic-link-domain") .build(); + static final int MAX_GET_ACCOUNTS_BATCH_SIZE = 100; + static final int MAX_DELETE_ACCOUNTS_BATCH_SIZE = 1000; static final int MAX_LIST_USERS_RESULTS = 1000; static final int MAX_IMPORT_USERS = 1000; @@ -171,6 +177,33 @@ UserRecord getUserByPhoneNumber(String phoneNumber) throws FirebaseAuthException return new UserRecord(response.getUsers().get(0), jsonFactory); } + Set getAccountInfo(@NonNull Collection identifiers) + throws FirebaseAuthException { + if (identifiers.isEmpty()) { + return new HashSet(); + } + + GetAccountInfoRequest payload = new GetAccountInfoRequest(); + for (UserIdentifier id : identifiers) { + id.populate(payload); + } + + GetAccountInfoResponse response = post( + "/accounts:lookup", payload, GetAccountInfoResponse.class); + + if (response == null) { + throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to parse server response"); + } + + Set results = new HashSet<>(); + if (response.getUsers() != null) { + for (GetAccountInfoResponse.User user : response.getUsers()) { + results.add(new UserRecord(user, jsonFactory)); + } + } + return results; + } + String createUser(CreateRequest request) throws FirebaseAuthException { GenericJson response = post( "/accounts", request.getProperties(), GenericJson.class); @@ -200,6 +233,23 @@ void deleteUser(String uid) throws FirebaseAuthException { } } + /** + * @pre uids != null + * @pre uids.size() <= MAX_DELETE_ACCOUNTS_BATCH_SIZE + */ + DeleteUsersResult deleteUsers(@NonNull List uids) throws FirebaseAuthException { + final Map payload = ImmutableMap.of( + "localIds", uids, + "force", true); + BatchDeleteResponse response = post( + "/accounts:batchDelete", payload, BatchDeleteResponse.class); + if (response == null) { + throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to delete users"); + } + + return new DeleteUsersResult(uids.size(), response); + } + DownloadAccountResponse listUsers(int maxResults, String pageToken) throws FirebaseAuthException { ImmutableMap.Builder builder = ImmutableMap.builder() .put("maxResults", maxResults); diff --git a/src/main/java/com/google/firebase/auth/GetUsersResult.java b/src/main/java/com/google/firebase/auth/GetUsersResult.java new file mode 100644 index 000000000..3ceec01cb --- /dev/null +++ b/src/main/java/com/google/firebase/auth/GetUsersResult.java @@ -0,0 +1,52 @@ +/* + * Copyright 2020 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 static com.google.common.base.Preconditions.checkNotNull; + +import com.google.firebase.internal.NonNull; +import java.util.Set; + +/** + * Represents the result of the {@link FirebaseAuth#getUsersAsync(Collection)} API. + */ +public final class GetUsersResult { + private final Set users; + private final Set notFound; + + GetUsersResult(@NonNull Set users, @NonNull Set notFound) { + this.users = checkNotNull(users); + this.notFound = checkNotNull(notFound); + } + + /** + * Set of user records corresponding to the set of users that were requested. Only users + * that were found are listed here. The result set is unordered. + */ + @NonNull + public Set getUsers() { + return this.users; + } + + /** + * Set of identifiers that were requested, but not found. + */ + @NonNull + public Set getNotFound() { + return this.notFound; + } +} diff --git a/src/main/java/com/google/firebase/auth/PhoneIdentifier.java b/src/main/java/com/google/firebase/auth/PhoneIdentifier.java new file mode 100644 index 000000000..bdc84fe92 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/PhoneIdentifier.java @@ -0,0 +1,49 @@ +/* + * Copyright 2020 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.GetAccountInfoRequest; +import com.google.firebase.internal.NonNull; + +/** + * Used for looking up an account by phone number. + * + * @see {FirebaseAuth#getUsers} + */ +public final class PhoneIdentifier extends UserIdentifier { + private final String phoneNumber; + + public PhoneIdentifier(@NonNull String phoneNumber) { + UserRecord.checkPhoneNumber(phoneNumber); + this.phoneNumber = phoneNumber; + } + + @Override + public String toString() { + return "PhoneIdentifier(" + phoneNumber + ")"; + } + + @Override + void populate(@NonNull GetAccountInfoRequest payload) { + payload.addPhoneNumber(phoneNumber); + } + + @Override + boolean matches(@NonNull UserRecord userRecord) { + return phoneNumber.equals(userRecord.getPhoneNumber()); + } +} diff --git a/src/main/java/com/google/firebase/auth/ProviderIdentifier.java b/src/main/java/com/google/firebase/auth/ProviderIdentifier.java new file mode 100644 index 000000000..25e00026d --- /dev/null +++ b/src/main/java/com/google/firebase/auth/ProviderIdentifier.java @@ -0,0 +1,56 @@ +/* + * Copyright 2020 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.GetAccountInfoRequest; +import com.google.firebase.internal.NonNull; + +/** + * Used for looking up an account by provider. + * + * @see {FirebaseAuth#getUsers} + */ +public final class ProviderIdentifier extends UserIdentifier { + private final String providerId; + private final String providerUid; + + public ProviderIdentifier(@NonNull String providerId, @NonNull String providerUid) { + UserRecord.checkProvider(providerId, providerUid); + this.providerId = providerId; + this.providerUid = providerUid; + } + + @Override + public String toString() { + return "ProviderIdentifier(" + providerId + ", " + providerUid + ")"; + } + + @Override + void populate(@NonNull GetAccountInfoRequest payload) { + payload.addFederatedUserId(providerId, providerUid); + } + + @Override + boolean matches(@NonNull UserRecord userRecord) { + for (UserInfo userInfo : userRecord.getProviderData()) { + if (providerId.equals(userInfo.getProviderId()) && providerUid.equals(userInfo.getUid())) { + return true; + } + } + return false; + } +} diff --git a/src/main/java/com/google/firebase/auth/UidIdentifier.java b/src/main/java/com/google/firebase/auth/UidIdentifier.java new file mode 100644 index 000000000..a4f7069d9 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/UidIdentifier.java @@ -0,0 +1,49 @@ +/* + * Copyright 2020 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.GetAccountInfoRequest; +import com.google.firebase.internal.NonNull; + +/** + * Used for looking up an account by uid. + * + * @see {FirebaseAuth#getUsers} + */ +public final class UidIdentifier extends UserIdentifier { + private final String uid; + + public UidIdentifier(@NonNull String uid) { + UserRecord.checkUid(uid); + this.uid = uid; + } + + @Override + public String toString() { + return "UidIdentifier(" + uid + ")"; + } + + @Override + void populate(@NonNull GetAccountInfoRequest payload) { + payload.addUid(uid); + } + + @Override + boolean matches(@NonNull UserRecord userRecord) { + return uid.equals(userRecord.getUid()); + } +} diff --git a/src/main/java/com/google/firebase/auth/UserIdentifier.java b/src/main/java/com/google/firebase/auth/UserIdentifier.java new file mode 100644 index 000000000..7ec9699e6 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/UserIdentifier.java @@ -0,0 +1,31 @@ +/* + * Copyright 2020 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.GetAccountInfoRequest; +import com.google.firebase.internal.NonNull; + +/** + * Identifies a user to be looked up. + */ +public abstract class UserIdentifier { + public abstract String toString(); + + abstract void populate(@NonNull GetAccountInfoRequest payload); + + abstract boolean matches(@NonNull UserRecord userRecord); +} diff --git a/src/main/java/com/google/firebase/auth/UserMetadata.java b/src/main/java/com/google/firebase/auth/UserMetadata.java index a2872371f..85a24a0fd 100644 --- a/src/main/java/com/google/firebase/auth/UserMetadata.java +++ b/src/main/java/com/google/firebase/auth/UserMetadata.java @@ -23,14 +23,16 @@ public class UserMetadata { private final long creationTimestamp; private final long lastSignInTimestamp; + private final long lastRefreshTimestamp; public UserMetadata(long creationTimestamp) { - this(creationTimestamp, 0L); + this(creationTimestamp, 0L, 0L); } - public UserMetadata(long creationTimestamp, long lastSignInTimestamp) { + public UserMetadata(long creationTimestamp, long lastSignInTimestamp, long lastRefreshTimestamp) { this.creationTimestamp = creationTimestamp; this.lastSignInTimestamp = lastSignInTimestamp; + this.lastRefreshTimestamp = lastRefreshTimestamp; } /** @@ -50,4 +52,13 @@ public long getCreationTimestamp() { public long getLastSignInTimestamp() { return lastSignInTimestamp; } + + /** + * Returns the time at which the user was last active (ID token refreshed). + *  + * @return Milliseconds since epoch timestamp, or 0 if the user was never active. + */ + public long getLastRefreshTimestamp() { + return lastRefreshTimestamp; + } } diff --git a/src/main/java/com/google/firebase/auth/UserRecord.java b/src/main/java/com/google/firebase/auth/UserRecord.java index e00450079..64e7c278c 100644 --- a/src/main/java/com/google/firebase/auth/UserRecord.java +++ b/src/main/java/com/google/firebase/auth/UserRecord.java @@ -20,6 +20,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import com.google.api.client.json.JsonFactory; +import com.google.api.client.util.DateTime; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -80,7 +81,15 @@ public class UserRecord implements UserInfo { } } this.tokensValidAfterTimestamp = response.getValidSince() * 1000; - this.userMetadata = new UserMetadata(response.getCreatedAt(), response.getLastLoginAt()); + + String lastRefreshAtRfc3339 = response.getLastRefreshAt(); + long lastRefreshAtMillis = 0; + if (!Strings.isNullOrEmpty(lastRefreshAtRfc3339)) { + lastRefreshAtMillis = DateTime.parseRfc3339(lastRefreshAtRfc3339).getValue(); + } + + this.userMetadata = new UserMetadata( + response.getCreatedAt(), response.getLastLoginAt(), lastRefreshAtMillis); this.customClaims = parseCustomClaims(response.getCustomClaims(), jsonFactory); } @@ -247,6 +256,11 @@ static void checkPhoneNumber(String phoneNumber) { "phone number must be a valid, E.164 compliant identifier starting with a '+' sign"); } + static void checkProvider(String providerId, String providerUid) { + checkArgument(!Strings.isNullOrEmpty(providerId), "providerId must be a non-empty string"); + checkArgument(!Strings.isNullOrEmpty(providerUid), "providerUid must be a non-empty string"); + } + static void checkUrl(String photoUrl) { checkArgument(!Strings.isNullOrEmpty(photoUrl), "url cannot be null or empty"); try { diff --git a/src/main/java/com/google/firebase/auth/internal/BatchDeleteResponse.java b/src/main/java/com/google/firebase/auth/internal/BatchDeleteResponse.java new file mode 100644 index 000000000..728cf6358 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/internal/BatchDeleteResponse.java @@ -0,0 +1,51 @@ +/* + * Copyright 2020 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.internal; + +import com.google.api.client.util.Key; +import java.util.List; + +/** + * Represents the response from Google identity Toolkit for a batch delete request. + */ +public class BatchDeleteResponse { + + @Key("errors") + private List errors; + + public List getErrors() { + return errors; + } + + public static class ErrorInfo { + @Key("index") + private int index; + + @Key("message") + private String message; + + // A 'localId' field also exists here, but is not currently exposed in the Admin SDK. + + public int getIndex() { + return index; + } + + public String getMessage() { + return message; + } + } +} diff --git a/src/main/java/com/google/firebase/auth/internal/GetAccountInfoRequest.java b/src/main/java/com/google/firebase/auth/internal/GetAccountInfoRequest.java new file mode 100644 index 000000000..67c4d0ee7 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/internal/GetAccountInfoRequest.java @@ -0,0 +1,80 @@ +/* + * Copyright 2020 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.internal; + +import com.google.api.client.util.Key; +import java.util.ArrayList; +import java.util.List; + +/** + * Represents the request to look up account information. + */ +public final class GetAccountInfoRequest { + + @Key("localId") + private List uids = null; + + @Key("email") + private List emails = null; + + @Key("phoneNumber") + private List phoneNumbers = null; + + @Key("federatedUserId") + private List federatedUserIds = null; + + private static final class FederatedUserId { + @Key("providerId") + private String providerId = null; + + @Key("rawId") + private String rawId = null; + + FederatedUserId(String providerId, String rawId) { + this.providerId = providerId; + this.rawId = rawId; + } + } + + public void addUid(String uid) { + if (uids == null) { + uids = new ArrayList<>(); + } + uids.add(uid); + } + + public void addEmail(String email) { + if (emails == null) { + emails = new ArrayList<>(); + } + emails.add(email); + } + + public void addPhoneNumber(String phoneNumber) { + if (phoneNumbers == null) { + phoneNumbers = new ArrayList<>(); + } + phoneNumbers.add(phoneNumber); + } + + public void addFederatedUserId(String providerId, String providerUid) { + if (federatedUserIds == null) { + federatedUserIds = new ArrayList<>(); + } + federatedUserIds.add(new FederatedUserId(providerId, providerUid)); + } +} 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 3d17c50f6..e84335891 100644 --- a/src/main/java/com/google/firebase/auth/internal/GetAccountInfoResponse.java +++ b/src/main/java/com/google/firebase/auth/internal/GetAccountInfoResponse.java @@ -73,6 +73,9 @@ public static class User { @Key("lastLoginAt") private long lastLoginAt; + @Key("lastRefreshAt") + private String lastRefreshAt; + @Key("validSince") private long validSince; @@ -119,6 +122,10 @@ public long getLastLoginAt() { return lastLoginAt; } + public String getLastRefreshAt() { + return lastRefreshAt; + } + public long getValidSince() { return validSince; } diff --git a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java index 6a8361d0d..aa45f51b3 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java +++ b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java @@ -137,6 +137,78 @@ public void testDeleteNonExistingUser() throws Exception { } } + @Test + public void testDeleteUsers() throws Exception { + UserRecord user1 = newUserWithParams(); + UserRecord user2 = newUserWithParams(); + UserRecord user3 = newUserWithParams(); + + DeleteUsersResult deleteUsersResult = + slowDeleteUsersAsync(ImmutableList.of(user1.getUid(), user2.getUid(), user3.getUid())) + .get(); + + assertEquals(3, deleteUsersResult.getSuccessCount()); + assertEquals(0, deleteUsersResult.getFailureCount()); + assertTrue(deleteUsersResult.getErrors().isEmpty()); + + GetUsersResult getUsersResult = + auth.getUsersAsync( + ImmutableList.of(new UidIdentifier(user1.getUid()), + new UidIdentifier(user2.getUid()), new UidIdentifier(user3.getUid()))) + .get(); + + assertTrue(getUsersResult.getUsers().isEmpty()); + assertEquals(3, getUsersResult.getNotFound().size()); + } + + @Test + public void testDeleteExistingAndNonExistingUsers() throws Exception { + UserRecord user1 = newUserWithParams(); + + DeleteUsersResult deleteUsersResult = + slowDeleteUsersAsync(ImmutableList.of(user1.getUid(), "uid-that-doesnt-exist")).get(); + + assertEquals(2, deleteUsersResult.getSuccessCount()); + assertEquals(0, deleteUsersResult.getFailureCount()); + assertTrue(deleteUsersResult.getErrors().isEmpty()); + + GetUsersResult getUsersResult = + auth.getUsersAsync(ImmutableList.of(new UidIdentifier(user1.getUid()), + new UidIdentifier("uid-that-doesnt-exist"))) + .get(); + + assertTrue(getUsersResult.getUsers().isEmpty()); + assertEquals(2, getUsersResult.getNotFound().size()); + } + + @Test + public void testDeleteUsersIsIdempotent() throws Exception { + UserRecord user1 = newUserWithParams(); + + DeleteUsersResult result = slowDeleteUsersAsync(ImmutableList.of(user1.getUid())).get(); + + assertEquals(1, result.getSuccessCount()); + assertEquals(0, result.getFailureCount()); + assertTrue(result.getErrors().isEmpty()); + + // Delete the user again to ensure that everything still counts as a success. + result = slowDeleteUsersAsync(ImmutableList.of(user1.getUid())).get(); + + assertEquals(1, result.getSuccessCount()); + assertEquals(0, result.getFailureCount()); + assertTrue(result.getErrors().isEmpty()); + } + + /** + * The {@code batchDelete} endpoint has a rate limit of 1 QPS. Use this test + * helper to ensure you don't exceed the quota. + */ + // TODO(rsgowman): When/if the rate limit is relaxed, eliminate this helper. + private ApiFuture slowDeleteUsersAsync(List uids) throws Exception { + TimeUnit.SECONDS.sleep(1); + return auth.deleteUsersAsync(uids); + } + @Test public void testCreateUserWithParams() throws Exception { RandomUser randomUser = RandomUser.create(); @@ -248,6 +320,35 @@ public void testUserLifecycle() throws Exception { } } + @Test + public void testLastRefreshTime() throws Exception { + RandomUser user = RandomUser.create(); + UserRecord newUserRecord = auth.createUser(new CreateRequest() + .setUid(user.uid) + .setEmail(user.email) + .setEmailVerified(false) + .setPassword("password")); + + try { + // New users should not have a lastRefreshTimestamp set. + assertEquals(0, newUserRecord.getUserMetadata().getLastRefreshTimestamp()); + + // Login to cause the lastRefreshTimestamp to be set. + signInWithPassword(newUserRecord.getEmail(), "password"); + + UserRecord userRecord = auth.getUser(newUserRecord.getUid()); + + // Ensure the lastRefreshTimestamp is approximately "now" (with a tollerance of 10 minutes). + long now = System.currentTimeMillis(); + long tollerance = TimeUnit.MINUTES.toMillis(10); + long lastRefreshTimestamp = userRecord.getUserMetadata().getLastRefreshTimestamp(); + assertTrue(now - tollerance <= lastRefreshTimestamp); + assertTrue(lastRefreshTimestamp <= now + tollerance); + } finally { + auth.deleteUser(newUserRecord.getUid()); + } + } + @Test public void testListUsers() throws Exception { final List uids = new ArrayList<>(); @@ -607,7 +708,7 @@ private Map parseLinkParameters(String link) throws Exception { return result; } - private String randomPhoneNumber() { + static String randomPhoneNumber() { Random random = new Random(); StringBuilder builder = new StringBuilder("+1"); for (int i = 0; i < 10; i++) { @@ -637,7 +738,7 @@ private String signInWithPassword(String email, String password) throws IOExcept GenericUrl url = new GenericUrl(VERIFY_PASSWORD_URL + "?key=" + IntegrationTestUtils.getApiKey()); Map content = ImmutableMap.of( - "email", email, "password", password); + "email", email, "password", password, "returnSecureToken", true); HttpRequest request = transport.createRequestFactory().buildPostRequest(url, new JsonHttpContent(jsonFactory, content)); request.setParser(new JsonObjectParser(jsonFactory)); @@ -696,9 +797,9 @@ private void checkRecreate(String uid) throws Exception { } } - private static class RandomUser { - private final String uid; - private final String email; + static class RandomUser { + final String uid; + final String email; private RandomUser(String uid, String email) { this.uid = uid; @@ -712,4 +813,21 @@ static RandomUser create() { return new RandomUser(uid, email); } } + + static UserRecord newUserWithParams() throws Exception { + return newUserWithParams(auth); + } + + static UserRecord newUserWithParams(FirebaseAuth auth) throws Exception { + // TODO(rsgowman): This function could be used throughout this file (similar to the other + // ports). + RandomUser randomUser = RandomUser.create(); + return auth.createUser(new CreateRequest() + .setUid(randomUser.uid) + .setEmail(randomUser.email) + .setPhoneNumber(randomPhoneNumber()) + .setDisplayName("Random User") + .setPhotoUrl("https://example.com/photo.png") + .setPassword("password")); + } } diff --git a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java index de0b7fa29..67d0448e9 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java +++ b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java @@ -33,6 +33,7 @@ import com.google.api.client.testing.http.MockHttpTransport; import com.google.api.client.testing.http.MockLowLevelHttpResponse; import com.google.auth.oauth2.GoogleCredentials; +import com.google.common.base.Strings; import com.google.common.base.Supplier; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -41,6 +42,8 @@ import com.google.firebase.FirebaseOptions; import com.google.firebase.TestOnlyImplFirebaseTrampolines; import com.google.firebase.auth.FirebaseUserManager.EmailLinkType; +import com.google.firebase.auth.UidIdentifier; +import com.google.firebase.auth.UserIdentifier; import com.google.firebase.auth.UserRecord.CreateRequest; import com.google.firebase.auth.UserRecord.UpdateRequest; import com.google.firebase.internal.SdkUtils; @@ -52,6 +55,8 @@ import java.io.IOException; import java.math.BigDecimal; import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; import java.util.List; import java.util.Map; @@ -167,6 +172,153 @@ public void testGetUserByPhoneNumberWithNotFoundError() throws Exception { } } + @Test + public void testGetUsersExceeds100() throws Exception { + FirebaseApp.initializeApp(new FirebaseOptions.Builder() + .setCredentials(credentials) + .build()); + List identifiers = new ArrayList<>(); + for (int i = 0; i < 101; i++) { + identifiers.add(new UidIdentifier("uid_" + i)); + } + + try { + FirebaseAuth.getInstance().getUsers(identifiers); + fail("No error thrown for too many supplied identifiers"); + } catch (IllegalArgumentException expected) { + // expected + } + } + + @Test + public void testGetUsersNull() throws Exception { + FirebaseApp.initializeApp(new FirebaseOptions.Builder() + .setCredentials(credentials) + .build()); + try { + FirebaseAuth.getInstance().getUsers(null); + fail("No error thrown for null identifiers"); + } catch (NullPointerException expected) { + // expected + } + } + + @Test + public void testGetUsersEmpty() throws Exception { + initializeAppForUserManagement(); + GetUsersResult result = FirebaseAuth.getInstance().getUsers(new ArrayList()); + assertTrue(result.getUsers().isEmpty()); + assertTrue(result.getNotFound().isEmpty()); + } + + @Test + public void testGetUsersAllNonExisting() throws Exception { + initializeAppForUserManagement("{ \"users\": [] }"); + List ids = ImmutableList.of( + new UidIdentifier("id-that-doesnt-exist")); + GetUsersResult result = FirebaseAuth.getInstance().getUsers(ids); + assertTrue(result.getUsers().isEmpty()); + assertEquals(ids.size(), result.getNotFound().size()); + assertTrue(result.getNotFound().containsAll(ids)); + } + + @Test + public void testGetUsersMultipleIdentifierTypes() throws Exception { + initializeAppForUserManagement(("" + + "{ " + + " 'users': [{ " + + " 'localId': 'uid1', " + + " 'email': 'user1@example.com', " + + " 'phoneNumber': '+15555550001' " + + " }, { " + + " 'localId': 'uid2', " + + " 'email': 'user2@example.com', " + + " 'phoneNumber': '+15555550002' " + + " }, { " + + " 'localId': 'uid3', " + + " 'email': 'user3@example.com', " + + " 'phoneNumber': '+15555550003' " + + " }, { " + + " 'localId': 'uid4', " + + " 'email': 'user4@example.com', " + + " 'phoneNumber': '+15555550004', " + + " 'providerUserInfo': [{ " + + " 'providerId': 'google.com', " + + " 'rawId': 'google_uid4' " + + " }] " + + " }] " + + "} " + ).replace("'", "\"")); + + UidIdentifier doesntExist = new UidIdentifier("this-uid-doesnt-exist"); + List ids = ImmutableList.of( + new UidIdentifier("uid1"), + new EmailIdentifier("user2@example.com"), + new PhoneIdentifier("+15555550003"), + new ProviderIdentifier("google.com", "google_uid4"), + doesntExist); + GetUsersResult result = FirebaseAuth.getInstance().getUsers(ids); + Collection uids = userRecordsToUids(result.getUsers()); + assertTrue(uids.containsAll(ImmutableList.of("uid1", "uid2", "uid3", "uid4"))); + assertEquals(1, result.getNotFound().size()); + assertTrue(result.getNotFound().contains(doesntExist)); + } + + private Collection userRecordsToUids(Collection userRecords) { + Collection uids = new HashSet<>(); + for (UserRecord userRecord : userRecords) { + uids.add(userRecord.getUid()); + } + return uids; + } + + @Test + public void testInvalidUidIdentifier() throws Exception { + try { + new UidIdentifier("too long " + Strings.repeat(".", 128)); + fail("No error thrown for invalid uid"); + } catch (IllegalArgumentException expected) { + // expected + } + } + + @Test + public void testInvalidEmailIdentifier() throws Exception { + try { + new EmailIdentifier("invalid email addr"); + fail("No error thrown for invalid email"); + } catch (IllegalArgumentException expected) { + // expected + } + } + + @Test + public void testInvalidPhoneIdentifier() throws Exception { + try { + new PhoneIdentifier("invalid phone number"); + fail("No error thrown for invalid phone number"); + } catch (IllegalArgumentException expected) { + // expected + } + } + + @Test + public void testInvalidProviderIdentifier() throws Exception { + try { + new ProviderIdentifier("", "valid-uid"); + fail("No error thrown for invalid provider id"); + } catch (IllegalArgumentException expected) { + // expected + } + + try { + new ProviderIdentifier("valid-id", ""); + fail("No error thrown for invalid provider uid"); + } catch (IllegalArgumentException expected) { + // expected + } + } + @Test public void testListUsers() throws Exception { final TestResponseInterceptor interceptor = initializeAppForUserManagement( @@ -278,6 +430,81 @@ public void testDeleteUser() throws Exception { checkRequestHeaders(interceptor); } + @Test + public void testDeleteUsersExceeds1000() throws Exception { + FirebaseApp.initializeApp(new FirebaseOptions.Builder() + .setCredentials(credentials) + .build()); + List ids = new ArrayList<>(); + for (int i = 0; i < 1001; i++) { + ids.add("id" + i); + } + try { + FirebaseAuth.getInstance().deleteUsersAsync(ids); + fail("No error thrown for too many uids"); + } catch (IllegalArgumentException expected) { + // expected + } + } + + @Test + public void testDeleteUsersInvalidId() throws Exception { + FirebaseApp.initializeApp(new FirebaseOptions.Builder() + .setCredentials(credentials) + .build()); + try { + FirebaseAuth.getInstance().deleteUsersAsync( + ImmutableList.of("too long " + Strings.repeat(".", 128))); + fail("No error thrown for too long uid"); + } catch (IllegalArgumentException expected) { + // expected + } + } + + @Test + public void testDeleteUsersIndexesErrorsCorrectly() throws Exception { + initializeAppForUserManagement(("" + + "{ " + + " 'errors': [{ " + + " 'index': 0, " + + " 'localId': 'uid1', " + + " 'message': 'NOT_DISABLED : Disable the account before batch deletion.' " + + " }, { " + + " 'index': 2, " + + " 'localId': 'uid3', " + + " 'message': 'something awful' " + + " }] " + + "} " + ).replace("'", "\"")); + + DeleteUsersResult result = FirebaseAuth.getInstance().deleteUsersAsync(ImmutableList.of( + "uid1", "uid2", "uid3", "uid4" + )).get(); + + assertEquals(2, result.getSuccessCount()); + assertEquals(2, result.getFailureCount()); + assertEquals(2, result.getErrors().size()); + assertEquals(0, result.getErrors().get(0).getIndex()); + assertEquals( + "NOT_DISABLED : Disable the account before batch deletion.", + result.getErrors().get(0).getReason()); + assertEquals(2, result.getErrors().get(1).getIndex()); + assertEquals("something awful", result.getErrors().get(1).getReason()); + } + + @Test + public void testDeleteUsersSuccess() throws Exception { + initializeAppForUserManagement("{}"); + + DeleteUsersResult result = FirebaseAuth.getInstance().deleteUsersAsync(ImmutableList.of( + "uid1", "uid2", "uid3" + )).get(); + + assertEquals(3, result.getSuccessCount()); + assertEquals(0, result.getFailureCount()); + assertTrue(result.getErrors().isEmpty()); + } + @Test public void testImportUsers() throws Exception { TestResponseInterceptor interceptor = initializeAppForUserManagement("{}"); diff --git a/src/test/java/com/google/firebase/auth/GetUsersIT.java b/src/test/java/com/google/firebase/auth/GetUsersIT.java new file mode 100644 index 000000000..efe2f783f --- /dev/null +++ b/src/test/java/com/google/firebase/auth/GetUsersIT.java @@ -0,0 +1,152 @@ +/* + * Copyright 2020 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import com.google.common.collect.ImmutableList; +import com.google.firebase.FirebaseApp; +import com.google.firebase.testing.IntegrationTestUtils; +import java.util.Collection; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +public class GetUsersIT { + private static FirebaseAuth auth; + private static UserRecord testUser1; + private static UserRecord testUser2; + private static UserRecord testUser3; + private static String importUserUid; + + @BeforeClass + public static void setUpClass() throws Exception { + FirebaseApp masterApp = IntegrationTestUtils.ensureDefaultApp(); + auth = FirebaseAuth.getInstance(masterApp); + + testUser1 = FirebaseAuthIT.newUserWithParams(auth); + testUser2 = FirebaseAuthIT.newUserWithParams(auth); + testUser3 = FirebaseAuthIT.newUserWithParams(auth); + + FirebaseAuthIT.RandomUser randomUser = FirebaseAuthIT.RandomUser.create(); + importUserUid = randomUser.uid; + String phone = FirebaseAuthIT.randomPhoneNumber(); + UserImportResult result = auth.importUsers(ImmutableList.of( + ImportUserRecord.builder() + .setUid(randomUser.uid) + .setEmail(randomUser.email) + .setPhoneNumber(phone) + .addUserProvider( + UserProvider.builder() + .setProviderId("google.com") + .setUid("google_" + randomUser.uid) + .build()) + .build() + )); + assertEquals(1, result.getSuccessCount()); + assertEquals(0, result.getFailureCount()); + } + + @AfterClass + public static void cleanup() throws Exception { + // TODO(rsgowman): deleteUsers (plural) would make more sense here, but it's currently rate + // limited to 1qps. When/if that's relaxed, change this to just delete them all at once. + auth.deleteUser(testUser1.getUid()); + auth.deleteUser(testUser2.getUid()); + auth.deleteUser(testUser3.getUid()); + auth.deleteUser(importUserUid); + } + + @Test + public void testVariousIdentifiers() throws Exception { + GetUsersResult result = auth.getUsersAsync(ImmutableList.of( + new UidIdentifier(testUser1.getUid()), + new EmailIdentifier(testUser2.getEmail()), + new PhoneIdentifier(testUser3.getPhoneNumber()), + new ProviderIdentifier("google.com", "google_" + importUserUid) + )).get(); + + Collection expectedUids = ImmutableList.of( + testUser1.getUid(), testUser2.getUid(), testUser3.getUid(), importUserUid); + + assertTrue(sameUsers(result.getUsers(), expectedUids)); + assertEquals(0, result.getNotFound().size()); + } + + @Test + public void testIgnoresNonExistingUsers() throws Exception { + UidIdentifier doesntExistId = new UidIdentifier("uid_that_doesnt_exist"); + GetUsersResult result = auth.getUsersAsync(ImmutableList.of( + new UidIdentifier(testUser1.getUid()), + doesntExistId, + new UidIdentifier(testUser3.getUid()) + )).get(); + + Collection expectedUids = ImmutableList.of(testUser1.getUid(), testUser3.getUid()); + + assertTrue(sameUsers(result.getUsers(), expectedUids)); + assertEquals(1, result.getNotFound().size()); + assertTrue(result.getNotFound().contains(doesntExistId)); + } + + @Test + public void testOnlyNonExistingUsers() throws Exception { + UidIdentifier doesntExistId = new UidIdentifier("uid_that_doesnt_exist"); + GetUsersResult result = auth.getUsersAsync(ImmutableList.of( + doesntExistId + )).get(); + + assertEquals(0, result.getUsers().size()); + assertEquals(1, result.getNotFound().size()); + assertTrue(result.getNotFound().contains(doesntExistId)); + } + + @Test + public void testDedupsDuplicateUsers() throws Exception { + GetUsersResult result = auth.getUsersAsync(ImmutableList.of( + new UidIdentifier(testUser1.getUid()), + new UidIdentifier(testUser1.getUid()) + )).get(); + + Collection expectedUids = ImmutableList.of(testUser1.getUid()); + + assertEquals(1, result.getUsers().size()); + assertTrue(sameUsers(result.getUsers(), expectedUids)); + assertEquals(0, result.getNotFound().size()); + } + + /** + * Checks to see if the userRecords collection contains the given uids. + * + *

Behaviour is undefined if there are duplicate entries in either of the parameters. + */ + private boolean sameUsers(Collection userRecords, Collection uids) { + if (userRecords.size() != uids.size()) { + return false; + } + + for (UserRecord userRecord : userRecords) { + if (!uids.contains(userRecord.getUid())) { + return false; + } + } + + return true; + } +} diff --git a/src/test/java/com/google/firebase/auth/ImportUserRecordTest.java b/src/test/java/com/google/firebase/auth/ImportUserRecordTest.java index 011a5cc04..e2ae36c09 100644 --- a/src/test/java/com/google/firebase/auth/ImportUserRecordTest.java +++ b/src/test/java/com/google/firebase/auth/ImportUserRecordTest.java @@ -62,7 +62,7 @@ public void testAllProperties() throws IOException { .setDisplayName("Test User") .setPhotoUrl("https://test.com/user.png") .setPhoneNumber("+1234567890") - .setUserMetadata(new UserMetadata(date.getTime(), date.getTime())) + .setUserMetadata(new UserMetadata(date.getTime(), date.getTime(), date.getTime())) .setDisabled(false) .setEmailVerified(true) .setPasswordHash("password".getBytes()) From 30801e312b53215d60c8c9a79fa9f3b7c9cd1d00 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Wed, 13 May 2020 10:37:37 -0700 Subject: [PATCH 008/354] chore: Setting the version of the Maven Javadoc plugin (#412) --- pom.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pom.xml b/pom.xml index b71b4e773..d89d227e1 100644 --- a/pom.xml +++ b/pom.xml @@ -99,6 +99,7 @@ maven-javadoc-plugin + 2.10.4 site @@ -303,6 +304,7 @@ maven-javadoc-plugin + 2.10.4 attach-javadocs From a4a5315f621ef48e026ab9058590b7977a6d8873 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Thu, 14 May 2020 11:01:54 -0700 Subject: [PATCH 009/354] [chore] Release 6.13.0 (#413) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index d89d227e1..ca1657fb8 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ com.google.firebase firebase-admin - 6.12.3-SNAPSHOT + 6.13.0 jar firebase-admin From 200d1f9efebb03201026d7f3b92857b954099676 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Thu, 14 May 2020 12:05:15 -0700 Subject: [PATCH 010/354] [chore] Release 6.13.0 take 2 (#414) * Upgated the gpg keys * Added temp verify script * Disabled tty for gpg import * Removing temp verification script * Updated publish commands --- .github/resources/firebase.asc.gpg | Bin 4056 -> 4061 bytes .github/scripts/publish_artifacts.sh | 2 +- .github/workflows/release.yml | 16 ++++++++-------- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/resources/firebase.asc.gpg b/.github/resources/firebase.asc.gpg index a946776c7fd1498a0399836a94785dc8fd5293ed..8fd7d0769b6056896ebff240b9955656f7c98a60 100644 GIT binary patch literal 4061 zcmV<34>TRDh12twaxxgrB=~nu4Ie_l{B4=u%Z!ZxI0Yr=F9w=iT{b&g_ zw-c$@r|V{b{y-ay=@PtDT7)9C*C09hepY{5sNuZs*%)_x^`u}LsM4fZ+W>w_1huO( z%@c>7hIE>+#}dDaru{9lKFrrwJ~m)gys#I`B@&VNwDtH@F5_E$O!~J06yi;j2f+Lr=e)3IkT*mj|4P^3n6MwY;}sTk0@P%l&sf zVK}afzDH%-Ga~R6e&tt7&eFqLx~Kt9?Ch$wFpKravh*+vc|l1uZmRT zQg6pu(L&zpsvS1Fb0qpUAjJEAsZi&$WXyKuG&`;?AOE@od`tm&azIt-fvFjs$A77A zfwiwhOi#eI35UZx0i*oi8-BQlmhFg1W38YzFUkDZSkQT$cL`$mecv+p`NPne#*Q-` z^a}Xh-T%r6-fU1CT|MxK0}z0VwojB5_C35grZ}bVI<37u$q9V-snzMXRL`QH_s;pd z+JH9TQ3Tw1_n<7(glRHMhS2AYVoU$#GSlj$XkS710%aSznI4joqCgvZ8J;d2{xi-G zK#|Pk6-9oXY5<5P6d%XxOaui|Bvt2T5dgXZ-Z%fG=6C|0j14qIKw&}K-pT;f7#{&9 zrBvU-Hdib)$r$$lx#w4fIXS;V_9N4S`0CHoS!gc*q<#Weu{g2yOR0_ zLbV|rO2){j48X0>)z~!Wk735N1OfNmiePSpB~vNurH6r%&9R^AwdTu!-=RB@6$tOu zFD=1luY?>%-(4`F8+^R6DLU_c{Nf$L_S8=~nt^V~jg>_`a?(rdv4M}co;tcE84tR_ zbAFf;!T+q!6?VThSH2VesUu>vHl-E^v@#LXC~kLnjP}6%{CCYDQ(3wh5V?QDqf2px zUmyq|`daYnw_i~B8m^)fz5@b!?m`tNh|z^!xxV8Xw6G+gqxC}?hW67f{z~FZq59W6 z^^@;&xL)PZHT$aB%p%!D9HP&qJscOSi=^~T{BaEaVnLFx(e3PN$e%KKcYSXCR11LpRq)QJWnyvh&58kSVK<%e=hI% zdOEP>StfE5yeD99e2B+Q=BP+~X2_;q4*+0!vY}E5Iq4$uVb&`1b)u6Ib4Z>3DYBbD zydh-S5Km;^20iFd-8#w>=r_PDxMpR5Z)Sv}fjB8ED2G=fD)CevCX&$?%kMrRGRQKr zlhjT$1oB1@bC+L!g5=3!P#@>zWm#0DBHi3d>V)*~&=xo02z@S9xX$~fLIgt337n=y zWC&~j_SM?|$JU{?Hj-B4eYqcEE6HK`ewdgRj{E``^)!rAcn&Y(E(Su6x2*k5QqFkdLqoSylqxE(D&K1sj+ zq{)|j&VLF{*+I${cFiX^m6e|Mg>OOhKdY3Ug+;t{+hO9BqxN+zT`;vAaz9gKi5&t;6N9hQC8Jh~Lb&mxdoDzRfgu=JkqV@QlhVO z#DG|EbK$Bl5m4~9n;t{`A=*xPn+VP{ZOPTF%Rix5qI?tm5~H)4L(9AOa*FcC#!4h$ zay6GGE+!(A@34Nh-_9@+-IAn}t^q~`fp6Ypgk?7&5IM6cdlo=oo<2Y)evnV$5N0Q9 z(d^pc&3h9aPo{Ri`3mGV-(Z*yre1c7xmhKmCIWF2fAy~B62OYXzIh7q)+rjmx2d%r zG&BL~G9NN%Wdu;P6A-}l7mB3#;};>bLJu%Zf5*LD!SQxit_ydd%RCSOG)5T2ZN++v znv=;S8`vSB0@e|sS#z#20S`Z-L?o`0z3Tj})7$;JX=*RR)KqnjZ9!=mpbJt8hpsRR zS98jB&cJm}8r>@$rK}rb`>AihF?l#SJOC>LChFmA3>=2detiMok)NLr>PR|A>_hZ+ zIx>&#L=O4sapFtit zwi+1b%q~hcr{1P-_W3FUYfdlArUU_HKeOSBakbl zGsPs&JSxcOA7$L&B!TI@HwL}Ed9#lEQ4Ph+M3!tREoCx>p|;=tr`)5uLr1<4!4)$M#KvhVNWK&n4m4$pu};hZKI;2? zRD}0kq_+arS4lQ0J zyV!~G@};dT53c&SFB-gJ%KFJyvgKTO?69z~7}SrHXCB-|pU>@Jx71J3ZEv_<5Q8q) zkt`{Wh#aZ~1)7|o*dGQktcJ+0_NYL3UxK6pUUWVikKFc-=fzqrniwP!K7%dNV`Rvm zwI5o@g$isY*jX&K{GZ?i=+SmL_Vul(ROv#oMj{eqRioz26fx7Tpdov3mdKzW)}r0r z2!CL#4s6j<&CH~ls&lZaPn7>%%o}NlnG%5DO;d#Dwt&;6NvEy>sN=`7db^O^)i)$~ z4@&yo4XW4S|t{h`jM_)%krF4g_bJcZ6>dk`_WNZ4nHcF@2(5IuCgy$!3U2@`Z2TnKK~3Xl0JTzVNg}$Z(RpH)to$6lf|FFF zGS{o&YE@nLc^U^}fNxAWgl|JrZIY|$^(|wjvFS$3Cf0W)kFuCTQrCDLnnWY=8a7Q6 zQM`YT4>_ktr+<{7=q5A^xn=4^tSmyIs;)BcW9>{eoHtbTN*p%9WltA)oxjT&D3Mb-E!8h6)>= ztCx0Ml9{PAY|aDh>Ldw^9al>|o?eW><8J`aY0>~&&=b=Q0(KrCb{}LpX!bg7v_HDx z`KM|v-ie?zh?24vxs2;fPVS`gVas!_;C29sG6Osdq%p?@nZ!ON-vpZz-g z>58&!*nw1zH-!A5`bI2^`O_r)zq#;3@xFVy#*S+OlTR@21g0ARQSD&UkE1uzyd8E3 zgLcJ9*X57fRX4uTJni0buamcXX37vnR&BL8zFMO}Lgc9qO`c)oY5=%a$4*RKMQ3F? zSRs(o+f_N`CSh2lO5sA9kHbsK*Yr%>yFADTS^~|}A*AATR6O^!#Kvd!cbY963!(|l z^nkJ+FJe?ski7dpkYt)sNZzA#RKqd0rr{vfoQYI}1U|pGDt^;=g;sC4L$8gUwrT3; zEUDvmKIN@72!otR4vr0dMaHq-)oQ}~a{lHfV#nKZ+-QC7>!iWG;-OA}N3XFq=`RG| z$LQHwMAu@sDw}|UE-b*9ZJS0^SCdD0_Qlkf_%uAQ(U{diEIPV4@j z<<`PYX%@`#0!a0tdWO*|3C1FrKnAiL!1SerD<}nDCQ z2so>P8Xz5P`%krULZ%hwWtW!=gEFCn@5ZoJkLvVsjQj?_gTijiyjQd;s;+?fEMEIl zCW_+-tHe1(pA!HtoTeq|lxrM{iU@)90oap)GEIqUB-CIKkMVwgc7A5iMm|VXlX9c;YSx z3RIqP!LqF6ofr8ell_En&(EMvLLs1qA4|f=kV=jbPQ76z_u0iIsBI((a|_S(VY-`*o&I+#7og2mvd1bVY^0 zA*7F*e;zlI^~iUzzhHPPLG^UG8LnIYHnC=D_HwMyZ8+%J>EEXHy*$CCin!zIgn)r& zeHOTG)WpHuyy3PDf7ogl1~I`lrUe)%qVtb$eDdDeZ@A8iku6Tjet@7P)bfU7#guou zNb?bve9@1LDToX%)Ev253ot{ZD&&1G#`Y`jLmP#YO>Q=2tLod!NV>DwuooOnEEYwd z%wu*~D`sKW5)K2S;}>U+y_=@mAnxRQ6opOoWzm#mRSppcyD!tUjZM|;Px{@dP zBG{0!Sh6P!W{N>7`Iv7y{~m4XVvzfBP=^b<7s5>mK*)n!RHgfXGw_f31aJ&4N^sd} zKi}q$0l3trF9dJx;9_Px#Vj?A_#0mF_z0|j*R$(DO7Xr zZpNqdnM~+ZY>e@$;S(kIV-V^a@J8BL)H&)4@&9HzfXT=Il+j@CSI@?zoDiESCgN>^ z#pHteO6U&MCvWmwSq8|;RRUvm<3^0BE|>K0i~@WuuaYpOz&I*9xh8}|@dg%V&CF|L zjFb)OOOe8-KaVW=c#S`Ss}RlrOZ?#O*e$Zfq;e%ryTPGShAnu~lCvuHZMrihx7xlx zOldQSRd0@j9j--aCzT7^QKEl=V$5g~I@@Wwp>jI3>!f7QuG#g>wSdkhT2p?%dzVVg5laj~V|q>A|JFUL}b98ib12!uE-b?Bau8(pUW}a91l>SVGGx;XOV)6025jq&C z=twVclr4(}0I?N&^aaVxtCBQCR))pC@EqriFb%+-DI5CrhP_PhAa9Y7I)@w zAM_59o0Qvqd3}Z+V6=o`M>WWVXE+&!?nF zLvnv8yA!_8okUw9?3-ONUs2IYTt}$V2|G^fgO<7OMVu!PXI9%Et z5s1`~8iy6(0=AAfa8|uITcKp490(SSVaum>4=Jh>ronEnW>OrY9wJ!mAe9Nte2*on?U zT?a*nuNb5(!8XBRAX&Flbv`2-6JPU)+QZd~>Y47db7Jx#eK<&zabmE98=cxKS(4eH z`~LxwSvU7Ve(gS#jSZH>(QfE86u7?#JTl6+$H~qAfD&Azhw=VtmZGh1f}l5yRriKO zX3M|cmHms6?b7brMTiPh)v#(~tj$p{+VI59Ahkf0I%tVP+Xj?1R=dnULjCiSmB;Cc zzX@;6nt{_4HjG2s=ocb|Z(~geFdCvH!qGK3{H6$Tt;%08lj+{Xb`OMw=DGkjlV{h_ zx9iiwLi3&5l>!iJ$*7<_?7dQnf`W4u`o2_78XQrmKSF^Z!L(q$2Cx*)8^bN}x$4h2 zPc!gA|Ee6=3VN*$WfOlPn2oiNcVVv4l}{Lag$6_g#i{+wc3b{JPE})$0Ls`Ly8NKx zaLzE8Ox77z1_sYV%zeTx%^~OcbNhtxQXGc!?+7{iV6Y1^7Gz;g&;d;w>0lgUe22E~ zroyyWSlW?m69Sv_H8w6(} z+xj5L1e3|mf@0}hxL)4wM&~%rV~a8p(g*_onDrC7%H3ckPbodKsE&y}hUW^iPB0YS zKQVl&_8p>?Iz|#3+QB^u4(nN<>I+5Os+4wTYOmAEP0i5wS%O$RFCHYuBN(C%R6Hs; zOe)l~$Ku1MN0fcI8&0cra9jplsOyUl3?+lOylU)YyUv8f2az|~#pXg{P<%~TSPV&% zyM^15lsNqI$ClcwXIjr;vmG5Alzg23T>^XwXFP=lXA|xipmA^L=c_Yb5tug$9cuS$ z25^~Q8azMB>oq7o#U-|4SIq)>HQp2Iy!uc>s#*_X71mG*Qm32k%8h<9w0}0LhcGd3 zxYyQGK+!}J1_V$Y>EobOVhcOY!xcBR5CA}j2WC-@<=w9j)-`uUaj#DLF!_HznM#Lz zY%1K9k61k8JS!~W!OCJ@!r+fds8CMcc^4VVHOp*umI1e0un{e=lkW@3-8A6{H$63bSEi^yFI;=q3{WtI|mI_}hmX5=8hD3!XCA4jA)ty$VeRhT!cfcTUV+ zK3XpIA(E-?V%<@jAurSB3lw2Ip%BO>p|Tu2=1Eez7S{=0wQmt8Yv{KmyNz(#8^qlq zW7rZ}3i8jb5eukdrQv0!PQOcOhev=~y-T+%1Mv+VJr2tJ7Mu$PMenudb8iF(prngJ zIEuBG;~hkP?>IDcTO@3vUC#;HCv$Sv!VdM#5&0>P-v&(K2e2Hzx{K*a>fXmcR1wqe3Z8{5+CFGDx5l!PvCe#wMNn%eApGB0;`?EY{JZHY`28~L zB=22wfOTv01{hJNXn8(G!PWNTuNq|dB)6xI$2^`NHro7vTmPmbFM<^KefX_TSPTg; zl*h&qo-dZ+$%a5yUNi+tbm(|zu={)Do3<1A4CNiurj6(It`Frh*sGKl=1UQN^^E^I z9FAjNimgI(YM*F+B6aANlQ&8Z9gXhok+j^{6zSo(cN8!i{KS9X+s{!r#x^7@kbth) z$+b5ul8vGL{J!_#OB^uHd&4K3dBk7rB~J$)fo;OFm%@aVAGE(ZPCZ1PrH(KtMo1~t z+Y9X5?o;j~Z5MN=C*A$7-a(UHc|fPyuFh^e!{~wJAJ9AzO;jj^hX#tL2Y@Me2++(m z){lC!c;^egc?B^a)Fla4>M3YTZ#5b=&OciVJCQDF<h~nMeWb9n*u6nmnT!UW{+K-Fpba>kTBsChG!vmLMk7CNGIo3=d-`YOBWZ@wT1{K^ zD0*+k1^Uy=_fL1&o()tA>K3E944{K;J8f6c=M9sXM?|H>?rUP&o0@7=QYB5|+Q{s^ zu4p?+s8d$@zcO6->L*;nGJXL_Y3Sv^_yF z=G;x4p%gDB0zUvhxk#G?iGD8Yk5dy6yGI5VlHX&d@l4ebjr;x8?iHtp&U%4@ZLjf~ zt%J+l3NK6v&&7VEwTV&Ely+pvh~5-Cs&qAo&5S*YA8?Jj?X}*PKE{0@b!=_Sg2^Ux zY{w0(nR)c$p3~SsxwI^&uCV}%z7oL$+ciBkJZ5aR0UA%cw-&;vy$|~_jFK`EQwP+( z4KwsI@xrjC-9h$FmJg-36Om%AG}fRW`H*+hfb)XOloW1Z3L7f{ub%JdpIJUgPA-bq6BG|q_Fs-h;T z>yDbjZIOs91!|4^+n`~dYA4JH+1LBHCNjabPA+o& z9XgjmGBm>#@WC!hDn9~uqaId;^A`Ny^~BNK8nF713ReS%2Grj5^oShCV@jF}dL?^s z*gf`7^qQ2G6bNwg(Sz&

z4r*r|VPWM6hN{&7xv$RQO}Zn7RnRZ!)V9sRB$hC;}* KX);zY-!1|?!O3F) diff --git a/.github/scripts/publish_artifacts.sh b/.github/scripts/publish_artifacts.sh index f4a2f1734..cd1a5b75c 100755 --- a/.github/scripts/publish_artifacts.sh +++ b/.github/scripts/publish_artifacts.sh @@ -20,7 +20,7 @@ set -u gpg --quiet --batch --yes --decrypt --passphrase="${GPG_PRIVATE_KEY}" \ --output firebase.asc .github/resources/firebase.asc.gpg -gpg --import firebase.asc +gpg --import --no-tty --batch --yes firebase.asc # Does the following: # 1. Compiles the source (compile phase) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0e1c307bc..985ce94d6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -91,6 +91,14 @@ jobs: id: preflight run: ./.github/scripts/publish_preflight_check.sh + - name: Publish to Maven Central + run: ./.github/scripts/publish_artifacts.sh + env: + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} + NEXUS_OSSRH_USERNAME: ${{ secrets.NEXUS_OSSRH_USERNAME }} + NEXUS_OSSRH_PASSWORD: ${{ secrets.NEXUS_OSSRH_PASSWORD }} + # We pull this action from a custom fork of a contributor until # https://github.com/actions/create-release/pull/32 is merged. Also note that v1 of # this action does not support the "body" parameter. @@ -105,14 +113,6 @@ jobs: draft: false prerelease: false - - name: Publish to Maven Central - run: ./.github/scripts/publish_artifacts.sh - env: - GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} - GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} - NEXUS_OSSRH_USERNAME: ${{ secrets.NEXUS_OSSRH_USERNAME }} - NEXUS_OSSRH_PASSWORD: ${{ secrets.NEXUS_OSSRH_PASSWORD }} - # Post to Twitter if explicitly opted-in by adding the label 'release:tweet'. - name: Post to Twitter if: success() && From 68c05d7e19c4f2e91a14b650753019f15ef2e05a Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Thu, 14 May 2020 12:16:43 -0700 Subject: [PATCH 011/354] [chore] Release 6.13.0 take 3 (#415) --- .github/resources/settings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/resources/settings.xml b/.github/resources/settings.xml index 708bbcb75..4afbaca26 100644 --- a/.github/resources/settings.xml +++ b/.github/resources/settings.xml @@ -21,7 +21,7 @@ gpg - B652FFD3865AF7A75830876F5F55C8F6985BB9DD + A9B90B41060565F56F348F948B6B459CFD695DE8 ${env.GPG_PASSPHRASE} From 8c9608dbe300cc7d0c6ddff98e3ab51dc41c05ea Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Thu, 14 May 2020 12:31:01 -0700 Subject: [PATCH 012/354] [chore] Release 6.13.0 take 4 (#416) --- pom.xml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pom.xml b/pom.xml index ca1657fb8..6a2989169 100644 --- a/pom.xml +++ b/pom.xml @@ -174,6 +174,12 @@ sign + + + --pinentry-mode + loopback + + From a6b81e8b597d390cc9d4728ddaf54a2d6f3e0357 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Tue, 19 May 2020 10:59:49 -0700 Subject: [PATCH 013/354] chore: Updated integration tests to use non-public rules (#417) --- .../firebase/database/integration/OrderByTestIT.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/test/java/com/google/firebase/database/integration/OrderByTestIT.java b/src/test/java/com/google/firebase/database/integration/OrderByTestIT.java index f1cc99dca..2160406eb 100644 --- a/src/test/java/com/google/firebase/database/integration/OrderByTestIT.java +++ b/src/test/java/com/google/firebase/database/integration/OrderByTestIT.java @@ -19,7 +19,6 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; -import com.google.api.client.json.GenericJson; import com.google.common.collect.ImmutableList; import com.google.firebase.FirebaseApp; import com.google.firebase.database.ChildEventListener; @@ -66,7 +65,9 @@ public static void setUpClass() { @AfterClass public static void tearDownClass() throws IOException { - uploadRules(masterApp, "{\"rules\": {\".read\": true, \".write\": true}}"); + uploadRules( + masterApp, + "{\"rules\": {\".read\": \"auth != null\", \".write\": \"auth != null\"}}"); } @Before @@ -81,7 +82,8 @@ public void checkAndCleanupApp() { private static String formatRules(DatabaseReference ref, String rules) { return String.format( - "{\"rules\": {\".read\": true, \".write\": true, \"%s\": %s}}", ref.getKey(), rules); + "{\"rules\": {\".read\": \"auth != null\", \".write\": \"auth != null\", \"%s\": %s}}", + ref.getKey(), rules); } private static void uploadRules(FirebaseApp app, String rules) throws IOException { @@ -922,7 +924,7 @@ public void onComplete(DatabaseError error, DatabaseReference ref) { @Test public void testStartAtAndEndAtOnValueIndex() - throws InterruptedException, ExecutionException, TimeoutException, TestFailure, IOException { + throws InterruptedException, ExecutionException, TimeoutException, TestFailure { DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp) ; Map initial = @@ -982,7 +984,7 @@ public void onChildAdded(DataSnapshot snapshot, String previousChildName) { @Test public void testRemovingDefaultListener() - throws InterruptedException, ExecutionException, TimeoutException, TestFailure, IOException { + throws InterruptedException, ExecutionException, TimeoutException, TestFailure { DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp) ; Object initialData = MapBuilder.of("key", "value"); From b128b9bc03305c6ee9fae62787a2a63821b92c11 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Tue, 9 Jun 2020 12:03:01 -0700 Subject: [PATCH 014/354] chore: Upgraded GCP and other util dependencies (#438) --- pom.xml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pom.xml b/pom.xml index 6a2989169..c663e2c7c 100644 --- a/pom.xml +++ b/pom.xml @@ -59,7 +59,7 @@ UTF-8 UTF-8 ${skipTests} - 4.1.45.Final + 4.1.50.Final @@ -393,37 +393,37 @@ com.google.api-client google-api-client - 1.30.1 + 1.30.9 com.google.api-client google-api-client-gson - 1.30.1 + 1.30.9 com.google.http-client google-http-client - 1.30.1 + 1.35.0 com.google.api api-common - 1.8.1 + 1.9.2 com.google.auth google-auth-library-oauth2-http - 0.17.1 + 0.20.0 com.google.cloud google-cloud-storage - 1.91.0 + 1.108.0 com.google.cloud google-cloud-firestore - 1.31.0 + 1.34.0 From 3485c98f42d483b2336aa4357546f6d56d2a9346 Mon Sep 17 00:00:00 2001 From: Pavlos-Petros Tournaris Date: Tue, 9 Jun 2020 22:43:37 +0300 Subject: [PATCH 015/354] Add FcmOptions on MulticastMessage (#439) --- .../firebase/messaging/MulticastMessage.java | 15 ++++++++++++++- .../firebase/messaging/MulticastMessageTest.java | 5 ++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/google/firebase/messaging/MulticastMessage.java b/src/main/java/com/google/firebase/messaging/MulticastMessage.java index 96a6f19db..b15e58e5f 100644 --- a/src/main/java/com/google/firebase/messaging/MulticastMessage.java +++ b/src/main/java/com/google/firebase/messaging/MulticastMessage.java @@ -51,6 +51,7 @@ public class MulticastMessage { private final AndroidConfig androidConfig; private final WebpushConfig webpushConfig; private final ApnsConfig apnsConfig; + private final FcmOptions fcmOptions; private MulticastMessage(Builder builder) { this.tokens = builder.tokens.build(); @@ -64,6 +65,7 @@ private MulticastMessage(Builder builder) { this.androidConfig = builder.androidConfig; this.webpushConfig = builder.webpushConfig; this.apnsConfig = builder.apnsConfig; + this.fcmOptions = builder.fcmOptions; } List getMessageList() { @@ -71,7 +73,8 @@ List getMessageList() { .setNotification(this.notification) .setAndroidConfig(this.androidConfig) .setApnsConfig(this.apnsConfig) - .setWebpushConfig(this.webpushConfig); + .setWebpushConfig(this.webpushConfig) + .setFcmOptions(this.fcmOptions); if (this.data != null) { builder.putAllData(this.data); } @@ -99,6 +102,7 @@ public static class Builder { private AndroidConfig androidConfig; private WebpushConfig webpushConfig; private ApnsConfig apnsConfig; + private FcmOptions fcmOptions; private Builder() {} @@ -170,6 +174,15 @@ public Builder setApnsConfig(ApnsConfig apnsConfig) { return this; } + /** + * Sets the {@link FcmOptions}, which can be overridden by the platform-specific {@code + * fcm_options} fields. + */ + public Builder setFcmOptions(FcmOptions fcmOptions) { + this.fcmOptions = fcmOptions; + return this; + } + /** * Adds the given key-value pair to the message as a data field. Key or the value may not be * null. diff --git a/src/test/java/com/google/firebase/messaging/MulticastMessageTest.java b/src/test/java/com/google/firebase/messaging/MulticastMessageTest.java index 25018ed4c..1ea57d8d5 100644 --- a/src/test/java/com/google/firebase/messaging/MulticastMessageTest.java +++ b/src/test/java/com/google/firebase/messaging/MulticastMessageTest.java @@ -39,6 +39,7 @@ public class MulticastMessageTest { .putData("key", "value") .build(); private static final Notification NOTIFICATION = new Notification("title", "body"); + private static final FcmOptions FCM_OPTIONS = FcmOptions.withAnalyticsLabel("analytics_label"); @Test public void testMulticastMessage() { @@ -47,6 +48,7 @@ public void testMulticastMessage() { .setApnsConfig(APNS) .setWebpushConfig(WEBPUSH) .setNotification(NOTIFICATION) + .setFcmOptions(FCM_OPTIONS) .putData("key1", "value1") .putAllData(ImmutableMap.of("key2", "value2")) .addToken("token1") @@ -96,7 +98,8 @@ private void assertMessage(Message message, String expectedToken) { assertSame(APNS, message.getApnsConfig()); assertSame(WEBPUSH, message.getWebpushConfig()); assertSame(NOTIFICATION, message.getNotification()); + assertSame(FCM_OPTIONS, message.getFcmOptions()); assertEquals(ImmutableMap.of("key1", "value1", "key2", "value2"), message.getData()); assertEquals(expectedToken, message.getToken()); } -} \ No newline at end of file +} From 2fa81c701159265a9aaf8a15ca427254300a4e4c Mon Sep 17 00:00:00 2001 From: chong-shao <31256040+chong-shao@users.noreply.github.com> Date: Mon, 15 Jun 2020 13:55:46 -0700 Subject: [PATCH 016/354] Support direct_boot_ok parameter (#329) --- .../firebase/messaging/AndroidConfig.java | 14 +++++++++++ .../firebase/messaging/MessageTest.java | 23 +++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/src/main/java/com/google/firebase/messaging/AndroidConfig.java b/src/main/java/com/google/firebase/messaging/AndroidConfig.java index dc0a45526..d16190881 100644 --- a/src/main/java/com/google/firebase/messaging/AndroidConfig.java +++ b/src/main/java/com/google/firebase/messaging/AndroidConfig.java @@ -51,6 +51,9 @@ public class AndroidConfig { @Key("fcm_options") private final AndroidFcmOptions fcmOptions; + + @Key("direct_boot_ok") + private final Boolean directBootOk; private AndroidConfig(Builder builder) { this.collapseKey = builder.collapseKey; @@ -75,6 +78,7 @@ private AndroidConfig(Builder builder) { this.data = builder.data.isEmpty() ? null : ImmutableMap.copyOf(builder.data); this.notification = builder.notification; this.fcmOptions = builder.fcmOptions; + this.directBootOk = builder.directBootOk; } /** @@ -103,6 +107,7 @@ public static class Builder { private final Map data = new HashMap<>(); private AndroidNotification notification; private AndroidFcmOptions fcmOptions; + private Boolean directBootOk; private Builder() {} @@ -201,6 +206,15 @@ public Builder setFcmOptions(AndroidFcmOptions androidFcmOptions) { return this; } + /** + * Sets the direct_boot_ok flag, If set to true, messages will be allowed to be delivered to + * the app while the device is in direct boot mode. + */ + public Builder setDirectBootOk(boolean directBootOk) { + this.directBootOk = directBootOk; + return this; + } + /** * Creates a new {@link AndroidConfig} instance from the parameters set on this builder. * diff --git a/src/test/java/com/google/firebase/messaging/MessageTest.java b/src/test/java/com/google/firebase/messaging/MessageTest.java index 778b4f109..c97fb6b67 100644 --- a/src/test/java/com/google/firebase/messaging/MessageTest.java +++ b/src/test/java/com/google/firebase/messaging/MessageTest.java @@ -200,6 +200,29 @@ public void testAndroidMessageWithNotification() throws IOException { assertJsonEquals(ImmutableMap.of("topic", "test-topic", "android", data), message); } + @Test + public void testAndroidMessageWithDirectBootOk() throws IOException { + Message message = Message.builder() + .setAndroidConfig(AndroidConfig.builder() + .setDirectBootOk(true) + .setNotification(AndroidNotification.builder() + .setTitle("android-title") + .setBody("android-body") + .build()) + .build()) + .setTopic("test-topic") + .build(); + Map notification = ImmutableMap.builder() + .put("title", "android-title") + .put("body", "android-body") + .build(); + Map data = ImmutableMap.of( + "direct_boot_ok", true, + "notification", notification + ); + assertJsonEquals(ImmutableMap.of("topic", "test-topic", "android", data), message); + } + @Test(expected = IllegalArgumentException.class) public void testAndroidNotificationWithNegativeCount() throws IllegalArgumentException { AndroidNotification.builder().setNotificationCount(-1).build(); From cc93b06796091de3207def69e8d23482fba52e7f Mon Sep 17 00:00:00 2001 From: rsgowman Date: Tue, 16 Jun 2020 14:25:14 -0400 Subject: [PATCH 017/354] Fixed a flaky auth integration test by retrying the GetUser() API call a few times with a small delay (#442) --- .../com/google/firebase/auth/FirebaseAuthIT.java | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java index aa45f51b3..2915d1ddd 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java +++ b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java @@ -336,7 +336,20 @@ public void testLastRefreshTime() throws Exception { // Login to cause the lastRefreshTimestamp to be set. signInWithPassword(newUserRecord.getEmail(), "password"); - UserRecord userRecord = auth.getUser(newUserRecord.getUid()); + // Attempt to retrieve the user 3 times (with a small delay between each + // attempt). Occassionally, this call retrieves the user data without the + // lastLoginTime/lastRefreshTime set; possibly because it's hitting a + // different server than the login request uses. + UserRecord userRecord = null; + for (int i = 0; i < 3; i++) { + userRecord = auth.getUser(newUserRecord.getUid()); + + if (userRecord.getUserMetadata().getLastRefreshTimestamp() != 0) { + break; + } + + TimeUnit.SECONDS.sleep((long)Math.pow(2, i)); + } // Ensure the lastRefreshTimestamp is approximately "now" (with a tollerance of 10 minutes). long now = System.currentTimeMillis(); From ebb0c9475a4fa6ffabc245249dd81aee78b7bf45 Mon Sep 17 00:00:00 2001 From: Horatiu Lazu Date: Wed, 17 Jun 2020 21:25:17 +0000 Subject: [PATCH 018/354] [chore] Release 6.14.0 (#444) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index c663e2c7c..49e72ad8b 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ com.google.firebase firebase-admin - 6.13.0 + 6.14.0 jar firebase-admin From f938b3b06fcc01141a7b2e706972bc7a9b970928 Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Fri, 19 Jun 2020 14:39:56 -0400 Subject: [PATCH 019/354] Fix documentation strings in Android Notification (#445) * Fix doc strings * Fix code font * Fix punctuation --- .../java/com/google/firebase/messaging/AndroidConfig.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/google/firebase/messaging/AndroidConfig.java b/src/main/java/com/google/firebase/messaging/AndroidConfig.java index d16190881..888f00f0d 100644 --- a/src/main/java/com/google/firebase/messaging/AndroidConfig.java +++ b/src/main/java/com/google/firebase/messaging/AndroidConfig.java @@ -198,7 +198,7 @@ public Builder setNotification(AndroidNotification notification) { } /** - * Sets the {@link AndroidFcmOptions}, which will override values set in the {@link FcmOptions} + * Sets the {@link AndroidFcmOptions}, which overrides values set in the {@link FcmOptions} * for Android messages. */ public Builder setFcmOptions(AndroidFcmOptions androidFcmOptions) { @@ -207,7 +207,7 @@ public Builder setFcmOptions(AndroidFcmOptions androidFcmOptions) { } /** - * Sets the direct_boot_ok flag, If set to true, messages will be allowed to be delivered to + * Sets the {@code direct_boot_ok} flag. If set to true, messages are delivered to * the app while the device is in direct boot mode. */ public Builder setDirectBootOk(boolean directBootOk) { From eeee30b52bb6340aa1653da21645c34678e49070 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Fri, 10 Jul 2020 11:19:49 -0700 Subject: [PATCH 020/354] fix: Importing GCP dependencies via official BOMs (#451) * fix: Importing GCP dependencies via official BOMs * Upgraded api-client BOM to 1.30.10 --- pom.xml | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/pom.xml b/pom.xml index 49e72ad8b..e9a2fc038 100644 --- a/pom.xml +++ b/pom.xml @@ -388,49 +388,60 @@ + + + + com.google.cloud + libraries-bom + 8.0.0 + pom + import + + + com.google.api-client + google-api-client-bom + 1.30.10 + pom + import + + + + com.google.api-client google-api-client - 1.30.9 com.google.api-client google-api-client-gson - 1.30.9 com.google.http-client google-http-client - 1.35.0 com.google.api api-common - 1.9.2 com.google.auth google-auth-library-oauth2-http - 0.20.0 com.google.cloud google-cloud-storage - 1.108.0 com.google.cloud google-cloud-firestore - 1.34.0 com.google.guava guava - 26.0-android org.slf4j From 0a0662e61d91d40cbd725ffd70c7c0268f868cb9 Mon Sep 17 00:00:00 2001 From: Micah Stairs Date: Thu, 16 Jul 2020 17:25:21 -0400 Subject: [PATCH 021/354] feat(auth): Add tenant operations, tenant-aware user operations, and provider config operations (#395) * Pull parts of FirebaseAuth into an abstract class. (#352) This moves parts of FirebaseAuth into an abstract class as part of adding multi-tenancy support. * Add Tenant class and its create and update request classes. (#344) This pull request adds the Tenant class (including it's create/update inner classes) as part of adding multi-tenancy support. * Add ListTenantsPage class. (#358) Add ListTenantsPage and some supporting code as part of adding multi-tenancy support. This code was very largely based off of ListUsersPage and ListUsersPageTest. * Add updateRequest method to Tenant class and add unit tests. (#361) Added some things to the Tenant class and added a few unit tests. This is part of the initiative to adding multi-tenancy support (see issue #332). * Create TenantManager class and wire through listTenants operation. (#369) Add the TenantManager class and wire through the listTenants operation. Also add unit tests to FirebaseUserManagerTest. * Add deleteTenant operation to TenantManager. (#372) This adds deleteTenant to the TenantManager class. I've added the relevant unit tests to FirebaseUserManagerTest. This is part of the initiative to adding multi-tenancy support (see issue #332). * Add getTenant operation to TenantManager. (#371) Added getTenant to the TenantManager class. Also added the relevant unit tests to FirebaseUserManagerTest. This is part of the initiative to adding multi-tenancy support (see issue #332). * Add createTenant and updateTenant operations. (#377) Added createTenant and updateTenant to the TenantManager class. Also added the relevant unit tests to FirebaseUserManagerTest. This is part of the initiative to adding multi-tenancy support (see issue #332). * Add integration tests for TenantManager operations. (#385) This adds some integration testing for all of the tenant operations in TenantManager. Several bugs were uncovered after running the tests, so these have been fixed. This is part of the initiative to adding multi-tenancy support (see issue #332). * Add firebase auth destroy check before tenant operations. (#386) This addresses some TODOs left as part of the initiative to add multi-tenancy support (see issue #332). * Make user operations tenant-aware. (#387) This makes user operations tenant-aware. I've added some integration tests to ensure that this is working correctly. This is part of the initiative to adding multi-tenancy support (see issue #332). * Remove unused AutoValue dependency. (#392) Remove unused AutoValue dependency (and remove Java 8 API dependency which was accidentally introduced). * Indicate how to get set up for the multitenancy integration tests. (#393) This documentation is based off of the instructions in https://github.com/firebase/firebase-admin-node/blob/master/CONTRIBUTING.md. * Add tenant-aware token generation and verification. (#391) This incorporates the tenant ID into the token generation and validation when using a tenant-aware client. This is part of the initiative to add multi-tenancy support (see issue #332). * Fix javadoc comment. * Trigger CI * Make several Op methods private. * Move createSessionCookie and verifySessionCookie back to FirebaseAuth. * Make verifySessionCookieOp private. * Fix a few javadoc comments. * Address Kevin's feedback. * Make TenantAwareFirebaseAuth final. * chore: Merging master into tenant-mgt (#422) * Fixed a bad merge * Add provider config management operations. (#433) Adds all of the OIDC and SAML provider config operations, related to adding multi-tenancy support. * Stop using deprecated MockHttpTransport.builder() method. * Moved tenant management code into a new package (#449) * Multi-tenancy refactor experiment * fix(auth): Completed tenant mgt refactor * Added license header to new class * Responding to code review comments: Consolidated error codes in AuthHttpClient * Improve unit test coverage of tenant/provider-related code (#453) I've improved the unit test coverage of tenant/provider-related code, and I've also removed a number of unused imports. * Fix integration tests. --- CONTRIBUTING.md | 17 +- checkstyle.xml | 9 +- .../firebase/auth/AbstractFirebaseAuth.java | 1723 +++++++++++++++++ .../google/firebase/auth/FirebaseAuth.java | 1231 +----------- .../google/firebase/auth/FirebaseToken.java | 13 +- .../firebase/auth/FirebaseTokenUtils.java | 15 +- .../auth/FirebaseTokenVerifierImpl.java | 26 +- .../firebase/auth/FirebaseUserManager.java | 311 +-- .../auth/ListProviderConfigsPage.java | 268 +++ .../google/firebase/auth/ListUsersPage.java | 8 +- .../firebase/auth/OidcProviderConfig.java | 177 ++ .../google/firebase/auth/ProviderConfig.java | 157 ++ .../firebase/auth/SamlProviderConfig.java | 343 ++++ .../com/google/firebase/auth/UserRecord.java | 20 +- .../auth/internal/AuthHttpClient.java | 160 ++ .../internal/FirebaseCustomAuthToken.java | 13 + .../auth/internal/FirebaseTokenFactory.java | 13 +- .../auth/internal/GetAccountInfoResponse.java | 9 +- .../ListOidcProviderConfigsResponse.java | 62 + .../internal/ListProviderConfigsResponse.java | 32 + .../ListSamlProviderConfigsResponse.java | 62 + .../auth/internal/ListTenantsResponse.java | 55 + .../multitenancy/FirebaseTenantClient.java | 111 ++ .../auth/multitenancy/ListTenantsPage.java | 245 +++ .../firebase/auth/multitenancy/Tenant.java | 195 ++ .../multitenancy/TenantAwareFirebaseAuth.java | 50 + .../auth/multitenancy/TenantManager.java | 287 +++ .../google/firebase/auth/FirebaseAuthIT.java | 712 ++++--- .../firebase/auth/FirebaseAuthTest.java | 25 +- .../auth/FirebaseTokenVerifierImplTest.java | 51 + .../auth/FirebaseUserManagerTest.java | 1446 ++++++++++++-- .../com/google/firebase/auth/GetUsersIT.java | 14 +- .../auth/ListProviderConfigsPageTest.java | 376 ++++ .../firebase/auth/ListUsersPageTest.java | 33 +- .../firebase/auth/OidcProviderConfigTest.java | 160 ++ .../auth/ProviderConfigTestUtils.java | 125 ++ .../firebase/auth/SamlProviderConfigTest.java | 322 +++ .../google/firebase/auth/UserTestUtils.java | 124 ++ .../internal/FirebaseTokenFactoryTest.java | 20 + .../ListOidcProviderConfigsResponseTest.java | 70 + .../ListSamlProviderConfigsResponseTest.java | 70 + .../internal/ListTenantsResponseTest.java | 69 + .../FirebaseTenantClientTest.java | 363 ++++ .../multitenancy/ListTenantsPageTest.java | 349 ++++ .../TenantAwareFirebaseAuthIT.java | 450 +++++ .../auth/multitenancy/TenantManagerIT.java | 158 ++ .../auth/multitenancy/TenantTest.java | 92 + .../database/FirebaseDatabaseTest.java | 8 +- src/test/resources/getUser.json | 3 +- src/test/resources/listOidc.json | 15 + src/test/resources/listSaml.json | 35 + src/test/resources/listTenants.json | 17 + src/test/resources/listUsers.json | 6 +- src/test/resources/oidc.json | 7 + src/test/resources/saml.json | 17 + src/test/resources/tenant.json | 8 + 56 files changed, 9028 insertions(+), 1729 deletions(-) create mode 100644 src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java create mode 100644 src/main/java/com/google/firebase/auth/ListProviderConfigsPage.java create mode 100644 src/main/java/com/google/firebase/auth/OidcProviderConfig.java create mode 100644 src/main/java/com/google/firebase/auth/ProviderConfig.java create mode 100644 src/main/java/com/google/firebase/auth/SamlProviderConfig.java create mode 100644 src/main/java/com/google/firebase/auth/internal/AuthHttpClient.java create mode 100644 src/main/java/com/google/firebase/auth/internal/ListOidcProviderConfigsResponse.java create mode 100644 src/main/java/com/google/firebase/auth/internal/ListProviderConfigsResponse.java create mode 100644 src/main/java/com/google/firebase/auth/internal/ListSamlProviderConfigsResponse.java create mode 100644 src/main/java/com/google/firebase/auth/internal/ListTenantsResponse.java create mode 100644 src/main/java/com/google/firebase/auth/multitenancy/FirebaseTenantClient.java create mode 100644 src/main/java/com/google/firebase/auth/multitenancy/ListTenantsPage.java create mode 100644 src/main/java/com/google/firebase/auth/multitenancy/Tenant.java create mode 100644 src/main/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuth.java create mode 100644 src/main/java/com/google/firebase/auth/multitenancy/TenantManager.java create mode 100644 src/test/java/com/google/firebase/auth/ListProviderConfigsPageTest.java create mode 100644 src/test/java/com/google/firebase/auth/OidcProviderConfigTest.java create mode 100644 src/test/java/com/google/firebase/auth/ProviderConfigTestUtils.java create mode 100644 src/test/java/com/google/firebase/auth/SamlProviderConfigTest.java create mode 100644 src/test/java/com/google/firebase/auth/UserTestUtils.java create mode 100644 src/test/java/com/google/firebase/auth/internal/ListOidcProviderConfigsResponseTest.java create mode 100644 src/test/java/com/google/firebase/auth/internal/ListSamlProviderConfigsResponseTest.java create mode 100644 src/test/java/com/google/firebase/auth/internal/ListTenantsResponseTest.java create mode 100644 src/test/java/com/google/firebase/auth/multitenancy/FirebaseTenantClientTest.java create mode 100644 src/test/java/com/google/firebase/auth/multitenancy/ListTenantsPageTest.java create mode 100644 src/test/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuthIT.java create mode 100644 src/test/java/com/google/firebase/auth/multitenancy/TenantManagerIT.java create mode 100644 src/test/java/com/google/firebase/auth/multitenancy/TenantTest.java create mode 100644 src/test/resources/listOidc.json create mode 100644 src/test/resources/listSaml.json create mode 100644 src/test/resources/listTenants.json create mode 100644 src/test/resources/oidc.json create mode 100644 src/test/resources/saml.json create mode 100644 src/test/resources/tenant.json diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 11012bfff..c346c5b9d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -141,8 +141,17 @@ Authentication Admin` role at [Google Cloud Platform Console / IAM & admin](https://console.cloud.google.com/iam-admin). This is required to ensure that exported user records contain the password hashes of the user accounts. Also obtain the web API key of the project from the "Settings > General" page, and save it as -`integration_apikey.txt` at the root of the codebase. Now run the following command to invoke the -integration test suite: +`integration_apikey.txt` at the root of the codebase. + +Some of the integration tests require an +[Identity Platform](https://cloud.google.com/identity-platform/) project with multi-tenancy +[enabled](https://cloud.google.com/identity-platform/docs/multi-tenancy-quickstart#enabling_multi-tenancy). +An existing Firebase project can be upgraded to an Identity Platform project without losing any +functionality via the +[Identity Platform Marketplace Page](https://console.cloud.google.com/customer-identity). Note that +charges may be incurred for active users beyond the Identity Platform free tier. + +Now run the following command to invoke the integration test suite: ``` mvn verify @@ -153,14 +162,14 @@ tests, specify the `-DskipUTs` flag. ### Generating API Docs -Invoke the [Maven Javadoc plugin](https://maven.apache.org/plugins/maven-javadoc-plugin/) as +Invoke the [Maven Javadoc plugin](https://maven.apache.org/plugins/maven-javadoc-plugin/) as follows to generate API docs for all packages in the codebase: ``` mvn javadoc:javadoc ``` -This will generate the API docs, and place them in the `target/site/apidocs` directory. +This will generate the API docs, and place them in the `target/site/apidocs` directory. To generate API docs for only the public APIs (i.e. ones that are not marked with `@hide` tags), you need to trigger the `devsite-apidocs` Maven profile. This profile uses Maven Javadoc plugin diff --git a/checkstyle.xml b/checkstyle.xml index 663918173..77e2dba54 100644 --- a/checkstyle.xml +++ b/checkstyle.xml @@ -42,15 +42,15 @@ - + - - + + @@ -229,6 +229,9 @@ + + + diff --git a/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java b/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java new file mode 100644 index 000000000..ad30d2cc3 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java @@ -0,0 +1,1723 @@ +/* + * Copyright 2020 Google LLC + * + * 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 static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +import com.google.api.client.json.JsonFactory; +import com.google.api.client.util.Clock; +import com.google.api.core.ApiFuture; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Strings; +import com.google.common.base.Supplier; +import com.google.common.base.Suppliers; +import com.google.firebase.FirebaseApp; +import com.google.firebase.auth.FirebaseUserManager.EmailLinkType; +import com.google.firebase.auth.FirebaseUserManager.UserImportRequest; +import com.google.firebase.auth.ListProviderConfigsPage.DefaultOidcProviderConfigSource; +import com.google.firebase.auth.ListProviderConfigsPage.DefaultSamlProviderConfigSource; +import com.google.firebase.auth.ListUsersPage.DefaultUserSource; +import com.google.firebase.auth.internal.FirebaseTokenFactory; +import com.google.firebase.internal.CallableOperation; +import com.google.firebase.internal.NonNull; +import com.google.firebase.internal.Nullable; +import java.io.IOException; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * This is the abstract class for server-side Firebase Authentication actions. + */ +public abstract class AbstractFirebaseAuth { + + private static final String ERROR_CUSTOM_TOKEN = "ERROR_CUSTOM_TOKEN"; + + private final Object lock = new Object(); + private final AtomicBoolean destroyed = new AtomicBoolean(false); + + private final FirebaseApp firebaseApp; + private final Supplier tokenFactory; + private final Supplier idTokenVerifier; + private final Supplier cookieVerifier; + private final Supplier userManager; + private final JsonFactory jsonFactory; + + protected AbstractFirebaseAuth(Builder builder) { + this.firebaseApp = checkNotNull(builder.firebaseApp); + this.tokenFactory = threadSafeMemoize(builder.tokenFactory); + this.idTokenVerifier = threadSafeMemoize(builder.idTokenVerifier); + this.cookieVerifier = threadSafeMemoize(builder.cookieVerifier); + this.userManager = threadSafeMemoize(builder.userManager); + this.jsonFactory = builder.firebaseApp.getOptions().getJsonFactory(); + } + + protected static Builder builderFromAppAndTenantId(final FirebaseApp app, final String tenantId) { + return AbstractFirebaseAuth.builder() + .setFirebaseApp(app) + .setTokenFactory( + new Supplier() { + @Override + public FirebaseTokenFactory get() { + return FirebaseTokenUtils.createTokenFactory(app, Clock.SYSTEM, tenantId); + } + }) + .setIdTokenVerifier( + new Supplier() { + @Override + public FirebaseTokenVerifier get() { + return FirebaseTokenUtils.createIdTokenVerifier(app, Clock.SYSTEM, tenantId); + } + }) + .setCookieVerifier( + new Supplier() { + @Override + public FirebaseTokenVerifier get() { + return FirebaseTokenUtils.createSessionCookieVerifier(app, Clock.SYSTEM); + } + }) + .setUserManager( + new Supplier() { + @Override + public FirebaseUserManager get() { + return FirebaseUserManager + .builder() + .setFirebaseApp(app) + .setTenantId(tenantId) + .build(); + } + }); + } + + /** + * Creates a Firebase custom token for the given UID. This token can then be sent back to a client + * application to be used with the signInWithCustomToken + * authentication API. + * + *

{@link FirebaseApp} must have been initialized with service account credentials to use call + * this method. + * + * @param uid The UID to store in the token. This identifies the user to other Firebase services + * (Realtime Database, Firebase Auth, etc.). Should be less than 128 characters. + * @return A Firebase custom token string. + * @throws IllegalArgumentException If the specified uid is null or empty, or if the app has not + * been initialized with service account credentials. + * @throws FirebaseAuthException If an error occurs while generating the custom token. + */ + public String createCustomToken(@NonNull String uid) throws FirebaseAuthException { + return createCustomToken(uid, null); + } + + /** + * Creates a Firebase custom token for the given UID, containing the specified additional claims. + * This token can then be sent back to a client application to be used with the signInWithCustomToken + * authentication API. + * + *

This method attempts to generate a token using: + * + *

    + *
  1. the private key of {@link FirebaseApp}'s service account credentials, if provided at + * initialization. + *
  2. the IAM + * service if a service account email was specified via {@link + * com.google.firebase.FirebaseOptions.Builder#setServiceAccountId(String)}. + *
  3. the App + * Identity service if the code is deployed in the Google App Engine standard + * environment. + *
  4. the local + * Metadata server if the code is deployed in a different GCP-managed environment like + * Google Compute Engine. + *
+ * + *

This method throws an exception when all the above fail. + * + * @param uid The UID to store in the token. This identifies the user to other Firebase services + * (Realtime Database, Firebase Auth, etc.). Should be less than 128 characters. + * @param developerClaims Additional claims to be stored in the token (and made available to + * security rules in Database, Storage, etc.). These must be able to be serialized to JSON + * (e.g. contain only Maps, Arrays, Strings, Booleans, Numbers, etc.) + * @return A Firebase custom token string. + * @throws IllegalArgumentException If the specified uid is null or empty. + * @throws IllegalStateException If the SDK fails to discover a viable approach for signing + * tokens. + * @throws FirebaseAuthException If an error occurs while generating the custom token. + */ + public String createCustomToken( + @NonNull String uid, @Nullable Map developerClaims) + throws FirebaseAuthException { + return createCustomTokenOp(uid, developerClaims).call(); + } + + /** + * Similar to {@link #createCustomToken(String)} but performs the operation asynchronously. + * + * @param uid The UID to store in the token. This identifies the user to other Firebase services + * (Realtime Database, Firebase Auth, etc.). Should be less than 128 characters. + * @return An {@code ApiFuture} which will complete successfully with the created Firebase custom + * token, or unsuccessfully with the failure Exception. + * @throws IllegalArgumentException If the specified uid is null or empty, or if the app has not + * been initialized with service account credentials. + */ + public ApiFuture createCustomTokenAsync(@NonNull String uid) { + return createCustomTokenAsync(uid, null); + } + + /** + * Similar to {@link #createCustomToken(String, Map)} but performs the operation asynchronously. + * + * @param uid The UID to store in the token. This identifies the user to other Firebase services + * (Realtime Database, Storage, etc.). Should be less than 128 characters. + * @param developerClaims Additional claims to be stored in the token (and made available to + * security rules in Database, Storage, etc.). These must be able to be serialized to JSON + * (e.g. contain only Maps, Arrays, Strings, Booleans, Numbers, etc.) + * @return An {@code ApiFuture} which will complete successfully with the created Firebase custom + * token, or unsuccessfully with the failure Exception. + * @throws IllegalArgumentException If the specified uid is null or empty, or if the app has not + * been initialized with service account credentials. + */ + public ApiFuture createCustomTokenAsync( + @NonNull String uid, @Nullable Map developerClaims) { + return createCustomTokenOp(uid, developerClaims).callAsync(firebaseApp); + } + + private CallableOperation createCustomTokenOp( + final String uid, final Map developerClaims) { + checkNotDestroyed(); + checkArgument(!Strings.isNullOrEmpty(uid), "uid must not be null or empty"); + final FirebaseTokenFactory tokenFactory = this.tokenFactory.get(); + return new CallableOperation() { + @Override + public String execute() throws FirebaseAuthException { + try { + return tokenFactory.createSignedCustomAuthTokenForUser(uid, developerClaims); + } catch (IOException e) { + throw new FirebaseAuthException( + ERROR_CUSTOM_TOKEN, "Failed to generate a custom token", e); + } + } + }; + } + + /** + * Parses and verifies a Firebase ID Token. + * + *

A Firebase application can identify itself to a trusted backend server by sending its + * Firebase ID Token (accessible via the {@code getToken} API in the Firebase Authentication + * client) with its requests. The backend server can then use the {@code verifyIdToken()} method + * to verify that the token is valid. This method ensures that the token is correctly signed, has + * not expired, and it was issued to the Firebase project associated with this {@link + * FirebaseAuth} instance. + * + *

This method does not check whether a token has been revoked. Use {@link + * #verifyIdToken(String, boolean)} to perform an additional revocation check. + * + * @param idToken A Firebase ID token string to parse and verify. + * @return A {@link FirebaseToken} representing the verified and decoded token. + * @throws IllegalArgumentException If the token is null, empty, or if the {@link FirebaseApp} + * instance does not have a project ID associated with it. + * @throws FirebaseAuthException If an error occurs while parsing or validating the token. + */ + public FirebaseToken verifyIdToken(@NonNull String idToken) throws FirebaseAuthException { + return verifyIdToken(idToken, false); + } + + /** + * Parses and verifies a Firebase ID Token. + * + *

A Firebase application can identify itself to a trusted backend server by sending its + * Firebase ID Token (accessible via the {@code getToken} API in the Firebase Authentication + * client) with its requests. The backend server can then use the {@code verifyIdToken()} method + * to verify that the token is valid. This method ensures that the token is correctly signed, has + * not expired, and it was issued to the Firebase project associated with this {@link + * FirebaseAuth} instance. + * + *

If {@code checkRevoked} is set to true, this method performs an additional check to see if + * the ID token has been revoked since it was issues. This requires making an additional remote + * API call. + * + * @param idToken A Firebase ID token string to parse and verify. + * @param checkRevoked A boolean denoting whether to check if the tokens were revoked. + * @return A {@link FirebaseToken} representing the verified and decoded token. + * @throws IllegalArgumentException If the token is null, empty, or if the {@link FirebaseApp} + * instance does not have a project ID associated with it. + * @throws FirebaseAuthException If an error occurs while parsing or validating the token. + */ + public FirebaseToken verifyIdToken(@NonNull String idToken, boolean checkRevoked) + throws FirebaseAuthException { + return verifyIdTokenOp(idToken, checkRevoked).call(); + } + + /** + * Similar to {@link #verifyIdToken(String)} but performs the operation asynchronously. + * + * @param idToken A Firebase ID Token to verify and parse. + * @return An {@code ApiFuture} which will complete successfully with the parsed token, or + * unsuccessfully with a {@link FirebaseAuthException}. + * @throws IllegalArgumentException If the token is null, empty, or if the {@link FirebaseApp} + * instance does not have a project ID associated with it. + */ + public ApiFuture verifyIdTokenAsync(@NonNull String idToken) { + return verifyIdTokenAsync(idToken, false); + } + + /** + * Similar to {@link #verifyIdToken(String, boolean)} but performs the operation asynchronously. + * + * @param idToken A Firebase ID Token to verify and parse. + * @param checkRevoked A boolean denoting whether to check if the tokens were revoked. + * @return An {@code ApiFuture} which will complete successfully with the parsed token, or + * unsuccessfully with a {@link FirebaseAuthException}. + * @throws IllegalArgumentException If the token is null, empty, or if the {@link FirebaseApp} + * instance does not have a project ID associated with it. + */ + public ApiFuture + verifyIdTokenAsync(@NonNull String idToken, boolean checkRevoked) { + return verifyIdTokenOp(idToken, checkRevoked).callAsync(firebaseApp); + } + + private CallableOperation verifyIdTokenOp( + final String idToken, final boolean checkRevoked) { + checkNotDestroyed(); + checkArgument(!Strings.isNullOrEmpty(idToken), "ID token must not be null or empty"); + final FirebaseTokenVerifier verifier = getIdTokenVerifier(checkRevoked); + return new CallableOperation() { + @Override + protected FirebaseToken execute() throws FirebaseAuthException { + return verifier.verifyToken(idToken); + } + }; + } + + @VisibleForTesting + FirebaseTokenVerifier getIdTokenVerifier(boolean checkRevoked) { + FirebaseTokenVerifier verifier = idTokenVerifier.get(); + if (checkRevoked) { + FirebaseUserManager userManager = getUserManager(); + verifier = RevocationCheckDecorator.decorateIdTokenVerifier(verifier, userManager); + } + return verifier; + } + + /** + * Revokes all refresh tokens for the specified user. + * + *

Updates the user's tokensValidAfterTimestamp to the current UTC time expressed in + * milliseconds since the epoch and truncated to 1 second accuracy. It is important that the + * server on which this is called has its clock set correctly and synchronized. + * + *

While this will revoke all sessions for a specified user and disable any new ID tokens for + * existing sessions from getting minted, existing ID tokens may remain active until their natural + * expiration (one hour). To verify that ID tokens are revoked, use {@link + * #verifyIdTokenAsync(String, boolean)}. + * + * @param uid The user id for which tokens are revoked. + * @throws IllegalArgumentException If the user ID is null or empty. + * @throws FirebaseAuthException If an error occurs while revoking tokens. + */ + public void revokeRefreshTokens(@NonNull String uid) throws FirebaseAuthException { + revokeRefreshTokensOp(uid).call(); + } + + /** + * Similar to {@link #revokeRefreshTokens(String)} but performs the operation asynchronously. + * + * @param uid The user id for which tokens are revoked. + * @return An {@code ApiFuture} which will complete successfully or fail with a {@link + * FirebaseAuthException} in the event of an error. + * @throws IllegalArgumentException If the user ID is null or empty. + */ + public ApiFuture revokeRefreshTokensAsync(@NonNull String uid) { + return revokeRefreshTokensOp(uid).callAsync(firebaseApp); + } + + private CallableOperation revokeRefreshTokensOp(final String uid) { + checkNotDestroyed(); + checkArgument(!Strings.isNullOrEmpty(uid), "uid must not be null or empty"); + final FirebaseUserManager userManager = getUserManager(); + return new CallableOperation() { + @Override + protected Void execute() throws FirebaseAuthException { + int currentTimeSeconds = (int) (System.currentTimeMillis() / 1000); + UserRecord.UpdateRequest request = + new UserRecord.UpdateRequest(uid).setValidSince(currentTimeSeconds); + userManager.updateUser(request, jsonFactory); + return null; + } + }; + } + + /** + * Gets the user data corresponding to the specified user ID. + * + * @param uid A user ID string. + * @return A {@link UserRecord} instance. + * @throws IllegalArgumentException If the user ID string is null or empty. + * @throws FirebaseAuthException If an error occurs while retrieving user data. + */ + public UserRecord getUser(@NonNull String uid) throws FirebaseAuthException { + return getUserOp(uid).call(); + } + + /** + * Similar to {@link #getUser(String)} but performs the operation asynchronously. + * + * @param uid A user ID string. + * @return An {@code ApiFuture} which will complete successfully with a {@link UserRecord} + * instance. If an error occurs while retrieving user data or if the specified user ID does + * not exist, the future throws a {@link FirebaseAuthException}. + * @throws IllegalArgumentException If the user ID string is null or empty. + */ + public ApiFuture getUserAsync(@NonNull String uid) { + return getUserOp(uid).callAsync(firebaseApp); + } + + private CallableOperation getUserOp(final String uid) { + checkNotDestroyed(); + checkArgument(!Strings.isNullOrEmpty(uid), "uid must not be null or empty"); + final FirebaseUserManager userManager = getUserManager(); + return new CallableOperation() { + @Override + protected UserRecord execute() throws FirebaseAuthException { + return userManager.getUserById(uid); + } + }; + } + + /** + * Gets the user data corresponding to the specified user email. + * + * @param email A user email address string. + * @return A {@link UserRecord} instance. + * @throws IllegalArgumentException If the email is null or empty. + * @throws FirebaseAuthException If an error occurs while retrieving user data. + */ + public UserRecord getUserByEmail(@NonNull String email) throws FirebaseAuthException { + return getUserByEmailOp(email).call(); + } + + /** + * Similar to {@link #getUserByEmail(String)} but performs the operation asynchronously. + * + * @param email A user email address string. + * @return An {@code ApiFuture} which will complete successfully with a {@link UserRecord} + * instance. If an error occurs while retrieving user data or if the email address does not + * correspond to a user, the future throws a {@link FirebaseAuthException}. + * @throws IllegalArgumentException If the email is null or empty. + */ + public ApiFuture getUserByEmailAsync(@NonNull String email) { + return getUserByEmailOp(email).callAsync(firebaseApp); + } + + private CallableOperation getUserByEmailOp( + final String email) { + checkNotDestroyed(); + checkArgument(!Strings.isNullOrEmpty(email), "email must not be null or empty"); + final FirebaseUserManager userManager = getUserManager(); + return new CallableOperation() { + @Override + protected UserRecord execute() throws FirebaseAuthException { + return userManager.getUserByEmail(email); + } + }; + } + + /** + * Gets the user data corresponding to the specified user phone number. + * + * @param phoneNumber A user phone number string. + * @return A a {@link UserRecord} instance. + * @throws IllegalArgumentException If the phone number is null or empty. + * @throws FirebaseAuthException If an error occurs while retrieving user data. + */ + public UserRecord getUserByPhoneNumber(@NonNull String phoneNumber) throws FirebaseAuthException { + return getUserByPhoneNumberOp(phoneNumber).call(); + } + + /** + * Gets the user data corresponding to the specified user phone number. + * + * @param phoneNumber A user phone number string. + * @return An {@code ApiFuture} which will complete successfully with a {@link UserRecord} + * instance. If an error occurs while retrieving user data or if the phone number does not + * correspond to a user, the future throws a {@link FirebaseAuthException}. + * @throws IllegalArgumentException If the phone number is null or empty. + */ + public ApiFuture getUserByPhoneNumberAsync(@NonNull String phoneNumber) { + return getUserByPhoneNumberOp(phoneNumber).callAsync(firebaseApp); + } + + private CallableOperation getUserByPhoneNumberOp( + final String phoneNumber) { + checkNotDestroyed(); + checkArgument(!Strings.isNullOrEmpty(phoneNumber), "phone number must not be null or empty"); + final FirebaseUserManager userManager = getUserManager(); + return new CallableOperation() { + @Override + protected UserRecord execute() throws FirebaseAuthException { + return userManager.getUserByPhoneNumber(phoneNumber); + } + }; + } + + /** + * Gets a page of users starting from the specified {@code pageToken}. Page size is limited to + * 1000 users. + * + * @param pageToken A non-empty page token string, or null to retrieve the first page of users. + * @return A {@link ListUsersPage} instance. + * @throws IllegalArgumentException If the specified page token is empty. + * @throws FirebaseAuthException If an error occurs while retrieving user data. + */ + public ListUsersPage listUsers(@Nullable String pageToken) throws FirebaseAuthException { + return listUsers(pageToken, FirebaseUserManager.MAX_LIST_USERS_RESULTS); + } + + /** + * Gets a page of users starting from the specified {@code pageToken}. + * + * @param pageToken A non-empty page token string, or null to retrieve the first page of users. + * @param maxResults Maximum number of users to include in the returned page. This may not exceed + * 1000. + * @return A {@link ListUsersPage} instance. + * @throws IllegalArgumentException If the specified page token is empty, or max results value is + * invalid. + * @throws FirebaseAuthException If an error occurs while retrieving user data. + */ + public ListUsersPage listUsers(@Nullable String pageToken, int maxResults) + throws FirebaseAuthException { + return listUsersOp(pageToken, maxResults).call(); + } + + /** + * Similar to {@link #listUsers(String)} but performs the operation asynchronously. + * + * @param pageToken A non-empty page token string, or null to retrieve the first page of users. + * @return An {@code ApiFuture} which will complete successfully with a {@link ListUsersPage} + * instance. If an error occurs while retrieving user data, the future throws an exception. + * @throws IllegalArgumentException If the specified page token is empty. + */ + public ApiFuture listUsersAsync(@Nullable String pageToken) { + return listUsersAsync(pageToken, FirebaseUserManager.MAX_LIST_USERS_RESULTS); + } + + /** + * Similar to {@link #listUsers(String, int)} but performs the operation asynchronously. + * + * @param pageToken A non-empty page token string, or null to retrieve the first page of users. + * @param maxResults Maximum number of users to include in the returned page. This may not exceed + * 1000. + * @return An {@code ApiFuture} which will complete successfully with a {@link ListUsersPage} + * instance. If an error occurs while retrieving user data, the future throws an exception. + * @throws IllegalArgumentException If the specified page token is empty, or max results value is + * invalid. + */ + public ApiFuture listUsersAsync(@Nullable String pageToken, int maxResults) { + return listUsersOp(pageToken, maxResults).callAsync(firebaseApp); + } + + private CallableOperation listUsersOp( + @Nullable final String pageToken, final int maxResults) { + checkNotDestroyed(); + final FirebaseUserManager userManager = getUserManager(); + final DefaultUserSource source = new DefaultUserSource(userManager, jsonFactory); + final ListUsersPage.Factory factory = new ListUsersPage.Factory(source, maxResults, pageToken); + return new CallableOperation() { + @Override + protected ListUsersPage execute() throws FirebaseAuthException { + return factory.create(); + } + }; + } + + /** + * Creates a new user account with the attributes contained in the specified {@link + * UserRecord.CreateRequest}. + * + * @param request A non-null {@link UserRecord.CreateRequest} instance. + * @return A {@link UserRecord} instance corresponding to the newly created account. + * @throws NullPointerException if the provided request is null. + * @throws FirebaseAuthException if an error occurs while creating the user account. + */ + public UserRecord createUser(@NonNull UserRecord.CreateRequest request) + throws FirebaseAuthException { + return createUserOp(request).call(); + } + + /** + * Similar to {@link #createUser} but performs the operation asynchronously. + * + * @param request A non-null {@link UserRecord.CreateRequest} instance. + * @return An {@code ApiFuture} which will complete successfully with a {@link UserRecord} + * instance corresponding to the newly created account. If an error occurs while creating the + * user account, the future throws a {@link FirebaseAuthException}. + * @throws NullPointerException if the provided request is null. + */ + public ApiFuture createUserAsync(@NonNull UserRecord.CreateRequest request) { + return createUserOp(request).callAsync(firebaseApp); + } + + private CallableOperation createUserOp( + final UserRecord.CreateRequest request) { + checkNotDestroyed(); + checkNotNull(request, "create request must not be null"); + final FirebaseUserManager userManager = getUserManager(); + return new CallableOperation() { + @Override + protected UserRecord execute() throws FirebaseAuthException { + String uid = userManager.createUser(request); + return userManager.getUserById(uid); + } + }; + } + + /** + * Updates an existing user account with the attributes contained in the specified {@link + * UserRecord.UpdateRequest}. + * + * @param request A non-null {@link UserRecord.UpdateRequest} instance. + * @return A {@link UserRecord} instance corresponding to the updated user account. + * @throws NullPointerException if the provided update request is null. + * @throws FirebaseAuthException if an error occurs while updating the user account. + */ + public UserRecord updateUser(@NonNull UserRecord.UpdateRequest request) + throws FirebaseAuthException { + return updateUserOp(request).call(); + } + + /** + * Similar to {@link #updateUser} but performs the operation asynchronously. + * + * @param request A non-null {@link UserRecord.UpdateRequest} instance. + * @return An {@code ApiFuture} which will complete successfully with a {@link UserRecord} + * instance corresponding to the updated user account. If an error occurs while updating the + * user account, the future throws a {@link FirebaseAuthException}. + */ + public ApiFuture updateUserAsync(@NonNull UserRecord.UpdateRequest request) { + return updateUserOp(request).callAsync(firebaseApp); + } + + private CallableOperation updateUserOp( + final UserRecord.UpdateRequest request) { + checkNotDestroyed(); + checkNotNull(request, "update request must not be null"); + final FirebaseUserManager userManager = getUserManager(); + return new CallableOperation() { + @Override + protected UserRecord execute() throws FirebaseAuthException { + userManager.updateUser(request, jsonFactory); + return userManager.getUserById(request.getUid()); + } + }; + } + + /** + * Sets the specified custom claims on an existing user account. A null claims value removes any + * claims currently set on the user account. The claims should serialize into a valid JSON string. + * The serialized claims must not be larger than 1000 characters. + * + * @param uid A user ID string. + * @param claims A map of custom claims or null. + * @throws FirebaseAuthException If an error occurs while updating custom claims. + * @throws IllegalArgumentException If the user ID string is null or empty, or the claims payload + * is invalid or too large. + */ + public void setCustomUserClaims(@NonNull String uid, @Nullable Map claims) + throws FirebaseAuthException { + setCustomUserClaimsOp(uid, claims).call(); + } + + /** + * @deprecated Use {@link #setCustomUserClaims(String, Map)} instead. + */ + public void setCustomClaims(@NonNull String uid, @Nullable Map claims) + throws FirebaseAuthException { + setCustomUserClaims(uid, claims); + } + + /** + * Similar to {@link #setCustomUserClaims(String, Map)} but performs the operation asynchronously. + * + * @param uid A user ID string. + * @param claims A map of custom claims or null. + * @return An {@code ApiFuture} which will complete successfully when the user account has been + * updated. If an error occurs while deleting the user account, the future throws a {@link + * FirebaseAuthException}. + * @throws IllegalArgumentException If the user ID string is null or empty. + */ + public ApiFuture setCustomUserClaimsAsync( + @NonNull String uid, @Nullable Map claims) { + return setCustomUserClaimsOp(uid, claims).callAsync(firebaseApp); + } + + private CallableOperation setCustomUserClaimsOp( + final String uid, final Map claims) { + checkNotDestroyed(); + checkArgument(!Strings.isNullOrEmpty(uid), "uid must not be null or empty"); + final FirebaseUserManager userManager = getUserManager(); + return new CallableOperation() { + @Override + protected Void execute() throws FirebaseAuthException { + final UserRecord.UpdateRequest request = + new UserRecord.UpdateRequest(uid).setCustomClaims(claims); + userManager.updateUser(request, jsonFactory); + return null; + } + }; + } + + /** + * Deletes the user identified by the specified user ID. + * + * @param uid A user ID string. + * @throws IllegalArgumentException If the user ID string is null or empty. + * @throws FirebaseAuthException If an error occurs while deleting the user. + */ + public void deleteUser(@NonNull String uid) throws FirebaseAuthException { + deleteUserOp(uid).call(); + } + + /** + * Similar to {@link #deleteUser(String)} but performs the operation asynchronously. + * + * @param uid A user ID string. + * @return An {@code ApiFuture} which will complete successfully when the specified user account + * has been deleted. If an error occurs while deleting the user account, the future throws a + * {@link FirebaseAuthException}. + * @throws IllegalArgumentException If the user ID string is null or empty. + */ + public ApiFuture deleteUserAsync(String uid) { + return deleteUserOp(uid).callAsync(firebaseApp); + } + + private CallableOperation deleteUserOp(final String uid) { + checkNotDestroyed(); + checkArgument(!Strings.isNullOrEmpty(uid), "uid must not be null or empty"); + final FirebaseUserManager userManager = getUserManager(); + return new CallableOperation() { + @Override + protected Void execute() throws FirebaseAuthException { + userManager.deleteUser(uid); + return null; + } + }; + } + + /** + * Imports the provided list of users into Firebase Auth. At most 1000 users can be imported at a + * time. This operation is optimized for bulk imports and will ignore checks on identifier + * uniqueness which could result in duplications. + * + *

{@link UserImportOptions} is required to import users with passwords. See {@link + * #importUsers(List, UserImportOptions)}. + * + * @param users A non-empty list of users to be imported. Length must not exceed 1000. + * @return A {@link UserImportResult} instance. + * @throws IllegalArgumentException If the users list is null, empty or has more than 1000 + * elements. Or if at least one user specifies a password. + * @throws FirebaseAuthException If an error occurs while importing users. + */ + public UserImportResult importUsers(List users) throws FirebaseAuthException { + return importUsers(users, null); + } + + /** + * Imports the provided list of users into Firebase Auth. At most 1000 users can be imported at a + * time. This operation is optimized for bulk imports and will ignore checks on identifier + * uniqueness which could result in duplications. + * + * @param users A non-empty list of users to be imported. Length must not exceed 1000. + * @param options a {@link UserImportOptions} instance or null. Required when importing users with + * passwords. + * @return A {@link UserImportResult} instance. + * @throws IllegalArgumentException If the users list is null, empty or has more than 1000 + * elements. Or if at least one user specifies a password, and options is null. + * @throws FirebaseAuthException If an error occurs while importing users. + */ + public UserImportResult importUsers( + List users, @Nullable UserImportOptions options) + throws FirebaseAuthException { + return importUsersOp(users, options).call(); + } + + /** + * Similar to {@link #importUsers(List)} but performs the operation asynchronously. + * + * @param users A non-empty list of users to be imported. Length must not exceed 1000. + * @return An {@code ApiFuture} which will complete successfully when the user accounts are + * imported. If an error occurs while importing the users, the future throws a {@link + * FirebaseAuthException}. + * @throws IllegalArgumentException If the users list is null, empty or has more than 1000 + * elements. Or if at least one user specifies a password. + */ + public ApiFuture importUsersAsync(List users) { + return importUsersAsync(users, null); + } + + /** + * Similar to {@link #importUsers(List, UserImportOptions)} but performs the operation + * asynchronously. + * + * @param users A non-empty list of users to be imported. Length must not exceed 1000. + * @param options a {@link UserImportOptions} instance or null. Required when importing users with + * passwords. + * @return An {@code ApiFuture} which will complete successfully when the user accounts are + * imported. If an error occurs while importing the users, the future throws a {@link + * FirebaseAuthException}. + * @throws IllegalArgumentException If the users list is null, empty or has more than 1000 + * elements. Or if at least one user specifies a password, and options is null. + */ + public ApiFuture importUsersAsync( + List users, @Nullable UserImportOptions options) { + return importUsersOp(users, options).callAsync(firebaseApp); + } + + private CallableOperation importUsersOp( + final List users, final UserImportOptions options) { + checkNotDestroyed(); + final UserImportRequest request = new UserImportRequest(users, options, jsonFactory); + final FirebaseUserManager userManager = getUserManager(); + return new CallableOperation() { + @Override + protected UserImportResult execute() throws FirebaseAuthException { + return userManager.importUsers(request); + } + }; + } + + /** + * Gets the user data corresponding to the specified identifiers. + * + *

There are no ordering guarantees; in particular, the nth entry in the users result list is + * not guaranteed to correspond to the nth entry in the input parameters list. + * + *

A maximum of 100 identifiers may be specified. If more than 100 identifiers are + * supplied, this method throws an {@link IllegalArgumentException}. + * + * @param identifiers The identifiers used to indicate which user records should be returned. Must + * have 100 or fewer entries. + * @return The corresponding user records. + * @throws IllegalArgumentException If any of the identifiers are invalid or if more than 100 + * identifiers are specified. + * @throws NullPointerException If the identifiers parameter is null. + * @throws FirebaseAuthException If an error occurs while retrieving user data. + */ + public GetUsersResult getUsers(@NonNull Collection identifiers) + throws FirebaseAuthException { + return getUsersOp(identifiers).call(); + } + + /** + * Gets the user data corresponding to the specified identifiers. + * + *

There are no ordering guarantees; in particular, the nth entry in the users result list is + * not guaranteed to correspond to the nth entry in the input parameters list. + * + *

A maximum of 100 identifiers may be specified. If more than 100 identifiers are + * supplied, this method throws an {@link IllegalArgumentException}. + * + * @param identifiers The identifiers used to indicate which user records should be returned. + * Must have 100 or fewer entries. + * @return An {@code ApiFuture} that resolves to the corresponding user records. + * @throws IllegalArgumentException If any of the identifiers are invalid or if more than 100 + * identifiers are specified. + * @throws NullPointerException If the identifiers parameter is null. + */ + public ApiFuture getUsersAsync(@NonNull Collection identifiers) { + return getUsersOp(identifiers).callAsync(firebaseApp); + } + + private CallableOperation getUsersOp( + @NonNull final Collection identifiers) { + checkNotDestroyed(); + checkNotNull(identifiers, "identifiers must not be null"); + checkArgument(identifiers.size() <= FirebaseUserManager.MAX_GET_ACCOUNTS_BATCH_SIZE, + "identifiers parameter must have <= " + FirebaseUserManager.MAX_GET_ACCOUNTS_BATCH_SIZE + + " entries."); + + final FirebaseUserManager userManager = getUserManager(); + return new CallableOperation() { + @Override + protected GetUsersResult execute() throws FirebaseAuthException { + Set users = userManager.getAccountInfo(identifiers); + Set notFound = new HashSet<>(); + for (UserIdentifier id : identifiers) { + if (!isUserFound(id, users)) { + notFound.add(id); + } + } + return new GetUsersResult(users, notFound); + } + }; + } + + private boolean isUserFound(UserIdentifier id, Collection userRecords) { + for (UserRecord userRecord : userRecords) { + if (id.matches(userRecord)) { + return true; + } + } + return false; + } + + /** + * Deletes the users specified by the given identifiers. + * + *

Deleting a non-existing user does not generate an error (the method is idempotent). + * Non-existing users are considered to be successfully deleted and are therefore included in the + * DeleteUsersResult.getSuccessCount() value. + * + *

A maximum of 1000 identifiers may be supplied. If more than 1000 identifiers are + * supplied, this method throws an {@link IllegalArgumentException}. + * + *

This API has a rate limit of 1 QPS. Exceeding the limit may result in a quota exceeded + * error. If you want to delete more than 1000 users, we suggest adding a delay to ensure you + * don't exceed this limit. + * + * @param uids The uids of the users to be deleted. Must have <= 1000 entries. + * @return The total number of successful/failed deletions, as well as the array of errors that + * correspond to the failed deletions. + * @throw IllegalArgumentException If any of the identifiers are invalid or if more than 1000 + * identifiers are specified. + * @throws FirebaseAuthException If an error occurs while deleting users. + */ + public DeleteUsersResult deleteUsers(List uids) throws FirebaseAuthException { + return deleteUsersOp(uids).call(); + } + + /** + * Similar to {@link #deleteUsers(List)} but performs the operation asynchronously. + * + * @param uids The uids of the users to be deleted. Must have <= 1000 entries. + * @return An {@code ApiFuture} that resolves to the total number of successful/failed + * deletions, as well as the array of errors that correspond to the failed deletions. If an + * error occurs while deleting the user account, the future throws a + * {@link FirebaseAuthException}. + * @throw IllegalArgumentException If any of the identifiers are invalid or if more than 1000 + * identifiers are specified. + */ + public ApiFuture deleteUsersAsync(List uids) { + return deleteUsersOp(uids).callAsync(firebaseApp); + } + + private CallableOperation deleteUsersOp( + final List uids) { + checkNotDestroyed(); + checkNotNull(uids, "uids must not be null"); + for (String uid : uids) { + UserRecord.checkUid(uid); + } + checkArgument(uids.size() <= FirebaseUserManager.MAX_DELETE_ACCOUNTS_BATCH_SIZE, + "uids parameter must have <= " + FirebaseUserManager.MAX_DELETE_ACCOUNTS_BATCH_SIZE + + " entries."); + final FirebaseUserManager userManager = getUserManager(); + return new CallableOperation() { + @Override + protected DeleteUsersResult execute() throws FirebaseAuthException { + return userManager.deleteUsers(uids); + } + }; + } + + /** + * Generates the out-of-band email action link for password reset flows for the specified email + * address. + * + * @param email The email of the user whose password is to be reset. + * @return A password reset link. + * @throws IllegalArgumentException If the email address is null or empty. + * @throws FirebaseAuthException If an error occurs while generating the link. + */ + public String generatePasswordResetLink(@NonNull String email) throws FirebaseAuthException { + return generatePasswordResetLink(email, null); + } + + /** + * Generates the out-of-band email action link for password reset flows for the specified email + * address. + * + * @param email The email of the user whose password is to be reset. + * @param settings The action code settings object which defines whether the link is to be handled + * by a mobile app and the additional state information to be passed in the deep link. + * @return A password reset link. + * @throws IllegalArgumentException If the email address is null or empty. + * @throws FirebaseAuthException If an error occurs while generating the link. + */ + public String generatePasswordResetLink( + @NonNull String email, @Nullable ActionCodeSettings settings) throws FirebaseAuthException { + return generateEmailActionLinkOp(EmailLinkType.PASSWORD_RESET, email, settings).call(); + } + + /** + * Similar to {@link #generatePasswordResetLink(String)} but performs the operation + * asynchronously. + * + * @param email The email of the user whose password is to be reset. + * @return An {@code ApiFuture} which will complete successfully with the generated email action + * link. If an error occurs while generating the link, the future throws a {@link + * FirebaseAuthException}. + * @throws IllegalArgumentException If the email address is null or empty. + */ + public ApiFuture generatePasswordResetLinkAsync(@NonNull String email) { + return generatePasswordResetLinkAsync(email, null); + } + + /** + * Similar to {@link #generatePasswordResetLink(String, ActionCodeSettings)} but performs the + * operation asynchronously. + * + * @param email The email of the user whose password is to be reset. + * @param settings The action code settings object which defines whether the link is to be handled + * by a mobile app and the additional state information to be passed in the deep link. + * @return An {@code ApiFuture} which will complete successfully with the generated email action + * link. If an error occurs while generating the link, the future throws a {@link + * FirebaseAuthException}. + * @throws IllegalArgumentException If the email address is null or empty. + */ + public ApiFuture generatePasswordResetLinkAsync( + @NonNull String email, @Nullable ActionCodeSettings settings) { + return generateEmailActionLinkOp(EmailLinkType.PASSWORD_RESET, email, settings) + .callAsync(firebaseApp); + } + + /** + * Generates the out-of-band email action link for email verification flows for the specified + * email address. + * + * @param email The email of the user to be verified. + * @return An email verification link. + * @throws IllegalArgumentException If the email address is null or empty. + * @throws FirebaseAuthException If an error occurs while generating the link. + */ + public String generateEmailVerificationLink(@NonNull String email) throws FirebaseAuthException { + return generateEmailVerificationLink(email, null); + } + + /** + * Generates the out-of-band email action link for email verification flows for the specified + * email address, using the action code settings provided. + * + * @param email The email of the user to be verified. + * @return An email verification link. + * @throws IllegalArgumentException If the email address is null or empty. + * @throws FirebaseAuthException If an error occurs while generating the link. + */ + public String generateEmailVerificationLink( + @NonNull String email, @Nullable ActionCodeSettings settings) throws FirebaseAuthException { + return generateEmailActionLinkOp(EmailLinkType.VERIFY_EMAIL, email, settings).call(); + } + + /** + * Similar to {@link #generateEmailVerificationLink(String)} but performs the operation + * asynchronously. + * + * @param email The email of the user to be verified. + * @return An {@code ApiFuture} which will complete successfully with the generated email action + * link. If an error occurs while generating the link, the future throws a {@link + * FirebaseAuthException}. + * @throws IllegalArgumentException If the email address is null or empty. + */ + public ApiFuture generateEmailVerificationLinkAsync(@NonNull String email) { + return generateEmailVerificationLinkAsync(email, null); + } + + /** + * Similar to {@link #generateEmailVerificationLink(String, ActionCodeSettings)} but performs the + * operation asynchronously. + * + * @param email The email of the user to be verified. + * @param settings The action code settings object which defines whether the link is to be handled + * by a mobile app and the additional state information to be passed in the deep link. + * @return An {@code ApiFuture} which will complete successfully with the generated email action + * link. If an error occurs while generating the link, the future throws a {@link + * FirebaseAuthException}. + * @throws IllegalArgumentException If the email address is null or empty. + */ + public ApiFuture generateEmailVerificationLinkAsync( + @NonNull String email, @Nullable ActionCodeSettings settings) { + return generateEmailActionLinkOp(EmailLinkType.VERIFY_EMAIL, email, settings) + .callAsync(firebaseApp); + } + + /** + * Generates the out-of-band email action link for email link sign-in flows, using the action code + * settings provided. + * + * @param email The email of the user signing in. + * @param settings The action code settings object which defines whether the link is to be handled + * by a mobile app and the additional state information to be passed in the deep link. + * @return An email verification link. + * @throws IllegalArgumentException If the email address is null or empty. + * @throws FirebaseAuthException If an error occurs while generating the link. + */ + public String generateSignInWithEmailLink( + @NonNull String email, @NonNull ActionCodeSettings settings) throws FirebaseAuthException { + return generateEmailActionLinkOp(EmailLinkType.EMAIL_SIGNIN, email, settings).call(); + } + + /** + * Similar to {@link #generateSignInWithEmailLink(String, ActionCodeSettings)} but performs the + * operation asynchronously. + * + * @param email The email of the user signing in. + * @param settings The action code settings object which defines whether the link is to be handled + * by a mobile app and the additional state information to be passed in the deep link. + * @return An {@code ApiFuture} which will complete successfully with the generated email action + * link. If an error occurs while generating the link, the future throws a {@link + * FirebaseAuthException}. + * @throws IllegalArgumentException If the email address is null or empty. + * @throws NullPointerException If the settings is null. + */ + public ApiFuture generateSignInWithEmailLinkAsync( + String email, @NonNull ActionCodeSettings settings) { + return generateEmailActionLinkOp(EmailLinkType.EMAIL_SIGNIN, email, settings) + .callAsync(firebaseApp); + } + + private CallableOperation generateEmailActionLinkOp( + final EmailLinkType type, final String email, final ActionCodeSettings settings) { + checkNotDestroyed(); + checkArgument(!Strings.isNullOrEmpty(email), "email must not be null or empty"); + if (type == EmailLinkType.EMAIL_SIGNIN) { + checkNotNull(settings, "ActionCodeSettings must not be null when generating sign-in links"); + } + final FirebaseUserManager userManager = getUserManager(); + return new CallableOperation() { + @Override + protected String execute() throws FirebaseAuthException { + return userManager.getEmailActionLink(type, email, settings); + } + }; + } + + /** + * Creates a new OpenID Connect auth provider config with the attributes contained in the + * specified {@link OidcProviderConfig.CreateRequest}. + * + * @param request A non-null {@link OidcProviderConfig.CreateRequest} instance. + * @return An {@link OidcProviderConfig} instance corresponding to the newly created provider + * config. + * @throws NullPointerException if the provided request is null. + * @throws IllegalArgumentException If the provider ID string is null or empty, or is not + * prefixed with 'oidc.'. + * @throws FirebaseAuthException if an error occurs while creating the provider config. + */ + public OidcProviderConfig createOidcProviderConfig( + @NonNull OidcProviderConfig.CreateRequest request) throws FirebaseAuthException { + return createOidcProviderConfigOp(request).call(); + } + + /** + * Similar to {@link #createOidcProviderConfig} but performs the operation asynchronously. + * + * @param request A non-null {@link OidcProviderConfig.CreateRequest} instance. + * @return An {@code ApiFuture} which will complete successfully with a {@link OidcProviderConfig} + * instance corresponding to the newly created provider config. If an error occurs while + * creating the provider config, the future throws a {@link FirebaseAuthException}. + * @throws NullPointerException if the provided request is null. + * @throws IllegalArgumentException If the provider ID string is null or empty, or is not + * prefixed with 'oidc.'. + */ + public ApiFuture createOidcProviderConfigAsync( + @NonNull OidcProviderConfig.CreateRequest request) { + return createOidcProviderConfigOp(request).callAsync(firebaseApp); + } + + private CallableOperation + createOidcProviderConfigOp(final OidcProviderConfig.CreateRequest request) { + checkNotDestroyed(); + checkNotNull(request, "Create request must not be null."); + OidcProviderConfig.checkOidcProviderId(request.getProviderId()); + final FirebaseUserManager userManager = getUserManager(); + return new CallableOperation() { + @Override + protected OidcProviderConfig execute() throws FirebaseAuthException { + return userManager.createOidcProviderConfig(request); + } + }; + } + + /** + * Updates an existing OpenID Connect auth provider config with the attributes contained in the + * specified {@link OidcProviderConfig.UpdateRequest}. + * + * @param request A non-null {@link OidcProviderConfig.UpdateRequest} instance. + * @return A {@link OidcProviderConfig} instance corresponding to the updated provider config. + * @throws NullPointerException if the provided update request is null. + * @throws IllegalArgumentException If the provided update request is invalid. + * @throws FirebaseAuthException if an error occurs while updating the provider config. + */ + public OidcProviderConfig updateOidcProviderConfig( + @NonNull OidcProviderConfig.UpdateRequest request) throws FirebaseAuthException { + return updateOidcProviderConfigOp(request).call(); + } + + /** + * Similar to {@link #updateOidcProviderConfig} but performs the operation asynchronously. + * + * @param request A non-null {@link OidcProviderConfig.UpdateRequest} instance. + * @return An {@code ApiFuture} which will complete successfully with a {@link OidcProviderConfig} + * instance corresponding to the updated provider config. If an error occurs while updating + * the provider config, the future throws a {@link FirebaseAuthException}. + * @throws NullPointerException if the provided update request is null. + * @throws IllegalArgumentException If the provided update request is invalid. + */ + public ApiFuture updateOidcProviderConfigAsync( + @NonNull OidcProviderConfig.UpdateRequest request) { + return updateOidcProviderConfigOp(request).callAsync(firebaseApp); + } + + private CallableOperation updateOidcProviderConfigOp( + final OidcProviderConfig.UpdateRequest request) { + checkNotDestroyed(); + checkNotNull(request, "Update request must not be null."); + checkArgument(!request.getProperties().isEmpty(), + "Update request must have at least one property set."); + final FirebaseUserManager userManager = getUserManager(); + return new CallableOperation() { + @Override + protected OidcProviderConfig execute() throws FirebaseAuthException { + return userManager.updateOidcProviderConfig(request); + } + }; + } + + /** + * Gets the OpenID Connect auth provider corresponding to the specified provider ID. + * + * @param providerId A provider ID string. + * @return An {@link OidcProviderConfig} instance. + * @throws IllegalArgumentException If the provider ID string is null or empty, or is not prefixed + * with 'oidc'. + * @throws FirebaseAuthException If an error occurs while retrieving the provider config. + */ + public OidcProviderConfig getOidcProviderConfig(@NonNull String providerId) + throws FirebaseAuthException { + return getOidcProviderConfigOp(providerId).call(); + } + + /** + * Similar to {@link #getOidcProviderConfig(String)} but performs the operation asynchronously. + * Page size is limited to 100 provider configs. + * + * @param providerId A provider ID string. + * @return An {@code ApiFuture} which will complete successfully with an + * {@link OidcProviderConfig} instance. If an error occurs while retrieving the provider + * config or if the specified provider ID does not exist, the future throws a + * {@link FirebaseAuthException}. + * @throws IllegalArgumentException If the provider ID string is null or empty, or is not + * prefixed with 'oidc.'. + */ + public ApiFuture getOidcProviderConfigAsync(@NonNull String providerId) { + return getOidcProviderConfigOp(providerId).callAsync(firebaseApp); + } + + private CallableOperation + getOidcProviderConfigOp(final String providerId) { + checkNotDestroyed(); + OidcProviderConfig.checkOidcProviderId(providerId); + final FirebaseUserManager userManager = getUserManager(); + return new CallableOperation() { + @Override + protected OidcProviderConfig execute() throws FirebaseAuthException { + return userManager.getOidcProviderConfig(providerId); + } + }; + } + + /** + * Gets a page of OpenID Connect auth provider configs starting from the specified + * {@code pageToken}. Page size is limited to 100 provider configs. + * + * @param pageToken A non-empty page token string, or null to retrieve the first page of provider + * configs. + * @return A {@link ListProviderConfigsPage} instance. + * @throws IllegalArgumentException If the specified page token is empty + * @throws FirebaseAuthException If an error occurs while retrieving provider config data. + */ + public ListProviderConfigsPage listOidcProviderConfigs( + @Nullable String pageToken) throws FirebaseAuthException { + int maxResults = FirebaseUserManager.MAX_LIST_PROVIDER_CONFIGS_RESULTS; + return listOidcProviderConfigsOp(pageToken, maxResults).call(); + } + + /** + * Gets a page of OpenID Connect auth provider configs starting from the specified + * {@code pageToken}. + * + * @param pageToken A non-empty page token string, or null to retrieve the first page of provider + * configs. + * @param maxResults Maximum number of provider configs to include in the returned page. This may + * not exceed 100. + * @return A {@link ListProviderConfigsPage} instance. + * @throws IllegalArgumentException If the specified page token is empty, or max results value is + * invalid. + * @throws FirebaseAuthException If an error occurs while retrieving provider config data. + */ + public ListProviderConfigsPage listOidcProviderConfigs( + @Nullable String pageToken, int maxResults) throws FirebaseAuthException { + return listOidcProviderConfigsOp(pageToken, maxResults).call(); + } + + /** + * Similar to {@link #listOidcProviderConfigs(String)} but performs the operation asynchronously. + * Page size is limited to 100 provider configs. + * + * @param pageToken A non-empty page token string, or null to retrieve the first page of provider + * configs. + * @return An {@code ApiFuture} which will complete successfully with a + * {@link ListProviderConfigsPage} instance. If an error occurs while retrieving provider + * config data, the future throws an exception. + * @throws IllegalArgumentException If the specified page token is empty. + */ + public ApiFuture> listOidcProviderConfigsAsync( + @Nullable String pageToken) { + int maxResults = FirebaseUserManager.MAX_LIST_PROVIDER_CONFIGS_RESULTS; + return listOidcProviderConfigsAsync(pageToken, maxResults); + } + + /** + * Similar to {@link #listOidcProviderConfigs(String, int)} but performs the operation + * asynchronously. + * + * @param pageToken A non-empty page token string, or null to retrieve the first page of provider + * configs. + * @param maxResults Maximum number of provider configs to include in the returned page. This may + * not exceed 100. + * @return An {@code ApiFuture} which will complete successfully with a + * {@link ListProviderConfigsPage} instance. If an error occurs while retrieving provider + * config data, the future throws an exception. + * @throws IllegalArgumentException If the specified page token is empty, or max results value is + * invalid. + */ + public ApiFuture> listOidcProviderConfigsAsync( + @Nullable String pageToken, + int maxResults) { + return listOidcProviderConfigsOp(pageToken, maxResults).callAsync(firebaseApp); + } + + private CallableOperation, FirebaseAuthException> + listOidcProviderConfigsOp(@Nullable final String pageToken, final int maxResults) { + checkNotDestroyed(); + final FirebaseUserManager userManager = getUserManager(); + final DefaultOidcProviderConfigSource source = new DefaultOidcProviderConfigSource(userManager); + final ListProviderConfigsPage.Factory factory = + new ListProviderConfigsPage.Factory(source, maxResults, pageToken); + return + new CallableOperation, FirebaseAuthException>() { + @Override + protected ListProviderConfigsPage execute() + throws FirebaseAuthException { + return factory.create(); + } + }; + } + + /** + * Deletes the OpenID Connect auth provider config identified by the specified provider ID. + * + * @param providerId A provider ID string. + * @throws IllegalArgumentException If the provider ID string is null or empty, or is not prefixed + * with 'oidc'. + * @throws FirebaseAuthException If an error occurs while deleting the provider config. + */ + public void deleteOidcProviderConfig(@NonNull String providerId) throws FirebaseAuthException { + deleteOidcProviderConfigOp(providerId).call(); + } + + /** + * Similar to {@link #deleteOidcProviderConfig} but performs the operation asynchronously. + * + * @param providerId A provider ID string. + * @return An {@code ApiFuture} which will complete successfully when the specified provider + * config has been deleted. If an error occurs while deleting the provider config, the future + * throws a {@link FirebaseAuthException}. + * @throws IllegalArgumentException If the provider ID string is null or empty, or is not prefixed + * with "oidc.". + */ + public ApiFuture deleteOidcProviderConfigAsync(String providerId) { + return deleteOidcProviderConfigOp(providerId).callAsync(firebaseApp); + } + + private CallableOperation deleteOidcProviderConfigOp( + final String providerId) { + checkNotDestroyed(); + OidcProviderConfig.checkOidcProviderId(providerId); + final FirebaseUserManager userManager = getUserManager(); + return new CallableOperation() { + @Override + protected Void execute() throws FirebaseAuthException { + userManager.deleteOidcProviderConfig(providerId); + return null; + } + }; + } + + /** + * Creates a new SAML Auth provider config with the attributes contained in the specified + * {@link SamlProviderConfig.CreateRequest}. + * + * @param request A non-null {@link SamlProviderConfig.CreateRequest} instance. + * @return An {@link SamlProviderConfig} instance corresponding to the newly created provider + * config. + * @throws NullPointerException if the provided request is null. + * @throws IllegalArgumentException If the provider ID string is null or empty, or is not prefixed + * with 'saml'. + * @throws FirebaseAuthException if an error occurs while creating the provider config. + */ + public SamlProviderConfig createSamlProviderConfig( + @NonNull SamlProviderConfig.CreateRequest request) throws FirebaseAuthException { + return createSamlProviderConfigOp(request).call(); + } + + /** + * Similar to {@link #createSamlProviderConfig} but performs the operation asynchronously. + * + * @param request A non-null {@link SamlProviderConfig.CreateRequest} instance. + * @return An {@code ApiFuture} which will complete successfully with a {@link SamlProviderConfig} + * instance corresponding to the newly created provider config. If an error occurs while + * creating the provider config, the future throws a {@link FirebaseAuthException}. + * @throws NullPointerException if the provided request is null. + * @throws IllegalArgumentException If the provider ID string is null or empty, or is not prefixed + * with 'saml'. + */ + public ApiFuture createSamlProviderConfigAsync( + @NonNull SamlProviderConfig.CreateRequest request) { + return createSamlProviderConfigOp(request).callAsync(firebaseApp); + } + + private CallableOperation + createSamlProviderConfigOp(final SamlProviderConfig.CreateRequest request) { + checkNotDestroyed(); + checkNotNull(request, "Create request must not be null."); + SamlProviderConfig.checkSamlProviderId(request.getProviderId()); + final FirebaseUserManager userManager = getUserManager(); + return new CallableOperation() { + @Override + protected SamlProviderConfig execute() throws FirebaseAuthException { + return userManager.createSamlProviderConfig(request); + } + }; + } + + /** + * Updates an existing SAML Auth provider config with the attributes contained in the specified + * {@link SamlProviderConfig.UpdateRequest}. + * + * @param request A non-null {@link SamlProviderConfig.UpdateRequest} instance. + * @return A {@link SamlProviderConfig} instance corresponding to the updated provider config. + * @throws NullPointerException if the provided update request is null. + * @throws IllegalArgumentException If the provided update request is invalid. + * @throws FirebaseAuthException if an error occurs while updating the provider config. + */ + public SamlProviderConfig updateSamlProviderConfig( + @NonNull SamlProviderConfig.UpdateRequest request) throws FirebaseAuthException { + return updateSamlProviderConfigOp(request).call(); + } + + /** + * Similar to {@link #updateSamlProviderConfig} but performs the operation asynchronously. + * + * @param request A non-null {@link SamlProviderConfig.UpdateRequest} instance. + * @return An {@code ApiFuture} which will complete successfully with a {@link SamlProviderConfig} + * instance corresponding to the updated provider config. If an error occurs while updating + * the provider config, the future throws a {@link FirebaseAuthException}. + * @throws NullPointerException if the provided update request is null. + * @throws IllegalArgumentException If the provided update request is invalid. + */ + public ApiFuture updateSamlProviderConfigAsync( + @NonNull SamlProviderConfig.UpdateRequest request) { + return updateSamlProviderConfigOp(request).callAsync(firebaseApp); + } + + private CallableOperation updateSamlProviderConfigOp( + final SamlProviderConfig.UpdateRequest request) { + checkNotDestroyed(); + checkNotNull(request, "Update request must not be null."); + checkArgument(!request.getProperties().isEmpty(), + "Update request must have at least one property set."); + final FirebaseUserManager userManager = getUserManager(); + return new CallableOperation() { + @Override + protected SamlProviderConfig execute() throws FirebaseAuthException { + return userManager.updateSamlProviderConfig(request); + } + }; + } + + /** + * Gets the SAML Auth provider config corresponding to the specified provider ID. + * + * @param providerId A provider ID string. + * @return An {@link SamlProviderConfig} instance. + * @throws IllegalArgumentException If the provider ID string is null or empty, or is not prefixed + * with 'saml'. + * @throws FirebaseAuthException If an error occurs while retrieving the provider config. + */ + public SamlProviderConfig getSamlProviderConfig(@NonNull String providerId) + throws FirebaseAuthException { + return getSamlProviderConfigOp(providerId).call(); + } + + /** + * Similar to {@link #getSamlProviderConfig(String)} but performs the operation asynchronously. + * Page size is limited to 100 provider configs. + * + * @param providerId A provider ID string. + * @return An {@code ApiFuture} which will complete successfully with an + * {@link SamlProviderConfig} instance. If an error occurs while retrieving the provider + * config or if the specified provider ID does not exist, the future throws a + * {@link FirebaseAuthException}. + * @throws IllegalArgumentException If the provider ID string is null or empty, or is not prefixed + * with 'saml'. + */ + public ApiFuture getSamlProviderConfigAsync(@NonNull String providerId) { + return getSamlProviderConfigOp(providerId).callAsync(firebaseApp); + } + + private CallableOperation + getSamlProviderConfigOp(final String providerId) { + checkNotDestroyed(); + SamlProviderConfig.checkSamlProviderId(providerId); + final FirebaseUserManager userManager = getUserManager(); + return new CallableOperation() { + @Override + protected SamlProviderConfig execute() throws FirebaseAuthException { + return userManager.getSamlProviderConfig(providerId); + } + }; + } + + /** + * Gets a page of SAML Auth provider configs starting from the specified {@code pageToken}. Page + * size is limited to 100 provider configs. + * + * @param pageToken A non-empty page token string, or null to retrieve the first page of provider + * configs. + * @return A {@link ListProviderConfigsPage} instance. + * @throws IllegalArgumentException If the specified page token is empty. + * @throws FirebaseAuthException If an error occurs while retrieving provider config data. + */ + public ListProviderConfigsPage listSamlProviderConfigs( + @Nullable String pageToken) throws FirebaseAuthException { + return listSamlProviderConfigs( + pageToken, + FirebaseUserManager.MAX_LIST_PROVIDER_CONFIGS_RESULTS); + } + + /** + * Gets a page of SAML Auth provider configs starting from the specified {@code pageToken}. + * + * @param pageToken A non-empty page token string, or null to retrieve the first page of provider + * configs. + * @param maxResults Maximum number of provider configs to include in the returned page. This may + * not exceed 100. + * @return A {@link ListProviderConfigsPage} instance. + * @throws IllegalArgumentException If the specified page token is empty, or max results value is + * invalid. + * @throws FirebaseAuthException If an error occurs while retrieving provider config data. + */ + public ListProviderConfigsPage listSamlProviderConfigs( + @Nullable String pageToken, int maxResults) throws FirebaseAuthException { + return listSamlProviderConfigsOp(pageToken, maxResults).call(); + } + + /** + * Similar to {@link #listSamlProviderConfigs(String)} but performs the operation asynchronously. + * Page size is limited to 100 provider configs. + * + * @param pageToken A non-empty page token string, or null to retrieve the first page of provider + * configs. + * @return An {@code ApiFuture} which will complete successfully with a + * {@link ListProviderConfigsPage} instance. If an error occurs while retrieving provider + * config data, the future throws an exception. + * @throws IllegalArgumentException If the specified page token is empty. + */ + public ApiFuture> listSamlProviderConfigsAsync( + @Nullable String pageToken) { + int maxResults = FirebaseUserManager.MAX_LIST_PROVIDER_CONFIGS_RESULTS; + return listSamlProviderConfigsAsync(pageToken, maxResults); + } + + /** + * Similar to {@link #listSamlProviderConfigs(String, int)} but performs the operation + * asynchronously. + * + * @param pageToken A non-empty page token string, or null to retrieve the first page of provider + * configs. + * @param maxResults Maximum number of provider configs to include in the returned page. This may + * not exceed 100. + * @return An {@code ApiFuture} which will complete successfully with a + * {@link ListProviderConfigsPage} instance. If an error occurs while retrieving provider + * config data, the future throws an exception. + * @throws IllegalArgumentException If the specified page token is empty, or max results value is + * invalid. + */ + public ApiFuture> listSamlProviderConfigsAsync( + @Nullable String pageToken, + int maxResults) { + return listSamlProviderConfigsOp(pageToken, maxResults).callAsync(firebaseApp); + } + + private CallableOperation, FirebaseAuthException> + listSamlProviderConfigsOp(@Nullable final String pageToken, final int maxResults) { + checkNotDestroyed(); + final FirebaseUserManager userManager = getUserManager(); + final DefaultSamlProviderConfigSource source = new DefaultSamlProviderConfigSource(userManager); + final ListProviderConfigsPage.Factory factory = + new ListProviderConfigsPage.Factory(source, maxResults, pageToken); + return + new CallableOperation, FirebaseAuthException>() { + @Override + protected ListProviderConfigsPage execute() + throws FirebaseAuthException { + return factory.create(); + } + }; + } + + /** + * Deletes the SAML Auth provider config identified by the specified provider ID. + * + * @param providerId A provider ID string. + * @throws IllegalArgumentException If the provider ID string is null or empty, or is not prefixed + * with "saml.". + * @throws FirebaseAuthException If an error occurs while deleting the provider config. + */ + public void deleteSamlProviderConfig(@NonNull String providerId) throws FirebaseAuthException { + deleteSamlProviderConfigOp(providerId).call(); + } + + /** + * Similar to {@link #deleteSamlProviderConfig} but performs the operation asynchronously. + * + * @param providerId A provider ID string. + * @return An {@code ApiFuture} which will complete successfully when the specified provider + * config has been deleted. If an error occurs while deleting the provider config, the future + * throws a {@link FirebaseAuthException}. + * @throws IllegalArgumentException If the provider ID string is null or empty, or is not prefixed + * with "saml.". + */ + public ApiFuture deleteSamlProviderConfigAsync(String providerId) { + return deleteSamlProviderConfigOp(providerId).callAsync(firebaseApp); + } + + private CallableOperation deleteSamlProviderConfigOp( + final String providerId) { + checkNotDestroyed(); + SamlProviderConfig.checkSamlProviderId(providerId); + final FirebaseUserManager userManager = getUserManager(); + return new CallableOperation() { + @Override + protected Void execute() throws FirebaseAuthException { + userManager.deleteSamlProviderConfig(providerId); + return null; + } + }; + } + + FirebaseApp getFirebaseApp() { + return this.firebaseApp; + } + + FirebaseTokenVerifier getCookieVerifier() { + return this.cookieVerifier.get(); + } + + FirebaseUserManager getUserManager() { + return this.userManager.get(); + } + + protected Supplier threadSafeMemoize(final Supplier supplier) { + return Suppliers.memoize( + new Supplier() { + @Override + public T get() { + checkNotNull(supplier); + synchronized (lock) { + checkNotDestroyed(); + return supplier.get(); + } + } + }); + } + + void checkNotDestroyed() { + synchronized (lock) { + checkState( + !destroyed.get(), + "FirebaseAuth instance is no longer alive. This happens when " + + "the parent FirebaseApp instance has been deleted."); + } + } + + final void destroy() { + synchronized (lock) { + doDestroy(); + destroyed.set(true); + } + } + + /** Performs any additional required clean up. */ + protected abstract void doDestroy(); + + static Builder builder() { + return new Builder(); + } + + static class Builder { + protected FirebaseApp firebaseApp; + private Supplier tokenFactory; + private Supplier idTokenVerifier; + private Supplier cookieVerifier; + private Supplier userManager; + + private Builder() {} + + Builder setFirebaseApp(FirebaseApp firebaseApp) { + this.firebaseApp = firebaseApp; + return this; + } + + Builder setTokenFactory(Supplier tokenFactory) { + this.tokenFactory = tokenFactory; + return this; + } + + Builder setIdTokenVerifier(Supplier idTokenVerifier) { + this.idTokenVerifier = idTokenVerifier; + return this; + } + + Builder setCookieVerifier(Supplier cookieVerifier) { + this.cookieVerifier = cookieVerifier; + return this; + } + + Builder setUserManager(Supplier userManager) { + this.userManager = userManager; + return this; + } + } +} diff --git a/src/main/java/com/google/firebase/auth/FirebaseAuth.java b/src/main/java/com/google/firebase/auth/FirebaseAuth.java index 923778af4..f44bd2343 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseAuth.java +++ b/src/main/java/com/google/firebase/auth/FirebaseAuth.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google Inc. + * Copyright 2017 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,36 +18,19 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.common.base.Preconditions.checkState; -import com.google.api.client.json.JsonFactory; import com.google.api.client.util.Clock; import com.google.api.core.ApiFuture; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; import com.google.common.base.Supplier; -import com.google.common.base.Suppliers; import com.google.firebase.FirebaseApp; import com.google.firebase.ImplFirebaseTrampolines; -import com.google.firebase.auth.FirebaseUserManager.EmailLinkType; -import com.google.firebase.auth.FirebaseUserManager.UserImportRequest; -import com.google.firebase.auth.ListUsersPage.DefaultUserSource; -import com.google.firebase.auth.ListUsersPage.PageFactory; -import com.google.firebase.auth.UserRecord.CreateRequest; -import com.google.firebase.auth.UserRecord.UpdateRequest; import com.google.firebase.auth.internal.FirebaseTokenFactory; +import com.google.firebase.auth.multitenancy.TenantManager; import com.google.firebase.internal.CallableOperation; import com.google.firebase.internal.FirebaseService; import com.google.firebase.internal.NonNull; -import com.google.firebase.internal.Nullable; - -import java.io.IOException; -import java.util.Collection; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.atomic.AtomicBoolean; /** * This class is the entry point for all server-side Firebase Authentication actions. @@ -57,29 +40,24 @@ * custom tokens for use by client-side code, verifying Firebase ID Tokens received from clients, or * creating new FirebaseApp instances that are scoped to a particular authentication UID. */ -public class FirebaseAuth { +public final class FirebaseAuth extends AbstractFirebaseAuth { private static final String SERVICE_ID = FirebaseAuth.class.getName(); - private static final String ERROR_CUSTOM_TOKEN = "ERROR_CUSTOM_TOKEN"; - - private final Object lock = new Object(); - private final AtomicBoolean destroyed = new AtomicBoolean(false); + private final Supplier tenantManager; - private final FirebaseApp firebaseApp; - private final Supplier tokenFactory; - private final Supplier idTokenVerifier; - private final Supplier cookieVerifier; - private final Supplier userManager; - private final JsonFactory jsonFactory; + FirebaseAuth(final Builder builder) { + super(builder); + tenantManager = threadSafeMemoize(new Supplier() { + @Override + public TenantManager get() { + return new TenantManager(builder.firebaseApp); + } + }); + } - private FirebaseAuth(Builder builder) { - this.firebaseApp = checkNotNull(builder.firebaseApp); - this.tokenFactory = threadSafeMemoize(builder.tokenFactory); - this.idTokenVerifier = threadSafeMemoize(builder.idTokenVerifier); - this.cookieVerifier = threadSafeMemoize(builder.cookieVerifier); - this.userManager = threadSafeMemoize(builder.userManager); - this.jsonFactory = firebaseApp.getOptions().getJsonFactory(); + public TenantManager getTenantManager() { + return tenantManager.get(); } /** @@ -98,8 +76,8 @@ public static FirebaseAuth getInstance() { * @return A FirebaseAuth instance. */ public static synchronized FirebaseAuth getInstance(FirebaseApp app) { - FirebaseAuthService service = ImplFirebaseTrampolines.getService(app, SERVICE_ID, - FirebaseAuthService.class); + FirebaseAuthService service = + ImplFirebaseTrampolines.getService(app, SERVICE_ID, FirebaseAuthService.class); if (service == null) { service = ImplFirebaseTrampolines.addService(app, new FirebaseAuthService(app)); } @@ -107,8 +85,8 @@ public static synchronized FirebaseAuth getInstance(FirebaseApp app) { } /** - * Creates a new Firebase session cookie from the given ID token and options. The returned JWT - * can be set as a server-side session cookie with a custom cookie policy. + * Creates a new Firebase session cookie from the given ID token and options. The returned JWT can + * be set as a server-side session cookie with a custom cookie policy. * * @param idToken The Firebase ID token to exchange for a session cookie. * @param options Additional options required to create the cookie. @@ -116,8 +94,8 @@ public static synchronized FirebaseAuth getInstance(FirebaseApp app) { * @throws IllegalArgumentException If the ID token is null or empty, or if options is null. * @throws FirebaseAuthException If an error occurs while generating the session cookie. */ - public String createSessionCookie( - @NonNull String idToken, @NonNull SessionCookieOptions options) throws FirebaseAuthException { + public String createSessionCookie(@NonNull String idToken, @NonNull SessionCookieOptions options) + throws FirebaseAuthException { return createSessionCookieOp(idToken, options).call(); } @@ -127,14 +105,14 @@ public String createSessionCookie( * * @param idToken The Firebase ID token to exchange for a session cookie. * @param options Additional options required to create the cookie. - * @return An {@code ApiFuture} which will complete successfully with a session cookie string. - * If an error occurs while generating the cookie or if the specified ID token is invalid, - * the future throws a {@link FirebaseAuthException}. + * @return An {@code ApiFuture} which will complete successfully with a session cookie string. If + * an error occurs while generating the cookie or if the specified ID token is invalid, the + * future throws a {@link FirebaseAuthException}. * @throws IllegalArgumentException If the ID token is null or empty, or if options is null. */ public ApiFuture createSessionCookieAsync( @NonNull String idToken, @NonNull SessionCookieOptions options) { - return createSessionCookieOp(idToken, options).callAsync(firebaseApp); + return createSessionCookieOp(idToken, options).callAsync(getFirebaseApp()); } private CallableOperation createSessionCookieOp( @@ -157,8 +135,8 @@ protected String execute() throws FirebaseAuthException { *

If verified successfully, returns a parsed version of the cookie from which the UID and the * other claims can be read. If the cookie is invalid, throws a {@link FirebaseAuthException}. * - *

This method does not check whether the cookie has been revoked. See - * {@link #verifySessionCookie(String, boolean)}. + *

This method does not check whether the cookie has been revoked. See {@link + * #verifySessionCookie(String, boolean)}. * * @param cookie A Firebase session cookie string to verify and parse. * @return A {@link FirebaseToken} representing the verified and decoded cookie. @@ -170,20 +148,18 @@ public FirebaseToken verifySessionCookie(String cookie) throws FirebaseAuthExcep /** * Parses and verifies a Firebase session cookie. * - *

If {@code checkRevoked} is true, additionally verifies that the cookie has not been - * revoked. + *

If {@code checkRevoked} is true, additionally verifies that the cookie has not been revoked. * *

If verified successfully, returns a parsed version of the cookie from which the UID and the - * other claims can be read. If the cookie is invalid or has been revoked while - * {@code checkRevoked} is true, throws a {@link FirebaseAuthException}. + * other claims can be read. If the cookie is invalid or has been revoked while {@code + * checkRevoked} is true, throws a {@link FirebaseAuthException}. * * @param cookie A Firebase session cookie string to verify and parse. - * @param checkRevoked A boolean indicating whether to check if the cookie was explicitly - * revoked. + * @param checkRevoked A boolean indicating whether to check if the cookie was explicitly revoked. * @return A {@link FirebaseToken} representing the verified and decoded cookie. */ - public FirebaseToken verifySessionCookie( - String cookie, boolean checkRevoked) throws FirebaseAuthException { + public FirebaseToken verifySessionCookie(String cookie, boolean checkRevoked) + throws FirebaseAuthException { return verifySessionCookieOp(cookie, checkRevoked).call(); } @@ -203,13 +179,12 @@ public ApiFuture verifySessionCookieAsync(String cookie) { * asynchronously. * * @param cookie A Firebase session cookie string to verify and parse. - * @param checkRevoked A boolean indicating whether to check if the cookie was explicitly - * revoked. + * @param checkRevoked A boolean indicating whether to check if the cookie was explicitly revoked. * @return An {@code ApiFuture} which will complete successfully with the parsed cookie, or * unsuccessfully with the failure Exception. */ public ApiFuture verifySessionCookieAsync(String cookie, boolean checkRevoked) { - return verifySessionCookieOp(cookie, checkRevoked).callAsync(firebaseApp); + return verifySessionCookieOp(cookie, checkRevoked).callAsync(getFirebaseApp()); } private CallableOperation verifySessionCookieOp( @@ -227,7 +202,7 @@ public FirebaseToken execute() throws FirebaseAuthException { @VisibleForTesting FirebaseTokenVerifier getSessionCookieVerifier(boolean checkRevoked) { - FirebaseTokenVerifier verifier = cookieVerifier.get(); + FirebaseTokenVerifier verifier = getCookieVerifier(); if (checkRevoked) { FirebaseUserManager userManager = getUserManager(); verifier = RevocationCheckDecorator.decorateSessionCookieVerifier(verifier, userManager); @@ -235,1109 +210,41 @@ FirebaseTokenVerifier getSessionCookieVerifier(boolean checkRevoked) { return verifier; } - /** - * Creates a Firebase custom token for the given UID. This token can then be sent back to a client - * application to be used with the - * signInWithCustomToken - * authentication API. - * - *

{@link FirebaseApp} must have been initialized with service account credentials to use - * call this method. - * - * @param uid The UID to store in the token. This identifies the user to other Firebase services - * (Realtime Database, Firebase Auth, etc.). Should be less than 128 characters. - * @return A Firebase custom token string. - * @throws IllegalArgumentException If the specified uid is null or empty, or if the app has not - * been initialized with service account credentials. - * @throws FirebaseAuthException If an error occurs while generating the custom token. - */ - public String createCustomToken(@NonNull String uid) throws FirebaseAuthException { - return createCustomToken(uid, null); - } - - /** - * Creates a Firebase custom token for the given UID, containing the specified additional - * claims. This token can then be sent back to a client application to be used with the - * signInWithCustomToken - * authentication API. - * - *

This method attempts to generate a token using: - *

    - *
  1. the private key of {@link FirebaseApp}'s service account credentials, if provided at - * initialization. - *
  2. the IAM service - * if a service account email was specified via - * {@link com.google.firebase.FirebaseOptions.Builder#setServiceAccountId(String)}. - *
  3. the App Identity - * service if the code is deployed in the Google App Engine standard environment. - *
  4. the - * local Metadata server if the code is deployed in a different GCP-managed environment - * like Google Compute Engine. - *
- * - *

This method throws an exception when all the above fail. - * - * @param uid The UID to store in the token. This identifies the user to other Firebase services - * (Realtime Database, Firebase Auth, etc.). Should be less than 128 characters. - * @param developerClaims Additional claims to be stored in the token (and made available to - * security rules in Database, Storage, etc.). These must be able to be serialized to JSON - * (e.g. contain only Maps, Arrays, Strings, Booleans, Numbers, etc.) - * @return A Firebase custom token string. - * @throws IllegalArgumentException If the specified uid is null or empty. - * @throws IllegalStateException If the SDK fails to discover a viable approach for signing - * tokens. - * @throws FirebaseAuthException If an error occurs while generating the custom token. - */ - public String createCustomToken(@NonNull String uid, - @Nullable Map developerClaims) throws FirebaseAuthException { - return createCustomTokenOp(uid, developerClaims).call(); - } - - /** - * Similar to {@link #createCustomToken(String)} but performs the operation asynchronously. - * - * @param uid The UID to store in the token. This identifies the user to other Firebase services - * (Realtime Database, Firebase Auth, etc.). Should be less than 128 characters. - * @return An {@code ApiFuture} which will complete successfully with the created Firebase custom - * token, or unsuccessfully with the failure Exception. - * @throws IllegalArgumentException If the specified uid is null or empty, or if the app has not - * been initialized with service account credentials. - */ - public ApiFuture createCustomTokenAsync(@NonNull String uid) { - return createCustomTokenAsync(uid, null); - } - - /** - * Similar to {@link #createCustomToken(String, Map)} but performs the operation - * asynchronously. - * - * @param uid The UID to store in the token. This identifies the user to other Firebase services - * (Realtime Database, Storage, etc.). Should be less than 128 characters. - * @param developerClaims Additional claims to be stored in the token (and made available to - * security rules in Database, Storage, etc.). These must be able to be serialized to JSON - * (e.g. contain only Maps, Arrays, Strings, Booleans, Numbers, etc.) - * @return An {@code ApiFuture} which will complete successfully with the created Firebase custom - * token, or unsuccessfully with the failure Exception. - * @throws IllegalArgumentException If the specified uid is null or empty, or if the app has not - * been initialized with service account credentials. - */ - public ApiFuture createCustomTokenAsync( - @NonNull String uid, @Nullable Map developerClaims) { - return createCustomTokenOp(uid, developerClaims).callAsync(firebaseApp); - } - - private CallableOperation createCustomTokenOp( - final String uid, final Map developerClaims) { - checkNotDestroyed(); - checkArgument(!Strings.isNullOrEmpty(uid), "uid must not be null or empty"); - final FirebaseTokenFactory tokenFactory = this.tokenFactory.get(); - return new CallableOperation() { - @Override - public String execute() throws FirebaseAuthException { - try { - return tokenFactory.createSignedCustomAuthTokenForUser(uid, developerClaims); - } catch (IOException e) { - throw new FirebaseAuthException(ERROR_CUSTOM_TOKEN, - "Failed to generate a custom token", e); - } - } - }; - } - - /** - * Parses and verifies a Firebase ID Token. - * - *

A Firebase application can identify itself to a trusted backend server by sending its - * Firebase ID Token (accessible via the {@code getToken} API in the Firebase Authentication - * client) with its requests. The backend server can then use the {@code verifyIdToken()} method - * to verify that the token is valid. This method ensures that the token is correctly signed, - * has not expired, and it was issued to the Firebase project associated with this - * {@link FirebaseAuth} instance. - * - *

This method does not check whether a token has been revoked. Use - * {@link #verifyIdToken(String, boolean)} to perform an additional revocation check. - * - * @param token A Firebase ID token string to parse and verify. - * @return A {@link FirebaseToken} representing the verified and decoded token. - * @throws IllegalArgumentException If the token is null, empty, or if the {@link FirebaseApp} - * instance does not have a project ID associated with it. - * @throws FirebaseAuthException If an error occurs while parsing or validating the token. - */ - public FirebaseToken verifyIdToken(@NonNull String token) throws FirebaseAuthException { - return verifyIdToken(token, false); - } - - /** - * Parses and verifies a Firebase ID Token. - * - *

A Firebase application can identify itself to a trusted backend server by sending its - * Firebase ID Token (accessible via the {@code getToken} API in the Firebase Authentication - * client) with its requests. The backend server can then use the {@code verifyIdToken()} method - * to verify that the token is valid. This method ensures that the token is correctly signed, - * has not expired, and it was issued to the Firebase project associated with this - * {@link FirebaseAuth} instance. - * - *

If {@code checkRevoked} is set to true, this method performs an additional check to see - * if the ID token has been revoked since it was issues. This requires making an additional - * remote API call. - * - * @param token A Firebase ID token string to parse and verify. - * @param checkRevoked A boolean denoting whether to check if the tokens were revoked. - * @return A {@link FirebaseToken} representing the verified and decoded token. - * @throws IllegalArgumentException If the token is null, empty, or if the {@link FirebaseApp} - * instance does not have a project ID associated with it. - * @throws FirebaseAuthException If an error occurs while parsing or validating the token. - */ - public FirebaseToken verifyIdToken( - @NonNull String token, boolean checkRevoked) throws FirebaseAuthException { - return verifyIdTokenOp(token, checkRevoked).call(); - } - - /** - * Similar to {@link #verifyIdToken(String)} but performs the operation asynchronously. - * - * @param token A Firebase ID Token to verify and parse. - * @return An {@code ApiFuture} which will complete successfully with the parsed token, or - * unsuccessfully with a {@link FirebaseAuthException}. - * @throws IllegalArgumentException If the token is null, empty, or if the {@link FirebaseApp} - * instance does not have a project ID associated with it. - */ - public ApiFuture verifyIdTokenAsync(@NonNull String token) { - return verifyIdTokenAsync(token, false); - } - - /** - * Similar to {@link #verifyIdToken(String, boolean)} but performs the operation asynchronously. - * - * @param token A Firebase ID Token to verify and parse. - * @param checkRevoked A boolean denoting whether to check if the tokens were revoked. - * @return An {@code ApiFuture} which will complete successfully with the parsed token, or - * unsuccessfully with a {@link FirebaseAuthException}. - * @throws IllegalArgumentException If the token is null, empty, or if the {@link FirebaseApp} - * instance does not have a project ID associated with it. - */ - public ApiFuture verifyIdTokenAsync(@NonNull String token, boolean checkRevoked) { - return verifyIdTokenOp(token, checkRevoked).callAsync(firebaseApp); - } - - private CallableOperation verifyIdTokenOp( - final String token, final boolean checkRevoked) { - checkNotDestroyed(); - checkArgument(!Strings.isNullOrEmpty(token), "ID token must not be null or empty"); - final FirebaseTokenVerifier verifier = getIdTokenVerifier(checkRevoked); - return new CallableOperation() { - @Override - protected FirebaseToken execute() throws FirebaseAuthException { - return verifier.verifyToken(token); - } - }; - } - - @VisibleForTesting - FirebaseTokenVerifier getIdTokenVerifier(boolean checkRevoked) { - FirebaseTokenVerifier verifier = idTokenVerifier.get(); - if (checkRevoked) { - FirebaseUserManager userManager = getUserManager(); - verifier = RevocationCheckDecorator.decorateIdTokenVerifier(verifier, userManager); - } - return verifier; - } - - /** - * Revokes all refresh tokens for the specified user. - * - *

Updates the user's tokensValidAfterTimestamp to the current UTC time expressed in - * milliseconds since the epoch and truncated to 1 second accuracy. It is important that the - * server on which this is called has its clock set correctly and synchronized. - * - *

While this will revoke all sessions for a specified user and disable any new ID tokens for - * existing sessions from getting minted, existing ID tokens may remain active until their - * natural expiration (one hour). - * To verify that ID tokens are revoked, use {@link #verifyIdTokenAsync(String, boolean)}. - * - * @param uid The user id for which tokens are revoked. - * @throws IllegalArgumentException If the user ID is null or empty. - * @throws FirebaseAuthException If an error occurs while revoking tokens. - */ - public void revokeRefreshTokens(@NonNull String uid) throws FirebaseAuthException { - revokeRefreshTokensOp(uid).call(); - } - - /** - * Similar to {@link #revokeRefreshTokens(String)} but performs the operation asynchronously. - * - * @param uid The user id for which tokens are revoked. - * @return An {@code ApiFuture} which will complete successfully or fail with a - * {@link FirebaseAuthException} in the event of an error. - * @throws IllegalArgumentException If the user ID is null or empty. - */ - public ApiFuture revokeRefreshTokensAsync(@NonNull String uid) { - return revokeRefreshTokensOp(uid).callAsync(firebaseApp); - } - - private CallableOperation revokeRefreshTokensOp(final String uid) { - checkNotDestroyed(); - checkArgument(!Strings.isNullOrEmpty(uid), "uid must not be null or empty"); - final FirebaseUserManager userManager = getUserManager(); - return new CallableOperation() { - @Override - protected Void execute() throws FirebaseAuthException { - int currentTimeSeconds = (int) (System.currentTimeMillis() / 1000); - UpdateRequest request = new UpdateRequest(uid).setValidSince(currentTimeSeconds); - userManager.updateUser(request, jsonFactory); - return null; - } - }; - } - - /** - * Gets the user data corresponding to the specified user ID. - * - * @param uid A user ID string. - * @return A {@link UserRecord} instance. - * @throws IllegalArgumentException If the user ID string is null or empty. - * @throws FirebaseAuthException If an error occurs while retrieving user data. - */ - public UserRecord getUser(@NonNull String uid) throws FirebaseAuthException { - return getUserOp(uid).call(); - } - - /** - * Similar to {@link #getUser(String)} but performs the operation asynchronously. - * - * @param uid A user ID string. - * @return An {@code ApiFuture} which will complete successfully with a {@link UserRecord} - * instance. If an error occurs while retrieving user data or if the specified user ID does - * not exist, the future throws a {@link FirebaseAuthException}. - * @throws IllegalArgumentException If the user ID string is null or empty. - */ - public ApiFuture getUserAsync(@NonNull String uid) { - return getUserOp(uid).callAsync(firebaseApp); - } - - private CallableOperation getUserOp(final String uid) { - checkNotDestroyed(); - checkArgument(!Strings.isNullOrEmpty(uid), "uid must not be null or empty"); - final FirebaseUserManager userManager = getUserManager(); - return new CallableOperation() { - @Override - protected UserRecord execute() throws FirebaseAuthException { - return userManager.getUserById(uid); - } - }; - } - - /** - * Gets the user data corresponding to the specified user email. - * - * @param email A user email address string. - * @return A {@link UserRecord} instance. - * @throws IllegalArgumentException If the email is null or empty. - * @throws FirebaseAuthException If an error occurs while retrieving user data. - */ - public UserRecord getUserByEmail(@NonNull String email) throws FirebaseAuthException { - return getUserByEmailOp(email).call(); - } - - /** - * Similar to {@link #getUserByEmail(String)} but performs the operation asynchronously. - * - * @param email A user email address string. - * @return An {@code ApiFuture} which will complete successfully with a {@link UserRecord} - * instance. If an error occurs while retrieving user data or if the email address does not - * correspond to a user, the future throws a {@link FirebaseAuthException}. - * @throws IllegalArgumentException If the email is null or empty. - */ - public ApiFuture getUserByEmailAsync(@NonNull String email) { - return getUserByEmailOp(email).callAsync(firebaseApp); - } - - private CallableOperation getUserByEmailOp( - final String email) { - checkNotDestroyed(); - checkArgument(!Strings.isNullOrEmpty(email), "email must not be null or empty"); - final FirebaseUserManager userManager = getUserManager(); - return new CallableOperation() { - @Override - protected UserRecord execute() throws FirebaseAuthException { - return userManager.getUserByEmail(email); - } - }; - } - - /** - * Gets the user data corresponding to the specified user phone number. - * - * @param phoneNumber A user phone number string. - * @return A a {@link UserRecord} instance. - * @throws IllegalArgumentException If the phone number is null or empty. - * @throws FirebaseAuthException If an error occurs while retrieving user data. - */ - public UserRecord getUserByPhoneNumber(@NonNull String phoneNumber) throws FirebaseAuthException { - return getUserByPhoneNumberOp(phoneNumber).call(); - } - - /** - * Gets the user data corresponding to the specified user phone number. - * - * @param phoneNumber A user phone number string. - * @return An {@code ApiFuture} which will complete successfully with a {@link UserRecord} - * instance. If an error occurs while retrieving user data or if the phone number does not - * correspond to a user, the future throws a {@link FirebaseAuthException}. - * @throws IllegalArgumentException If the phone number is null or empty. - */ - public ApiFuture getUserByPhoneNumberAsync(@NonNull String phoneNumber) { - return getUserByPhoneNumberOp(phoneNumber).callAsync(firebaseApp); - } - - private CallableOperation getUserByPhoneNumberOp( - final String phoneNumber) { - checkNotDestroyed(); - checkArgument(!Strings.isNullOrEmpty(phoneNumber), "phone number must not be null or empty"); - final FirebaseUserManager userManager = getUserManager(); - return new CallableOperation() { - @Override - protected UserRecord execute() throws FirebaseAuthException { - return userManager.getUserByPhoneNumber(phoneNumber); - } - }; - } - - /** - * Gets the user data corresponding to the specified identifiers. - * - *

There are no ordering guarantees; in particular, the nth entry in the users result list is - * not guaranteed to correspond to the nth entry in the input parameters list. - * - *

A maximum of 100 identifiers may be specified. If more than 100 identifiers are - * supplied, this method throws an {@link IllegalArgumentException}. - * - * @param identifiers The identifiers used to indicate which user records should be returned. Must - * have 100 or fewer entries. - * @return The corresponding user records. - * @throws IllegalArgumentException If any of the identifiers are invalid or if more than 100 - * identifiers are specified. - * @throws NullPointerException If the identifiers parameter is null. - * @throws FirebaseAuthException If an error occurs while retrieving user data. - */ - public GetUsersResult getUsers(@NonNull Collection identifiers) - throws FirebaseAuthException { - return getUsersOp(identifiers).call(); - } - - /** - * Gets the user data corresponding to the specified identifiers. - * - *

There are no ordering guarantees; in particular, the nth entry in the users result list is - * not guaranteed to correspond to the nth entry in the input parameters list. - * - *

A maximum of 100 identifiers may be specified. If more than 100 identifiers are - * supplied, this method throws an {@link IllegalArgumentException}. - * - * @param identifiers The identifiers used to indicate which user records should be returned. - * Must have 100 or fewer entries. - * @return An {@code ApiFuture} that resolves to the corresponding user records. - * @throws IllegalArgumentException If any of the identifiers are invalid or if more than 100 - * identifiers are specified. - * @throws NullPointerException If the identifiers parameter is null. - */ - public ApiFuture getUsersAsync(@NonNull Collection identifiers) { - return getUsersOp(identifiers).callAsync(firebaseApp); - } - - private CallableOperation getUsersOp( - @NonNull final Collection identifiers) { - checkNotDestroyed(); - checkNotNull(identifiers, "identifiers must not be null"); - checkArgument(identifiers.size() <= FirebaseUserManager.MAX_GET_ACCOUNTS_BATCH_SIZE, - "identifiers parameter must have <= " + FirebaseUserManager.MAX_GET_ACCOUNTS_BATCH_SIZE - + " entries."); - - final FirebaseUserManager userManager = getUserManager(); - return new CallableOperation() { - @Override - protected GetUsersResult execute() throws FirebaseAuthException { - Set users = userManager.getAccountInfo(identifiers); - Set notFound = new HashSet<>(); - for (UserIdentifier id : identifiers) { - if (!isUserFound(id, users)) { - notFound.add(id); - } - } - return new GetUsersResult(users, notFound); - } - }; - } - - private boolean isUserFound(UserIdentifier id, Collection userRecords) { - for (UserRecord userRecord : userRecords) { - if (id.matches(userRecord)) { - return true; - } - } - return false; - } - - /** - * Gets a page of users starting from the specified {@code pageToken}. Page size is - * limited to 1000 users. - * - * @param pageToken A non-empty page token string, or null to retrieve the first page of users. - * @return A {@link ListUsersPage} instance. - * @throws IllegalArgumentException If the specified page token is empty. - * @throws FirebaseAuthException If an error occurs while retrieving user data. - */ - public ListUsersPage listUsers(@Nullable String pageToken) throws FirebaseAuthException { - return listUsers(pageToken, FirebaseUserManager.MAX_LIST_USERS_RESULTS); - } - - /** - * Gets a page of users starting from the specified {@code pageToken}. - * - * @param pageToken A non-empty page token string, or null to retrieve the first page of users. - * @param maxResults Maximum number of users to include in the returned page. This may not - * exceed 1000. - * @return A {@link ListUsersPage} instance. - * @throws IllegalArgumentException If the specified page token is empty, or max results value - * is invalid. - * @throws FirebaseAuthException If an error occurs while retrieving user data. - */ - public ListUsersPage listUsers( - @Nullable String pageToken, int maxResults) throws FirebaseAuthException { - return listUsersOp(pageToken, maxResults).call(); - } - - /** - * Similar to {@link #listUsers(String)} but performs the operation asynchronously. - * - * @param pageToken A non-empty page token string, or null to retrieve the first page of users. - * @return An {@code ApiFuture} which will complete successfully with a {@link ListUsersPage} - * instance. If an error occurs while retrieving user data, the future throws an exception. - * @throws IllegalArgumentException If the specified page token is empty. - */ - public ApiFuture listUsersAsync(@Nullable String pageToken) { - return listUsersAsync(pageToken, FirebaseUserManager.MAX_LIST_USERS_RESULTS); - } - - /** - * Similar to {@link #listUsers(String, int)} but performs the operation asynchronously. - * - * @param pageToken A non-empty page token string, or null to retrieve the first page of users. - * @param maxResults Maximum number of users to include in the returned page. This may not - * exceed 1000. - * @return An {@code ApiFuture} which will complete successfully with a {@link ListUsersPage} - * instance. If an error occurs while retrieving user data, the future throws an exception. - * @throws IllegalArgumentException If the specified page token is empty, or max results value - * is invalid. - */ - public ApiFuture listUsersAsync(@Nullable String pageToken, int maxResults) { - return listUsersOp(pageToken, maxResults).callAsync(firebaseApp); - } - - private CallableOperation listUsersOp( - @Nullable final String pageToken, final int maxResults) { - checkNotDestroyed(); - final FirebaseUserManager userManager = getUserManager(); - final PageFactory factory = new PageFactory( - new DefaultUserSource(userManager, jsonFactory), maxResults, pageToken); - return new CallableOperation() { - @Override - protected ListUsersPage execute() throws FirebaseAuthException { - return factory.create(); - } - }; - } - - /** - * Creates a new user account with the attributes contained in the specified - * {@link CreateRequest}. - * - * @param request A non-null {@link CreateRequest} instance. - * @return A {@link UserRecord} instance corresponding to the newly created account. - * @throws NullPointerException if the provided request is null. - * @throws FirebaseAuthException if an error occurs while creating the user account. - */ - public UserRecord createUser(@NonNull CreateRequest request) throws FirebaseAuthException { - return createUserOp(request).call(); - } - - /** - * Similar to {@link #createUser(CreateRequest)} but performs the operation asynchronously. - * - * @param request A non-null {@link CreateRequest} instance. - * @return An {@code ApiFuture} which will complete successfully with a {@link UserRecord} - * instance corresponding to the newly created account. If an error occurs while creating the - * user account, the future throws a {@link FirebaseAuthException}. - * @throws NullPointerException if the provided request is null. - */ - public ApiFuture createUserAsync(@NonNull CreateRequest request) { - return createUserOp(request).callAsync(firebaseApp); - } - - private CallableOperation createUserOp( - final CreateRequest request) { - checkNotDestroyed(); - checkNotNull(request, "create request must not be null"); - final FirebaseUserManager userManager = getUserManager(); - return new CallableOperation() { - @Override - protected UserRecord execute() throws FirebaseAuthException { - String uid = userManager.createUser(request); - return userManager.getUserById(uid); - } - }; - } - - /** - * Updates an existing user account with the attributes contained in the specified - * {@link UpdateRequest}. - * - * @param request A non-null {@link UpdateRequest} instance. - * @return A {@link UserRecord} instance corresponding to the updated user account. - * account, the task fails with a {@link FirebaseAuthException}. - * @throws NullPointerException if the provided update request is null. - * @throws FirebaseAuthException if an error occurs while updating the user account. - */ - public UserRecord updateUser(@NonNull UpdateRequest request) throws FirebaseAuthException { - return updateUserOp(request).call(); - } - - /** - * Similar to {@link #updateUser(UpdateRequest)} but performs the operation asynchronously. - * - * @param request A non-null {@link UpdateRequest} instance. - * @return An {@code ApiFuture} which will complete successfully with a {@link UserRecord} - * instance corresponding to the updated user account. If an error occurs while updating the - * user account, the future throws a {@link FirebaseAuthException}. - */ - public ApiFuture updateUserAsync(@NonNull UpdateRequest request) { - return updateUserOp(request).callAsync(firebaseApp); - } - - private CallableOperation updateUserOp( - final UpdateRequest request) { - checkNotDestroyed(); - checkNotNull(request, "update request must not be null"); - final FirebaseUserManager userManager = getUserManager(); - return new CallableOperation() { - @Override - protected UserRecord execute() throws FirebaseAuthException { - userManager.updateUser(request, jsonFactory); - return userManager.getUserById(request.getUid()); - } - }; - } - - /** - * Sets the specified custom claims on an existing user account. A null claims value removes - * any claims currently set on the user account. The claims should serialize into a valid JSON - * string. The serialized claims must not be larger than 1000 characters. - * - * @param uid A user ID string. - * @param claims A map of custom claims or null. - * @throws FirebaseAuthException If an error occurs while updating custom claims. - * @throws IllegalArgumentException If the user ID string is null or empty, or the claims - * payload is invalid or too large. - */ - public void setCustomUserClaims(@NonNull String uid, - @Nullable Map claims) throws FirebaseAuthException { - setCustomUserClaimsOp(uid, claims).call(); - } - - /** - * @deprecated Use {@link #setCustomUserClaims(String, Map)} instead. - */ - public void setCustomClaims(@NonNull String uid, - @Nullable Map claims) throws FirebaseAuthException { - setCustomUserClaims(uid, claims); - } - - /** - * Similar to {@link #setCustomUserClaims(String, Map)} but performs the operation asynchronously. - * - * @param uid A user ID string. - * @param claims A map of custom claims or null. - * @return An {@code ApiFuture} which will complete successfully when the user account has been - * updated. If an error occurs while deleting the user account, the future throws a - * {@link FirebaseAuthException}. - * @throws IllegalArgumentException If the user ID string is null or empty. - */ - public ApiFuture setCustomUserClaimsAsync( - @NonNull String uid, @Nullable Map claims) { - return setCustomUserClaimsOp(uid, claims).callAsync(firebaseApp); - } - - private CallableOperation setCustomUserClaimsOp( - final String uid, final Map claims) { - checkNotDestroyed(); - checkArgument(!Strings.isNullOrEmpty(uid), "uid must not be null or empty"); - final FirebaseUserManager userManager = getUserManager(); - return new CallableOperation() { - @Override - protected Void execute() throws FirebaseAuthException { - final UpdateRequest request = new UpdateRequest(uid).setCustomClaims(claims); - userManager.updateUser(request, jsonFactory); - return null; - } - }; - } - - /** - * Deletes the user identified by the specified user ID. - * - * @param uid A user ID string. - * @throws IllegalArgumentException If the user ID string is null or empty. - * @throws FirebaseAuthException If an error occurs while deleting the user. - */ - public void deleteUser(@NonNull String uid) throws FirebaseAuthException { - deleteUserOp(uid).call(); - } - - /** - * Similar to {@link #deleteUser(String)} but performs the operation asynchronously. - * - * @param uid A user ID string. - * @return An {@code ApiFuture} which will complete successfully when the specified user account - * has been deleted. If an error occurs while deleting the user account, the future throws a - * {@link FirebaseAuthException}. - * @throws IllegalArgumentException If the user ID string is null or empty. - */ - public ApiFuture deleteUserAsync(String uid) { - return deleteUserOp(uid).callAsync(firebaseApp); - } - - private CallableOperation deleteUserOp(final String uid) { - checkNotDestroyed(); - checkArgument(!Strings.isNullOrEmpty(uid), "uid must not be null or empty"); - final FirebaseUserManager userManager = getUserManager(); - return new CallableOperation() { - @Override - protected Void execute() throws FirebaseAuthException { - userManager.deleteUser(uid); - return null; - } - }; - } - - /** - * Deletes the users specified by the given identifiers. - * - *

Deleting a non-existing user does not generate an error (the method is idempotent). - * Non-existing users are considered to be successfully deleted and are therefore included in the - * DeleteUsersResult.getSuccessCount() value. - * - *

A maximum of 1000 identifiers may be supplied. If more than 1000 identifiers are - * supplied, this method throws an {@link IllegalArgumentException}. - * - *

This API has a rate limit of 1 QPS. Exceeding the limit may result in a quota exceeded - * error. If you want to delete more than 1000 users, we suggest adding a delay to ensure you - * don't exceed this limit. - * - * @param uids The uids of the users to be deleted. Must have <= 1000 entries. - * @return The total number of successful/failed deletions, as well as the array of errors that - * correspond to the failed deletions. - * @throw IllegalArgumentException If any of the identifiers are invalid or if more than 1000 - * identifiers are specified. - * @throws FirebaseAuthException If an error occurs while deleting users. - */ - public DeleteUsersResult deleteUsers(List uids) throws FirebaseAuthException { - return deleteUsersOp(uids).call(); - } - - /** - * Similar to {@link #deleteUsers(List)} but performs the operation asynchronously. - * - * @param uids The uids of the users to be deleted. Must have <= 1000 entries. - * @return An {@code ApiFuture} that resolves to the total number of successful/failed - * deletions, as well as the array of errors that correspond to the failed deletions. If an - * error occurs while deleting the user account, the future throws a - * {@link FirebaseAuthException}. - * @throw IllegalArgumentException If any of the identifiers are invalid or if more than 1000 - * identifiers are specified. - */ - public ApiFuture deleteUsersAsync(List uids) { - return deleteUsersOp(uids).callAsync(firebaseApp); - } - - private CallableOperation deleteUsersOp( - final List uids) { - checkNotDestroyed(); - checkNotNull(uids, "uids must not be null"); - for (String uid : uids) { - UserRecord.checkUid(uid); - } - checkArgument(uids.size() <= FirebaseUserManager.MAX_DELETE_ACCOUNTS_BATCH_SIZE, - "uids parameter must have <= " + FirebaseUserManager.MAX_DELETE_ACCOUNTS_BATCH_SIZE - + " entries."); - final FirebaseUserManager userManager = getUserManager(); - return new CallableOperation() { - @Override - protected DeleteUsersResult execute() throws FirebaseAuthException { - return userManager.deleteUsers(uids); - } - }; - } - - /** - * Imports the provided list of users into Firebase Auth. You can import a maximum of 1000 users - * at a time. This operation is optimized for bulk imports and does not check identifier - * uniqueness which could result in duplications. - * - *

{@link UserImportOptions} is required to import users with passwords. See - * {@link #importUsers(List, UserImportOptions)}. - * - * @param users A non-empty list of users to be imported. Length must not exceed 1000. - * @return A {@link UserImportResult} instance. - * @throws IllegalArgumentException If the users list is null, empty or has more than 1000 - * elements. Or if at least one user specifies a password. - * @throws FirebaseAuthException If an error occurs while importing users. - */ - public UserImportResult importUsers(List users) throws FirebaseAuthException { - return importUsers(users, null); - } - - /** - * Imports the provided list of users into Firebase Auth. At most 1000 users can be imported at a - * time. This operation is optimized for bulk imports and will ignore checks on identifier - * uniqueness which could result in duplications. - * - * @param users A non-empty list of users to be imported. Length must not exceed 1000. - * @param options a {@link UserImportOptions} instance or null. Required when importing users - * with passwords. - * @return A {@link UserImportResult} instance. - * @throws IllegalArgumentException If the users list is null, empty or has more than 1000 - * elements. Or if at least one user specifies a password, and options is null. - * @throws FirebaseAuthException If an error occurs while importing users. - */ - public UserImportResult importUsers(List users, - @Nullable UserImportOptions options) throws FirebaseAuthException { - return importUsersOp(users, options).call(); - } - - /** - * Similar to {@link #importUsers(List)} but performs the operation asynchronously. - * - * @param users A non-empty list of users to be imported. Length must not exceed 1000. - * @return An {@code ApiFuture} which will complete successfully when the user accounts are - * imported. If an error occurs while importing the users, the future throws a - * {@link FirebaseAuthException}. - * @throws IllegalArgumentException If the users list is null, empty or has more than 1000 - * elements. Or if at least one user specifies a password. - */ - public ApiFuture importUsersAsync(List users) { - return importUsersAsync(users, null); - } - - /** - * Similar to {@link #importUsers(List, UserImportOptions)} but performs the operation - * asynchronously. - * - * @param users A non-empty list of users to be imported. Length must not exceed 1000. - * @param options a {@link UserImportOptions} instance or null. Required when importing users - * with passwords. - * @return An {@code ApiFuture} which will complete successfully when the user accounts are - * imported. If an error occurs while importing the users, the future throws a - * {@link FirebaseAuthException}. - * @throws IllegalArgumentException If the users list is null, empty or has more than 1000 - * elements. Or if at least one user specifies a password, and options is null. - */ - public ApiFuture importUsersAsync(List users, - @Nullable UserImportOptions options) { - return importUsersOp(users, options).callAsync(firebaseApp); - } - - private CallableOperation importUsersOp( - final List users, final UserImportOptions options) { - checkNotDestroyed(); - final UserImportRequest request = new UserImportRequest(users, options, jsonFactory); - final FirebaseUserManager userManager = getUserManager(); - return new CallableOperation() { - @Override - protected UserImportResult execute() throws FirebaseAuthException { - return userManager.importUsers(request); - } - }; - } - - /** - * Generates the out-of-band email action link for password reset flows for the specified email - * address. - * - * @param email The email of the user whose password is to be reset. - * @return A password reset link. - * @throws IllegalArgumentException If the email address is null or empty. - * @throws FirebaseAuthException If an error occurs while generating the link. - */ - public String generatePasswordResetLink(@NonNull String email) throws FirebaseAuthException { - return generatePasswordResetLink(email, null); - } - - /** - * Generates the out-of-band email action link for password reset flows for the specified email - * address. - * - * @param email The email of the user whose password is to be reset. - * @param settings The action code settings object which defines whether - * the link is to be handled by a mobile app and the additional state information to be - * passed in the deep link. - * @return A password reset link. - * @throws IllegalArgumentException If the email address is null or empty. - * @throws FirebaseAuthException If an error occurs while generating the link. - */ - public String generatePasswordResetLink( - @NonNull String email, @Nullable ActionCodeSettings settings) throws FirebaseAuthException { - return generateEmailActionLinkOp(EmailLinkType.PASSWORD_RESET, email, settings).call(); - } - - /** - * Similar to {@link #generatePasswordResetLink(String)} but performs the operation - * asynchronously. - * - * @param email The email of the user whose password is to be reset. - * @return An {@code ApiFuture} which will complete successfully with the generated email action - * link. If an error occurs while generating the link, the future throws a - * {@link FirebaseAuthException}. - * @throws IllegalArgumentException If the email address is null or empty. - */ - public ApiFuture generatePasswordResetLinkAsync(@NonNull String email) { - return generatePasswordResetLinkAsync(email, null); - } - - /** - * Similar to {@link #generatePasswordResetLink(String, ActionCodeSettings)} but performs the - * operation asynchronously. - * - * @param email The email of the user whose password is to be reset. - * @param settings The action code settings object which defines whether - * the link is to be handled by a mobile app and the additional state information to be - * passed in the deep link. - * @return An {@code ApiFuture} which will complete successfully with the generated email action - * link. If an error occurs while generating the link, the future throws a - * {@link FirebaseAuthException}. - * @throws IllegalArgumentException If the email address is null or empty. - */ - public ApiFuture generatePasswordResetLinkAsync( - @NonNull String email, @Nullable ActionCodeSettings settings) { - return generateEmailActionLinkOp(EmailLinkType.PASSWORD_RESET, email, settings) - .callAsync(firebaseApp); - } - - /** - * Generates the out-of-band email action link for email verification flows for the specified - * email address. - * - * @param email The email of the user to be verified. - * @return An email verification link. - * @throws IllegalArgumentException If the email address is null or empty. - * @throws FirebaseAuthException If an error occurs while generating the link. - */ - public String generateEmailVerificationLink(@NonNull String email) throws FirebaseAuthException { - return generateEmailVerificationLink(email, null); - } - - /** - * Generates the out-of-band email action link for email verification flows for the specified - * email address, using the action code settings provided. - * - * @param email The email of the user to be verified. - * @return An email verification link. - * @throws IllegalArgumentException If the email address is null or empty. - * @throws FirebaseAuthException If an error occurs while generating the link. - */ - public String generateEmailVerificationLink( - @NonNull String email, @Nullable ActionCodeSettings settings) throws FirebaseAuthException { - return generateEmailActionLinkOp(EmailLinkType.VERIFY_EMAIL, email, settings).call(); - } - - /** - * Similar to {@link #generateEmailVerificationLink(String)} but performs the - * operation asynchronously. - * - * @param email The email of the user to be verified. - * @return An {@code ApiFuture} which will complete successfully with the generated email action - * link. If an error occurs while generating the link, the future throws a - * {@link FirebaseAuthException}. - * @throws IllegalArgumentException If the email address is null or empty. - */ - public ApiFuture generateEmailVerificationLinkAsync(@NonNull String email) { - return generateEmailVerificationLinkAsync(email, null); - } - - /** - * Similar to {@link #generateEmailVerificationLink(String, ActionCodeSettings)} but performs the - * operation asynchronously. - * - * @param email The email of the user to be verified. - * @param settings The action code settings object which defines whether - * the link is to be handled by a mobile app and the additional state information to be - * passed in the deep link. - * @return An {@code ApiFuture} which will complete successfully with the generated email action - * link. If an error occurs while generating the link, the future throws a - * {@link FirebaseAuthException}. - * @throws IllegalArgumentException If the email address is null or empty. - */ - public ApiFuture generateEmailVerificationLinkAsync( - @NonNull String email, @Nullable ActionCodeSettings settings) { - return generateEmailActionLinkOp(EmailLinkType.VERIFY_EMAIL, email, settings) - .callAsync(firebaseApp); - } - - /** - * Generates the out-of-band email action link for email link sign-in flows, using the action - * code settings provided. - * - * @param email The email of the user signing in. - * @param settings The action code settings object which defines whether - * the link is to be handled by a mobile app and the additional state information to be - * passed in the deep link. - * @return An email verification link. - * @throws IllegalArgumentException If the email address is null or empty. - * @throws FirebaseAuthException If an error occurs while generating the link. - */ - public String generateSignInWithEmailLink( - @NonNull String email, @NonNull ActionCodeSettings settings) throws FirebaseAuthException { - return generateEmailActionLinkOp(EmailLinkType.EMAIL_SIGNIN, email, settings).call(); - } - - /** - * Similar to {@link #generateSignInWithEmailLink(String, ActionCodeSettings)} but performs the - * operation asynchronously. - * - * @param email The email of the user signing in. - * @param settings The action code settings object which defines whether - * the link is to be handled by a mobile app and the additional state information to be - * passed in the deep link. - * @return An {@code ApiFuture} which will complete successfully with the generated email action - * link. If an error occurs while generating the link, the future throws a - * {@link FirebaseAuthException}. - * @throws IllegalArgumentException If the email address is null or empty. - * @throws NullPointerException If the settings is null. - */ - public ApiFuture generateSignInWithEmailLinkAsync( - String email, @NonNull ActionCodeSettings settings) { - return generateEmailActionLinkOp(EmailLinkType.EMAIL_SIGNIN, email, settings) - .callAsync(firebaseApp); - } - - @VisibleForTesting - FirebaseUserManager getUserManager() { - return this.userManager.get(); - } - - private CallableOperation generateEmailActionLinkOp( - final EmailLinkType type, final String email, final ActionCodeSettings settings) { - checkNotDestroyed(); - checkArgument(!Strings.isNullOrEmpty(email), "email must not be null or empty"); - if (type == EmailLinkType.EMAIL_SIGNIN) { - checkNotNull(settings, "ActionCodeSettings must not be null when generating sign-in links"); - } - final FirebaseUserManager userManager = getUserManager(); - return new CallableOperation() { - @Override - protected String execute() throws FirebaseAuthException { - return userManager.getEmailActionLink(type, email, settings); - } - }; - } - - private Supplier threadSafeMemoize(final Supplier supplier) { - return Suppliers.memoize(new Supplier() { - @Override - public T get() { - checkNotNull(supplier); - synchronized (lock) { - checkNotDestroyed(); - return supplier.get(); - } - } - }); - } - - private void checkNotDestroyed() { - synchronized (lock) { - checkState(!destroyed.get(), "FirebaseAuth instance is no longer alive. This happens when " - + "the parent FirebaseApp instance has been deleted."); - } - } - - private void destroy() { - synchronized (lock) { - destroyed.set(true); - } - } + @Override + protected void doDestroy() { } private static FirebaseAuth fromApp(final FirebaseApp app) { - return FirebaseAuth.builder() - .setFirebaseApp(app) - .setTokenFactory(new Supplier() { - @Override - public FirebaseTokenFactory get() { - return FirebaseTokenUtils.createTokenFactory(app, Clock.SYSTEM); - } - }) - .setIdTokenVerifier(new Supplier() { - @Override - public FirebaseTokenVerifier get() { - return FirebaseTokenUtils.createIdTokenVerifier(app, Clock.SYSTEM); - } - }) - .setCookieVerifier(new Supplier() { - @Override - public FirebaseTokenVerifier get() { - return FirebaseTokenUtils.createSessionCookieVerifier(app, Clock.SYSTEM); - } - }) - .setUserManager(new Supplier() { - @Override - public FirebaseUserManager get() { - return new FirebaseUserManager(app); - } - }) - .build(); - } - - @VisibleForTesting - static Builder builder() { - return new Builder(); - } - - static class Builder { - private FirebaseApp firebaseApp; - private Supplier tokenFactory; - private Supplier idTokenVerifier; - private Supplier cookieVerifier; - private Supplier userManager; - - private Builder() { } - - Builder setFirebaseApp(FirebaseApp firebaseApp) { - this.firebaseApp = firebaseApp; - return this; - } - - Builder setTokenFactory(Supplier tokenFactory) { - this.tokenFactory = tokenFactory; - return this; - } - - Builder setIdTokenVerifier(Supplier idTokenVerifier) { - this.idTokenVerifier = idTokenVerifier; - return this; - } - - Builder setCookieVerifier(Supplier cookieVerifier) { - this.cookieVerifier = cookieVerifier; - return this; - } - - Builder setUserManager(Supplier userManager) { - this.userManager = userManager; - return this; - } - - FirebaseAuth build() { - return new FirebaseAuth(this); - } + return new FirebaseAuth( + AbstractFirebaseAuth.builder() + .setFirebaseApp(app) + .setTokenFactory( + new Supplier() { + @Override + public FirebaseTokenFactory get() { + return FirebaseTokenUtils.createTokenFactory(app, Clock.SYSTEM); + } + }) + .setIdTokenVerifier( + new Supplier() { + @Override + public FirebaseTokenVerifier get() { + return FirebaseTokenUtils.createIdTokenVerifier(app, Clock.SYSTEM); + } + }) + .setCookieVerifier( + new Supplier() { + @Override + public FirebaseTokenVerifier get() { + return FirebaseTokenUtils.createSessionCookieVerifier(app, Clock.SYSTEM); + } + }) + .setUserManager( + new Supplier() { + @Override + public FirebaseUserManager get() { + return FirebaseUserManager.builder().setFirebaseApp(app).build(); + } + })); } private static class FirebaseAuthService extends FirebaseService { diff --git a/src/main/java/com/google/firebase/auth/FirebaseToken.java b/src/main/java/com/google/firebase/auth/FirebaseToken.java index 3d7b0b254..835fa41c9 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseToken.java +++ b/src/main/java/com/google/firebase/auth/FirebaseToken.java @@ -42,6 +42,15 @@ public String getUid() { return (String) claims.get("sub"); } + /** Returns the tenant ID for the this token. */ + public String getTenantId() { + Map firebase = (Map) claims.get("firebase"); + if (firebase == null) { + return null; + } + return (String) firebase.get("tenant"); + } + /** Returns the Issuer for the this token. */ public String getIssuer() { return (String) claims.get("iss"); @@ -57,14 +66,14 @@ public String getPicture() { return (String) claims.get("picture"); } - /** + /** * Returns the e-mail address for this user, or {@code null} if it's unavailable. */ public String getEmail() { return (String) claims.get("email"); } - /** + /** * Indicates if the email address returned by {@link #getEmail()} has been verified as good. */ public boolean isEmailVerified() { diff --git a/src/main/java/com/google/firebase/auth/FirebaseTokenUtils.java b/src/main/java/com/google/firebase/auth/FirebaseTokenUtils.java index dbb562872..e0105e9aa 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseTokenUtils.java +++ b/src/main/java/com/google/firebase/auth/FirebaseTokenUtils.java @@ -30,6 +30,7 @@ import com.google.firebase.ImplFirebaseTrampolines; import com.google.firebase.auth.internal.CryptoSigners; import com.google.firebase.auth.internal.FirebaseTokenFactory; +import com.google.firebase.internal.Nullable; import java.io.IOException; @@ -52,11 +53,17 @@ final class FirebaseTokenUtils { private FirebaseTokenUtils() { } static FirebaseTokenFactory createTokenFactory(FirebaseApp firebaseApp, Clock clock) { + return createTokenFactory(firebaseApp, clock, null); + } + + static FirebaseTokenFactory createTokenFactory( + FirebaseApp firebaseApp, Clock clock, @Nullable String tenantId) { try { return new FirebaseTokenFactory( firebaseApp.getOptions().getJsonFactory(), clock, - CryptoSigners.getCryptoSigner(firebaseApp)); + CryptoSigners.getCryptoSigner(firebaseApp), + tenantId); } catch (IOException e) { throw new IllegalStateException( "Failed to initialize FirebaseTokenFactory. Make sure to initialize the SDK " @@ -68,6 +75,11 @@ static FirebaseTokenFactory createTokenFactory(FirebaseApp firebaseApp, Clock cl } static FirebaseTokenVerifierImpl createIdTokenVerifier(FirebaseApp app, Clock clock) { + return createIdTokenVerifier(app, clock, null); + } + + static FirebaseTokenVerifierImpl createIdTokenVerifier( + FirebaseApp app, Clock clock, @Nullable String tenantId) { String projectId = ImplFirebaseTrampolines.getProjectId(app); checkState(!Strings.isNullOrEmpty(projectId), "Must initialize FirebaseApp with a project ID to call verifyIdToken()"); @@ -82,6 +94,7 @@ static FirebaseTokenVerifierImpl createIdTokenVerifier(FirebaseApp app, Clock cl .setJsonFactory(app.getOptions().getJsonFactory()) .setPublicKeysManager(publicKeysManager) .setIdTokenVerifier(idTokenVerifier) + .setTenantId(tenantId) .build(); } diff --git a/src/main/java/com/google/firebase/auth/FirebaseTokenVerifierImpl.java b/src/main/java/com/google/firebase/auth/FirebaseTokenVerifierImpl.java index c164173a6..e1a5a9a19 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseTokenVerifierImpl.java +++ b/src/main/java/com/google/firebase/auth/FirebaseTokenVerifierImpl.java @@ -28,6 +28,7 @@ import com.google.api.client.util.ArrayMap; import com.google.common.base.Joiner; import com.google.common.base.Strings; +import com.google.firebase.internal.Nullable; import java.io.IOException; import java.math.BigDecimal; import java.security.GeneralSecurityException; @@ -45,6 +46,7 @@ final class FirebaseTokenVerifierImpl implements FirebaseTokenVerifier { "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit"; private static final String ERROR_INVALID_CREDENTIAL = "ERROR_INVALID_CREDENTIAL"; private static final String ERROR_RUNTIME_EXCEPTION = "ERROR_RUNTIME_EXCEPTION"; + static final String TENANT_ID_MISMATCH_ERROR = "tenant-id-mismatch"; private final JsonFactory jsonFactory; private final GooglePublicKeysManager publicKeysManager; @@ -53,6 +55,7 @@ final class FirebaseTokenVerifierImpl implements FirebaseTokenVerifier { private final String shortName; private final String articledShortName; private final String docUrl; + private final String tenantId; private FirebaseTokenVerifierImpl(Builder builder) { this.jsonFactory = checkNotNull(builder.jsonFactory); @@ -65,6 +68,7 @@ private FirebaseTokenVerifierImpl(Builder builder) { this.shortName = builder.shortName; this.articledShortName = prefixWithIndefiniteArticle(this.shortName); this.docUrl = builder.docUrl; + this.tenantId = Strings.nullToEmpty(builder.tenantId); } /** @@ -90,7 +94,9 @@ public FirebaseToken verifyToken(String token) throws FirebaseAuthException { IdToken idToken = parse(token); checkContents(idToken); checkSignature(idToken); - return new FirebaseToken(idToken.getPayload()); + FirebaseToken firebaseToken = new FirebaseToken(idToken.getPayload()); + checkTenantId(firebaseToken); + return firebaseToken; } GooglePublicKeysManager getPublicKeysManager() { @@ -278,6 +284,18 @@ private boolean containsLegacyUidField(IdToken.Payload payload) { return false; } + private void checkTenantId(final FirebaseToken firebaseToken) throws FirebaseAuthException { + String tokenTenantId = Strings.nullToEmpty(firebaseToken.getTenantId()); + if (!this.tenantId.equals(tokenTenantId)) { + throw new FirebaseAuthException( + TENANT_ID_MISMATCH_ERROR, + String.format( + "The tenant ID ('%s') of the token did not match the expected value ('%s')", + tokenTenantId, + tenantId)); + } + } + static Builder builder() { return new Builder(); } @@ -290,6 +308,7 @@ static final class Builder { private String shortName; private IdTokenVerifier idTokenVerifier; private String docUrl; + private String tenantId; private Builder() { } @@ -323,6 +342,11 @@ Builder setDocUrl(String docUrl) { return this; } + Builder setTenantId(@Nullable String tenantId) { + this.tenantId = tenantId; + return this; + } + FirebaseTokenVerifierImpl build() { return new FirebaseTokenVerifierImpl(this); } diff --git a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java index ab8759c4f..b73882277 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java +++ b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java @@ -20,37 +20,30 @@ import static com.google.common.base.Preconditions.checkNotNull; import com.google.api.client.http.GenericUrl; -import com.google.api.client.http.HttpContent; -import com.google.api.client.http.HttpRequest; import com.google.api.client.http.HttpRequestFactory; -import com.google.api.client.http.HttpResponse; -import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.HttpResponseInterceptor; -import com.google.api.client.http.json.JsonHttpContent; import com.google.api.client.json.GenericJson; import com.google.api.client.json.JsonFactory; -import com.google.api.client.json.JsonObjectParser; import com.google.api.client.util.Key; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Joiner; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.firebase.FirebaseApp; import com.google.firebase.ImplFirebaseTrampolines; -import com.google.firebase.auth.UserRecord.CreateRequest; -import com.google.firebase.auth.UserRecord.UpdateRequest; +import com.google.firebase.auth.internal.AuthHttpClient; import com.google.firebase.auth.internal.BatchDeleteResponse; import com.google.firebase.auth.internal.DownloadAccountResponse; import com.google.firebase.auth.internal.GetAccountInfoRequest; import com.google.firebase.auth.internal.GetAccountInfoResponse; -import com.google.firebase.auth.internal.HttpErrorResponse; +import com.google.firebase.auth.internal.ListOidcProviderConfigsResponse; +import com.google.firebase.auth.internal.ListSamlProviderConfigsResponse; import com.google.firebase.auth.internal.UploadAccountResponse; import com.google.firebase.internal.ApiClientUtils; import com.google.firebase.internal.NonNull; import com.google.firebase.internal.Nullable; -import com.google.firebase.internal.SdkUtils; -import java.io.IOException; import java.util.Collection; import java.util.HashSet; import java.util.List; @@ -66,30 +59,7 @@ */ class FirebaseUserManager { - static final String USER_NOT_FOUND_ERROR = "user-not-found"; - static final String INTERNAL_ERROR = "internal-error"; - - // Map of server-side error codes to SDK error codes. - // SDK error codes defined at: https://firebase.google.com/docs/auth/admin/errors - private static final Map ERROR_CODES = ImmutableMap.builder() - .put("CLAIMS_TOO_LARGE", "claims-too-large") - .put("CONFIGURATION_NOT_FOUND", "project-not-found") - .put("INSUFFICIENT_PERMISSION", "insufficient-permission") - .put("DUPLICATE_EMAIL", "email-already-exists") - .put("DUPLICATE_LOCAL_ID", "uid-already-exists") - .put("EMAIL_EXISTS", "email-already-exists") - .put("INVALID_CLAIMS", "invalid-claims") - .put("INVALID_EMAIL", "invalid-email") - .put("INVALID_PAGE_SELECTION", "invalid-page-token") - .put("INVALID_PHONE_NUMBER", "invalid-phone-number") - .put("PHONE_NUMBER_EXISTS", "phone-number-already-exists") - .put("PROJECT_NOT_FOUND", "project-not-found") - .put("USER_NOT_FOUND", USER_NOT_FOUND_ERROR) - .put("WEAK_PASSWORD", "invalid-password") - .put("UNAUTHORIZED_DOMAIN", "unauthorized-continue-uri") - .put("INVALID_DYNAMIC_LINK_DOMAIN", "invalid-dynamic-link-domain") - .build(); - + static final int MAX_LIST_PROVIDER_CONFIGS_RESULTS = 100; static final int MAX_GET_ACCOUNTS_BATCH_SIZE = 100; static final int MAX_DELETE_ACCOUNTS_BATCH_SIZE = 1000; static final int MAX_LIST_USERS_RESULTS = 1000; @@ -100,45 +70,41 @@ class FirebaseUserManager { "iss", "jti", "nbf", "nonce", "sub", "firebase"); private static final String ID_TOOLKIT_URL = - "https://identitytoolkit.googleapis.com/v1/projects/%s"; - private static final String CLIENT_VERSION_HEADER = "X-Client-Version"; + "https://identitytoolkit.googleapis.com/%s/projects/%s"; - private final String baseUrl; + private final String userMgtBaseUrl; + private final String idpConfigMgtBaseUrl; private final JsonFactory jsonFactory; - private final HttpRequestFactory requestFactory; - private final String clientVersion = "Java/Admin/" + SdkUtils.getVersion(); - - private HttpResponseInterceptor interceptor; - - /** - * Creates a new FirebaseUserManager instance. - * - * @param app A non-null {@link FirebaseApp}. - */ - FirebaseUserManager(@NonNull FirebaseApp app) { - this(app, null); - } + private final AuthHttpClient httpClient; - FirebaseUserManager(@NonNull FirebaseApp app, @Nullable HttpRequestFactory requestFactory) { - checkNotNull(app, "FirebaseApp must not be null"); + private FirebaseUserManager(Builder builder) { + FirebaseApp app = checkNotNull(builder.app, "FirebaseApp must not be null"); String projectId = ImplFirebaseTrampolines.getProjectId(app); checkArgument(!Strings.isNullOrEmpty(projectId), "Project ID is required to access the auth service. Use a service account credential or " + "set the project ID explicitly via FirebaseOptions. Alternatively you can also " + "set the project ID via the GOOGLE_CLOUD_PROJECT environment variable."); - this.baseUrl = String.format(ID_TOOLKIT_URL, projectId); - this.jsonFactory = app.getOptions().getJsonFactory(); - - if (requestFactory == null) { - requestFactory = ApiClientUtils.newAuthorizedRequestFactory(app); + final String idToolkitUrlV1 = String.format(ID_TOOLKIT_URL, "v1", projectId); + final String idToolkitUrlV2 = String.format(ID_TOOLKIT_URL, "v2", projectId); + final String tenantId = builder.tenantId; + if (tenantId == null) { + this.userMgtBaseUrl = idToolkitUrlV1; + this.idpConfigMgtBaseUrl = idToolkitUrlV2; + } else { + checkArgument(!tenantId.isEmpty(), "Tenant ID must not be empty."); + this.userMgtBaseUrl = idToolkitUrlV1 + "/tenants/" + tenantId; + this.idpConfigMgtBaseUrl = idToolkitUrlV2 + "/tenants/" + tenantId; } - this.requestFactory = requestFactory; + this.jsonFactory = app.getOptions().getJsonFactory(); + HttpRequestFactory requestFactory = builder.requestFactory == null + ? ApiClientUtils.newAuthorizedRequestFactory(app) : builder.requestFactory; + this.httpClient = new AuthHttpClient(jsonFactory, requestFactory); } @VisibleForTesting void setInterceptor(HttpResponseInterceptor interceptor) { - this.interceptor = interceptor; + httpClient.setInterceptor(interceptor); } UserRecord getUserById(String uid) throws FirebaseAuthException { @@ -147,7 +113,8 @@ UserRecord getUserById(String uid) throws FirebaseAuthException { GetAccountInfoResponse response = post( "/accounts:lookup", payload, GetAccountInfoResponse.class); if (response == null || response.getUsers() == null || response.getUsers().isEmpty()) { - throw new FirebaseAuthException(USER_NOT_FOUND_ERROR, + throw new FirebaseAuthException( + AuthHttpClient.USER_NOT_FOUND_ERROR, "No user record found for the provided user ID: " + uid); } return new UserRecord(response.getUsers().get(0), jsonFactory); @@ -159,7 +126,8 @@ UserRecord getUserByEmail(String email) throws FirebaseAuthException { GetAccountInfoResponse response = post( "/accounts:lookup", payload, GetAccountInfoResponse.class); if (response == null || response.getUsers() == null || response.getUsers().isEmpty()) { - throw new FirebaseAuthException(USER_NOT_FOUND_ERROR, + throw new FirebaseAuthException( + AuthHttpClient.USER_NOT_FOUND_ERROR, "No user record found for the provided email: " + email); } return new UserRecord(response.getUsers().get(0), jsonFactory); @@ -171,7 +139,8 @@ UserRecord getUserByPhoneNumber(String phoneNumber) throws FirebaseAuthException GetAccountInfoResponse response = post( "/accounts:lookup", payload, GetAccountInfoResponse.class); if (response == null || response.getUsers() == null || response.getUsers().isEmpty()) { - throw new FirebaseAuthException(USER_NOT_FOUND_ERROR, + throw new FirebaseAuthException( + AuthHttpClient.USER_NOT_FOUND_ERROR, "No user record found for the provided phone number: " + phoneNumber); } return new UserRecord(response.getUsers().get(0), jsonFactory); @@ -180,7 +149,7 @@ UserRecord getUserByPhoneNumber(String phoneNumber) throws FirebaseAuthException Set getAccountInfo(@NonNull Collection identifiers) throws FirebaseAuthException { if (identifiers.isEmpty()) { - return new HashSet(); + return new HashSet<>(); } GetAccountInfoRequest payload = new GetAccountInfoRequest(); @@ -192,7 +161,8 @@ Set getAccountInfo(@NonNull Collection identifiers) "/accounts:lookup", payload, GetAccountInfoResponse.class); if (response == null) { - throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to parse server response"); + throw new FirebaseAuthException( + AuthHttpClient.INTERNAL_ERROR, "Failed to parse server response"); } Set results = new HashSet<>(); @@ -204,7 +174,7 @@ Set getAccountInfo(@NonNull Collection identifiers) return results; } - String createUser(CreateRequest request) throws FirebaseAuthException { + String createUser(UserRecord.CreateRequest request) throws FirebaseAuthException { GenericJson response = post( "/accounts", request.getProperties(), GenericJson.class); if (response != null) { @@ -213,14 +183,16 @@ String createUser(CreateRequest request) throws FirebaseAuthException { return uid; } } - throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to create new user"); + throw new FirebaseAuthException(AuthHttpClient.INTERNAL_ERROR, "Failed to create new user"); } - void updateUser(UpdateRequest request, JsonFactory jsonFactory) throws FirebaseAuthException { + void updateUser(UserRecord.UpdateRequest request, JsonFactory jsonFactory) + throws FirebaseAuthException { GenericJson response = post( "/accounts:update", request.getProperties(jsonFactory), GenericJson.class); if (response == null || !request.getUid().equals(response.get("localId"))) { - throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to update user: " + request.getUid()); + throw new FirebaseAuthException( + AuthHttpClient.INTERNAL_ERROR, "Failed to update user: " + request.getUid()); } } @@ -229,7 +201,8 @@ void deleteUser(String uid) throws FirebaseAuthException { GenericJson response = post( "/accounts:delete", payload, GenericJson.class); if (response == null || !response.containsKey("kind")) { - throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to delete user: " + uid); + throw new FirebaseAuthException( + AuthHttpClient.INTERNAL_ERROR, "Failed to delete user: " + uid); } } @@ -238,13 +211,13 @@ void deleteUser(String uid) throws FirebaseAuthException { * @pre uids.size() <= MAX_DELETE_ACCOUNTS_BATCH_SIZE */ DeleteUsersResult deleteUsers(@NonNull List uids) throws FirebaseAuthException { - final Map payload = ImmutableMap.of( + final Map payload = ImmutableMap.of( "localIds", uids, "force", true); BatchDeleteResponse response = post( "/accounts:batchDelete", payload, BatchDeleteResponse.class); if (response == null) { - throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to delete users"); + throw new FirebaseAuthException(AuthHttpClient.INTERNAL_ERROR, "Failed to delete users"); } return new DeleteUsersResult(uids.size(), response); @@ -258,12 +231,12 @@ DownloadAccountResponse listUsers(int maxResults, String pageToken) throws Fireb builder.put("nextPageToken", pageToken); } - GenericUrl url = new GenericUrl(baseUrl + "/accounts:batchGet"); + GenericUrl url = new GenericUrl(userMgtBaseUrl + "/accounts:batchGet"); url.putAll(builder.build()); - DownloadAccountResponse response = sendRequest( + DownloadAccountResponse response = httpClient.sendRequest( "GET", url, null, DownloadAccountResponse.class); if (response == null) { - throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to retrieve users."); + throw new FirebaseAuthException(AuthHttpClient.INTERNAL_ERROR, "Failed to retrieve users."); } return response; } @@ -273,7 +246,7 @@ UserImportResult importUsers(UserImportRequest request) throws FirebaseAuthExcep UploadAccountResponse response = post( "/accounts:batchCreate", request, UploadAccountResponse.class); if (response == null) { - throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to import users."); + throw new FirebaseAuthException(AuthHttpClient.INTERNAL_ERROR, "Failed to import users."); } return new UserImportResult(request.getUsersCount(), response); } @@ -289,7 +262,8 @@ String createSessionCookie(String idToken, return cookie; } } - throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to create session cookie"); + throw new FirebaseAuthException( + AuthHttpClient.INTERNAL_ERROR, "Failed to create session cookie"); } String getEmailActionLink(EmailLinkType type, String email, @@ -308,64 +282,119 @@ String getEmailActionLink(EmailLinkType type, String email, return link; } } - throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to create email action link"); + throw new FirebaseAuthException( + AuthHttpClient.INTERNAL_ERROR, "Failed to create email action link"); } - private T post(String path, Object content, Class clazz) throws FirebaseAuthException { - checkArgument(!Strings.isNullOrEmpty(path), "path must not be null or empty"); - checkNotNull(content, "content must not be null for POST requests"); - GenericUrl url = new GenericUrl(baseUrl + path); - return sendRequest("POST", url, content, clazz); + OidcProviderConfig createOidcProviderConfig( + OidcProviderConfig.CreateRequest request) throws FirebaseAuthException { + GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + "/oauthIdpConfigs"); + url.set("oauthIdpConfigId", request.getProviderId()); + return httpClient.sendRequest("POST", url, request.getProperties(), OidcProviderConfig.class); } - private T sendRequest( - String method, GenericUrl url, - @Nullable Object content, Class clazz) throws FirebaseAuthException { - - checkArgument(!Strings.isNullOrEmpty(method), "method must not be null or empty"); - checkNotNull(url, "url must not be null"); - checkNotNull(clazz, "response class must not be null"); - HttpResponse response = null; - try { - HttpContent httpContent = content != null ? new JsonHttpContent(jsonFactory, content) : null; - HttpRequest request = requestFactory.buildRequest(method, url, httpContent); - request.setParser(new JsonObjectParser(jsonFactory)); - request.getHeaders().set(CLIENT_VERSION_HEADER, clientVersion); - request.setResponseInterceptor(interceptor); - response = request.execute(); - return response.parseAs(clazz); - } catch (HttpResponseException e) { - // Server responded with an HTTP error - handleHttpError(e); - return null; - } catch (IOException e) { - // All other IO errors (Connection refused, reset, parse error etc.) + SamlProviderConfig createSamlProviderConfig( + SamlProviderConfig.CreateRequest request) throws FirebaseAuthException { + GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + "/inboundSamlConfigs"); + url.set("inboundSamlConfigId", request.getProviderId()); + return httpClient.sendRequest("POST", url, request.getProperties(), SamlProviderConfig.class); + } + + OidcProviderConfig updateOidcProviderConfig(OidcProviderConfig.UpdateRequest request) + throws FirebaseAuthException { + Map properties = request.getProperties(); + GenericUrl url = + new GenericUrl(idpConfigMgtBaseUrl + getOidcUrlSuffix(request.getProviderId())); + url.put("updateMask", Joiner.on(",").join(AuthHttpClient.generateMask(properties))); + return httpClient.sendRequest("PATCH", url, properties, OidcProviderConfig.class); + } + + SamlProviderConfig updateSamlProviderConfig(SamlProviderConfig.UpdateRequest request) + throws FirebaseAuthException { + Map properties = request.getProperties(); + GenericUrl url = + new GenericUrl(idpConfigMgtBaseUrl + getSamlUrlSuffix(request.getProviderId())); + url.put("updateMask", Joiner.on(",").join(AuthHttpClient.generateMask(properties))); + return httpClient.sendRequest("PATCH", url, properties, SamlProviderConfig.class); + } + + OidcProviderConfig getOidcProviderConfig(String providerId) throws FirebaseAuthException { + GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + getOidcUrlSuffix(providerId)); + return httpClient.sendRequest("GET", url, null, OidcProviderConfig.class); + } + + SamlProviderConfig getSamlProviderConfig(String providerId) throws FirebaseAuthException { + GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + getSamlUrlSuffix(providerId)); + return httpClient.sendRequest("GET", url, null, SamlProviderConfig.class); + } + + ListOidcProviderConfigsResponse listOidcProviderConfigs(int maxResults, String pageToken) + throws FirebaseAuthException { + ImmutableMap.Builder builder = + ImmutableMap.builder().put("pageSize", maxResults); + if (pageToken != null) { + checkArgument(!pageToken.equals( + ListProviderConfigsPage.END_OF_LIST), "Invalid end of list page token."); + builder.put("nextPageToken", pageToken); + } + + GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + "/oauthIdpConfigs"); + url.putAll(builder.build()); + ListOidcProviderConfigsResponse response = + httpClient.sendRequest("GET", url, null, ListOidcProviderConfigsResponse.class); + if (response == null) { throw new FirebaseAuthException( - INTERNAL_ERROR, "Error while calling user management backend service", e); - } finally { - if (response != null) { - try { - response.disconnect(); - } catch (IOException ignored) { - // Ignored - } - } + AuthHttpClient.INTERNAL_ERROR, "Failed to retrieve provider configs."); } + return response; } - private void handleHttpError(HttpResponseException e) throws FirebaseAuthException { - try { - HttpErrorResponse response = jsonFactory.fromString(e.getContent(), HttpErrorResponse.class); - String code = ERROR_CODES.get(response.getErrorCode()); - if (code != null) { - throw new FirebaseAuthException(code, "User management service responded with an error", e); - } - } catch (IOException ignored) { - // Ignored + ListSamlProviderConfigsResponse listSamlProviderConfigs(int maxResults, String pageToken) + throws FirebaseAuthException { + ImmutableMap.Builder builder = + ImmutableMap.builder().put("pageSize", maxResults); + if (pageToken != null) { + checkArgument(!pageToken.equals( + ListProviderConfigsPage.END_OF_LIST), "Invalid end of list page token."); + builder.put("nextPageToken", pageToken); + } + + GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + "/inboundSamlConfigs"); + url.putAll(builder.build()); + ListSamlProviderConfigsResponse response = + httpClient.sendRequest("GET", url, null, ListSamlProviderConfigsResponse.class); + if (response == null) { + throw new FirebaseAuthException( + AuthHttpClient.INTERNAL_ERROR, "Failed to retrieve provider configs."); } - String msg = String.format( - "Unexpected HTTP response with status: %d; body: %s", e.getStatusCode(), e.getContent()); - throw new FirebaseAuthException(INTERNAL_ERROR, msg, e); + return response; + } + + void deleteOidcProviderConfig(String providerId) throws FirebaseAuthException { + GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + getOidcUrlSuffix(providerId)); + httpClient.sendRequest("DELETE", url, null, GenericJson.class); + } + + void deleteSamlProviderConfig(String providerId) throws FirebaseAuthException { + GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + getSamlUrlSuffix(providerId)); + httpClient.sendRequest("DELETE", url, null, GenericJson.class); + } + + private static String getOidcUrlSuffix(String providerId) { + checkArgument(!Strings.isNullOrEmpty(providerId), "Provider ID must not be null or empty."); + return "/oauthIdpConfigs/" + providerId; + } + + private static String getSamlUrlSuffix(String providerId) { + checkArgument(!Strings.isNullOrEmpty(providerId), "Provider ID must not be null or empty."); + return "/inboundSamlConfigs/" + providerId; + } + + private T post(String path, Object content, Class clazz) throws FirebaseAuthException { + checkArgument(!Strings.isNullOrEmpty(path), "path must not be null or empty"); + checkNotNull(content, "content must not be null for POST requests"); + GenericUrl url = new GenericUrl(userMgtBaseUrl + path); + return httpClient.sendRequest("POST", url, content, clazz); } static class UserImportRequest extends GenericJson { @@ -407,4 +436,34 @@ enum EmailLinkType { EMAIL_SIGNIN, PASSWORD_RESET, } + + static Builder builder() { + return new Builder(); + } + + static class Builder { + + private FirebaseApp app; + private String tenantId; + private HttpRequestFactory requestFactory; + + Builder setFirebaseApp(FirebaseApp app) { + this.app = app; + return this; + } + + Builder setTenantId(String tenantId) { + this.tenantId = tenantId; + return this; + } + + Builder setHttpRequestFactory(HttpRequestFactory requestFactory) { + this.requestFactory = requestFactory; + return this; + } + + FirebaseUserManager build() { + return new FirebaseUserManager(this); + } + } } diff --git a/src/main/java/com/google/firebase/auth/ListProviderConfigsPage.java b/src/main/java/com/google/firebase/auth/ListProviderConfigsPage.java new file mode 100644 index 000000000..361f932bd --- /dev/null +++ b/src/main/java/com/google/firebase/auth/ListProviderConfigsPage.java @@ -0,0 +1,268 @@ +/* + * Copyright 2020 Google LLC + * + * 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 static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.api.gax.paging.Page; +import com.google.common.collect.ImmutableList; +import com.google.firebase.auth.internal.ListOidcProviderConfigsResponse; +import com.google.firebase.auth.internal.ListProviderConfigsResponse; +import com.google.firebase.auth.internal.ListSamlProviderConfigsResponse; +import com.google.firebase.internal.NonNull; +import com.google.firebase.internal.Nullable; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; + +/** + * Represents a page of {@link ProviderConfig} instances. + * + *

Provides methods for iterating over the provider configs in the current page, and calling up + * subsequent pages of provider configs. + * + *

Instances of this class are thread-safe and immutable. + */ +public class ListProviderConfigsPage implements Page { + + static final String END_OF_LIST = ""; + + private final ListProviderConfigsResponse currentBatch; + private final ProviderConfigSource source; + private final int maxResults; + + private ListProviderConfigsPage( + @NonNull ListProviderConfigsResponse currentBatch, + @NonNull ProviderConfigSource source, + int maxResults) { + this.currentBatch = checkNotNull(currentBatch); + this.source = checkNotNull(source); + this.maxResults = maxResults; + } + + /** + * Checks if there is another page of provider configs available to retrieve. + * + * @return true if another page is available, or false otherwise. + */ + @Override + public boolean hasNextPage() { + return !END_OF_LIST.equals(currentBatch.getPageToken()); + } + + /** + * Returns the string token that identifies the next page. + * + *

Never returns null. Returns empty string if there are no more pages available to be + * retrieved. + * + * @return A non-null string token (possibly empty, representing no more pages) + */ + @NonNull + @Override + public String getNextPageToken() { + return currentBatch.getPageToken(); + } + + /** + * Returns the next page of provider configs. + * + * @return A new {@link ListProviderConfigsPage} instance, or null if there are no more pages. + */ + @Nullable + @Override + public ListProviderConfigsPage getNextPage() { + if (hasNextPage()) { + Factory factory = new Factory(source, maxResults, currentBatch.getPageToken()); + try { + return factory.create(); + } catch (FirebaseAuthException e) { + throw new RuntimeException(e); + } + } + return null; + } + + /** + * Returns an {@link Iterable} that facilitates transparently iterating over all the provider + * configs in the current Firebase project, starting from this page. + * + *

The {@link Iterator} instances produced by the returned {@link Iterable} never buffers more + * than one page of provider configs at a time. It is safe to abandon the iterators (i.e. break + * the loops) at any time. + * + * @return a new {@link Iterable} instance. + */ + @NonNull + @Override + public Iterable iterateAll() { + return new ProviderConfigIterable(this); + } + + /** + * Returns an {@link Iterable} over the provider configs in this page. + * + * @return a {@link Iterable} instance. + */ + @NonNull + @Override + public Iterable getValues() { + return currentBatch.getProviderConfigs(); + } + + private static class ProviderConfigIterable implements Iterable { + + private final ListProviderConfigsPage startingPage; + + ProviderConfigIterable(@NonNull ListProviderConfigsPage startingPage) { + this.startingPage = checkNotNull(startingPage, "starting page must not be null"); + } + + @Override + @NonNull + public Iterator iterator() { + return new ProviderConfigIterator(startingPage); + } + + /** + * An {@link Iterator} that cycles through provider configs, one at a time. + * + *

It buffers the last retrieved batch of provider configs in memory. The {@code maxResults} + * parameter is an upper bound on the batch size. + */ + private static class ProviderConfigIterator implements Iterator { + + private ListProviderConfigsPage currentPage; + private List batch; + private int index = 0; + + private ProviderConfigIterator(ListProviderConfigsPage startingPage) { + setCurrentPage(startingPage); + } + + @Override + public boolean hasNext() { + if (index == batch.size()) { + if (currentPage.hasNextPage()) { + setCurrentPage(currentPage.getNextPage()); + } else { + return false; + } + } + + return index < batch.size(); + } + + @Override + public T next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + return batch.get(index++); + } + + @Override + public void remove() { + throw new UnsupportedOperationException("remove operation not supported"); + } + + private void setCurrentPage(ListProviderConfigsPage page) { + this.currentPage = checkNotNull(page); + this.batch = ImmutableList.copyOf(page.getValues()); + this.index = 0; + } + } + } + + /** + * Represents a source of provider config data that can be queried to load a batch of provider + * configs. + */ + interface ProviderConfigSource { + @NonNull + ListProviderConfigsResponse fetch(int maxResults, String pageToken) + throws FirebaseAuthException; + } + + static class DefaultOidcProviderConfigSource implements ProviderConfigSource { + + private final FirebaseUserManager userManager; + + DefaultOidcProviderConfigSource(FirebaseUserManager userManager) { + this.userManager = checkNotNull(userManager, "User manager must not be null."); + } + + @Override + public ListOidcProviderConfigsResponse fetch(int maxResults, String pageToken) + throws FirebaseAuthException { + return userManager.listOidcProviderConfigs(maxResults, pageToken); + } + } + + static class DefaultSamlProviderConfigSource implements ProviderConfigSource { + + private final FirebaseUserManager userManager; + + DefaultSamlProviderConfigSource(FirebaseUserManager userManager) { + this.userManager = checkNotNull(userManager, "User manager must not be null."); + } + + @Override + public ListSamlProviderConfigsResponse fetch(int maxResults, String pageToken) + throws FirebaseAuthException { + return userManager.listSamlProviderConfigs(maxResults, pageToken); + } + } + + /** + * A simple factory class for {@link ProviderConfigsPage} instances. + * + *

Performs argument validation before attempting to load any provider config data (which is + * expensive, and hence may be performed asynchronously on a separate thread). + */ + static class Factory { + + private final ProviderConfigSource source; + private final int maxResults; + private final String pageToken; + + Factory(@NonNull ProviderConfigSource source) { + this(source, FirebaseUserManager.MAX_LIST_PROVIDER_CONFIGS_RESULTS, null); + } + + Factory( + @NonNull ProviderConfigSource source, + int maxResults, + @Nullable String pageToken) { + checkArgument( + maxResults > 0 && maxResults <= FirebaseUserManager.MAX_LIST_PROVIDER_CONFIGS_RESULTS, + "maxResults must be a positive integer that does not exceed %s", + FirebaseUserManager.MAX_LIST_PROVIDER_CONFIGS_RESULTS); + checkArgument(!END_OF_LIST.equals(pageToken), "invalid end of list page token"); + this.source = checkNotNull(source, "source must not be null"); + this.maxResults = maxResults; + this.pageToken = pageToken; + } + + ListProviderConfigsPage create() throws FirebaseAuthException { + ListProviderConfigsResponse batch = source.fetch(maxResults, pageToken); + return new ListProviderConfigsPage(batch, source, maxResults); + } + } +} + diff --git a/src/main/java/com/google/firebase/auth/ListUsersPage.java b/src/main/java/com/google/firebase/auth/ListUsersPage.java index f406366ba..ba727af5a 100644 --- a/src/main/java/com/google/firebase/auth/ListUsersPage.java +++ b/src/main/java/com/google/firebase/auth/ListUsersPage.java @@ -80,7 +80,7 @@ public String getNextPageToken() { @Override public ListUsersPage getNextPage() { if (hasNextPage()) { - PageFactory factory = new PageFactory(source, maxResults, currentBatch.getNextPageToken()); + Factory factory = new Factory(source, maxResults, currentBatch.getNextPageToken()); try { return factory.create(); } catch (FirebaseAuthException e) { @@ -237,17 +237,17 @@ String getNextPageToken() { * before attempting to load any user data (which is expensive, and hence may be performed * asynchronously on a separate thread). */ - static class PageFactory { + static class Factory { private final UserSource source; private final int maxResults; private final String pageToken; - PageFactory(@NonNull UserSource source) { + Factory(@NonNull UserSource source) { this(source, FirebaseUserManager.MAX_LIST_USERS_RESULTS, null); } - PageFactory(@NonNull UserSource source, int maxResults, @Nullable String pageToken) { + Factory(@NonNull UserSource source, int maxResults, @Nullable String pageToken) { checkArgument(maxResults > 0 && maxResults <= FirebaseUserManager.MAX_LIST_USERS_RESULTS, "maxResults must be a positive integer that does not exceed %s", FirebaseUserManager.MAX_LIST_USERS_RESULTS); diff --git a/src/main/java/com/google/firebase/auth/OidcProviderConfig.java b/src/main/java/com/google/firebase/auth/OidcProviderConfig.java new file mode 100644 index 000000000..879b7e79f --- /dev/null +++ b/src/main/java/com/google/firebase/auth/OidcProviderConfig.java @@ -0,0 +1,177 @@ +/* + * Copyright 2020 Google LLC + * + * 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 static com.google.common.base.Preconditions.checkArgument; + +import com.google.api.client.util.Key; +import com.google.common.base.Strings; + +/** + * Contains metadata associated with an OIDC Auth provider. + * + *

Instances of this class are immutable and thread safe. + */ +public final class OidcProviderConfig extends ProviderConfig { + + @Key("clientId") + private String clientId; + + @Key("issuer") + private String issuer; + + public String getClientId() { + return clientId; + } + + public String getIssuer() { + return issuer; + } + + /** + * Returns a new {@link UpdateRequest}, which can be used to update the attributes of this + * provider config. + * + * @return A non-null {@link UpdateRequest} instance. + */ + public UpdateRequest updateRequest() { + return new UpdateRequest(getProviderId()); + } + + static void checkOidcProviderId(String providerId) { + checkArgument(!Strings.isNullOrEmpty(providerId), "Provider ID must not be null or empty."); + checkArgument(providerId.startsWith("oidc."), + "Invalid OIDC provider ID (must be prefixed with 'oidc.'): " + providerId); + } + + /** + * A specification class for creating a new OIDC Auth provider. + * + *

Set the initial attributes of the new provider by calling various setter methods available + * in this class. + */ + public static final class CreateRequest extends AbstractCreateRequest { + + /** + * Creates a new {@link CreateRequest}, which can be used to create a new OIDC Auth provider. + * + *

The returned object should be passed to + * {@link AbstractFirebaseAuth#createOidcProviderConfig(CreateRequest)} to save the config. + */ + public CreateRequest() { } + + /** + * Sets the ID for the new provider. + * + * @param providerId A non-null, non-empty provider ID string. + * @throws IllegalArgumentException If the provider ID is null or empty, or is not prefixed with + * 'oidc.'. + */ + @Override + public CreateRequest setProviderId(String providerId) { + checkOidcProviderId(providerId); + return super.setProviderId(providerId); + } + + /** + * Sets the client ID for the new provider. + * + * @param clientId A non-null, non-empty client ID string. + * @throws IllegalArgumentException If the client ID is null or empty. + */ + public CreateRequest setClientId(String clientId) { + checkArgument(!Strings.isNullOrEmpty(clientId), "Client ID must not be null or empty."); + properties.put("clientId", clientId); + return this; + } + + /** + * Sets the issuer for the new provider. + * + * @param issuer A non-null, non-empty issuer URL string. + * @throws IllegalArgumentException If the issuer URL is null or empty, or if the format is + * invalid. + */ + public CreateRequest setIssuer(String issuer) { + checkArgument(!Strings.isNullOrEmpty(issuer), "Issuer must not be null or empty."); + assertValidUrl(issuer); + properties.put("issuer", issuer); + return this; + } + + CreateRequest getThis() { + return this; + } + } + + /** + * A specification class for updating an existing OIDC Auth provider. + * + *

An instance of this class can be obtained via a {@link OidcProviderConfig} object, or from + * a provider ID string. Specify the changes to be made to the provider config by calling the + * various setter methods available in this class. + */ + public static final class UpdateRequest extends AbstractUpdateRequest { + + /** + * Creates a new {@link UpdateRequest}, which can be used to updates an existing OIDC Auth + * provider. + * + *

The returned object should be passed to + * {@link AbstractFirebaseAuth#updateOidcProviderConfig(CreateRequest)} to save the updated + * config. + * + * @param providerId A non-null, non-empty provider ID string. + * @throws IllegalArgumentException If the provider ID is null or empty, or is not prefixed with + * "oidc.". + */ + public UpdateRequest(String providerId) { + super(providerId); + checkOidcProviderId(providerId); + } + + /** + * Sets the client ID for the exsting provider. + * + * @param clientId A non-null, non-empty client ID string. + * @throws IllegalArgumentException If the client ID is null or empty. + */ + public UpdateRequest setClientId(String clientId) { + checkArgument(!Strings.isNullOrEmpty(clientId), "Client ID must not be null or empty."); + properties.put("clientId", clientId); + return this; + } + + /** + * Sets the issuer for the existing provider. + * + * @param issuer A non-null, non-empty issuer URL string. + * @throws IllegalArgumentException If the issuer URL is null or empty, or if the format is + * invalid. + */ + public UpdateRequest setIssuer(String issuer) { + checkArgument(!Strings.isNullOrEmpty(issuer), "Issuer must not be null or empty."); + assertValidUrl(issuer); + properties.put("issuer", issuer); + return this; + } + + UpdateRequest getThis() { + return this; + } + } +} diff --git a/src/main/java/com/google/firebase/auth/ProviderConfig.java b/src/main/java/com/google/firebase/auth/ProviderConfig.java new file mode 100644 index 000000000..921a07b5b --- /dev/null +++ b/src/main/java/com/google/firebase/auth/ProviderConfig.java @@ -0,0 +1,157 @@ +/* + * Copyright 2020 Google LLC + * + * 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 static com.google.common.base.Preconditions.checkArgument; + +import com.google.api.client.util.Key; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableMap; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; + +/** + * The base class for Auth providers. + */ +public abstract class ProviderConfig { + + @Key("name") + private String resourceName; + + @Key("displayName") + private String displayName; + + @Key("enabled") + private boolean enabled; + + public String getProviderId() { + return resourceName.substring(resourceName.lastIndexOf("/") + 1); + } + + public String getDisplayName() { + return displayName; + } + + public boolean isEnabled() { + return enabled; + } + + static void assertValidUrl(String url) throws IllegalArgumentException { + try { + new URL(url); + } catch (MalformedURLException e) { + throw new IllegalArgumentException(url + " is a malformed URL.", e); + } + } + + /** + * A base specification class for creating a new provider. + * + *

Set the initial attributes of the new provider by calling various setter methods available + * in this class. + */ + public abstract static class AbstractCreateRequest> { + + final Map properties = new HashMap<>(); + String providerId; + + T setProviderId(String providerId) { + this.providerId = providerId; + return getThis(); + } + + String getProviderId() { + return providerId; + } + + /** + * Sets the display name for the new provider. + * + * @param displayName A non-null, non-empty display name string. + * @throws IllegalArgumentException If the display name is null or empty. + */ + public T setDisplayName(String displayName) { + checkArgument(!Strings.isNullOrEmpty(displayName), "Display name must not be null or empty."); + properties.put("displayName", displayName); + return getThis(); + } + + /** + * Sets whether to allow the user to sign in with the provider. + * + * @param enabled A boolean indicating whether the user can sign in with the provider. + */ + public T setEnabled(boolean enabled) { + properties.put("enabled", enabled); + return getThis(); + } + + Map getProperties() { + return ImmutableMap.copyOf(properties); + } + + abstract T getThis(); + } + + /** + * A base class for updating the attributes of an existing provider. + */ + public abstract static class AbstractUpdateRequest> { + + final String providerId; + final Map properties = new HashMap<>(); + + AbstractUpdateRequest(String providerId) { + checkArgument(!Strings.isNullOrEmpty(providerId), "Provider ID must not be null or empty."); + this.providerId = providerId; + } + + String getProviderId() { + return providerId; + } + + /** + * Sets the display name for the existing provider. + * + * @param displayName A non-null, non-empty display name string. + * @throws IllegalArgumentException If the display name is null or empty. + */ + public T setDisplayName(String displayName) { + checkArgument(!Strings.isNullOrEmpty(displayName), "Display name must not be null or empty."); + properties.put("displayName", displayName); + return getThis(); + } + + /** + * Sets whether to allow the user to sign in with the provider. + * + * @param enabled A boolean indicating whether the user can sign in with the provider. + */ + public T setEnabled(boolean enabled) { + properties.put("enabled", enabled); + return getThis(); + } + + Map getProperties() { + return ImmutableMap.copyOf(properties); + } + + abstract T getThis(); + } +} diff --git a/src/main/java/com/google/firebase/auth/SamlProviderConfig.java b/src/main/java/com/google/firebase/auth/SamlProviderConfig.java new file mode 100644 index 000000000..76f8594a8 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/SamlProviderConfig.java @@ -0,0 +1,343 @@ +/* + * Copyright 2020 Google LLC + * + * 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 static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.api.client.json.GenericJson; +import com.google.api.client.util.Key; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Contains metadata associated with a SAML Auth provider. + * + *

Instances of this class are immutable and thread safe. + */ +public final class SamlProviderConfig extends ProviderConfig { + + @Key("idpConfig") + private GenericJson idpConfig; + + @Key("spConfig") + private GenericJson spConfig; + + public String getIdpEntityId() { + return (String) idpConfig.get("idpEntityId"); + } + + public String getSsoUrl() { + return (String) idpConfig.get("ssoUrl"); + } + + public List getX509Certificates() { + List> idpCertificates = + (List>) idpConfig.get("idpCertificates"); + checkNotNull(idpCertificates); + ImmutableList.Builder certificates = ImmutableList.builder(); + for (Map idpCertificate : idpCertificates) { + certificates.add(idpCertificate.get("x509Certificate")); + } + return certificates.build(); + } + + public String getRpEntityId() { + return (String) spConfig.get("spEntityId"); + } + + public String getCallbackUrl() { + return (String) spConfig.get("callbackUri"); + } + + /** + * Returns a new {@link UpdateRequest}, which can be used to update the attributes of this + * provider config. + * + * @return a non-null {@link UpdateRequest} instance. + */ + public UpdateRequest updateRequest() { + return new UpdateRequest(getProviderId()); + } + + static void checkSamlProviderId(String providerId) { + checkArgument(!Strings.isNullOrEmpty(providerId), "Provider ID must not be null or empty."); + checkArgument(providerId.startsWith("saml."), + "Invalid SAML provider ID (must be prefixed with 'saml.'): " + providerId); + } + + private static List ensureNestedList(Map outerMap, String id) { + List list = (List) outerMap.get(id); + if (list == null) { + list = new ArrayList(); + outerMap.put(id, list); + } + return list; + } + + private static Map ensureNestedMap(Map outerMap, String id) { + Map map = (Map) outerMap.get(id); + if (map == null) { + map = new HashMap(); + outerMap.put(id, map); + } + return map; + } + + /** + * A specification class for creating a new SAML Auth provider. + * + *

Set the initial attributes of the new provider by calling various setter methods available + * in this class. + */ + public static final class CreateRequest extends AbstractCreateRequest { + + /** + * Creates a new {@link CreateRequest}, which can be used to create a new SAML Auth provider. + * + *

The returned object should be passed to + * {@link AbstractFirebaseAuth#createSamlProviderConfig(CreateRequest)} to register the provider + * information persistently. + */ + public CreateRequest() { } + + /** + * Sets the ID for the new provider. + * + * @param providerId A non-null, non-empty provider ID string. + * @throws IllegalArgumentException If the provider ID is null or empty, or is not prefixed with + * 'saml.'. + */ + @Override + public CreateRequest setProviderId(String providerId) { + checkSamlProviderId(providerId); + return super.setProviderId(providerId); + } + + /** + * Sets the IDP entity ID for the new provider. + * + * @param idpEntityId A non-null, non-empty IDP entity ID string. + * @throws IllegalArgumentException If the IDP entity ID is null or empty. + */ + public CreateRequest setIdpEntityId(String idpEntityId) { + checkArgument(!Strings.isNullOrEmpty(idpEntityId), + "IDP entity ID must not be null or empty."); + ensureNestedMap(properties, "idpConfig").put("idpEntityId", idpEntityId); + return this; + } + + /** + * Sets the SSO URL for the new provider. + * + * @param ssoUrl A non-null, non-empty SSO URL string. + * @throws IllegalArgumentException If the SSO URL is null or empty, or if the format is + * invalid. + */ + public CreateRequest setSsoUrl(String ssoUrl) { + checkArgument(!Strings.isNullOrEmpty(ssoUrl), "SSO URL must not be null or empty."); + assertValidUrl(ssoUrl); + ensureNestedMap(properties, "idpConfig").put("ssoUrl", ssoUrl); + return this; + } + + /** + * Adds a x509 certificate to the new provider. + * + * @param x509Certificate A non-null, non-empty x509 certificate string. + * @throws IllegalArgumentException If the x509 certificate is null or empty. + */ + public CreateRequest addX509Certificate(String x509Certificate) { + checkArgument(!Strings.isNullOrEmpty(x509Certificate), + "The x509 certificate must not be null or empty."); + Map idpConfigProperties = ensureNestedMap(properties, "idpConfig"); + List x509Certificates = ensureNestedList(idpConfigProperties, "idpCertificates"); + x509Certificates.add(ImmutableMap.of("x509Certificate", x509Certificate)); + return this; + } + + /** + * Adds a collection of x509 certificates to the new provider. + * + * @param x509Certificates A non-null, non-empty collection of x509 certificate strings. + * @throws IllegalArgumentException If the collection is null or empty, or if any x509 + * certificates are null or empty. + */ + public CreateRequest addAllX509Certificates(Collection x509Certificates) { + checkArgument(x509Certificates != null, + "The collection of x509 certificates must not be null."); + checkArgument(!x509Certificates.isEmpty(), + "The collection of x509 certificates must not be empty."); + for (String certificate : x509Certificates) { + addX509Certificate(certificate); + } + return this; + } + + /** + * Sets the RP entity ID for the new provider. + * + * @param rpEntityId A non-null, non-empty RP entity ID string. + * @throws IllegalArgumentException If the RP entity ID is null or empty. + */ + public CreateRequest setRpEntityId(String rpEntityId) { + checkArgument(!Strings.isNullOrEmpty(rpEntityId), "RP entity ID must not be null or empty."); + ensureNestedMap(properties, "spConfig").put("spEntityId", rpEntityId); + return this; + } + + /** + * Sets the callback URL for the new provider. + * + * @param callbackUrl A non-null, non-empty callback URL string. + * @throws IllegalArgumentException If the callback URL is null or empty, or if the format is + * invalid. + */ + public CreateRequest setCallbackUrl(String callbackUrl) { + checkArgument(!Strings.isNullOrEmpty(callbackUrl), "Callback URL must not be null or empty."); + assertValidUrl(callbackUrl); + ensureNestedMap(properties, "spConfig").put("callbackUri", callbackUrl); + return this; + } + + CreateRequest getThis() { + return this; + } + } + + /** + * A specification class for updating an existing SAML Auth provider. + * + *

An instance of this class can be obtained via a {@link SamlProviderConfig} object, or from + * a provider ID string. Specify the changes to be made to the provider config by calling the + * various setter methods available in this class. + */ + public static final class UpdateRequest extends AbstractUpdateRequest { + /** + * Creates a new {@link UpdateRequest}, which can be used to updates an existing SAML Auth + * provider. + * + *

The returned object should be passed to + * {@link AbstractFirebaseAuth#updateSamlProviderConfig(UpdateRequest)} to update the provider + * information persistently. + * + * @param providerId a non-null, non-empty provider ID string. + * @throws IllegalArgumentException If the provider ID is null or empty, or is not prefixed with + * 'saml.'. + */ + public UpdateRequest(String providerId) { + super(providerId); + checkSamlProviderId(providerId); + } + + /** + * Sets the IDP entity ID for the existing provider. + * + * @param idpEntityId A non-null, non-empty IDP entity ID string. + * @throws IllegalArgumentException If the IDP entity ID is null or empty. + */ + public UpdateRequest setIdpEntityId(String idpEntityId) { + checkArgument(!Strings.isNullOrEmpty(idpEntityId), + "IDP entity ID must not be null or empty."); + ensureNestedMap(properties, "idpConfig").put("idpEntityId", idpEntityId); + return this; + } + + /** + * Sets the SSO URL for the existing provider. + * + * @param ssoUrl A non-null, non-empty SSO URL string. + * @throws IllegalArgumentException If the SSO URL is null or empty, or if the format is + * invalid. + */ + public UpdateRequest setSsoUrl(String ssoUrl) { + checkArgument(!Strings.isNullOrEmpty(ssoUrl), "SSO URL must not be null or empty."); + assertValidUrl(ssoUrl); + ensureNestedMap(properties, "idpConfig").put("ssoUrl", ssoUrl); + return this; + } + + /** + * Adds a x509 certificate to the existing provider. + * + * @param x509Certificate A non-null, non-empty x509 certificate string. + * @throws IllegalArgumentException If the x509 certificate is null or empty. + */ + public UpdateRequest addX509Certificate(String x509Certificate) { + checkArgument(!Strings.isNullOrEmpty(x509Certificate), + "The x509 certificate must not be null or empty."); + Map idpConfigProperties = ensureNestedMap(properties, "idpConfig"); + List x509Certificates = ensureNestedList(idpConfigProperties, "idpCertificates"); + x509Certificates.add(ImmutableMap.of("x509Certificate", x509Certificate)); + return this; + } + + /** + * Adds a collection of x509 certificates to the existing provider. + * + * @param x509Certificates A non-null, non-empty collection of x509 certificate strings. + * @throws IllegalArgumentException If the collection is null or empty, or if any x509 + * certificates are null or empty. + */ + public UpdateRequest addAllX509Certificates(Collection x509Certificates) { + checkArgument(x509Certificates != null, + "The collection of x509 certificates must not be null."); + checkArgument(!x509Certificates.isEmpty(), + "The collection of x509 certificates must not be empty."); + for (String certificate : x509Certificates) { + addX509Certificate(certificate); + } + return this; + } + + /** + * Sets the RP entity ID for the existing provider. + * + * @param rpEntityId A non-null, non-empty RP entity ID string. + * @throws IllegalArgumentException If the RP entity ID is null or empty. + */ + public UpdateRequest setRpEntityId(String rpEntityId) { + checkArgument(!Strings.isNullOrEmpty(rpEntityId), "RP entity ID must not be null or empty."); + ensureNestedMap(properties, "spConfig").put("spEntityId", rpEntityId); + return this; + } + + /** + * Sets the callback URL for the exising provider. + * + * @param callbackUrl A non-null, non-empty callback URL string. + * @throws IllegalArgumentException If the callback URL is null or empty, or if the format is + * invalid. + */ + public UpdateRequest setCallbackUrl(String callbackUrl) { + checkArgument(!Strings.isNullOrEmpty(callbackUrl), "Callback URL must not be null or empty."); + assertValidUrl(callbackUrl); + ensureNestedMap(properties, "spConfig").put("callbackUri", callbackUrl); + return this; + } + + UpdateRequest getThis() { + return this; + } + } +} diff --git a/src/main/java/com/google/firebase/auth/UserRecord.java b/src/main/java/com/google/firebase/auth/UserRecord.java index 64e7c278c..0af08f65b 100644 --- a/src/main/java/com/google/firebase/auth/UserRecord.java +++ b/src/main/java/com/google/firebase/auth/UserRecord.java @@ -50,6 +50,7 @@ public class UserRecord implements UserInfo { private static final int MAX_CLAIMS_PAYLOAD_SIZE = 1000; private final String uid; + private final String tenantId; private final String email; private final String phoneNumber; private final boolean emailVerified; @@ -66,6 +67,7 @@ public class UserRecord implements UserInfo { checkNotNull(jsonFactory, "jsonFactory must not be null"); checkArgument(!Strings.isNullOrEmpty(response.getUid()), "uid must not be null or empty"); this.uid = response.getUid(); + this.tenantId = response.getTenantId(); this.email = response.getEmail(); this.phoneNumber = response.getPhoneNumber(); this.emailVerified = response.isEmailVerified(); @@ -116,6 +118,16 @@ public String getUid() { return uid; } + /** + * Returns the tenant ID associated with this user, if one exists. + * + * @return a tenant ID string or null. + */ + @Nullable + public String getTenantId() { + return this.tenantId; + } + /** * Returns the provider ID of this user. * @@ -199,9 +211,9 @@ public UserInfo[] getProviderData() { } /** - * Returns a timestamp in milliseconds since epoch, truncated down to the closest second. + * Returns a timestamp in milliseconds since epoch, truncated down to the closest second. * Tokens minted before this timestamp are considered invalid. - * + * * @return Timestamp in milliseconds since the epoch. Tokens minted before this timestamp are * considered invalid. */ @@ -371,10 +383,10 @@ public CreateRequest setEmailVerified(boolean emailVerified) { /** * Sets the display name for the new user. * - * @param displayName a non-null, non-empty display name string. + * @param displayName a non-null display name string. */ public CreateRequest setDisplayName(String displayName) { - checkNotNull(displayName, "displayName cannot be null or empty"); + checkNotNull(displayName, "displayName cannot be null"); properties.put("displayName", displayName); return this; } diff --git a/src/main/java/com/google/firebase/auth/internal/AuthHttpClient.java b/src/main/java/com/google/firebase/auth/internal/AuthHttpClient.java new file mode 100644 index 000000000..ad77236d1 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/internal/AuthHttpClient.java @@ -0,0 +1,160 @@ +/* + * Copyright 2020 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.internal; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.api.client.http.GenericUrl; +import com.google.api.client.http.HttpContent; +import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.HttpRequestFactory; +import com.google.api.client.http.HttpResponse; +import com.google.api.client.http.HttpResponseException; +import com.google.api.client.http.HttpResponseInterceptor; +import com.google.api.client.http.json.JsonHttpContent; +import com.google.api.client.json.JsonFactory; +import com.google.api.client.json.JsonObjectParser; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSortedSet; +import com.google.firebase.auth.FirebaseAuthException; +import com.google.firebase.internal.Nullable; +import com.google.firebase.internal.SdkUtils; +import java.io.IOException; +import java.util.Map; +import java.util.Set; + +/** + * Provides a convenient API for making REST calls to the Firebase Auth backend servers. + */ +public final class AuthHttpClient { + + public static final String CONFIGURATION_NOT_FOUND_ERROR = "configuration-not-found"; + public static final String INTERNAL_ERROR = "internal-error"; + public static final String TENANT_NOT_FOUND_ERROR = "tenant-not-found"; + public static final String USER_NOT_FOUND_ERROR = "user-not-found"; + + private static final String CLIENT_VERSION_HEADER = "X-Client-Version"; + + private static final String CLIENT_VERSION = "Java/Admin/" + SdkUtils.getVersion(); + + // Map of server-side error codes to SDK error codes. + // SDK error codes defined at: https://firebase.google.com/docs/auth/admin/errors + private static final Map ERROR_CODES = ImmutableMap.builder() + .put("CLAIMS_TOO_LARGE", "claims-too-large") + .put("CONFIGURATION_NOT_FOUND", CONFIGURATION_NOT_FOUND_ERROR) + .put("INSUFFICIENT_PERMISSION", "insufficient-permission") + .put("DUPLICATE_EMAIL", "email-already-exists") + .put("DUPLICATE_LOCAL_ID", "uid-already-exists") + .put("EMAIL_EXISTS", "email-already-exists") + .put("INVALID_CLAIMS", "invalid-claims") + .put("INVALID_EMAIL", "invalid-email") + .put("INVALID_PAGE_SELECTION", "invalid-page-token") + .put("INVALID_PHONE_NUMBER", "invalid-phone-number") + .put("PHONE_NUMBER_EXISTS", "phone-number-already-exists") + .put("PROJECT_NOT_FOUND", "project-not-found") + .put("USER_NOT_FOUND", USER_NOT_FOUND_ERROR) + .put("WEAK_PASSWORD", "invalid-password") + .put("UNAUTHORIZED_DOMAIN", "unauthorized-continue-uri") + .put("INVALID_DYNAMIC_LINK_DOMAIN", "invalid-dynamic-link-domain") + .put("TENANT_NOT_FOUND", TENANT_NOT_FOUND_ERROR) + .build(); + + private final JsonFactory jsonFactory; + private final HttpRequestFactory requestFactory; + + private HttpResponseInterceptor interceptor; + + public AuthHttpClient(JsonFactory jsonFactory, HttpRequestFactory requestFactory) { + this.jsonFactory = jsonFactory; + this.requestFactory = requestFactory; + } + + public static Set generateMask(Map properties) { + ImmutableSortedSet.Builder maskBuilder = ImmutableSortedSet.naturalOrder(); + for (Map.Entry entry : properties.entrySet()) { + if (entry.getValue() instanceof Map) { + Set childMask = generateMask((Map) entry.getValue()); + for (String childProperty : childMask) { + maskBuilder.add(entry.getKey() + "." + childProperty); + } + } else { + maskBuilder.add(entry.getKey()); + } + } + return maskBuilder.build(); + } + + public void setInterceptor(HttpResponseInterceptor interceptor) { + this.interceptor = interceptor; + } + + public T sendRequest( + String method, GenericUrl url, + @Nullable Object content, Class clazz) throws FirebaseAuthException { + + checkArgument(!Strings.isNullOrEmpty(method), "method must not be null or empty"); + checkNotNull(url, "url must not be null"); + checkNotNull(clazz, "response class must not be null"); + HttpResponse response = null; + try { + HttpContent httpContent = content != null ? new JsonHttpContent(jsonFactory, content) : null; + HttpRequest request = + requestFactory.buildRequest(method.equals("PATCH") ? "POST" : method, url, httpContent); + request.setParser(new JsonObjectParser(jsonFactory)); + request.getHeaders().set(CLIENT_VERSION_HEADER, CLIENT_VERSION); + if (method.equals("PATCH")) { + request.getHeaders().set("X-HTTP-Method-Override", "PATCH"); + } + request.setResponseInterceptor(interceptor); + response = request.execute(); + return response.parseAs(clazz); + } catch (HttpResponseException e) { + // Server responded with an HTTP error + handleHttpError(e); + return null; + } catch (IOException e) { + // All other IO errors (Connection refused, reset, parse error etc.) + throw new FirebaseAuthException( + INTERNAL_ERROR, "Error while calling the Firebase Auth backend service", e); + } finally { + if (response != null) { + try { + response.disconnect(); + } catch (IOException ignored) { + // Ignored + } + } + } + } + + private void handleHttpError(HttpResponseException e) throws FirebaseAuthException { + try { + HttpErrorResponse response = jsonFactory.fromString(e.getContent(), HttpErrorResponse.class); + String code = ERROR_CODES.get(response.getErrorCode()); + if (code != null) { + throw new FirebaseAuthException(code, "Firebase Auth service responded with an error", e); + } + } catch (IOException ignored) { + // Ignored + } + String msg = String.format( + "Unexpected HTTP response with status: %d; body: %s", e.getStatusCode(), e.getContent()); + throw new FirebaseAuthException(INTERNAL_ERROR, msg, e); + } +} diff --git a/src/main/java/com/google/firebase/auth/internal/FirebaseCustomAuthToken.java b/src/main/java/com/google/firebase/auth/internal/FirebaseCustomAuthToken.java index e67576464..2fe0b1859 100644 --- a/src/main/java/com/google/firebase/auth/internal/FirebaseCustomAuthToken.java +++ b/src/main/java/com/google/firebase/auth/internal/FirebaseCustomAuthToken.java @@ -22,6 +22,7 @@ import com.google.api.client.json.webtoken.JsonWebSignature; import com.google.api.client.util.Key; import com.google.firebase.auth.FirebaseToken; +import com.google.firebase.internal.Nullable; import java.io.IOException; @@ -77,6 +78,9 @@ public static class Payload extends IdToken.Payload { @Key("claims") private GenericJson developerClaims; + @Key("tenant_id") + private String tenantId; + public final String getUid() { return uid; } @@ -95,6 +99,15 @@ public Payload setDeveloperClaims(GenericJson developerClaims) { return this; } + public final String getTenantId() { + return tenantId; + } + + public Payload setTenantId(String tenantId) { + this.tenantId = tenantId; + return this; + } + @Override public Payload setIssuer(String issuer) { return (Payload) super.setIssuer(issuer); diff --git a/src/main/java/com/google/firebase/auth/internal/FirebaseTokenFactory.java b/src/main/java/com/google/firebase/auth/internal/FirebaseTokenFactory.java index 95d313134..778911d46 100644 --- a/src/main/java/com/google/firebase/auth/internal/FirebaseTokenFactory.java +++ b/src/main/java/com/google/firebase/auth/internal/FirebaseTokenFactory.java @@ -25,8 +25,9 @@ import com.google.api.client.util.Base64; import com.google.api.client.util.Clock; import com.google.api.client.util.StringUtils; - import com.google.common.base.Strings; +import com.google.firebase.internal.Nullable; + import java.io.IOException; import java.util.Collection; import java.util.Map; @@ -41,11 +42,18 @@ public class FirebaseTokenFactory { private final JsonFactory jsonFactory; private final Clock clock; private final CryptoSigner signer; + private final String tenantId; public FirebaseTokenFactory(JsonFactory jsonFactory, Clock clock, CryptoSigner signer) { + this(jsonFactory, clock, signer, null); + } + + public FirebaseTokenFactory( + JsonFactory jsonFactory, Clock clock, CryptoSigner signer, @Nullable String tenantId) { this.jsonFactory = checkNotNull(jsonFactory); this.clock = checkNotNull(clock); this.signer = checkNotNull(signer); + this.tenantId = tenantId; } String createSignedCustomAuthTokenForUser(String uid) throws IOException { @@ -68,6 +76,9 @@ public String createSignedCustomAuthTokenForUser( .setAudience(FirebaseCustomAuthToken.FIREBASE_AUDIENCE) .setIssuedAtTimeSeconds(issuedAt) .setExpirationTimeSeconds(issuedAt + FirebaseCustomAuthToken.TOKEN_DURATION_SECONDS); + if (!Strings.isNullOrEmpty(tenantId)) { + payload.setTenantId(tenantId); + } if (developerClaims != null) { Collection reservedNames = payload.getClassInfo().getNames(); 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 e84335891..7bde3eb39 100644 --- a/src/main/java/com/google/firebase/auth/internal/GetAccountInfoResponse.java +++ b/src/main/java/com/google/firebase/auth/internal/GetAccountInfoResponse.java @@ -46,6 +46,9 @@ public static class User { @Key("localId") private String uid; + @Key("tenantId") + private String tenantId; + @Key("email") private String email; @@ -86,6 +89,10 @@ public String getUid() { return uid; } + public String getTenantId() { + return tenantId; + } + public String getEmail() { return email; } @@ -129,7 +136,7 @@ public String getLastRefreshAt() { public long getValidSince() { return validSince; } - + public String getCustomClaims() { return customClaims; } diff --git a/src/main/java/com/google/firebase/auth/internal/ListOidcProviderConfigsResponse.java b/src/main/java/com/google/firebase/auth/internal/ListOidcProviderConfigsResponse.java new file mode 100644 index 000000000..187f98cf5 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/internal/ListOidcProviderConfigsResponse.java @@ -0,0 +1,62 @@ +/* + * Copyright 2020 Google LLC + * + * 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.internal; + +import com.google.api.client.util.Key; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; +import com.google.firebase.auth.OidcProviderConfig; +import java.util.List; + +/** + * JSON data binding for ListOAuthIdpConfigsResponse messages sent by Google identity toolkit + * service. + */ +public final class ListOidcProviderConfigsResponse + implements ListProviderConfigsResponse { + + @Key("oauthIdpConfigs") + private List providerConfigs; + + @Key("nextPageToken") + private String pageToken; + + @VisibleForTesting + public ListOidcProviderConfigsResponse( + List providerConfigs, String pageToken) { + this.providerConfigs = providerConfigs; + this.pageToken = pageToken; + } + + public ListOidcProviderConfigsResponse() { } + + @Override + public List getProviderConfigs() { + return providerConfigs == null ? ImmutableList.of() : providerConfigs; + } + + @Override + public boolean hasProviderConfigs() { + return providerConfigs != null && !providerConfigs.isEmpty(); + } + + @Override + public String getPageToken() { + return Strings.nullToEmpty(pageToken); + } +} diff --git a/src/main/java/com/google/firebase/auth/internal/ListProviderConfigsResponse.java b/src/main/java/com/google/firebase/auth/internal/ListProviderConfigsResponse.java new file mode 100644 index 000000000..2f25ae623 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/internal/ListProviderConfigsResponse.java @@ -0,0 +1,32 @@ +/* + * Copyright 2020 Google LLC + * + * 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.internal; + +import com.google.firebase.auth.ProviderConfig; +import java.util.List; + +/** + * Interface for config list response messages sent by Google identity toolkit service. + */ +public interface ListProviderConfigsResponse { + + public List getProviderConfigs(); + + public boolean hasProviderConfigs(); + + public String getPageToken(); +} diff --git a/src/main/java/com/google/firebase/auth/internal/ListSamlProviderConfigsResponse.java b/src/main/java/com/google/firebase/auth/internal/ListSamlProviderConfigsResponse.java new file mode 100644 index 000000000..55b944d53 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/internal/ListSamlProviderConfigsResponse.java @@ -0,0 +1,62 @@ +/* + * Copyright 2020 Google LLC + * + * 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.internal; + +import com.google.api.client.util.Key; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; +import com.google.firebase.auth.SamlProviderConfig; +import java.util.List; + +/** + * JSON data binding for ListInboundSamlConfigsResponse messages sent by Google identity toolkit + * service. + */ +public final class ListSamlProviderConfigsResponse + implements ListProviderConfigsResponse { + + @Key("inboundSamlConfigs") + private List providerConfigs; + + @Key("nextPageToken") + private String pageToken; + + @VisibleForTesting + public ListSamlProviderConfigsResponse( + List providerConfigs, String pageToken) { + this.providerConfigs = providerConfigs; + this.pageToken = pageToken; + } + + public ListSamlProviderConfigsResponse() { } + + @Override + public List getProviderConfigs() { + return providerConfigs == null ? ImmutableList.of() : providerConfigs; + } + + @Override + public boolean hasProviderConfigs() { + return providerConfigs != null && !providerConfigs.isEmpty(); + } + + @Override + public String getPageToken() { + return Strings.nullToEmpty(pageToken); + } +} diff --git a/src/main/java/com/google/firebase/auth/internal/ListTenantsResponse.java b/src/main/java/com/google/firebase/auth/internal/ListTenantsResponse.java new file mode 100644 index 000000000..f1086921f --- /dev/null +++ b/src/main/java/com/google/firebase/auth/internal/ListTenantsResponse.java @@ -0,0 +1,55 @@ +/* + * Copyright 2020 Google LLC + * + * 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.internal; + +import com.google.api.client.util.Key; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableList; +import com.google.firebase.auth.multitenancy.Tenant; +import java.util.List; + +/** + * JSON data binding for ListTenantsResponse messages sent by Google identity toolkit service. + */ +public final class ListTenantsResponse { + + @Key("tenants") + private List tenants; + + @Key("pageToken") + private String pageToken; + + @VisibleForTesting + public ListTenantsResponse(List tenants, String pageToken) { + this.tenants = tenants; + this.pageToken = pageToken; + } + + public ListTenantsResponse() { } + + public List getTenants() { + return tenants == null ? ImmutableList.of() : tenants; + } + + public boolean hasTenants() { + return tenants != null && !tenants.isEmpty(); + } + + public String getPageToken() { + return pageToken == null ? "" : pageToken; + } +} diff --git a/src/main/java/com/google/firebase/auth/multitenancy/FirebaseTenantClient.java b/src/main/java/com/google/firebase/auth/multitenancy/FirebaseTenantClient.java new file mode 100644 index 000000000..1278e63d5 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/multitenancy/FirebaseTenantClient.java @@ -0,0 +1,111 @@ +/* + * Copyright 2020 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.multitenancy; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.api.client.http.GenericUrl; +import com.google.api.client.http.HttpRequestFactory; +import com.google.api.client.http.HttpResponseInterceptor; +import com.google.api.client.json.GenericJson; +import com.google.api.client.json.JsonFactory; +import com.google.common.base.Joiner; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableMap; +import com.google.firebase.FirebaseApp; +import com.google.firebase.ImplFirebaseTrampolines; +import com.google.firebase.auth.FirebaseAuthException; +import com.google.firebase.auth.internal.AuthHttpClient; +import com.google.firebase.auth.internal.ListTenantsResponse; +import com.google.firebase.internal.ApiClientUtils; +import java.util.Map; + +final class FirebaseTenantClient { + + static final int MAX_LIST_TENANTS_RESULTS = 100; + + private static final String ID_TOOLKIT_URL = + "https://identitytoolkit.googleapis.com/%s/projects/%s"; + + private final String tenantMgtBaseUrl; + private final AuthHttpClient httpClient; + + FirebaseTenantClient(FirebaseApp app) { + checkNotNull(app, "FirebaseApp must not be null"); + String projectId = ImplFirebaseTrampolines.getProjectId(app); + checkArgument(!Strings.isNullOrEmpty(projectId), + "Project ID is required to access the auth service. Use a service account credential or " + + "set the project ID explicitly via FirebaseOptions. Alternatively you can also " + + "set the project ID via the GOOGLE_CLOUD_PROJECT environment variable."); + this.tenantMgtBaseUrl = String.format(ID_TOOLKIT_URL, "v2", projectId); + JsonFactory jsonFactory = app.getOptions().getJsonFactory(); + HttpRequestFactory requestFactory = ApiClientUtils.newAuthorizedRequestFactory(app); + this.httpClient = new AuthHttpClient(jsonFactory, requestFactory); + } + + void setInterceptor(HttpResponseInterceptor interceptor) { + httpClient.setInterceptor(interceptor); + } + + Tenant getTenant(String tenantId) throws FirebaseAuthException { + GenericUrl url = new GenericUrl(tenantMgtBaseUrl + getTenantUrlSuffix(tenantId)); + return httpClient.sendRequest("GET", url, null, Tenant.class); + } + + Tenant createTenant(Tenant.CreateRequest request) throws FirebaseAuthException { + GenericUrl url = new GenericUrl(tenantMgtBaseUrl + "/tenants"); + return httpClient.sendRequest("POST", url, request.getProperties(), Tenant.class); + } + + Tenant updateTenant(Tenant.UpdateRequest request) throws FirebaseAuthException { + Map properties = request.getProperties(); + GenericUrl url = new GenericUrl(tenantMgtBaseUrl + getTenantUrlSuffix(request.getTenantId())); + url.put("updateMask", Joiner.on(",").join(AuthHttpClient.generateMask(properties))); + return httpClient.sendRequest("PATCH", url, properties, Tenant.class); + } + + void deleteTenant(String tenantId) throws FirebaseAuthException { + GenericUrl url = new GenericUrl(tenantMgtBaseUrl + getTenantUrlSuffix(tenantId)); + httpClient.sendRequest("DELETE", url, null, GenericJson.class); + } + + ListTenantsResponse listTenants(int maxResults, String pageToken) + throws FirebaseAuthException { + ImmutableMap.Builder builder = + ImmutableMap.builder().put("pageSize", maxResults); + if (pageToken != null) { + checkArgument(!pageToken.equals( + ListTenantsPage.END_OF_LIST), "Invalid end of list page token."); + builder.put("pageToken", pageToken); + } + + GenericUrl url = new GenericUrl(tenantMgtBaseUrl + "/tenants"); + url.putAll(builder.build()); + ListTenantsResponse response = httpClient.sendRequest( + "GET", url, null, ListTenantsResponse.class); + if (response == null) { + throw new FirebaseAuthException(AuthHttpClient.INTERNAL_ERROR, "Failed to retrieve tenants."); + } + return response; + } + + private static String getTenantUrlSuffix(String tenantId) { + checkArgument(!Strings.isNullOrEmpty(tenantId), "Tenant ID must not be null or empty."); + return "/tenants/" + tenantId; + } +} diff --git a/src/main/java/com/google/firebase/auth/multitenancy/ListTenantsPage.java b/src/main/java/com/google/firebase/auth/multitenancy/ListTenantsPage.java new file mode 100644 index 000000000..c1f393ddb --- /dev/null +++ b/src/main/java/com/google/firebase/auth/multitenancy/ListTenantsPage.java @@ -0,0 +1,245 @@ +/* + * Copyright 2020 Google LLC + * + * 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.multitenancy; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.api.gax.paging.Page; +import com.google.common.collect.ImmutableList; +import com.google.firebase.auth.FirebaseAuthException; +import com.google.firebase.auth.internal.ListTenantsResponse; +import com.google.firebase.internal.NonNull; +import com.google.firebase.internal.Nullable; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; + +/** + * Represents a page of {@link Tenant} instances. + * + *

Provides methods for iterating over the tenants in the current page, and calling up + * subsequent pages of tenants. + * + *

Instances of this class are thread-safe and immutable. + */ +public class ListTenantsPage implements Page { + + static final String END_OF_LIST = ""; + + private final ListTenantsResponse currentBatch; + private final TenantSource source; + private final int maxResults; + + private ListTenantsPage( + @NonNull ListTenantsResponse currentBatch, @NonNull TenantSource source, int maxResults) { + this.currentBatch = checkNotNull(currentBatch); + this.source = checkNotNull(source); + this.maxResults = maxResults; + } + + /** + * Checks if there is another page of tenants available to retrieve. + * + * @return true if another page is available, or false otherwise. + */ + @Override + public boolean hasNextPage() { + return !END_OF_LIST.equals(currentBatch.getPageToken()); + } + + /** + * Returns the string token that identifies the next page. + * + *

Never returns null. Returns empty string if there are no more pages available to be + * retrieved. + * + * @return A non-null string token (possibly empty, representing no more pages) + */ + @NonNull + @Override + public String getNextPageToken() { + return currentBatch.getPageToken(); + } + + /** + * Returns the next page of tenants. + * + * @return A new {@link ListTenantsPage} instance, or null if there are no more pages. + */ + @Nullable + @Override + public ListTenantsPage getNextPage() { + if (hasNextPage()) { + PageFactory factory = new PageFactory(source, maxResults, currentBatch.getPageToken()); + try { + return factory.create(); + } catch (FirebaseAuthException e) { + throw new RuntimeException(e); + } + } + return null; + } + + /** + * Returns an {@link Iterable} that facilitates transparently iterating over all the tenants in + * the current Firebase project, starting from this page. + * + *

The {@link Iterator} instances produced by the returned {@link Iterable} never buffers more + * than one page of tenants at a time. It is safe to abandon the iterators (i.e. break the loops) + * at any time. + * + * @return a new {@link Iterable} instance. + */ + @NonNull + @Override + public Iterable iterateAll() { + return new TenantIterable(this); + } + + /** + * Returns an {@link Iterable} over the tenants in this page. + * + * @return a {@link Iterable} instance. + */ + @NonNull + @Override + public Iterable getValues() { + return currentBatch.getTenants(); + } + + private static class TenantIterable implements Iterable { + + private final ListTenantsPage startingPage; + + TenantIterable(@NonNull ListTenantsPage startingPage) { + this.startingPage = checkNotNull(startingPage, "starting page must not be null"); + } + + @Override + @NonNull + public Iterator iterator() { + return new TenantIterator(startingPage); + } + + /** + * An {@link Iterator} that cycles through tenants, one at a time. + * + *

It buffers the last retrieved batch of tenants in memory. The {@code maxResults} parameter + * is an upper bound on the batch size. + */ + private static class TenantIterator implements Iterator { + + private ListTenantsPage currentPage; + private List batch; + private int index = 0; + + private TenantIterator(ListTenantsPage startingPage) { + setCurrentPage(startingPage); + } + + @Override + public boolean hasNext() { + if (index == batch.size()) { + if (currentPage.hasNextPage()) { + setCurrentPage(currentPage.getNextPage()); + } else { + return false; + } + } + + return index < batch.size(); + } + + @Override + public Tenant next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + return batch.get(index++); + } + + @Override + public void remove() { + throw new UnsupportedOperationException("remove operation not supported"); + } + + private void setCurrentPage(ListTenantsPage page) { + this.currentPage = checkNotNull(page); + this.batch = ImmutableList.copyOf(page.getValues()); + this.index = 0; + } + } + } + + /** + * Represents a source of tenant data that can be queried to load a batch of tenants. + */ + interface TenantSource { + @NonNull + ListTenantsResponse fetch(int maxResults, String pageToken) + throws FirebaseAuthException; + } + + static class DefaultTenantSource implements TenantSource { + + private final FirebaseTenantClient tenantClient; + + DefaultTenantSource(FirebaseTenantClient tenantClient) { + this.tenantClient = checkNotNull(tenantClient, "Tenant client must not be null."); + } + + @Override + public ListTenantsResponse fetch(int maxResults, String pageToken) + throws FirebaseAuthException { + return tenantClient.listTenants(maxResults, pageToken); + } + } + + /** + * A simple factory class for {@link ListTenantsPage} instances. + * + *

Performs argument validation before attempting to load any tenant data (which is expensive, + * and hence may be performed asynchronously on a separate thread). + */ + static class PageFactory { + + private final TenantSource source; + private final int maxResults; + private final String pageToken; + + PageFactory(@NonNull TenantSource source) { + this(source, FirebaseTenantClient.MAX_LIST_TENANTS_RESULTS, null); + } + + PageFactory(@NonNull TenantSource source, int maxResults, @Nullable String pageToken) { + checkArgument(maxResults > 0 && maxResults <= FirebaseTenantClient.MAX_LIST_TENANTS_RESULTS, + "maxResults must be a positive integer that does not exceed %s", + FirebaseTenantClient.MAX_LIST_TENANTS_RESULTS); + checkArgument(!END_OF_LIST.equals(pageToken), "Invalid end of list page token."); + this.source = checkNotNull(source, "Source must not be null."); + this.maxResults = maxResults; + this.pageToken = pageToken; + } + + ListTenantsPage create() throws FirebaseAuthException { + ListTenantsResponse batch = source.fetch(maxResults, pageToken); + return new ListTenantsPage(batch, source, maxResults); + } + } +} + diff --git a/src/main/java/com/google/firebase/auth/multitenancy/Tenant.java b/src/main/java/com/google/firebase/auth/multitenancy/Tenant.java new file mode 100644 index 000000000..57d215e96 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/multitenancy/Tenant.java @@ -0,0 +1,195 @@ +/* + * Copyright 2020 Google LLC + * + * 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.multitenancy; + +import static com.google.common.base.Preconditions.checkArgument; + +import com.google.api.client.util.Key; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableMap; +import java.util.HashMap; +import java.util.Map; + +/** + * Contains metadata associated with a Firebase tenant. + * + *

Instances of this class are immutable and thread safe. + */ +public final class Tenant { + + @Key("name") + private String resourceName; + + @Key("displayName") + private String displayName; + + @Key("allowPasswordSignup") + private boolean passwordSignInAllowed; + + @Key("enableEmailLinkSignin") + private boolean emailLinkSignInEnabled; + + public String getTenantId() { + return resourceName.substring(resourceName.lastIndexOf("/") + 1); + } + + public String getDisplayName() { + return displayName; + } + + public boolean isPasswordSignInAllowed() { + return passwordSignInAllowed; + } + + public boolean isEmailLinkSignInEnabled() { + return emailLinkSignInEnabled; + } + + /** + * Returns a new {@link UpdateRequest}, which can be used to update the attributes of this tenant. + * + * @return a non-null {@link UpdateRequest} instance. + */ + public UpdateRequest updateRequest() { + return new UpdateRequest(getTenantId()); + } + + /** + * A specification class for creating a new tenant. + * + *

Set the initial attributes of the new tenant by calling various setter methods available in + * this class. None of the attributes are required. + */ + public static final class CreateRequest { + + private final Map properties = new HashMap<>(); + + /** + * Creates a new {@link CreateRequest}, which can be used to create a new tenant. + * + *

The returned object should be passed to {@link TenantManager#createTenant(CreateRequest)} + * to register the tenant information persistently. + */ + public CreateRequest() { } + + /** + * Sets the display name for the new tenant. + * + * @param displayName a non-null, non-empty display name string. + */ + public CreateRequest setDisplayName(String displayName) { + checkArgument(!Strings.isNullOrEmpty(displayName), "display name must not be null or empty"); + properties.put("displayName", displayName); + return this; + } + + /** + * Sets whether to allow email/password user authentication. + * + * @param passwordSignInAllowed a boolean indicating whether users can be authenticated using + * an email and password. + */ + public CreateRequest setPasswordSignInAllowed(boolean passwordSignInAllowed) { + properties.put("allowPasswordSignup", passwordSignInAllowed); + return this; + } + + /** + * Sets whether to enable email link user authentication. + * + * @param emailLinkSignInEnabled a boolean indicating whether users can be authenticated using + * an email link. + */ + public CreateRequest setEmailLinkSignInEnabled(boolean emailLinkSignInEnabled) { + properties.put("enableEmailLinkSignin", emailLinkSignInEnabled); + return this; + } + + Map getProperties() { + return ImmutableMap.copyOf(properties); + } + } + + /** + * A class for updating the attributes of an existing tenant. + * + *

An instance of this class can be obtained via a {@link Tenant} object, or from a tenant ID + * string. Specify the changes to be made to the tenant by calling the various setter methods + * available in this class. + */ + public static final class UpdateRequest { + + private final String tenantId; + private final Map properties = new HashMap<>(); + + /** + * Creates a new {@link UpdateRequest}, which can be used to update the attributes of the + * of the tenant identified by the specified tenant ID. + * + *

This method allows updating attributes of a tenant account, without first having to call + * {@link TenantManager#getTenant(String)}. + * + * @param tenantId a non-null, non-empty tenant ID string. + * @throws IllegalArgumentException If the tenant ID is null or empty. + */ + public UpdateRequest(String tenantId) { + checkArgument(!Strings.isNullOrEmpty(tenantId), "tenant ID must not be null or empty"); + this.tenantId = tenantId; + } + + String getTenantId() { + return tenantId; + } + + /** + * Sets the display name of the existing tenant. + * + * @param displayName a non-null, non-empty display name string. + */ + public UpdateRequest setDisplayName(String displayName) { + checkArgument(!Strings.isNullOrEmpty(displayName), "display name must not be null or empty"); + properties.put("displayName", displayName); + return this; + } + + /** + * Sets whether to allow email/password user authentication. + * + * @param passwordSignInAllowed a boolean indicating whether users can be authenticated using + * an email and password. + */ + public UpdateRequest setPasswordSignInAllowed(boolean passwordSignInAllowed) { + properties.put("allowPasswordSignup", passwordSignInAllowed); + return this; + } + + /** + * Sets whether to enable email link user authentication. + * + * @param emailLinkSignInEnabled a boolean indicating whether users can be authenticated using + * an email link. + */ + public UpdateRequest setEmailLinkSignInEnabled(boolean emailLinkSignInEnabled) { + properties.put("enableEmailLinkSignin", emailLinkSignInEnabled); + return this; + } + + Map getProperties() { + return ImmutableMap.copyOf(properties); + } + } +} diff --git a/src/main/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuth.java b/src/main/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuth.java new file mode 100644 index 000000000..540437404 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuth.java @@ -0,0 +1,50 @@ +/* + * Copyright 2020 Google LLC + * + * 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.multitenancy; + +import static com.google.common.base.Preconditions.checkArgument; + +import com.google.common.base.Strings; +import com.google.firebase.FirebaseApp; +import com.google.firebase.auth.AbstractFirebaseAuth; + +/** + * The tenant-aware Firebase client. + * + *

This can be used to perform a variety of authentication-related operations, scoped to a + * particular tenant. + */ +public final class TenantAwareFirebaseAuth extends AbstractFirebaseAuth { + + private final String tenantId; + + TenantAwareFirebaseAuth(final FirebaseApp firebaseApp, final String tenantId) { + super(builderFromAppAndTenantId(firebaseApp, tenantId)); + checkArgument(!Strings.isNullOrEmpty(tenantId)); + this.tenantId = tenantId; + } + + /** Returns the client's tenant ID. */ + public String getTenantId() { + return tenantId; + } + + @Override + protected void doDestroy() { + // Nothing extra needs to be destroyed. + } +} diff --git a/src/main/java/com/google/firebase/auth/multitenancy/TenantManager.java b/src/main/java/com/google/firebase/auth/multitenancy/TenantManager.java new file mode 100644 index 000000000..11f26b096 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/multitenancy/TenantManager.java @@ -0,0 +1,287 @@ +/* + * Copyright 2020 Google LLC + * + * 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.multitenancy; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.api.client.http.HttpResponseInterceptor; +import com.google.api.core.ApiFuture; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Strings; +import com.google.firebase.FirebaseApp; +import com.google.firebase.auth.FirebaseAuth; +import com.google.firebase.auth.FirebaseAuthException; +import com.google.firebase.auth.multitenancy.ListTenantsPage.DefaultTenantSource; +import com.google.firebase.auth.multitenancy.ListTenantsPage.PageFactory; +import com.google.firebase.auth.multitenancy.ListTenantsPage.TenantSource; +import com.google.firebase.auth.multitenancy.Tenant.CreateRequest; +import com.google.firebase.auth.multitenancy.Tenant.UpdateRequest; +import com.google.firebase.internal.CallableOperation; +import com.google.firebase.internal.NonNull; +import com.google.firebase.internal.Nullable; +import java.util.HashMap; +import java.util.Map; + +/** + * This class can be used to perform a variety of tenant-related operations, including creating, + * updating, and listing tenants. + */ +public final class TenantManager { + + private final FirebaseApp firebaseApp; + private final FirebaseTenantClient tenantClient; + private final Map tenantAwareAuths; + + /** + * Creates a new {@link TenantManager} instance. For internal use only. Use + * {@link FirebaseAuth#getTenantManager()} to obtain an instance for regular use. + * + * @hide + */ + public TenantManager(FirebaseApp firebaseApp) { + this.firebaseApp = firebaseApp; + this.tenantClient = new FirebaseTenantClient(firebaseApp); + this.tenantAwareAuths = new HashMap<>(); + } + + @VisibleForTesting + void setInterceptor(HttpResponseInterceptor interceptor) { + this.tenantClient.setInterceptor(interceptor); + } + + /** + * Gets the tenant corresponding to the specified tenant ID. + * + * @param tenantId A tenant ID string. + * @return A {@link Tenant} instance. + * @throws IllegalArgumentException If the tenant ID string is null or empty. + * @throws FirebaseAuthException If an error occurs while retrieving user data. + */ + public Tenant getTenant(@NonNull String tenantId) throws FirebaseAuthException { + return getTenantOp(tenantId).call(); + } + + public synchronized TenantAwareFirebaseAuth getAuthForTenant(@NonNull String tenantId) { + checkArgument(!Strings.isNullOrEmpty(tenantId), "Tenant ID must not be null or empty."); + if (!tenantAwareAuths.containsKey(tenantId)) { + tenantAwareAuths.put(tenantId, new TenantAwareFirebaseAuth(firebaseApp, tenantId)); + } + return tenantAwareAuths.get(tenantId); + } + + /** + * Similar to {@link #getTenant(String)} but performs the operation asynchronously. + * + * @param tenantId A tenantId string. + * @return An {@code ApiFuture} which will complete successfully with a {@link Tenant} instance + * If an error occurs while retrieving tenant data or if the specified tenant ID does not + * exist, the future throws a {@link FirebaseAuthException}. + * @throws IllegalArgumentException If the tenant ID string is null or empty. + */ + public ApiFuture getTenantAsync(@NonNull String tenantId) { + return getTenantOp(tenantId).callAsync(firebaseApp); + } + + private CallableOperation getTenantOp(final String tenantId) { + checkArgument(!Strings.isNullOrEmpty(tenantId), "Tenant ID must not be null or empty."); + return new CallableOperation() { + @Override + protected Tenant execute() throws FirebaseAuthException { + return tenantClient.getTenant(tenantId); + } + }; + } + + /** + * Gets a page of tenants starting from the specified {@code pageToken}. Page size will be limited + * to 1000 tenants. + * + * @param pageToken A non-empty page token string, or null to retrieve the first page of tenants. + * @return A {@link ListTenantsPage} instance. + * @throws IllegalArgumentException If the specified page token is empty. + * @throws FirebaseAuthException If an error occurs while retrieving tenant data. + */ + public ListTenantsPage listTenants(@Nullable String pageToken) throws FirebaseAuthException { + return listTenants(pageToken, FirebaseTenantClient.MAX_LIST_TENANTS_RESULTS); + } + + /** + * Gets a page of tenants starting from the specified {@code pageToken}. + * + * @param pageToken A non-empty page token string, or null to retrieve the first page of tenants. + * @param maxResults Maximum number of tenants to include in the returned page. This may not + * exceed 1000. + * @return A {@link ListTenantsPage} instance. + * @throws IllegalArgumentException If the specified page token is empty, or max results value is + * invalid. + * @throws FirebaseAuthException If an error occurs while retrieving tenant data. + */ + public ListTenantsPage listTenants(@Nullable String pageToken, int maxResults) + throws FirebaseAuthException { + return listTenantsOp(pageToken, maxResults).call(); + } + + /** + * Similar to {@link #listTenants(String)} but performs the operation asynchronously. + * + * @param pageToken A non-empty page token string, or null to retrieve the first page of tenants. + * @return An {@code ApiFuture} which will complete successfully with a {@link ListTenantsPage} + * instance. If an error occurs while retrieving tenant data, the future throws an exception. + * @throws IllegalArgumentException If the specified page token is empty. + */ + public ApiFuture listTenantsAsync(@Nullable String pageToken) { + return listTenantsAsync(pageToken, FirebaseTenantClient.MAX_LIST_TENANTS_RESULTS); + } + + /** + * Similar to {@link #listTenants(String, int)} but performs the operation asynchronously. + * + * @param pageToken A non-empty page token string, or null to retrieve the first page of tenants. + * @param maxResults Maximum number of tenants to include in the returned page. This may not + * exceed 1000. + * @return An {@code ApiFuture} which will complete successfully with a {@link ListTenantsPage} + * instance. If an error occurs while retrieving tenant data, the future throws an exception. + * @throws IllegalArgumentException If the specified page token is empty, or max results value is + * invalid. + */ + public ApiFuture listTenantsAsync(@Nullable String pageToken, int maxResults) { + return listTenantsOp(pageToken, maxResults).callAsync(firebaseApp); + } + + private CallableOperation listTenantsOp( + @Nullable final String pageToken, final int maxResults) { + final TenantSource tenantSource = new DefaultTenantSource(tenantClient); + final PageFactory factory = new PageFactory(tenantSource, maxResults, pageToken); + return new CallableOperation() { + @Override + protected ListTenantsPage execute() throws FirebaseAuthException { + return factory.create(); + } + }; + } + + /** + * Creates a new tenant with the attributes contained in the specified {@link CreateRequest}. + * + * @param request A non-null {@link CreateRequest} instance. + * @return A {@link Tenant} instance corresponding to the newly created tenant. + * @throws NullPointerException if the provided request is null. + * @throws FirebaseAuthException if an error occurs while creating the tenant. + */ + public Tenant createTenant(@NonNull CreateRequest request) throws FirebaseAuthException { + return createTenantOp(request).call(); + } + + /** + * Similar to {@link #createTenant(CreateRequest)} but performs the operation asynchronously. + * + * @param request A non-null {@link CreateRequest} instance. + * @return An {@code ApiFuture} which will complete successfully with a {@link Tenant} + * instance corresponding to the newly created tenant. If an error occurs while creating the + * tenant, the future throws a {@link FirebaseAuthException}. + * @throws NullPointerException if the provided request is null. + */ + public ApiFuture createTenantAsync(@NonNull CreateRequest request) { + return createTenantOp(request).callAsync(firebaseApp); + } + + private CallableOperation createTenantOp( + final CreateRequest request) { + checkNotNull(request, "Create request must not be null."); + return new CallableOperation() { + @Override + protected Tenant execute() throws FirebaseAuthException { + return tenantClient.createTenant(request); + } + }; + } + + + /** + * Updates an existing user account with the attributes contained in the specified {@link + * UpdateRequest}. + * + * @param request A non-null {@link UpdateRequest} instance. + * @return A {@link Tenant} instance corresponding to the updated user account. + * @throws NullPointerException if the provided update request is null. + * @throws FirebaseAuthException if an error occurs while updating the user account. + */ + public Tenant updateTenant(@NonNull UpdateRequest request) throws FirebaseAuthException { + return updateTenantOp(request).call(); + } + + /** + * Similar to {@link #updateTenant(UpdateRequest)} but performs the operation asynchronously. + * + * @param request A non-null {@link UpdateRequest} instance. + * @return An {@code ApiFuture} which will complete successfully with a {@link Tenant} + * instance corresponding to the updated user account. If an error occurs while updating the + * user account, the future throws a {@link FirebaseAuthException}. + */ + public ApiFuture updateTenantAsync(@NonNull UpdateRequest request) { + return updateTenantOp(request).callAsync(firebaseApp); + } + + private CallableOperation updateTenantOp( + final UpdateRequest request) { + checkNotNull(request, "Update request must not be null."); + checkArgument(!request.getProperties().isEmpty(), + "Tenant update must have at least one property set."); + return new CallableOperation() { + @Override + protected Tenant execute() throws FirebaseAuthException { + return tenantClient.updateTenant(request); + } + }; + } + + /** + * Deletes the tenant identified by the specified tenant ID. + * + * @param tenantId A tenant ID string. + * @throws IllegalArgumentException If the tenant ID string is null or empty. + * @throws FirebaseAuthException If an error occurs while deleting the tenant. + */ + public void deleteTenant(@NonNull String tenantId) throws FirebaseAuthException { + deleteTenantOp(tenantId).call(); + } + + /** + * Similar to {@link #deleteTenant(String)} but performs the operation asynchronously. + * + * @param tenantId A tenant ID string. + * @return An {@code ApiFuture} which will complete successfully when the specified tenant account + * has been deleted. If an error occurs while deleting the tenant account, the future throws a + * {@link FirebaseAuthException}. + * @throws IllegalArgumentException If the tenant ID string is null or empty. + */ + public ApiFuture deleteTenantAsync(String tenantId) { + return deleteTenantOp(tenantId).callAsync(firebaseApp); + } + + private CallableOperation deleteTenantOp(final String tenantId) { + checkArgument(!Strings.isNullOrEmpty(tenantId), "Tenant ID must not be null or empty."); + return new CallableOperation() { + @Override + protected Void execute() throws FirebaseAuthException { + tenantClient.deleteTenant(tenantId); + return null; + } + }; + } +} diff --git a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java index 2915d1ddd..803591d81 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java +++ b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java @@ -46,9 +46,12 @@ import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; import com.google.firebase.ImplFirebaseTrampolines; -import com.google.firebase.auth.UserRecord.CreateRequest; -import com.google.firebase.auth.UserRecord.UpdateRequest; +import com.google.firebase.auth.ProviderConfigTestUtils.TemporaryProviderConfig; +import com.google.firebase.auth.UserTestUtils.RandomUser; +import com.google.firebase.auth.UserTestUtils.TemporaryUser; import com.google.firebase.auth.hash.Scrypt; +import com.google.firebase.auth.internal.AuthHttpClient; +import com.google.firebase.internal.Nullable; import com.google.firebase.testing.IntegrationTestUtils; import java.io.IOException; import java.net.URLDecoder; @@ -56,15 +59,13 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Random; -import java.util.UUID; import java.util.concurrent.ExecutionException; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; -import org.junit.BeforeClass; +import org.junit.Rule; import org.junit.Test; public class FirebaseAuthIT { @@ -81,13 +82,12 @@ public class FirebaseAuthIT { private static final HttpTransport transport = Utils.getDefaultTransport(); private static final String ACTION_LINK_CONTINUE_URL = "http://localhost/?a=1&b=2#c=3"; - private static FirebaseAuth auth; + private static final FirebaseAuth auth = FirebaseAuth.getInstance( + IntegrationTestUtils.ensureDefaultApp()); - @BeforeClass - public static void setUpClass() { - FirebaseApp masterApp = IntegrationTestUtils.ensureDefaultApp(); - auth = FirebaseAuth.getInstance(masterApp); - } + @Rule public final TemporaryUser temporaryUser = new TemporaryUser(auth); + @Rule public final TemporaryProviderConfig temporaryProviderConfig = + new TemporaryProviderConfig(auth); @Test public void testGetNonExistingUser() throws Exception { @@ -96,7 +96,7 @@ public void testGetNonExistingUser() throws Exception { fail("No error thrown for non existing uid"); } catch (ExecutionException e) { assertTrue(e.getCause() instanceof FirebaseAuthException); - assertEquals(FirebaseUserManager.USER_NOT_FOUND_ERROR, + assertEquals(AuthHttpClient.USER_NOT_FOUND_ERROR, ((FirebaseAuthException) e.getCause()).getErrorCode()); } } @@ -108,7 +108,7 @@ public void testGetNonExistingUserByEmail() throws Exception { fail("No error thrown for non existing email"); } catch (ExecutionException e) { assertTrue(e.getCause() instanceof FirebaseAuthException); - assertEquals(FirebaseUserManager.USER_NOT_FOUND_ERROR, + assertEquals(AuthHttpClient.USER_NOT_FOUND_ERROR, ((FirebaseAuthException) e.getCause()).getErrorCode()); } } @@ -116,11 +116,11 @@ public void testGetNonExistingUserByEmail() throws Exception { @Test public void testUpdateNonExistingUser() throws Exception { try { - auth.updateUserAsync(new UpdateRequest("non.existing")).get(); + auth.updateUserAsync(new UserRecord.UpdateRequest("non.existing")).get(); fail("No error thrown for non existing uid"); } catch (ExecutionException e) { assertTrue(e.getCause() instanceof FirebaseAuthException); - assertEquals(FirebaseUserManager.USER_NOT_FOUND_ERROR, + assertEquals(AuthHttpClient.USER_NOT_FOUND_ERROR, ((FirebaseAuthException) e.getCause()).getErrorCode()); } } @@ -132,7 +132,7 @@ public void testDeleteNonExistingUser() throws Exception { fail("No error thrown for non existing uid"); } catch (ExecutionException e) { assertTrue(e.getCause() instanceof FirebaseAuthException); - assertEquals(FirebaseUserManager.USER_NOT_FOUND_ERROR, + assertEquals(AuthHttpClient.USER_NOT_FOUND_ERROR, ((FirebaseAuthException) e.getCause()).getErrorCode()); } } @@ -211,50 +211,46 @@ private ApiFuture slowDeleteUsersAsync(List uids) thr @Test public void testCreateUserWithParams() throws Exception { - RandomUser randomUser = RandomUser.create(); - String phone = randomPhoneNumber(); - CreateRequest user = new CreateRequest() - .setUid(randomUser.uid) - .setEmail(randomUser.email) - .setPhoneNumber(phone) + RandomUser randomUser = UserTestUtils.generateRandomUserInfo(); + UserRecord.CreateRequest user = new UserRecord.CreateRequest() + .setUid(randomUser.getUid()) + .setEmail(randomUser.getEmail()) + .setPhoneNumber(randomUser.getPhoneNumber()) .setDisplayName("Random User") .setPhotoUrl("https://example.com/photo.png") .setEmailVerified(true) .setPassword("password"); - UserRecord userRecord = auth.createUserAsync(user).get(); - try { - assertEquals(randomUser.uid, userRecord.getUid()); - assertEquals("Random User", userRecord.getDisplayName()); - assertEquals(randomUser.email, userRecord.getEmail()); - assertEquals(phone, userRecord.getPhoneNumber()); - assertEquals("https://example.com/photo.png", userRecord.getPhotoUrl()); - assertTrue(userRecord.isEmailVerified()); - assertFalse(userRecord.isDisabled()); - - assertEquals(2, userRecord.getProviderData().length); - List providers = new ArrayList<>(); - for (UserInfo provider : userRecord.getProviderData()) { - providers.add(provider.getProviderId()); - } - assertTrue(providers.contains("password")); - assertTrue(providers.contains("phone")); + UserRecord userRecord = temporaryUser.create(user); + assertEquals(randomUser.getUid(), userRecord.getUid()); + assertEquals("Random User", userRecord.getDisplayName()); + assertEquals(randomUser.getEmail(), userRecord.getEmail()); + assertEquals(randomUser.getPhoneNumber(), userRecord.getPhoneNumber()); + assertEquals("https://example.com/photo.png", userRecord.getPhotoUrl()); + assertTrue(userRecord.isEmailVerified()); + assertFalse(userRecord.isDisabled()); - checkRecreate(randomUser.uid); - } finally { - auth.deleteUserAsync(userRecord.getUid()).get(); + assertEquals(2, userRecord.getProviderData().length); + List providers = new ArrayList<>(); + for (UserInfo provider : userRecord.getProviderData()) { + providers.add(provider.getProviderId()); } + assertTrue(providers.contains("password")); + assertTrue(providers.contains("phone")); + + checkRecreateUser(randomUser.getUid()); } @Test public void testUserLifecycle() throws Exception { // Create user - UserRecord userRecord = auth.createUserAsync(new CreateRequest()).get(); + UserRecord userRecord = auth.createUserAsync(new UserRecord.CreateRequest()).get(); String uid = userRecord.getUid(); // Get user userRecord = auth.getUserAsync(userRecord.getUid()).get(); assertEquals(uid, userRecord.getUid()); + assertNull(userRecord.getTenantId()); assertNull(userRecord.getDisplayName()); assertNull(userRecord.getEmail()); assertNull(userRecord.getPhoneNumber()); @@ -267,20 +263,20 @@ public void testUserLifecycle() throws Exception { assertTrue(userRecord.getCustomClaims().isEmpty()); // Update user - RandomUser randomUser = RandomUser.create(); - String phone = randomPhoneNumber(); - UpdateRequest request = userRecord.updateRequest() + RandomUser randomUser = UserTestUtils.generateRandomUserInfo(); + UserRecord.UpdateRequest request = userRecord.updateRequest() .setDisplayName("Updated Name") - .setEmail(randomUser.email) - .setPhoneNumber(phone) + .setEmail(randomUser.getEmail()) + .setPhoneNumber(randomUser.getPhoneNumber()) .setPhotoUrl("https://example.com/photo.png") .setEmailVerified(true) .setPassword("secret"); userRecord = auth.updateUserAsync(request).get(); assertEquals(uid, userRecord.getUid()); + assertNull(userRecord.getTenantId()); assertEquals("Updated Name", userRecord.getDisplayName()); - assertEquals(randomUser.email, userRecord.getEmail()); - assertEquals(phone, userRecord.getPhoneNumber()); + assertEquals(randomUser.getEmail(), userRecord.getEmail()); + assertEquals(randomUser.getPhoneNumber(), userRecord.getPhoneNumber()); assertEquals("https://example.com/photo.png", userRecord.getPhotoUrl()); assertTrue(userRecord.isEmailVerified()); assertFalse(userRecord.isDisabled()); @@ -299,8 +295,9 @@ public void testUserLifecycle() throws Exception { .setDisabled(true); userRecord = auth.updateUserAsync(request).get(); assertEquals(uid, userRecord.getUid()); + assertNull(userRecord.getTenantId()); assertNull(userRecord.getDisplayName()); - assertEquals(randomUser.email, userRecord.getEmail()); + assertEquals(randomUser.getEmail(), userRecord.getEmail()); assertNull(userRecord.getPhoneNumber()); assertNull(userRecord.getPhotoUrl()); assertTrue(userRecord.isEmailVerified()); @@ -310,22 +307,15 @@ public void testUserLifecycle() throws Exception { // Delete user auth.deleteUserAsync(userRecord.getUid()).get(); - try { - auth.getUserAsync(userRecord.getUid()).get(); - fail("No error thrown for deleted user"); - } catch (ExecutionException e) { - assertTrue(e.getCause() instanceof FirebaseAuthException); - assertEquals(FirebaseUserManager.USER_NOT_FOUND_ERROR, - ((FirebaseAuthException) e.getCause()).getErrorCode()); - } + UserTestUtils.assertUserDoesNotExist(auth, userRecord.getUid()); } @Test public void testLastRefreshTime() throws Exception { - RandomUser user = RandomUser.create(); - UserRecord newUserRecord = auth.createUser(new CreateRequest() - .setUid(user.uid) - .setEmail(user.email) + RandomUser user = UserTestUtils.generateRandomUserInfo(); + UserRecord newUserRecord = auth.createUser(new UserRecord.CreateRequest() + .setUid(user.getUid()) + .setEmail(user.getEmail()) .setEmailVerified(false) .setPassword("password")); @@ -366,110 +356,105 @@ public void testLastRefreshTime() throws Exception { public void testListUsers() throws Exception { final List uids = new ArrayList<>(); - try { - uids.add(auth.createUserAsync(new CreateRequest().setPassword("password")).get().getUid()); - uids.add(auth.createUserAsync(new CreateRequest().setPassword("password")).get().getUid()); - uids.add(auth.createUserAsync(new CreateRequest().setPassword("password")).get().getUid()); - - // Test list by batches - final AtomicInteger collected = new AtomicInteger(0); - ListUsersPage page = auth.listUsersAsync(null).get(); - while (page != null) { - for (ExportedUserRecord user : page.getValues()) { - if (uids.contains(user.getUid())) { - collected.incrementAndGet(); - assertNotNull("Missing passwordHash field. A common cause would be " - + "forgetting to add the \"Firebase Authentication Admin\" permission. See " - + "instructions in CONTRIBUTING.md", user.getPasswordHash()); - assertNotNull(user.getPasswordSalt()); - } - } - page = page.getNextPage(); - } - assertEquals(uids.size(), collected.get()); + for (int i = 0; i < 3; i++) { + UserRecord.CreateRequest createRequest = + new UserRecord.CreateRequest().setPassword("password"); + uids.add(temporaryUser.create(createRequest).getUid()); + } - // Test iterate all - collected.set(0); - page = auth.listUsersAsync(null).get(); - for (ExportedUserRecord user : page.iterateAll()) { + // Test list by batches + final AtomicInteger collected = new AtomicInteger(0); + ListUsersPage page = auth.listUsersAsync(null).get(); + while (page != null) { + for (ExportedUserRecord user : page.getValues()) { if (uids.contains(user.getUid())) { collected.incrementAndGet(); - assertNotNull(user.getPasswordHash()); + assertNotNull("Missing passwordHash field. A common cause would be " + + "forgetting to add the \"Firebase Authentication Admin\" permission. See " + + "instructions in CONTRIBUTING.md", user.getPasswordHash()); assertNotNull(user.getPasswordSalt()); + assertNull(user.getTenantId()); } } - assertEquals(uids.size(), collected.get()); - - // Test iterate async - collected.set(0); - final Semaphore semaphore = new Semaphore(0); - final AtomicReference error = new AtomicReference<>(); - ApiFuture pageFuture = auth.listUsersAsync(null); - ApiFutures.addCallback(pageFuture, new ApiFutureCallback() { - @Override - public void onFailure(Throwable t) { - error.set(t); - semaphore.release(); - } + page = page.getNextPage(); + } + assertEquals(uids.size(), collected.get()); + + // Test iterate all + collected.set(0); + page = auth.listUsersAsync(null).get(); + for (ExportedUserRecord user : page.iterateAll()) { + if (uids.contains(user.getUid())) { + collected.incrementAndGet(); + assertNotNull(user.getPasswordHash()); + assertNotNull(user.getPasswordSalt()); + assertNull(user.getTenantId()); + } + } + assertEquals(uids.size(), collected.get()); + + // Test iterate async + collected.set(0); + final Semaphore semaphore = new Semaphore(0); + final AtomicReference error = new AtomicReference<>(); + ApiFuture pageFuture = auth.listUsersAsync(null); + ApiFutures.addCallback(pageFuture, new ApiFutureCallback() { + @Override + public void onFailure(Throwable t) { + error.set(t); + semaphore.release(); + } - @Override - public void onSuccess(ListUsersPage result) { - for (ExportedUserRecord user : result.iterateAll()) { - if (uids.contains(user.getUid())) { - collected.incrementAndGet(); - assertNotNull(user.getPasswordHash()); - assertNotNull(user.getPasswordSalt()); - } + @Override + public void onSuccess(ListUsersPage result) { + for (ExportedUserRecord user : result.iterateAll()) { + if (uids.contains(user.getUid())) { + collected.incrementAndGet(); + assertNotNull(user.getPasswordHash()); + assertNotNull(user.getPasswordSalt()); + assertNull(user.getTenantId()); } - semaphore.release(); } - }, MoreExecutors.directExecutor()); - semaphore.acquire(); - assertEquals(uids.size(), collected.get()); - assertNull(error.get()); - } finally { - for (String uid : uids) { - auth.deleteUserAsync(uid).get(); + semaphore.release(); } - } + }, MoreExecutors.directExecutor()); + semaphore.acquire(); + assertEquals(uids.size(), collected.get()); + assertNull(error.get()); } @Test public void testCustomClaims() throws Exception { - UserRecord userRecord = auth.createUserAsync(new CreateRequest()).get(); + UserRecord userRecord = temporaryUser.create(new UserRecord.CreateRequest()); String uid = userRecord.getUid(); - try { - // New user should not have any claims - assertTrue(userRecord.getCustomClaims().isEmpty()); - - Map expected = ImmutableMap.of( - "admin", true, "package", "gold"); - auth.setCustomUserClaimsAsync(uid, expected).get(); - - // Should have 2 claims - UserRecord updatedUser = auth.getUserAsync(uid).get(); - assertEquals(2, updatedUser.getCustomClaims().size()); - for (Map.Entry entry : expected.entrySet()) { - assertEquals(entry.getValue(), updatedUser.getCustomClaims().get(entry.getKey())); - } + // New user should not have any claims + assertTrue(userRecord.getCustomClaims().isEmpty()); - // User's ID token should have the custom claims - String customToken = auth.createCustomTokenAsync(uid).get(); - String idToken = signInWithCustomToken(customToken); - FirebaseToken decoded = auth.verifyIdTokenAsync(idToken).get(); - Map result = decoded.getClaims(); - for (Map.Entry entry : expected.entrySet()) { - assertEquals(entry.getValue(), result.get(entry.getKey())); - } + Map expected = ImmutableMap.of( + "admin", true, "package", "gold"); + auth.setCustomUserClaimsAsync(uid, expected).get(); - // Should be able to remove custom claims - auth.setCustomUserClaimsAsync(uid, null).get(); - updatedUser = auth.getUserAsync(uid).get(); - assertTrue(updatedUser.getCustomClaims().isEmpty()); - } finally { - auth.deleteUserAsync(uid).get(); + // Should have 2 claims + UserRecord updatedUser = auth.getUserAsync(uid).get(); + assertEquals(2, updatedUser.getCustomClaims().size()); + for (Map.Entry entry : expected.entrySet()) { + assertEquals(entry.getValue(), updatedUser.getCustomClaims().get(entry.getKey())); } + + // User's ID token should have the custom claims + String customToken = auth.createCustomTokenAsync(uid).get(); + String idToken = signInWithCustomToken(customToken); + FirebaseToken decoded = auth.verifyIdTokenAsync(idToken).get(); + Map result = decoded.getClaims(); + for (Map.Entry entry : expected.entrySet()) { + assertEquals(entry.getValue(), result.get(entry.getKey())); + } + + // Should be able to remove custom claims + auth.setCustomUserClaimsAsync(uid, null).get(); + updatedUser = auth.getUserAsync(uid).get(); + assertTrue(updatedUser.getCustomClaims().isEmpty()); } @Test @@ -527,7 +512,7 @@ public void testVerifyIdToken() throws Exception { } idToken = signInWithCustomToken(customToken); decoded = auth.verifyIdTokenAsync(idToken, true).get(); - assertEquals("user2", decoded.getUid()); + assertEquals("user2", decoded.getUid()); auth.deleteUserAsync("user2"); } @@ -581,32 +566,29 @@ public void testCustomTokenWithClaims() throws Exception { @Test public void testImportUsers() throws Exception { - RandomUser randomUser = RandomUser.create(); + RandomUser randomUser = UserTestUtils.generateRandomUserInfo(); ImportUserRecord user = ImportUserRecord.builder() - .setUid(randomUser.uid) - .setEmail(randomUser.email) + .setUid(randomUser.getUid()) + .setEmail(randomUser.getEmail()) .build(); UserImportResult result = auth.importUsersAsync(ImmutableList.of(user)).get(); + temporaryUser.registerUid(randomUser.getUid()); assertEquals(1, result.getSuccessCount()); assertEquals(0, result.getFailureCount()); - try { - UserRecord savedUser = auth.getUserAsync(randomUser.uid).get(); - assertEquals(randomUser.email, savedUser.getEmail()); - } finally { - auth.deleteUserAsync(randomUser.uid).get(); - } + UserRecord savedUser = auth.getUserAsync(randomUser.getUid()).get(); + assertEquals(randomUser.getEmail(), savedUser.getEmail()); } @Test public void testImportUsersWithPassword() throws Exception { - RandomUser randomUser = RandomUser.create(); + RandomUser randomUser = UserTestUtils.generateRandomUserInfo(); final byte[] passwordHash = BaseEncoding.base64().decode( "V358E8LdWJXAO7muq0CufVpEOXaj8aFiC7T/rcaGieN04q/ZPJ08WhJEHGjj9lz/2TT+/86N5VjVoc5DdBhBiw=="); ImportUserRecord user = ImportUserRecord.builder() - .setUid(randomUser.uid) - .setEmail(randomUser.email) + .setUid(randomUser.getUid()) + .setEmail(randomUser.getEmail()) .setPasswordHash(passwordHash) .setPasswordSalt("NaCl".getBytes()) .build(); @@ -622,88 +604,309 @@ public void testImportUsersWithPassword() throws Exception { .setRounds(8) .setMemoryCost(14) .build())).get(); + temporaryUser.registerUid(randomUser.getUid()); assertEquals(1, result.getSuccessCount()); assertEquals(0, result.getFailureCount()); - try { - UserRecord savedUser = auth.getUserAsync(randomUser.uid).get(); - assertEquals(randomUser.email, savedUser.getEmail()); - String idToken = signInWithPassword(randomUser.email, "password"); - assertFalse(Strings.isNullOrEmpty(idToken)); - } finally { - auth.deleteUserAsync(randomUser.uid).get(); - } + UserRecord savedUser = auth.getUserAsync(randomUser.getUid()).get(); + assertEquals(randomUser.getEmail(), savedUser.getEmail()); + String idToken = signInWithPassword(randomUser.getEmail(), "password"); + assertFalse(Strings.isNullOrEmpty(idToken)); } @Test public void testGeneratePasswordResetLink() throws Exception { - RandomUser user = RandomUser.create(); - auth.createUser(new CreateRequest() - .setUid(user.uid) - .setEmail(user.email) + RandomUser user = UserTestUtils.generateRandomUserInfo(); + temporaryUser.create(new UserRecord.CreateRequest() + .setUid(user.getUid()) + .setEmail(user.getEmail()) .setEmailVerified(false) .setPassword("password")); - try { - String link = auth.generatePasswordResetLink(user.email, ActionCodeSettings.builder() - .setUrl(ACTION_LINK_CONTINUE_URL) - .setHandleCodeInApp(false) - .build()); - Map linkParams = parseLinkParameters(link); - assertEquals(ACTION_LINK_CONTINUE_URL, linkParams.get("continueUrl")); - String email = resetPassword(user.email, "password", "newpassword", - linkParams.get("oobCode")); - assertEquals(user.email, email); - // Password reset also verifies the user's email - assertTrue(auth.getUser(user.uid).isEmailVerified()); - } finally { - auth.deleteUser(user.uid); - } + String link = auth.generatePasswordResetLink(user.getEmail(), ActionCodeSettings.builder() + .setUrl(ACTION_LINK_CONTINUE_URL) + .setHandleCodeInApp(false) + .build()); + Map linkParams = parseLinkParameters(link); + assertEquals(ACTION_LINK_CONTINUE_URL, linkParams.get("continueUrl")); + String email = resetPassword(user.getEmail(), "password", "newpassword", + linkParams.get("oobCode")); + assertEquals(user.getEmail(), email); + // Password reset also verifies the user's email + assertTrue(auth.getUser(user.getUid()).isEmailVerified()); } @Test public void testGenerateEmailVerificationResetLink() throws Exception { - RandomUser user = RandomUser.create(); - auth.createUser(new CreateRequest() - .setUid(user.uid) - .setEmail(user.email) + RandomUser user = UserTestUtils.generateRandomUserInfo(); + temporaryUser.create(new UserRecord.CreateRequest() + .setUid(user.getUid()) + .setEmail(user.getEmail()) .setEmailVerified(false) .setPassword("password")); - try { - String link = auth.generateEmailVerificationLink(user.email, ActionCodeSettings.builder() - .setUrl(ACTION_LINK_CONTINUE_URL) - .setHandleCodeInApp(false) - .build()); - Map linkParams = parseLinkParameters(link); - assertEquals(ACTION_LINK_CONTINUE_URL, linkParams.get("continueUrl")); - // There doesn't seem to be a public API for verifying an email, so we cannot do a more - // thorough test here. - assertEquals("verifyEmail", linkParams.get("mode")); - } finally { - auth.deleteUser(user.uid); - } + String link = auth.generateEmailVerificationLink(user.getEmail(), ActionCodeSettings.builder() + .setUrl(ACTION_LINK_CONTINUE_URL) + .setHandleCodeInApp(false) + .build()); + Map linkParams = parseLinkParameters(link); + assertEquals(ACTION_LINK_CONTINUE_URL, linkParams.get("continueUrl")); + // There doesn't seem to be a public API for verifying an email, so we cannot do a more + // thorough test here. + assertEquals("verifyEmail", linkParams.get("mode")); } @Test public void testGenerateSignInWithEmailLink() throws Exception { - RandomUser user = RandomUser.create(); - auth.createUser(new CreateRequest() - .setUid(user.uid) - .setEmail(user.email) + RandomUser user = UserTestUtils.generateRandomUserInfo(); + temporaryUser.create(new UserRecord.CreateRequest() + .setUid(user.getUid()) + .setEmail(user.getEmail()) .setEmailVerified(false) .setPassword("password")); - try { - String link = auth.generateSignInWithEmailLink(user.email, ActionCodeSettings.builder() - .setUrl(ACTION_LINK_CONTINUE_URL) - .setHandleCodeInApp(false) - .build()); - Map linkParams = parseLinkParameters(link); - assertEquals(ACTION_LINK_CONTINUE_URL, linkParams.get("continueUrl")); - String idToken = signInWithEmailLink(user.email, linkParams.get("oobCode")); - assertFalse(Strings.isNullOrEmpty(idToken)); - assertTrue(auth.getUser(user.uid).isEmailVerified()); - } finally { - auth.deleteUser(user.uid); + String link = auth.generateSignInWithEmailLink(user.getEmail(), ActionCodeSettings.builder() + .setUrl(ACTION_LINK_CONTINUE_URL) + .setHandleCodeInApp(false) + .build()); + Map linkParams = parseLinkParameters(link); + assertEquals(ACTION_LINK_CONTINUE_URL, linkParams.get("continueUrl")); + String idToken = signInWithEmailLink(user.getEmail(), linkParams.get("oobCode")); + assertFalse(Strings.isNullOrEmpty(idToken)); + assertTrue(auth.getUser(user.getUid()).isEmailVerified()); + } + + @Test + public void testOidcProviderConfigLifecycle() throws Exception { + // Create provider config + String providerId = "oidc.provider-id"; + OidcProviderConfig config = temporaryProviderConfig.createOidcProviderConfig( + new OidcProviderConfig.CreateRequest() + .setProviderId(providerId) + .setDisplayName("DisplayName") + .setEnabled(true) + .setClientId("ClientId") + .setIssuer("https://oidc.com/issuer")); + assertEquals(providerId, config.getProviderId()); + assertEquals("DisplayName", config.getDisplayName()); + assertTrue(config.isEnabled()); + assertEquals("ClientId", config.getClientId()); + assertEquals("https://oidc.com/issuer", config.getIssuer()); + + // Get provider config + config = auth.getOidcProviderConfigAsync(providerId).get(); + assertEquals(providerId, config.getProviderId()); + assertEquals("DisplayName", config.getDisplayName()); + assertTrue(config.isEnabled()); + assertEquals("ClientId", config.getClientId()); + assertEquals("https://oidc.com/issuer", config.getIssuer()); + + // Update provider config + OidcProviderConfig.UpdateRequest updateRequest = + new OidcProviderConfig.UpdateRequest(providerId) + .setDisplayName("NewDisplayName") + .setEnabled(false) + .setClientId("NewClientId") + .setIssuer("https://oidc.com/new-issuer"); + config = auth.updateOidcProviderConfigAsync(updateRequest).get(); + assertEquals(providerId, config.getProviderId()); + assertEquals("NewDisplayName", config.getDisplayName()); + assertFalse(config.isEnabled()); + assertEquals("NewClientId", config.getClientId()); + assertEquals("https://oidc.com/new-issuer", config.getIssuer()); + + // Delete provider config + temporaryProviderConfig.deleteOidcProviderConfig(providerId); + ProviderConfigTestUtils.assertOidcProviderConfigDoesNotExist(auth, providerId); + } + + @Test + public void testListOidcProviderConfigs() throws Exception { + final List providerIds = new ArrayList<>(); + + // Create provider configs + for (int i = 0; i < 3; i++) { + String providerId = "oidc.provider-id" + i; + providerIds.add(providerId); + temporaryProviderConfig.createOidcProviderConfig( + new OidcProviderConfig.CreateRequest() + .setProviderId(providerId) + .setClientId("CLIENT_ID") + .setIssuer("https://oidc.com/issuer")); + } + + // Test list by batches + final AtomicInteger collected = new AtomicInteger(0); + ListProviderConfigsPage page = + auth.listOidcProviderConfigsAsync(null).get(); + while (page != null) { + for (OidcProviderConfig providerConfig : page.getValues()) { + if (checkOidcProviderConfig(providerIds, providerConfig)) { + collected.incrementAndGet(); + } + } + page = page.getNextPage(); + } + assertEquals(providerIds.size(), collected.get()); + + // Test iterate all + collected.set(0); + page = auth.listOidcProviderConfigsAsync(null).get(); + for (OidcProviderConfig providerConfig : page.iterateAll()) { + if (checkOidcProviderConfig(providerIds, providerConfig)) { + collected.incrementAndGet(); + } + } + assertEquals(providerIds.size(), collected.get()); + + // Test iterate async + collected.set(0); + final Semaphore semaphore = new Semaphore(0); + final AtomicReference error = new AtomicReference<>(); + ApiFuture> pageFuture = + auth.listOidcProviderConfigsAsync(null); + ApiFutures.addCallback( + pageFuture, + new ApiFutureCallback>() { + @Override + public void onFailure(Throwable t) { + error.set(t); + semaphore.release(); + } + + @Override + public void onSuccess(ListProviderConfigsPage result) { + for (OidcProviderConfig providerConfig : result.iterateAll()) { + if (checkOidcProviderConfig(providerIds, providerConfig)) { + collected.incrementAndGet(); + } + } + semaphore.release(); + } + }, MoreExecutors.directExecutor()); + semaphore.acquire(); + assertEquals(providerIds.size(), collected.get()); + assertNull(error.get()); + } + + @Test + public void testSamlProviderConfigLifecycle() throws Exception { + // Create provider config + String providerId = "saml.provider-id"; + SamlProviderConfig config = temporaryProviderConfig.createSamlProviderConfig( + new SamlProviderConfig.CreateRequest() + .setProviderId(providerId) + .setDisplayName("DisplayName") + .setEnabled(true) + .setIdpEntityId("IDP_ENTITY_ID") + .setSsoUrl("https://example.com/login") + .addX509Certificate("certificate1") + .addX509Certificate("certificate2") + .setRpEntityId("RP_ENTITY_ID") + .setCallbackUrl("https://projectId.firebaseapp.com/__/auth/handler")); + assertEquals(providerId, config.getProviderId()); + assertEquals("DisplayName", config.getDisplayName()); + assertTrue(config.isEnabled()); + assertEquals("IDP_ENTITY_ID", config.getIdpEntityId()); + assertEquals("https://example.com/login", config.getSsoUrl()); + assertEquals(ImmutableList.of("certificate1", "certificate2"), config.getX509Certificates()); + assertEquals("RP_ENTITY_ID", config.getRpEntityId()); + assertEquals("https://projectId.firebaseapp.com/__/auth/handler", config.getCallbackUrl()); + + config = auth.getSamlProviderConfig(providerId); + assertEquals(providerId, config.getProviderId()); + assertEquals("DisplayName", config.getDisplayName()); + assertTrue(config.isEnabled()); + assertEquals("IDP_ENTITY_ID", config.getIdpEntityId()); + assertEquals("https://example.com/login", config.getSsoUrl()); + assertEquals(ImmutableList.of("certificate1", "certificate2"), config.getX509Certificates()); + assertEquals("RP_ENTITY_ID", config.getRpEntityId()); + assertEquals("https://projectId.firebaseapp.com/__/auth/handler", config.getCallbackUrl()); + + // Update provider config + SamlProviderConfig.UpdateRequest updateRequest = + new SamlProviderConfig.UpdateRequest(providerId) + .setDisplayName("NewDisplayName") + .setEnabled(false) + .addX509Certificate("certificate"); + config = auth.updateSamlProviderConfigAsync(updateRequest).get(); + assertEquals(providerId, config.getProviderId()); + assertEquals("NewDisplayName", config.getDisplayName()); + assertFalse(config.isEnabled()); + assertEquals(ImmutableList.of("certificate"), config.getX509Certificates()); + + // Delete provider config + temporaryProviderConfig.deleteSamlProviderConfig(providerId); + ProviderConfigTestUtils.assertSamlProviderConfigDoesNotExist(auth, providerId); + } + + @Test + public void testListSamlProviderConfigs() throws Exception { + final List providerIds = new ArrayList<>(); + + // Create provider configs + for (int i = 0; i < 3; i++) { + String providerId = "saml.provider-id" + i; + providerIds.add(providerId); + temporaryProviderConfig.createSamlProviderConfig( + new SamlProviderConfig.CreateRequest() + .setProviderId(providerId) + .setIdpEntityId("IDP_ENTITY_ID") + .setSsoUrl("https://example.com/login") + .addX509Certificate("certificate") + .setRpEntityId("RP_ENTITY_ID") + .setCallbackUrl("https://projectId.firebaseapp.com/__/auth/handler")); + } + + // Test list by batches + final AtomicInteger collected = new AtomicInteger(0); + ListProviderConfigsPage page = + auth.listSamlProviderConfigsAsync(null).get(); + while (page != null) { + for (SamlProviderConfig providerConfig : page.getValues()) { + if (checkSamlProviderConfig(providerIds, providerConfig)) { + collected.incrementAndGet(); + } + } + page = page.getNextPage(); + } + assertEquals(providerIds.size(), collected.get()); + + // Test iterate all + collected.set(0); + page = auth.listSamlProviderConfigsAsync(null).get(); + for (SamlProviderConfig providerConfig : page.iterateAll()) { + if (checkSamlProviderConfig(providerIds, providerConfig)) { + collected.incrementAndGet(); + } } + assertEquals(providerIds.size(), collected.get()); + + // Test iterate async + collected.set(0); + final Semaphore semaphore = new Semaphore(0); + final AtomicReference error = new AtomicReference<>(); + ApiFuture> pageFuture = + auth.listSamlProviderConfigsAsync(null); + ApiFutures.addCallback( + pageFuture, + new ApiFutureCallback>() { + @Override + public void onFailure(Throwable t) { + error.set(t); + semaphore.release(); + } + + @Override + public void onSuccess(ListProviderConfigsPage result) { + for (SamlProviderConfig providerConfig : result.iterateAll()) { + if (checkSamlProviderConfig(providerIds, providerConfig)) { + collected.incrementAndGet(); + } + } + semaphore.release(); + } + }, MoreExecutors.directExecutor()); + semaphore.acquire(); + assertEquals(providerIds.size(), collected.get()); + assertNull(error.get()); } private Map parseLinkParameters(String link) throws Exception { @@ -721,22 +924,22 @@ private Map parseLinkParameters(String link) throws Exception { return result; } - static String randomPhoneNumber() { - Random random = new Random(); - StringBuilder builder = new StringBuilder("+1"); - for (int i = 0; i < 10; i++) { - builder.append(random.nextInt(10)); - } - return builder.toString(); + private String signInWithCustomToken(String customToken) throws IOException { + return signInWithCustomToken(customToken, null); } - private String signInWithCustomToken(String customToken) throws IOException { - GenericUrl url = new GenericUrl(VERIFY_CUSTOM_TOKEN_URL + "?key=" + private String signInWithCustomToken( + String customToken, @Nullable String tenantId) throws IOException { + final GenericUrl url = new GenericUrl(VERIFY_CUSTOM_TOKEN_URL + "?key=" + IntegrationTestUtils.getApiKey()); - Map content = ImmutableMap.of( - "token", customToken, "returnSecureToken", true); + ImmutableMap.Builder content = ImmutableMap.builder(); + content.put("token", customToken); + content.put("returnSecureToken", true); + if (tenantId != null) { + content.put("tenantId", tenantId); + } HttpRequest request = transport.createRequestFactory().buildPostRequest(url, - new JsonHttpContent(jsonFactory, content)); + new JsonHttpContent(jsonFactory, content.build())); request.setParser(new JsonObjectParser(jsonFactory)); HttpResponse response = request.execute(); try { @@ -800,9 +1003,9 @@ private String signInWithEmailLink( } } - private void checkRecreate(String uid) throws Exception { + private void checkRecreateUser(String uid) throws Exception { try { - auth.createUserAsync(new CreateRequest().setUid(uid)).get(); + auth.createUserAsync(new UserRecord.CreateRequest().setUid(uid)).get(); fail("No error thrown for creating user with existing ID"); } catch (ExecutionException e) { assertTrue(e.getCause() instanceof FirebaseAuthException); @@ -810,21 +1013,25 @@ private void checkRecreate(String uid) throws Exception { } } - static class RandomUser { - final String uid; - final String email; - - private RandomUser(String uid, String email) { - this.uid = uid; - this.email = email; + private boolean checkOidcProviderConfig(List providerIds, OidcProviderConfig config) { + if (providerIds.contains(config.getProviderId())) { + assertEquals("CLIENT_ID", config.getClientId()); + assertEquals("https://oidc.com/issuer", config.getIssuer()); + return true; } + return false; + } - static RandomUser create() { - final String uid = UUID.randomUUID().toString().replaceAll("-", ""); - final String email = ("test" + uid.substring(0, 12) + "@example." - + uid.substring(12) + ".com").toLowerCase(); - return new RandomUser(uid, email); + private boolean checkSamlProviderConfig(List providerIds, SamlProviderConfig config) { + if (providerIds.contains(config.getProviderId())) { + assertEquals("IDP_ENTITY_ID", config.getIdpEntityId()); + assertEquals("https://example.com/login", config.getSsoUrl()); + assertEquals(ImmutableList.of("certificate"), config.getX509Certificates()); + assertEquals("RP_ENTITY_ID", config.getRpEntityId()); + assertEquals("https://projectId.firebaseapp.com/__/auth/handler", config.getCallbackUrl()); + return true; } + return false; } static UserRecord newUserWithParams() throws Exception { @@ -834,13 +1041,14 @@ static UserRecord newUserWithParams() throws Exception { static UserRecord newUserWithParams(FirebaseAuth auth) throws Exception { // TODO(rsgowman): This function could be used throughout this file (similar to the other // ports). - RandomUser randomUser = RandomUser.create(); - return auth.createUser(new CreateRequest() - .setUid(randomUser.uid) - .setEmail(randomUser.email) - .setPhoneNumber(randomPhoneNumber()) + RandomUser randomUser = UserTestUtils.generateRandomUserInfo(); + return auth.createUser(new UserRecord.CreateRequest() + .setUid(randomUser.getUid()) + .setEmail(randomUser.getEmail()) + .setPhoneNumber(randomUser.getPhoneNumber()) .setDisplayName("Random User") .setPhotoUrl("https://example.com/photo.png") .setPassword("password")); } } + diff --git a/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java b/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java index 1bc05174f..635e1bfba 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java +++ b/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java @@ -32,7 +32,6 @@ import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; import com.google.firebase.TestOnlyImplFirebaseTrampolines; -import com.google.firebase.auth.internal.FirebaseTokenFactory; import com.google.firebase.testing.ServiceAccount; import com.google.firebase.testing.TestUtils; import java.lang.reflect.InvocationTargetException; @@ -429,12 +428,12 @@ private FirebaseAuth getAuthForIdTokenVerification(FirebaseTokenVerifier tokenVe private FirebaseAuth getAuthForIdTokenVerification( Supplier tokenVerifierSupplier) { FirebaseApp app = FirebaseApp.initializeApp(firebaseOptions); - FirebaseUserManager userManager = new FirebaseUserManager(app); - return FirebaseAuth.builder() - .setFirebaseApp(app) - .setIdTokenVerifier(tokenVerifierSupplier) - .setUserManager(Suppliers.ofInstance(userManager)) - .build(); + FirebaseUserManager userManager = FirebaseUserManager.builder().setFirebaseApp(app).build(); + return new FirebaseAuth( + AbstractFirebaseAuth.builder() + .setFirebaseApp(app) + .setIdTokenVerifier(tokenVerifierSupplier) + .setUserManager(Suppliers.ofInstance(userManager))); } private FirebaseAuth getAuthForSessionCookieVerification(FirebaseTokenVerifier tokenVerifier) { @@ -444,12 +443,12 @@ private FirebaseAuth getAuthForSessionCookieVerification(FirebaseTokenVerifier t private FirebaseAuth getAuthForSessionCookieVerification( Supplier tokenVerifierSupplier) { FirebaseApp app = FirebaseApp.initializeApp(firebaseOptions); - FirebaseUserManager userManager = new FirebaseUserManager(app); - return FirebaseAuth.builder() - .setFirebaseApp(app) - .setCookieVerifier(tokenVerifierSupplier) - .setUserManager(Suppliers.ofInstance(userManager)) - .build(); + FirebaseUserManager userManager = FirebaseUserManager.builder().setFirebaseApp(app).build(); + return new FirebaseAuth( + AbstractFirebaseAuth.builder() + .setFirebaseApp(app) + .setCookieVerifier(tokenVerifierSupplier) + .setUserManager(Suppliers.ofInstance(userManager))); } private static class MockTokenVerifier implements FirebaseTokenVerifier { diff --git a/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java b/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java index b10817afb..5dd1e9c14 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java +++ b/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java @@ -29,6 +29,7 @@ import com.google.api.client.testing.http.MockLowLevelHttpResponse; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.firebase.testing.ServiceAccount; import java.io.IOException; import java.util.concurrent.TimeUnit; @@ -216,6 +217,50 @@ public void testMalformedToken() throws Exception { tokenVerifier.verifyToken("not.a.jwt"); } + @Test + public void testVerifyTokenDifferentTenantIds() { + try { + fullyPopulatedBuilder() + .setTenantId("TENANT_1") + .build() + .verifyToken(createTokenWithTenantId("TENANT_2")); + } catch (FirebaseAuthException e) { + assertEquals(FirebaseTokenVerifierImpl.TENANT_ID_MISMATCH_ERROR, e.getErrorCode()); + assertEquals( + "The tenant ID ('TENANT_2') of the token did not match the expected value ('TENANT_1')", + e.getMessage()); + } + } + + @Test + public void testVerifyTokenMissingTenantId() { + try { + fullyPopulatedBuilder() + .setTenantId("TENANT_ID") + .build() + .verifyToken(tokenFactory.createToken()); + } catch (FirebaseAuthException e) { + assertEquals(FirebaseTokenVerifierImpl.TENANT_ID_MISMATCH_ERROR, e.getErrorCode()); + assertEquals( + "The tenant ID ('') of the token did not match the expected value ('TENANT_ID')", + e.getMessage()); + } + } + + @Test + public void testVerifyTokenUnexpectedTenantId() { + try { + fullyPopulatedBuilder() + .build() + .verifyToken(createTokenWithTenantId("TENANT_ID")); + } catch (FirebaseAuthException e) { + assertEquals(FirebaseTokenVerifierImpl.TENANT_ID_MISMATCH_ERROR, e.getErrorCode()); + assertEquals( + "The tenant ID ('TENANT_ID') of the token did not match the expected value ('')", + e.getMessage()); + } + } + @Test(expected = NullPointerException.class) public void testBuilderNoPublicKeysManager() { fullyPopulatedBuilder().setPublicKeysManager(null).build(); @@ -337,4 +382,10 @@ private String createTokenWithTimestamps(long issuedAtSeconds, long expirationSe payload.setExpirationTimeSeconds(expirationSeconds); return tokenFactory.createToken(payload); } + + private String createTokenWithTenantId(String tenantId) { + Payload payload = tokenFactory.createTokenPayload(); + payload.set("firebase", ImmutableMap.of("tenant", tenantId)); + return tokenFactory.createToken(payload); + } } diff --git a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java index 67d0448e9..d407fd8ec 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java +++ b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java @@ -16,10 +16,12 @@ package com.google.firebase.auth; +import static org.hamcrest.core.IsInstanceOf.instanceOf; 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.assertThat; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -42,10 +44,9 @@ import com.google.firebase.FirebaseOptions; import com.google.firebase.TestOnlyImplFirebaseTrampolines; import com.google.firebase.auth.FirebaseUserManager.EmailLinkType; -import com.google.firebase.auth.UidIdentifier; -import com.google.firebase.auth.UserIdentifier; -import com.google.firebase.auth.UserRecord.CreateRequest; -import com.google.firebase.auth.UserRecord.UpdateRequest; +import com.google.firebase.auth.internal.AuthHttpClient; +import com.google.firebase.auth.multitenancy.TenantAwareFirebaseAuth; +import com.google.firebase.auth.multitenancy.TenantManager; import com.google.firebase.internal.SdkUtils; import com.google.firebase.testing.MultiRequestMockHttpTransport; import com.google.firebase.testing.TestResponseInterceptor; @@ -67,8 +68,12 @@ public class FirebaseUserManagerTest { + private static final JsonFactory JSON_FACTORY = Utils.getDefaultJsonFactory(); + private static final String TEST_TOKEN = "token"; + private static final GoogleCredentials credentials = new MockGoogleCredentials(TEST_TOKEN); + private static final ActionCodeSettings ACTION_CODE_SETTINGS = ActionCodeSettings.builder() .setUrl("https://example.dynamic.link") .setHandleCodeInApp(true) @@ -78,9 +83,15 @@ public class FirebaseUserManagerTest { .setAndroidInstallApp(true) .setAndroidMinimumVersion("6") .build(); + private static final Map ACTION_CODE_SETTINGS_MAP = ACTION_CODE_SETTINGS.getProperties(); + private static final String PROJECT_BASE_URL = + "https://identitytoolkit.googleapis.com/v2/projects/test-project-id"; + + private static final String TENANTS_BASE_URL = PROJECT_BASE_URL + "/tenants"; + @After public void tearDown() { TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); @@ -120,9 +131,9 @@ public void testGetUserWithNotFoundError() throws Exception { FirebaseAuth.getInstance().getUserAsync("testuser").get(); fail("No error thrown for invalid response"); } catch (ExecutionException e) { - assertTrue(e.getCause() instanceof FirebaseAuthException); + assertThat(e.getCause(), instanceOf(FirebaseAuthException.class)); FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); - assertEquals(FirebaseUserManager.USER_NOT_FOUND_ERROR, authException.getErrorCode()); + assertEquals(AuthHttpClient.USER_NOT_FOUND_ERROR, authException.getErrorCode()); } } @@ -143,9 +154,9 @@ public void testGetUserByEmailWithNotFoundError() throws Exception { FirebaseAuth.getInstance().getUserByEmailAsync("testuser@example.com").get(); fail("No error thrown for invalid response"); } catch (ExecutionException e) { - assertTrue(e.getCause() instanceof FirebaseAuthException); + assertThat(e.getCause(), instanceOf(FirebaseAuthException.class)); FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); - assertEquals(FirebaseUserManager.USER_NOT_FOUND_ERROR, authException.getErrorCode()); + assertEquals(AuthHttpClient.USER_NOT_FOUND_ERROR, authException.getErrorCode()); } } @@ -166,9 +177,9 @@ public void testGetUserByPhoneNumberWithNotFoundError() throws Exception { FirebaseAuth.getInstance().getUserByPhoneNumberAsync("+1234567890").get(); fail("No error thrown for invalid response"); } catch (ExecutionException e) { - assertTrue(e.getCause() instanceof FirebaseAuthException); + assertThat(e.getCause(), instanceOf(FirebaseAuthException.class)); FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); - assertEquals(FirebaseUserManager.USER_NOT_FOUND_ERROR, authException.getErrorCode()); + assertEquals(AuthHttpClient.USER_NOT_FOUND_ERROR, authException.getErrorCode()); } } @@ -371,7 +382,8 @@ public void testCreateUser() throws Exception { TestResponseInterceptor interceptor = initializeAppForUserManagement( TestUtils.loadResource("createUser.json"), TestUtils.loadResource("getUser.json")); - UserRecord user = FirebaseAuth.getInstance().createUserAsync(new CreateRequest()).get(); + UserRecord user = + FirebaseAuth.getInstance().createUserAsync(new UserRecord.CreateRequest()).get(); checkUserRecord(user); checkRequestHeaders(interceptor); } @@ -382,7 +394,7 @@ public void testUpdateUser() throws Exception { TestUtils.loadResource("createUser.json"), TestUtils.loadResource("getUser.json")); UserRecord user = FirebaseAuth.getInstance() - .updateUserAsync(new UpdateRequest("testuser")).get(); + .updateUserAsync(new UserRecord.UpdateRequest("testuser")).get(); checkUserRecord(user); checkRequestHeaders(interceptor); } @@ -397,12 +409,9 @@ public void testSetCustomAttributes() throws Exception { FirebaseAuth.getInstance().setCustomUserClaimsAsync("testuser", claims).get(); checkRequestHeaders(interceptor); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - interceptor.getResponse().getRequest().getContent().writeTo(out); - JsonFactory jsonFactory = Utils.getDefaultJsonFactory(); - GenericJson parsed = jsonFactory.fromString(new String(out.toByteArray()), GenericJson.class); + GenericJson parsed = parseRequestContent(interceptor); assertEquals("testuser", parsed.get("localId")); - assertEquals(jsonFactory.toString(claims), parsed.get("customAttributes")); + assertEquals(JSON_FACTORY.toString(claims), parsed.get("customAttributes")); } @Test @@ -413,10 +422,7 @@ public void testRevokeRefreshTokens() throws Exception { FirebaseAuth.getInstance().revokeRefreshTokensAsync("testuser").get(); checkRequestHeaders(interceptor); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - interceptor.getResponse().getRequest().getContent().writeTo(out); - JsonFactory jsonFactory = Utils.getDefaultJsonFactory(); - GenericJson parsed = jsonFactory.fromString(new String(out.toByteArray()), GenericJson.class); + GenericJson parsed = parseRequestContent(interceptor); assertEquals("testuser", parsed.get("localId")); assertNotNull(parsed.get("validSince")); } @@ -518,14 +524,11 @@ public void testImportUsers() throws Exception { assertEquals(0, result.getFailureCount()); assertTrue(result.getErrors().isEmpty()); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - interceptor.getResponse().getRequest().getContent().writeTo(out); - JsonFactory jsonFactory = Utils.getDefaultJsonFactory(); - GenericJson parsed = jsonFactory.fromString(new String(out.toByteArray()), GenericJson.class); + GenericJson parsed = parseRequestContent(interceptor); assertEquals(1, parsed.size()); List> expected = ImmutableList.of( - user1.getProperties(jsonFactory), - user2.getProperties(jsonFactory) + user1.getProperties(JSON_FACTORY), + user2.getProperties(JSON_FACTORY) ); assertEquals(expected, parsed.get("users")); } @@ -558,15 +561,12 @@ public void testImportUsersError() throws Exception { assertEquals(2, error.getIndex()); assertEquals("Another error occurred in user3", error.getReason()); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - interceptor.getResponse().getRequest().getContent().writeTo(out); - JsonFactory jsonFactory = Utils.getDefaultJsonFactory(); - GenericJson parsed = jsonFactory.fromString(new String(out.toByteArray()), GenericJson.class); + GenericJson parsed = parseRequestContent(interceptor); assertEquals(1, parsed.size()); List> expected = ImmutableList.of( - user1.getProperties(jsonFactory), - user2.getProperties(jsonFactory), - user3.getProperties(jsonFactory) + user1.getProperties(JSON_FACTORY), + user2.getProperties(JSON_FACTORY), + user3.getProperties(JSON_FACTORY) ); assertEquals(expected, parsed.get("users")); } @@ -596,14 +596,11 @@ protected Map getOptions() { assertEquals(0, result.getFailureCount()); assertTrue(result.getErrors().isEmpty()); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - interceptor.getResponse().getRequest().getContent().writeTo(out); - JsonFactory jsonFactory = Utils.getDefaultJsonFactory(); - GenericJson parsed = jsonFactory.fromString(new String(out.toByteArray()), GenericJson.class); + GenericJson parsed = parseRequestContent(interceptor); assertEquals(4, parsed.size()); List> expected = ImmutableList.of( - user1.getProperties(jsonFactory), - user2.getProperties(jsonFactory) + user1.getProperties(JSON_FACTORY), + user2.getProperties(JSON_FACTORY) ); assertEquals(expected, parsed.get("users")); assertEquals("MOCK_HASH", parsed.get("hashAlgorithm")); @@ -669,10 +666,7 @@ public void testCreateSessionCookie() throws Exception { assertEquals("MockCookieString", cookie); checkRequestHeaders(interceptor); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - interceptor.getResponse().getRequest().getContent().writeTo(out); - JsonFactory jsonFactory = Utils.getDefaultJsonFactory(); - GenericJson parsed = jsonFactory.fromString(new String(out.toByteArray()), GenericJson.class); + GenericJson parsed = parseRequestContent(interceptor); assertEquals(2, parsed.size()); assertEquals("testToken", parsed.get("idToken")); assertEquals(new BigDecimal(3600), parsed.get("validDuration")); @@ -754,13 +748,13 @@ public void call(FirebaseAuth auth) throws Exception { .add(new UserManagerOp() { @Override public void call(FirebaseAuth auth) throws Exception { - auth.createUserAsync(new CreateRequest()).get(); + auth.createUserAsync(new UserRecord.CreateRequest()).get(); } }) .add(new UserManagerOp() { @Override public void call(FirebaseAuth auth) throws Exception { - auth.updateUserAsync(new UpdateRequest("test")).get(); + auth.updateUserAsync(new UserRecord.UpdateRequest("test")).get(); } }) .add(new UserManagerOp() { @@ -790,12 +784,12 @@ public void call(FirebaseAuth auth) throws Exception { operation.call(auth); fail("No error thrown for HTTP error: " + code); } catch (ExecutionException e) { - assertTrue(e.getCause() instanceof FirebaseAuthException); + assertThat(e.getCause(), instanceOf(FirebaseAuthException.class)); FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); String msg = String.format("Unexpected HTTP response with status: %d; body: {}", code); assertEquals(msg, authException.getMessage()); - assertTrue(authException.getCause() instanceof HttpResponseException); - assertEquals(FirebaseUserManager.INTERNAL_ERROR, authException.getErrorCode()); + assertThat(authException.getCause(), instanceOf(HttpResponseException.class)); + assertEquals(AuthHttpClient.INTERNAL_ERROR, authException.getErrorCode()); } } } @@ -808,11 +802,11 @@ public void call(FirebaseAuth auth) throws Exception { operation.call(auth); fail("No error thrown for HTTP error"); } catch (ExecutionException e) { - assertTrue(e.getCause().toString(), e.getCause() instanceof FirebaseAuthException); + assertThat(e.getCause().toString(), e.getCause(), instanceOf(FirebaseAuthException.class)); FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); - assertEquals("User management service responded with an error", authException.getMessage()); - assertTrue(authException.getCause() instanceof HttpResponseException); - assertEquals(FirebaseUserManager.USER_NOT_FOUND_ERROR, authException.getErrorCode()); + assertEquals("Firebase Auth service responded with an error", authException.getMessage()); + assertThat(authException.getCause(), instanceOf(HttpResponseException.class)); + assertEquals(AuthHttpClient.USER_NOT_FOUND_ERROR, authException.getErrorCode()); } } } @@ -824,10 +818,10 @@ public void testGetUserMalformedJsonError() throws Exception { FirebaseAuth.getInstance().getUserAsync("testuser").get(); fail("No error thrown for JSON error"); } catch (ExecutionException e) { - assertTrue(e.getCause() instanceof FirebaseAuthException); + assertThat(e.getCause(), instanceOf(FirebaseAuthException.class)); FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); - assertTrue(authException.getCause() instanceof IOException); - assertEquals(FirebaseUserManager.INTERNAL_ERROR, authException.getErrorCode()); + assertThat(authException.getCause(), instanceOf(IOException.class)); + assertEquals(AuthHttpClient.INTERNAL_ERROR, authException.getErrorCode()); } } @@ -841,12 +835,12 @@ public void testGetUserUnexpectedHttpError() throws Exception { auth.getUserAsync("testuser").get(); fail("No error thrown for JSON error"); } catch (ExecutionException e) { - assertTrue(e.getCause() instanceof FirebaseAuthException); + assertThat(e.getCause(), instanceOf(FirebaseAuthException.class)); FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); - assertTrue(authException.getCause() instanceof HttpResponseException); + assertThat(authException.getCause(), instanceOf(HttpResponseException.class)); assertEquals("Unexpected HTTP response with status: 500; body: {\"not\" json}", authException.getMessage()); - assertEquals(FirebaseUserManager.INTERNAL_ERROR, authException.getErrorCode()); + assertEquals(AuthHttpClient.INTERNAL_ERROR, authException.getErrorCode()); } } @@ -874,13 +868,13 @@ public void testTimeout() throws Exception { @Test public void testUserBuilder() { - Map map = new CreateRequest().getProperties(); + Map map = new UserRecord.CreateRequest().getProperties(); assertTrue(map.isEmpty()); } @Test public void testUserBuilderWithParams() { - Map map = new CreateRequest() + Map map = new UserRecord.CreateRequest() .setUid("TestUid") .setDisplayName("Display Name") .setPhotoUrl("http://test.com/example.png") @@ -901,7 +895,7 @@ public void testUserBuilderWithParams() { @Test public void testInvalidUid() { - CreateRequest user = new CreateRequest(); + UserRecord.CreateRequest user = new UserRecord.CreateRequest(); try { user.setUid(null); fail("No error thrown for null uid"); @@ -926,7 +920,7 @@ public void testInvalidUid() { @Test public void testInvalidDisplayName() { - CreateRequest user = new CreateRequest(); + UserRecord.CreateRequest user = new UserRecord.CreateRequest(); try { user.setDisplayName(null); fail("No error thrown for null display name"); @@ -937,7 +931,7 @@ public void testInvalidDisplayName() { @Test public void testInvalidPhotoUrl() { - CreateRequest user = new CreateRequest(); + UserRecord.CreateRequest user = new UserRecord.CreateRequest(); try { user.setPhotoUrl(null); fail("No error thrown for null photo url"); @@ -962,7 +956,7 @@ public void testInvalidPhotoUrl() { @Test public void testInvalidEmail() { - CreateRequest user = new CreateRequest(); + UserRecord.CreateRequest user = new UserRecord.CreateRequest(); try { user.setEmail(null); fail("No error thrown for null email"); @@ -987,7 +981,7 @@ public void testInvalidEmail() { @Test public void testInvalidPhoneNumber() { - CreateRequest user = new CreateRequest(); + UserRecord.CreateRequest user = new UserRecord.CreateRequest(); try { user.setPhoneNumber(null); fail("No error thrown for null phone number"); @@ -1012,7 +1006,7 @@ public void testInvalidPhoneNumber() { @Test public void testInvalidPassword() { - CreateRequest user = new CreateRequest(); + UserRecord.CreateRequest user = new UserRecord.CreateRequest(); try { user.setPassword(null); fail("No error thrown for null password"); @@ -1030,7 +1024,7 @@ public void testInvalidPassword() { @Test public void testUserUpdater() throws IOException { - UpdateRequest update = new UpdateRequest("test"); + UserRecord.UpdateRequest update = new UserRecord.UpdateRequest("test"); Map claims = ImmutableMap.of("admin", true, "package", "gold"); Map map = update .setDisplayName("Display Name") @@ -1040,7 +1034,7 @@ public void testUserUpdater() throws IOException { .setEmailVerified(true) .setPassword("secret") .setCustomClaims(claims) - .getProperties(Utils.getDefaultJsonFactory()); + .getProperties(JSON_FACTORY); assertEquals(8, map.size()); assertEquals(update.getUid(), map.get("localId")); assertEquals("Display Name", map.get("displayName")); @@ -1049,12 +1043,12 @@ public void testUserUpdater() throws IOException { assertEquals("+1234567890", map.get("phoneNumber")); assertTrue((Boolean) map.get("emailVerified")); assertEquals("secret", map.get("password")); - assertEquals(Utils.getDefaultJsonFactory().toString(claims), map.get("customAttributes")); + assertEquals(JSON_FACTORY.toString(claims), map.get("customAttributes")); } @Test public void testNullJsonFactory() { - UpdateRequest update = new UpdateRequest("test"); + UserRecord.UpdateRequest update = new UserRecord.UpdateRequest("test"); Map claims = ImmutableMap.of("admin", true, "package", "gold"); update.setCustomClaims(claims); try { @@ -1067,10 +1061,10 @@ public void testNullJsonFactory() { @Test public void testNullCustomClaims() { - UpdateRequest update = new UpdateRequest("test"); + UserRecord.UpdateRequest update = new UserRecord.UpdateRequest("test"); Map map = update .setCustomClaims(null) - .getProperties(Utils.getDefaultJsonFactory()); + .getProperties(JSON_FACTORY); assertEquals(2, map.size()); assertEquals(update.getUid(), map.get("localId")); assertEquals("{}", map.get("customAttributes")); @@ -1078,10 +1072,10 @@ public void testNullCustomClaims() { @Test public void testEmptyCustomClaims() { - UpdateRequest update = new UpdateRequest("test"); + UserRecord.UpdateRequest update = new UserRecord.UpdateRequest("test"); Map map = update .setCustomClaims(ImmutableMap.of()) - .getProperties(Utils.getDefaultJsonFactory()); + .getProperties(JSON_FACTORY); assertEquals(2, map.size()); assertEquals(update.getUid(), map.get("localId")); assertEquals("{}", map.get("customAttributes")); @@ -1089,31 +1083,31 @@ public void testEmptyCustomClaims() { @Test public void testDeleteDisplayName() { - Map map = new UpdateRequest("test") + Map map = new UserRecord.UpdateRequest("test") .setDisplayName(null) - .getProperties(Utils.getDefaultJsonFactory()); + .getProperties(JSON_FACTORY); assertEquals(ImmutableList.of("DISPLAY_NAME"), map.get("deleteAttribute")); } @Test public void testDeletePhotoUrl() { - Map map = new UpdateRequest("test") + Map map = new UserRecord.UpdateRequest("test") .setPhotoUrl(null) - .getProperties(Utils.getDefaultJsonFactory()); + .getProperties(JSON_FACTORY); assertEquals(ImmutableList.of("PHOTO_URL"), map.get("deleteAttribute")); } @Test public void testDeletePhoneNumber() { - Map map = new UpdateRequest("test") + Map map = new UserRecord.UpdateRequest("test") .setPhoneNumber(null) - .getProperties(Utils.getDefaultJsonFactory()); + .getProperties(JSON_FACTORY); assertEquals(ImmutableList.of("phone"), map.get("deleteProvider")); } @Test public void testInvalidUpdatePhotoUrl() { - UpdateRequest update = new UpdateRequest("test"); + UserRecord.UpdateRequest update = new UserRecord.UpdateRequest("test"); try { update.setPhotoUrl(""); fail("No error thrown for invalid photo url"); @@ -1131,7 +1125,7 @@ public void testInvalidUpdatePhotoUrl() { @Test public void testInvalidUpdateEmail() { - UpdateRequest update = new UpdateRequest("test"); + UserRecord.UpdateRequest update = new UserRecord.UpdateRequest("test"); try { update.setEmail(null); fail("No error thrown for null email"); @@ -1156,7 +1150,7 @@ public void testInvalidUpdateEmail() { @Test public void testInvalidUpdatePhoneNumber() { - UpdateRequest update = new UpdateRequest("test"); + UserRecord.UpdateRequest update = new UserRecord.UpdateRequest("test"); try { update.setPhoneNumber(""); @@ -1175,7 +1169,7 @@ public void testInvalidUpdatePhoneNumber() { @Test public void testInvalidUpdatePassword() { - UpdateRequest update = new UpdateRequest("test"); + UserRecord.UpdateRequest update = new UserRecord.UpdateRequest("test"); try { update.setPassword(null); fail("No error thrown for null password"); @@ -1193,7 +1187,7 @@ public void testInvalidUpdatePassword() { @Test public void testInvalidCustomClaims() { - UpdateRequest update = new UpdateRequest("test"); + UserRecord.UpdateRequest update = new UserRecord.UpdateRequest("test"); for (String claim : FirebaseUserManager.RESERVED_CLAIMS) { try { update.setCustomClaims(ImmutableMap.of(claim, "value")); @@ -1210,10 +1204,10 @@ public void testLargeCustomClaims() { for (int i = 0; i < 1001; i++) { builder.append("a"); } - UpdateRequest update = new UpdateRequest("test"); + UserRecord.UpdateRequest update = new UserRecord.UpdateRequest("test"); update.setCustomClaims(ImmutableMap.of("key", builder.toString())); try { - update.getProperties(Utils.getDefaultJsonFactory()); + update.getProperties(JSON_FACTORY); fail("No error thrown for large claims payload"); } catch (Exception ignore) { // expected @@ -1245,10 +1239,7 @@ public void testGeneratePasswordResetLinkWithSettings() throws Exception { assertEquals("https://mock-oob-link.for.auth.tests", link); checkRequestHeaders(interceptor); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - interceptor.getResponse().getRequest().getContent().writeTo(out); - JsonFactory jsonFactory = Utils.getDefaultJsonFactory(); - GenericJson parsed = jsonFactory.fromString(new String(out.toByteArray()), GenericJson.class); + GenericJson parsed = parseRequestContent(interceptor); assertEquals(3 + ACTION_CODE_SETTINGS_MAP.size(), parsed.size()); assertEquals("test@example.com", parsed.get("email")); assertEquals("PASSWORD_RESET", parsed.get("requestType")); @@ -1267,10 +1258,7 @@ public void testGeneratePasswordResetLink() throws Exception { assertEquals("https://mock-oob-link.for.auth.tests", link); checkRequestHeaders(interceptor); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - interceptor.getResponse().getRequest().getContent().writeTo(out); - JsonFactory jsonFactory = Utils.getDefaultJsonFactory(); - GenericJson parsed = jsonFactory.fromString(new String(out.toByteArray()), GenericJson.class); + GenericJson parsed = parseRequestContent(interceptor); assertEquals(3, parsed.size()); assertEquals("test@example.com", parsed.get("email")); assertEquals("PASSWORD_RESET", parsed.get("requestType")); @@ -1302,10 +1290,7 @@ public void testGenerateEmailVerificationLinkWithSettings() throws Exception { assertEquals("https://mock-oob-link.for.auth.tests", link); checkRequestHeaders(interceptor); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - interceptor.getResponse().getRequest().getContent().writeTo(out); - JsonFactory jsonFactory = Utils.getDefaultJsonFactory(); - GenericJson parsed = jsonFactory.fromString(new String(out.toByteArray()), GenericJson.class); + GenericJson parsed = parseRequestContent(interceptor); assertEquals(3 + ACTION_CODE_SETTINGS_MAP.size(), parsed.size()); assertEquals("test@example.com", parsed.get("email")); assertEquals("VERIFY_EMAIL", parsed.get("requestType")); @@ -1324,10 +1309,7 @@ public void testGenerateEmailVerificationLink() throws Exception { assertEquals("https://mock-oob-link.for.auth.tests", link); checkRequestHeaders(interceptor); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - interceptor.getResponse().getRequest().getContent().writeTo(out); - JsonFactory jsonFactory = Utils.getDefaultJsonFactory(); - GenericJson parsed = jsonFactory.fromString(new String(out.toByteArray()), GenericJson.class); + GenericJson parsed = parseRequestContent(interceptor); assertEquals(3, parsed.size()); assertEquals("test@example.com", parsed.get("email")); assertEquals("VERIFY_EMAIL", parsed.get("requestType")); @@ -1372,10 +1354,7 @@ public void testGenerateSignInWithEmailLinkWithSettings() throws Exception { assertEquals("https://mock-oob-link.for.auth.tests", link); checkRequestHeaders(interceptor); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - interceptor.getResponse().getRequest().getContent().writeTo(out); - JsonFactory jsonFactory = Utils.getDefaultJsonFactory(); - GenericJson parsed = jsonFactory.fromString(new String(out.toByteArray()), GenericJson.class); + GenericJson parsed = parseRequestContent(interceptor); assertEquals(3 + ACTION_CODE_SETTINGS_MAP.size(), parsed.size()); assertEquals("test@example.com", parsed.get("email")); assertEquals("EMAIL_SIGNIN", parsed.get("requestType")); @@ -1397,7 +1376,7 @@ public void testHttpErrorWithCode() { fail("No exception thrown for HTTP error"); } catch (FirebaseAuthException e) { assertEquals("unauthorized-continue-uri", e.getErrorCode()); - assertTrue(e.getCause() instanceof HttpResponseException); + assertThat(e.getCause(), instanceOf(HttpResponseException.class)); } } @@ -1413,11 +1392,1188 @@ public void testUnexpectedHttpError() { fail("No exception thrown for HTTP error"); } catch (FirebaseAuthException e) { assertEquals("internal-error", e.getErrorCode()); - assertTrue(e.getCause() instanceof HttpResponseException); + assertThat(e.getCause(), instanceOf(HttpResponseException.class)); + } + } + + @Test + public void testCreateOidcProvider() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("oidc.json")); + OidcProviderConfig.CreateRequest createRequest = + new OidcProviderConfig.CreateRequest() + .setProviderId("oidc.provider-id") + .setDisplayName("DISPLAY_NAME") + .setEnabled(true) + .setClientId("CLIENT_ID") + .setIssuer("https://oidc.com/issuer"); + + OidcProviderConfig config = FirebaseAuth.getInstance().createOidcProviderConfig(createRequest); + + checkOidcProviderConfig(config, "oidc.provider-id"); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "POST", PROJECT_BASE_URL + "/oauthIdpConfigs"); + GenericJson parsed = parseRequestContent(interceptor); + assertEquals("DISPLAY_NAME", parsed.get("displayName")); + assertTrue((boolean) parsed.get("enabled")); + assertEquals("CLIENT_ID", parsed.get("clientId")); + assertEquals("https://oidc.com/issuer", parsed.get("issuer")); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals("oidc.provider-id", url.getFirst("oauthIdpConfigId")); + } + + @Test + public void testCreateOidcProviderAsync() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("oidc.json")); + OidcProviderConfig.CreateRequest createRequest = + new OidcProviderConfig.CreateRequest() + .setProviderId("oidc.provider-id") + .setDisplayName("DISPLAY_NAME") + .setEnabled(true) + .setClientId("CLIENT_ID") + .setIssuer("https://oidc.com/issuer"); + + OidcProviderConfig config = + FirebaseAuth.getInstance().createOidcProviderConfigAsync(createRequest).get(); + + checkOidcProviderConfig(config, "oidc.provider-id"); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "POST", PROJECT_BASE_URL + "/oauthIdpConfigs"); + GenericJson parsed = parseRequestContent(interceptor); + assertEquals("DISPLAY_NAME", parsed.get("displayName")); + assertTrue((boolean) parsed.get("enabled")); + assertEquals("CLIENT_ID", parsed.get("clientId")); + assertEquals("https://oidc.com/issuer", parsed.get("issuer")); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals("oidc.provider-id", url.getFirst("oauthIdpConfigId")); + } + + @Test + public void testCreateOidcProviderMinimal() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("oidc.json")); + // Only the 'enabled' and 'displayName' fields can be omitted from an OIDC provider config + // creation request. + OidcProviderConfig.CreateRequest createRequest = + new OidcProviderConfig.CreateRequest() + .setProviderId("oidc.provider-id") + .setClientId("CLIENT_ID") + .setIssuer("https://oidc.com/issuer"); + + FirebaseAuth.getInstance().createOidcProviderConfig(createRequest); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "POST", PROJECT_BASE_URL + "/oauthIdpConfigs"); + GenericJson parsed = parseRequestContent(interceptor); + assertNull(parsed.get("displayName")); + assertNull(parsed.get("enabled")); + assertEquals("CLIENT_ID", parsed.get("clientId")); + assertEquals("https://oidc.com/issuer", parsed.get("issuer")); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals("oidc.provider-id", url.getFirst("oauthIdpConfigId")); + } + + @Test + public void testCreateOidcProviderError() throws Exception { + TestResponseInterceptor interceptor = + initializeAppForUserManagementWithStatusCode(404, + "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}"); + OidcProviderConfig.CreateRequest createRequest = + new OidcProviderConfig.CreateRequest().setProviderId("oidc.provider-id"); + try { + FirebaseAuth.getInstance().createOidcProviderConfig(createRequest); + fail("No error thrown for invalid response"); + } catch (FirebaseAuthException e) { + assertEquals(AuthHttpClient.INTERNAL_ERROR, e.getErrorCode()); + } + checkUrl(interceptor, "POST", PROJECT_BASE_URL + "/oauthIdpConfigs"); + } + + @Test + public void testCreateOidcProviderMissingId() throws Exception { + initializeAppForUserManagement(TestUtils.loadResource("oidc.json")); + OidcProviderConfig.CreateRequest createRequest = + new OidcProviderConfig.CreateRequest() + .setDisplayName("DISPLAY_NAME") + .setEnabled(true) + .setClientId("CLIENT_ID") + .setIssuer("https://oidc.com/issuer"); + try { + FirebaseAuth.getInstance().createOidcProviderConfig(createRequest); + fail("No error thrown for invalid response"); + } catch (IllegalArgumentException e) { + // Expected. + } + } + + @Test + public void testTenantAwareCreateOidcProvider() throws Exception { + TestResponseInterceptor interceptor = initializeAppForTenantAwareUserManagement( + "TENANT_ID", + TestUtils.loadResource("oidc.json")); + OidcProviderConfig.CreateRequest createRequest = + new OidcProviderConfig.CreateRequest() + .setProviderId("oidc.provider-id") + .setDisplayName("DISPLAY_NAME") + .setEnabled(true) + .setClientId("CLIENT_ID") + .setIssuer("https://oidc.com/issuer"); + TenantAwareFirebaseAuth tenantAwareAuth = + FirebaseAuth.getInstance().getTenantManager().getAuthForTenant("TENANT_ID"); + + tenantAwareAuth.createOidcProviderConfig(createRequest); + + checkRequestHeaders(interceptor); + checkUrl(interceptor, "POST", TENANTS_BASE_URL + "/TENANT_ID/oauthIdpConfigs"); + } + + @Test + public void testUpdateOidcProvider() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("oidc.json")); + OidcProviderConfig.UpdateRequest request = + new OidcProviderConfig.UpdateRequest("oidc.provider-id") + .setDisplayName("DISPLAY_NAME") + .setEnabled(true) + .setClientId("CLIENT_ID") + .setIssuer("https://oidc.com/issuer"); + + OidcProviderConfig config = FirebaseAuth.getInstance().updateOidcProviderConfig(request); + + checkOidcProviderConfig(config, "oidc.provider-id"); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "PATCH", PROJECT_BASE_URL + "/oauthIdpConfigs/oidc.provider-id"); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals("clientId,displayName,enabled,issuer", url.getFirst("updateMask")); + GenericJson parsed = parseRequestContent(interceptor); + assertEquals("DISPLAY_NAME", parsed.get("displayName")); + assertTrue((boolean) parsed.get("enabled")); + assertEquals("CLIENT_ID", parsed.get("clientId")); + assertEquals("https://oidc.com/issuer", parsed.get("issuer")); + } + + @Test + public void testUpdateOidcProviderAsync() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("oidc.json")); + OidcProviderConfig.UpdateRequest request = + new OidcProviderConfig.UpdateRequest("oidc.provider-id") + .setDisplayName("DISPLAY_NAME") + .setEnabled(true) + .setClientId("CLIENT_ID") + .setIssuer("https://oidc.com/issuer"); + + OidcProviderConfig config = + FirebaseAuth.getInstance().updateOidcProviderConfigAsync(request).get(); + + checkOidcProviderConfig(config, "oidc.provider-id"); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "PATCH", PROJECT_BASE_URL + "/oauthIdpConfigs/oidc.provider-id"); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals("clientId,displayName,enabled,issuer", url.getFirst("updateMask")); + GenericJson parsed = parseRequestContent(interceptor); + assertEquals("DISPLAY_NAME", parsed.get("displayName")); + assertTrue((boolean) parsed.get("enabled")); + assertEquals("CLIENT_ID", parsed.get("clientId")); + assertEquals("https://oidc.com/issuer", parsed.get("issuer")); + } + + @Test + public void testUpdateOidcProviderMinimal() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("oidc.json")); + OidcProviderConfig.UpdateRequest request = + new OidcProviderConfig.UpdateRequest("oidc.provider-id").setDisplayName("DISPLAY_NAME"); + + OidcProviderConfig config = FirebaseAuth.getInstance().updateOidcProviderConfig(request); + + checkOidcProviderConfig(config, "oidc.provider-id"); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "PATCH", PROJECT_BASE_URL + "/oauthIdpConfigs/oidc.provider-id"); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals("displayName", url.getFirst("updateMask")); + GenericJson parsed = parseRequestContent(interceptor); + assertEquals(1, parsed.size()); + assertEquals("DISPLAY_NAME", parsed.get("displayName")); + } + + @Test + public void testUpdateOidcProviderConfigNoValues() throws Exception { + initializeAppForUserManagement(TestUtils.loadResource("oidc.json")); + try { + FirebaseAuth.getInstance().updateOidcProviderConfig( + new OidcProviderConfig.UpdateRequest("oidc.provider-id")); + fail("No error thrown for empty provider config update"); + } catch (IllegalArgumentException e) { + // expected + } + } + + @Test + public void testUpdateOidcProviderConfigError() { + TestResponseInterceptor interceptor = + initializeAppForUserManagementWithStatusCode(404, + "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}"); + OidcProviderConfig.UpdateRequest request = + new OidcProviderConfig.UpdateRequest("oidc.provider-id").setDisplayName("DISPLAY_NAME"); + try { + FirebaseAuth.getInstance().updateOidcProviderConfig(request); + fail("No error thrown for invalid response"); + } catch (FirebaseAuthException e) { + assertEquals(AuthHttpClient.INTERNAL_ERROR, e.getErrorCode()); + } + checkUrl(interceptor, "PATCH", PROJECT_BASE_URL + "/oauthIdpConfigs/oidc.provider-id"); + } + + @Test + public void testTenantAwareUpdateOidcProvider() throws Exception { + TestResponseInterceptor interceptor = initializeAppForTenantAwareUserManagement( + "TENANT_ID", + TestUtils.loadResource("oidc.json")); + TenantAwareFirebaseAuth tenantAwareAuth = + FirebaseAuth.getInstance().getTenantManager().getAuthForTenant("TENANT_ID"); + OidcProviderConfig.UpdateRequest request = + new OidcProviderConfig.UpdateRequest("oidc.provider-id") + .setDisplayName("DISPLAY_NAME") + .setEnabled(true) + .setClientId("CLIENT_ID") + .setIssuer("https://oidc.com/issuer"); + + OidcProviderConfig config = tenantAwareAuth.updateOidcProviderConfig(request); + + checkOidcProviderConfig(config, "oidc.provider-id"); + checkRequestHeaders(interceptor); + String expectedUrl = TENANTS_BASE_URL + "/TENANT_ID/oauthIdpConfigs/oidc.provider-id"; + checkUrl(interceptor, "PATCH", expectedUrl); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals("clientId,displayName,enabled,issuer", url.getFirst("updateMask")); + GenericJson parsed = parseRequestContent(interceptor); + assertEquals("DISPLAY_NAME", parsed.get("displayName")); + assertTrue((boolean) parsed.get("enabled")); + assertEquals("CLIENT_ID", parsed.get("clientId")); + assertEquals("https://oidc.com/issuer", parsed.get("issuer")); + } + + @Test + public void testGetOidcProviderConfig() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("oidc.json")); + + OidcProviderConfig config = + FirebaseAuth.getInstance().getOidcProviderConfig("oidc.provider-id"); + + checkOidcProviderConfig(config, "oidc.provider-id"); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "GET", PROJECT_BASE_URL + "/oauthIdpConfigs/oidc.provider-id"); + } + + @Test + public void testGetOidcProviderConfigAsync() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("oidc.json")); + + OidcProviderConfig config = + FirebaseAuth.getInstance().getOidcProviderConfigAsync("oidc.provider-id").get(); + + checkOidcProviderConfig(config, "oidc.provider-id"); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "GET", PROJECT_BASE_URL + "/oauthIdpConfigs/oidc.provider-id"); + } + + @Test + public void testGetOidcProviderConfigMissingId() throws Exception { + initializeAppForUserManagement(TestUtils.loadResource("oidc.json")); + + try { + FirebaseAuth.getInstance().getOidcProviderConfig(null); + fail("No error thrown for missing provider ID."); + } catch (IllegalArgumentException e) { + // Expected. + } + } + + @Test + public void testGetOidcProviderConfigInvalidId() throws Exception { + initializeAppForUserManagement(TestUtils.loadResource("oidc.json")); + + try { + FirebaseAuth.getInstance().getOidcProviderConfig("saml.invalid-oidc-provider-id"); + fail("No error thrown for invalid provider ID."); + } catch (IllegalArgumentException e) { + // Expected. + } + } + + @Test + public void testGetOidcProviderConfigWithNotFoundError() throws Exception { + TestResponseInterceptor interceptor = + initializeAppForUserManagementWithStatusCode(404, + "{\"error\": {\"message\": \"CONFIGURATION_NOT_FOUND\"}}"); + try { + FirebaseAuth.getInstance().getOidcProviderConfig("oidc.provider-id"); + fail("No error thrown for invalid response"); + } catch (FirebaseAuthException e) { + assertEquals(AuthHttpClient.CONFIGURATION_NOT_FOUND_ERROR, e.getErrorCode()); + } + checkUrl(interceptor, "GET", PROJECT_BASE_URL + "/oauthIdpConfigs/oidc.provider-id"); + } + + @Test + public void testGetTenantAwareOidcProviderConfig() throws Exception { + TestResponseInterceptor interceptor = initializeAppForTenantAwareUserManagement( + "TENANT_ID", + TestUtils.loadResource("oidc.json")); + TenantAwareFirebaseAuth tenantAwareAuth = + FirebaseAuth.getInstance().getTenantManager().getAuthForTenant("TENANT_ID"); + + OidcProviderConfig config = tenantAwareAuth.getOidcProviderConfig("oidc.provider-id"); + + checkOidcProviderConfig(config, "oidc.provider-id"); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "GET", TENANTS_BASE_URL + "/TENANT_ID/oauthIdpConfigs/oidc.provider-id"); + } + + @Test + public void testListOidcProviderConfigs() throws Exception { + final TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("listOidc.json")); + ListProviderConfigsPage page = + FirebaseAuth.getInstance().listOidcProviderConfigs(null, 99); + + ImmutableList providerConfigs = ImmutableList.copyOf(page.getValues()); + assertEquals(2, providerConfigs.size()); + checkOidcProviderConfig(providerConfigs.get(0), "oidc.provider-id1"); + checkOidcProviderConfig(providerConfigs.get(1), "oidc.provider-id2"); + assertEquals("", page.getNextPageToken()); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "GET", PROJECT_BASE_URL + "/oauthIdpConfigs"); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals(99, url.getFirst("pageSize")); + assertNull(url.getFirst("nextPageToken")); + } + + @Test + public void testListOidcProviderConfigsAsync() throws Exception { + final TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("listOidc.json")); + ListProviderConfigsPage page = + FirebaseAuth.getInstance().listOidcProviderConfigsAsync(null, 99).get(); + + ImmutableList providerConfigs = ImmutableList.copyOf(page.getValues()); + assertEquals(2, providerConfigs.size()); + checkOidcProviderConfig(providerConfigs.get(0), "oidc.provider-id1"); + checkOidcProviderConfig(providerConfigs.get(1), "oidc.provider-id2"); + assertEquals("", page.getNextPageToken()); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "GET", PROJECT_BASE_URL + "/oauthIdpConfigs"); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals(99, url.getFirst("pageSize")); + assertNull(url.getFirst("nextPageToken")); + } + + @Test + public void testListOidcProviderConfigsError() throws Exception { + TestResponseInterceptor interceptor = + initializeAppForUserManagementWithStatusCode(404, + "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}"); + + try { + FirebaseAuth.getInstance().listOidcProviderConfigs(null, 99); + fail("No error thrown for invalid response"); + } catch (FirebaseAuthException e) { + assertEquals(AuthHttpClient.INTERNAL_ERROR, e.getErrorCode()); + } + checkUrl(interceptor, "GET", PROJECT_BASE_URL + "/oauthIdpConfigs"); + } + + @Test + public void testListOidcProviderConfigsWithPageToken() throws Exception { + final TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("listOidc.json")); + ListProviderConfigsPage page = + FirebaseAuth.getInstance().listOidcProviderConfigs("token", 99); + + ImmutableList providerConfigs = ImmutableList.copyOf(page.getValues()); + assertEquals(2, providerConfigs.size()); + checkOidcProviderConfig(providerConfigs.get(0), "oidc.provider-id1"); + checkOidcProviderConfig(providerConfigs.get(1), "oidc.provider-id2"); + assertEquals("", page.getNextPageToken()); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "GET", PROJECT_BASE_URL + "/oauthIdpConfigs"); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals(99, url.getFirst("pageSize")); + assertEquals("token", url.getFirst("nextPageToken")); + } + + @Test + public void testListZeroOidcProviderConfigs() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement("{}"); + ListProviderConfigsPage page = + FirebaseAuth.getInstance().listOidcProviderConfigs(null); + assertTrue(Iterables.isEmpty(page.getValues())); + assertEquals("", page.getNextPageToken()); + checkRequestHeaders(interceptor); + } + + @Test + public void testTenantAwareListOidcProviderConfigs() throws Exception { + final TestResponseInterceptor interceptor = initializeAppForTenantAwareUserManagement( + "TENANT_ID", + TestUtils.loadResource("listOidc.json")); + TenantAwareFirebaseAuth tenantAwareAuth = + FirebaseAuth.getInstance().getTenantManager().getAuthForTenant("TENANT_ID"); + ListProviderConfigsPage page = + tenantAwareAuth.listOidcProviderConfigs(null, 99); + + ImmutableList providerConfigs = ImmutableList.copyOf(page.getValues()); + assertEquals(2, providerConfigs.size()); + checkOidcProviderConfig(providerConfigs.get(0), "oidc.provider-id1"); + checkOidcProviderConfig(providerConfigs.get(1), "oidc.provider-id2"); + assertEquals("", page.getNextPageToken()); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "GET", TENANTS_BASE_URL + "/TENANT_ID/oauthIdpConfigs"); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals(99, url.getFirst("pageSize")); + assertNull(url.getFirst("nextPageToken")); + } + + @Test + public void testDeleteOidcProviderConfig() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement("{}"); + + FirebaseAuth.getInstance().deleteOidcProviderConfig("oidc.provider-id"); + + checkRequestHeaders(interceptor); + checkUrl(interceptor, "DELETE", PROJECT_BASE_URL + "/oauthIdpConfigs/oidc.provider-id"); + } + + @Test + public void testDeleteOidcProviderConfigAsync() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement("{}"); + + FirebaseAuth.getInstance().deleteOidcProviderConfigAsync("oidc.provider-id").get(); + + checkRequestHeaders(interceptor); + checkUrl(interceptor, "DELETE", PROJECT_BASE_URL + "/oauthIdpConfigs/oidc.provider-id"); + } + + @Test + public void testDeleteOidcProviderMissingId() throws Exception { + initializeAppForUserManagement("{}"); + + try { + FirebaseAuth.getInstance().deleteOidcProviderConfig(null); + fail("No error thrown for missing provider ID."); + } catch (IllegalArgumentException e) { + // Expected. + } + } + + @Test + public void testDeleteOidcProviderInvalidId() throws Exception { + initializeAppForUserManagement("{}"); + + try { + FirebaseAuth.getInstance().deleteOidcProviderConfig("saml.invalid-oidc-provider-id"); + fail("No error thrown for invalid provider ID."); + } catch (IllegalArgumentException e) { + // Expected. + } + } + + @Test + public void testDeleteOidcProviderConfigWithNotFoundError() { + TestResponseInterceptor interceptor = + initializeAppForUserManagementWithStatusCode(404, + "{\"error\": {\"message\": \"CONFIGURATION_NOT_FOUND\"}}"); + try { + FirebaseAuth.getInstance().deleteOidcProviderConfig("oidc.UNKNOWN"); + fail("No error thrown for invalid response"); + } catch (FirebaseAuthException e) { + assertEquals(AuthHttpClient.CONFIGURATION_NOT_FOUND_ERROR, e.getErrorCode()); } + checkUrl(interceptor, "DELETE", PROJECT_BASE_URL + "/oauthIdpConfigs/oidc.UNKNOWN"); } - private static TestResponseInterceptor initializeAppForUserManagement(String ...responses) { + @Test + public void testTenantAwareDeleteOidcProviderConfig() throws Exception { + TestResponseInterceptor interceptor = initializeAppForTenantAwareUserManagement( + "TENANT_ID", + "{}"); + TenantAwareFirebaseAuth tenantAwareAuth = + FirebaseAuth.getInstance().getTenantManager().getAuthForTenant("TENANT_ID"); + + tenantAwareAuth.deleteOidcProviderConfig("oidc.provider-id"); + + checkRequestHeaders(interceptor); + String expectedUrl = TENANTS_BASE_URL + "/TENANT_ID/oauthIdpConfigs/oidc.provider-id"; + checkUrl(interceptor, "DELETE", expectedUrl); + } + + @Test + public void testCreateSamlProvider() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("saml.json")); + SamlProviderConfig.CreateRequest createRequest = + new SamlProviderConfig.CreateRequest() + .setProviderId("saml.provider-id") + .setDisplayName("DISPLAY_NAME") + .setEnabled(true) + .setIdpEntityId("IDP_ENTITY_ID") + .setSsoUrl("https://example.com/login") + .addX509Certificate("certificate1") + .addX509Certificate("certificate2") + .setRpEntityId("RP_ENTITY_ID") + .setCallbackUrl("https://projectId.firebaseapp.com/__/auth/handler"); + + SamlProviderConfig config = FirebaseAuth.getInstance().createSamlProviderConfig(createRequest); + + checkSamlProviderConfig(config, "saml.provider-id"); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "POST", PROJECT_BASE_URL + "/inboundSamlConfigs"); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals("saml.provider-id", url.getFirst("inboundSamlConfigId")); + + GenericJson parsed = parseRequestContent(interceptor); + assertEquals("DISPLAY_NAME", parsed.get("displayName")); + assertTrue((boolean) parsed.get("enabled")); + + Map idpConfig = (Map) parsed.get("idpConfig"); + assertNotNull(idpConfig); + assertEquals(3, idpConfig.size()); + assertEquals("IDP_ENTITY_ID", idpConfig.get("idpEntityId")); + assertEquals("https://example.com/login", idpConfig.get("ssoUrl")); + List idpCertificates = (List) idpConfig.get("idpCertificates"); + assertNotNull(idpCertificates); + assertEquals(2, idpCertificates.size()); + assertEquals(ImmutableMap.of("x509Certificate", "certificate1"), idpCertificates.get(0)); + assertEquals(ImmutableMap.of("x509Certificate", "certificate2"), idpCertificates.get(1)); + + Map spConfig = (Map) parsed.get("spConfig"); + assertNotNull(spConfig); + assertEquals(2, spConfig.size()); + assertEquals("RP_ENTITY_ID", spConfig.get("spEntityId")); + assertEquals("https://projectId.firebaseapp.com/__/auth/handler", spConfig.get("callbackUri")); + } + + @Test + public void testCreateSamlProviderAsync() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("saml.json")); + SamlProviderConfig.CreateRequest createRequest = + new SamlProviderConfig.CreateRequest() + .setProviderId("saml.provider-id") + .setDisplayName("DISPLAY_NAME") + .setEnabled(true) + .setIdpEntityId("IDP_ENTITY_ID") + .setSsoUrl("https://example.com/login") + .addX509Certificate("certificate1") + .addX509Certificate("certificate2") + .setRpEntityId("RP_ENTITY_ID") + .setCallbackUrl("https://projectId.firebaseapp.com/__/auth/handler"); + + SamlProviderConfig config = + FirebaseAuth.getInstance().createSamlProviderConfigAsync(createRequest).get(); + + checkSamlProviderConfig(config, "saml.provider-id"); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "POST", PROJECT_BASE_URL + "/inboundSamlConfigs"); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals("saml.provider-id", url.getFirst("inboundSamlConfigId")); + + GenericJson parsed = parseRequestContent(interceptor); + assertEquals("DISPLAY_NAME", parsed.get("displayName")); + assertTrue((boolean) parsed.get("enabled")); + + Map idpConfig = (Map) parsed.get("idpConfig"); + assertNotNull(idpConfig); + assertEquals(3, idpConfig.size()); + assertEquals("IDP_ENTITY_ID", idpConfig.get("idpEntityId")); + assertEquals("https://example.com/login", idpConfig.get("ssoUrl")); + List idpCertificates = (List) idpConfig.get("idpCertificates"); + assertNotNull(idpCertificates); + assertEquals(2, idpCertificates.size()); + assertEquals(ImmutableMap.of("x509Certificate", "certificate1"), idpCertificates.get(0)); + assertEquals(ImmutableMap.of("x509Certificate", "certificate2"), idpCertificates.get(1)); + + Map spConfig = (Map) parsed.get("spConfig"); + assertNotNull(spConfig); + assertEquals(2, spConfig.size()); + assertEquals("RP_ENTITY_ID", spConfig.get("spEntityId")); + assertEquals("https://projectId.firebaseapp.com/__/auth/handler", spConfig.get("callbackUri")); + } + + @Test + public void testCreateSamlProviderMinimal() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("saml.json")); + // Only the 'enabled', 'displayName', and 'signRequest' fields can be omitted from a SAML + // provider config creation request. + SamlProviderConfig.CreateRequest createRequest = + new SamlProviderConfig.CreateRequest() + .setProviderId("saml.provider-id") + .setIdpEntityId("IDP_ENTITY_ID") + .setSsoUrl("https://example.com/login") + .addX509Certificate("certificate") + .setRpEntityId("RP_ENTITY_ID") + .setCallbackUrl("https://projectId.firebaseapp.com/__/auth/handler"); + + FirebaseAuth.getInstance().createSamlProviderConfig(createRequest); + + checkRequestHeaders(interceptor); + checkUrl(interceptor, "POST", PROJECT_BASE_URL + "/inboundSamlConfigs"); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals("saml.provider-id", url.getFirst("inboundSamlConfigId")); + + GenericJson parsed = parseRequestContent(interceptor); + assertNull(parsed.get("displayName")); + assertNull(parsed.get("enabled")); + Map idpConfig = (Map) parsed.get("idpConfig"); + assertNotNull(idpConfig); + assertEquals(3, idpConfig.size()); + assertEquals("IDP_ENTITY_ID", idpConfig.get("idpEntityId")); + assertEquals("https://example.com/login", idpConfig.get("ssoUrl")); + List idpCertificates = (List) idpConfig.get("idpCertificates"); + assertNotNull(idpCertificates); + assertEquals(1, idpCertificates.size()); + assertEquals(ImmutableMap.of("x509Certificate", "certificate"), idpCertificates.get(0)); + Map spConfig = (Map) parsed.get("spConfig"); + assertNotNull(spConfig); + assertEquals(2, spConfig.size()); + assertEquals("RP_ENTITY_ID", spConfig.get("spEntityId")); + assertEquals("https://projectId.firebaseapp.com/__/auth/handler", spConfig.get("callbackUri")); + } + + @Test + public void testCreateSamlProviderError() { + TestResponseInterceptor interceptor = + initializeAppForUserManagementWithStatusCode(404, + "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}"); + SamlProviderConfig.CreateRequest createRequest = + new SamlProviderConfig.CreateRequest().setProviderId("saml.provider-id"); + try { + FirebaseAuth.getInstance().createSamlProviderConfig(createRequest); + fail("No error thrown for invalid response"); + } catch (FirebaseAuthException e) { + assertEquals(AuthHttpClient.INTERNAL_ERROR, e.getErrorCode()); + } + checkUrl(interceptor, "POST", PROJECT_BASE_URL + "/inboundSamlConfigs"); + } + + @Test + public void testCreateSamlProviderMissingId() throws Exception { + initializeAppForUserManagement(TestUtils.loadResource("saml.json")); + SamlProviderConfig.CreateRequest createRequest = + new SamlProviderConfig.CreateRequest() + .setDisplayName("DISPLAY_NAME") + .setEnabled(true) + .setIdpEntityId("IDP_ENTITY_ID") + .setSsoUrl("https://example.com/login") + .addX509Certificate("certificate1") + .addX509Certificate("certificate2") + .setRpEntityId("RP_ENTITY_ID") + .setCallbackUrl("https://projectId.firebaseapp.com/__/auth/handler"); + try { + FirebaseAuth.getInstance().createSamlProviderConfig(createRequest); + fail("No error thrown for invalid response"); + } catch (IllegalArgumentException e) { + // Expected. + } + } + + @Test + public void testTenantAwareCreateSamlProvider() throws Exception { + TestResponseInterceptor interceptor = initializeAppForTenantAwareUserManagement( + "TENANT_ID", + TestUtils.loadResource("saml.json")); + SamlProviderConfig.CreateRequest createRequest = + new SamlProviderConfig.CreateRequest() + .setProviderId("saml.provider-id") + .setDisplayName("DISPLAY_NAME") + .setEnabled(true) + .setIdpEntityId("IDP_ENTITY_ID") + .setSsoUrl("https://example.com/login") + .addX509Certificate("certificate1") + .addX509Certificate("certificate2") + .setRpEntityId("RP_ENTITY_ID") + .setCallbackUrl("https://projectId.firebaseapp.com/__/auth/handler"); + TenantAwareFirebaseAuth tenantAwareAuth = + FirebaseAuth.getInstance().getTenantManager().getAuthForTenant("TENANT_ID"); + + tenantAwareAuth.createSamlProviderConfig(createRequest); + + checkRequestHeaders(interceptor); + checkUrl(interceptor, "POST", TENANTS_BASE_URL + "/TENANT_ID/inboundSamlConfigs"); + } + + @Test + public void testUpdateSamlProvider() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("saml.json")); + SamlProviderConfig.UpdateRequest updateRequest = + new SamlProviderConfig.UpdateRequest("saml.provider-id") + .setDisplayName("DISPLAY_NAME") + .setEnabled(true) + .setIdpEntityId("IDP_ENTITY_ID") + .setSsoUrl("https://example.com/login") + .addX509Certificate("certificate1") + .addX509Certificate("certificate2") + .setRpEntityId("RP_ENTITY_ID") + .setCallbackUrl("https://projectId.firebaseapp.com/__/auth/handler"); + + SamlProviderConfig config = FirebaseAuth.getInstance().updateSamlProviderConfig(updateRequest); + + checkSamlProviderConfig(config, "saml.provider-id"); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "PATCH", PROJECT_BASE_URL + "/inboundSamlConfigs/saml.provider-id"); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals( + "displayName,enabled,idpConfig.idpCertificates,idpConfig.idpEntityId,idpConfig.ssoUrl," + + "spConfig.callbackUri,spConfig.spEntityId", + url.getFirst("updateMask")); + + GenericJson parsed = parseRequestContent(interceptor); + assertEquals("DISPLAY_NAME", parsed.get("displayName")); + assertTrue((boolean) parsed.get("enabled")); + + Map idpConfig = (Map) parsed.get("idpConfig"); + assertNotNull(idpConfig); + assertEquals(3, idpConfig.size()); + assertEquals("IDP_ENTITY_ID", idpConfig.get("idpEntityId")); + assertEquals("https://example.com/login", idpConfig.get("ssoUrl")); + List idpCertificates = (List) idpConfig.get("idpCertificates"); + assertNotNull(idpCertificates); + assertEquals(2, idpCertificates.size()); + assertEquals(ImmutableMap.of("x509Certificate", "certificate1"), idpCertificates.get(0)); + assertEquals(ImmutableMap.of("x509Certificate", "certificate2"), idpCertificates.get(1)); + + Map spConfig = (Map) parsed.get("spConfig"); + assertNotNull(spConfig); + assertEquals(2, spConfig.size()); + assertEquals("RP_ENTITY_ID", spConfig.get("spEntityId")); + assertEquals("https://projectId.firebaseapp.com/__/auth/handler", spConfig.get("callbackUri")); + } + + @Test + public void testUpdateSamlProviderAsync() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("saml.json")); + SamlProviderConfig.UpdateRequest updateRequest = + new SamlProviderConfig.UpdateRequest("saml.provider-id") + .setDisplayName("DISPLAY_NAME") + .setEnabled(true) + .setIdpEntityId("IDP_ENTITY_ID") + .setSsoUrl("https://example.com/login") + .addX509Certificate("certificate1") + .addX509Certificate("certificate2") + .setRpEntityId("RP_ENTITY_ID") + .setCallbackUrl("https://projectId.firebaseapp.com/__/auth/handler"); + + SamlProviderConfig config = + FirebaseAuth.getInstance().updateSamlProviderConfigAsync(updateRequest).get(); + + checkSamlProviderConfig(config, "saml.provider-id"); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "PATCH", PROJECT_BASE_URL + "/inboundSamlConfigs/saml.provider-id"); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals( + "displayName,enabled,idpConfig.idpCertificates,idpConfig.idpEntityId,idpConfig.ssoUrl," + + "spConfig.callbackUri,spConfig.spEntityId", + url.getFirst("updateMask")); + + GenericJson parsed = parseRequestContent(interceptor); + assertEquals("DISPLAY_NAME", parsed.get("displayName")); + assertTrue((boolean) parsed.get("enabled")); + + Map idpConfig = (Map) parsed.get("idpConfig"); + assertNotNull(idpConfig); + assertEquals(3, idpConfig.size()); + assertEquals("IDP_ENTITY_ID", idpConfig.get("idpEntityId")); + assertEquals("https://example.com/login", idpConfig.get("ssoUrl")); + List idpCertificates = (List) idpConfig.get("idpCertificates"); + assertNotNull(idpCertificates); + assertEquals(2, idpCertificates.size()); + assertEquals(ImmutableMap.of("x509Certificate", "certificate1"), idpCertificates.get(0)); + assertEquals(ImmutableMap.of("x509Certificate", "certificate2"), idpCertificates.get(1)); + + Map spConfig = (Map) parsed.get("spConfig"); + assertNotNull(spConfig); + assertEquals(2, spConfig.size()); + assertEquals("RP_ENTITY_ID", spConfig.get("spEntityId")); + assertEquals("https://projectId.firebaseapp.com/__/auth/handler", spConfig.get("callbackUri")); + } + + @Test + public void testUpdateSamlProviderMinimal() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("saml.json")); + SamlProviderConfig.UpdateRequest request = + new SamlProviderConfig.UpdateRequest("saml.provider-id").setDisplayName("DISPLAY_NAME"); + + SamlProviderConfig config = FirebaseAuth.getInstance().updateSamlProviderConfig(request); + + checkSamlProviderConfig(config, "saml.provider-id"); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "PATCH", PROJECT_BASE_URL + "/inboundSamlConfigs/saml.provider-id"); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals("displayName", url.getFirst("updateMask")); + GenericJson parsed = parseRequestContent(interceptor); + assertEquals(1, parsed.size()); + assertEquals("DISPLAY_NAME", parsed.get("displayName")); + } + + @Test + public void testUpdateSamlProviderConfigNoValues() throws Exception { + initializeAppForUserManagement(TestUtils.loadResource("saml.json")); + try { + FirebaseAuth.getInstance().updateSamlProviderConfig( + new SamlProviderConfig.UpdateRequest("saml.provider-id")); + fail("No error thrown for empty provider config update"); + } catch (IllegalArgumentException e) { + // expected + } + } + + @Test + public void testUpdateSamlProviderConfigError() throws Exception { + TestResponseInterceptor interceptor = + initializeAppForUserManagementWithStatusCode(404, + "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}"); + SamlProviderConfig.UpdateRequest request = + new SamlProviderConfig.UpdateRequest("saml.provider-id").setDisplayName("DISPLAY_NAME"); + try { + FirebaseAuth.getInstance().updateSamlProviderConfig(request); + fail("No error thrown for invalid response"); + } catch (FirebaseAuthException e) { + assertEquals(AuthHttpClient.INTERNAL_ERROR, e.getErrorCode()); + } + checkUrl(interceptor, "PATCH", PROJECT_BASE_URL + "/inboundSamlConfigs/saml.provider-id"); + } + + @Test + public void testTenantAwareUpdateSamlProvider() throws Exception { + TestResponseInterceptor interceptor = initializeAppForTenantAwareUserManagement( + "TENANT_ID", + TestUtils.loadResource("saml.json")); + TenantAwareFirebaseAuth tenantAwareAuth = + FirebaseAuth.getInstance().getTenantManager().getAuthForTenant("TENANT_ID"); + SamlProviderConfig.UpdateRequest updateRequest = + new SamlProviderConfig.UpdateRequest("saml.provider-id") + .setDisplayName("DISPLAY_NAME") + .setEnabled(true) + .setIdpEntityId("IDP_ENTITY_ID") + .setSsoUrl("https://example.com/login"); + + SamlProviderConfig config = tenantAwareAuth.updateSamlProviderConfig(updateRequest); + + checkSamlProviderConfig(config, "saml.provider-id"); + checkRequestHeaders(interceptor); + String expectedUrl = TENANTS_BASE_URL + "/TENANT_ID/inboundSamlConfigs/saml.provider-id"; + checkUrl(interceptor, "PATCH", expectedUrl); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals("displayName,enabled,idpConfig.idpEntityId,idpConfig.ssoUrl", + url.getFirst("updateMask")); + + GenericJson parsed = parseRequestContent(interceptor); + assertEquals("DISPLAY_NAME", parsed.get("displayName")); + assertTrue((boolean) parsed.get("enabled")); + Map idpConfig = (Map) parsed.get("idpConfig"); + assertNotNull(idpConfig); + assertEquals(2, idpConfig.size()); + assertEquals("IDP_ENTITY_ID", idpConfig.get("idpEntityId")); + assertEquals("https://example.com/login", idpConfig.get("ssoUrl")); + } + + @Test + public void testGetSamlProviderConfig() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("saml.json")); + + SamlProviderConfig config = + FirebaseAuth.getInstance().getSamlProviderConfig("saml.provider-id"); + + checkSamlProviderConfig(config, "saml.provider-id"); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "GET", PROJECT_BASE_URL + "/inboundSamlConfigs/saml.provider-id"); + } + + @Test + public void testGetSamlProviderConfigAsync() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("saml.json")); + + SamlProviderConfig config = + FirebaseAuth.getInstance().getSamlProviderConfigAsync("saml.provider-id").get(); + + checkSamlProviderConfig(config, "saml.provider-id"); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "GET", PROJECT_BASE_URL + "/inboundSamlConfigs/saml.provider-id"); + } + + @Test + public void testGetSamlProviderConfigMissingId() throws Exception { + initializeAppForUserManagement(TestUtils.loadResource("saml.json")); + + try { + FirebaseAuth.getInstance().getSamlProviderConfig(null); + fail("No error thrown for missing provider ID."); + } catch (IllegalArgumentException e) { + // Expected. + } + } + + @Test + public void testGetSamlProviderConfigInvalidId() throws Exception { + initializeAppForUserManagement(TestUtils.loadResource("saml.json")); + + try { + FirebaseAuth.getInstance().getSamlProviderConfig("oidc.invalid-saml-provider-id"); + fail("No error thrown for invalid provider ID."); + } catch (IllegalArgumentException e) { + // Expected. + } + } + + @Test + public void testGetSamlProviderConfigWithNotFoundError() { + TestResponseInterceptor interceptor = + initializeAppForUserManagementWithStatusCode(404, + "{\"error\": {\"message\": \"CONFIGURATION_NOT_FOUND\"}}"); + try { + FirebaseAuth.getInstance().getSamlProviderConfig("saml.provider-id"); + fail("No error thrown for invalid response"); + } catch (FirebaseAuthException e) { + assertEquals(AuthHttpClient.CONFIGURATION_NOT_FOUND_ERROR, e.getErrorCode()); + } + checkUrl(interceptor, "GET", PROJECT_BASE_URL + "/inboundSamlConfigs/saml.provider-id"); + } + + @Test + public void testGetTenantAwareSamlProviderConfig() throws Exception { + TestResponseInterceptor interceptor = initializeAppForTenantAwareUserManagement( + "TENANT_ID", + TestUtils.loadResource("saml.json")); + TenantAwareFirebaseAuth tenantAwareAuth = + FirebaseAuth.getInstance().getTenantManager().getAuthForTenant("TENANT_ID"); + + SamlProviderConfig config = tenantAwareAuth.getSamlProviderConfig("saml.provider-id"); + + checkSamlProviderConfig(config, "saml.provider-id"); + checkRequestHeaders(interceptor); + String expectedUrl = TENANTS_BASE_URL + "/TENANT_ID/inboundSamlConfigs/saml.provider-id"; + checkUrl(interceptor, "GET", expectedUrl); + } + + @Test + public void testListSamlProviderConfigs() throws Exception { + final TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("listSaml.json")); + ListProviderConfigsPage page = + FirebaseAuth.getInstance().listSamlProviderConfigs(null, 99); + + ImmutableList providerConfigs = ImmutableList.copyOf(page.getValues()); + assertEquals(2, providerConfigs.size()); + checkSamlProviderConfig(providerConfigs.get(0), "saml.provider-id1"); + checkSamlProviderConfig(providerConfigs.get(1), "saml.provider-id2"); + assertEquals("", page.getNextPageToken()); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "GET", PROJECT_BASE_URL + "/inboundSamlConfigs"); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals(99, url.getFirst("pageSize")); + assertNull(url.getFirst("nextPageToken")); + } + + @Test + public void testListSamlProviderConfigsAsync() throws Exception { + final TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("listSaml.json")); + ListProviderConfigsPage page = + FirebaseAuth.getInstance().listSamlProviderConfigsAsync(null, 99).get(); + + ImmutableList providerConfigs = ImmutableList.copyOf(page.getValues()); + assertEquals(2, providerConfigs.size()); + checkSamlProviderConfig(providerConfigs.get(0), "saml.provider-id1"); + checkSamlProviderConfig(providerConfigs.get(1), "saml.provider-id2"); + assertEquals("", page.getNextPageToken()); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "GET", PROJECT_BASE_URL + "/inboundSamlConfigs"); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals(99, url.getFirst("pageSize")); + assertNull(url.getFirst("nextPageToken")); + } + + @Test + public void testListSamlProviderConfigsError() throws Exception { + TestResponseInterceptor interceptor = + initializeAppForUserManagementWithStatusCode(404, + "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}"); + + try { + FirebaseAuth.getInstance().listSamlProviderConfigs(null, 99); + fail("No error thrown for invalid response"); + } catch (FirebaseAuthException e) { + assertEquals(AuthHttpClient.INTERNAL_ERROR, e.getErrorCode()); + } + checkUrl(interceptor, "GET", PROJECT_BASE_URL + "/inboundSamlConfigs"); + } + + @Test + public void testListSamlProviderConfigsWithPageToken() throws Exception { + final TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("listSaml.json")); + ListProviderConfigsPage page = + FirebaseAuth.getInstance().listSamlProviderConfigs("token", 99); + + ImmutableList providerConfigs = ImmutableList.copyOf(page.getValues()); + assertEquals(2, providerConfigs.size()); + checkSamlProviderConfig(providerConfigs.get(0), "saml.provider-id1"); + checkSamlProviderConfig(providerConfigs.get(1), "saml.provider-id2"); + assertEquals("", page.getNextPageToken()); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "GET", PROJECT_BASE_URL + "/inboundSamlConfigs"); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals(99, url.getFirst("pageSize")); + assertEquals("token", url.getFirst("nextPageToken")); + } + + @Test + public void testListZeroSamlProviderConfigs() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement("{}"); + ListProviderConfigsPage page = + FirebaseAuth.getInstance().listSamlProviderConfigs(null); + assertTrue(Iterables.isEmpty(page.getValues())); + assertEquals("", page.getNextPageToken()); + checkRequestHeaders(interceptor); + } + + @Test + public void testTenantAwareListSamlProviderConfigs() throws Exception { + final TestResponseInterceptor interceptor = initializeAppForTenantAwareUserManagement( + "TENANT_ID", + TestUtils.loadResource("listSaml.json")); + TenantAwareFirebaseAuth tenantAwareAuth = + FirebaseAuth.getInstance().getTenantManager().getAuthForTenant("TENANT_ID"); + ListProviderConfigsPage page = + tenantAwareAuth.listSamlProviderConfigs(null, 99); + + ImmutableList providerConfigs = ImmutableList.copyOf(page.getValues()); + assertEquals(2, providerConfigs.size()); + checkSamlProviderConfig(providerConfigs.get(0), "saml.provider-id1"); + checkSamlProviderConfig(providerConfigs.get(1), "saml.provider-id2"); + assertEquals("", page.getNextPageToken()); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "GET", TENANTS_BASE_URL + "/TENANT_ID/inboundSamlConfigs"); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals(99, url.getFirst("pageSize")); + assertNull(url.getFirst("nextPageToken")); + } + + @Test + public void testDeleteSamlProviderConfig() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement("{}"); + + FirebaseAuth.getInstance().deleteSamlProviderConfig("saml.provider-id"); + + checkRequestHeaders(interceptor); + checkUrl(interceptor, "DELETE", PROJECT_BASE_URL + "/inboundSamlConfigs/saml.provider-id"); + } + + @Test + public void testDeleteSamlProviderConfigAsync() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement("{}"); + + FirebaseAuth.getInstance().deleteSamlProviderConfigAsync("saml.provider-id").get(); + + checkRequestHeaders(interceptor); + checkUrl(interceptor, "DELETE", PROJECT_BASE_URL + "/inboundSamlConfigs/saml.provider-id"); + } + + @Test + public void testDeleteSamlProviderMissingId() throws Exception { + initializeAppForUserManagement("{}"); + + try { + FirebaseAuth.getInstance().deleteSamlProviderConfig(null); + fail("No error thrown for missing provider ID."); + } catch (IllegalArgumentException e) { + // Expected. + } + } + + @Test + public void testDeleteSamlProviderInvalidId() throws Exception { + initializeAppForUserManagement("{}"); + + try { + FirebaseAuth.getInstance().deleteSamlProviderConfig("oidc.invalid-saml-provider-id"); + fail("No error thrown for invalid provider ID."); + } catch (IllegalArgumentException e) { + // Expected. + } + } + + @Test + public void testDeleteSamlProviderConfigWithNotFoundError() { + TestResponseInterceptor interceptor = + initializeAppForUserManagementWithStatusCode(404, + "{\"error\": {\"message\": \"CONFIGURATION_NOT_FOUND\"}}"); + try { + FirebaseAuth.getInstance().deleteSamlProviderConfig("saml.UNKNOWN"); + fail("No error thrown for invalid response"); + } catch (FirebaseAuthException e) { + assertEquals(AuthHttpClient.CONFIGURATION_NOT_FOUND_ERROR, e.getErrorCode()); + } + checkUrl(interceptor, "DELETE", PROJECT_BASE_URL + "/inboundSamlConfigs/saml.UNKNOWN"); + } + + @Test + public void testTenantAwareDeleteSamlProviderConfig() throws Exception { + TestResponseInterceptor interceptor = initializeAppForTenantAwareUserManagement( + "TENANT_ID", + "{}"); + TenantAwareFirebaseAuth tenantAwareAuth = + FirebaseAuth.getInstance().getTenantManager().getAuthForTenant("TENANT_ID"); + + tenantAwareAuth.deleteSamlProviderConfig("saml.provider-id"); + + checkRequestHeaders(interceptor); + String expectedUrl = TENANTS_BASE_URL + "/TENANT_ID/inboundSamlConfigs/saml.provider-id"; + checkUrl(interceptor, "DELETE", expectedUrl); + } + + private static TestResponseInterceptor initializeAppForUserManagementWithStatusCode( + int statusCode, String response) { + FirebaseApp.initializeApp(new FirebaseOptions.Builder() + .setCredentials(credentials) + .setHttpTransport( + new MockHttpTransport.Builder().setLowLevelHttpResponse( + new MockLowLevelHttpResponse().setContent(response).setStatusCode(statusCode)).build()) + .setProjectId("test-project-id") + .build()); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + FirebaseAuth.getInstance().getUserManager().setInterceptor(interceptor); + return interceptor; + } + + private static TestResponseInterceptor initializeAppForTenantAwareUserManagement( + String tenantId, + String... responses) { + initializeAppWithResponses(responses); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + TenantManager tenantManager = FirebaseAuth.getInstance().getTenantManager(); + AbstractFirebaseAuth auth = tenantManager.getAuthForTenant(tenantId); + auth.getUserManager().setInterceptor(interceptor); + return interceptor; + } + + private static TestResponseInterceptor initializeAppForUserManagement(String... responses) { + initializeAppWithResponses(responses); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + FirebaseAuth.getInstance().getUserManager().setInterceptor(interceptor); + return interceptor; + } + + private static void initializeAppWithResponses(String... responses) { List mocks = new ArrayList<>(); for (String response : responses) { mocks.add(new MockLowLevelHttpResponse().setContent(response)); @@ -1428,11 +2584,13 @@ private static TestResponseInterceptor initializeAppForUserManagement(String ... .setHttpTransport(transport) .setProjectId("test-project-id") .build()); - FirebaseAuth auth = FirebaseAuth.getInstance(); - FirebaseUserManager userManager = auth.getUserManager(); - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - userManager.setInterceptor(interceptor); - return interceptor; + } + + private static GenericJson parseRequestContent(TestResponseInterceptor interceptor) + throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + interceptor.getResponse().getRequest().getContent().writeTo(out); + return JSON_FACTORY.fromString(new String(out.toByteArray()), GenericJson.class); } private static FirebaseAuth getRetryDisabledAuth(MockLowLevelHttpResponse response) { @@ -1444,15 +2602,19 @@ private static FirebaseAuth getRetryDisabledAuth(MockLowLevelHttpResponse respon .setProjectId("test-project-id") .setHttpTransport(transport) .build()); - return FirebaseAuth.builder() - .setFirebaseApp(app) - .setUserManager(new Supplier() { - @Override - public FirebaseUserManager get() { - return new FirebaseUserManager(app, transport.createRequestFactory()); - } - }) - .build(); + return new FirebaseAuth( + AbstractFirebaseAuth.builder() + .setFirebaseApp(app) + .setUserManager(new Supplier() { + @Override + public FirebaseUserManager get() { + return FirebaseUserManager + .builder() + .setFirebaseApp(app) + .setHttpRequestFactory(transport.createRequestFactory()) + .build(); + } + })); } private static void checkUserRecord(UserRecord userRecord) { @@ -1467,6 +2629,7 @@ private static void checkUserRecord(UserRecord userRecord) { assertFalse(userRecord.isDisabled()); assertTrue(userRecord.isEmailVerified()); assertEquals(1494364393000L, userRecord.getTokensValidAfterTimestamp()); + assertEquals("testTenant", userRecord.getTenantId()); UserInfo provider = userRecord.getProviderData()[0]; assertEquals("testuser@example.com", provider.getUid()); @@ -1488,6 +2651,25 @@ private static void checkUserRecord(UserRecord userRecord) { assertEquals("gold", claims.get("package")); } + private static void checkOidcProviderConfig(OidcProviderConfig config, String providerId) { + assertEquals(providerId, config.getProviderId()); + assertEquals("DISPLAY_NAME", config.getDisplayName()); + assertTrue(config.isEnabled()); + assertEquals("CLIENT_ID", config.getClientId()); + assertEquals("https://oidc.com/issuer", config.getIssuer()); + } + + private static void checkSamlProviderConfig(SamlProviderConfig config, String providerId) { + assertEquals(providerId, config.getProviderId()); + assertEquals("DISPLAY_NAME", config.getDisplayName()); + assertTrue(config.isEnabled()); + assertEquals("IDP_ENTITY_ID", config.getIdpEntityId()); + assertEquals("https://example.com/login", config.getSsoUrl()); + assertEquals(ImmutableList.of("certificate1", "certificate2"), config.getX509Certificates()); + assertEquals("RP_ENTITY_ID", config.getRpEntityId()); + assertEquals("https://projectId.firebaseapp.com/__/auth/handler", config.getCallbackUrl()); + } + private static void checkRequestHeaders(TestResponseInterceptor interceptor) { HttpHeaders headers = interceptor.getResponse().getRequest().getHeaders(); String auth = "Bearer " + TEST_TOKEN; @@ -1497,8 +2679,20 @@ private static void checkRequestHeaders(TestResponseInterceptor interceptor) { assertEquals(clientVersion, headers.getFirstHeaderStringValue("X-Client-Version")); } + private static void checkUrl(TestResponseInterceptor interceptor, String method, String url) { + HttpRequest request = interceptor.getResponse().getRequest(); + if (method.equals("PATCH")) { + assertEquals("PATCH", + request.getHeaders().getFirstHeaderStringValue("X-HTTP-Method-Override")); + assertEquals("POST", request.getRequestMethod()); + } else { + assertEquals(method, request.getRequestMethod()); + } + assertEquals(url, request.getUrl().toString().split("\\?")[0]); + } + private interface UserManagerOp { void call(FirebaseAuth auth) throws Exception; } - + } diff --git a/src/test/java/com/google/firebase/auth/GetUsersIT.java b/src/test/java/com/google/firebase/auth/GetUsersIT.java index efe2f783f..a0bdcb6c6 100644 --- a/src/test/java/com/google/firebase/auth/GetUsersIT.java +++ b/src/test/java/com/google/firebase/auth/GetUsersIT.java @@ -21,6 +21,7 @@ import com.google.common.collect.ImmutableList; import com.google.firebase.FirebaseApp; +import com.google.firebase.auth.UserTestUtils.RandomUser; import com.google.firebase.testing.IntegrationTestUtils; import java.util.Collection; @@ -44,18 +45,17 @@ public static void setUpClass() throws Exception { testUser2 = FirebaseAuthIT.newUserWithParams(auth); testUser3 = FirebaseAuthIT.newUserWithParams(auth); - FirebaseAuthIT.RandomUser randomUser = FirebaseAuthIT.RandomUser.create(); - importUserUid = randomUser.uid; - String phone = FirebaseAuthIT.randomPhoneNumber(); + RandomUser randomUser = UserTestUtils.generateRandomUserInfo(); + importUserUid = randomUser.getUid(); UserImportResult result = auth.importUsers(ImmutableList.of( ImportUserRecord.builder() - .setUid(randomUser.uid) - .setEmail(randomUser.email) - .setPhoneNumber(phone) + .setUid(randomUser.getUid()) + .setEmail(randomUser.getEmail()) + .setPhoneNumber(randomUser.getPhoneNumber()) .addUserProvider( UserProvider.builder() .setProviderId("google.com") - .setUid("google_" + randomUser.uid) + .setUid("google_" + randomUser.getUid()) .build()) .build() )); diff --git a/src/test/java/com/google/firebase/auth/ListProviderConfigsPageTest.java b/src/test/java/com/google/firebase/auth/ListProviderConfigsPageTest.java new file mode 100644 index 000000000..ba08f9c77 --- /dev/null +++ b/src/test/java/com/google/firebase/auth/ListProviderConfigsPageTest.java @@ -0,0 +1,376 @@ +/* + * Copyright 2020 Google LLC + * + * 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 static junit.framework.TestCase.assertTrue; +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; + +import com.google.api.client.googleapis.util.Utils; +import com.google.common.collect.ImmutableList; +import com.google.firebase.auth.internal.ListOidcProviderConfigsResponse; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; + +import org.junit.Test; + +public class ListProviderConfigsPageTest { + + @Test + public void testSinglePage() throws FirebaseAuthException, IOException { + TestProviderConfigSource source = new TestProviderConfigSource(3); + ListProviderConfigsPage page = + new ListProviderConfigsPage.Factory(source).create(); + assertFalse(page.hasNextPage()); + assertEquals(ListProviderConfigsPage.END_OF_LIST, page.getNextPageToken()); + assertNull(page.getNextPage()); + + ImmutableList providerConfigs = ImmutableList.copyOf(page.getValues()); + assertEquals(3, providerConfigs.size()); + for (int i = 0; i < 3; i++) { + assertEquals("oidc.provider-id-" + i, providerConfigs.get(i).getProviderId()); + } + assertEquals(1, source.calls.size()); + assertNull(source.calls.get(0)); + } + + @Test + public void testMultiplePages() throws FirebaseAuthException, IOException { + ListOidcProviderConfigsResponse response = new ListOidcProviderConfigsResponse( + ImmutableList.of( + newOidcProviderConfig("oidc.provider-id-0"), + newOidcProviderConfig("oidc.provider-id-1"), + newOidcProviderConfig("oidc.provider-id-2")), + "token"); + TestProviderConfigSource source = new TestProviderConfigSource(response); + ListProviderConfigsPage page1 = + new ListProviderConfigsPage.Factory(source).create(); + assertTrue(page1.hasNextPage()); + assertEquals("token", page1.getNextPageToken()); + ImmutableList providerConfigs = ImmutableList.copyOf(page1.getValues()); + assertEquals(3, providerConfigs.size()); + for (int i = 0; i < 3; i++) { + assertEquals("oidc.provider-id-" + i, providerConfigs.get(i).getProviderId()); + } + + response = new ListOidcProviderConfigsResponse( + ImmutableList.of( + newOidcProviderConfig("oidc.provider-id-3"), + newOidcProviderConfig("oidc.provider-id-4"), + newOidcProviderConfig("oidc.provider-id-5")), + ListProviderConfigsPage.END_OF_LIST); + source.response = response; + ListProviderConfigsPage page2 = page1.getNextPage(); + assertFalse(page2.hasNextPage()); + assertEquals(ListProviderConfigsPage.END_OF_LIST, page2.getNextPageToken()); + providerConfigs = ImmutableList.copyOf(page2.getValues()); + assertEquals(3, providerConfigs.size()); + for (int i = 3; i < 6; i++) { + assertEquals("oidc.provider-id-" + i, providerConfigs.get(i - 3).getProviderId()); + } + + assertEquals(2, source.calls.size()); + assertNull(source.calls.get(0)); + assertEquals("token", source.calls.get(1)); + + // Should iterate all provider configs from both pages + int iterations = 0; + for (OidcProviderConfig providerConfig : page1.iterateAll()) { + iterations++; + } + assertEquals(6, iterations); + assertEquals(3, source.calls.size()); + assertEquals("token", source.calls.get(2)); + + // Should only iterate provider configs in the last page + iterations = 0; + for (OidcProviderConfig providerConfig : page2.iterateAll()) { + iterations++; + } + assertEquals(3, iterations); + assertEquals(3, source.calls.size()); + } + + @Test + public void testListProviderConfigsIterable() throws FirebaseAuthException, IOException { + TestProviderConfigSource source = new TestProviderConfigSource(3); + ListProviderConfigsPage page = + new ListProviderConfigsPage.Factory(source).create(); + Iterable providerConfigs = page.iterateAll(); + + int iterations = 0; + for (OidcProviderConfig providerConfig : providerConfigs) { + assertEquals("oidc.provider-id-" + iterations, providerConfig.getProviderId()); + iterations++; + } + assertEquals(3, iterations); + assertEquals(1, source.calls.size()); + assertNull(source.calls.get(0)); + + // Should result in a new iterator + iterations = 0; + for (OidcProviderConfig providerConfig : providerConfigs) { + assertEquals("oidc.provider-id-" + iterations, providerConfig.getProviderId()); + iterations++; + } + assertEquals(3, iterations); + assertEquals(1, source.calls.size()); + assertNull(source.calls.get(0)); + } + + @Test + public void testListProviderConfigsIterator() throws FirebaseAuthException, IOException { + TestProviderConfigSource source = new TestProviderConfigSource(3); + ListProviderConfigsPage page = + new ListProviderConfigsPage.Factory(source).create(); + Iterable providerConfigs = page.iterateAll(); + Iterator iterator = providerConfigs.iterator(); + int iterations = 0; + while (iterator.hasNext()) { + assertEquals("oidc.provider-id-" + iterations, iterator.next().getProviderId()); + iterations++; + } + assertEquals(3, iterations); + assertEquals(1, source.calls.size()); + assertNull(source.calls.get(0)); + + while (iterator.hasNext()) { + fail("Should not be able to to iterate any more"); + } + try { + iterator.next(); + fail("Should not be able to iterate any more"); + } catch (NoSuchElementException expected) { + // expected + } + assertEquals(1, source.calls.size()); + } + + @Test + public void testListProviderConfigsPagedIterable() throws FirebaseAuthException, IOException { + ListOidcProviderConfigsResponse response = new ListOidcProviderConfigsResponse( + ImmutableList.of( + newOidcProviderConfig("oidc.provider-id-0"), + newOidcProviderConfig("oidc.provider-id-1"), + newOidcProviderConfig("oidc.provider-id-2")), + "token"); + TestProviderConfigSource source = new TestProviderConfigSource(response); + ListProviderConfigsPage page = + new ListProviderConfigsPage.Factory(source).create(); + int iterations = 0; + for (OidcProviderConfig providerConfig : page.iterateAll()) { + assertEquals("oidc.provider-id-" + iterations, providerConfig.getProviderId()); + iterations++; + if (iterations == 3) { + assertEquals(1, source.calls.size()); + assertNull(source.calls.get(0)); + response = new ListOidcProviderConfigsResponse( + ImmutableList.of( + newOidcProviderConfig("oidc.provider-id-3"), + newOidcProviderConfig("oidc.provider-id-4"), + newOidcProviderConfig("oidc.provider-id-5")), + ListProviderConfigsPage.END_OF_LIST); + source.response = response; + } + } + + assertEquals(6, iterations); + assertEquals(2, source.calls.size()); + assertEquals("token", source.calls.get(1)); + } + + @Test + public void testListProviderConfigsPagedIterator() throws FirebaseAuthException, IOException { + ListOidcProviderConfigsResponse response = new ListOidcProviderConfigsResponse( + ImmutableList.of( + newOidcProviderConfig("oidc.provider-id-0"), + newOidcProviderConfig("oidc.provider-id-1"), + newOidcProviderConfig("oidc.provider-id-2")), + "token"); + TestProviderConfigSource source = new TestProviderConfigSource(response); + ListProviderConfigsPage page = + new ListProviderConfigsPage.Factory(source).create(); + Iterator providerConfigs = page.iterateAll().iterator(); + int iterations = 0; + while (providerConfigs.hasNext()) { + assertEquals("oidc.provider-id-" + iterations, providerConfigs.next().getProviderId()); + iterations++; + if (iterations == 3) { + assertEquals(1, source.calls.size()); + assertNull(source.calls.get(0)); + response = new ListOidcProviderConfigsResponse( + ImmutableList.of( + newOidcProviderConfig("oidc.provider-id-3"), + newOidcProviderConfig("oidc.provider-id-4"), + newOidcProviderConfig("oidc.provider-id-5")), + ListProviderConfigsPage.END_OF_LIST); + source.response = response; + } + } + + assertEquals(6, iterations); + assertEquals(2, source.calls.size()); + assertEquals("token", source.calls.get(1)); + assertFalse(providerConfigs.hasNext()); + try { + providerConfigs.next(); + } catch (NoSuchElementException e) { + // expected + } + } + + @Test + public void testPageWithNoproviderConfigs() throws FirebaseAuthException { + ListOidcProviderConfigsResponse response = new ListOidcProviderConfigsResponse( + ImmutableList.of(), ListProviderConfigsPage.END_OF_LIST); + TestProviderConfigSource source = new TestProviderConfigSource(response); + ListProviderConfigsPage page = + new ListProviderConfigsPage.Factory(source).create(); + assertFalse(page.hasNextPage()); + assertEquals(ListProviderConfigsPage.END_OF_LIST, page.getNextPageToken()); + assertNull(page.getNextPage()); + assertEquals(0, ImmutableList.copyOf(page.getValues()).size()); + assertEquals(1, source.calls.size()); + } + + @Test + public void testIterableWithNoproviderConfigs() throws FirebaseAuthException { + ListOidcProviderConfigsResponse response = new ListOidcProviderConfigsResponse( + ImmutableList.of(), ListProviderConfigsPage.END_OF_LIST); + TestProviderConfigSource source = new TestProviderConfigSource(response); + ListProviderConfigsPage page = + new ListProviderConfigsPage.Factory(source).create(); + for (OidcProviderConfig providerConfig : page.iterateAll()) { + fail("Should not be able to iterate, but got: " + providerConfig); + } + assertEquals(1, source.calls.size()); + } + + @Test + public void testIteratorWithNoproviderConfigs() throws FirebaseAuthException { + ListOidcProviderConfigsResponse response = new ListOidcProviderConfigsResponse( + ImmutableList.of(), ListProviderConfigsPage.END_OF_LIST); + TestProviderConfigSource source = new TestProviderConfigSource(response); + + ListProviderConfigsPage page = + new ListProviderConfigsPage.Factory(source).create(); + Iterator iterator = page.iterateAll().iterator(); + while (iterator.hasNext()) { + fail("Should not be able to iterate"); + } + assertEquals(1, source.calls.size()); + } + + @Test + public void testRemove() throws FirebaseAuthException, IOException { + ListOidcProviderConfigsResponse response = new ListOidcProviderConfigsResponse( + ImmutableList.of(newOidcProviderConfig("oidc.provider-id-1")), + ListProviderConfigsPage.END_OF_LIST); + TestProviderConfigSource source = new TestProviderConfigSource(response); + + ListProviderConfigsPage page = + new ListProviderConfigsPage.Factory(source).create(); + Iterator iterator = page.iterateAll().iterator(); + while (iterator.hasNext()) { + assertNotNull(iterator.next()); + try { + iterator.remove(); + } catch (UnsupportedOperationException expected) { + // expected + } + } + } + + @Test(expected = NullPointerException.class) + public void testNullSource() { + new ListProviderConfigsPage.Factory(null); + } + + @Test + public void testInvalidPageToken() throws IOException { + TestProviderConfigSource source = new TestProviderConfigSource(1); + try { + new ListProviderConfigsPage.Factory(source, 1000, ""); + fail("No error thrown for empty page token"); + } catch (IllegalArgumentException expected) { + // expected + } + } + + @Test + public void testInvalidMaxResults() throws IOException { + TestProviderConfigSource source = new TestProviderConfigSource(1); + try { + new ListProviderConfigsPage.Factory(source, 1001, ""); + fail("No error thrown for maxResult > 1000"); + } catch (IllegalArgumentException expected) { + // expected + } + + try { + new ListProviderConfigsPage.Factory(source, 0, "next"); + fail("No error thrown for maxResult = 0"); + } catch (IllegalArgumentException expected) { + // expected + } + + try { + new ListProviderConfigsPage.Factory(source, -1, "next"); + fail("No error thrown for maxResult < 0"); + } catch (IllegalArgumentException expected) { + // expected + } + } + + private static OidcProviderConfig newOidcProviderConfig(String providerConfigId) + throws IOException { + return Utils.getDefaultJsonFactory().fromString( + String.format("{\"name\":\"%s\"}", providerConfigId), OidcProviderConfig.class); + } + + private static class TestProviderConfigSource + implements ListProviderConfigsPage.ProviderConfigSource { + + private ListOidcProviderConfigsResponse response; + private final List calls = new ArrayList<>(); + + TestProviderConfigSource(int providerConfigCount) throws IOException { + ImmutableList.Builder providerConfigs = ImmutableList.builder(); + for (int i = 0; i < providerConfigCount; i++) { + providerConfigs.add(newOidcProviderConfig("oidc.provider-id-" + i)); + } + this.response = new ListOidcProviderConfigsResponse( + providerConfigs.build(), ListProviderConfigsPage.END_OF_LIST); + } + + TestProviderConfigSource(ListOidcProviderConfigsResponse response) { + this.response = response; + } + + @Override + public ListOidcProviderConfigsResponse fetch(int maxResults, String pageToken) { + calls.add(pageToken); + return response; + } + } +} diff --git a/src/test/java/com/google/firebase/auth/ListUsersPageTest.java b/src/test/java/com/google/firebase/auth/ListUsersPageTest.java index fb4a7d275..5e848069d 100644 --- a/src/test/java/com/google/firebase/auth/ListUsersPageTest.java +++ b/src/test/java/com/google/firebase/auth/ListUsersPageTest.java @@ -27,6 +27,7 @@ import com.google.api.client.json.JsonFactory; import com.google.common.collect.ImmutableList; import com.google.common.io.BaseEncoding; +import com.google.firebase.auth.ListUsersPage; import com.google.firebase.auth.ListUsersPage.ListUsersResult; import com.google.firebase.auth.internal.DownloadAccountResponse; import java.io.IOException; @@ -45,7 +46,7 @@ public class ListUsersPageTest { @Test public void testSinglePage() throws FirebaseAuthException, IOException { TestUserSource source = new TestUserSource(3); - ListUsersPage page = new ListUsersPage.PageFactory(source).create(); + ListUsersPage page = new ListUsersPage.Factory(source).create(); assertFalse(page.hasNextPage()); assertEquals(ListUsersPage.END_OF_LIST, page.getNextPageToken()); assertNull(page.getNextPage()); @@ -68,7 +69,7 @@ public void testRedactedPasswords() throws FirebaseAuthException, IOException { newUser("user2", REDACTED_BASE64)), ListUsersPage.END_OF_LIST); TestUserSource source = new TestUserSource(result); - ListUsersPage page = new ListUsersPage.PageFactory(source).create(); + ListUsersPage page = new ListUsersPage.Factory(source).create(); assertFalse(page.hasNextPage()); assertEquals(ListUsersPage.END_OF_LIST, page.getNextPageToken()); assertNull(page.getNextPage()); @@ -89,7 +90,7 @@ public void testMultiplePages() throws FirebaseAuthException, IOException { ImmutableList.of(newUser("user0"), newUser("user1"), newUser("user2")), "token"); TestUserSource source = new TestUserSource(result); - ListUsersPage page1 = new ListUsersPage.PageFactory(source).create(); + ListUsersPage page1 = new ListUsersPage.Factory(source).create(); assertTrue(page1.hasNextPage()); assertEquals("token", page1.getNextPageToken()); ImmutableList users = ImmutableList.copyOf(page1.getValues()); @@ -136,7 +137,7 @@ public void testMultiplePages() throws FirebaseAuthException, IOException { @Test public void testListUsersIterable() throws FirebaseAuthException, IOException { TestUserSource source = new TestUserSource(3); - ListUsersPage page = new ListUsersPage.PageFactory(source).create(); + ListUsersPage page = new ListUsersPage.Factory(source).create(); Iterable users = page.iterateAll(); int iterations = 0; @@ -162,7 +163,7 @@ public void testListUsersIterable() throws FirebaseAuthException, IOException { @Test public void testListUsersIterator() throws FirebaseAuthException, IOException { TestUserSource source = new TestUserSource(3); - ListUsersPage page = new ListUsersPage.PageFactory(source).create(); + ListUsersPage page = new ListUsersPage.Factory(source).create(); Iterable users = page.iterateAll(); Iterator iterator = users.iterator(); int iterations = 0; @@ -192,7 +193,7 @@ public void testListUsersPagedIterable() throws FirebaseAuthException, IOExcepti ImmutableList.of(newUser("user0"), newUser("user1"), newUser("user2")), "token"); TestUserSource source = new TestUserSource(result); - ListUsersPage page = new ListUsersPage.PageFactory(source).create(); + ListUsersPage page = new ListUsersPage.Factory(source).create(); int iterations = 0; for (ExportedUserRecord user : page.iterateAll()) { assertEquals("user" + iterations, user.getUid()); @@ -218,7 +219,7 @@ public void testListUsersPagedIterator() throws FirebaseAuthException, IOExcepti ImmutableList.of(newUser("user0"), newUser("user1"), newUser("user2")), "token"); TestUserSource source = new TestUserSource(result); - ListUsersPage page = new ListUsersPage.PageFactory(source).create(); + ListUsersPage page = new ListUsersPage.Factory(source).create(); Iterator users = page.iterateAll().iterator(); int iterations = 0; while (users.hasNext()) { @@ -251,7 +252,7 @@ public void testPageWithNoUsers() throws FirebaseAuthException { ImmutableList.of(), ListUsersPage.END_OF_LIST); TestUserSource source = new TestUserSource(result); - ListUsersPage page = new ListUsersPage.PageFactory(source).create(); + ListUsersPage page = new ListUsersPage.Factory(source).create(); assertFalse(page.hasNextPage()); assertEquals(ListUsersPage.END_OF_LIST, page.getNextPageToken()); assertNull(page.getNextPage()); @@ -265,7 +266,7 @@ public void testIterableWithNoUsers() throws FirebaseAuthException { ImmutableList.of(), ListUsersPage.END_OF_LIST); TestUserSource source = new TestUserSource(result); - ListUsersPage page = new ListUsersPage.PageFactory(source).create(); + ListUsersPage page = new ListUsersPage.Factory(source).create(); for (ExportedUserRecord user : page.iterateAll()) { fail("Should not be able to iterate, but got: " + user); } @@ -279,7 +280,7 @@ public void testIteratorWithNoUsers() throws FirebaseAuthException { ListUsersPage.END_OF_LIST); TestUserSource source = new TestUserSource(result); - ListUsersPage page = new ListUsersPage.PageFactory(source).create(); + ListUsersPage page = new ListUsersPage.Factory(source).create(); Iterator iterator = page.iterateAll().iterator(); while (iterator.hasNext()) { fail("Should not be able to iterate"); @@ -294,7 +295,7 @@ public void testRemove() throws FirebaseAuthException, IOException { ListUsersPage.END_OF_LIST); TestUserSource source = new TestUserSource(result); - ListUsersPage page = new ListUsersPage.PageFactory(source).create(); + ListUsersPage page = new ListUsersPage.Factory(source).create(); Iterator iterator = page.iterateAll().iterator(); while (iterator.hasNext()) { assertNotNull(iterator.next()); @@ -308,14 +309,14 @@ public void testRemove() throws FirebaseAuthException, IOException { @Test(expected = NullPointerException.class) public void testNullSource() { - new ListUsersPage.PageFactory(null); + new ListUsersPage.Factory(null); } @Test public void testInvalidPageToken() throws IOException { TestUserSource source = new TestUserSource(1); try { - new ListUsersPage.PageFactory(source, 1000, ""); + new ListUsersPage.Factory(source, 1000, ""); fail("No error thrown for empty page token"); } catch (IllegalArgumentException expected) { // expected @@ -326,21 +327,21 @@ public void testInvalidPageToken() throws IOException { public void testInvalidMaxResults() throws IOException { TestUserSource source = new TestUserSource(1); try { - new ListUsersPage.PageFactory(source, 1001, ""); + new ListUsersPage.Factory(source, 1001, ""); fail("No error thrown for maxResult > 1000"); } catch (IllegalArgumentException expected) { // expected } try { - new ListUsersPage.PageFactory(source, 0, "next"); + new ListUsersPage.Factory(source, 0, "next"); fail("No error thrown for maxResult = 0"); } catch (IllegalArgumentException expected) { // expected } try { - new ListUsersPage.PageFactory(source, -1, "next"); + new ListUsersPage.Factory(source, -1, "next"); fail("No error thrown for maxResult < 0"); } catch (IllegalArgumentException expected) { // expected diff --git a/src/test/java/com/google/firebase/auth/OidcProviderConfigTest.java b/src/test/java/com/google/firebase/auth/OidcProviderConfigTest.java new file mode 100644 index 000000000..1fb4ca37b --- /dev/null +++ b/src/test/java/com/google/firebase/auth/OidcProviderConfigTest.java @@ -0,0 +1,160 @@ +/* + * Copyright 2020 Google LLC + * + * 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import com.google.api.client.googleapis.util.Utils; +import com.google.api.client.json.JsonFactory; +import java.io.IOException; +import java.util.Map; +import org.junit.Test; + +public class OidcProviderConfigTest { + + private static final JsonFactory jsonFactory = Utils.getDefaultJsonFactory(); + + private static final String OIDC_JSON_STRING = + ("{" + + " 'name': 'projects/projectId/oauthIdpConfigs/oidc.provider-id'," + + " 'displayName': 'DISPLAY_NAME'," + + " 'enabled': true," + + " 'clientId': 'CLIENT_ID'," + + " 'issuer': 'https://oidc.com/issuer'" + + "}").replace("'", "\""); + + @Test + public void testJsonDeserialization() throws IOException { + OidcProviderConfig config = jsonFactory.fromString(OIDC_JSON_STRING, OidcProviderConfig.class); + + assertEquals("oidc.provider-id", config.getProviderId()); + assertEquals("DISPLAY_NAME", config.getDisplayName()); + assertTrue(config.isEnabled()); + assertEquals("CLIENT_ID", config.getClientId()); + assertEquals("https://oidc.com/issuer", config.getIssuer()); + } + + @Test + public void testCreateRequest() throws IOException { + OidcProviderConfig.CreateRequest createRequest = new OidcProviderConfig.CreateRequest(); + createRequest + .setProviderId("oidc.provider-id") + .setDisplayName("DISPLAY_NAME") + .setEnabled(false) + .setClientId("CLIENT_ID") + .setIssuer("https://oidc.com/issuer"); + + assertEquals("oidc.provider-id", createRequest.getProviderId()); + Map properties = createRequest.getProperties(); + assertEquals(properties.size(), 4); + assertEquals("DISPLAY_NAME", (String) properties.get("displayName")); + assertFalse((boolean) properties.get("enabled")); + assertEquals("CLIENT_ID", (String) properties.get("clientId")); + assertEquals("https://oidc.com/issuer", (String) properties.get("issuer")); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateRequestMissingProviderId() { + new OidcProviderConfig.CreateRequest().setProviderId(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateRequestInvalidProviderId() { + new OidcProviderConfig.CreateRequest().setProviderId("saml.provider-id"); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateRequestMissingDisplayName() { + new OidcProviderConfig.CreateRequest().setDisplayName(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateRequestMissingClientId() { + new OidcProviderConfig.CreateRequest().setClientId(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateRequestMissingIssuer() { + new OidcProviderConfig.CreateRequest().setIssuer(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateRequestInvalidIssuerUrl() { + new OidcProviderConfig.CreateRequest().setIssuer("not a valid url"); + } + + @Test + public void testUpdateRequestFromOidcProviderConfig() throws IOException { + OidcProviderConfig config = jsonFactory.fromString(OIDC_JSON_STRING, OidcProviderConfig.class); + + OidcProviderConfig.UpdateRequest updateRequest = config.updateRequest(); + + assertEquals("oidc.provider-id", updateRequest.getProviderId()); + assertTrue(updateRequest.getProperties().isEmpty()); + } + + @Test + public void testUpdateRequest() throws IOException { + OidcProviderConfig.UpdateRequest updateRequest = + new OidcProviderConfig.UpdateRequest("oidc.provider-id"); + updateRequest + .setDisplayName("DISPLAY_NAME") + .setEnabled(false) + .setClientId("CLIENT_ID") + .setIssuer("https://oidc.com/issuer"); + + assertEquals("oidc.provider-id", updateRequest.getProviderId()); + Map properties = updateRequest.getProperties(); + assertEquals(properties.size(), 4); + assertEquals("DISPLAY_NAME", (String) properties.get("displayName")); + assertFalse((boolean) properties.get("enabled")); + assertEquals("CLIENT_ID", (String) properties.get("clientId")); + assertEquals("https://oidc.com/issuer", (String) properties.get("issuer")); + } + + @Test(expected = IllegalArgumentException.class) + public void testUpdateRequestMissingProviderId() { + new OidcProviderConfig.UpdateRequest(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testUpdateRequestInvalidProviderId() { + new OidcProviderConfig.UpdateRequest("saml.provider-id"); + } + + @Test(expected = IllegalArgumentException.class) + public void testUpdateRequestMissingDisplayName() { + new OidcProviderConfig.UpdateRequest("oidc.provider-id").setDisplayName(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testUpdateRequestMissingClientId() { + new OidcProviderConfig.UpdateRequest("oidc.provider-id").setClientId(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testUpdateRequestMissingIssuer() { + new OidcProviderConfig.UpdateRequest("oidc.provider-id").setIssuer(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testUpdateRequestInvalidIssuerUrl() { + new OidcProviderConfig.UpdateRequest("oidc.provider-id").setIssuer("not a valid url"); + } +} diff --git a/src/test/java/com/google/firebase/auth/ProviderConfigTestUtils.java b/src/test/java/com/google/firebase/auth/ProviderConfigTestUtils.java new file mode 100644 index 000000000..c01ac6501 --- /dev/null +++ b/src/test/java/com/google/firebase/auth/ProviderConfigTestUtils.java @@ -0,0 +1,125 @@ +/* + * Copyright 2020 Google LLC + * + * 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 static com.google.common.base.Preconditions.checkArgument; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import com.google.firebase.auth.internal.AuthHttpClient; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutionException; +import org.junit.rules.ExternalResource; + +public class ProviderConfigTestUtils { + + public static void assertOidcProviderConfigDoesNotExist( + AbstractFirebaseAuth firebaseAuth, String providerId) throws Exception { + try { + firebaseAuth.getOidcProviderConfigAsync(providerId).get(); + fail("No error thrown for getting a deleted OIDC provider config."); + } catch (ExecutionException e) { + assertTrue(e.getCause() instanceof FirebaseAuthException); + assertEquals(AuthHttpClient.CONFIGURATION_NOT_FOUND_ERROR, + ((FirebaseAuthException) e.getCause()).getErrorCode()); + } + } + + public static void assertSamlProviderConfigDoesNotExist( + AbstractFirebaseAuth firebaseAuth, String providerId) throws Exception { + try { + firebaseAuth.getSamlProviderConfigAsync(providerId).get(); + fail("No error thrown for getting a deleted SAML provider config."); + } catch (ExecutionException e) { + assertTrue(e.getCause() instanceof FirebaseAuthException); + assertEquals(AuthHttpClient.CONFIGURATION_NOT_FOUND_ERROR, + ((FirebaseAuthException) e.getCause()).getErrorCode()); + } + } + + /** + * Creates temporary provider configs for testing, and deletes them at the end of each test case. + */ + public static final class TemporaryProviderConfig extends ExternalResource { + + private final AbstractFirebaseAuth auth; + private final List oidcIds = new ArrayList<>(); + private final List samlIds = new ArrayList<>(); + + public TemporaryProviderConfig(AbstractFirebaseAuth auth) { + this.auth = auth; + } + + public synchronized OidcProviderConfig createOidcProviderConfig( + OidcProviderConfig.CreateRequest request) throws FirebaseAuthException { + OidcProviderConfig config = auth.createOidcProviderConfig(request); + oidcIds.add(config.getProviderId()); + return config; + } + + public synchronized void deleteOidcProviderConfig( + String providerId) throws FirebaseAuthException { + checkArgument(oidcIds.contains(providerId), + "Provider ID is not currently associated with a temporary OIDC provider config: " + + providerId); + auth.deleteOidcProviderConfig(providerId); + oidcIds.remove(providerId); + } + + public synchronized SamlProviderConfig createSamlProviderConfig( + SamlProviderConfig.CreateRequest request) throws FirebaseAuthException { + SamlProviderConfig config = auth.createSamlProviderConfig(request); + samlIds.add(config.getProviderId()); + return config; + } + + public synchronized void deleteSamlProviderConfig( + String providerId) throws FirebaseAuthException { + checkArgument(samlIds.contains(providerId), + "Provider ID is not currently associated with a temporary SAML provider config: " + + providerId); + auth.deleteSamlProviderConfig(providerId); + samlIds.remove(providerId); + } + + @Override + protected synchronized void after() { + // Delete OIDC provider configs. + for (String id : oidcIds) { + try { + auth.deleteOidcProviderConfig(id); + } catch (Exception ignore) { + // Ignore + } + } + oidcIds.clear(); + + // Delete SAML provider configs. + for (String id : samlIds) { + try { + auth.deleteSamlProviderConfig(id); + } catch (Exception ignore) { + // Ignore + } + } + samlIds.clear(); + } + } +} + diff --git a/src/test/java/com/google/firebase/auth/SamlProviderConfigTest.java b/src/test/java/com/google/firebase/auth/SamlProviderConfigTest.java new file mode 100644 index 000000000..e957c1ddb --- /dev/null +++ b/src/test/java/com/google/firebase/auth/SamlProviderConfigTest.java @@ -0,0 +1,322 @@ +/* + * Copyright 2020 Google LLC + * + * 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import com.google.api.client.googleapis.util.Utils; +import com.google.api.client.json.JsonFactory; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import java.io.IOException; +import java.util.List; +import java.util.Map; +import org.junit.Test; + +public class SamlProviderConfigTest { + + private static final JsonFactory jsonFactory = Utils.getDefaultJsonFactory(); + + private static final String SAML_JSON_STRING = + ("{" + + " 'name': 'projects/projectId/inboundSamlConfigs/saml.provider-id'," + + " 'displayName': 'DISPLAY_NAME'," + + " 'enabled': true," + + " 'idpConfig': {" + + " 'idpEntityId': 'IDP_ENTITY_ID'," + + " 'ssoUrl': 'https://example.com/login'," + + " 'idpCertificates': [" + + " { 'x509Certificate': 'certificate1' }," + + " { 'x509Certificate': 'certificate2' }" + + " ]" + + " }," + + " 'spConfig': {" + + " 'spEntityId': 'RP_ENTITY_ID'," + + " 'callbackUri': 'https://projectId.firebaseapp.com/__/auth/handler'" + + " }" + + "}").replace("'", "\""); + + @Test + public void testJsonDeserialization() throws IOException { + SamlProviderConfig config = jsonFactory.fromString(SAML_JSON_STRING, SamlProviderConfig.class); + + assertEquals("saml.provider-id", config.getProviderId()); + assertEquals("DISPLAY_NAME", config.getDisplayName()); + assertTrue(config.isEnabled()); + assertEquals("IDP_ENTITY_ID", config.getIdpEntityId()); + assertEquals("https://example.com/login", config.getSsoUrl()); + assertEquals(ImmutableList.of("certificate1", "certificate2"), config.getX509Certificates()); + assertEquals("RP_ENTITY_ID", config.getRpEntityId()); + assertEquals("https://projectId.firebaseapp.com/__/auth/handler", config.getCallbackUrl()); + } + + @Test + public void testCreateRequest() throws IOException { + SamlProviderConfig.CreateRequest createRequest = + new SamlProviderConfig.CreateRequest() + .setProviderId("saml.provider-id") + .setDisplayName("DISPLAY_NAME") + .setEnabled(false) + .setIdpEntityId("IDP_ENTITY_ID") + .setSsoUrl("https://example.com/login") + .addX509Certificate("certificate1") + .addX509Certificate("certificate2") + .setRpEntityId("RP_ENTITY_ID") + .setCallbackUrl("https://projectId.firebaseapp.com/__/auth/handler"); + + assertEquals("saml.provider-id", createRequest.getProviderId()); + Map properties = createRequest.getProperties(); + assertEquals(4, properties.size()); + assertEquals("DISPLAY_NAME", (String) properties.get("displayName")); + assertFalse((boolean) properties.get("enabled")); + + Map idpConfig = (Map) properties.get("idpConfig"); + assertNotNull(idpConfig); + assertEquals(3, idpConfig.size()); + assertEquals("IDP_ENTITY_ID", idpConfig.get("idpEntityId")); + assertEquals("https://example.com/login", idpConfig.get("ssoUrl")); + List idpCertificates = (List) idpConfig.get("idpCertificates"); + assertNotNull(idpCertificates); + assertEquals(2, idpCertificates.size()); + assertEquals(ImmutableMap.of("x509Certificate", "certificate1"), idpCertificates.get(0)); + assertEquals(ImmutableMap.of("x509Certificate", "certificate2"), idpCertificates.get(1)); + + Map spConfig = (Map) properties.get("spConfig"); + assertNotNull(spConfig); + assertEquals(2, spConfig.size()); + assertEquals("RP_ENTITY_ID", spConfig.get("spEntityId")); + assertEquals("https://projectId.firebaseapp.com/__/auth/handler", spConfig.get("callbackUri")); + } + + @Test + public void testCreateRequestX509Certificates() throws IOException { + SamlProviderConfig.CreateRequest createRequest = + new SamlProviderConfig.CreateRequest() + .addX509Certificate("certificate1") + .addAllX509Certificates(ImmutableList.of("certificate2", "certificate3")) + .addX509Certificate("certificate4"); + + Map properties = createRequest.getProperties(); + assertEquals(1, properties.size()); + Map idpConfig = (Map) properties.get("idpConfig"); + assertNotNull(idpConfig); + assertEquals(1, idpConfig.size()); + + List idpCertificates = (List) idpConfig.get("idpCertificates"); + assertNotNull(idpCertificates); + assertEquals(4, idpCertificates.size()); + assertEquals(ImmutableMap.of("x509Certificate", "certificate1"), idpCertificates.get(0)); + assertEquals(ImmutableMap.of("x509Certificate", "certificate2"), idpCertificates.get(1)); + assertEquals(ImmutableMap.of("x509Certificate", "certificate3"), idpCertificates.get(2)); + assertEquals(ImmutableMap.of("x509Certificate", "certificate4"), idpCertificates.get(3)); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateRequestMissingProviderId() { + new SamlProviderConfig.CreateRequest().setProviderId(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateRequestInvalidProviderId() { + new SamlProviderConfig.CreateRequest().setProviderId("oidc.provider-id"); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateRequestMissingDisplayName() { + new SamlProviderConfig.CreateRequest().setDisplayName(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateRequestMissingIdpEntityId() { + new SamlProviderConfig.CreateRequest().setIdpEntityId(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateRequestMissingSsoUrl() { + new SamlProviderConfig.CreateRequest().setSsoUrl(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateRequestInvalidSsoUrl() { + new SamlProviderConfig.CreateRequest().setSsoUrl("not a valid url"); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateRequestMissingX509Certificate() { + new SamlProviderConfig.CreateRequest().addX509Certificate(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateRequestNullX509CertificatesCollection() { + new SamlProviderConfig.CreateRequest().addAllX509Certificates(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateRequestEmptyX509CertificatesCollection() { + new SamlProviderConfig.CreateRequest().addAllX509Certificates(ImmutableList.of()); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateRequestMissingRpEntityId() { + new SamlProviderConfig.CreateRequest().setRpEntityId(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateRequestMissingCallbackUrl() { + new SamlProviderConfig.CreateRequest().setCallbackUrl(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateRequestInvalidCallbackUrl() { + new SamlProviderConfig.CreateRequest().setCallbackUrl("not a valid url"); + } + + @Test + public void testUpdateRequestFromSamlProviderConfig() throws IOException { + SamlProviderConfig config = jsonFactory.fromString(SAML_JSON_STRING, SamlProviderConfig.class); + + SamlProviderConfig.UpdateRequest updateRequest = config.updateRequest(); + + assertEquals("saml.provider-id", updateRequest.getProviderId()); + assertTrue(updateRequest.getProperties().isEmpty()); + } + + @Test + public void testUpdateRequest() throws IOException { + SamlProviderConfig.UpdateRequest updateRequest = + new SamlProviderConfig.UpdateRequest("saml.provider-id"); + updateRequest + .setDisplayName("DISPLAY_NAME") + .setEnabled(false) + .setIdpEntityId("IDP_ENTITY_ID") + .setSsoUrl("https://example.com/login") + .addX509Certificate("certificate1") + .addX509Certificate("certificate2") + .setRpEntityId("RP_ENTITY_ID") + .setCallbackUrl("https://projectId.firebaseapp.com/__/auth/handler"); + + Map properties = updateRequest.getProperties(); + assertEquals(4, properties.size()); + assertEquals("DISPLAY_NAME", (String) properties.get("displayName")); + assertFalse((boolean) properties.get("enabled")); + + Map idpConfig = (Map) properties.get("idpConfig"); + assertNotNull(idpConfig); + assertEquals(3, idpConfig.size()); + assertEquals("IDP_ENTITY_ID", idpConfig.get("idpEntityId")); + assertEquals("https://example.com/login", idpConfig.get("ssoUrl")); + List idpCertificates = (List) idpConfig.get("idpCertificates"); + assertNotNull(idpCertificates); + assertEquals(2, idpCertificates.size()); + assertEquals(ImmutableMap.of("x509Certificate", "certificate1"), idpCertificates.get(0)); + assertEquals(ImmutableMap.of("x509Certificate", "certificate2"), idpCertificates.get(1)); + + Map spConfig = (Map) properties.get("spConfig"); + assertNotNull(spConfig); + assertEquals(2, spConfig.size()); + assertEquals("RP_ENTITY_ID", spConfig.get("spEntityId")); + assertEquals("https://projectId.firebaseapp.com/__/auth/handler", spConfig.get("callbackUri")); + } + + @Test + public void testUpdateRequestX509Certificates() throws IOException { + SamlProviderConfig.UpdateRequest updateRequest = + new SamlProviderConfig.UpdateRequest("saml.provider-id"); + updateRequest + .addX509Certificate("certificate1") + .addAllX509Certificates(ImmutableList.of("certificate2", "certificate3")) + .addX509Certificate("certificate4"); + + Map properties = updateRequest.getProperties(); + assertEquals(1, properties.size()); + Map idpConfig = (Map) properties.get("idpConfig"); + assertNotNull(idpConfig); + assertEquals(1, idpConfig.size()); + + List idpCertificates = (List) idpConfig.get("idpCertificates"); + assertNotNull(idpCertificates); + assertEquals(4, idpCertificates.size()); + assertEquals(ImmutableMap.of("x509Certificate", "certificate1"), idpCertificates.get(0)); + assertEquals(ImmutableMap.of("x509Certificate", "certificate2"), idpCertificates.get(1)); + assertEquals(ImmutableMap.of("x509Certificate", "certificate3"), idpCertificates.get(2)); + assertEquals(ImmutableMap.of("x509Certificate", "certificate4"), idpCertificates.get(3)); + } + + @Test(expected = IllegalArgumentException.class) + public void testUpdateRequestMissingProviderId() { + new SamlProviderConfig.UpdateRequest(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testUpdateRequestInvalidProviderId() { + new SamlProviderConfig.UpdateRequest("oidc.invalid-saml-provider-id"); + } + + @Test(expected = IllegalArgumentException.class) + public void testUpdateRequestMissingDisplayName() { + new SamlProviderConfig.UpdateRequest("saml.provider-id").setDisplayName(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testUpdateRequestMissingIdpEntityId() { + new SamlProviderConfig.UpdateRequest("saml.provider-id").setIdpEntityId(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testUpdateRequestMissingSsoUrl() { + new SamlProviderConfig.UpdateRequest("saml.provider-id").setSsoUrl(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testUpdateRequestInvalidSsoUrl() { + new SamlProviderConfig.UpdateRequest("saml.provider-id").setSsoUrl("not a valid url"); + } + + @Test(expected = IllegalArgumentException.class) + public void testUpdateRequestMissingX509Certificate() { + new SamlProviderConfig.UpdateRequest("saml.provider-id").addX509Certificate(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testUpdateRequestNullX509CertificatesCollection() { + new SamlProviderConfig.UpdateRequest("saml.provider-id").addAllX509Certificates(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testUpdateRequestEmptyX509CertificatesCollection() { + new SamlProviderConfig.UpdateRequest("saml.provider-id") + .addAllX509Certificates(ImmutableList.of()); + } + + @Test(expected = IllegalArgumentException.class) + public void testUpdateRequestMissingRpEntityId() { + new SamlProviderConfig.UpdateRequest("saml.provider-id").setRpEntityId(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testUpdateRequestMissingCallbackUrl() { + new SamlProviderConfig.UpdateRequest("saml.provider-id").setCallbackUrl(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testUpdateRequestInvalidCallbackUrl() { + new SamlProviderConfig.UpdateRequest("saml.provider-id").setCallbackUrl("not a valid url"); + } +} diff --git a/src/test/java/com/google/firebase/auth/UserTestUtils.java b/src/test/java/com/google/firebase/auth/UserTestUtils.java new file mode 100644 index 000000000..aa86e6e19 --- /dev/null +++ b/src/test/java/com/google/firebase/auth/UserTestUtils.java @@ -0,0 +1,124 @@ +/* + * Copyright 2020 Google LLC + * + * 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import com.google.firebase.auth.internal.AuthHttpClient; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.UUID; +import java.util.concurrent.ExecutionException; +import org.junit.rules.ExternalResource; + +public final class UserTestUtils { + + public static void assertUserDoesNotExist(AbstractFirebaseAuth firebaseAuth, String uid) + throws Exception { + try { + firebaseAuth.getUserAsync(uid).get(); + fail("No error thrown for getting a user which was expected to be absent."); + } catch (ExecutionException e) { + assertTrue(e.getCause() instanceof FirebaseAuthException); + assertEquals(AuthHttpClient.USER_NOT_FOUND_ERROR, + ((FirebaseAuthException) e.getCause()).getErrorCode()); + } + } + + public static RandomUser generateRandomUserInfo() { + String uid = UUID.randomUUID().toString().replaceAll("-", ""); + String email = String.format( + "test%s@example.%s.com", + uid.substring(0, 12), + uid.substring(12)).toLowerCase(); + return new RandomUser(uid, email, generateRandomPhoneNumber()); + } + + private static String generateRandomPhoneNumber() { + Random random = new Random(); + StringBuilder builder = new StringBuilder("+1"); + for (int i = 0; i < 10; i++) { + builder.append(random.nextInt(10)); + } + return builder.toString(); + } + + public static class RandomUser { + private final String uid; + private final String email; + private final String phoneNumber; + + private RandomUser(String uid, String email, String phoneNumber) { + this.uid = uid; + this.email = email; + this.phoneNumber = phoneNumber; + } + + public String getUid() { + return uid; + } + + public String getEmail() { + return email; + } + + public String getPhoneNumber() { + return phoneNumber; + } + } + + /** + * Creates temporary Firebase user accounts for testing, and deletes them at the end of each + * test case. + */ + public static final class TemporaryUser extends ExternalResource { + + private final AbstractFirebaseAuth auth; + private final List users = new ArrayList<>(); + + public TemporaryUser(AbstractFirebaseAuth auth) { + this.auth = auth; + } + + public UserRecord create(UserRecord.CreateRequest request) throws FirebaseAuthException { + UserRecord user = auth.createUser(request); + registerUid(user.getUid()); + return user; + } + + public synchronized void registerUid(String uid) { + users.add(uid); + } + + @Override + protected synchronized void after() { + for (String uid : users) { + try { + auth.deleteUser(uid); + } catch (Exception ignore) { + // Ignore + } + } + + users.clear(); + } + } +} + diff --git a/src/test/java/com/google/firebase/auth/internal/FirebaseTokenFactoryTest.java b/src/test/java/com/google/firebase/auth/internal/FirebaseTokenFactoryTest.java index 71b95bee8..1c85ceeee 100644 --- a/src/test/java/com/google/firebase/auth/internal/FirebaseTokenFactoryTest.java +++ b/src/test/java/com/google/firebase/auth/internal/FirebaseTokenFactoryTest.java @@ -18,6 +18,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import com.google.api.client.json.GenericJson; @@ -45,6 +46,7 @@ public class FirebaseTokenFactoryTest { private static final String USER_ID = "fuber"; private static final GenericJson EXTRA_CLAIMS = new GenericJson(); private static final String ISSUER = "test-484@mg-test-1210.iam.gserviceaccount.com"; + private static final String TENANT_ID = "tenant-id"; static { EXTRA_CLAIMS.set("one", 2).set("three", "four").setFactory(FACTORY); @@ -71,6 +73,7 @@ public void checkSignatureForToken() throws Exception { assertEquals(USER_ID, signedJwt.getPayload().getUid()); assertEquals(2L, signedJwt.getPayload().getIssuedAtTimeSeconds().longValue()); assertTrue(TestUtils.verifySignature(signedJwt, ImmutableList.of(keys.getPublic()))); + assertNull(signedJwt.getPayload().getTenantId()); jwt = tokenFactory.createSignedCustomAuthTokenForUser(USER_ID); signedJwt = FirebaseCustomAuthToken.parse(FACTORY, jwt); @@ -80,6 +83,23 @@ public void checkSignatureForToken() throws Exception { assertEquals(USER_ID, signedJwt.getPayload().getUid()); assertEquals(2L, signedJwt.getPayload().getIssuedAtTimeSeconds().longValue()); assertTrue(TestUtils.verifySignature(signedJwt, ImmutableList.of(keys.getPublic()))); + assertNull(signedJwt.getPayload().getTenantId()); + } + + @Test + public void tokenWithTenantId() throws Exception { + KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); + keyGen.initialize(512); + KeyPair keys = keyGen.genKeyPair(); + FixedClock clock = new FixedClock(2002L); + CryptoSigner cryptoSigner = new TestCryptoSigner(keys.getPrivate()); + FirebaseTokenFactory tokenFactory = + new FirebaseTokenFactory(FACTORY, clock, cryptoSigner, TENANT_ID); + + String jwt = tokenFactory.createSignedCustomAuthTokenForUser(USER_ID); + FirebaseCustomAuthToken signedJwt = FirebaseCustomAuthToken.parse(FACTORY, jwt); + + assertEquals(TENANT_ID, signedJwt.getPayload().getTenantId()); } @Test diff --git a/src/test/java/com/google/firebase/auth/internal/ListOidcProviderConfigsResponseTest.java b/src/test/java/com/google/firebase/auth/internal/ListOidcProviderConfigsResponseTest.java new file mode 100644 index 000000000..fbe587e56 --- /dev/null +++ b/src/test/java/com/google/firebase/auth/internal/ListOidcProviderConfigsResponseTest.java @@ -0,0 +1,70 @@ +/* + * Copyright 2020 Google LLC + * + * 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.internal; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import com.google.api.client.googleapis.util.Utils; +import com.google.api.client.json.JsonFactory; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.firebase.auth.OidcProviderConfig; + +import org.junit.Test; + +public class ListOidcProviderConfigsResponseTest { + + private static final JsonFactory JSON_FACTORY = Utils.getDefaultJsonFactory(); + + @Test + public void testDefaultValues() throws Exception { + ListOidcProviderConfigsResponse response = new ListOidcProviderConfigsResponse(); + + assertEquals(0, response.getProviderConfigs().size()); + assertFalse(response.hasProviderConfigs()); + assertEquals("", response.getPageToken()); + } + + @Test + public void testEmptyTenantList() throws Exception { + ListOidcProviderConfigsResponse response = + new ListOidcProviderConfigsResponse(ImmutableList.of(), "PAGE_TOKEN"); + + assertEquals(0, response.getProviderConfigs().size()); + assertFalse(response.hasProviderConfigs()); + } + + @Test + public void testDeserialization() throws Exception { + String json = JSON_FACTORY.toString( + ImmutableMap.of( + "oauthIdpConfigs", ImmutableList.of( + ImmutableMap.of("name", "projects/projectId/oauthIdpConfigs/oidc.provider-id-1"), + ImmutableMap.of("name", "projects/projectId/oauthIdpConfigs/oidc.provider-id-2")), + "nextPageToken", "PAGE_TOKEN")); + ListOidcProviderConfigsResponse response = + JSON_FACTORY.fromString(json, ListOidcProviderConfigsResponse.class); + + assertEquals(2, response.getProviderConfigs().size()); + assertEquals("oidc.provider-id-1", response.getProviderConfigs().get(0).getProviderId()); + assertEquals("oidc.provider-id-2", response.getProviderConfigs().get(1).getProviderId()); + assertTrue(response.hasProviderConfigs()); + assertEquals("PAGE_TOKEN", response.getPageToken()); + } +} diff --git a/src/test/java/com/google/firebase/auth/internal/ListSamlProviderConfigsResponseTest.java b/src/test/java/com/google/firebase/auth/internal/ListSamlProviderConfigsResponseTest.java new file mode 100644 index 000000000..6950fe352 --- /dev/null +++ b/src/test/java/com/google/firebase/auth/internal/ListSamlProviderConfigsResponseTest.java @@ -0,0 +1,70 @@ +/* + * Copyright 2020 Google LLC + * + * 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.internal; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import com.google.api.client.googleapis.util.Utils; +import com.google.api.client.json.JsonFactory; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.firebase.auth.SamlProviderConfig; + +import org.junit.Test; + +public class ListSamlProviderConfigsResponseTest { + + private static final JsonFactory JSON_FACTORY = Utils.getDefaultJsonFactory(); + + @Test + public void testDefaultValues() throws Exception { + ListSamlProviderConfigsResponse response = new ListSamlProviderConfigsResponse(); + + assertEquals(0, response.getProviderConfigs().size()); + assertFalse(response.hasProviderConfigs()); + assertEquals("", response.getPageToken()); + } + + @Test + public void testEmptyTenantList() throws Exception { + ListSamlProviderConfigsResponse response = + new ListSamlProviderConfigsResponse(ImmutableList.of(), "PAGE_TOKEN"); + + assertEquals(0, response.getProviderConfigs().size()); + assertFalse(response.hasProviderConfigs()); + } + + @Test + public void testDeserialization() throws Exception { + String json = JSON_FACTORY.toString( + ImmutableMap.of( + "inboundSamlConfigs", ImmutableList.of( + ImmutableMap.of("name", "projects/projectId/inboundSamlConfigs/saml.provider-id-1"), + ImmutableMap.of("name", "projects/projectId/inboundSamlConfigs/saml.provider-id-2")), + "nextPageToken", "PAGE_TOKEN")); + ListSamlProviderConfigsResponse response = + JSON_FACTORY.fromString(json, ListSamlProviderConfigsResponse.class); + + assertEquals(2, response.getProviderConfigs().size()); + assertEquals("saml.provider-id-1", response.getProviderConfigs().get(0).getProviderId()); + assertEquals("saml.provider-id-2", response.getProviderConfigs().get(1).getProviderId()); + assertTrue(response.hasProviderConfigs()); + assertEquals("PAGE_TOKEN", response.getPageToken()); + } +} diff --git a/src/test/java/com/google/firebase/auth/internal/ListTenantsResponseTest.java b/src/test/java/com/google/firebase/auth/internal/ListTenantsResponseTest.java new file mode 100644 index 000000000..7d65966bb --- /dev/null +++ b/src/test/java/com/google/firebase/auth/internal/ListTenantsResponseTest.java @@ -0,0 +1,69 @@ +/* + * Copyright 2020 Google LLC + * + * 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.internal; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import com.google.api.client.googleapis.util.Utils; +import com.google.api.client.json.JsonFactory; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.firebase.auth.multitenancy.Tenant; + +import org.junit.Test; + +public class ListTenantsResponseTest { + + private static final JsonFactory JSON_FACTORY = Utils.getDefaultJsonFactory(); + + @Test + public void testDefaultValues() throws Exception { + ListTenantsResponse response = new ListTenantsResponse(); + + assertEquals(0, response.getTenants().size()); + assertFalse(response.hasTenants()); + assertEquals("", response.getPageToken()); + } + + @Test + public void testEmptyTenantList() throws Exception { + ListTenantsResponse response = + new ListTenantsResponse(ImmutableList.of(), "PAGE_TOKEN"); + + assertEquals(0, response.getTenants().size()); + assertFalse(response.hasTenants()); + } + + @Test + public void testDeserialization() throws Exception { + String json = JSON_FACTORY.toString( + ImmutableMap.of( + "tenants", ImmutableList.of( + ImmutableMap.of("name", "projects/project-id/resource/TENANT_1"), + ImmutableMap.of("name", "projects/project-id/resource/TENANT_2")), + "pageToken", "PAGE_TOKEN")); + ListTenantsResponse response = JSON_FACTORY.fromString(json, ListTenantsResponse.class); + + assertEquals(2, response.getTenants().size()); + assertEquals("TENANT_1", response.getTenants().get(0).getTenantId()); + assertEquals("TENANT_2", response.getTenants().get(1).getTenantId()); + assertTrue(response.hasTenants()); + assertEquals("PAGE_TOKEN", response.getPageToken()); + } +} diff --git a/src/test/java/com/google/firebase/auth/multitenancy/FirebaseTenantClientTest.java b/src/test/java/com/google/firebase/auth/multitenancy/FirebaseTenantClientTest.java new file mode 100644 index 000000000..36660dc28 --- /dev/null +++ b/src/test/java/com/google/firebase/auth/multitenancy/FirebaseTenantClientTest.java @@ -0,0 +1,363 @@ +/* + * Copyright 2020 Google LLC + * + * 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.multitenancy; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import com.google.api.client.googleapis.util.Utils; +import com.google.api.client.http.GenericUrl; +import com.google.api.client.http.HttpHeaders; +import com.google.api.client.http.HttpRequest; +import com.google.api.client.json.GenericJson; +import com.google.api.client.json.JsonFactory; +import com.google.api.client.testing.http.MockHttpTransport; +import com.google.api.client.testing.http.MockLowLevelHttpResponse; +import com.google.auth.oauth2.GoogleCredentials; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.TestOnlyImplFirebaseTrampolines; +import com.google.firebase.auth.FirebaseAuth; +import com.google.firebase.auth.FirebaseAuthException; +import com.google.firebase.auth.MockGoogleCredentials; +import com.google.firebase.auth.internal.AuthHttpClient; +import com.google.firebase.internal.SdkUtils; +import com.google.firebase.testing.MultiRequestMockHttpTransport; +import com.google.firebase.testing.TestResponseInterceptor; +import com.google.firebase.testing.TestUtils; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import org.junit.After; +import org.junit.Test; + +public class FirebaseTenantClientTest { + + private static final JsonFactory JSON_FACTORY = Utils.getDefaultJsonFactory(); + + private static final String TEST_TOKEN = "token"; + + private static final GoogleCredentials credentials = new MockGoogleCredentials(TEST_TOKEN); + + private static final String PROJECT_BASE_URL = + "https://identitytoolkit.googleapis.com/v2/projects/test-project-id"; + + private static final String TENANTS_BASE_URL = PROJECT_BASE_URL + "/tenants"; + + @After + public void tearDown() { + TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); + } + + @Test + public void testGetTenant() throws Exception { + TestResponseInterceptor interceptor = initializeAppForTenantManagement( + TestUtils.loadResource("tenant.json")); + + Tenant tenant = FirebaseAuth.getInstance().getTenantManager().getTenant("TENANT_1"); + + checkTenant(tenant, "TENANT_1"); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "GET", TENANTS_BASE_URL + "/TENANT_1"); + } + + @Test + public void testGetTenantWithNotFoundError() { + TestResponseInterceptor interceptor = + initializeAppForTenantManagementWithStatusCode(404, + "{\"error\": {\"message\": \"TENANT_NOT_FOUND\"}}"); + try { + FirebaseAuth.getInstance().getTenantManager().getTenant("UNKNOWN"); + fail("No error thrown for invalid response"); + } catch (FirebaseAuthException e) { + assertEquals(AuthHttpClient.TENANT_NOT_FOUND_ERROR, e.getErrorCode()); + } + checkUrl(interceptor, "GET", TENANTS_BASE_URL + "/UNKNOWN"); + } + + @Test + public void testListTenants() throws Exception { + final TestResponseInterceptor interceptor = initializeAppForTenantManagement( + TestUtils.loadResource("listTenants.json")); + + ListTenantsPage page = FirebaseAuth.getInstance().getTenantManager().listTenants(null, 99); + + ImmutableList tenants = ImmutableList.copyOf(page.getValues()); + assertEquals(2, tenants.size()); + checkTenant(tenants.get(0), "TENANT_1"); + checkTenant(tenants.get(1), "TENANT_2"); + assertEquals("", page.getNextPageToken()); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "GET", TENANTS_BASE_URL); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals(99, url.getFirst("pageSize")); + assertNull(url.getFirst("pageToken")); + } + + @Test + public void testListTenantsWithPageToken() throws Exception { + final TestResponseInterceptor interceptor = initializeAppForTenantManagement( + TestUtils.loadResource("listTenants.json")); + + ListTenantsPage page = FirebaseAuth.getInstance().getTenantManager().listTenants("token", 99); + + ImmutableList tenants = ImmutableList.copyOf(page.getValues()); + assertEquals(2, tenants.size()); + checkTenant(tenants.get(0), "TENANT_1"); + checkTenant(tenants.get(1), "TENANT_2"); + assertEquals("", page.getNextPageToken()); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "GET", TENANTS_BASE_URL); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals(99, url.getFirst("pageSize")); + assertEquals("token", url.getFirst("pageToken")); + } + + @Test + public void testListZeroTenants() throws Exception { + final TestResponseInterceptor interceptor = initializeAppForTenantManagement("{}"); + + ListTenantsPage page = FirebaseAuth.getInstance().getTenantManager().listTenants(null); + + assertTrue(Iterables.isEmpty(page.getValues())); + assertEquals("", page.getNextPageToken()); + checkRequestHeaders(interceptor); + } + + @Test + public void testCreateTenant() throws Exception { + TestResponseInterceptor interceptor = initializeAppForTenantManagement( + TestUtils.loadResource("tenant.json")); + Tenant.CreateRequest request = new Tenant.CreateRequest() + .setDisplayName("DISPLAY_NAME") + .setPasswordSignInAllowed(true) + .setEmailLinkSignInEnabled(false); + + Tenant tenant = FirebaseAuth.getInstance().getTenantManager().createTenant(request); + + checkTenant(tenant, "TENANT_1"); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "POST", TENANTS_BASE_URL); + GenericJson parsed = parseRequestContent(interceptor); + assertEquals("DISPLAY_NAME", parsed.get("displayName")); + assertEquals(true, parsed.get("allowPasswordSignup")); + assertEquals(false, parsed.get("enableEmailLinkSignin")); + } + + @Test + public void testCreateTenantMinimal() throws Exception { + TestResponseInterceptor interceptor = initializeAppForTenantManagement( + TestUtils.loadResource("tenant.json")); + Tenant.CreateRequest request = new Tenant.CreateRequest(); + + Tenant tenant = FirebaseAuth.getInstance().getTenantManager().createTenant(request); + + checkTenant(tenant, "TENANT_1"); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "POST", TENANTS_BASE_URL); + GenericJson parsed = parseRequestContent(interceptor); + assertNull(parsed.get("displayName")); + assertNull(parsed.get("allowPasswordSignup")); + assertNull(parsed.get("enableEmailLinkSignin")); + } + + @Test + public void testCreateTenantError() { + TestResponseInterceptor interceptor = + initializeAppForTenantManagementWithStatusCode(404, + "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}"); + try { + FirebaseAuth.getInstance().getTenantManager().createTenant(new Tenant.CreateRequest()); + fail("No error thrown for invalid response"); + } catch (FirebaseAuthException e) { + assertEquals(AuthHttpClient.INTERNAL_ERROR, e.getErrorCode()); + } + checkUrl(interceptor, "POST", TENANTS_BASE_URL); + } + + @Test + public void testUpdateTenant() throws Exception { + TestResponseInterceptor interceptor = initializeAppForTenantManagement( + TestUtils.loadResource("tenant.json")); + Tenant.UpdateRequest request = new Tenant.UpdateRequest("TENANT_1") + .setDisplayName("DISPLAY_NAME") + .setPasswordSignInAllowed(true) + .setEmailLinkSignInEnabled(false); + + Tenant tenant = FirebaseAuth.getInstance().getTenantManager().updateTenant(request); + + checkTenant(tenant, "TENANT_1"); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "PATCH", TENANTS_BASE_URL + "/TENANT_1"); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals("allowPasswordSignup,displayName,enableEmailLinkSignin", + url.getFirst("updateMask")); + GenericJson parsed = parseRequestContent(interceptor); + assertEquals("DISPLAY_NAME", parsed.get("displayName")); + assertEquals(true, parsed.get("allowPasswordSignup")); + assertEquals(false, parsed.get("enableEmailLinkSignin")); + } + + @Test + public void testUpdateTenantMinimal() throws Exception { + TestResponseInterceptor interceptor = initializeAppForTenantManagement( + TestUtils.loadResource("tenant.json")); + Tenant.UpdateRequest request = + new Tenant.UpdateRequest("TENANT_1").setDisplayName("DISPLAY_NAME"); + + Tenant tenant = FirebaseAuth.getInstance().getTenantManager().updateTenant(request); + + checkTenant(tenant, "TENANT_1"); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "PATCH", TENANTS_BASE_URL + "/TENANT_1"); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals("displayName", url.getFirst("updateMask")); + GenericJson parsed = parseRequestContent(interceptor); + assertEquals("DISPLAY_NAME", parsed.get("displayName")); + assertNull(parsed.get("allowPasswordSignup")); + assertNull(parsed.get("enableEmailLinkSignin")); + } + + @Test + public void testUpdateTenantNoValues() throws Exception { + initializeAppForTenantManagement(TestUtils.loadResource("tenant.json")); + TenantManager tenantManager = FirebaseAuth.getInstance().getTenantManager(); + try { + tenantManager.updateTenant(new Tenant.UpdateRequest("TENANT_1")); + fail("No error thrown for empty tenant update"); + } catch (IllegalArgumentException e) { + // expected + } + } + + @Test + public void testUpdateTenantError() { + TestResponseInterceptor interceptor = + initializeAppForTenantManagementWithStatusCode(404, + "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}"); + Tenant.UpdateRequest request = + new Tenant.UpdateRequest("TENANT_1").setDisplayName("DISPLAY_NAME"); + try { + FirebaseAuth.getInstance().getTenantManager().updateTenant(request); + fail("No error thrown for invalid response"); + } catch (FirebaseAuthException e) { + assertEquals(AuthHttpClient.INTERNAL_ERROR, e.getErrorCode()); + } + checkUrl(interceptor, "PATCH", TENANTS_BASE_URL + "/TENANT_1"); + } + + @Test + public void testDeleteTenant() throws Exception { + TestResponseInterceptor interceptor = initializeAppForTenantManagement("{}"); + + FirebaseAuth.getInstance().getTenantManager().deleteTenant("TENANT_1"); + + checkRequestHeaders(interceptor); + checkUrl(interceptor, "DELETE", TENANTS_BASE_URL + "/TENANT_1"); + } + + @Test + public void testDeleteTenantWithNotFoundError() { + TestResponseInterceptor interceptor = + initializeAppForTenantManagementWithStatusCode(404, + "{\"error\": {\"message\": \"TENANT_NOT_FOUND\"}}"); + try { + FirebaseAuth.getInstance().getTenantManager().deleteTenant("UNKNOWN"); + fail("No error thrown for invalid response"); + } catch (FirebaseAuthException e) { + assertEquals(AuthHttpClient.TENANT_NOT_FOUND_ERROR, e.getErrorCode()); + } + checkUrl(interceptor, "DELETE", TENANTS_BASE_URL + "/UNKNOWN"); + } + + private static void checkTenant(Tenant tenant, String tenantId) { + assertEquals(tenantId, tenant.getTenantId()); + assertEquals("DISPLAY_NAME", tenant.getDisplayName()); + assertTrue(tenant.isPasswordSignInAllowed()); + assertFalse(tenant.isEmailLinkSignInEnabled()); + } + + private static void checkRequestHeaders(TestResponseInterceptor interceptor) { + HttpHeaders headers = interceptor.getResponse().getRequest().getHeaders(); + String auth = "Bearer " + TEST_TOKEN; + assertEquals(auth, headers.getFirstHeaderStringValue("Authorization")); + + String clientVersion = "Java/Admin/" + SdkUtils.getVersion(); + assertEquals(clientVersion, headers.getFirstHeaderStringValue("X-Client-Version")); + } + + private static void checkUrl(TestResponseInterceptor interceptor, String method, String url) { + HttpRequest request = interceptor.getResponse().getRequest(); + if (method.equals("PATCH")) { + assertEquals("PATCH", + request.getHeaders().getFirstHeaderStringValue("X-HTTP-Method-Override")); + assertEquals("POST", request.getRequestMethod()); + } else { + assertEquals(method, request.getRequestMethod()); + } + assertEquals(url, request.getUrl().toString().split("\\?")[0]); + } + + private static TestResponseInterceptor initializeAppForTenantManagement(String... responses) { + initializeAppWithResponses(responses); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + FirebaseAuth.getInstance().getTenantManager().setInterceptor(interceptor); + return interceptor; + } + + private static TestResponseInterceptor initializeAppForTenantManagementWithStatusCode( + int statusCode, String response) { + FirebaseApp.initializeApp(new FirebaseOptions.Builder() + .setCredentials(credentials) + .setHttpTransport( + new MockHttpTransport.Builder() + .setLowLevelHttpResponse( + new MockLowLevelHttpResponse().setContent(response).setStatusCode(statusCode)) + .build()) + .setProjectId("test-project-id") + .build()); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + FirebaseAuth.getInstance().getTenantManager().setInterceptor(interceptor); + return interceptor; + } + + private static void initializeAppWithResponses(String... responses) { + List mocks = new ArrayList<>(); + for (String response : responses) { + mocks.add(new MockLowLevelHttpResponse().setContent(response)); + } + MockHttpTransport transport = new MultiRequestMockHttpTransport(mocks); + FirebaseApp.initializeApp(new FirebaseOptions.Builder() + .setCredentials(credentials) + .setHttpTransport(transport) + .setProjectId("test-project-id") + .build()); + } + + private static GenericJson parseRequestContent(TestResponseInterceptor interceptor) + throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + interceptor.getResponse().getRequest().getContent().writeTo(out); + return JSON_FACTORY.fromString(new String(out.toByteArray()), GenericJson.class); + } +} diff --git a/src/test/java/com/google/firebase/auth/multitenancy/ListTenantsPageTest.java b/src/test/java/com/google/firebase/auth/multitenancy/ListTenantsPageTest.java new file mode 100644 index 000000000..10830592f --- /dev/null +++ b/src/test/java/com/google/firebase/auth/multitenancy/ListTenantsPageTest.java @@ -0,0 +1,349 @@ +/* + * Copyright 2020 Google LLC + * + * 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.multitenancy; + +import static junit.framework.TestCase.assertTrue; +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; + +import com.google.api.client.googleapis.util.Utils; +import com.google.common.collect.ImmutableList; +import com.google.firebase.auth.FirebaseAuthException; +import com.google.firebase.auth.internal.ListTenantsResponse; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; + +import org.junit.Test; + +public class ListTenantsPageTest { + + @Test + public void testSinglePage() throws FirebaseAuthException, IOException { + TestTenantSource source = new TestTenantSource(3); + ListTenantsPage page = new ListTenantsPage.PageFactory(source).create(); + assertFalse(page.hasNextPage()); + assertEquals(ListTenantsPage.END_OF_LIST, page.getNextPageToken()); + assertNull(page.getNextPage()); + + ImmutableList tenants = ImmutableList.copyOf(page.getValues()); + assertEquals(3, tenants.size()); + for (int i = 0; i < 3; i++) { + assertEquals("tenant" + i, tenants.get(i).getTenantId()); + } + assertEquals(1, source.calls.size()); + assertNull(source.calls.get(0)); + } + + @Test + public void testMultiplePages() throws FirebaseAuthException, IOException { + ListTenantsResponse response = new ListTenantsResponse( + ImmutableList.of(newTenant("tenant0"), newTenant("tenant1"), newTenant("tenant2")), + "token"); + TestTenantSource source = new TestTenantSource(response); + ListTenantsPage page1 = new ListTenantsPage.PageFactory(source).create(); + assertTrue(page1.hasNextPage()); + assertEquals("token", page1.getNextPageToken()); + ImmutableList tenants = ImmutableList.copyOf(page1.getValues()); + assertEquals(3, tenants.size()); + for (int i = 0; i < 3; i++) { + assertEquals("tenant" + i, tenants.get(i).getTenantId()); + } + + response = new ListTenantsResponse( + ImmutableList.of(newTenant("tenant3"), newTenant("tenant4"), newTenant("tenant5")), + ListTenantsPage.END_OF_LIST); + source.response = response; + ListTenantsPage page2 = page1.getNextPage(); + assertFalse(page2.hasNextPage()); + assertEquals(ListTenantsPage.END_OF_LIST, page2.getNextPageToken()); + tenants = ImmutableList.copyOf(page2.getValues()); + assertEquals(3, tenants.size()); + for (int i = 3; i < 6; i++) { + assertEquals("tenant" + i, tenants.get(i - 3).getTenantId()); + } + + assertEquals(2, source.calls.size()); + assertNull(source.calls.get(0)); + assertEquals("token", source.calls.get(1)); + + // Should iterate all tenants from both pages + int iterations = 0; + for (Tenant tenant : page1.iterateAll()) { + iterations++; + } + assertEquals(6, iterations); + assertEquals(3, source.calls.size()); + assertEquals("token", source.calls.get(2)); + + // Should only iterate tenants in the last page + iterations = 0; + for (Tenant tenant : page2.iterateAll()) { + iterations++; + } + assertEquals(3, iterations); + assertEquals(3, source.calls.size()); + } + + @Test + public void testListTenantsIterable() throws FirebaseAuthException, IOException { + TestTenantSource source = new TestTenantSource(3); + ListTenantsPage page = new ListTenantsPage.PageFactory(source).create(); + Iterable tenants = page.iterateAll(); + + int iterations = 0; + for (Tenant tenant : tenants) { + assertEquals("tenant" + iterations, tenant.getTenantId()); + iterations++; + } + assertEquals(3, iterations); + assertEquals(1, source.calls.size()); + assertNull(source.calls.get(0)); + + // Should result in a new iterator + iterations = 0; + for (Tenant tenant : tenants) { + assertEquals("tenant" + iterations, tenant.getTenantId()); + iterations++; + } + assertEquals(3, iterations); + assertEquals(1, source.calls.size()); + assertNull(source.calls.get(0)); + } + + @Test + public void testListTenantsIterator() throws FirebaseAuthException, IOException { + TestTenantSource source = new TestTenantSource(3); + ListTenantsPage page = new ListTenantsPage.PageFactory(source).create(); + Iterable tenants = page.iterateAll(); + Iterator iterator = tenants.iterator(); + int iterations = 0; + while (iterator.hasNext()) { + assertEquals("tenant" + iterations, iterator.next().getTenantId()); + iterations++; + } + assertEquals(3, iterations); + assertEquals(1, source.calls.size()); + assertNull(source.calls.get(0)); + + while (iterator.hasNext()) { + fail("Should not be able to to iterate any more"); + } + try { + iterator.next(); + fail("Should not be able to iterate any more"); + } catch (NoSuchElementException expected) { + // expected + } + assertEquals(1, source.calls.size()); + } + + @Test + public void testListTenantsPagedIterable() throws FirebaseAuthException, IOException { + ListTenantsResponse response = new ListTenantsResponse( + ImmutableList.of(newTenant("tenant0"), newTenant("tenant1"), newTenant("tenant2")), + "token"); + TestTenantSource source = new TestTenantSource(response); + ListTenantsPage page = new ListTenantsPage.PageFactory(source).create(); + int iterations = 0; + for (Tenant tenant : page.iterateAll()) { + assertEquals("tenant" + iterations, tenant.getTenantId()); + iterations++; + if (iterations == 3) { + assertEquals(1, source.calls.size()); + assertNull(source.calls.get(0)); + response = new ListTenantsResponse( + ImmutableList.of(newTenant("tenant3"), newTenant("tenant4"), newTenant("tenant5")), + ListTenantsPage.END_OF_LIST); + source.response = response; + } + } + + assertEquals(6, iterations); + assertEquals(2, source.calls.size()); + assertEquals("token", source.calls.get(1)); + } + + @Test + public void testListTenantsPagedIterator() throws FirebaseAuthException, IOException { + ListTenantsResponse response = new ListTenantsResponse( + ImmutableList.of(newTenant("tenant0"), newTenant("tenant1"), newTenant("tenant2")), + "token"); + TestTenantSource source = new TestTenantSource(response); + ListTenantsPage page = new ListTenantsPage.PageFactory(source).create(); + Iterator tenants = page.iterateAll().iterator(); + int iterations = 0; + while (tenants.hasNext()) { + assertEquals("tenant" + iterations, tenants.next().getTenantId()); + iterations++; + if (iterations == 3) { + assertEquals(1, source.calls.size()); + assertNull(source.calls.get(0)); + response = new ListTenantsResponse( + ImmutableList.of(newTenant("tenant3"), newTenant("tenant4"), newTenant("tenant5")), + ListTenantsPage.END_OF_LIST); + source.response = response; + } + } + + assertEquals(6, iterations); + assertEquals(2, source.calls.size()); + assertEquals("token", source.calls.get(1)); + assertFalse(tenants.hasNext()); + try { + tenants.next(); + } catch (NoSuchElementException e) { + // expected + } + } + + @Test + public void testPageWithNoTenants() throws FirebaseAuthException { + ListTenantsResponse response = new ListTenantsResponse( + ImmutableList.of(), + ListTenantsPage.END_OF_LIST); + TestTenantSource source = new TestTenantSource(response); + ListTenantsPage page = new ListTenantsPage.PageFactory(source).create(); + assertFalse(page.hasNextPage()); + assertEquals(ListTenantsPage.END_OF_LIST, page.getNextPageToken()); + assertNull(page.getNextPage()); + assertEquals(0, ImmutableList.copyOf(page.getValues()).size()); + assertEquals(1, source.calls.size()); + } + + @Test + public void testIterableWithNoTenants() throws FirebaseAuthException { + ListTenantsResponse response = new ListTenantsResponse( + ImmutableList.of(), + ListTenantsPage.END_OF_LIST); + TestTenantSource source = new TestTenantSource(response); + ListTenantsPage page = new ListTenantsPage.PageFactory(source).create(); + for (Tenant tenant : page.iterateAll()) { + fail("Should not be able to iterate, but got: " + tenant); + } + assertEquals(1, source.calls.size()); + } + + @Test + public void testIteratorWithNoTenants() throws FirebaseAuthException { + ListTenantsResponse response = new ListTenantsResponse( + ImmutableList.of(), + ListTenantsPage.END_OF_LIST); + TestTenantSource source = new TestTenantSource(response); + + ListTenantsPage page = new ListTenantsPage.PageFactory(source).create(); + Iterator iterator = page.iterateAll().iterator(); + while (iterator.hasNext()) { + fail("Should not be able to iterate"); + } + assertEquals(1, source.calls.size()); + } + + @Test + public void testRemove() throws FirebaseAuthException, IOException { + ListTenantsResponse response = new ListTenantsResponse( + ImmutableList.of(newTenant("tenant1")), + ListTenantsPage.END_OF_LIST); + TestTenantSource source = new TestTenantSource(response); + + ListTenantsPage page = new ListTenantsPage.PageFactory(source).create(); + Iterator iterator = page.iterateAll().iterator(); + while (iterator.hasNext()) { + assertNotNull(iterator.next()); + try { + iterator.remove(); + } catch (UnsupportedOperationException expected) { + // expected + } + } + } + + @Test(expected = NullPointerException.class) + public void testNullSource() { + new ListTenantsPage.PageFactory(null); + } + + @Test + public void testInvalidPageToken() throws IOException { + TestTenantSource source = new TestTenantSource(1); + try { + new ListTenantsPage.PageFactory(source, 1000, ""); + fail("No error thrown for empty page token"); + } catch (IllegalArgumentException expected) { + // expected + } + } + + @Test + public void testInvalidMaxResults() throws IOException { + TestTenantSource source = new TestTenantSource(1); + try { + new ListTenantsPage.PageFactory(source, 1001, ""); + fail("No error thrown for maxResult > 1000"); + } catch (IllegalArgumentException expected) { + // expected + } + + try { + new ListTenantsPage.PageFactory(source, 0, "next"); + fail("No error thrown for maxResult = 0"); + } catch (IllegalArgumentException expected) { + // expected + } + + try { + new ListTenantsPage.PageFactory(source, -1, "next"); + fail("No error thrown for maxResult < 0"); + } catch (IllegalArgumentException expected) { + // expected + } + } + + private static Tenant newTenant(String tenantId) throws IOException { + return Utils.getDefaultJsonFactory().fromString( + String.format("{\"name\":\"%s\"}", tenantId), Tenant.class); + } + + private static class TestTenantSource implements ListTenantsPage.TenantSource { + + private ListTenantsResponse response; + private final List calls = new ArrayList<>(); + + TestTenantSource(int tenantCount) throws IOException { + ImmutableList.Builder tenants = ImmutableList.builder(); + for (int i = 0; i < tenantCount; i++) { + tenants.add(newTenant("tenant" + i)); + } + this.response = new ListTenantsResponse(tenants.build(), ListTenantsPage.END_OF_LIST); + } + + TestTenantSource(ListTenantsResponse response) { + this.response = response; + } + + @Override + public ListTenantsResponse fetch(int maxResults, String pageToken) { + calls.add(pageToken); + return response; + } + } +} diff --git a/src/test/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuthIT.java b/src/test/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuthIT.java new file mode 100644 index 000000000..61a841902 --- /dev/null +++ b/src/test/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuthIT.java @@ -0,0 +1,450 @@ +/* + * Copyright 2020 Google LLC + * + * 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.multitenancy; + +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.assertTrue; +import static org.junit.Assert.fail; + +import com.google.api.client.googleapis.util.Utils; +import com.google.api.client.http.GenericUrl; +import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.HttpResponse; +import com.google.api.client.http.HttpTransport; +import com.google.api.client.http.json.JsonHttpContent; +import com.google.api.client.json.GenericJson; +import com.google.api.client.json.JsonFactory; +import com.google.api.client.json.JsonObjectParser; +import com.google.api.core.ApiFuture; +import com.google.api.core.ApiFutureCallback; +import com.google.api.core.ApiFutures; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.firebase.FirebaseApp; +import com.google.firebase.auth.ExportedUserRecord; +import com.google.firebase.auth.FirebaseAuth; +import com.google.firebase.auth.FirebaseAuthException; +import com.google.firebase.auth.FirebaseToken; +import com.google.firebase.auth.ListProviderConfigsPage; +import com.google.firebase.auth.ListUsersPage; +import com.google.firebase.auth.OidcProviderConfig; +import com.google.firebase.auth.ProviderConfigTestUtils; +import com.google.firebase.auth.ProviderConfigTestUtils.TemporaryProviderConfig; +import com.google.firebase.auth.SamlProviderConfig; +import com.google.firebase.auth.UserRecord; +import com.google.firebase.auth.UserTestUtils; +import com.google.firebase.auth.UserTestUtils.RandomUser; +import com.google.firebase.auth.UserTestUtils.TemporaryUser; +import com.google.firebase.internal.Nullable; +import com.google.firebase.testing.IntegrationTestUtils; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Semaphore; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; + +public class TenantAwareFirebaseAuthIT { + + private static final String VERIFY_CUSTOM_TOKEN_URL = + "https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyCustomToken"; + private static final JsonFactory jsonFactory = Utils.getDefaultJsonFactory(); + private static final HttpTransport transport = Utils.getDefaultTransport(); + + private static TenantManager tenantManager; + private static TenantAwareFirebaseAuth tenantAwareAuth; + private static String tenantId; + + @Rule public final TemporaryUser temporaryUser = new TemporaryUser(tenantAwareAuth); + @Rule public final TemporaryProviderConfig temporaryProviderConfig = + new TemporaryProviderConfig(tenantAwareAuth); + + @BeforeClass + public static void setUpClass() throws Exception { + FirebaseApp masterApp = IntegrationTestUtils.ensureDefaultApp(); + tenantManager = FirebaseAuth.getInstance(masterApp).getTenantManager(); + Tenant.CreateRequest tenantCreateRequest = + new Tenant.CreateRequest().setDisplayName("DisplayName"); + tenantId = tenantManager.createTenant(tenantCreateRequest).getTenantId(); + tenantAwareAuth = tenantManager.getAuthForTenant(tenantId); + } + + @AfterClass + public static void tearDownClass() throws Exception { + tenantManager.deleteTenant(tenantId); + } + + @Test + public void testUserLifecycle() throws Exception { + // Create user + UserRecord userRecord = temporaryUser.create(new UserRecord.CreateRequest()); + String uid = userRecord.getUid(); + + // Get user + userRecord = tenantAwareAuth.getUserAsync(userRecord.getUid()).get(); + assertEquals(uid, userRecord.getUid()); + assertEquals(tenantId, userRecord.getTenantId()); + assertNull(userRecord.getDisplayName()); + assertNull(userRecord.getEmail()); + assertNull(userRecord.getPhoneNumber()); + assertNull(userRecord.getPhotoUrl()); + assertFalse(userRecord.isEmailVerified()); + assertFalse(userRecord.isDisabled()); + assertTrue(userRecord.getUserMetadata().getCreationTimestamp() > 0); + assertEquals(0, userRecord.getUserMetadata().getLastSignInTimestamp()); + assertEquals(0, userRecord.getProviderData().length); + assertTrue(userRecord.getCustomClaims().isEmpty()); + + // Update user + RandomUser randomUser = UserTestUtils.generateRandomUserInfo(); + UserRecord.UpdateRequest request = userRecord.updateRequest() + .setDisplayName("Updated Name") + .setEmail(randomUser.getEmail()) + .setPhoneNumber(randomUser.getPhoneNumber()) + .setPhotoUrl("https://example.com/photo.png") + .setEmailVerified(true) + .setPassword("secret"); + userRecord = tenantAwareAuth.updateUserAsync(request).get(); + assertEquals(uid, userRecord.getUid()); + assertEquals(tenantId, userRecord.getTenantId()); + assertEquals("Updated Name", userRecord.getDisplayName()); + assertEquals(randomUser.getEmail(), userRecord.getEmail()); + assertEquals(randomUser.getPhoneNumber(), userRecord.getPhoneNumber()); + assertEquals("https://example.com/photo.png", userRecord.getPhotoUrl()); + assertTrue(userRecord.isEmailVerified()); + assertFalse(userRecord.isDisabled()); + assertEquals(2, userRecord.getProviderData().length); + assertTrue(userRecord.getCustomClaims().isEmpty()); + + // Get user by email + userRecord = tenantAwareAuth.getUserByEmailAsync(userRecord.getEmail()).get(); + assertEquals(uid, userRecord.getUid()); + + // Disable user and remove properties + request = userRecord.updateRequest() + .setPhotoUrl(null) + .setDisplayName(null) + .setPhoneNumber(null) + .setDisabled(true); + userRecord = tenantAwareAuth.updateUserAsync(request).get(); + assertEquals(uid, userRecord.getUid()); + assertEquals(tenantId, userRecord.getTenantId()); + assertNull(userRecord.getDisplayName()); + assertEquals(randomUser.getEmail(), userRecord.getEmail()); + assertNull(userRecord.getPhoneNumber()); + assertNull(userRecord.getPhotoUrl()); + assertTrue(userRecord.isEmailVerified()); + assertTrue(userRecord.isDisabled()); + assertEquals(1, userRecord.getProviderData().length); + assertTrue(userRecord.getCustomClaims().isEmpty()); + + // Delete user + tenantAwareAuth.deleteUserAsync(userRecord.getUid()).get(); + UserTestUtils.assertUserDoesNotExist(tenantAwareAuth, userRecord.getUid()); + } + + @Test + public void testListUsers() throws Exception { + final List uids = new ArrayList<>(); + + for (int i = 0; i < 3; i++) { + UserRecord.CreateRequest createRequest = + new UserRecord.CreateRequest().setPassword("password"); + uids.add(temporaryUser.create(createRequest).getUid()); + } + + // Test list by batches + final AtomicInteger collected = new AtomicInteger(0); + ListUsersPage page = tenantAwareAuth.listUsersAsync(null).get(); + while (page != null) { + for (ExportedUserRecord user : page.getValues()) { + if (uids.contains(user.getUid())) { + collected.incrementAndGet(); + assertNotNull("Missing passwordHash field. A common cause would be " + + "forgetting to add the \"Firebase Authentication Admin\" permission. See " + + "instructions in CONTRIBUTING.md", user.getPasswordHash()); + assertNotNull(user.getPasswordSalt()); + assertEquals(tenantId, user.getTenantId()); + } + } + page = page.getNextPage(); + } + assertEquals(uids.size(), collected.get()); + + // Test iterate all + collected.set(0); + page = tenantAwareAuth.listUsersAsync(null).get(); + for (ExportedUserRecord user : page.iterateAll()) { + if (uids.contains(user.getUid())) { + collected.incrementAndGet(); + assertNotNull(user.getPasswordHash()); + assertNotNull(user.getPasswordSalt()); + assertEquals(tenantId, user.getTenantId()); + } + } + assertEquals(uids.size(), collected.get()); + + // Test iterate async + collected.set(0); + final Semaphore semaphore = new Semaphore(0); + final AtomicReference error = new AtomicReference<>(); + ApiFuture pageFuture = tenantAwareAuth.listUsersAsync(null); + ApiFutures.addCallback(pageFuture, new ApiFutureCallback() { + @Override + public void onFailure(Throwable t) { + error.set(t); + semaphore.release(); + } + + @Override + public void onSuccess(ListUsersPage result) { + for (ExportedUserRecord user : result.iterateAll()) { + if (uids.contains(user.getUid())) { + collected.incrementAndGet(); + assertNotNull(user.getPasswordHash()); + assertNotNull(user.getPasswordSalt()); + assertEquals(tenantId, user.getTenantId()); + } + } + semaphore.release(); + } + }, MoreExecutors.directExecutor()); + semaphore.acquire(); + assertEquals(uids.size(), collected.get()); + assertNull(error.get()); + } + + @Test + public void testCustomToken() throws Exception { + String customToken = tenantAwareAuth.createCustomTokenAsync("user1").get(); + String idToken = signInWithCustomToken(customToken, tenantId); + FirebaseToken decoded = tenantAwareAuth.verifyIdTokenAsync(idToken).get(); + assertEquals("user1", decoded.getUid()); + assertEquals(tenantId, decoded.getTenantId()); + } + + @Test + public void testVerifyTokenWithWrongTenantAwareClient() throws Exception { + String customToken = tenantAwareAuth.createCustomTokenAsync("user").get(); + String idToken = signInWithCustomToken(customToken, tenantId); + + try { + tenantManager.getAuthForTenant("OTHER").verifyIdTokenAsync(idToken).get(); + fail("No error thrown for verifying a token with the wrong tenant-aware client"); + } catch (ExecutionException e) { + assertTrue(e.getCause() instanceof FirebaseAuthException); + assertEquals("tenant-id-mismatch", + ((FirebaseAuthException) e.getCause()).getErrorCode()); + } + } + + @Test + public void testOidcProviderConfigLifecycle() throws Exception { + // Create provider config + String providerId = "oidc.provider-id"; + OidcProviderConfig config = + temporaryProviderConfig.createOidcProviderConfig( + new OidcProviderConfig.CreateRequest() + .setProviderId(providerId) + .setDisplayName("DisplayName") + .setEnabled(true) + .setClientId("ClientId") + .setIssuer("https://oidc.com/issuer")); + assertEquals(providerId, config.getProviderId()); + assertEquals("DisplayName", config.getDisplayName()); + assertEquals("ClientId", config.getClientId()); + assertEquals("https://oidc.com/issuer", config.getIssuer()); + + // Get provider config + config = tenantAwareAuth.getOidcProviderConfigAsync(providerId).get(); + assertEquals(providerId, config.getProviderId()); + assertEquals("DisplayName", config.getDisplayName()); + assertEquals("ClientId", config.getClientId()); + assertEquals("https://oidc.com/issuer", config.getIssuer()); + + // Update provider config + OidcProviderConfig.UpdateRequest updateRequest = + new OidcProviderConfig.UpdateRequest(providerId) + .setDisplayName("NewDisplayName") + .setEnabled(false) + .setClientId("NewClientId") + .setIssuer("https://oidc.com/new-issuer"); + config = tenantAwareAuth.updateOidcProviderConfigAsync(updateRequest).get(); + assertEquals(providerId, config.getProviderId()); + assertEquals("NewDisplayName", config.getDisplayName()); + assertFalse(config.isEnabled()); + assertEquals("NewClientId", config.getClientId()); + assertEquals("https://oidc.com/new-issuer", config.getIssuer()); + + // Delete provider config + temporaryProviderConfig.deleteOidcProviderConfig(providerId); + ProviderConfigTestUtils.assertOidcProviderConfigDoesNotExist(tenantAwareAuth, providerId); + } + + @Test + public void testListOidcProviderConfigs() throws Exception { + final List providerIds = new ArrayList<>(); + + // Create provider configs + for (int i = 0; i < 3; i++) { + String providerId = "oidc.provider-id" + i; + providerIds.add(providerId); + temporaryProviderConfig.createOidcProviderConfig( + new OidcProviderConfig.CreateRequest() + .setProviderId(providerId) + .setClientId("CLIENT_ID") + .setIssuer("https://oidc.com/issuer")); + } + + // List provider configs + // NOTE: We do not need to test all of the different ways we can iterate over the provider + // configs, since this testing is already performed in FirebaseAuthIT with the tenant-agnostic + // tests. + final AtomicInteger collected = new AtomicInteger(0); + ListProviderConfigsPage page = + tenantAwareAuth.listOidcProviderConfigsAsync(null).get(); + for (OidcProviderConfig providerConfig : page.iterateAll()) { + if (providerIds.contains(providerConfig.getProviderId())) { + collected.incrementAndGet(); + assertEquals("CLIENT_ID", providerConfig.getClientId()); + assertEquals("https://oidc.com/issuer", providerConfig.getIssuer()); + } + } + assertEquals(providerIds.size(), collected.get()); + } + + @Test + public void testSamlProviderConfigLifecycle() throws Exception { + // Create provider config + String providerId = "saml.provider-id"; + SamlProviderConfig config = temporaryProviderConfig.createSamlProviderConfig( + new SamlProviderConfig.CreateRequest() + .setProviderId(providerId) + .setDisplayName("DisplayName") + .setEnabled(true) + .setIdpEntityId("IDP_ENTITY_ID") + .setSsoUrl("https://example.com/login") + .addX509Certificate("certificate1") + .addX509Certificate("certificate2") + .setRpEntityId("RP_ENTITY_ID") + .setCallbackUrl("https://projectId.firebaseapp.com/__/auth/handler")); + assertEquals(providerId, config.getProviderId()); + assertEquals("DisplayName", config.getDisplayName()); + assertTrue(config.isEnabled()); + assertEquals("IDP_ENTITY_ID", config.getIdpEntityId()); + assertEquals("https://example.com/login", config.getSsoUrl()); + assertEquals(ImmutableList.of("certificate1", "certificate2"), config.getX509Certificates()); + assertEquals("RP_ENTITY_ID", config.getRpEntityId()); + assertEquals("https://projectId.firebaseapp.com/__/auth/handler", config.getCallbackUrl()); + + config = tenantAwareAuth.getSamlProviderConfig(providerId); + assertEquals(providerId, config.getProviderId()); + assertEquals("DisplayName", config.getDisplayName()); + assertTrue(config.isEnabled()); + assertEquals("IDP_ENTITY_ID", config.getIdpEntityId()); + assertEquals("https://example.com/login", config.getSsoUrl()); + assertEquals(ImmutableList.of("certificate1", "certificate2"), config.getX509Certificates()); + assertEquals("RP_ENTITY_ID", config.getRpEntityId()); + assertEquals("https://projectId.firebaseapp.com/__/auth/handler", config.getCallbackUrl()); + + // Update provider config + SamlProviderConfig.UpdateRequest updateRequest = + new SamlProviderConfig.UpdateRequest(providerId) + .setDisplayName("NewDisplayName") + .setEnabled(false) + .addX509Certificate("certificate"); + config = tenantAwareAuth.updateSamlProviderConfigAsync(updateRequest).get(); + assertEquals(providerId, config.getProviderId()); + assertEquals("NewDisplayName", config.getDisplayName()); + assertFalse(config.isEnabled()); + assertEquals(ImmutableList.of("certificate"), config.getX509Certificates()); + + // Delete provider config + temporaryProviderConfig.deleteSamlProviderConfig(providerId); + ProviderConfigTestUtils.assertSamlProviderConfigDoesNotExist(tenantAwareAuth, providerId); + } + + @Test + public void testListSamlProviderConfigs() throws Exception { + final List providerIds = new ArrayList<>(); + + // Create provider configs + for (int i = 0; i < 3; i++) { + String providerId = "saml.provider-id" + i; + providerIds.add(providerId); + temporaryProviderConfig.createSamlProviderConfig( + new SamlProviderConfig.CreateRequest() + .setProviderId(providerId) + .setIdpEntityId("IDP_ENTITY_ID") + .setSsoUrl("https://example.com/login") + .addX509Certificate("certificate") + .setRpEntityId("RP_ENTITY_ID") + .setCallbackUrl("https://projectId.firebaseapp.com/__/auth/handler")); + } + + // List provider configs + // NOTE: We do not need to test all of the different ways we can iterate over the provider + // configs, since this testing is already performed in FirebaseAuthIT with the tenant-agnostic + // tests. + final AtomicInteger collected = new AtomicInteger(0); + ListProviderConfigsPage page = + tenantAwareAuth.listSamlProviderConfigsAsync(null).get(); + for (SamlProviderConfig config : page.iterateAll()) { + if (providerIds.contains(config.getProviderId())) { + collected.incrementAndGet(); + assertEquals("IDP_ENTITY_ID", config.getIdpEntityId()); + assertEquals("https://example.com/login", config.getSsoUrl()); + assertEquals(ImmutableList.of("certificate"), config.getX509Certificates()); + assertEquals("RP_ENTITY_ID", config.getRpEntityId()); + assertEquals("https://projectId.firebaseapp.com/__/auth/handler", config.getCallbackUrl()); + } + } + assertEquals(providerIds.size(), collected.get()); + } + + private String signInWithCustomToken( + String customToken, @Nullable String tenantId) throws IOException { + final GenericUrl url = new GenericUrl(VERIFY_CUSTOM_TOKEN_URL + "?key=" + + IntegrationTestUtils.getApiKey()); + ImmutableMap.Builder content = ImmutableMap.builder(); + content.put("token", customToken); + content.put("returnSecureToken", true); + if (tenantId != null) { + content.put("tenantId", tenantId); + } + HttpRequest request = transport.createRequestFactory().buildPostRequest(url, + new JsonHttpContent(jsonFactory, content.build())); + request.setParser(new JsonObjectParser(jsonFactory)); + HttpResponse response = request.execute(); + try { + GenericJson json = response.parseAs(GenericJson.class); + return json.get("idToken").toString(); + } finally { + response.disconnect(); + } + } +} diff --git a/src/test/java/com/google/firebase/auth/multitenancy/TenantManagerIT.java b/src/test/java/com/google/firebase/auth/multitenancy/TenantManagerIT.java new file mode 100644 index 000000000..086e25aa2 --- /dev/null +++ b/src/test/java/com/google/firebase/auth/multitenancy/TenantManagerIT.java @@ -0,0 +1,158 @@ +/* + * Copyright 2020 Google LLC + * + * 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.multitenancy; + +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.assertTrue; +import static org.junit.Assert.fail; + +import com.google.api.core.ApiFuture; +import com.google.api.core.ApiFutureCallback; +import com.google.api.core.ApiFutures; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.firebase.auth.FirebaseAuth; +import com.google.firebase.auth.FirebaseAuthException; +import com.google.firebase.auth.internal.AuthHttpClient; +import com.google.firebase.testing.IntegrationTestUtils; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Semaphore; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.Test; + +public class TenantManagerIT { + + private static final FirebaseAuth auth = FirebaseAuth.getInstance( + IntegrationTestUtils.ensureDefaultApp()); + + @Test + public void testTenantLifecycle() throws Exception { + TenantManager tenantManager = auth.getTenantManager(); + + // Create tenant + Tenant.CreateRequest createRequest = new Tenant.CreateRequest().setDisplayName("DisplayName"); + Tenant tenant = tenantManager.createTenantAsync(createRequest).get(); + assertEquals("DisplayName", tenant.getDisplayName()); + assertFalse(tenant.isPasswordSignInAllowed()); + assertFalse(tenant.isEmailLinkSignInEnabled()); + String tenantId = tenant.getTenantId(); + + // Get tenant + tenant = tenantManager.getTenantAsync(tenantId).get(); + assertEquals(tenantId, tenant.getTenantId()); + assertEquals("DisplayName", tenant.getDisplayName()); + assertFalse(tenant.isPasswordSignInAllowed()); + assertFalse(tenant.isEmailLinkSignInEnabled()); + + // Update tenant + Tenant.UpdateRequest updateRequest = tenant.updateRequest() + .setDisplayName("UpdatedName") + .setPasswordSignInAllowed(true) + .setEmailLinkSignInEnabled(true); + tenant = tenantManager.updateTenantAsync(updateRequest).get(); + assertEquals(tenantId, tenant.getTenantId()); + assertEquals("UpdatedName", tenant.getDisplayName()); + assertTrue(tenant.isPasswordSignInAllowed()); + assertTrue(tenant.isEmailLinkSignInEnabled()); + + // Delete tenant + tenantManager.deleteTenantAsync(tenant.getTenantId()).get(); + try { + tenantManager.getTenantAsync(tenant.getTenantId()).get(); + fail("No error thrown for getting a deleted tenant"); + } catch (ExecutionException e) { + assertTrue(e.getCause() instanceof FirebaseAuthException); + assertEquals(AuthHttpClient.TENANT_NOT_FOUND_ERROR, + ((FirebaseAuthException) e.getCause()).getErrorCode()); + } + } + + @Test + public void testListTenants() throws Exception { + TenantManager tenantManager = auth.getTenantManager(); + final List tenantIds = new ArrayList<>(); + + try { + for (int i = 0; i < 3; i++) { + Tenant.CreateRequest createRequest = + new Tenant.CreateRequest().setDisplayName("DisplayName" + i); + tenantIds.add(tenantManager.createTenantAsync(createRequest).get().getTenantId()); + } + + // Test list by batches + final AtomicInteger collected = new AtomicInteger(0); + ListTenantsPage page = tenantManager.listTenantsAsync(null).get(); + while (page != null) { + for (Tenant tenant : page.getValues()) { + if (tenantIds.contains(tenant.getTenantId())) { + collected.incrementAndGet(); + assertNotNull(tenant.getDisplayName()); + } + } + page = page.getNextPage(); + } + assertEquals(tenantIds.size(), collected.get()); + + // Test iterate all + collected.set(0); + page = tenantManager.listTenantsAsync(null).get(); + for (Tenant tenant : page.iterateAll()) { + if (tenantIds.contains(tenant.getTenantId())) { + collected.incrementAndGet(); + assertNotNull(tenant.getDisplayName()); + } + } + assertEquals(tenantIds.size(), collected.get()); + + // Test iterate async + collected.set(0); + final Semaphore semaphore = new Semaphore(0); + final AtomicReference error = new AtomicReference<>(); + ApiFuture pageFuture = tenantManager.listTenantsAsync(null); + ApiFutures.addCallback(pageFuture, new ApiFutureCallback() { + @Override + public void onFailure(Throwable t) { + error.set(t); + semaphore.release(); + } + + @Override + public void onSuccess(ListTenantsPage result) { + for (Tenant tenant : result.iterateAll()) { + if (tenantIds.contains(tenant.getTenantId())) { + collected.incrementAndGet(); + assertNotNull(tenant.getDisplayName()); + } + } + semaphore.release(); + } + }, MoreExecutors.directExecutor()); + semaphore.acquire(); + assertEquals(tenantIds.size(), collected.get()); + assertNull(error.get()); + } finally { + for (String tenantId : tenantIds) { + tenantManager.deleteTenantAsync(tenantId).get(); + } + } + } +} diff --git a/src/test/java/com/google/firebase/auth/multitenancy/TenantTest.java b/src/test/java/com/google/firebase/auth/multitenancy/TenantTest.java new file mode 100644 index 000000000..7ea4f2539 --- /dev/null +++ b/src/test/java/com/google/firebase/auth/multitenancy/TenantTest.java @@ -0,0 +1,92 @@ +/* + * Copyright 2020 Google LLC + * + * 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.multitenancy; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import com.google.api.client.googleapis.util.Utils; +import com.google.api.client.json.JsonFactory; +import java.io.IOException; +import java.util.Map; +import org.junit.Test; + +public class TenantTest { + + private static final JsonFactory JSON_FACTORY = Utils.getDefaultJsonFactory(); + + private static final String TENANT_JSON_STRING = + "{" + + "\"name\":\"projects/project-id/resource/TENANT_ID\"," + + "\"displayName\":\"DISPLAY_NAME\"," + + "\"allowPasswordSignup\":true," + + "\"enableEmailLinkSignin\":false" + + "}"; + + @Test + public void testJsonDeserialization() throws IOException { + Tenant tenant = JSON_FACTORY.fromString(TENANT_JSON_STRING, Tenant.class); + + assertEquals(tenant.getTenantId(), "TENANT_ID"); + assertEquals(tenant.getDisplayName(), "DISPLAY_NAME"); + assertTrue(tenant.isPasswordSignInAllowed()); + assertFalse(tenant.isEmailLinkSignInEnabled()); + } + + @Test + public void testUpdateRequestFromTenant() throws IOException { + Tenant tenant = JSON_FACTORY.fromString(TENANT_JSON_STRING, Tenant.class); + + Tenant.UpdateRequest updateRequest = tenant.updateRequest(); + + assertEquals("TENANT_ID", updateRequest.getTenantId()); + assertTrue(updateRequest.getProperties().isEmpty()); + } + + @Test + public void testUpdateRequestFromTenantId() throws IOException { + Tenant.UpdateRequest updateRequest = new Tenant.UpdateRequest("TENANT_ID"); + updateRequest + .setDisplayName("DISPLAY_NAME") + .setPasswordSignInAllowed(false) + .setEmailLinkSignInEnabled(true); + + assertEquals("TENANT_ID", updateRequest.getTenantId()); + Map properties = updateRequest.getProperties(); + assertEquals(properties.size(), 3); + assertEquals("DISPLAY_NAME", (String) properties.get("displayName")); + assertFalse((boolean) properties.get("allowPasswordSignup")); + assertTrue((boolean) properties.get("enableEmailLinkSignin")); + } + + @Test + public void testCreateRequest() throws IOException { + Tenant.CreateRequest createRequest = new Tenant.CreateRequest(); + createRequest + .setDisplayName("DISPLAY_NAME") + .setPasswordSignInAllowed(false) + .setEmailLinkSignInEnabled(true); + + Map properties = createRequest.getProperties(); + assertEquals(properties.size(), 3); + assertEquals("DISPLAY_NAME", (String) properties.get("displayName")); + assertFalse((boolean) properties.get("allowPasswordSignup")); + assertTrue((boolean) properties.get("enableEmailLinkSignin")); + } +} + diff --git a/src/test/java/com/google/firebase/database/FirebaseDatabaseTest.java b/src/test/java/com/google/firebase/database/FirebaseDatabaseTest.java index d8395035e..ba7816173 100644 --- a/src/test/java/com/google/firebase/database/FirebaseDatabaseTest.java +++ b/src/test/java/com/google/firebase/database/FirebaseDatabaseTest.java @@ -23,6 +23,7 @@ import static org.junit.Assert.assertSame; import static org.junit.Assert.fail; +import com.google.auth.oauth2.GoogleCredentials; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -33,11 +34,12 @@ import com.google.firebase.database.util.EmulatorHelper; import com.google.firebase.testing.ServiceAccount; import com.google.firebase.testing.TestUtils; +import java.io.IOException; import java.util.List; import org.junit.Test; public class FirebaseDatabaseTest { - + private static final FirebaseOptions firebaseOptions = new FirebaseOptions.Builder() .setCredentials(TestUtils.getCertCredential(ServiceAccount.EDITOR.asStream())) @@ -198,7 +200,7 @@ public void testInitAfterAppDelete() { } @Test - public void testDbUrlIsEmulatorUrlWhenSettingOptionsManually() { + public void testDbUrlIsEmulatorUrlWhenSettingOptionsManually() throws IOException { List testCases = ImmutableList.of( // cases where the env var is ignored because the supplied DB URL is a valid emulator URL @@ -235,7 +237,7 @@ public void testDbUrlIsEmulatorUrlWhenSettingOptionsManually() { } @Test - public void testDbUrlIsEmulatorUrlForDbRefWithPath() { + public void testDbUrlIsEmulatorUrlForDbRefWithPath() throws IOException { List testCases = ImmutableList.of( new CustomTestCase("http://my-custom-hosted-emulator.com:80?ns=dummy-ns", diff --git a/src/test/resources/getUser.json b/src/test/resources/getUser.json index 019c665be..3d57f1082 100644 --- a/src/test/resources/getUser.json +++ b/src/test/resources/getUser.json @@ -24,6 +24,7 @@ "validSince" : "1494364393", "disabled" : false, "createdAt" : "1234567890", - "customAttributes" : "{\"admin\": true, \"package\": \"gold\"}" + "customAttributes" : "{\"admin\": true, \"package\": \"gold\"}", + "tenantId": "testTenant" } ] } diff --git a/src/test/resources/listOidc.json b/src/test/resources/listOidc.json new file mode 100644 index 000000000..0c13ea48b --- /dev/null +++ b/src/test/resources/listOidc.json @@ -0,0 +1,15 @@ +{ + "oauthIdpConfigs" : [ { + "name": "projects/projectId/oauthIdpConfigs/oidc.provider-id1", + "displayName" : "DISPLAY_NAME", + "enabled" : true, + "clientId" : "CLIENT_ID", + "issuer" : "https://oidc.com/issuer" + }, { + "name": "projects/projectId/oauthIdpConfigs/oidc.provider-id2", + "displayName" : "DISPLAY_NAME", + "enabled" : true, + "clientId" : "CLIENT_ID", + "issuer" : "https://oidc.com/issuer" + } ] +} diff --git a/src/test/resources/listSaml.json b/src/test/resources/listSaml.json new file mode 100644 index 000000000..64b1e1e36 --- /dev/null +++ b/src/test/resources/listSaml.json @@ -0,0 +1,35 @@ +{ + "inboundSamlConfigs" : [ { + "name": "projects/projectId/inboundSamlConfigs/saml.provider-id1", + "displayName" : "DISPLAY_NAME", + "enabled" : true, + "idpConfig": { + "idpEntityId": "IDP_ENTITY_ID", + "ssoUrl": "https://example.com/login", + "idpCertificates": [ + { "x509Certificate": "certificate1" }, + { "x509Certificate": "certificate2" } + ] + }, + "spConfig": { + "spEntityId": "RP_ENTITY_ID", + "callbackUri": "https://projectId.firebaseapp.com/__/auth/handler" + } + }, { + "name": "projects/projectId/inboundSamlConfigs/saml.provider-id2", + "displayName" : "DISPLAY_NAME", + "enabled" : true, + "idpConfig": { + "idpEntityId": "IDP_ENTITY_ID", + "ssoUrl": "https://example.com/login", + "idpCertificates": [ + { "x509Certificate": "certificate1" }, + { "x509Certificate": "certificate2" } + ] + }, + "spConfig": { + "spEntityId": "RP_ENTITY_ID", + "callbackUri": "https://projectId.firebaseapp.com/__/auth/handler" + } + } ] +} diff --git a/src/test/resources/listTenants.json b/src/test/resources/listTenants.json new file mode 100644 index 000000000..1e2f3a01a --- /dev/null +++ b/src/test/resources/listTenants.json @@ -0,0 +1,17 @@ +{ + "tenants" : [ { + "name" : "TENANT_1", + "displayName" : "DISPLAY_NAME", + "allowPasswordSignup" : true, + "enableEmailLinkSignin" : false, + "disableAuth" : true, + "enableAnonymousUser" : false + }, { + "name" : "TENANT_2", + "displayName" : "DISPLAY_NAME", + "allowPasswordSignup" : true, + "enableEmailLinkSignin" : false, + "disableAuth" : true, + "enableAnonymousUser" : false + } ] +} diff --git a/src/test/resources/listUsers.json b/src/test/resources/listUsers.json index 06d0b34c9..47e169709 100644 --- a/src/test/resources/listUsers.json +++ b/src/test/resources/listUsers.json @@ -24,7 +24,8 @@ "validSince" : "1494364393", "disabled" : false, "createdAt" : "1234567890", - "customAttributes" : "{\"admin\": true, \"package\": \"gold\"}" + "customAttributes" : "{\"admin\": true, \"package\": \"gold\"}", + "tenantId": "testTenant" }, { "localId" : "testuser", "email" : "testuser@example.com", @@ -50,6 +51,7 @@ "validSince" : "1494364393", "disabled" : false, "createdAt" : "1234567890", - "customAttributes" : "{\"admin\": true, \"package\": \"gold\"}" + "customAttributes" : "{\"admin\": true, \"package\": \"gold\"}", + "tenantId": "testTenant" } ] } diff --git a/src/test/resources/oidc.json b/src/test/resources/oidc.json new file mode 100644 index 000000000..e2f1845de --- /dev/null +++ b/src/test/resources/oidc.json @@ -0,0 +1,7 @@ +{ + "name": "projects/projectId/oauthIdpConfigs/oidc.provider-id", + "displayName" : "DISPLAY_NAME", + "enabled" : true, + "clientId" : "CLIENT_ID", + "issuer" : "https://oidc.com/issuer" +} diff --git a/src/test/resources/saml.json b/src/test/resources/saml.json new file mode 100644 index 000000000..ef425b0a8 --- /dev/null +++ b/src/test/resources/saml.json @@ -0,0 +1,17 @@ +{ + "name": "projects/projectId/inboundSamlConfigs/saml.provider-id", + "displayName": "DISPLAY_NAME", + "enabled": true, + "idpConfig": { + "idpEntityId": "IDP_ENTITY_ID", + "ssoUrl": "https://example.com/login", + "idpCertificates": [ + { "x509Certificate": "certificate1" }, + { "x509Certificate": "certificate2" } + ] + }, + "spConfig": { + "spEntityId": "RP_ENTITY_ID", + "callbackUri": "https://projectId.firebaseapp.com/__/auth/handler" + } +} diff --git a/src/test/resources/tenant.json b/src/test/resources/tenant.json new file mode 100644 index 000000000..cc8565f8b --- /dev/null +++ b/src/test/resources/tenant.json @@ -0,0 +1,8 @@ +{ + "name" : "TENANT_1", + "displayName" : "DISPLAY_NAME", + "allowPasswordSignup" : true, + "enableEmailLinkSignin" : false, + "disableAuth" : true, + "enableAnonymousUser" : false +} From d347ad0c1737cc55f1c3d2b38a53646c43bd83bd Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Thu, 23 Jul 2020 14:41:55 -0400 Subject: [PATCH 022/354] [chore] Release 6.15.0 (#457) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index e9a2fc038..adbf23931 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ com.google.firebase firebase-admin - 6.14.0 + 6.15.0 jar firebase-admin From 7b991238067137818907847826513bbd62a8f68a Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Tue, 28 Jul 2020 12:59:01 -0700 Subject: [PATCH 023/354] chore(auth): Added snippets for multitenancy and IdP management (#461) * chore(auth): Added snippets for multitenancy and IdP management * fix: Fixed some broken snippet tags * fix(auth): Fixed a typo and clarified a comment --- .../snippets/FirebaseAuthSnippets.java | 478 +++++++++++++++++- 1 file changed, 474 insertions(+), 4 deletions(-) diff --git a/src/test/java/com/google/firebase/snippets/FirebaseAuthSnippets.java b/src/test/java/com/google/firebase/snippets/FirebaseAuthSnippets.java index fec3f5d2d..b6103b4ef 100644 --- a/src/test/java/com/google/firebase/snippets/FirebaseAuthSnippets.java +++ b/src/test/java/com/google/firebase/snippets/FirebaseAuthSnippets.java @@ -24,8 +24,12 @@ import com.google.firebase.auth.FirebaseAuthException; import com.google.firebase.auth.FirebaseToken; import com.google.firebase.auth.ImportUserRecord; +import com.google.firebase.auth.ListProviderConfigsPage; import com.google.firebase.auth.ListUsersPage; +import com.google.firebase.auth.OidcProviderConfig; +import com.google.firebase.auth.SamlProviderConfig; import com.google.firebase.auth.SessionCookieOptions; +import com.google.firebase.auth.UserImportHash; import com.google.firebase.auth.UserImportOptions; import com.google.firebase.auth.UserImportResult; import com.google.firebase.auth.UserProvider; @@ -37,6 +41,10 @@ import com.google.firebase.auth.hash.Pbkdf2Sha256; import com.google.firebase.auth.hash.Scrypt; import com.google.firebase.auth.hash.StandardScrypt; +import com.google.firebase.auth.multitenancy.ListTenantsPage; +import com.google.firebase.auth.multitenancy.Tenant; +import com.google.firebase.auth.multitenancy.TenantAwareFirebaseAuth; +import com.google.firebase.auth.multitenancy.TenantManager; import com.google.firebase.database.DatabaseReference; import com.google.firebase.database.FirebaseDatabase; import java.net.URI; @@ -623,7 +631,7 @@ public void generatePasswordResetLink() { email, actionCodeSettings); // Construct email verification template, embed the link and send // using custom SMTP server. - sendCustomPasswordResetEmail(email, displayName, link); + sendCustomEmail(email, displayName, link); } catch (FirebaseAuthException e) { System.out.println("Error generating email link: " + e.getMessage()); } @@ -640,7 +648,7 @@ public void generateEmailVerificationLink() { email, actionCodeSettings); // Construct email verification template, embed the link and send // using custom SMTP server. - sendCustomPasswordResetEmail(email, displayName, link); + sendCustomEmail(email, displayName, link); } catch (FirebaseAuthException e) { System.out.println("Error generating email link: " + e.getMessage()); } @@ -657,14 +665,476 @@ public void generateSignInWithEmailLink() { email, actionCodeSettings); // Construct email verification template, embed the link and send // using custom SMTP server. - sendCustomPasswordResetEmail(email, displayName, link); + sendCustomEmail(email, displayName, link); } catch (FirebaseAuthException e) { System.out.println("Error generating email link: " + e.getMessage()); } // [END sign_in_with_email_link] } + // ===================================================================================== + // https://cloud.google.com/identity-platform/docs/managing-providers-programmatically + // ===================================================================================== + + public void createSamlProviderConfig() throws FirebaseAuthException { + // [START create_saml_provider] + SamlProviderConfig.CreateRequest request = new SamlProviderConfig.CreateRequest() + .setDisplayName("SAML provider name") + .setEnabled(true) + .setProviderId("saml.myProvider") + .setIdpEntityId("IDP_ENTITY_ID") + .setSsoUrl("https://example.com/saml/sso/1234/") + .addX509Certificate("-----BEGIN CERTIFICATE-----\nCERT1...\n-----END CERTIFICATE-----") + .addX509Certificate("-----BEGIN CERTIFICATE-----\nCERT2...\n-----END CERTIFICATE-----") + .setRpEntityId("RP_ENTITY_ID") + .setCallbackUrl("https://project-id.firebaseapp.com/__/auth/handler"); + SamlProviderConfig saml = FirebaseAuth.getInstance().createSamlProviderConfig(request); + System.out.println("Created new SAML provider: " + saml.getProviderId()); + // [END create_saml_provider] + } + + public void updateSamlProviderConfig() throws FirebaseAuthException { + // [START update_saml_provider] + SamlProviderConfig.UpdateRequest request = + new SamlProviderConfig.UpdateRequest("saml.myProvider") + .addX509Certificate("-----BEGIN CERTIFICATE-----\nCERT2...\n-----END CERTIFICATE-----") + .addX509Certificate("-----BEGIN CERTIFICATE-----\nCERT3...\n-----END CERTIFICATE-----"); + SamlProviderConfig saml = FirebaseAuth.getInstance().updateSamlProviderConfig(request); + System.out.println("Updated SAML provider: " + saml.getProviderId()); + // [END update_saml_provider] + } + + public void getSamlProviderConfig() throws FirebaseAuthException { + // [START get_saml_provider] + SamlProviderConfig saml = FirebaseAuth.getInstance().getSamlProviderConfig("saml.myProvider"); + System.out.println(saml.getDisplayName() + ": " + saml.isEnabled()); + // [END get_saml_provider] + } + + public void deleteSamlProviderConfig() throws FirebaseAuthException { + // [START delete_saml_provider] + FirebaseAuth.getInstance().deleteSamlProviderConfig("saml.myProvider"); + // [END delete_saml_provider] + } + + public void listSamlProviderConfigs() throws FirebaseAuthException { + // [START list_saml_providers] + ListProviderConfigsPage page = FirebaseAuth.getInstance() + .listSamlProviderConfigs("nextPageToken"); + for (SamlProviderConfig config : page.iterateAll()) { + System.out.println(config.getProviderId()); + } + // [END list_saml_providers] + } + + public void createOidcProviderConfig() throws FirebaseAuthException { + // [START create_oidc_provider] + OidcProviderConfig.CreateRequest request = new OidcProviderConfig.CreateRequest() + .setDisplayName("OIDC provider name") + .setEnabled(true) + .setProviderId("oidc.myProvider") + .setClientId("CLIENT_ID2") + .setIssuer("https://oidc.com/CLIENT_ID2"); + OidcProviderConfig oidc = FirebaseAuth.getInstance().createOidcProviderConfig(request); + System.out.println("Created new OIDC provider: " + oidc.getProviderId()); + // [END create_oidc_provider] + } + + public void updateOidcProviderConfig() throws FirebaseAuthException { + // [START update_oidc_provider] + OidcProviderConfig.UpdateRequest request = + new OidcProviderConfig.UpdateRequest("oidc.myProvider") + .setDisplayName("OIDC provider name") + .setEnabled(true) + .setClientId("CLIENT_ID") + .setIssuer("https://oidc.com"); + OidcProviderConfig oidc = FirebaseAuth.getInstance().updateOidcProviderConfig(request); + System.out.println("Updated OIDC provider: " + oidc.getProviderId()); + // [END update_oidc_provider] + } + + public void getOidcProviderConfig() throws FirebaseAuthException { + // [START get_oidc_provider] + OidcProviderConfig oidc = FirebaseAuth.getInstance().getOidcProviderConfig("oidc.myProvider"); + System.out.println(oidc.getDisplayName() + ": " + oidc.isEnabled()); + // [END get_oidc_provider] + } + + public void deleteOidcProviderConfig() throws FirebaseAuthException { + // [START delete_oidc_provider] + FirebaseAuth.getInstance().deleteOidcProviderConfig("oidc.myProvider"); + // [END delete_oidc_provider] + } + + public void listOidcProviderConfigs() throws FirebaseAuthException { + // [START list_oidc_providers] + ListProviderConfigsPage page = FirebaseAuth.getInstance() + .listOidcProviderConfigs("nextPageToken"); + for (OidcProviderConfig oidc : page.iterateAll()) { + System.out.println(oidc.getProviderId()); + } + // [END list_oidc_providers] + } + + // ================================================================================ + // https://cloud.google.com/identity-platform/docs/multi-tenancy-managing-tenants + // ================================================================================= + + public TenantAwareFirebaseAuth getTenantAwareFirebaseAuth(String tenantId) { + // [START get_tenant_client] + FirebaseAuth auth = FirebaseAuth.getInstance(); + TenantManager tenantManager = auth.getTenantManager(); + TenantAwareFirebaseAuth tenantAuth = tenantManager.getAuthForTenant(tenantId); + // [END get_tenant_client] + + return tenantAuth; + } + + public void getTenant(String tenantId) throws FirebaseAuthException { + // [START get_tenant] + Tenant tenant = FirebaseAuth.getInstance().getTenantManager().getTenant(tenantId); + System.out.println("Retrieved tenant: " + tenant.getTenantId()); + // [END get_tenant] + } + + public void createTenant() throws FirebaseAuthException { + // [START create_tenant] + Tenant.CreateRequest request = new Tenant.CreateRequest() + .setDisplayName("myTenant1") + .setEmailLinkSignInEnabled(true) + .setPasswordSignInAllowed(true); + Tenant tenant = FirebaseAuth.getInstance().getTenantManager().createTenant(request); + System.out.println("Created tenant: " + tenant.getTenantId()); + // [END create_tenant] + } + + public void updateTenant(String tenantId) throws FirebaseAuthException { + // [START update_tenant] + Tenant.UpdateRequest request = new Tenant.UpdateRequest(tenantId) + .setDisplayName("updatedName") + .setPasswordSignInAllowed(false); + Tenant tenant = FirebaseAuth.getInstance().getTenantManager().updateTenant(request); + System.out.println("Updated tenant: " + tenant.getTenantId()); + // [END update_tenant] + } + + public void deleteTenant(String tenantId) throws FirebaseAuthException { + // [START delete_tenant] + FirebaseAuth.getInstance().getTenantManager().deleteTenant(tenantId); + // [END delete_tenant] + } + + public void listTenants() throws FirebaseAuthException { + // [START list_tenants] + ListTenantsPage page = FirebaseAuth.getInstance().getTenantManager().listTenants(null); + for (Tenant tenant : page.iterateAll()) { + System.out.println("Retrieved tenant: " + tenant.getTenantId()); + } + // [END list_tenants] + } + + public void createProviderTenant() throws FirebaseAuthException { + // [START get_tenant_client_short] + TenantAwareFirebaseAuth tenantAuth = FirebaseAuth.getInstance().getTenantManager() + .getAuthForTenant("TENANT-ID"); + // [END get_tenant_client_short] + + // [START create_saml_provider_tenant] + SamlProviderConfig.CreateRequest request = new SamlProviderConfig.CreateRequest() + .setDisplayName("SAML provider name") + .setEnabled(true) + .setProviderId("saml.myProvider") + .setIdpEntityId("IDP_ENTITY_ID") + .setSsoUrl("https://example.com/saml/sso/1234/") + .addX509Certificate("-----BEGIN CERTIFICATE-----\nCERT1...\n-----END CERTIFICATE-----") + .addX509Certificate("-----BEGIN CERTIFICATE-----\nCERT2...\n-----END CERTIFICATE-----") + .setRpEntityId("RP_ENTITY_ID") + .setCallbackUrl("https://project-id.firebaseapp.com/__/auth/handler"); + SamlProviderConfig saml = tenantAuth.createSamlProviderConfig(request); + System.out.println("Created new SAML provider: " + saml.getProviderId()); + // [END create_saml_provider_tenant] + } + + public void updateProviderTenant( + TenantAwareFirebaseAuth tenantAuth) throws FirebaseAuthException { + // [START update_saml_provider_tenant] + SamlProviderConfig.UpdateRequest request = + new SamlProviderConfig.UpdateRequest("saml.myProvider") + .addX509Certificate("-----BEGIN CERTIFICATE-----\nCERT2...\n-----END CERTIFICATE-----") + .addX509Certificate("-----BEGIN CERTIFICATE-----\nCERT3...\n-----END CERTIFICATE-----"); + SamlProviderConfig saml = tenantAuth.updateSamlProviderConfig(request); + System.out.println("Updated SAML provider: " + saml.getProviderId()); + // [END update_saml_provider_tenant] + } + + public void getProviderTenant(TenantAwareFirebaseAuth tenantAuth) throws FirebaseAuthException { + // [START get_saml_provider_tenant] + SamlProviderConfig saml = tenantAuth.getSamlProviderConfig("saml.myProvider"); + + // Get display name and whether it is enabled. + System.out.println(saml.getDisplayName() + " " + saml.isEnabled()); + // [END get_saml_provider_tenant] + } + + public void listProvidersTenant(TenantAwareFirebaseAuth tenantAuth) throws FirebaseAuthException { + // [START list_saml_providers_tenant] + ListProviderConfigsPage page = tenantAuth.listSamlProviderConfigs( + "nextPageToken"); + for (SamlProviderConfig saml : page.iterateAll()) { + System.out.println(saml.getProviderId()); + } + // [END list_saml_providers_tenant] + } + + public void deleteProviderTenant( + TenantAwareFirebaseAuth tenantAuth) throws FirebaseAuthException { + // [START delete_saml_provider_tenant] + tenantAuth.deleteSamlProviderConfig("saml.myProvider"); + // [END delete_saml_provider_tenant] + } + + public void getUserTenant( + TenantAwareFirebaseAuth tenantAuth, String uid) throws FirebaseAuthException { + // [START get_user_tenant] + // Get an auth client from the firebase.App + UserRecord user = tenantAuth.getUser(uid); + System.out.println("Successfully fetched user data: " + user.getDisplayName()); + // [END get_user_tenant] + } + + public void getUserByEmailTenant( + TenantAwareFirebaseAuth tenantAuth, String email) throws FirebaseAuthException { + // [START get_user_by_email_tenant] + // Get an auth client from the firebase.App + UserRecord user = tenantAuth.getUserByEmail(email); + System.out.println("Successfully fetched user data: " + user.getDisplayName()); + // [END get_user_by_email_tenant] + } + + public void createUserTenant(TenantAwareFirebaseAuth tenantAuth) throws FirebaseAuthException { + // [START create_user_tenant] + UserRecord.CreateRequest request = new UserRecord.CreateRequest() + .setEmail("user@example.com") + .setEmailVerified(false) + .setPhoneNumber("+15555550100") + .setPassword("secretPassword") + .setDisplayName("John Doe") + .setPhotoUrl("http://www.example.com/12345678/photo.png") + .setDisabled(false); + UserRecord user = tenantAuth.createUser(request); + System.out.println("Successfully created user: " + user.getDisplayName()); + // [END create_user_tenant] + } + + public void updateUserTenant( + TenantAwareFirebaseAuth tenantAuth, String uid) throws FirebaseAuthException { + // [START update_user_tenant] + UserRecord.UpdateRequest request = new UserRecord.UpdateRequest(uid) + .setEmail("user@example.com") + .setEmailVerified(true) + .setPhoneNumber("+15555550100") + .setPassword("newPassword") + .setDisplayName("John Doe") + .setPhotoUrl("http://www.example.com/12345678/photo.png") + .setDisabled(true); + UserRecord user = tenantAuth.updateUser(request); + System.out.println("Successfully updated user: " + user.getDisplayName()); + // [END update_user_tenant] + } + + public void deleteUserTenant( + TenantAwareFirebaseAuth tenantAuth, String uid) throws FirebaseAuthException { + // [START delete_user_tenant] + tenantAuth.deleteUser(uid); + + System.out.println("Successfully deleted user: " + uid); + // [END delete_user_tenant] + } + + public void listUsersTenant(TenantAwareFirebaseAuth tenantAuth) throws FirebaseAuthException { + // [START list_all_users_tenant] + // Note, behind the scenes, the ListUsersPage retrieves 1000 Users at a time + // through the API + ListUsersPage page = tenantAuth.listUsers(null); + for (ExportedUserRecord user : page.iterateAll()) { + System.out.println("User: " + user.getUid()); + } + + // Iterating by pages 100 users at a time. + page = tenantAuth.listUsers(null, 100); + while (page != null) { + for (ExportedUserRecord user : page.getValues()) { + System.out.println("User: " + user.getUid()); + } + + page = page.getNextPage(); + } + // [END list_all_users_tenant] + } + + public void importWithHmacTenant( + TenantAwareFirebaseAuth tenantAuth) throws FirebaseAuthException { + // [START import_with_hmac_tenant] + List users = new ArrayList<>(); + users.add(ImportUserRecord.builder() + .setUid("uid1") + .setEmail("user1@example.com") + .setPasswordHash("password-hash-1".getBytes()) + .setPasswordSalt("salt1".getBytes()) + .build()); + users.add(ImportUserRecord.builder() + .setUid("uid2") + .setEmail("user2@example.com") + .setPasswordHash("password-hash-2".getBytes()) + .setPasswordSalt("salt2".getBytes()) + .build()); + UserImportHash hmacSha256 = HmacSha256.builder() + .setKey("secret".getBytes()) + .build(); + UserImportResult result = tenantAuth.importUsers(users, UserImportOptions.withHash(hmacSha256)); + + for (ErrorInfo error : result.getErrors()) { + System.out.println("Failed to import user: " + error.getReason()); + } + // [END import_with_hmac_tenant] + } + + public void importWithoutPasswordTenant( + TenantAwareFirebaseAuth tenantAuth) throws FirebaseAuthException { + // [START import_without_password_tenant] + List users = new ArrayList<>(); + users.add(ImportUserRecord.builder() + .setUid("some-uid") + .setDisplayName("John Doe") + .setEmail("johndoe@acme.com") + .setPhotoUrl("https://www.example.com/12345678/photo.png") + .setEmailVerified(true) + .setPhoneNumber("+11234567890") + // Set this user as admin. + .putCustomClaim("admin", true) + // User with SAML provider. + .addUserProvider(UserProvider.builder() + .setUid("saml-uid") + .setEmail("johndoe@acme.com") + .setDisplayName("John Doe") + .setPhotoUrl("https://www.example.com/12345678/photo.png") + .setProviderId("saml.acme") + .build()) + .build()); + + UserImportResult result = tenantAuth.importUsers(users); + + for (ErrorInfo error : result.getErrors()) { + System.out.println("Failed to import user: " + error.getReason()); + } + // [END import_without_password_tenant] + } + + public void verifyIdTokenTenant(TenantAwareFirebaseAuth tenantAuth, String idToken) { + // [START verify_id_token_tenant] + try { + // idToken comes from the client app + FirebaseToken token = tenantAuth.verifyIdToken(idToken); + // TenantId on the FirebaseToken should be set to TENANT-ID. + // Otherwise "tenant-id-mismatch" error thrown. + System.out.println("Verified ID token from tenant: " + token.getTenantId()); + } catch (FirebaseAuthException e) { + System.out.println("error verifying ID token: " + e.getMessage()); + } + // [END verify_id_token_tenant] + } + + public void verifyIdTokenAccessControlTenant(TenantAwareFirebaseAuth tenantAuth, String idToken) { + // [START id_token_access_control_tenant] + try { + // idToken comes from the client app + FirebaseToken token = tenantAuth.verifyIdToken(idToken); + if ("TENANT-ID1".equals(token.getTenantId())) { + // Allow appropriate level of access for TENANT-ID1. + } else if ("TENANT-ID2".equals(token.getTenantId())) { + // Allow appropriate level of access for TENANT-ID2. + } else { + // Access not allowed -- Handle error + } + } catch (FirebaseAuthException e) { + System.out.println("error verifying ID token: " + e.getMessage()); + } + // [END id_token_access_control_tenant] + } + + public void revokeRefreshTokensTenant( + TenantAwareFirebaseAuth tenantAuth, String uid) throws FirebaseAuthException { + // [START revoke_tokens_tenant] + // Revoke all refresh tokens for a specified user in a specified tenant for whatever reason. + // Retrieve the timestamp of the revocation, in seconds since the epoch. + tenantAuth.revokeRefreshTokens(uid); + + // accessing the user's TokenValidAfter + UserRecord user = tenantAuth.getUser(uid); + + + long timestamp = user.getTokensValidAfterTimestamp() / 1000; + System.out.println("the refresh tokens were revoked at: " + timestamp + " (UTC seconds)"); + // [END revoke_tokens_tenant] + } + + public void verifyIdTokenAndCheckRevokedTenant( + TenantAwareFirebaseAuth tenantAuth, String idToken) { + // [START verify_id_token_and_check_revoked_tenant] + // Verify the ID token for a specific tenant while checking if the token is revoked. + boolean checkRevoked = true; + try { + FirebaseToken token = tenantAuth.verifyIdToken(idToken, checkRevoked); + System.out.println("Verified ID token for: " + token.getUid()); + } catch (FirebaseAuthException e) { + if ("id-token-revoked".equals(e.getErrorCode())) { + // Token is revoked. Inform the user to re-authenticate or signOut() the user. + } else { + // Token is invalid + } + } + // [END verify_id_token_and_check_revoked_tenant] + } + + public void customClaimsVerifyTenant( + TenantAwareFirebaseAuth tenantAuth, String idToken) throws FirebaseAuthException { + // [START verify_custom_claims_tenant] + // Verify the ID token first. + FirebaseToken token = tenantAuth.verifyIdToken(idToken); + if (Boolean.TRUE.equals(token.getClaims().get("admin"))) { + //Allow access to requested admin resource. + } + // [END verify_custom_claims_tenant] + } + + public void generateEmailVerificationLinkTenant( + TenantAwareFirebaseAuth tenantAuth, + String email, + String displayName) throws FirebaseAuthException { + // [START email_verification_link_tenant] + ActionCodeSettings actionCodeSettings = ActionCodeSettings.builder() + // URL you want to redirect back to. The domain (www.example.com) for + // this URL must be whitelisted in the GCP Console. + .setUrl("https://www.example.com/checkout?cartId=1234") + // This must be true for email link sign-in. + .setHandleCodeInApp(true) + .setIosBundleId("com.example.ios") + .setAndroidPackageName("com.example.android") + .setAndroidInstallApp(true) + .setAndroidMinimumVersion("12") + // FDL custom domain. + .setDynamicLinkDomain("coolapp.page.link") + .build(); + + String link = tenantAuth.generateEmailVerificationLink(email, actionCodeSettings); + + // Construct email verification template, embed the link and send + // using custom SMTP server. + sendCustomEmail(email, displayName, link); + // [END email_verification_link_tenant] + } + // Place holder method to make the compiler happy. This is referenced by all email action // link snippets. - private void sendCustomPasswordResetEmail(String email, String displayName, String link) {} + private void sendCustomEmail(String email, String displayName, String link) {} } From 3564eeb7a6593fb7f23264c81c2f41d015c0ef8b Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Mon, 10 Aug 2020 10:07:34 -0700 Subject: [PATCH 024/354] feat(auth): Support for creating tenant-scoped session cookies (#467) * feat(auth): Support for creating tenant-scoped session cookies * fix: Cleaned up the unit test --- pom.xml | 4 +- .../firebase/auth/AbstractFirebaseAuth.java | 251 +++++++++++---- .../google/firebase/auth/FirebaseAuth.java | 209 +++---------- .../firebase/auth/FirebaseTokenUtils.java | 12 +- .../multitenancy/TenantAwareFirebaseAuth.java | 65 +++- .../auth/multitenancy/TenantManager.java | 2 +- .../firebase/auth/FirebaseAuthTest.java | 110 +++---- .../auth/FirebaseTokenVerifierImplTest.java | 13 + .../auth/FirebaseUserManagerTest.java | 25 +- .../firebase/auth/MockTokenVerifier.java | 59 ++++ .../TenantAwareFirebaseAuthTest.java | 292 ++++++++++++++++++ 11 files changed, 705 insertions(+), 337 deletions(-) create mode 100644 src/test/java/com/google/firebase/auth/MockTokenVerifier.java create mode 100644 src/test/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuthTest.java diff --git a/pom.xml b/pom.xml index adbf23931..b0187543a 100644 --- a/pom.xml +++ b/pom.xml @@ -198,7 +198,7 @@ org.jacoco jacoco-maven-plugin - 0.7.9 + 0.8.5 pre-unit-test @@ -289,7 +289,7 @@ maven-surefire-plugin - 2.19.1 + 2.22.0 ${skipUTs} diff --git a/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java b/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java index ad30d2cc3..6c841070c 100644 --- a/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java +++ b/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java @@ -62,50 +62,13 @@ public abstract class AbstractFirebaseAuth { private final Supplier userManager; private final JsonFactory jsonFactory; - protected AbstractFirebaseAuth(Builder builder) { + protected AbstractFirebaseAuth(Builder builder) { this.firebaseApp = checkNotNull(builder.firebaseApp); this.tokenFactory = threadSafeMemoize(builder.tokenFactory); this.idTokenVerifier = threadSafeMemoize(builder.idTokenVerifier); this.cookieVerifier = threadSafeMemoize(builder.cookieVerifier); this.userManager = threadSafeMemoize(builder.userManager); - this.jsonFactory = builder.firebaseApp.getOptions().getJsonFactory(); - } - - protected static Builder builderFromAppAndTenantId(final FirebaseApp app, final String tenantId) { - return AbstractFirebaseAuth.builder() - .setFirebaseApp(app) - .setTokenFactory( - new Supplier() { - @Override - public FirebaseTokenFactory get() { - return FirebaseTokenUtils.createTokenFactory(app, Clock.SYSTEM, tenantId); - } - }) - .setIdTokenVerifier( - new Supplier() { - @Override - public FirebaseTokenVerifier get() { - return FirebaseTokenUtils.createIdTokenVerifier(app, Clock.SYSTEM, tenantId); - } - }) - .setCookieVerifier( - new Supplier() { - @Override - public FirebaseTokenVerifier get() { - return FirebaseTokenUtils.createSessionCookieVerifier(app, Clock.SYSTEM); - } - }) - .setUserManager( - new Supplier() { - @Override - public FirebaseUserManager get() { - return FirebaseUserManager - .builder() - .setFirebaseApp(app) - .setTenantId(tenantId) - .build(); - } - }); + this.jsonFactory = firebaseApp.getOptions().getJsonFactory(); } /** @@ -220,6 +183,51 @@ public String execute() throws FirebaseAuthException { }; } + /** + * Creates a new Firebase session cookie from the given ID token and options. The returned JWT can + * be set as a server-side session cookie with a custom cookie policy. + * + * @param idToken The Firebase ID token to exchange for a session cookie. + * @param options Additional options required to create the cookie. + * @return A Firebase session cookie string. + * @throws IllegalArgumentException If the ID token is null or empty, or if options is null. + * @throws FirebaseAuthException If an error occurs while generating the session cookie. + */ + public String createSessionCookie(@NonNull String idToken, @NonNull SessionCookieOptions options) + throws FirebaseAuthException { + return createSessionCookieOp(idToken, options).call(); + } + + /** + * Similar to {@link #createSessionCookie(String, SessionCookieOptions)} but performs the + * operation asynchronously. + * + * @param idToken The Firebase ID token to exchange for a session cookie. + * @param options Additional options required to create the cookie. + * @return An {@code ApiFuture} which will complete successfully with a session cookie string. If + * an error occurs while generating the cookie or if the specified ID token is invalid, the + * future throws a {@link FirebaseAuthException}. + * @throws IllegalArgumentException If the ID token is null or empty, or if options is null. + */ + public ApiFuture createSessionCookieAsync( + @NonNull String idToken, @NonNull SessionCookieOptions options) { + return createSessionCookieOp(idToken, options).callAsync(firebaseApp); + } + + private CallableOperation createSessionCookieOp( + final String idToken, final SessionCookieOptions options) { + checkNotDestroyed(); + checkArgument(!Strings.isNullOrEmpty(idToken), "idToken must not be null or empty"); + checkNotNull(options, "options must not be null"); + final FirebaseUserManager userManager = getUserManager(); + return new CallableOperation() { + @Override + protected String execute() throws FirebaseAuthException { + return userManager.createSessionCookie(idToken, options); + } + }; + } + /** * Parses and verifies a Firebase ID Token. * @@ -320,6 +328,87 @@ FirebaseTokenVerifier getIdTokenVerifier(boolean checkRevoked) { return verifier; } + /** + * Parses and verifies a Firebase session cookie. + * + *

If verified successfully, returns a parsed version of the cookie from which the UID and the + * other claims can be read. If the cookie is invalid, throws a {@link FirebaseAuthException}. + * + *

This method does not check whether the cookie has been revoked. See {@link + * #verifySessionCookie(String, boolean)}. + * + * @param cookie A Firebase session cookie string to verify and parse. + * @return A {@link FirebaseToken} representing the verified and decoded cookie. + */ + public FirebaseToken verifySessionCookie(String cookie) throws FirebaseAuthException { + return verifySessionCookie(cookie, false); + } + + /** + * Parses and verifies a Firebase session cookie. + * + *

If {@code checkRevoked} is true, additionally verifies that the cookie has not been revoked. + * + *

If verified successfully, returns a parsed version of the cookie from which the UID and the + * other claims can be read. If the cookie is invalid or has been revoked while {@code + * checkRevoked} is true, throws a {@link FirebaseAuthException}. + * + * @param cookie A Firebase session cookie string to verify and parse. + * @param checkRevoked A boolean indicating whether to check if the cookie was explicitly revoked. + * @return A {@link FirebaseToken} representing the verified and decoded cookie. + */ + public FirebaseToken verifySessionCookie(String cookie, boolean checkRevoked) + throws FirebaseAuthException { + return verifySessionCookieOp(cookie, checkRevoked).call(); + } + + /** + * Similar to {@link #verifySessionCookie(String)} but performs the operation asynchronously. + * + * @param cookie A Firebase session cookie string to verify and parse. + * @return An {@code ApiFuture} which will complete successfully with the parsed cookie, or + * unsuccessfully with the failure Exception. + */ + public ApiFuture verifySessionCookieAsync(String cookie) { + return verifySessionCookieAsync(cookie, false); + } + + /** + * Similar to {@link #verifySessionCookie(String, boolean)} but performs the operation + * asynchronously. + * + * @param cookie A Firebase session cookie string to verify and parse. + * @param checkRevoked A boolean indicating whether to check if the cookie was explicitly revoked. + * @return An {@code ApiFuture} which will complete successfully with the parsed cookie, or + * unsuccessfully with the failure Exception. + */ + public ApiFuture verifySessionCookieAsync(String cookie, boolean checkRevoked) { + return verifySessionCookieOp(cookie, checkRevoked).callAsync(firebaseApp); + } + + private CallableOperation verifySessionCookieOp( + final String cookie, final boolean checkRevoked) { + checkNotDestroyed(); + checkArgument(!Strings.isNullOrEmpty(cookie), "Session cookie must not be null or empty"); + final FirebaseTokenVerifier sessionCookieVerifier = getSessionCookieVerifier(checkRevoked); + return new CallableOperation() { + @Override + public FirebaseToken execute() throws FirebaseAuthException { + return sessionCookieVerifier.verifyToken(cookie); + } + }; + } + + @VisibleForTesting + FirebaseTokenVerifier getSessionCookieVerifier(boolean checkRevoked) { + FirebaseTokenVerifier verifier = cookieVerifier.get(); + if (checkRevoked) { + FirebaseUserManager userManager = getUserManager(); + verifier = RevocationCheckDecorator.decorateSessionCookieVerifier(verifier, userManager); + } + return verifier; + } + /** * Revokes all refresh tokens for the specified user. * @@ -1637,19 +1726,11 @@ protected Void execute() throws FirebaseAuthException { }; } - FirebaseApp getFirebaseApp() { - return this.firebaseApp; - } - - FirebaseTokenVerifier getCookieVerifier() { - return this.cookieVerifier.get(); - } - FirebaseUserManager getUserManager() { return this.userManager.get(); } - protected Supplier threadSafeMemoize(final Supplier supplier) { + Supplier threadSafeMemoize(final Supplier supplier) { return Suppliers.memoize( new Supplier() { @Override @@ -1663,7 +1744,7 @@ public T get() { }); } - void checkNotDestroyed() { + private void checkNotDestroyed() { synchronized (lock) { checkState( !destroyed.get(), @@ -1682,42 +1763,80 @@ final void destroy() { /** Performs any additional required clean up. */ protected abstract void doDestroy(); - static Builder builder() { - return new Builder(); - } + protected abstract static class Builder> { - static class Builder { - protected FirebaseApp firebaseApp; + private FirebaseApp firebaseApp; private Supplier tokenFactory; private Supplier idTokenVerifier; private Supplier cookieVerifier; - private Supplier userManager; + private Supplier userManager; - private Builder() {} + protected abstract T getThis(); + + public FirebaseApp getFirebaseApp() { + return firebaseApp; + } - Builder setFirebaseApp(FirebaseApp firebaseApp) { + public T setFirebaseApp(FirebaseApp firebaseApp) { this.firebaseApp = firebaseApp; - return this; + return getThis(); } - Builder setTokenFactory(Supplier tokenFactory) { + public T setTokenFactory(Supplier tokenFactory) { this.tokenFactory = tokenFactory; - return this; + return getThis(); } - Builder setIdTokenVerifier(Supplier idTokenVerifier) { + public T setIdTokenVerifier(Supplier idTokenVerifier) { this.idTokenVerifier = idTokenVerifier; - return this; + return getThis(); } - Builder setCookieVerifier(Supplier cookieVerifier) { + public T setCookieVerifier(Supplier cookieVerifier) { this.cookieVerifier = cookieVerifier; - return this; + return getThis(); } - Builder setUserManager(Supplier userManager) { + public T setUserManager(Supplier userManager) { this.userManager = userManager; - return this; + return getThis(); } } + + protected static > T populateBuilderFromApp( + Builder builder, final FirebaseApp app, @Nullable final String tenantId) { + return builder.setFirebaseApp(app) + .setTokenFactory( + new Supplier() { + @Override + public FirebaseTokenFactory get() { + return FirebaseTokenUtils.createTokenFactory(app, Clock.SYSTEM, tenantId); + } + }) + .setIdTokenVerifier( + new Supplier() { + @Override + public FirebaseTokenVerifier get() { + return FirebaseTokenUtils.createIdTokenVerifier(app, Clock.SYSTEM, tenantId); + } + }) + .setCookieVerifier( + new Supplier() { + @Override + public FirebaseTokenVerifier get() { + return FirebaseTokenUtils.createSessionCookieVerifier(app, Clock.SYSTEM, tenantId); + } + }) + .setUserManager( + new Supplier() { + @Override + public FirebaseUserManager get() { + return FirebaseUserManager + .builder() + .setFirebaseApp(app) + .setTenantId(tenantId) + .build(); + } + }); + } } diff --git a/src/main/java/com/google/firebase/auth/FirebaseAuth.java b/src/main/java/com/google/firebase/auth/FirebaseAuth.java index f44bd2343..417ea6436 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseAuth.java +++ b/src/main/java/com/google/firebase/auth/FirebaseAuth.java @@ -16,21 +16,11 @@ package com.google.firebase.auth; -import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.base.Preconditions.checkNotNull; - -import com.google.api.client.util.Clock; -import com.google.api.core.ApiFuture; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Strings; import com.google.common.base.Supplier; import com.google.firebase.FirebaseApp; import com.google.firebase.ImplFirebaseTrampolines; -import com.google.firebase.auth.internal.FirebaseTokenFactory; import com.google.firebase.auth.multitenancy.TenantManager; -import com.google.firebase.internal.CallableOperation; import com.google.firebase.internal.FirebaseService; -import com.google.firebase.internal.NonNull; /** * This class is the entry point for all server-side Firebase Authentication actions. @@ -46,14 +36,9 @@ public final class FirebaseAuth extends AbstractFirebaseAuth { private final Supplier tenantManager; - FirebaseAuth(final Builder builder) { + private FirebaseAuth(final Builder builder) { super(builder); - tenantManager = threadSafeMemoize(new Supplier() { - @Override - public TenantManager get() { - return new TenantManager(builder.firebaseApp); - } - }); + tenantManager = threadSafeMemoize(builder.tenantManager); } public TenantManager getTenantManager() { @@ -84,167 +69,18 @@ public static synchronized FirebaseAuth getInstance(FirebaseApp app) { return service.getInstance(); } - /** - * Creates a new Firebase session cookie from the given ID token and options. The returned JWT can - * be set as a server-side session cookie with a custom cookie policy. - * - * @param idToken The Firebase ID token to exchange for a session cookie. - * @param options Additional options required to create the cookie. - * @return A Firebase session cookie string. - * @throws IllegalArgumentException If the ID token is null or empty, or if options is null. - * @throws FirebaseAuthException If an error occurs while generating the session cookie. - */ - public String createSessionCookie(@NonNull String idToken, @NonNull SessionCookieOptions options) - throws FirebaseAuthException { - return createSessionCookieOp(idToken, options).call(); - } - - /** - * Similar to {@link #createSessionCookie(String, SessionCookieOptions)} but performs the - * operation asynchronously. - * - * @param idToken The Firebase ID token to exchange for a session cookie. - * @param options Additional options required to create the cookie. - * @return An {@code ApiFuture} which will complete successfully with a session cookie string. If - * an error occurs while generating the cookie or if the specified ID token is invalid, the - * future throws a {@link FirebaseAuthException}. - * @throws IllegalArgumentException If the ID token is null or empty, or if options is null. - */ - public ApiFuture createSessionCookieAsync( - @NonNull String idToken, @NonNull SessionCookieOptions options) { - return createSessionCookieOp(idToken, options).callAsync(getFirebaseApp()); - } - - private CallableOperation createSessionCookieOp( - final String idToken, final SessionCookieOptions options) { - checkNotDestroyed(); - checkArgument(!Strings.isNullOrEmpty(idToken), "idToken must not be null or empty"); - checkNotNull(options, "options must not be null"); - final FirebaseUserManager userManager = getUserManager(); - return new CallableOperation() { - @Override - protected String execute() throws FirebaseAuthException { - return userManager.createSessionCookie(idToken, options); - } - }; - } - - /** - * Parses and verifies a Firebase session cookie. - * - *

If verified successfully, returns a parsed version of the cookie from which the UID and the - * other claims can be read. If the cookie is invalid, throws a {@link FirebaseAuthException}. - * - *

This method does not check whether the cookie has been revoked. See {@link - * #verifySessionCookie(String, boolean)}. - * - * @param cookie A Firebase session cookie string to verify and parse. - * @return A {@link FirebaseToken} representing the verified and decoded cookie. - */ - public FirebaseToken verifySessionCookie(String cookie) throws FirebaseAuthException { - return verifySessionCookie(cookie, false); - } - - /** - * Parses and verifies a Firebase session cookie. - * - *

If {@code checkRevoked} is true, additionally verifies that the cookie has not been revoked. - * - *

If verified successfully, returns a parsed version of the cookie from which the UID and the - * other claims can be read. If the cookie is invalid or has been revoked while {@code - * checkRevoked} is true, throws a {@link FirebaseAuthException}. - * - * @param cookie A Firebase session cookie string to verify and parse. - * @param checkRevoked A boolean indicating whether to check if the cookie was explicitly revoked. - * @return A {@link FirebaseToken} representing the verified and decoded cookie. - */ - public FirebaseToken verifySessionCookie(String cookie, boolean checkRevoked) - throws FirebaseAuthException { - return verifySessionCookieOp(cookie, checkRevoked).call(); - } - - /** - * Similar to {@link #verifySessionCookie(String)} but performs the operation asynchronously. - * - * @param cookie A Firebase session cookie string to verify and parse. - * @return An {@code ApiFuture} which will complete successfully with the parsed cookie, or - * unsuccessfully with the failure Exception. - */ - public ApiFuture verifySessionCookieAsync(String cookie) { - return verifySessionCookieAsync(cookie, false); - } - - /** - * Similar to {@link #verifySessionCookie(String, boolean)} but performs the operation - * asynchronously. - * - * @param cookie A Firebase session cookie string to verify and parse. - * @param checkRevoked A boolean indicating whether to check if the cookie was explicitly revoked. - * @return An {@code ApiFuture} which will complete successfully with the parsed cookie, or - * unsuccessfully with the failure Exception. - */ - public ApiFuture verifySessionCookieAsync(String cookie, boolean checkRevoked) { - return verifySessionCookieOp(cookie, checkRevoked).callAsync(getFirebaseApp()); - } - - private CallableOperation verifySessionCookieOp( - final String cookie, final boolean checkRevoked) { - checkNotDestroyed(); - checkArgument(!Strings.isNullOrEmpty(cookie), "Session cookie must not be null or empty"); - final FirebaseTokenVerifier sessionCookieVerifier = getSessionCookieVerifier(checkRevoked); - return new CallableOperation() { - @Override - public FirebaseToken execute() throws FirebaseAuthException { - return sessionCookieVerifier.verifyToken(cookie); - } - }; - } - - @VisibleForTesting - FirebaseTokenVerifier getSessionCookieVerifier(boolean checkRevoked) { - FirebaseTokenVerifier verifier = getCookieVerifier(); - if (checkRevoked) { - FirebaseUserManager userManager = getUserManager(); - verifier = RevocationCheckDecorator.decorateSessionCookieVerifier(verifier, userManager); - } - return verifier; - } - @Override protected void doDestroy() { } private static FirebaseAuth fromApp(final FirebaseApp app) { - return new FirebaseAuth( - AbstractFirebaseAuth.builder() - .setFirebaseApp(app) - .setTokenFactory( - new Supplier() { - @Override - public FirebaseTokenFactory get() { - return FirebaseTokenUtils.createTokenFactory(app, Clock.SYSTEM); - } - }) - .setIdTokenVerifier( - new Supplier() { - @Override - public FirebaseTokenVerifier get() { - return FirebaseTokenUtils.createIdTokenVerifier(app, Clock.SYSTEM); - } - }) - .setCookieVerifier( - new Supplier() { - @Override - public FirebaseTokenVerifier get() { - return FirebaseTokenUtils.createSessionCookieVerifier(app, Clock.SYSTEM); - } - }) - .setUserManager( - new Supplier() { - @Override - public FirebaseUserManager get() { - return FirebaseUserManager.builder().setFirebaseApp(app).build(); - } - })); + return populateBuilderFromApp(builder(), app, null) + .setTenantManager(new Supplier() { + @Override + public TenantManager get() { + return new TenantManager(app); + } + }) + .build(); } private static class FirebaseAuthService extends FirebaseService { @@ -258,4 +94,29 @@ public void destroy() { instance.destroy(); } } + + static Builder builder() { + return new Builder(); + } + + static class Builder extends AbstractFirebaseAuth.Builder { + + private Supplier tenantManager; + + private Builder() { } + + @Override + protected Builder getThis() { + return this; + } + + public Builder setTenantManager(Supplier tenantManager) { + this.tenantManager = tenantManager; + return this; + } + + public FirebaseAuth build() { + return new FirebaseAuth(this); + } + } } diff --git a/src/main/java/com/google/firebase/auth/FirebaseTokenUtils.java b/src/main/java/com/google/firebase/auth/FirebaseTokenUtils.java index e0105e9aa..7c8f9f0a5 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseTokenUtils.java +++ b/src/main/java/com/google/firebase/auth/FirebaseTokenUtils.java @@ -99,6 +99,11 @@ static FirebaseTokenVerifierImpl createIdTokenVerifier( } static FirebaseTokenVerifierImpl createSessionCookieVerifier(FirebaseApp app, Clock clock) { + return createSessionCookieVerifier(app, clock, null); + } + + static FirebaseTokenVerifierImpl createSessionCookieVerifier( + FirebaseApp app, Clock clock, @Nullable String tenantId) { String projectId = ImplFirebaseTrampolines.getProjectId(app); checkState(!Strings.isNullOrEmpty(projectId), "Must initialize FirebaseApp with a project ID to call verifySessionCookie()"); @@ -107,12 +112,13 @@ static FirebaseTokenVerifierImpl createSessionCookieVerifier(FirebaseApp app, Cl GooglePublicKeysManager publicKeysManager = newPublicKeysManager( app.getOptions(), clock, SESSION_COOKIE_CERT_URL); return FirebaseTokenVerifierImpl.builder() - .setJsonFactory(app.getOptions().getJsonFactory()) - .setPublicKeysManager(publicKeysManager) - .setIdTokenVerifier(idTokenVerifier) .setShortName("session cookie") .setMethod("verifySessionCookie()") .setDocUrl("https://firebase.google.com/docs/auth/admin/manage-cookies") + .setJsonFactory(app.getOptions().getJsonFactory()) + .setPublicKeysManager(publicKeysManager) + .setIdTokenVerifier(idTokenVerifier) + .setTenantId(tenantId) .build(); } diff --git a/src/main/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuth.java b/src/main/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuth.java index 540437404..95ef169da 100644 --- a/src/main/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuth.java +++ b/src/main/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuth.java @@ -18,9 +18,16 @@ import static com.google.common.base.Preconditions.checkArgument; +import com.google.api.core.ApiAsyncFunction; +import com.google.api.core.ApiFuture; +import com.google.api.core.ApiFutures; import com.google.common.base.Strings; +import com.google.common.util.concurrent.MoreExecutors; import com.google.firebase.FirebaseApp; import com.google.firebase.auth.AbstractFirebaseAuth; +import com.google.firebase.auth.FirebaseAuthException; +import com.google.firebase.auth.FirebaseToken; +import com.google.firebase.auth.SessionCookieOptions; /** * The tenant-aware Firebase client. @@ -32,10 +39,10 @@ public final class TenantAwareFirebaseAuth extends AbstractFirebaseAuth { private final String tenantId; - TenantAwareFirebaseAuth(final FirebaseApp firebaseApp, final String tenantId) { - super(builderFromAppAndTenantId(firebaseApp, tenantId)); - checkArgument(!Strings.isNullOrEmpty(tenantId)); - this.tenantId = tenantId; + private TenantAwareFirebaseAuth(Builder builder) { + super(builder); + checkArgument(!Strings.isNullOrEmpty(builder.tenantId)); + this.tenantId = builder.tenantId; } /** Returns the client's tenant ID. */ @@ -43,8 +50,58 @@ public String getTenantId() { return tenantId; } + @Override + public String createSessionCookie( + String idToken, SessionCookieOptions options) throws FirebaseAuthException { + verifyIdToken(idToken); + return super.createSessionCookie(idToken, options); + } + + @Override + public ApiFuture createSessionCookieAsync( + final String idToken, final SessionCookieOptions options) { + ApiFuture future = verifyIdTokenAsync(idToken); + return ApiFutures.transformAsync(future, new ApiAsyncFunction() { + @Override + public ApiFuture apply(FirebaseToken input) { + return TenantAwareFirebaseAuth.super.createSessionCookieAsync(idToken, options); + } + }, MoreExecutors.directExecutor()); + } + @Override protected void doDestroy() { // Nothing extra needs to be destroyed. } + + static TenantAwareFirebaseAuth fromApp(FirebaseApp app, String tenantId) { + return populateBuilderFromApp(builder(), app, tenantId) + .setTenantId(tenantId) + .build(); + } + + static Builder builder() { + return new Builder(); + } + + static class Builder extends AbstractFirebaseAuth.Builder { + + private String tenantId; + + private Builder() { } + + @Override + protected Builder getThis() { + return this; + } + + public Builder setTenantId(String tenantId) { + this.tenantId = tenantId; + return this; + } + + TenantAwareFirebaseAuth build() { + return new TenantAwareFirebaseAuth(this); + } + } } diff --git a/src/main/java/com/google/firebase/auth/multitenancy/TenantManager.java b/src/main/java/com/google/firebase/auth/multitenancy/TenantManager.java index 11f26b096..dcb226d28 100644 --- a/src/main/java/com/google/firebase/auth/multitenancy/TenantManager.java +++ b/src/main/java/com/google/firebase/auth/multitenancy/TenantManager.java @@ -79,7 +79,7 @@ public Tenant getTenant(@NonNull String tenantId) throws FirebaseAuthException { public synchronized TenantAwareFirebaseAuth getAuthForTenant(@NonNull String tenantId) { checkArgument(!Strings.isNullOrEmpty(tenantId), "Tenant ID must not be null or empty."); if (!tenantAwareAuths.containsKey(tenantId)) { - tenantAwareAuths.put(tenantId, new TenantAwareFirebaseAuth(firebaseApp, tenantId)); + tenantAwareAuths.put(tenantId, TenantAwareFirebaseAuth.fromApp(firebaseApp, tenantId)); } return tenantAwareAuths.get(tenantId); } diff --git a/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java b/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java index 635e1bfba..bfc8d1790 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java +++ b/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java @@ -20,6 +20,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -28,11 +29,11 @@ import com.google.common.base.Defaults; import com.google.common.base.Supplier; import com.google.common.base.Suppliers; -import com.google.common.collect.ImmutableMap; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; import com.google.firebase.TestOnlyImplFirebaseTrampolines; import com.google.firebase.testing.ServiceAccount; +import com.google.firebase.testing.TestResponseInterceptor; import com.google.firebase.testing.TestUtils; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -43,7 +44,6 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; - import org.junit.After; import org.junit.Test; @@ -169,8 +169,7 @@ public void testDefaultIdTokenVerifier() { @Test public void testIdTokenVerifierInitializedOnDemand() throws Exception { - FirebaseTokenVerifier tokenVerifier = MockTokenVerifier.fromResult( - getFirebaseToken("idTokenUser")); + FirebaseTokenVerifier tokenVerifier = MockTokenVerifier.fromUid("idTokenUser"); CountingSupplier countingSupplier = new CountingSupplier<>( Suppliers.ofInstance(tokenVerifier)); @@ -185,37 +184,33 @@ public void testIdTokenVerifierInitializedOnDemand() throws Exception { @Test public void testVerifyIdTokenWithNull() { - MockTokenVerifier tokenVerifier = MockTokenVerifier.fromResult(null); - tokenVerifier.lastTokenString = "_init_"; + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromUid("uid"); FirebaseAuth auth = getAuthForIdTokenVerification(tokenVerifier); try { auth.verifyIdTokenAsync(null); fail("No error thrown for null id token"); } catch (IllegalArgumentException expected) { - assertEquals("_init_", tokenVerifier.getLastTokenString()); + assertNull(tokenVerifier.getLastTokenString()); } } @Test public void testVerifyIdTokenWithEmptyString() { - MockTokenVerifier tokenVerifier = MockTokenVerifier.fromResult(null); - tokenVerifier.lastTokenString = "_init_"; + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromUid("uid"); FirebaseAuth auth = getAuthForIdTokenVerification(tokenVerifier); try { auth.verifyIdTokenAsync(""); fail("No error thrown for null id token"); } catch (IllegalArgumentException expected) { - assertEquals("_init_", tokenVerifier.getLastTokenString()); + assertNull(tokenVerifier.getLastTokenString()); } - } @Test public void testVerifyIdToken() throws Exception { - MockTokenVerifier tokenVerifier = MockTokenVerifier.fromResult( - getFirebaseToken("testUser")); + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromUid("testUser"); FirebaseAuth auth = getAuthForIdTokenVerification(tokenVerifier); FirebaseToken firebaseToken = auth.verifyIdToken("idtoken"); @@ -242,8 +237,7 @@ public void testVerifyIdTokenFailure() { @Test public void testVerifyIdTokenAsync() throws Exception { - MockTokenVerifier tokenVerifier = MockTokenVerifier.fromResult( - getFirebaseToken("testUser")); + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromUid("testUser"); FirebaseAuth auth = getAuthForIdTokenVerification(tokenVerifier); FirebaseToken firebaseToken = auth.verifyIdTokenAsync("idtoken").get(); @@ -300,8 +294,7 @@ public void testDefaultSessionCookieVerifier() { @Test public void testSessionCookieVerifierInitializedOnDemand() throws Exception { - FirebaseTokenVerifier tokenVerifier = MockTokenVerifier.fromResult( - getFirebaseToken("cookieUser")); + FirebaseTokenVerifier tokenVerifier = MockTokenVerifier.fromUid("cookieUser"); CountingSupplier countingSupplier = new CountingSupplier<>( Suppliers.ofInstance(tokenVerifier)); FirebaseAuth auth = getAuthForSessionCookieVerification(countingSupplier); @@ -316,37 +309,33 @@ public void testSessionCookieVerifierInitializedOnDemand() throws Exception { @Test public void testVerifySessionCookieWithNull() { - MockTokenVerifier tokenVerifier = MockTokenVerifier.fromResult(null); - tokenVerifier.lastTokenString = "_init_"; + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromUid("uid"); FirebaseAuth auth = getAuthForSessionCookieVerification(tokenVerifier); try { auth.verifySessionCookieAsync(null); fail("No error thrown for null id token"); } catch (IllegalArgumentException expected) { - assertEquals("_init_", tokenVerifier.getLastTokenString()); + assertNull(tokenVerifier.getLastTokenString()); } } @Test public void testVerifySessionCookieWithEmptyString() { - MockTokenVerifier tokenVerifier = MockTokenVerifier.fromResult(null); - tokenVerifier.lastTokenString = "_init_"; + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromUid("uid"); FirebaseAuth auth = getAuthForSessionCookieVerification(tokenVerifier); try { auth.verifySessionCookieAsync(""); fail("No error thrown for null id token"); } catch (IllegalArgumentException expected) { - assertEquals("_init_", tokenVerifier.getLastTokenString()); + assertNull(tokenVerifier.getLastTokenString()); } - } @Test public void testVerifySessionCookie() throws Exception { - MockTokenVerifier tokenVerifier = MockTokenVerifier.fromResult( - getFirebaseToken("testUser")); + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromUid("testUser"); FirebaseAuth auth = getAuthForSessionCookieVerification(tokenVerifier); FirebaseToken firebaseToken = auth.verifySessionCookie("idtoken"); @@ -373,8 +362,7 @@ public void testVerifySessionCookieFailure() { @Test public void testVerifySessionCookieAsync() throws Exception { - MockTokenVerifier tokenVerifier = MockTokenVerifier.fromResult( - getFirebaseToken("testUser")); + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromUid("testUser"); FirebaseAuth auth = getAuthForSessionCookieVerification(tokenVerifier); FirebaseToken firebaseToken = auth.verifySessionCookieAsync("idtoken").get(); @@ -417,10 +405,6 @@ public void testVerifySessionCookieWithCheckRevokedAsyncFailure() throws Interru } } - private FirebaseToken getFirebaseToken(String subject) { - return new FirebaseToken(ImmutableMap.of("sub", subject)); - } - private FirebaseAuth getAuthForIdTokenVerification(FirebaseTokenVerifier tokenVerifier) { return getAuthForIdTokenVerification(Suppliers.ofInstance(tokenVerifier)); } @@ -429,11 +413,11 @@ private FirebaseAuth getAuthForIdTokenVerification( Supplier tokenVerifierSupplier) { FirebaseApp app = FirebaseApp.initializeApp(firebaseOptions); FirebaseUserManager userManager = FirebaseUserManager.builder().setFirebaseApp(app).build(); - return new FirebaseAuth( - AbstractFirebaseAuth.builder() - .setFirebaseApp(app) - .setIdTokenVerifier(tokenVerifierSupplier) - .setUserManager(Suppliers.ofInstance(userManager))); + return FirebaseAuth.builder() + .setFirebaseApp(app) + .setIdTokenVerifier(tokenVerifierSupplier) + .setUserManager(Suppliers.ofInstance(userManager)) + .build(); } private FirebaseAuth getAuthForSessionCookieVerification(FirebaseTokenVerifier tokenVerifier) { @@ -444,45 +428,23 @@ private FirebaseAuth getAuthForSessionCookieVerification( Supplier tokenVerifierSupplier) { FirebaseApp app = FirebaseApp.initializeApp(firebaseOptions); FirebaseUserManager userManager = FirebaseUserManager.builder().setFirebaseApp(app).build(); - return new FirebaseAuth( - AbstractFirebaseAuth.builder() - .setFirebaseApp(app) - .setCookieVerifier(tokenVerifierSupplier) - .setUserManager(Suppliers.ofInstance(userManager))); + return FirebaseAuth.builder() + .setFirebaseApp(app) + .setCookieVerifier(tokenVerifierSupplier) + .setUserManager(Suppliers.ofInstance(userManager)) + .build(); } - private static class MockTokenVerifier implements FirebaseTokenVerifier { - - private String lastTokenString; - - private FirebaseToken result; - private FirebaseAuthException exception; - - private MockTokenVerifier(FirebaseToken result, FirebaseAuthException exception) { - this.result = result; - this.exception = exception; - } - - @Override - public FirebaseToken verifyToken(String token) throws FirebaseAuthException { - lastTokenString = token; - if (exception != null) { - throw exception; - } - return result; - } - - String getLastTokenString() { - return this.lastTokenString; - } - - static MockTokenVerifier fromResult(FirebaseToken result) { - return new MockTokenVerifier(result, null); - } - - static MockTokenVerifier fromException(FirebaseAuthException exception) { - return new MockTokenVerifier(null, exception); - } + public static TestResponseInterceptor setUserManager( + AbstractFirebaseAuth.Builder builder, FirebaseApp app, String tenantId) { + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + FirebaseUserManager userManager = FirebaseUserManager.builder() + .setFirebaseApp(app) + .setTenantId(tenantId) + .build(); + userManager.setInterceptor(interceptor); + builder.setUserManager(Suppliers.ofInstance(userManager)); + return interceptor; } private static class CountingSupplier implements Supplier { diff --git a/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java b/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java index 5dd1e9c14..379da0a57 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java +++ b/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java @@ -217,6 +217,19 @@ public void testMalformedToken() throws Exception { tokenVerifier.verifyToken("not.a.jwt"); } + @Test + public void testVerifyTokenWithTenantId() throws FirebaseAuthException { + FirebaseTokenVerifierImpl verifier = fullyPopulatedBuilder() + .setTenantId("TENANT_1") + .build(); + + FirebaseToken firebaseToken = verifier.verifyToken(createTokenWithTenantId("TENANT_1")); + + assertEquals(TEST_TOKEN_ISSUER, firebaseToken.getIssuer()); + assertEquals(TestTokenFactory.UID, firebaseToken.getUid()); + assertEquals("TENANT_1", firebaseToken.getTenantId()); + } + @Test public void testVerifyTokenDifferentTenantIds() { try { diff --git a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java index d407fd8ec..7012ef6bf 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java +++ b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java @@ -2602,19 +2602,18 @@ private static FirebaseAuth getRetryDisabledAuth(MockLowLevelHttpResponse respon .setProjectId("test-project-id") .setHttpTransport(transport) .build()); - return new FirebaseAuth( - AbstractFirebaseAuth.builder() - .setFirebaseApp(app) - .setUserManager(new Supplier() { - @Override - public FirebaseUserManager get() { - return FirebaseUserManager - .builder() - .setFirebaseApp(app) - .setHttpRequestFactory(transport.createRequestFactory()) - .build(); - } - })); + return FirebaseAuth.builder() + .setFirebaseApp(app) + .setUserManager(new Supplier() { + @Override + public FirebaseUserManager get() { + return FirebaseUserManager.builder() + .setFirebaseApp(app) + .setHttpRequestFactory(transport.createRequestFactory()) + .build(); + } + }) + .build(); } private static void checkUserRecord(UserRecord userRecord) { diff --git a/src/test/java/com/google/firebase/auth/MockTokenVerifier.java b/src/test/java/com/google/firebase/auth/MockTokenVerifier.java new file mode 100644 index 000000000..51f7d5a47 --- /dev/null +++ b/src/test/java/com/google/firebase/auth/MockTokenVerifier.java @@ -0,0 +1,59 @@ +/* + * Copyright 2020 Google LLC + * + * 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.common.collect.ImmutableMap; +import java.util.concurrent.atomic.AtomicReference; + +public class MockTokenVerifier implements FirebaseTokenVerifier { + + private final FirebaseToken result; + private final FirebaseAuthException exception; + private final AtomicReference lastTokenString = new AtomicReference<>(); + + private MockTokenVerifier(FirebaseToken result, FirebaseAuthException exception) { + this.result = result; + this.exception = exception; + } + + @Override + public FirebaseToken verifyToken(String token) throws FirebaseAuthException { + lastTokenString.set(token); + if (exception != null) { + throw exception; + } + return result; + } + + public String getLastTokenString() { + return this.lastTokenString.get(); + } + + public static MockTokenVerifier fromUid(String uid) { + long iat = System.currentTimeMillis() / 1000; + return fromResult(new FirebaseToken(ImmutableMap.of( + "sub", uid, "iat", iat))); + } + + public static MockTokenVerifier fromResult(FirebaseToken result) { + return new MockTokenVerifier(result, null); + } + + public static MockTokenVerifier fromException(FirebaseAuthException exception) { + return new MockTokenVerifier(null, exception); + } +} diff --git a/src/test/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuthTest.java b/src/test/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuthTest.java new file mode 100644 index 000000000..df7bcf00d --- /dev/null +++ b/src/test/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuthTest.java @@ -0,0 +1,292 @@ +/* + * Copyright 2020 Google LLC + * + * 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.multitenancy; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import com.google.api.client.googleapis.util.Utils; +import com.google.api.client.http.HttpMethods; +import com.google.api.client.http.HttpRequest; +import com.google.api.client.json.GenericJson; +import com.google.api.client.testing.http.MockHttpTransport; +import com.google.api.client.testing.http.MockLowLevelHttpResponse; +import com.google.common.base.Suppliers; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.TestOnlyImplFirebaseTrampolines; +import com.google.firebase.auth.FirebaseAuthException; +import com.google.firebase.auth.FirebaseAuthTest; +import com.google.firebase.auth.FirebaseToken; +import com.google.firebase.auth.MockGoogleCredentials; +import com.google.firebase.auth.MockTokenVerifier; +import com.google.firebase.auth.SessionCookieOptions; +import com.google.firebase.testing.TestResponseInterceptor; +import com.google.firebase.testing.TestUtils; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.math.BigDecimal; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import org.junit.After; +import org.junit.Test; + +public class TenantAwareFirebaseAuthTest { + + private static final String TENANT_ID = "test-tenant"; + + private static final String AUTH_BASE_URL = "/v1/projects/test-project-id/tenants/" + TENANT_ID; + + private static final SessionCookieOptions COOKIE_OPTIONS = SessionCookieOptions.builder() + .setExpiresIn(TimeUnit.HOURS.toMillis(1)) + .build(); + + private static final FirebaseAuthException AUTH_EXCEPTION = new FirebaseAuthException( + "code", "reason"); + + private static final String CREATE_COOKIE_RESPONSE = TestUtils.loadResource( + "createSessionCookie.json"); + + @After + public void tearDown() { + TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); + } + + @Test + public void testCreateSessionCookieAsync() throws Exception { + MockTokenVerifier verifier = MockTokenVerifier.fromUid("uid"); + TenantAwareFirebaseAuth.Builder builder = builderForTokenVerification(verifier); + TestResponseInterceptor interceptor = setUserManager(builder, CREATE_COOKIE_RESPONSE); + TenantAwareFirebaseAuth auth = builder.build(); + + String cookie = auth.createSessionCookieAsync("testToken", COOKIE_OPTIONS).get(); + + assertEquals("MockCookieString", cookie); + assertEquals("testToken", verifier.getLastTokenString()); + GenericJson parsed = parseRequestContent(interceptor); + assertEquals(2, parsed.size()); + assertEquals("testToken", parsed.get("idToken")); + assertEquals(new BigDecimal(3600), parsed.get("validDuration")); + checkUrl(interceptor, AUTH_BASE_URL + ":createSessionCookie"); + } + + @Test + public void testCreateSessionCookieAsyncError() throws Exception { + TenantAwareFirebaseAuth.Builder builder = builderForTokenVerification( + MockTokenVerifier.fromException(AUTH_EXCEPTION)); + TestResponseInterceptor interceptor = setUserManager(builder, CREATE_COOKIE_RESPONSE); + TenantAwareFirebaseAuth auth = builder.build(); + + try { + auth.createSessionCookieAsync("testToken", COOKIE_OPTIONS).get(); + fail("No error thrown for invalid ID token"); + } catch (ExecutionException e) { + assertTrue(e.getCause() instanceof FirebaseAuthException); + FirebaseAuthException cause = (FirebaseAuthException) e.getCause(); + assertEquals("code", cause.getErrorCode()); + } + + assertNull(interceptor.getResponse()); + } + + @Test + public void testCreateSessionCookie() throws Exception { + TenantAwareFirebaseAuth.Builder builder = builderForTokenVerification( + MockTokenVerifier.fromUid("uid")); + TestResponseInterceptor interceptor = setUserManager(builder, CREATE_COOKIE_RESPONSE); + TenantAwareFirebaseAuth auth = builder.build(); + + String cookie = auth.createSessionCookie("testToken", COOKIE_OPTIONS); + + assertEquals("MockCookieString", cookie); + GenericJson parsed = parseRequestContent(interceptor); + assertEquals(2, parsed.size()); + assertEquals("testToken", parsed.get("idToken")); + assertEquals(new BigDecimal(3600), parsed.get("validDuration")); + checkUrl(interceptor, AUTH_BASE_URL + ":createSessionCookie"); + } + + @Test + public void testCreateSessionCookieError() { + TenantAwareFirebaseAuth.Builder builder = builderForTokenVerification( + MockTokenVerifier.fromException(AUTH_EXCEPTION)); + TestResponseInterceptor interceptor = setUserManager(builder, CREATE_COOKIE_RESPONSE); + TenantAwareFirebaseAuth auth = builder.build(); + + try { + auth.createSessionCookie("testToken", COOKIE_OPTIONS); + fail("No error thrown for invalid ID token"); + } catch (FirebaseAuthException e) { + assertEquals("code", e.getErrorCode()); + } + + assertNull(interceptor.getResponse()); + } + + @Test + public void testVerifySessionCookie() throws FirebaseAuthException { + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromUid("uid"); + TenantAwareFirebaseAuth.Builder builder = builderForTokenVerification(tokenVerifier); + setUserManager(builder); + TenantAwareFirebaseAuth auth = builder.build(); + + FirebaseToken token = auth.verifySessionCookie("cookie"); + + assertEquals("uid", token.getUid()); + assertEquals("cookie", tokenVerifier.getLastTokenString()); + } + + @Test + public void testVerifySessionCookieFailure() { + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException(AUTH_EXCEPTION); + TenantAwareFirebaseAuth.Builder builder = builderForTokenVerification(tokenVerifier); + setUserManager(builder); + TenantAwareFirebaseAuth auth = builder.build(); + + try { + auth.verifySessionCookie("cookie"); + fail("No error thrown for invalid token"); + } catch (FirebaseAuthException authException) { + assertEquals("code", authException.getErrorCode()); + assertEquals("cookie", tokenVerifier.getLastTokenString()); + } + } + + @Test + public void testVerifySessionCookieAsync() throws Exception { + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromUid("uid"); + TenantAwareFirebaseAuth.Builder builder = builderForTokenVerification(tokenVerifier); + setUserManager(builder); + TenantAwareFirebaseAuth auth = builder.build(); + + FirebaseToken firebaseToken = auth.verifySessionCookieAsync("cookie").get(); + + assertEquals("uid", firebaseToken.getUid()); + assertEquals("cookie", tokenVerifier.getLastTokenString()); + } + + @Test + public void testVerifySessionCookieAsyncFailure() throws InterruptedException { + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException(AUTH_EXCEPTION); + TenantAwareFirebaseAuth.Builder builder = builderForTokenVerification(tokenVerifier); + setUserManager(builder); + TenantAwareFirebaseAuth auth = builder.build(); + + try { + auth.verifySessionCookieAsync("cookie").get(); + fail("No error thrown for invalid token"); + } catch (ExecutionException e) { + FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); + assertEquals("code", authException.getErrorCode()); + assertEquals("cookie", tokenVerifier.getLastTokenString()); + } + } + + @Test + public void testVerifySessionCookieWithCheckRevoked() throws FirebaseAuthException { + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromUid("uid"); + TenantAwareFirebaseAuth.Builder builder = builderForTokenVerification(tokenVerifier); + TestResponseInterceptor interceptor = setUserManager( + builder, TestUtils.loadResource("getUser.json")); + TenantAwareFirebaseAuth auth = builder.build(); + + FirebaseToken token = auth.verifySessionCookie("cookie", true); + + assertEquals("uid", token.getUid()); + assertEquals("cookie", tokenVerifier.getLastTokenString()); + checkUrl(interceptor, AUTH_BASE_URL + "/accounts:lookup"); + } + + @Test + public void testVerifySessionCookieWithCheckRevokedFailure() { + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException(AUTH_EXCEPTION); + TenantAwareFirebaseAuth.Builder builder = builderForTokenVerification(tokenVerifier); + setUserManager(builder); + TenantAwareFirebaseAuth auth = builder.build(); + + try { + auth.verifySessionCookie("cookie", true); + fail("No error thrown for invalid token"); + } catch (FirebaseAuthException e) { + assertEquals("code", e.getErrorCode()); + assertEquals("cookie", tokenVerifier.getLastTokenString()); + } + } + + @Test + public void testVerifySessionCookieWithCheckRevokedAsyncFailure() throws InterruptedException { + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException(AUTH_EXCEPTION); + TenantAwareFirebaseAuth.Builder builder = builderForTokenVerification(tokenVerifier); + setUserManager(builder); + TenantAwareFirebaseAuth auth = builder.build(); + + try { + auth.verifySessionCookieAsync("cookie", true).get(); + fail("No error thrown for invalid token"); + } catch (ExecutionException e) { + FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); + assertEquals("code", authException.getErrorCode()); + assertEquals("cookie", tokenVerifier.getLastTokenString()); + } + } + + private static TenantAwareFirebaseAuth.Builder builderForTokenVerification( + MockTokenVerifier verifier) { + return TenantAwareFirebaseAuth.builder() + .setTenantId(TENANT_ID) + .setIdTokenVerifier(Suppliers.ofInstance(verifier)) + .setCookieVerifier(Suppliers.ofInstance(verifier)); + } + + private static TestResponseInterceptor setUserManager(TenantAwareFirebaseAuth.Builder builder) { + return setUserManager(builder, "{}"); + } + + private static TestResponseInterceptor setUserManager( + TenantAwareFirebaseAuth.Builder builder, String response) { + FirebaseApp app = initializeAppWithResponse(response); + builder.setFirebaseApp(app); + return FirebaseAuthTest.setUserManager(builder, app, TENANT_ID); + } + + private static FirebaseApp initializeAppWithResponse(String response) { + MockHttpTransport transport = new MockHttpTransport.Builder() + .setLowLevelHttpResponse(new MockLowLevelHttpResponse().setContent(response)) + .build(); + return FirebaseApp.initializeApp(new FirebaseOptions.Builder() + .setCredentials(new MockGoogleCredentials("token")) + .setHttpTransport(transport) + .setProjectId("test-project-id") + .build()); + } + + private static GenericJson parseRequestContent(TestResponseInterceptor interceptor) + throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + interceptor.getResponse().getRequest().getContent().writeTo(out); + return Utils.getDefaultJsonFactory().fromString( + new String(out.toByteArray()), GenericJson.class); + } + + private static void checkUrl(TestResponseInterceptor interceptor, String url) { + HttpRequest request = interceptor.getResponse().getRequest(); + assertEquals(HttpMethods.POST, request.getRequestMethod()); + assertEquals(url, request.getUrl().getRawPath()); + } +} From 8c86411a5acef67906699bb543df52161ee93a0a Mon Sep 17 00:00:00 2001 From: shambhand <33409682+shambhand@users.noreply.github.com> Date: Tue, 11 Aug 2020 01:22:29 +0530 Subject: [PATCH 025/354] fix(fcm): Added toString() in TopicManagementResponse for logging (#469) * Added toString() in TopicManagementResponse for logging (#343) * Used MoreObjects.toStringHelper() * removed .DS_Store & added it to .gitignore * unit test case for TopicManagementResponse toString() functionality added for logging * fixed checkStyle issue of line should not greater than 100 char --- .gitignore | 1 + .../firebase/messaging/TopicManagementResponse.java | 9 +++++++++ .../firebase/messaging/InstanceIdClientImplTest.java | 11 +++++++++++ 3 files changed, 21 insertions(+) diff --git a/.gitignore b/.gitignore index a869aa65d..6e7d29cfd 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ target/ release.properties integration_cert.json integration_apikey.txt +.DS_Store diff --git a/src/main/java/com/google/firebase/messaging/TopicManagementResponse.java b/src/main/java/com/google/firebase/messaging/TopicManagementResponse.java index b8f92576e..bbf4c944a 100644 --- a/src/main/java/com/google/firebase/messaging/TopicManagementResponse.java +++ b/src/main/java/com/google/firebase/messaging/TopicManagementResponse.java @@ -20,6 +20,7 @@ import static com.google.common.base.Preconditions.checkState; import com.google.api.client.json.GenericJson; +import com.google.common.base.MoreObjects; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.firebase.internal.NonNull; @@ -122,5 +123,13 @@ public int getIndex() { public String getReason() { return reason; } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("index", index) + .add("reason", reason) + .toString(); + } } } diff --git a/src/test/java/com/google/firebase/messaging/InstanceIdClientImplTest.java b/src/test/java/com/google/firebase/messaging/InstanceIdClientImplTest.java index f939c8382..c7e1cc24a 100644 --- a/src/test/java/com/google/firebase/messaging/InstanceIdClientImplTest.java +++ b/src/test/java/com/google/firebase/messaging/InstanceIdClientImplTest.java @@ -340,6 +340,17 @@ public void testTopicManagementResponseWithEmptyList() { new TopicManagementResponse(ImmutableList.of()); } + @Test + public void testTopicManagementResponseErrorToString() { + GenericJson json = new GenericJson().set("error", "test error"); + ImmutableList jsonList = ImmutableList.of(json); + + TopicManagementResponse topicManagementResponse = new TopicManagementResponse(jsonList); + + String expected = "[Error{index=0, reason=unknown-error}]"; + assertEquals(expected, topicManagementResponse.getErrors().toString()); + } + private static InstanceIdClientImpl initInstanceIdClient( final MockLowLevelHttpResponse mockResponse, final HttpResponseInterceptor interceptor) { From e72bf8abb3fb7b681845f6d2cc0cdea5f80af61b Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Tue, 11 Aug 2020 16:23:20 -0400 Subject: [PATCH 026/354] Fix javadoc error caused by having protected parameters public methods (#470) --- .../google/firebase/auth/AbstractFirebaseAuth.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java b/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java index 6c841070c..7061d2b44 100644 --- a/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java +++ b/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java @@ -1782,11 +1782,6 @@ public T setFirebaseApp(FirebaseApp firebaseApp) { return getThis(); } - public T setTokenFactory(Supplier tokenFactory) { - this.tokenFactory = tokenFactory; - return getThis(); - } - public T setIdTokenVerifier(Supplier idTokenVerifier) { this.idTokenVerifier = idTokenVerifier; return getThis(); @@ -1797,10 +1792,15 @@ public T setCookieVerifier(Supplier cookieVerif return getThis(); } - public T setUserManager(Supplier userManager) { + T setUserManager(Supplier userManager) { this.userManager = userManager; return getThis(); } + + T setTokenFactory(Supplier tokenFactory) { + this.tokenFactory = tokenFactory; + return getThis(); + } } protected static > T populateBuilderFromApp( From 36eb4eaae143e808c529659b72c0aa234c8a4678 Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Wed, 12 Aug 2020 13:46:18 -0400 Subject: [PATCH 027/354] [chore] Release 6.16.0 (#471) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index b0187543a..c2dc9f336 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ com.google.firebase firebase-admin - 6.15.0 + 6.16.0 jar firebase-admin From cc520a721ef3ced13c6c1e34fa010d082a948b99 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Tue, 18 Aug 2020 10:24:17 -0700 Subject: [PATCH 028/354] change: New error handling APIs (merging v7 branch into master) (#465) * Added core types for the error handling revamp (#350) * Added core types for the error handling revamp * Fixed copyright year * Added ErrorHandlingHttpClient API (#353) * Added core error handling abstractions * Added unit tests for the new functionality * Exposed getErrorCode as getErrorCodeNew * Enabled error code assertions * Error handling revamp for the FirebaseMessaging API (#357) * Added core error handling abstractions * Added unit tests for the new functionality * Exposed getErrorCode as getErrorCodeNew * Enabled error code assertions * Error handling revamp for FCM APIs * Cleaned up the FirebaseMessagingException * Cleaned up the AbstractHttpErrorHandler class * Updated tests * Error handling revamp for FirebaseInstanceId API (#359) * Delete instance ID API error handling revamp * Added tests for IO and parse errors * fix(auth): Migrated user management APIs to the new error handling scheme (#360) * Error handling revamp in FirebaseUserManager * Updated integration tests; Added documentation * Assigning the correct ErrorCode for auth errors * Moved AuthErrorHandler to a separate top-level class * Error handling revamp for token verification APIs (#362) * Error handling revamp for token verification APIs * Updated javadocs * Error handling revamp for the custom token creation API (#366) * Error handlign revamp for the custom token creation API * Using the correct authorized HTTP client for IAM requests * Error handling revamp for the project management API (#367) * Error handling revamp for the project management API * Minor code and test cleanup * Fixed some lint errors; Removed requestFactory reference from project mgt service impl * Renamed getErrorCodeNew() to getErrorCode() (#379) * Minor code and test cleanup * Renamed getErrorCodeNew() to getErrorCode() in FirebaseException * Fixing checkstyle error * Fixing some deprecation warnings (#380) * Handling IID error codes correctly (#381) * Removed old deprecated APIs (#383) * fix: Removed unused FirebaseAppStore abstraction (#427) * fix: Removed unused FirebaseAppStore abstraction * Using the keySet of App instances to populate the app names list * chore: Removing redundant test dependency (#441) * chore: Make user import hashing classes final (#425) * chore: Merged with v7 branch with master (#456) * fix(fcm): Replacing deprecated Batch API constructor (#460) * fix: Handling http method override in ErrorHandlingHttpClient (#459) * fix: Handling JSON serialization/response interception at ErrorHandlingHttpClient (#462) * fix: Handling JSON serialization and response interception at ErrorHandlingHttpClient * fix: Removing redundant method override header * feat: Added new error codes for IdP management and multitenancy (#458) * feat: Added new error codes for IdP management and multitenancy * fix: Updated integration tests * fix: Renamed helper method * fix: Removing some calls to deprecated APIs (#464) * chore: Support for specifying query parameters in HttpRequestInfo (#463) * chore: Support for specifying query parameters in HttpRequestInfo * fix: Removing redundant JsonObjectParser from HttpClient * fix: Fixing a series of javadoc warnings (#466) * fix: Made some APIs on AbstractFirebaseAuth.Builder package-protected for consistency * Apply suggestions from code review Co-authored-by: egilmorez Co-authored-by: Kevin Cheung * fix: Minor updates to API ref docs based on code review comments * fix: Fixing API doc wording Co-authored-by: Horatiu Lazu Co-authored-by: egilmorez Co-authored-by: Kevin Cheung --- pom.xml | 6 - .../java/com/google/firebase/ErrorCode.java | 109 +++++ .../java/com/google/firebase/FirebaseApp.java | 42 +- .../FirebaseAppLifecycleListener.java | 2 +- .../google/firebase/FirebaseException.java | 51 ++- .../com/google/firebase/FirebaseOptions.java | 20 +- .../google/firebase/IncomingHttpResponse.java | 114 +++++ .../google/firebase/OutgoingHttpRequest.java | 98 ++++ .../firebase/auth/AbstractFirebaseAuth.java | 26 +- .../google/firebase/auth/AuthErrorCode.java | 104 +++++ .../firebase/auth/FirebaseAuthException.java | 36 +- .../firebase/auth/FirebaseTokenUtils.java | 4 + .../auth/FirebaseTokenVerifierImpl.java | 131 ++++-- .../firebase/auth/FirebaseUserManager.java | 230 ++++------ .../auth/ListProviderConfigsPage.java | 20 +- .../firebase/auth/OidcProviderConfig.java | 2 +- .../auth/RevocationCheckDecorator.java | 23 +- .../com/google/firebase/auth/hash/Bcrypt.java | 2 +- .../google/firebase/auth/hash/HmacMd5.java | 2 +- .../google/firebase/auth/hash/HmacSha1.java | 2 +- .../google/firebase/auth/hash/HmacSha256.java | 2 +- .../google/firebase/auth/hash/HmacSha512.java | 2 +- .../com/google/firebase/auth/hash/Md5.java | 2 +- .../firebase/auth/hash/Pbkdf2Sha256.java | 2 +- .../google/firebase/auth/hash/PbkdfSha1.java | 2 +- .../com/google/firebase/auth/hash/Sha1.java | 2 +- .../com/google/firebase/auth/hash/Sha256.java | 2 +- .../com/google/firebase/auth/hash/Sha512.java | 2 +- .../firebase/auth/hash/StandardScrypt.java | 2 +- .../auth/internal/AuthErrorHandler.java | 222 +++++++++ .../auth/internal/AuthHttpClient.java | 111 +---- .../firebase/auth/internal/CryptoSigner.java | 5 +- .../firebase/auth/internal/CryptoSigners.java | 90 ++-- .../auth/internal/FirebaseTokenFactory.java | 38 +- .../auth/internal/HttpErrorResponse.java | 49 -- .../multitenancy/FirebaseTenantClient.java | 46 +- .../auth/multitenancy/ListTenantsPage.java | 12 +- .../auth/multitenancy/TenantManager.java | 9 +- .../database/core/JvmAuthTokenProvider.java | 2 +- .../firebase/iid/FirebaseInstanceId.java | 101 +++-- .../iid/FirebaseInstanceIdException.java | 6 +- .../internal/AbstractHttpErrorHandler.java | 154 +++++++ .../AbstractPlatformErrorHandler.java | 98 ++++ .../firebase/internal/ApiClientUtils.java | 2 + .../internal/ErrorHandlingHttpClient.java | 144 ++++++ .../firebase/internal/FirebaseAppStore.java | 78 ---- .../firebase/internal/HttpErrorHandler.java | 44 ++ .../firebase/internal/HttpRequestInfo.java | 132 ++++++ .../firebase/messaging/FirebaseMessaging.java | 4 - .../FirebaseMessagingClientImpl.java | 255 ++++++----- .../messaging/FirebaseMessagingException.java | 48 +- .../messaging/InstanceIdClientImpl.java | 165 +++---- .../firebase/messaging/LightSettings.java | 4 +- .../messaging/MessagingErrorCode.java | 43 ++ .../firebase/messaging/Notification.java | 31 +- .../MessagingServiceErrorResponse.java | 37 +- .../FirebaseProjectManagementException.java | 21 +- .../FirebaseProjectManagementServiceImpl.java | 66 +-- .../projectmanagement/HttpHelper.java | 170 +++---- .../com/google/firebase/FirebaseAppTest.java | 33 +- .../firebase/FirebaseExceptionTest.java | 124 +++++ .../google/firebase/FirebaseOptionsTest.java | 39 +- .../firebase/IncomingHttpResponseTest.java | 141 ++++++ .../firebase/OutgoingHttpRequestTest.java | 89 ++++ .../google/firebase/ThreadManagerTest.java | 7 +- .../google/firebase/auth/FirebaseAuthIT.java | 121 +++-- .../firebase/auth/FirebaseAuthTest.java | 177 ++++++-- .../auth/FirebaseTokenVerifierImplTest.java | 244 +++++++--- .../auth/FirebaseUserManagerTest.java | 423 ++++++++++++------ .../auth/ProviderConfigTestUtils.java | 12 +- .../google/firebase/auth/UserTestUtils.java | 4 +- .../auth/internal/CryptoSignersTest.java | 54 ++- .../internal/FirebaseTokenFactoryTest.java | 7 +- .../FirebaseTenantClientTest.java | 91 ++-- .../TenantAwareFirebaseAuthIT.java | 5 +- .../TenantAwareFirebaseAuthTest.java | 15 +- .../auth/multitenancy/TenantManagerIT.java | 8 +- .../firebase/cloud/FirestoreClientTest.java | 15 +- .../firebase/cloud/StorageClientTest.java | 10 +- .../firebase/database/DataSnapshotTest.java | 2 +- .../database/DatabaseReferenceTest.java | 2 +- .../database/FirebaseDatabaseTest.java | 4 +- .../google/firebase/database/TestHelpers.java | 4 +- .../database/ValueExpectationHelper.java | 4 +- .../collection/RBTreeSortedMapTest.java | 18 +- .../core/JvmAuthTokenProviderTest.java | 28 +- .../database/core/JvmPlatformTest.java | 4 +- .../firebase/database/core/RepoTest.java | 2 +- .../firebase/database/core/SyncPointTest.java | 2 +- .../DefaultPersistenceManagerTest.java | 2 +- .../persistence/RandomPersistenceTest.java | 2 +- .../database/integration/DataTestIT.java | 4 +- .../FirebaseDatabaseAuthTestIT.java | 16 +- .../integration/FirebaseDatabaseTestIT.java | 4 +- .../database/integration/RulesTestIT.java | 2 +- .../database/integration/ShutdownExample.java | 2 +- .../database/utilities/ValidationTest.java | 38 +- .../firebase/iid/FirebaseInstanceIdTest.java | 170 +++++-- .../AbstractPlatformErrorHandlerTest.java | 298 ++++++++++++ .../internal/CallableOperationTest.java | 3 +- .../internal/ErrorHandlingHttpClientTest.java | 358 +++++++++++++++ .../internal/FirebaseAppStoreTest.java | 17 +- .../FirebaseRequestInitializerTest.java | 10 +- .../internal/FirebaseThreadManagersTest.java | 8 +- .../firebase/internal/TestApiClientUtils.java | 21 +- .../firebase/messaging/BatchResponseTest.java | 5 +- .../FirebaseMessagingClientImplTest.java | 137 ++++-- .../messaging/FirebaseMessagingIT.java | 46 +- .../messaging/FirebaseMessagingTest.java | 3 +- .../messaging/InstanceIdClientImplTest.java | 127 ++++-- .../firebase/messaging/MessageTest.java | 33 +- .../messaging/MulticastMessageTest.java | 5 +- .../firebase/messaging/SendResponseTest.java | 5 +- ...ebaseProjectManagementServiceImplTest.java | 182 ++++++-- .../FirebaseProjectManagementTest.java | 5 +- .../projectmanagement/IosAppTest.java | 3 +- .../snippets/FirebaseAppSnippets.java | 14 +- .../snippets/FirebaseAuthSnippets.java | 3 +- .../snippets/FirebaseDatabaseSnippets.java | 2 +- .../snippets/FirebaseMessagingSnippets.java | 24 +- .../snippets/FirebaseStorageSnippets.java | 2 +- .../firebase/testing/FirebaseAppRule.java | 2 - .../testing/IntegrationTestUtils.java | 3 +- .../google/firebase/testing/TestUtils.java | 10 +- 124 files changed, 4687 insertions(+), 1797 deletions(-) create mode 100644 src/main/java/com/google/firebase/ErrorCode.java create mode 100644 src/main/java/com/google/firebase/IncomingHttpResponse.java create mode 100644 src/main/java/com/google/firebase/OutgoingHttpRequest.java create mode 100644 src/main/java/com/google/firebase/auth/AuthErrorCode.java create mode 100644 src/main/java/com/google/firebase/auth/internal/AuthErrorHandler.java delete mode 100644 src/main/java/com/google/firebase/auth/internal/HttpErrorResponse.java create mode 100644 src/main/java/com/google/firebase/internal/AbstractHttpErrorHandler.java create mode 100644 src/main/java/com/google/firebase/internal/AbstractPlatformErrorHandler.java create mode 100644 src/main/java/com/google/firebase/internal/ErrorHandlingHttpClient.java delete mode 100644 src/main/java/com/google/firebase/internal/FirebaseAppStore.java create mode 100644 src/main/java/com/google/firebase/internal/HttpErrorHandler.java create mode 100644 src/main/java/com/google/firebase/internal/HttpRequestInfo.java create mode 100644 src/main/java/com/google/firebase/messaging/MessagingErrorCode.java create mode 100644 src/test/java/com/google/firebase/FirebaseExceptionTest.java create mode 100644 src/test/java/com/google/firebase/IncomingHttpResponseTest.java create mode 100644 src/test/java/com/google/firebase/OutgoingHttpRequestTest.java create mode 100644 src/test/java/com/google/firebase/internal/AbstractPlatformErrorHandlerTest.java create mode 100644 src/test/java/com/google/firebase/internal/ErrorHandlingHttpClientTest.java diff --git a/pom.xml b/pom.xml index c2dc9f336..a72301d5b 100644 --- a/pom.xml +++ b/pom.xml @@ -477,12 +477,6 @@ 0.6 test - - com.cedarsoftware - java-util - 1.26.0 - test - junit junit diff --git a/src/main/java/com/google/firebase/ErrorCode.java b/src/main/java/com/google/firebase/ErrorCode.java new file mode 100644 index 000000000..0eaa9cec2 --- /dev/null +++ b/src/main/java/com/google/firebase/ErrorCode.java @@ -0,0 +1,109 @@ +/* + * Copyright 2020 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; + +/** + * Platform-wide error codes that can be raised by Admin SDK APIs. + */ +public enum ErrorCode { + + /** + * Client specified an invalid argument. + */ + INVALID_ARGUMENT, + + /** + * Request cannot be executed in the current system state, such as deleting a non-empty + * directory. + */ + FAILED_PRECONDITION, + + /** + * Client specified an invalid range. + */ + OUT_OF_RANGE, + + /** + * Request not authenticated due to missing, invalid, or expired OAuth token. + */ + UNAUTHENTICATED, + + /** + * Client does not have sufficient permission. This can happen because the OAuth token does + * not have the right scopes, the client doesn't have permission, or the API has not been + * enabled for the client project. + */ + PERMISSION_DENIED, + + /** + * A specified resource is not found, or the request is rejected for unknown reasons, + * such as a blocked network address. + */ + NOT_FOUND, + + /** + * Concurrency conflict, such as read-modify-write conflict. + */ + CONFLICT, + + /** + * Concurrency conflict, such as read-modify-write conflict. + */ + ABORTED, + + /** + * The resource that a client tried to create already exists. + */ + ALREADY_EXISTS, + + /** + * Either out of resource quota or rate limited. + */ + RESOURCE_EXHAUSTED, + + /** + * Request cancelled by the client. + */ + CANCELLED, + + /** + * Unrecoverable data loss or data corruption. The client should report the error to the user. + */ + DATA_LOSS, + + /** + * Unknown server error. Typically a server bug. + */ + UNKNOWN, + + /** + * Internal server error. Typically a server bug. + */ + INTERNAL, + + /** + * Service unavailable. Typically the server is down. + */ + UNAVAILABLE, + + /** + * Request deadline exceeded. This happens only if the caller sets a deadline that is + * shorter than the method's default deadline (i.e. requested deadline is not enough for the + * server to process the request) and the request did not finish within the deadline. + */ + DEADLINE_EXCEEDED, +} diff --git a/src/main/java/com/google/firebase/FirebaseApp.java b/src/main/java/com/google/firebase/FirebaseApp.java index c60f0ad5e..38482541c 100644 --- a/src/main/java/com/google/firebase/FirebaseApp.java +++ b/src/main/java/com/google/firebase/FirebaseApp.java @@ -34,9 +34,7 @@ import com.google.common.base.Joiner; import com.google.common.base.MoreObjects; import com.google.common.base.Strings; -import com.google.common.base.Supplier; import com.google.common.collect.ImmutableList; -import com.google.firebase.internal.FirebaseAppStore; import com.google.firebase.internal.FirebaseScheduledExecutor; import com.google.firebase.internal.FirebaseService; import com.google.firebase.internal.ListenableFuture2ApiFuture; @@ -48,10 +46,8 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.Future; import java.util.concurrent.ScheduledExecutorService; @@ -121,7 +117,6 @@ private FirebaseApp(String name, FirebaseOptions options, TokenRefresher.Factory /** Returns a list of all FirebaseApps. */ public static List getApps() { - // TODO: reenable persistence. See b/28158809. synchronized (appsLock) { return ImmutableList.copyOf(instances.values()); } @@ -221,21 +216,16 @@ public static FirebaseApp initializeApp(FirebaseOptions options, String name) { static FirebaseApp initializeApp(FirebaseOptions options, String name, TokenRefresher.Factory tokenRefresherFactory) { - FirebaseAppStore appStore = FirebaseAppStore.initialize(); String normalizedName = normalize(name); - final FirebaseApp firebaseApp; synchronized (appsLock) { checkState( !instances.containsKey(normalizedName), "FirebaseApp name " + normalizedName + " already exists!"); - firebaseApp = new FirebaseApp(normalizedName, options, tokenRefresherFactory); + FirebaseApp firebaseApp = new FirebaseApp(normalizedName, options, tokenRefresherFactory); instances.put(normalizedName, firebaseApp); + return firebaseApp; } - - appStore.persistApp(firebaseApp); - - return firebaseApp; } @VisibleForTesting @@ -251,19 +241,13 @@ static void clearInstancesForTest() { } private static List getAllAppNames() { - Set allAppNames = new HashSet<>(); + List allAppNames; synchronized (appsLock) { - for (FirebaseApp app : instances.values()) { - allAppNames.add(app.getName()); - } - FirebaseAppStore appStore = FirebaseAppStore.getInstance(); - if (appStore != null) { - allAppNames.addAll(appStore.getAllPersistedAppNames()); - } + allAppNames = new ArrayList<>(instances.keySet()); } - List sortedNameList = new ArrayList<>(allAppNames); - Collections.sort(sortedNameList); - return sortedNameList; + + Collections.sort(allAppNames); + return ImmutableList.copyOf(allAppNames); } /** Normalizes the app name. */ @@ -359,11 +343,6 @@ public void delete() { synchronized (appsLock) { instances.remove(name); } - - FirebaseAppStore appStore = FirebaseAppStore.getInstance(); - if (appStore != null) { - appStore.removeApp(name); - } } private void checkNotDeleted() { @@ -582,18 +561,17 @@ enum State { private static FirebaseOptions getOptionsFromEnvironment() throws IOException { String defaultConfig = System.getenv(FIREBASE_CONFIG_ENV_VAR); if (Strings.isNullOrEmpty(defaultConfig)) { - return new FirebaseOptions.Builder() + return FirebaseOptions.builder() .setCredentials(APPLICATION_DEFAULT_CREDENTIALS) .build(); } JsonFactory jsonFactory = Utils.getDefaultJsonFactory(); - FirebaseOptions.Builder builder = new FirebaseOptions.Builder(); + FirebaseOptions.Builder builder = FirebaseOptions.builder(); JsonParser parser; if (defaultConfig.startsWith("{")) { parser = jsonFactory.createJsonParser(defaultConfig); } else { - FileReader reader; - reader = new FileReader(defaultConfig); + FileReader reader = new FileReader(defaultConfig); parser = jsonFactory.createJsonParser(reader); } parser.parseAndClose(builder); diff --git a/src/main/java/com/google/firebase/FirebaseAppLifecycleListener.java b/src/main/java/com/google/firebase/FirebaseAppLifecycleListener.java index 60118c249..6493edb60 100644 --- a/src/main/java/com/google/firebase/FirebaseAppLifecycleListener.java +++ b/src/main/java/com/google/firebase/FirebaseAppLifecycleListener.java @@ -19,7 +19,7 @@ /** * A listener which gets notified when {@link com.google.firebase.FirebaseApp} gets deleted. */ -// TODO: consider making it public in a future release. +@Deprecated interface FirebaseAppLifecycleListener { /** diff --git a/src/main/java/com/google/firebase/FirebaseException.java b/src/main/java/com/google/firebase/FirebaseException.java index f78b3fb98..a5bb80424 100644 --- a/src/main/java/com/google/firebase/FirebaseException.java +++ b/src/main/java/com/google/firebase/FirebaseException.java @@ -17,24 +17,55 @@ package com.google.firebase; import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; import com.google.common.base.Strings; import com.google.firebase.internal.NonNull; +import com.google.firebase.internal.Nullable; -/** Base class for all Firebase exceptions. */ +/** + * Base class for all Firebase exceptions. + */ public class FirebaseException extends Exception { - // TODO(b/27677218): Exceptions should have non-empty messages. - @Deprecated - protected FirebaseException() {} + private final ErrorCode errorCode; + private final IncomingHttpResponse httpResponse; + + public FirebaseException( + @NonNull ErrorCode errorCode, + @NonNull String message, + @Nullable Throwable cause, + @Nullable IncomingHttpResponse httpResponse) { + super(message, cause); + checkArgument(!Strings.isNullOrEmpty(message), "Message must not be null or empty"); + this.errorCode = checkNotNull(errorCode, "ErrorCode must not be null"); + this.httpResponse = httpResponse; + } + + public FirebaseException( + @NonNull ErrorCode errorCode, + @NonNull String message, + @Nullable Throwable cause) { + this(errorCode, message, cause, null); + } - public FirebaseException(@NonNull String detailMessage) { - super(detailMessage); - checkArgument(!Strings.isNullOrEmpty(detailMessage), "Detail message must not be empty"); + /** + * Returns the platform-wide error code associated with this exception. + * + * @return A Firebase error code. + */ + public final ErrorCode getErrorCode() { + return errorCode; } - public FirebaseException(@NonNull String detailMessage, Throwable cause) { - super(detailMessage, cause); - checkArgument(!Strings.isNullOrEmpty(detailMessage), "Detail message must not be empty"); + /** + * Returns the HTTP response that resulted in this exception. If the exception was not caused by + * an HTTP error response, returns null. + * + * @return An HTTP response or null. + */ + @Nullable + public final IncomingHttpResponse getHttpResponse() { + return httpResponse; } } diff --git a/src/main/java/com/google/firebase/FirebaseOptions.java b/src/main/java/com/google/firebase/FirebaseOptions.java index f0561d5e5..6ee074d6f 100644 --- a/src/main/java/com/google/firebase/FirebaseOptions.java +++ b/src/main/java/com/google/firebase/FirebaseOptions.java @@ -223,6 +223,16 @@ public static Builder builder() { return new Builder(); } + /** + * Creates a new {@code Builder} from the options object. + * + *

The new builder is not backed by this object's values; that is, changes made to the new + * builder don't change the values of the origin object. + */ + public Builder toBuilder() { + return new Builder(this); + } + /** * Builder for constructing {@link FirebaseOptions}. */ @@ -249,7 +259,12 @@ public static final class Builder { private int connectTimeout; private int readTimeout; - /** Constructs an empty builder. */ + /** + * Constructs an empty builder. + * + * @deprecated Use {@link FirebaseOptions#builder()} instead. + */ + @Deprecated public Builder() {} /** @@ -257,7 +272,10 @@ public Builder() {} * *

The new builder is not backed by this object's values, that is changes made to the new * builder don't change the values of the origin object. + * + * @deprecated Use {@link FirebaseOptions#toBuilder()} instead. */ + @Deprecated public Builder(FirebaseOptions options) { databaseUrl = options.databaseUrl; storageBucket = options.storageBucket; diff --git a/src/main/java/com/google/firebase/IncomingHttpResponse.java b/src/main/java/com/google/firebase/IncomingHttpResponse.java new file mode 100644 index 000000000..cfeac5e70 --- /dev/null +++ b/src/main/java/com/google/firebase/IncomingHttpResponse.java @@ -0,0 +1,114 @@ +/* + * Copyright 2020 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; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.HttpResponse; +import com.google.api.client.http.HttpResponseException; +import com.google.common.collect.ImmutableMap; +import com.google.firebase.database.annotations.Nullable; +import java.util.Map; + +/** + * Contains information that describes an HTTP response received by the SDK. + */ +public final class IncomingHttpResponse { + + private final int statusCode; + private final String content; + private final Map headers; + private final OutgoingHttpRequest request; + + /** + * Creates an {@code IncomingHttpResponse} from a successful response and the content read + * from it. The caller is expected to read the content from the response, and handle any errors + * that may occur while reading. + * + * @param response A successful response. + * @param content Content read from the response. + */ + public IncomingHttpResponse(HttpResponse response, @Nullable String content) { + checkNotNull(response, "response must not be null"); + this.statusCode = response.getStatusCode(); + this.content = content; + this.headers = ImmutableMap.copyOf(response.getHeaders()); + this.request = new OutgoingHttpRequest(response.getRequest()); + } + + /** + * Creates an {@code IncomingHttpResponse} from an HTTP error response. + * + * @param e The exception representing the HTTP error response. + * @param request The request that resulted in the error. + */ + public IncomingHttpResponse(HttpResponseException e, HttpRequest request) { + this(e, new OutgoingHttpRequest(request)); + } + + /** + * Creates an {@code IncomingHttpResponse} from an HTTP error response. + * + * @param e The exception representing the HTTP error response. + * @param request The request that resulted in the error. + */ + public IncomingHttpResponse(HttpResponseException e, OutgoingHttpRequest request) { + checkNotNull(e, "exception must not be null"); + this.statusCode = e.getStatusCode(); + this.content = e.getContent(); + this.headers = ImmutableMap.copyOf(e.getHeaders()); + this.request = checkNotNull(request, "request must not be null"); + } + + /** + * Returns the status code of the response. + * + * @return An HTTP status code (e.g. 500). + */ + public int getStatusCode() { + return this.statusCode; + } + + /** + * Returns the content of the response as a string. + * + * @return HTTP content or null. + */ + @Nullable + public String getContent() { + return this.content; + } + + /** + * Returns the headers set on the response. + * + * @return An immutable map of headers (possibly empty). + */ + public Map getHeaders() { + return this.headers; + } + + /** + * Returns the request that resulted in this response. + * + * @return An HTTP request. + */ + public OutgoingHttpRequest getRequest() { + return request; + } +} diff --git a/src/main/java/com/google/firebase/OutgoingHttpRequest.java b/src/main/java/com/google/firebase/OutgoingHttpRequest.java new file mode 100644 index 000000000..44af4bff0 --- /dev/null +++ b/src/main/java/com/google/firebase/OutgoingHttpRequest.java @@ -0,0 +1,98 @@ +/* + * Copyright 2020 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; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.api.client.http.HttpContent; +import com.google.api.client.http.HttpRequest; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableMap; +import com.google.firebase.internal.Nullable; +import java.util.Map; + +/** + * Contains the information that describe an HTTP request made by the SDK. + */ +public final class OutgoingHttpRequest { + + private final String method; + private final String url; + private final HttpContent content; + private final Map headers; + + /** + * Creates an {@code OutgoingHttpRequest} from the HTTP method and URL. + * + * @param method HTTP method name. + * @param url Target HTTP URL of the request. + */ + public OutgoingHttpRequest(String method, String url) { + checkArgument(!Strings.isNullOrEmpty(method), "method must not be null or empty"); + checkArgument(!Strings.isNullOrEmpty(url), "url must not be empty"); + this.method = method; + this.url = url; + this.content = null; + this.headers = ImmutableMap.of(); + } + + OutgoingHttpRequest(HttpRequest request) { + checkNotNull(request, "request must not be null"); + this.method = request.getRequestMethod(); + this.url = request.getUrl().toString(); + this.content = request.getContent(); + this.headers = ImmutableMap.copyOf(request.getHeaders()); + } + + /** + * Returns the HTTP method of the request. + * + * @return An HTTP method string (e.g. GET). + */ + public String getMethod() { + return method; + } + + /** + * Returns the URL of the request. + * + * @return An absolute HTTP URL. + */ + public String getUrl() { + return url; + } + + /** + * Returns any content that was sent with the request. + * + * @return HTTP content or null. + */ + @Nullable + public HttpContent getContent() { + return content; + } + + /** + * Returns the headers set on the request. + * + * @return An immutable map of headers (possibly empty). + */ + public Map getHeaders() { + return headers; + } +} diff --git a/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java b/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java index 7061d2b44..8004548a5 100644 --- a/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java +++ b/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java @@ -37,7 +37,6 @@ import com.google.firebase.internal.CallableOperation; import com.google.firebase.internal.NonNull; import com.google.firebase.internal.Nullable; -import java.io.IOException; import java.util.Collection; import java.util.HashSet; import java.util.List; @@ -50,8 +49,6 @@ */ public abstract class AbstractFirebaseAuth { - private static final String ERROR_CUSTOM_TOKEN = "ERROR_CUSTOM_TOKEN"; - private final Object lock = new Object(); private final AtomicBoolean destroyed = new AtomicBoolean(false); @@ -173,12 +170,7 @@ private CallableOperation createCustomTokenOp( return new CallableOperation() { @Override public String execute() throws FirebaseAuthException { - try { - return tokenFactory.createSignedCustomAuthTokenForUser(uid, developerClaims); - } catch (IOException e) { - throw new FirebaseAuthException( - ERROR_CUSTOM_TOKEN, "Failed to generate a custom token", e); - } + return tokenFactory.createSignedCustomAuthTokenForUser(uid, developerClaims); } }; } @@ -902,7 +894,7 @@ protected UserImportResult execute() throws FirebaseAuthException { * not guaranteed to correspond to the nth entry in the input parameters list. * *

A maximum of 100 identifiers may be specified. If more than 100 identifiers are - * supplied, this method throws an {@link IllegalArgumentException}. + * supplied, this method throws an {@code IllegalArgumentException}. * * @param identifiers The identifiers used to indicate which user records should be returned. Must * have 100 or fewer entries. @@ -924,7 +916,7 @@ public GetUsersResult getUsers(@NonNull Collection identifiers) * not guaranteed to correspond to the nth entry in the input parameters list. * *

A maximum of 100 identifiers may be specified. If more than 100 identifiers are - * supplied, this method throws an {@link IllegalArgumentException}. + * supplied, this method throws an {@code IllegalArgumentException}. * * @param identifiers The identifiers used to indicate which user records should be returned. * Must have 100 or fewer entries. @@ -978,7 +970,7 @@ private boolean isUserFound(UserIdentifier id, Collection userRecord * DeleteUsersResult.getSuccessCount() value. * *

A maximum of 1000 identifiers may be supplied. If more than 1000 identifiers are - * supplied, this method throws an {@link IllegalArgumentException}. + * supplied, this method throws an {@code IllegalArgumentException}. * *

This API has a rate limit of 1 QPS. Exceeding the limit may result in a quota exceeded * error. If you want to delete more than 1000 users, we suggest adding a delay to ensure you @@ -987,7 +979,7 @@ private boolean isUserFound(UserIdentifier id, Collection userRecord * @param uids The uids of the users to be deleted. Must have <= 1000 entries. * @return The total number of successful/failed deletions, as well as the array of errors that * correspond to the failed deletions. - * @throw IllegalArgumentException If any of the identifiers are invalid or if more than 1000 + * @throws IllegalArgumentException If any of the identifiers are invalid or if more than 1000 * identifiers are specified. * @throws FirebaseAuthException If an error occurs while deleting users. */ @@ -1003,7 +995,7 @@ public DeleteUsersResult deleteUsers(List uids) throws FirebaseAuthExcep * deletions, as well as the array of errors that correspond to the failed deletions. If an * error occurs while deleting the user account, the future throws a * {@link FirebaseAuthException}. - * @throw IllegalArgumentException If any of the identifiers are invalid or if more than 1000 + * @throws IllegalArgumentException If any of the identifiers are invalid or if more than 1000 * identifiers are specified. */ public ApiFuture deleteUsersAsync(List uids) { @@ -1831,11 +1823,7 @@ public FirebaseTokenVerifier get() { new Supplier() { @Override public FirebaseUserManager get() { - return FirebaseUserManager - .builder() - .setFirebaseApp(app) - .setTenantId(tenantId) - .build(); + return FirebaseUserManager.createUserManager(app, tenantId); } }); } diff --git a/src/main/java/com/google/firebase/auth/AuthErrorCode.java b/src/main/java/com/google/firebase/auth/AuthErrorCode.java new file mode 100644 index 000000000..bea067eee --- /dev/null +++ b/src/main/java/com/google/firebase/auth/AuthErrorCode.java @@ -0,0 +1,104 @@ +/* + * Copyright 2020 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; + +/** + * Error codes that can be raised by the Firebase Auth APIs. + */ +public enum AuthErrorCode { + + /** + * Failed to retrieve public key certificates required to verify JWTs. + */ + CERTIFICATE_FETCH_FAILED, + + /** + * No IdP configuration found for the given identifier. + */ + CONFIGURATION_NOT_FOUND, + + /** + * A user already exists with the provided email. + */ + EMAIL_ALREADY_EXISTS, + + /** + * The specified ID token is expired. + */ + EXPIRED_ID_TOKEN, + + /** + * The specified session cookie is expired. + */ + EXPIRED_SESSION_COOKIE, + + /** + * The provided dynamic link domain is not configured or authorized for the current project. + */ + INVALID_DYNAMIC_LINK_DOMAIN, + + /** + * The specified ID token is invalid. + */ + INVALID_ID_TOKEN, + + /** + * The specified session cookie is invalid. + */ + INVALID_SESSION_COOKIE, + + /** + * A user already exists with the provided phone number. + */ + PHONE_NUMBER_ALREADY_EXISTS, + + /** + * The specified ID token has been revoked. + */ + REVOKED_ID_TOKEN, + + /** + * The specified session cookie has been revoked. + */ + REVOKED_SESSION_COOKIE, + + /** + * Tenant ID in the JWT does not match. + */ + TENANT_ID_MISMATCH, + + /** + * No tenant found for the given identifier. + */ + TENANT_NOT_FOUND, + + /** + * A user already exists with the provided UID. + */ + UID_ALREADY_EXISTS, + + /** + * The domain of the continue URL is not whitelisted. Whitelist the domain in the Firebase + * console. + */ + UNAUTHORIZED_CONTINUE_URL, + + /** + * No user record found for the given identifier. + */ + USER_NOT_FOUND, +} diff --git a/src/main/java/com/google/firebase/auth/FirebaseAuthException.java b/src/main/java/com/google/firebase/auth/FirebaseAuthException.java index 2314a69d2..53c980668 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseAuthException.java +++ b/src/main/java/com/google/firebase/auth/FirebaseAuthException.java @@ -16,17 +16,11 @@ package com.google.firebase.auth; -// TODO: Move it out from firebase-common. Temporary host it their for -// database's integration.http://b/27624510. - -// TODO: Decide if changing this not enforcing an error code. Need to align -// with the decision in http://b/27677218. Also, need to turn this into abstract later. - -import static com.google.common.base.Preconditions.checkArgument; - -import com.google.common.base.Strings; +import com.google.firebase.ErrorCode; import com.google.firebase.FirebaseException; +import com.google.firebase.IncomingHttpResponse; import com.google.firebase.internal.NonNull; +import com.google.firebase.internal.Nullable; /** * Generic exception related to Firebase Authentication. Check the error code and message for more @@ -34,22 +28,24 @@ */ public class FirebaseAuthException extends FirebaseException { - private final String errorCode; + private final AuthErrorCode errorCode; - public FirebaseAuthException(@NonNull String errorCode, @NonNull String detailMessage) { - this(errorCode, detailMessage, null); + public FirebaseAuthException( + @NonNull ErrorCode errorCode, + @NonNull String message, + Throwable cause, + IncomingHttpResponse response, + AuthErrorCode authErrorCode) { + super(errorCode, message, cause, response); + this.errorCode = authErrorCode; } - public FirebaseAuthException(@NonNull String errorCode, @NonNull String detailMessage, - Throwable throwable) { - super(detailMessage, throwable); - checkArgument(!Strings.isNullOrEmpty(errorCode)); - this.errorCode = errorCode; + public FirebaseAuthException(FirebaseException base) { + this(base.getErrorCode(), base.getMessage(), base.getCause(), base.getHttpResponse(), null); } - /** Returns an error code that may provide more information about the error. */ - @NonNull - public String getErrorCode() { + @Nullable + public AuthErrorCode getAuthErrorCode() { return errorCode; } } diff --git a/src/main/java/com/google/firebase/auth/FirebaseTokenUtils.java b/src/main/java/com/google/firebase/auth/FirebaseTokenUtils.java index 7c8f9f0a5..873dbe7ac 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseTokenUtils.java +++ b/src/main/java/com/google/firebase/auth/FirebaseTokenUtils.java @@ -94,6 +94,8 @@ static FirebaseTokenVerifierImpl createIdTokenVerifier( .setJsonFactory(app.getOptions().getJsonFactory()) .setPublicKeysManager(publicKeysManager) .setIdTokenVerifier(idTokenVerifier) + .setInvalidTokenErrorCode(AuthErrorCode.INVALID_ID_TOKEN) + .setExpiredTokenErrorCode(AuthErrorCode.EXPIRED_ID_TOKEN) .setTenantId(tenantId) .build(); } @@ -115,6 +117,8 @@ static FirebaseTokenVerifierImpl createSessionCookieVerifier( .setShortName("session cookie") .setMethod("verifySessionCookie()") .setDocUrl("https://firebase.google.com/docs/auth/admin/manage-cookies") + .setInvalidTokenErrorCode(AuthErrorCode.INVALID_SESSION_COOKIE) + .setExpiredTokenErrorCode(AuthErrorCode.EXPIRED_SESSION_COOKIE) .setJsonFactory(app.getOptions().getJsonFactory()) .setPublicKeysManager(publicKeysManager) .setIdTokenVerifier(idTokenVerifier) diff --git a/src/main/java/com/google/firebase/auth/FirebaseTokenVerifierImpl.java b/src/main/java/com/google/firebase/auth/FirebaseTokenVerifierImpl.java index e1a5a9a19..273ec1532 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseTokenVerifierImpl.java +++ b/src/main/java/com/google/firebase/auth/FirebaseTokenVerifierImpl.java @@ -28,11 +28,13 @@ import com.google.api.client.util.ArrayMap; import com.google.common.base.Joiner; import com.google.common.base.Strings; +import com.google.firebase.ErrorCode; import com.google.firebase.internal.Nullable; import java.io.IOException; import java.math.BigDecimal; import java.security.GeneralSecurityException; import java.security.PublicKey; +import java.util.List; /** * The default implementation of the {@link FirebaseTokenVerifier} interface. Uses the Google API @@ -44,9 +46,6 @@ final class FirebaseTokenVerifierImpl implements FirebaseTokenVerifier { private static final String RS256 = "RS256"; private static final String FIREBASE_AUDIENCE = "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit"; - private static final String ERROR_INVALID_CREDENTIAL = "ERROR_INVALID_CREDENTIAL"; - private static final String ERROR_RUNTIME_EXCEPTION = "ERROR_RUNTIME_EXCEPTION"; - static final String TENANT_ID_MISMATCH_ERROR = "tenant-id-mismatch"; private final JsonFactory jsonFactory; private final GooglePublicKeysManager publicKeysManager; @@ -55,6 +54,8 @@ final class FirebaseTokenVerifierImpl implements FirebaseTokenVerifier { private final String shortName; private final String articledShortName; private final String docUrl; + private final AuthErrorCode invalidTokenErrorCode; + private final AuthErrorCode expiredTokenErrorCode; private final String tenantId; private FirebaseTokenVerifierImpl(Builder builder) { @@ -68,6 +69,8 @@ private FirebaseTokenVerifierImpl(Builder builder) { this.shortName = builder.shortName; this.articledShortName = prefixWithIndefiniteArticle(this.shortName); this.docUrl = builder.docUrl; + this.invalidTokenErrorCode = checkNotNull(builder.invalidTokenErrorCode); + this.expiredTokenErrorCode = checkNotNull(builder.expiredTokenErrorCode); this.tenantId = Strings.nullToEmpty(builder.tenantId); } @@ -143,38 +146,28 @@ private IdToken parse(String token) throws FirebaseAuthException { shortName, docUrl, articledShortName); - throw new FirebaseAuthException(ERROR_INVALID_CREDENTIAL, detailedError, e); - } - } - - private void checkContents(final IdToken token) throws FirebaseAuthException { - String errorMessage = getErrorIfContentInvalid(token); - if (errorMessage != null) { - String detailedError = String.format("%s %s", errorMessage, getVerifyTokenMessage()); - throw new FirebaseAuthException(ERROR_INVALID_CREDENTIAL, detailedError); + throw newException(detailedError, invalidTokenErrorCode, e); } } private void checkSignature(IdToken token) throws FirebaseAuthException { - try { - if (!isSignatureValid(token)) { - throw new FirebaseAuthException(ERROR_INVALID_CREDENTIAL, - String.format( - "Failed to verify the signature of Firebase %s. %s", - shortName, - getVerifyTokenMessage())); - } - } catch (GeneralSecurityException | IOException e) { - throw new FirebaseAuthException( - ERROR_RUNTIME_EXCEPTION, "Error while verifying signature.", e); + if (!isSignatureValid(token)) { + String message = String.format( + "Failed to verify the signature of Firebase %s. %s", + shortName, + getVerifyTokenMessage()); + throw newException(message, invalidTokenErrorCode); } } - private String getErrorIfContentInvalid(final IdToken idToken) { + private void checkContents(final IdToken idToken) throws FirebaseAuthException { final Header header = idToken.getHeader(); final Payload payload = idToken.getPayload(); + final long currentTimeMillis = idTokenVerifier.getClock().currentTimeMillis(); String errorMessage = null; + AuthErrorCode errorCode = invalidTokenErrorCode; + if (header.getKeyId() == null) { errorMessage = getErrorForTokenWithoutKid(header, payload); } else if (!RS256.equals(header.getAlgorithm())) { @@ -209,14 +202,35 @@ private String getErrorIfContentInvalid(final IdToken idToken) { errorMessage = String.format( "Firebase %s has \"sub\" (subject) claim longer than 128 characters.", shortName); - } else if (!verifyTimestamps(idToken)) { + } else if (!idToken.verifyExpirationTime( + currentTimeMillis, idTokenVerifier.getAcceptableTimeSkewSeconds())) { errorMessage = String.format( - "Firebase %s has expired or is not yet valid. Get a fresh %s and try again.", + "Firebase %s has expired. Get a fresh %s and try again.", shortName, shortName); + // Also set the expired error code. + errorCode = expiredTokenErrorCode; + } else if (!idToken.verifyIssuedAtTime( + currentTimeMillis, idTokenVerifier.getAcceptableTimeSkewSeconds())) { + errorMessage = String.format( + "Firebase %s is not yet valid.", + shortName); + } + + if (errorMessage != null) { + String detailedError = String.format("%s %s", errorMessage, getVerifyTokenMessage()); + throw newException(detailedError, errorCode); } + } - return errorMessage; + private FirebaseAuthException newException(String message, AuthErrorCode errorCode) { + return newException(message, errorCode, null); + } + + private FirebaseAuthException newException( + String message, AuthErrorCode errorCode, Throwable cause) { + return new FirebaseAuthException( + ErrorCode.INVALID_ARGUMENT, message, cause, null, errorCode); } private String getVerifyTokenMessage() { @@ -230,15 +244,44 @@ private String getVerifyTokenMessage() { * Verifies the cryptographic signature on the FirebaseToken. Can block on a web request to fetch * the keys if they have expired. */ - private boolean isSignatureValid(IdToken token) throws GeneralSecurityException, IOException { - for (PublicKey key : publicKeysManager.getPublicKeys()) { - if (token.verifySignature(key)) { + private boolean isSignatureValid(IdToken token) throws FirebaseAuthException { + for (PublicKey key : fetchPublicKeys()) { + if (isSignatureValid(token, key)) { return true; } } + return false; } + private boolean isSignatureValid(IdToken token, PublicKey key) throws FirebaseAuthException { + try { + return token.verifySignature(key); + } catch (GeneralSecurityException e) { + // This doesn't happen under usual circumstances. Seems to only happen if the crypto + // setup of the runtime is incorrect in some way. + throw new FirebaseAuthException( + ErrorCode.UNKNOWN, + String.format("Unexpected error while verifying %s: %s", shortName, e.getMessage()), + e, + null, + invalidTokenErrorCode); + } + } + + private List fetchPublicKeys() throws FirebaseAuthException { + try { + return publicKeysManager.getPublicKeys(); + } catch (GeneralSecurityException | IOException e) { + throw new FirebaseAuthException( + ErrorCode.UNKNOWN, + "Error while fetching public key certificates: " + e.getMessage(), + e, + null, + AuthErrorCode.CERTIFICATE_FETCH_FAILED); + } + } + private String getErrorForTokenWithoutKid(IdToken.Header header, IdToken.Payload payload) { if (isCustomToken(payload)) { return String.format("%s expects %s, but was given a custom token.", @@ -261,11 +304,6 @@ private String getProjectIdMatchMessage() { shortName); } - private boolean verifyTimestamps(IdToken token) { - long currentTimeMillis = idTokenVerifier.getClock().currentTimeMillis(); - return token.verifyTime(currentTimeMillis, idTokenVerifier.getAcceptableTimeSkewSeconds()); - } - private boolean isCustomToken(IdToken.Payload payload) { return FIREBASE_AUDIENCE.equals(payload.getAudience()); } @@ -287,12 +325,11 @@ private boolean containsLegacyUidField(IdToken.Payload payload) { private void checkTenantId(final FirebaseToken firebaseToken) throws FirebaseAuthException { String tokenTenantId = Strings.nullToEmpty(firebaseToken.getTenantId()); if (!this.tenantId.equals(tokenTenantId)) { - throw new FirebaseAuthException( - TENANT_ID_MISMATCH_ERROR, - String.format( - "The tenant ID ('%s') of the token did not match the expected value ('%s')", - tokenTenantId, - tenantId)); + String message = String.format( + "The tenant ID ('%s') of the token did not match the expected value ('%s')", + tokenTenantId, + tenantId); + throw newException(message, AuthErrorCode.TENANT_ID_MISMATCH); } } @@ -308,6 +345,8 @@ static final class Builder { private String shortName; private IdTokenVerifier idTokenVerifier; private String docUrl; + private AuthErrorCode invalidTokenErrorCode; + private AuthErrorCode expiredTokenErrorCode; private String tenantId; private Builder() { } @@ -342,6 +381,16 @@ Builder setDocUrl(String docUrl) { return this; } + Builder setInvalidTokenErrorCode(AuthErrorCode invalidTokenErrorCode) { + this.invalidTokenErrorCode = invalidTokenErrorCode; + return this; + } + + Builder setExpiredTokenErrorCode(AuthErrorCode expiredTokenErrorCode) { + this.expiredTokenErrorCode = expiredTokenErrorCode; + return this; + } + Builder setTenantId(@Nullable String tenantId) { this.tenantId = tenantId; return this; diff --git a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java index b73882277..554d0179a 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java +++ b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java @@ -19,7 +19,6 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; -import com.google.api.client.http.GenericUrl; import com.google.api.client.http.HttpRequestFactory; import com.google.api.client.http.HttpResponseInterceptor; import com.google.api.client.json.GenericJson; @@ -30,8 +29,10 @@ import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.firebase.ErrorCode; import com.google.firebase.FirebaseApp; import com.google.firebase.ImplFirebaseTrampolines; +import com.google.firebase.IncomingHttpResponse; import com.google.firebase.auth.internal.AuthHttpClient; import com.google.firebase.auth.internal.BatchDeleteResponse; import com.google.firebase.auth.internal.DownloadAccountResponse; @@ -41,9 +42,9 @@ import com.google.firebase.auth.internal.ListSamlProviderConfigsResponse; import com.google.firebase.auth.internal.UploadAccountResponse; import com.google.firebase.internal.ApiClientUtils; +import com.google.firebase.internal.HttpRequestInfo; import com.google.firebase.internal.NonNull; import com.google.firebase.internal.Nullable; - import java.util.Collection; import java.util.HashSet; import java.util.List; @@ -57,7 +58,7 @@ * @see * Google Identity Toolkit */ -class FirebaseUserManager { +final class FirebaseUserManager { static final int MAX_LIST_PROVIDER_CONFIGS_RESULTS = 100; static final int MAX_GET_ACCOUNTS_BATCH_SIZE = 100; @@ -78,12 +79,12 @@ class FirebaseUserManager { private final AuthHttpClient httpClient; private FirebaseUserManager(Builder builder) { - FirebaseApp app = checkNotNull(builder.app, "FirebaseApp must not be null"); - String projectId = ImplFirebaseTrampolines.getProjectId(app); + String projectId = builder.projectId; checkArgument(!Strings.isNullOrEmpty(projectId), "Project ID is required to access the auth service. Use a service account credential or " + "set the project ID explicitly via FirebaseOptions. Alternatively you can also " + "set the project ID via the GOOGLE_CLOUD_PROJECT environment variable."); + this.jsonFactory = checkNotNull(builder.jsonFactory, "JsonFactory must not be null"); final String idToolkitUrlV1 = String.format(ID_TOOLKIT_URL, "v1", projectId); final String idToolkitUrlV2 = String.format(ID_TOOLKIT_URL, "v2", projectId); final String tenantId = builder.tenantId; @@ -96,10 +97,7 @@ private FirebaseUserManager(Builder builder) { this.idpConfigMgtBaseUrl = idToolkitUrlV2 + "/tenants/" + tenantId; } - this.jsonFactory = app.getOptions().getJsonFactory(); - HttpRequestFactory requestFactory = builder.requestFactory == null - ? ApiClientUtils.newAuthorizedRequestFactory(app) : builder.requestFactory; - this.httpClient = new AuthHttpClient(jsonFactory, requestFactory); + this.httpClient = new AuthHttpClient(jsonFactory, builder.requestFactory); } @VisibleForTesting @@ -110,40 +108,19 @@ void setInterceptor(HttpResponseInterceptor interceptor) { UserRecord getUserById(String uid) throws FirebaseAuthException { final Map payload = ImmutableMap.of( "localId", ImmutableList.of(uid)); - GetAccountInfoResponse response = post( - "/accounts:lookup", payload, GetAccountInfoResponse.class); - if (response == null || response.getUsers() == null || response.getUsers().isEmpty()) { - throw new FirebaseAuthException( - AuthHttpClient.USER_NOT_FOUND_ERROR, - "No user record found for the provided user ID: " + uid); - } - return new UserRecord(response.getUsers().get(0), jsonFactory); + return lookupUserAccount(payload, "user ID: " + uid); } UserRecord getUserByEmail(String email) throws FirebaseAuthException { final Map payload = ImmutableMap.of( "email", ImmutableList.of(email)); - GetAccountInfoResponse response = post( - "/accounts:lookup", payload, GetAccountInfoResponse.class); - if (response == null || response.getUsers() == null || response.getUsers().isEmpty()) { - throw new FirebaseAuthException( - AuthHttpClient.USER_NOT_FOUND_ERROR, - "No user record found for the provided email: " + email); - } - return new UserRecord(response.getUsers().get(0), jsonFactory); + return lookupUserAccount(payload, "email: " + email); } UserRecord getUserByPhoneNumber(String phoneNumber) throws FirebaseAuthException { final Map payload = ImmutableMap.of( "phoneNumber", ImmutableList.of(phoneNumber)); - GetAccountInfoResponse response = post( - "/accounts:lookup", payload, GetAccountInfoResponse.class); - if (response == null || response.getUsers() == null || response.getUsers().isEmpty()) { - throw new FirebaseAuthException( - AuthHttpClient.USER_NOT_FOUND_ERROR, - "No user record found for the provided phone number: " + phoneNumber); - } - return new UserRecord(response.getUsers().get(0), jsonFactory); + return lookupUserAccount(payload, "phone number: " + phoneNumber); } Set getAccountInfo(@NonNull Collection identifiers) @@ -159,12 +136,6 @@ Set getAccountInfo(@NonNull Collection identifiers) GetAccountInfoResponse response = post( "/accounts:lookup", payload, GetAccountInfoResponse.class); - - if (response == null) { - throw new FirebaseAuthException( - AuthHttpClient.INTERNAL_ERROR, "Failed to parse server response"); - } - Set results = new HashSet<>(); if (response.getUsers() != null) { for (GetAccountInfoResponse.User user : response.getUsers()) { @@ -175,51 +146,26 @@ Set getAccountInfo(@NonNull Collection identifiers) } String createUser(UserRecord.CreateRequest request) throws FirebaseAuthException { - GenericJson response = post( - "/accounts", request.getProperties(), GenericJson.class); - if (response != null) { - String uid = (String) response.get("localId"); - if (!Strings.isNullOrEmpty(uid)) { - return uid; - } - } - throw new FirebaseAuthException(AuthHttpClient.INTERNAL_ERROR, "Failed to create new user"); + GenericJson response = post("/accounts", request.getProperties(), GenericJson.class); + return (String) response.get("localId"); } void updateUser(UserRecord.UpdateRequest request, JsonFactory jsonFactory) throws FirebaseAuthException { - GenericJson response = post( - "/accounts:update", request.getProperties(jsonFactory), GenericJson.class); - if (response == null || !request.getUid().equals(response.get("localId"))) { - throw new FirebaseAuthException( - AuthHttpClient.INTERNAL_ERROR, "Failed to update user: " + request.getUid()); - } + post("/accounts:update", request.getProperties(jsonFactory), GenericJson.class); } void deleteUser(String uid) throws FirebaseAuthException { final Map payload = ImmutableMap.of("localId", uid); - GenericJson response = post( - "/accounts:delete", payload, GenericJson.class); - if (response == null || !response.containsKey("kind")) { - throw new FirebaseAuthException( - AuthHttpClient.INTERNAL_ERROR, "Failed to delete user: " + uid); - } + post("/accounts:delete", payload, GenericJson.class); } - /** - * @pre uids != null - * @pre uids.size() <= MAX_DELETE_ACCOUNTS_BATCH_SIZE - */ DeleteUsersResult deleteUsers(@NonNull List uids) throws FirebaseAuthException { final Map payload = ImmutableMap.of( "localIds", uids, "force", true); BatchDeleteResponse response = post( "/accounts:batchDelete", payload, BatchDeleteResponse.class); - if (response == null) { - throw new FirebaseAuthException(AuthHttpClient.INTERNAL_ERROR, "Failed to delete users"); - } - return new DeleteUsersResult(uids.size(), response); } @@ -231,23 +177,16 @@ DownloadAccountResponse listUsers(int maxResults, String pageToken) throws Fireb builder.put("nextPageToken", pageToken); } - GenericUrl url = new GenericUrl(userMgtBaseUrl + "/accounts:batchGet"); - url.putAll(builder.build()); - DownloadAccountResponse response = httpClient.sendRequest( - "GET", url, null, DownloadAccountResponse.class); - if (response == null) { - throw new FirebaseAuthException(AuthHttpClient.INTERNAL_ERROR, "Failed to retrieve users."); - } - return response; + String url = userMgtBaseUrl + "/accounts:batchGet"; + HttpRequestInfo requestInfo = HttpRequestInfo.buildGetRequest(url) + .addAllParameters(builder.build()); + return httpClient.sendRequest(requestInfo, DownloadAccountResponse.class); } UserImportResult importUsers(UserImportRequest request) throws FirebaseAuthException { checkNotNull(request); UploadAccountResponse response = post( "/accounts:batchCreate", request, UploadAccountResponse.class); - if (response == null) { - throw new FirebaseAuthException(AuthHttpClient.INTERNAL_ERROR, "Failed to import users."); - } return new UserImportResult(request.getUsersCount(), response); } @@ -256,14 +195,7 @@ String createSessionCookie(String idToken, final Map payload = ImmutableMap.of( "idToken", idToken, "validDuration", options.getExpiresInSeconds()); GenericJson response = post(":createSessionCookie", payload, GenericJson.class); - if (response != null) { - String cookie = (String) response.get("sessionCookie"); - if (!Strings.isNullOrEmpty(cookie)) { - return cookie; - } - } - throw new FirebaseAuthException( - AuthHttpClient.INTERNAL_ERROR, "Failed to create session cookie"); + return (String) response.get("sessionCookie"); } String getEmailActionLink(EmailLinkType type, String email, @@ -275,57 +207,70 @@ String getEmailActionLink(EmailLinkType type, String email, if (settings != null) { payload.putAll(settings.getProperties()); } + GenericJson response = post("/accounts:sendOobCode", payload.build(), GenericJson.class); - if (response != null) { - String link = (String) response.get("oobLink"); - if (!Strings.isNullOrEmpty(link)) { - return link; - } + return (String) response.get("oobLink"); + } + + private UserRecord lookupUserAccount( + Map payload, String identifier) throws FirebaseAuthException { + HttpRequestInfo requestInfo = HttpRequestInfo.buildJsonPostRequest( + userMgtBaseUrl + "/accounts:lookup", payload); + IncomingHttpResponse response = httpClient.sendRequest(requestInfo); + GetAccountInfoResponse parsed = httpClient.parse(response, GetAccountInfoResponse.class); + if (parsed.getUsers() == null || parsed.getUsers().isEmpty()) { + throw new FirebaseAuthException(ErrorCode.NOT_FOUND, + "No user record found for the provided " + identifier, + null, + response, + AuthErrorCode.USER_NOT_FOUND); } - throw new FirebaseAuthException( - AuthHttpClient.INTERNAL_ERROR, "Failed to create email action link"); + + return new UserRecord(parsed.getUsers().get(0), jsonFactory); } OidcProviderConfig createOidcProviderConfig( OidcProviderConfig.CreateRequest request) throws FirebaseAuthException { - GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + "/oauthIdpConfigs"); - url.set("oauthIdpConfigId", request.getProviderId()); - return httpClient.sendRequest("POST", url, request.getProperties(), OidcProviderConfig.class); + String url = idpConfigMgtBaseUrl + "/oauthIdpConfigs"; + HttpRequestInfo requestInfo = HttpRequestInfo.buildJsonPostRequest(url, request.getProperties()) + .addParameter("oauthIdpConfigId", request.getProviderId()); + return httpClient.sendRequest(requestInfo, OidcProviderConfig.class); } SamlProviderConfig createSamlProviderConfig( SamlProviderConfig.CreateRequest request) throws FirebaseAuthException { - GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + "/inboundSamlConfigs"); - url.set("inboundSamlConfigId", request.getProviderId()); - return httpClient.sendRequest("POST", url, request.getProperties(), SamlProviderConfig.class); + String url = idpConfigMgtBaseUrl + "/inboundSamlConfigs"; + HttpRequestInfo requestInfo = HttpRequestInfo.buildJsonPostRequest(url, request.getProperties()) + .addParameter("inboundSamlConfigId", request.getProviderId()); + return httpClient.sendRequest(requestInfo, SamlProviderConfig.class); } OidcProviderConfig updateOidcProviderConfig(OidcProviderConfig.UpdateRequest request) throws FirebaseAuthException { Map properties = request.getProperties(); - GenericUrl url = - new GenericUrl(idpConfigMgtBaseUrl + getOidcUrlSuffix(request.getProviderId())); - url.put("updateMask", Joiner.on(",").join(AuthHttpClient.generateMask(properties))); - return httpClient.sendRequest("PATCH", url, properties, OidcProviderConfig.class); + String url = idpConfigMgtBaseUrl + getOidcUrlSuffix(request.getProviderId()); + HttpRequestInfo requestInfo = HttpRequestInfo.buildJsonPatchRequest(url, properties) + .addParameter("updateMask", Joiner.on(",").join(AuthHttpClient.generateMask(properties))); + return httpClient.sendRequest(requestInfo, OidcProviderConfig.class); } SamlProviderConfig updateSamlProviderConfig(SamlProviderConfig.UpdateRequest request) throws FirebaseAuthException { Map properties = request.getProperties(); - GenericUrl url = - new GenericUrl(idpConfigMgtBaseUrl + getSamlUrlSuffix(request.getProviderId())); - url.put("updateMask", Joiner.on(",").join(AuthHttpClient.generateMask(properties))); - return httpClient.sendRequest("PATCH", url, properties, SamlProviderConfig.class); + String url = idpConfigMgtBaseUrl + getSamlUrlSuffix(request.getProviderId()); + HttpRequestInfo requestInfo = HttpRequestInfo.buildJsonPatchRequest(url, properties) + .addParameter("updateMask", Joiner.on(",").join(AuthHttpClient.generateMask(properties))); + return httpClient.sendRequest(requestInfo, SamlProviderConfig.class); } OidcProviderConfig getOidcProviderConfig(String providerId) throws FirebaseAuthException { - GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + getOidcUrlSuffix(providerId)); - return httpClient.sendRequest("GET", url, null, OidcProviderConfig.class); + String url = idpConfigMgtBaseUrl + getOidcUrlSuffix(providerId); + return httpClient.sendRequest(HttpRequestInfo.buildGetRequest(url), OidcProviderConfig.class); } SamlProviderConfig getSamlProviderConfig(String providerId) throws FirebaseAuthException { - GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + getSamlUrlSuffix(providerId)); - return httpClient.sendRequest("GET", url, null, SamlProviderConfig.class); + String url = idpConfigMgtBaseUrl + getSamlUrlSuffix(providerId); + return httpClient.sendRequest(HttpRequestInfo.buildGetRequest(url), SamlProviderConfig.class); } ListOidcProviderConfigsResponse listOidcProviderConfigs(int maxResults, String pageToken) @@ -338,15 +283,10 @@ ListOidcProviderConfigsResponse listOidcProviderConfigs(int maxResults, String p builder.put("nextPageToken", pageToken); } - GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + "/oauthIdpConfigs"); - url.putAll(builder.build()); - ListOidcProviderConfigsResponse response = - httpClient.sendRequest("GET", url, null, ListOidcProviderConfigsResponse.class); - if (response == null) { - throw new FirebaseAuthException( - AuthHttpClient.INTERNAL_ERROR, "Failed to retrieve provider configs."); - } - return response; + String url = idpConfigMgtBaseUrl + "/oauthIdpConfigs"; + HttpRequestInfo requestInfo = HttpRequestInfo.buildGetRequest(url) + .addAllParameters(builder.build()); + return httpClient.sendRequest(requestInfo, ListOidcProviderConfigsResponse.class); } ListSamlProviderConfigsResponse listSamlProviderConfigs(int maxResults, String pageToken) @@ -359,25 +299,20 @@ ListSamlProviderConfigsResponse listSamlProviderConfigs(int maxResults, String p builder.put("nextPageToken", pageToken); } - GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + "/inboundSamlConfigs"); - url.putAll(builder.build()); - ListSamlProviderConfigsResponse response = - httpClient.sendRequest("GET", url, null, ListSamlProviderConfigsResponse.class); - if (response == null) { - throw new FirebaseAuthException( - AuthHttpClient.INTERNAL_ERROR, "Failed to retrieve provider configs."); - } - return response; + String url = idpConfigMgtBaseUrl + "/inboundSamlConfigs"; + HttpRequestInfo requestInfo = HttpRequestInfo.buildGetRequest(url) + .addAllParameters(builder.build()); + return httpClient.sendRequest(requestInfo, ListSamlProviderConfigsResponse.class); } void deleteOidcProviderConfig(String providerId) throws FirebaseAuthException { - GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + getOidcUrlSuffix(providerId)); - httpClient.sendRequest("DELETE", url, null, GenericJson.class); + String url = idpConfigMgtBaseUrl + getOidcUrlSuffix(providerId); + httpClient.sendRequest(HttpRequestInfo.buildDeleteRequest(url)); } void deleteSamlProviderConfig(String providerId) throws FirebaseAuthException { - GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + getSamlUrlSuffix(providerId)); - httpClient.sendRequest("DELETE", url, null, GenericJson.class); + String url = idpConfigMgtBaseUrl + getSamlUrlSuffix(providerId); + httpClient.sendRequest(HttpRequestInfo.buildDeleteRequest(url)); } private static String getOidcUrlSuffix(String providerId) { @@ -393,8 +328,8 @@ private static String getSamlUrlSuffix(String providerId) { private T post(String path, Object content, Class clazz) throws FirebaseAuthException { checkArgument(!Strings.isNullOrEmpty(path), "path must not be null or empty"); checkNotNull(content, "content must not be null for POST requests"); - GenericUrl url = new GenericUrl(userMgtBaseUrl + path); - return httpClient.sendRequest("POST", url, content, clazz); + String url = userMgtBaseUrl + path; + return httpClient.sendRequest(HttpRequestInfo.buildJsonPostRequest(url, content), clazz); } static class UserImportRequest extends GenericJson { @@ -437,18 +372,30 @@ enum EmailLinkType { PASSWORD_RESET, } + static FirebaseUserManager createUserManager(FirebaseApp app, String tenantId) { + return FirebaseUserManager.builder() + .setProjectId(ImplFirebaseTrampolines.getProjectId(app)) + .setTenantId(tenantId) + .setHttpRequestFactory(ApiClientUtils.newAuthorizedRequestFactory(app)) + .setJsonFactory(app.getOptions().getJsonFactory()) + .build(); + } + static Builder builder() { return new Builder(); } static class Builder { - private FirebaseApp app; + private String projectId; private String tenantId; private HttpRequestFactory requestFactory; + private JsonFactory jsonFactory; - Builder setFirebaseApp(FirebaseApp app) { - this.app = app; + private Builder() { } + + public Builder setProjectId(String projectId) { + this.projectId = projectId; return this; } @@ -462,6 +409,11 @@ Builder setHttpRequestFactory(HttpRequestFactory requestFactory) { return this; } + public Builder setJsonFactory(JsonFactory jsonFactory) { + this.jsonFactory = jsonFactory; + return this; + } + FirebaseUserManager build() { return new FirebaseUserManager(this); } diff --git a/src/main/java/com/google/firebase/auth/ListProviderConfigsPage.java b/src/main/java/com/google/firebase/auth/ListProviderConfigsPage.java index 361f932bd..0e35337a9 100644 --- a/src/main/java/com/google/firebase/auth/ListProviderConfigsPage.java +++ b/src/main/java/com/google/firebase/auth/ListProviderConfigsPage.java @@ -88,7 +88,7 @@ public String getNextPageToken() { @Override public ListProviderConfigsPage getNextPage() { if (hasNextPage()) { - Factory factory = new Factory(source, maxResults, currentBatch.getPageToken()); + Factory factory = new Factory<>(source, maxResults, currentBatch.getPageToken()); try { return factory.create(); } catch (FirebaseAuthException e) { @@ -99,25 +99,25 @@ public ListProviderConfigsPage getNextPage() { } /** - * Returns an {@link Iterable} that facilitates transparently iterating over all the provider + * Returns an {@code Iterable} that facilitates transparently iterating over all the provider * configs in the current Firebase project, starting from this page. * - *

The {@link Iterator} instances produced by the returned {@link Iterable} never buffers more + *

The {@code Iterator} instances produced by the returned {@code Iterable} never buffers more * than one page of provider configs at a time. It is safe to abandon the iterators (i.e. break * the loops) at any time. * - * @return a new {@link Iterable} instance. + * @return a new {@code Iterable} instance. */ @NonNull @Override public Iterable iterateAll() { - return new ProviderConfigIterable(this); + return new ProviderConfigIterable<>(this); } /** - * Returns an {@link Iterable} over the provider configs in this page. + * Returns an {@code Iterable} over the provider configs in this page. * - * @return a {@link Iterable} instance. + * @return a {@code Iterable} instance. */ @NonNull @Override @@ -136,7 +136,7 @@ private static class ProviderConfigIterable implements @Override @NonNull public Iterator iterator() { - return new ProviderConfigIterator(startingPage); + return new ProviderConfigIterator<>(startingPage); } /** @@ -230,7 +230,7 @@ public ListSamlProviderConfigsResponse fetch(int maxResults, String pageToken) } /** - * A simple factory class for {@link ProviderConfigsPage} instances. + * A simple factory class for {@link ListProviderConfigsPage} instances. * *

Performs argument validation before attempting to load any provider config data (which is * expensive, and hence may be performed asynchronously on a separate thread). @@ -261,7 +261,7 @@ static class Factory { ListProviderConfigsPage create() throws FirebaseAuthException { ListProviderConfigsResponse batch = source.fetch(maxResults, pageToken); - return new ListProviderConfigsPage(batch, source, maxResults); + return new ListProviderConfigsPage<>(batch, source, maxResults); } } } diff --git a/src/main/java/com/google/firebase/auth/OidcProviderConfig.java b/src/main/java/com/google/firebase/auth/OidcProviderConfig.java index 879b7e79f..26931788e 100644 --- a/src/main/java/com/google/firebase/auth/OidcProviderConfig.java +++ b/src/main/java/com/google/firebase/auth/OidcProviderConfig.java @@ -132,7 +132,7 @@ public static final class UpdateRequest extends AbstractUpdateRequestThe returned object should be passed to - * {@link AbstractFirebaseAuth#updateOidcProviderConfig(CreateRequest)} to save the updated + * {@link AbstractFirebaseAuth#updateOidcProviderConfig(UpdateRequest)} to save the updated * config. * * @param providerId A non-null, non-empty provider ID string. diff --git a/src/main/java/com/google/firebase/auth/RevocationCheckDecorator.java b/src/main/java/com/google/firebase/auth/RevocationCheckDecorator.java index e53ad25c4..74cda69c9 100644 --- a/src/main/java/com/google/firebase/auth/RevocationCheckDecorator.java +++ b/src/main/java/com/google/firebase/auth/RevocationCheckDecorator.java @@ -20,30 +20,27 @@ import static com.google.common.base.Preconditions.checkNotNull; import com.google.common.base.Strings; +import com.google.firebase.ErrorCode; /** * A decorator for adding token revocation checks to an existing {@link FirebaseTokenVerifier}. */ class RevocationCheckDecorator implements FirebaseTokenVerifier { - static final String ID_TOKEN_REVOKED_ERROR = "id-token-revoked"; - static final String SESSION_COOKIE_REVOKED_ERROR = "session-cookie-revoked"; - private final FirebaseTokenVerifier tokenVerifier; private final FirebaseUserManager userManager; - private final String errorCode; + private final AuthErrorCode errorCode; private final String shortName; private RevocationCheckDecorator( FirebaseTokenVerifier tokenVerifier, FirebaseUserManager userManager, - String errorCode, + AuthErrorCode errorCode, String shortName) { this.tokenVerifier = checkNotNull(tokenVerifier); this.userManager = checkNotNull(userManager); - checkArgument(!Strings.isNullOrEmpty(errorCode)); + this.errorCode = checkNotNull(errorCode); checkArgument(!Strings.isNullOrEmpty(shortName)); - this.errorCode = errorCode; this.shortName = shortName; } @@ -55,8 +52,14 @@ private RevocationCheckDecorator( public FirebaseToken verifyToken(String token) throws FirebaseAuthException { FirebaseToken firebaseToken = tokenVerifier.verifyToken(token); if (isRevoked(firebaseToken)) { - throw new FirebaseAuthException(errorCode, "Firebase " + shortName + " revoked"); + throw new FirebaseAuthException( + ErrorCode.INVALID_ARGUMENT, + "Firebase " + shortName + " is revoked.", + null, + null, + errorCode); } + return firebaseToken; } @@ -69,12 +72,12 @@ private boolean isRevoked(FirebaseToken firebaseToken) throws FirebaseAuthExcept static RevocationCheckDecorator decorateIdTokenVerifier( FirebaseTokenVerifier tokenVerifier, FirebaseUserManager userManager) { return new RevocationCheckDecorator( - tokenVerifier, userManager, ID_TOKEN_REVOKED_ERROR, "id token"); + tokenVerifier, userManager, AuthErrorCode.REVOKED_ID_TOKEN, "id token"); } static RevocationCheckDecorator decorateSessionCookieVerifier( FirebaseTokenVerifier tokenVerifier, FirebaseUserManager userManager) { return new RevocationCheckDecorator( - tokenVerifier, userManager, SESSION_COOKIE_REVOKED_ERROR, "session cookie"); + tokenVerifier, userManager, AuthErrorCode.REVOKED_SESSION_COOKIE, "session cookie"); } } diff --git a/src/main/java/com/google/firebase/auth/hash/Bcrypt.java b/src/main/java/com/google/firebase/auth/hash/Bcrypt.java index 2b5f89029..9c55d8d56 100644 --- a/src/main/java/com/google/firebase/auth/hash/Bcrypt.java +++ b/src/main/java/com/google/firebase/auth/hash/Bcrypt.java @@ -24,7 +24,7 @@ * Represents the Bcrypt password hashing algorithm. Can be used as an instance of * {@link com.google.firebase.auth.UserImportHash} when importing users. */ -public class Bcrypt extends UserImportHash { +public final class Bcrypt extends UserImportHash { private Bcrypt() { super("BCRYPT"); diff --git a/src/main/java/com/google/firebase/auth/hash/HmacMd5.java b/src/main/java/com/google/firebase/auth/hash/HmacMd5.java index b67574358..b2ffdb852 100644 --- a/src/main/java/com/google/firebase/auth/hash/HmacMd5.java +++ b/src/main/java/com/google/firebase/auth/hash/HmacMd5.java @@ -20,7 +20,7 @@ * Represents the HMAC MD5 password hashing algorithm. Can be used as an instance of * {@link com.google.firebase.auth.UserImportHash} when importing users. */ -public class HmacMd5 extends Hmac { +public final class HmacMd5 extends Hmac { private HmacMd5(Builder builder) { super("HMAC_MD5", builder); diff --git a/src/main/java/com/google/firebase/auth/hash/HmacSha1.java b/src/main/java/com/google/firebase/auth/hash/HmacSha1.java index a9ecefd6f..964e5e60d 100644 --- a/src/main/java/com/google/firebase/auth/hash/HmacSha1.java +++ b/src/main/java/com/google/firebase/auth/hash/HmacSha1.java @@ -20,7 +20,7 @@ * Represents the HMAC SHA1 password hashing algorithm. Can be used as an instance of * {@link com.google.firebase.auth.UserImportHash} when importing users. */ -public class HmacSha1 extends Hmac { +public final class HmacSha1 extends Hmac { private HmacSha1(Builder builder) { super("HMAC_SHA1", builder); diff --git a/src/main/java/com/google/firebase/auth/hash/HmacSha256.java b/src/main/java/com/google/firebase/auth/hash/HmacSha256.java index 78f131cff..92917e6f3 100644 --- a/src/main/java/com/google/firebase/auth/hash/HmacSha256.java +++ b/src/main/java/com/google/firebase/auth/hash/HmacSha256.java @@ -20,7 +20,7 @@ * Represents the HMAC SHA256 password hashing algorithm. Can be used as an instance of * {@link com.google.firebase.auth.UserImportHash} when importing users. */ -public class HmacSha256 extends Hmac { +public final class HmacSha256 extends Hmac { private HmacSha256(Builder builder) { super("HMAC_SHA256", builder); diff --git a/src/main/java/com/google/firebase/auth/hash/HmacSha512.java b/src/main/java/com/google/firebase/auth/hash/HmacSha512.java index 21e6a2b25..b5a0e09ec 100644 --- a/src/main/java/com/google/firebase/auth/hash/HmacSha512.java +++ b/src/main/java/com/google/firebase/auth/hash/HmacSha512.java @@ -20,7 +20,7 @@ * Represents the HMAC SHA512 password hashing algorithm. Can be used as an instance of * {@link com.google.firebase.auth.UserImportHash} when importing users. */ -public class HmacSha512 extends Hmac { +public final class HmacSha512 extends Hmac { private HmacSha512(Builder builder) { super("HMAC_SHA512", builder); diff --git a/src/main/java/com/google/firebase/auth/hash/Md5.java b/src/main/java/com/google/firebase/auth/hash/Md5.java index 2abbe55ba..353b07f01 100644 --- a/src/main/java/com/google/firebase/auth/hash/Md5.java +++ b/src/main/java/com/google/firebase/auth/hash/Md5.java @@ -20,7 +20,7 @@ * Represents the MD5 password hashing algorithm. Can be used as an instance of * {@link com.google.firebase.auth.UserImportHash} when importing users. */ -public class Md5 extends RepeatableHash { +public final class Md5 extends RepeatableHash { private Md5(Builder builder) { super("MD5", 0, 8192, builder); diff --git a/src/main/java/com/google/firebase/auth/hash/Pbkdf2Sha256.java b/src/main/java/com/google/firebase/auth/hash/Pbkdf2Sha256.java index 4c5108e35..6c3ffeff2 100644 --- a/src/main/java/com/google/firebase/auth/hash/Pbkdf2Sha256.java +++ b/src/main/java/com/google/firebase/auth/hash/Pbkdf2Sha256.java @@ -20,7 +20,7 @@ * Represents the PBKDF2 SHA256 password hashing algorithm. Can be used as an instance of * {@link com.google.firebase.auth.UserImportHash} when importing users. */ -public class Pbkdf2Sha256 extends RepeatableHash { +public final class Pbkdf2Sha256 extends RepeatableHash { private Pbkdf2Sha256(Builder builder) { super("PBKDF2_SHA256", 0, 120000, builder); diff --git a/src/main/java/com/google/firebase/auth/hash/PbkdfSha1.java b/src/main/java/com/google/firebase/auth/hash/PbkdfSha1.java index 8afe3f4ab..647a365b3 100644 --- a/src/main/java/com/google/firebase/auth/hash/PbkdfSha1.java +++ b/src/main/java/com/google/firebase/auth/hash/PbkdfSha1.java @@ -20,7 +20,7 @@ * Represents the PBKDF SHA1 password hashing algorithm. Can be used as an instance of * {@link com.google.firebase.auth.UserImportHash} when importing users. */ -public class PbkdfSha1 extends RepeatableHash { +public final class PbkdfSha1 extends RepeatableHash { private PbkdfSha1(Builder builder) { super("PBKDF_SHA1", 0, 120000, builder); diff --git a/src/main/java/com/google/firebase/auth/hash/Sha1.java b/src/main/java/com/google/firebase/auth/hash/Sha1.java index 385f4310c..9de01b0b8 100644 --- a/src/main/java/com/google/firebase/auth/hash/Sha1.java +++ b/src/main/java/com/google/firebase/auth/hash/Sha1.java @@ -20,7 +20,7 @@ * Represents the SHA1 password hashing algorithm. Can be used as an instance of * {@link com.google.firebase.auth.UserImportHash} when importing users. */ -public class Sha1 extends RepeatableHash { +public final class Sha1 extends RepeatableHash { private Sha1(Builder builder) { super("SHA1", 1, 8192, builder); diff --git a/src/main/java/com/google/firebase/auth/hash/Sha256.java b/src/main/java/com/google/firebase/auth/hash/Sha256.java index f65aee19a..d0185195e 100644 --- a/src/main/java/com/google/firebase/auth/hash/Sha256.java +++ b/src/main/java/com/google/firebase/auth/hash/Sha256.java @@ -20,7 +20,7 @@ * Represents the SHA256 password hashing algorithm. Can be used as an instance of * {@link com.google.firebase.auth.UserImportHash} when importing users. */ -public class Sha256 extends RepeatableHash { +public final class Sha256 extends RepeatableHash { private Sha256(Builder builder) { super("SHA256", 1, 8192, builder); diff --git a/src/main/java/com/google/firebase/auth/hash/Sha512.java b/src/main/java/com/google/firebase/auth/hash/Sha512.java index e582520a9..f468abe1c 100644 --- a/src/main/java/com/google/firebase/auth/hash/Sha512.java +++ b/src/main/java/com/google/firebase/auth/hash/Sha512.java @@ -20,7 +20,7 @@ * Represents the SHA512 password hashing algorithm. Can be used as an instance of * {@link com.google.firebase.auth.UserImportHash} when importing users. */ -public class Sha512 extends RepeatableHash { +public final class Sha512 extends RepeatableHash { private Sha512(Builder builder) { super("SHA512", 1, 8192, builder); diff --git a/src/main/java/com/google/firebase/auth/hash/StandardScrypt.java b/src/main/java/com/google/firebase/auth/hash/StandardScrypt.java index 49f7d72f5..139fd114f 100644 --- a/src/main/java/com/google/firebase/auth/hash/StandardScrypt.java +++ b/src/main/java/com/google/firebase/auth/hash/StandardScrypt.java @@ -24,7 +24,7 @@ * Represents the Standard Scrypt password hashing algorithm. Can be used as an instance of * {@link com.google.firebase.auth.UserImportHash} when importing users. */ -public class StandardScrypt extends UserImportHash { +public final class StandardScrypt extends UserImportHash { private final int derivedKeyLength; private final int blockSize; diff --git a/src/main/java/com/google/firebase/auth/internal/AuthErrorHandler.java b/src/main/java/com/google/firebase/auth/internal/AuthErrorHandler.java new file mode 100644 index 000000000..e98911407 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/internal/AuthErrorHandler.java @@ -0,0 +1,222 @@ +/* + * Copyright 2020 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.internal; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.api.client.json.GenericJson; +import com.google.api.client.json.JsonFactory; +import com.google.api.client.util.Key; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableMap; +import com.google.firebase.ErrorCode; +import com.google.firebase.FirebaseException; +import com.google.firebase.auth.AuthErrorCode; +import com.google.firebase.auth.FirebaseAuthException; +import com.google.firebase.internal.AbstractHttpErrorHandler; +import com.google.firebase.internal.Nullable; +import java.io.IOException; +import java.util.Map; + +final class AuthErrorHandler extends AbstractHttpErrorHandler { + + private static final Map ERROR_CODES = + ImmutableMap.builder() + .put( + "CONFIGURATION_NOT_FOUND", + new AuthError( + ErrorCode.NOT_FOUND, + "No IdP configuration found corresponding to the provided identifier", + AuthErrorCode.CONFIGURATION_NOT_FOUND)) + .put( + "DUPLICATE_EMAIL", + new AuthError( + ErrorCode.ALREADY_EXISTS, + "The user with the provided email already exists", + AuthErrorCode.EMAIL_ALREADY_EXISTS)) + .put( + "DUPLICATE_LOCAL_ID", + new AuthError( + ErrorCode.ALREADY_EXISTS, + "The user with the provided uid already exists", + AuthErrorCode.UID_ALREADY_EXISTS)) + .put( + "EMAIL_EXISTS", + new AuthError( + ErrorCode.ALREADY_EXISTS, + "The user with the provided email already exists", + AuthErrorCode.EMAIL_ALREADY_EXISTS)) + .put( + "INVALID_DYNAMIC_LINK_DOMAIN", + new AuthError( + ErrorCode.INVALID_ARGUMENT, + "The provided dynamic link domain is not " + + "configured or authorized for the current project", + AuthErrorCode.INVALID_DYNAMIC_LINK_DOMAIN)) + .put( + "PHONE_NUMBER_EXISTS", + new AuthError( + ErrorCode.ALREADY_EXISTS, + "The user with the provided phone number already exists", + AuthErrorCode.PHONE_NUMBER_ALREADY_EXISTS)) + .put( + "TENANT_NOT_FOUND", + new AuthError( + ErrorCode.NOT_FOUND, + "No tenant found for the given identifier", + AuthErrorCode.TENANT_NOT_FOUND)) + .put( + "UNAUTHORIZED_DOMAIN", + new AuthError( + ErrorCode.INVALID_ARGUMENT, + "The domain of the continue URL is not whitelisted", + AuthErrorCode.UNAUTHORIZED_CONTINUE_URL)) + .put( + "USER_NOT_FOUND", + new AuthError( + ErrorCode.NOT_FOUND, + "No user record found for the given identifier", + AuthErrorCode.USER_NOT_FOUND)) + .build(); + + private final JsonFactory jsonFactory; + + AuthErrorHandler(JsonFactory jsonFactory) { + this.jsonFactory = checkNotNull(jsonFactory); + } + + @Override + protected FirebaseAuthException createException(FirebaseException base) { + String response = getResponse(base); + AuthServiceErrorResponse parsed = safeParse(response); + AuthError errorInfo = ERROR_CODES.get(parsed.getCode()); + if (errorInfo != null) { + return new FirebaseAuthException( + errorInfo.getErrorCode(), + errorInfo.buildMessage(parsed), + base.getCause(), + base.getHttpResponse(), + errorInfo.getAuthErrorCode()); + } + + return new FirebaseAuthException(base); + } + + private String getResponse(FirebaseException base) { + if (base.getHttpResponse() == null) { + return null; + } + + return base.getHttpResponse().getContent(); + } + + private AuthServiceErrorResponse safeParse(String response) { + AuthServiceErrorResponse parsed = new AuthServiceErrorResponse(); + if (!Strings.isNullOrEmpty(response)) { + try { + jsonFactory.createJsonParser(response).parse(parsed); + } catch (IOException ignore) { + // Ignore any error that may occur while parsing the error response. The server + // may have responded with a non-json payload. + } + } + + return parsed; + } + + private static class AuthError { + + private final ErrorCode errorCode; + private final String message; + private final AuthErrorCode authErrorCode; + + AuthError(ErrorCode errorCode, String message, AuthErrorCode authErrorCode) { + this.errorCode = errorCode; + this.message = message; + this.authErrorCode = authErrorCode; + } + + ErrorCode getErrorCode() { + return errorCode; + } + + AuthErrorCode getAuthErrorCode() { + return authErrorCode; + } + + String buildMessage(AuthServiceErrorResponse response) { + StringBuilder builder = new StringBuilder(this.message) + .append(" (").append(response.getCode()).append(")"); + String detail = response.getDetail(); + if (!Strings.isNullOrEmpty(detail)) { + builder.append(": ").append(detail); + } else { + builder.append("."); + } + + return builder.toString(); + } + } + + /** + * JSON data binding for JSON error messages sent by Google identity toolkit service. These + * error messages take the form `{"error": {"message": "CODE: OPTIONAL DETAILS"}}`. + */ + private static class AuthServiceErrorResponse { + + @Key("error") + private GenericJson error; + + @Nullable + public String getCode() { + String message = getMessage(); + if (Strings.isNullOrEmpty(message)) { + return null; + } + + int separator = message.indexOf(':'); + if (separator != -1) { + return message.substring(0, separator); + } + + return message; + } + + @Nullable + public String getDetail() { + String message = getMessage(); + if (Strings.isNullOrEmpty(message)) { + return null; + } + + int separator = message.indexOf(':'); + if (separator != -1) { + return message.substring(separator + 1).trim(); + } + + return null; + } + + private String getMessage() { + if (error == null) { + return null; + } + + return (String) error.get("message"); + } + } +} diff --git a/src/main/java/com/google/firebase/auth/internal/AuthHttpClient.java b/src/main/java/com/google/firebase/auth/internal/AuthHttpClient.java index ad77236d1..e8413f13f 100644 --- a/src/main/java/com/google/firebase/auth/internal/AuthHttpClient.java +++ b/src/main/java/com/google/firebase/auth/internal/AuthHttpClient.java @@ -16,26 +16,15 @@ package com.google.firebase.auth.internal; -import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.base.Preconditions.checkNotNull; - -import com.google.api.client.http.GenericUrl; -import com.google.api.client.http.HttpContent; -import com.google.api.client.http.HttpRequest; import com.google.api.client.http.HttpRequestFactory; -import com.google.api.client.http.HttpResponse; -import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.HttpResponseInterceptor; -import com.google.api.client.http.json.JsonHttpContent; import com.google.api.client.json.JsonFactory; -import com.google.api.client.json.JsonObjectParser; -import com.google.common.base.Strings; -import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSortedSet; +import com.google.firebase.IncomingHttpResponse; import com.google.firebase.auth.FirebaseAuthException; -import com.google.firebase.internal.Nullable; +import com.google.firebase.internal.ErrorHandlingHttpClient; +import com.google.firebase.internal.HttpRequestInfo; import com.google.firebase.internal.SdkUtils; -import java.io.IOException; import java.util.Map; import java.util.Set; @@ -44,45 +33,17 @@ */ public final class AuthHttpClient { - public static final String CONFIGURATION_NOT_FOUND_ERROR = "configuration-not-found"; - public static final String INTERNAL_ERROR = "internal-error"; - public static final String TENANT_NOT_FOUND_ERROR = "tenant-not-found"; - public static final String USER_NOT_FOUND_ERROR = "user-not-found"; - private static final String CLIENT_VERSION_HEADER = "X-Client-Version"; private static final String CLIENT_VERSION = "Java/Admin/" + SdkUtils.getVersion(); - // Map of server-side error codes to SDK error codes. - // SDK error codes defined at: https://firebase.google.com/docs/auth/admin/errors - private static final Map ERROR_CODES = ImmutableMap.builder() - .put("CLAIMS_TOO_LARGE", "claims-too-large") - .put("CONFIGURATION_NOT_FOUND", CONFIGURATION_NOT_FOUND_ERROR) - .put("INSUFFICIENT_PERMISSION", "insufficient-permission") - .put("DUPLICATE_EMAIL", "email-already-exists") - .put("DUPLICATE_LOCAL_ID", "uid-already-exists") - .put("EMAIL_EXISTS", "email-already-exists") - .put("INVALID_CLAIMS", "invalid-claims") - .put("INVALID_EMAIL", "invalid-email") - .put("INVALID_PAGE_SELECTION", "invalid-page-token") - .put("INVALID_PHONE_NUMBER", "invalid-phone-number") - .put("PHONE_NUMBER_EXISTS", "phone-number-already-exists") - .put("PROJECT_NOT_FOUND", "project-not-found") - .put("USER_NOT_FOUND", USER_NOT_FOUND_ERROR) - .put("WEAK_PASSWORD", "invalid-password") - .put("UNAUTHORIZED_DOMAIN", "unauthorized-continue-uri") - .put("INVALID_DYNAMIC_LINK_DOMAIN", "invalid-dynamic-link-domain") - .put("TENANT_NOT_FOUND", TENANT_NOT_FOUND_ERROR) - .build(); - + private final ErrorHandlingHttpClient httpClient; private final JsonFactory jsonFactory; - private final HttpRequestFactory requestFactory; - - private HttpResponseInterceptor interceptor; public AuthHttpClient(JsonFactory jsonFactory, HttpRequestFactory requestFactory) { + AuthErrorHandler authErrorHandler = new AuthErrorHandler(jsonFactory); + this.httpClient = new ErrorHandlingHttpClient<>(requestFactory, jsonFactory, authErrorHandler); this.jsonFactory = jsonFactory; - this.requestFactory = requestFactory; } public static Set generateMask(Map properties) { @@ -101,60 +62,20 @@ public static Set generateMask(Map properties) { } public void setInterceptor(HttpResponseInterceptor interceptor) { - this.interceptor = interceptor; + this.httpClient.setInterceptor(interceptor); } - public T sendRequest( - String method, GenericUrl url, - @Nullable Object content, Class clazz) throws FirebaseAuthException { + public T sendRequest(HttpRequestInfo request, Class clazz) throws FirebaseAuthException { + IncomingHttpResponse response = this.sendRequest(request); + return this.parse(response, clazz); + } - checkArgument(!Strings.isNullOrEmpty(method), "method must not be null or empty"); - checkNotNull(url, "url must not be null"); - checkNotNull(clazz, "response class must not be null"); - HttpResponse response = null; - try { - HttpContent httpContent = content != null ? new JsonHttpContent(jsonFactory, content) : null; - HttpRequest request = - requestFactory.buildRequest(method.equals("PATCH") ? "POST" : method, url, httpContent); - request.setParser(new JsonObjectParser(jsonFactory)); - request.getHeaders().set(CLIENT_VERSION_HEADER, CLIENT_VERSION); - if (method.equals("PATCH")) { - request.getHeaders().set("X-HTTP-Method-Override", "PATCH"); - } - request.setResponseInterceptor(interceptor); - response = request.execute(); - return response.parseAs(clazz); - } catch (HttpResponseException e) { - // Server responded with an HTTP error - handleHttpError(e); - return null; - } catch (IOException e) { - // All other IO errors (Connection refused, reset, parse error etc.) - throw new FirebaseAuthException( - INTERNAL_ERROR, "Error while calling the Firebase Auth backend service", e); - } finally { - if (response != null) { - try { - response.disconnect(); - } catch (IOException ignored) { - // Ignored - } - } - } + public IncomingHttpResponse sendRequest(HttpRequestInfo request) throws FirebaseAuthException { + request.addHeader(CLIENT_VERSION_HEADER, CLIENT_VERSION); + return httpClient.send(request); } - private void handleHttpError(HttpResponseException e) throws FirebaseAuthException { - try { - HttpErrorResponse response = jsonFactory.fromString(e.getContent(), HttpErrorResponse.class); - String code = ERROR_CODES.get(response.getErrorCode()); - if (code != null) { - throw new FirebaseAuthException(code, "Firebase Auth service responded with an error", e); - } - } catch (IOException ignored) { - // Ignored - } - String msg = String.format( - "Unexpected HTTP response with status: %d; body: %s", e.getStatusCode(), e.getContent()); - throw new FirebaseAuthException(INTERNAL_ERROR, msg, e); + public T parse(IncomingHttpResponse response, Class clazz) throws FirebaseAuthException { + return httpClient.parse(response, clazz); } } diff --git a/src/main/java/com/google/firebase/auth/internal/CryptoSigner.java b/src/main/java/com/google/firebase/auth/internal/CryptoSigner.java index 3036f9f28..2ff30a20c 100644 --- a/src/main/java/com/google/firebase/auth/internal/CryptoSigner.java +++ b/src/main/java/com/google/firebase/auth/internal/CryptoSigner.java @@ -16,6 +16,7 @@ package com.google.firebase.auth.internal; +import com.google.firebase.auth.FirebaseAuthException; import com.google.firebase.internal.NonNull; import java.io.IOException; @@ -32,10 +33,10 @@ interface CryptoSigner { * * @param payload Data to be signed * @return Signature as a byte array - * @throws IOException If an error occurs during signing + * @throws FirebaseAuthException If an error occurs during signing */ @NonNull - byte[] sign(@NonNull byte[] payload) throws IOException; + byte[] sign(@NonNull byte[] payload) throws FirebaseAuthException; /** * Returns the client email of the service account used to sign payloads. diff --git a/src/main/java/com/google/firebase/auth/internal/CryptoSigners.java b/src/main/java/com/google/firebase/auth/internal/CryptoSigners.java index 6ea70b880..7bc54afdf 100644 --- a/src/main/java/com/google/firebase/auth/internal/CryptoSigners.java +++ b/src/main/java/com/google/firebase/auth/internal/CryptoSigners.java @@ -8,10 +8,8 @@ import com.google.api.client.http.HttpRequestFactory; import com.google.api.client.http.HttpResponse; import com.google.api.client.http.HttpResponseInterceptor; -import com.google.api.client.http.json.JsonHttpContent; +import com.google.api.client.json.GenericJson; import com.google.api.client.json.JsonFactory; -import com.google.api.client.json.JsonObjectParser; -import com.google.api.client.util.Key; import com.google.api.client.util.StringUtils; import com.google.auth.ServiceAccountSigner; import com.google.auth.oauth2.GoogleCredentials; @@ -21,9 +19,13 @@ import com.google.common.io.BaseEncoding; import com.google.common.io.ByteStreams; import com.google.firebase.FirebaseApp; -import com.google.firebase.FirebaseOptions; +import com.google.firebase.FirebaseException; import com.google.firebase.ImplFirebaseTrampolines; -import com.google.firebase.internal.FirebaseRequestInitializer; +import com.google.firebase.auth.FirebaseAuthException; +import com.google.firebase.internal.AbstractPlatformErrorHandler; +import com.google.firebase.internal.ApiClientUtils; +import com.google.firebase.internal.ErrorHandlingHttpClient; +import com.google.firebase.internal.HttpRequestInfo; import com.google.firebase.internal.NonNull; import java.io.IOException; import java.util.Map; @@ -34,7 +36,9 @@ public class CryptoSigners { private static final String METADATA_SERVICE_URL = - "http://metadata/computeMetadata/v1/instance/service-accounts/default/email"; + "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/email"; + + private CryptoSigners() { } /** * A {@link CryptoSigner} implementation that uses service account credentials or equivalent @@ -69,48 +73,33 @@ static class IAMCryptoSigner implements CryptoSigner { private static final String IAM_SIGN_BLOB_URL = "https://iam.googleapis.com/v1/projects/-/serviceAccounts/%s:signBlob"; - private final HttpRequestFactory requestFactory; - private final JsonFactory jsonFactory; private final String serviceAccount; - private HttpResponseInterceptor interceptor; + private final ErrorHandlingHttpClient httpClient; IAMCryptoSigner( @NonNull HttpRequestFactory requestFactory, @NonNull JsonFactory jsonFactory, @NonNull String serviceAccount) { - this.requestFactory = checkNotNull(requestFactory); - this.jsonFactory = checkNotNull(jsonFactory); checkArgument(!Strings.isNullOrEmpty(serviceAccount)); this.serviceAccount = serviceAccount; + this.httpClient = new ErrorHandlingHttpClient<>( + requestFactory, + jsonFactory, + new IAMErrorHandler(jsonFactory)); } void setInterceptor(HttpResponseInterceptor interceptor) { - this.interceptor = interceptor; + httpClient.setInterceptor(interceptor); } @Override - public byte[] sign(byte[] payload) throws IOException { - String encodedUrl = String.format(IAM_SIGN_BLOB_URL, serviceAccount); - HttpResponse response = null; + public byte[] sign(byte[] payload) throws FirebaseAuthException { String encodedPayload = BaseEncoding.base64().encode(payload); Map content = ImmutableMap.of("bytesToSign", encodedPayload); - try { - HttpRequest request = requestFactory.buildPostRequest(new GenericUrl(encodedUrl), - new JsonHttpContent(jsonFactory, content)); - request.setParser(new JsonObjectParser(jsonFactory)); - request.setResponseInterceptor(interceptor); - response = request.execute(); - SignBlobResponse parsed = response.parseAs(SignBlobResponse.class); - return BaseEncoding.base64().decode(parsed.signature); - } finally { - if (response != null) { - try { - response.disconnect(); - } catch (IOException ignored) { - // Ignored - } - } - } + String encodedUrl = String.format(IAM_SIGN_BLOB_URL, serviceAccount); + HttpRequestInfo requestInfo = HttpRequestInfo.buildJsonPostRequest(encodedUrl, content); + GenericJson parsed = httpClient.sendAndParse(requestInfo, GenericJson.class); + return BaseEncoding.base64().decode((String) parsed.get("signature")); } @Override @@ -119,9 +108,17 @@ public String getAccount() { } } - public static class SignBlobResponse { - @Key("signature") - private String signature; + private static class IAMErrorHandler + extends AbstractPlatformErrorHandler { + + IAMErrorHandler(JsonFactory jsonFactory) { + super(jsonFactory); + } + + @Override + protected FirebaseAuthException createException(FirebaseException base) { + return new FirebaseAuthException(base); + } } /** @@ -136,14 +133,12 @@ public static CryptoSigner getCryptoSigner(FirebaseApp firebaseApp) throws IOExc return new ServiceAccountCryptoSigner((ServiceAccountCredentials) credentials); } - FirebaseOptions options = firebaseApp.getOptions(); - HttpRequestFactory requestFactory = options.getHttpTransport().createRequestFactory( - new FirebaseRequestInitializer(firebaseApp)); - JsonFactory jsonFactory = options.getJsonFactory(); + HttpRequestFactory requestFactory = ApiClientUtils.newAuthorizedRequestFactory(firebaseApp); + JsonFactory jsonFactory = firebaseApp.getOptions().getJsonFactory(); // If the SDK was initialized with a service account email, use it with the IAM service // to sign bytes. - String serviceAccountId = options.getServiceAccountId(); + String serviceAccountId = firebaseApp.getOptions().getServiceAccountId(); if (!Strings.isNullOrEmpty(serviceAccountId)) { return new IAMCryptoSigner(requestFactory, jsonFactory, serviceAccountId); } @@ -156,15 +151,22 @@ public static CryptoSigner getCryptoSigner(FirebaseApp firebaseApp) throws IOExc // Attempt to discover a service account email from the local Metadata service. Use it // with the IAM service to sign bytes. - HttpRequest request = requestFactory.buildGetRequest(new GenericUrl(METADATA_SERVICE_URL)); + serviceAccountId = discoverServiceAccountId(firebaseApp); + return new IAMCryptoSigner(requestFactory, jsonFactory, serviceAccountId); + } + + private static String discoverServiceAccountId(FirebaseApp firebaseApp) throws IOException { + HttpRequestFactory metadataRequestFactory = + ApiClientUtils.newUnauthorizedRequestFactory(firebaseApp); + HttpRequest request = metadataRequestFactory.buildGetRequest( + new GenericUrl(METADATA_SERVICE_URL)); request.getHeaders().set("Metadata-Flavor", "Google"); HttpResponse response = request.execute(); try { byte[] output = ByteStreams.toByteArray(response.getContent()); - serviceAccountId = StringUtils.newStringUtf8(output).trim(); - return new IAMCryptoSigner(requestFactory, jsonFactory, serviceAccountId); + return StringUtils.newStringUtf8(output).trim(); } finally { - response.disconnect(); + ApiClientUtils.disconnectQuietly(response); } } } diff --git a/src/main/java/com/google/firebase/auth/internal/FirebaseTokenFactory.java b/src/main/java/com/google/firebase/auth/internal/FirebaseTokenFactory.java index 778911d46..b5aa1e31a 100644 --- a/src/main/java/com/google/firebase/auth/internal/FirebaseTokenFactory.java +++ b/src/main/java/com/google/firebase/auth/internal/FirebaseTokenFactory.java @@ -25,7 +25,9 @@ import com.google.api.client.util.Base64; import com.google.api.client.util.Clock; import com.google.api.client.util.StringUtils; +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; +import com.google.firebase.auth.FirebaseAuthException; import com.google.firebase.internal.Nullable; import java.io.IOException; @@ -44,10 +46,6 @@ public class FirebaseTokenFactory { private final CryptoSigner signer; private final String tenantId; - public FirebaseTokenFactory(JsonFactory jsonFactory, Clock clock, CryptoSigner signer) { - this(jsonFactory, clock, signer, null); - } - public FirebaseTokenFactory( JsonFactory jsonFactory, Clock clock, CryptoSigner signer, @Nullable String tenantId) { this.jsonFactory = checkNotNull(jsonFactory); @@ -56,12 +54,17 @@ public FirebaseTokenFactory( this.tenantId = tenantId; } - String createSignedCustomAuthTokenForUser(String uid) throws IOException { + @VisibleForTesting + FirebaseTokenFactory(JsonFactory jsonFactory, Clock clock, CryptoSigner signer) { + this(jsonFactory, clock, signer, null); + } + + String createSignedCustomAuthTokenForUser(String uid) throws FirebaseAuthException { return createSignedCustomAuthTokenForUser(uid, null); } public String createSignedCustomAuthTokenForUser( - String uid, Map developerClaims) throws IOException { + String uid, Map developerClaims) throws FirebaseAuthException { checkArgument(!Strings.isNullOrEmpty(uid), "Uid must be provided."); checkArgument(uid.length() <= 128, "Uid must be shorter than 128 characters."); @@ -88,20 +91,33 @@ public String createSignedCustomAuthTokenForUser( String.format("developerClaims must not contain a reserved key: %s", key)); } } + GenericJson jsonObject = new GenericJson(); jsonObject.putAll(developerClaims); payload.setDeveloperClaims(jsonObject); } + return signPayload(header, payload); } - private String signPayload(JsonWebSignature.Header header, - FirebaseCustomAuthToken.Payload payload) throws IOException { - String headerString = Base64.encodeBase64URLSafeString(jsonFactory.toByteArray(header)); - String payloadString = Base64.encodeBase64URLSafeString(jsonFactory.toByteArray(payload)); - String content = headerString + "." + payloadString; + private String signPayload( + JsonWebSignature.Header header, + FirebaseCustomAuthToken.Payload payload) throws FirebaseAuthException { + String content = encodePayload(header, payload); byte[] contentBytes = StringUtils.getBytesUtf8(content); String signature = Base64.encodeBase64URLSafeString(signer.sign(contentBytes)); return content + "." + signature; } + + private String encodePayload( + JsonWebSignature.Header header, FirebaseCustomAuthToken.Payload payload) { + try { + String headerString = Base64.encodeBase64URLSafeString(jsonFactory.toByteArray(header)); + String payloadString = Base64.encodeBase64URLSafeString(jsonFactory.toByteArray(payload)); + return headerString + "." + payloadString; + } catch (IOException e) { + throw new IllegalArgumentException( + "Failed to encode JWT with the given claims: " + e.getMessage(), e); + } + } } diff --git a/src/main/java/com/google/firebase/auth/internal/HttpErrorResponse.java b/src/main/java/com/google/firebase/auth/internal/HttpErrorResponse.java deleted file mode 100644 index d4be4b4a6..000000000 --- a/src/main/java/com/google/firebase/auth/internal/HttpErrorResponse.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2017 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.internal; - -import com.google.api.client.util.Key; -import com.google.common.base.Strings; - -/** - * JSON data binding for JSON error messages sent by Google identity toolkit service. - */ -public class HttpErrorResponse { - - @Key("error") - private Error error; - - public String getErrorCode() { - if (error != null) { - if (!Strings.isNullOrEmpty(error.getCode())) { - return error.getCode(); - } - } - return "unknown"; - } - - public static class Error { - - @Key("message") - private String code; - - public String getCode() { - return code; - } - } - -} diff --git a/src/main/java/com/google/firebase/auth/multitenancy/FirebaseTenantClient.java b/src/main/java/com/google/firebase/auth/multitenancy/FirebaseTenantClient.java index 1278e63d5..5b776a49a 100644 --- a/src/main/java/com/google/firebase/auth/multitenancy/FirebaseTenantClient.java +++ b/src/main/java/com/google/firebase/auth/multitenancy/FirebaseTenantClient.java @@ -19,7 +19,6 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; -import com.google.api.client.http.GenericUrl; import com.google.api.client.http.HttpRequestFactory; import com.google.api.client.http.HttpResponseInterceptor; import com.google.api.client.json.GenericJson; @@ -33,6 +32,7 @@ import com.google.firebase.auth.internal.AuthHttpClient; import com.google.firebase.auth.internal.ListTenantsResponse; import com.google.firebase.internal.ApiClientUtils; +import com.google.firebase.internal.HttpRequestInfo; import java.util.Map; final class FirebaseTenantClient { @@ -46,15 +46,19 @@ final class FirebaseTenantClient { private final AuthHttpClient httpClient; FirebaseTenantClient(FirebaseApp app) { - checkNotNull(app, "FirebaseApp must not be null"); - String projectId = ImplFirebaseTrampolines.getProjectId(app); + this( + ImplFirebaseTrampolines.getProjectId(checkNotNull(app)), + app.getOptions().getJsonFactory(), + ApiClientUtils.newAuthorizedRequestFactory(app)); + } + + FirebaseTenantClient( + String projectId, JsonFactory jsonFactory, HttpRequestFactory requestFactory) { checkArgument(!Strings.isNullOrEmpty(projectId), "Project ID is required to access the auth service. Use a service account credential or " + "set the project ID explicitly via FirebaseOptions. Alternatively you can also " + "set the project ID via the GOOGLE_CLOUD_PROJECT environment variable."); this.tenantMgtBaseUrl = String.format(ID_TOOLKIT_URL, "v2", projectId); - JsonFactory jsonFactory = app.getOptions().getJsonFactory(); - HttpRequestFactory requestFactory = ApiClientUtils.newAuthorizedRequestFactory(app); this.httpClient = new AuthHttpClient(jsonFactory, requestFactory); } @@ -63,25 +67,28 @@ void setInterceptor(HttpResponseInterceptor interceptor) { } Tenant getTenant(String tenantId) throws FirebaseAuthException { - GenericUrl url = new GenericUrl(tenantMgtBaseUrl + getTenantUrlSuffix(tenantId)); - return httpClient.sendRequest("GET", url, null, Tenant.class); + String url = tenantMgtBaseUrl + getTenantUrlSuffix(tenantId); + return httpClient.sendRequest(HttpRequestInfo.buildGetRequest(url), Tenant.class); } Tenant createTenant(Tenant.CreateRequest request) throws FirebaseAuthException { - GenericUrl url = new GenericUrl(tenantMgtBaseUrl + "/tenants"); - return httpClient.sendRequest("POST", url, request.getProperties(), Tenant.class); + String url = tenantMgtBaseUrl + "/tenants"; + return httpClient.sendRequest( + HttpRequestInfo.buildJsonPostRequest(url, request.getProperties()), + Tenant.class); } Tenant updateTenant(Tenant.UpdateRequest request) throws FirebaseAuthException { Map properties = request.getProperties(); - GenericUrl url = new GenericUrl(tenantMgtBaseUrl + getTenantUrlSuffix(request.getTenantId())); - url.put("updateMask", Joiner.on(",").join(AuthHttpClient.generateMask(properties))); - return httpClient.sendRequest("PATCH", url, properties, Tenant.class); + String url = tenantMgtBaseUrl + getTenantUrlSuffix(request.getTenantId()); + HttpRequestInfo requestInfo = HttpRequestInfo.buildJsonPatchRequest(url, properties) + .addParameter("updateMask", Joiner.on(",").join(AuthHttpClient.generateMask(properties))); + return httpClient.sendRequest(requestInfo, Tenant.class); } void deleteTenant(String tenantId) throws FirebaseAuthException { - GenericUrl url = new GenericUrl(tenantMgtBaseUrl + getTenantUrlSuffix(tenantId)); - httpClient.sendRequest("DELETE", url, null, GenericJson.class); + String url = tenantMgtBaseUrl + getTenantUrlSuffix(tenantId); + httpClient.sendRequest(HttpRequestInfo.buildDeleteRequest(url), GenericJson.class); } ListTenantsResponse listTenants(int maxResults, String pageToken) @@ -94,14 +101,9 @@ ListTenantsResponse listTenants(int maxResults, String pageToken) builder.put("pageToken", pageToken); } - GenericUrl url = new GenericUrl(tenantMgtBaseUrl + "/tenants"); - url.putAll(builder.build()); - ListTenantsResponse response = httpClient.sendRequest( - "GET", url, null, ListTenantsResponse.class); - if (response == null) { - throw new FirebaseAuthException(AuthHttpClient.INTERNAL_ERROR, "Failed to retrieve tenants."); - } - return response; + HttpRequestInfo requestInfo = HttpRequestInfo.buildGetRequest(tenantMgtBaseUrl + "/tenants") + .addAllParameters(builder.build()); + return httpClient.sendRequest(requestInfo, ListTenantsResponse.class); } private static String getTenantUrlSuffix(String tenantId) { diff --git a/src/main/java/com/google/firebase/auth/multitenancy/ListTenantsPage.java b/src/main/java/com/google/firebase/auth/multitenancy/ListTenantsPage.java index c1f393ddb..5f9917bce 100644 --- a/src/main/java/com/google/firebase/auth/multitenancy/ListTenantsPage.java +++ b/src/main/java/com/google/firebase/auth/multitenancy/ListTenantsPage.java @@ -96,14 +96,14 @@ public ListTenantsPage getNextPage() { } /** - * Returns an {@link Iterable} that facilitates transparently iterating over all the tenants in + * Returns an {@code Iterable} that facilitates transparently iterating over all the tenants in * the current Firebase project, starting from this page. * - *

The {@link Iterator} instances produced by the returned {@link Iterable} never buffers more + *

The {@code Iterator} instances produced by the returned {@code Iterable} never buffers more * than one page of tenants at a time. It is safe to abandon the iterators (i.e. break the loops) * at any time. * - * @return a new {@link Iterable} instance. + * @return a new {@code Iterable} instance. */ @NonNull @Override @@ -112,9 +112,9 @@ public Iterable iterateAll() { } /** - * Returns an {@link Iterable} over the tenants in this page. + * Returns an {@code Iterable} over the tenants in this page. * - * @return a {@link Iterable} instance. + * @return a {@code Iterable} instance. */ @NonNull @Override @@ -137,7 +137,7 @@ public Iterator iterator() { } /** - * An {@link Iterator} that cycles through tenants, one at a time. + * An {@code Iterator} that cycles through tenants, one at a time. * *

It buffers the last retrieved batch of tenants in memory. The {@code maxResults} parameter * is an upper bound on the batch size. diff --git a/src/main/java/com/google/firebase/auth/multitenancy/TenantManager.java b/src/main/java/com/google/firebase/auth/multitenancy/TenantManager.java index dcb226d28..a30c0b884 100644 --- a/src/main/java/com/google/firebase/auth/multitenancy/TenantManager.java +++ b/src/main/java/com/google/firebase/auth/multitenancy/TenantManager.java @@ -54,8 +54,13 @@ public final class TenantManager { * @hide */ public TenantManager(FirebaseApp firebaseApp) { - this.firebaseApp = firebaseApp; - this.tenantClient = new FirebaseTenantClient(firebaseApp); + this(firebaseApp, new FirebaseTenantClient(firebaseApp)); + } + + @VisibleForTesting + TenantManager(FirebaseApp firebaseApp, FirebaseTenantClient tenantClient) { + this.firebaseApp = checkNotNull(firebaseApp); + this.tenantClient = checkNotNull(tenantClient); this.tenantAwareAuths = new HashMap<>(); } diff --git a/src/main/java/com/google/firebase/database/core/JvmAuthTokenProvider.java b/src/main/java/com/google/firebase/database/core/JvmAuthTokenProvider.java index a1cb79688..9b862ba86 100644 --- a/src/main/java/com/google/firebase/database/core/JvmAuthTokenProvider.java +++ b/src/main/java/com/google/firebase/database/core/JvmAuthTokenProvider.java @@ -112,7 +112,7 @@ private static class TokenChangeListenerWrapper implements CredentialsChangedLis } @Override - public void onChanged(OAuth2Credentials credentials) throws IOException { + public void onChanged(OAuth2Credentials credentials) { // When this event fires, it is guaranteed that credentials.getAccessToken() will return a // valid OAuth2 token. final AccessToken accessToken = credentials.getAccessToken(); diff --git a/src/main/java/com/google/firebase/iid/FirebaseInstanceId.java b/src/main/java/com/google/firebase/iid/FirebaseInstanceId.java index 7ac527778..822654402 100644 --- a/src/main/java/com/google/firebase/iid/FirebaseInstanceId.java +++ b/src/main/java/com/google/firebase/iid/FirebaseInstanceId.java @@ -17,29 +17,27 @@ package com.google.firebase.iid; import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; -import com.google.api.client.http.GenericUrl; -import com.google.api.client.http.HttpRequest; import com.google.api.client.http.HttpRequestFactory; -import com.google.api.client.http.HttpResponse; -import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.HttpResponseInterceptor; -import com.google.api.client.http.HttpTransport; -import com.google.api.client.json.JsonFactory; -import com.google.api.client.json.JsonObjectParser; import com.google.api.core.ApiFuture; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; import com.google.common.collect.ImmutableMap; -import com.google.common.io.ByteStreams; import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseException; import com.google.firebase.ImplFirebaseTrampolines; +import com.google.firebase.IncomingHttpResponse; +import com.google.firebase.database.annotations.Nullable; +import com.google.firebase.internal.AbstractHttpErrorHandler; +import com.google.firebase.internal.ApiClientUtils; import com.google.firebase.internal.CallableOperation; -import com.google.firebase.internal.FirebaseRequestInitializer; +import com.google.firebase.internal.ErrorHandlingHttpClient; import com.google.firebase.internal.FirebaseService; +import com.google.firebase.internal.HttpRequestInfo; import com.google.firebase.internal.NonNull; -import java.io.IOException; import java.util.Map; /** @@ -64,22 +62,30 @@ public class FirebaseInstanceId { .build(); private final FirebaseApp app; - private final HttpRequestFactory requestFactory; - private final JsonFactory jsonFactory; private final String projectId; - - private HttpResponseInterceptor interceptor; + private final ErrorHandlingHttpClient httpClient; private FirebaseInstanceId(FirebaseApp app) { - HttpTransport httpTransport = app.getOptions().getHttpTransport(); - this.app = app; - this.requestFactory = httpTransport.createRequestFactory(new FirebaseRequestInitializer(app)); - this.jsonFactory = app.getOptions().getJsonFactory(); - this.projectId = ImplFirebaseTrampolines.getProjectId(app); + this(app, null); + } + + @VisibleForTesting + FirebaseInstanceId(FirebaseApp app, @Nullable HttpRequestFactory requestFactory) { + this.app = checkNotNull(app, "app must not be null"); + String projectId = ImplFirebaseTrampolines.getProjectId(app); checkArgument(!Strings.isNullOrEmpty(projectId), "Project ID is required to access instance ID service. Use a service account credential or " + "set the project ID explicitly via FirebaseOptions. Alternatively you can also " + "set the project ID via the GOOGLE_CLOUD_PROJECT environment variable."); + this.projectId = projectId; + if (requestFactory == null) { + requestFactory = ApiClientUtils.newAuthorizedRequestFactory(app); + } + + this.httpClient = new ErrorHandlingHttpClient<>( + requestFactory, + app.getOptions().getJsonFactory(), + new InstanceIdErrorHandler()); } /** @@ -107,7 +113,7 @@ public static synchronized FirebaseInstanceId getInstance(FirebaseApp app) { @VisibleForTesting void setInterceptor(HttpResponseInterceptor interceptor) { - this.interceptor = interceptor; + httpClient.setInterceptor(interceptor); } /** @@ -146,42 +152,45 @@ private CallableOperation deleteInstanceIdOp( protected Void execute() throws FirebaseInstanceIdException { String url = String.format( "%s/project/%s/instanceId/%s", IID_SERVICE_URL, projectId, instanceId); - HttpResponse response = null; - try { - HttpRequest request = requestFactory.buildDeleteRequest(new GenericUrl(url)); - request.setParser(new JsonObjectParser(jsonFactory)); - request.setResponseInterceptor(interceptor); - response = request.execute(); - ByteStreams.exhaust(response.getContent()); - } catch (Exception e) { - handleError(instanceId, e); - } finally { - disconnectQuietly(response); - } + HttpRequestInfo request = HttpRequestInfo.buildDeleteRequest(url); + httpClient.send(request); return null; } }; } - private static void disconnectQuietly(HttpResponse response) { - if (response != null) { - try { - response.disconnect(); - } catch (IOException ignored) { - // ignored + private static class InstanceIdErrorHandler + extends AbstractHttpErrorHandler { + + @Override + protected FirebaseInstanceIdException createException(FirebaseException base) { + String message = base.getMessage(); + String customMessage = getCustomMessage(base); + if (!Strings.isNullOrEmpty(customMessage)) { + message = customMessage; } + + return new FirebaseInstanceIdException(base, message); } - } - private void handleError(String instanceId, Exception e) throws FirebaseInstanceIdException { - String msg = "Error while invoking instance ID service."; - if (e instanceof HttpResponseException) { - int statusCode = ((HttpResponseException) e).getStatusCode(); - if (ERROR_CODES.containsKey(statusCode)) { - msg = String.format("Instance ID \"%s\": %s", instanceId, ERROR_CODES.get(statusCode)); + private String getCustomMessage(FirebaseException base) { + IncomingHttpResponse response = base.getHttpResponse(); + if (response != null) { + String instanceId = extractInstanceId(response); + String description = ERROR_CODES.get(response.getStatusCode()); + if (description != null) { + return String.format("Instance ID \"%s\": %s", instanceId, description); + } } + + return null; + } + + private String extractInstanceId(IncomingHttpResponse response) { + String url = response.getRequest().getUrl(); + int index = url.lastIndexOf('/'); + return url.substring(index + 1); } - throw new FirebaseInstanceIdException(msg, e); } private static final String SERVICE_ID = FirebaseInstanceId.class.getName(); diff --git a/src/main/java/com/google/firebase/iid/FirebaseInstanceIdException.java b/src/main/java/com/google/firebase/iid/FirebaseInstanceIdException.java index dfe0087fc..482a23a3d 100644 --- a/src/main/java/com/google/firebase/iid/FirebaseInstanceIdException.java +++ b/src/main/java/com/google/firebase/iid/FirebaseInstanceIdException.java @@ -21,9 +21,9 @@ /** * Represents an exception encountered while interacting with the Firebase instance ID service. */ -public class FirebaseInstanceIdException extends FirebaseException { +public final class FirebaseInstanceIdException extends FirebaseException { - FirebaseInstanceIdException(String detailMessage, Throwable cause) { - super(detailMessage, cause); + FirebaseInstanceIdException(FirebaseException base, String message) { + super(base.getErrorCode(), message, base.getCause(), base.getHttpResponse()); } } diff --git a/src/main/java/com/google/firebase/internal/AbstractHttpErrorHandler.java b/src/main/java/com/google/firebase/internal/AbstractHttpErrorHandler.java new file mode 100644 index 000000000..4d1f6b26e --- /dev/null +++ b/src/main/java/com/google/firebase/internal/AbstractHttpErrorHandler.java @@ -0,0 +1,154 @@ +/* + * Copyright 2020 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.internal; + +import com.google.api.client.http.HttpResponseException; +import com.google.api.client.http.HttpStatusCodes; +import com.google.common.collect.ImmutableMap; +import com.google.firebase.ErrorCode; +import com.google.firebase.FirebaseException; +import com.google.firebase.IncomingHttpResponse; +import java.io.IOException; +import java.net.NoRouteToHostException; +import java.net.SocketTimeoutException; +import java.net.UnknownHostException; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * An abstract HttpErrorHandler implementation that maps HTTP status codes to Firebase error codes. + * Also provides reasonable default implementations to other error handler methods in the + * HttpErrorHandler interface. + */ +public abstract class AbstractHttpErrorHandler + implements HttpErrorHandler { + + private static final Map HTTP_ERROR_CODES = + ImmutableMap.builder() + .put(HttpStatusCodes.STATUS_CODE_BAD_REQUEST, ErrorCode.INVALID_ARGUMENT) + .put(HttpStatusCodes.STATUS_CODE_UNAUTHORIZED, ErrorCode.UNAUTHENTICATED) + .put(HttpStatusCodes.STATUS_CODE_FORBIDDEN, ErrorCode.PERMISSION_DENIED) + .put(HttpStatusCodes.STATUS_CODE_NOT_FOUND, ErrorCode.NOT_FOUND) + .put(HttpStatusCodes.STATUS_CODE_CONFLICT, ErrorCode.CONFLICT) + .put(429, ErrorCode.RESOURCE_EXHAUSTED) + .put(HttpStatusCodes.STATUS_CODE_SERVER_ERROR, ErrorCode.INTERNAL) + .put(HttpStatusCodes.STATUS_CODE_SERVICE_UNAVAILABLE, ErrorCode.UNAVAILABLE) + .build(); + + @Override + public final T handleHttpResponseException( + HttpResponseException e, IncomingHttpResponse response) { + FirebaseException base = this.httpResponseErrorToBaseException(e, response); + return this.createException(base); + } + + @Override + public final T handleIOException(IOException e) { + FirebaseException base = this.ioErrorToBaseException(e); + return this.createException(base); + } + + @Override + public final T handleParseException(IOException e, IncomingHttpResponse response) { + FirebaseException base = this.parseErrorToBaseException(e, response); + return this.createException(base); + } + + /** + * Creates a FirebaseException from the given HTTP response error. Error code is determined from + * the HTTP status code of the response. Error message includes both the status code and full + * response payload to aid in debugging. + * + * @param e HTTP response exception. + * @param response Incoming HTTP response. + * @return A FirebaseException instance. + */ + protected FirebaseException httpResponseErrorToBaseException( + HttpResponseException e, IncomingHttpResponse response) { + ErrorCode code = HTTP_ERROR_CODES.get(e.getStatusCode()); + if (code == null) { + code = ErrorCode.UNKNOWN; + } + + String message = String.format("Unexpected HTTP response with status: %d\n%s", + e.getStatusCode(), e.getContent()); + return new FirebaseException(code, message, e, response); + } + + /** + * Creates a FirebaseException from the given IOException. If IOException resulted from a socket + * timeout, sets the error code DEADLINE_EXCEEDED. If the IOException resulted from a network + * outage or other connectivity issue, sets the error code to UNAVAILABLE. In all other cases sets + * the error code to UNKNOWN. + * + * @param e IOException to create the new exception from. + * @return A FirebaseException instance. + */ + protected FirebaseException ioErrorToBaseException(IOException e) { + ErrorCode code = ErrorCode.UNKNOWN; + String message = "Unknown error while making a remote service call" ; + if (isInstance(e, SocketTimeoutException.class)) { + code = ErrorCode.DEADLINE_EXCEEDED; + message = "Timed out while making an API call"; + } + + if (isInstance(e, UnknownHostException.class) || isInstance(e, NoRouteToHostException.class)) { + code = ErrorCode.UNAVAILABLE; + message = "Failed to establish a connection"; + } + + return new FirebaseException(code, message + ": " + e.getMessage(), e); + } + + protected FirebaseException parseErrorToBaseException( + IOException e, IncomingHttpResponse response) { + return new FirebaseException( + ErrorCode.UNKNOWN, "Error while parsing HTTP response: " + e.getMessage(), e, response); + } + + /** + * Converts the given base FirebaseException to a more specific exception type. The base exception + * is guaranteed to have an error code, a message and a cause. But the HTTP response is only set + * if the exception occurred after receiving a response from a remote server. + * + * @param base A FirebaseException. + * @return A more specific exception created from the base. + */ + protected abstract T createException(FirebaseException base); + + /** + * Checks if the given exception stack t contains an instance of type. + */ + private boolean isInstance(IOException t, Class type) { + Throwable current = t; + Set chain = new HashSet<>(); + while (current != null) { + if (!chain.add(current)) { + break; + } + + if (type.isInstance(current)) { + return true; + } + + current = current.getCause(); + } + + return false; + } +} diff --git a/src/main/java/com/google/firebase/internal/AbstractPlatformErrorHandler.java b/src/main/java/com/google/firebase/internal/AbstractPlatformErrorHandler.java new file mode 100644 index 000000000..b0909e4f7 --- /dev/null +++ b/src/main/java/com/google/firebase/internal/AbstractPlatformErrorHandler.java @@ -0,0 +1,98 @@ +/* + * Copyright 2020 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.internal; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.api.client.http.HttpResponseException; +import com.google.api.client.json.JsonFactory; +import com.google.api.client.util.Key; +import com.google.common.base.Strings; +import com.google.firebase.ErrorCode; +import com.google.firebase.FirebaseException; +import com.google.firebase.IncomingHttpResponse; +import java.io.IOException; + +/** + * An abstract HttpErrorHandler that handles Google Cloud error responses. Format of these + * error responses are defined at https://cloud.google.com/apis/design/errors. + */ +public abstract class AbstractPlatformErrorHandler + extends AbstractHttpErrorHandler { + + protected final JsonFactory jsonFactory; + + public AbstractPlatformErrorHandler(JsonFactory jsonFactory) { + this.jsonFactory = checkNotNull(jsonFactory, "jsonFactory must not be null"); + } + + @Override + protected final FirebaseException httpResponseErrorToBaseException( + HttpResponseException e, IncomingHttpResponse response) { + FirebaseException base = super.httpResponseErrorToBaseException(e, response); + PlatformErrorResponse parsedError = this.parseErrorResponse(e.getContent()); + + ErrorCode code = base.getErrorCode(); + String status = parsedError.getStatus(); + if (!Strings.isNullOrEmpty(status)) { + code = Enum.valueOf(ErrorCode.class, parsedError.getStatus()); + } + + String message = parsedError.getMessage(); + if (Strings.isNullOrEmpty(message)) { + message = base.getMessage(); + } + + return new FirebaseException(code, message, e, response); + } + + private PlatformErrorResponse parseErrorResponse(String content) { + PlatformErrorResponse response = new PlatformErrorResponse(); + if (!Strings.isNullOrEmpty(content)) { + try { + jsonFactory.createJsonParser(content).parseAndClose(response); + } catch (IOException e) { + // Ignore any error that may occur while parsing the error response. The server + // may have responded with a non-json payload. Return an empty return value, and + // let the base class logic come into play. + } + } + + return response; + } + + public static class PlatformErrorResponse { + @Key("error") + private PlatformError error; + + String getStatus() { + return error != null ? error.status : null; + } + + String getMessage() { + return error != null ? error.message : null; + } + } + + public static class PlatformError { + @Key("status") + private String status; + + @Key("message") + private String message; + } +} diff --git a/src/main/java/com/google/firebase/internal/ApiClientUtils.java b/src/main/java/com/google/firebase/internal/ApiClientUtils.java index 36ccf5cc8..f2724196b 100644 --- a/src/main/java/com/google/firebase/internal/ApiClientUtils.java +++ b/src/main/java/com/google/firebase/internal/ApiClientUtils.java @@ -35,6 +35,8 @@ public class ApiClientUtils { .setMaxIntervalMillis(60 * 1000) .build(); + private ApiClientUtils() { } + /** * Creates a new {@code HttpRequestFactory} which provides authorization (OAuth2), timeouts and * automatic retries. diff --git a/src/main/java/com/google/firebase/internal/ErrorHandlingHttpClient.java b/src/main/java/com/google/firebase/internal/ErrorHandlingHttpClient.java new file mode 100644 index 000000000..5efdd0ec2 --- /dev/null +++ b/src/main/java/com/google/firebase/internal/ErrorHandlingHttpClient.java @@ -0,0 +1,144 @@ +/* + * Copyright 2020 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.internal; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.HttpRequestFactory; +import com.google.api.client.http.HttpResponse; +import com.google.api.client.http.HttpResponseException; +import com.google.api.client.http.HttpResponseInterceptor; +import com.google.api.client.json.JsonFactory; +import com.google.api.client.json.JsonParser; +import com.google.common.io.CharStreams; +import com.google.firebase.FirebaseException; +import com.google.firebase.IncomingHttpResponse; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; + +/** + * An HTTP client implementation that handles any errors that may occur during HTTP calls, and + * converts them into an instance of FirebaseException. + */ +public final class ErrorHandlingHttpClient { + + private final HttpRequestFactory requestFactory; + private final JsonFactory jsonFactory; + private final HttpErrorHandler errorHandler; + + private HttpResponseInterceptor interceptor; + + public ErrorHandlingHttpClient( + HttpRequestFactory requestFactory, + JsonFactory jsonFactory, + HttpErrorHandler errorHandler) { + this.requestFactory = checkNotNull(requestFactory, "requestFactory must not be null"); + this.jsonFactory = checkNotNull(jsonFactory, "jsonFactory must not be null"); + this.errorHandler = checkNotNull(errorHandler, "errorHandler must not be null"); + } + + public ErrorHandlingHttpClient setInterceptor(HttpResponseInterceptor interceptor) { + this.interceptor = interceptor; + return this; + } + + /** + * Sends the given HTTP request to the target endpoint, and parses the response while handling + * any errors that may occur along the way. + * + * @param requestInfo Outgoing request configuration. + * @param responseType Class to parse the response into. + * @param Parsed response type. + * @return Parsed response object. + * @throws T If any error occurs while making the request. + */ + public V sendAndParse(HttpRequestInfo requestInfo, Class responseType) throws T { + IncomingHttpResponse response = send(requestInfo); + return parse(response, responseType); + } + + /** + * Sends the given HTTP request to the target endpoint, and parses the response while handling + * any errors that may occur along the way. This method can be used when the response should + * be parsed into an instance of a private or protected class, which cannot be instantiated + * outside the call-site. + * + * @param requestInfo Outgoing request configuration. + * @param destination Object to parse the response into. + * @throws T If any error occurs while making the request. + */ + public void sendAndParse(HttpRequestInfo requestInfo, Object destination) throws T { + IncomingHttpResponse response = send(requestInfo); + parse(response, destination); + } + + public IncomingHttpResponse send(HttpRequestInfo requestInfo) throws T { + HttpRequest request = createHttpRequest(requestInfo); + + HttpResponse response = null; + try { + response = request.execute(); + // Read and buffer the content. Otherwise if a parse error occurs later, + // we lose the content stream. + String content = null; + InputStream stream = response.getContent(); + if (stream != null) { + // Stream is null when the response body is empty (e.g. 204 No Content responses). + content = CharStreams.toString(new InputStreamReader(stream, response.getContentCharset())); + } + + return new IncomingHttpResponse(response, content); + } catch (HttpResponseException e) { + throw errorHandler.handleHttpResponseException(e, new IncomingHttpResponse(e, request)); + } catch (IOException e) { + throw errorHandler.handleIOException(e); + } finally { + ApiClientUtils.disconnectQuietly(response); + } + } + + public V parse(IncomingHttpResponse response, Class responseType) throws T { + checkNotNull(responseType, "responseType must not be null"); + try { + JsonParser parser = jsonFactory.createJsonParser(response.getContent()); + return parser.parseAndClose(responseType); + } catch (IOException e) { + throw errorHandler.handleParseException(e, response); + } + } + + public void parse(IncomingHttpResponse response, Object destination) throws T { + try { + JsonParser parser = jsonFactory.createJsonParser(response.getContent()); + parser.parse(destination); + } catch (IOException e) { + throw errorHandler.handleParseException(e, response); + } + } + + private HttpRequest createHttpRequest(HttpRequestInfo requestInfo) throws T { + try { + return requestInfo.newHttpRequest(requestFactory, jsonFactory) + .setResponseInterceptor(interceptor); + } catch (IOException e) { + // Handle request initialization errors (credential loading and other config errors) + throw errorHandler.handleIOException(e); + } + } +} diff --git a/src/main/java/com/google/firebase/internal/FirebaseAppStore.java b/src/main/java/com/google/firebase/internal/FirebaseAppStore.java deleted file mode 100644 index 778295655..000000000 --- a/src/main/java/com/google/firebase/internal/FirebaseAppStore.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright 2017 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.internal; - -import com.google.common.annotations.VisibleForTesting; -import com.google.firebase.FirebaseApp; -import com.google.firebase.FirebaseOptions; - -import java.util.Collections; -import java.util.Set; -import java.util.concurrent.atomic.AtomicReference; - -/** No-op base class of FirebaseAppStore. */ -public class FirebaseAppStore { - - private static final AtomicReference sInstance = new AtomicReference<>(); - - FirebaseAppStore() {} - - @Nullable - public static FirebaseAppStore getInstance() { - return sInstance.get(); - } - - // TODO: reenable persistence. See b/28158809. - public static FirebaseAppStore initialize() { - sInstance.compareAndSet(null /* expected */, new FirebaseAppStore()); - return sInstance.get(); - } - - /** - * @hide - */ - public static void setInstanceForTest(FirebaseAppStore firebaseAppStore) { - sInstance.set(firebaseAppStore); - } - - @VisibleForTesting - public static void clearInstanceForTest() { - FirebaseAppStore instance = sInstance.get(); - if (instance != null) { - instance.resetStore(); - } - sInstance.set(null); - } - - /** The returned set is mutable. */ - public Set getAllPersistedAppNames() { - return Collections.emptySet(); - } - - public void persistApp(@NonNull FirebaseApp app) {} - - public void removeApp(@NonNull String name) {} - - /** - * @return The restored {@link FirebaseOptions}, or null if it doesn't exist. - */ - public FirebaseOptions restoreAppOptions(@NonNull String name) { - return null; - } - - protected void resetStore() {} -} diff --git a/src/main/java/com/google/firebase/internal/HttpErrorHandler.java b/src/main/java/com/google/firebase/internal/HttpErrorHandler.java new file mode 100644 index 000000000..988b45a45 --- /dev/null +++ b/src/main/java/com/google/firebase/internal/HttpErrorHandler.java @@ -0,0 +1,44 @@ +/* + * Copyright 2020 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.internal; + +import com.google.api.client.http.HttpResponseException; +import com.google.firebase.FirebaseException; +import com.google.firebase.IncomingHttpResponse; +import java.io.IOException; + +/** + * An interface for handling all sorts of exceptions that may occur while making an HTTP call and + * converting them into some instance of FirebaseException. + */ +public interface HttpErrorHandler { + + /** + * Handle any low-level transport and initialization errors. + */ + T handleIOException(IOException e); + + /** + * Handle HTTP response exceptions (caused by HTTP error responses). + */ + T handleHttpResponseException(HttpResponseException e, IncomingHttpResponse response); + + /** + * Handle any errors that may occur while parsing the response payload. + */ + T handleParseException(IOException e, IncomingHttpResponse response); +} diff --git a/src/main/java/com/google/firebase/internal/HttpRequestInfo.java b/src/main/java/com/google/firebase/internal/HttpRequestInfo.java new file mode 100644 index 000000000..375e332fb --- /dev/null +++ b/src/main/java/com/google/firebase/internal/HttpRequestInfo.java @@ -0,0 +1,132 @@ +/* + * Copyright 2020 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.internal; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.api.client.http.GenericUrl; +import com.google.api.client.http.HttpContent; +import com.google.api.client.http.HttpMethods; +import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.HttpRequestFactory; +import com.google.api.client.http.json.JsonHttpContent; +import com.google.api.client.json.JsonFactory; +import com.google.common.base.Strings; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +/** + * Internal API for configuring outgoing HTTP requests. To be used with the + * {@link ErrorHandlingHttpClient} class. + */ +public final class HttpRequestInfo { + + private final String method; + private final GenericUrl url; + private final HttpContent content; + private final Object jsonContent; + private final Map headers = new HashMap<>(); + + private HttpRequestInfo(String method, GenericUrl url, HttpContent content, Object jsonContent) { + checkArgument(!Strings.isNullOrEmpty(method), "method must not be null"); + this.method = method; + this.url = checkNotNull(url, "url must not be null"); + this.content = content; + this.jsonContent = jsonContent; + } + + public HttpRequestInfo addHeader(String name, String value) { + this.headers.put(name, value); + return this; + } + + public HttpRequestInfo addAllHeaders(Map headers) { + this.headers.putAll(headers); + return this; + } + + public HttpRequestInfo addParameter(String name, Object value) { + this.url.put(name, value); + return this; + } + + public HttpRequestInfo addAllParameters(Map params) { + this.url.putAll(params); + return this; + } + + public static HttpRequestInfo buildGetRequest(String url) { + return buildRequest(HttpMethods.GET, url, null); + } + + public static HttpRequestInfo buildDeleteRequest(String url) { + return buildRequest(HttpMethods.DELETE, url, null); + } + + public static HttpRequestInfo buildRequest( + String method, String url, @Nullable HttpContent content) { + return new HttpRequestInfo(method, new GenericUrl(url), content, null); + } + + public static HttpRequestInfo buildJsonPostRequest(String url, @Nullable Object content) { + return buildJsonRequest(HttpMethods.POST, url, content); + } + + public static HttpRequestInfo buildJsonPatchRequest(String url, @Nullable Object content) { + return buildJsonRequest(HttpMethods.PATCH, url, content); + } + + public static HttpRequestInfo buildJsonRequest( + String method, String url, @Nullable Object content) { + return new HttpRequestInfo(method, new GenericUrl(url), null, content); + } + + HttpRequest newHttpRequest( + HttpRequestFactory factory, JsonFactory jsonFactory) throws IOException { + HttpRequest request; + HttpContent httpContent = getContent(jsonFactory); + if (factory.getTransport().supportsMethod(method)) { + request = factory.buildRequest(method, url, httpContent); + } else { + // Some HttpTransport implementations (notably NetHttpTransport) don't support new methods + // like PATCH. We try to emulate such requests over POST by setting the method override + // header, which is recognized by most Google backend APIs. + request = factory.buildPostRequest(url, httpContent); + request.getHeaders().set("X-HTTP-Method-Override", method); + } + + for (Map.Entry entry : headers.entrySet()) { + request.getHeaders().set(entry.getKey(), entry.getValue()); + } + + return request; + } + + private HttpContent getContent(JsonFactory jsonFactory) { + if (content != null) { + return content; + } + + if (jsonContent != null) { + return new JsonHttpContent(jsonFactory, jsonContent); + } + + return null; + } +} diff --git a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java index ee25957b3..e1b4f794c 100644 --- a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java +++ b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java @@ -41,10 +41,6 @@ */ public class FirebaseMessaging { - static final String INTERNAL_ERROR = "internal-error"; - - static final String UNKNOWN_ERROR = "unknown-error"; - private final FirebaseApp app; private final Supplier messagingClient; private final Supplier instanceIdClient; diff --git a/src/main/java/com/google/firebase/messaging/FirebaseMessagingClientImpl.java b/src/main/java/com/google/firebase/messaging/FirebaseMessagingClientImpl.java index 53d5ab00b..43b9b340b 100644 --- a/src/main/java/com/google/firebase/messaging/FirebaseMessagingClientImpl.java +++ b/src/main/java/com/google/firebase/messaging/FirebaseMessagingClientImpl.java @@ -21,26 +21,33 @@ import com.google.api.client.googleapis.batch.BatchCallback; import com.google.api.client.googleapis.batch.BatchRequest; +import com.google.api.client.googleapis.services.json.AbstractGoogleJsonClient; import com.google.api.client.http.GenericUrl; import com.google.api.client.http.HttpHeaders; +import com.google.api.client.http.HttpMethods; import com.google.api.client.http.HttpRequest; import com.google.api.client.http.HttpRequestFactory; import com.google.api.client.http.HttpRequestInitializer; -import com.google.api.client.http.HttpResponse; import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.HttpResponseInterceptor; +import com.google.api.client.http.HttpTransport; import com.google.api.client.http.json.JsonHttpContent; import com.google.api.client.json.JsonFactory; import com.google.api.client.json.JsonObjectParser; -import com.google.api.client.json.JsonParser; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.firebase.ErrorCode; import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseException; import com.google.firebase.ImplFirebaseTrampolines; +import com.google.firebase.IncomingHttpResponse; +import com.google.firebase.OutgoingHttpRequest; +import com.google.firebase.internal.AbstractPlatformErrorHandler; import com.google.firebase.internal.ApiClientUtils; -import com.google.firebase.internal.Nullable; +import com.google.firebase.internal.ErrorHandlingHttpClient; +import com.google.firebase.internal.HttpRequestInfo; import com.google.firebase.internal.SdkUtils; import com.google.firebase.messaging.internal.MessagingServiceErrorResponse; import com.google.firebase.messaging.internal.MessagingServiceResponse; @@ -55,37 +62,19 @@ final class FirebaseMessagingClientImpl implements FirebaseMessagingClient { private static final String FCM_URL = "https://fcm.googleapis.com/v1/projects/%s/messages:send"; - private static final String FCM_BATCH_URL = "https://fcm.googleapis.com/batch"; - - private static final String API_FORMAT_VERSION_HEADER = "X-GOOG-API-FORMAT-VERSION"; - - private static final String CLIENT_VERSION_HEADER = "X-Firebase-Client"; - - private static final Map FCM_ERROR_CODES = - ImmutableMap.builder() - // FCM v1 canonical error codes - .put("NOT_FOUND", "registration-token-not-registered") - .put("PERMISSION_DENIED", "mismatched-credential") - .put("RESOURCE_EXHAUSTED", "message-rate-exceeded") - .put("UNAUTHENTICATED", "third-party-auth-error") - - // FCM v1 new error codes - .put("APNS_AUTH_ERROR", "third-party-auth-error") - .put("INTERNAL", FirebaseMessaging.INTERNAL_ERROR) - .put("INVALID_ARGUMENT", "invalid-argument") - .put("QUOTA_EXCEEDED", "message-rate-exceeded") - .put("SENDER_ID_MISMATCH", "mismatched-credential") - .put("THIRD_PARTY_AUTH_ERROR", "third-party-auth-error") - .put("UNAVAILABLE", "server-unavailable") - .put("UNREGISTERED", "registration-token-not-registered") - .build(); + private static final Map COMMON_HEADERS = + ImmutableMap.of( + "X-GOOG-API-FORMAT-VERSION", "2", + "X-Firebase-Client", "fire-admin-java/" + SdkUtils.getVersion()); private final String fcmSendUrl; private final HttpRequestFactory requestFactory; private final HttpRequestFactory childRequestFactory; private final JsonFactory jsonFactory; private final HttpResponseInterceptor responseInterceptor; - private final String clientVersion = "fire-admin-java/" + SdkUtils.getVersion(); + private final MessagingErrorHandler errorHandler; + private final ErrorHandlingHttpClient httpClient; + private final MessagingBatchClient batchClient; private FirebaseMessagingClientImpl(Builder builder) { checkArgument(!Strings.isNullOrEmpty(builder.projectId)); @@ -94,6 +83,10 @@ private FirebaseMessagingClientImpl(Builder builder) { this.childRequestFactory = checkNotNull(builder.childRequestFactory); this.jsonFactory = checkNotNull(builder.jsonFactory); this.responseInterceptor = builder.responseInterceptor; + this.errorHandler = new MessagingErrorHandler(this.jsonFactory); + this.httpClient = new ErrorHandlingHttpClient<>(requestFactory, jsonFactory, errorHandler) + .setInterceptor(responseInterceptor); + this.batchClient = new MessagingBatchClient(requestFactory.getTransport(), jsonFactory); } @VisibleForTesting @@ -116,67 +109,48 @@ JsonFactory getJsonFactory() { return jsonFactory; } - @VisibleForTesting - String getClientVersion() { - return clientVersion; - } - public String send(Message message, boolean dryRun) throws FirebaseMessagingException { - try { - return sendSingleRequest(message, dryRun); - } catch (HttpResponseException e) { - throw createExceptionFromResponse(e); - } catch (IOException e) { - throw new FirebaseMessagingException( - FirebaseMessaging.INTERNAL_ERROR, "Error while calling FCM backend service", e); - } + return sendSingleRequest(message, dryRun); } public BatchResponse sendAll( List messages, boolean dryRun) throws FirebaseMessagingException { - try { - return sendBatchRequest(messages, dryRun); - } catch (HttpResponseException e) { - throw createExceptionFromResponse(e); - } catch (IOException e) { - throw new FirebaseMessagingException( - FirebaseMessaging.INTERNAL_ERROR, "Error while calling FCM backend service", e); - } + return sendBatchRequest(messages, dryRun); } - private String sendSingleRequest(Message message, boolean dryRun) throws IOException { - HttpRequest request = requestFactory.buildPostRequest( - new GenericUrl(fcmSendUrl), - new JsonHttpContent(jsonFactory, message.wrapForTransport(dryRun))); - setCommonFcmHeaders(request.getHeaders()); - request.setParser(new JsonObjectParser(jsonFactory)); - request.setResponseInterceptor(responseInterceptor); - HttpResponse response = request.execute(); - try { - MessagingServiceResponse parsed = new MessagingServiceResponse(); - jsonFactory.createJsonParser(response.getContent()).parseAndClose(parsed); - return parsed.getMessageId(); - } finally { - ApiClientUtils.disconnectQuietly(response); - } + private String sendSingleRequest( + Message message, boolean dryRun) throws FirebaseMessagingException { + HttpRequestInfo request = + HttpRequestInfo.buildJsonPostRequest( + fcmSendUrl, message.wrapForTransport(dryRun)) + .addAllHeaders(COMMON_HEADERS); + MessagingServiceResponse parsed = httpClient.sendAndParse( + request, MessagingServiceResponse.class); + return parsed.getMessageId(); } private BatchResponse sendBatchRequest( - List messages, boolean dryRun) throws IOException { + List messages, boolean dryRun) throws FirebaseMessagingException { MessagingBatchCallback callback = new MessagingBatchCallback(); - BatchRequest batch = newBatchRequest(messages, dryRun, callback); - batch.execute(); - return new BatchResponseImpl(callback.getResponses()); + try { + BatchRequest batch = newBatchRequest(messages, dryRun, callback); + batch.execute(); + return new BatchResponseImpl(callback.getResponses()); + } catch (HttpResponseException e) { + OutgoingHttpRequest req = new OutgoingHttpRequest( + HttpMethods.POST, MessagingBatchClient.FCM_BATCH_URL); + IncomingHttpResponse resp = new IncomingHttpResponse(e, req); + throw errorHandler.handleHttpResponseException(e, resp); + } catch (IOException e) { + throw errorHandler.handleIOException(e); + } } private BatchRequest newBatchRequest( List messages, boolean dryRun, MessagingBatchCallback callback) throws IOException { - BatchRequest batch = new BatchRequest( - requestFactory.getTransport(), getBatchRequestInitializer()); - batch.setBatchUrl(new GenericUrl(FCM_BATCH_URL)); - + BatchRequest batch = batchClient.batch(getBatchRequestInitializer()); final JsonObjectParser jsonParser = new JsonObjectParser(this.jsonFactory); final GenericUrl sendUrl = new GenericUrl(fcmSendUrl); for (Message message : messages) { @@ -186,36 +160,19 @@ private BatchRequest newBatchRequest( sendUrl, new JsonHttpContent(jsonFactory, message.wrapForTransport(dryRun))); request.setParser(jsonParser); - setCommonFcmHeaders(request.getHeaders()); + request.getHeaders().putAll(COMMON_HEADERS); batch.queue( request, MessagingServiceResponse.class, MessagingServiceErrorResponse.class, callback); } return batch; } - private void setCommonFcmHeaders(HttpHeaders headers) { - headers.set(API_FORMAT_VERSION_HEADER, "2"); - headers.set(CLIENT_VERSION_HEADER, clientVersion); - } - - private FirebaseMessagingException createExceptionFromResponse(HttpResponseException e) { - MessagingServiceErrorResponse response = new MessagingServiceErrorResponse(); - if (e.getContent() != null) { - try { - JsonParser parser = jsonFactory.createJsonParser(e.getContent()); - parser.parseAndClose(response); - } catch (IOException ignored) { - // ignored - } - } - - return newException(response, e); - } - private HttpRequestInitializer getBatchRequestInitializer() { return new HttpRequestInitializer() { @Override public void initialize(HttpRequest request) throws IOException { + // Batch requests are not executed on the ErrorHandlingHttpClient. Therefore, they + // require some special handling at initialization. HttpRequestInitializer initializer = requestFactory.getInitializer(); if (initializer != null) { initializer.initialize(request); @@ -283,30 +240,6 @@ FirebaseMessagingClientImpl build() { } } - private static FirebaseMessagingException newException(MessagingServiceErrorResponse response) { - return newException(response, null); - } - - private static FirebaseMessagingException newException( - MessagingServiceErrorResponse response, @Nullable HttpResponseException e) { - String code = FCM_ERROR_CODES.get(response.getErrorCode()); - if (code == null) { - code = FirebaseMessaging.UNKNOWN_ERROR; - } - - String msg = response.getErrorMessage(); - if (Strings.isNullOrEmpty(msg)) { - if (e != null) { - msg = String.format("Unexpected HTTP response with status: %d; body: %s", - e.getStatusCode(), e.getContent()); - } else { - msg = String.format("Unexpected HTTP response: %s", response.toString()); - } - } - - return new FirebaseMessagingException(code, msg, e); - } - private static class MessagingBatchCallback implements BatchCallback { @@ -319,13 +252,97 @@ public void onSuccess( } @Override - public void onFailure( - MessagingServiceErrorResponse error, HttpHeaders responseHeaders) { - responses.add(SendResponse.fromException(newException(error))); + public void onFailure(MessagingServiceErrorResponse error, HttpHeaders responseHeaders) { + // We only specify error codes and message for these partial failures. Recall that these + // exceptions are never actually thrown, but only made accessible via SendResponse. + FirebaseException base = createFirebaseException(error); + FirebaseMessagingException exception = FirebaseMessagingException.withMessagingErrorCode( + base, error.getMessagingErrorCode()); + responses.add(SendResponse.fromException(exception)); } List getResponses() { return this.responses.build(); } + + private FirebaseException createFirebaseException(MessagingServiceErrorResponse error) { + String status = error.getStatus(); + ErrorCode errorCode = Strings.isNullOrEmpty(status) + ? ErrorCode.UNKNOWN : Enum.valueOf(ErrorCode.class, status); + + String msg = error.getErrorMessage(); + if (Strings.isNullOrEmpty(msg)) { + msg = String.format("Unexpected HTTP response: %s", error.toString()); + } + + return new FirebaseException(errorCode, msg, null); + } + } + + private static class MessagingErrorHandler + extends AbstractPlatformErrorHandler { + + private MessagingErrorHandler(JsonFactory jsonFactory) { + super(jsonFactory); + } + + @Override + protected FirebaseMessagingException createException(FirebaseException base) { + String response = getResponse(base); + MessagingServiceErrorResponse parsed = safeParse(response); + return FirebaseMessagingException.withMessagingErrorCode( + base, parsed.getMessagingErrorCode()); + } + + private String getResponse(FirebaseException base) { + if (base.getHttpResponse() == null) { + return null; + } + + return base.getHttpResponse().getContent(); + } + + private MessagingServiceErrorResponse safeParse(String response) { + if (!Strings.isNullOrEmpty(response)) { + try { + return jsonFactory.createJsonParser(response) + .parseAndClose(MessagingServiceErrorResponse.class); + } catch (IOException ignore) { + // Ignore any error that may occur while parsing the error response. The server + // may have responded with a non-json payload. + } + } + + return new MessagingServiceErrorResponse(); + } + } + + private static class MessagingBatchClient extends AbstractGoogleJsonClient { + + private static final String FCM_ROOT_URL = "https://fcm.googleapis.com"; + private static final String FCM_BATCH_PATH = "batch"; + private static final String FCM_BATCH_URL = String.format( + "%s/%s", FCM_ROOT_URL, FCM_BATCH_PATH); + + MessagingBatchClient(HttpTransport transport, JsonFactory jsonFactory) { + super(new Builder(transport, jsonFactory)); + } + + private MessagingBatchClient(Builder builder) { + super(builder); + } + + private static class Builder extends AbstractGoogleJsonClient.Builder { + Builder(HttpTransport transport, JsonFactory jsonFactory) { + super(transport, jsonFactory, FCM_ROOT_URL, "", null, false); + setBatchPath(FCM_BATCH_PATH); + setApplicationName("fire-admin-java"); + } + + @Override + public AbstractGoogleJsonClient build() { + return new MessagingBatchClient(this); + } + } } } diff --git a/src/main/java/com/google/firebase/messaging/FirebaseMessagingException.java b/src/main/java/com/google/firebase/messaging/FirebaseMessagingException.java index 5f57474ea..a3d92788a 100644 --- a/src/main/java/com/google/firebase/messaging/FirebaseMessagingException.java +++ b/src/main/java/com/google/firebase/messaging/FirebaseMessagingException.java @@ -16,26 +16,54 @@ package com.google.firebase.messaging; -import static com.google.common.base.Preconditions.checkArgument; - -import com.google.common.base.Strings; +import com.google.common.annotations.VisibleForTesting; +import com.google.firebase.ErrorCode; import com.google.firebase.FirebaseException; +import com.google.firebase.IncomingHttpResponse; import com.google.firebase.internal.NonNull; +import com.google.firebase.internal.Nullable; + +public final class FirebaseMessagingException extends FirebaseException { -public class FirebaseMessagingException extends FirebaseException { + private final MessagingErrorCode errorCode; - private final String errorCode; + @VisibleForTesting + FirebaseMessagingException(@NonNull ErrorCode code, @NonNull String message) { + this(code, message, null, null, null); + } - FirebaseMessagingException(String errorCode, String message, Throwable cause) { - super(message, cause); - checkArgument(!Strings.isNullOrEmpty(errorCode)); + private FirebaseMessagingException( + @NonNull ErrorCode code, + @NonNull String message, + @Nullable Throwable cause, + @Nullable IncomingHttpResponse response, + @Nullable MessagingErrorCode errorCode) { + super(code, message, cause, response); this.errorCode = errorCode; } + static FirebaseMessagingException withMessagingErrorCode( + FirebaseException base, @Nullable MessagingErrorCode errorCode) { + return new FirebaseMessagingException( + base.getErrorCode(), + base.getMessage(), + base.getCause(), + base.getHttpResponse(), + errorCode); + } + + static FirebaseMessagingException withCustomMessage(FirebaseException base, String message) { + return new FirebaseMessagingException( + base.getErrorCode(), + message, + base.getCause(), + base.getHttpResponse(), + null); + } /** Returns an error code that may provide more information about the error. */ - @NonNull - public String getErrorCode() { + @Nullable + public MessagingErrorCode getMessagingErrorCode() { return errorCode; } } diff --git a/src/main/java/com/google/firebase/messaging/InstanceIdClientImpl.java b/src/main/java/com/google/firebase/messaging/InstanceIdClientImpl.java index 15f8158a5..5648fcf0c 100644 --- a/src/main/java/com/google/firebase/messaging/InstanceIdClientImpl.java +++ b/src/main/java/com/google/firebase/messaging/InstanceIdClientImpl.java @@ -16,27 +16,20 @@ package com.google.firebase.messaging; -import static com.google.common.base.Preconditions.checkNotNull; - -import com.google.api.client.http.GenericUrl; -import com.google.api.client.http.HttpRequest; import com.google.api.client.http.HttpRequestFactory; -import com.google.api.client.http.HttpResponse; -import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.HttpResponseInterceptor; -import com.google.api.client.http.json.JsonHttpContent; import com.google.api.client.json.GenericJson; import com.google.api.client.json.JsonFactory; -import com.google.api.client.json.JsonObjectParser; -import com.google.api.client.json.JsonParser; import com.google.api.client.util.Key; -import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; import com.google.common.collect.ImmutableMap; import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseException; +import com.google.firebase.internal.AbstractHttpErrorHandler; import com.google.firebase.internal.ApiClientUtils; +import com.google.firebase.internal.ErrorHandlingHttpClient; +import com.google.firebase.internal.HttpRequestInfo; import com.google.firebase.internal.Nullable; - import java.io.IOException; import java.util.List; import java.util.Map; @@ -53,18 +46,7 @@ final class InstanceIdClientImpl implements InstanceIdClient { private static final String IID_UNSUBSCRIBE_PATH = "iid/v1:batchRemove"; - static final Map IID_ERROR_CODES = - ImmutableMap.builder() - .put(400, "invalid-argument") - .put(401, "authentication-error") - .put(403, "authentication-error") - .put(500, FirebaseMessaging.INTERNAL_ERROR) - .put(503, "server-unavailable") - .build(); - - private final HttpRequestFactory requestFactory; - private final JsonFactory jsonFactory; - private final HttpResponseInterceptor responseInterceptor; + private final ErrorHandlingHttpClient requestFactory; InstanceIdClientImpl(HttpRequestFactory requestFactory, JsonFactory jsonFactory) { this(requestFactory, jsonFactory, null); @@ -74,9 +56,9 @@ final class InstanceIdClientImpl implements InstanceIdClient { HttpRequestFactory requestFactory, JsonFactory jsonFactory, @Nullable HttpResponseInterceptor responseInterceptor) { - this.requestFactory = checkNotNull(requestFactory); - this.jsonFactory = checkNotNull(jsonFactory); - this.responseInterceptor = responseInterceptor; + InstanceIdErrorHandler errorHandler = new InstanceIdErrorHandler(jsonFactory); + this.requestFactory = new ErrorHandlingHttpClient<>(requestFactory, jsonFactory, errorHandler) + .setInterceptor(responseInterceptor); } static InstanceIdClientImpl fromApp(FirebaseApp app) { @@ -85,76 +67,32 @@ static InstanceIdClientImpl fromApp(FirebaseApp app) { app.getOptions().getJsonFactory()); } - @VisibleForTesting - HttpRequestFactory getRequestFactory() { - return requestFactory; - } - - @VisibleForTesting - JsonFactory getJsonFactory() { - return jsonFactory; - } - public TopicManagementResponse subscribeToTopic( String topic, List registrationTokens) throws FirebaseMessagingException { - try { - return sendInstanceIdRequest(topic, registrationTokens, IID_SUBSCRIBE_PATH); - } catch (HttpResponseException e) { - throw createExceptionFromResponse(e); - } catch (IOException e) { - throw new FirebaseMessagingException( - FirebaseMessaging.INTERNAL_ERROR, "Error while calling IID backend service", e); - } + return sendInstanceIdRequest(topic, registrationTokens, IID_SUBSCRIBE_PATH); } public TopicManagementResponse unsubscribeFromTopic( String topic, List registrationTokens) throws FirebaseMessagingException { - try { - return sendInstanceIdRequest(topic, registrationTokens, IID_UNSUBSCRIBE_PATH); - } catch (HttpResponseException e) { - throw createExceptionFromResponse(e); - } catch (IOException e) { - throw new FirebaseMessagingException( - FirebaseMessaging.INTERNAL_ERROR, "Error while calling IID backend service", e); - } + return sendInstanceIdRequest(topic, registrationTokens, IID_UNSUBSCRIBE_PATH); } private TopicManagementResponse sendInstanceIdRequest( - String topic, List registrationTokens, String path) throws IOException { + String topic, + List registrationTokens, + String path) throws FirebaseMessagingException { + String url = String.format("%s/%s", IID_HOST, path); Map payload = ImmutableMap.of( "to", getPrefixedTopic(topic), "registration_tokens", registrationTokens ); - HttpResponse response = null; - try { - HttpRequest request = requestFactory.buildPostRequest( - new GenericUrl(url), new JsonHttpContent(jsonFactory, payload)); - request.getHeaders().set("access_token_auth", "true"); - request.setParser(new JsonObjectParser(jsonFactory)); - request.setResponseInterceptor(responseInterceptor); - response = request.execute(); - - JsonParser parser = jsonFactory.createJsonParser(response.getContent()); - InstanceIdServiceResponse parsedResponse = new InstanceIdServiceResponse(); - parser.parse(parsedResponse); - return new TopicManagementResponse(parsedResponse.results); - } finally { - ApiClientUtils.disconnectQuietly(response); - } - } - private FirebaseMessagingException createExceptionFromResponse(HttpResponseException e) { - InstanceIdServiceErrorResponse response = new InstanceIdServiceErrorResponse(); - if (e.getContent() != null) { - try { - JsonParser parser = jsonFactory.createJsonParser(e.getContent()); - parser.parseAndClose(response); - } catch (IOException ignored) { - // ignored - } - } - return newException(response, e); + HttpRequestInfo request = HttpRequestInfo.buildJsonPostRequest(url, payload) + .addHeader("access_token_auth", "true"); + InstanceIdServiceResponse response = new InstanceIdServiceResponse(); + requestFactory.sendAndParse(request, response); + return new TopicManagementResponse(response.results); } private String getPrefixedTopic(String topic) { @@ -165,21 +103,6 @@ private String getPrefixedTopic(String topic) { } } - private static FirebaseMessagingException newException( - InstanceIdServiceErrorResponse response, HttpResponseException e) { - // Infer error code from HTTP status - String code = IID_ERROR_CODES.get(e.getStatusCode()); - if (code == null) { - code = FirebaseMessaging.UNKNOWN_ERROR; - } - String msg = response.error; - if (Strings.isNullOrEmpty(msg)) { - msg = String.format("Unexpected HTTP response with status: %d; body: %s", - e.getStatusCode(), e.getContent()); - } - return new FirebaseMessagingException(code, msg, e); - } - private static class InstanceIdServiceResponse { @Key("results") private List results; @@ -189,4 +112,54 @@ private static class InstanceIdServiceErrorResponse { @Key("error") private String error; } + + private static class InstanceIdErrorHandler + extends AbstractHttpErrorHandler { + + private final JsonFactory jsonFactory; + + InstanceIdErrorHandler(JsonFactory jsonFactory) { + this.jsonFactory = jsonFactory; + } + + @Override + protected FirebaseMessagingException createException(FirebaseException base) { + String message = getCustomMessage(base); + return FirebaseMessagingException.withCustomMessage(base, message); + } + + private String getCustomMessage(FirebaseException base) { + String response = getResponse(base); + InstanceIdServiceErrorResponse parsed = safeParse(response); + if (!Strings.isNullOrEmpty(parsed.error)) { + return "Error while calling the IID service: " + parsed.error; + } + + return base.getMessage(); + } + + private String getResponse(FirebaseException base) { + if (base.getHttpResponse() == null) { + return null; + } + + return base.getHttpResponse().getContent(); + } + + private InstanceIdServiceErrorResponse safeParse(String response) { + InstanceIdServiceErrorResponse parsed = new InstanceIdServiceErrorResponse(); + if (!Strings.isNullOrEmpty(response)) { + // Parse the error response from the IID service. + // Sample response: {"error": "error message text"} + try { + jsonFactory.createJsonParser(response).parse(parsed); + } catch (IOException ignore) { + // Ignore any error that may occur while parsing the error response. The server + // may have responded with a non-json payload. + } + } + + return parsed; + } + } } diff --git a/src/main/java/com/google/firebase/messaging/LightSettings.java b/src/main/java/com/google/firebase/messaging/LightSettings.java index 75a692090..e0e898374 100644 --- a/src/main/java/com/google/firebase/messaging/LightSettings.java +++ b/src/main/java/com/google/firebase/messaging/LightSettings.java @@ -61,7 +61,7 @@ private Builder() {} /** * Sets the lightSettingsColor value with a string. * - * @param lightSettingsColor LightSettingsColor specified in the {@code #rrggbb} format. + * @param color LightSettingsColor specified in the {@code #rrggbb} format. * @return This builder. */ public Builder setColorFromString(String color) { @@ -72,7 +72,7 @@ public Builder setColorFromString(String color) { /** * Sets the lightSettingsColor value in the light settings. * - * @param lightSettingsColor Color to be used in the light settings. + * @param color Color to be used in the light settings. * @return This builder. */ public Builder setColor(LightSettingsColor color) { diff --git a/src/main/java/com/google/firebase/messaging/MessagingErrorCode.java b/src/main/java/com/google/firebase/messaging/MessagingErrorCode.java new file mode 100644 index 000000000..b566befbd --- /dev/null +++ b/src/main/java/com/google/firebase/messaging/MessagingErrorCode.java @@ -0,0 +1,43 @@ +package com.google.firebase.messaging; + +/** + * Error codes that can be raised by the Cloud Messaging APIs. + */ +public enum MessagingErrorCode { + + /** + * APNs certificate or web push auth key was invalid or missing. + */ + THIRD_PARTY_AUTH_ERROR, + + /** + * One or more arguments specified in the request were invalid. + */ + INVALID_ARGUMENT, + + /** + * Internal server error. + */ + INTERNAL, + + /** + * Sending limit exceeded for the message target. + */ + QUOTA_EXCEEDED, + + /** + * The authenticated sender ID is different from the sender ID for the registration token. + */ + SENDER_ID_MISMATCH, + + /** + * Cloud Messaging service is temporarily unavailable. + */ + UNAVAILABLE, + + /** + * App instance was unregistered from FCM. This usually means that the token used is no longer + * valid and a new one must be used. + */ + UNREGISTERED, +} diff --git a/src/main/java/com/google/firebase/messaging/Notification.java b/src/main/java/com/google/firebase/messaging/Notification.java index d9b2034ee..a8f48c2ef 100644 --- a/src/main/java/com/google/firebase/messaging/Notification.java +++ b/src/main/java/com/google/firebase/messaging/Notification.java @@ -33,33 +33,6 @@ public class Notification { @Key("image") private final String image; - /** - * Creates a new {@code Notification} using the given title and body. - * - * @param title Title of the notification. - * @param body Body of the notification. - * - * @deprecated Use {@link #Notification(Builder)} instead. - */ - public Notification(String title, String body) { - this(title, body, null); - } - - /** - * Creates a new {@code Notification} using the given title, body, and image. - * - * @param title Title of the notification. - * @param body Body of the notification. - * @param imageUrl URL of the image that is going to be displayed in the notification. - * - * @deprecated Use {@link #Notification(Builder)} instead. - */ - public Notification(String title, String body, String imageUrl) { - this.title = title; - this.body = body; - this.image = imageUrl; - } - private Notification(Builder builder) { this.title = builder.title; this.body = builder.body; @@ -67,9 +40,9 @@ private Notification(Builder builder) { } /** - * Creates a new {@link Notification.Builder}. + * Creates a new {@link Builder}. * - * @return A {@link Notification.Builder} instance. + * @return A {@link Builder} instance. */ public static Builder builder() { return new Builder(); diff --git a/src/main/java/com/google/firebase/messaging/internal/MessagingServiceErrorResponse.java b/src/main/java/com/google/firebase/messaging/internal/MessagingServiceErrorResponse.java index d63f3af95..7c21199cc 100644 --- a/src/main/java/com/google/firebase/messaging/internal/MessagingServiceErrorResponse.java +++ b/src/main/java/com/google/firebase/messaging/internal/MessagingServiceErrorResponse.java @@ -2,14 +2,28 @@ import com.google.api.client.json.GenericJson; import com.google.api.client.util.Key; +import com.google.common.collect.ImmutableMap; import com.google.firebase.internal.Nullable; +import com.google.firebase.messaging.MessagingErrorCode; import java.util.List; import java.util.Map; /** * The DTO for parsing error responses from the FCM service. */ -public class MessagingServiceErrorResponse extends GenericJson { +public final class MessagingServiceErrorResponse extends GenericJson { + + private static final Map MESSAGING_ERROR_CODES = + ImmutableMap.builder() + .put("APNS_AUTH_ERROR", MessagingErrorCode.THIRD_PARTY_AUTH_ERROR) + .put("INTERNAL", MessagingErrorCode.INTERNAL) + .put("INVALID_ARGUMENT", MessagingErrorCode.INVALID_ARGUMENT) + .put("QUOTA_EXCEEDED", MessagingErrorCode.QUOTA_EXCEEDED) + .put("SENDER_ID_MISMATCH", MessagingErrorCode.SENDER_ID_MISMATCH) + .put("THIRD_PARTY_AUTH_ERROR", MessagingErrorCode.THIRD_PARTY_AUTH_ERROR) + .put("UNAVAILABLE", MessagingErrorCode.UNAVAILABLE) + .put("UNREGISTERED", MessagingErrorCode.UNREGISTERED) + .build(); private static final String FCM_ERROR_TYPE = "type.googleapis.com/google.firebase.fcm.v1.FcmError"; @@ -17,23 +31,35 @@ public class MessagingServiceErrorResponse extends GenericJson { @Key("error") private Map error; + public String getStatus() { + if (error == null) { + return null; + } + + return (String) error.get("status"); + } + + @Nullable - public String getErrorCode() { + public MessagingErrorCode getMessagingErrorCode() { if (error == null) { return null; } + Object details = error.get("details"); - if (details != null && details instanceof List) { + if (details instanceof List) { for (Object detail : (List) details) { if (detail instanceof Map) { Map detailMap = (Map) detail; if (FCM_ERROR_TYPE.equals(detailMap.get("@type"))) { - return (String) detailMap.get("errorCode"); + String errorCode = (String) detailMap.get("errorCode"); + return MESSAGING_ERROR_CODES.get(errorCode); } } } } - return (String) error.get("status"); + + return null; } @Nullable @@ -41,6 +67,7 @@ public String getErrorMessage() { if (error != null) { return (String) error.get("message"); } + return null; } } diff --git a/src/main/java/com/google/firebase/projectmanagement/FirebaseProjectManagementException.java b/src/main/java/com/google/firebase/projectmanagement/FirebaseProjectManagementException.java index fa939b0c4..580ae76f6 100644 --- a/src/main/java/com/google/firebase/projectmanagement/FirebaseProjectManagementException.java +++ b/src/main/java/com/google/firebase/projectmanagement/FirebaseProjectManagementException.java @@ -15,18 +15,27 @@ package com.google.firebase.projectmanagement; +import com.google.firebase.ErrorCode; import com.google.firebase.FirebaseException; -import com.google.firebase.internal.Nullable; +import com.google.firebase.IncomingHttpResponse; +import com.google.firebase.database.annotations.Nullable; +import com.google.firebase.internal.NonNull; /** * An exception encountered while interacting with the Firebase Project Management Service. */ -public class FirebaseProjectManagementException extends FirebaseException { - FirebaseProjectManagementException(String detailMessage) { - this(detailMessage, null); +public final class FirebaseProjectManagementException extends FirebaseException { + + FirebaseProjectManagementException(@NonNull FirebaseException base) { + this(base, base.getMessage()); + } + + FirebaseProjectManagementException(@NonNull FirebaseException base, @NonNull String message) { + super(base.getErrorCode(), message, base.getCause(), base.getHttpResponse()); } - FirebaseProjectManagementException(String detailMessage, @Nullable Throwable cause) { - super(detailMessage, cause); + FirebaseProjectManagementException( + @NonNull ErrorCode code, @NonNull String message, @Nullable IncomingHttpResponse response) { + super(code, message, null, response); } } diff --git a/src/main/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImpl.java b/src/main/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImpl.java index 8abced696..0f9eb3b55 100644 --- a/src/main/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImpl.java +++ b/src/main/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImpl.java @@ -31,8 +31,10 @@ import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.firebase.ErrorCode; import com.google.firebase.FirebaseApp; import com.google.firebase.ImplFirebaseTrampolines; +import com.google.firebase.IncomingHttpResponse; import com.google.firebase.internal.ApiClientUtils; import com.google.firebase.internal.CallableOperation; import java.nio.charset.StandardCharsets; @@ -56,7 +58,6 @@ class FirebaseProjectManagementServiceImpl implements AndroidAppService, IosAppS private final FirebaseApp app; private final Sleeper sleeper; private final Scheduler scheduler; - private final HttpRequestFactory requestFactory; private final HttpHelper httpHelper; private final CreateAndroidAppFromAppIdFunction createAndroidAppFromAppIdFunction = @@ -78,15 +79,9 @@ class FirebaseProjectManagementServiceImpl implements AndroidAppService, IosAppS this.app = checkNotNull(app); this.sleeper = checkNotNull(sleeper); this.scheduler = checkNotNull(scheduler); - this.requestFactory = checkNotNull(requestFactory); this.httpHelper = new HttpHelper(app.getOptions().getJsonFactory(), requestFactory); } - @VisibleForTesting - HttpRequestFactory getRequestFactory() { - return requestFactory; - } - @VisibleForTesting void setInterceptor(HttpResponseInterceptor interceptor) { httpHelper.setInterceptor(interceptor); @@ -318,14 +313,14 @@ protected String execute() throws FirebaseProjectManagementException { payloadBuilder.put("display_name", displayName); } OperationResponse operationResponseInstance = new OperationResponse(); - httpHelper.makePostRequest( + IncomingHttpResponse response = httpHelper.makePostRequest( url, payloadBuilder.build(), operationResponseInstance, projectId, "Project ID"); if (Strings.isNullOrEmpty(operationResponseInstance.name)) { - throw HttpHelper.createFirebaseProjectManagementException( + String message = buildMessage( namespace, "Bundle ID", - "Unable to create App: server returned null operation name.", - /* cause= */ null); + "Unable to create App: server returned null operation name."); + throw new FirebaseProjectManagementException(ErrorCode.INTERNAL, message, response); } return operationResponseInstance.name; } @@ -341,7 +336,8 @@ private String pollOperation(String projectId, String operationName) * Math.pow(POLL_EXPONENTIAL_BACKOFF_FACTOR, currentAttempt)); sleepOrThrow(projectId, delayMillis); OperationResponse operationResponseInstance = new OperationResponse(); - httpHelper.makeGetRequest(url, operationResponseInstance, projectId, "Project ID"); + IncomingHttpResponse response = httpHelper.makeGetRequest( + url, operationResponseInstance, projectId, "Project ID"); if (!operationResponseInstance.done) { continue; } @@ -349,19 +345,20 @@ private String pollOperation(String projectId, String operationName) // or 'error' is set. if (operationResponseInstance.response == null || Strings.isNullOrEmpty(operationResponseInstance.response.appId)) { - throw HttpHelper.createFirebaseProjectManagementException( + String message = buildMessage( projectId, "Project ID", - "Unable to create App: internal server error.", - /* cause= */ null); + "Unable to create App: internal server error."); + throw new FirebaseProjectManagementException(ErrorCode.INTERNAL, message, response); } return operationResponseInstance.response.appId; } - throw HttpHelper.createFirebaseProjectManagementException( + + String message = buildMessage( projectId, "Project ID", - "Unable to create App: deadline exceeded.", - /* cause= */ null); + "Unable to create App: deadline exceeded."); + throw new FirebaseProjectManagementException(ErrorCode.DEADLINE_EXCEEDED, message, null); } /** @@ -420,19 +417,22 @@ private WaitOperationRunnable( public void run() { String url = String.format("%s/v1/%s", FIREBASE_PROJECT_MANAGEMENT_URL, operationName); OperationResponse operationResponseInstance = new OperationResponse(); + IncomingHttpResponse httpResponse; try { - httpHelper.makeGetRequest(url, operationResponseInstance, projectId, "Project ID"); + httpResponse = httpHelper.makeGetRequest( + url, operationResponseInstance, projectId, "Project ID"); } catch (FirebaseProjectManagementException e) { settableFuture.setException(e); return; } if (!operationResponseInstance.done) { if (numberOfPreviousPolls + 1 >= MAXIMUM_POLLING_ATTEMPTS) { - settableFuture.setException(HttpHelper.createFirebaseProjectManagementException( - projectId, + String message = buildMessage(projectId, "Project ID", - "Unable to create App: deadline exceeded.", - /* cause= */ null)); + "Unable to create App: deadline exceeded."); + FirebaseProjectManagementException exception = new FirebaseProjectManagementException( + ErrorCode.DEADLINE_EXCEEDED, message, httpResponse); + settableFuture.setException(exception); } else { long delayMillis = (long) ( POLL_BASE_WAIT_TIME_MILLIS @@ -451,11 +451,12 @@ public void run() { // or 'error' is set. if (operationResponseInstance.response == null || Strings.isNullOrEmpty(operationResponseInstance.response.appId)) { - settableFuture.setException(HttpHelper.createFirebaseProjectManagementException( - projectId, + String message = buildMessage(projectId, "Project ID", - "Unable to create App: internal server error.", - /* cause= */ null)); + "Unable to create App: internal server error."); + FirebaseProjectManagementException exception = new FirebaseProjectManagementException( + ErrorCode.INTERNAL, message, httpResponse); + settableFuture.setException(exception); } else { settableFuture.set(operationResponseInstance.response.appId); } @@ -765,14 +766,17 @@ private void sleepOrThrow(String projectId, long delayMillis) try { sleeper.sleep(delayMillis); } catch (InterruptedException e) { - throw HttpHelper.createFirebaseProjectManagementException( - projectId, + String message = buildMessage(projectId, "Project ID", - "Unable to create App: exponential backoff interrupted.", - /* cause= */ null); + "Unable to create App: exponential backoff interrupted."); + throw new FirebaseProjectManagementException(ErrorCode.ABORTED, message, null); } } + private String buildMessage(String resourceId, String resourceIdName, String description) { + return String.format("%s \"%s\": %s", resourceIdName, resourceId, description); + } + /* Helper types. */ private interface CreateAppFromAppIdFunction extends ApiFunction {} diff --git a/src/main/java/com/google/firebase/projectmanagement/HttpHelper.java b/src/main/java/com/google/firebase/projectmanagement/HttpHelper.java index 2143c1453..fb29f7b86 100644 --- a/src/main/java/com/google/firebase/projectmanagement/HttpHelper.java +++ b/src/main/java/com/google/firebase/projectmanagement/HttpHelper.java @@ -16,84 +16,57 @@ package com.google.firebase.projectmanagement; -import com.google.api.client.http.GenericUrl; -import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.HttpMethods; import com.google.api.client.http.HttpRequestFactory; -import com.google.api.client.http.HttpResponse; -import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.HttpResponseInterceptor; -import com.google.api.client.http.json.JsonHttpContent; import com.google.api.client.json.JsonFactory; -import com.google.api.client.json.JsonObjectParser; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Charsets; -import com.google.common.collect.ImmutableMap; -import com.google.firebase.internal.Nullable; +import com.google.firebase.FirebaseException; +import com.google.firebase.IncomingHttpResponse; +import com.google.firebase.internal.AbstractPlatformErrorHandler; +import com.google.firebase.internal.ErrorHandlingHttpClient; +import com.google.firebase.internal.HttpRequestInfo; import com.google.firebase.internal.SdkUtils; -import java.io.IOException; - -class HttpHelper { - - @VisibleForTesting static final String PATCH_OVERRIDE_KEY = "X-HTTP-Method-Override"; - @VisibleForTesting static final String PATCH_OVERRIDE_VALUE = "PATCH"; - private static final ImmutableMap ERROR_CODES = - ImmutableMap.builder() - .put(401, "Request not authorized.") - .put(403, "Client does not have sufficient privileges.") - .put(404, "Failed to find the resource.") - .put(409, "The resource already exists.") - .put(429, "Request throttled by the backend server.") - .put(500, "Internal server error.") - .put(503, "Backend servers are over capacity. Try again later.") - .build(); + +final class HttpHelper { + private static final String CLIENT_VERSION_HEADER = "X-Client-Version"; - private final String clientVersion = "Java/Admin/" + SdkUtils.getVersion(); - private final JsonFactory jsonFactory; - private final HttpRequestFactory requestFactory; - private HttpResponseInterceptor interceptor; + private static final String CLIENT_VERSION = "Java/Admin/" + SdkUtils.getVersion(); + + private final ErrorHandlingHttpClient httpClient; HttpHelper(JsonFactory jsonFactory, HttpRequestFactory requestFactory) { - this.jsonFactory = jsonFactory; - this.requestFactory = requestFactory; + ProjectManagementErrorHandler errorHandler = new ProjectManagementErrorHandler(jsonFactory); + this.httpClient = new ErrorHandlingHttpClient<>(requestFactory, jsonFactory, errorHandler); } void setInterceptor(HttpResponseInterceptor interceptor) { - this.interceptor = interceptor; + httpClient.setInterceptor(interceptor); } - void makeGetRequest( + IncomingHttpResponse makeGetRequest( String url, T parsedResponseInstance, String requestIdentifier, String requestIdentifierDescription) throws FirebaseProjectManagementException { - try { - makeRequest( - requestFactory.buildGetRequest(new GenericUrl(url)), - parsedResponseInstance, - requestIdentifier, - requestIdentifierDescription); - } catch (IOException e) { - handleError(requestIdentifier, requestIdentifierDescription, e); - } + return makeRequest( + HttpRequestInfo.buildGetRequest(url), + parsedResponseInstance, + requestIdentifier, + requestIdentifierDescription); } - void makePostRequest( + IncomingHttpResponse makePostRequest( String url, Object payload, T parsedResponseInstance, String requestIdentifier, String requestIdentifierDescription) throws FirebaseProjectManagementException { - try { - makeRequest( - requestFactory.buildPostRequest( - new GenericUrl(url), new JsonHttpContent(jsonFactory, payload)), - parsedResponseInstance, - requestIdentifier, - requestIdentifierDescription); - } catch (IOException e) { - handleError(requestIdentifier, requestIdentifierDescription, e); - } + return makeRequest( + HttpRequestInfo.buildJsonPostRequest(url, payload), + parsedResponseInstance, + requestIdentifier, + requestIdentifierDescription); } void makePatchRequest( @@ -102,15 +75,11 @@ void makePatchRequest( T parsedResponseInstance, String requestIdentifier, String requestIdentifierDescription) throws FirebaseProjectManagementException { - try { - HttpRequest baseRequest = requestFactory.buildPostRequest( - new GenericUrl(url), new JsonHttpContent(jsonFactory, payload)); - baseRequest.getHeaders().set(PATCH_OVERRIDE_KEY, PATCH_OVERRIDE_VALUE); - makeRequest( - baseRequest, parsedResponseInstance, requestIdentifier, requestIdentifierDescription); - } catch (IOException e) { - handleError(requestIdentifier, requestIdentifierDescription, e); - } + makeRequest( + HttpRequestInfo.buildJsonRequest(HttpMethods.PATCH, url, payload), + parsedResponseInstance, + requestIdentifier, + requestIdentifierDescription); } void makeDeleteRequest( @@ -118,69 +87,40 @@ void makeDeleteRequest( T parsedResponseInstance, String requestIdentifier, String requestIdentifierDescription) throws FirebaseProjectManagementException { - try { - makeRequest( - requestFactory.buildDeleteRequest(new GenericUrl(url)), - parsedResponseInstance, - requestIdentifier, - requestIdentifierDescription); - } catch (IOException e) { - handleError(requestIdentifier, requestIdentifierDescription, e); - } + makeRequest( + HttpRequestInfo.buildDeleteRequest(url), + parsedResponseInstance, + requestIdentifier, + requestIdentifierDescription); } - void makeRequest( - HttpRequest baseRequest, + private IncomingHttpResponse makeRequest( + HttpRequestInfo baseRequest, T parsedResponseInstance, String requestIdentifier, String requestIdentifierDescription) throws FirebaseProjectManagementException { - HttpResponse response = null; try { - baseRequest.getHeaders().set(CLIENT_VERSION_HEADER, clientVersion); - baseRequest.setParser(new JsonObjectParser(jsonFactory)); - baseRequest.setResponseInterceptor(interceptor); - response = baseRequest.execute(); - jsonFactory.createJsonParser(response.getContent(), Charsets.UTF_8) - .parseAndClose(parsedResponseInstance); - } catch (Exception e) { - handleError(requestIdentifier, requestIdentifierDescription, e); - } finally { - disconnectQuietly(response); + baseRequest.addHeader(CLIENT_VERSION_HEADER, CLIENT_VERSION); + IncomingHttpResponse response = httpClient.send(baseRequest); + httpClient.parse(response, parsedResponseInstance); + return response; + } catch (FirebaseProjectManagementException e) { + String message = String.format( + "%s \"%s\": %s", requestIdentifierDescription, requestIdentifier, e.getMessage()); + throw new FirebaseProjectManagementException(e, message); } } - private static void disconnectQuietly(HttpResponse response) { - if (response != null) { - try { - response.disconnect(); - } catch (IOException ignored) { - // Ignored. - } - } - } + private static class ProjectManagementErrorHandler + extends AbstractPlatformErrorHandler { - private static void handleError( - String requestIdentifier, String requestIdentifierDescription, Exception e) - throws FirebaseProjectManagementException { - String messageBody = "Error while invoking Firebase Project Management service."; - if (e instanceof HttpResponseException) { - int statusCode = ((HttpResponseException) e).getStatusCode(); - if (ERROR_CODES.containsKey(statusCode)) { - messageBody = ERROR_CODES.get(statusCode); - } + ProjectManagementErrorHandler(JsonFactory jsonFactory) { + super(jsonFactory); } - throw createFirebaseProjectManagementException( - requestIdentifier, requestIdentifierDescription, messageBody, e); - } - static FirebaseProjectManagementException createFirebaseProjectManagementException( - String requestIdentifier, - String requestIdentifierDescription, - String messageBody, - @Nullable Exception cause) { - return new FirebaseProjectManagementException( - String.format( - "%s \"%s\": %s", requestIdentifierDescription, requestIdentifier, messageBody), - cause); + @Override + protected FirebaseProjectManagementException createException(FirebaseException base) { + return new FirebaseProjectManagementException(base); + } } } diff --git a/src/test/java/com/google/firebase/FirebaseAppTest.java b/src/test/java/com/google/firebase/FirebaseAppTest.java index eee9efdb8..d481e3c0e 100644 --- a/src/test/java/com/google/firebase/FirebaseAppTest.java +++ b/src/test/java/com/google/firebase/FirebaseAppTest.java @@ -35,13 +35,11 @@ import com.google.auth.oauth2.OAuth2Credentials.CredentialsChangedListener; import com.google.common.base.Defaults; import com.google.common.base.Strings; -import com.google.common.base.Supplier; import com.google.common.base.Suppliers; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.firebase.FirebaseApp.TokenRefresher; -import com.google.firebase.FirebaseOptions.Builder; import com.google.firebase.database.FirebaseDatabase; import com.google.firebase.testing.FirebaseAppRule; import com.google.firebase.testing.ServiceAccount; @@ -74,7 +72,7 @@ public class FirebaseAppTest { private static final FirebaseOptions OPTIONS = - new FirebaseOptions.Builder() + FirebaseOptions.builder() .setCredentials(TestUtils.getCertCredential(ServiceAccount.EDITOR.asStream())) .build(); @@ -110,7 +108,7 @@ public void testGetInstancePersistedNotInitialized() { @Test public void testGetProjectIdFromOptions() { - FirebaseOptions options = new FirebaseOptions.Builder(OPTIONS) + FirebaseOptions options = OPTIONS.toBuilder() .setProjectId("explicit-project-id") .build(); FirebaseApp app = FirebaseApp.initializeApp(options, "myApp"); @@ -131,7 +129,7 @@ public void testGetProjectIdFromEnvironment() { for (String variable : variables) { String gcloudProject = System.getenv(variable); TestUtils.setEnvironmentVariables(ImmutableMap.of(variable, "project-id-1")); - FirebaseOptions options = new FirebaseOptions.Builder() + FirebaseOptions options = FirebaseOptions.builder() .setCredentials(new MockGoogleCredentials()) .build(); try { @@ -155,7 +153,7 @@ public void testProjectIdEnvironmentVariablePrecedence() { TestUtils.setEnvironmentVariables(ImmutableMap.of( "GCLOUD_PROJECT", "project-id-1", "GOOGLE_CLOUD_PROJECT", "project-id-2")); - FirebaseOptions options = new FirebaseOptions.Builder() + FirebaseOptions options = FirebaseOptions.builder() .setCredentials(new MockGoogleCredentials()) .build(); try { @@ -239,7 +237,7 @@ public void testGetNullApp() { @Test public void testToString() throws IOException { FirebaseOptions options = - new FirebaseOptions.Builder() + FirebaseOptions.builder() .setCredentials(GoogleCredentials.fromStream(ServiceAccount.EDITOR.asStream())) .build(); FirebaseApp app = FirebaseApp.initializeApp(options, "app"); @@ -461,14 +459,13 @@ public void testTokenRefresherStateMachine() { @Test public void testAppWithAuthVariableOverrides() { Map authVariableOverrides = ImmutableMap.of("uid", "uid1"); - FirebaseOptions options = - new FirebaseOptions.Builder(getMockCredentialOptions()) - .setDatabaseAuthVariableOverride(authVariableOverrides) - .build(); + FirebaseOptions options = getMockCredentialOptions().toBuilder() + .setDatabaseAuthVariableOverride(authVariableOverrides) + .build(); FirebaseApp app = FirebaseApp.initializeApp(options, "testGetAppWithUid"); assertEquals("uid1", app.getOptions().getDatabaseAuthVariableOverride().get("uid")); String token = TestOnlyImplFirebaseTrampolines.getToken(app, false); - Assert.assertTrue(!token.isEmpty()); + Assert.assertFalse(token.isEmpty()); } @Test(expected = IllegalArgumentException.class) @@ -579,16 +576,6 @@ public void testFirebaseConfigStringIgnoresInvalidKey() { assertEquals("hipster-chat-mock", firebaseApp.getOptions().getProjectId()); } - @Test(expected = IllegalArgumentException.class) - public void testFirebaseExceptionNullDetail() { - new FirebaseException(null); - } - - @Test(expected = IllegalArgumentException.class) - public void testFirebaseExceptionEmptyDetail() { - new FirebaseException(""); - } - @Test public void testFirebaseAppCreationWithEmptySupplier() { FirebaseApp.initializeApp(FirebaseOptions.builder() @@ -609,7 +596,7 @@ private static void setFirebaseConfigEnvironmentVariable(String configJSON) { } private static FirebaseOptions getMockCredentialOptions() { - return new Builder().setCredentials(new MockGoogleCredentials()).build(); + return FirebaseOptions.builder().setCredentials(new MockGoogleCredentials()).build(); } private static void invokePublicInstanceMethodWithDefaultValues(Object instance, Method method) diff --git a/src/test/java/com/google/firebase/FirebaseExceptionTest.java b/src/test/java/com/google/firebase/FirebaseExceptionTest.java new file mode 100644 index 000000000..efe4e39fa --- /dev/null +++ b/src/test/java/com/google/firebase/FirebaseExceptionTest.java @@ -0,0 +1,124 @@ +/* + * Copyright 2020 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; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; + +import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.HttpResponseException; +import com.google.api.client.http.HttpStatusCodes; +import com.google.api.client.testing.http.MockLowLevelHttpRequest; +import com.google.api.client.testing.http.MockLowLevelHttpResponse; +import com.google.firebase.testing.TestUtils; +import java.io.IOException; +import org.junit.Test; + +@SuppressWarnings("ThrowableNotThrown") +public class FirebaseExceptionTest { + + @Test(expected = NullPointerException.class) + public void testFirebaseExceptionWithoutErrorCode() { + new FirebaseException( + null, + "Test error", + null, + null); + } + + @Test(expected = IllegalArgumentException.class) + public void testFirebaseExceptionWithNullMessage() { + new FirebaseException( + ErrorCode.INTERNAL, + null, + null, + null); + } + + @Test(expected = IllegalArgumentException.class) + public void testFirebaseExceptionWithEmptyMessage() { + new FirebaseException( + ErrorCode.INTERNAL, + "", + null, + null); + } + + @Test + public void testFirebaseExceptionWithoutResponseAndCause() { + FirebaseException exception = new FirebaseException( + ErrorCode.INTERNAL, + "Test error", + null, + null); + + assertEquals(ErrorCode.INTERNAL, exception.getErrorCode()); + assertEquals("Test error", exception.getMessage()); + assertNull(exception.getHttpResponse()); + assertNull(exception.getCause()); + } + + @Test + public void testFirebaseExceptionWithResponse() throws IOException { + HttpResponseException httpError = createHttpResponseException(); + OutgoingHttpRequest request = new OutgoingHttpRequest( + "GET", "https://firebase.google.com"); + IncomingHttpResponse response = new IncomingHttpResponse(httpError, request); + + FirebaseException exception = new FirebaseException( + ErrorCode.INTERNAL, + "Test error", + null, + response); + + assertEquals(ErrorCode.INTERNAL, exception.getErrorCode()); + assertEquals("Test error", exception.getMessage()); + assertSame(response, exception.getHttpResponse()); + assertNull(exception.getCause()); + } + + @Test + public void testFirebaseExceptionWithCause() { + Exception cause = new Exception("root cause"); + + FirebaseException exception = new FirebaseException( + ErrorCode.INTERNAL, + "Test error", + cause); + + assertEquals(ErrorCode.INTERNAL, exception.getErrorCode()); + assertEquals("Test error", exception.getMessage()); + assertNull(exception.getHttpResponse()); + assertSame(cause, exception.getCause()); + } + + private HttpResponseException createHttpResponseException() throws IOException { + MockLowLevelHttpResponse lowLevelResponse = new MockLowLevelHttpResponse() + .setStatusCode(HttpStatusCodes.STATUS_CODE_SERVER_ERROR) + .setContent("{}"); + MockLowLevelHttpRequest lowLevelRequest = new MockLowLevelHttpRequest() + .setResponse(lowLevelResponse); + HttpRequest request = TestUtils.createRequest(lowLevelRequest); + try { + request.execute(); + throw new IOException("HttpResponseException not thrown"); + } catch (HttpResponseException e) { + return e; + } + } +} diff --git a/src/test/java/com/google/firebase/FirebaseOptionsTest.java b/src/test/java/com/google/firebase/FirebaseOptionsTest.java index a3dac26fd..c74215beb 100644 --- a/src/test/java/com/google/firebase/FirebaseOptionsTest.java +++ b/src/test/java/com/google/firebase/FirebaseOptionsTest.java @@ -17,14 +17,13 @@ package com.google.firebase; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; -import com.google.api.client.googleapis.auth.oauth2.GoogleCredential; import com.google.api.client.http.javanet.NetHttpTransport; import com.google.api.client.json.gson.GsonFactory; import com.google.auth.oauth2.AccessToken; @@ -49,7 +48,7 @@ public class FirebaseOptionsTest { private static final String FIREBASE_PROJECT_ID = "explicit-project-id"; private static final FirebaseOptions ALL_VALUES_OPTIONS = - new FirebaseOptions.Builder() + FirebaseOptions.builder() .setDatabaseUrl(FIREBASE_DB_URL) .setStorageBucket(FIREBASE_STORAGE_BUCKET) .setProjectId(FIREBASE_PROJECT_ID) @@ -76,11 +75,9 @@ protected ThreadFactory getThreadFactory() { public void createOptionsWithAllValuesSet() throws IOException { GsonFactory jsonFactory = new GsonFactory(); NetHttpTransport httpTransport = new NetHttpTransport(); - FirestoreOptions firestoreOptions = FirestoreOptions.newBuilder() - .setTimestampsInSnapshotsEnabled(true) - .build(); + FirestoreOptions firestoreOptions = FirestoreOptions.newBuilder().build(); FirebaseOptions firebaseOptions = - new FirebaseOptions.Builder() + FirebaseOptions.builder() .setDatabaseUrl(FIREBASE_DB_URL) .setStorageBucket(FIREBASE_STORAGE_BUCKET) .setCredentials(GoogleCredentials.fromStream(ServiceAccount.EDITOR.asStream())) @@ -106,14 +103,14 @@ public void createOptionsWithAllValuesSet() throws IOException { assertNotNull(credentials); assertTrue(credentials instanceof ServiceAccountCredentials); assertEquals( - GoogleCredential.fromStream(ServiceAccount.EDITOR.asStream()).getServiceAccountId(), + ServiceAccount.EDITOR.getEmail(), ((ServiceAccountCredentials) credentials).getClientEmail()); } @Test public void createOptionsWithOnlyMandatoryValuesSet() throws IOException { FirebaseOptions firebaseOptions = - new FirebaseOptions.Builder() + FirebaseOptions.builder() .setCredentials(GoogleCredentials.fromStream(ServiceAccount.EDITOR.asStream())) .build(); assertNotNull(firebaseOptions.getJsonFactory()); @@ -128,7 +125,7 @@ public void createOptionsWithOnlyMandatoryValuesSet() throws IOException { assertNotNull(credentials); assertTrue(credentials instanceof ServiceAccountCredentials); assertEquals( - GoogleCredential.fromStream(ServiceAccount.EDITOR.asStream()).getServiceAccountId(), + ServiceAccount.EDITOR.getEmail(), ((ServiceAccountCredentials) credentials).getClientEmail()); assertNull(firebaseOptions.getFirestoreOptions()); } @@ -136,7 +133,7 @@ public void createOptionsWithOnlyMandatoryValuesSet() throws IOException { @Test public void createOptionsWithCustomFirebaseCredential() { FirebaseOptions firebaseOptions = - new FirebaseOptions.Builder() + FirebaseOptions.builder() .setCredentials(new GoogleCredentials() { @Override public AccessToken refreshAccessToken() { @@ -156,17 +153,17 @@ public AccessToken refreshAccessToken() { @Test(expected = NullPointerException.class) public void createOptionsWithCredentialMissing() { - new FirebaseOptions.Builder().build().getCredentials(); + FirebaseOptions.builder().build().getCredentials(); } @Test(expected = NullPointerException.class) public void createOptionsWithNullCredentials() { - new FirebaseOptions.Builder().setCredentials((GoogleCredentials) null).build(); + FirebaseOptions.builder().setCredentials((GoogleCredentials) null).build(); } @Test(expected = IllegalArgumentException.class) public void createOptionsWithStorageBucketUrl() throws IOException { - new FirebaseOptions.Builder() + FirebaseOptions.builder() .setCredentials(GoogleCredentials.fromStream(ServiceAccount.EDITOR.asStream())) .setStorageBucket("gs://mock-storage-bucket") .build(); @@ -174,7 +171,7 @@ public void createOptionsWithStorageBucketUrl() throws IOException { @Test(expected = NullPointerException.class) public void createOptionsWithNullThreadManager() { - new FirebaseOptions.Builder() + FirebaseOptions.builder() .setCredentials(TestUtils.getCertCredential(ServiceAccount.EDITOR.asStream())) .setThreadManager(null) .build(); @@ -182,7 +179,7 @@ public void createOptionsWithNullThreadManager() { @Test public void checkToBuilderCreatesNewEquivalentInstance() { - FirebaseOptions allValuesOptionsCopy = new FirebaseOptions.Builder(ALL_VALUES_OPTIONS).build(); + FirebaseOptions allValuesOptionsCopy = ALL_VALUES_OPTIONS.toBuilder().build(); assertNotSame(ALL_VALUES_OPTIONS, allValuesOptionsCopy); assertEquals(ALL_VALUES_OPTIONS.getCredentials(), allValuesOptionsCopy.getCredentials()); assertEquals(ALL_VALUES_OPTIONS.getDatabaseUrl(), allValuesOptionsCopy.getDatabaseUrl()); @@ -198,7 +195,7 @@ public void checkToBuilderCreatesNewEquivalentInstance() { @Test(expected = IllegalArgumentException.class) public void createOptionsWithInvalidConnectTimeout() { - new FirebaseOptions.Builder() + FirebaseOptions.builder() .setCredentials(TestUtils.getCertCredential(ServiceAccount.EDITOR.asStream())) .setConnectTimeout(-1) .build(); @@ -206,7 +203,7 @@ public void createOptionsWithInvalidConnectTimeout() { @Test(expected = IllegalArgumentException.class) public void createOptionsWithInvalidReadTimeout() { - new FirebaseOptions.Builder() + FirebaseOptions.builder() .setCredentials(TestUtils.getCertCredential(ServiceAccount.EDITOR.asStream())) .setReadTimeout(-1) .build(); @@ -216,14 +213,14 @@ public void createOptionsWithInvalidReadTimeout() { public void testNotEquals() throws IOException { GoogleCredentials credentials = GoogleCredentials.fromStream(ServiceAccount.EDITOR.asStream()); FirebaseOptions options1 = - new FirebaseOptions.Builder() + FirebaseOptions.builder() .setCredentials(credentials) .build(); FirebaseOptions options2 = - new FirebaseOptions.Builder() + FirebaseOptions.builder() .setCredentials(credentials) .setDatabaseUrl("https://test.firebaseio.com") .build(); - assertFalse(options1.equals(options2)); + assertNotEquals(options1, options2); } } diff --git a/src/test/java/com/google/firebase/IncomingHttpResponseTest.java b/src/test/java/com/google/firebase/IncomingHttpResponseTest.java new file mode 100644 index 000000000..4da63d12b --- /dev/null +++ b/src/test/java/com/google/firebase/IncomingHttpResponseTest.java @@ -0,0 +1,141 @@ +/* + * Copyright 2020 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; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import com.google.api.client.http.GenericUrl; +import com.google.api.client.http.HttpMethods; +import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.HttpResponse; +import com.google.api.client.http.HttpResponseException; +import com.google.api.client.http.HttpStatusCodes; +import com.google.api.client.testing.http.MockLowLevelHttpRequest; +import com.google.api.client.testing.http.MockLowLevelHttpResponse; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.firebase.testing.TestUtils; +import java.io.IOException; +import java.util.Map; +import org.junit.Test; + +public class IncomingHttpResponseTest { + + private static final String TEST_URL = "https://firebase.google.com/response"; + private static final OutgoingHttpRequest REQUEST = new OutgoingHttpRequest("GET", TEST_URL); + private static final Map RESPONSE_HEADERS = + ImmutableMap.of("x-firebase-client", ImmutableList.of("test-version")); + private static final String RESPONSE_BODY = "test response"; + + @Test(expected = NullPointerException.class) + public void testNullHttpResponse() { + new IncomingHttpResponse(null, "content"); + } + + @Test + public void testNullHttpResponseException() throws IOException { + try { + new IncomingHttpResponse(null, REQUEST); + fail("No exception thrown for null HttpResponseException"); + } catch (NullPointerException ignore) { + // expected + } + + HttpRequest request = createHttpRequest(); + try { + new IncomingHttpResponse(null, request); + fail("No exception thrown for null HttpResponseException"); + } catch (NullPointerException ignore) { + // expected + } + } + + @Test + public void testIncomingHttpResponse() throws IOException { + HttpResponseException httpError = createHttpResponseException(); + + IncomingHttpResponse response = new IncomingHttpResponse(httpError, REQUEST); + + assertEquals(HttpStatusCodes.STATUS_CODE_SERVER_ERROR, response.getStatusCode()); + assertEquals(RESPONSE_BODY, response.getContent()); + assertEquals(RESPONSE_HEADERS, response.getHeaders()); + assertFalse(response.getHeaders().isEmpty()); + assertSame(REQUEST, response.getRequest()); + } + + @Test + public void testIncomingHttpResponseWithRequest() throws IOException { + HttpResponseException httpError = createHttpResponseException(); + HttpRequest httpRequest = createHttpRequest(); + + IncomingHttpResponse response = new IncomingHttpResponse(httpError, httpRequest); + + assertEquals(HttpStatusCodes.STATUS_CODE_SERVER_ERROR, response.getStatusCode()); + assertEquals(RESPONSE_BODY, response.getContent()); + assertEquals(RESPONSE_HEADERS, response.getHeaders()); + OutgoingHttpRequest request = response.getRequest(); + assertEquals(HttpMethods.POST, request.getMethod()); + assertEquals(TEST_URL, request.getUrl()); + } + + @Test + public void testIncomingHttpResponseWithResponse() throws IOException { + HttpResponse httpResponse = createHttpResponse(); + + IncomingHttpResponse response = new IncomingHttpResponse(httpResponse, RESPONSE_BODY); + + assertEquals(HttpStatusCodes.STATUS_CODE_OK, response.getStatusCode()); + assertEquals(RESPONSE_BODY, response.getContent()); + assertTrue(response.getHeaders().isEmpty()); + OutgoingHttpRequest request = response.getRequest(); + assertEquals(HttpMethods.POST, request.getMethod()); + assertEquals(TEST_URL, request.getUrl()); + } + + private HttpResponseException createHttpResponseException() throws IOException { + MockLowLevelHttpResponse lowLevelResponse = new MockLowLevelHttpResponse() + .setStatusCode(HttpStatusCodes.STATUS_CODE_SERVER_ERROR) + .addHeader("X-Firebase-Client", "test-version") + .setContent(RESPONSE_BODY); + MockLowLevelHttpRequest lowLevelRequest = new MockLowLevelHttpRequest() + .setResponse(lowLevelResponse); + HttpRequest request = TestUtils.createRequest(lowLevelRequest, new GenericUrl(TEST_URL)); + try { + request.execute(); + throw new IOException("HttpResponseException not thrown"); + } catch (HttpResponseException e) { + return e; + } + } + + private HttpRequest createHttpRequest() throws IOException { + MockLowLevelHttpResponse lowLevelResponse = new MockLowLevelHttpResponse() + .setContent("{}"); + MockLowLevelHttpRequest lowLevelRequest = new MockLowLevelHttpRequest() + .setResponse(lowLevelResponse); + return TestUtils.createRequest(lowLevelRequest, new GenericUrl(TEST_URL)); + } + + private HttpResponse createHttpResponse() throws IOException { + HttpRequest request = createHttpRequest(); + return request.execute(); + } +} diff --git a/src/test/java/com/google/firebase/OutgoingHttpRequestTest.java b/src/test/java/com/google/firebase/OutgoingHttpRequestTest.java new file mode 100644 index 000000000..792d621f5 --- /dev/null +++ b/src/test/java/com/google/firebase/OutgoingHttpRequestTest.java @@ -0,0 +1,89 @@ +/* + * Copyright 2020 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; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +import com.google.api.client.googleapis.util.Utils; +import com.google.api.client.http.GenericUrl; +import com.google.api.client.http.HttpMethods; +import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.json.JsonHttpContent; +import com.google.api.client.testing.http.MockHttpTransport; +import com.google.common.collect.ImmutableMap; +import java.io.IOException; +import org.junit.Test; + +public class OutgoingHttpRequestTest { + + private static final String TEST_URL = "https://firebase.google.com/request"; + + @Test(expected = NullPointerException.class) + public void testNullHttpRequest() { + new OutgoingHttpRequest(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testNullMethod() { + new OutgoingHttpRequest(null, TEST_URL); + } + + @Test(expected = IllegalArgumentException.class) + public void testEmptyMethod() { + new OutgoingHttpRequest("", TEST_URL); + } + + @Test(expected = IllegalArgumentException.class) + public void testNullUrl() { + new OutgoingHttpRequest(HttpMethods.GET, null); + } + + @Test(expected = IllegalArgumentException.class) + public void testEmptyUrl() { + new OutgoingHttpRequest(HttpMethods.GET, ""); + } + + @Test + public void testOutgoingHttpRequest() { + OutgoingHttpRequest request = new OutgoingHttpRequest(HttpMethods.GET, TEST_URL); + + assertEquals(HttpMethods.GET, request.getMethod()); + assertEquals(TEST_URL, request.getUrl()); + assertNull(request.getContent()); + assertTrue(request.getHeaders().isEmpty()); + } + + @Test + public void testOutgoingHttpRequestWithContent() throws IOException { + JsonHttpContent streamingContent = new JsonHttpContent( + Utils.getDefaultJsonFactory(), + ImmutableMap.of("key", "value")); + HttpRequest httpRequest = new MockHttpTransport().createRequestFactory() + .buildPostRequest(new GenericUrl(TEST_URL), streamingContent); + httpRequest.getHeaders().set("X-Firebase-Client", "test-version"); + + OutgoingHttpRequest request = new OutgoingHttpRequest(httpRequest); + + assertEquals(HttpMethods.POST, request.getMethod()); + assertEquals(TEST_URL, request.getUrl()); + assertSame(streamingContent, request.getContent()); + assertEquals("test-version", request.getHeaders().get("x-firebase-client")); + } +} diff --git a/src/test/java/com/google/firebase/ThreadManagerTest.java b/src/test/java/com/google/firebase/ThreadManagerTest.java index d8689a4da..22401de30 100644 --- a/src/test/java/com/google/firebase/ThreadManagerTest.java +++ b/src/test/java/com/google/firebase/ThreadManagerTest.java @@ -187,8 +187,7 @@ public void testAppLifecycleWithServiceCall() { } @Test - public void testAppLifecycleWithMultipleServiceCalls() - throws ExecutionException, InterruptedException { + public void testAppLifecycleWithMultipleServiceCalls() { MockThreadManager threadManager = new MockThreadManager(executor); // Initializing an app should initialize the executor. @@ -235,7 +234,7 @@ public void testAppLifecycleWithMultipleServiceCalls() } private FirebaseOptions buildOptions(ThreadManager threadManager) { - return new FirebaseOptions.Builder() + return FirebaseOptions.builder() .setCredentials(new MockGoogleCredentials()) .setProjectId("mock-project-id") .setThreadManager(threadManager) @@ -278,7 +277,7 @@ private static class Event { private final FirebaseApp app; private ExecutorService executor; - public Event(int type, @Nullable FirebaseApp app, @Nullable ExecutorService executor) { + Event(int type, @Nullable FirebaseApp app, @Nullable ExecutorService executor) { this.type = type; this.app = app; this.executor = executor; diff --git a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java index 803591d81..35fa21d4d 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java +++ b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java @@ -43,6 +43,7 @@ import com.google.common.collect.ImmutableMap; import com.google.common.io.BaseEncoding; import com.google.common.util.concurrent.MoreExecutors; +import com.google.firebase.ErrorCode; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; import com.google.firebase.ImplFirebaseTrampolines; @@ -50,7 +51,6 @@ import com.google.firebase.auth.UserTestUtils.RandomUser; import com.google.firebase.auth.UserTestUtils.TemporaryUser; import com.google.firebase.auth.hash.Scrypt; -import com.google.firebase.auth.internal.AuthHttpClient; import com.google.firebase.internal.Nullable; import com.google.firebase.testing.IntegrationTestUtils; import java.io.IOException; @@ -96,8 +96,14 @@ public void testGetNonExistingUser() throws Exception { fail("No error thrown for non existing uid"); } catch (ExecutionException e) { assertTrue(e.getCause() instanceof FirebaseAuthException); - assertEquals(AuthHttpClient.USER_NOT_FOUND_ERROR, - ((FirebaseAuthException) e.getCause()).getErrorCode()); + FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); + assertEquals( + "No user record found for the provided user ID: non.existing", + authException.getMessage()); + assertEquals(ErrorCode.NOT_FOUND, authException.getErrorCode()); + assertNull(authException.getCause()); + assertNotNull(authException.getHttpResponse()); + assertEquals(AuthErrorCode.USER_NOT_FOUND, authException.getAuthErrorCode()); } } @@ -108,8 +114,14 @@ public void testGetNonExistingUserByEmail() throws Exception { fail("No error thrown for non existing email"); } catch (ExecutionException e) { assertTrue(e.getCause() instanceof FirebaseAuthException); - assertEquals(AuthHttpClient.USER_NOT_FOUND_ERROR, - ((FirebaseAuthException) e.getCause()).getErrorCode()); + FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); + assertEquals( + "No user record found for the provided email: non.existing@definitely.non.existing", + authException.getMessage()); + assertEquals(ErrorCode.NOT_FOUND, authException.getErrorCode()); + assertNull(authException.getCause()); + assertNotNull(authException.getHttpResponse()); + assertEquals(AuthErrorCode.USER_NOT_FOUND, authException.getAuthErrorCode()); } } @@ -120,8 +132,14 @@ public void testUpdateNonExistingUser() throws Exception { fail("No error thrown for non existing uid"); } catch (ExecutionException e) { assertTrue(e.getCause() instanceof FirebaseAuthException); - assertEquals(AuthHttpClient.USER_NOT_FOUND_ERROR, - ((FirebaseAuthException) e.getCause()).getErrorCode()); + FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); + assertEquals( + "No user record found for the given identifier (USER_NOT_FOUND).", + authException.getMessage()); + assertEquals(ErrorCode.NOT_FOUND, authException.getErrorCode()); + assertNotNull(authException.getCause()); + assertNotNull(authException.getHttpResponse()); + assertEquals(AuthErrorCode.USER_NOT_FOUND, authException.getAuthErrorCode()); } } @@ -132,8 +150,14 @@ public void testDeleteNonExistingUser() throws Exception { fail("No error thrown for non existing uid"); } catch (ExecutionException e) { assertTrue(e.getCause() instanceof FirebaseAuthException); - assertEquals(AuthHttpClient.USER_NOT_FOUND_ERROR, - ((FirebaseAuthException) e.getCause()).getErrorCode()); + FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); + assertEquals( + "No user record found for the given identifier (USER_NOT_FOUND).", + authException.getMessage()); + assertEquals(ErrorCode.NOT_FOUND, authException.getErrorCode()); + assertNotNull(authException.getCause()); + assertNotNull(authException.getHttpResponse()); + assertEquals(AuthErrorCode.USER_NOT_FOUND, authException.getAuthErrorCode()); } } @@ -313,43 +337,39 @@ public void testUserLifecycle() throws Exception { @Test public void testLastRefreshTime() throws Exception { RandomUser user = UserTestUtils.generateRandomUserInfo(); - UserRecord newUserRecord = auth.createUser(new UserRecord.CreateRequest() - .setUid(user.getUid()) - .setEmail(user.getEmail()) - .setEmailVerified(false) - .setPassword("password")); + UserRecord newUserRecord = temporaryUser.create(new UserRecord.CreateRequest() + .setUid(user.getUid()) + .setEmail(user.getEmail()) + .setEmailVerified(false) + .setPassword("password")); - try { - // New users should not have a lastRefreshTimestamp set. - assertEquals(0, newUserRecord.getUserMetadata().getLastRefreshTimestamp()); - - // Login to cause the lastRefreshTimestamp to be set. - signInWithPassword(newUserRecord.getEmail(), "password"); - - // Attempt to retrieve the user 3 times (with a small delay between each - // attempt). Occassionally, this call retrieves the user data without the - // lastLoginTime/lastRefreshTime set; possibly because it's hitting a - // different server than the login request uses. - UserRecord userRecord = null; - for (int i = 0; i < 3; i++) { - userRecord = auth.getUser(newUserRecord.getUid()); - - if (userRecord.getUserMetadata().getLastRefreshTimestamp() != 0) { - break; - } + // New users should not have a lastRefreshTimestamp set. + assertEquals(0, newUserRecord.getUserMetadata().getLastRefreshTimestamp()); - TimeUnit.SECONDS.sleep((long)Math.pow(2, i)); + // Login to cause the lastRefreshTimestamp to be set. + signInWithPassword(newUserRecord.getEmail(), "password"); + + // Attempt to retrieve the user 3 times (with a small delay between each + // attempt). Occasionally, this call retrieves the user data without the + // lastLoginTime/lastRefreshTime set; possibly because it's hitting a + // different server than the login request uses. + UserRecord userRecord = null; + for (int i = 0; i < 3; i++) { + userRecord = auth.getUser(newUserRecord.getUid()); + + if (userRecord.getUserMetadata().getLastRefreshTimestamp() != 0) { + break; } - // Ensure the lastRefreshTimestamp is approximately "now" (with a tollerance of 10 minutes). - long now = System.currentTimeMillis(); - long tollerance = TimeUnit.MINUTES.toMillis(10); - long lastRefreshTimestamp = userRecord.getUserMetadata().getLastRefreshTimestamp(); - assertTrue(now - tollerance <= lastRefreshTimestamp); - assertTrue(lastRefreshTimestamp <= now + tollerance); - } finally { - auth.deleteUser(newUserRecord.getUid()); + TimeUnit.SECONDS.sleep((long)Math.pow(2, i)); } + + // Ensure the lastRefreshTimestamp is approximately "now" (with a tolerance of 10 minutes). + long now = System.currentTimeMillis(); + long tolerance = TimeUnit.MINUTES.toMillis(10); + long lastRefreshTimestamp = userRecord.getUserMetadata().getLastRefreshTimestamp(); + assertTrue(now - tolerance <= lastRefreshTimestamp); + assertTrue(lastRefreshTimestamp <= now + tolerance); } @Test @@ -473,7 +493,7 @@ public void testCustomTokenWithIAM() throws Exception { if (token == null) { token = credentials.refreshAccessToken(); } - FirebaseOptions options = new FirebaseOptions.Builder() + FirebaseOptions options = FirebaseOptions.builder() .setCredentials(GoogleCredentials.create(token)) .setServiceAccountId(((ServiceAccountSigner) credentials).getAccount()) .setProjectId(IntegrationTestUtils.getProjectId()) @@ -507,8 +527,8 @@ public void testVerifyIdToken() throws Exception { fail("expecting exception"); } catch (ExecutionException e) { assertTrue(e.getCause() instanceof FirebaseAuthException); - assertEquals(RevocationCheckDecorator.ID_TOKEN_REVOKED_ERROR, - ((FirebaseAuthException) e.getCause()).getErrorCode()); + assertEquals(AuthErrorCode.REVOKED_ID_TOKEN, + ((FirebaseAuthException) e.getCause()).getAuthErrorCode()); } idToken = signInWithCustomToken(customToken); decoded = auth.verifyIdTokenAsync(idToken, true).get(); @@ -541,8 +561,8 @@ public void testVerifySessionCookie() throws Exception { fail("expecting exception"); } catch (ExecutionException e) { assertTrue(e.getCause() instanceof FirebaseAuthException); - assertEquals(RevocationCheckDecorator.SESSION_COOKIE_REVOKED_ERROR, - ((FirebaseAuthException) e.getCause()).getErrorCode()); + assertEquals(AuthErrorCode.REVOKED_SESSION_COOKIE, + ((FirebaseAuthException) e.getCause()).getAuthErrorCode()); } idToken = signInWithCustomToken(customToken); @@ -1009,7 +1029,14 @@ private void checkRecreateUser(String uid) throws Exception { fail("No error thrown for creating user with existing ID"); } catch (ExecutionException e) { assertTrue(e.getCause() instanceof FirebaseAuthException); - assertEquals("uid-already-exists", ((FirebaseAuthException) e.getCause()).getErrorCode()); + FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); + assertEquals(ErrorCode.ALREADY_EXISTS, authException.getErrorCode()); + assertEquals( + "The user with the provided uid already exists (DUPLICATE_LOCAL_ID).", + authException.getMessage()); + assertNotNull(authException.getCause()); + assertNotNull(authException.getHttpResponse()); + assertEquals(AuthErrorCode.UID_ALREADY_EXISTS, authException.getAuthErrorCode()); } } diff --git a/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java b/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java index bfc8d1790..e34fede1f 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java +++ b/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java @@ -25,10 +25,15 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import com.google.api.client.testing.http.MockHttpTransport; +import com.google.api.client.testing.http.MockLowLevelHttpResponse; import com.google.api.core.ApiFuture; import com.google.common.base.Defaults; import com.google.common.base.Supplier; import com.google.common.base.Suppliers; +import com.google.common.collect.ImmutableMap; +import com.google.firebase.ErrorCode; + import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; import com.google.firebase.TestOnlyImplFirebaseTrampolines; @@ -53,6 +58,11 @@ public class FirebaseAuthTest { .setCredentials(TestUtils.getCertCredential(ServiceAccount.EDITOR.asStream())) .build(); + private static final FirebaseAuthException testException = new FirebaseAuthException( + ErrorCode.INVALID_ARGUMENT, "Test error message", null, null, null); + private static final long VALID_SINCE = 1494364393; + private static final String TEST_USER = "testUser"; + @After public void cleanup() { TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); @@ -145,14 +155,14 @@ public void testProjectIdNotRequiredAtInitialization() { assertNotNull(FirebaseAuth.getInstance(app)); } - @Test(expected = IllegalArgumentException.class) + @Test(expected = NullPointerException.class) public void testAuthExceptionNullErrorCode() { - new FirebaseAuthException(null, "test"); + new FirebaseAuthException(null, "test", null, null, null); } @Test(expected = IllegalArgumentException.class) - public void testAuthExceptionEmptyErrorCode() { - new FirebaseAuthException("", "test"); + public void testAuthExceptionNullMessage() { + new FirebaseAuthException(ErrorCode.INTERNAL, null, null, null, null); } @Test @@ -219,19 +229,48 @@ public void testVerifyIdToken() throws Exception { assertEquals("idtoken", tokenVerifier.getLastTokenString()); } + @Test + public void testVerifyIdTokenWithRevocationCheck() throws Exception { + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromResult( + getFirebaseToken(VALID_SINCE + 1000)); + FirebaseAuth auth = getAuthForIdTokenVerificationWithRevocationCheck(tokenVerifier); + + FirebaseToken firebaseToken = auth.verifyIdToken("idtoken", true); + + assertEquals("testUser", firebaseToken.getUid()); + assertEquals("idtoken", tokenVerifier.getLastTokenString()); + } + + @Test + public void testVerifyIdTokenWithRevocationCheckFailure() { + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromResult( + getFirebaseToken(VALID_SINCE - 1000)); + FirebaseAuth auth = getAuthForIdTokenVerificationWithRevocationCheck(tokenVerifier); + + try { + auth.verifyIdToken("idtoken", true); + fail("No error thrown for revoked ID token"); + } catch (FirebaseAuthException e) { + assertEquals(ErrorCode.INVALID_ARGUMENT, e.getErrorCode()); + assertEquals("Firebase id token is revoked.", e.getMessage()); + assertNull(e.getCause()); + assertNull(e.getHttpResponse()); + assertEquals(AuthErrorCode.REVOKED_ID_TOKEN, e.getAuthErrorCode()); + } + + assertEquals("idtoken", tokenVerifier.getLastTokenString()); + } + @Test public void testVerifyIdTokenFailure() { - MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException( - new FirebaseAuthException("TEST_CODE", "Test error message")); + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException(testException); FirebaseAuth auth = getAuthForIdTokenVerification(tokenVerifier); try { auth.verifyIdToken("idtoken"); fail("No error thrown for invalid token"); } catch (FirebaseAuthException authException) { - assertEquals("TEST_CODE", authException.getErrorCode()); - assertEquals("Test error message", authException.getMessage()); - assertEquals("idtoken", tokenVerifier.getLastTokenString()); + assertSame(testException, authException); } } @@ -248,8 +287,7 @@ public void testVerifyIdTokenAsync() throws Exception { @Test public void testVerifyIdTokenAsyncFailure() throws InterruptedException { - MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException( - new FirebaseAuthException("TEST_CODE", "Test error message")); + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException(testException); FirebaseAuth auth = getAuthForIdTokenVerification(tokenVerifier); try { @@ -257,16 +295,13 @@ public void testVerifyIdTokenAsyncFailure() throws InterruptedException { fail("No error thrown for invalid token"); } catch (ExecutionException e) { FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); - assertEquals("TEST_CODE", authException.getErrorCode()); - assertEquals("Test error message", authException.getMessage()); - assertEquals("idtoken", tokenVerifier.getLastTokenString()); + assertSame(testException, authException); } } @Test public void testVerifyIdTokenWithCheckRevokedAsyncFailure() throws InterruptedException { - MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException( - new FirebaseAuthException("TEST_CODE", "Test error message")); + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException(testException); FirebaseAuth auth = getAuthForIdTokenVerification(tokenVerifier); try { @@ -274,9 +309,7 @@ public void testVerifyIdTokenWithCheckRevokedAsyncFailure() throws InterruptedEx fail("No error thrown for invalid token"); } catch (ExecutionException e) { FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); - assertEquals("TEST_CODE", authException.getErrorCode()); - assertEquals("Test error message", authException.getMessage()); - assertEquals("idtoken", tokenVerifier.getLastTokenString()); + assertSame(testException, authException); } } @@ -346,18 +379,47 @@ public void testVerifySessionCookie() throws Exception { @Test public void testVerifySessionCookieFailure() { - MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException( - new FirebaseAuthException("TEST_CODE", "Test error message")); + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException(testException); FirebaseAuth auth = getAuthForSessionCookieVerification(tokenVerifier); try { auth.verifySessionCookie("idtoken"); fail("No error thrown for invalid token"); } catch (FirebaseAuthException authException) { - assertEquals("TEST_CODE", authException.getErrorCode()); - assertEquals("Test error message", authException.getMessage()); - assertEquals("idtoken", tokenVerifier.getLastTokenString()); + assertSame(testException, authException); + } + } + + @Test + public void testVerifySessionCookieWithRevocationCheck() throws Exception { + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromResult( + getFirebaseToken(VALID_SINCE + 1000)); + FirebaseAuth auth = getAuthForSessionCookieVerificationWithRevocationCheck(tokenVerifier); + + FirebaseToken firebaseToken = auth.verifySessionCookie("cookie", true); + + assertEquals("testUser", firebaseToken.getUid()); + assertEquals("cookie", tokenVerifier.getLastTokenString()); + } + + @Test + public void testVerifySessionCookieWithRevocationCheckFailure() { + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromResult( + getFirebaseToken(VALID_SINCE - 1000)); + FirebaseAuth auth = getAuthForSessionCookieVerificationWithRevocationCheck(tokenVerifier); + + try { + auth.verifySessionCookie("cookie", true); + fail("No error thrown for revoked session cookie"); + } catch (FirebaseAuthException e) { + assertEquals(ErrorCode.INVALID_ARGUMENT, e.getErrorCode()); + assertEquals("Firebase session cookie is revoked.", e.getMessage()); + assertNull(e.getCause()); + assertNull(e.getHttpResponse()); + assertEquals(AuthErrorCode.REVOKED_SESSION_COOKIE, e.getAuthErrorCode()); } + + assertEquals("cookie", tokenVerifier.getLastTokenString()); } @Test @@ -373,8 +435,7 @@ public void testVerifySessionCookieAsync() throws Exception { @Test public void testVerifySessionCookieAsyncFailure() throws InterruptedException { - MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException( - new FirebaseAuthException("TEST_CODE", "Test error message")); + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException(testException); FirebaseAuth auth = getAuthForSessionCookieVerification(tokenVerifier); try { @@ -382,16 +443,13 @@ public void testVerifySessionCookieAsyncFailure() throws InterruptedException { fail("No error thrown for invalid token"); } catch (ExecutionException e) { FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); - assertEquals("TEST_CODE", authException.getErrorCode()); - assertEquals("Test error message", authException.getMessage()); - assertEquals("idtoken", tokenVerifier.getLastTokenString()); + assertSame(testException, authException); } } @Test public void testVerifySessionCookieWithCheckRevokedAsyncFailure() throws InterruptedException { - MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException( - new FirebaseAuthException("TEST_CODE", "Test error message")); + MockTokenVerifier tokenVerifier = MockTokenVerifier.fromException(testException); FirebaseAuth auth = getAuthForSessionCookieVerification(tokenVerifier); try { @@ -399,12 +457,24 @@ public void testVerifySessionCookieWithCheckRevokedAsyncFailure() throws Interru fail("No error thrown for invalid token"); } catch (ExecutionException e) { FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); - assertEquals("TEST_CODE", authException.getErrorCode()); - assertEquals("Test error message", authException.getMessage()); - assertEquals("idtoken", tokenVerifier.getLastTokenString()); + assertSame(testException, authException); } } + private FirebaseToken getFirebaseToken(String subject) { + return new FirebaseToken(ImmutableMap.of("sub", subject)); + } + + private FirebaseToken getFirebaseToken(long issuedAt) { + return new FirebaseToken(ImmutableMap.of("sub", TEST_USER, "iat", issuedAt)); + } + + FirebaseAuth getAuthForIdTokenVerificationWithRevocationCheck( + FirebaseTokenVerifier tokenVerifier) { + FirebaseApp app = getFirebaseAppForUserRetrieval(); + return getAuthForIdTokenVerification(app, Suppliers.ofInstance(tokenVerifier)); + } + private FirebaseAuth getAuthForIdTokenVerification(FirebaseTokenVerifier tokenVerifier) { return getAuthForIdTokenVerification(Suppliers.ofInstance(tokenVerifier)); } @@ -412,7 +482,13 @@ private FirebaseAuth getAuthForIdTokenVerification(FirebaseTokenVerifier tokenVe private FirebaseAuth getAuthForIdTokenVerification( Supplier tokenVerifierSupplier) { FirebaseApp app = FirebaseApp.initializeApp(firebaseOptions); - FirebaseUserManager userManager = FirebaseUserManager.builder().setFirebaseApp(app).build(); + return getAuthForIdTokenVerification(app, tokenVerifierSupplier); + } + + private FirebaseAuth getAuthForIdTokenVerification( + FirebaseApp app, + Supplier tokenVerifierSupplier) { + FirebaseUserManager userManager = FirebaseUserManager.createUserManager(app, null); return FirebaseAuth.builder() .setFirebaseApp(app) .setIdTokenVerifier(tokenVerifierSupplier) @@ -420,6 +496,12 @@ private FirebaseAuth getAuthForIdTokenVerification( .build(); } + FirebaseAuth getAuthForSessionCookieVerificationWithRevocationCheck( + FirebaseTokenVerifier tokenVerifier) { + FirebaseApp app = getFirebaseAppForUserRetrieval(); + return getAuthForSessionCookieVerification(app, Suppliers.ofInstance(tokenVerifier)); + } + private FirebaseAuth getAuthForSessionCookieVerification(FirebaseTokenVerifier tokenVerifier) { return getAuthForSessionCookieVerification(Suppliers.ofInstance(tokenVerifier)); } @@ -427,7 +509,13 @@ private FirebaseAuth getAuthForSessionCookieVerification(FirebaseTokenVerifier t private FirebaseAuth getAuthForSessionCookieVerification( Supplier tokenVerifierSupplier) { FirebaseApp app = FirebaseApp.initializeApp(firebaseOptions); - FirebaseUserManager userManager = FirebaseUserManager.builder().setFirebaseApp(app).build(); + return getAuthForSessionCookieVerification(app, tokenVerifierSupplier); + } + + private FirebaseAuth getAuthForSessionCookieVerification( + FirebaseApp app, + Supplier tokenVerifierSupplier) { + FirebaseUserManager userManager = FirebaseUserManager.createUserManager(app, null); return FirebaseAuth.builder() .setFirebaseApp(app) .setCookieVerifier(tokenVerifierSupplier) @@ -435,13 +523,22 @@ private FirebaseAuth getAuthForSessionCookieVerification( .build(); } + private FirebaseApp getFirebaseAppForUserRetrieval() { + String getUserResponse = TestUtils.loadResource("getUser.json"); + MockHttpTransport transport = new MockHttpTransport.Builder() + .setLowLevelHttpResponse(new MockLowLevelHttpResponse().setContent(getUserResponse)) + .build(); + return FirebaseApp.initializeApp(FirebaseOptions.builder() + .setCredentials(new MockGoogleCredentials("test-token")) + .setHttpTransport(transport) + .setProjectId("test-project-id") + .build()); + } + public static TestResponseInterceptor setUserManager( AbstractFirebaseAuth.Builder builder, FirebaseApp app, String tenantId) { TestResponseInterceptor interceptor = new TestResponseInterceptor(); - FirebaseUserManager userManager = FirebaseUserManager.builder() - .setFirebaseApp(app) - .setTenantId(tenantId) - .build(); + FirebaseUserManager userManager = FirebaseUserManager.createUserManager(app, tenantId); userManager.setInterceptor(interceptor); builder.setUserManager(Suppliers.ofInstance(userManager)); return interceptor; diff --git a/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java b/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java index 379da0a57..833e2ed95 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java +++ b/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java @@ -17,6 +17,7 @@ package com.google.firebase.auth; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import com.google.api.client.auth.openidconnect.IdTokenVerifier; @@ -30,15 +31,15 @@ import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.firebase.ErrorCode; import com.google.firebase.testing.ServiceAccount; import java.io.IOException; +import java.security.GeneralSecurityException; import java.util.concurrent.TimeUnit; import org.junit.Assert; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; public class FirebaseTokenVerifierImplTest { @@ -55,9 +56,6 @@ public class FirebaseTokenVerifierImplTest { private static final String TEST_TOKEN_ISSUER = "https://test.token.issuer"; - @Rule - public ExpectedException thrown = ExpectedException.none(); - private FirebaseTokenVerifier tokenVerifier; private TestTokenFactory tokenFactory; @@ -80,108 +78,188 @@ public void testVerifyToken() throws Exception { } @Test - public void testVerifyTokenWithoutKeyId() throws Exception { + public void testVerifyTokenWithoutKeyId() { String token = createTokenWithoutKeyId(); - thrown.expectMessage("Firebase test token has no \"kid\" claim."); - tokenVerifier.verifyToken(token); + try { + tokenVerifier.verifyToken(token); + } catch (FirebaseAuthException e) { + String message = "Firebase test token has no \"kid\" claim. " + + "See https://test.doc.url for details on how to retrieve a test token."; + checkInvalidTokenException(e, message); + } } @Test - public void testVerifyTokenFirebaseCustomToken() throws Exception { + public void testVerifyTokenFirebaseCustomToken() { String token = createCustomToken(); - thrown.expectMessage("verifyTestToken() expects a test token, but was given a custom token."); - tokenVerifier.verifyToken(token); + try { + tokenVerifier.verifyToken(token); + } catch (FirebaseAuthException e) { + String message = "verifyTestToken() expects a test token, but was given a custom token. " + + "See https://test.doc.url for details on how to retrieve a test token."; + checkInvalidTokenException(e, message); + } } @Test - public void testVerifyTokenIncorrectAlgorithm() throws Exception { + public void testVerifyTokenIncorrectAlgorithm() { String token = createTokenWithIncorrectAlgorithm(); - thrown.expectMessage("Firebase test token has incorrect algorithm."); - tokenVerifier.verifyToken(token); + try { + tokenVerifier.verifyToken(token); + } catch (FirebaseAuthException e) { + String message = "Firebase test token has incorrect algorithm. " + + "Expected \"RS256\" but got \"HSA\". " + + "See https://test.doc.url for details on how to retrieve a test token."; + checkInvalidTokenException(e, message); + } } @Test - public void testVerifyTokenIncorrectAudience() throws Exception { + public void testVerifyTokenIncorrectAudience() { String token = createTokenWithIncorrectAudience(); - thrown.expectMessage("Firebase test token has incorrect \"aud\" (audience) claim."); - tokenVerifier.verifyToken(token); + try { + tokenVerifier.verifyToken(token); + } catch (FirebaseAuthException e) { + String message = "Firebase test token has incorrect \"aud\" (audience) claim. " + + "Expected \"proj-test-101\" but got \"invalid-audience\". " + + "Make sure the test token comes from the same Firebase project as the service account " + + "used to authenticate this SDK. " + + "See https://test.doc.url for details on how to retrieve a test token."; + checkInvalidTokenException(e, message); + } } @Test - public void testVerifyTokenIncorrectIssuer() throws Exception { + public void testVerifyTokenIncorrectIssuer() { String token = createTokenWithIncorrectIssuer(); - thrown.expectMessage("Firebase test token has incorrect \"iss\" (issuer) claim."); - tokenVerifier.verifyToken(token); + try { + tokenVerifier.verifyToken(token); + } catch (FirebaseAuthException e) { + String message = "Firebase test token has incorrect \"iss\" (issuer) claim. " + + "Expected \"https://test.token.issuer\" but got " + + "\"https://incorrect.issuer.prefix/proj-test-101\". Make sure the test token comes " + + "from the same Firebase project as the service account used to authenticate this SDK. " + + "See https://test.doc.url for details on how to retrieve a test token."; + checkInvalidTokenException(e, message); + } } @Test - public void testVerifyTokenMissingSubject() throws Exception { + public void testVerifyTokenMissingSubject() { String token = createTokenWithSubject(null); - thrown.expectMessage("Firebase test token has no \"sub\" (subject) claim."); - tokenVerifier.verifyToken(token); + try { + tokenVerifier.verifyToken(token); + } catch (FirebaseAuthException e) { + String message = "Firebase test token has no \"sub\" (subject) claim. " + + "See https://test.doc.url for details on how to retrieve a test token."; + checkInvalidTokenException(e, message); + } } @Test - public void testVerifyTokenEmptySubject() throws Exception { + public void testVerifyTokenEmptySubject() { String token = createTokenWithSubject(""); - thrown.expectMessage("Firebase test token has an empty string \"sub\" (subject) claim."); - tokenVerifier.verifyToken(token); + try { + tokenVerifier.verifyToken(token); + } catch (FirebaseAuthException e) { + String message = "Firebase test token has an empty string \"sub\" (subject) claim. " + + "See https://test.doc.url for details on how to retrieve a test token."; + checkInvalidTokenException(e, message); + } } @Test - public void testVerifyTokenLongSubject() throws Exception { + public void testVerifyTokenLongSubject() { String token = createTokenWithSubject(Strings.repeat("a", 129)); - thrown.expectMessage( - "Firebase test token has \"sub\" (subject) claim longer than 128 characters."); - tokenVerifier.verifyToken(token); + try { + tokenVerifier.verifyToken(token); + } catch (FirebaseAuthException e) { + String message = "Firebase test token has \"sub\" (subject) claim longer " + + "than 128 characters. " + + "See https://test.doc.url for details on how to retrieve a test token."; + checkInvalidTokenException(e, message); + } } @Test - public void testVerifyTokenIssuedAtInFuture() throws Exception { + public void testVerifyTokenIssuedAtInFuture() { long tenMinutesIntoTheFuture = (TestTokenFactory.CLOCK.currentTimeMillis() / 1000) + TimeUnit.MINUTES.toSeconds(10); String token = createTokenWithTimestamps( tenMinutesIntoTheFuture, tenMinutesIntoTheFuture + TimeUnit.HOURS.toSeconds(1)); - thrown.expectMessage("Firebase test token has expired or is not yet valid."); - tokenVerifier.verifyToken(token); + try { + tokenVerifier.verifyToken(token); + } catch (FirebaseAuthException e) { + String message = "Firebase test token is not yet valid. " + + "See https://test.doc.url for details on how to retrieve a test token."; + checkInvalidTokenException(e, message); + } } @Test - public void testVerifyTokenExpired() throws Exception { + public void testVerifyTokenExpired() { long twoHoursInPast = (TestTokenFactory.CLOCK.currentTimeMillis() / 1000) - TimeUnit.HOURS.toSeconds(2); String token = createTokenWithTimestamps( twoHoursInPast, twoHoursInPast + TimeUnit.HOURS.toSeconds(1)); - thrown.expectMessage("Firebase test token has expired or is not yet valid."); - tokenVerifier.verifyToken(token); + try { + tokenVerifier.verifyToken(token); + } catch (FirebaseAuthException e) { + String message = "Firebase test token has expired. " + + "Get a fresh test token and try again. " + + "See https://test.doc.url for details on how to retrieve a test token."; + checkException(e, message, AuthErrorCode.EXPIRED_ID_TOKEN); + } } @Test - public void testVerifyTokenIncorrectCert() throws Exception { + public void testVerifyTokenSignatureMismatch() { String token = tokenFactory.createToken(); GooglePublicKeysManager publicKeysManager = newPublicKeysManager( ServiceAccount.NONE.getCert()); FirebaseTokenVerifier tokenVerifier = newTestTokenVerifier(publicKeysManager); - thrown.expectMessage("Failed to verify the signature of Firebase test token. " - + "See https://test.doc.url for details on how to retrieve a test token."); - tokenVerifier.verifyToken(token); + try { + tokenVerifier.verifyToken(token); + } catch (FirebaseAuthException e) { + String message = "Failed to verify the signature of Firebase test token. " + + "See https://test.doc.url for details on how to retrieve a test token."; + checkInvalidTokenException(e, message); + } } @Test - public void verifyTokenCertificateError() { + public void testMalformedCert() { + String token = tokenFactory.createToken(); + GooglePublicKeysManager publicKeysManager = newPublicKeysManager("malformed.cert"); + FirebaseTokenVerifier tokenVerifier = newTestTokenVerifier(publicKeysManager); + + try { + tokenVerifier.verifyToken(token); + } catch (FirebaseAuthException e) { + String message = "Error while fetching public key certificates: Could not parse certificate"; + assertEquals(ErrorCode.UNKNOWN, e.getErrorCode()); + assertTrue(e.getMessage().startsWith(message)); + assertTrue(e.getCause() instanceof GeneralSecurityException); + assertNull(e.getHttpResponse()); + assertEquals(AuthErrorCode.CERTIFICATE_FETCH_FAILED, e.getAuthErrorCode()); + } + } + + @Test + public void testCertificateFetchError() { MockHttpTransport failingTransport = new MockHttpTransport() { @Override public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { @@ -195,26 +273,57 @@ public LowLevelHttpRequest buildRequest(String method, String url) throws IOExce try { idTokenVerifier.verifyToken(token); Assert.fail("No exception thrown"); - } catch (FirebaseAuthException expected) { - assertTrue(expected.getCause() instanceof IOException); - assertEquals("Expected error", expected.getCause().getMessage()); + } catch (FirebaseAuthException e) { + String message = "Error while fetching public key certificates: Expected error"; + assertEquals(ErrorCode.UNKNOWN, e.getErrorCode()); + assertEquals(message, e.getMessage()); + assertTrue(e.getCause() instanceof IOException); + assertNull(e.getHttpResponse()); + assertEquals(AuthErrorCode.CERTIFICATE_FETCH_FAILED, e.getAuthErrorCode()); } } @Test - public void testLegacyCustomToken() throws Exception { - thrown.expectMessage( - "verifyTestToken() expects a test token, but was given a legacy custom token."); - tokenVerifier.verifyToken(LEGACY_CUSTOM_TOKEN); + public void testMalformedSignature() { + String token = tokenFactory.createToken(); + String[] segments = token.split("\\."); + token = String.format("%s.%s.%s", segments[0], segments[1], "MalformedSignature"); + + try { + tokenVerifier.verifyToken(token); + } catch (FirebaseAuthException e) { + String message = "Failed to verify the signature of Firebase test token. " + + "See https://test.doc.url for details on how to retrieve a test token."; + checkInvalidTokenException(e, message); + } } @Test - public void testMalformedToken() throws Exception { - thrown.expectMessage( - "Failed to parse Firebase test token. Make sure you passed a string that represents a " - + "complete and valid JWT. See https://test.doc.url for details on how to retrieve " - + "a test token."); - tokenVerifier.verifyToken("not.a.jwt"); + public void testLegacyCustomToken() { + try { + tokenVerifier.verifyToken(LEGACY_CUSTOM_TOKEN); + } catch (FirebaseAuthException e) { + String message = "verifyTestToken() expects a test token, but was given a " + + "legacy custom token. " + + "See https://test.doc.url for details on how to retrieve a test token."; + checkInvalidTokenException(e, message); + } + } + + @Test + public void testMalformedToken() { + try { + tokenVerifier.verifyToken("not.a.jwt"); + } catch (FirebaseAuthException e) { + String message = "Failed to parse Firebase test token. " + + "Make sure you passed a string that represents a complete and valid JWT. " + + "See https://test.doc.url for details on how to retrieve a test token."; + assertEquals(ErrorCode.INVALID_ARGUMENT, e.getErrorCode()); + assertEquals(message, e.getMessage()); + assertTrue(e.getCause() instanceof IllegalArgumentException); + assertNull(e.getHttpResponse()); + assertEquals(AuthErrorCode.INVALID_ID_TOKEN, e.getAuthErrorCode()); + } } @Test @@ -238,7 +347,7 @@ public void testVerifyTokenDifferentTenantIds() { .build() .verifyToken(createTokenWithTenantId("TENANT_2")); } catch (FirebaseAuthException e) { - assertEquals(FirebaseTokenVerifierImpl.TENANT_ID_MISMATCH_ERROR, e.getErrorCode()); + assertEquals(AuthErrorCode.TENANT_ID_MISMATCH, e.getAuthErrorCode()); assertEquals( "The tenant ID ('TENANT_2') of the token did not match the expected value ('TENANT_1')", e.getMessage()); @@ -253,7 +362,7 @@ public void testVerifyTokenMissingTenantId() { .build() .verifyToken(tokenFactory.createToken()); } catch (FirebaseAuthException e) { - assertEquals(FirebaseTokenVerifierImpl.TENANT_ID_MISMATCH_ERROR, e.getErrorCode()); + assertEquals(AuthErrorCode.TENANT_ID_MISMATCH, e.getAuthErrorCode()); assertEquals( "The tenant ID ('') of the token did not match the expected value ('TENANT_ID')", e.getMessage()); @@ -267,7 +376,7 @@ public void testVerifyTokenUnexpectedTenantId() { .build() .verifyToken(createTokenWithTenantId("TENANT_ID")); } catch (FirebaseAuthException e) { - assertEquals(FirebaseTokenVerifierImpl.TENANT_ID_MISMATCH_ERROR, e.getErrorCode()); + assertEquals(AuthErrorCode.TENANT_ID_MISMATCH, e.getAuthErrorCode()); assertEquals( "The tenant ID ('TENANT_ID') of the token did not match the expected value ('')", e.getMessage()); @@ -323,13 +432,8 @@ private GooglePublicKeysManager newPublicKeysManager(HttpTransport transport) { } private FirebaseTokenVerifier newTestTokenVerifier(GooglePublicKeysManager publicKeysManager) { - return FirebaseTokenVerifierImpl.builder() - .setShortName("test token") - .setMethod("verifyTestToken()") - .setDocUrl("https://test.doc.url") - .setJsonFactory(TestTokenFactory.JSON_FACTORY) + return fullyPopulatedBuilder() .setPublicKeysManager(publicKeysManager) - .setIdTokenVerifier(newIdTokenVerifier()) .build(); } @@ -340,6 +444,8 @@ private FirebaseTokenVerifierImpl.Builder fullyPopulatedBuilder() { .setDocUrl("https://test.doc.url") .setJsonFactory(TestTokenFactory.JSON_FACTORY) .setPublicKeysManager(newPublicKeysManager(ServiceAccount.EDITOR.getCert())) + .setInvalidTokenErrorCode(AuthErrorCode.INVALID_ID_TOKEN) + .setExpiredTokenErrorCode(AuthErrorCode.EXPIRED_ID_TOKEN) .setIdTokenVerifier(newIdTokenVerifier()); } @@ -396,6 +502,18 @@ private String createTokenWithTimestamps(long issuedAtSeconds, long expirationSe return tokenFactory.createToken(payload); } + private void checkInvalidTokenException(FirebaseAuthException e, String message) { + checkException(e, message, AuthErrorCode.INVALID_ID_TOKEN); + } + + private void checkException(FirebaseAuthException e, String message, AuthErrorCode errorCode) { + assertEquals(ErrorCode.INVALID_ARGUMENT, e.getErrorCode()); + assertEquals(message, e.getMessage()); + assertNull(e.getCause()); + assertNull(e.getHttpResponse()); + assertEquals(errorCode, e.getAuthErrorCode()); + } + private String createTokenWithTenantId(String tenantId) { Payload payload = tokenFactory.createTokenPayload(); payload.set("firebase", ImmutableMap.of("tenant", tenantId)); diff --git a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java index 7012ef6bf..772f9ba8d 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java +++ b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java @@ -40,18 +40,17 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; +import com.google.firebase.ErrorCode; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; import com.google.firebase.TestOnlyImplFirebaseTrampolines; import com.google.firebase.auth.FirebaseUserManager.EmailLinkType; -import com.google.firebase.auth.internal.AuthHttpClient; import com.google.firebase.auth.multitenancy.TenantAwareFirebaseAuth; import com.google.firebase.auth.multitenancy.TenantManager; import com.google.firebase.internal.SdkUtils; import com.google.firebase.testing.MultiRequestMockHttpTransport; import com.google.firebase.testing.TestResponseInterceptor; import com.google.firebase.testing.TestUtils; - import java.io.ByteArrayOutputStream; import java.io.IOException; import java.math.BigDecimal; @@ -60,7 +59,6 @@ import java.util.HashSet; import java.util.List; import java.util.Map; - import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import org.junit.After; @@ -92,6 +90,10 @@ public class FirebaseUserManagerTest { private static final String TENANTS_BASE_URL = PROJECT_BASE_URL + "/tenants"; + private static final String SAML_RESPONSE = TestUtils.loadResource("saml.json"); + + private static final String OIDC_RESPONSE = TestUtils.loadResource("oidc.json"); + @After public void tearDown() { TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); @@ -99,7 +101,7 @@ public void tearDown() { @Test public void testProjectIdRequired() { - FirebaseApp.initializeApp(new FirebaseOptions.Builder() + FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(credentials) .build()); FirebaseAuth auth = FirebaseAuth.getInstance(); @@ -133,7 +135,12 @@ public void testGetUserWithNotFoundError() throws Exception { } catch (ExecutionException e) { assertThat(e.getCause(), instanceOf(FirebaseAuthException.class)); FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); - assertEquals(AuthHttpClient.USER_NOT_FOUND_ERROR, authException.getErrorCode()); + assertEquals(ErrorCode.NOT_FOUND, authException.getErrorCode()); + assertEquals( + "No user record found for the provided user ID: testuser", authException.getMessage()); + assertNull(authException.getCause()); + assertNotNull(authException.getHttpResponse()); + assertEquals(AuthErrorCode.USER_NOT_FOUND, authException.getAuthErrorCode()); } } @@ -156,7 +163,13 @@ public void testGetUserByEmailWithNotFoundError() throws Exception { } catch (ExecutionException e) { assertThat(e.getCause(), instanceOf(FirebaseAuthException.class)); FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); - assertEquals(AuthHttpClient.USER_NOT_FOUND_ERROR, authException.getErrorCode()); + assertEquals(ErrorCode.NOT_FOUND, authException.getErrorCode()); + assertEquals( + "No user record found for the provided email: testuser@example.com", + authException.getMessage()); + assertNull(authException.getCause()); + assertNotNull(authException.getHttpResponse()); + assertEquals(AuthErrorCode.USER_NOT_FOUND, authException.getAuthErrorCode()); } } @@ -179,13 +192,19 @@ public void testGetUserByPhoneNumberWithNotFoundError() throws Exception { } catch (ExecutionException e) { assertThat(e.getCause(), instanceOf(FirebaseAuthException.class)); FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); - assertEquals(AuthHttpClient.USER_NOT_FOUND_ERROR, authException.getErrorCode()); + assertEquals(ErrorCode.NOT_FOUND, authException.getErrorCode()); + assertEquals( + "No user record found for the provided phone number: +1234567890", + authException.getMessage()); + assertNull(authException.getCause()); + assertNotNull(authException.getHttpResponse()); + assertEquals(AuthErrorCode.USER_NOT_FOUND, authException.getAuthErrorCode()); } } @Test public void testGetUsersExceeds100() throws Exception { - FirebaseApp.initializeApp(new FirebaseOptions.Builder() + FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(credentials) .build()); List identifiers = new ArrayList<>(); @@ -203,7 +222,7 @@ public void testGetUsersExceeds100() throws Exception { @Test public void testGetUsersNull() throws Exception { - FirebaseApp.initializeApp(new FirebaseOptions.Builder() + FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(credentials) .build()); try { @@ -262,7 +281,7 @@ public void testGetUsersMultipleIdentifierTypes() throws Exception { ).replace("'", "\"")); UidIdentifier doesntExist = new UidIdentifier("this-uid-doesnt-exist"); - List ids = ImmutableList.of( + List ids = ImmutableList.of( new UidIdentifier("uid1"), new EmailIdentifier("user2@example.com"), new PhoneIdentifier("+15555550003"), @@ -284,7 +303,7 @@ private Collection userRecordsToUids(Collection userRecords) } @Test - public void testInvalidUidIdentifier() throws Exception { + public void testInvalidUidIdentifier() { try { new UidIdentifier("too long " + Strings.repeat(".", 128)); fail("No error thrown for invalid uid"); @@ -294,7 +313,7 @@ public void testInvalidUidIdentifier() throws Exception { } @Test - public void testInvalidEmailIdentifier() throws Exception { + public void testInvalidEmailIdentifier() { try { new EmailIdentifier("invalid email addr"); fail("No error thrown for invalid email"); @@ -304,7 +323,7 @@ public void testInvalidEmailIdentifier() throws Exception { } @Test - public void testInvalidPhoneIdentifier() throws Exception { + public void testInvalidPhoneIdentifier() { try { new PhoneIdentifier("invalid phone number"); fail("No error thrown for invalid phone number"); @@ -314,7 +333,7 @@ public void testInvalidPhoneIdentifier() throws Exception { } @Test - public void testInvalidProviderIdentifier() throws Exception { + public void testInvalidProviderIdentifier() { try { new ProviderIdentifier("", "valid-uid"); fail("No error thrown for invalid provider id"); @@ -437,8 +456,8 @@ public void testDeleteUser() throws Exception { } @Test - public void testDeleteUsersExceeds1000() throws Exception { - FirebaseApp.initializeApp(new FirebaseOptions.Builder() + public void testDeleteUsersExceeds1000() { + FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(credentials) .build()); List ids = new ArrayList<>(); @@ -454,8 +473,8 @@ public void testDeleteUsersExceeds1000() throws Exception { } @Test - public void testDeleteUsersInvalidId() throws Exception { - FirebaseApp.initializeApp(new FirebaseOptions.Builder() + public void testDeleteUsersInvalidId() { + FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(credentials) .build()); try { @@ -773,9 +792,15 @@ public void call(FirebaseAuth auth) throws Exception { MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); FirebaseAuth auth = getRetryDisabledAuth(response); + Map codes = ImmutableMap.of( + 302, ErrorCode.UNKNOWN, + 400, ErrorCode.INVALID_ARGUMENT, + 401, ErrorCode.UNAUTHENTICATED, + 404, ErrorCode.NOT_FOUND, + 500, ErrorCode.INTERNAL); // Test for common HTTP error codes - for (int code : ImmutableList.of(302, 400, 401, 404, 500)) { + for (int code : codes.keySet()) { for (UserManagerOp operation : operations) { // Need to reset these every iteration response.setContent("{}"); @@ -786,15 +811,17 @@ public void call(FirebaseAuth auth) throws Exception { } catch (ExecutionException e) { assertThat(e.getCause(), instanceOf(FirebaseAuthException.class)); FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); - String msg = String.format("Unexpected HTTP response with status: %d; body: {}", code); + assertEquals(codes.get(code), authException.getErrorCode()); + String msg = String.format("Unexpected HTTP response with status: %d\n{}", code); assertEquals(msg, authException.getMessage()); - assertThat(authException.getCause(), instanceOf(HttpResponseException.class)); - assertEquals(AuthHttpClient.INTERNAL_ERROR, authException.getErrorCode()); + assertTrue(authException.getCause() instanceof HttpResponseException); + assertNotNull(authException.getHttpResponse()); + assertNull(authException.getAuthErrorCode()); } } } - // Test error payload parsing + // Test error payload with code for (UserManagerOp operation : operations) { response.setContent("{\"error\": {\"message\": \"USER_NOT_FOUND\"}}"); response.setStatusCode(500); @@ -804,9 +831,33 @@ public void call(FirebaseAuth auth) throws Exception { } catch (ExecutionException e) { assertThat(e.getCause().toString(), e.getCause(), instanceOf(FirebaseAuthException.class)); FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); - assertEquals("Firebase Auth service responded with an error", authException.getMessage()); - assertThat(authException.getCause(), instanceOf(HttpResponseException.class)); - assertEquals(AuthHttpClient.USER_NOT_FOUND_ERROR, authException.getErrorCode()); + assertEquals(ErrorCode.NOT_FOUND, authException.getErrorCode()); + assertEquals( + "No user record found for the given identifier (USER_NOT_FOUND).", + authException.getMessage()); + assertTrue(authException.getCause() instanceof HttpResponseException); + assertNotNull(authException.getHttpResponse()); + assertEquals(AuthErrorCode.USER_NOT_FOUND, authException.getAuthErrorCode()); + } + } + + // Test error payload with code and details + for (UserManagerOp operation : operations) { + response.setContent("{\"error\": {\"message\": \"USER_NOT_FOUND: Extra details\"}}"); + response.setStatusCode(500); + try { + operation.call(auth); + fail("No error thrown for HTTP error"); + } catch (ExecutionException e) { + assertTrue(e.getCause().toString(), e.getCause() instanceof FirebaseAuthException); + FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); + assertEquals(ErrorCode.NOT_FOUND, authException.getErrorCode()); + assertEquals( + "No user record found for the given identifier (USER_NOT_FOUND): Extra details", + authException.getMessage()); + assertTrue(authException.getCause() instanceof HttpResponseException); + assertNotNull(authException.getHttpResponse()); + assertEquals(AuthErrorCode.USER_NOT_FOUND, authException.getAuthErrorCode()); } } } @@ -820,8 +871,12 @@ public void testGetUserMalformedJsonError() throws Exception { } catch (ExecutionException e) { assertThat(e.getCause(), instanceOf(FirebaseAuthException.class)); FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); - assertThat(authException.getCause(), instanceOf(IOException.class)); - assertEquals(AuthHttpClient.INTERNAL_ERROR, authException.getErrorCode()); + assertEquals(ErrorCode.UNKNOWN, authException.getErrorCode()); + assertTrue( + authException.getMessage().startsWith("Error while parsing HTTP response: ")); + assertTrue(authException.getCause() instanceof IOException); + assertNotNull(authException.getHttpResponse()); + assertNull(authException.getAuthErrorCode()); } } @@ -837,10 +892,12 @@ public void testGetUserUnexpectedHttpError() throws Exception { } catch (ExecutionException e) { assertThat(e.getCause(), instanceOf(FirebaseAuthException.class)); FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); - assertThat(authException.getCause(), instanceOf(HttpResponseException.class)); - assertEquals("Unexpected HTTP response with status: 500; body: {\"not\" json}", + assertEquals(ErrorCode.INTERNAL, authException.getErrorCode()); + assertEquals("Unexpected HTTP response with status: 500\n{\"not\" json}", authException.getMessage()); - assertEquals(AuthHttpClient.INTERNAL_ERROR, authException.getErrorCode()); + assertTrue(authException.getCause() instanceof HttpResponseException); + assertNotNull(authException.getHttpResponse()); + assertNull(authException.getAuthErrorCode()); } } @@ -848,7 +905,7 @@ public void testGetUserUnexpectedHttpError() throws Exception { public void testTimeout() throws Exception { MockHttpTransport transport = new MultiRequestMockHttpTransport(ImmutableList.of( new MockLowLevelHttpResponse().setContent(TestUtils.loadResource("getUser.json")))); - FirebaseApp.initializeApp(new FirebaseOptions.Builder() + FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(credentials) .setProjectId("test-project-id") .setHttpTransport(transport) @@ -1375,8 +1432,33 @@ public void testHttpErrorWithCode() { userManager.getEmailActionLink(EmailLinkType.PASSWORD_RESET, "test@example.com", null); fail("No exception thrown for HTTP error"); } catch (FirebaseAuthException e) { - assertEquals("unauthorized-continue-uri", e.getErrorCode()); - assertThat(e.getCause(), instanceOf(HttpResponseException.class)); + assertEquals(ErrorCode.INVALID_ARGUMENT, e.getErrorCode()); + assertEquals( + "The domain of the continue URL is not whitelisted (UNAUTHORIZED_DOMAIN).", + e.getMessage()); + assertEquals(AuthErrorCode.UNAUTHORIZED_CONTINUE_URL, e.getAuthErrorCode()); + assertTrue(e.getCause() instanceof HttpResponseException); + assertNotNull(e.getHttpResponse()); + } + } + + @Test + public void testHttpErrorWithUnknownCode() { + String content = "{\"error\": {\"message\": \"SOMETHING_NEW\"}}"; + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() + .setContent(content) + .setStatusCode(500); + FirebaseAuth auth = getRetryDisabledAuth(response); + FirebaseUserManager userManager = auth.getUserManager(); + try { + userManager.getEmailActionLink(EmailLinkType.PASSWORD_RESET, "test@example.com", null); + fail("No exception thrown for HTTP error"); + } catch (FirebaseAuthException e) { + assertEquals(ErrorCode.INTERNAL, e.getErrorCode()); + assertEquals("Unexpected HTTP response with status: 500\n" + content, e.getMessage()); + assertNull(e.getAuthErrorCode()); + assertTrue(e.getCause() instanceof HttpResponseException); + assertNotNull(e.getHttpResponse()); } } @@ -1391,15 +1473,17 @@ public void testUnexpectedHttpError() { userManager.getEmailActionLink(EmailLinkType.PASSWORD_RESET, "test@example.com", null); fail("No exception thrown for HTTP error"); } catch (FirebaseAuthException e) { - assertEquals("internal-error", e.getErrorCode()); - assertThat(e.getCause(), instanceOf(HttpResponseException.class)); + assertEquals(ErrorCode.INTERNAL, e.getErrorCode()); + assertEquals("Unexpected HTTP response with status: 500\n{}", e.getMessage()); + assertTrue(e.getCause() instanceof HttpResponseException); + assertNotNull(e.getHttpResponse()); + assertNull(e.getAuthErrorCode()); } } @Test public void testCreateOidcProvider() throws Exception { - TestResponseInterceptor interceptor = initializeAppForUserManagement( - TestUtils.loadResource("oidc.json")); + TestResponseInterceptor interceptor = initializeAppForUserManagement(OIDC_RESPONSE); OidcProviderConfig.CreateRequest createRequest = new OidcProviderConfig.CreateRequest() .setProviderId("oidc.provider-id") @@ -1424,8 +1508,7 @@ public void testCreateOidcProvider() throws Exception { @Test public void testCreateOidcProviderAsync() throws Exception { - TestResponseInterceptor interceptor = initializeAppForUserManagement( - TestUtils.loadResource("oidc.json")); + TestResponseInterceptor interceptor = initializeAppForUserManagement(OIDC_RESPONSE); OidcProviderConfig.CreateRequest createRequest = new OidcProviderConfig.CreateRequest() .setProviderId("oidc.provider-id") @@ -1451,8 +1534,7 @@ public void testCreateOidcProviderAsync() throws Exception { @Test public void testCreateOidcProviderMinimal() throws Exception { - TestResponseInterceptor interceptor = initializeAppForUserManagement( - TestUtils.loadResource("oidc.json")); + TestResponseInterceptor interceptor = initializeAppForUserManagement(OIDC_RESPONSE); // Only the 'enabled' and 'displayName' fields can be omitted from an OIDC provider config // creation request. OidcProviderConfig.CreateRequest createRequest = @@ -1474,24 +1556,32 @@ public void testCreateOidcProviderMinimal() throws Exception { } @Test - public void testCreateOidcProviderError() throws Exception { - TestResponseInterceptor interceptor = - initializeAppForUserManagementWithStatusCode(404, - "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}"); + public void testCreateOidcProviderError() { + String message = "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}"; + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() + .setContent(message) + .setStatusCode(500); + FirebaseAuth auth = getRetryDisabledAuth(response); OidcProviderConfig.CreateRequest createRequest = new OidcProviderConfig.CreateRequest().setProviderId("oidc.provider-id"); + try { - FirebaseAuth.getInstance().createOidcProviderConfig(createRequest); + auth.createOidcProviderConfig(createRequest); fail("No error thrown for invalid response"); } catch (FirebaseAuthException e) { - assertEquals(AuthHttpClient.INTERNAL_ERROR, e.getErrorCode()); + assertEquals(ErrorCode.INTERNAL, e.getErrorCode()); + assertEquals( + "Unexpected HTTP response with status: 500\n" + message, + e.getMessage()); + assertTrue(e.getCause() instanceof HttpResponseException); + assertNotNull(e.getHttpResponse()); + assertNull(e.getAuthErrorCode()); } - checkUrl(interceptor, "POST", PROJECT_BASE_URL + "/oauthIdpConfigs"); } @Test public void testCreateOidcProviderMissingId() throws Exception { - initializeAppForUserManagement(TestUtils.loadResource("oidc.json")); + initializeAppForUserManagement(OIDC_RESPONSE); OidcProviderConfig.CreateRequest createRequest = new OidcProviderConfig.CreateRequest() .setDisplayName("DISPLAY_NAME") @@ -1509,8 +1599,7 @@ public void testCreateOidcProviderMissingId() throws Exception { @Test public void testTenantAwareCreateOidcProvider() throws Exception { TestResponseInterceptor interceptor = initializeAppForTenantAwareUserManagement( - "TENANT_ID", - TestUtils.loadResource("oidc.json")); + "TENANT_ID", OIDC_RESPONSE); OidcProviderConfig.CreateRequest createRequest = new OidcProviderConfig.CreateRequest() .setProviderId("oidc.provider-id") @@ -1529,8 +1618,7 @@ public void testTenantAwareCreateOidcProvider() throws Exception { @Test public void testUpdateOidcProvider() throws Exception { - TestResponseInterceptor interceptor = initializeAppForUserManagement( - TestUtils.loadResource("oidc.json")); + TestResponseInterceptor interceptor = initializeAppForUserManagement(OIDC_RESPONSE); OidcProviderConfig.UpdateRequest request = new OidcProviderConfig.UpdateRequest("oidc.provider-id") .setDisplayName("DISPLAY_NAME") @@ -1554,8 +1642,7 @@ public void testUpdateOidcProvider() throws Exception { @Test public void testUpdateOidcProviderAsync() throws Exception { - TestResponseInterceptor interceptor = initializeAppForUserManagement( - TestUtils.loadResource("oidc.json")); + TestResponseInterceptor interceptor = initializeAppForUserManagement(OIDC_RESPONSE); OidcProviderConfig.UpdateRequest request = new OidcProviderConfig.UpdateRequest("oidc.provider-id") .setDisplayName("DISPLAY_NAME") @@ -1580,8 +1667,7 @@ public void testUpdateOidcProviderAsync() throws Exception { @Test public void testUpdateOidcProviderMinimal() throws Exception { - TestResponseInterceptor interceptor = initializeAppForUserManagement( - TestUtils.loadResource("oidc.json")); + TestResponseInterceptor interceptor = initializeAppForUserManagement(OIDC_RESPONSE); OidcProviderConfig.UpdateRequest request = new OidcProviderConfig.UpdateRequest("oidc.provider-id").setDisplayName("DISPLAY_NAME"); @@ -1599,7 +1685,7 @@ public void testUpdateOidcProviderMinimal() throws Exception { @Test public void testUpdateOidcProviderConfigNoValues() throws Exception { - initializeAppForUserManagement(TestUtils.loadResource("oidc.json")); + initializeAppForUserManagement(OIDC_RESPONSE); try { FirebaseAuth.getInstance().updateOidcProviderConfig( new OidcProviderConfig.UpdateRequest("oidc.provider-id")); @@ -1611,25 +1697,32 @@ public void testUpdateOidcProviderConfigNoValues() throws Exception { @Test public void testUpdateOidcProviderConfigError() { - TestResponseInterceptor interceptor = - initializeAppForUserManagementWithStatusCode(404, - "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}"); + String message = "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}"; + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() + .setContent(message) + .setStatusCode(500); + FirebaseAuth auth = getRetryDisabledAuth(response); OidcProviderConfig.UpdateRequest request = new OidcProviderConfig.UpdateRequest("oidc.provider-id").setDisplayName("DISPLAY_NAME"); + try { - FirebaseAuth.getInstance().updateOidcProviderConfig(request); + auth.updateOidcProviderConfig(request); fail("No error thrown for invalid response"); } catch (FirebaseAuthException e) { - assertEquals(AuthHttpClient.INTERNAL_ERROR, e.getErrorCode()); + assertEquals(ErrorCode.INTERNAL, e.getErrorCode()); + assertEquals( + "Unexpected HTTP response with status: 500\n" + message, + e.getMessage()); + assertTrue(e.getCause() instanceof HttpResponseException); + assertNotNull(e.getHttpResponse()); + assertNull(e.getAuthErrorCode()); } - checkUrl(interceptor, "PATCH", PROJECT_BASE_URL + "/oauthIdpConfigs/oidc.provider-id"); } @Test public void testTenantAwareUpdateOidcProvider() throws Exception { TestResponseInterceptor interceptor = initializeAppForTenantAwareUserManagement( - "TENANT_ID", - TestUtils.loadResource("oidc.json")); + "TENANT_ID", OIDC_RESPONSE); TenantAwareFirebaseAuth tenantAwareAuth = FirebaseAuth.getInstance().getTenantManager().getAuthForTenant("TENANT_ID"); OidcProviderConfig.UpdateRequest request = @@ -1656,8 +1749,7 @@ public void testTenantAwareUpdateOidcProvider() throws Exception { @Test public void testGetOidcProviderConfig() throws Exception { - TestResponseInterceptor interceptor = initializeAppForUserManagement( - TestUtils.loadResource("oidc.json")); + TestResponseInterceptor interceptor = initializeAppForUserManagement(OIDC_RESPONSE); OidcProviderConfig config = FirebaseAuth.getInstance().getOidcProviderConfig("oidc.provider-id"); @@ -1669,8 +1761,7 @@ public void testGetOidcProviderConfig() throws Exception { @Test public void testGetOidcProviderConfigAsync() throws Exception { - TestResponseInterceptor interceptor = initializeAppForUserManagement( - TestUtils.loadResource("oidc.json")); + TestResponseInterceptor interceptor = initializeAppForUserManagement(OIDC_RESPONSE); OidcProviderConfig config = FirebaseAuth.getInstance().getOidcProviderConfigAsync("oidc.provider-id").get(); @@ -1682,7 +1773,7 @@ public void testGetOidcProviderConfigAsync() throws Exception { @Test public void testGetOidcProviderConfigMissingId() throws Exception { - initializeAppForUserManagement(TestUtils.loadResource("oidc.json")); + initializeAppForUserManagement(OIDC_RESPONSE); try { FirebaseAuth.getInstance().getOidcProviderConfig(null); @@ -1694,7 +1785,7 @@ public void testGetOidcProviderConfigMissingId() throws Exception { @Test public void testGetOidcProviderConfigInvalidId() throws Exception { - initializeAppForUserManagement(TestUtils.loadResource("oidc.json")); + initializeAppForUserManagement(OIDC_RESPONSE); try { FirebaseAuth.getInstance().getOidcProviderConfig("saml.invalid-oidc-provider-id"); @@ -1705,7 +1796,7 @@ public void testGetOidcProviderConfigInvalidId() throws Exception { } @Test - public void testGetOidcProviderConfigWithNotFoundError() throws Exception { + public void testGetOidcProviderConfigWithNotFoundError() { TestResponseInterceptor interceptor = initializeAppForUserManagementWithStatusCode(404, "{\"error\": {\"message\": \"CONFIGURATION_NOT_FOUND\"}}"); @@ -1713,7 +1804,14 @@ public void testGetOidcProviderConfigWithNotFoundError() throws Exception { FirebaseAuth.getInstance().getOidcProviderConfig("oidc.provider-id"); fail("No error thrown for invalid response"); } catch (FirebaseAuthException e) { - assertEquals(AuthHttpClient.CONFIGURATION_NOT_FOUND_ERROR, e.getErrorCode()); + assertEquals(ErrorCode.NOT_FOUND, e.getErrorCode()); + assertEquals( + "No IdP configuration found corresponding to the provided identifier " + + "(CONFIGURATION_NOT_FOUND).", + e.getMessage()); + assertTrue(e.getCause() instanceof HttpResponseException); + assertNotNull(e.getHttpResponse()); + assertEquals(AuthErrorCode.CONFIGURATION_NOT_FOUND, e.getAuthErrorCode()); } checkUrl(interceptor, "GET", PROJECT_BASE_URL + "/oauthIdpConfigs/oidc.provider-id"); } @@ -1721,8 +1819,7 @@ public void testGetOidcProviderConfigWithNotFoundError() throws Exception { @Test public void testGetTenantAwareOidcProviderConfig() throws Exception { TestResponseInterceptor interceptor = initializeAppForTenantAwareUserManagement( - "TENANT_ID", - TestUtils.loadResource("oidc.json")); + "TENANT_ID", OIDC_RESPONSE); TenantAwareFirebaseAuth tenantAwareAuth = FirebaseAuth.getInstance().getTenantManager().getAuthForTenant("TENANT_ID"); @@ -1772,18 +1869,25 @@ public void testListOidcProviderConfigsAsync() throws Exception { } @Test - public void testListOidcProviderConfigsError() throws Exception { - TestResponseInterceptor interceptor = - initializeAppForUserManagementWithStatusCode(404, - "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}"); + public void testListOidcProviderConfigsError() { + String message = "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}"; + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() + .setContent(message) + .setStatusCode(500); + FirebaseAuth auth = getRetryDisabledAuth(response); try { - FirebaseAuth.getInstance().listOidcProviderConfigs(null, 99); + auth.listOidcProviderConfigs(null, 99); fail("No error thrown for invalid response"); } catch (FirebaseAuthException e) { - assertEquals(AuthHttpClient.INTERNAL_ERROR, e.getErrorCode()); + assertEquals(ErrorCode.INTERNAL, e.getErrorCode()); + assertEquals( + "Unexpected HTTP response with status: 500\n" + message, + e.getMessage()); + assertTrue(e.getCause() instanceof HttpResponseException); + assertNotNull(e.getHttpResponse()); + assertNull(e.getAuthErrorCode()); } - checkUrl(interceptor, "GET", PROJECT_BASE_URL + "/oauthIdpConfigs"); } @Test @@ -1890,7 +1994,14 @@ public void testDeleteOidcProviderConfigWithNotFoundError() { FirebaseAuth.getInstance().deleteOidcProviderConfig("oidc.UNKNOWN"); fail("No error thrown for invalid response"); } catch (FirebaseAuthException e) { - assertEquals(AuthHttpClient.CONFIGURATION_NOT_FOUND_ERROR, e.getErrorCode()); + assertEquals(ErrorCode.NOT_FOUND, e.getErrorCode()); + assertEquals( + "No IdP configuration found corresponding to the provided identifier " + + "(CONFIGURATION_NOT_FOUND).", + e.getMessage()); + assertTrue(e.getCause() instanceof HttpResponseException); + assertNotNull(e.getHttpResponse()); + assertEquals(AuthErrorCode.CONFIGURATION_NOT_FOUND, e.getAuthErrorCode()); } checkUrl(interceptor, "DELETE", PROJECT_BASE_URL + "/oauthIdpConfigs/oidc.UNKNOWN"); } @@ -1912,8 +2023,7 @@ public void testTenantAwareDeleteOidcProviderConfig() throws Exception { @Test public void testCreateSamlProvider() throws Exception { - TestResponseInterceptor interceptor = initializeAppForUserManagement( - TestUtils.loadResource("saml.json")); + TestResponseInterceptor interceptor = initializeAppForUserManagement(SAML_RESPONSE); SamlProviderConfig.CreateRequest createRequest = new SamlProviderConfig.CreateRequest() .setProviderId("saml.provider-id") @@ -1958,8 +2068,7 @@ public void testCreateSamlProvider() throws Exception { @Test public void testCreateSamlProviderAsync() throws Exception { - TestResponseInterceptor interceptor = initializeAppForUserManagement( - TestUtils.loadResource("saml.json")); + TestResponseInterceptor interceptor = initializeAppForUserManagement(SAML_RESPONSE); SamlProviderConfig.CreateRequest createRequest = new SamlProviderConfig.CreateRequest() .setProviderId("saml.provider-id") @@ -2005,8 +2114,7 @@ public void testCreateSamlProviderAsync() throws Exception { @Test public void testCreateSamlProviderMinimal() throws Exception { - TestResponseInterceptor interceptor = initializeAppForUserManagement( - TestUtils.loadResource("saml.json")); + TestResponseInterceptor interceptor = initializeAppForUserManagement(SAML_RESPONSE); // Only the 'enabled', 'displayName', and 'signRequest' fields can be omitted from a SAML // provider config creation request. SamlProviderConfig.CreateRequest createRequest = @@ -2046,23 +2154,31 @@ public void testCreateSamlProviderMinimal() throws Exception { @Test public void testCreateSamlProviderError() { - TestResponseInterceptor interceptor = - initializeAppForUserManagementWithStatusCode(404, - "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}"); + String message = "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}"; + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() + .setContent(message) + .setStatusCode(500); + FirebaseAuth auth = getRetryDisabledAuth(response); SamlProviderConfig.CreateRequest createRequest = new SamlProviderConfig.CreateRequest().setProviderId("saml.provider-id"); + try { - FirebaseAuth.getInstance().createSamlProviderConfig(createRequest); + auth.createSamlProviderConfig(createRequest); fail("No error thrown for invalid response"); } catch (FirebaseAuthException e) { - assertEquals(AuthHttpClient.INTERNAL_ERROR, e.getErrorCode()); + assertEquals(ErrorCode.INTERNAL, e.getErrorCode()); + assertEquals( + "Unexpected HTTP response with status: 500\n" + message, + e.getMessage()); + assertTrue(e.getCause() instanceof HttpResponseException); + assertNotNull(e.getHttpResponse()); + assertNull(e.getAuthErrorCode()); } - checkUrl(interceptor, "POST", PROJECT_BASE_URL + "/inboundSamlConfigs"); } @Test public void testCreateSamlProviderMissingId() throws Exception { - initializeAppForUserManagement(TestUtils.loadResource("saml.json")); + initializeAppForUserManagement(SAML_RESPONSE); SamlProviderConfig.CreateRequest createRequest = new SamlProviderConfig.CreateRequest() .setDisplayName("DISPLAY_NAME") @@ -2084,8 +2200,7 @@ public void testCreateSamlProviderMissingId() throws Exception { @Test public void testTenantAwareCreateSamlProvider() throws Exception { TestResponseInterceptor interceptor = initializeAppForTenantAwareUserManagement( - "TENANT_ID", - TestUtils.loadResource("saml.json")); + "TENANT_ID", SAML_RESPONSE); SamlProviderConfig.CreateRequest createRequest = new SamlProviderConfig.CreateRequest() .setProviderId("saml.provider-id") @@ -2108,8 +2223,7 @@ public void testTenantAwareCreateSamlProvider() throws Exception { @Test public void testUpdateSamlProvider() throws Exception { - TestResponseInterceptor interceptor = initializeAppForUserManagement( - TestUtils.loadResource("saml.json")); + TestResponseInterceptor interceptor = initializeAppForUserManagement(SAML_RESPONSE); SamlProviderConfig.UpdateRequest updateRequest = new SamlProviderConfig.UpdateRequest("saml.provider-id") .setDisplayName("DISPLAY_NAME") @@ -2156,8 +2270,7 @@ public void testUpdateSamlProvider() throws Exception { @Test public void testUpdateSamlProviderAsync() throws Exception { - TestResponseInterceptor interceptor = initializeAppForUserManagement( - TestUtils.loadResource("saml.json")); + TestResponseInterceptor interceptor = initializeAppForUserManagement(SAML_RESPONSE); SamlProviderConfig.UpdateRequest updateRequest = new SamlProviderConfig.UpdateRequest("saml.provider-id") .setDisplayName("DISPLAY_NAME") @@ -2205,8 +2318,7 @@ public void testUpdateSamlProviderAsync() throws Exception { @Test public void testUpdateSamlProviderMinimal() throws Exception { - TestResponseInterceptor interceptor = initializeAppForUserManagement( - TestUtils.loadResource("saml.json")); + TestResponseInterceptor interceptor = initializeAppForUserManagement(SAML_RESPONSE); SamlProviderConfig.UpdateRequest request = new SamlProviderConfig.UpdateRequest("saml.provider-id").setDisplayName("DISPLAY_NAME"); @@ -2224,7 +2336,7 @@ public void testUpdateSamlProviderMinimal() throws Exception { @Test public void testUpdateSamlProviderConfigNoValues() throws Exception { - initializeAppForUserManagement(TestUtils.loadResource("saml.json")); + initializeAppForUserManagement(SAML_RESPONSE); try { FirebaseAuth.getInstance().updateSamlProviderConfig( new SamlProviderConfig.UpdateRequest("saml.provider-id")); @@ -2235,26 +2347,33 @@ public void testUpdateSamlProviderConfigNoValues() throws Exception { } @Test - public void testUpdateSamlProviderConfigError() throws Exception { - TestResponseInterceptor interceptor = - initializeAppForUserManagementWithStatusCode(404, - "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}"); + public void testUpdateSamlProviderConfigError() { + String message = "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}"; + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() + .setContent(message) + .setStatusCode(500); + FirebaseAuth auth = getRetryDisabledAuth(response); SamlProviderConfig.UpdateRequest request = new SamlProviderConfig.UpdateRequest("saml.provider-id").setDisplayName("DISPLAY_NAME"); + try { - FirebaseAuth.getInstance().updateSamlProviderConfig(request); + auth.updateSamlProviderConfig(request); fail("No error thrown for invalid response"); } catch (FirebaseAuthException e) { - assertEquals(AuthHttpClient.INTERNAL_ERROR, e.getErrorCode()); + assertEquals(ErrorCode.INTERNAL, e.getErrorCode()); + assertEquals( + "Unexpected HTTP response with status: 500\n" + message, + e.getMessage()); + assertTrue(e.getCause() instanceof HttpResponseException); + assertNotNull(e.getHttpResponse()); + assertNull(e.getAuthErrorCode()); } - checkUrl(interceptor, "PATCH", PROJECT_BASE_URL + "/inboundSamlConfigs/saml.provider-id"); } @Test public void testTenantAwareUpdateSamlProvider() throws Exception { TestResponseInterceptor interceptor = initializeAppForTenantAwareUserManagement( - "TENANT_ID", - TestUtils.loadResource("saml.json")); + "TENANT_ID", SAML_RESPONSE); TenantAwareFirebaseAuth tenantAwareAuth = FirebaseAuth.getInstance().getTenantManager().getAuthForTenant("TENANT_ID"); SamlProviderConfig.UpdateRequest updateRequest = @@ -2286,8 +2405,7 @@ public void testTenantAwareUpdateSamlProvider() throws Exception { @Test public void testGetSamlProviderConfig() throws Exception { - TestResponseInterceptor interceptor = initializeAppForUserManagement( - TestUtils.loadResource("saml.json")); + TestResponseInterceptor interceptor = initializeAppForUserManagement(SAML_RESPONSE); SamlProviderConfig config = FirebaseAuth.getInstance().getSamlProviderConfig("saml.provider-id"); @@ -2299,8 +2417,7 @@ public void testGetSamlProviderConfig() throws Exception { @Test public void testGetSamlProviderConfigAsync() throws Exception { - TestResponseInterceptor interceptor = initializeAppForUserManagement( - TestUtils.loadResource("saml.json")); + TestResponseInterceptor interceptor = initializeAppForUserManagement(SAML_RESPONSE); SamlProviderConfig config = FirebaseAuth.getInstance().getSamlProviderConfigAsync("saml.provider-id").get(); @@ -2324,7 +2441,7 @@ public void testGetSamlProviderConfigMissingId() throws Exception { @Test public void testGetSamlProviderConfigInvalidId() throws Exception { - initializeAppForUserManagement(TestUtils.loadResource("saml.json")); + initializeAppForUserManagement(SAML_RESPONSE); try { FirebaseAuth.getInstance().getSamlProviderConfig("oidc.invalid-saml-provider-id"); @@ -2343,7 +2460,14 @@ public void testGetSamlProviderConfigWithNotFoundError() { FirebaseAuth.getInstance().getSamlProviderConfig("saml.provider-id"); fail("No error thrown for invalid response"); } catch (FirebaseAuthException e) { - assertEquals(AuthHttpClient.CONFIGURATION_NOT_FOUND_ERROR, e.getErrorCode()); + assertEquals(ErrorCode.NOT_FOUND, e.getErrorCode()); + assertEquals( + "No IdP configuration found corresponding to the provided identifier " + + "(CONFIGURATION_NOT_FOUND).", + e.getMessage()); + assertTrue(e.getCause() instanceof HttpResponseException); + assertNotNull(e.getHttpResponse()); + assertEquals(AuthErrorCode.CONFIGURATION_NOT_FOUND, e.getAuthErrorCode()); } checkUrl(interceptor, "GET", PROJECT_BASE_URL + "/inboundSamlConfigs/saml.provider-id"); } @@ -2351,8 +2475,7 @@ public void testGetSamlProviderConfigWithNotFoundError() { @Test public void testGetTenantAwareSamlProviderConfig() throws Exception { TestResponseInterceptor interceptor = initializeAppForTenantAwareUserManagement( - "TENANT_ID", - TestUtils.loadResource("saml.json")); + "TENANT_ID", SAML_RESPONSE); TenantAwareFirebaseAuth tenantAwareAuth = FirebaseAuth.getInstance().getTenantManager().getAuthForTenant("TENANT_ID"); @@ -2403,18 +2526,25 @@ public void testListSamlProviderConfigsAsync() throws Exception { } @Test - public void testListSamlProviderConfigsError() throws Exception { - TestResponseInterceptor interceptor = - initializeAppForUserManagementWithStatusCode(404, - "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}"); + public void testListSamlProviderConfigsError() { + String message = "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}"; + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() + .setContent(message) + .setStatusCode(500); + FirebaseAuth auth = getRetryDisabledAuth(response); try { - FirebaseAuth.getInstance().listSamlProviderConfigs(null, 99); + auth.listSamlProviderConfigs(null, 99); fail("No error thrown for invalid response"); } catch (FirebaseAuthException e) { - assertEquals(AuthHttpClient.INTERNAL_ERROR, e.getErrorCode()); + assertEquals(ErrorCode.INTERNAL, e.getErrorCode()); + assertEquals( + "Unexpected HTTP response with status: 500\n" + message, + e.getMessage()); + assertTrue(e.getCause() instanceof HttpResponseException); + assertNotNull(e.getHttpResponse()); + assertNull(e.getAuthErrorCode()); } - checkUrl(interceptor, "GET", PROJECT_BASE_URL + "/inboundSamlConfigs"); } @Test @@ -2521,7 +2651,14 @@ public void testDeleteSamlProviderConfigWithNotFoundError() { FirebaseAuth.getInstance().deleteSamlProviderConfig("saml.UNKNOWN"); fail("No error thrown for invalid response"); } catch (FirebaseAuthException e) { - assertEquals(AuthHttpClient.CONFIGURATION_NOT_FOUND_ERROR, e.getErrorCode()); + assertEquals(ErrorCode.NOT_FOUND, e.getErrorCode()); + assertEquals( + "No IdP configuration found corresponding to the provided identifier " + + "(CONFIGURATION_NOT_FOUND).", + e.getMessage()); + assertTrue(e.getCause() instanceof HttpResponseException); + assertNotNull(e.getHttpResponse()); + assertEquals(AuthErrorCode.CONFIGURATION_NOT_FOUND, e.getAuthErrorCode()); } checkUrl(interceptor, "DELETE", PROJECT_BASE_URL + "/inboundSamlConfigs/saml.UNKNOWN"); } @@ -2543,7 +2680,7 @@ public void testTenantAwareDeleteSamlProviderConfig() throws Exception { private static TestResponseInterceptor initializeAppForUserManagementWithStatusCode( int statusCode, String response) { - FirebaseApp.initializeApp(new FirebaseOptions.Builder() + FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(credentials) .setHttpTransport( new MockHttpTransport.Builder().setLowLevelHttpResponse( @@ -2579,7 +2716,7 @@ private static void initializeAppWithResponses(String... responses) { mocks.add(new MockLowLevelHttpResponse().setContent(response)); } MockHttpTransport transport = new MultiRequestMockHttpTransport(mocks); - FirebaseApp.initializeApp(new FirebaseOptions.Builder() + FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(credentials) .setHttpTransport(transport) .setProjectId("test-project-id") @@ -2597,9 +2734,8 @@ private static FirebaseAuth getRetryDisabledAuth(MockLowLevelHttpResponse respon final MockHttpTransport transport = new MockHttpTransport.Builder() .setLowLevelHttpResponse(response) .build(); - final FirebaseApp app = FirebaseApp.initializeApp(new FirebaseOptions.Builder() + final FirebaseApp app = FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(credentials) - .setProjectId("test-project-id") .setHttpTransport(transport) .build()); return FirebaseAuth.builder() @@ -2608,9 +2744,10 @@ private static FirebaseAuth getRetryDisabledAuth(MockLowLevelHttpResponse respon @Override public FirebaseUserManager get() { return FirebaseUserManager.builder() - .setFirebaseApp(app) - .setHttpRequestFactory(transport.createRequestFactory()) - .build(); + .setProjectId("test-project-id") + .setHttpRequestFactory(transport.createRequestFactory()) + .setJsonFactory(Utils.getDefaultJsonFactory()) + .build(); } }) .build(); @@ -2680,13 +2817,7 @@ private static void checkRequestHeaders(TestResponseInterceptor interceptor) { private static void checkUrl(TestResponseInterceptor interceptor, String method, String url) { HttpRequest request = interceptor.getResponse().getRequest(); - if (method.equals("PATCH")) { - assertEquals("PATCH", - request.getHeaders().getFirstHeaderStringValue("X-HTTP-Method-Override")); - assertEquals("POST", request.getRequestMethod()); - } else { - assertEquals(method, request.getRequestMethod()); - } + assertEquals(method, request.getRequestMethod()); assertEquals(url, request.getUrl().toString().split("\\?")[0]); } diff --git a/src/test/java/com/google/firebase/auth/ProviderConfigTestUtils.java b/src/test/java/com/google/firebase/auth/ProviderConfigTestUtils.java index c01ac6501..e027882c0 100644 --- a/src/test/java/com/google/firebase/auth/ProviderConfigTestUtils.java +++ b/src/test/java/com/google/firebase/auth/ProviderConfigTestUtils.java @@ -21,7 +21,7 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; -import com.google.firebase.auth.internal.AuthHttpClient; +import com.google.firebase.ErrorCode; import java.util.ArrayList; import java.util.List; import java.util.concurrent.ExecutionException; @@ -36,8 +36,9 @@ public static void assertOidcProviderConfigDoesNotExist( fail("No error thrown for getting a deleted OIDC provider config."); } catch (ExecutionException e) { assertTrue(e.getCause() instanceof FirebaseAuthException); - assertEquals(AuthHttpClient.CONFIGURATION_NOT_FOUND_ERROR, - ((FirebaseAuthException) e.getCause()).getErrorCode()); + FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); + assertEquals(ErrorCode.NOT_FOUND, authException.getErrorCode()); + assertEquals(AuthErrorCode.CONFIGURATION_NOT_FOUND, authException.getAuthErrorCode()); } } @@ -48,8 +49,9 @@ public static void assertSamlProviderConfigDoesNotExist( fail("No error thrown for getting a deleted SAML provider config."); } catch (ExecutionException e) { assertTrue(e.getCause() instanceof FirebaseAuthException); - assertEquals(AuthHttpClient.CONFIGURATION_NOT_FOUND_ERROR, - ((FirebaseAuthException) e.getCause()).getErrorCode()); + FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); + assertEquals(ErrorCode.NOT_FOUND, authException.getErrorCode()); + assertEquals(AuthErrorCode.CONFIGURATION_NOT_FOUND, authException.getAuthErrorCode()); } } diff --git a/src/test/java/com/google/firebase/auth/UserTestUtils.java b/src/test/java/com/google/firebase/auth/UserTestUtils.java index aa86e6e19..8d6f7fc32 100644 --- a/src/test/java/com/google/firebase/auth/UserTestUtils.java +++ b/src/test/java/com/google/firebase/auth/UserTestUtils.java @@ -20,7 +20,7 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; -import com.google.firebase.auth.internal.AuthHttpClient; +import com.google.firebase.ErrorCode; import java.util.ArrayList; import java.util.List; import java.util.Random; @@ -37,7 +37,7 @@ public static void assertUserDoesNotExist(AbstractFirebaseAuth firebaseAuth, Str fail("No error thrown for getting a user which was expected to be absent."); } catch (ExecutionException e) { assertTrue(e.getCause() instanceof FirebaseAuthException); - assertEquals(AuthHttpClient.USER_NOT_FOUND_ERROR, + assertEquals(ErrorCode.NOT_FOUND, ((FirebaseAuthException) e.getCause()).getErrorCode()); } } diff --git a/src/test/java/com/google/firebase/auth/internal/CryptoSignersTest.java b/src/test/java/com/google/firebase/auth/internal/CryptoSignersTest.java index bc98add3c..32f9fd543 100644 --- a/src/test/java/com/google/firebase/auth/internal/CryptoSignersTest.java +++ b/src/test/java/com/google/firebase/auth/internal/CryptoSignersTest.java @@ -18,10 +18,14 @@ import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import com.google.api.client.googleapis.util.Utils; +import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.HttpStatusCodes; import com.google.api.client.testing.http.MockHttpTransport; import com.google.api.client.testing.http.MockLowLevelHttpResponse; import com.google.auth.ServiceAccountSigner; @@ -29,21 +33,22 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.io.BaseEncoding; +import com.google.firebase.ErrorCode; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; import com.google.firebase.TestOnlyImplFirebaseTrampolines; +import com.google.firebase.auth.FirebaseAuthException; import com.google.firebase.auth.MockGoogleCredentials; import com.google.firebase.testing.MultiRequestMockHttpTransport; import com.google.firebase.testing.ServiceAccount; import com.google.firebase.testing.TestResponseInterceptor; -import java.io.IOException; import org.junit.After; import org.junit.Test; public class CryptoSignersTest { @Test - public void testServiceAccountCryptoSigner() throws IOException { + public void testServiceAccountCryptoSigner() throws Exception { ServiceAccountCredentials credentials = ServiceAccountCredentials.fromStream( ServiceAccount.EDITOR.asStream()); byte[] expected = credentials.sign("foo".getBytes()); @@ -63,7 +68,7 @@ public void testInvalidServiceAccountCryptoSigner() { } @Test - public void testIAMCryptoSigner() throws IOException { + public void testIAMCryptoSigner() throws Exception { String signature = BaseEncoding.base64().encode("signed-bytes".getBytes()); String response = Utils.getDefaultJsonFactory().toString( ImmutableMap.of("signature", signature)); @@ -84,6 +89,29 @@ public void testIAMCryptoSigner() throws IOException { assertEquals(url, interceptor.getResponse().getRequest().getUrl().toString()); } + @Test + public void testIAMCryptoSignerHttpError() { + String error = "{\"error\": {\"status\":\"INTERNAL\", \"message\": \"Test error\"}}"; + MockHttpTransport transport = new MockHttpTransport.Builder() + .setLowLevelHttpResponse(new MockLowLevelHttpResponse() + .setStatusCode(HttpStatusCodes.STATUS_CODE_SERVER_ERROR) + .setContent(error)) + .build(); + CryptoSigners.IAMCryptoSigner signer = new CryptoSigners.IAMCryptoSigner( + transport.createRequestFactory(), + Utils.getDefaultJsonFactory(), + "test-service-account@iam.gserviceaccount.com"); + try { + signer.sign("foo".getBytes()); + } catch (FirebaseAuthException e) { + assertEquals(ErrorCode.INTERNAL, e.getErrorCode()); + assertEquals("Test error", e.getMessage()); + assertNotNull(e.getCause()); + assertNotNull(e.getHttpResponse()); + assertNull(e.getAuthErrorCode()); + } + } + @Test public void testInvalidIAMCryptoSigner() { try { @@ -119,7 +147,7 @@ public void testInvalidIAMCryptoSigner() { } @Test - public void testMetadataService() throws IOException { + public void testMetadataService() throws Exception { String signature = BaseEncoding.base64().encode("signed-bytes".getBytes()); String response = Utils.getDefaultJsonFactory().toString( ImmutableMap.of("signature", signature)); @@ -127,7 +155,7 @@ public void testMetadataService() throws IOException { ImmutableList.of( new MockLowLevelHttpResponse().setContent("metadata-server@iam.gserviceaccount.com"), new MockLowLevelHttpResponse().setContent(response))); - FirebaseOptions options = new FirebaseOptions.Builder() + FirebaseOptions options = FirebaseOptions.builder() .setCredentials(new MockGoogleCredentials("test-token")) .setHttpTransport(transport) .build(); @@ -142,11 +170,13 @@ public void testMetadataService() throws IOException { assertArrayEquals("signed-bytes".getBytes(), data); final String url = "https://iam.googleapis.com/v1/projects/-/serviceAccounts/" + "metadata-server@iam.gserviceaccount.com:signBlob"; - assertEquals(url, interceptor.getResponse().getRequest().getUrl().toString()); + HttpRequest request = interceptor.getResponse().getRequest(); + assertEquals(url, request.getUrl().toString()); + assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); } @Test - public void testExplicitServiceAccountEmail() throws IOException { + public void testExplicitServiceAccountEmail() throws Exception { String signature = BaseEncoding.base64().encode("signed-bytes".getBytes()); String response = Utils.getDefaultJsonFactory().toString( ImmutableMap.of("signature", signature)); @@ -155,7 +185,7 @@ public void testExplicitServiceAccountEmail() throws IOException { MockHttpTransport transport = new MultiRequestMockHttpTransport( ImmutableList.of( new MockLowLevelHttpResponse().setContent(response))); - FirebaseOptions options = new FirebaseOptions.Builder() + FirebaseOptions options = FirebaseOptions.builder() .setServiceAccountId("explicit-service-account@iam.gserviceaccount.com") .setCredentials(new MockGoogleCredentialsWithSigner("test-token")) .setHttpTransport(transport) @@ -170,13 +200,15 @@ public void testExplicitServiceAccountEmail() throws IOException { assertArrayEquals("signed-bytes".getBytes(), data); final String url = "https://iam.googleapis.com/v1/projects/-/serviceAccounts/" + "explicit-service-account@iam.gserviceaccount.com:signBlob"; - assertEquals(url, interceptor.getResponse().getRequest().getUrl().toString()); + HttpRequest request = interceptor.getResponse().getRequest(); + assertEquals(url, request.getUrl().toString()); + assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); } @Test - public void testCredentialsWithSigner() throws IOException { + public void testCredentialsWithSigner() throws Exception { // Should fall back to signing-enabled credential - FirebaseOptions options = new FirebaseOptions.Builder() + FirebaseOptions options = FirebaseOptions.builder() .setCredentials(new MockGoogleCredentialsWithSigner("test-token")) .build(); FirebaseApp app = FirebaseApp.initializeApp(options, "customApp"); diff --git a/src/test/java/com/google/firebase/auth/internal/FirebaseTokenFactoryTest.java b/src/test/java/com/google/firebase/auth/internal/FirebaseTokenFactoryTest.java index 1c85ceeee..875c781e9 100644 --- a/src/test/java/com/google/firebase/auth/internal/FirebaseTokenFactoryTest.java +++ b/src/test/java/com/google/firebase/auth/internal/FirebaseTokenFactoryTest.java @@ -29,8 +29,9 @@ import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.firebase.ErrorCode; +import com.google.firebase.auth.FirebaseAuthException; import com.google.firebase.testing.TestUtils; -import java.io.IOException; import java.security.GeneralSecurityException; import java.security.KeyPair; import java.security.KeyPairGenerator; @@ -158,12 +159,12 @@ private static class TestCryptoSigner implements CryptoSigner { } @Override - public byte[] sign(byte[] payload) throws IOException { + public byte[] sign(byte[] payload) throws FirebaseAuthException { try { return SecurityUtils.sign(SecurityUtils.getSha256WithRsaSignatureAlgorithm(), privateKey, payload); } catch (GeneralSecurityException e) { - throw new IOException(e); + throw new FirebaseAuthException(ErrorCode.UNKNOWN, "Failed to sign token", e, null, null); } } diff --git a/src/test/java/com/google/firebase/auth/multitenancy/FirebaseTenantClientTest.java b/src/test/java/com/google/firebase/auth/multitenancy/FirebaseTenantClientTest.java index 36660dc28..4bc84cb4d 100644 --- a/src/test/java/com/google/firebase/auth/multitenancy/FirebaseTenantClientTest.java +++ b/src/test/java/com/google/firebase/auth/multitenancy/FirebaseTenantClientTest.java @@ -18,6 +18,8 @@ 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.assertTrue; import static org.junit.Assert.fail; @@ -26,6 +28,7 @@ import com.google.api.client.http.GenericUrl; import com.google.api.client.http.HttpHeaders; import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.HttpResponseException; import com.google.api.client.json.GenericJson; import com.google.api.client.json.JsonFactory; import com.google.api.client.testing.http.MockHttpTransport; @@ -33,13 +36,14 @@ import com.google.auth.oauth2.GoogleCredentials; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; +import com.google.firebase.ErrorCode; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; import com.google.firebase.TestOnlyImplFirebaseTrampolines; +import com.google.firebase.auth.AuthErrorCode; import com.google.firebase.auth.FirebaseAuth; import com.google.firebase.auth.FirebaseAuthException; import com.google.firebase.auth.MockGoogleCredentials; -import com.google.firebase.auth.internal.AuthHttpClient; import com.google.firebase.internal.SdkUtils; import com.google.firebase.testing.MultiRequestMockHttpTransport; import com.google.firebase.testing.TestResponseInterceptor; @@ -90,7 +94,12 @@ public void testGetTenantWithNotFoundError() { FirebaseAuth.getInstance().getTenantManager().getTenant("UNKNOWN"); fail("No error thrown for invalid response"); } catch (FirebaseAuthException e) { - assertEquals(AuthHttpClient.TENANT_NOT_FOUND_ERROR, e.getErrorCode()); + assertEquals(ErrorCode.NOT_FOUND, e.getErrorCode()); + assertEquals( + "No tenant found for the given identifier (TENANT_NOT_FOUND).", e.getMessage()); + assertTrue(e.getCause() instanceof HttpResponseException); + assertNotNull(e.getHttpResponse()); + assertEquals(AuthErrorCode.TENANT_NOT_FOUND, e.getAuthErrorCode()); } checkUrl(interceptor, "GET", TENANTS_BASE_URL + "/UNKNOWN"); } @@ -183,16 +192,21 @@ public void testCreateTenantMinimal() throws Exception { @Test public void testCreateTenantError() { - TestResponseInterceptor interceptor = - initializeAppForTenantManagementWithStatusCode(404, - "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}"); + String message = "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}"; + TenantManager tenantManager = createRetryDisabledTenantManager(new MockLowLevelHttpResponse() + .setStatusCode(500) + .setContent(message)); + try { - FirebaseAuth.getInstance().getTenantManager().createTenant(new Tenant.CreateRequest()); + tenantManager.createTenant(new Tenant.CreateRequest()); fail("No error thrown for invalid response"); } catch (FirebaseAuthException e) { - assertEquals(AuthHttpClient.INTERNAL_ERROR, e.getErrorCode()); + assertEquals(ErrorCode.INTERNAL, e.getErrorCode()); + assertEquals("Unexpected HTTP response with status: 500\n" + message, e.getMessage()); + assertTrue(e.getCause() instanceof HttpResponseException); + assertNotNull(e.getHttpResponse()); + assertNull(e.getAuthErrorCode()); } - checkUrl(interceptor, "POST", TENANTS_BASE_URL); } @Test @@ -252,18 +266,23 @@ public void testUpdateTenantNoValues() throws Exception { @Test public void testUpdateTenantError() { - TestResponseInterceptor interceptor = - initializeAppForTenantManagementWithStatusCode(404, - "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}"); + String message = "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}"; + TenantManager tenantManager = createRetryDisabledTenantManager(new MockLowLevelHttpResponse() + .setStatusCode(500) + .setContent(message)); Tenant.UpdateRequest request = new Tenant.UpdateRequest("TENANT_1").setDisplayName("DISPLAY_NAME"); + try { - FirebaseAuth.getInstance().getTenantManager().updateTenant(request); + tenantManager.updateTenant(request); fail("No error thrown for invalid response"); } catch (FirebaseAuthException e) { - assertEquals(AuthHttpClient.INTERNAL_ERROR, e.getErrorCode()); + assertEquals(ErrorCode.INTERNAL, e.getErrorCode()); + assertEquals("Unexpected HTTP response with status: 500\n" + message, e.getMessage()); + assertTrue(e.getCause() instanceof HttpResponseException); + assertNotNull(e.getHttpResponse()); + assertNull(e.getAuthErrorCode()); } - checkUrl(interceptor, "PATCH", TENANTS_BASE_URL + "/TENANT_1"); } @Test @@ -285,7 +304,10 @@ public void testDeleteTenantWithNotFoundError() { FirebaseAuth.getInstance().getTenantManager().deleteTenant("UNKNOWN"); fail("No error thrown for invalid response"); } catch (FirebaseAuthException e) { - assertEquals(AuthHttpClient.TENANT_NOT_FOUND_ERROR, e.getErrorCode()); + assertEquals(ErrorCode.NOT_FOUND, e.getErrorCode()); + assertEquals("No tenant found for the given identifier (TENANT_NOT_FOUND).", e.getMessage()); + assertTrue(e.getCause() instanceof HttpResponseException); + assertNotNull(e.getHttpResponse()); } checkUrl(interceptor, "DELETE", TENANTS_BASE_URL + "/UNKNOWN"); } @@ -308,18 +330,22 @@ private static void checkRequestHeaders(TestResponseInterceptor interceptor) { private static void checkUrl(TestResponseInterceptor interceptor, String method, String url) { HttpRequest request = interceptor.getResponse().getRequest(); - if (method.equals("PATCH")) { - assertEquals("PATCH", - request.getHeaders().getFirstHeaderStringValue("X-HTTP-Method-Override")); - assertEquals("POST", request.getRequestMethod()); - } else { - assertEquals(method, request.getRequestMethod()); - } + assertEquals(method, request.getRequestMethod()); assertEquals(url, request.getUrl().toString().split("\\?")[0]); } private static TestResponseInterceptor initializeAppForTenantManagement(String... responses) { - initializeAppWithResponses(responses); + List mocks = new ArrayList<>(); + for (String response : responses) { + mocks.add(new MockLowLevelHttpResponse().setContent(response)); + } + MockHttpTransport transport = new MultiRequestMockHttpTransport(mocks); + FirebaseApp.initializeApp(FirebaseOptions.builder() + .setCredentials(credentials) + .setHttpTransport(transport) + .setProjectId("test-project-id") + .build()); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); FirebaseAuth.getInstance().getTenantManager().setInterceptor(interceptor); return interceptor; @@ -327,7 +353,7 @@ private static TestResponseInterceptor initializeAppForTenantManagement(String.. private static TestResponseInterceptor initializeAppForTenantManagementWithStatusCode( int statusCode, String response) { - FirebaseApp.initializeApp(new FirebaseOptions.Builder() + FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(credentials) .setHttpTransport( new MockHttpTransport.Builder() @@ -341,17 +367,16 @@ private static TestResponseInterceptor initializeAppForTenantManagementWithStatu return interceptor; } - private static void initializeAppWithResponses(String... responses) { - List mocks = new ArrayList<>(); - for (String response : responses) { - mocks.add(new MockLowLevelHttpResponse().setContent(response)); - } - MockHttpTransport transport = new MultiRequestMockHttpTransport(mocks); - FirebaseApp.initializeApp(new FirebaseOptions.Builder() + private static TenantManager createRetryDisabledTenantManager(MockLowLevelHttpResponse response) { + MockHttpTransport transport = new MockHttpTransport.Builder() + .setLowLevelHttpResponse(response) + .build(); + FirebaseApp app = FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(credentials) - .setHttpTransport(transport) - .setProjectId("test-project-id") .build()); + FirebaseTenantClient tenantClient = new FirebaseTenantClient( + "test-project-id", Utils.getDefaultJsonFactory(), transport.createRequestFactory()); + return new TenantManager(app, tenantClient); } private static GenericJson parseRequestContent(TestResponseInterceptor interceptor) diff --git a/src/test/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuthIT.java b/src/test/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuthIT.java index 61a841902..fd8e4af83 100644 --- a/src/test/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuthIT.java +++ b/src/test/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuthIT.java @@ -39,6 +39,7 @@ import com.google.common.collect.ImmutableMap; import com.google.common.util.concurrent.MoreExecutors; import com.google.firebase.FirebaseApp; +import com.google.firebase.auth.AuthErrorCode; import com.google.firebase.auth.ExportedUserRecord; import com.google.firebase.auth.FirebaseAuth; import com.google.firebase.auth.FirebaseAuthException; @@ -257,8 +258,8 @@ public void testVerifyTokenWithWrongTenantAwareClient() throws Exception { fail("No error thrown for verifying a token with the wrong tenant-aware client"); } catch (ExecutionException e) { assertTrue(e.getCause() instanceof FirebaseAuthException); - assertEquals("tenant-id-mismatch", - ((FirebaseAuthException) e.getCause()).getErrorCode()); + assertEquals(AuthErrorCode.TENANT_ID_MISMATCH, + ((FirebaseAuthException) e.getCause()).getAuthErrorCode()); } } diff --git a/src/test/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuthTest.java b/src/test/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuthTest.java index df7bcf00d..db0099791 100644 --- a/src/test/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuthTest.java +++ b/src/test/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuthTest.java @@ -28,6 +28,7 @@ import com.google.api.client.testing.http.MockHttpTransport; import com.google.api.client.testing.http.MockLowLevelHttpResponse; import com.google.common.base.Suppliers; +import com.google.firebase.ErrorCode; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; import com.google.firebase.TestOnlyImplFirebaseTrampolines; @@ -58,7 +59,7 @@ public class TenantAwareFirebaseAuthTest { .build(); private static final FirebaseAuthException AUTH_EXCEPTION = new FirebaseAuthException( - "code", "reason"); + ErrorCode.INVALID_ARGUMENT, "Test error message", null, null, null); private static final String CREATE_COOKIE_RESPONSE = TestUtils.loadResource( "createSessionCookie.json"); @@ -99,7 +100,7 @@ public void testCreateSessionCookieAsyncError() throws Exception { } catch (ExecutionException e) { assertTrue(e.getCause() instanceof FirebaseAuthException); FirebaseAuthException cause = (FirebaseAuthException) e.getCause(); - assertEquals("code", cause.getErrorCode()); + assertEquals(ErrorCode.INVALID_ARGUMENT, cause.getErrorCode()); } assertNull(interceptor.getResponse()); @@ -133,7 +134,7 @@ public void testCreateSessionCookieError() { auth.createSessionCookie("testToken", COOKIE_OPTIONS); fail("No error thrown for invalid ID token"); } catch (FirebaseAuthException e) { - assertEquals("code", e.getErrorCode()); + assertEquals(ErrorCode.INVALID_ARGUMENT, e.getErrorCode()); } assertNull(interceptor.getResponse()); @@ -163,7 +164,7 @@ public void testVerifySessionCookieFailure() { auth.verifySessionCookie("cookie"); fail("No error thrown for invalid token"); } catch (FirebaseAuthException authException) { - assertEquals("code", authException.getErrorCode()); + assertEquals(ErrorCode.INVALID_ARGUMENT, authException.getErrorCode()); assertEquals("cookie", tokenVerifier.getLastTokenString()); } } @@ -193,7 +194,7 @@ public void testVerifySessionCookieAsyncFailure() throws InterruptedException { fail("No error thrown for invalid token"); } catch (ExecutionException e) { FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); - assertEquals("code", authException.getErrorCode()); + assertEquals(ErrorCode.INVALID_ARGUMENT, authException.getErrorCode()); assertEquals("cookie", tokenVerifier.getLastTokenString()); } } @@ -224,7 +225,7 @@ public void testVerifySessionCookieWithCheckRevokedFailure() { auth.verifySessionCookie("cookie", true); fail("No error thrown for invalid token"); } catch (FirebaseAuthException e) { - assertEquals("code", e.getErrorCode()); + assertEquals(ErrorCode.INVALID_ARGUMENT, e.getErrorCode()); assertEquals("cookie", tokenVerifier.getLastTokenString()); } } @@ -241,7 +242,7 @@ public void testVerifySessionCookieWithCheckRevokedAsyncFailure() throws Interru fail("No error thrown for invalid token"); } catch (ExecutionException e) { FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); - assertEquals("code", authException.getErrorCode()); + assertEquals(ErrorCode.INVALID_ARGUMENT, authException.getErrorCode()); assertEquals("cookie", tokenVerifier.getLastTokenString()); } } diff --git a/src/test/java/com/google/firebase/auth/multitenancy/TenantManagerIT.java b/src/test/java/com/google/firebase/auth/multitenancy/TenantManagerIT.java index 086e25aa2..44d75e830 100644 --- a/src/test/java/com/google/firebase/auth/multitenancy/TenantManagerIT.java +++ b/src/test/java/com/google/firebase/auth/multitenancy/TenantManagerIT.java @@ -27,9 +27,10 @@ import com.google.api.core.ApiFutureCallback; import com.google.api.core.ApiFutures; import com.google.common.util.concurrent.MoreExecutors; +import com.google.firebase.ErrorCode; +import com.google.firebase.auth.AuthErrorCode; import com.google.firebase.auth.FirebaseAuth; import com.google.firebase.auth.FirebaseAuthException; -import com.google.firebase.auth.internal.AuthHttpClient; import com.google.firebase.testing.IntegrationTestUtils; import java.util.ArrayList; import java.util.List; @@ -81,8 +82,9 @@ public void testTenantLifecycle() throws Exception { fail("No error thrown for getting a deleted tenant"); } catch (ExecutionException e) { assertTrue(e.getCause() instanceof FirebaseAuthException); - assertEquals(AuthHttpClient.TENANT_NOT_FOUND_ERROR, - ((FirebaseAuthException) e.getCause()).getErrorCode()); + FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); + assertEquals(ErrorCode.NOT_FOUND, authException.getErrorCode()); + assertEquals(AuthErrorCode.TENANT_NOT_FOUND, authException.getAuthErrorCode()); } } diff --git a/src/test/java/com/google/firebase/cloud/FirestoreClientTest.java b/src/test/java/com/google/firebase/cloud/FirestoreClientTest.java index 03627f202..08ee49ff0 100644 --- a/src/test/java/com/google/firebase/cloud/FirestoreClientTest.java +++ b/src/test/java/com/google/firebase/cloud/FirestoreClientTest.java @@ -12,7 +12,6 @@ import com.google.cloud.firestore.FirestoreOptions; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; -import com.google.firebase.FirebaseOptions.Builder; import com.google.firebase.ImplFirebaseTrampolines; import com.google.firebase.TestOnlyImplFirebaseTrampolines; import com.google.firebase.auth.MockGoogleCredentials; @@ -23,11 +22,10 @@ public class FirestoreClientTest { - public static final FirestoreOptions FIRESTORE_OPTIONS = FirestoreOptions.newBuilder() + private static final FirestoreOptions FIRESTORE_OPTIONS = FirestoreOptions.newBuilder() // Setting credentials is not required (they get overridden by Admin SDK), but without // this Firestore logs an ugly warning during tests. .setCredentials(new MockGoogleCredentials("test-token")) - .setTimestampsInSnapshotsEnabled(true) .build(); @After @@ -37,7 +35,7 @@ public void tearDown() { @Test public void testExplicitProjectId() throws IOException { - FirebaseApp app = FirebaseApp.initializeApp(new FirebaseOptions.Builder() + FirebaseApp app = FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(GoogleCredentials.fromStream(ServiceAccount.EDITOR.asStream())) .setProjectId("explicit-project-id") .setFirestoreOptions(FIRESTORE_OPTIONS) @@ -51,7 +49,7 @@ public void testExplicitProjectId() throws IOException { @Test public void testServiceAccountProjectId() throws IOException { - FirebaseApp app = FirebaseApp.initializeApp(new FirebaseOptions.Builder() + FirebaseApp app = FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(GoogleCredentials.fromStream(ServiceAccount.EDITOR.asStream())) .setFirestoreOptions(FIRESTORE_OPTIONS) .build()); @@ -64,7 +62,7 @@ public void testServiceAccountProjectId() throws IOException { @Test public void testFirestoreOptions() throws IOException { - FirebaseApp app = FirebaseApp.initializeApp(new Builder() + FirebaseApp app = FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(GoogleCredentials.fromStream(ServiceAccount.EDITOR.asStream())) .setProjectId("explicit-project-id") .setFirestoreOptions(FIRESTORE_OPTIONS) @@ -80,11 +78,10 @@ public void testFirestoreOptions() throws IOException { @Test public void testFirestoreOptionsOverride() throws IOException { - FirebaseApp app = FirebaseApp.initializeApp(new Builder() + FirebaseApp app = FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(GoogleCredentials.fromStream(ServiceAccount.EDITOR.asStream())) .setProjectId("explicit-project-id") .setFirestoreOptions(FirestoreOptions.newBuilder() - .setTimestampsInSnapshotsEnabled(true) .setProjectId("other-project-id") .setCredentials(GoogleCredentials.fromStream(ServiceAccount.EDITOR.asStream())) .build()) @@ -104,7 +101,7 @@ public void testFirestoreOptionsOverride() throws IOException { @Test public void testAppDelete() throws IOException { - FirebaseApp app = FirebaseApp.initializeApp(new FirebaseOptions.Builder() + FirebaseApp app = FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(GoogleCredentials.fromStream(ServiceAccount.EDITOR.asStream())) .setProjectId("mock-project-id") .setFirestoreOptions(FIRESTORE_OPTIONS) diff --git a/src/test/java/com/google/firebase/cloud/StorageClientTest.java b/src/test/java/com/google/firebase/cloud/StorageClientTest.java index 41535f3d9..190fad6cc 100644 --- a/src/test/java/com/google/firebase/cloud/StorageClientTest.java +++ b/src/test/java/com/google/firebase/cloud/StorageClientTest.java @@ -41,7 +41,7 @@ public void tearDown() { @Test public void testInvalidConfiguration() throws IOException { - FirebaseApp app = FirebaseApp.initializeApp(new FirebaseOptions.Builder() + FirebaseApp app = FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(GoogleCredentials.fromStream(ServiceAccount.EDITOR.asStream())) .build()); try { @@ -54,7 +54,7 @@ public void testInvalidConfiguration() throws IOException { @Test public void testInvalidBucketName() throws IOException { - FirebaseApp app = FirebaseApp.initializeApp(new FirebaseOptions.Builder() + FirebaseApp app = FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(GoogleCredentials.fromStream(ServiceAccount.EDITOR.asStream())) .setStorageBucket("mock-bucket-name") .build()); @@ -75,7 +75,7 @@ public void testInvalidBucketName() throws IOException { @Test public void testAppDelete() throws IOException { - FirebaseApp app = FirebaseApp.initializeApp(new FirebaseOptions.Builder() + FirebaseApp app = FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(GoogleCredentials.fromStream(ServiceAccount.EDITOR.asStream())) .setStorageBucket("mock-bucket-name") .build()); @@ -93,7 +93,7 @@ public void testAppDelete() throws IOException { @Test public void testNonExistingBucket() throws IOException { - FirebaseApp app = FirebaseApp.initializeApp(new FirebaseOptions.Builder() + FirebaseApp app = FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(GoogleCredentials.fromStream(ServiceAccount.EDITOR.asStream())) .setStorageBucket("mock-bucket-name") .build()); @@ -115,7 +115,7 @@ public void testNonExistingBucket() throws IOException { @Test public void testBucket() throws IOException { - FirebaseApp app = FirebaseApp.initializeApp(new FirebaseOptions.Builder() + FirebaseApp app = FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(GoogleCredentials.fromStream(ServiceAccount.EDITOR.asStream())) .setStorageBucket("mock-bucket-name") .build()); diff --git a/src/test/java/com/google/firebase/database/DataSnapshotTest.java b/src/test/java/com/google/firebase/database/DataSnapshotTest.java index 0764bdbf4..083d8ce09 100644 --- a/src/test/java/com/google/firebase/database/DataSnapshotTest.java +++ b/src/test/java/com/google/firebase/database/DataSnapshotTest.java @@ -51,7 +51,7 @@ public class DataSnapshotTest { @BeforeClass public static void setUpClass() throws IOException { testApp = FirebaseApp.initializeApp( - new FirebaseOptions.Builder() + FirebaseOptions.builder() .setCredentials(GoogleCredentials.fromStream(ServiceAccount.EDITOR.asStream())) .setDatabaseUrl("https://admin-java-sdk.firebaseio.com") .build()); diff --git a/src/test/java/com/google/firebase/database/DatabaseReferenceTest.java b/src/test/java/com/google/firebase/database/DatabaseReferenceTest.java index 5e7fc5374..207173f74 100644 --- a/src/test/java/com/google/firebase/database/DatabaseReferenceTest.java +++ b/src/test/java/com/google/firebase/database/DatabaseReferenceTest.java @@ -64,7 +64,7 @@ public class DatabaseReferenceTest { @BeforeClass public static void setUpClass() throws IOException { testApp = FirebaseApp.initializeApp( - new FirebaseOptions.Builder() + FirebaseOptions.builder() .setCredentials(GoogleCredentials.fromStream(ServiceAccount.EDITOR.asStream())) .setDatabaseUrl(DB_URL) .build()); diff --git a/src/test/java/com/google/firebase/database/FirebaseDatabaseTest.java b/src/test/java/com/google/firebase/database/FirebaseDatabaseTest.java index ba7816173..2bdc6185e 100644 --- a/src/test/java/com/google/firebase/database/FirebaseDatabaseTest.java +++ b/src/test/java/com/google/firebase/database/FirebaseDatabaseTest.java @@ -41,12 +41,12 @@ public class FirebaseDatabaseTest { private static final FirebaseOptions firebaseOptions = - new FirebaseOptions.Builder() + FirebaseOptions.builder() .setCredentials(TestUtils.getCertCredential(ServiceAccount.EDITOR.asStream())) .setDatabaseUrl("https://firebase-db-test.firebaseio.com") .build(); private static final FirebaseOptions firebaseOptionsWithoutDatabaseUrl = - new FirebaseOptions.Builder() + FirebaseOptions.builder() .setCredentials(TestUtils.getCertCredential(ServiceAccount.EDITOR.asStream())) .build(); diff --git a/src/test/java/com/google/firebase/database/TestHelpers.java b/src/test/java/com/google/firebase/database/TestHelpers.java index cfd15034d..00d9a3797 100644 --- a/src/test/java/com/google/firebase/database/TestHelpers.java +++ b/src/test/java/com/google/firebase/database/TestHelpers.java @@ -16,7 +16,6 @@ package com.google.firebase.database; -import static com.cedarsoftware.util.DeepEquals.deepEquals; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -43,6 +42,7 @@ import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.UUID; import java.util.concurrent.Executors; @@ -241,7 +241,7 @@ public static void setHijackHash(DatabaseReference ref, boolean hijackHash) { * a true and more effective super set. */ public static void assertDeepEquals(Object a, Object b) { - if (!deepEquals(a, b)) { + if (!Objects.deepEquals(a, b)) { fail("Values different.\nExpected: " + a + "\nActual: " + b); } } diff --git a/src/test/java/com/google/firebase/database/ValueExpectationHelper.java b/src/test/java/com/google/firebase/database/ValueExpectationHelper.java index f7f15a03d..9732bf73b 100644 --- a/src/test/java/com/google/firebase/database/ValueExpectationHelper.java +++ b/src/test/java/com/google/firebase/database/ValueExpectationHelper.java @@ -18,10 +18,10 @@ import static org.junit.Assert.fail; -import com.cedarsoftware.util.DeepEquals; import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import java.util.Objects; import java.util.concurrent.Semaphore; public class ValueExpectationHelper { @@ -39,7 +39,7 @@ public void add(final Query query, final Object expected) { public void onDataChange(DataSnapshot snapshot) { Object result = snapshot.getValue(); // Hack to handle race condition in initial data - if (DeepEquals.deepEquals(expected, result)) { + if (Objects.deepEquals(expected, result)) { // We may pass through intermediate states, but we should end up with // the correct // state diff --git a/src/test/java/com/google/firebase/database/collection/RBTreeSortedMapTest.java b/src/test/java/com/google/firebase/database/collection/RBTreeSortedMapTest.java index 2fbc8ba90..f29ef208a 100644 --- a/src/test/java/com/google/firebase/database/collection/RBTreeSortedMapTest.java +++ b/src/test/java/com/google/firebase/database/collection/RBTreeSortedMapTest.java @@ -262,7 +262,7 @@ public void successorKeyIsCorrect() { lastKey = entry.getKey(); } if (lastKey != null) { - assertEquals(null, map.getSuccessorKey(lastKey)); + assertNull(map.getSuccessorKey(lastKey)); } } } @@ -295,13 +295,13 @@ public int compare(Integer o1, Integer o2) { arraycopy = arraycopy.insert(key, value); copyWithDifferentComparator = copyWithDifferentComparator.insert(key, value); } - Assert.assertTrue(map.equals(copy)); - Assert.assertTrue(map.equals(arraycopy)); - Assert.assertTrue(arraycopy.equals(map)); - - Assert.assertFalse(map.equals(copyWithDifferentComparator)); - Assert.assertFalse(map.equals(copy.remove(copy.getMaxKey()))); - Assert.assertFalse(map.equals(copy.insert(copy.getMaxKey() + 1, 1))); - Assert.assertFalse(map.equals(arraycopy.remove(arraycopy.getMaxKey()))); + Assert.assertEquals(map, copy); + Assert.assertEquals(map, arraycopy); + Assert.assertEquals(arraycopy, map); + + Assert.assertNotEquals(map, copyWithDifferentComparator); + Assert.assertNotEquals(map, copy.remove(copy.getMaxKey())); + Assert.assertNotEquals(map, copy.insert(copy.getMaxKey() + 1, 1)); + Assert.assertNotEquals(map, arraycopy.remove(arraycopy.getMaxKey())); } } diff --git a/src/test/java/com/google/firebase/database/core/JvmAuthTokenProviderTest.java b/src/test/java/com/google/firebase/database/core/JvmAuthTokenProviderTest.java index 7b8fe5229..9bc55950b 100644 --- a/src/test/java/com/google/firebase/database/core/JvmAuthTokenProviderTest.java +++ b/src/test/java/com/google/firebase/database/core/JvmAuthTokenProviderTest.java @@ -20,7 +20,6 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; -import com.cedarsoftware.util.DeepEquals; import com.google.auth.oauth2.AccessToken; import com.google.auth.oauth2.OAuth2Credentials; import com.google.common.collect.ImmutableMap; @@ -38,6 +37,7 @@ import java.util.Date; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -67,7 +67,7 @@ public void testGetToken() throws IOException, InterruptedException { credentials.refresh(); assertEquals(1, refreshDetector.count); - FirebaseOptions options = new FirebaseOptions.Builder() + FirebaseOptions options = FirebaseOptions.builder() .setCredentials(credentials) .build(); FirebaseApp app = FirebaseApp.initializeApp(options); @@ -87,7 +87,7 @@ public void testGetTokenNoRefresh() throws IOException, InterruptedException { credentials.refresh(); assertEquals(1, refreshDetector.count); - FirebaseOptions options = new FirebaseOptions.Builder() + FirebaseOptions options = FirebaseOptions.builder() .setCredentials(credentials) .build(); FirebaseApp app = FirebaseApp.initializeApp(options); @@ -103,7 +103,7 @@ public void testGetTokenNoRefresh() throws IOException, InterruptedException { public void testGetTokenWithAuthOverrides() throws InterruptedException, IOException { MockGoogleCredentials credentials = new MockGoogleCredentials("mock-token"); Map auth = ImmutableMap.of("uid", "test"); - FirebaseOptions options = new FirebaseOptions.Builder() + FirebaseOptions options = FirebaseOptions.builder() .setCredentials(credentials) .setDatabaseAuthVariableOverride(auth) .build(); @@ -119,11 +119,11 @@ public void testGetTokenWithAuthOverrides() throws InterruptedException, IOExcep public void testGetTokenError() throws InterruptedException { MockGoogleCredentials credentials = new MockGoogleCredentials("mock-token") { @Override - public AccessToken refreshAccessToken() throws IOException { + public AccessToken refreshAccessToken() { throw new RuntimeException("Test error"); } }; - FirebaseOptions options = new FirebaseOptions.Builder() + FirebaseOptions options = FirebaseOptions.builder() .setCredentials(credentials) .build(); FirebaseApp app = FirebaseApp.initializeApp(options); @@ -139,13 +139,13 @@ public void testAddTokenChangeListener() throws IOException { final AtomicInteger counter = new AtomicInteger(0); MockGoogleCredentials credentials = new MockGoogleCredentials() { @Override - public AccessToken refreshAccessToken() throws IOException { + public AccessToken refreshAccessToken() { Date expiry = new Date(System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1)); return new AccessToken("token-" + counter.getAndIncrement(), expiry); } }; - FirebaseOptions options = new FirebaseOptions.Builder() + FirebaseOptions options = FirebaseOptions.builder() .setCredentials(credentials) .build(); FirebaseApp app = FirebaseApp.initializeApp(options); @@ -172,7 +172,7 @@ public void onTokenChange(String token) { @Test public void testTokenChangeListenerThread() throws InterruptedException, IOException { MockGoogleCredentials credentials = new MockGoogleCredentials(); - FirebaseOptions options = new FirebaseOptions.Builder() + FirebaseOptions options = FirebaseOptions.builder() .setCredentials(credentials) .build(); FirebaseApp app = FirebaseApp.initializeApp(options); @@ -210,12 +210,12 @@ public void testTokenAutoRefresh() throws InterruptedException { final Semaphore semaphore = new Semaphore(0); credentials.addChangeListener(new OAuth2Credentials.CredentialsChangedListener() { @Override - public void onChanged(OAuth2Credentials credentials) throws IOException { + public void onChanged(OAuth2Credentials credentials) { semaphore.release(); } }); - FirebaseOptions options = new FirebaseOptions.Builder() + FirebaseOptions options = FirebaseOptions.builder() .setCredentials(credentials) .build(); FirebaseApp app = FirebaseApp.initializeApp(options); @@ -234,8 +234,8 @@ private void assertToken(String token, String expectedToken, Map assertEquals(expectedToken, map.get("token")); - Map auth = (Map)map.get("auth"); - DeepEquals.deepEquals(expectedAuth, auth); + Map auth = (Map) map.get("auth"); + assertTrue(Objects.deepEquals(expectedAuth, auth)); } private static class TestGetTokenListener @@ -271,7 +271,7 @@ private static class TokenRefreshDetector private int count = 0; @Override - public void onChanged(OAuth2Credentials credentials) throws IOException { + public void onChanged(OAuth2Credentials credentials) { count++; } } diff --git a/src/test/java/com/google/firebase/database/core/JvmPlatformTest.java b/src/test/java/com/google/firebase/database/core/JvmPlatformTest.java index c6912916f..044b583b3 100644 --- a/src/test/java/com/google/firebase/database/core/JvmPlatformTest.java +++ b/src/test/java/com/google/firebase/database/core/JvmPlatformTest.java @@ -56,7 +56,7 @@ protected ThreadFactory getThreadFactory() { return Executors.defaultThreadFactory(); } }; - FirebaseOptions options = new FirebaseOptions.Builder() + FirebaseOptions options = FirebaseOptions.builder() .setCredentials(TestUtils.getCertCredential(ServiceAccount.EDITOR.asStream())) .setThreadManager(threadManager) .build(); @@ -80,7 +80,7 @@ protected ThreadFactory getThreadFactory() { @Test public void userAgentHasCorrectParts() { - FirebaseOptions options = new FirebaseOptions.Builder() + FirebaseOptions options = FirebaseOptions.builder() .setCredentials(TestUtils.getCertCredential(ServiceAccount.EDITOR.asStream())) .build(); FirebaseApp app = FirebaseApp.initializeApp(options, "userAgentApp"); diff --git a/src/test/java/com/google/firebase/database/core/RepoTest.java b/src/test/java/com/google/firebase/database/core/RepoTest.java index ec9696e55..9c2281d4c 100644 --- a/src/test/java/com/google/firebase/database/core/RepoTest.java +++ b/src/test/java/com/google/firebase/database/core/RepoTest.java @@ -61,7 +61,7 @@ public class RepoTest { @BeforeClass public static void setUpClass() throws IOException { FirebaseApp testApp = FirebaseApp.initializeApp( - new FirebaseOptions.Builder() + FirebaseOptions.builder() .setCredentials(GoogleCredentials.fromStream(ServiceAccount.EDITOR.asStream())) .setDatabaseUrl("https://admin-java-sdk.firebaseio.com") .build()); diff --git a/src/test/java/com/google/firebase/database/core/SyncPointTest.java b/src/test/java/com/google/firebase/database/core/SyncPointTest.java index 91be0cce7..158a803e4 100644 --- a/src/test/java/com/google/firebase/database/core/SyncPointTest.java +++ b/src/test/java/com/google/firebase/database/core/SyncPointTest.java @@ -68,7 +68,7 @@ public class SyncPointTest { @BeforeClass public static void setUpClass() throws IOException { testApp = FirebaseApp.initializeApp( - new FirebaseOptions.Builder() + FirebaseOptions.builder() .setCredentials(GoogleCredentials.fromStream(ServiceAccount.EDITOR.asStream())) .setDatabaseUrl("https://admin-java-sdk.firebaseio.com") .build()); diff --git a/src/test/java/com/google/firebase/database/core/persistence/DefaultPersistenceManagerTest.java b/src/test/java/com/google/firebase/database/core/persistence/DefaultPersistenceManagerTest.java index 99450944c..342700a3d 100644 --- a/src/test/java/com/google/firebase/database/core/persistence/DefaultPersistenceManagerTest.java +++ b/src/test/java/com/google/firebase/database/core/persistence/DefaultPersistenceManagerTest.java @@ -56,7 +56,7 @@ public class DefaultPersistenceManagerTest { @BeforeClass public static void setUpClass() throws IOException { testApp = FirebaseApp.initializeApp( - new FirebaseOptions.Builder() + FirebaseOptions.builder() .setCredentials(GoogleCredentials.fromStream(ServiceAccount.EDITOR.asStream())) .setDatabaseUrl("https://admin-java-sdk.firebaseio.com") .build()); diff --git a/src/test/java/com/google/firebase/database/core/persistence/RandomPersistenceTest.java b/src/test/java/com/google/firebase/database/core/persistence/RandomPersistenceTest.java index 58358a8ff..d55db51c3 100644 --- a/src/test/java/com/google/firebase/database/core/persistence/RandomPersistenceTest.java +++ b/src/test/java/com/google/firebase/database/core/persistence/RandomPersistenceTest.java @@ -73,7 +73,7 @@ public class RandomPersistenceTest { @BeforeClass public static void setUpClass() throws IOException { testApp = FirebaseApp.initializeApp( - new FirebaseOptions.Builder() + FirebaseOptions.builder() .setCredentials(GoogleCredentials.fromStream(ServiceAccount.EDITOR.asStream())) .setDatabaseUrl("https://admin-java-sdk.firebaseio.com") .build()); diff --git a/src/test/java/com/google/firebase/database/integration/DataTestIT.java b/src/test/java/com/google/firebase/database/integration/DataTestIT.java index 2b2124a14..d832f995d 100644 --- a/src/test/java/com/google/firebase/database/integration/DataTestIT.java +++ b/src/test/java/com/google/firebase/database/integration/DataTestIT.java @@ -24,7 +24,6 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; -import com.cedarsoftware.util.DeepEquals; import com.google.common.collect.ImmutableList; import com.google.firebase.FirebaseApp; import com.google.firebase.database.ChildEventListener; @@ -56,6 +55,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Random; import java.util.concurrent.ExecutionException; import java.util.concurrent.Semaphore; @@ -1645,7 +1645,7 @@ public void testUpdateAfterSetLeafNodeWorks() throws InterruptedException { ref.addValueEventListener(new ValueEventListener() { @Override public void onDataChange(DataSnapshot snapshot) { - if (DeepEquals.deepEquals(snapshot.getValue(), expected)) { + if (Objects.deepEquals(snapshot.getValue(), expected)) { semaphore.release(); } } diff --git a/src/test/java/com/google/firebase/database/integration/FirebaseDatabaseAuthTestIT.java b/src/test/java/com/google/firebase/database/integration/FirebaseDatabaseAuthTestIT.java index d210039cc..58e313450 100644 --- a/src/test/java/com/google/firebase/database/integration/FirebaseDatabaseAuthTestIT.java +++ b/src/test/java/com/google/firebase/database/integration/FirebaseDatabaseAuthTestIT.java @@ -78,7 +78,7 @@ public void testAuthWithValidCertificateCredential() throws InterruptedException @Test public void testAuthWithInvalidCertificateCredential() throws InterruptedException, IOException { FirebaseOptions options = - new FirebaseOptions.Builder() + FirebaseOptions.builder() .setDatabaseUrl(IntegrationTestUtils.getDatabaseUrl()) .setCredentials(GoogleCredentials.fromStream(ServiceAccount.NONE.asStream())) .build(); @@ -94,10 +94,9 @@ public void testDatabaseAuthVariablesAuthorization() throws InterruptedException "uid", "test", "custom", "secret" ); - FirebaseOptions options = - new FirebaseOptions.Builder(masterApp.getOptions()) - .setDatabaseAuthVariableOverride(authVariableOverrides) - .build(); + FirebaseOptions options = masterApp.getOptions().toBuilder() + .setDatabaseAuthVariableOverride(authVariableOverrides) + .build(); FirebaseApp testUidApp = FirebaseApp.initializeApp(options, "testGetAppWithUid"); FirebaseDatabase masterDb = FirebaseDatabase.getInstance(masterApp); FirebaseDatabase testAuthOverridesDb = FirebaseDatabase.getInstance(testUidApp); @@ -114,10 +113,9 @@ public void testDatabaseAuthVariablesAuthorization() throws InterruptedException @Test public void testDatabaseAuthVariablesNoAuthorization() throws InterruptedException { - FirebaseOptions options = - new FirebaseOptions.Builder(masterApp.getOptions()) - .setDatabaseAuthVariableOverride(null) - .build(); + FirebaseOptions options = masterApp.getOptions().toBuilder() + .setDatabaseAuthVariableOverride(null) + .build(); FirebaseApp testUidApp = FirebaseApp.initializeApp(options, "testServiceAccountDatabaseWithNoAuth"); diff --git a/src/test/java/com/google/firebase/database/integration/FirebaseDatabaseTestIT.java b/src/test/java/com/google/firebase/database/integration/FirebaseDatabaseTestIT.java index f6451d9c5..84ec321a5 100644 --- a/src/test/java/com/google/firebase/database/integration/FirebaseDatabaseTestIT.java +++ b/src/test/java/com/google/firebase/database/integration/FirebaseDatabaseTestIT.java @@ -255,7 +255,7 @@ public void onCancelled(DatabaseError error) { private static FirebaseApp appWithDbUrl(String dbUrl, String name) { try { - FirebaseOptions options = new FirebaseOptions.Builder() + FirebaseOptions options = FirebaseOptions.builder() .setDatabaseUrl(dbUrl) .setCredentials(GoogleCredentials.fromStream( IntegrationTestUtils.getServiceAccountCertificate())) @@ -268,7 +268,7 @@ private static FirebaseApp appWithDbUrl(String dbUrl, String name) { private static FirebaseApp appWithoutDbUrl(String name) { try { - FirebaseOptions options = new FirebaseOptions.Builder() + FirebaseOptions options = FirebaseOptions.builder() .setCredentials(GoogleCredentials.fromStream( IntegrationTestUtils.getServiceAccountCertificate())) .build(); diff --git a/src/test/java/com/google/firebase/database/integration/RulesTestIT.java b/src/test/java/com/google/firebase/database/integration/RulesTestIT.java index 1745ce220..b46c1287f 100644 --- a/src/test/java/com/google/firebase/database/integration/RulesTestIT.java +++ b/src/test/java/com/google/firebase/database/integration/RulesTestIT.java @@ -103,7 +103,7 @@ public class RulesTestIT { public static void setUpClass() throws IOException { // Init app with non-admin privileges Map auth = MapBuilder.of("uid", "my-service-worker"); - FirebaseOptions options = new FirebaseOptions.Builder() + FirebaseOptions options = FirebaseOptions.builder() .setCredentials(GoogleCredentials.fromStream( IntegrationTestUtils.getServiceAccountCertificate())) .setDatabaseUrl(IntegrationTestUtils.getDatabaseUrl()) diff --git a/src/test/java/com/google/firebase/database/integration/ShutdownExample.java b/src/test/java/com/google/firebase/database/integration/ShutdownExample.java index 020e0416e..ded28fe0f 100644 --- a/src/test/java/com/google/firebase/database/integration/ShutdownExample.java +++ b/src/test/java/com/google/firebase/database/integration/ShutdownExample.java @@ -32,7 +32,7 @@ public static void main(String[] args) { FirebaseApp app = FirebaseApp.initializeApp( - new FirebaseOptions.Builder() + FirebaseOptions.builder() .setDatabaseUrl("https://admin-java-sdk.firebaseio.com") .build()); diff --git a/src/test/java/com/google/firebase/database/utilities/ValidationTest.java b/src/test/java/com/google/firebase/database/utilities/ValidationTest.java index b9e32a851..3293f5d6d 100644 --- a/src/test/java/com/google/firebase/database/utilities/ValidationTest.java +++ b/src/test/java/com/google/firebase/database/utilities/ValidationTest.java @@ -18,9 +18,11 @@ import static org.junit.Assert.fail; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.firebase.database.DatabaseException; import com.google.firebase.database.core.Path; +import java.util.List; import java.util.Map; import org.junit.Test; @@ -144,32 +146,32 @@ public void testNonWritablePath() { @Test public void testUpdate() { - Map[] updates = new Map[]{ - ImmutableMap.of("foo", "value"), - ImmutableMap.of("foo", ""), - ImmutableMap.of("foo", 10D), - ImmutableMap.of(".foo", "foo"), - ImmutableMap.of("foo", "value", "bar", "value"), - }; + List> updates = ImmutableList.>of( + ImmutableMap.of("foo", "value"), + ImmutableMap.of("foo", ""), + ImmutableMap.of("foo", 10D), + ImmutableMap.of(".foo", "foo"), + ImmutableMap.of("foo", "value", "bar", "value") + ); Path path = new Path("path"); - for (Map map : updates) { + for (Map map : updates) { Validation.parseAndValidateUpdate(path, map); } } @Test public void testInvalidUpdate() { - Map[] invalidUpdates = new Map[]{ - ImmutableMap.of(".sv", "foo"), - ImmutableMap.of(".value", "foo"), - ImmutableMap.of(".priority", ImmutableMap.of("a", "b")), - ImmutableMap.of("foo", "value", "foo/bar", "value"), - ImmutableMap.of("foo", Double.POSITIVE_INFINITY), - ImmutableMap.of("foo", Double.NEGATIVE_INFINITY), - ImmutableMap.of("foo", Double.NaN), - }; + List> invalidUpdates = ImmutableList.>of( + ImmutableMap.of(".sv", "foo"), + ImmutableMap.of(".value", "foo"), + ImmutableMap.of(".priority", ImmutableMap.of("a", "b")), + ImmutableMap.of("foo", "value", "foo/bar", "value"), + ImmutableMap.of("foo", Double.POSITIVE_INFINITY), + ImmutableMap.of("foo", Double.NEGATIVE_INFINITY), + ImmutableMap.of("foo", Double.NaN) + ); Path path = new Path("path"); - for (Map map : invalidUpdates) { + for (Map map : invalidUpdates) { try { Validation.parseAndValidateUpdate(path, map); fail("No error thrown for invalid update: " + map); diff --git a/src/test/java/com/google/firebase/iid/FirebaseInstanceIdTest.java b/src/test/java/com/google/firebase/iid/FirebaseInstanceIdTest.java index 366bb7e29..f15861a62 100644 --- a/src/test/java/com/google/firebase/iid/FirebaseInstanceIdTest.java +++ b/src/test/java/com/google/firebase/iid/FirebaseInstanceIdTest.java @@ -23,18 +23,25 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import com.google.api.client.http.HttpMethods; import com.google.api.client.http.HttpRequest; import com.google.api.client.http.HttpResponseException; +import com.google.api.client.http.HttpTransport; import com.google.api.client.testing.http.MockHttpTransport; import com.google.api.client.testing.http.MockLowLevelHttpResponse; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.firebase.ErrorCode; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; +import com.google.firebase.IncomingHttpResponse; +import com.google.firebase.OutgoingHttpRequest; import com.google.firebase.TestOnlyImplFirebaseTrampolines; import com.google.firebase.auth.MockGoogleCredentials; import com.google.firebase.testing.GenericFunction; import com.google.firebase.testing.TestResponseInterceptor; +import com.google.firebase.testing.TestUtils; +import java.io.IOException; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutionException; @@ -43,6 +50,25 @@ public class FirebaseInstanceIdTest { + private static final Map ERROR_MESSAGES = ImmutableMap.of( + 404, "Instance ID \"test-iid\": Failed to find the instance ID.", + 409, "Instance ID \"test-iid\": Already deleted.", + 429, "Instance ID \"test-iid\": Request throttled out by the backend server.", + 500, "Instance ID \"test-iid\": Internal server error.", + 501, "Unexpected HTTP response with status: 501\ntest error" + ); + + private static final Map ERROR_CODES = ImmutableMap.of( + 404, ErrorCode.NOT_FOUND, + 409, ErrorCode.CONFLICT, + 429, ErrorCode.RESOURCE_EXHAUSTED, + 500, ErrorCode.INTERNAL, + 501, ErrorCode.UNKNOWN + ); + + private static final String TEST_URL = + "https://console.firebase.google.com/v1/project/test-project/instanceId/test-iid"; + @After public void tearDown() { TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); @@ -50,7 +76,7 @@ public void tearDown() { @Test public void testNoProjectId() { - FirebaseOptions options = new FirebaseOptions.Builder() + FirebaseOptions options = FirebaseOptions.builder() .setCredentials(new MockGoogleCredentials("test-token")) .build(); FirebaseApp.initializeApp(options); @@ -64,7 +90,7 @@ public void testNoProjectId() { @Test public void testInvalidInstanceId() { - FirebaseOptions options = new FirebaseOptions.Builder() + FirebaseOptions options = FirebaseOptions.builder() .setCredentials(new MockGoogleCredentials("test-token")) .setProjectId("test-project") .build(); @@ -96,7 +122,7 @@ public void testDeleteInstanceId() throws Exception { MockHttpTransport transport = new MockHttpTransport.Builder() .setLowLevelHttpResponse(response) .build(); - FirebaseOptions options = new FirebaseOptions.Builder() + FirebaseOptions options = FirebaseOptions.builder() .setCredentials(new MockGoogleCredentials("test-token")) .setProjectId("test-project") .setHttpTransport(transport) @@ -123,7 +149,6 @@ public Void call(Object... args) throws Exception { } ); - String url = "https://console.firebase.google.com/v1/project/test-project/instanceId/test-iid"; for (GenericFunction fn : functions) { TestResponseInterceptor interceptor = new TestResponseInterceptor(); instanceId.setInterceptor(interceptor); @@ -131,54 +156,113 @@ public Void call(Object... args) throws Exception { assertNotNull(interceptor.getResponse()); HttpRequest request = interceptor.getResponse().getRequest(); - assertEquals("DELETE", request.getRequestMethod()); - assertEquals(url, request.getUrl().toString()); + assertEquals(HttpMethods.DELETE, request.getRequestMethod()); + assertEquals(TEST_URL, request.getUrl().toString()); assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); } } @Test public void testDeleteInstanceIdError() throws Exception { - Map errors = ImmutableMap.of( - 404, "Instance ID \"test-iid\": Failed to find the instance ID.", - 429, "Instance ID \"test-iid\": Request throttled out by the backend server.", - 500, "Instance ID \"test-iid\": Internal server error.", - 501, "Error while invoking instance ID service." - ); + final MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + MockHttpTransport transport = new MockHttpTransport.Builder() + .setLowLevelHttpResponse(response) + .build(); + FirebaseOptions options = FirebaseOptions.builder() + .setCredentials(new MockGoogleCredentials("test-token")) + .setProjectId("test-project") + .setHttpTransport(transport) + .build(); + FirebaseApp app = FirebaseApp.initializeApp(options); - String url = "https://console.firebase.google.com/v1/project/test-project/instanceId/test-iid"; - for (Map.Entry entry : errors.entrySet()) { - MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() - .setStatusCode(entry.getKey()) - .setContent("test error"); - MockHttpTransport transport = new MockHttpTransport.Builder() - .setLowLevelHttpResponse(response) - .build(); - FirebaseOptions options = new FirebaseOptions.Builder() - .setCredentials(new MockGoogleCredentials("test-token")) - .setProjectId("test-project") - .setHttpTransport(transport) - .build(); - final FirebaseApp app = FirebaseApp.initializeApp(options); - - FirebaseInstanceId instanceId = FirebaseInstanceId.getInstance(); - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - instanceId.setInterceptor(interceptor); - try { - instanceId.deleteInstanceIdAsync("test-iid").get(); - fail("No error thrown for HTTP error"); - } catch (ExecutionException e) { - assertTrue(e.getCause() instanceof FirebaseInstanceIdException); - assertEquals(entry.getValue(), e.getCause().getMessage()); - assertTrue(e.getCause().getCause() instanceof HttpResponseException); - } + // Disable retries by passing a regular HttpRequestFactory. + FirebaseInstanceId instanceId = new FirebaseInstanceId(app, transport.createRequestFactory()); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + instanceId.setInterceptor(interceptor); - assertNotNull(interceptor.getResponse()); - HttpRequest request = interceptor.getResponse().getRequest(); - assertEquals("DELETE", request.getRequestMethod()); - assertEquals(url, request.getUrl().toString()); - assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); + try { + for (int statusCode : ERROR_CODES.keySet()) { + response.setStatusCode(statusCode).setContent("test error"); + + try { + instanceId.deleteInstanceIdAsync("test-iid").get(); + fail("No error thrown for HTTP error"); + } catch (ExecutionException e) { + assertTrue(e.getCause() instanceof FirebaseInstanceIdException); + checkFirebaseInstanceIdException((FirebaseInstanceIdException) e.getCause(), statusCode); + } + + assertNotNull(interceptor.getResponse()); + HttpRequest request = interceptor.getResponse().getRequest(); + assertEquals(HttpMethods.DELETE, request.getRequestMethod()); + assertEquals(TEST_URL, request.getUrl().toString()); + } + } finally { app.delete(); } } + + @Test + public void testDeleteInstanceIdTransportError() throws Exception { + HttpTransport transport = TestUtils.createFaultyHttpTransport(); + FirebaseOptions options = FirebaseOptions.builder() + .setCredentials(new MockGoogleCredentials("test-token")) + .setProjectId("test-project") + .setHttpTransport(transport) + .build(); + FirebaseApp app = FirebaseApp.initializeApp(options); + // Disable retries by passing a regular HttpRequestFactory. + FirebaseInstanceId instanceId = new FirebaseInstanceId(app, transport.createRequestFactory()); + + try { + instanceId.deleteInstanceIdAsync("test-iid").get(); + fail("No error thrown for HTTP error"); + } catch (ExecutionException e) { + assertTrue(e.getCause() instanceof FirebaseInstanceIdException); + FirebaseInstanceIdException error = (FirebaseInstanceIdException) e.getCause(); + assertEquals(ErrorCode.UNKNOWN, error.getErrorCode()); + assertEquals( + "Unknown error while making a remote service call: transport error", + error.getMessage()); + assertTrue(error.getCause() instanceof IOException); + assertNull(error.getHttpResponse()); + } + } + + @Test + public void testDeleteInstanceIdInvalidJsonIgnored() throws Exception { + final MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + MockHttpTransport transport = new MockHttpTransport.Builder() + .setLowLevelHttpResponse(response) + .build(); + FirebaseOptions options = FirebaseOptions.builder() + .setCredentials(new MockGoogleCredentials("test-token")) + .setProjectId("test-project") + .setHttpTransport(transport) + .build(); + FirebaseApp app = FirebaseApp.initializeApp(options); + + // Disable retries by passing a regular HttpRequestFactory. + FirebaseInstanceId instanceId = new FirebaseInstanceId(app, transport.createRequestFactory()); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + instanceId.setInterceptor(interceptor); + response.setContent("not json"); + + instanceId.deleteInstanceIdAsync("test-iid").get(); + + assertNotNull(interceptor.getResponse()); + } + + private void checkFirebaseInstanceIdException(FirebaseInstanceIdException error, int statusCode) { + assertEquals(ERROR_CODES.get(statusCode), error.getErrorCode()); + assertEquals(ERROR_MESSAGES.get(statusCode), error.getMessage()); + assertTrue(error.getCause() instanceof HttpResponseException); + + IncomingHttpResponse httpResponse = error.getHttpResponse(); + assertNotNull(httpResponse); + assertEquals(statusCode, httpResponse.getStatusCode()); + OutgoingHttpRequest request = httpResponse.getRequest(); + assertEquals(HttpMethods.DELETE, request.getMethod()); + assertEquals(TEST_URL, request.getUrl()); + } } diff --git a/src/test/java/com/google/firebase/internal/AbstractPlatformErrorHandlerTest.java b/src/test/java/com/google/firebase/internal/AbstractPlatformErrorHandlerTest.java new file mode 100644 index 000000000..7e927f51b --- /dev/null +++ b/src/test/java/com/google/firebase/internal/AbstractPlatformErrorHandlerTest.java @@ -0,0 +1,298 @@ +/* + * Copyright 2020 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.internal; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import com.google.api.client.googleapis.util.Utils; +import com.google.api.client.http.HttpMethods; +import com.google.api.client.http.HttpStatusCodes; +import com.google.api.client.http.LowLevelHttpRequest; +import com.google.api.client.testing.http.MockHttpTransport; +import com.google.api.client.testing.http.MockLowLevelHttpResponse; +import com.google.api.client.util.GenericData; +import com.google.firebase.ErrorCode; +import com.google.firebase.FirebaseException; +import com.google.firebase.IncomingHttpResponse; +import java.io.IOException; +import java.net.NoRouteToHostException; +import java.net.SocketTimeoutException; +import java.net.UnknownHostException; +import org.junit.Test; + +public class AbstractPlatformErrorHandlerTest { + + private static final HttpRequestInfo TEST_REQUEST = HttpRequestInfo.buildGetRequest( + "https://firebase.google.com"); + + @Test + public void testPlatformError() { + String payload = "{\"error\": {\"status\": \"UNAVAILABLE\", \"message\": \"Test error\"}}"; + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() + .setStatusCode(HttpStatusCodes.STATUS_CODE_SERVER_ERROR) + .setContent(payload); + ErrorHandlingHttpClient client = createHttpClient(response); + + try { + client.sendAndParse(TEST_REQUEST, GenericData.class); + fail("No exception thrown for HTTP error response"); + } catch (MockFirebaseException e) { + assertEquals(ErrorCode.UNAVAILABLE, e.getErrorCode()); + assertEquals("Test error", e.getMessage()); + assertHttpResponse(e, HttpStatusCodes.STATUS_CODE_SERVER_ERROR, payload); + assertNotNull(e.getCause()); + } + } + + @Test + public void testNonJsonError() { + String payload = "not json"; + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() + .setStatusCode(HttpStatusCodes.STATUS_CODE_SERVER_ERROR) + .setContent(payload); + ErrorHandlingHttpClient client = createHttpClient(response); + + try { + client.sendAndParse(TEST_REQUEST, GenericData.class); + fail("No exception thrown for HTTP error response"); + } catch (MockFirebaseException e) { + assertEquals(ErrorCode.INTERNAL, e.getErrorCode()); + assertEquals("Unexpected HTTP response with status: 500\nnot json", e.getMessage()); + assertHttpResponse(e, HttpStatusCodes.STATUS_CODE_SERVER_ERROR, payload); + assertNotNull(e.getCause()); + } + } + + @Test + public void testPlatformErrorWithoutCode() { + String payload = "{\"error\": {\"message\": \"Test error\"}}"; + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() + .setStatusCode(HttpStatusCodes.STATUS_CODE_SERVER_ERROR) + .setContent(payload); + ErrorHandlingHttpClient client = createHttpClient(response); + + try { + client.sendAndParse(TEST_REQUEST, GenericData.class); + fail("No exception thrown for HTTP error response"); + } catch (MockFirebaseException e) { + assertEquals(ErrorCode.INTERNAL, e.getErrorCode()); + assertEquals("Test error", e.getMessage()); + assertHttpResponse(e, HttpStatusCodes.STATUS_CODE_SERVER_ERROR, payload); + assertNotNull(e.getCause()); + } + } + + @Test + public void testPlatformErrorWithoutMessage() { + String payload = "{\"error\": {\"status\": \"INVALID_ARGUMENT\"}}"; + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() + .setStatusCode(HttpStatusCodes.STATUS_CODE_SERVER_ERROR) + .setContent(payload); + ErrorHandlingHttpClient client = createHttpClient(response); + + try { + client.sendAndParse(TEST_REQUEST, GenericData.class); + fail("No exception thrown for HTTP error response"); + } catch (MockFirebaseException e) { + assertEquals(ErrorCode.INVALID_ARGUMENT, e.getErrorCode()); + assertEquals("Unexpected HTTP response with status: 500\n" + payload, e.getMessage()); + assertHttpResponse(e, HttpStatusCodes.STATUS_CODE_SERVER_ERROR, payload); + assertNotNull(e.getCause()); + } + } + + @Test + public void testPlatformErrorWithoutCodeOrMessage() { + String payload = "{}"; + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() + .setStatusCode(HttpStatusCodes.STATUS_CODE_SERVER_ERROR) + .setContent(payload); + ErrorHandlingHttpClient client = createHttpClient(response); + + try { + client.sendAndParse(TEST_REQUEST, GenericData.class); + fail("No exception thrown for HTTP error response"); + } catch (MockFirebaseException e) { + assertEquals(ErrorCode.INTERNAL, e.getErrorCode()); + assertEquals("Unexpected HTTP response with status: 500\n" + payload, e.getMessage()); + assertHttpResponse(e, HttpStatusCodes.STATUS_CODE_SERVER_ERROR, payload); + assertNotNull(e.getCause()); + } + } + + @Test + public void testGenericIOException() { + IOException exception = new IOException("Test"); + ErrorHandlingHttpClient client = createHttpClient(exception); + + try { + client.sendAndParse(TEST_REQUEST, GenericData.class); + fail("No exception thrown for HTTP error response"); + } catch (MockFirebaseException e) { + assertEquals(ErrorCode.UNKNOWN, e.getErrorCode()); + assertEquals( + "Unknown error while making a remote service call: Test", e.getMessage()); + assertNull(e.getHttpResponse()); + assertSame(exception, e.getCause()); + } + } + + @Test + public void testTimeoutError() { + IOException exception = new IOException("Test", new SocketTimeoutException()); + ErrorHandlingHttpClient client = createHttpClient(exception); + + try { + client.sendAndParse(TEST_REQUEST, GenericData.class); + fail("No exception thrown for HTTP error response"); + } catch (MockFirebaseException e) { + assertEquals(ErrorCode.DEADLINE_EXCEEDED, e.getErrorCode()); + assertEquals( + "Timed out while making an API call: Test", e.getMessage()); + assertNull(e.getHttpResponse()); + assertSame(exception, e.getCause()); + } + } + + @Test + public void testNoRouteToHostError() { + IOException exception = new IOException("Test", new NoRouteToHostException()); + ErrorHandlingHttpClient client = createHttpClient(exception); + + try { + client.sendAndParse(TEST_REQUEST, GenericData.class); + fail("No exception thrown for HTTP error response"); + } catch (MockFirebaseException e) { + assertEquals(ErrorCode.UNAVAILABLE, e.getErrorCode()); + assertEquals( + "Failed to establish a connection: Test", e.getMessage()); + assertNull(e.getHttpResponse()); + assertSame(exception, e.getCause()); + } + } + + @Test + public void testUnknownHostError() { + IOException exception = new IOException("Test", new UnknownHostException()); + ErrorHandlingHttpClient client = createHttpClient(exception); + + try { + client.sendAndParse(TEST_REQUEST, GenericData.class); + fail("No exception thrown for HTTP error response"); + } catch (MockFirebaseException e) { + assertEquals(ErrorCode.UNAVAILABLE, e.getErrorCode()); + assertEquals( + "Failed to establish a connection: Test", e.getMessage()); + assertNull(e.getHttpResponse()); + assertSame(exception, e.getCause()); + } + } + + @Test + public void testParseError() { + String payload = "not json"; + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() + .setContent(payload); + ErrorHandlingHttpClient client = createHttpClient(response); + + try { + client.sendAndParse(TEST_REQUEST, GenericData.class); + fail("No exception thrown for HTTP error response"); + } catch (MockFirebaseException e) { + assertEquals(ErrorCode.UNKNOWN, e.getErrorCode()); + assertTrue(e.getMessage().startsWith("Error while parsing HTTP response: ")); + assertHttpResponse(e, HttpStatusCodes.STATUS_CODE_OK, payload); + assertNotNull(e.getCause()); + } + } + + @Test + public void testUnknownHttpError() { + String payload = "{\"error\": {\"message\": \"Test error\"}}"; + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() + .setStatusCode(512) + .setContent(payload); + ErrorHandlingHttpClient client = createHttpClient(response); + + try { + client.sendAndParse(TEST_REQUEST, GenericData.class); + fail("No exception thrown for HTTP error response"); + } catch (MockFirebaseException e) { + assertEquals(ErrorCode.UNKNOWN, e.getErrorCode()); + assertEquals("Test error", e.getMessage()); + assertHttpResponse(e, 512, payload); + assertNotNull(e.getCause()); + } + } + + private ErrorHandlingHttpClient createHttpClient( + MockLowLevelHttpResponse response) { + MockHttpTransport transport = new MockHttpTransport.Builder() + .setLowLevelHttpResponse(response) + .build(); + return new ErrorHandlingHttpClient<>( + transport.createRequestFactory(), + Utils.getDefaultJsonFactory(), + new TestPlatformErrorHandler()); + } + + private ErrorHandlingHttpClient createHttpClient( + final IOException exception) { + MockHttpTransport transport = new MockHttpTransport(){ + @Override + public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { + throw exception; + } + }; + return new ErrorHandlingHttpClient<>( + transport.createRequestFactory(), + Utils.getDefaultJsonFactory(), + new TestPlatformErrorHandler()); + } + + private void assertHttpResponse(FirebaseException e, int statusCode, String content) { + IncomingHttpResponse httpResponse = e.getHttpResponse(); + assertNotNull(httpResponse); + assertEquals(statusCode, httpResponse.getStatusCode()); + assertEquals(content, httpResponse.getContent()); + assertEquals(HttpMethods.GET, httpResponse.getRequest().getMethod()); + } + + private static class TestPlatformErrorHandler extends + AbstractPlatformErrorHandler { + + TestPlatformErrorHandler() { + super(Utils.getDefaultJsonFactory()); + } + + @Override + protected MockFirebaseException createException(FirebaseException base) { + return new MockFirebaseException(base); + } + } + + private static class MockFirebaseException extends FirebaseException { + MockFirebaseException(FirebaseException base) { + super(base.getErrorCode(), base.getMessage(), base.getCause(), base.getHttpResponse()); + } + } +} diff --git a/src/test/java/com/google/firebase/internal/CallableOperationTest.java b/src/test/java/com/google/firebase/internal/CallableOperationTest.java index 2ead9a9c7..d38c96776 100644 --- a/src/test/java/com/google/firebase/internal/CallableOperationTest.java +++ b/src/test/java/com/google/firebase/internal/CallableOperationTest.java @@ -24,7 +24,6 @@ import com.google.common.util.concurrent.ThreadFactoryBuilder; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; -import com.google.firebase.FirebaseOptions.Builder; import com.google.firebase.TestOnlyImplFirebaseTrampolines; import com.google.firebase.auth.MockGoogleCredentials; import com.google.firebase.internal.FirebaseThreadManagers.GlobalThreadManager; @@ -38,7 +37,7 @@ public class CallableOperationTest { private static final String TEST_FIREBASE_THREAD = "test-firebase-thread"; - private static final FirebaseOptions OPTIONS = new Builder() + private static final FirebaseOptions OPTIONS = FirebaseOptions.builder() .setCredentials(new MockGoogleCredentials()) .setThreadManager(new MockThreadManager()) .build(); diff --git a/src/test/java/com/google/firebase/internal/ErrorHandlingHttpClientTest.java b/src/test/java/com/google/firebase/internal/ErrorHandlingHttpClientTest.java new file mode 100644 index 000000000..591914002 --- /dev/null +++ b/src/test/java/com/google/firebase/internal/ErrorHandlingHttpClientTest.java @@ -0,0 +1,358 @@ +/* + * Copyright 2020 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.internal; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.fail; + +import com.google.api.client.googleapis.util.Utils; +import com.google.api.client.http.ByteArrayContent; +import com.google.api.client.http.HttpContent; +import com.google.api.client.http.HttpMethods; +import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.HttpRequestFactory; +import com.google.api.client.http.HttpResponseException; +import com.google.api.client.http.HttpStatusCodes; +import com.google.api.client.http.LowLevelHttpRequest; +import com.google.api.client.json.JsonFactory; +import com.google.api.client.testing.http.MockHttpTransport; +import com.google.api.client.testing.http.MockLowLevelHttpResponse; +import com.google.api.client.testing.util.MockSleeper; +import com.google.api.client.util.GenericData; +import com.google.auth.oauth2.AccessToken; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.firebase.ErrorCode; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseException; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.IncomingHttpResponse; +import com.google.firebase.auth.MockGoogleCredentials; +import com.google.firebase.testing.TestResponseInterceptor; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import org.junit.Test; + +public class ErrorHandlingHttpClientTest { + + private static final JsonFactory DEFAULT_JSON_FACTORY = Utils.getDefaultJsonFactory(); + + private static final HttpRequestInfo TEST_REQUEST = HttpRequestInfo.buildGetRequest( + "https://firebase.google.com"); + + @Test(expected = NullPointerException.class) + public void testNullRequestFactory() { + new ErrorHandlingHttpClient<>( + null, + DEFAULT_JSON_FACTORY, + new TestHttpErrorHandler()); + } + + @Test(expected = NullPointerException.class) + public void testNullJsonFactory() { + new ErrorHandlingHttpClient<>( + Utils.getDefaultTransport().createRequestFactory(), + null, + new TestHttpErrorHandler()); + } + + @Test(expected = NullPointerException.class) + public void testNullErrorHandler() { + new ErrorHandlingHttpClient<>( + Utils.getDefaultTransport().createRequestFactory(), + DEFAULT_JSON_FACTORY, + null); + } + + @Test + public void testSuccessfulRequest() throws FirebaseException { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() + .setContent("{\"foo\": \"bar\"}"); + ErrorHandlingHttpClient client = createHttpClient(response); + + GenericData body = client.sendAndParse(TEST_REQUEST, GenericData.class); + + assertEquals(1, body.size()); + assertEquals("bar", body.get("foo")); + } + + @Test + public void testSuccessfulRequestWithoutContent() throws FirebaseException { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() + .setZeroContent(); + ErrorHandlingHttpClient client = createHttpClient(response); + + IncomingHttpResponse responseInfo = client.send(TEST_REQUEST); + + assertEquals(HttpStatusCodes.STATUS_CODE_OK, responseInfo.getStatusCode()); + assertNull(responseInfo.getContent()); + } + + @Test + public void testSuccessfulRequestWithHeadersAndBody() throws FirebaseException, IOException { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() + .setContent("{\"foo\": \"bar\"}"); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + ErrorHandlingHttpClient client = createHttpClient(response) + .setInterceptor(interceptor); + + HttpRequestInfo request = HttpRequestInfo.buildJsonPostRequest( + "https://firebase.google.com", ImmutableMap.of("key", "value")); + + request.addHeader("h1", "v1") + .addAllHeaders(ImmutableMap.of("h2", "v2", "h3", "v3")); + GenericData body = client.sendAndParse(request, GenericData.class); + + assertEquals(1, body.size()); + assertEquals("bar", body.get("foo")); + HttpRequest last = interceptor.getLastRequest(); + assertEquals(HttpMethods.POST, last.getRequestMethod()); + assertEquals("v1", last.getHeaders().get("h1")); + assertEquals("v2", last.getHeaders().get("h2")); + assertEquals("v3", last.getHeaders().get("h3")); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + last.getContent().writeTo(out); + assertEquals("{\"key\":\"value\"}", out.toString()); + } + + @Test + public void testSuccessfulRequestWithNonJsonBody() throws FirebaseException, IOException { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() + .setContent("{\"foo\": \"bar\"}"); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + ErrorHandlingHttpClient client = createHttpClient(response) + .setInterceptor(interceptor); + HttpContent content = new ByteArrayContent("text/plain", "Test".getBytes()); + + HttpRequestInfo request = HttpRequestInfo.buildRequest( + HttpMethods.POST, "https://firebase.google.com", content); + + GenericData body = client.sendAndParse(request, GenericData.class); + + assertEquals(1, body.size()); + assertEquals("bar", body.get("foo")); + HttpRequest last = interceptor.getLastRequest(); + assertEquals(HttpMethods.POST, last.getRequestMethod()); + assertEquals("text/plain", last.getContent().getType()); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + last.getContent().writeTo(out); + assertEquals("Test", out.toString()); + } + + @Test + public void testUnsupportedMethod() throws FirebaseException, IOException { + MockHttpTransport transport = new MockHttpTransport.Builder() + .setLowLevelHttpResponse(new MockLowLevelHttpResponse().setContent("{}")) + .setSupportedMethods(ImmutableSet.of(HttpMethods.GET, HttpMethods.POST)) + .build(); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + ErrorHandlingHttpClient client = new ErrorHandlingHttpClient<>( + transport.createRequestFactory(), DEFAULT_JSON_FACTORY, new TestHttpErrorHandler()); + client.setInterceptor(interceptor); + HttpRequestInfo patchRequest = HttpRequestInfo.buildJsonRequest( + HttpMethods.PATCH, "https://firebase.google.com", ImmutableMap.of("key", "value")); + + client.sendAndParse(patchRequest, GenericData.class); + + HttpRequest last = interceptor.getLastRequest(); + assertEquals(HttpMethods.POST, last.getRequestMethod()); + assertEquals(HttpMethods.PATCH, last.getHeaders().get("X-HTTP-Method-Override")); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + last.getContent().writeTo(out); + assertEquals("{\"key\":\"value\"}", out.toString()); + } + + @Test + public void testNetworkError() { + final IOException exception = new IOException("Test"); + MockHttpTransport transport = new MockHttpTransport(){ + @Override + public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { + throw exception; + } + }; + ErrorHandlingHttpClient client = new ErrorHandlingHttpClient<>( + transport.createRequestFactory(), + DEFAULT_JSON_FACTORY, + new TestHttpErrorHandler()); + + try { + client.sendAndParse(TEST_REQUEST, GenericData.class); + fail("No exception thrown for HTTP error response"); + } catch (FirebaseException e) { + assertEquals(ErrorCode.UNKNOWN, e.getErrorCode()); + assertEquals("IO error: Test", e.getMessage()); + assertNull(e.getHttpResponse()); + assertSame(exception, e.getCause()); + } + } + + @Test + public void testErrorResponse() { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() + .setStatusCode(HttpStatusCodes.STATUS_CODE_SERVER_ERROR) + .addHeader("Custom-Header", "value") + .setContent("{}"); + ErrorHandlingHttpClient client = createHttpClient(response); + + try { + client.sendAndParse(TEST_REQUEST, GenericData.class); + fail("No exception thrown for HTTP error response"); + } catch (FirebaseException e) { + assertEquals(ErrorCode.INTERNAL, e.getErrorCode()); + assertEquals("Example error message: {}", e.getMessage()); + assertHttpResponse(e, HttpStatusCodes.STATUS_CODE_SERVER_ERROR, "{}"); + IncomingHttpResponse httpResponse = e.getHttpResponse(); + assertEquals(1, httpResponse.getHeaders().size()); + assertEquals(ImmutableList.of("value"), httpResponse.getHeaders().get("custom-header")); + assertNotNull(e.getCause()); + } + } + + @Test + public void testParseError() { + String payload = "not json"; + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() + .setContent(payload); + ErrorHandlingHttpClient client = createHttpClient(response); + + try { + client.sendAndParse(TEST_REQUEST, GenericData.class); + fail("No exception thrown for HTTP error response"); + } catch (FirebaseException e) { + assertEquals(ErrorCode.UNKNOWN, e.getErrorCode()); + assertEquals("Parse error", e.getMessage()); + assertHttpResponse(e, HttpStatusCodes.STATUS_CODE_OK, payload); + assertNotNull(e.getCause()); + } + } + + @Test + public void testRetryOnError() { + CountingLowLevelHttpRequest request = CountingLowLevelHttpRequest.fromStatus(503); + MockHttpTransport transport = new MockHttpTransport.Builder() + .setLowLevelHttpRequest(request) + .build(); + + FirebaseApp app = FirebaseApp.initializeApp(FirebaseOptions.builder() + .setCredentials(new MockGoogleCredentials("token")) + .setHttpTransport(transport) + .build()); + RetryConfig retryConfig = RetryConfig.builder() + .setMaxRetries(4) + .setRetryStatusCodes(ImmutableList.of(503)) + .setSleeper(new MockSleeper()) + .build(); + HttpRequestFactory requestFactory = ApiClientUtils.newAuthorizedRequestFactory( + app, retryConfig); + ErrorHandlingHttpClient client = new ErrorHandlingHttpClient<>( + requestFactory, Utils.getDefaultJsonFactory(), new TestHttpErrorHandler()); + + try { + client.sendAndParse(TEST_REQUEST, GenericData.class); + fail("No exception thrown for HTTP error response"); + } catch (FirebaseException e) { + assertEquals(ErrorCode.INTERNAL, e.getErrorCode()); + assertEquals("Example error message: null", e.getMessage()); + assertHttpResponse(e, HttpStatusCodes.STATUS_CODE_SERVICE_UNAVAILABLE, null); + assertNotNull(e.getCause()); + + assertEquals(5, request.getCount()); + } finally { + app.delete(); + } + } + + @Test + public void testRequestInitializationError() { + CountingLowLevelHttpRequest request = CountingLowLevelHttpRequest.fromStatus(503); + MockHttpTransport transport = new MockHttpTransport.Builder() + .setLowLevelHttpRequest(request) + .build(); + + FirebaseApp app = FirebaseApp.initializeApp(FirebaseOptions.builder() + .setCredentials(new MockGoogleCredentials() { + @Override + public AccessToken refreshAccessToken() throws IOException { + throw new IOException("Failed to fetch credentials"); + } + }) + .setHttpTransport(transport) + .build()); + HttpRequestFactory requestFactory = ApiClientUtils.newAuthorizedRequestFactory(app); + ErrorHandlingHttpClient client = new ErrorHandlingHttpClient<>( + requestFactory, Utils.getDefaultJsonFactory(), new TestHttpErrorHandler()); + + try { + client.sendAndParse(TEST_REQUEST, GenericData.class); + fail("No exception thrown for HTTP error response"); + } catch (FirebaseException e) { + assertEquals(ErrorCode.UNKNOWN, e.getErrorCode()); + assertEquals("IO error: Failed to fetch credentials", e.getMessage()); + assertNull(e.getHttpResponse()); + assertNotNull(e.getCause()); + } finally { + app.delete(); + } + } + + private ErrorHandlingHttpClient createHttpClient( + MockLowLevelHttpResponse response) { + MockHttpTransport transport = new MockHttpTransport.Builder() + .setLowLevelHttpResponse(response) + .build(); + return new ErrorHandlingHttpClient<>( + transport.createRequestFactory(), + DEFAULT_JSON_FACTORY, + new TestHttpErrorHandler()); + } + + private void assertHttpResponse(FirebaseException e, int statusCode, String content) { + IncomingHttpResponse httpResponse = e.getHttpResponse(); + assertNotNull(httpResponse); + assertEquals(statusCode, httpResponse.getStatusCode()); + assertEquals(content, httpResponse.getContent()); + assertEquals("GET", httpResponse.getRequest().getMethod()); + } + + private static class TestHttpErrorHandler implements HttpErrorHandler { + @Override + public FirebaseException handleIOException(IOException e) { + return new FirebaseException( + ErrorCode.UNKNOWN, "IO error: " + e.getMessage(), e); + } + + @Override + public FirebaseException handleHttpResponseException( + HttpResponseException e, IncomingHttpResponse response) { + return new FirebaseException( + ErrorCode.INTERNAL, "Example error message: " + e.getContent(), e, response); + } + + @Override + public FirebaseException handleParseException(IOException e, IncomingHttpResponse response) { + return new FirebaseException(ErrorCode.UNKNOWN, "Parse error", e, response); + } + } +} diff --git a/src/test/java/com/google/firebase/internal/FirebaseAppStoreTest.java b/src/test/java/com/google/firebase/internal/FirebaseAppStoreTest.java index 4b3699d82..b6b502fdc 100644 --- a/src/test/java/com/google/firebase/internal/FirebaseAppStoreTest.java +++ b/src/test/java/com/google/firebase/internal/FirebaseAppStoreTest.java @@ -16,8 +16,6 @@ package com.google.firebase.internal; -import static org.junit.Assert.assertTrue; - import com.google.auth.oauth2.GoogleCredentials; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; @@ -34,7 +32,7 @@ public class FirebaseAppStoreTest { private static final String FIREBASE_DB_URL = "https://mock-project.firebaseio.com"; private static final FirebaseOptions ALL_VALUES_OPTIONS = - new FirebaseOptions.Builder() + FirebaseOptions.builder() .setDatabaseUrl(FIREBASE_DB_URL) .setCredentials(TestUtils.getCertCredential(ServiceAccount.EDITOR.asStream())) .build(); @@ -55,7 +53,7 @@ public void incompatibleAppInitializedDoesntThrow() throws IOException { FirebaseApp.initializeApp(ALL_VALUES_OPTIONS, name); TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); FirebaseOptions options = - new FirebaseOptions.Builder() + FirebaseOptions.builder() .setCredentials(GoogleCredentials.fromStream(ServiceAccount.EDITOR.asStream())) .build(); FirebaseApp.initializeApp(options, name); @@ -66,18 +64,9 @@ public void incompatibleDefaultAppInitializedDoesntThrow() throws IOException { FirebaseApp.initializeApp(ALL_VALUES_OPTIONS); TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); FirebaseOptions options = - new FirebaseOptions.Builder() + FirebaseOptions.builder() .setCredentials(GoogleCredentials.fromStream(ServiceAccount.EDITOR.asStream())) .build(); FirebaseApp.initializeApp(options); } - - @Test - public void persistenceDisabled() { - String name = "myApp"; - FirebaseApp.initializeApp(ALL_VALUES_OPTIONS, name); - TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); - FirebaseAppStore appStore = FirebaseAppStore.getInstance(); - assertTrue(!appStore.getAllPersistedAppNames().contains(name)); - } } diff --git a/src/test/java/com/google/firebase/internal/FirebaseRequestInitializerTest.java b/src/test/java/com/google/firebase/internal/FirebaseRequestInitializerTest.java index 1f610f5e3..fde2f918b 100644 --- a/src/test/java/com/google/firebase/internal/FirebaseRequestInitializerTest.java +++ b/src/test/java/com/google/firebase/internal/FirebaseRequestInitializerTest.java @@ -46,7 +46,7 @@ public void tearDown() { @Test public void testDefaultSettings() throws Exception { - FirebaseApp app = FirebaseApp.initializeApp(new FirebaseOptions.Builder() + FirebaseApp app = FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(new MockGoogleCredentials("token")) .build()); HttpRequest request = TestUtils.createRequest(); @@ -65,7 +65,7 @@ public void testDefaultSettings() throws Exception { @Test public void testExplicitTimeouts() throws Exception { - FirebaseApp app = FirebaseApp.initializeApp(new FirebaseOptions.Builder() + FirebaseApp app = FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(new MockGoogleCredentials("token")) .setConnectTimeout(CONNECT_TIMEOUT_MILLIS) .setReadTimeout(READ_TIMEOUT_MILLIS) @@ -85,7 +85,7 @@ public void testExplicitTimeouts() throws Exception { @Test public void testRetryConfig() throws Exception { - FirebaseApp app = FirebaseApp.initializeApp(new FirebaseOptions.Builder() + FirebaseApp app = FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(new MockGoogleCredentials("token")) .build()); RetryConfig retryConfig = RetryConfig.builder() @@ -106,7 +106,7 @@ public void testRetryConfig() throws Exception { @Test public void testRetryConfigWithIOExceptionHandling() throws Exception { - FirebaseApp app = FirebaseApp.initializeApp(new FirebaseOptions.Builder() + FirebaseApp app = FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(new MockGoogleCredentials("token")) .build()); RetryConfig retryConfig = RetryConfig.builder() @@ -128,7 +128,7 @@ public void testRetryConfigWithIOExceptionHandling() throws Exception { @Test public void testCredentialsRetryHandler() throws Exception { - FirebaseApp app = FirebaseApp.initializeApp(new FirebaseOptions.Builder() + FirebaseApp app = FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(new MockGoogleCredentials("token")) .build()); RetryConfig retryConfig = RetryConfig.builder() diff --git a/src/test/java/com/google/firebase/internal/FirebaseThreadManagersTest.java b/src/test/java/com/google/firebase/internal/FirebaseThreadManagersTest.java index 2b117f29f..286cdf09f 100644 --- a/src/test/java/com/google/firebase/internal/FirebaseThreadManagersTest.java +++ b/src/test/java/com/google/firebase/internal/FirebaseThreadManagersTest.java @@ -49,7 +49,7 @@ public void tearDown() { @Test public void testGlobalThreadManager() { MockThreadManager threadManager = new MockThreadManager(); - FirebaseOptions options = new FirebaseOptions.Builder() + FirebaseOptions options = FirebaseOptions.builder() .setCredentials(new MockGoogleCredentials()) .setThreadManager(threadManager) .build(); @@ -74,7 +74,7 @@ public void testGlobalThreadManager() { @Test public void testGlobalThreadManagerWithMultipleApps() { MockThreadManager threadManager = new MockThreadManager(); - FirebaseOptions options = new FirebaseOptions.Builder() + FirebaseOptions options = FirebaseOptions.builder() .setCredentials(new MockGoogleCredentials()) .build(); FirebaseApp defaultApp = FirebaseApp.initializeApp(options); @@ -99,7 +99,7 @@ public void testGlobalThreadManagerWithMultipleApps() { @Test public void testGlobalThreadManagerReInit() { MockThreadManager threadManager = new MockThreadManager(); - FirebaseOptions options = new FirebaseOptions.Builder() + FirebaseOptions options = FirebaseOptions.builder() .setCredentials(new MockGoogleCredentials()) .setThreadManager(threadManager) .build(); @@ -125,7 +125,7 @@ public void testGlobalThreadManagerReInit() { @Test public void testDefaultThreadManager() throws Exception { - FirebaseOptions options = new FirebaseOptions.Builder() + FirebaseOptions options = FirebaseOptions.builder() .setCredentials(new MockGoogleCredentials()) .build(); FirebaseApp defaultApp = FirebaseApp.initializeApp(options); diff --git a/src/test/java/com/google/firebase/internal/TestApiClientUtils.java b/src/test/java/com/google/firebase/internal/TestApiClientUtils.java index 9f4bc2d09..510e08cd5 100644 --- a/src/test/java/com/google/firebase/internal/TestApiClientUtils.java +++ b/src/test/java/com/google/firebase/internal/TestApiClientUtils.java @@ -18,17 +18,14 @@ import static com.google.firebase.internal.ApiClientUtils.DEFAULT_RETRY_CONFIG; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; -import com.google.api.client.http.GenericUrl; import com.google.api.client.http.HttpRequest; import com.google.api.client.http.HttpRequestFactory; import com.google.api.client.http.HttpUnsuccessfulResponseHandler; import com.google.api.client.testing.util.MockSleeper; import com.google.firebase.FirebaseApp; import com.google.firebase.internal.RetryInitializer.RetryHandlerDecorator; -import java.io.IOException; public class TestApiClientUtils { @@ -39,8 +36,6 @@ public class TestApiClientUtils { .setSleeper(new MockSleeper()) .build(); - private static final GenericUrl TEST_URL = new GenericUrl("https://firebase.google.com"); - /** * Creates a new {@code HttpRequestFactory} which provides authorization (OAuth2), timeouts and * automatic retries. Bypasses exponential backoff between consecutive retries for faster @@ -65,20 +60,12 @@ public static HttpRequestFactory retryDisabledRequestFactory(FirebaseApp app) { } /** - * Checks whther the given HttpRequestFactory has been configured for authorization and + * Checks whether the given HttpRequest has been configured for authorization and * automatic retries. * - * @param requestFactory The HttpRequestFactory to check. + * @param request The HttpRequest to check. */ - public static void assertAuthAndRetrySupport(HttpRequestFactory requestFactory) { - assertTrue(requestFactory.getInitializer() instanceof FirebaseRequestInitializer); - HttpRequest request; - try { - request = requestFactory.buildGetRequest(TEST_URL); - } catch (IOException e) { - throw new RuntimeException("Failed to initialize request", e); - } - + public static void assertAuthAndRetrySupport(HttpRequest request) { // Verify authorization assertTrue(request.getHeaders().getAuthorization().startsWith("Bearer ")); @@ -89,7 +76,7 @@ public static void assertAuthAndRetrySupport(HttpRequestFactory requestFactory) .getRetryConfig(); assertEquals(DEFAULT_RETRY_CONFIG.getMaxRetries(), retryConfig.getMaxRetries()); assertEquals(DEFAULT_RETRY_CONFIG.getMaxIntervalMillis(), retryConfig.getMaxIntervalMillis()); - assertFalse(retryConfig.isRetryOnIOExceptions()); + assertEquals(DEFAULT_RETRY_CONFIG.isRetryOnIOExceptions(), retryConfig.isRetryOnIOExceptions()); assertEquals(DEFAULT_RETRY_CONFIG.getRetryStatusCodes(), retryConfig.getRetryStatusCodes()); } } diff --git a/src/test/java/com/google/firebase/messaging/BatchResponseTest.java b/src/test/java/com/google/firebase/messaging/BatchResponseTest.java index 9c174f569..441822220 100644 --- a/src/test/java/com/google/firebase/messaging/BatchResponseTest.java +++ b/src/test/java/com/google/firebase/messaging/BatchResponseTest.java @@ -21,6 +21,7 @@ import static org.junit.Assert.fail; import com.google.common.collect.ImmutableList; +import com.google.firebase.ErrorCode; import java.util.ArrayList; import java.util.List; import org.junit.Test; @@ -43,8 +44,8 @@ public void testSomeResponse() { ImmutableList responses = ImmutableList.of( SendResponse.fromMessageId("message1"), SendResponse.fromMessageId("message2"), - SendResponse.fromException(new FirebaseMessagingException("error-code", - "error-message", null)) + SendResponse.fromException( + new FirebaseMessagingException(ErrorCode.INTERNAL, "error-message")) ); BatchResponse batchResponse = new BatchResponseImpl(responses); diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingClientImplTest.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingClientImplTest.java index 0b84e008e..9f8780e89 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingClientImplTest.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingClientImplTest.java @@ -27,6 +27,7 @@ import com.google.api.client.googleapis.util.Utils; import com.google.api.client.http.GenericUrl; import com.google.api.client.http.HttpHeaders; +import com.google.api.client.http.HttpMethods; import com.google.api.client.http.HttpRequest; import com.google.api.client.http.HttpRequestInitializer; import com.google.api.client.http.HttpResponseException; @@ -37,8 +38,10 @@ import com.google.api.client.testing.http.MockLowLevelHttpResponse; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.firebase.ErrorCode; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; +import com.google.firebase.OutgoingHttpRequest; import com.google.firebase.auth.MockGoogleCredentials; import com.google.firebase.internal.SdkUtils; import com.google.firebase.messaging.WebpushNotification.Action; @@ -62,6 +65,11 @@ public class FirebaseMessagingClientImplTest { private static final List HTTP_ERRORS = ImmutableList.of(401, 404, 500); + private static final Map HTTP_2_ERROR = ImmutableMap.of( + 401, ErrorCode.UNAUTHENTICATED, + 404, ErrorCode.NOT_FOUND, + 500, ErrorCode.INTERNAL); + private static final String MOCK_RESPONSE = "{\"name\": \"mock-name\"}"; private static final String MOCK_BATCH_SUCCESS_RESPONSE = TestUtils.loadResource( @@ -128,8 +136,8 @@ public void testSendHttpError() { client.send(EMPTY_MESSAGE, DRY_RUN_DISABLED); fail("No error thrown for HTTP error"); } catch (FirebaseMessagingException error) { - checkExceptionFromHttpResponse(error, "unknown-error", - "Unexpected HTTP response with status: " + code + "; body: {}"); + checkExceptionFromHttpResponse(error, HTTP_2_ERROR.get(code), null, + "Unexpected HTTP response with status: " + code + "\n{}"); } checkRequestHeader(interceptor.getLastRequest()); } @@ -143,9 +151,12 @@ public void testSendTransportError() { client.send(EMPTY_MESSAGE, DRY_RUN_DISABLED); fail("No error thrown for HTTP error"); } catch (FirebaseMessagingException error) { - assertEquals("internal-error", error.getErrorCode()); - assertEquals("Error while calling FCM backend service", error.getMessage()); + assertEquals(ErrorCode.UNKNOWN, error.getErrorCode()); + assertEquals("Unknown error while making a remote service call: transport error", + error.getMessage()); assertTrue(error.getCause() instanceof IOException); + assertNull(error.getHttpResponse()); + assertNull(error.getMessagingErrorCode()); } } @@ -160,8 +171,11 @@ public void testSendSuccessResponseWithUnexpectedPayload() { client.send(entry.getKey(), DRY_RUN_DISABLED); fail("No error thrown for malformed response"); } catch (FirebaseMessagingException error) { - assertEquals("internal-error", error.getErrorCode()); - assertEquals("Error while calling FCM backend service", error.getMessage()); + assertEquals(ErrorCode.UNKNOWN, error.getErrorCode()); + assertTrue(error.getMessage().startsWith("Error while parsing HTTP response: ")); + assertNotNull(error.getCause()); + assertNotNull(error.getHttpResponse()); + assertNull(error.getMessagingErrorCode()); } checkRequestHeader(interceptor.getLastRequest()); } @@ -176,8 +190,8 @@ public void testSendErrorWithZeroContentResponse() { client.send(EMPTY_MESSAGE, DRY_RUN_DISABLED); fail("No error thrown for HTTP error"); } catch (FirebaseMessagingException error) { - checkExceptionFromHttpResponse(error, "unknown-error", - "Unexpected HTTP response with status: " + code + "; body: null"); + checkExceptionFromHttpResponse(error, HTTP_2_ERROR.get(code), null, + "Unexpected HTTP response with status: " + code + "\nnull"); } checkRequestHeader(interceptor.getLastRequest()); } @@ -192,8 +206,8 @@ public void testSendErrorWithMalformedResponse() { client.send(EMPTY_MESSAGE, DRY_RUN_DISABLED); fail("No error thrown for HTTP error"); } catch (FirebaseMessagingException error) { - checkExceptionFromHttpResponse(error, "unknown-error", - "Unexpected HTTP response with status: " + code + "; body: not json"); + checkExceptionFromHttpResponse(error, HTTP_2_ERROR.get(code), null, + "Unexpected HTTP response with status: " + code + "\nnot json"); } checkRequestHeader(interceptor.getLastRequest()); } @@ -209,7 +223,7 @@ public void testSendErrorWithDetails() { client.send(EMPTY_MESSAGE, DRY_RUN_DISABLED); fail("No error thrown for HTTP error"); } catch (FirebaseMessagingException error) { - checkExceptionFromHttpResponse(error, "invalid-argument"); + checkExceptionFromHttpResponse(error, ErrorCode.INVALID_ARGUMENT, null); } checkRequestHeader(interceptor.getLastRequest()); } @@ -225,7 +239,7 @@ public void testSendErrorWithCanonicalCode() { client.send(EMPTY_MESSAGE, DRY_RUN_DISABLED); fail("No error thrown for HTTP error"); } catch (FirebaseMessagingException error) { - checkExceptionFromHttpResponse(error, "registration-token-not-registered"); + checkExceptionFromHttpResponse(error, ErrorCode.NOT_FOUND, null); } checkRequestHeader(interceptor.getLastRequest()); } @@ -243,7 +257,8 @@ public void testSendErrorWithFcmError() { client.send(EMPTY_MESSAGE, DRY_RUN_DISABLED); fail("No error thrown for HTTP error"); } catch (FirebaseMessagingException error) { - checkExceptionFromHttpResponse(error, "registration-token-not-registered"); + checkExceptionFromHttpResponse(error, ErrorCode.INVALID_ARGUMENT, + MessagingErrorCode.UNREGISTERED); } checkRequestHeader(interceptor.getLastRequest()); } @@ -261,7 +276,44 @@ public void testSendErrorWithThirdPartyError() { client.send(EMPTY_MESSAGE, DRY_RUN_DISABLED); fail("No error thrown for HTTP error"); } catch (FirebaseMessagingException error) { - checkExceptionFromHttpResponse(error, "third-party-auth-error"); + checkExceptionFromHttpResponse(error, ErrorCode.INVALID_ARGUMENT, + MessagingErrorCode.THIRD_PARTY_AUTH_ERROR); + } + checkRequestHeader(interceptor.getLastRequest()); + } + } + + @Test + public void testSendErrorWithUnknownFcmErrorCode() { + for (int code : HTTP_ERRORS) { + response.setStatusCode(code).setContent( + "{\"error\": {\"status\": \"INVALID_ARGUMENT\", \"message\": \"test error\", " + + "\"details\":[{\"@type\": \"type.googleapis.com/google.firebase.fcm" + + ".v1.FcmError\", \"errorCode\": \"UNKNOWN_FCM_ERROR\"}]}}"); + + try { + client.send(EMPTY_MESSAGE, DRY_RUN_DISABLED); + fail("No error thrown for HTTP error"); + } catch (FirebaseMessagingException error) { + checkExceptionFromHttpResponse(error, ErrorCode.INVALID_ARGUMENT, null); + } + checkRequestHeader(interceptor.getLastRequest()); + } + } + + @Test + public void testSendErrorWithDetailsAndNoCode() { + for (int code : HTTP_ERRORS) { + response.setStatusCode(code).setContent( + "{\"error\": {\"status\": \"INVALID_ARGUMENT\", \"message\": \"test error\", " + + "\"details\":[{\"@type\": \"type.googleapis.com/google.firebase.fcm" + + ".v1.FcmError\"}]}}"); + + try { + client.send(EMPTY_MESSAGE, DRY_RUN_DISABLED); + fail("No error thrown for HTTP error"); + } catch (FirebaseMessagingException error) { + checkExceptionFromHttpResponse(error, ErrorCode.INVALID_ARGUMENT, null); } checkRequestHeader(interceptor.getLastRequest()); } @@ -338,8 +390,8 @@ public void testSendAllHttpError() { client.sendAll(MESSAGE_LIST, DRY_RUN_DISABLED); fail("No error thrown for HTTP error"); } catch (FirebaseMessagingException error) { - checkExceptionFromHttpResponse(error, "unknown-error", - "Unexpected HTTP response with status: " + code + "; body: {}"); + checkExceptionFromHttpResponse(error, HTTP_2_ERROR.get(code), null, + "Unexpected HTTP response with status: " + code + "\n{}"); } checkBatchRequestHeader(interceptor.getLastRequest()); } @@ -353,9 +405,12 @@ public void testSendAllTransportError() { client.sendAll(MESSAGE_LIST, DRY_RUN_DISABLED); fail("No error thrown for HTTP error"); } catch (FirebaseMessagingException error) { - assertEquals("internal-error", error.getErrorCode()); - assertEquals("Error while calling FCM backend service", error.getMessage()); + assertEquals(ErrorCode.UNKNOWN, error.getErrorCode()); + assertEquals( + "Unknown error while making a remote service call: transport error", error.getMessage()); assertTrue(error.getCause() instanceof IOException); + assertNull(error.getHttpResponse()); + assertNull(error.getMessagingErrorCode()); } } @@ -368,8 +423,8 @@ public void testSendAllErrorWithEmptyResponse() { client.sendAll(MESSAGE_LIST, DRY_RUN_DISABLED); fail("No error thrown for HTTP error"); } catch (FirebaseMessagingException error) { - checkExceptionFromHttpResponse(error, "unknown-error", - "Unexpected HTTP response with status: " + code + "; body: null"); + checkExceptionFromHttpResponse(error, HTTP_2_ERROR.get(code), null, + "Unexpected HTTP response with status: " + code + "\nnull"); } checkBatchRequestHeader(interceptor.getLastRequest()); } @@ -385,7 +440,7 @@ public void testSendAllErrorWithDetails() { client.sendAll(MESSAGE_LIST, DRY_RUN_DISABLED); fail("No error thrown for HTTP error"); } catch (FirebaseMessagingException error) { - checkExceptionFromHttpResponse(error, "invalid-argument"); + checkExceptionFromHttpResponse(error, ErrorCode.INVALID_ARGUMENT, null); } checkBatchRequestHeader(interceptor.getLastRequest()); } @@ -401,7 +456,7 @@ public void testSendAllErrorWithCanonicalCode() { client.sendAll(MESSAGE_LIST, DRY_RUN_DISABLED); fail("No error thrown for HTTP error"); } catch (FirebaseMessagingException error) { - checkExceptionFromHttpResponse(error, "registration-token-not-registered"); + checkExceptionFromHttpResponse(error, ErrorCode.NOT_FOUND, null); } checkBatchRequestHeader(interceptor.getLastRequest()); } @@ -419,7 +474,8 @@ public void testSendAllErrorWithFcmError() { client.sendAll(MESSAGE_LIST, DRY_RUN_DISABLED); fail("No error thrown for HTTP error"); } catch (FirebaseMessagingException error) { - checkExceptionFromHttpResponse(error, "registration-token-not-registered"); + checkExceptionFromHttpResponse(error, ErrorCode.INVALID_ARGUMENT, + MessagingErrorCode.UNREGISTERED); } checkBatchRequestHeader(interceptor.getLastRequest()); } @@ -437,8 +493,9 @@ public void testSendAllErrorWithoutMessage() { client.sendAll(MESSAGE_LIST, DRY_RUN_DISABLED); fail("No error thrown for HTTP error"); } catch (FirebaseMessagingException error) { - checkExceptionFromHttpResponse(error, "registration-token-not-registered", - "Unexpected HTTP response with status: " + code + "; body: " + responseBody); + checkExceptionFromHttpResponse(error, ErrorCode.INVALID_ARGUMENT, + MessagingErrorCode.UNREGISTERED, + "Unexpected HTTP response with status: " + code + "\n" + responseBody); } checkBatchRequestHeader(interceptor.getLastRequest()); } @@ -466,7 +523,7 @@ public void testBuilderNullChildRequestFactory() { @Test public void testFromApp() throws IOException { - FirebaseOptions options = new FirebaseOptions.Builder() + FirebaseOptions options = FirebaseOptions.builder() .setCredentials(new MockGoogleCredentials("test-token")) .setProjectId("test-project") .build(); @@ -476,7 +533,6 @@ public void testFromApp() throws IOException { FirebaseMessagingClientImpl client = FirebaseMessagingClientImpl.fromApp(app); assertEquals(TEST_FCM_URL, client.getFcmSendUrl()); - assertEquals("fire-admin-java/" + SdkUtils.getVersion(), client.getClientVersion()); assertSame(options.getJsonFactory(), client.getJsonFactory()); HttpRequest request = client.getRequestFactory().buildGetRequest( @@ -569,7 +625,10 @@ private void assertBatchResponse( FirebaseMessagingException exception = sendResponse.getException(); assertNotNull(exception); - assertEquals("invalid-argument", exception.getErrorCode()); + assertEquals(ErrorCode.INVALID_ARGUMENT, exception.getErrorCode()); + assertNull(exception.getCause()); + assertNull(exception.getHttpResponse()); + assertEquals(MessagingErrorCode.INVALID_ARGUMENT, exception.getMessagingErrorCode()); } checkBatchRequestHeader(interceptor.getLastRequest()); @@ -610,15 +669,26 @@ private FirebaseMessagingClientImpl.Builder fullyPopulatedBuilder() { } private void checkExceptionFromHttpResponse( - FirebaseMessagingException error, String expectedCode) { - checkExceptionFromHttpResponse(error, expectedCode, "test error"); + FirebaseMessagingException error, + ErrorCode expectedCode, + MessagingErrorCode expectedMessagingCode) { + checkExceptionFromHttpResponse(error, expectedCode, expectedMessagingCode, "test error"); } private void checkExceptionFromHttpResponse( - FirebaseMessagingException error, String expectedCode, String expectedMessage) { + FirebaseMessagingException error, + ErrorCode expectedCode, + MessagingErrorCode expectedMessagingCode, + String expectedMessage) { assertEquals(expectedCode, error.getErrorCode()); assertEquals(expectedMessage, error.getMessage()); assertTrue(error.getCause() instanceof HttpResponseException); + assertEquals(expectedMessagingCode, error.getMessagingErrorCode()); + + assertNotNull(error.getHttpResponse()); + OutgoingHttpRequest request = error.getHttpResponse().getRequest(); + assertEquals(HttpMethods.POST, request.getMethod()); + assertTrue(request.getUrl().startsWith("https://fcm.googleapis.com")); } private static Map> buildTestMessages() { @@ -633,7 +703,10 @@ private static Map> buildTestMessages() { // Notification message builder.put( Message.builder() - .setNotification(new Notification("test title", "test body")) + .setNotification(Notification.builder() + .setTitle("test title") + .setBody("test body") + .build()) .setTopic("test-topic") .build(), ImmutableMap.of( diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java index 4d9af55d0..9ae7bba24 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingIT.java @@ -22,10 +22,13 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import com.google.api.client.http.HttpResponseException; import com.google.common.collect.ImmutableList; +import com.google.firebase.ErrorCode; import com.google.firebase.testing.IntegrationTestUtils; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.ExecutionException; import org.junit.BeforeClass; import org.junit.Test; @@ -72,22 +75,52 @@ public void testSend() throws Exception { assertTrue(id != null && id.matches("^projects/.*/messages/.*$")); } + @Test + public void testSendError() throws InterruptedException { + FirebaseMessaging messaging = FirebaseMessaging.getInstance(); + Message message = Message.builder() + .setNotification(Notification.builder() + .setTitle("Title") + .setBody("Body") + .build()) + .setToken("not-a-token") + .build(); + try { + messaging.sendAsync(message, true).get(); + } catch (ExecutionException e) { + FirebaseMessagingException cause = (FirebaseMessagingException) e.getCause(); + assertEquals(ErrorCode.INVALID_ARGUMENT, cause.getErrorCode()); + assertEquals(MessagingErrorCode.INVALID_ARGUMENT, cause.getMessagingErrorCode()); + assertNotNull(cause.getHttpResponse()); + assertTrue(cause.getCause() instanceof HttpResponseException); + } + } + @Test public void testSendAll() throws Exception { List messages = new ArrayList<>(); messages.add( Message.builder() - .setNotification(new Notification("Title", "Body")) + .setNotification(Notification.builder() + .setTitle("Title") + .setBody("Body") + .build()) .setTopic("foo-bar") .build()); messages.add( Message.builder() - .setNotification(new Notification("Title", "Body")) + .setNotification(Notification.builder() + .setTitle("Title") + .setBody("Body") + .build()) .setTopic("foo-bar") .build()); messages.add( Message.builder() - .setNotification(new Notification("Title", "Body")) + .setNotification(Notification.builder() + .setTitle("Title") + .setBody("Body") + .build()) .setToken("not-a-token") .build()); @@ -110,7 +143,7 @@ public void testSendAll() throws Exception { assertNull(responses.get(2).getMessageId()); FirebaseMessagingException exception = responses.get(2).getException(); assertNotNull(exception); - assertEquals("invalid-argument", exception.getErrorCode()); + assertEquals(ErrorCode.INVALID_ARGUMENT, exception.getErrorCode()); } @Test @@ -139,7 +172,10 @@ public void testSendFiveHundred() throws Exception { @Test public void testSendMulticast() throws Exception { MulticastMessage multicastMessage = MulticastMessage.builder() - .setNotification(new Notification("Title", "Body")) + .setNotification(Notification.builder() + .setTitle("Title") + .setBody("Body") + .build()) .addToken("not-a-token") .addToken("also-not-a-token") .build(); diff --git a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java index 496823175..8e8e79e07 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java @@ -27,6 +27,7 @@ import com.google.common.base.Supplier; import com.google.common.base.Suppliers; import com.google.common.collect.ImmutableList; +import com.google.firebase.ErrorCode; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; import com.google.firebase.TestOnlyImplFirebaseTrampolines; @@ -50,7 +51,7 @@ public class FirebaseMessagingTest { .addToken("test-fcm-token2") .build(); private static final FirebaseMessagingException TEST_EXCEPTION = - new FirebaseMessagingException("TEST_CODE", "Test error message", new Exception()); + new FirebaseMessagingException(ErrorCode.INTERNAL, "Test error message"); private static final ImmutableList.Builder TOO_MANY_IDS = ImmutableList.builder(); diff --git a/src/test/java/com/google/firebase/messaging/InstanceIdClientImplTest.java b/src/test/java/com/google/firebase/messaging/InstanceIdClientImplTest.java index c7e1cc24a..7724e4f6e 100644 --- a/src/test/java/com/google/firebase/messaging/InstanceIdClientImplTest.java +++ b/src/test/java/com/google/firebase/messaging/InstanceIdClientImplTest.java @@ -17,12 +17,13 @@ package com.google.firebase.messaging; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import com.google.api.client.googleapis.util.Utils; -import com.google.api.client.http.GenericUrl; +import com.google.api.client.http.HttpMethods; import com.google.api.client.http.HttpRequest; import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.HttpResponseInterceptor; @@ -31,8 +32,12 @@ import com.google.api.client.testing.http.MockHttpTransport; import com.google.api.client.testing.http.MockLowLevelHttpResponse; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.firebase.ErrorCode; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; +import com.google.firebase.IncomingHttpResponse; +import com.google.firebase.OutgoingHttpRequest; import com.google.firebase.TestOnlyImplFirebaseTrampolines; import com.google.firebase.auth.MockGoogleCredentials; import com.google.firebase.testing.TestResponseInterceptor; @@ -55,6 +60,11 @@ public class InstanceIdClientImplTest { private static final List HTTP_ERRORS = ImmutableList.of(401, 404, 500); + private static final Map HTTP_2_ERROR = ImmutableMap.of( + 401, ErrorCode.UNAUTHENTICATED, + 404, ErrorCode.NOT_FOUND, + 500, ErrorCode.INTERNAL); + @After public void tearDown() { TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); @@ -98,14 +108,16 @@ public void testSubscribeError() { TestResponseInterceptor interceptor = new TestResponseInterceptor(); InstanceIdClient client = initInstanceIdClient(response, interceptor); + String content = "{\"error\": \"ErrorCode\"}"; for (int statusCode : HTTP_ERRORS) { - response.setStatusCode(statusCode).setContent("{\"error\": \"test error\"}"); + response.setStatusCode(statusCode).setContent(content); try { client.subscribeToTopic("test-topic", ImmutableList.of("id1", "id2")); fail("No error thrown for HTTP error"); } catch (FirebaseMessagingException error) { - checkExceptionFromHttpResponse(error, statusCode, "test error"); + String expectedMessage = "Error while calling the IID service: ErrorCode"; + checkExceptionFromHttpResponse(error, statusCode, expectedMessage); } checkTopicManagementRequestHeader(interceptor.getLastRequest(), TEST_IID_SUBSCRIBE_URL); @@ -124,7 +136,7 @@ public void testSubscribeEmptyPayloadError() { fail("No error thrown for HTTP error"); } catch (FirebaseMessagingException error) { checkExceptionFromHttpResponse(error, 500, - "Unexpected HTTP response with status: 500; body: {}"); + "Unexpected HTTP response with status: 500\n{}"); } checkTopicManagementRequestHeader(interceptor.getLastRequest(), TEST_IID_SUBSCRIBE_URL); @@ -142,7 +154,7 @@ public void testSubscribeMalformedError() { fail("No error thrown for HTTP error"); } catch (FirebaseMessagingException error) { checkExceptionFromHttpResponse(error, 500, - "Unexpected HTTP response with status: 500; body: not json"); + "Unexpected HTTP response with status: 500\nnot json"); } checkTopicManagementRequestHeader(interceptor.getLastRequest(), TEST_IID_SUBSCRIBE_URL); @@ -160,7 +172,7 @@ public void testSubscribeZeroContentError() { fail("No error thrown for HTTP error"); } catch (FirebaseMessagingException error) { checkExceptionFromHttpResponse(error, 500, - "Unexpected HTTP response with status: 500; body: null"); + "Unexpected HTTP response with status: 500\nnull"); } checkTopicManagementRequestHeader(interceptor.getLastRequest(), TEST_IID_SUBSCRIBE_URL); @@ -174,8 +186,26 @@ public void testSubscribeTransportError() { client.subscribeToTopic("test-topic", ImmutableList.of("id1", "id2")); fail("No error thrown for HTTP error"); } catch (FirebaseMessagingException error) { - assertEquals("internal-error", error.getErrorCode()); - assertEquals("Error while calling IID backend service", error.getMessage()); + assertEquals(ErrorCode.UNKNOWN, error.getErrorCode()); + assertEquals( + "Unknown error while making a remote service call: transport error", error.getMessage()); + assertTrue(error.getCause() instanceof IOException); + } + } + + @Test + public void testSubscribeParseError() { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() + .setContent("not json"); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + InstanceIdClient client = initInstanceIdClient(response, interceptor); + + try { + client.subscribeToTopic("test-topic", ImmutableList.of("id1", "id2")); + fail("No error thrown for HTTP error"); + } catch (FirebaseMessagingException error) { + assertEquals(ErrorCode.UNKNOWN, error.getErrorCode()); + assertTrue(error.getMessage().startsWith("Error while parsing HTTP response: ")); assertTrue(error.getCause() instanceof IOException); } } @@ -218,14 +248,16 @@ public void testUnsubscribeError() { TestResponseInterceptor interceptor = new TestResponseInterceptor(); InstanceIdClient client = initInstanceIdClient(response, interceptor); + String content = "{\"error\": \"ErrorCode\"}"; for (int statusCode : HTTP_ERRORS) { - response.setStatusCode(statusCode).setContent("{\"error\": \"test error\"}"); + response.setStatusCode(statusCode).setContent(content); try { client.unsubscribeFromTopic("test-topic", ImmutableList.of("id1", "id2")); fail("No error thrown for HTTP error"); } catch (FirebaseMessagingException error) { - checkExceptionFromHttpResponse(error, statusCode, "test error"); + String expectedMessage = "Error while calling the IID service: ErrorCode"; + checkExceptionFromHttpResponse(error, statusCode, expectedMessage); } checkTopicManagementRequestHeader(interceptor.getLastRequest(), TEST_IID_UNSUBSCRIBE_URL); @@ -244,7 +276,7 @@ public void testUnsubscribeEmptyPayloadError() { fail("No error thrown for HTTP error"); } catch (FirebaseMessagingException error) { checkExceptionFromHttpResponse(error, 500, - "Unexpected HTTP response with status: 500; body: {}"); + "Unexpected HTTP response with status: 500\n{}"); } checkTopicManagementRequestHeader(interceptor.getLastRequest(), TEST_IID_UNSUBSCRIBE_URL); @@ -262,7 +294,7 @@ public void testUnsubscribeMalformedError() { fail("No error thrown for HTTP error"); } catch (FirebaseMessagingException error) { checkExceptionFromHttpResponse(error, 500, - "Unexpected HTTP response with status: 500; body: not json"); + "Unexpected HTTP response with status: 500\nnot json"); } checkTopicManagementRequestHeader(interceptor.getLastRequest(), TEST_IID_UNSUBSCRIBE_URL); @@ -280,7 +312,7 @@ public void testUnsubscribeZeroContentError() { fail("No error thrown for HTTP error"); } catch (FirebaseMessagingException error) { checkExceptionFromHttpResponse(error, 500, - "Unexpected HTTP response with status: 500; body: null"); + "Unexpected HTTP response with status: 500\nnull"); } checkTopicManagementRequestHeader(interceptor.getLastRequest(), TEST_IID_UNSUBSCRIBE_URL); @@ -294,8 +326,26 @@ public void testUnsubscribeTransportError() { client.unsubscribeFromTopic("test-topic", ImmutableList.of("id1", "id2")); fail("No error thrown for HTTP error"); } catch (FirebaseMessagingException error) { - assertEquals("internal-error", error.getErrorCode()); - assertEquals("Error while calling IID backend service", error.getMessage()); + assertEquals(ErrorCode.UNKNOWN, error.getErrorCode()); + assertEquals( + "Unknown error while making a remote service call: transport error", error.getMessage()); + assertTrue(error.getCause() instanceof IOException); + } + } + + @Test + public void testUnsubscribeParseError() { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() + .setContent("not json"); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + InstanceIdClient client = initInstanceIdClient(response, interceptor); + + try { + client.unsubscribeFromTopic("test-topic", ImmutableList.of("id1", "id2")); + fail("No error thrown for HTTP error"); + } catch (FirebaseMessagingException error) { + assertEquals(ErrorCode.UNKNOWN, error.getErrorCode()); + assertTrue(error.getMessage().startsWith("Error while parsing HTTP response: ")); assertTrue(error.getCause() instanceof IOException); } } @@ -311,9 +361,15 @@ public void testJsonFactoryIsNull() { } @Test - public void testFromApp() throws IOException { - FirebaseOptions options = new FirebaseOptions.Builder() + public void testFromApp() { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse() + .setStatusCode(400).setZeroContent(); + MockHttpTransport transport = new MockHttpTransport.Builder() + .setLowLevelHttpResponse(response) + .build(); + FirebaseOptions options = FirebaseOptions.builder() .setCredentials(new MockGoogleCredentials("test-token")) + .setHttpTransport(transport) .setProjectId("test-project") .build(); FirebaseApp app = FirebaseApp.initializeApp(options); @@ -321,10 +377,14 @@ public void testFromApp() throws IOException { try { InstanceIdClientImpl client = InstanceIdClientImpl.fromApp(app); - assertSame(options.getJsonFactory(), client.getJsonFactory()); - HttpRequest request = client.getRequestFactory().buildGetRequest( - new GenericUrl("https://example.com")); - assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); + client.subscribeToTopic("test-topic", ImmutableList.of("id1", "id2")); + fail("No error thrown for error response"); + } catch (FirebaseMessagingException e) { + assertNotNull(e.getHttpResponse()); + + List auth = ImmutableList.of("Bearer test-token"); + OutgoingHttpRequest request = e.getHttpResponse().getRequest(); + assertEquals(auth, request.getHeaders().get("authorization")); } finally { app.delete(); } @@ -388,11 +448,20 @@ private void checkTopicManagementRequestHeader( assertEquals(expectedUrl, request.getUrl().toString()); } - private void checkExceptionFromHttpResponse(FirebaseMessagingException error, - int expectedCode, String expectedMessage) { - assertEquals(getTopicManagementErrorCode(expectedCode), error.getErrorCode()); + private void checkExceptionFromHttpResponse( + FirebaseMessagingException error, int statusCode, String expectedMessage) { + assertEquals(HTTP_2_ERROR.get(statusCode), error.getErrorCode()); assertEquals(expectedMessage, error.getMessage()); assertTrue(error.getCause() instanceof HttpResponseException); + assertNull(error.getMessagingErrorCode()); + + IncomingHttpResponse httpResponse = error.getHttpResponse(); + assertNotNull(httpResponse); + assertEquals(statusCode, httpResponse.getStatusCode()); + + OutgoingHttpRequest request = httpResponse.getRequest(); + assertEquals(HttpMethods.POST, request.getMethod()); + assertTrue(request.getUrl().startsWith("https://iid.googleapis.com")); } private InstanceIdClient initClientWithFaultyTransport() { @@ -400,12 +469,4 @@ private InstanceIdClient initClientWithFaultyTransport() { TestUtils.createFaultyHttpTransport().createRequestFactory(), Utils.getDefaultJsonFactory()); } - - private String getTopicManagementErrorCode(int statusCode) { - String code = InstanceIdClientImpl.IID_ERROR_CODES.get(statusCode); - if (code == null) { - code = "unknown-error"; - } - return code; - } } diff --git a/src/test/java/com/google/firebase/messaging/MessageTest.java b/src/test/java/com/google/firebase/messaging/MessageTest.java index c97fb6b67..90b867d5f 100644 --- a/src/test/java/com/google/firebase/messaging/MessageTest.java +++ b/src/test/java/com/google/firebase/messaging/MessageTest.java @@ -29,11 +29,8 @@ import java.io.IOException; import java.math.BigDecimal; import java.math.BigInteger; -import java.text.SimpleDateFormat; -import java.util.Date; import java.util.HashMap; import java.util.List; -import java.util.Locale; import java.util.Map; import java.util.concurrent.TimeUnit; import org.junit.Test; @@ -94,7 +91,10 @@ public void testPrefixedTopicName() throws IOException { @Test public void testNotificationMessageDeprecatedConstructor() throws IOException { Message message = Message.builder() - .setNotification(new Notification("title", "body")) + .setNotification(Notification.builder() + .setTitle("title") + .setBody("body") + .build()) .setTopic("test-topic") .build(); Map data = ImmutableMap.of("title", "title", "body", "body"); @@ -752,7 +752,11 @@ public void testIncorrectAnalyticsLabelFormat() { @Test public void testImageInNotificationDeprecatedConstructor() throws IOException { Message message = Message.builder() - .setNotification(new Notification("title", "body", TEST_IMAGE_URL)) + .setNotification(Notification.builder() + .setTitle("title") + .setBody("body") + .setImage(TEST_IMAGE_URL) + .build()) .setTopic("test-topic") .build(); Map data = ImmutableMap.of( @@ -778,7 +782,11 @@ public void testImageInNotification() throws IOException { @Test public void testImageInAndroidNotification() throws IOException { Message message = Message.builder() - .setNotification(new Notification("title", "body", TEST_IMAGE_URL)) + .setNotification(Notification.builder() + .setTitle("title") + .setBody("body") + .setImage(TEST_IMAGE_URL) + .build()) .setAndroidConfig(AndroidConfig.builder() .setNotification(AndroidNotification.builder() .setTitle("android-title") @@ -808,7 +816,11 @@ public void testImageInAndroidNotification() throws IOException { public void testImageInApnsNotification() throws IOException { Message message = Message.builder() .setTopic("test-topic") - .setNotification(new Notification("title", "body", TEST_IMAGE_URL)) + .setNotification(Notification.builder() + .setTitle("title") + .setBody("body") + .setImage(TEST_IMAGE_URL) + .build()) .setApnsConfig( ApnsConfig.builder().setAps(Aps.builder().build()) .setFcmOptions(ApnsFcmOptions.builder().setImage(TEST_IMAGE_URL_APNS).build()) @@ -835,7 +847,7 @@ public void testImageInApnsNotification() throws IOException { } @Test - public void testInvalidColorInAndroidNotificationLightSettings() throws IOException { + public void testInvalidColorInAndroidNotificationLightSettings() { try { LightSettings.Builder lightSettingsBuilder = LightSettings.builder() .setColorFromString("#01020K") @@ -853,7 +865,10 @@ public void testInvalidColorInAndroidNotificationLightSettings() throws IOExcept public void testExtendedAndroidNotificationParameters() throws IOException { long[] vibrateTimings = {1000L, 1001L}; Message message = Message.builder() - .setNotification(new Notification("title", "body")) + .setNotification(Notification.builder() + .setTitle("title") + .setBody("body") + .build()) .setAndroidConfig(AndroidConfig.builder() .setNotification(AndroidNotification.builder() .setTitle("android-title") diff --git a/src/test/java/com/google/firebase/messaging/MulticastMessageTest.java b/src/test/java/com/google/firebase/messaging/MulticastMessageTest.java index 1ea57d8d5..f26d0816f 100644 --- a/src/test/java/com/google/firebase/messaging/MulticastMessageTest.java +++ b/src/test/java/com/google/firebase/messaging/MulticastMessageTest.java @@ -38,7 +38,10 @@ public class MulticastMessageTest { private static final WebpushConfig WEBPUSH = WebpushConfig.builder() .putData("key", "value") .build(); - private static final Notification NOTIFICATION = new Notification("title", "body"); + private static final Notification NOTIFICATION = Notification.builder() + .setTitle("title") + .setBody("body") + .build(); private static final FcmOptions FCM_OPTIONS = FcmOptions.withAnalyticsLabel("analytics_label"); @Test diff --git a/src/test/java/com/google/firebase/messaging/SendResponseTest.java b/src/test/java/com/google/firebase/messaging/SendResponseTest.java index e9667cc77..d90f1e6b6 100644 --- a/src/test/java/com/google/firebase/messaging/SendResponseTest.java +++ b/src/test/java/com/google/firebase/messaging/SendResponseTest.java @@ -22,6 +22,7 @@ import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; +import com.google.firebase.ErrorCode; import org.junit.Test; public class SendResponseTest { @@ -37,8 +38,8 @@ public void testSuccessfulResponse() { @Test public void testFailureResponse() { - FirebaseMessagingException exception = new FirebaseMessagingException("error-code", - "error-message", null); + FirebaseMessagingException exception = new FirebaseMessagingException( + ErrorCode.INTERNAL, "error-message"); SendResponse response = SendResponse.fromException(exception); assertNull(response.getMessageId()); diff --git a/src/test/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImplTest.java b/src/test/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImplTest.java index 87afe609c..8d227ef52 100644 --- a/src/test/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImplTest.java +++ b/src/test/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImplTest.java @@ -18,12 +18,11 @@ import static com.google.firebase.projectmanagement.FirebaseProjectManagementServiceImpl.FIREBASE_PROJECT_MANAGEMENT_URL; import static com.google.firebase.projectmanagement.FirebaseProjectManagementServiceImpl.MAXIMUM_LIST_APPS_PAGE_SIZE; -import static com.google.firebase.projectmanagement.HttpHelper.PATCH_OVERRIDE_KEY; -import static com.google.firebase.projectmanagement.HttpHelper.PATCH_OVERRIDE_VALUE; import static com.google.firebase.projectmanagement.ShaCertificateType.SHA_1; import static com.google.firebase.projectmanagement.ShaCertificateType.SHA_256; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -40,12 +39,15 @@ import com.google.common.base.Charsets; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.firebase.ErrorCode; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; import com.google.firebase.TestOnlyImplFirebaseTrampolines; import com.google.firebase.auth.MockGoogleCredentials; +import com.google.firebase.internal.SdkUtils; import com.google.firebase.internal.TestApiClientUtils; import com.google.firebase.testing.MultiRequestMockHttpTransport; +import com.google.firebase.testing.TestUtils; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.ArrayList; @@ -69,6 +71,7 @@ public class FirebaseProjectManagementServiceImplTest { private static final String IOS_APP_ID = "test-ios-app-id"; private static final String IOS_APP_RESOURCE_NAME = "ios/11111"; private static final String ANDROID_APP_RESOURCE_NAME = "android/11111"; + private static final String CLIENT_VERSION = "Java/Admin/" + SdkUtils.getVersion(); private static final IosAppMetadata IOS_APP_METADATA = new IosAppMetadata(IOS_APP_RESOURCE_NAME, IOS_APP_ID, DISPLAY_NAME, PROJECT_ID, BUNDLE_ID); private static final AndroidAppMetadata ANDROID_APP_METADATA = @@ -272,7 +275,65 @@ public void getIosAppHttpError() { serviceImpl.getIosApp(IOS_APP_ID); fail("No exception thrown for HTTP error"); } catch (FirebaseProjectManagementException e) { - assertEquals("App ID \"test-ios-app-id\": Internal server error.", e.getMessage()); + assertEquals(ErrorCode.INTERNAL, e.getErrorCode()); + assertEquals( + "App ID \"test-ios-app-id\": Unexpected HTTP response with status: 500\n{}", + e.getMessage()); + assertNotNull(e.getCause()); + assertNotNull(e.getHttpResponse()); + } + } + + @Test + public void getIosAppHttpErrorWithCode() { + firstRpcResponse.setStatusCode(500); + firstRpcResponse.setContent( + "{\"error\": {\"status\":\"NOT_FOUND\", \"message\":\"Test error\"}}"); + serviceImpl = initServiceImpl(firstRpcResponse, interceptor); + + try { + serviceImpl.getIosApp(IOS_APP_ID); + fail("No exception thrown for HTTP error"); + } catch (FirebaseProjectManagementException e) { + assertEquals(ErrorCode.NOT_FOUND, e.getErrorCode()); + assertEquals("App ID \"test-ios-app-id\": Test error", e.getMessage()); + assertNotNull(e.getCause()); + assertNotNull(e.getHttpResponse()); + } + } + + @Test + public void getIosAppParseError() { + firstRpcResponse.setContent("not json"); + serviceImpl = initServiceImpl(firstRpcResponse, interceptor); + + try { + serviceImpl.getIosApp(IOS_APP_ID); + fail("No exception thrown for HTTP error"); + } catch (FirebaseProjectManagementException e) { + assertEquals(ErrorCode.UNKNOWN, e.getErrorCode()); + assertTrue(e.getMessage().startsWith( + "App ID \"test-ios-app-id\": Error while parsing HTTP response")); + assertNotNull(e.getCause()); + assertNotNull(e.getHttpResponse()); + } + } + + @Test + public void getIosAppTransportError() { + FirebaseProjectManagementServiceImpl serviceImpl = initServiceImplWithFaultyTransport(); + + try { + serviceImpl.getIosApp(IOS_APP_ID); + fail("No exception thrown for HTTP error"); + } catch (FirebaseProjectManagementException e) { + assertEquals(ErrorCode.UNKNOWN, e.getErrorCode()); + assertEquals( + "App ID \"test-ios-app-id\": Unknown error while making a remote service call: " + + "transport error", + e.getMessage()); + assertNotNull(e.getCause()); + assertNull(e.getHttpResponse()); } } @@ -467,11 +528,10 @@ public void setIosDisplayName() throws Exception { "%s/v1beta1/projects/-/iosApps/%s?update_mask=display_name", FIREBASE_PROJECT_MANAGEMENT_URL, IOS_APP_ID); - checkRequestHeader(expectedUrl, HttpMethod.POST); + checkRequestHeader(expectedUrl, HttpMethod.PATCH); ImmutableMap payload = ImmutableMap.builder().put("display_name", DISPLAY_NAME).build(); checkRequestPayload(payload); - checkPatchRequest(); } @Test @@ -485,11 +545,10 @@ public void setIosDisplayNameAsync() throws Exception { "%s/v1beta1/projects/-/iosApps/%s?update_mask=display_name", FIREBASE_PROJECT_MANAGEMENT_URL, IOS_APP_ID); - checkRequestHeader(expectedUrl, HttpMethod.POST); + checkRequestHeader(expectedUrl, HttpMethod.PATCH); ImmutableMap payload = ImmutableMap.builder().put("display_name", DISPLAY_NAME).build(); checkRequestPayload(payload); - checkPatchRequest(); } @Test @@ -554,7 +613,65 @@ public void getAndroidAppHttpError() { serviceImpl.getAndroidApp(ANDROID_APP_ID); fail("No exception thrown for HTTP error"); } catch (FirebaseProjectManagementException e) { - assertEquals("App ID \"test-android-app-id\": Internal server error.", e.getMessage()); + assertEquals(ErrorCode.INTERNAL, e.getErrorCode()); + assertEquals( + "App ID \"test-android-app-id\": Unexpected HTTP response with status: 500\n{}", + e.getMessage()); + assertNotNull(e.getCause()); + assertNotNull(e.getHttpResponse()); + } + } + + @Test + public void getAndroidAppHttpErrorWithCode() { + firstRpcResponse.setStatusCode(500); + firstRpcResponse.setContent( + "{\"error\": {\"status\":\"NOT_FOUND\", \"message\":\"Test error\"}}"); + serviceImpl = initServiceImpl(firstRpcResponse, interceptor); + + try { + serviceImpl.getAndroidApp(ANDROID_APP_ID); + fail("No exception thrown for HTTP error"); + } catch (FirebaseProjectManagementException e) { + assertEquals(ErrorCode.NOT_FOUND, e.getErrorCode()); + assertEquals("App ID \"test-android-app-id\": Test error", e.getMessage()); + assertNotNull(e.getCause()); + assertNotNull(e.getHttpResponse()); + } + } + + @Test + public void getAndroidAppParseError() { + firstRpcResponse.setContent("not json"); + serviceImpl = initServiceImpl(firstRpcResponse, interceptor); + + try { + serviceImpl.getAndroidApp(ANDROID_APP_ID); + fail("No exception thrown for HTTP error"); + } catch (FirebaseProjectManagementException e) { + assertEquals(ErrorCode.UNKNOWN, e.getErrorCode()); + assertTrue(e.getMessage().startsWith( + "App ID \"test-android-app-id\": Error while parsing HTTP response")); + assertNotNull(e.getCause()); + assertNotNull(e.getHttpResponse()); + } + } + + @Test + public void getAndroidAppTransportError() { + FirebaseProjectManagementServiceImpl serviceImpl = initServiceImplWithFaultyTransport(); + + try { + serviceImpl.getAndroidApp(ANDROID_APP_ID); + fail("No exception thrown for HTTP error"); + } catch (FirebaseProjectManagementException e) { + assertEquals(ErrorCode.UNKNOWN, e.getErrorCode()); + assertEquals( + "App ID \"test-android-app-id\": Unknown error while making a remote service call: " + + "transport error", + e.getMessage()); + assertNotNull(e.getCause()); + assertNull(e.getHttpResponse()); } } @@ -751,11 +868,10 @@ public void setAndroidDisplayName() throws Exception { "%s/v1beta1/projects/-/androidApps/%s?update_mask=display_name", FIREBASE_PROJECT_MANAGEMENT_URL, ANDROID_APP_ID); - checkRequestHeader(expectedUrl, HttpMethod.POST); + checkRequestHeader(expectedUrl, HttpMethod.PATCH); ImmutableMap payload = ImmutableMap.builder().put("display_name", DISPLAY_NAME).build(); checkRequestPayload(payload); - checkPatchRequest(); } @Test @@ -769,11 +885,10 @@ public void setAndroidDisplayNameAsync() throws Exception { "%s/v1beta1/projects/-/androidApps/%s?update_mask=display_name", FIREBASE_PROJECT_MANAGEMENT_URL, ANDROID_APP_ID); - checkRequestHeader(expectedUrl, HttpMethod.POST); + checkRequestHeader(expectedUrl, HttpMethod.PATCH); ImmutableMap payload = ImmutableMap.builder().put("display_name", DISPLAY_NAME).build(); checkRequestPayload(payload); - checkPatchRequest(); } @Test @@ -918,17 +1033,25 @@ public void deleteShaCertificateAsync() throws Exception { } @Test - public void testAuthAndRetriesSupport() { - FirebaseOptions options = new FirebaseOptions.Builder() + public void testAuthAndRetriesSupport() throws Exception { + List mockResponses = ImmutableList.of( + new MockLowLevelHttpResponse().setContent("{}")); + MockHttpTransport transport = new MultiRequestMockHttpTransport(mockResponses); + FirebaseOptions options = FirebaseOptions.builder() .setCredentials(new MockGoogleCredentials("test-token")) .setProjectId(PROJECT_ID) + .setHttpTransport(transport) .build(); FirebaseApp app = FirebaseApp.initializeApp(options); FirebaseProjectManagementServiceImpl serviceImpl = new FirebaseProjectManagementServiceImpl(app); + serviceImpl.setInterceptor(interceptor); + + serviceImpl.deleteShaCertificate(SHA1_RESOURCE_NAME); - TestApiClientUtils.assertAuthAndRetrySupport(serviceImpl.getRequestFactory()); + assertEquals(1, interceptor.getNumberOfResponses()); + TestApiClientUtils.assertAuthAndRetrySupport(interceptor.getResponse(0).getRequest()); } @Test @@ -937,7 +1060,7 @@ public void testHttpRetries() throws Exception { firstRpcResponse.setStatusCode(503).setContent("{}"), new MockLowLevelHttpResponse().setContent("{}")); MockHttpTransport transport = new MultiRequestMockHttpTransport(mockResponses); - FirebaseOptions options = new FirebaseOptions.Builder() + FirebaseOptions options = FirebaseOptions.builder() .setCredentials(new MockGoogleCredentials("test-token")) .setProjectId(PROJECT_ID) .setHttpTransport(transport) @@ -965,7 +1088,7 @@ private static FirebaseProjectManagementServiceImpl initServiceImpl( List mockResponses, MultiRequestTestResponseInterceptor interceptor) { MockHttpTransport transport = new MultiRequestMockHttpTransport(mockResponses); - FirebaseOptions options = new FirebaseOptions.Builder() + FirebaseOptions options = FirebaseOptions.builder() .setCredentials(new MockGoogleCredentials("test-token")) .setProjectId(PROJECT_ID) .setHttpTransport(transport) @@ -978,6 +1101,16 @@ private static FirebaseProjectManagementServiceImpl initServiceImpl( return serviceImpl; } + private static FirebaseProjectManagementServiceImpl initServiceImplWithFaultyTransport() { + FirebaseOptions options = FirebaseOptions.builder() + .setCredentials(new MockGoogleCredentials("test-token")) + .setProjectId(PROJECT_ID) + .setHttpTransport(TestUtils.createFaultyHttpTransport()) + .build(); + FirebaseApp app = FirebaseApp.initializeApp(options); + return new FirebaseProjectManagementServiceImpl(app); + } + private void checkRequestHeader(String expectedUrl, HttpMethod httpMethod) { assertEquals( "The number of HttpResponses is not equal to 1.", 1, interceptor.getNumberOfResponses()); @@ -990,6 +1123,7 @@ private void checkRequestHeader(int index, String expectedUrl, HttpMethod httpMe assertEquals(httpMethod.name(), request.getRequestMethod()); assertEquals(expectedUrl, request.getUrl().toString()); assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); + assertEquals(CLIENT_VERSION, request.getHeaders().get("X-Client-Version")); } private void checkRequestPayload(Map expected) throws IOException { @@ -1007,21 +1141,11 @@ private void checkRequestPayload(int index, Map expected) throws assertEquals(expected, parsed); } - private void checkPatchRequest() { - assertEquals( - "The number of HttpResponses is not equal to 1.", 1, interceptor.getNumberOfResponses()); - checkPatchRequest(0); - } - - private void checkPatchRequest(int index) { - assertTrue(interceptor.getResponse(index).getRequest().getHeaders() - .getHeaderStringValues(PATCH_OVERRIDE_KEY).contains(PATCH_OVERRIDE_VALUE)); - } - private enum HttpMethod { GET, POST, - DELETE + DELETE, + PATCH, } /** diff --git a/src/test/java/com/google/firebase/projectmanagement/FirebaseProjectManagementTest.java b/src/test/java/com/google/firebase/projectmanagement/FirebaseProjectManagementTest.java index 7569b0bc2..30fcc3344 100644 --- a/src/test/java/com/google/firebase/projectmanagement/FirebaseProjectManagementTest.java +++ b/src/test/java/com/google/firebase/projectmanagement/FirebaseProjectManagementTest.java @@ -26,6 +26,7 @@ import com.google.api.core.ApiFutures; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; +import com.google.firebase.ErrorCode; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; import com.google.firebase.TestOnlyImplFirebaseTrampolines; @@ -52,7 +53,7 @@ public class FirebaseProjectManagementTest { private static final String TEST_PROJECT_ID = "hello-world"; private static final String TEST_APP_BUNDLE_ID = "com.hello.world"; private static final FirebaseProjectManagementException FIREBASE_PROJECT_MANAGEMENT_EXCEPTION = - new FirebaseProjectManagementException("Error!", null); + new FirebaseProjectManagementException(ErrorCode.UNKNOWN, "Error!", null); @Rule public final MockitoRule mockitoRule = MockitoJUnit.rule(); @@ -67,7 +68,7 @@ public class FirebaseProjectManagementTest { @BeforeClass public static void setUpClass() { - FirebaseOptions options = new FirebaseOptions.Builder() + FirebaseOptions options = FirebaseOptions.builder() .setCredentials(new MockGoogleCredentials("test-token")) .setProjectId(TEST_PROJECT_ID) .build(); diff --git a/src/test/java/com/google/firebase/projectmanagement/IosAppTest.java b/src/test/java/com/google/firebase/projectmanagement/IosAppTest.java index 269d3dae0..02d50279b 100644 --- a/src/test/java/com/google/firebase/projectmanagement/IosAppTest.java +++ b/src/test/java/com/google/firebase/projectmanagement/IosAppTest.java @@ -26,6 +26,7 @@ import com.google.api.core.ApiFuture; import com.google.api.core.ApiFutures; +import com.google.firebase.ErrorCode; import com.google.firebase.TestOnlyImplFirebaseTrampolines; import java.util.concurrent.ExecutionException; import org.junit.After; @@ -53,7 +54,7 @@ public class IosAppTest { + "SOME_OTHER_KEYsome-other-value" + ""; private static final FirebaseProjectManagementException FIREBASE_PROJECT_MANAGEMENT_EXCEPTION = - new FirebaseProjectManagementException("Error!", null); + new FirebaseProjectManagementException(ErrorCode.UNKNOWN, "Error!", null); private static final IosAppMetadata TEST_IOS_APP_METADATA = new IosAppMetadata( TEST_APP_NAME, diff --git a/src/test/java/com/google/firebase/snippets/FirebaseAppSnippets.java b/src/test/java/com/google/firebase/snippets/FirebaseAppSnippets.java index ed1ad464e..bf3d4ca6b 100644 --- a/src/test/java/com/google/firebase/snippets/FirebaseAppSnippets.java +++ b/src/test/java/com/google/firebase/snippets/FirebaseAppSnippets.java @@ -33,7 +33,7 @@ public void initializeWithServiceAccount() throws IOException { // [START initialize_sdk_with_service_account] FileInputStream serviceAccount = new FileInputStream("path/to/serviceAccountKey.json"); - FirebaseOptions options = new FirebaseOptions.Builder() + FirebaseOptions options = FirebaseOptions.builder() .setCredentials(GoogleCredentials.fromStream(serviceAccount)) .setDatabaseUrl("https://.firebaseio.com/") .build(); @@ -44,7 +44,7 @@ public void initializeWithServiceAccount() throws IOException { public void initializeWithDefaultCredentials() throws IOException { // [START initialize_sdk_with_application_default] - FirebaseOptions options = new FirebaseOptions.Builder() + FirebaseOptions options = FirebaseOptions.builder() .setCredentials(GoogleCredentials.getApplicationDefault()) .setDatabaseUrl("https://.firebaseio.com/") .build(); @@ -57,7 +57,7 @@ public void initializeWithRefreshToken() throws IOException { // [START initialize_sdk_with_refresh_token] FileInputStream refreshToken = new FileInputStream("path/to/refreshToken.json"); - FirebaseOptions options = new FirebaseOptions.Builder() + FirebaseOptions options = FirebaseOptions.builder() .setCredentials(GoogleCredentials.fromStream(refreshToken)) .setDatabaseUrl("https://.firebaseio.com/") .build(); @@ -73,7 +73,7 @@ public void initializeWithDefaultConfig() { } public void initializeDefaultApp() throws IOException { - FirebaseOptions defaultOptions = new FirebaseOptions.Builder() + FirebaseOptions defaultOptions = FirebaseOptions.builder() .setCredentials(GoogleCredentials.getApplicationDefault()) .build(); @@ -94,10 +94,10 @@ public void initializeDefaultApp() throws IOException { } public void initializeCustomApp() throws Exception { - FirebaseOptions defaultOptions = new FirebaseOptions.Builder() + FirebaseOptions defaultOptions = FirebaseOptions.builder() .setCredentials(GoogleCredentials.getApplicationDefault()) .build(); - FirebaseOptions otherAppConfig = new FirebaseOptions.Builder() + FirebaseOptions otherAppConfig = FirebaseOptions.builder() .setCredentials(GoogleCredentials.getApplicationDefault()) .build(); @@ -123,7 +123,7 @@ public void initializeCustomApp() throws Exception { public void initializeWithServiceAccountId() throws IOException { // [START initialize_sdk_with_service_account_id] - FirebaseOptions options = new FirebaseOptions.Builder() + FirebaseOptions options = FirebaseOptions.builder() .setCredentials(GoogleCredentials.getApplicationDefault()) .setServiceAccountId("my-client-id@my-project-id.iam.gserviceaccount.com") .build(); diff --git a/src/test/java/com/google/firebase/snippets/FirebaseAuthSnippets.java b/src/test/java/com/google/firebase/snippets/FirebaseAuthSnippets.java index b6103b4ef..e86496a21 100644 --- a/src/test/java/com/google/firebase/snippets/FirebaseAuthSnippets.java +++ b/src/test/java/com/google/firebase/snippets/FirebaseAuthSnippets.java @@ -18,6 +18,7 @@ import com.google.common.io.BaseEncoding; import com.google.firebase.auth.ActionCodeSettings; +import com.google.firebase.auth.AuthErrorCode; import com.google.firebase.auth.ErrorInfo; import com.google.firebase.auth.ExportedUserRecord; import com.google.firebase.auth.FirebaseAuth; @@ -262,7 +263,7 @@ public static void verifyIdTokenCheckRevoked(String idToken) { // Token is valid and not revoked. String uid = decodedToken.getUid(); } catch (FirebaseAuthException e) { - if (e.getErrorCode().equals("id-token-revoked")) { + if (e.getAuthErrorCode() == AuthErrorCode.REVOKED_ID_TOKEN) { // Token has been revoked. Inform the user to re-authenticate or signOut() the user. } else { // Token is invalid. diff --git a/src/test/java/com/google/firebase/snippets/FirebaseDatabaseSnippets.java b/src/test/java/com/google/firebase/snippets/FirebaseDatabaseSnippets.java index baf4d3b69..079946649 100644 --- a/src/test/java/com/google/firebase/snippets/FirebaseDatabaseSnippets.java +++ b/src/test/java/com/google/firebase/snippets/FirebaseDatabaseSnippets.java @@ -677,7 +677,7 @@ public void initializeApp() throws IOException { FileInputStream serviceAccount = new FileInputStream("path/to/serviceAccount.json"); // Initialize the app with a service account, granting admin privileges - FirebaseOptions options = new FirebaseOptions.Builder() + FirebaseOptions options = FirebaseOptions.builder() .setCredentials(GoogleCredentials.fromStream(serviceAccount)) .setDatabaseUrl("https://.firebaseio.com") .build(); diff --git a/src/test/java/com/google/firebase/snippets/FirebaseMessagingSnippets.java b/src/test/java/com/google/firebase/snippets/FirebaseMessagingSnippets.java index d6e4417e1..f1432e160 100644 --- a/src/test/java/com/google/firebase/snippets/FirebaseMessagingSnippets.java +++ b/src/test/java/com/google/firebase/snippets/FirebaseMessagingSnippets.java @@ -88,9 +88,10 @@ public void sendToCondition() throws FirebaseMessagingException { // See documentation on defining a message payload. Message message = Message.builder() - .setNotification(new Notification( - "$GOOG up 1.43% on the day", - "$GOOG gained 11.80 points to close at 835.67, up 1.43% on the day.")) + .setNotification(Notification.builder() + .setTitle("$GOOG up 1.43% on the day") + .setBody("$GOOG gained 11.80 points to close at 835.67, up 1.43% on the day.") + .build()) .setCondition(condition) .build(); @@ -125,12 +126,18 @@ public void sendAll() throws FirebaseMessagingException { // Create a list containing up to 500 messages. List messages = Arrays.asList( Message.builder() - .setNotification(new Notification("Price drop", "5% off all electronics")) + .setNotification(Notification.builder() + .setTitle("Price drop") + .setBody("5% off all electronics") + .build()) .setToken(registrationToken) .build(), // ... Message.builder() - .setNotification(new Notification("Price drop", "2% off all books")) + .setNotification(Notification.builder() + .setTitle("Price drop") + .setBody("2% off all books") + .build()) .setTopic("readers-club") .build() ); @@ -251,9 +258,10 @@ public Message webpushMessage() { public Message allPlatformsMessage() { // [START multi_platforms_message] Message message = Message.builder() - .setNotification(new Notification( - "$GOOG up 1.43% on the day", - "$GOOG gained 11.80 points to close at 835.67, up 1.43% on the day.")) + .setNotification(Notification.builder() + .setTitle("$GOOG up 1.43% on the day") + .setBody("$GOOG gained 11.80 points to close at 835.67, up 1.43% on the day.") + .build()) .setAndroidConfig(AndroidConfig.builder() .setTtl(3600 * 1000) .setNotification(AndroidNotification.builder() diff --git a/src/test/java/com/google/firebase/snippets/FirebaseStorageSnippets.java b/src/test/java/com/google/firebase/snippets/FirebaseStorageSnippets.java index 380dcff1b..706cc1b16 100644 --- a/src/test/java/com/google/firebase/snippets/FirebaseStorageSnippets.java +++ b/src/test/java/com/google/firebase/snippets/FirebaseStorageSnippets.java @@ -33,7 +33,7 @@ public void initializeAppForStorage() throws IOException { // [START init_admin_sdk_for_storage] FileInputStream serviceAccount = new FileInputStream("path/to/serviceAccountKey.json"); - FirebaseOptions options = new FirebaseOptions.Builder() + FirebaseOptions options = FirebaseOptions.builder() .setCredentials(GoogleCredentials.fromStream(serviceAccount)) .setStorageBucket(".appspot.com") .build(); diff --git a/src/test/java/com/google/firebase/testing/FirebaseAppRule.java b/src/test/java/com/google/firebase/testing/FirebaseAppRule.java index 28123293f..1049a1f2f 100644 --- a/src/test/java/com/google/firebase/testing/FirebaseAppRule.java +++ b/src/test/java/com/google/firebase/testing/FirebaseAppRule.java @@ -17,7 +17,6 @@ package com.google.firebase.testing; import com.google.firebase.TestOnlyImplFirebaseTrampolines; -import com.google.firebase.internal.FirebaseAppStore; import org.junit.rules.TestRule; import org.junit.runner.Description; import org.junit.runners.model.Statement; @@ -42,6 +41,5 @@ public void evaluate() throws Throwable { private void resetState() { TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); - FirebaseAppStore.clearInstanceForTest(); } } diff --git a/src/test/java/com/google/firebase/testing/IntegrationTestUtils.java b/src/test/java/com/google/firebase/testing/IntegrationTestUtils.java index d3abaa096..c9890354e 100644 --- a/src/test/java/com/google/firebase/testing/IntegrationTestUtils.java +++ b/src/test/java/com/google/firebase/testing/IntegrationTestUtils.java @@ -110,7 +110,6 @@ public static synchronized FirebaseApp ensureDefaultApp() { .setStorageBucket(getStorageBucket()) .setCredentials(TestUtils.getCertCredential(getServiceAccountCertificate())) .setFirestoreOptions(FirestoreOptions.newBuilder() - .setTimestampsInSnapshotsEnabled(true) .setCredentials(TestUtils.getCertCredential(getServiceAccountCertificate())) .build()) .build(); @@ -121,7 +120,7 @@ public static synchronized FirebaseApp ensureDefaultApp() { public static FirebaseApp initApp(String name) { FirebaseOptions options = - new FirebaseOptions.Builder() + FirebaseOptions.builder() .setDatabaseUrl(getDatabaseUrl()) .setCredentials(TestUtils.getCertCredential(getServiceAccountCertificate())) .build(); diff --git a/src/test/java/com/google/firebase/testing/TestUtils.java b/src/test/java/com/google/firebase/testing/TestUtils.java index a63aa8d10..e44ca2be8 100644 --- a/src/test/java/com/google/firebase/testing/TestUtils.java +++ b/src/test/java/com/google/firebase/testing/TestUtils.java @@ -162,11 +162,19 @@ public static HttpRequest createRequest() throws IOException { } public static HttpRequest createRequest(MockLowLevelHttpRequest request) throws IOException { + return createRequest(request, TEST_URL); + } + + /** + * Creates a test HTTP POST request for the given target URL. + */ + public static HttpRequest createRequest( + MockLowLevelHttpRequest request, GenericUrl url) throws IOException { HttpTransport transport = new MockHttpTransport.Builder() .setLowLevelHttpRequest(request) .build(); HttpRequestFactory requestFactory = transport.createRequestFactory(); - return requestFactory.buildPostRequest(TEST_URL, new EmptyContent()); + return requestFactory.buildPostRequest(url, new EmptyContent()); } public static HttpTransport createFaultyHttpTransport() { From 47d43475d04ea6777c7c04dd15c1ddf379e21cd8 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Wed, 19 Aug 2020 10:47:28 -0700 Subject: [PATCH 029/354] [chore] Release 7.0.0 (#473) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index a72301d5b..0f40ea078 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ com.google.firebase firebase-admin - 6.16.0 + 7.0.0 jar firebase-admin From 062223d527c9f30aaca033a3b1dfe174f97f07b2 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Mon, 31 Aug 2020 10:51:26 -0700 Subject: [PATCH 030/354] fix(auth): Support verifying tenant ID tokens in FirebaseAuth (#475) * fix(auth): Support verifying tenant ID tokens in FirebaseAuth * fix: Fixing the error message for null tenant ID --- .../auth/FirebaseTokenVerifierImpl.java | 8 ++-- .../com/google/firebase/FirebaseAppTest.java | 26 ++++++------- .../auth/FirebaseTokenVerifierImplTest.java | 38 +++++++++++++++++-- .../TenantAwareFirebaseAuthIT.java | 5 +++ 4 files changed, 56 insertions(+), 21 deletions(-) diff --git a/src/main/java/com/google/firebase/auth/FirebaseTokenVerifierImpl.java b/src/main/java/com/google/firebase/auth/FirebaseTokenVerifierImpl.java index 273ec1532..bbe741f5b 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseTokenVerifierImpl.java +++ b/src/main/java/com/google/firebase/auth/FirebaseTokenVerifierImpl.java @@ -71,7 +71,7 @@ private FirebaseTokenVerifierImpl(Builder builder) { this.docUrl = builder.docUrl; this.invalidTokenErrorCode = checkNotNull(builder.invalidTokenErrorCode); this.expiredTokenErrorCode = checkNotNull(builder.expiredTokenErrorCode); - this.tenantId = Strings.nullToEmpty(builder.tenantId); + this.tenantId = builder.tenantId; } /** @@ -323,11 +323,11 @@ private boolean containsLegacyUidField(IdToken.Payload payload) { } private void checkTenantId(final FirebaseToken firebaseToken) throws FirebaseAuthException { - String tokenTenantId = Strings.nullToEmpty(firebaseToken.getTenantId()); - if (!this.tenantId.equals(tokenTenantId)) { + String tokenTenantId = firebaseToken.getTenantId(); + if (this.tenantId != null && !this.tenantId.equals(tokenTenantId)) { String message = String.format( "The tenant ID ('%s') of the token did not match the expected value ('%s')", - tokenTenantId, + Strings.nullToEmpty(tokenTenantId), tenantId); throw newException(message, AuthErrorCode.TENANT_ID_MISMATCH); } diff --git a/src/test/java/com/google/firebase/FirebaseAppTest.java b/src/test/java/com/google/firebase/FirebaseAppTest.java index d481e3c0e..26ca93432 100644 --- a/src/test/java/com/google/firebase/FirebaseAppTest.java +++ b/src/test/java/com/google/firebase/FirebaseAppTest.java @@ -66,7 +66,7 @@ import org.junit.Test; import org.mockito.Mockito; -/** +/** * Unit tests for {@link com.google.firebase.FirebaseApp}. */ public class FirebaseAppTest { @@ -472,8 +472,8 @@ public void testAppWithAuthVariableOverrides() { public void testEmptyFirebaseConfigFile() { setFirebaseConfigEnvironmentVariable("firebase_config_empty.json"); FirebaseApp.initializeApp(); - } - + } + @Test public void testEmptyFirebaseConfigString() { setFirebaseConfigEnvironmentVariable(""); @@ -481,7 +481,7 @@ public void testEmptyFirebaseConfigString() { assertNull(firebaseApp.getOptions().getProjectId()); assertNull(firebaseApp.getOptions().getStorageBucket()); assertNull(firebaseApp.getOptions().getDatabaseUrl()); - assertTrue(firebaseApp.getOptions().getDatabaseAuthVariableOverride().isEmpty()); + assertTrue(firebaseApp.getOptions().getDatabaseAuthVariableOverride().isEmpty()); } @Test @@ -542,20 +542,20 @@ public void testEnvironmentVariableIgnored() { @Test public void testValidFirebaseConfigString() { - setFirebaseConfigEnvironmentVariable("{" - + "\"databaseAuthVariableOverride\": {" - + "\"uid\":" - + "\"testuser\"" - + "}," - + "\"databaseUrl\": \"https://hipster-chat.firebaseio.mock\"," - + "\"projectId\": \"hipster-chat-mock\"," - + "\"storageBucket\": \"hipster-chat.appspot.mock\"" + setFirebaseConfigEnvironmentVariable("{" + + "\"databaseAuthVariableOverride\": {" + + "\"uid\":" + + "\"testuser\"" + + "}," + + "\"databaseUrl\": \"https://hipster-chat.firebaseio.mock\"," + + "\"projectId\": \"hipster-chat-mock\"," + + "\"storageBucket\": \"hipster-chat.appspot.mock\"" + "}"); FirebaseApp firebaseApp = FirebaseApp.initializeApp(); assertEquals("hipster-chat-mock", firebaseApp.getOptions().getProjectId()); assertEquals("hipster-chat.appspot.mock", firebaseApp.getOptions().getStorageBucket()); assertEquals("https://hipster-chat.firebaseio.mock", firebaseApp.getOptions().getDatabaseUrl()); - assertEquals("testuser", + assertEquals("testuser", firebaseApp.getOptions().getDatabaseAuthVariableOverride().get("uid")); } diff --git a/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java b/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java index 833e2ed95..189cb223e 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java +++ b/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java @@ -328,6 +328,17 @@ public void testMalformedToken() { @Test public void testVerifyTokenWithTenantId() throws FirebaseAuthException { + FirebaseTokenVerifierImpl verifier = fullyPopulatedBuilder().build(); + + FirebaseToken firebaseToken = verifier.verifyToken(createTokenWithTenantId("TENANT_1")); + + assertEquals(TEST_TOKEN_ISSUER, firebaseToken.getIssuer()); + assertEquals(TestTokenFactory.UID, firebaseToken.getUid()); + assertEquals("TENANT_1", firebaseToken.getTenantId()); + } + + @Test + public void testVerifyTokenWithMatchingTenantId() throws FirebaseAuthException { FirebaseTokenVerifierImpl verifier = fullyPopulatedBuilder() .setTenantId("TENANT_1") .build(); @@ -341,11 +352,13 @@ public void testVerifyTokenWithTenantId() throws FirebaseAuthException { @Test public void testVerifyTokenDifferentTenantIds() { - try { - fullyPopulatedBuilder() + FirebaseTokenVerifierImpl verifier = fullyPopulatedBuilder() .setTenantId("TENANT_1") - .build() - .verifyToken(createTokenWithTenantId("TENANT_2")); + .build(); + String token = createTokenWithTenantId("TENANT_2"); + + try { + verifier.verifyToken(token); } catch (FirebaseAuthException e) { assertEquals(AuthErrorCode.TENANT_ID_MISMATCH, e.getAuthErrorCode()); assertEquals( @@ -354,6 +367,23 @@ public void testVerifyTokenDifferentTenantIds() { } } + @Test + public void testVerifyTokenNoTenantId() { + FirebaseTokenVerifierImpl verifier = fullyPopulatedBuilder() + .setTenantId("TENANT_1") + .build(); + String token = tokenFactory.createToken(); + + try { + verifier.verifyToken(token); + } catch (FirebaseAuthException e) { + assertEquals(AuthErrorCode.TENANT_ID_MISMATCH, e.getAuthErrorCode()); + assertEquals( + "The tenant ID ('') of the token did not match the expected value ('TENANT_1')", + e.getMessage()); + } + } + @Test public void testVerifyTokenMissingTenantId() { try { diff --git a/src/test/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuthIT.java b/src/test/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuthIT.java index fd8e4af83..1ebac213f 100644 --- a/src/test/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuthIT.java +++ b/src/test/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuthIT.java @@ -261,6 +261,11 @@ public void testVerifyTokenWithWrongTenantAwareClient() throws Exception { assertEquals(AuthErrorCode.TENANT_ID_MISMATCH, ((FirebaseAuthException) e.getCause()).getAuthErrorCode()); } + + // Verifies with FirebaseAuth + FirebaseToken decoded = FirebaseAuth.getInstance().verifyIdToken(idToken); + assertEquals("user", decoded.getUid()); + assertEquals(tenantId, decoded.getTenantId()); } @Test From 0b356e1fdf6da40f01315f54dad0322a0d732803 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Thu, 10 Sep 2020 14:27:15 -0700 Subject: [PATCH 031/354] fix: Cleaning up FirebaseApp state management (#476) --- .../java/com/google/firebase/FirebaseApp.java | 8 ++- .../firebase/ImplFirebaseTrampolines.java | 1 - .../firebase/auth/AbstractFirebaseAuth.java | 48 ---------------- .../google/firebase/auth/FirebaseAuth.java | 8 --- .../multitenancy/TenantAwareFirebaseAuth.java | 5 -- .../google/firebase/cloud/StorageClient.java | 8 --- .../firebase/database/FirebaseDatabase.java | 6 +- .../firebase/iid/FirebaseInstanceId.java | 7 --- .../firebase/internal/FirebaseService.java | 6 +- .../firebase/messaging/FirebaseMessaging.java | 7 --- .../FirebaseProjectManagement.java | 5 -- .../FirebaseProjectManagementServiceImpl.java | 8 --- .../com/google/firebase/FirebaseAppTest.java | 34 +++++++++++ .../firebase/auth/FirebaseAuthTest.java | 57 +++++++++---------- .../firebase/iid/FirebaseInstanceIdTest.java | 35 ++++++++---- 15 files changed, 100 insertions(+), 143 deletions(-) diff --git a/src/main/java/com/google/firebase/FirebaseApp.java b/src/main/java/com/google/firebase/FirebaseApp.java index 38482541c..06188cd55 100644 --- a/src/main/java/com/google/firebase/FirebaseApp.java +++ b/src/main/java/com/google/firebase/FirebaseApp.java @@ -277,6 +277,8 @@ public FirebaseOptions getOptions() { */ @Nullable String getProjectId() { + checkNotDeleted(); + // Try to get project ID from user-specified options. String projectId = options.getProjectId(); @@ -314,8 +316,10 @@ public String toString() { } /** - * Deletes the {@link FirebaseApp} and all its data. All calls to this {@link FirebaseApp} - * instance will throw once it has been called. + * Deletes this {@link FirebaseApp} object, and releases any local state and managed resources + * associated with it. All calls to this {@link FirebaseApp} instance will throw once this method + * has been called. This also releases any managed resources allocated by other services + * attached to this object instance (e.g. {@code FirebaseAuth}). * *

A no-op if delete was called before. */ diff --git a/src/main/java/com/google/firebase/ImplFirebaseTrampolines.java b/src/main/java/com/google/firebase/ImplFirebaseTrampolines.java index 7a50e0b07..3a7f6499a 100644 --- a/src/main/java/com/google/firebase/ImplFirebaseTrampolines.java +++ b/src/main/java/com/google/firebase/ImplFirebaseTrampolines.java @@ -26,7 +26,6 @@ import com.google.firebase.internal.NonNull; import java.util.concurrent.Callable; -import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ThreadFactory; diff --git a/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java b/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java index 8004548a5..fc9c8df0b 100644 --- a/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java +++ b/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java @@ -18,7 +18,6 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.common.base.Preconditions.checkState; import com.google.api.client.json.JsonFactory; import com.google.api.client.util.Clock; @@ -164,7 +163,6 @@ public ApiFuture createCustomTokenAsync( private CallableOperation createCustomTokenOp( final String uid, final Map developerClaims) { - checkNotDestroyed(); checkArgument(!Strings.isNullOrEmpty(uid), "uid must not be null or empty"); final FirebaseTokenFactory tokenFactory = this.tokenFactory.get(); return new CallableOperation() { @@ -208,7 +206,6 @@ public ApiFuture createSessionCookieAsync( private CallableOperation createSessionCookieOp( final String idToken, final SessionCookieOptions options) { - checkNotDestroyed(); checkArgument(!Strings.isNullOrEmpty(idToken), "idToken must not be null or empty"); checkNotNull(options, "options must not be null"); final FirebaseUserManager userManager = getUserManager(); @@ -299,7 +296,6 @@ public ApiFuture verifyIdTokenAsync(@NonNull String idToken) { private CallableOperation verifyIdTokenOp( final String idToken, final boolean checkRevoked) { - checkNotDestroyed(); checkArgument(!Strings.isNullOrEmpty(idToken), "ID token must not be null or empty"); final FirebaseTokenVerifier verifier = getIdTokenVerifier(checkRevoked); return new CallableOperation() { @@ -380,7 +376,6 @@ public ApiFuture verifySessionCookieAsync(String cookie, boolean private CallableOperation verifySessionCookieOp( final String cookie, final boolean checkRevoked) { - checkNotDestroyed(); checkArgument(!Strings.isNullOrEmpty(cookie), "Session cookie must not be null or empty"); final FirebaseTokenVerifier sessionCookieVerifier = getSessionCookieVerifier(checkRevoked); return new CallableOperation() { @@ -434,7 +429,6 @@ public ApiFuture revokeRefreshTokensAsync(@NonNull String uid) { } private CallableOperation revokeRefreshTokensOp(final String uid) { - checkNotDestroyed(); checkArgument(!Strings.isNullOrEmpty(uid), "uid must not be null or empty"); final FirebaseUserManager userManager = getUserManager(); return new CallableOperation() { @@ -475,7 +469,6 @@ public ApiFuture getUserAsync(@NonNull String uid) { } private CallableOperation getUserOp(final String uid) { - checkNotDestroyed(); checkArgument(!Strings.isNullOrEmpty(uid), "uid must not be null or empty"); final FirebaseUserManager userManager = getUserManager(); return new CallableOperation() { @@ -513,7 +506,6 @@ public ApiFuture getUserByEmailAsync(@NonNull String email) { private CallableOperation getUserByEmailOp( final String email) { - checkNotDestroyed(); checkArgument(!Strings.isNullOrEmpty(email), "email must not be null or empty"); final FirebaseUserManager userManager = getUserManager(); return new CallableOperation() { @@ -551,7 +543,6 @@ public ApiFuture getUserByPhoneNumberAsync(@NonNull String phoneNumb private CallableOperation getUserByPhoneNumberOp( final String phoneNumber) { - checkNotDestroyed(); checkArgument(!Strings.isNullOrEmpty(phoneNumber), "phone number must not be null or empty"); final FirebaseUserManager userManager = getUserManager(); return new CallableOperation() { @@ -620,7 +611,6 @@ public ApiFuture listUsersAsync(@Nullable String pageToken, int m private CallableOperation listUsersOp( @Nullable final String pageToken, final int maxResults) { - checkNotDestroyed(); final FirebaseUserManager userManager = getUserManager(); final DefaultUserSource source = new DefaultUserSource(userManager, jsonFactory); final ListUsersPage.Factory factory = new ListUsersPage.Factory(source, maxResults, pageToken); @@ -661,7 +651,6 @@ public ApiFuture createUserAsync(@NonNull UserRecord.CreateRequest r private CallableOperation createUserOp( final UserRecord.CreateRequest request) { - checkNotDestroyed(); checkNotNull(request, "create request must not be null"); final FirebaseUserManager userManager = getUserManager(); return new CallableOperation() { @@ -701,7 +690,6 @@ public ApiFuture updateUserAsync(@NonNull UserRecord.UpdateRequest r private CallableOperation updateUserOp( final UserRecord.UpdateRequest request) { - checkNotDestroyed(); checkNotNull(request, "update request must not be null"); final FirebaseUserManager userManager = getUserManager(); return new CallableOperation() { @@ -754,7 +742,6 @@ public ApiFuture setCustomUserClaimsAsync( private CallableOperation setCustomUserClaimsOp( final String uid, final Map claims) { - checkNotDestroyed(); checkArgument(!Strings.isNullOrEmpty(uid), "uid must not be null or empty"); final FirebaseUserManager userManager = getUserManager(); return new CallableOperation() { @@ -793,7 +780,6 @@ public ApiFuture deleteUserAsync(String uid) { } private CallableOperation deleteUserOp(final String uid) { - checkNotDestroyed(); checkArgument(!Strings.isNullOrEmpty(uid), "uid must not be null or empty"); final FirebaseUserManager userManager = getUserManager(); return new CallableOperation() { @@ -876,7 +862,6 @@ public ApiFuture importUsersAsync( private CallableOperation importUsersOp( final List users, final UserImportOptions options) { - checkNotDestroyed(); final UserImportRequest request = new UserImportRequest(users, options, jsonFactory); final FirebaseUserManager userManager = getUserManager(); return new CallableOperation() { @@ -931,7 +916,6 @@ public ApiFuture getUsersAsync(@NonNull Collection getUsersOp( @NonNull final Collection identifiers) { - checkNotDestroyed(); checkNotNull(identifiers, "identifiers must not be null"); checkArgument(identifiers.size() <= FirebaseUserManager.MAX_GET_ACCOUNTS_BATCH_SIZE, "identifiers parameter must have <= " + FirebaseUserManager.MAX_GET_ACCOUNTS_BATCH_SIZE @@ -1004,7 +988,6 @@ public ApiFuture deleteUsersAsync(List uids) { private CallableOperation deleteUsersOp( final List uids) { - checkNotDestroyed(); checkNotNull(uids, "uids must not be null"); for (String uid : uids) { UserRecord.checkUid(uid); @@ -1178,7 +1161,6 @@ public ApiFuture generateSignInWithEmailLinkAsync( private CallableOperation generateEmailActionLinkOp( final EmailLinkType type, final String email, final ActionCodeSettings settings) { - checkNotDestroyed(); checkArgument(!Strings.isNullOrEmpty(email), "email must not be null or empty"); if (type == EmailLinkType.EMAIL_SIGNIN) { checkNotNull(settings, "ActionCodeSettings must not be null when generating sign-in links"); @@ -1227,7 +1209,6 @@ public ApiFuture createOidcProviderConfigAsync( private CallableOperation createOidcProviderConfigOp(final OidcProviderConfig.CreateRequest request) { - checkNotDestroyed(); checkNotNull(request, "Create request must not be null."); OidcProviderConfig.checkOidcProviderId(request.getProviderId()); final FirebaseUserManager userManager = getUserManager(); @@ -1271,7 +1252,6 @@ public ApiFuture updateOidcProviderConfigAsync( private CallableOperation updateOidcProviderConfigOp( final OidcProviderConfig.UpdateRequest request) { - checkNotDestroyed(); checkNotNull(request, "Update request must not be null."); checkArgument(!request.getProperties().isEmpty(), "Update request must have at least one property set."); @@ -1316,7 +1296,6 @@ public ApiFuture getOidcProviderConfigAsync(@NonNull String private CallableOperation getOidcProviderConfigOp(final String providerId) { - checkNotDestroyed(); OidcProviderConfig.checkOidcProviderId(providerId); final FirebaseUserManager userManager = getUserManager(); return new CallableOperation() { @@ -1400,7 +1379,6 @@ public ApiFuture> listOidcProviderCo private CallableOperation, FirebaseAuthException> listOidcProviderConfigsOp(@Nullable final String pageToken, final int maxResults) { - checkNotDestroyed(); final FirebaseUserManager userManager = getUserManager(); final DefaultOidcProviderConfigSource source = new DefaultOidcProviderConfigSource(userManager); final ListProviderConfigsPage.Factory factory = @@ -1443,7 +1421,6 @@ public ApiFuture deleteOidcProviderConfigAsync(String providerId) { private CallableOperation deleteOidcProviderConfigOp( final String providerId) { - checkNotDestroyed(); OidcProviderConfig.checkOidcProviderId(providerId); final FirebaseUserManager userManager = getUserManager(); return new CallableOperation() { @@ -1490,7 +1467,6 @@ public ApiFuture createSamlProviderConfigAsync( private CallableOperation createSamlProviderConfigOp(final SamlProviderConfig.CreateRequest request) { - checkNotDestroyed(); checkNotNull(request, "Create request must not be null."); SamlProviderConfig.checkSamlProviderId(request.getProviderId()); final FirebaseUserManager userManager = getUserManager(); @@ -1534,7 +1510,6 @@ public ApiFuture updateSamlProviderConfigAsync( private CallableOperation updateSamlProviderConfigOp( final SamlProviderConfig.UpdateRequest request) { - checkNotDestroyed(); checkNotNull(request, "Update request must not be null."); checkArgument(!request.getProperties().isEmpty(), "Update request must have at least one property set."); @@ -1579,7 +1554,6 @@ public ApiFuture getSamlProviderConfigAsync(@NonNull String private CallableOperation getSamlProviderConfigOp(final String providerId) { - checkNotDestroyed(); SamlProviderConfig.checkSamlProviderId(providerId); final FirebaseUserManager userManager = getUserManager(); return new CallableOperation() { @@ -1663,7 +1637,6 @@ public ApiFuture> listSamlProviderCo private CallableOperation, FirebaseAuthException> listSamlProviderConfigsOp(@Nullable final String pageToken, final int maxResults) { - checkNotDestroyed(); final FirebaseUserManager userManager = getUserManager(); final DefaultSamlProviderConfigSource source = new DefaultSamlProviderConfigSource(userManager); final ListProviderConfigsPage.Factory factory = @@ -1706,7 +1679,6 @@ public ApiFuture deleteSamlProviderConfigAsync(String providerId) { private CallableOperation deleteSamlProviderConfigOp( final String providerId) { - checkNotDestroyed(); SamlProviderConfig.checkSamlProviderId(providerId); final FirebaseUserManager userManager = getUserManager(); return new CallableOperation() { @@ -1729,32 +1701,12 @@ Supplier threadSafeMemoize(final Supplier supplier) { public T get() { checkNotNull(supplier); synchronized (lock) { - checkNotDestroyed(); return supplier.get(); } } }); } - private void checkNotDestroyed() { - synchronized (lock) { - checkState( - !destroyed.get(), - "FirebaseAuth instance is no longer alive. This happens when " - + "the parent FirebaseApp instance has been deleted."); - } - } - - final void destroy() { - synchronized (lock) { - doDestroy(); - destroyed.set(true); - } - } - - /** Performs any additional required clean up. */ - protected abstract void doDestroy(); - protected abstract static class Builder> { private FirebaseApp firebaseApp; diff --git a/src/main/java/com/google/firebase/auth/FirebaseAuth.java b/src/main/java/com/google/firebase/auth/FirebaseAuth.java index 417ea6436..27e79960d 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseAuth.java +++ b/src/main/java/com/google/firebase/auth/FirebaseAuth.java @@ -69,9 +69,6 @@ public static synchronized FirebaseAuth getInstance(FirebaseApp app) { return service.getInstance(); } - @Override - protected void doDestroy() { } - private static FirebaseAuth fromApp(final FirebaseApp app) { return populateBuilderFromApp(builder(), app, null) .setTenantManager(new Supplier() { @@ -88,11 +85,6 @@ private static class FirebaseAuthService extends FirebaseService { FirebaseAuthService(FirebaseApp app) { super(SERVICE_ID, FirebaseAuth.fromApp(app)); } - - @Override - public void destroy() { - instance.destroy(); - } } static Builder builder() { diff --git a/src/main/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuth.java b/src/main/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuth.java index 95ef169da..bc374c036 100644 --- a/src/main/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuth.java +++ b/src/main/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuth.java @@ -69,11 +69,6 @@ public ApiFuture apply(FirebaseToken input) { }, MoreExecutors.directExecutor()); } - @Override - protected void doDestroy() { - // Nothing extra needs to be destroyed. - } - static TenantAwareFirebaseAuth fromApp(FirebaseApp app, String tenantId) { return populateBuilderFromApp(builder(), app, tenantId) .setTenantId(tenantId) diff --git a/src/main/java/com/google/firebase/cloud/StorageClient.java b/src/main/java/com/google/firebase/cloud/StorageClient.java index d6a1567f4..955abd7e2 100644 --- a/src/main/java/com/google/firebase/cloud/StorageClient.java +++ b/src/main/java/com/google/firebase/cloud/StorageClient.java @@ -106,13 +106,5 @@ private static class StorageClientService extends FirebaseService StorageClientService(StorageClient client) { super(SERVICE_ID, client); } - - @Override - public void destroy() { - // NOTE: We don't explicitly tear down anything here, but public methods of StorageClient - // will now fail because calls to getOptions() and getToken() will hit FirebaseApp, - // which will throw once the app is deleted. - } } - } diff --git a/src/main/java/com/google/firebase/database/FirebaseDatabase.java b/src/main/java/com/google/firebase/database/FirebaseDatabase.java index 0a79932db..306ce9274 100644 --- a/src/main/java/com/google/firebase/database/FirebaseDatabase.java +++ b/src/main/java/com/google/firebase/database/FirebaseDatabase.java @@ -173,7 +173,7 @@ static FirebaseDatabase createForTests( return db; } - /** + /** * @return The version for this build of the Firebase Database client */ public static String getSdkVersion() { @@ -358,6 +358,10 @@ DatabaseConfig getConfig() { return this.config; } + /** + * Tears down the WebSocket connections and background threads started by this {@code + * FirebaseDatabase} instance thus disconnecting from the remote database. + */ void destroy() { synchronized (lock) { if (destroyed.get()) { diff --git a/src/main/java/com/google/firebase/iid/FirebaseInstanceId.java b/src/main/java/com/google/firebase/iid/FirebaseInstanceId.java index 822654402..47026e6f1 100644 --- a/src/main/java/com/google/firebase/iid/FirebaseInstanceId.java +++ b/src/main/java/com/google/firebase/iid/FirebaseInstanceId.java @@ -200,12 +200,5 @@ private static class FirebaseInstanceIdService extends FirebaseService Type of the service */ -public abstract class FirebaseService { +public class FirebaseService { private final String id; protected final T instance; @@ -62,5 +62,7 @@ public final T getInstance() { * Tear down this FirebaseService instance and the service object wrapped in it, cleaning up * any allocated resources in the process. */ - public abstract void destroy(); + public void destroy() { + // Child classes can override this method to implement any service-specific cleanup logic. + } } diff --git a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java index e1b4f794c..a08e8cfaf 100644 --- a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java +++ b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java @@ -403,13 +403,6 @@ private static class FirebaseMessagingService extends FirebaseService { + MockFirebaseService() { + super("MockFirebaseService", new Object()); + } + } } diff --git a/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java b/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java index e34fede1f..bd7aa0ff8 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java +++ b/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java @@ -28,7 +28,6 @@ import com.google.api.client.testing.http.MockHttpTransport; import com.google.api.client.testing.http.MockLowLevelHttpResponse; import com.google.api.core.ApiFuture; -import com.google.common.base.Defaults; import com.google.common.base.Supplier; import com.google.common.base.Suppliers; import com.google.common.collect.ImmutableMap; @@ -40,11 +39,6 @@ import com.google.firebase.testing.ServiceAccount; import com.google.firebase.testing.TestResponseInterceptor; import com.google.firebase.testing.TestUtils; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.util.ArrayList; -import java.util.List; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -105,26 +99,33 @@ public void testInvokeAfterAppDelete() throws Exception { assertNotNull(auth); app.delete(); - for (Method method : auth.getClass().getDeclaredMethods()) { - int modifiers = method.getModifiers(); - if (!Modifier.isPublic(modifiers) || Modifier.isStatic(modifiers)) { - continue; - } - - List parameters = new ArrayList<>(method.getParameterTypes().length); - for (Class parameterType : method.getParameterTypes()) { - parameters.add(Defaults.defaultValue(parameterType)); - } - try { - method.invoke(auth, parameters.toArray()); - fail("No error thrown when invoking auth after deleting app; method: " + method.getName()); - } catch (InvocationTargetException expected) { - String message = "FirebaseAuth instance is no longer alive. This happens when " - + "the parent FirebaseApp instance has been deleted."; - Throwable cause = expected.getCause(); - assertTrue(cause instanceof IllegalStateException); - assertEquals(message, cause.getMessage()); - } + String message = "FirebaseApp 'testInvokeAfterAppDelete' was deleted"; + try { + FirebaseAuth.getInstance(app); + fail("No error thrown when invoking auth after deleting app"); + } catch (IllegalStateException ex) { + assertEquals(message, ex.getMessage()); + } + + try { + auth.createCustomToken("uid"); + fail("No error thrown when invoking auth after deleting app"); + } catch (IllegalStateException ex) { + assertEquals(message, ex.getMessage()); + } + + try { + auth.verifyIdToken("idToken"); + fail("No error thrown when invoking auth after deleting app"); + } catch (IllegalStateException ex) { + assertEquals(message, ex.getMessage()); + } + + try { + auth.getUser("uid"); + fail("No error thrown when invoking auth after deleting app"); + } catch (IllegalStateException ex) { + assertEquals(message, ex.getMessage()); } } @@ -461,10 +462,6 @@ public void testVerifySessionCookieWithCheckRevokedAsyncFailure() throws Interru } } - private FirebaseToken getFirebaseToken(String subject) { - return new FirebaseToken(ImmutableMap.of("sub", subject)); - } - private FirebaseToken getFirebaseToken(long issuedAt) { return new FirebaseToken(ImmutableMap.of("sub", TEST_USER, "iat", issuedAt)); } diff --git a/src/test/java/com/google/firebase/iid/FirebaseInstanceIdTest.java b/src/test/java/com/google/firebase/iid/FirebaseInstanceIdTest.java index f15861a62..27864d992 100644 --- a/src/test/java/com/google/firebase/iid/FirebaseInstanceIdTest.java +++ b/src/test/java/com/google/firebase/iid/FirebaseInstanceIdTest.java @@ -50,6 +50,11 @@ public class FirebaseInstanceIdTest { + private static final FirebaseOptions APP_OPTIONS = FirebaseOptions.builder() + .setCredentials(new MockGoogleCredentials("test-token")) + .setProjectId("test-project") + .build(); + private static final Map ERROR_MESSAGES = ImmutableMap.of( 404, "Instance ID \"test-iid\": Failed to find the instance ID.", 409, "Instance ID \"test-iid\": Already deleted.", @@ -88,13 +93,25 @@ public void testNoProjectId() { } } + @Test + public void testInvokeAfterAppDelete() { + FirebaseApp app = FirebaseApp.initializeApp(APP_OPTIONS, "testInvokeAfterAppDelete"); + FirebaseInstanceId instanceId = FirebaseInstanceId.getInstance(app); + assertNotNull(instanceId); + app.delete(); + + try { + FirebaseInstanceId.getInstance(app); + fail("No error thrown when invoking instanceId after deleting app"); + } catch (IllegalStateException ex) { + String message = "FirebaseApp 'testInvokeAfterAppDelete' was deleted"; + assertEquals(message, ex.getMessage()); + } + } + @Test public void testInvalidInstanceId() { - FirebaseOptions options = FirebaseOptions.builder() - .setCredentials(new MockGoogleCredentials("test-token")) - .setProjectId("test-project") - .build(); - FirebaseApp.initializeApp(options); + FirebaseApp.initializeApp(APP_OPTIONS); FirebaseInstanceId instanceId = FirebaseInstanceId.getInstance(); TestResponseInterceptor interceptor = new TestResponseInterceptor(); @@ -122,9 +139,7 @@ public void testDeleteInstanceId() throws Exception { MockHttpTransport transport = new MockHttpTransport.Builder() .setLowLevelHttpResponse(response) .build(); - FirebaseOptions options = FirebaseOptions.builder() - .setCredentials(new MockGoogleCredentials("test-token")) - .setProjectId("test-project") + FirebaseOptions options = APP_OPTIONS.toBuilder() .setHttpTransport(transport) .build(); FirebaseApp app = FirebaseApp.initializeApp(options); @@ -168,9 +183,7 @@ public void testDeleteInstanceIdError() throws Exception { MockHttpTransport transport = new MockHttpTransport.Builder() .setLowLevelHttpResponse(response) .build(); - FirebaseOptions options = FirebaseOptions.builder() - .setCredentials(new MockGoogleCredentials("test-token")) - .setProjectId("test-project") + FirebaseOptions options = APP_OPTIONS.toBuilder() .setHttpTransport(transport) .build(); FirebaseApp app = FirebaseApp.initializeApp(options); From fba507bd4cff639865653da717e7b1a5dafea8fd Mon Sep 17 00:00:00 2001 From: kentengjin Date: Wed, 30 Sep 2020 11:41:22 -0700 Subject: [PATCH 032/354] fix(auth): Migrate IAM SignBlob to IAMCredentials SignBlob (#480) * Migrate IAM SignBlob to IAMCredentials SignBlob Point all SignBlob calls to IAMCredentials * Fix FirebaseToken tests Change proto field to fix the test --- .../google/firebase/auth/internal/CryptoSigners.java | 10 +++++----- .../firebase/auth/FirebaseCustomTokenTest.java | 4 ++-- .../firebase/auth/internal/CryptoSignersTest.java | 12 ++++++------ 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/main/java/com/google/firebase/auth/internal/CryptoSigners.java b/src/main/java/com/google/firebase/auth/internal/CryptoSigners.java index 7bc54afdf..f167b09a0 100644 --- a/src/main/java/com/google/firebase/auth/internal/CryptoSigners.java +++ b/src/main/java/com/google/firebase/auth/internal/CryptoSigners.java @@ -65,13 +65,13 @@ public String getAccount() { /** * @ {@link CryptoSigner} implementation that uses the - * - * Google IAM service to sign data. + * + * Google IAMCredentials service to sign data. */ static class IAMCryptoSigner implements CryptoSigner { private static final String IAM_SIGN_BLOB_URL = - "https://iam.googleapis.com/v1/projects/-/serviceAccounts/%s:signBlob"; + "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/%s:signBlob"; private final String serviceAccount; private final ErrorHandlingHttpClient httpClient; @@ -95,11 +95,11 @@ void setInterceptor(HttpResponseInterceptor interceptor) { @Override public byte[] sign(byte[] payload) throws FirebaseAuthException { String encodedPayload = BaseEncoding.base64().encode(payload); - Map content = ImmutableMap.of("bytesToSign", encodedPayload); + Map content = ImmutableMap.of("payload", encodedPayload); String encodedUrl = String.format(IAM_SIGN_BLOB_URL, serviceAccount); HttpRequestInfo requestInfo = HttpRequestInfo.buildJsonPostRequest(encodedUrl, content); GenericJson parsed = httpClient.sendAndParse(requestInfo, GenericJson.class); - return BaseEncoding.base64().decode((String) parsed.get("signature")); + return BaseEncoding.base64().decode((String) parsed.get("signedBlob")); } @Override diff --git a/src/test/java/com/google/firebase/auth/FirebaseCustomTokenTest.java b/src/test/java/com/google/firebase/auth/FirebaseCustomTokenTest.java index b277dce14..5e37161cd 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseCustomTokenTest.java +++ b/src/test/java/com/google/firebase/auth/FirebaseCustomTokenTest.java @@ -92,7 +92,7 @@ public void testCreateCustomTokenWithDeveloperClaims() throws Exception { public void testCreateCustomTokenWithoutServiceAccountCredentials() throws Exception { MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); String content = Utils.getDefaultJsonFactory().toString( - ImmutableMap.of("signature", BaseEncoding.base64().encode("test-signature".getBytes()))); + ImmutableMap.of("signedBlob", BaseEncoding.base64().encode("test-signature".getBytes()))); response.setContent(content); MockHttpTransport transport = new MultiRequestMockHttpTransport(ImmutableList.of(response)); @@ -117,7 +117,7 @@ public void testCreateCustomTokenWithoutServiceAccountCredentials() throws Excep @Test public void testCreateCustomTokenWithDiscoveredServiceAccount() throws Exception { String content = Utils.getDefaultJsonFactory().toString( - ImmutableMap.of("signature", BaseEncoding.base64().encode("test-signature".getBytes()))); + ImmutableMap.of("signedBlob", BaseEncoding.base64().encode("test-signature".getBytes()))); List responses = ImmutableList.of( // Service account discovery response new MockLowLevelHttpResponse().setContent("test@service.account"), diff --git a/src/test/java/com/google/firebase/auth/internal/CryptoSignersTest.java b/src/test/java/com/google/firebase/auth/internal/CryptoSignersTest.java index 32f9fd543..fee4d8fb8 100644 --- a/src/test/java/com/google/firebase/auth/internal/CryptoSignersTest.java +++ b/src/test/java/com/google/firebase/auth/internal/CryptoSignersTest.java @@ -71,7 +71,7 @@ public void testInvalidServiceAccountCryptoSigner() { public void testIAMCryptoSigner() throws Exception { String signature = BaseEncoding.base64().encode("signed-bytes".getBytes()); String response = Utils.getDefaultJsonFactory().toString( - ImmutableMap.of("signature", signature)); + ImmutableMap.of("signedBlob", signature)); MockHttpTransport transport = new MockHttpTransport.Builder() .setLowLevelHttpResponse(new MockLowLevelHttpResponse().setContent(response)) .build(); @@ -84,7 +84,7 @@ public void testIAMCryptoSigner() throws Exception { byte[] data = signer.sign("foo".getBytes()); assertArrayEquals("signed-bytes".getBytes(), data); - final String url = "https://iam.googleapis.com/v1/projects/-/serviceAccounts/" + final String url = "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/" + "test-service-account@iam.gserviceaccount.com:signBlob"; assertEquals(url, interceptor.getResponse().getRequest().getUrl().toString()); } @@ -150,7 +150,7 @@ public void testInvalidIAMCryptoSigner() { public void testMetadataService() throws Exception { String signature = BaseEncoding.base64().encode("signed-bytes".getBytes()); String response = Utils.getDefaultJsonFactory().toString( - ImmutableMap.of("signature", signature)); + ImmutableMap.of("signedBlob", signature)); MockHttpTransport transport = new MultiRequestMockHttpTransport( ImmutableList.of( new MockLowLevelHttpResponse().setContent("metadata-server@iam.gserviceaccount.com"), @@ -168,7 +168,7 @@ public void testMetadataService() throws Exception { byte[] data = signer.sign("foo".getBytes()); assertArrayEquals("signed-bytes".getBytes(), data); - final String url = "https://iam.googleapis.com/v1/projects/-/serviceAccounts/" + final String url = "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/" + "metadata-server@iam.gserviceaccount.com:signBlob"; HttpRequest request = interceptor.getResponse().getRequest(); assertEquals(url, request.getUrl().toString()); @@ -179,7 +179,7 @@ public void testMetadataService() throws Exception { public void testExplicitServiceAccountEmail() throws Exception { String signature = BaseEncoding.base64().encode("signed-bytes".getBytes()); String response = Utils.getDefaultJsonFactory().toString( - ImmutableMap.of("signature", signature)); + ImmutableMap.of("signedBlob", signature)); // Explicit service account should get precedence MockHttpTransport transport = new MultiRequestMockHttpTransport( @@ -198,7 +198,7 @@ public void testExplicitServiceAccountEmail() throws Exception { byte[] data = signer.sign("foo".getBytes()); assertArrayEquals("signed-bytes".getBytes(), data); - final String url = "https://iam.googleapis.com/v1/projects/-/serviceAccounts/" + final String url = "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/" + "explicit-service-account@iam.gserviceaccount.com:signBlob"; HttpRequest request = interceptor.getResponse().getRequest(); assertEquals(url, request.getUrl().toString()); From 408b85f7d0072e8fb3b462aa2317843d683e7ad7 Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Wed, 7 Oct 2020 15:50:03 -0400 Subject: [PATCH 033/354] [chore] Release 7.0.1 (#484) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 0f40ea078..0bde2d2f4 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ com.google.firebase firebase-admin - 7.0.0 + 7.0.1 jar firebase-admin From ba56dcde18fb6cade53269edcca75f5ac101df4b Mon Sep 17 00:00:00 2001 From: Rodrigo Lazo Date: Thu, 15 Oct 2020 14:35:23 -0400 Subject: [PATCH 034/354] Fix comment about max number of events (#488) Multicast message can send up to 500 messages, not 100 https://github.com/firebase/firebase-admin-java/blob/408b85f7d0072e8fb3b462aa2317843d683e7ad7/src/main/java/com/google/firebase/messaging/MulticastMessage.java#L46 --- .../com/google/firebase/snippets/FirebaseMessagingSnippets.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/com/google/firebase/snippets/FirebaseMessagingSnippets.java b/src/test/java/com/google/firebase/snippets/FirebaseMessagingSnippets.java index f1432e160..4a4a25472 100644 --- a/src/test/java/com/google/firebase/snippets/FirebaseMessagingSnippets.java +++ b/src/test/java/com/google/firebase/snippets/FirebaseMessagingSnippets.java @@ -151,7 +151,7 @@ public void sendAll() throws FirebaseMessagingException { public void sendMulticast() throws FirebaseMessagingException { // [START send_multicast] - // Create a list containing up to 100 registration tokens. + // Create a list containing up to 500 registration tokens. // These registration tokens come from the client FCM SDKs. List registrationTokens = Arrays.asList( "YOUR_REGISTRATION_TOKEN_1", From 5f3b696db4bf4550203a47834dfa19a8a33d750b Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Mon, 14 Dec 2020 14:40:18 -0500 Subject: [PATCH 035/354] feat(rc): Add Remote Config Management API (#502) - Add Remote Config Management API --- .../firebase/remoteconfig/Condition.java | 174 +++ .../remoteconfig/FirebaseRemoteConfig.java | 415 ++++++ .../FirebaseRemoteConfigClient.java | 43 + .../FirebaseRemoteConfigClientImpl.java | 269 ++++ .../FirebaseRemoteConfigException.java | 63 + .../remoteconfig/ListVersionsOptions.java | 198 +++ .../remoteconfig/ListVersionsPage.java | 268 ++++ .../firebase/remoteconfig/Parameter.java | 167 +++ .../firebase/remoteconfig/ParameterGroup.java | 138 ++ .../firebase/remoteconfig/ParameterValue.java | 127 ++ .../firebase/remoteconfig/PublishOptions.java | 33 + .../remoteconfig/RemoteConfigErrorCode.java | 60 + .../remoteconfig/RemoteConfigUtil.java | 93 ++ .../firebase/remoteconfig/TagColor.java | 45 + .../firebase/remoteconfig/Template.java | 281 ++++ .../google/firebase/remoteconfig/User.java | 98 ++ .../google/firebase/remoteconfig/Version.java | 225 +++ .../RemoteConfigServiceErrorResponse.java | 72 + .../internal/TemplateResponse.java | 424 ++++++ .../remoteconfig/internal/package-info.java | 20 + .../firebase/remoteconfig/ConditionTest.java | 93 ++ .../FirebaseRemoteConfigClientImplTest.java | 1234 +++++++++++++++++ .../remoteconfig/FirebaseRemoteConfigIT.java | 199 +++ .../FirebaseRemoteConfigTest.java | 600 ++++++++ .../remoteconfig/ListVersionsPageTest.java | 338 +++++ .../remoteconfig/MockRemoteConfigClient.java | 89 ++ .../remoteconfig/ParameterGroupTest.java | 87 ++ .../firebase/remoteconfig/ParameterTest.java | 91 ++ .../remoteconfig/ParameterValueTest.java | 54 + .../firebase/remoteconfig/TemplateTest.java | 346 +++++ .../firebase/remoteconfig/UserTest.java | 62 + .../firebase/remoteconfig/VersionTest.java | 105 ++ src/test/resources/getRemoteConfig.json | 56 + .../resources/listRemoteConfigVersions.json | 50 + 34 files changed, 6617 insertions(+) create mode 100644 src/main/java/com/google/firebase/remoteconfig/Condition.java create mode 100644 src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfig.java create mode 100644 src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClient.java create mode 100644 src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImpl.java create mode 100644 src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigException.java create mode 100644 src/main/java/com/google/firebase/remoteconfig/ListVersionsOptions.java create mode 100644 src/main/java/com/google/firebase/remoteconfig/ListVersionsPage.java create mode 100644 src/main/java/com/google/firebase/remoteconfig/Parameter.java create mode 100644 src/main/java/com/google/firebase/remoteconfig/ParameterGroup.java create mode 100644 src/main/java/com/google/firebase/remoteconfig/ParameterValue.java create mode 100644 src/main/java/com/google/firebase/remoteconfig/PublishOptions.java create mode 100644 src/main/java/com/google/firebase/remoteconfig/RemoteConfigErrorCode.java create mode 100644 src/main/java/com/google/firebase/remoteconfig/RemoteConfigUtil.java create mode 100644 src/main/java/com/google/firebase/remoteconfig/TagColor.java create mode 100644 src/main/java/com/google/firebase/remoteconfig/Template.java create mode 100644 src/main/java/com/google/firebase/remoteconfig/User.java create mode 100644 src/main/java/com/google/firebase/remoteconfig/Version.java create mode 100644 src/main/java/com/google/firebase/remoteconfig/internal/RemoteConfigServiceErrorResponse.java create mode 100644 src/main/java/com/google/firebase/remoteconfig/internal/TemplateResponse.java create mode 100644 src/main/java/com/google/firebase/remoteconfig/internal/package-info.java create mode 100644 src/test/java/com/google/firebase/remoteconfig/ConditionTest.java create mode 100644 src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java create mode 100644 src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigIT.java create mode 100644 src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java create mode 100644 src/test/java/com/google/firebase/remoteconfig/ListVersionsPageTest.java create mode 100644 src/test/java/com/google/firebase/remoteconfig/MockRemoteConfigClient.java create mode 100644 src/test/java/com/google/firebase/remoteconfig/ParameterGroupTest.java create mode 100644 src/test/java/com/google/firebase/remoteconfig/ParameterTest.java create mode 100644 src/test/java/com/google/firebase/remoteconfig/ParameterValueTest.java create mode 100644 src/test/java/com/google/firebase/remoteconfig/TemplateTest.java create mode 100644 src/test/java/com/google/firebase/remoteconfig/UserTest.java create mode 100644 src/test/java/com/google/firebase/remoteconfig/VersionTest.java create mode 100644 src/test/resources/getRemoteConfig.json create mode 100644 src/test/resources/listRemoteConfigVersions.json diff --git a/src/main/java/com/google/firebase/remoteconfig/Condition.java b/src/main/java/com/google/firebase/remoteconfig/Condition.java new file mode 100644 index 000000000..6ccde59d9 --- /dev/null +++ b/src/main/java/com/google/firebase/remoteconfig/Condition.java @@ -0,0 +1,174 @@ +/* + * Copyright 2020 Google LLC + * + * 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.remoteconfig; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.base.Strings; +import com.google.firebase.internal.NonNull; +import com.google.firebase.internal.Nullable; +import com.google.firebase.remoteconfig.internal.TemplateResponse.ConditionResponse; + +import java.util.Objects; + +/** + * Represents a Remote Config condition that can be included in a {@link Template}. + * A condition targets a specific group of users. A list of these conditions make up + * part of a Remote Config template. + */ +public final class Condition { + + private String name; + private String expression; + private TagColor tagColor; + + /** + * Creates a new {@link Condition}. + * + * @param name A non-null, non-empty, and unique name of this condition. + * @param expression A non-null and non-empty expression of this condition. + */ + public Condition(@NonNull String name, @NonNull String expression) { + this(name, expression, null); + } + + /** + * Creates a new {@link Condition}. + * + * @param name A non-null, non-empty, and unique name of this condition. + * @param expression A non-null and non-empty expression of this condition. + * @param tagColor A color associated with this condition for display purposes in the + * Firebase Console. Not specifying this value results in the console picking an + * arbitrary color to associate with the condition. + */ + public Condition(@NonNull String name, @NonNull String expression, @Nullable TagColor tagColor) { + checkArgument(!Strings.isNullOrEmpty(name), "condition name must not be null or empty"); + checkArgument(!Strings.isNullOrEmpty(expression), + "condition expression must not be null or empty"); + this.name = name; + this.expression = expression; + this.tagColor = tagColor; + } + + Condition(@NonNull ConditionResponse conditionResponse) { + checkNotNull(conditionResponse); + this.name = conditionResponse.getName(); + this.expression = conditionResponse.getExpression(); + if (!Strings.isNullOrEmpty(conditionResponse.getTagColor())) { + this.tagColor = TagColor.valueOf(conditionResponse.getTagColor()); + } + } + + /** + * Gets the name of the condition. + * + * @return The name of the condition. + */ + @NonNull + public String getName() { + return name; + } + + /** + * Gets the expression of the condition. + * + * @return The expression of the condition. + */ + @NonNull + public String getExpression() { + return expression; + } + + /** + * Gets the tag color of the condition used for display purposes in the Firebase Console. + * + * @return The tag color of the condition. + */ + @NonNull + public TagColor getTagColor() { + return tagColor; + } + + /** + * Sets the name of the condition. + * + * @param name A non-empty and unique name of this condition. + * @return This {@link Condition}. + */ + public Condition setName(@NonNull String name) { + checkArgument(!Strings.isNullOrEmpty(name), "condition name must not be null or empty"); + this.name = name; + return this; + } + + /** + * Sets the expression of the condition. + * + *

See + * condition expressions for the expected syntax of this field. + * + * @param expression The logic of this condition. + * @return This {@link Condition}. + */ + public Condition setExpression(@NonNull String expression) { + checkArgument(!Strings.isNullOrEmpty(expression), + "condition expression must not be null or empty"); + this.expression = expression; + return this; + } + + /** + * Sets the tag color of the condition. + * + *

The color associated with this condition for display purposes in the Firebase Console. + * Not specifying this value results in the console picking an arbitrary color to associate + * with the condition. + * + * @param tagColor The tag color of this condition. + * @return This {@link Condition}. + */ + public Condition setTagColor(@Nullable TagColor tagColor) { + this.tagColor = tagColor; + return this; + } + + ConditionResponse toConditionResponse() { + return new ConditionResponse() + .setName(this.name) + .setExpression(this.expression) + .setTagColor(this.tagColor == null ? null : this.tagColor.getColor()); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Condition condition = (Condition) o; + return Objects.equals(name, condition.name) + && Objects.equals(expression, condition.expression) && tagColor == condition.tagColor; + } + + @Override + public int hashCode() { + return Objects.hash(name, expression, tagColor); + } +} diff --git a/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfig.java b/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfig.java new file mode 100644 index 000000000..41a0afbe4 --- /dev/null +++ b/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfig.java @@ -0,0 +1,415 @@ +/* + * Copyright 2020 Google LLC + * + * 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.remoteconfig; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.api.core.ApiFuture; +import com.google.common.annotations.VisibleForTesting; +import com.google.firebase.FirebaseApp; +import com.google.firebase.ImplFirebaseTrampolines; +import com.google.firebase.internal.CallableOperation; +import com.google.firebase.internal.FirebaseService; +import com.google.firebase.internal.NonNull; + +/** + * This class is the entry point for all server-side Firebase Remote Config actions. + * + *

You can get an instance of {@link FirebaseRemoteConfig} via {@link #getInstance(FirebaseApp)}, + * and then use it to manage Remote Config templates. + */ +public final class FirebaseRemoteConfig { + + private static final String SERVICE_ID = FirebaseRemoteConfig.class.getName(); + private final FirebaseApp app; + private final FirebaseRemoteConfigClient remoteConfigClient; + + @VisibleForTesting + FirebaseRemoteConfig(FirebaseApp app, FirebaseRemoteConfigClient client) { + this.app = checkNotNull(app); + this.remoteConfigClient = checkNotNull(client); + } + + private FirebaseRemoteConfig(FirebaseApp app) { + this(app, FirebaseRemoteConfigClientImpl.fromApp(app)); + } + + /** + * Gets the {@link FirebaseRemoteConfig} instance for the default {@link FirebaseApp}. + * + * @return The {@link FirebaseRemoteConfig} instance for the default {@link FirebaseApp}. + */ + public static FirebaseRemoteConfig getInstance() { + return getInstance(FirebaseApp.getInstance()); + } + + /** + * Gets the {@link FirebaseRemoteConfig} instance for the specified {@link FirebaseApp}. + * + * @return The {@link FirebaseRemoteConfig} instance for the specified {@link FirebaseApp}. + */ + public static synchronized FirebaseRemoteConfig getInstance(FirebaseApp app) { + FirebaseRemoteConfigService service = ImplFirebaseTrampolines.getService(app, SERVICE_ID, + FirebaseRemoteConfigService.class); + if (service == null) { + service = ImplFirebaseTrampolines.addService(app, new FirebaseRemoteConfigService(app)); + } + return service.getInstance(); + } + + /** + * Gets the current active version of the Remote Config template. + * + * @return A {@link Template}. + * @throws FirebaseRemoteConfigException If an error occurs while getting the template. + */ + public Template getTemplate() throws FirebaseRemoteConfigException { + return getTemplateOp().call(); + } + + /** + * Similar to {@link #getTemplate()} but performs the operation asynchronously. + * + * @return An {@code ApiFuture} that completes with a {@link Template} when + * the template is available. + */ + public ApiFuture