From 68e969f4c73bb5fffcf85b20d47b3afcb0d1722e Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Wed, 22 Jul 2020 18:34:04 -0700 Subject: [PATCH] chore: Merged with v7 branch with master --- .github/resources/firebase.asc.gpg | Bin 0 -> 4061 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 ++ CONTRIBUTING.md | 17 +- checkstyle.xml | 9 +- pom.xml | 156 +- .../firebase/auth/AbstractFirebaseAuth.java | 1715 ++++++++++++++++ .../google/firebase/auth/AuthErrorCode.java | 2 + .../firebase/auth/DeleteUsersResult.java | 74 + .../google/firebase/auth/EmailIdentifier.java | 49 + .../google/firebase/auth/FirebaseAuth.java | 1078 +--------- .../google/firebase/auth/FirebaseToken.java | 13 +- .../firebase/auth/FirebaseTokenUtils.java | 15 +- .../auth/FirebaseTokenVerifierImpl.java | 28 +- .../firebase/auth/FirebaseUserManager.java | 248 ++- .../google/firebase/auth/GetUsersResult.java | 52 + .../auth/ListProviderConfigsPage.java | 268 +++ .../google/firebase/auth/ListUsersPage.java | 8 +- .../firebase/auth/OidcProviderConfig.java | 177 ++ .../google/firebase/auth/PhoneIdentifier.java | 49 + .../google/firebase/auth/ProviderConfig.java | 157 ++ .../firebase/auth/ProviderIdentifier.java | 56 + .../firebase/auth/SamlProviderConfig.java | 343 ++++ .../google/firebase/auth/UidIdentifier.java | 49 + .../google/firebase/auth/UserIdentifier.java | 31 + .../google/firebase/auth/UserMetadata.java | 15 +- .../com/google/firebase/auth/UserRecord.java | 36 +- .../auth/{ => internal}/AuthErrorHandler.java | 4 +- .../auth/internal/AuthHttpClient.java | 105 + .../auth/internal/BatchDeleteResponse.java | 51 + .../internal/FirebaseCustomAuthToken.java | 13 + .../auth/internal/FirebaseTokenFactory.java | 16 +- .../auth/internal/GetAccountInfoRequest.java | 80 + .../auth/internal/GetAccountInfoResponse.java | 16 +- .../ListOidcProviderConfigsResponse.java | 62 + .../internal/ListProviderConfigsResponse.java | 32 + .../ListSamlProviderConfigsResponse.java | 62 + .../auth/internal/ListTenantsResponse.java | 55 + .../multitenancy/FirebaseTenantClient.java | 106 + .../auth/multitenancy/ListTenantsPage.java | 245 +++ .../firebase/auth/multitenancy/Tenant.java | 195 ++ .../multitenancy/TenantAwareFirebaseAuth.java | 50 + .../auth/multitenancy/TenantManager.java | 287 +++ .../firebase/internal/HttpRequestInfo.java | 13 +- .../firebase/messaging/AndroidConfig.java | 16 +- .../firebase/messaging/BatchResponse.java | 29 +- .../firebase/messaging/BatchResponseImpl.java | 58 + .../FirebaseMessagingClientImpl.java | 2 +- .../firebase/messaging/MulticastMessage.java | 15 +- .../google/firebase/auth/FirebaseAuthIT.java | 818 +++++--- .../firebase/auth/FirebaseAuthTest.java | 24 +- .../auth/FirebaseTokenVerifierImplTest.java | 62 +- .../auth/FirebaseUserManagerTest.java | 1754 +++++++++++++++-- .../com/google/firebase/auth/GetUsersIT.java | 152 ++ .../firebase/auth/ImportUserRecordTest.java | 2 +- .../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 | 451 +++++ .../auth/multitenancy/TenantManagerIT.java | 158 ++ .../auth/multitenancy/TenantTest.java | 92 + .../database/FirebaseDatabaseTest.java | 8 +- .../database/integration/OrderByTestIT.java | 12 +- .../firebase/messaging/BatchResponseTest.java | 8 +- .../messaging/FirebaseMessagingTest.java | 2 +- .../firebase/messaging/MessageTest.java | 23 + .../messaging/MulticastMessageTest.java | 5 +- 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 + 90 files changed, 10732 insertions(+), 1663 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 create mode 100644 src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java 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/ListProviderConfigsPage.java create mode 100644 src/main/java/com/google/firebase/auth/OidcProviderConfig.java create mode 100644 src/main/java/com/google/firebase/auth/PhoneIdentifier.java create mode 100644 src/main/java/com/google/firebase/auth/ProviderConfig.java create mode 100644 src/main/java/com/google/firebase/auth/ProviderIdentifier.java create mode 100644 src/main/java/com/google/firebase/auth/SamlProviderConfig.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 rename src/main/java/com/google/firebase/auth/{ => internal}/AuthErrorHandler.java (97%) create mode 100644 src/main/java/com/google/firebase/auth/internal/AuthHttpClient.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/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/main/java/com/google/firebase/messaging/BatchResponseImpl.java create mode 100644 src/test/java/com/google/firebase/auth/GetUsersIT.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/.github/resources/firebase.asc.gpg b/.github/resources/firebase.asc.gpg new file mode 100644 index 0000000000000000000000000000000000000000..8fd7d0769b6056896ebff240b9955656f7c98a60 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<}n|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..4afbaca26 --- /dev/null +++ b/.github/resources/settings.xml @@ -0,0 +1,29 @@ + + + + false + + + + ossrh + ${env.NEXUS_OSSRH_USERNAME} + ${env.NEXUS_OSSRH_PASSWORD} + + + + + + release + + true + + + gpg + A9B90B41060565F56F348F948B6B459CFD695DE8 + ${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..cd1a5b75c --- /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 --no-tty --batch --yes 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.community/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..58ecbfa35 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: pull_request 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..985ce94d6 --- /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 + + - 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. + - 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 + + # 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/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/pom.xml b/pom.xml index b13382689..33e235d6e 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ com.google.firebase firebase-admin - 6.12.3-SNAPSHOT + 6.14.0 jar firebase-admin @@ -59,7 +59,7 @@ UTF-8 UTF-8 ${skipTests} - 4.1.34.Final + 4.1.50.Final @@ -99,6 +99,7 @@ maven-javadoc-plugin + 2.10.4 site @@ -161,56 +162,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 @@ -221,6 +174,12 @@ sign + + + --pinentry-mode + loopback + + @@ -295,6 +254,7 @@ + maven-checkstyle-plugin 2.17 @@ -315,6 +275,8 @@ + + maven-compiler-plugin 3.6.1 @@ -323,6 +285,8 @@ 1.7 + + maven-surefire-plugin 2.19.1 @@ -330,32 +294,69 @@ ${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 +365,7 @@ ossrh https://oss.sonatype.org/ - false + true @@ -387,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.1 com.google.api-client google-api-client-gson - 1.30.1 com.google.http-client google-http-client - 1.30.1 com.google.api api-common - 1.8.1 com.google.auth google-auth-library-oauth2-http - 0.17.1 com.google.cloud google-cloud-storage - 1.91.0 com.google.cloud google-cloud-firestore - 1.31.0 com.google.guava guava - 26.0-android org.slf4j 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..ee3e67b15 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java @@ -0,0 +1,1715 @@ +/* + * 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.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 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 { + return tokenFactory.createSignedCustomAuthTokenForUser(uid, developerClaims); + } + }; + } + + /** + * 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/AuthErrorCode.java b/src/main/java/com/google/firebase/auth/AuthErrorCode.java index 463c24318..c1625187f 100644 --- a/src/main/java/com/google/firebase/auth/AuthErrorCode.java +++ b/src/main/java/com/google/firebase/auth/AuthErrorCode.java @@ -71,6 +71,8 @@ public enum AuthErrorCode { */ REVOKED_SESSION_COOKIE, + TENANT_ID_MISMATCH, + /** * A user already exists with the provided UID. */ 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 7cd738ec5..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,32 +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.util.List; -import java.util.Map; -import java.util.concurrent.atomic.AtomicBoolean; /** * This class is the entry point for all server-side Firebase Authentication actions. @@ -53,27 +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 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(); } /** @@ -92,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)); } @@ -101,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. @@ -110,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(); } @@ -121,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( @@ -151,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. @@ -164,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(); } @@ -197,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( @@ -221,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); @@ -229,962 +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 { - return tokenFactory.createSignedCustomAuthTokenForUser(uid, developerClaims); - } - }; - } - - /** - * 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 a page of users starting from the specified {@code pageToken}. Page size will be - * 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(); - } - - /** - * 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; - } - }; - } - - /** - * 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); - } - }; - } - - /** - * 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 cff1a6cfd..2718513c4 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()"); @@ -84,6 +96,7 @@ static FirebaseTokenVerifierImpl createIdTokenVerifier(FirebaseApp app, Clock cl .setIdTokenVerifier(idTokenVerifier) .setInvalidTokenErrorCode(AuthErrorCode.INVALID_ID_TOKEN) .setExpiredTokenErrorCode(AuthErrorCode.EXPIRED_ID_TOKEN) + .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 b34cfb516..273ec1532 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseTokenVerifierImpl.java +++ b/src/main/java/com/google/firebase/auth/FirebaseTokenVerifierImpl.java @@ -29,6 +29,7 @@ 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; @@ -55,6 +56,7 @@ final class FirebaseTokenVerifierImpl implements FirebaseTokenVerifier { private final String docUrl; private final AuthErrorCode invalidTokenErrorCode; private final AuthErrorCode expiredTokenErrorCode; + private final String tenantId; private FirebaseTokenVerifierImpl(Builder builder) { this.jsonFactory = checkNotNull(builder.jsonFactory); @@ -69,6 +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); } /** @@ -94,7 +97,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() { @@ -317,6 +322,17 @@ 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)) { + 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); + } + } + static Builder builder() { return new Builder(); } @@ -331,6 +347,7 @@ static final class Builder { private String docUrl; private AuthErrorCode invalidTokenErrorCode; private AuthErrorCode expiredTokenErrorCode; + private String tenantId; private Builder() { } @@ -364,16 +381,21 @@ Builder setDocUrl(String docUrl) { return this; } - public Builder setInvalidTokenErrorCode(AuthErrorCode invalidTokenErrorCode) { + Builder setInvalidTokenErrorCode(AuthErrorCode invalidTokenErrorCode) { this.invalidTokenErrorCode = invalidTokenErrorCode; return this; } - public Builder setExpiredTokenErrorCode(AuthErrorCode expiredTokenErrorCode) { + Builder setExpiredTokenErrorCode(AuthErrorCode expiredTokenErrorCode) { this.expiredTokenErrorCode = expiredTokenErrorCode; 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 650f9d019..55b854e76 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java +++ b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java @@ -20,15 +20,14 @@ 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.HttpRequestFactory; 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.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; @@ -36,20 +35,23 @@ import com.google.firebase.FirebaseApp; import com.google.firebase.ImplFirebaseTrampolines; import com.google.firebase.IncomingHttpResponse; -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.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.ErrorHandlingHttpClient; -import com.google.firebase.internal.HttpRequestInfo; import com.google.firebase.internal.NonNull; import com.google.firebase.internal.Nullable; -import com.google.firebase.internal.SdkUtils; +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 @@ -60,6 +62,9 @@ */ class FirebaseUserManager { + 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; static final int MAX_IMPORT_USERS = 1000; @@ -68,45 +73,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"; - private static final String CLIENT_VERSION = "Java/Admin/" + SdkUtils.getVersion(); + "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 ErrorHandlingHttpClient httpClient; + private final AuthHttpClient httpClient; - private HttpResponseInterceptor interceptor; - - /** - * Creates a new FirebaseUserManager instance. - * - * @param app A non-null {@link FirebaseApp}. - */ - FirebaseUserManager(@NonNull FirebaseApp app) { - this(app, null); - } - - FirebaseUserManager(@NonNull FirebaseApp app, @Nullable HttpRequestFactory requestFactory) { - checkNotNull(app, "FirebaseApp must not be null"); + 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; } - AuthErrorHandler authErrorHandler = new AuthErrorHandler(jsonFactory); - this.httpClient = new ErrorHandlingHttpClient<>(requestFactory, jsonFactory, authErrorHandler); + 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 { @@ -127,13 +128,35 @@ UserRecord getUserByPhoneNumber(String phoneNumber) throws FirebaseAuthException return lookupUserAccount(payload, "phone number: " + phoneNumber); } - String createUser(CreateRequest request) throws FirebaseAuthException { - GenericJson response = post( - "/accounts", request.getProperties(), GenericJson.class); + 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); + Set results = new HashSet<>(); + if (response.getUsers() != null) { + for (GetAccountInfoResponse.User user : response.getUsers()) { + results.add(new UserRecord(user, jsonFactory)); + } + } + return results; + } + + String createUser(UserRecord.CreateRequest request) throws FirebaseAuthException { + GenericJson response = post("/accounts", request.getProperties(), GenericJson.class); return (String) response.get("localId"); } - void updateUser(UpdateRequest request, JsonFactory jsonFactory) throws FirebaseAuthException { + void updateUser(UserRecord.UpdateRequest request, JsonFactory jsonFactory) + throws FirebaseAuthException { post("/accounts:update", request.getProperties(jsonFactory), GenericJson.class); } @@ -142,6 +165,15 @@ void deleteUser(String uid) throws FirebaseAuthException { post("/accounts:delete", payload, GenericJson.class); } + DeleteUsersResult deleteUsers(@NonNull List uids) throws FirebaseAuthException { + final Map payload = ImmutableMap.of( + "localIds", uids, + "force", true); + BatchDeleteResponse response = post( + "/accounts:batchDelete", payload, BatchDeleteResponse.class); + return new DeleteUsersResult(uids.size(), response); + } + DownloadAccountResponse listUsers(int maxResults, String pageToken) throws FirebaseAuthException { ImmutableMap.Builder builder = ImmutableMap.builder() .put("maxResults", maxResults); @@ -150,16 +182,15 @@ 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()); - return sendRequest( - HttpMethods.GET, url.toString(), null, DownloadAccountResponse.class); + return httpClient.sendRequest(HttpMethods.GET, url, null, DownloadAccountResponse.class); } UserImportResult importUsers(UserImportRequest request) throws FirebaseAuthException { checkNotNull(request); UploadAccountResponse response = post( - "/accounts:batchCreate", request, UploadAccountResponse.class); + "/accounts:batchCreate", request, UploadAccountResponse.class); return new UserImportResult(request.getUsersCount(), response); } @@ -187,8 +218,8 @@ String getEmailActionLink(EmailLinkType type, String email, private UserRecord lookupUserAccount( Map payload, String identifier) throws FirebaseAuthException { - IncomingHttpResponse response = sendRequest( - HttpMethods.POST, baseUrl + "/accounts:lookup", payload); + IncomingHttpResponse response = httpClient.sendRequest( + HttpMethods.POST, new GenericUrl(userMgtBaseUrl + "/accounts:lookup"), payload); GetAccountInfoResponse parsed = httpClient.parse(response, GetAccountInfoResponse.class); if (parsed.getUsers() == null || parsed.getUsers().isEmpty()) { throw new FirebaseAuthException(ErrorCode.NOT_FOUND, @@ -201,28 +232,103 @@ private UserRecord lookupUserAccount( return new UserRecord(parsed.getUsers().get(0), jsonFactory); } - 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"); - return sendRequest(HttpMethods.POST, baseUrl + path, 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); + } + + 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); } - private T sendRequest( - String method, String url, @Nullable Object content, Class clazz) + 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); + } - IncomingHttpResponse response = sendRequest(method, url, content); - return httpClient.parse(response, clazz); + SamlProviderConfig getSamlProviderConfig(String providerId) throws FirebaseAuthException { + GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + getSamlUrlSuffix(providerId)); + return httpClient.sendRequest("GET", url, null, SamlProviderConfig.class); } - private IncomingHttpResponse sendRequest( - String method, String url, @Nullable Object content) throws FirebaseAuthException { + 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); + } - HttpContent httpContent = content != null ? new JsonHttpContent(jsonFactory, content) : null; - HttpRequestInfo requestInfo = HttpRequestInfo.buildRequest(method, url, httpContent) - .addHeader(CLIENT_VERSION_HEADER, CLIENT_VERSION) - .setResponseInterceptor(interceptor); - return httpClient.send(requestInfo); + GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + "/oauthIdpConfigs"); + url.putAll(builder.build()); + return httpClient.sendRequest("GET", url, null, ListOidcProviderConfigsResponse.class); + } + + 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()); + return httpClient.sendRequest("GET", url, null, ListSamlProviderConfigsResponse.class); + } + + 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(HttpMethods.POST, url, content, clazz); } static class UserImportRequest extends GenericJson { @@ -264,4 +370,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/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/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/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/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/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/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/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..0af08f65b 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; @@ -49,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; @@ -65,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(); @@ -80,7 +83,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); } @@ -107,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. * @@ -190,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. */ @@ -247,6 +268,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 { @@ -357,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/AuthErrorHandler.java b/src/main/java/com/google/firebase/auth/internal/AuthErrorHandler.java similarity index 97% rename from src/main/java/com/google/firebase/auth/AuthErrorHandler.java rename to src/main/java/com/google/firebase/auth/internal/AuthErrorHandler.java index 6ed3f50ce..0fc5ef3ea 100644 --- a/src/main/java/com/google/firebase/auth/AuthErrorHandler.java +++ b/src/main/java/com/google/firebase/auth/internal/AuthErrorHandler.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.firebase.auth; +package com.google.firebase.auth.internal; import static com.google.common.base.Preconditions.checkNotNull; @@ -25,6 +25,8 @@ 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; 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..70f94955e --- /dev/null +++ b/src/main/java/com/google/firebase/auth/internal/AuthHttpClient.java @@ -0,0 +1,105 @@ +/* + * 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.HttpRequestFactory; +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.common.base.Strings; +import com.google.common.collect.ImmutableSortedSet; +import com.google.firebase.IncomingHttpResponse; +import com.google.firebase.auth.FirebaseAuthException; +import com.google.firebase.internal.ErrorHandlingHttpClient; +import com.google.firebase.internal.HttpRequestInfo; +import com.google.firebase.internal.Nullable; +import com.google.firebase.internal.SdkUtils; +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 { + + private static final String CLIENT_VERSION_HEADER = "X-Client-Version"; + + private static final String CLIENT_VERSION = "Java/Admin/" + SdkUtils.getVersion(); + + private final JsonFactory jsonFactory; + private final ErrorHandlingHttpClient httpClient; + + private HttpResponseInterceptor interceptor; + + public AuthHttpClient(JsonFactory jsonFactory, HttpRequestFactory requestFactory) { + this.jsonFactory = jsonFactory; + AuthErrorHandler authErrorHandler = new AuthErrorHandler(jsonFactory); + this.httpClient = new ErrorHandlingHttpClient<>(requestFactory, jsonFactory, authErrorHandler); + } + + 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 IncomingHttpResponse sendRequest( + String method, GenericUrl url, @Nullable Object content) throws FirebaseAuthException { + checkArgument(!Strings.isNullOrEmpty(method), "method must not be null or empty"); + checkNotNull(url, "url must not be null"); + String httpMethod = method.equals("PATCH") ? "POST" : method; + HttpContent httpContent = content != null ? new JsonHttpContent(jsonFactory, content) : null; + HttpRequestInfo request = HttpRequestInfo.buildRequest(httpMethod, url, httpContent) + .addHeader(CLIENT_VERSION_HEADER, CLIENT_VERSION) + .setResponseInterceptor(interceptor); + if (method.equals("PATCH")) { + request.addHeader("X-HTTP-Method-Override", "PATCH"); + } + + return httpClient.send(request); + } + + public T sendRequest( + String method, GenericUrl url, + @Nullable Object content, Class clazz) throws FirebaseAuthException { + + IncomingHttpResponse response = this.sendRequest(method, url, content); + return this.parse(response, clazz); + } + + 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/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/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 56be2015d..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,8 +25,11 @@ 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; import java.util.Collection; import java.util.Map; @@ -41,11 +44,19 @@ 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) { + 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; + } + + @VisibleForTesting + FirebaseTokenFactory(JsonFactory jsonFactory, Clock clock, CryptoSigner signer) { + this(jsonFactory, clock, signer, null); } String createSignedCustomAuthTokenForUser(String uid) throws FirebaseAuthException { @@ -68,6 +79,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/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..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; @@ -73,6 +76,9 @@ public static class User { @Key("lastLoginAt") private long lastLoginAt; + @Key("lastRefreshAt") + private String lastRefreshAt; + @Key("validSince") private long validSince; @@ -83,6 +89,10 @@ public String getUid() { return uid; } + public String getTenantId() { + return tenantId; + } + public String getEmail() { return email; } @@ -119,10 +129,14 @@ public long getLastLoginAt() { return lastLoginAt; } + public String getLastRefreshAt() { + return lastRefreshAt; + } + 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..1f6256cff --- /dev/null +++ b/src/main/java/com/google/firebase/auth/multitenancy/FirebaseTenantClient.java @@ -0,0 +1,106 @@ +/* + * 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()); + return httpClient.sendRequest("GET", url, null, ListTenantsResponse.class); + } + + 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/main/java/com/google/firebase/internal/HttpRequestInfo.java b/src/main/java/com/google/firebase/internal/HttpRequestInfo.java index 683ddcf73..e92a76b14 100644 --- a/src/main/java/com/google/firebase/internal/HttpRequestInfo.java +++ b/src/main/java/com/google/firebase/internal/HttpRequestInfo.java @@ -17,6 +17,7 @@ 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; @@ -41,11 +42,11 @@ public final class HttpRequestInfo { private final Map headers = new HashMap<>(); private HttpResponseInterceptor interceptor; - private HttpRequestInfo(String method, String url, HttpContent content) { + private HttpRequestInfo(String method, GenericUrl url, HttpContent content) { checkArgument(!Strings.isNullOrEmpty(method), "method must not be null"); - checkArgument(!Strings.isNullOrEmpty(url), "url must not be null"); + checkNotNull(url, "url must not be null"); this.method = method; - this.url = new GenericUrl(url); + this.url = url; this.content = content; } @@ -69,7 +70,7 @@ public static HttpRequestInfo buildGetRequest(String url) { } public static HttpRequestInfo buildDeleteRequest(String url) { - return new HttpRequestInfo(HttpMethods.DELETE, url, null); + return buildRequest(HttpMethods.DELETE, url, null); } public static HttpRequestInfo buildPostRequest(String url, HttpContent content) { @@ -77,6 +78,10 @@ public static HttpRequestInfo buildPostRequest(String url, HttpContent content) } public static HttpRequestInfo buildRequest(String method, String url, HttpContent content) { + return buildRequest(method, new GenericUrl(url), content); + } + + public static HttpRequestInfo buildRequest(String method, GenericUrl url, HttpContent content) { return new HttpRequestInfo(method, url, content); } diff --git a/src/main/java/com/google/firebase/messaging/AndroidConfig.java b/src/main/java/com/google/firebase/messaging/AndroidConfig.java index dc0a45526..888f00f0d 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() {} @@ -193,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) { @@ -201,6 +206,15 @@ public Builder setFcmOptions(AndroidFcmOptions androidFcmOptions) { return this; } + /** + * 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) { + this.directBootOk = directBootOk; + return this; + } + /** * Creates a new {@link AndroidConfig} instance from the parameters set on this builder. * 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 72471536b..e5bc94bfb 100644 --- a/src/main/java/com/google/firebase/messaging/FirebaseMessagingClientImpl.java +++ b/src/main/java/com/google/firebase/messaging/FirebaseMessagingClientImpl.java @@ -139,7 +139,7 @@ private BatchResponse sendBatchRequest( try { BatchRequest batch = newBatchRequest(messages, dryRun, callback); batch.execute(); - return new BatchResponse(callback.getResponses()); + return new BatchResponseImpl(callback.getResponses()); } catch (HttpResponseException e) { OutgoingHttpRequest req = new OutgoingHttpRequest(HttpMethods.POST, FCM_BATCH_URL); IncomingHttpResponse resp = new IncomingHttpResponse(e, req); 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/auth/FirebaseAuthIT.java b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java index c36013c7f..35fa21d4d 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java +++ b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java @@ -47,9 +47,11 @@ 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.internal.Nullable; import com.google.firebase.testing.IntegrationTestUtils; import java.io.IOException; import java.net.URLDecoder; @@ -57,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 { @@ -82,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 { @@ -129,7 +128,7 @@ 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); @@ -162,52 +161,120 @@ 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(); - 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()); @@ -220,20 +287,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()); @@ -252,8 +319,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()); @@ -263,130 +331,150 @@ 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); - FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); - assertEquals( - "No user record found for the provided user ID: " + userRecord.getUid(), - authException.getMessage()); - assertEquals(ErrorCode.NOT_FOUND, authException.getErrorCode()); - assertNull(authException.getCause()); - assertNotNull(authException.getHttpResponse()); - assertEquals(AuthErrorCode.USER_NOT_FOUND, authException.getAuthErrorCode()); + UserTestUtils.assertUserDoesNotExist(auth, userRecord.getUid()); + } + + @Test + public void testLastRefreshTime() throws Exception { + RandomUser user = UserTestUtils.generateRandomUserInfo(); + UserRecord newUserRecord = temporaryUser.create(new UserRecord.CreateRequest() + .setUid(user.getUid()) + .setEmail(user.getEmail()) + .setEmailVerified(false) + .setPassword("password")); + + // 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). 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; + } + + 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 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()); - @Override - public void onSuccess(ListUsersPage result) { - for (ExportedUserRecord user : result.iterateAll()) { - if (uids.contains(user.getUid())) { - collected.incrementAndGet(); - assertNotNull(user.getPasswordHash()); - assertNotNull(user.getPasswordSalt()); - } + // 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()); + 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 @@ -444,7 +532,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"); } @@ -498,32 +586,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(); @@ -539,88 +624,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 { @@ -638,22 +944,22 @@ private Map parseLinkParameters(String link) throws Exception { return result; } - private 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 { @@ -668,7 +974,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)); @@ -717,9 +1023,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); @@ -734,20 +1040,42 @@ private void checkRecreate(String uid) throws Exception { } } - private static class RandomUser { - private final String uid; - private 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 { + 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 = 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 46309b99d..bfbb77364 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java +++ b/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java @@ -499,12 +499,12 @@ private FirebaseAuth getAuthForIdTokenVerification( private FirebaseAuth getAuthForIdTokenVerification( FirebaseApp app, Supplier tokenVerifierSupplier) { - 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))); } FirebaseAuth getAuthForSessionCookieVerificationWithRevocationCheck( @@ -526,12 +526,12 @@ private FirebaseAuth getAuthForSessionCookieVerification( private FirebaseAuth getAuthForSessionCookieVerification( FirebaseApp app, Supplier tokenVerifierSupplier) { - 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 FirebaseApp getFirebaseAppForUserRetrieval() { diff --git a/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java b/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java index 99361e052..e895bc987 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java +++ b/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java @@ -30,6 +30,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.ErrorCode; import com.google.firebase.testing.ServiceAccount; import java.io.IOException; @@ -325,6 +326,50 @@ public void testMalformedToken() { } } + @Test + public void testVerifyTokenDifferentTenantIds() { + try { + fullyPopulatedBuilder() + .setTenantId("TENANT_1") + .build() + .verifyToken(createTokenWithTenantId("TENANT_2")); + } catch (FirebaseAuthException e) { + 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()); + } + } + + @Test + public void testVerifyTokenMissingTenantId() { + try { + fullyPopulatedBuilder() + .setTenantId("TENANT_ID") + .build() + .verifyToken(tokenFactory.createToken()); + } catch (FirebaseAuthException e) { + assertEquals(AuthErrorCode.TENANT_ID_MISMATCH, e.getAuthErrorCode()); + 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(AuthErrorCode.TENANT_ID_MISMATCH, e.getAuthErrorCode()); + 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(); @@ -374,15 +419,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()) - .setInvalidTokenErrorCode(AuthErrorCode.INVALID_ID_TOKEN) - .setExpiredTokenErrorCode(AuthErrorCode.EXPIRED_ID_TOKEN) .build(); } @@ -393,6 +431,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()); } @@ -460,4 +500,10 @@ private void checkException(FirebaseAuthException e, String message, AuthErrorCo assertNull(e.getHttpResponse()); assertEquals(errorCode, e.getAuthErrorCode()); } + + 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 af4577053..a71280b87 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; @@ -33,6 +35,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; @@ -42,8 +45,9 @@ import com.google.firebase.FirebaseOptions; import com.google.firebase.TestOnlyImplFirebaseTrampolines; import com.google.firebase.auth.FirebaseUserManager.EmailLinkType; -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; @@ -53,6 +57,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; import java.util.concurrent.ExecutionException; @@ -62,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) @@ -73,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(); @@ -115,7 +131,7 @@ 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(ErrorCode.NOT_FOUND, authException.getErrorCode()); assertEquals( @@ -143,7 +159,7 @@ 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(ErrorCode.NOT_FOUND, authException.getErrorCode()); assertEquals( @@ -172,7 +188,7 @@ 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(ErrorCode.NOT_FOUND, authException.getErrorCode()); assertEquals( @@ -184,6 +200,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( @@ -199,7 +362,7 @@ public void testListUsers() throws Exception { checkRequestHeaders(interceptor); GenericUrl url = interceptor.getResponse().getRequest().getUrl(); - assertEquals("999", url.getFirst("maxResults")); + assertEquals(999, url.getFirst("maxResults")); assertNull(url.getFirst("nextPageToken")); } @@ -218,7 +381,7 @@ public void testListUsersWithPageToken() throws Exception { checkRequestHeaders(interceptor); GenericUrl url = interceptor.getResponse().getRequest().getUrl(); - assertEquals("999", url.getFirst("maxResults")); + assertEquals(999, url.getFirst("maxResults")); assertEquals("token", url.getFirst("nextPageToken")); } @@ -236,7 +399,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); } @@ -247,7 +411,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); } @@ -262,12 +426,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 @@ -278,10 +439,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")); } @@ -295,6 +453,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("{}"); @@ -308,14 +541,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")); } @@ -348,15 +578,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")); } @@ -386,14 +613,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")); @@ -459,10 +683,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")); @@ -544,13 +765,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() { @@ -586,7 +807,7 @@ 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(); assertEquals(codes.get(code), authException.getErrorCode()); String msg = String.format("Unexpected HTTP response with status: %d\n{}", code); @@ -606,7 +827,7 @@ 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(ErrorCode.NOT_FOUND, authException.getErrorCode()); assertEquals( @@ -646,7 +867,7 @@ 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(); assertEquals(ErrorCode.UNKNOWN, authException.getErrorCode()); assertTrue( @@ -667,7 +888,7 @@ 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(); assertEquals(ErrorCode.INTERNAL, authException.getErrorCode()); assertEquals("Unexpected HTTP response with status: 500\n{\"not\" json}", @@ -702,13 +923,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") @@ -729,7 +950,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"); @@ -754,7 +975,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"); @@ -765,7 +986,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"); @@ -790,7 +1011,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"); @@ -815,7 +1036,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"); @@ -840,7 +1061,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"); @@ -858,7 +1079,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") @@ -868,7 +1089,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")); @@ -877,12 +1098,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 { @@ -895,10 +1116,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")); @@ -906,10 +1127,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")); @@ -917,31 +1138,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"); @@ -959,7 +1180,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"); @@ -984,7 +1205,7 @@ public void testInvalidUpdateEmail() { @Test public void testInvalidUpdatePhoneNumber() { - UpdateRequest update = new UpdateRequest("test"); + UserRecord.UpdateRequest update = new UserRecord.UpdateRequest("test"); try { update.setPhoneNumber(""); @@ -1003,7 +1224,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"); @@ -1021,7 +1242,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")); @@ -1038,10 +1259,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 @@ -1073,10 +1294,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")); @@ -1095,10 +1313,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")); @@ -1130,10 +1345,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")); @@ -1152,10 +1364,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")); @@ -1200,10 +1409,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")); @@ -1273,88 +1479,1304 @@ public void testUnexpectedHttpError() { } } - private static TestResponseInterceptor initializeAppForUserManagement(String ...responses) { - List mocks = new ArrayList<>(); - for (String response : responses) { - mocks.add(new MockLowLevelHttpResponse().setContent(response)); + @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(ErrorCode.NOT_FOUND, e.getErrorCode()); } - MockHttpTransport transport = new MultiRequestMockHttpTransport(mocks); - FirebaseApp.initializeApp(FirebaseOptions.builder() - .setCredentials(credentials) - .setHttpTransport(transport) - .setProjectId("test-project-id") - .build()); - FirebaseAuth auth = FirebaseAuth.getInstance(); - FirebaseUserManager userManager = auth.getUserManager(); - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - userManager.setInterceptor(interceptor); - return interceptor; + + checkUrl(interceptor, "POST", PROJECT_BASE_URL + "/oauthIdpConfigs"); } - private static FirebaseAuth getRetryDisabledAuth(MockLowLevelHttpResponse response) { - final MockHttpTransport transport = new MockHttpTransport.Builder() - .setLowLevelHttpResponse(response) - .build(); - final FirebaseApp app = FirebaseApp.initializeApp(FirebaseOptions.builder() - .setCredentials(credentials) - .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(); + @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. + } } - private static void checkUserRecord(UserRecord userRecord) { - assertEquals("testuser", userRecord.getUid()); - assertEquals("testuser@example.com", userRecord.getEmail()); - assertEquals("+1234567890", userRecord.getPhoneNumber()); - assertEquals("Test User", userRecord.getDisplayName()); - assertEquals("http://www.example.com/testuser/photo.png", userRecord.getPhotoUrl()); - assertEquals(1234567890, userRecord.getUserMetadata().getCreationTimestamp()); - assertEquals(0, userRecord.getUserMetadata().getLastSignInTimestamp()); - assertEquals(2, userRecord.getProviderData().length); - assertFalse(userRecord.isDisabled()); - assertTrue(userRecord.isEmailVerified()); - assertEquals(1494364393000L, userRecord.getTokensValidAfterTimestamp()); + @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); - UserInfo provider = userRecord.getProviderData()[0]; - assertEquals("testuser@example.com", provider.getUid()); - assertEquals("testuser@example.com", provider.getEmail()); - assertNull(provider.getPhoneNumber()); - assertEquals("Test User", provider.getDisplayName()); - assertEquals("http://www.example.com/testuser/photo.png", provider.getPhotoUrl()); - assertEquals("password", provider.getProviderId()); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "POST", TENANTS_BASE_URL + "/TENANT_ID/oauthIdpConfigs"); + } - provider = userRecord.getProviderData()[1]; - assertEquals("+1234567890", provider.getUid()); - assertNull(provider.getEmail()); - assertEquals("+1234567890", provider.getPhoneNumber()); - assertEquals("phone", provider.getProviderId()); + @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"); - Map claims = userRecord.getCustomClaims(); - assertEquals(2, claims.size()); - assertTrue((boolean) claims.get("admin")); - assertEquals("gold", claims.get("package")); + 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")); } - private static void checkRequestHeaders(TestResponseInterceptor interceptor) { - HttpHeaders headers = interceptor.getResponse().getRequest().getHeaders(); - String auth = "Bearer " + TEST_TOKEN; - assertEquals(auth, headers.getFirstHeaderStringValue("Authorization")); + @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")); + } - String clientVersion = "Java/Admin/" + SdkUtils.getVersion(); - assertEquals(clientVersion, headers.getFirstHeaderStringValue("X-Client-Version")); + @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")); } - private interface UserManagerOp { - void call(FirebaseAuth auth) throws Exception; + @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(ErrorCode.NOT_FOUND, 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(ErrorCode.NOT_FOUND, 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(ErrorCode.NOT_FOUND, 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(ErrorCode.NOT_FOUND, e.getErrorCode()); + } + checkUrl(interceptor, "DELETE", PROJECT_BASE_URL + "/oauthIdpConfigs/oidc.UNKNOWN"); + } + + @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(ErrorCode.NOT_FOUND, 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(ErrorCode.NOT_FOUND, 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(ErrorCode.NOT_FOUND, 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(ErrorCode.NOT_FOUND, 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(ErrorCode.NOT_FOUND, 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)); + } + 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); + } + + private static FirebaseAuth getRetryDisabledAuth(MockLowLevelHttpResponse response) { + final MockHttpTransport transport = new MockHttpTransport.Builder() + .setLowLevelHttpResponse(response) + .build(); + final FirebaseApp app = FirebaseApp.initializeApp(new FirebaseOptions.Builder() + .setCredentials(credentials) + .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(); + } + })); + } + + private static void checkUserRecord(UserRecord userRecord) { + assertEquals("testuser", userRecord.getUid()); + assertEquals("testuser@example.com", userRecord.getEmail()); + assertEquals("+1234567890", userRecord.getPhoneNumber()); + assertEquals("Test User", userRecord.getDisplayName()); + assertEquals("http://www.example.com/testuser/photo.png", userRecord.getPhotoUrl()); + assertEquals(1234567890, userRecord.getUserMetadata().getCreationTimestamp()); + assertEquals(0, userRecord.getUserMetadata().getLastSignInTimestamp()); + assertEquals(2, userRecord.getProviderData().length); + 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()); + assertEquals("testuser@example.com", provider.getEmail()); + assertNull(provider.getPhoneNumber()); + assertEquals("Test User", provider.getDisplayName()); + assertEquals("http://www.example.com/testuser/photo.png", provider.getPhotoUrl()); + assertEquals("password", provider.getProviderId()); + + provider = userRecord.getProviderData()[1]; + assertEquals("+1234567890", provider.getUid()); + assertNull(provider.getEmail()); + assertEquals("+1234567890", provider.getPhoneNumber()); + assertEquals("phone", provider.getProviderId()); + + Map claims = userRecord.getCustomClaims(); + assertEquals(2, claims.size()); + assertTrue((boolean) claims.get("admin")); + 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; + 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 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 new file mode 100644 index 000000000..a0bdcb6c6 --- /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.auth.UserTestUtils.RandomUser; +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); + + RandomUser randomUser = UserTestUtils.generateRandomUserInfo(); + importUserUid = randomUser.getUid(); + UserImportResult result = auth.importUsers(ImmutableList.of( + ImportUserRecord.builder() + .setUid(randomUser.getUid()) + .setEmail(randomUser.getEmail()) + .setPhoneNumber(randomUser.getPhoneNumber()) + .addUserProvider( + UserProvider.builder() + .setProviderId("google.com") + .setUid("google_" + randomUser.getUid()) + .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()) 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..588ce3317 --- /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.ErrorCode; +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(ErrorCode.NOT_FOUND, + ((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(ErrorCode.NOT_FOUND, + ((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..8d6f7fc32 --- /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.ErrorCode; +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(ErrorCode.NOT_FOUND, + ((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 7c75f64a1..875c781e9 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; @@ -46,6 +47,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); @@ -72,6 +74,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); @@ -81,6 +84,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..86eb6b4a8 --- /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.ErrorCode; +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.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(ErrorCode.NOT_FOUND, 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(ErrorCode.NOT_FOUND, 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(ErrorCode.NOT_FOUND, 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(ErrorCode.NOT_FOUND, 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..fd8e4af83 --- /dev/null +++ b/src/test/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuthIT.java @@ -0,0 +1,451 @@ +/* + * 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.AuthErrorCode; +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(AuthErrorCode.TENANT_ID_MISMATCH, + ((FirebaseAuthException) e.getCause()).getAuthErrorCode()); + } + } + + @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..1949dc5a2 --- /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.ErrorCode; +import com.google.firebase.auth.FirebaseAuth; +import com.google.firebase.auth.FirebaseAuthException; +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(ErrorCode.NOT_FOUND, + ((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 c9c8decd7..2bdc6185e 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 = 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/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"); diff --git a/src/test/java/com/google/firebase/messaging/BatchResponseTest.java b/src/test/java/com/google/firebase/messaging/BatchResponseTest.java index 0dd2a61c0..441822220 100644 --- a/src/test/java/com/google/firebase/messaging/BatchResponseTest.java +++ b/src/test/java/com/google/firebase/messaging/BatchResponseTest.java @@ -32,7 +32,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()); @@ -48,7 +48,7 @@ public void testSomeResponse() { new FirebaseMessagingException(ErrorCode.INTERNAL, "error-message")) ); - BatchResponse batchResponse = new BatchResponse(responses); + BatchResponse batchResponse = new BatchResponseImpl(responses); assertEquals(2, batchResponse.getSuccessCount()); assertEquals(1, batchResponse.getFailureCount()); @@ -62,7 +62,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 { @@ -75,6 +75,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 3e76285b9..8e8e79e07 100644 --- a/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java +++ b/src/test/java/com/google/firebase/messaging/FirebaseMessagingTest.java @@ -669,7 +669,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 { diff --git a/src/test/java/com/google/firebase/messaging/MessageTest.java b/src/test/java/com/google/firebase/messaging/MessageTest.java index 65caae293..90b867d5f 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(); diff --git a/src/test/java/com/google/firebase/messaging/MulticastMessageTest.java b/src/test/java/com/google/firebase/messaging/MulticastMessageTest.java index 83b751c64..f26d0816f 100644 --- a/src/test/java/com/google/firebase/messaging/MulticastMessageTest.java +++ b/src/test/java/com/google/firebase/messaging/MulticastMessageTest.java @@ -42,6 +42,7 @@ public class MulticastMessageTest { .setTitle("title") .setBody("body") .build(); + private static final FcmOptions FCM_OPTIONS = FcmOptions.withAnalyticsLabel("analytics_label"); @Test public void testMulticastMessage() { @@ -50,6 +51,7 @@ public void testMulticastMessage() { .setApnsConfig(APNS) .setWebpushConfig(WEBPUSH) .setNotification(NOTIFICATION) + .setFcmOptions(FCM_OPTIONS) .putData("key1", "value1") .putAllData(ImmutableMap.of("key2", "value2")) .addToken("token1") @@ -99,7 +101,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 +} 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 +}