diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 11012bfff..c346c5b9d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -141,8 +141,17 @@ Authentication Admin` role at [Google Cloud Platform Console / IAM & admin](https://console.cloud.google.com/iam-admin). This is required to ensure that exported user records contain the password hashes of the user accounts. Also obtain the web API key of the project from the "Settings > General" page, and save it as -`integration_apikey.txt` at the root of the codebase. Now run the following command to invoke the -integration test suite: +`integration_apikey.txt` at the root of the codebase. + +Some of the integration tests require an +[Identity Platform](https://cloud.google.com/identity-platform/) project with multi-tenancy +[enabled](https://cloud.google.com/identity-platform/docs/multi-tenancy-quickstart#enabling_multi-tenancy). +An existing Firebase project can be upgraded to an Identity Platform project without losing any +functionality via the +[Identity Platform Marketplace Page](https://console.cloud.google.com/customer-identity). Note that +charges may be incurred for active users beyond the Identity Platform free tier. + +Now run the following command to invoke the integration test suite: ``` mvn verify @@ -153,14 +162,14 @@ tests, specify the `-DskipUTs` flag. ### Generating API Docs -Invoke the [Maven Javadoc plugin](https://maven.apache.org/plugins/maven-javadoc-plugin/) as +Invoke the [Maven Javadoc plugin](https://maven.apache.org/plugins/maven-javadoc-plugin/) as follows to generate API docs for all packages in the codebase: ``` mvn javadoc:javadoc ``` -This will generate the API docs, and place them in the `target/site/apidocs` directory. +This will generate the API docs, and place them in the `target/site/apidocs` directory. To generate API docs for only the public APIs (i.e. ones that are not marked with `@hide` tags), you need to trigger the `devsite-apidocs` Maven profile. This profile uses Maven Javadoc plugin diff --git a/checkstyle.xml b/checkstyle.xml index 663918173..77e2dba54 100644 --- a/checkstyle.xml +++ b/checkstyle.xml @@ -42,15 +42,15 @@ - + - - + + @@ -229,6 +229,9 @@ + + + diff --git a/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java b/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java new file mode 100644 index 000000000..ad30d2cc3 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java @@ -0,0 +1,1723 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.auth; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +import com.google.api.client.json.JsonFactory; +import com.google.api.client.util.Clock; +import com.google.api.core.ApiFuture; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Strings; +import com.google.common.base.Supplier; +import com.google.common.base.Suppliers; +import com.google.firebase.FirebaseApp; +import com.google.firebase.auth.FirebaseUserManager.EmailLinkType; +import com.google.firebase.auth.FirebaseUserManager.UserImportRequest; +import com.google.firebase.auth.ListProviderConfigsPage.DefaultOidcProviderConfigSource; +import com.google.firebase.auth.ListProviderConfigsPage.DefaultSamlProviderConfigSource; +import com.google.firebase.auth.ListUsersPage.DefaultUserSource; +import com.google.firebase.auth.internal.FirebaseTokenFactory; +import com.google.firebase.internal.CallableOperation; +import com.google.firebase.internal.NonNull; +import com.google.firebase.internal.Nullable; +import java.io.IOException; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * This is the abstract class for server-side Firebase Authentication actions. + */ +public abstract class AbstractFirebaseAuth { + + private static final String ERROR_CUSTOM_TOKEN = "ERROR_CUSTOM_TOKEN"; + + private final Object lock = new Object(); + private final AtomicBoolean destroyed = new AtomicBoolean(false); + + private final FirebaseApp firebaseApp; + private final Supplier tokenFactory; + private final Supplier idTokenVerifier; + private final Supplier cookieVerifier; + private final Supplier userManager; + private final JsonFactory jsonFactory; + + protected AbstractFirebaseAuth(Builder builder) { + this.firebaseApp = checkNotNull(builder.firebaseApp); + this.tokenFactory = threadSafeMemoize(builder.tokenFactory); + this.idTokenVerifier = threadSafeMemoize(builder.idTokenVerifier); + this.cookieVerifier = threadSafeMemoize(builder.cookieVerifier); + this.userManager = threadSafeMemoize(builder.userManager); + this.jsonFactory = builder.firebaseApp.getOptions().getJsonFactory(); + } + + protected static Builder builderFromAppAndTenantId(final FirebaseApp app, final String tenantId) { + return AbstractFirebaseAuth.builder() + .setFirebaseApp(app) + .setTokenFactory( + new Supplier() { + @Override + public FirebaseTokenFactory get() { + return FirebaseTokenUtils.createTokenFactory(app, Clock.SYSTEM, tenantId); + } + }) + .setIdTokenVerifier( + new Supplier() { + @Override + public FirebaseTokenVerifier get() { + return FirebaseTokenUtils.createIdTokenVerifier(app, Clock.SYSTEM, tenantId); + } + }) + .setCookieVerifier( + new Supplier() { + @Override + public FirebaseTokenVerifier get() { + return FirebaseTokenUtils.createSessionCookieVerifier(app, Clock.SYSTEM); + } + }) + .setUserManager( + new Supplier() { + @Override + public FirebaseUserManager get() { + return FirebaseUserManager + .builder() + .setFirebaseApp(app) + .setTenantId(tenantId) + .build(); + } + }); + } + + /** + * Creates a Firebase custom token for the given UID. This token can then be sent back to a client + * application to be used with the signInWithCustomToken + * authentication API. + * + *

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

This method attempts to generate a token using: - *

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Performs argument validation before attempting to load any provider config data (which is + * expensive, and hence may be performed asynchronously on a separate thread). + */ + static class Factory { + + private final ProviderConfigSource source; + private final int maxResults; + private final String pageToken; + + Factory(@NonNull ProviderConfigSource source) { + this(source, FirebaseUserManager.MAX_LIST_PROVIDER_CONFIGS_RESULTS, null); + } + + Factory( + @NonNull ProviderConfigSource source, + int maxResults, + @Nullable String pageToken) { + checkArgument( + maxResults > 0 && maxResults <= FirebaseUserManager.MAX_LIST_PROVIDER_CONFIGS_RESULTS, + "maxResults must be a positive integer that does not exceed %s", + FirebaseUserManager.MAX_LIST_PROVIDER_CONFIGS_RESULTS); + checkArgument(!END_OF_LIST.equals(pageToken), "invalid end of list page token"); + this.source = checkNotNull(source, "source must not be null"); + this.maxResults = maxResults; + this.pageToken = pageToken; + } + + ListProviderConfigsPage create() throws FirebaseAuthException { + ListProviderConfigsResponse batch = source.fetch(maxResults, pageToken); + return new ListProviderConfigsPage(batch, source, maxResults); + } + } +} + diff --git a/src/main/java/com/google/firebase/auth/ListUsersPage.java b/src/main/java/com/google/firebase/auth/ListUsersPage.java index f406366ba..ba727af5a 100644 --- a/src/main/java/com/google/firebase/auth/ListUsersPage.java +++ b/src/main/java/com/google/firebase/auth/ListUsersPage.java @@ -80,7 +80,7 @@ public String getNextPageToken() { @Override public ListUsersPage getNextPage() { if (hasNextPage()) { - PageFactory factory = new PageFactory(source, maxResults, currentBatch.getNextPageToken()); + Factory factory = new Factory(source, maxResults, currentBatch.getNextPageToken()); try { return factory.create(); } catch (FirebaseAuthException e) { @@ -237,17 +237,17 @@ String getNextPageToken() { * before attempting to load any user data (which is expensive, and hence may be performed * asynchronously on a separate thread). */ - static class PageFactory { + static class Factory { private final UserSource source; private final int maxResults; private final String pageToken; - PageFactory(@NonNull UserSource source) { + Factory(@NonNull UserSource source) { this(source, FirebaseUserManager.MAX_LIST_USERS_RESULTS, null); } - PageFactory(@NonNull UserSource source, int maxResults, @Nullable String pageToken) { + Factory(@NonNull UserSource source, int maxResults, @Nullable String pageToken) { checkArgument(maxResults > 0 && maxResults <= FirebaseUserManager.MAX_LIST_USERS_RESULTS, "maxResults must be a positive integer that does not exceed %s", FirebaseUserManager.MAX_LIST_USERS_RESULTS); diff --git a/src/main/java/com/google/firebase/auth/OidcProviderConfig.java b/src/main/java/com/google/firebase/auth/OidcProviderConfig.java new file mode 100644 index 000000000..879b7e79f --- /dev/null +++ b/src/main/java/com/google/firebase/auth/OidcProviderConfig.java @@ -0,0 +1,177 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.auth; + +import static com.google.common.base.Preconditions.checkArgument; + +import com.google.api.client.util.Key; +import com.google.common.base.Strings; + +/** + * Contains metadata associated with an OIDC Auth provider. + * + *

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

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

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

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

The returned object should be passed to + * {@link AbstractFirebaseAuth#updateOidcProviderConfig(CreateRequest)} to save the updated + * config. + * + * @param providerId A non-null, non-empty provider ID string. + * @throws IllegalArgumentException If the provider ID is null or empty, or is not prefixed with + * "oidc.". + */ + public UpdateRequest(String providerId) { + super(providerId); + checkOidcProviderId(providerId); + } + + /** + * Sets the client ID for the exsting provider. + * + * @param clientId A non-null, non-empty client ID string. + * @throws IllegalArgumentException If the client ID is null or empty. + */ + public UpdateRequest setClientId(String clientId) { + checkArgument(!Strings.isNullOrEmpty(clientId), "Client ID must not be null or empty."); + properties.put("clientId", clientId); + return this; + } + + /** + * Sets the issuer for the existing provider. + * + * @param issuer A non-null, non-empty issuer URL string. + * @throws IllegalArgumentException If the issuer URL is null or empty, or if the format is + * invalid. + */ + public UpdateRequest setIssuer(String issuer) { + checkArgument(!Strings.isNullOrEmpty(issuer), "Issuer must not be null or empty."); + assertValidUrl(issuer); + properties.put("issuer", issuer); + return this; + } + + UpdateRequest getThis() { + return this; + } + } +} diff --git a/src/main/java/com/google/firebase/auth/ProviderConfig.java b/src/main/java/com/google/firebase/auth/ProviderConfig.java new file mode 100644 index 000000000..921a07b5b --- /dev/null +++ b/src/main/java/com/google/firebase/auth/ProviderConfig.java @@ -0,0 +1,157 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.auth; + +import static com.google.common.base.Preconditions.checkArgument; + +import com.google.api.client.util.Key; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableMap; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; + +/** + * The base class for Auth providers. + */ +public abstract class ProviderConfig { + + @Key("name") + private String resourceName; + + @Key("displayName") + private String displayName; + + @Key("enabled") + private boolean enabled; + + public String getProviderId() { + return resourceName.substring(resourceName.lastIndexOf("/") + 1); + } + + public String getDisplayName() { + return displayName; + } + + public boolean isEnabled() { + return enabled; + } + + static void assertValidUrl(String url) throws IllegalArgumentException { + try { + new URL(url); + } catch (MalformedURLException e) { + throw new IllegalArgumentException(url + " is a malformed URL.", e); + } + } + + /** + * A base specification class for creating a new provider. + * + *

Set the initial attributes of the new provider by calling various setter methods available + * in this class. + */ + public abstract static class AbstractCreateRequest> { + + final Map properties = new HashMap<>(); + String providerId; + + T setProviderId(String providerId) { + this.providerId = providerId; + return getThis(); + } + + String getProviderId() { + return providerId; + } + + /** + * Sets the display name for the new provider. + * + * @param displayName A non-null, non-empty display name string. + * @throws IllegalArgumentException If the display name is null or empty. + */ + public T setDisplayName(String displayName) { + checkArgument(!Strings.isNullOrEmpty(displayName), "Display name must not be null or empty."); + properties.put("displayName", displayName); + return getThis(); + } + + /** + * Sets whether to allow the user to sign in with the provider. + * + * @param enabled A boolean indicating whether the user can sign in with the provider. + */ + public T setEnabled(boolean enabled) { + properties.put("enabled", enabled); + return getThis(); + } + + Map getProperties() { + return ImmutableMap.copyOf(properties); + } + + abstract T getThis(); + } + + /** + * A base class for updating the attributes of an existing provider. + */ + public abstract static class AbstractUpdateRequest> { + + final String providerId; + final Map properties = new HashMap<>(); + + AbstractUpdateRequest(String providerId) { + checkArgument(!Strings.isNullOrEmpty(providerId), "Provider ID must not be null or empty."); + this.providerId = providerId; + } + + String getProviderId() { + return providerId; + } + + /** + * Sets the display name for the existing provider. + * + * @param displayName A non-null, non-empty display name string. + * @throws IllegalArgumentException If the display name is null or empty. + */ + public T setDisplayName(String displayName) { + checkArgument(!Strings.isNullOrEmpty(displayName), "Display name must not be null or empty."); + properties.put("displayName", displayName); + return getThis(); + } + + /** + * Sets whether to allow the user to sign in with the provider. + * + * @param enabled A boolean indicating whether the user can sign in with the provider. + */ + public T setEnabled(boolean enabled) { + properties.put("enabled", enabled); + return getThis(); + } + + Map getProperties() { + return ImmutableMap.copyOf(properties); + } + + abstract T getThis(); + } +} diff --git a/src/main/java/com/google/firebase/auth/SamlProviderConfig.java b/src/main/java/com/google/firebase/auth/SamlProviderConfig.java new file mode 100644 index 000000000..76f8594a8 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/SamlProviderConfig.java @@ -0,0 +1,343 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.auth; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.api.client.json.GenericJson; +import com.google.api.client.util.Key; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Contains metadata associated with a SAML Auth provider. + * + *

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

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

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

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

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

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

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

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

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

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

Performs argument validation before attempting to load any tenant data (which is expensive, + * and hence may be performed asynchronously on a separate thread). + */ + static class PageFactory { + + private final TenantSource source; + private final int maxResults; + private final String pageToken; + + PageFactory(@NonNull TenantSource source) { + this(source, FirebaseTenantClient.MAX_LIST_TENANTS_RESULTS, null); + } + + PageFactory(@NonNull TenantSource source, int maxResults, @Nullable String pageToken) { + checkArgument(maxResults > 0 && maxResults <= FirebaseTenantClient.MAX_LIST_TENANTS_RESULTS, + "maxResults must be a positive integer that does not exceed %s", + FirebaseTenantClient.MAX_LIST_TENANTS_RESULTS); + checkArgument(!END_OF_LIST.equals(pageToken), "Invalid end of list page token."); + this.source = checkNotNull(source, "Source must not be null."); + this.maxResults = maxResults; + this.pageToken = pageToken; + } + + ListTenantsPage create() throws FirebaseAuthException { + ListTenantsResponse batch = source.fetch(maxResults, pageToken); + return new ListTenantsPage(batch, source, maxResults); + } + } +} + diff --git a/src/main/java/com/google/firebase/auth/multitenancy/Tenant.java b/src/main/java/com/google/firebase/auth/multitenancy/Tenant.java new file mode 100644 index 000000000..57d215e96 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/multitenancy/Tenant.java @@ -0,0 +1,195 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.auth.multitenancy; + +import static com.google.common.base.Preconditions.checkArgument; + +import com.google.api.client.util.Key; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableMap; +import java.util.HashMap; +import java.util.Map; + +/** + * Contains metadata associated with a Firebase tenant. + * + *

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

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

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

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

This method allows updating attributes of a tenant account, without first having to call + * {@link TenantManager#getTenant(String)}. + * + * @param tenantId a non-null, non-empty tenant ID string. + * @throws IllegalArgumentException If the tenant ID is null or empty. + */ + public UpdateRequest(String tenantId) { + checkArgument(!Strings.isNullOrEmpty(tenantId), "tenant ID must not be null or empty"); + this.tenantId = tenantId; + } + + String getTenantId() { + return tenantId; + } + + /** + * Sets the display name of the existing tenant. + * + * @param displayName a non-null, non-empty display name string. + */ + public UpdateRequest setDisplayName(String displayName) { + checkArgument(!Strings.isNullOrEmpty(displayName), "display name must not be null or empty"); + properties.put("displayName", displayName); + return this; + } + + /** + * Sets whether to allow email/password user authentication. + * + * @param passwordSignInAllowed a boolean indicating whether users can be authenticated using + * an email and password. + */ + public UpdateRequest setPasswordSignInAllowed(boolean passwordSignInAllowed) { + properties.put("allowPasswordSignup", passwordSignInAllowed); + return this; + } + + /** + * Sets whether to enable email link user authentication. + * + * @param emailLinkSignInEnabled a boolean indicating whether users can be authenticated using + * an email link. + */ + public UpdateRequest setEmailLinkSignInEnabled(boolean emailLinkSignInEnabled) { + properties.put("enableEmailLinkSignin", emailLinkSignInEnabled); + return this; + } + + Map getProperties() { + return ImmutableMap.copyOf(properties); + } + } +} diff --git a/src/main/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuth.java b/src/main/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuth.java new file mode 100644 index 000000000..540437404 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuth.java @@ -0,0 +1,50 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.auth.multitenancy; + +import static com.google.common.base.Preconditions.checkArgument; + +import com.google.common.base.Strings; +import com.google.firebase.FirebaseApp; +import com.google.firebase.auth.AbstractFirebaseAuth; + +/** + * The tenant-aware Firebase client. + * + *

This can be used to perform a variety of authentication-related operations, scoped to a + * particular tenant. + */ +public final class TenantAwareFirebaseAuth extends AbstractFirebaseAuth { + + private final String tenantId; + + TenantAwareFirebaseAuth(final FirebaseApp firebaseApp, final String tenantId) { + super(builderFromAppAndTenantId(firebaseApp, tenantId)); + checkArgument(!Strings.isNullOrEmpty(tenantId)); + this.tenantId = tenantId; + } + + /** Returns the client's tenant ID. */ + public String getTenantId() { + return tenantId; + } + + @Override + protected void doDestroy() { + // Nothing extra needs to be destroyed. + } +} diff --git a/src/main/java/com/google/firebase/auth/multitenancy/TenantManager.java b/src/main/java/com/google/firebase/auth/multitenancy/TenantManager.java new file mode 100644 index 000000000..11f26b096 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/multitenancy/TenantManager.java @@ -0,0 +1,287 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.auth.multitenancy; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.api.client.http.HttpResponseInterceptor; +import com.google.api.core.ApiFuture; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Strings; +import com.google.firebase.FirebaseApp; +import com.google.firebase.auth.FirebaseAuth; +import com.google.firebase.auth.FirebaseAuthException; +import com.google.firebase.auth.multitenancy.ListTenantsPage.DefaultTenantSource; +import com.google.firebase.auth.multitenancy.ListTenantsPage.PageFactory; +import com.google.firebase.auth.multitenancy.ListTenantsPage.TenantSource; +import com.google.firebase.auth.multitenancy.Tenant.CreateRequest; +import com.google.firebase.auth.multitenancy.Tenant.UpdateRequest; +import com.google.firebase.internal.CallableOperation; +import com.google.firebase.internal.NonNull; +import com.google.firebase.internal.Nullable; +import java.util.HashMap; +import java.util.Map; + +/** + * This class can be used to perform a variety of tenant-related operations, including creating, + * updating, and listing tenants. + */ +public final class TenantManager { + + private final FirebaseApp firebaseApp; + private final FirebaseTenantClient tenantClient; + private final Map tenantAwareAuths; + + /** + * Creates a new {@link TenantManager} instance. For internal use only. Use + * {@link FirebaseAuth#getTenantManager()} to obtain an instance for regular use. + * + * @hide + */ + public TenantManager(FirebaseApp firebaseApp) { + this.firebaseApp = firebaseApp; + this.tenantClient = new FirebaseTenantClient(firebaseApp); + this.tenantAwareAuths = new HashMap<>(); + } + + @VisibleForTesting + void setInterceptor(HttpResponseInterceptor interceptor) { + this.tenantClient.setInterceptor(interceptor); + } + + /** + * Gets the tenant corresponding to the specified tenant ID. + * + * @param tenantId A tenant ID string. + * @return A {@link Tenant} instance. + * @throws IllegalArgumentException If the tenant ID string is null or empty. + * @throws FirebaseAuthException If an error occurs while retrieving user data. + */ + public Tenant getTenant(@NonNull String tenantId) throws FirebaseAuthException { + return getTenantOp(tenantId).call(); + } + + public synchronized TenantAwareFirebaseAuth getAuthForTenant(@NonNull String tenantId) { + checkArgument(!Strings.isNullOrEmpty(tenantId), "Tenant ID must not be null or empty."); + if (!tenantAwareAuths.containsKey(tenantId)) { + tenantAwareAuths.put(tenantId, new TenantAwareFirebaseAuth(firebaseApp, tenantId)); + } + return tenantAwareAuths.get(tenantId); + } + + /** + * Similar to {@link #getTenant(String)} but performs the operation asynchronously. + * + * @param tenantId A tenantId string. + * @return An {@code ApiFuture} which will complete successfully with a {@link Tenant} instance + * If an error occurs while retrieving tenant data or if the specified tenant ID does not + * exist, the future throws a {@link FirebaseAuthException}. + * @throws IllegalArgumentException If the tenant ID string is null or empty. + */ + public ApiFuture getTenantAsync(@NonNull String tenantId) { + return getTenantOp(tenantId).callAsync(firebaseApp); + } + + private CallableOperation getTenantOp(final String tenantId) { + checkArgument(!Strings.isNullOrEmpty(tenantId), "Tenant ID must not be null or empty."); + return new CallableOperation() { + @Override + protected Tenant execute() throws FirebaseAuthException { + return tenantClient.getTenant(tenantId); + } + }; + } + + /** + * Gets a page of tenants starting from the specified {@code pageToken}. Page size will be limited + * to 1000 tenants. + * + * @param pageToken A non-empty page token string, or null to retrieve the first page of tenants. + * @return A {@link ListTenantsPage} instance. + * @throws IllegalArgumentException If the specified page token is empty. + * @throws FirebaseAuthException If an error occurs while retrieving tenant data. + */ + public ListTenantsPage listTenants(@Nullable String pageToken) throws FirebaseAuthException { + return listTenants(pageToken, FirebaseTenantClient.MAX_LIST_TENANTS_RESULTS); + } + + /** + * Gets a page of tenants starting from the specified {@code pageToken}. + * + * @param pageToken A non-empty page token string, or null to retrieve the first page of tenants. + * @param maxResults Maximum number of tenants to include in the returned page. This may not + * exceed 1000. + * @return A {@link ListTenantsPage} instance. + * @throws IllegalArgumentException If the specified page token is empty, or max results value is + * invalid. + * @throws FirebaseAuthException If an error occurs while retrieving tenant data. + */ + public ListTenantsPage listTenants(@Nullable String pageToken, int maxResults) + throws FirebaseAuthException { + return listTenantsOp(pageToken, maxResults).call(); + } + + /** + * Similar to {@link #listTenants(String)} but performs the operation asynchronously. + * + * @param pageToken A non-empty page token string, or null to retrieve the first page of tenants. + * @return An {@code ApiFuture} which will complete successfully with a {@link ListTenantsPage} + * instance. If an error occurs while retrieving tenant data, the future throws an exception. + * @throws IllegalArgumentException If the specified page token is empty. + */ + public ApiFuture listTenantsAsync(@Nullable String pageToken) { + return listTenantsAsync(pageToken, FirebaseTenantClient.MAX_LIST_TENANTS_RESULTS); + } + + /** + * Similar to {@link #listTenants(String, int)} but performs the operation asynchronously. + * + * @param pageToken A non-empty page token string, or null to retrieve the first page of tenants. + * @param maxResults Maximum number of tenants to include in the returned page. This may not + * exceed 1000. + * @return An {@code ApiFuture} which will complete successfully with a {@link ListTenantsPage} + * instance. If an error occurs while retrieving tenant data, the future throws an exception. + * @throws IllegalArgumentException If the specified page token is empty, or max results value is + * invalid. + */ + public ApiFuture listTenantsAsync(@Nullable String pageToken, int maxResults) { + return listTenantsOp(pageToken, maxResults).callAsync(firebaseApp); + } + + private CallableOperation listTenantsOp( + @Nullable final String pageToken, final int maxResults) { + final TenantSource tenantSource = new DefaultTenantSource(tenantClient); + final PageFactory factory = new PageFactory(tenantSource, maxResults, pageToken); + return new CallableOperation() { + @Override + protected ListTenantsPage execute() throws FirebaseAuthException { + return factory.create(); + } + }; + } + + /** + * Creates a new tenant with the attributes contained in the specified {@link CreateRequest}. + * + * @param request A non-null {@link CreateRequest} instance. + * @return A {@link Tenant} instance corresponding to the newly created tenant. + * @throws NullPointerException if the provided request is null. + * @throws FirebaseAuthException if an error occurs while creating the tenant. + */ + public Tenant createTenant(@NonNull CreateRequest request) throws FirebaseAuthException { + return createTenantOp(request).call(); + } + + /** + * Similar to {@link #createTenant(CreateRequest)} but performs the operation asynchronously. + * + * @param request A non-null {@link CreateRequest} instance. + * @return An {@code ApiFuture} which will complete successfully with a {@link Tenant} + * instance corresponding to the newly created tenant. If an error occurs while creating the + * tenant, the future throws a {@link FirebaseAuthException}. + * @throws NullPointerException if the provided request is null. + */ + public ApiFuture createTenantAsync(@NonNull CreateRequest request) { + return createTenantOp(request).callAsync(firebaseApp); + } + + private CallableOperation createTenantOp( + final CreateRequest request) { + checkNotNull(request, "Create request must not be null."); + return new CallableOperation() { + @Override + protected Tenant execute() throws FirebaseAuthException { + return tenantClient.createTenant(request); + } + }; + } + + + /** + * Updates an existing user account with the attributes contained in the specified {@link + * UpdateRequest}. + * + * @param request A non-null {@link UpdateRequest} instance. + * @return A {@link Tenant} instance corresponding to the updated user account. + * @throws NullPointerException if the provided update request is null. + * @throws FirebaseAuthException if an error occurs while updating the user account. + */ + public Tenant updateTenant(@NonNull UpdateRequest request) throws FirebaseAuthException { + return updateTenantOp(request).call(); + } + + /** + * Similar to {@link #updateTenant(UpdateRequest)} but performs the operation asynchronously. + * + * @param request A non-null {@link UpdateRequest} instance. + * @return An {@code ApiFuture} which will complete successfully with a {@link Tenant} + * instance corresponding to the updated user account. If an error occurs while updating the + * user account, the future throws a {@link FirebaseAuthException}. + */ + public ApiFuture updateTenantAsync(@NonNull UpdateRequest request) { + return updateTenantOp(request).callAsync(firebaseApp); + } + + private CallableOperation updateTenantOp( + final UpdateRequest request) { + checkNotNull(request, "Update request must not be null."); + checkArgument(!request.getProperties().isEmpty(), + "Tenant update must have at least one property set."); + return new CallableOperation() { + @Override + protected Tenant execute() throws FirebaseAuthException { + return tenantClient.updateTenant(request); + } + }; + } + + /** + * Deletes the tenant identified by the specified tenant ID. + * + * @param tenantId A tenant ID string. + * @throws IllegalArgumentException If the tenant ID string is null or empty. + * @throws FirebaseAuthException If an error occurs while deleting the tenant. + */ + public void deleteTenant(@NonNull String tenantId) throws FirebaseAuthException { + deleteTenantOp(tenantId).call(); + } + + /** + * Similar to {@link #deleteTenant(String)} but performs the operation asynchronously. + * + * @param tenantId A tenant ID string. + * @return An {@code ApiFuture} which will complete successfully when the specified tenant account + * has been deleted. If an error occurs while deleting the tenant account, the future throws a + * {@link FirebaseAuthException}. + * @throws IllegalArgumentException If the tenant ID string is null or empty. + */ + public ApiFuture deleteTenantAsync(String tenantId) { + return deleteTenantOp(tenantId).callAsync(firebaseApp); + } + + private CallableOperation deleteTenantOp(final String tenantId) { + checkArgument(!Strings.isNullOrEmpty(tenantId), "Tenant ID must not be null or empty."); + return new CallableOperation() { + @Override + protected Void execute() throws FirebaseAuthException { + tenantClient.deleteTenant(tenantId); + return null; + } + }; + } +} diff --git a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java index aa45f51b3..d5bfbae7f 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java +++ b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java @@ -46,9 +46,12 @@ import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; import com.google.firebase.ImplFirebaseTrampolines; -import com.google.firebase.auth.UserRecord.CreateRequest; -import com.google.firebase.auth.UserRecord.UpdateRequest; +import com.google.firebase.auth.ProviderConfigTestUtils.TemporaryProviderConfig; +import com.google.firebase.auth.UserTestUtils.RandomUser; +import com.google.firebase.auth.UserTestUtils.TemporaryUser; import com.google.firebase.auth.hash.Scrypt; +import com.google.firebase.auth.internal.AuthHttpClient; +import com.google.firebase.internal.Nullable; import com.google.firebase.testing.IntegrationTestUtils; import java.io.IOException; import java.net.URLDecoder; @@ -56,15 +59,13 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Random; -import java.util.UUID; import java.util.concurrent.ExecutionException; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; -import org.junit.BeforeClass; +import org.junit.Rule; import org.junit.Test; public class FirebaseAuthIT { @@ -81,13 +82,12 @@ public class FirebaseAuthIT { private static final HttpTransport transport = Utils.getDefaultTransport(); private static final String ACTION_LINK_CONTINUE_URL = "http://localhost/?a=1&b=2#c=3"; - private static FirebaseAuth auth; + private static final FirebaseAuth auth = FirebaseAuth.getInstance( + IntegrationTestUtils.ensureDefaultApp()); - @BeforeClass - public static void setUpClass() { - FirebaseApp masterApp = IntegrationTestUtils.ensureDefaultApp(); - auth = FirebaseAuth.getInstance(masterApp); - } + @Rule public final TemporaryUser temporaryUser = new TemporaryUser(auth); + @Rule public final TemporaryProviderConfig temporaryProviderConfig = + new TemporaryProviderConfig(auth); @Test public void testGetNonExistingUser() throws Exception { @@ -96,7 +96,7 @@ public void testGetNonExistingUser() throws Exception { fail("No error thrown for non existing uid"); } catch (ExecutionException e) { assertTrue(e.getCause() instanceof FirebaseAuthException); - assertEquals(FirebaseUserManager.USER_NOT_FOUND_ERROR, + assertEquals(AuthHttpClient.USER_NOT_FOUND_ERROR, ((FirebaseAuthException) e.getCause()).getErrorCode()); } } @@ -108,7 +108,7 @@ public void testGetNonExistingUserByEmail() throws Exception { fail("No error thrown for non existing email"); } catch (ExecutionException e) { assertTrue(e.getCause() instanceof FirebaseAuthException); - assertEquals(FirebaseUserManager.USER_NOT_FOUND_ERROR, + assertEquals(AuthHttpClient.USER_NOT_FOUND_ERROR, ((FirebaseAuthException) e.getCause()).getErrorCode()); } } @@ -116,11 +116,11 @@ public void testGetNonExistingUserByEmail() throws Exception { @Test public void testUpdateNonExistingUser() throws Exception { try { - auth.updateUserAsync(new UpdateRequest("non.existing")).get(); + auth.updateUserAsync(new UserRecord.UpdateRequest("non.existing")).get(); fail("No error thrown for non existing uid"); } catch (ExecutionException e) { assertTrue(e.getCause() instanceof FirebaseAuthException); - assertEquals(FirebaseUserManager.USER_NOT_FOUND_ERROR, + assertEquals(AuthHttpClient.USER_NOT_FOUND_ERROR, ((FirebaseAuthException) e.getCause()).getErrorCode()); } } @@ -132,7 +132,7 @@ public void testDeleteNonExistingUser() throws Exception { fail("No error thrown for non existing uid"); } catch (ExecutionException e) { assertTrue(e.getCause() instanceof FirebaseAuthException); - assertEquals(FirebaseUserManager.USER_NOT_FOUND_ERROR, + assertEquals(AuthHttpClient.USER_NOT_FOUND_ERROR, ((FirebaseAuthException) e.getCause()).getErrorCode()); } } @@ -211,50 +211,46 @@ private ApiFuture slowDeleteUsersAsync(List uids) thr @Test public void testCreateUserWithParams() throws Exception { - RandomUser randomUser = RandomUser.create(); - String phone = randomPhoneNumber(); - CreateRequest user = new CreateRequest() - .setUid(randomUser.uid) - .setEmail(randomUser.email) - .setPhoneNumber(phone) + RandomUser randomUser = UserTestUtils.generateRandomUserInfo(); + UserRecord.CreateRequest user = new UserRecord.CreateRequest() + .setUid(randomUser.getUid()) + .setEmail(randomUser.getEmail()) + .setPhoneNumber(randomUser.getPhoneNumber()) .setDisplayName("Random User") .setPhotoUrl("https://example.com/photo.png") .setEmailVerified(true) .setPassword("password"); - UserRecord userRecord = auth.createUserAsync(user).get(); - try { - assertEquals(randomUser.uid, userRecord.getUid()); - assertEquals("Random User", userRecord.getDisplayName()); - assertEquals(randomUser.email, userRecord.getEmail()); - assertEquals(phone, userRecord.getPhoneNumber()); - assertEquals("https://example.com/photo.png", userRecord.getPhotoUrl()); - assertTrue(userRecord.isEmailVerified()); - assertFalse(userRecord.isDisabled()); - - assertEquals(2, userRecord.getProviderData().length); - List providers = new ArrayList<>(); - for (UserInfo provider : userRecord.getProviderData()) { - providers.add(provider.getProviderId()); - } - assertTrue(providers.contains("password")); - assertTrue(providers.contains("phone")); + UserRecord userRecord = temporaryUser.create(user); + assertEquals(randomUser.getUid(), userRecord.getUid()); + assertEquals("Random User", userRecord.getDisplayName()); + assertEquals(randomUser.getEmail(), userRecord.getEmail()); + assertEquals(randomUser.getPhoneNumber(), userRecord.getPhoneNumber()); + assertEquals("https://example.com/photo.png", userRecord.getPhotoUrl()); + assertTrue(userRecord.isEmailVerified()); + assertFalse(userRecord.isDisabled()); - checkRecreate(randomUser.uid); - } finally { - auth.deleteUserAsync(userRecord.getUid()).get(); + assertEquals(2, userRecord.getProviderData().length); + List providers = new ArrayList<>(); + for (UserInfo provider : userRecord.getProviderData()) { + providers.add(provider.getProviderId()); } + assertTrue(providers.contains("password")); + assertTrue(providers.contains("phone")); + + checkRecreateUser(randomUser.getUid()); } @Test public void testUserLifecycle() throws Exception { // Create user - UserRecord userRecord = auth.createUserAsync(new CreateRequest()).get(); + UserRecord userRecord = auth.createUserAsync(new UserRecord.CreateRequest()).get(); String uid = userRecord.getUid(); // Get user userRecord = auth.getUserAsync(userRecord.getUid()).get(); assertEquals(uid, userRecord.getUid()); + assertNull(userRecord.getTenantId()); assertNull(userRecord.getDisplayName()); assertNull(userRecord.getEmail()); assertNull(userRecord.getPhoneNumber()); @@ -267,20 +263,20 @@ public void testUserLifecycle() throws Exception { assertTrue(userRecord.getCustomClaims().isEmpty()); // Update user - RandomUser randomUser = RandomUser.create(); - String phone = randomPhoneNumber(); - UpdateRequest request = userRecord.updateRequest() + RandomUser randomUser = UserTestUtils.generateRandomUserInfo(); + UserRecord.UpdateRequest request = userRecord.updateRequest() .setDisplayName("Updated Name") - .setEmail(randomUser.email) - .setPhoneNumber(phone) + .setEmail(randomUser.getEmail()) + .setPhoneNumber(randomUser.getPhoneNumber()) .setPhotoUrl("https://example.com/photo.png") .setEmailVerified(true) .setPassword("secret"); userRecord = auth.updateUserAsync(request).get(); assertEquals(uid, userRecord.getUid()); + assertNull(userRecord.getTenantId()); assertEquals("Updated Name", userRecord.getDisplayName()); - assertEquals(randomUser.email, userRecord.getEmail()); - assertEquals(phone, userRecord.getPhoneNumber()); + assertEquals(randomUser.getEmail(), userRecord.getEmail()); + assertEquals(randomUser.getPhoneNumber(), userRecord.getPhoneNumber()); assertEquals("https://example.com/photo.png", userRecord.getPhotoUrl()); assertTrue(userRecord.isEmailVerified()); assertFalse(userRecord.isDisabled()); @@ -299,8 +295,9 @@ public void testUserLifecycle() throws Exception { .setDisabled(true); userRecord = auth.updateUserAsync(request).get(); assertEquals(uid, userRecord.getUid()); + assertNull(userRecord.getTenantId()); assertNull(userRecord.getDisplayName()); - assertEquals(randomUser.email, userRecord.getEmail()); + assertEquals(randomUser.getEmail(), userRecord.getEmail()); assertNull(userRecord.getPhoneNumber()); assertNull(userRecord.getPhotoUrl()); assertTrue(userRecord.isEmailVerified()); @@ -310,22 +307,15 @@ public void testUserLifecycle() throws Exception { // Delete user auth.deleteUserAsync(userRecord.getUid()).get(); - try { - auth.getUserAsync(userRecord.getUid()).get(); - fail("No error thrown for deleted user"); - } catch (ExecutionException e) { - assertTrue(e.getCause() instanceof FirebaseAuthException); - assertEquals(FirebaseUserManager.USER_NOT_FOUND_ERROR, - ((FirebaseAuthException) e.getCause()).getErrorCode()); - } + UserTestUtils.assertUserDoesNotExist(auth, userRecord.getUid()); } @Test public void testLastRefreshTime() throws Exception { - RandomUser user = RandomUser.create(); - UserRecord newUserRecord = auth.createUser(new CreateRequest() - .setUid(user.uid) - .setEmail(user.email) + RandomUser user = UserTestUtils.generateRandomUserInfo(); + UserRecord newUserRecord = auth.createUser(new UserRecord.CreateRequest() + .setUid(user.getUid()) + .setEmail(user.getEmail()) .setEmailVerified(false) .setPassword("password")); @@ -353,110 +343,105 @@ public void testLastRefreshTime() throws Exception { public void testListUsers() throws Exception { final List uids = new ArrayList<>(); - try { - uids.add(auth.createUserAsync(new CreateRequest().setPassword("password")).get().getUid()); - uids.add(auth.createUserAsync(new CreateRequest().setPassword("password")).get().getUid()); - uids.add(auth.createUserAsync(new CreateRequest().setPassword("password")).get().getUid()); - - // Test list by batches - final AtomicInteger collected = new AtomicInteger(0); - ListUsersPage page = auth.listUsersAsync(null).get(); - while (page != null) { - for (ExportedUserRecord user : page.getValues()) { - if (uids.contains(user.getUid())) { - collected.incrementAndGet(); - assertNotNull("Missing passwordHash field. A common cause would be " - + "forgetting to add the \"Firebase Authentication Admin\" permission. See " - + "instructions in CONTRIBUTING.md", user.getPasswordHash()); - assertNotNull(user.getPasswordSalt()); - } - } - page = page.getNextPage(); - } - assertEquals(uids.size(), collected.get()); + for (int i = 0; i < 3; i++) { + UserRecord.CreateRequest createRequest = + new UserRecord.CreateRequest().setPassword("password"); + uids.add(temporaryUser.create(createRequest).getUid()); + } - // Test iterate all - collected.set(0); - page = auth.listUsersAsync(null).get(); - for (ExportedUserRecord user : page.iterateAll()) { + // Test list by batches + final AtomicInteger collected = new AtomicInteger(0); + ListUsersPage page = auth.listUsersAsync(null).get(); + while (page != null) { + for (ExportedUserRecord user : page.getValues()) { if (uids.contains(user.getUid())) { collected.incrementAndGet(); - assertNotNull(user.getPasswordHash()); + assertNotNull("Missing passwordHash field. A common cause would be " + + "forgetting to add the \"Firebase Authentication Admin\" permission. See " + + "instructions in CONTRIBUTING.md", user.getPasswordHash()); assertNotNull(user.getPasswordSalt()); + assertNull(user.getTenantId()); } } - assertEquals(uids.size(), collected.get()); - - // Test iterate async - collected.set(0); - final Semaphore semaphore = new Semaphore(0); - final AtomicReference error = new AtomicReference<>(); - ApiFuture pageFuture = auth.listUsersAsync(null); - ApiFutures.addCallback(pageFuture, new ApiFutureCallback() { - @Override - public void onFailure(Throwable t) { - error.set(t); - semaphore.release(); - } + page = page.getNextPage(); + } + assertEquals(uids.size(), collected.get()); + + // Test iterate all + collected.set(0); + page = auth.listUsersAsync(null).get(); + for (ExportedUserRecord user : page.iterateAll()) { + if (uids.contains(user.getUid())) { + collected.incrementAndGet(); + assertNotNull(user.getPasswordHash()); + assertNotNull(user.getPasswordSalt()); + assertNull(user.getTenantId()); + } + } + assertEquals(uids.size(), collected.get()); + + // Test iterate async + collected.set(0); + final Semaphore semaphore = new Semaphore(0); + final AtomicReference error = new AtomicReference<>(); + ApiFuture pageFuture = auth.listUsersAsync(null); + ApiFutures.addCallback(pageFuture, new ApiFutureCallback() { + @Override + public void onFailure(Throwable t) { + error.set(t); + semaphore.release(); + } - @Override - public void onSuccess(ListUsersPage result) { - for (ExportedUserRecord user : result.iterateAll()) { - if (uids.contains(user.getUid())) { - collected.incrementAndGet(); - assertNotNull(user.getPasswordHash()); - assertNotNull(user.getPasswordSalt()); - } + @Override + public void onSuccess(ListUsersPage result) { + for (ExportedUserRecord user : result.iterateAll()) { + if (uids.contains(user.getUid())) { + collected.incrementAndGet(); + assertNotNull(user.getPasswordHash()); + assertNotNull(user.getPasswordSalt()); + assertNull(user.getTenantId()); } - semaphore.release(); } - }, MoreExecutors.directExecutor()); - semaphore.acquire(); - assertEquals(uids.size(), collected.get()); - assertNull(error.get()); - } finally { - for (String uid : uids) { - auth.deleteUserAsync(uid).get(); + semaphore.release(); } - } + }, MoreExecutors.directExecutor()); + semaphore.acquire(); + assertEquals(uids.size(), collected.get()); + assertNull(error.get()); } @Test public void testCustomClaims() throws Exception { - UserRecord userRecord = auth.createUserAsync(new CreateRequest()).get(); + UserRecord userRecord = temporaryUser.create(new UserRecord.CreateRequest()); String uid = userRecord.getUid(); - try { - // New user should not have any claims - assertTrue(userRecord.getCustomClaims().isEmpty()); - - Map expected = ImmutableMap.of( - "admin", true, "package", "gold"); - auth.setCustomUserClaimsAsync(uid, expected).get(); - - // Should have 2 claims - UserRecord updatedUser = auth.getUserAsync(uid).get(); - assertEquals(2, updatedUser.getCustomClaims().size()); - for (Map.Entry entry : expected.entrySet()) { - assertEquals(entry.getValue(), updatedUser.getCustomClaims().get(entry.getKey())); - } + // New user should not have any claims + assertTrue(userRecord.getCustomClaims().isEmpty()); - // User's ID token should have the custom claims - String customToken = auth.createCustomTokenAsync(uid).get(); - String idToken = signInWithCustomToken(customToken); - FirebaseToken decoded = auth.verifyIdTokenAsync(idToken).get(); - Map result = decoded.getClaims(); - for (Map.Entry entry : expected.entrySet()) { - assertEquals(entry.getValue(), result.get(entry.getKey())); - } + Map expected = ImmutableMap.of( + "admin", true, "package", "gold"); + auth.setCustomUserClaimsAsync(uid, expected).get(); - // Should be able to remove custom claims - auth.setCustomUserClaimsAsync(uid, null).get(); - updatedUser = auth.getUserAsync(uid).get(); - assertTrue(updatedUser.getCustomClaims().isEmpty()); - } finally { - auth.deleteUserAsync(uid).get(); + // Should have 2 claims + UserRecord updatedUser = auth.getUserAsync(uid).get(); + assertEquals(2, updatedUser.getCustomClaims().size()); + for (Map.Entry entry : expected.entrySet()) { + assertEquals(entry.getValue(), updatedUser.getCustomClaims().get(entry.getKey())); } + + // User's ID token should have the custom claims + String customToken = auth.createCustomTokenAsync(uid).get(); + String idToken = signInWithCustomToken(customToken); + FirebaseToken decoded = auth.verifyIdTokenAsync(idToken).get(); + Map result = decoded.getClaims(); + for (Map.Entry entry : expected.entrySet()) { + assertEquals(entry.getValue(), result.get(entry.getKey())); + } + + // Should be able to remove custom claims + auth.setCustomUserClaimsAsync(uid, null).get(); + updatedUser = auth.getUserAsync(uid).get(); + assertTrue(updatedUser.getCustomClaims().isEmpty()); } @Test @@ -514,7 +499,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"); } @@ -568,32 +553,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(); @@ -609,88 +591,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 { @@ -708,22 +911,22 @@ private Map parseLinkParameters(String link) throws Exception { return result; } - static String randomPhoneNumber() { - Random random = new Random(); - StringBuilder builder = new StringBuilder("+1"); - for (int i = 0; i < 10; i++) { - builder.append(random.nextInt(10)); - } - return builder.toString(); + private String signInWithCustomToken(String customToken) throws IOException { + return signInWithCustomToken(customToken, null); } - private String signInWithCustomToken(String customToken) throws IOException { - GenericUrl url = new GenericUrl(VERIFY_CUSTOM_TOKEN_URL + "?key=" + private String signInWithCustomToken( + String customToken, @Nullable String tenantId) throws IOException { + final GenericUrl url = new GenericUrl(VERIFY_CUSTOM_TOKEN_URL + "?key=" + IntegrationTestUtils.getApiKey()); - Map content = ImmutableMap.of( - "token", customToken, "returnSecureToken", true); + ImmutableMap.Builder content = ImmutableMap.builder(); + content.put("token", customToken); + content.put("returnSecureToken", true); + if (tenantId != null) { + content.put("tenantId", tenantId); + } HttpRequest request = transport.createRequestFactory().buildPostRequest(url, - new JsonHttpContent(jsonFactory, content)); + new JsonHttpContent(jsonFactory, content.build())); request.setParser(new JsonObjectParser(jsonFactory)); HttpResponse response = request.execute(); try { @@ -787,9 +990,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); @@ -797,21 +1000,25 @@ private void checkRecreate(String uid) throws Exception { } } - static class RandomUser { - final String uid; - final String email; - - private RandomUser(String uid, String email) { - this.uid = uid; - this.email = email; + private boolean checkOidcProviderConfig(List providerIds, OidcProviderConfig config) { + if (providerIds.contains(config.getProviderId())) { + assertEquals("CLIENT_ID", config.getClientId()); + assertEquals("https://oidc.com/issuer", config.getIssuer()); + return true; } + return false; + } - static RandomUser create() { - final String uid = UUID.randomUUID().toString().replaceAll("-", ""); - final String email = ("test" + uid.substring(0, 12) + "@example." - + uid.substring(12) + ".com").toLowerCase(); - return new RandomUser(uid, email); + private boolean checkSamlProviderConfig(List providerIds, SamlProviderConfig config) { + if (providerIds.contains(config.getProviderId())) { + assertEquals("IDP_ENTITY_ID", config.getIdpEntityId()); + assertEquals("https://example.com/login", config.getSsoUrl()); + assertEquals(ImmutableList.of("certificate"), config.getX509Certificates()); + assertEquals("RP_ENTITY_ID", config.getRpEntityId()); + assertEquals("https://projectId.firebaseapp.com/__/auth/handler", config.getCallbackUrl()); + return true; } + return false; } static UserRecord newUserWithParams() throws Exception { @@ -821,13 +1028,14 @@ static UserRecord newUserWithParams() throws Exception { static UserRecord newUserWithParams(FirebaseAuth auth) throws Exception { // TODO(rsgowman): This function could be used throughout this file (similar to the other // ports). - RandomUser randomUser = RandomUser.create(); - return auth.createUser(new CreateRequest() - .setUid(randomUser.uid) - .setEmail(randomUser.email) - .setPhoneNumber(randomPhoneNumber()) + RandomUser randomUser = UserTestUtils.generateRandomUserInfo(); + return auth.createUser(new UserRecord.CreateRequest() + .setUid(randomUser.getUid()) + .setEmail(randomUser.getEmail()) + .setPhoneNumber(randomUser.getPhoneNumber()) .setDisplayName("Random User") .setPhotoUrl("https://example.com/photo.png") .setPassword("password")); } } + diff --git a/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java b/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java index 1bc05174f..635e1bfba 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java +++ b/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java @@ -32,7 +32,6 @@ import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; import com.google.firebase.TestOnlyImplFirebaseTrampolines; -import com.google.firebase.auth.internal.FirebaseTokenFactory; import com.google.firebase.testing.ServiceAccount; import com.google.firebase.testing.TestUtils; import java.lang.reflect.InvocationTargetException; @@ -429,12 +428,12 @@ private FirebaseAuth getAuthForIdTokenVerification(FirebaseTokenVerifier tokenVe private FirebaseAuth getAuthForIdTokenVerification( Supplier tokenVerifierSupplier) { FirebaseApp app = FirebaseApp.initializeApp(firebaseOptions); - FirebaseUserManager userManager = new FirebaseUserManager(app); - return FirebaseAuth.builder() - .setFirebaseApp(app) - .setIdTokenVerifier(tokenVerifierSupplier) - .setUserManager(Suppliers.ofInstance(userManager)) - .build(); + FirebaseUserManager userManager = FirebaseUserManager.builder().setFirebaseApp(app).build(); + return new FirebaseAuth( + AbstractFirebaseAuth.builder() + .setFirebaseApp(app) + .setIdTokenVerifier(tokenVerifierSupplier) + .setUserManager(Suppliers.ofInstance(userManager))); } private FirebaseAuth getAuthForSessionCookieVerification(FirebaseTokenVerifier tokenVerifier) { @@ -444,12 +443,12 @@ private FirebaseAuth getAuthForSessionCookieVerification(FirebaseTokenVerifier t private FirebaseAuth getAuthForSessionCookieVerification( Supplier tokenVerifierSupplier) { FirebaseApp app = FirebaseApp.initializeApp(firebaseOptions); - FirebaseUserManager userManager = new FirebaseUserManager(app); - return FirebaseAuth.builder() - .setFirebaseApp(app) - .setCookieVerifier(tokenVerifierSupplier) - .setUserManager(Suppliers.ofInstance(userManager)) - .build(); + FirebaseUserManager userManager = FirebaseUserManager.builder().setFirebaseApp(app).build(); + return new FirebaseAuth( + AbstractFirebaseAuth.builder() + .setFirebaseApp(app) + .setCookieVerifier(tokenVerifierSupplier) + .setUserManager(Suppliers.ofInstance(userManager))); } private static class MockTokenVerifier implements FirebaseTokenVerifier { diff --git a/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java b/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java index b10817afb..5dd1e9c14 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java +++ b/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java @@ -29,6 +29,7 @@ import com.google.api.client.testing.http.MockLowLevelHttpResponse; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.firebase.testing.ServiceAccount; import java.io.IOException; import java.util.concurrent.TimeUnit; @@ -216,6 +217,50 @@ public void testMalformedToken() throws Exception { tokenVerifier.verifyToken("not.a.jwt"); } + @Test + public void testVerifyTokenDifferentTenantIds() { + try { + fullyPopulatedBuilder() + .setTenantId("TENANT_1") + .build() + .verifyToken(createTokenWithTenantId("TENANT_2")); + } catch (FirebaseAuthException e) { + assertEquals(FirebaseTokenVerifierImpl.TENANT_ID_MISMATCH_ERROR, e.getErrorCode()); + assertEquals( + "The tenant ID ('TENANT_2') of the token did not match the expected value ('TENANT_1')", + e.getMessage()); + } + } + + @Test + public void testVerifyTokenMissingTenantId() { + try { + fullyPopulatedBuilder() + .setTenantId("TENANT_ID") + .build() + .verifyToken(tokenFactory.createToken()); + } catch (FirebaseAuthException e) { + assertEquals(FirebaseTokenVerifierImpl.TENANT_ID_MISMATCH_ERROR, e.getErrorCode()); + assertEquals( + "The tenant ID ('') of the token did not match the expected value ('TENANT_ID')", + e.getMessage()); + } + } + + @Test + public void testVerifyTokenUnexpectedTenantId() { + try { + fullyPopulatedBuilder() + .build() + .verifyToken(createTokenWithTenantId("TENANT_ID")); + } catch (FirebaseAuthException e) { + assertEquals(FirebaseTokenVerifierImpl.TENANT_ID_MISMATCH_ERROR, e.getErrorCode()); + assertEquals( + "The tenant ID ('TENANT_ID') of the token did not match the expected value ('')", + e.getMessage()); + } + } + @Test(expected = NullPointerException.class) public void testBuilderNoPublicKeysManager() { fullyPopulatedBuilder().setPublicKeysManager(null).build(); @@ -337,4 +382,10 @@ private String createTokenWithTimestamps(long issuedAtSeconds, long expirationSe payload.setExpirationTimeSeconds(expirationSeconds); return tokenFactory.createToken(payload); } + + private String createTokenWithTenantId(String tenantId) { + Payload payload = tokenFactory.createTokenPayload(); + payload.set("firebase", ImmutableMap.of("tenant", tenantId)); + return tokenFactory.createToken(payload); + } } diff --git a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java index 67d0448e9..d407fd8ec 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java +++ b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java @@ -16,10 +16,12 @@ package com.google.firebase.auth; +import static org.hamcrest.core.IsInstanceOf.instanceOf; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -42,10 +44,9 @@ import com.google.firebase.FirebaseOptions; import com.google.firebase.TestOnlyImplFirebaseTrampolines; import com.google.firebase.auth.FirebaseUserManager.EmailLinkType; -import com.google.firebase.auth.UidIdentifier; -import com.google.firebase.auth.UserIdentifier; -import com.google.firebase.auth.UserRecord.CreateRequest; -import com.google.firebase.auth.UserRecord.UpdateRequest; +import com.google.firebase.auth.internal.AuthHttpClient; +import com.google.firebase.auth.multitenancy.TenantAwareFirebaseAuth; +import com.google.firebase.auth.multitenancy.TenantManager; import com.google.firebase.internal.SdkUtils; import com.google.firebase.testing.MultiRequestMockHttpTransport; import com.google.firebase.testing.TestResponseInterceptor; @@ -67,8 +68,12 @@ public class FirebaseUserManagerTest { + private static final JsonFactory JSON_FACTORY = Utils.getDefaultJsonFactory(); + private static final String TEST_TOKEN = "token"; + private static final GoogleCredentials credentials = new MockGoogleCredentials(TEST_TOKEN); + private static final ActionCodeSettings ACTION_CODE_SETTINGS = ActionCodeSettings.builder() .setUrl("https://example.dynamic.link") .setHandleCodeInApp(true) @@ -78,9 +83,15 @@ public class FirebaseUserManagerTest { .setAndroidInstallApp(true) .setAndroidMinimumVersion("6") .build(); + private static final Map ACTION_CODE_SETTINGS_MAP = ACTION_CODE_SETTINGS.getProperties(); + private static final String PROJECT_BASE_URL = + "https://identitytoolkit.googleapis.com/v2/projects/test-project-id"; + + private static final String TENANTS_BASE_URL = PROJECT_BASE_URL + "/tenants"; + @After public void tearDown() { TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); @@ -120,9 +131,9 @@ public void testGetUserWithNotFoundError() throws Exception { FirebaseAuth.getInstance().getUserAsync("testuser").get(); fail("No error thrown for invalid response"); } catch (ExecutionException e) { - assertTrue(e.getCause() instanceof FirebaseAuthException); + assertThat(e.getCause(), instanceOf(FirebaseAuthException.class)); FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); - assertEquals(FirebaseUserManager.USER_NOT_FOUND_ERROR, authException.getErrorCode()); + assertEquals(AuthHttpClient.USER_NOT_FOUND_ERROR, authException.getErrorCode()); } } @@ -143,9 +154,9 @@ public void testGetUserByEmailWithNotFoundError() throws Exception { FirebaseAuth.getInstance().getUserByEmailAsync("testuser@example.com").get(); fail("No error thrown for invalid response"); } catch (ExecutionException e) { - assertTrue(e.getCause() instanceof FirebaseAuthException); + assertThat(e.getCause(), instanceOf(FirebaseAuthException.class)); FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); - assertEquals(FirebaseUserManager.USER_NOT_FOUND_ERROR, authException.getErrorCode()); + assertEquals(AuthHttpClient.USER_NOT_FOUND_ERROR, authException.getErrorCode()); } } @@ -166,9 +177,9 @@ public void testGetUserByPhoneNumberWithNotFoundError() throws Exception { FirebaseAuth.getInstance().getUserByPhoneNumberAsync("+1234567890").get(); fail("No error thrown for invalid response"); } catch (ExecutionException e) { - assertTrue(e.getCause() instanceof FirebaseAuthException); + assertThat(e.getCause(), instanceOf(FirebaseAuthException.class)); FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); - assertEquals(FirebaseUserManager.USER_NOT_FOUND_ERROR, authException.getErrorCode()); + assertEquals(AuthHttpClient.USER_NOT_FOUND_ERROR, authException.getErrorCode()); } } @@ -371,7 +382,8 @@ public void testCreateUser() throws Exception { TestResponseInterceptor interceptor = initializeAppForUserManagement( TestUtils.loadResource("createUser.json"), TestUtils.loadResource("getUser.json")); - UserRecord user = FirebaseAuth.getInstance().createUserAsync(new CreateRequest()).get(); + UserRecord user = + FirebaseAuth.getInstance().createUserAsync(new UserRecord.CreateRequest()).get(); checkUserRecord(user); checkRequestHeaders(interceptor); } @@ -382,7 +394,7 @@ public void testUpdateUser() throws Exception { TestUtils.loadResource("createUser.json"), TestUtils.loadResource("getUser.json")); UserRecord user = FirebaseAuth.getInstance() - .updateUserAsync(new UpdateRequest("testuser")).get(); + .updateUserAsync(new UserRecord.UpdateRequest("testuser")).get(); checkUserRecord(user); checkRequestHeaders(interceptor); } @@ -397,12 +409,9 @@ public void testSetCustomAttributes() throws Exception { FirebaseAuth.getInstance().setCustomUserClaimsAsync("testuser", claims).get(); checkRequestHeaders(interceptor); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - interceptor.getResponse().getRequest().getContent().writeTo(out); - JsonFactory jsonFactory = Utils.getDefaultJsonFactory(); - GenericJson parsed = jsonFactory.fromString(new String(out.toByteArray()), GenericJson.class); + GenericJson parsed = parseRequestContent(interceptor); assertEquals("testuser", parsed.get("localId")); - assertEquals(jsonFactory.toString(claims), parsed.get("customAttributes")); + assertEquals(JSON_FACTORY.toString(claims), parsed.get("customAttributes")); } @Test @@ -413,10 +422,7 @@ public void testRevokeRefreshTokens() throws Exception { FirebaseAuth.getInstance().revokeRefreshTokensAsync("testuser").get(); checkRequestHeaders(interceptor); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - interceptor.getResponse().getRequest().getContent().writeTo(out); - JsonFactory jsonFactory = Utils.getDefaultJsonFactory(); - GenericJson parsed = jsonFactory.fromString(new String(out.toByteArray()), GenericJson.class); + GenericJson parsed = parseRequestContent(interceptor); assertEquals("testuser", parsed.get("localId")); assertNotNull(parsed.get("validSince")); } @@ -518,14 +524,11 @@ public void testImportUsers() throws Exception { assertEquals(0, result.getFailureCount()); assertTrue(result.getErrors().isEmpty()); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - interceptor.getResponse().getRequest().getContent().writeTo(out); - JsonFactory jsonFactory = Utils.getDefaultJsonFactory(); - GenericJson parsed = jsonFactory.fromString(new String(out.toByteArray()), GenericJson.class); + GenericJson parsed = parseRequestContent(interceptor); assertEquals(1, parsed.size()); List> expected = ImmutableList.of( - user1.getProperties(jsonFactory), - user2.getProperties(jsonFactory) + user1.getProperties(JSON_FACTORY), + user2.getProperties(JSON_FACTORY) ); assertEquals(expected, parsed.get("users")); } @@ -558,15 +561,12 @@ public void testImportUsersError() throws Exception { assertEquals(2, error.getIndex()); assertEquals("Another error occurred in user3", error.getReason()); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - interceptor.getResponse().getRequest().getContent().writeTo(out); - JsonFactory jsonFactory = Utils.getDefaultJsonFactory(); - GenericJson parsed = jsonFactory.fromString(new String(out.toByteArray()), GenericJson.class); + GenericJson parsed = parseRequestContent(interceptor); assertEquals(1, parsed.size()); List> expected = ImmutableList.of( - user1.getProperties(jsonFactory), - user2.getProperties(jsonFactory), - user3.getProperties(jsonFactory) + user1.getProperties(JSON_FACTORY), + user2.getProperties(JSON_FACTORY), + user3.getProperties(JSON_FACTORY) ); assertEquals(expected, parsed.get("users")); } @@ -596,14 +596,11 @@ protected Map getOptions() { assertEquals(0, result.getFailureCount()); assertTrue(result.getErrors().isEmpty()); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - interceptor.getResponse().getRequest().getContent().writeTo(out); - JsonFactory jsonFactory = Utils.getDefaultJsonFactory(); - GenericJson parsed = jsonFactory.fromString(new String(out.toByteArray()), GenericJson.class); + GenericJson parsed = parseRequestContent(interceptor); assertEquals(4, parsed.size()); List> expected = ImmutableList.of( - user1.getProperties(jsonFactory), - user2.getProperties(jsonFactory) + user1.getProperties(JSON_FACTORY), + user2.getProperties(JSON_FACTORY) ); assertEquals(expected, parsed.get("users")); assertEquals("MOCK_HASH", parsed.get("hashAlgorithm")); @@ -669,10 +666,7 @@ public void testCreateSessionCookie() throws Exception { assertEquals("MockCookieString", cookie); checkRequestHeaders(interceptor); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - interceptor.getResponse().getRequest().getContent().writeTo(out); - JsonFactory jsonFactory = Utils.getDefaultJsonFactory(); - GenericJson parsed = jsonFactory.fromString(new String(out.toByteArray()), GenericJson.class); + GenericJson parsed = parseRequestContent(interceptor); assertEquals(2, parsed.size()); assertEquals("testToken", parsed.get("idToken")); assertEquals(new BigDecimal(3600), parsed.get("validDuration")); @@ -754,13 +748,13 @@ public void call(FirebaseAuth auth) throws Exception { .add(new UserManagerOp() { @Override public void call(FirebaseAuth auth) throws Exception { - auth.createUserAsync(new CreateRequest()).get(); + auth.createUserAsync(new UserRecord.CreateRequest()).get(); } }) .add(new UserManagerOp() { @Override public void call(FirebaseAuth auth) throws Exception { - auth.updateUserAsync(new UpdateRequest("test")).get(); + auth.updateUserAsync(new UserRecord.UpdateRequest("test")).get(); } }) .add(new UserManagerOp() { @@ -790,12 +784,12 @@ public void call(FirebaseAuth auth) throws Exception { operation.call(auth); fail("No error thrown for HTTP error: " + code); } catch (ExecutionException e) { - assertTrue(e.getCause() instanceof FirebaseAuthException); + assertThat(e.getCause(), instanceOf(FirebaseAuthException.class)); FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); String msg = String.format("Unexpected HTTP response with status: %d; body: {}", code); assertEquals(msg, authException.getMessage()); - assertTrue(authException.getCause() instanceof HttpResponseException); - assertEquals(FirebaseUserManager.INTERNAL_ERROR, authException.getErrorCode()); + assertThat(authException.getCause(), instanceOf(HttpResponseException.class)); + assertEquals(AuthHttpClient.INTERNAL_ERROR, authException.getErrorCode()); } } } @@ -808,11 +802,11 @@ public void call(FirebaseAuth auth) throws Exception { operation.call(auth); fail("No error thrown for HTTP error"); } catch (ExecutionException e) { - assertTrue(e.getCause().toString(), e.getCause() instanceof FirebaseAuthException); + assertThat(e.getCause().toString(), e.getCause(), instanceOf(FirebaseAuthException.class)); FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); - assertEquals("User management service responded with an error", authException.getMessage()); - assertTrue(authException.getCause() instanceof HttpResponseException); - assertEquals(FirebaseUserManager.USER_NOT_FOUND_ERROR, authException.getErrorCode()); + assertEquals("Firebase Auth service responded with an error", authException.getMessage()); + assertThat(authException.getCause(), instanceOf(HttpResponseException.class)); + assertEquals(AuthHttpClient.USER_NOT_FOUND_ERROR, authException.getErrorCode()); } } } @@ -824,10 +818,10 @@ public void testGetUserMalformedJsonError() throws Exception { FirebaseAuth.getInstance().getUserAsync("testuser").get(); fail("No error thrown for JSON error"); } catch (ExecutionException e) { - assertTrue(e.getCause() instanceof FirebaseAuthException); + assertThat(e.getCause(), instanceOf(FirebaseAuthException.class)); FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); - assertTrue(authException.getCause() instanceof IOException); - assertEquals(FirebaseUserManager.INTERNAL_ERROR, authException.getErrorCode()); + assertThat(authException.getCause(), instanceOf(IOException.class)); + assertEquals(AuthHttpClient.INTERNAL_ERROR, authException.getErrorCode()); } } @@ -841,12 +835,12 @@ public void testGetUserUnexpectedHttpError() throws Exception { auth.getUserAsync("testuser").get(); fail("No error thrown for JSON error"); } catch (ExecutionException e) { - assertTrue(e.getCause() instanceof FirebaseAuthException); + assertThat(e.getCause(), instanceOf(FirebaseAuthException.class)); FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); - assertTrue(authException.getCause() instanceof HttpResponseException); + assertThat(authException.getCause(), instanceOf(HttpResponseException.class)); assertEquals("Unexpected HTTP response with status: 500; body: {\"not\" json}", authException.getMessage()); - assertEquals(FirebaseUserManager.INTERNAL_ERROR, authException.getErrorCode()); + assertEquals(AuthHttpClient.INTERNAL_ERROR, authException.getErrorCode()); } } @@ -874,13 +868,13 @@ public void testTimeout() throws Exception { @Test public void testUserBuilder() { - Map map = new CreateRequest().getProperties(); + Map map = new UserRecord.CreateRequest().getProperties(); assertTrue(map.isEmpty()); } @Test public void testUserBuilderWithParams() { - Map map = new CreateRequest() + Map map = new UserRecord.CreateRequest() .setUid("TestUid") .setDisplayName("Display Name") .setPhotoUrl("http://test.com/example.png") @@ -901,7 +895,7 @@ public void testUserBuilderWithParams() { @Test public void testInvalidUid() { - CreateRequest user = new CreateRequest(); + UserRecord.CreateRequest user = new UserRecord.CreateRequest(); try { user.setUid(null); fail("No error thrown for null uid"); @@ -926,7 +920,7 @@ public void testInvalidUid() { @Test public void testInvalidDisplayName() { - CreateRequest user = new CreateRequest(); + UserRecord.CreateRequest user = new UserRecord.CreateRequest(); try { user.setDisplayName(null); fail("No error thrown for null display name"); @@ -937,7 +931,7 @@ public void testInvalidDisplayName() { @Test public void testInvalidPhotoUrl() { - CreateRequest user = new CreateRequest(); + UserRecord.CreateRequest user = new UserRecord.CreateRequest(); try { user.setPhotoUrl(null); fail("No error thrown for null photo url"); @@ -962,7 +956,7 @@ public void testInvalidPhotoUrl() { @Test public void testInvalidEmail() { - CreateRequest user = new CreateRequest(); + UserRecord.CreateRequest user = new UserRecord.CreateRequest(); try { user.setEmail(null); fail("No error thrown for null email"); @@ -987,7 +981,7 @@ public void testInvalidEmail() { @Test public void testInvalidPhoneNumber() { - CreateRequest user = new CreateRequest(); + UserRecord.CreateRequest user = new UserRecord.CreateRequest(); try { user.setPhoneNumber(null); fail("No error thrown for null phone number"); @@ -1012,7 +1006,7 @@ public void testInvalidPhoneNumber() { @Test public void testInvalidPassword() { - CreateRequest user = new CreateRequest(); + UserRecord.CreateRequest user = new UserRecord.CreateRequest(); try { user.setPassword(null); fail("No error thrown for null password"); @@ -1030,7 +1024,7 @@ public void testInvalidPassword() { @Test public void testUserUpdater() throws IOException { - UpdateRequest update = new UpdateRequest("test"); + UserRecord.UpdateRequest update = new UserRecord.UpdateRequest("test"); Map claims = ImmutableMap.of("admin", true, "package", "gold"); Map map = update .setDisplayName("Display Name") @@ -1040,7 +1034,7 @@ public void testUserUpdater() throws IOException { .setEmailVerified(true) .setPassword("secret") .setCustomClaims(claims) - .getProperties(Utils.getDefaultJsonFactory()); + .getProperties(JSON_FACTORY); assertEquals(8, map.size()); assertEquals(update.getUid(), map.get("localId")); assertEquals("Display Name", map.get("displayName")); @@ -1049,12 +1043,12 @@ public void testUserUpdater() throws IOException { assertEquals("+1234567890", map.get("phoneNumber")); assertTrue((Boolean) map.get("emailVerified")); assertEquals("secret", map.get("password")); - assertEquals(Utils.getDefaultJsonFactory().toString(claims), map.get("customAttributes")); + assertEquals(JSON_FACTORY.toString(claims), map.get("customAttributes")); } @Test public void testNullJsonFactory() { - UpdateRequest update = new UpdateRequest("test"); + UserRecord.UpdateRequest update = new UserRecord.UpdateRequest("test"); Map claims = ImmutableMap.of("admin", true, "package", "gold"); update.setCustomClaims(claims); try { @@ -1067,10 +1061,10 @@ public void testNullJsonFactory() { @Test public void testNullCustomClaims() { - UpdateRequest update = new UpdateRequest("test"); + UserRecord.UpdateRequest update = new UserRecord.UpdateRequest("test"); Map map = update .setCustomClaims(null) - .getProperties(Utils.getDefaultJsonFactory()); + .getProperties(JSON_FACTORY); assertEquals(2, map.size()); assertEquals(update.getUid(), map.get("localId")); assertEquals("{}", map.get("customAttributes")); @@ -1078,10 +1072,10 @@ public void testNullCustomClaims() { @Test public void testEmptyCustomClaims() { - UpdateRequest update = new UpdateRequest("test"); + UserRecord.UpdateRequest update = new UserRecord.UpdateRequest("test"); Map map = update .setCustomClaims(ImmutableMap.of()) - .getProperties(Utils.getDefaultJsonFactory()); + .getProperties(JSON_FACTORY); assertEquals(2, map.size()); assertEquals(update.getUid(), map.get("localId")); assertEquals("{}", map.get("customAttributes")); @@ -1089,31 +1083,31 @@ public void testEmptyCustomClaims() { @Test public void testDeleteDisplayName() { - Map map = new UpdateRequest("test") + Map map = new UserRecord.UpdateRequest("test") .setDisplayName(null) - .getProperties(Utils.getDefaultJsonFactory()); + .getProperties(JSON_FACTORY); assertEquals(ImmutableList.of("DISPLAY_NAME"), map.get("deleteAttribute")); } @Test public void testDeletePhotoUrl() { - Map map = new UpdateRequest("test") + Map map = new UserRecord.UpdateRequest("test") .setPhotoUrl(null) - .getProperties(Utils.getDefaultJsonFactory()); + .getProperties(JSON_FACTORY); assertEquals(ImmutableList.of("PHOTO_URL"), map.get("deleteAttribute")); } @Test public void testDeletePhoneNumber() { - Map map = new UpdateRequest("test") + Map map = new UserRecord.UpdateRequest("test") .setPhoneNumber(null) - .getProperties(Utils.getDefaultJsonFactory()); + .getProperties(JSON_FACTORY); assertEquals(ImmutableList.of("phone"), map.get("deleteProvider")); } @Test public void testInvalidUpdatePhotoUrl() { - UpdateRequest update = new UpdateRequest("test"); + UserRecord.UpdateRequest update = new UserRecord.UpdateRequest("test"); try { update.setPhotoUrl(""); fail("No error thrown for invalid photo url"); @@ -1131,7 +1125,7 @@ public void testInvalidUpdatePhotoUrl() { @Test public void testInvalidUpdateEmail() { - UpdateRequest update = new UpdateRequest("test"); + UserRecord.UpdateRequest update = new UserRecord.UpdateRequest("test"); try { update.setEmail(null); fail("No error thrown for null email"); @@ -1156,7 +1150,7 @@ public void testInvalidUpdateEmail() { @Test public void testInvalidUpdatePhoneNumber() { - UpdateRequest update = new UpdateRequest("test"); + UserRecord.UpdateRequest update = new UserRecord.UpdateRequest("test"); try { update.setPhoneNumber(""); @@ -1175,7 +1169,7 @@ public void testInvalidUpdatePhoneNumber() { @Test public void testInvalidUpdatePassword() { - UpdateRequest update = new UpdateRequest("test"); + UserRecord.UpdateRequest update = new UserRecord.UpdateRequest("test"); try { update.setPassword(null); fail("No error thrown for null password"); @@ -1193,7 +1187,7 @@ public void testInvalidUpdatePassword() { @Test public void testInvalidCustomClaims() { - UpdateRequest update = new UpdateRequest("test"); + UserRecord.UpdateRequest update = new UserRecord.UpdateRequest("test"); for (String claim : FirebaseUserManager.RESERVED_CLAIMS) { try { update.setCustomClaims(ImmutableMap.of(claim, "value")); @@ -1210,10 +1204,10 @@ public void testLargeCustomClaims() { for (int i = 0; i < 1001; i++) { builder.append("a"); } - UpdateRequest update = new UpdateRequest("test"); + UserRecord.UpdateRequest update = new UserRecord.UpdateRequest("test"); update.setCustomClaims(ImmutableMap.of("key", builder.toString())); try { - update.getProperties(Utils.getDefaultJsonFactory()); + update.getProperties(JSON_FACTORY); fail("No error thrown for large claims payload"); } catch (Exception ignore) { // expected @@ -1245,10 +1239,7 @@ public void testGeneratePasswordResetLinkWithSettings() throws Exception { assertEquals("https://mock-oob-link.for.auth.tests", link); checkRequestHeaders(interceptor); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - interceptor.getResponse().getRequest().getContent().writeTo(out); - JsonFactory jsonFactory = Utils.getDefaultJsonFactory(); - GenericJson parsed = jsonFactory.fromString(new String(out.toByteArray()), GenericJson.class); + GenericJson parsed = parseRequestContent(interceptor); assertEquals(3 + ACTION_CODE_SETTINGS_MAP.size(), parsed.size()); assertEquals("test@example.com", parsed.get("email")); assertEquals("PASSWORD_RESET", parsed.get("requestType")); @@ -1267,10 +1258,7 @@ public void testGeneratePasswordResetLink() throws Exception { assertEquals("https://mock-oob-link.for.auth.tests", link); checkRequestHeaders(interceptor); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - interceptor.getResponse().getRequest().getContent().writeTo(out); - JsonFactory jsonFactory = Utils.getDefaultJsonFactory(); - GenericJson parsed = jsonFactory.fromString(new String(out.toByteArray()), GenericJson.class); + GenericJson parsed = parseRequestContent(interceptor); assertEquals(3, parsed.size()); assertEquals("test@example.com", parsed.get("email")); assertEquals("PASSWORD_RESET", parsed.get("requestType")); @@ -1302,10 +1290,7 @@ public void testGenerateEmailVerificationLinkWithSettings() throws Exception { assertEquals("https://mock-oob-link.for.auth.tests", link); checkRequestHeaders(interceptor); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - interceptor.getResponse().getRequest().getContent().writeTo(out); - JsonFactory jsonFactory = Utils.getDefaultJsonFactory(); - GenericJson parsed = jsonFactory.fromString(new String(out.toByteArray()), GenericJson.class); + GenericJson parsed = parseRequestContent(interceptor); assertEquals(3 + ACTION_CODE_SETTINGS_MAP.size(), parsed.size()); assertEquals("test@example.com", parsed.get("email")); assertEquals("VERIFY_EMAIL", parsed.get("requestType")); @@ -1324,10 +1309,7 @@ public void testGenerateEmailVerificationLink() throws Exception { assertEquals("https://mock-oob-link.for.auth.tests", link); checkRequestHeaders(interceptor); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - interceptor.getResponse().getRequest().getContent().writeTo(out); - JsonFactory jsonFactory = Utils.getDefaultJsonFactory(); - GenericJson parsed = jsonFactory.fromString(new String(out.toByteArray()), GenericJson.class); + GenericJson parsed = parseRequestContent(interceptor); assertEquals(3, parsed.size()); assertEquals("test@example.com", parsed.get("email")); assertEquals("VERIFY_EMAIL", parsed.get("requestType")); @@ -1372,10 +1354,7 @@ public void testGenerateSignInWithEmailLinkWithSettings() throws Exception { assertEquals("https://mock-oob-link.for.auth.tests", link); checkRequestHeaders(interceptor); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - interceptor.getResponse().getRequest().getContent().writeTo(out); - JsonFactory jsonFactory = Utils.getDefaultJsonFactory(); - GenericJson parsed = jsonFactory.fromString(new String(out.toByteArray()), GenericJson.class); + GenericJson parsed = parseRequestContent(interceptor); assertEquals(3 + ACTION_CODE_SETTINGS_MAP.size(), parsed.size()); assertEquals("test@example.com", parsed.get("email")); assertEquals("EMAIL_SIGNIN", parsed.get("requestType")); @@ -1397,7 +1376,7 @@ public void testHttpErrorWithCode() { fail("No exception thrown for HTTP error"); } catch (FirebaseAuthException e) { assertEquals("unauthorized-continue-uri", e.getErrorCode()); - assertTrue(e.getCause() instanceof HttpResponseException); + assertThat(e.getCause(), instanceOf(HttpResponseException.class)); } } @@ -1413,11 +1392,1188 @@ public void testUnexpectedHttpError() { fail("No exception thrown for HTTP error"); } catch (FirebaseAuthException e) { assertEquals("internal-error", e.getErrorCode()); - assertTrue(e.getCause() instanceof HttpResponseException); + assertThat(e.getCause(), instanceOf(HttpResponseException.class)); + } + } + + @Test + public void testCreateOidcProvider() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("oidc.json")); + OidcProviderConfig.CreateRequest createRequest = + new OidcProviderConfig.CreateRequest() + .setProviderId("oidc.provider-id") + .setDisplayName("DISPLAY_NAME") + .setEnabled(true) + .setClientId("CLIENT_ID") + .setIssuer("https://oidc.com/issuer"); + + OidcProviderConfig config = FirebaseAuth.getInstance().createOidcProviderConfig(createRequest); + + checkOidcProviderConfig(config, "oidc.provider-id"); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "POST", PROJECT_BASE_URL + "/oauthIdpConfigs"); + GenericJson parsed = parseRequestContent(interceptor); + assertEquals("DISPLAY_NAME", parsed.get("displayName")); + assertTrue((boolean) parsed.get("enabled")); + assertEquals("CLIENT_ID", parsed.get("clientId")); + assertEquals("https://oidc.com/issuer", parsed.get("issuer")); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals("oidc.provider-id", url.getFirst("oauthIdpConfigId")); + } + + @Test + public void testCreateOidcProviderAsync() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("oidc.json")); + OidcProviderConfig.CreateRequest createRequest = + new OidcProviderConfig.CreateRequest() + .setProviderId("oidc.provider-id") + .setDisplayName("DISPLAY_NAME") + .setEnabled(true) + .setClientId("CLIENT_ID") + .setIssuer("https://oidc.com/issuer"); + + OidcProviderConfig config = + FirebaseAuth.getInstance().createOidcProviderConfigAsync(createRequest).get(); + + checkOidcProviderConfig(config, "oidc.provider-id"); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "POST", PROJECT_BASE_URL + "/oauthIdpConfigs"); + GenericJson parsed = parseRequestContent(interceptor); + assertEquals("DISPLAY_NAME", parsed.get("displayName")); + assertTrue((boolean) parsed.get("enabled")); + assertEquals("CLIENT_ID", parsed.get("clientId")); + assertEquals("https://oidc.com/issuer", parsed.get("issuer")); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals("oidc.provider-id", url.getFirst("oauthIdpConfigId")); + } + + @Test + public void testCreateOidcProviderMinimal() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("oidc.json")); + // Only the 'enabled' and 'displayName' fields can be omitted from an OIDC provider config + // creation request. + OidcProviderConfig.CreateRequest createRequest = + new OidcProviderConfig.CreateRequest() + .setProviderId("oidc.provider-id") + .setClientId("CLIENT_ID") + .setIssuer("https://oidc.com/issuer"); + + FirebaseAuth.getInstance().createOidcProviderConfig(createRequest); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "POST", PROJECT_BASE_URL + "/oauthIdpConfigs"); + GenericJson parsed = parseRequestContent(interceptor); + assertNull(parsed.get("displayName")); + assertNull(parsed.get("enabled")); + assertEquals("CLIENT_ID", parsed.get("clientId")); + assertEquals("https://oidc.com/issuer", parsed.get("issuer")); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals("oidc.provider-id", url.getFirst("oauthIdpConfigId")); + } + + @Test + public void testCreateOidcProviderError() throws Exception { + TestResponseInterceptor interceptor = + initializeAppForUserManagementWithStatusCode(404, + "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}"); + OidcProviderConfig.CreateRequest createRequest = + new OidcProviderConfig.CreateRequest().setProviderId("oidc.provider-id"); + try { + FirebaseAuth.getInstance().createOidcProviderConfig(createRequest); + fail("No error thrown for invalid response"); + } catch (FirebaseAuthException e) { + assertEquals(AuthHttpClient.INTERNAL_ERROR, e.getErrorCode()); + } + checkUrl(interceptor, "POST", PROJECT_BASE_URL + "/oauthIdpConfigs"); + } + + @Test + public void testCreateOidcProviderMissingId() throws Exception { + initializeAppForUserManagement(TestUtils.loadResource("oidc.json")); + OidcProviderConfig.CreateRequest createRequest = + new OidcProviderConfig.CreateRequest() + .setDisplayName("DISPLAY_NAME") + .setEnabled(true) + .setClientId("CLIENT_ID") + .setIssuer("https://oidc.com/issuer"); + try { + FirebaseAuth.getInstance().createOidcProviderConfig(createRequest); + fail("No error thrown for invalid response"); + } catch (IllegalArgumentException e) { + // Expected. + } + } + + @Test + public void testTenantAwareCreateOidcProvider() throws Exception { + TestResponseInterceptor interceptor = initializeAppForTenantAwareUserManagement( + "TENANT_ID", + TestUtils.loadResource("oidc.json")); + OidcProviderConfig.CreateRequest createRequest = + new OidcProviderConfig.CreateRequest() + .setProviderId("oidc.provider-id") + .setDisplayName("DISPLAY_NAME") + .setEnabled(true) + .setClientId("CLIENT_ID") + .setIssuer("https://oidc.com/issuer"); + TenantAwareFirebaseAuth tenantAwareAuth = + FirebaseAuth.getInstance().getTenantManager().getAuthForTenant("TENANT_ID"); + + tenantAwareAuth.createOidcProviderConfig(createRequest); + + checkRequestHeaders(interceptor); + checkUrl(interceptor, "POST", TENANTS_BASE_URL + "/TENANT_ID/oauthIdpConfigs"); + } + + @Test + public void testUpdateOidcProvider() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("oidc.json")); + OidcProviderConfig.UpdateRequest request = + new OidcProviderConfig.UpdateRequest("oidc.provider-id") + .setDisplayName("DISPLAY_NAME") + .setEnabled(true) + .setClientId("CLIENT_ID") + .setIssuer("https://oidc.com/issuer"); + + OidcProviderConfig config = FirebaseAuth.getInstance().updateOidcProviderConfig(request); + + checkOidcProviderConfig(config, "oidc.provider-id"); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "PATCH", PROJECT_BASE_URL + "/oauthIdpConfigs/oidc.provider-id"); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals("clientId,displayName,enabled,issuer", url.getFirst("updateMask")); + GenericJson parsed = parseRequestContent(interceptor); + assertEquals("DISPLAY_NAME", parsed.get("displayName")); + assertTrue((boolean) parsed.get("enabled")); + assertEquals("CLIENT_ID", parsed.get("clientId")); + assertEquals("https://oidc.com/issuer", parsed.get("issuer")); + } + + @Test + public void testUpdateOidcProviderAsync() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("oidc.json")); + OidcProviderConfig.UpdateRequest request = + new OidcProviderConfig.UpdateRequest("oidc.provider-id") + .setDisplayName("DISPLAY_NAME") + .setEnabled(true) + .setClientId("CLIENT_ID") + .setIssuer("https://oidc.com/issuer"); + + OidcProviderConfig config = + FirebaseAuth.getInstance().updateOidcProviderConfigAsync(request).get(); + + checkOidcProviderConfig(config, "oidc.provider-id"); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "PATCH", PROJECT_BASE_URL + "/oauthIdpConfigs/oidc.provider-id"); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals("clientId,displayName,enabled,issuer", url.getFirst("updateMask")); + GenericJson parsed = parseRequestContent(interceptor); + assertEquals("DISPLAY_NAME", parsed.get("displayName")); + assertTrue((boolean) parsed.get("enabled")); + assertEquals("CLIENT_ID", parsed.get("clientId")); + assertEquals("https://oidc.com/issuer", parsed.get("issuer")); + } + + @Test + public void testUpdateOidcProviderMinimal() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("oidc.json")); + OidcProviderConfig.UpdateRequest request = + new OidcProviderConfig.UpdateRequest("oidc.provider-id").setDisplayName("DISPLAY_NAME"); + + OidcProviderConfig config = FirebaseAuth.getInstance().updateOidcProviderConfig(request); + + checkOidcProviderConfig(config, "oidc.provider-id"); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "PATCH", PROJECT_BASE_URL + "/oauthIdpConfigs/oidc.provider-id"); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals("displayName", url.getFirst("updateMask")); + GenericJson parsed = parseRequestContent(interceptor); + assertEquals(1, parsed.size()); + assertEquals("DISPLAY_NAME", parsed.get("displayName")); + } + + @Test + public void testUpdateOidcProviderConfigNoValues() throws Exception { + initializeAppForUserManagement(TestUtils.loadResource("oidc.json")); + try { + FirebaseAuth.getInstance().updateOidcProviderConfig( + new OidcProviderConfig.UpdateRequest("oidc.provider-id")); + fail("No error thrown for empty provider config update"); + } catch (IllegalArgumentException e) { + // expected + } + } + + @Test + public void testUpdateOidcProviderConfigError() { + TestResponseInterceptor interceptor = + initializeAppForUserManagementWithStatusCode(404, + "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}"); + OidcProviderConfig.UpdateRequest request = + new OidcProviderConfig.UpdateRequest("oidc.provider-id").setDisplayName("DISPLAY_NAME"); + try { + FirebaseAuth.getInstance().updateOidcProviderConfig(request); + fail("No error thrown for invalid response"); + } catch (FirebaseAuthException e) { + assertEquals(AuthHttpClient.INTERNAL_ERROR, e.getErrorCode()); + } + checkUrl(interceptor, "PATCH", PROJECT_BASE_URL + "/oauthIdpConfigs/oidc.provider-id"); + } + + @Test + public void testTenantAwareUpdateOidcProvider() throws Exception { + TestResponseInterceptor interceptor = initializeAppForTenantAwareUserManagement( + "TENANT_ID", + TestUtils.loadResource("oidc.json")); + TenantAwareFirebaseAuth tenantAwareAuth = + FirebaseAuth.getInstance().getTenantManager().getAuthForTenant("TENANT_ID"); + OidcProviderConfig.UpdateRequest request = + new OidcProviderConfig.UpdateRequest("oidc.provider-id") + .setDisplayName("DISPLAY_NAME") + .setEnabled(true) + .setClientId("CLIENT_ID") + .setIssuer("https://oidc.com/issuer"); + + OidcProviderConfig config = tenantAwareAuth.updateOidcProviderConfig(request); + + checkOidcProviderConfig(config, "oidc.provider-id"); + checkRequestHeaders(interceptor); + String expectedUrl = TENANTS_BASE_URL + "/TENANT_ID/oauthIdpConfigs/oidc.provider-id"; + checkUrl(interceptor, "PATCH", expectedUrl); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals("clientId,displayName,enabled,issuer", url.getFirst("updateMask")); + GenericJson parsed = parseRequestContent(interceptor); + assertEquals("DISPLAY_NAME", parsed.get("displayName")); + assertTrue((boolean) parsed.get("enabled")); + assertEquals("CLIENT_ID", parsed.get("clientId")); + assertEquals("https://oidc.com/issuer", parsed.get("issuer")); + } + + @Test + public void testGetOidcProviderConfig() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("oidc.json")); + + OidcProviderConfig config = + FirebaseAuth.getInstance().getOidcProviderConfig("oidc.provider-id"); + + checkOidcProviderConfig(config, "oidc.provider-id"); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "GET", PROJECT_BASE_URL + "/oauthIdpConfigs/oidc.provider-id"); + } + + @Test + public void testGetOidcProviderConfigAsync() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("oidc.json")); + + OidcProviderConfig config = + FirebaseAuth.getInstance().getOidcProviderConfigAsync("oidc.provider-id").get(); + + checkOidcProviderConfig(config, "oidc.provider-id"); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "GET", PROJECT_BASE_URL + "/oauthIdpConfigs/oidc.provider-id"); + } + + @Test + public void testGetOidcProviderConfigMissingId() throws Exception { + initializeAppForUserManagement(TestUtils.loadResource("oidc.json")); + + try { + FirebaseAuth.getInstance().getOidcProviderConfig(null); + fail("No error thrown for missing provider ID."); + } catch (IllegalArgumentException e) { + // Expected. + } + } + + @Test + public void testGetOidcProviderConfigInvalidId() throws Exception { + initializeAppForUserManagement(TestUtils.loadResource("oidc.json")); + + try { + FirebaseAuth.getInstance().getOidcProviderConfig("saml.invalid-oidc-provider-id"); + fail("No error thrown for invalid provider ID."); + } catch (IllegalArgumentException e) { + // Expected. + } + } + + @Test + public void testGetOidcProviderConfigWithNotFoundError() throws Exception { + TestResponseInterceptor interceptor = + initializeAppForUserManagementWithStatusCode(404, + "{\"error\": {\"message\": \"CONFIGURATION_NOT_FOUND\"}}"); + try { + FirebaseAuth.getInstance().getOidcProviderConfig("oidc.provider-id"); + fail("No error thrown for invalid response"); + } catch (FirebaseAuthException e) { + assertEquals(AuthHttpClient.CONFIGURATION_NOT_FOUND_ERROR, e.getErrorCode()); + } + checkUrl(interceptor, "GET", PROJECT_BASE_URL + "/oauthIdpConfigs/oidc.provider-id"); + } + + @Test + public void testGetTenantAwareOidcProviderConfig() throws Exception { + TestResponseInterceptor interceptor = initializeAppForTenantAwareUserManagement( + "TENANT_ID", + TestUtils.loadResource("oidc.json")); + TenantAwareFirebaseAuth tenantAwareAuth = + FirebaseAuth.getInstance().getTenantManager().getAuthForTenant("TENANT_ID"); + + OidcProviderConfig config = tenantAwareAuth.getOidcProviderConfig("oidc.provider-id"); + + checkOidcProviderConfig(config, "oidc.provider-id"); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "GET", TENANTS_BASE_URL + "/TENANT_ID/oauthIdpConfigs/oidc.provider-id"); + } + + @Test + public void testListOidcProviderConfigs() throws Exception { + final TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("listOidc.json")); + ListProviderConfigsPage page = + FirebaseAuth.getInstance().listOidcProviderConfigs(null, 99); + + ImmutableList providerConfigs = ImmutableList.copyOf(page.getValues()); + assertEquals(2, providerConfigs.size()); + checkOidcProviderConfig(providerConfigs.get(0), "oidc.provider-id1"); + checkOidcProviderConfig(providerConfigs.get(1), "oidc.provider-id2"); + assertEquals("", page.getNextPageToken()); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "GET", PROJECT_BASE_URL + "/oauthIdpConfigs"); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals(99, url.getFirst("pageSize")); + assertNull(url.getFirst("nextPageToken")); + } + + @Test + public void testListOidcProviderConfigsAsync() throws Exception { + final TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("listOidc.json")); + ListProviderConfigsPage page = + FirebaseAuth.getInstance().listOidcProviderConfigsAsync(null, 99).get(); + + ImmutableList providerConfigs = ImmutableList.copyOf(page.getValues()); + assertEquals(2, providerConfigs.size()); + checkOidcProviderConfig(providerConfigs.get(0), "oidc.provider-id1"); + checkOidcProviderConfig(providerConfigs.get(1), "oidc.provider-id2"); + assertEquals("", page.getNextPageToken()); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "GET", PROJECT_BASE_URL + "/oauthIdpConfigs"); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals(99, url.getFirst("pageSize")); + assertNull(url.getFirst("nextPageToken")); + } + + @Test + public void testListOidcProviderConfigsError() throws Exception { + TestResponseInterceptor interceptor = + initializeAppForUserManagementWithStatusCode(404, + "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}"); + + try { + FirebaseAuth.getInstance().listOidcProviderConfigs(null, 99); + fail("No error thrown for invalid response"); + } catch (FirebaseAuthException e) { + assertEquals(AuthHttpClient.INTERNAL_ERROR, e.getErrorCode()); + } + checkUrl(interceptor, "GET", PROJECT_BASE_URL + "/oauthIdpConfigs"); + } + + @Test + public void testListOidcProviderConfigsWithPageToken() throws Exception { + final TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("listOidc.json")); + ListProviderConfigsPage page = + FirebaseAuth.getInstance().listOidcProviderConfigs("token", 99); + + ImmutableList providerConfigs = ImmutableList.copyOf(page.getValues()); + assertEquals(2, providerConfigs.size()); + checkOidcProviderConfig(providerConfigs.get(0), "oidc.provider-id1"); + checkOidcProviderConfig(providerConfigs.get(1), "oidc.provider-id2"); + assertEquals("", page.getNextPageToken()); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "GET", PROJECT_BASE_URL + "/oauthIdpConfigs"); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals(99, url.getFirst("pageSize")); + assertEquals("token", url.getFirst("nextPageToken")); + } + + @Test + public void testListZeroOidcProviderConfigs() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement("{}"); + ListProviderConfigsPage page = + FirebaseAuth.getInstance().listOidcProviderConfigs(null); + assertTrue(Iterables.isEmpty(page.getValues())); + assertEquals("", page.getNextPageToken()); + checkRequestHeaders(interceptor); + } + + @Test + public void testTenantAwareListOidcProviderConfigs() throws Exception { + final TestResponseInterceptor interceptor = initializeAppForTenantAwareUserManagement( + "TENANT_ID", + TestUtils.loadResource("listOidc.json")); + TenantAwareFirebaseAuth tenantAwareAuth = + FirebaseAuth.getInstance().getTenantManager().getAuthForTenant("TENANT_ID"); + ListProviderConfigsPage page = + tenantAwareAuth.listOidcProviderConfigs(null, 99); + + ImmutableList providerConfigs = ImmutableList.copyOf(page.getValues()); + assertEquals(2, providerConfigs.size()); + checkOidcProviderConfig(providerConfigs.get(0), "oidc.provider-id1"); + checkOidcProviderConfig(providerConfigs.get(1), "oidc.provider-id2"); + assertEquals("", page.getNextPageToken()); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "GET", TENANTS_BASE_URL + "/TENANT_ID/oauthIdpConfigs"); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals(99, url.getFirst("pageSize")); + assertNull(url.getFirst("nextPageToken")); + } + + @Test + public void testDeleteOidcProviderConfig() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement("{}"); + + FirebaseAuth.getInstance().deleteOidcProviderConfig("oidc.provider-id"); + + checkRequestHeaders(interceptor); + checkUrl(interceptor, "DELETE", PROJECT_BASE_URL + "/oauthIdpConfigs/oidc.provider-id"); + } + + @Test + public void testDeleteOidcProviderConfigAsync() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement("{}"); + + FirebaseAuth.getInstance().deleteOidcProviderConfigAsync("oidc.provider-id").get(); + + checkRequestHeaders(interceptor); + checkUrl(interceptor, "DELETE", PROJECT_BASE_URL + "/oauthIdpConfigs/oidc.provider-id"); + } + + @Test + public void testDeleteOidcProviderMissingId() throws Exception { + initializeAppForUserManagement("{}"); + + try { + FirebaseAuth.getInstance().deleteOidcProviderConfig(null); + fail("No error thrown for missing provider ID."); + } catch (IllegalArgumentException e) { + // Expected. + } + } + + @Test + public void testDeleteOidcProviderInvalidId() throws Exception { + initializeAppForUserManagement("{}"); + + try { + FirebaseAuth.getInstance().deleteOidcProviderConfig("saml.invalid-oidc-provider-id"); + fail("No error thrown for invalid provider ID."); + } catch (IllegalArgumentException e) { + // Expected. + } + } + + @Test + public void testDeleteOidcProviderConfigWithNotFoundError() { + TestResponseInterceptor interceptor = + initializeAppForUserManagementWithStatusCode(404, + "{\"error\": {\"message\": \"CONFIGURATION_NOT_FOUND\"}}"); + try { + FirebaseAuth.getInstance().deleteOidcProviderConfig("oidc.UNKNOWN"); + fail("No error thrown for invalid response"); + } catch (FirebaseAuthException e) { + assertEquals(AuthHttpClient.CONFIGURATION_NOT_FOUND_ERROR, e.getErrorCode()); } + checkUrl(interceptor, "DELETE", PROJECT_BASE_URL + "/oauthIdpConfigs/oidc.UNKNOWN"); } - private static TestResponseInterceptor initializeAppForUserManagement(String ...responses) { + @Test + public void testTenantAwareDeleteOidcProviderConfig() throws Exception { + TestResponseInterceptor interceptor = initializeAppForTenantAwareUserManagement( + "TENANT_ID", + "{}"); + TenantAwareFirebaseAuth tenantAwareAuth = + FirebaseAuth.getInstance().getTenantManager().getAuthForTenant("TENANT_ID"); + + tenantAwareAuth.deleteOidcProviderConfig("oidc.provider-id"); + + checkRequestHeaders(interceptor); + String expectedUrl = TENANTS_BASE_URL + "/TENANT_ID/oauthIdpConfigs/oidc.provider-id"; + checkUrl(interceptor, "DELETE", expectedUrl); + } + + @Test + public void testCreateSamlProvider() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("saml.json")); + SamlProviderConfig.CreateRequest createRequest = + new SamlProviderConfig.CreateRequest() + .setProviderId("saml.provider-id") + .setDisplayName("DISPLAY_NAME") + .setEnabled(true) + .setIdpEntityId("IDP_ENTITY_ID") + .setSsoUrl("https://example.com/login") + .addX509Certificate("certificate1") + .addX509Certificate("certificate2") + .setRpEntityId("RP_ENTITY_ID") + .setCallbackUrl("https://projectId.firebaseapp.com/__/auth/handler"); + + SamlProviderConfig config = FirebaseAuth.getInstance().createSamlProviderConfig(createRequest); + + checkSamlProviderConfig(config, "saml.provider-id"); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "POST", PROJECT_BASE_URL + "/inboundSamlConfigs"); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals("saml.provider-id", url.getFirst("inboundSamlConfigId")); + + GenericJson parsed = parseRequestContent(interceptor); + assertEquals("DISPLAY_NAME", parsed.get("displayName")); + assertTrue((boolean) parsed.get("enabled")); + + Map idpConfig = (Map) parsed.get("idpConfig"); + assertNotNull(idpConfig); + assertEquals(3, idpConfig.size()); + assertEquals("IDP_ENTITY_ID", idpConfig.get("idpEntityId")); + assertEquals("https://example.com/login", idpConfig.get("ssoUrl")); + List idpCertificates = (List) idpConfig.get("idpCertificates"); + assertNotNull(idpCertificates); + assertEquals(2, idpCertificates.size()); + assertEquals(ImmutableMap.of("x509Certificate", "certificate1"), idpCertificates.get(0)); + assertEquals(ImmutableMap.of("x509Certificate", "certificate2"), idpCertificates.get(1)); + + Map spConfig = (Map) parsed.get("spConfig"); + assertNotNull(spConfig); + assertEquals(2, spConfig.size()); + assertEquals("RP_ENTITY_ID", spConfig.get("spEntityId")); + assertEquals("https://projectId.firebaseapp.com/__/auth/handler", spConfig.get("callbackUri")); + } + + @Test + public void testCreateSamlProviderAsync() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("saml.json")); + SamlProviderConfig.CreateRequest createRequest = + new SamlProviderConfig.CreateRequest() + .setProviderId("saml.provider-id") + .setDisplayName("DISPLAY_NAME") + .setEnabled(true) + .setIdpEntityId("IDP_ENTITY_ID") + .setSsoUrl("https://example.com/login") + .addX509Certificate("certificate1") + .addX509Certificate("certificate2") + .setRpEntityId("RP_ENTITY_ID") + .setCallbackUrl("https://projectId.firebaseapp.com/__/auth/handler"); + + SamlProviderConfig config = + FirebaseAuth.getInstance().createSamlProviderConfigAsync(createRequest).get(); + + checkSamlProviderConfig(config, "saml.provider-id"); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "POST", PROJECT_BASE_URL + "/inboundSamlConfigs"); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals("saml.provider-id", url.getFirst("inboundSamlConfigId")); + + GenericJson parsed = parseRequestContent(interceptor); + assertEquals("DISPLAY_NAME", parsed.get("displayName")); + assertTrue((boolean) parsed.get("enabled")); + + Map idpConfig = (Map) parsed.get("idpConfig"); + assertNotNull(idpConfig); + assertEquals(3, idpConfig.size()); + assertEquals("IDP_ENTITY_ID", idpConfig.get("idpEntityId")); + assertEquals("https://example.com/login", idpConfig.get("ssoUrl")); + List idpCertificates = (List) idpConfig.get("idpCertificates"); + assertNotNull(idpCertificates); + assertEquals(2, idpCertificates.size()); + assertEquals(ImmutableMap.of("x509Certificate", "certificate1"), idpCertificates.get(0)); + assertEquals(ImmutableMap.of("x509Certificate", "certificate2"), idpCertificates.get(1)); + + Map spConfig = (Map) parsed.get("spConfig"); + assertNotNull(spConfig); + assertEquals(2, spConfig.size()); + assertEquals("RP_ENTITY_ID", spConfig.get("spEntityId")); + assertEquals("https://projectId.firebaseapp.com/__/auth/handler", spConfig.get("callbackUri")); + } + + @Test + public void testCreateSamlProviderMinimal() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("saml.json")); + // Only the 'enabled', 'displayName', and 'signRequest' fields can be omitted from a SAML + // provider config creation request. + SamlProviderConfig.CreateRequest createRequest = + new SamlProviderConfig.CreateRequest() + .setProviderId("saml.provider-id") + .setIdpEntityId("IDP_ENTITY_ID") + .setSsoUrl("https://example.com/login") + .addX509Certificate("certificate") + .setRpEntityId("RP_ENTITY_ID") + .setCallbackUrl("https://projectId.firebaseapp.com/__/auth/handler"); + + FirebaseAuth.getInstance().createSamlProviderConfig(createRequest); + + checkRequestHeaders(interceptor); + checkUrl(interceptor, "POST", PROJECT_BASE_URL + "/inboundSamlConfigs"); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals("saml.provider-id", url.getFirst("inboundSamlConfigId")); + + GenericJson parsed = parseRequestContent(interceptor); + assertNull(parsed.get("displayName")); + assertNull(parsed.get("enabled")); + Map idpConfig = (Map) parsed.get("idpConfig"); + assertNotNull(idpConfig); + assertEquals(3, idpConfig.size()); + assertEquals("IDP_ENTITY_ID", idpConfig.get("idpEntityId")); + assertEquals("https://example.com/login", idpConfig.get("ssoUrl")); + List idpCertificates = (List) idpConfig.get("idpCertificates"); + assertNotNull(idpCertificates); + assertEquals(1, idpCertificates.size()); + assertEquals(ImmutableMap.of("x509Certificate", "certificate"), idpCertificates.get(0)); + Map spConfig = (Map) parsed.get("spConfig"); + assertNotNull(spConfig); + assertEquals(2, spConfig.size()); + assertEquals("RP_ENTITY_ID", spConfig.get("spEntityId")); + assertEquals("https://projectId.firebaseapp.com/__/auth/handler", spConfig.get("callbackUri")); + } + + @Test + public void testCreateSamlProviderError() { + TestResponseInterceptor interceptor = + initializeAppForUserManagementWithStatusCode(404, + "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}"); + SamlProviderConfig.CreateRequest createRequest = + new SamlProviderConfig.CreateRequest().setProviderId("saml.provider-id"); + try { + FirebaseAuth.getInstance().createSamlProviderConfig(createRequest); + fail("No error thrown for invalid response"); + } catch (FirebaseAuthException e) { + assertEquals(AuthHttpClient.INTERNAL_ERROR, e.getErrorCode()); + } + checkUrl(interceptor, "POST", PROJECT_BASE_URL + "/inboundSamlConfigs"); + } + + @Test + public void testCreateSamlProviderMissingId() throws Exception { + initializeAppForUserManagement(TestUtils.loadResource("saml.json")); + SamlProviderConfig.CreateRequest createRequest = + new SamlProviderConfig.CreateRequest() + .setDisplayName("DISPLAY_NAME") + .setEnabled(true) + .setIdpEntityId("IDP_ENTITY_ID") + .setSsoUrl("https://example.com/login") + .addX509Certificate("certificate1") + .addX509Certificate("certificate2") + .setRpEntityId("RP_ENTITY_ID") + .setCallbackUrl("https://projectId.firebaseapp.com/__/auth/handler"); + try { + FirebaseAuth.getInstance().createSamlProviderConfig(createRequest); + fail("No error thrown for invalid response"); + } catch (IllegalArgumentException e) { + // Expected. + } + } + + @Test + public void testTenantAwareCreateSamlProvider() throws Exception { + TestResponseInterceptor interceptor = initializeAppForTenantAwareUserManagement( + "TENANT_ID", + TestUtils.loadResource("saml.json")); + SamlProviderConfig.CreateRequest createRequest = + new SamlProviderConfig.CreateRequest() + .setProviderId("saml.provider-id") + .setDisplayName("DISPLAY_NAME") + .setEnabled(true) + .setIdpEntityId("IDP_ENTITY_ID") + .setSsoUrl("https://example.com/login") + .addX509Certificate("certificate1") + .addX509Certificate("certificate2") + .setRpEntityId("RP_ENTITY_ID") + .setCallbackUrl("https://projectId.firebaseapp.com/__/auth/handler"); + TenantAwareFirebaseAuth tenantAwareAuth = + FirebaseAuth.getInstance().getTenantManager().getAuthForTenant("TENANT_ID"); + + tenantAwareAuth.createSamlProviderConfig(createRequest); + + checkRequestHeaders(interceptor); + checkUrl(interceptor, "POST", TENANTS_BASE_URL + "/TENANT_ID/inboundSamlConfigs"); + } + + @Test + public void testUpdateSamlProvider() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("saml.json")); + SamlProviderConfig.UpdateRequest updateRequest = + new SamlProviderConfig.UpdateRequest("saml.provider-id") + .setDisplayName("DISPLAY_NAME") + .setEnabled(true) + .setIdpEntityId("IDP_ENTITY_ID") + .setSsoUrl("https://example.com/login") + .addX509Certificate("certificate1") + .addX509Certificate("certificate2") + .setRpEntityId("RP_ENTITY_ID") + .setCallbackUrl("https://projectId.firebaseapp.com/__/auth/handler"); + + SamlProviderConfig config = FirebaseAuth.getInstance().updateSamlProviderConfig(updateRequest); + + checkSamlProviderConfig(config, "saml.provider-id"); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "PATCH", PROJECT_BASE_URL + "/inboundSamlConfigs/saml.provider-id"); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals( + "displayName,enabled,idpConfig.idpCertificates,idpConfig.idpEntityId,idpConfig.ssoUrl," + + "spConfig.callbackUri,spConfig.spEntityId", + url.getFirst("updateMask")); + + GenericJson parsed = parseRequestContent(interceptor); + assertEquals("DISPLAY_NAME", parsed.get("displayName")); + assertTrue((boolean) parsed.get("enabled")); + + Map idpConfig = (Map) parsed.get("idpConfig"); + assertNotNull(idpConfig); + assertEquals(3, idpConfig.size()); + assertEquals("IDP_ENTITY_ID", idpConfig.get("idpEntityId")); + assertEquals("https://example.com/login", idpConfig.get("ssoUrl")); + List idpCertificates = (List) idpConfig.get("idpCertificates"); + assertNotNull(idpCertificates); + assertEquals(2, idpCertificates.size()); + assertEquals(ImmutableMap.of("x509Certificate", "certificate1"), idpCertificates.get(0)); + assertEquals(ImmutableMap.of("x509Certificate", "certificate2"), idpCertificates.get(1)); + + Map spConfig = (Map) parsed.get("spConfig"); + assertNotNull(spConfig); + assertEquals(2, spConfig.size()); + assertEquals("RP_ENTITY_ID", spConfig.get("spEntityId")); + assertEquals("https://projectId.firebaseapp.com/__/auth/handler", spConfig.get("callbackUri")); + } + + @Test + public void testUpdateSamlProviderAsync() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("saml.json")); + SamlProviderConfig.UpdateRequest updateRequest = + new SamlProviderConfig.UpdateRequest("saml.provider-id") + .setDisplayName("DISPLAY_NAME") + .setEnabled(true) + .setIdpEntityId("IDP_ENTITY_ID") + .setSsoUrl("https://example.com/login") + .addX509Certificate("certificate1") + .addX509Certificate("certificate2") + .setRpEntityId("RP_ENTITY_ID") + .setCallbackUrl("https://projectId.firebaseapp.com/__/auth/handler"); + + SamlProviderConfig config = + FirebaseAuth.getInstance().updateSamlProviderConfigAsync(updateRequest).get(); + + checkSamlProviderConfig(config, "saml.provider-id"); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "PATCH", PROJECT_BASE_URL + "/inboundSamlConfigs/saml.provider-id"); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals( + "displayName,enabled,idpConfig.idpCertificates,idpConfig.idpEntityId,idpConfig.ssoUrl," + + "spConfig.callbackUri,spConfig.spEntityId", + url.getFirst("updateMask")); + + GenericJson parsed = parseRequestContent(interceptor); + assertEquals("DISPLAY_NAME", parsed.get("displayName")); + assertTrue((boolean) parsed.get("enabled")); + + Map idpConfig = (Map) parsed.get("idpConfig"); + assertNotNull(idpConfig); + assertEquals(3, idpConfig.size()); + assertEquals("IDP_ENTITY_ID", idpConfig.get("idpEntityId")); + assertEquals("https://example.com/login", idpConfig.get("ssoUrl")); + List idpCertificates = (List) idpConfig.get("idpCertificates"); + assertNotNull(idpCertificates); + assertEquals(2, idpCertificates.size()); + assertEquals(ImmutableMap.of("x509Certificate", "certificate1"), idpCertificates.get(0)); + assertEquals(ImmutableMap.of("x509Certificate", "certificate2"), idpCertificates.get(1)); + + Map spConfig = (Map) parsed.get("spConfig"); + assertNotNull(spConfig); + assertEquals(2, spConfig.size()); + assertEquals("RP_ENTITY_ID", spConfig.get("spEntityId")); + assertEquals("https://projectId.firebaseapp.com/__/auth/handler", spConfig.get("callbackUri")); + } + + @Test + public void testUpdateSamlProviderMinimal() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("saml.json")); + SamlProviderConfig.UpdateRequest request = + new SamlProviderConfig.UpdateRequest("saml.provider-id").setDisplayName("DISPLAY_NAME"); + + SamlProviderConfig config = FirebaseAuth.getInstance().updateSamlProviderConfig(request); + + checkSamlProviderConfig(config, "saml.provider-id"); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "PATCH", PROJECT_BASE_URL + "/inboundSamlConfigs/saml.provider-id"); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals("displayName", url.getFirst("updateMask")); + GenericJson parsed = parseRequestContent(interceptor); + assertEquals(1, parsed.size()); + assertEquals("DISPLAY_NAME", parsed.get("displayName")); + } + + @Test + public void testUpdateSamlProviderConfigNoValues() throws Exception { + initializeAppForUserManagement(TestUtils.loadResource("saml.json")); + try { + FirebaseAuth.getInstance().updateSamlProviderConfig( + new SamlProviderConfig.UpdateRequest("saml.provider-id")); + fail("No error thrown for empty provider config update"); + } catch (IllegalArgumentException e) { + // expected + } + } + + @Test + public void testUpdateSamlProviderConfigError() throws Exception { + TestResponseInterceptor interceptor = + initializeAppForUserManagementWithStatusCode(404, + "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}"); + SamlProviderConfig.UpdateRequest request = + new SamlProviderConfig.UpdateRequest("saml.provider-id").setDisplayName("DISPLAY_NAME"); + try { + FirebaseAuth.getInstance().updateSamlProviderConfig(request); + fail("No error thrown for invalid response"); + } catch (FirebaseAuthException e) { + assertEquals(AuthHttpClient.INTERNAL_ERROR, e.getErrorCode()); + } + checkUrl(interceptor, "PATCH", PROJECT_BASE_URL + "/inboundSamlConfigs/saml.provider-id"); + } + + @Test + public void testTenantAwareUpdateSamlProvider() throws Exception { + TestResponseInterceptor interceptor = initializeAppForTenantAwareUserManagement( + "TENANT_ID", + TestUtils.loadResource("saml.json")); + TenantAwareFirebaseAuth tenantAwareAuth = + FirebaseAuth.getInstance().getTenantManager().getAuthForTenant("TENANT_ID"); + SamlProviderConfig.UpdateRequest updateRequest = + new SamlProviderConfig.UpdateRequest("saml.provider-id") + .setDisplayName("DISPLAY_NAME") + .setEnabled(true) + .setIdpEntityId("IDP_ENTITY_ID") + .setSsoUrl("https://example.com/login"); + + SamlProviderConfig config = tenantAwareAuth.updateSamlProviderConfig(updateRequest); + + checkSamlProviderConfig(config, "saml.provider-id"); + checkRequestHeaders(interceptor); + String expectedUrl = TENANTS_BASE_URL + "/TENANT_ID/inboundSamlConfigs/saml.provider-id"; + checkUrl(interceptor, "PATCH", expectedUrl); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals("displayName,enabled,idpConfig.idpEntityId,idpConfig.ssoUrl", + url.getFirst("updateMask")); + + GenericJson parsed = parseRequestContent(interceptor); + assertEquals("DISPLAY_NAME", parsed.get("displayName")); + assertTrue((boolean) parsed.get("enabled")); + Map idpConfig = (Map) parsed.get("idpConfig"); + assertNotNull(idpConfig); + assertEquals(2, idpConfig.size()); + assertEquals("IDP_ENTITY_ID", idpConfig.get("idpEntityId")); + assertEquals("https://example.com/login", idpConfig.get("ssoUrl")); + } + + @Test + public void testGetSamlProviderConfig() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("saml.json")); + + SamlProviderConfig config = + FirebaseAuth.getInstance().getSamlProviderConfig("saml.provider-id"); + + checkSamlProviderConfig(config, "saml.provider-id"); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "GET", PROJECT_BASE_URL + "/inboundSamlConfigs/saml.provider-id"); + } + + @Test + public void testGetSamlProviderConfigAsync() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("saml.json")); + + SamlProviderConfig config = + FirebaseAuth.getInstance().getSamlProviderConfigAsync("saml.provider-id").get(); + + checkSamlProviderConfig(config, "saml.provider-id"); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "GET", PROJECT_BASE_URL + "/inboundSamlConfigs/saml.provider-id"); + } + + @Test + public void testGetSamlProviderConfigMissingId() throws Exception { + initializeAppForUserManagement(TestUtils.loadResource("saml.json")); + + try { + FirebaseAuth.getInstance().getSamlProviderConfig(null); + fail("No error thrown for missing provider ID."); + } catch (IllegalArgumentException e) { + // Expected. + } + } + + @Test + public void testGetSamlProviderConfigInvalidId() throws Exception { + initializeAppForUserManagement(TestUtils.loadResource("saml.json")); + + try { + FirebaseAuth.getInstance().getSamlProviderConfig("oidc.invalid-saml-provider-id"); + fail("No error thrown for invalid provider ID."); + } catch (IllegalArgumentException e) { + // Expected. + } + } + + @Test + public void testGetSamlProviderConfigWithNotFoundError() { + TestResponseInterceptor interceptor = + initializeAppForUserManagementWithStatusCode(404, + "{\"error\": {\"message\": \"CONFIGURATION_NOT_FOUND\"}}"); + try { + FirebaseAuth.getInstance().getSamlProviderConfig("saml.provider-id"); + fail("No error thrown for invalid response"); + } catch (FirebaseAuthException e) { + assertEquals(AuthHttpClient.CONFIGURATION_NOT_FOUND_ERROR, e.getErrorCode()); + } + checkUrl(interceptor, "GET", PROJECT_BASE_URL + "/inboundSamlConfigs/saml.provider-id"); + } + + @Test + public void testGetTenantAwareSamlProviderConfig() throws Exception { + TestResponseInterceptor interceptor = initializeAppForTenantAwareUserManagement( + "TENANT_ID", + TestUtils.loadResource("saml.json")); + TenantAwareFirebaseAuth tenantAwareAuth = + FirebaseAuth.getInstance().getTenantManager().getAuthForTenant("TENANT_ID"); + + SamlProviderConfig config = tenantAwareAuth.getSamlProviderConfig("saml.provider-id"); + + checkSamlProviderConfig(config, "saml.provider-id"); + checkRequestHeaders(interceptor); + String expectedUrl = TENANTS_BASE_URL + "/TENANT_ID/inboundSamlConfigs/saml.provider-id"; + checkUrl(interceptor, "GET", expectedUrl); + } + + @Test + public void testListSamlProviderConfigs() throws Exception { + final TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("listSaml.json")); + ListProviderConfigsPage page = + FirebaseAuth.getInstance().listSamlProviderConfigs(null, 99); + + ImmutableList providerConfigs = ImmutableList.copyOf(page.getValues()); + assertEquals(2, providerConfigs.size()); + checkSamlProviderConfig(providerConfigs.get(0), "saml.provider-id1"); + checkSamlProviderConfig(providerConfigs.get(1), "saml.provider-id2"); + assertEquals("", page.getNextPageToken()); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "GET", PROJECT_BASE_URL + "/inboundSamlConfigs"); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals(99, url.getFirst("pageSize")); + assertNull(url.getFirst("nextPageToken")); + } + + @Test + public void testListSamlProviderConfigsAsync() throws Exception { + final TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("listSaml.json")); + ListProviderConfigsPage page = + FirebaseAuth.getInstance().listSamlProviderConfigsAsync(null, 99).get(); + + ImmutableList providerConfigs = ImmutableList.copyOf(page.getValues()); + assertEquals(2, providerConfigs.size()); + checkSamlProviderConfig(providerConfigs.get(0), "saml.provider-id1"); + checkSamlProviderConfig(providerConfigs.get(1), "saml.provider-id2"); + assertEquals("", page.getNextPageToken()); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "GET", PROJECT_BASE_URL + "/inboundSamlConfigs"); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals(99, url.getFirst("pageSize")); + assertNull(url.getFirst("nextPageToken")); + } + + @Test + public void testListSamlProviderConfigsError() throws Exception { + TestResponseInterceptor interceptor = + initializeAppForUserManagementWithStatusCode(404, + "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}"); + + try { + FirebaseAuth.getInstance().listSamlProviderConfigs(null, 99); + fail("No error thrown for invalid response"); + } catch (FirebaseAuthException e) { + assertEquals(AuthHttpClient.INTERNAL_ERROR, e.getErrorCode()); + } + checkUrl(interceptor, "GET", PROJECT_BASE_URL + "/inboundSamlConfigs"); + } + + @Test + public void testListSamlProviderConfigsWithPageToken() throws Exception { + final TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("listSaml.json")); + ListProviderConfigsPage page = + FirebaseAuth.getInstance().listSamlProviderConfigs("token", 99); + + ImmutableList providerConfigs = ImmutableList.copyOf(page.getValues()); + assertEquals(2, providerConfigs.size()); + checkSamlProviderConfig(providerConfigs.get(0), "saml.provider-id1"); + checkSamlProviderConfig(providerConfigs.get(1), "saml.provider-id2"); + assertEquals("", page.getNextPageToken()); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "GET", PROJECT_BASE_URL + "/inboundSamlConfigs"); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals(99, url.getFirst("pageSize")); + assertEquals("token", url.getFirst("nextPageToken")); + } + + @Test + public void testListZeroSamlProviderConfigs() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement("{}"); + ListProviderConfigsPage page = + FirebaseAuth.getInstance().listSamlProviderConfigs(null); + assertTrue(Iterables.isEmpty(page.getValues())); + assertEquals("", page.getNextPageToken()); + checkRequestHeaders(interceptor); + } + + @Test + public void testTenantAwareListSamlProviderConfigs() throws Exception { + final TestResponseInterceptor interceptor = initializeAppForTenantAwareUserManagement( + "TENANT_ID", + TestUtils.loadResource("listSaml.json")); + TenantAwareFirebaseAuth tenantAwareAuth = + FirebaseAuth.getInstance().getTenantManager().getAuthForTenant("TENANT_ID"); + ListProviderConfigsPage page = + tenantAwareAuth.listSamlProviderConfigs(null, 99); + + ImmutableList providerConfigs = ImmutableList.copyOf(page.getValues()); + assertEquals(2, providerConfigs.size()); + checkSamlProviderConfig(providerConfigs.get(0), "saml.provider-id1"); + checkSamlProviderConfig(providerConfigs.get(1), "saml.provider-id2"); + assertEquals("", page.getNextPageToken()); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "GET", TENANTS_BASE_URL + "/TENANT_ID/inboundSamlConfigs"); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals(99, url.getFirst("pageSize")); + assertNull(url.getFirst("nextPageToken")); + } + + @Test + public void testDeleteSamlProviderConfig() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement("{}"); + + FirebaseAuth.getInstance().deleteSamlProviderConfig("saml.provider-id"); + + checkRequestHeaders(interceptor); + checkUrl(interceptor, "DELETE", PROJECT_BASE_URL + "/inboundSamlConfigs/saml.provider-id"); + } + + @Test + public void testDeleteSamlProviderConfigAsync() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement("{}"); + + FirebaseAuth.getInstance().deleteSamlProviderConfigAsync("saml.provider-id").get(); + + checkRequestHeaders(interceptor); + checkUrl(interceptor, "DELETE", PROJECT_BASE_URL + "/inboundSamlConfigs/saml.provider-id"); + } + + @Test + public void testDeleteSamlProviderMissingId() throws Exception { + initializeAppForUserManagement("{}"); + + try { + FirebaseAuth.getInstance().deleteSamlProviderConfig(null); + fail("No error thrown for missing provider ID."); + } catch (IllegalArgumentException e) { + // Expected. + } + } + + @Test + public void testDeleteSamlProviderInvalidId() throws Exception { + initializeAppForUserManagement("{}"); + + try { + FirebaseAuth.getInstance().deleteSamlProviderConfig("oidc.invalid-saml-provider-id"); + fail("No error thrown for invalid provider ID."); + } catch (IllegalArgumentException e) { + // Expected. + } + } + + @Test + public void testDeleteSamlProviderConfigWithNotFoundError() { + TestResponseInterceptor interceptor = + initializeAppForUserManagementWithStatusCode(404, + "{\"error\": {\"message\": \"CONFIGURATION_NOT_FOUND\"}}"); + try { + FirebaseAuth.getInstance().deleteSamlProviderConfig("saml.UNKNOWN"); + fail("No error thrown for invalid response"); + } catch (FirebaseAuthException e) { + assertEquals(AuthHttpClient.CONFIGURATION_NOT_FOUND_ERROR, e.getErrorCode()); + } + checkUrl(interceptor, "DELETE", PROJECT_BASE_URL + "/inboundSamlConfigs/saml.UNKNOWN"); + } + + @Test + public void testTenantAwareDeleteSamlProviderConfig() throws Exception { + TestResponseInterceptor interceptor = initializeAppForTenantAwareUserManagement( + "TENANT_ID", + "{}"); + TenantAwareFirebaseAuth tenantAwareAuth = + FirebaseAuth.getInstance().getTenantManager().getAuthForTenant("TENANT_ID"); + + tenantAwareAuth.deleteSamlProviderConfig("saml.provider-id"); + + checkRequestHeaders(interceptor); + String expectedUrl = TENANTS_BASE_URL + "/TENANT_ID/inboundSamlConfigs/saml.provider-id"; + checkUrl(interceptor, "DELETE", expectedUrl); + } + + private static TestResponseInterceptor initializeAppForUserManagementWithStatusCode( + int statusCode, String response) { + FirebaseApp.initializeApp(new FirebaseOptions.Builder() + .setCredentials(credentials) + .setHttpTransport( + new MockHttpTransport.Builder().setLowLevelHttpResponse( + new MockLowLevelHttpResponse().setContent(response).setStatusCode(statusCode)).build()) + .setProjectId("test-project-id") + .build()); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + FirebaseAuth.getInstance().getUserManager().setInterceptor(interceptor); + return interceptor; + } + + private static TestResponseInterceptor initializeAppForTenantAwareUserManagement( + String tenantId, + String... responses) { + initializeAppWithResponses(responses); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + TenantManager tenantManager = FirebaseAuth.getInstance().getTenantManager(); + AbstractFirebaseAuth auth = tenantManager.getAuthForTenant(tenantId); + auth.getUserManager().setInterceptor(interceptor); + return interceptor; + } + + private static TestResponseInterceptor initializeAppForUserManagement(String... responses) { + initializeAppWithResponses(responses); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + FirebaseAuth.getInstance().getUserManager().setInterceptor(interceptor); + return interceptor; + } + + private static void initializeAppWithResponses(String... responses) { List mocks = new ArrayList<>(); for (String response : responses) { mocks.add(new MockLowLevelHttpResponse().setContent(response)); @@ -1428,11 +2584,13 @@ private static TestResponseInterceptor initializeAppForUserManagement(String ... .setHttpTransport(transport) .setProjectId("test-project-id") .build()); - FirebaseAuth auth = FirebaseAuth.getInstance(); - FirebaseUserManager userManager = auth.getUserManager(); - TestResponseInterceptor interceptor = new TestResponseInterceptor(); - userManager.setInterceptor(interceptor); - return interceptor; + } + + private static GenericJson parseRequestContent(TestResponseInterceptor interceptor) + throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + interceptor.getResponse().getRequest().getContent().writeTo(out); + return JSON_FACTORY.fromString(new String(out.toByteArray()), GenericJson.class); } private static FirebaseAuth getRetryDisabledAuth(MockLowLevelHttpResponse response) { @@ -1444,15 +2602,19 @@ private static FirebaseAuth getRetryDisabledAuth(MockLowLevelHttpResponse respon .setProjectId("test-project-id") .setHttpTransport(transport) .build()); - return FirebaseAuth.builder() - .setFirebaseApp(app) - .setUserManager(new Supplier() { - @Override - public FirebaseUserManager get() { - return new FirebaseUserManager(app, transport.createRequestFactory()); - } - }) - .build(); + return new FirebaseAuth( + AbstractFirebaseAuth.builder() + .setFirebaseApp(app) + .setUserManager(new Supplier() { + @Override + public FirebaseUserManager get() { + return FirebaseUserManager + .builder() + .setFirebaseApp(app) + .setHttpRequestFactory(transport.createRequestFactory()) + .build(); + } + })); } private static void checkUserRecord(UserRecord userRecord) { @@ -1467,6 +2629,7 @@ private static void checkUserRecord(UserRecord userRecord) { assertFalse(userRecord.isDisabled()); assertTrue(userRecord.isEmailVerified()); assertEquals(1494364393000L, userRecord.getTokensValidAfterTimestamp()); + assertEquals("testTenant", userRecord.getTenantId()); UserInfo provider = userRecord.getProviderData()[0]; assertEquals("testuser@example.com", provider.getUid()); @@ -1488,6 +2651,25 @@ private static void checkUserRecord(UserRecord userRecord) { assertEquals("gold", claims.get("package")); } + private static void checkOidcProviderConfig(OidcProviderConfig config, String providerId) { + assertEquals(providerId, config.getProviderId()); + assertEquals("DISPLAY_NAME", config.getDisplayName()); + assertTrue(config.isEnabled()); + assertEquals("CLIENT_ID", config.getClientId()); + assertEquals("https://oidc.com/issuer", config.getIssuer()); + } + + private static void checkSamlProviderConfig(SamlProviderConfig config, String providerId) { + assertEquals(providerId, config.getProviderId()); + assertEquals("DISPLAY_NAME", config.getDisplayName()); + assertTrue(config.isEnabled()); + assertEquals("IDP_ENTITY_ID", config.getIdpEntityId()); + assertEquals("https://example.com/login", config.getSsoUrl()); + assertEquals(ImmutableList.of("certificate1", "certificate2"), config.getX509Certificates()); + assertEquals("RP_ENTITY_ID", config.getRpEntityId()); + assertEquals("https://projectId.firebaseapp.com/__/auth/handler", config.getCallbackUrl()); + } + private static void checkRequestHeaders(TestResponseInterceptor interceptor) { HttpHeaders headers = interceptor.getResponse().getRequest().getHeaders(); String auth = "Bearer " + TEST_TOKEN; @@ -1497,8 +2679,20 @@ private static void checkRequestHeaders(TestResponseInterceptor interceptor) { assertEquals(clientVersion, headers.getFirstHeaderStringValue("X-Client-Version")); } + private static void checkUrl(TestResponseInterceptor interceptor, String method, String url) { + HttpRequest request = interceptor.getResponse().getRequest(); + if (method.equals("PATCH")) { + assertEquals("PATCH", + request.getHeaders().getFirstHeaderStringValue("X-HTTP-Method-Override")); + assertEquals("POST", request.getRequestMethod()); + } else { + assertEquals(method, request.getRequestMethod()); + } + assertEquals(url, request.getUrl().toString().split("\\?")[0]); + } + private interface UserManagerOp { void call(FirebaseAuth auth) throws Exception; } - + } diff --git a/src/test/java/com/google/firebase/auth/GetUsersIT.java b/src/test/java/com/google/firebase/auth/GetUsersIT.java index efe2f783f..a0bdcb6c6 100644 --- a/src/test/java/com/google/firebase/auth/GetUsersIT.java +++ b/src/test/java/com/google/firebase/auth/GetUsersIT.java @@ -21,6 +21,7 @@ import com.google.common.collect.ImmutableList; import com.google.firebase.FirebaseApp; +import com.google.firebase.auth.UserTestUtils.RandomUser; import com.google.firebase.testing.IntegrationTestUtils; import java.util.Collection; @@ -44,18 +45,17 @@ public static void setUpClass() throws Exception { testUser2 = FirebaseAuthIT.newUserWithParams(auth); testUser3 = FirebaseAuthIT.newUserWithParams(auth); - FirebaseAuthIT.RandomUser randomUser = FirebaseAuthIT.RandomUser.create(); - importUserUid = randomUser.uid; - String phone = FirebaseAuthIT.randomPhoneNumber(); + RandomUser randomUser = UserTestUtils.generateRandomUserInfo(); + importUserUid = randomUser.getUid(); UserImportResult result = auth.importUsers(ImmutableList.of( ImportUserRecord.builder() - .setUid(randomUser.uid) - .setEmail(randomUser.email) - .setPhoneNumber(phone) + .setUid(randomUser.getUid()) + .setEmail(randomUser.getEmail()) + .setPhoneNumber(randomUser.getPhoneNumber()) .addUserProvider( UserProvider.builder() .setProviderId("google.com") - .setUid("google_" + randomUser.uid) + .setUid("google_" + randomUser.getUid()) .build()) .build() )); diff --git a/src/test/java/com/google/firebase/auth/ListProviderConfigsPageTest.java b/src/test/java/com/google/firebase/auth/ListProviderConfigsPageTest.java new file mode 100644 index 000000000..ba08f9c77 --- /dev/null +++ b/src/test/java/com/google/firebase/auth/ListProviderConfigsPageTest.java @@ -0,0 +1,376 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.auth; + +import static junit.framework.TestCase.assertTrue; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; + +import com.google.api.client.googleapis.util.Utils; +import com.google.common.collect.ImmutableList; +import com.google.firebase.auth.internal.ListOidcProviderConfigsResponse; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; + +import org.junit.Test; + +public class ListProviderConfigsPageTest { + + @Test + public void testSinglePage() throws FirebaseAuthException, IOException { + TestProviderConfigSource source = new TestProviderConfigSource(3); + ListProviderConfigsPage page = + new ListProviderConfigsPage.Factory(source).create(); + assertFalse(page.hasNextPage()); + assertEquals(ListProviderConfigsPage.END_OF_LIST, page.getNextPageToken()); + assertNull(page.getNextPage()); + + ImmutableList providerConfigs = ImmutableList.copyOf(page.getValues()); + assertEquals(3, providerConfigs.size()); + for (int i = 0; i < 3; i++) { + assertEquals("oidc.provider-id-" + i, providerConfigs.get(i).getProviderId()); + } + assertEquals(1, source.calls.size()); + assertNull(source.calls.get(0)); + } + + @Test + public void testMultiplePages() throws FirebaseAuthException, IOException { + ListOidcProviderConfigsResponse response = new ListOidcProviderConfigsResponse( + ImmutableList.of( + newOidcProviderConfig("oidc.provider-id-0"), + newOidcProviderConfig("oidc.provider-id-1"), + newOidcProviderConfig("oidc.provider-id-2")), + "token"); + TestProviderConfigSource source = new TestProviderConfigSource(response); + ListProviderConfigsPage page1 = + new ListProviderConfigsPage.Factory(source).create(); + assertTrue(page1.hasNextPage()); + assertEquals("token", page1.getNextPageToken()); + ImmutableList providerConfigs = ImmutableList.copyOf(page1.getValues()); + assertEquals(3, providerConfigs.size()); + for (int i = 0; i < 3; i++) { + assertEquals("oidc.provider-id-" + i, providerConfigs.get(i).getProviderId()); + } + + response = new ListOidcProviderConfigsResponse( + ImmutableList.of( + newOidcProviderConfig("oidc.provider-id-3"), + newOidcProviderConfig("oidc.provider-id-4"), + newOidcProviderConfig("oidc.provider-id-5")), + ListProviderConfigsPage.END_OF_LIST); + source.response = response; + ListProviderConfigsPage page2 = page1.getNextPage(); + assertFalse(page2.hasNextPage()); + assertEquals(ListProviderConfigsPage.END_OF_LIST, page2.getNextPageToken()); + providerConfigs = ImmutableList.copyOf(page2.getValues()); + assertEquals(3, providerConfigs.size()); + for (int i = 3; i < 6; i++) { + assertEquals("oidc.provider-id-" + i, providerConfigs.get(i - 3).getProviderId()); + } + + assertEquals(2, source.calls.size()); + assertNull(source.calls.get(0)); + assertEquals("token", source.calls.get(1)); + + // Should iterate all provider configs from both pages + int iterations = 0; + for (OidcProviderConfig providerConfig : page1.iterateAll()) { + iterations++; + } + assertEquals(6, iterations); + assertEquals(3, source.calls.size()); + assertEquals("token", source.calls.get(2)); + + // Should only iterate provider configs in the last page + iterations = 0; + for (OidcProviderConfig providerConfig : page2.iterateAll()) { + iterations++; + } + assertEquals(3, iterations); + assertEquals(3, source.calls.size()); + } + + @Test + public void testListProviderConfigsIterable() throws FirebaseAuthException, IOException { + TestProviderConfigSource source = new TestProviderConfigSource(3); + ListProviderConfigsPage page = + new ListProviderConfigsPage.Factory(source).create(); + Iterable providerConfigs = page.iterateAll(); + + int iterations = 0; + for (OidcProviderConfig providerConfig : providerConfigs) { + assertEquals("oidc.provider-id-" + iterations, providerConfig.getProviderId()); + iterations++; + } + assertEquals(3, iterations); + assertEquals(1, source.calls.size()); + assertNull(source.calls.get(0)); + + // Should result in a new iterator + iterations = 0; + for (OidcProviderConfig providerConfig : providerConfigs) { + assertEquals("oidc.provider-id-" + iterations, providerConfig.getProviderId()); + iterations++; + } + assertEquals(3, iterations); + assertEquals(1, source.calls.size()); + assertNull(source.calls.get(0)); + } + + @Test + public void testListProviderConfigsIterator() throws FirebaseAuthException, IOException { + TestProviderConfigSource source = new TestProviderConfigSource(3); + ListProviderConfigsPage page = + new ListProviderConfigsPage.Factory(source).create(); + Iterable providerConfigs = page.iterateAll(); + Iterator iterator = providerConfigs.iterator(); + int iterations = 0; + while (iterator.hasNext()) { + assertEquals("oidc.provider-id-" + iterations, iterator.next().getProviderId()); + iterations++; + } + assertEquals(3, iterations); + assertEquals(1, source.calls.size()); + assertNull(source.calls.get(0)); + + while (iterator.hasNext()) { + fail("Should not be able to to iterate any more"); + } + try { + iterator.next(); + fail("Should not be able to iterate any more"); + } catch (NoSuchElementException expected) { + // expected + } + assertEquals(1, source.calls.size()); + } + + @Test + public void testListProviderConfigsPagedIterable() throws FirebaseAuthException, IOException { + ListOidcProviderConfigsResponse response = new ListOidcProviderConfigsResponse( + ImmutableList.of( + newOidcProviderConfig("oidc.provider-id-0"), + newOidcProviderConfig("oidc.provider-id-1"), + newOidcProviderConfig("oidc.provider-id-2")), + "token"); + TestProviderConfigSource source = new TestProviderConfigSource(response); + ListProviderConfigsPage page = + new ListProviderConfigsPage.Factory(source).create(); + int iterations = 0; + for (OidcProviderConfig providerConfig : page.iterateAll()) { + assertEquals("oidc.provider-id-" + iterations, providerConfig.getProviderId()); + iterations++; + if (iterations == 3) { + assertEquals(1, source.calls.size()); + assertNull(source.calls.get(0)); + response = new ListOidcProviderConfigsResponse( + ImmutableList.of( + newOidcProviderConfig("oidc.provider-id-3"), + newOidcProviderConfig("oidc.provider-id-4"), + newOidcProviderConfig("oidc.provider-id-5")), + ListProviderConfigsPage.END_OF_LIST); + source.response = response; + } + } + + assertEquals(6, iterations); + assertEquals(2, source.calls.size()); + assertEquals("token", source.calls.get(1)); + } + + @Test + public void testListProviderConfigsPagedIterator() throws FirebaseAuthException, IOException { + ListOidcProviderConfigsResponse response = new ListOidcProviderConfigsResponse( + ImmutableList.of( + newOidcProviderConfig("oidc.provider-id-0"), + newOidcProviderConfig("oidc.provider-id-1"), + newOidcProviderConfig("oidc.provider-id-2")), + "token"); + TestProviderConfigSource source = new TestProviderConfigSource(response); + ListProviderConfigsPage page = + new ListProviderConfigsPage.Factory(source).create(); + Iterator providerConfigs = page.iterateAll().iterator(); + int iterations = 0; + while (providerConfigs.hasNext()) { + assertEquals("oidc.provider-id-" + iterations, providerConfigs.next().getProviderId()); + iterations++; + if (iterations == 3) { + assertEquals(1, source.calls.size()); + assertNull(source.calls.get(0)); + response = new ListOidcProviderConfigsResponse( + ImmutableList.of( + newOidcProviderConfig("oidc.provider-id-3"), + newOidcProviderConfig("oidc.provider-id-4"), + newOidcProviderConfig("oidc.provider-id-5")), + ListProviderConfigsPage.END_OF_LIST); + source.response = response; + } + } + + assertEquals(6, iterations); + assertEquals(2, source.calls.size()); + assertEquals("token", source.calls.get(1)); + assertFalse(providerConfigs.hasNext()); + try { + providerConfigs.next(); + } catch (NoSuchElementException e) { + // expected + } + } + + @Test + public void testPageWithNoproviderConfigs() throws FirebaseAuthException { + ListOidcProviderConfigsResponse response = new ListOidcProviderConfigsResponse( + ImmutableList.of(), ListProviderConfigsPage.END_OF_LIST); + TestProviderConfigSource source = new TestProviderConfigSource(response); + ListProviderConfigsPage page = + new ListProviderConfigsPage.Factory(source).create(); + assertFalse(page.hasNextPage()); + assertEquals(ListProviderConfigsPage.END_OF_LIST, page.getNextPageToken()); + assertNull(page.getNextPage()); + assertEquals(0, ImmutableList.copyOf(page.getValues()).size()); + assertEquals(1, source.calls.size()); + } + + @Test + public void testIterableWithNoproviderConfigs() throws FirebaseAuthException { + ListOidcProviderConfigsResponse response = new ListOidcProviderConfigsResponse( + ImmutableList.of(), ListProviderConfigsPage.END_OF_LIST); + TestProviderConfigSource source = new TestProviderConfigSource(response); + ListProviderConfigsPage page = + new ListProviderConfigsPage.Factory(source).create(); + for (OidcProviderConfig providerConfig : page.iterateAll()) { + fail("Should not be able to iterate, but got: " + providerConfig); + } + assertEquals(1, source.calls.size()); + } + + @Test + public void testIteratorWithNoproviderConfigs() throws FirebaseAuthException { + ListOidcProviderConfigsResponse response = new ListOidcProviderConfigsResponse( + ImmutableList.of(), ListProviderConfigsPage.END_OF_LIST); + TestProviderConfigSource source = new TestProviderConfigSource(response); + + ListProviderConfigsPage page = + new ListProviderConfigsPage.Factory(source).create(); + Iterator iterator = page.iterateAll().iterator(); + while (iterator.hasNext()) { + fail("Should not be able to iterate"); + } + assertEquals(1, source.calls.size()); + } + + @Test + public void testRemove() throws FirebaseAuthException, IOException { + ListOidcProviderConfigsResponse response = new ListOidcProviderConfigsResponse( + ImmutableList.of(newOidcProviderConfig("oidc.provider-id-1")), + ListProviderConfigsPage.END_OF_LIST); + TestProviderConfigSource source = new TestProviderConfigSource(response); + + ListProviderConfigsPage page = + new ListProviderConfigsPage.Factory(source).create(); + Iterator iterator = page.iterateAll().iterator(); + while (iterator.hasNext()) { + assertNotNull(iterator.next()); + try { + iterator.remove(); + } catch (UnsupportedOperationException expected) { + // expected + } + } + } + + @Test(expected = NullPointerException.class) + public void testNullSource() { + new ListProviderConfigsPage.Factory(null); + } + + @Test + public void testInvalidPageToken() throws IOException { + TestProviderConfigSource source = new TestProviderConfigSource(1); + try { + new ListProviderConfigsPage.Factory(source, 1000, ""); + fail("No error thrown for empty page token"); + } catch (IllegalArgumentException expected) { + // expected + } + } + + @Test + public void testInvalidMaxResults() throws IOException { + TestProviderConfigSource source = new TestProviderConfigSource(1); + try { + new ListProviderConfigsPage.Factory(source, 1001, ""); + fail("No error thrown for maxResult > 1000"); + } catch (IllegalArgumentException expected) { + // expected + } + + try { + new ListProviderConfigsPage.Factory(source, 0, "next"); + fail("No error thrown for maxResult = 0"); + } catch (IllegalArgumentException expected) { + // expected + } + + try { + new ListProviderConfigsPage.Factory(source, -1, "next"); + fail("No error thrown for maxResult < 0"); + } catch (IllegalArgumentException expected) { + // expected + } + } + + private static OidcProviderConfig newOidcProviderConfig(String providerConfigId) + throws IOException { + return Utils.getDefaultJsonFactory().fromString( + String.format("{\"name\":\"%s\"}", providerConfigId), OidcProviderConfig.class); + } + + private static class TestProviderConfigSource + implements ListProviderConfigsPage.ProviderConfigSource { + + private ListOidcProviderConfigsResponse response; + private final List calls = new ArrayList<>(); + + TestProviderConfigSource(int providerConfigCount) throws IOException { + ImmutableList.Builder providerConfigs = ImmutableList.builder(); + for (int i = 0; i < providerConfigCount; i++) { + providerConfigs.add(newOidcProviderConfig("oidc.provider-id-" + i)); + } + this.response = new ListOidcProviderConfigsResponse( + providerConfigs.build(), ListProviderConfigsPage.END_OF_LIST); + } + + TestProviderConfigSource(ListOidcProviderConfigsResponse response) { + this.response = response; + } + + @Override + public ListOidcProviderConfigsResponse fetch(int maxResults, String pageToken) { + calls.add(pageToken); + return response; + } + } +} diff --git a/src/test/java/com/google/firebase/auth/ListUsersPageTest.java b/src/test/java/com/google/firebase/auth/ListUsersPageTest.java index fb4a7d275..5e848069d 100644 --- a/src/test/java/com/google/firebase/auth/ListUsersPageTest.java +++ b/src/test/java/com/google/firebase/auth/ListUsersPageTest.java @@ -27,6 +27,7 @@ import com.google.api.client.json.JsonFactory; import com.google.common.collect.ImmutableList; import com.google.common.io.BaseEncoding; +import com.google.firebase.auth.ListUsersPage; import com.google.firebase.auth.ListUsersPage.ListUsersResult; import com.google.firebase.auth.internal.DownloadAccountResponse; import java.io.IOException; @@ -45,7 +46,7 @@ public class ListUsersPageTest { @Test public void testSinglePage() throws FirebaseAuthException, IOException { TestUserSource source = new TestUserSource(3); - ListUsersPage page = new ListUsersPage.PageFactory(source).create(); + ListUsersPage page = new ListUsersPage.Factory(source).create(); assertFalse(page.hasNextPage()); assertEquals(ListUsersPage.END_OF_LIST, page.getNextPageToken()); assertNull(page.getNextPage()); @@ -68,7 +69,7 @@ public void testRedactedPasswords() throws FirebaseAuthException, IOException { newUser("user2", REDACTED_BASE64)), ListUsersPage.END_OF_LIST); TestUserSource source = new TestUserSource(result); - ListUsersPage page = new ListUsersPage.PageFactory(source).create(); + ListUsersPage page = new ListUsersPage.Factory(source).create(); assertFalse(page.hasNextPage()); assertEquals(ListUsersPage.END_OF_LIST, page.getNextPageToken()); assertNull(page.getNextPage()); @@ -89,7 +90,7 @@ public void testMultiplePages() throws FirebaseAuthException, IOException { ImmutableList.of(newUser("user0"), newUser("user1"), newUser("user2")), "token"); TestUserSource source = new TestUserSource(result); - ListUsersPage page1 = new ListUsersPage.PageFactory(source).create(); + ListUsersPage page1 = new ListUsersPage.Factory(source).create(); assertTrue(page1.hasNextPage()); assertEquals("token", page1.getNextPageToken()); ImmutableList users = ImmutableList.copyOf(page1.getValues()); @@ -136,7 +137,7 @@ public void testMultiplePages() throws FirebaseAuthException, IOException { @Test public void testListUsersIterable() throws FirebaseAuthException, IOException { TestUserSource source = new TestUserSource(3); - ListUsersPage page = new ListUsersPage.PageFactory(source).create(); + ListUsersPage page = new ListUsersPage.Factory(source).create(); Iterable users = page.iterateAll(); int iterations = 0; @@ -162,7 +163,7 @@ public void testListUsersIterable() throws FirebaseAuthException, IOException { @Test public void testListUsersIterator() throws FirebaseAuthException, IOException { TestUserSource source = new TestUserSource(3); - ListUsersPage page = new ListUsersPage.PageFactory(source).create(); + ListUsersPage page = new ListUsersPage.Factory(source).create(); Iterable users = page.iterateAll(); Iterator iterator = users.iterator(); int iterations = 0; @@ -192,7 +193,7 @@ public void testListUsersPagedIterable() throws FirebaseAuthException, IOExcepti ImmutableList.of(newUser("user0"), newUser("user1"), newUser("user2")), "token"); TestUserSource source = new TestUserSource(result); - ListUsersPage page = new ListUsersPage.PageFactory(source).create(); + ListUsersPage page = new ListUsersPage.Factory(source).create(); int iterations = 0; for (ExportedUserRecord user : page.iterateAll()) { assertEquals("user" + iterations, user.getUid()); @@ -218,7 +219,7 @@ public void testListUsersPagedIterator() throws FirebaseAuthException, IOExcepti ImmutableList.of(newUser("user0"), newUser("user1"), newUser("user2")), "token"); TestUserSource source = new TestUserSource(result); - ListUsersPage page = new ListUsersPage.PageFactory(source).create(); + ListUsersPage page = new ListUsersPage.Factory(source).create(); Iterator users = page.iterateAll().iterator(); int iterations = 0; while (users.hasNext()) { @@ -251,7 +252,7 @@ public void testPageWithNoUsers() throws FirebaseAuthException { ImmutableList.of(), ListUsersPage.END_OF_LIST); TestUserSource source = new TestUserSource(result); - ListUsersPage page = new ListUsersPage.PageFactory(source).create(); + ListUsersPage page = new ListUsersPage.Factory(source).create(); assertFalse(page.hasNextPage()); assertEquals(ListUsersPage.END_OF_LIST, page.getNextPageToken()); assertNull(page.getNextPage()); @@ -265,7 +266,7 @@ public void testIterableWithNoUsers() throws FirebaseAuthException { ImmutableList.of(), ListUsersPage.END_OF_LIST); TestUserSource source = new TestUserSource(result); - ListUsersPage page = new ListUsersPage.PageFactory(source).create(); + ListUsersPage page = new ListUsersPage.Factory(source).create(); for (ExportedUserRecord user : page.iterateAll()) { fail("Should not be able to iterate, but got: " + user); } @@ -279,7 +280,7 @@ public void testIteratorWithNoUsers() throws FirebaseAuthException { ListUsersPage.END_OF_LIST); TestUserSource source = new TestUserSource(result); - ListUsersPage page = new ListUsersPage.PageFactory(source).create(); + ListUsersPage page = new ListUsersPage.Factory(source).create(); Iterator iterator = page.iterateAll().iterator(); while (iterator.hasNext()) { fail("Should not be able to iterate"); @@ -294,7 +295,7 @@ public void testRemove() throws FirebaseAuthException, IOException { ListUsersPage.END_OF_LIST); TestUserSource source = new TestUserSource(result); - ListUsersPage page = new ListUsersPage.PageFactory(source).create(); + ListUsersPage page = new ListUsersPage.Factory(source).create(); Iterator iterator = page.iterateAll().iterator(); while (iterator.hasNext()) { assertNotNull(iterator.next()); @@ -308,14 +309,14 @@ public void testRemove() throws FirebaseAuthException, IOException { @Test(expected = NullPointerException.class) public void testNullSource() { - new ListUsersPage.PageFactory(null); + new ListUsersPage.Factory(null); } @Test public void testInvalidPageToken() throws IOException { TestUserSource source = new TestUserSource(1); try { - new ListUsersPage.PageFactory(source, 1000, ""); + new ListUsersPage.Factory(source, 1000, ""); fail("No error thrown for empty page token"); } catch (IllegalArgumentException expected) { // expected @@ -326,21 +327,21 @@ public void testInvalidPageToken() throws IOException { public void testInvalidMaxResults() throws IOException { TestUserSource source = new TestUserSource(1); try { - new ListUsersPage.PageFactory(source, 1001, ""); + new ListUsersPage.Factory(source, 1001, ""); fail("No error thrown for maxResult > 1000"); } catch (IllegalArgumentException expected) { // expected } try { - new ListUsersPage.PageFactory(source, 0, "next"); + new ListUsersPage.Factory(source, 0, "next"); fail("No error thrown for maxResult = 0"); } catch (IllegalArgumentException expected) { // expected } try { - new ListUsersPage.PageFactory(source, -1, "next"); + new ListUsersPage.Factory(source, -1, "next"); fail("No error thrown for maxResult < 0"); } catch (IllegalArgumentException expected) { // expected diff --git a/src/test/java/com/google/firebase/auth/OidcProviderConfigTest.java b/src/test/java/com/google/firebase/auth/OidcProviderConfigTest.java new file mode 100644 index 000000000..1fb4ca37b --- /dev/null +++ b/src/test/java/com/google/firebase/auth/OidcProviderConfigTest.java @@ -0,0 +1,160 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.auth; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import com.google.api.client.googleapis.util.Utils; +import com.google.api.client.json.JsonFactory; +import java.io.IOException; +import java.util.Map; +import org.junit.Test; + +public class OidcProviderConfigTest { + + private static final JsonFactory jsonFactory = Utils.getDefaultJsonFactory(); + + private static final String OIDC_JSON_STRING = + ("{" + + " 'name': 'projects/projectId/oauthIdpConfigs/oidc.provider-id'," + + " 'displayName': 'DISPLAY_NAME'," + + " 'enabled': true," + + " 'clientId': 'CLIENT_ID'," + + " 'issuer': 'https://oidc.com/issuer'" + + "}").replace("'", "\""); + + @Test + public void testJsonDeserialization() throws IOException { + OidcProviderConfig config = jsonFactory.fromString(OIDC_JSON_STRING, OidcProviderConfig.class); + + assertEquals("oidc.provider-id", config.getProviderId()); + assertEquals("DISPLAY_NAME", config.getDisplayName()); + assertTrue(config.isEnabled()); + assertEquals("CLIENT_ID", config.getClientId()); + assertEquals("https://oidc.com/issuer", config.getIssuer()); + } + + @Test + public void testCreateRequest() throws IOException { + OidcProviderConfig.CreateRequest createRequest = new OidcProviderConfig.CreateRequest(); + createRequest + .setProviderId("oidc.provider-id") + .setDisplayName("DISPLAY_NAME") + .setEnabled(false) + .setClientId("CLIENT_ID") + .setIssuer("https://oidc.com/issuer"); + + assertEquals("oidc.provider-id", createRequest.getProviderId()); + Map properties = createRequest.getProperties(); + assertEquals(properties.size(), 4); + assertEquals("DISPLAY_NAME", (String) properties.get("displayName")); + assertFalse((boolean) properties.get("enabled")); + assertEquals("CLIENT_ID", (String) properties.get("clientId")); + assertEquals("https://oidc.com/issuer", (String) properties.get("issuer")); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateRequestMissingProviderId() { + new OidcProviderConfig.CreateRequest().setProviderId(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateRequestInvalidProviderId() { + new OidcProviderConfig.CreateRequest().setProviderId("saml.provider-id"); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateRequestMissingDisplayName() { + new OidcProviderConfig.CreateRequest().setDisplayName(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateRequestMissingClientId() { + new OidcProviderConfig.CreateRequest().setClientId(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateRequestMissingIssuer() { + new OidcProviderConfig.CreateRequest().setIssuer(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateRequestInvalidIssuerUrl() { + new OidcProviderConfig.CreateRequest().setIssuer("not a valid url"); + } + + @Test + public void testUpdateRequestFromOidcProviderConfig() throws IOException { + OidcProviderConfig config = jsonFactory.fromString(OIDC_JSON_STRING, OidcProviderConfig.class); + + OidcProviderConfig.UpdateRequest updateRequest = config.updateRequest(); + + assertEquals("oidc.provider-id", updateRequest.getProviderId()); + assertTrue(updateRequest.getProperties().isEmpty()); + } + + @Test + public void testUpdateRequest() throws IOException { + OidcProviderConfig.UpdateRequest updateRequest = + new OidcProviderConfig.UpdateRequest("oidc.provider-id"); + updateRequest + .setDisplayName("DISPLAY_NAME") + .setEnabled(false) + .setClientId("CLIENT_ID") + .setIssuer("https://oidc.com/issuer"); + + assertEquals("oidc.provider-id", updateRequest.getProviderId()); + Map properties = updateRequest.getProperties(); + assertEquals(properties.size(), 4); + assertEquals("DISPLAY_NAME", (String) properties.get("displayName")); + assertFalse((boolean) properties.get("enabled")); + assertEquals("CLIENT_ID", (String) properties.get("clientId")); + assertEquals("https://oidc.com/issuer", (String) properties.get("issuer")); + } + + @Test(expected = IllegalArgumentException.class) + public void testUpdateRequestMissingProviderId() { + new OidcProviderConfig.UpdateRequest(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testUpdateRequestInvalidProviderId() { + new OidcProviderConfig.UpdateRequest("saml.provider-id"); + } + + @Test(expected = IllegalArgumentException.class) + public void testUpdateRequestMissingDisplayName() { + new OidcProviderConfig.UpdateRequest("oidc.provider-id").setDisplayName(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testUpdateRequestMissingClientId() { + new OidcProviderConfig.UpdateRequest("oidc.provider-id").setClientId(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testUpdateRequestMissingIssuer() { + new OidcProviderConfig.UpdateRequest("oidc.provider-id").setIssuer(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testUpdateRequestInvalidIssuerUrl() { + new OidcProviderConfig.UpdateRequest("oidc.provider-id").setIssuer("not a valid url"); + } +} diff --git a/src/test/java/com/google/firebase/auth/ProviderConfigTestUtils.java b/src/test/java/com/google/firebase/auth/ProviderConfigTestUtils.java new file mode 100644 index 000000000..c01ac6501 --- /dev/null +++ b/src/test/java/com/google/firebase/auth/ProviderConfigTestUtils.java @@ -0,0 +1,125 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.auth; + +import static com.google.common.base.Preconditions.checkArgument; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import com.google.firebase.auth.internal.AuthHttpClient; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutionException; +import org.junit.rules.ExternalResource; + +public class ProviderConfigTestUtils { + + public static void assertOidcProviderConfigDoesNotExist( + AbstractFirebaseAuth firebaseAuth, String providerId) throws Exception { + try { + firebaseAuth.getOidcProviderConfigAsync(providerId).get(); + fail("No error thrown for getting a deleted OIDC provider config."); + } catch (ExecutionException e) { + assertTrue(e.getCause() instanceof FirebaseAuthException); + assertEquals(AuthHttpClient.CONFIGURATION_NOT_FOUND_ERROR, + ((FirebaseAuthException) e.getCause()).getErrorCode()); + } + } + + public static void assertSamlProviderConfigDoesNotExist( + AbstractFirebaseAuth firebaseAuth, String providerId) throws Exception { + try { + firebaseAuth.getSamlProviderConfigAsync(providerId).get(); + fail("No error thrown for getting a deleted SAML provider config."); + } catch (ExecutionException e) { + assertTrue(e.getCause() instanceof FirebaseAuthException); + assertEquals(AuthHttpClient.CONFIGURATION_NOT_FOUND_ERROR, + ((FirebaseAuthException) e.getCause()).getErrorCode()); + } + } + + /** + * Creates temporary provider configs for testing, and deletes them at the end of each test case. + */ + public static final class TemporaryProviderConfig extends ExternalResource { + + private final AbstractFirebaseAuth auth; + private final List oidcIds = new ArrayList<>(); + private final List samlIds = new ArrayList<>(); + + public TemporaryProviderConfig(AbstractFirebaseAuth auth) { + this.auth = auth; + } + + public synchronized OidcProviderConfig createOidcProviderConfig( + OidcProviderConfig.CreateRequest request) throws FirebaseAuthException { + OidcProviderConfig config = auth.createOidcProviderConfig(request); + oidcIds.add(config.getProviderId()); + return config; + } + + public synchronized void deleteOidcProviderConfig( + String providerId) throws FirebaseAuthException { + checkArgument(oidcIds.contains(providerId), + "Provider ID is not currently associated with a temporary OIDC provider config: " + + providerId); + auth.deleteOidcProviderConfig(providerId); + oidcIds.remove(providerId); + } + + public synchronized SamlProviderConfig createSamlProviderConfig( + SamlProviderConfig.CreateRequest request) throws FirebaseAuthException { + SamlProviderConfig config = auth.createSamlProviderConfig(request); + samlIds.add(config.getProviderId()); + return config; + } + + public synchronized void deleteSamlProviderConfig( + String providerId) throws FirebaseAuthException { + checkArgument(samlIds.contains(providerId), + "Provider ID is not currently associated with a temporary SAML provider config: " + + providerId); + auth.deleteSamlProviderConfig(providerId); + samlIds.remove(providerId); + } + + @Override + protected synchronized void after() { + // Delete OIDC provider configs. + for (String id : oidcIds) { + try { + auth.deleteOidcProviderConfig(id); + } catch (Exception ignore) { + // Ignore + } + } + oidcIds.clear(); + + // Delete SAML provider configs. + for (String id : samlIds) { + try { + auth.deleteSamlProviderConfig(id); + } catch (Exception ignore) { + // Ignore + } + } + samlIds.clear(); + } + } +} + diff --git a/src/test/java/com/google/firebase/auth/SamlProviderConfigTest.java b/src/test/java/com/google/firebase/auth/SamlProviderConfigTest.java new file mode 100644 index 000000000..e957c1ddb --- /dev/null +++ b/src/test/java/com/google/firebase/auth/SamlProviderConfigTest.java @@ -0,0 +1,322 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.auth; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import com.google.api.client.googleapis.util.Utils; +import com.google.api.client.json.JsonFactory; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import java.io.IOException; +import java.util.List; +import java.util.Map; +import org.junit.Test; + +public class SamlProviderConfigTest { + + private static final JsonFactory jsonFactory = Utils.getDefaultJsonFactory(); + + private static final String SAML_JSON_STRING = + ("{" + + " 'name': 'projects/projectId/inboundSamlConfigs/saml.provider-id'," + + " 'displayName': 'DISPLAY_NAME'," + + " 'enabled': true," + + " 'idpConfig': {" + + " 'idpEntityId': 'IDP_ENTITY_ID'," + + " 'ssoUrl': 'https://example.com/login'," + + " 'idpCertificates': [" + + " { 'x509Certificate': 'certificate1' }," + + " { 'x509Certificate': 'certificate2' }" + + " ]" + + " }," + + " 'spConfig': {" + + " 'spEntityId': 'RP_ENTITY_ID'," + + " 'callbackUri': 'https://projectId.firebaseapp.com/__/auth/handler'" + + " }" + + "}").replace("'", "\""); + + @Test + public void testJsonDeserialization() throws IOException { + SamlProviderConfig config = jsonFactory.fromString(SAML_JSON_STRING, SamlProviderConfig.class); + + assertEquals("saml.provider-id", config.getProviderId()); + assertEquals("DISPLAY_NAME", config.getDisplayName()); + assertTrue(config.isEnabled()); + assertEquals("IDP_ENTITY_ID", config.getIdpEntityId()); + assertEquals("https://example.com/login", config.getSsoUrl()); + assertEquals(ImmutableList.of("certificate1", "certificate2"), config.getX509Certificates()); + assertEquals("RP_ENTITY_ID", config.getRpEntityId()); + assertEquals("https://projectId.firebaseapp.com/__/auth/handler", config.getCallbackUrl()); + } + + @Test + public void testCreateRequest() throws IOException { + SamlProviderConfig.CreateRequest createRequest = + new SamlProviderConfig.CreateRequest() + .setProviderId("saml.provider-id") + .setDisplayName("DISPLAY_NAME") + .setEnabled(false) + .setIdpEntityId("IDP_ENTITY_ID") + .setSsoUrl("https://example.com/login") + .addX509Certificate("certificate1") + .addX509Certificate("certificate2") + .setRpEntityId("RP_ENTITY_ID") + .setCallbackUrl("https://projectId.firebaseapp.com/__/auth/handler"); + + assertEquals("saml.provider-id", createRequest.getProviderId()); + Map properties = createRequest.getProperties(); + assertEquals(4, properties.size()); + assertEquals("DISPLAY_NAME", (String) properties.get("displayName")); + assertFalse((boolean) properties.get("enabled")); + + Map idpConfig = (Map) properties.get("idpConfig"); + assertNotNull(idpConfig); + assertEquals(3, idpConfig.size()); + assertEquals("IDP_ENTITY_ID", idpConfig.get("idpEntityId")); + assertEquals("https://example.com/login", idpConfig.get("ssoUrl")); + List idpCertificates = (List) idpConfig.get("idpCertificates"); + assertNotNull(idpCertificates); + assertEquals(2, idpCertificates.size()); + assertEquals(ImmutableMap.of("x509Certificate", "certificate1"), idpCertificates.get(0)); + assertEquals(ImmutableMap.of("x509Certificate", "certificate2"), idpCertificates.get(1)); + + Map spConfig = (Map) properties.get("spConfig"); + assertNotNull(spConfig); + assertEquals(2, spConfig.size()); + assertEquals("RP_ENTITY_ID", spConfig.get("spEntityId")); + assertEquals("https://projectId.firebaseapp.com/__/auth/handler", spConfig.get("callbackUri")); + } + + @Test + public void testCreateRequestX509Certificates() throws IOException { + SamlProviderConfig.CreateRequest createRequest = + new SamlProviderConfig.CreateRequest() + .addX509Certificate("certificate1") + .addAllX509Certificates(ImmutableList.of("certificate2", "certificate3")) + .addX509Certificate("certificate4"); + + Map properties = createRequest.getProperties(); + assertEquals(1, properties.size()); + Map idpConfig = (Map) properties.get("idpConfig"); + assertNotNull(idpConfig); + assertEquals(1, idpConfig.size()); + + List idpCertificates = (List) idpConfig.get("idpCertificates"); + assertNotNull(idpCertificates); + assertEquals(4, idpCertificates.size()); + assertEquals(ImmutableMap.of("x509Certificate", "certificate1"), idpCertificates.get(0)); + assertEquals(ImmutableMap.of("x509Certificate", "certificate2"), idpCertificates.get(1)); + assertEquals(ImmutableMap.of("x509Certificate", "certificate3"), idpCertificates.get(2)); + assertEquals(ImmutableMap.of("x509Certificate", "certificate4"), idpCertificates.get(3)); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateRequestMissingProviderId() { + new SamlProviderConfig.CreateRequest().setProviderId(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateRequestInvalidProviderId() { + new SamlProviderConfig.CreateRequest().setProviderId("oidc.provider-id"); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateRequestMissingDisplayName() { + new SamlProviderConfig.CreateRequest().setDisplayName(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateRequestMissingIdpEntityId() { + new SamlProviderConfig.CreateRequest().setIdpEntityId(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateRequestMissingSsoUrl() { + new SamlProviderConfig.CreateRequest().setSsoUrl(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateRequestInvalidSsoUrl() { + new SamlProviderConfig.CreateRequest().setSsoUrl("not a valid url"); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateRequestMissingX509Certificate() { + new SamlProviderConfig.CreateRequest().addX509Certificate(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateRequestNullX509CertificatesCollection() { + new SamlProviderConfig.CreateRequest().addAllX509Certificates(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateRequestEmptyX509CertificatesCollection() { + new SamlProviderConfig.CreateRequest().addAllX509Certificates(ImmutableList.of()); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateRequestMissingRpEntityId() { + new SamlProviderConfig.CreateRequest().setRpEntityId(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateRequestMissingCallbackUrl() { + new SamlProviderConfig.CreateRequest().setCallbackUrl(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateRequestInvalidCallbackUrl() { + new SamlProviderConfig.CreateRequest().setCallbackUrl("not a valid url"); + } + + @Test + public void testUpdateRequestFromSamlProviderConfig() throws IOException { + SamlProviderConfig config = jsonFactory.fromString(SAML_JSON_STRING, SamlProviderConfig.class); + + SamlProviderConfig.UpdateRequest updateRequest = config.updateRequest(); + + assertEquals("saml.provider-id", updateRequest.getProviderId()); + assertTrue(updateRequest.getProperties().isEmpty()); + } + + @Test + public void testUpdateRequest() throws IOException { + SamlProviderConfig.UpdateRequest updateRequest = + new SamlProviderConfig.UpdateRequest("saml.provider-id"); + updateRequest + .setDisplayName("DISPLAY_NAME") + .setEnabled(false) + .setIdpEntityId("IDP_ENTITY_ID") + .setSsoUrl("https://example.com/login") + .addX509Certificate("certificate1") + .addX509Certificate("certificate2") + .setRpEntityId("RP_ENTITY_ID") + .setCallbackUrl("https://projectId.firebaseapp.com/__/auth/handler"); + + Map properties = updateRequest.getProperties(); + assertEquals(4, properties.size()); + assertEquals("DISPLAY_NAME", (String) properties.get("displayName")); + assertFalse((boolean) properties.get("enabled")); + + Map idpConfig = (Map) properties.get("idpConfig"); + assertNotNull(idpConfig); + assertEquals(3, idpConfig.size()); + assertEquals("IDP_ENTITY_ID", idpConfig.get("idpEntityId")); + assertEquals("https://example.com/login", idpConfig.get("ssoUrl")); + List idpCertificates = (List) idpConfig.get("idpCertificates"); + assertNotNull(idpCertificates); + assertEquals(2, idpCertificates.size()); + assertEquals(ImmutableMap.of("x509Certificate", "certificate1"), idpCertificates.get(0)); + assertEquals(ImmutableMap.of("x509Certificate", "certificate2"), idpCertificates.get(1)); + + Map spConfig = (Map) properties.get("spConfig"); + assertNotNull(spConfig); + assertEquals(2, spConfig.size()); + assertEquals("RP_ENTITY_ID", spConfig.get("spEntityId")); + assertEquals("https://projectId.firebaseapp.com/__/auth/handler", spConfig.get("callbackUri")); + } + + @Test + public void testUpdateRequestX509Certificates() throws IOException { + SamlProviderConfig.UpdateRequest updateRequest = + new SamlProviderConfig.UpdateRequest("saml.provider-id"); + updateRequest + .addX509Certificate("certificate1") + .addAllX509Certificates(ImmutableList.of("certificate2", "certificate3")) + .addX509Certificate("certificate4"); + + Map properties = updateRequest.getProperties(); + assertEquals(1, properties.size()); + Map idpConfig = (Map) properties.get("idpConfig"); + assertNotNull(idpConfig); + assertEquals(1, idpConfig.size()); + + List idpCertificates = (List) idpConfig.get("idpCertificates"); + assertNotNull(idpCertificates); + assertEquals(4, idpCertificates.size()); + assertEquals(ImmutableMap.of("x509Certificate", "certificate1"), idpCertificates.get(0)); + assertEquals(ImmutableMap.of("x509Certificate", "certificate2"), idpCertificates.get(1)); + assertEquals(ImmutableMap.of("x509Certificate", "certificate3"), idpCertificates.get(2)); + assertEquals(ImmutableMap.of("x509Certificate", "certificate4"), idpCertificates.get(3)); + } + + @Test(expected = IllegalArgumentException.class) + public void testUpdateRequestMissingProviderId() { + new SamlProviderConfig.UpdateRequest(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testUpdateRequestInvalidProviderId() { + new SamlProviderConfig.UpdateRequest("oidc.invalid-saml-provider-id"); + } + + @Test(expected = IllegalArgumentException.class) + public void testUpdateRequestMissingDisplayName() { + new SamlProviderConfig.UpdateRequest("saml.provider-id").setDisplayName(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testUpdateRequestMissingIdpEntityId() { + new SamlProviderConfig.UpdateRequest("saml.provider-id").setIdpEntityId(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testUpdateRequestMissingSsoUrl() { + new SamlProviderConfig.UpdateRequest("saml.provider-id").setSsoUrl(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testUpdateRequestInvalidSsoUrl() { + new SamlProviderConfig.UpdateRequest("saml.provider-id").setSsoUrl("not a valid url"); + } + + @Test(expected = IllegalArgumentException.class) + public void testUpdateRequestMissingX509Certificate() { + new SamlProviderConfig.UpdateRequest("saml.provider-id").addX509Certificate(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testUpdateRequestNullX509CertificatesCollection() { + new SamlProviderConfig.UpdateRequest("saml.provider-id").addAllX509Certificates(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testUpdateRequestEmptyX509CertificatesCollection() { + new SamlProviderConfig.UpdateRequest("saml.provider-id") + .addAllX509Certificates(ImmutableList.of()); + } + + @Test(expected = IllegalArgumentException.class) + public void testUpdateRequestMissingRpEntityId() { + new SamlProviderConfig.UpdateRequest("saml.provider-id").setRpEntityId(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testUpdateRequestMissingCallbackUrl() { + new SamlProviderConfig.UpdateRequest("saml.provider-id").setCallbackUrl(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testUpdateRequestInvalidCallbackUrl() { + new SamlProviderConfig.UpdateRequest("saml.provider-id").setCallbackUrl("not a valid url"); + } +} diff --git a/src/test/java/com/google/firebase/auth/UserTestUtils.java b/src/test/java/com/google/firebase/auth/UserTestUtils.java new file mode 100644 index 000000000..aa86e6e19 --- /dev/null +++ b/src/test/java/com/google/firebase/auth/UserTestUtils.java @@ -0,0 +1,124 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.auth; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import com.google.firebase.auth.internal.AuthHttpClient; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.UUID; +import java.util.concurrent.ExecutionException; +import org.junit.rules.ExternalResource; + +public final class UserTestUtils { + + public static void assertUserDoesNotExist(AbstractFirebaseAuth firebaseAuth, String uid) + throws Exception { + try { + firebaseAuth.getUserAsync(uid).get(); + fail("No error thrown for getting a user which was expected to be absent."); + } catch (ExecutionException e) { + assertTrue(e.getCause() instanceof FirebaseAuthException); + assertEquals(AuthHttpClient.USER_NOT_FOUND_ERROR, + ((FirebaseAuthException) e.getCause()).getErrorCode()); + } + } + + public static RandomUser generateRandomUserInfo() { + String uid = UUID.randomUUID().toString().replaceAll("-", ""); + String email = String.format( + "test%s@example.%s.com", + uid.substring(0, 12), + uid.substring(12)).toLowerCase(); + return new RandomUser(uid, email, generateRandomPhoneNumber()); + } + + private static String generateRandomPhoneNumber() { + Random random = new Random(); + StringBuilder builder = new StringBuilder("+1"); + for (int i = 0; i < 10; i++) { + builder.append(random.nextInt(10)); + } + return builder.toString(); + } + + public static class RandomUser { + private final String uid; + private final String email; + private final String phoneNumber; + + private RandomUser(String uid, String email, String phoneNumber) { + this.uid = uid; + this.email = email; + this.phoneNumber = phoneNumber; + } + + public String getUid() { + return uid; + } + + public String getEmail() { + return email; + } + + public String getPhoneNumber() { + return phoneNumber; + } + } + + /** + * Creates temporary Firebase user accounts for testing, and deletes them at the end of each + * test case. + */ + public static final class TemporaryUser extends ExternalResource { + + private final AbstractFirebaseAuth auth; + private final List users = new ArrayList<>(); + + public TemporaryUser(AbstractFirebaseAuth auth) { + this.auth = auth; + } + + public UserRecord create(UserRecord.CreateRequest request) throws FirebaseAuthException { + UserRecord user = auth.createUser(request); + registerUid(user.getUid()); + return user; + } + + public synchronized void registerUid(String uid) { + users.add(uid); + } + + @Override + protected synchronized void after() { + for (String uid : users) { + try { + auth.deleteUser(uid); + } catch (Exception ignore) { + // Ignore + } + } + + users.clear(); + } + } +} + diff --git a/src/test/java/com/google/firebase/auth/internal/FirebaseTokenFactoryTest.java b/src/test/java/com/google/firebase/auth/internal/FirebaseTokenFactoryTest.java index 71b95bee8..1c85ceeee 100644 --- a/src/test/java/com/google/firebase/auth/internal/FirebaseTokenFactoryTest.java +++ b/src/test/java/com/google/firebase/auth/internal/FirebaseTokenFactoryTest.java @@ -18,6 +18,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import com.google.api.client.json.GenericJson; @@ -45,6 +46,7 @@ public class FirebaseTokenFactoryTest { private static final String USER_ID = "fuber"; private static final GenericJson EXTRA_CLAIMS = new GenericJson(); private static final String ISSUER = "test-484@mg-test-1210.iam.gserviceaccount.com"; + private static final String TENANT_ID = "tenant-id"; static { EXTRA_CLAIMS.set("one", 2).set("three", "four").setFactory(FACTORY); @@ -71,6 +73,7 @@ public void checkSignatureForToken() throws Exception { assertEquals(USER_ID, signedJwt.getPayload().getUid()); assertEquals(2L, signedJwt.getPayload().getIssuedAtTimeSeconds().longValue()); assertTrue(TestUtils.verifySignature(signedJwt, ImmutableList.of(keys.getPublic()))); + assertNull(signedJwt.getPayload().getTenantId()); jwt = tokenFactory.createSignedCustomAuthTokenForUser(USER_ID); signedJwt = FirebaseCustomAuthToken.parse(FACTORY, jwt); @@ -80,6 +83,23 @@ public void checkSignatureForToken() throws Exception { assertEquals(USER_ID, signedJwt.getPayload().getUid()); assertEquals(2L, signedJwt.getPayload().getIssuedAtTimeSeconds().longValue()); assertTrue(TestUtils.verifySignature(signedJwt, ImmutableList.of(keys.getPublic()))); + assertNull(signedJwt.getPayload().getTenantId()); + } + + @Test + public void tokenWithTenantId() throws Exception { + KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); + keyGen.initialize(512); + KeyPair keys = keyGen.genKeyPair(); + FixedClock clock = new FixedClock(2002L); + CryptoSigner cryptoSigner = new TestCryptoSigner(keys.getPrivate()); + FirebaseTokenFactory tokenFactory = + new FirebaseTokenFactory(FACTORY, clock, cryptoSigner, TENANT_ID); + + String jwt = tokenFactory.createSignedCustomAuthTokenForUser(USER_ID); + FirebaseCustomAuthToken signedJwt = FirebaseCustomAuthToken.parse(FACTORY, jwt); + + assertEquals(TENANT_ID, signedJwt.getPayload().getTenantId()); } @Test diff --git a/src/test/java/com/google/firebase/auth/internal/ListOidcProviderConfigsResponseTest.java b/src/test/java/com/google/firebase/auth/internal/ListOidcProviderConfigsResponseTest.java new file mode 100644 index 000000000..fbe587e56 --- /dev/null +++ b/src/test/java/com/google/firebase/auth/internal/ListOidcProviderConfigsResponseTest.java @@ -0,0 +1,70 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.auth.internal; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import com.google.api.client.googleapis.util.Utils; +import com.google.api.client.json.JsonFactory; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.firebase.auth.OidcProviderConfig; + +import org.junit.Test; + +public class ListOidcProviderConfigsResponseTest { + + private static final JsonFactory JSON_FACTORY = Utils.getDefaultJsonFactory(); + + @Test + public void testDefaultValues() throws Exception { + ListOidcProviderConfigsResponse response = new ListOidcProviderConfigsResponse(); + + assertEquals(0, response.getProviderConfigs().size()); + assertFalse(response.hasProviderConfigs()); + assertEquals("", response.getPageToken()); + } + + @Test + public void testEmptyTenantList() throws Exception { + ListOidcProviderConfigsResponse response = + new ListOidcProviderConfigsResponse(ImmutableList.of(), "PAGE_TOKEN"); + + assertEquals(0, response.getProviderConfigs().size()); + assertFalse(response.hasProviderConfigs()); + } + + @Test + public void testDeserialization() throws Exception { + String json = JSON_FACTORY.toString( + ImmutableMap.of( + "oauthIdpConfigs", ImmutableList.of( + ImmutableMap.of("name", "projects/projectId/oauthIdpConfigs/oidc.provider-id-1"), + ImmutableMap.of("name", "projects/projectId/oauthIdpConfigs/oidc.provider-id-2")), + "nextPageToken", "PAGE_TOKEN")); + ListOidcProviderConfigsResponse response = + JSON_FACTORY.fromString(json, ListOidcProviderConfigsResponse.class); + + assertEquals(2, response.getProviderConfigs().size()); + assertEquals("oidc.provider-id-1", response.getProviderConfigs().get(0).getProviderId()); + assertEquals("oidc.provider-id-2", response.getProviderConfigs().get(1).getProviderId()); + assertTrue(response.hasProviderConfigs()); + assertEquals("PAGE_TOKEN", response.getPageToken()); + } +} diff --git a/src/test/java/com/google/firebase/auth/internal/ListSamlProviderConfigsResponseTest.java b/src/test/java/com/google/firebase/auth/internal/ListSamlProviderConfigsResponseTest.java new file mode 100644 index 000000000..6950fe352 --- /dev/null +++ b/src/test/java/com/google/firebase/auth/internal/ListSamlProviderConfigsResponseTest.java @@ -0,0 +1,70 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.auth.internal; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import com.google.api.client.googleapis.util.Utils; +import com.google.api.client.json.JsonFactory; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.firebase.auth.SamlProviderConfig; + +import org.junit.Test; + +public class ListSamlProviderConfigsResponseTest { + + private static final JsonFactory JSON_FACTORY = Utils.getDefaultJsonFactory(); + + @Test + public void testDefaultValues() throws Exception { + ListSamlProviderConfigsResponse response = new ListSamlProviderConfigsResponse(); + + assertEquals(0, response.getProviderConfigs().size()); + assertFalse(response.hasProviderConfigs()); + assertEquals("", response.getPageToken()); + } + + @Test + public void testEmptyTenantList() throws Exception { + ListSamlProviderConfigsResponse response = + new ListSamlProviderConfigsResponse(ImmutableList.of(), "PAGE_TOKEN"); + + assertEquals(0, response.getProviderConfigs().size()); + assertFalse(response.hasProviderConfigs()); + } + + @Test + public void testDeserialization() throws Exception { + String json = JSON_FACTORY.toString( + ImmutableMap.of( + "inboundSamlConfigs", ImmutableList.of( + ImmutableMap.of("name", "projects/projectId/inboundSamlConfigs/saml.provider-id-1"), + ImmutableMap.of("name", "projects/projectId/inboundSamlConfigs/saml.provider-id-2")), + "nextPageToken", "PAGE_TOKEN")); + ListSamlProviderConfigsResponse response = + JSON_FACTORY.fromString(json, ListSamlProviderConfigsResponse.class); + + assertEquals(2, response.getProviderConfigs().size()); + assertEquals("saml.provider-id-1", response.getProviderConfigs().get(0).getProviderId()); + assertEquals("saml.provider-id-2", response.getProviderConfigs().get(1).getProviderId()); + assertTrue(response.hasProviderConfigs()); + assertEquals("PAGE_TOKEN", response.getPageToken()); + } +} diff --git a/src/test/java/com/google/firebase/auth/internal/ListTenantsResponseTest.java b/src/test/java/com/google/firebase/auth/internal/ListTenantsResponseTest.java new file mode 100644 index 000000000..7d65966bb --- /dev/null +++ b/src/test/java/com/google/firebase/auth/internal/ListTenantsResponseTest.java @@ -0,0 +1,69 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.auth.internal; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import com.google.api.client.googleapis.util.Utils; +import com.google.api.client.json.JsonFactory; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.firebase.auth.multitenancy.Tenant; + +import org.junit.Test; + +public class ListTenantsResponseTest { + + private static final JsonFactory JSON_FACTORY = Utils.getDefaultJsonFactory(); + + @Test + public void testDefaultValues() throws Exception { + ListTenantsResponse response = new ListTenantsResponse(); + + assertEquals(0, response.getTenants().size()); + assertFalse(response.hasTenants()); + assertEquals("", response.getPageToken()); + } + + @Test + public void testEmptyTenantList() throws Exception { + ListTenantsResponse response = + new ListTenantsResponse(ImmutableList.of(), "PAGE_TOKEN"); + + assertEquals(0, response.getTenants().size()); + assertFalse(response.hasTenants()); + } + + @Test + public void testDeserialization() throws Exception { + String json = JSON_FACTORY.toString( + ImmutableMap.of( + "tenants", ImmutableList.of( + ImmutableMap.of("name", "projects/project-id/resource/TENANT_1"), + ImmutableMap.of("name", "projects/project-id/resource/TENANT_2")), + "pageToken", "PAGE_TOKEN")); + ListTenantsResponse response = JSON_FACTORY.fromString(json, ListTenantsResponse.class); + + assertEquals(2, response.getTenants().size()); + assertEquals("TENANT_1", response.getTenants().get(0).getTenantId()); + assertEquals("TENANT_2", response.getTenants().get(1).getTenantId()); + assertTrue(response.hasTenants()); + assertEquals("PAGE_TOKEN", response.getPageToken()); + } +} diff --git a/src/test/java/com/google/firebase/auth/multitenancy/FirebaseTenantClientTest.java b/src/test/java/com/google/firebase/auth/multitenancy/FirebaseTenantClientTest.java new file mode 100644 index 000000000..36660dc28 --- /dev/null +++ b/src/test/java/com/google/firebase/auth/multitenancy/FirebaseTenantClientTest.java @@ -0,0 +1,363 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.auth.multitenancy; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import com.google.api.client.googleapis.util.Utils; +import com.google.api.client.http.GenericUrl; +import com.google.api.client.http.HttpHeaders; +import com.google.api.client.http.HttpRequest; +import com.google.api.client.json.GenericJson; +import com.google.api.client.json.JsonFactory; +import com.google.api.client.testing.http.MockHttpTransport; +import com.google.api.client.testing.http.MockLowLevelHttpResponse; +import com.google.auth.oauth2.GoogleCredentials; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.TestOnlyImplFirebaseTrampolines; +import com.google.firebase.auth.FirebaseAuth; +import com.google.firebase.auth.FirebaseAuthException; +import com.google.firebase.auth.MockGoogleCredentials; +import com.google.firebase.auth.internal.AuthHttpClient; +import com.google.firebase.internal.SdkUtils; +import com.google.firebase.testing.MultiRequestMockHttpTransport; +import com.google.firebase.testing.TestResponseInterceptor; +import com.google.firebase.testing.TestUtils; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import org.junit.After; +import org.junit.Test; + +public class FirebaseTenantClientTest { + + private static final JsonFactory JSON_FACTORY = Utils.getDefaultJsonFactory(); + + private static final String TEST_TOKEN = "token"; + + private static final GoogleCredentials credentials = new MockGoogleCredentials(TEST_TOKEN); + + private static final String PROJECT_BASE_URL = + "https://identitytoolkit.googleapis.com/v2/projects/test-project-id"; + + private static final String TENANTS_BASE_URL = PROJECT_BASE_URL + "/tenants"; + + @After + public void tearDown() { + TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); + } + + @Test + public void testGetTenant() throws Exception { + TestResponseInterceptor interceptor = initializeAppForTenantManagement( + TestUtils.loadResource("tenant.json")); + + Tenant tenant = FirebaseAuth.getInstance().getTenantManager().getTenant("TENANT_1"); + + checkTenant(tenant, "TENANT_1"); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "GET", TENANTS_BASE_URL + "/TENANT_1"); + } + + @Test + public void testGetTenantWithNotFoundError() { + TestResponseInterceptor interceptor = + initializeAppForTenantManagementWithStatusCode(404, + "{\"error\": {\"message\": \"TENANT_NOT_FOUND\"}}"); + try { + FirebaseAuth.getInstance().getTenantManager().getTenant("UNKNOWN"); + fail("No error thrown for invalid response"); + } catch (FirebaseAuthException e) { + assertEquals(AuthHttpClient.TENANT_NOT_FOUND_ERROR, e.getErrorCode()); + } + checkUrl(interceptor, "GET", TENANTS_BASE_URL + "/UNKNOWN"); + } + + @Test + public void testListTenants() throws Exception { + final TestResponseInterceptor interceptor = initializeAppForTenantManagement( + TestUtils.loadResource("listTenants.json")); + + ListTenantsPage page = FirebaseAuth.getInstance().getTenantManager().listTenants(null, 99); + + ImmutableList tenants = ImmutableList.copyOf(page.getValues()); + assertEquals(2, tenants.size()); + checkTenant(tenants.get(0), "TENANT_1"); + checkTenant(tenants.get(1), "TENANT_2"); + assertEquals("", page.getNextPageToken()); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "GET", TENANTS_BASE_URL); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals(99, url.getFirst("pageSize")); + assertNull(url.getFirst("pageToken")); + } + + @Test + public void testListTenantsWithPageToken() throws Exception { + final TestResponseInterceptor interceptor = initializeAppForTenantManagement( + TestUtils.loadResource("listTenants.json")); + + ListTenantsPage page = FirebaseAuth.getInstance().getTenantManager().listTenants("token", 99); + + ImmutableList tenants = ImmutableList.copyOf(page.getValues()); + assertEquals(2, tenants.size()); + checkTenant(tenants.get(0), "TENANT_1"); + checkTenant(tenants.get(1), "TENANT_2"); + assertEquals("", page.getNextPageToken()); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "GET", TENANTS_BASE_URL); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals(99, url.getFirst("pageSize")); + assertEquals("token", url.getFirst("pageToken")); + } + + @Test + public void testListZeroTenants() throws Exception { + final TestResponseInterceptor interceptor = initializeAppForTenantManagement("{}"); + + ListTenantsPage page = FirebaseAuth.getInstance().getTenantManager().listTenants(null); + + assertTrue(Iterables.isEmpty(page.getValues())); + assertEquals("", page.getNextPageToken()); + checkRequestHeaders(interceptor); + } + + @Test + public void testCreateTenant() throws Exception { + TestResponseInterceptor interceptor = initializeAppForTenantManagement( + TestUtils.loadResource("tenant.json")); + Tenant.CreateRequest request = new Tenant.CreateRequest() + .setDisplayName("DISPLAY_NAME") + .setPasswordSignInAllowed(true) + .setEmailLinkSignInEnabled(false); + + Tenant tenant = FirebaseAuth.getInstance().getTenantManager().createTenant(request); + + checkTenant(tenant, "TENANT_1"); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "POST", TENANTS_BASE_URL); + GenericJson parsed = parseRequestContent(interceptor); + assertEquals("DISPLAY_NAME", parsed.get("displayName")); + assertEquals(true, parsed.get("allowPasswordSignup")); + assertEquals(false, parsed.get("enableEmailLinkSignin")); + } + + @Test + public void testCreateTenantMinimal() throws Exception { + TestResponseInterceptor interceptor = initializeAppForTenantManagement( + TestUtils.loadResource("tenant.json")); + Tenant.CreateRequest request = new Tenant.CreateRequest(); + + Tenant tenant = FirebaseAuth.getInstance().getTenantManager().createTenant(request); + + checkTenant(tenant, "TENANT_1"); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "POST", TENANTS_BASE_URL); + GenericJson parsed = parseRequestContent(interceptor); + assertNull(parsed.get("displayName")); + assertNull(parsed.get("allowPasswordSignup")); + assertNull(parsed.get("enableEmailLinkSignin")); + } + + @Test + public void testCreateTenantError() { + TestResponseInterceptor interceptor = + initializeAppForTenantManagementWithStatusCode(404, + "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}"); + try { + FirebaseAuth.getInstance().getTenantManager().createTenant(new Tenant.CreateRequest()); + fail("No error thrown for invalid response"); + } catch (FirebaseAuthException e) { + assertEquals(AuthHttpClient.INTERNAL_ERROR, e.getErrorCode()); + } + checkUrl(interceptor, "POST", TENANTS_BASE_URL); + } + + @Test + public void testUpdateTenant() throws Exception { + TestResponseInterceptor interceptor = initializeAppForTenantManagement( + TestUtils.loadResource("tenant.json")); + Tenant.UpdateRequest request = new Tenant.UpdateRequest("TENANT_1") + .setDisplayName("DISPLAY_NAME") + .setPasswordSignInAllowed(true) + .setEmailLinkSignInEnabled(false); + + Tenant tenant = FirebaseAuth.getInstance().getTenantManager().updateTenant(request); + + checkTenant(tenant, "TENANT_1"); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "PATCH", TENANTS_BASE_URL + "/TENANT_1"); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals("allowPasswordSignup,displayName,enableEmailLinkSignin", + url.getFirst("updateMask")); + GenericJson parsed = parseRequestContent(interceptor); + assertEquals("DISPLAY_NAME", parsed.get("displayName")); + assertEquals(true, parsed.get("allowPasswordSignup")); + assertEquals(false, parsed.get("enableEmailLinkSignin")); + } + + @Test + public void testUpdateTenantMinimal() throws Exception { + TestResponseInterceptor interceptor = initializeAppForTenantManagement( + TestUtils.loadResource("tenant.json")); + Tenant.UpdateRequest request = + new Tenant.UpdateRequest("TENANT_1").setDisplayName("DISPLAY_NAME"); + + Tenant tenant = FirebaseAuth.getInstance().getTenantManager().updateTenant(request); + + checkTenant(tenant, "TENANT_1"); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "PATCH", TENANTS_BASE_URL + "/TENANT_1"); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals("displayName", url.getFirst("updateMask")); + GenericJson parsed = parseRequestContent(interceptor); + assertEquals("DISPLAY_NAME", parsed.get("displayName")); + assertNull(parsed.get("allowPasswordSignup")); + assertNull(parsed.get("enableEmailLinkSignin")); + } + + @Test + public void testUpdateTenantNoValues() throws Exception { + initializeAppForTenantManagement(TestUtils.loadResource("tenant.json")); + TenantManager tenantManager = FirebaseAuth.getInstance().getTenantManager(); + try { + tenantManager.updateTenant(new Tenant.UpdateRequest("TENANT_1")); + fail("No error thrown for empty tenant update"); + } catch (IllegalArgumentException e) { + // expected + } + } + + @Test + public void testUpdateTenantError() { + TestResponseInterceptor interceptor = + initializeAppForTenantManagementWithStatusCode(404, + "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}"); + Tenant.UpdateRequest request = + new Tenant.UpdateRequest("TENANT_1").setDisplayName("DISPLAY_NAME"); + try { + FirebaseAuth.getInstance().getTenantManager().updateTenant(request); + fail("No error thrown for invalid response"); + } catch (FirebaseAuthException e) { + assertEquals(AuthHttpClient.INTERNAL_ERROR, e.getErrorCode()); + } + checkUrl(interceptor, "PATCH", TENANTS_BASE_URL + "/TENANT_1"); + } + + @Test + public void testDeleteTenant() throws Exception { + TestResponseInterceptor interceptor = initializeAppForTenantManagement("{}"); + + FirebaseAuth.getInstance().getTenantManager().deleteTenant("TENANT_1"); + + checkRequestHeaders(interceptor); + checkUrl(interceptor, "DELETE", TENANTS_BASE_URL + "/TENANT_1"); + } + + @Test + public void testDeleteTenantWithNotFoundError() { + TestResponseInterceptor interceptor = + initializeAppForTenantManagementWithStatusCode(404, + "{\"error\": {\"message\": \"TENANT_NOT_FOUND\"}}"); + try { + FirebaseAuth.getInstance().getTenantManager().deleteTenant("UNKNOWN"); + fail("No error thrown for invalid response"); + } catch (FirebaseAuthException e) { + assertEquals(AuthHttpClient.TENANT_NOT_FOUND_ERROR, e.getErrorCode()); + } + checkUrl(interceptor, "DELETE", TENANTS_BASE_URL + "/UNKNOWN"); + } + + private static void checkTenant(Tenant tenant, String tenantId) { + assertEquals(tenantId, tenant.getTenantId()); + assertEquals("DISPLAY_NAME", tenant.getDisplayName()); + assertTrue(tenant.isPasswordSignInAllowed()); + assertFalse(tenant.isEmailLinkSignInEnabled()); + } + + private static void checkRequestHeaders(TestResponseInterceptor interceptor) { + HttpHeaders headers = interceptor.getResponse().getRequest().getHeaders(); + String auth = "Bearer " + TEST_TOKEN; + assertEquals(auth, headers.getFirstHeaderStringValue("Authorization")); + + String clientVersion = "Java/Admin/" + SdkUtils.getVersion(); + assertEquals(clientVersion, headers.getFirstHeaderStringValue("X-Client-Version")); + } + + private static void checkUrl(TestResponseInterceptor interceptor, String method, String url) { + HttpRequest request = interceptor.getResponse().getRequest(); + if (method.equals("PATCH")) { + assertEquals("PATCH", + request.getHeaders().getFirstHeaderStringValue("X-HTTP-Method-Override")); + assertEquals("POST", request.getRequestMethod()); + } else { + assertEquals(method, request.getRequestMethod()); + } + assertEquals(url, request.getUrl().toString().split("\\?")[0]); + } + + private static TestResponseInterceptor initializeAppForTenantManagement(String... responses) { + initializeAppWithResponses(responses); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + FirebaseAuth.getInstance().getTenantManager().setInterceptor(interceptor); + return interceptor; + } + + private static TestResponseInterceptor initializeAppForTenantManagementWithStatusCode( + int statusCode, String response) { + FirebaseApp.initializeApp(new FirebaseOptions.Builder() + .setCredentials(credentials) + .setHttpTransport( + new MockHttpTransport.Builder() + .setLowLevelHttpResponse( + new MockLowLevelHttpResponse().setContent(response).setStatusCode(statusCode)) + .build()) + .setProjectId("test-project-id") + .build()); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + FirebaseAuth.getInstance().getTenantManager().setInterceptor(interceptor); + return interceptor; + } + + private static void initializeAppWithResponses(String... responses) { + List mocks = new ArrayList<>(); + for (String response : responses) { + mocks.add(new MockLowLevelHttpResponse().setContent(response)); + } + MockHttpTransport transport = new MultiRequestMockHttpTransport(mocks); + FirebaseApp.initializeApp(new FirebaseOptions.Builder() + .setCredentials(credentials) + .setHttpTransport(transport) + .setProjectId("test-project-id") + .build()); + } + + private static GenericJson parseRequestContent(TestResponseInterceptor interceptor) + throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + interceptor.getResponse().getRequest().getContent().writeTo(out); + return JSON_FACTORY.fromString(new String(out.toByteArray()), GenericJson.class); + } +} diff --git a/src/test/java/com/google/firebase/auth/multitenancy/ListTenantsPageTest.java b/src/test/java/com/google/firebase/auth/multitenancy/ListTenantsPageTest.java new file mode 100644 index 000000000..10830592f --- /dev/null +++ b/src/test/java/com/google/firebase/auth/multitenancy/ListTenantsPageTest.java @@ -0,0 +1,349 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.auth.multitenancy; + +import static junit.framework.TestCase.assertTrue; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; + +import com.google.api.client.googleapis.util.Utils; +import com.google.common.collect.ImmutableList; +import com.google.firebase.auth.FirebaseAuthException; +import com.google.firebase.auth.internal.ListTenantsResponse; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; + +import org.junit.Test; + +public class ListTenantsPageTest { + + @Test + public void testSinglePage() throws FirebaseAuthException, IOException { + TestTenantSource source = new TestTenantSource(3); + ListTenantsPage page = new ListTenantsPage.PageFactory(source).create(); + assertFalse(page.hasNextPage()); + assertEquals(ListTenantsPage.END_OF_LIST, page.getNextPageToken()); + assertNull(page.getNextPage()); + + ImmutableList tenants = ImmutableList.copyOf(page.getValues()); + assertEquals(3, tenants.size()); + for (int i = 0; i < 3; i++) { + assertEquals("tenant" + i, tenants.get(i).getTenantId()); + } + assertEquals(1, source.calls.size()); + assertNull(source.calls.get(0)); + } + + @Test + public void testMultiplePages() throws FirebaseAuthException, IOException { + ListTenantsResponse response = new ListTenantsResponse( + ImmutableList.of(newTenant("tenant0"), newTenant("tenant1"), newTenant("tenant2")), + "token"); + TestTenantSource source = new TestTenantSource(response); + ListTenantsPage page1 = new ListTenantsPage.PageFactory(source).create(); + assertTrue(page1.hasNextPage()); + assertEquals("token", page1.getNextPageToken()); + ImmutableList tenants = ImmutableList.copyOf(page1.getValues()); + assertEquals(3, tenants.size()); + for (int i = 0; i < 3; i++) { + assertEquals("tenant" + i, tenants.get(i).getTenantId()); + } + + response = new ListTenantsResponse( + ImmutableList.of(newTenant("tenant3"), newTenant("tenant4"), newTenant("tenant5")), + ListTenantsPage.END_OF_LIST); + source.response = response; + ListTenantsPage page2 = page1.getNextPage(); + assertFalse(page2.hasNextPage()); + assertEquals(ListTenantsPage.END_OF_LIST, page2.getNextPageToken()); + tenants = ImmutableList.copyOf(page2.getValues()); + assertEquals(3, tenants.size()); + for (int i = 3; i < 6; i++) { + assertEquals("tenant" + i, tenants.get(i - 3).getTenantId()); + } + + assertEquals(2, source.calls.size()); + assertNull(source.calls.get(0)); + assertEquals("token", source.calls.get(1)); + + // Should iterate all tenants from both pages + int iterations = 0; + for (Tenant tenant : page1.iterateAll()) { + iterations++; + } + assertEquals(6, iterations); + assertEquals(3, source.calls.size()); + assertEquals("token", source.calls.get(2)); + + // Should only iterate tenants in the last page + iterations = 0; + for (Tenant tenant : page2.iterateAll()) { + iterations++; + } + assertEquals(3, iterations); + assertEquals(3, source.calls.size()); + } + + @Test + public void testListTenantsIterable() throws FirebaseAuthException, IOException { + TestTenantSource source = new TestTenantSource(3); + ListTenantsPage page = new ListTenantsPage.PageFactory(source).create(); + Iterable tenants = page.iterateAll(); + + int iterations = 0; + for (Tenant tenant : tenants) { + assertEquals("tenant" + iterations, tenant.getTenantId()); + iterations++; + } + assertEquals(3, iterations); + assertEquals(1, source.calls.size()); + assertNull(source.calls.get(0)); + + // Should result in a new iterator + iterations = 0; + for (Tenant tenant : tenants) { + assertEquals("tenant" + iterations, tenant.getTenantId()); + iterations++; + } + assertEquals(3, iterations); + assertEquals(1, source.calls.size()); + assertNull(source.calls.get(0)); + } + + @Test + public void testListTenantsIterator() throws FirebaseAuthException, IOException { + TestTenantSource source = new TestTenantSource(3); + ListTenantsPage page = new ListTenantsPage.PageFactory(source).create(); + Iterable tenants = page.iterateAll(); + Iterator iterator = tenants.iterator(); + int iterations = 0; + while (iterator.hasNext()) { + assertEquals("tenant" + iterations, iterator.next().getTenantId()); + iterations++; + } + assertEquals(3, iterations); + assertEquals(1, source.calls.size()); + assertNull(source.calls.get(0)); + + while (iterator.hasNext()) { + fail("Should not be able to to iterate any more"); + } + try { + iterator.next(); + fail("Should not be able to iterate any more"); + } catch (NoSuchElementException expected) { + // expected + } + assertEquals(1, source.calls.size()); + } + + @Test + public void testListTenantsPagedIterable() throws FirebaseAuthException, IOException { + ListTenantsResponse response = new ListTenantsResponse( + ImmutableList.of(newTenant("tenant0"), newTenant("tenant1"), newTenant("tenant2")), + "token"); + TestTenantSource source = new TestTenantSource(response); + ListTenantsPage page = new ListTenantsPage.PageFactory(source).create(); + int iterations = 0; + for (Tenant tenant : page.iterateAll()) { + assertEquals("tenant" + iterations, tenant.getTenantId()); + iterations++; + if (iterations == 3) { + assertEquals(1, source.calls.size()); + assertNull(source.calls.get(0)); + response = new ListTenantsResponse( + ImmutableList.of(newTenant("tenant3"), newTenant("tenant4"), newTenant("tenant5")), + ListTenantsPage.END_OF_LIST); + source.response = response; + } + } + + assertEquals(6, iterations); + assertEquals(2, source.calls.size()); + assertEquals("token", source.calls.get(1)); + } + + @Test + public void testListTenantsPagedIterator() throws FirebaseAuthException, IOException { + ListTenantsResponse response = new ListTenantsResponse( + ImmutableList.of(newTenant("tenant0"), newTenant("tenant1"), newTenant("tenant2")), + "token"); + TestTenantSource source = new TestTenantSource(response); + ListTenantsPage page = new ListTenantsPage.PageFactory(source).create(); + Iterator tenants = page.iterateAll().iterator(); + int iterations = 0; + while (tenants.hasNext()) { + assertEquals("tenant" + iterations, tenants.next().getTenantId()); + iterations++; + if (iterations == 3) { + assertEquals(1, source.calls.size()); + assertNull(source.calls.get(0)); + response = new ListTenantsResponse( + ImmutableList.of(newTenant("tenant3"), newTenant("tenant4"), newTenant("tenant5")), + ListTenantsPage.END_OF_LIST); + source.response = response; + } + } + + assertEquals(6, iterations); + assertEquals(2, source.calls.size()); + assertEquals("token", source.calls.get(1)); + assertFalse(tenants.hasNext()); + try { + tenants.next(); + } catch (NoSuchElementException e) { + // expected + } + } + + @Test + public void testPageWithNoTenants() throws FirebaseAuthException { + ListTenantsResponse response = new ListTenantsResponse( + ImmutableList.of(), + ListTenantsPage.END_OF_LIST); + TestTenantSource source = new TestTenantSource(response); + ListTenantsPage page = new ListTenantsPage.PageFactory(source).create(); + assertFalse(page.hasNextPage()); + assertEquals(ListTenantsPage.END_OF_LIST, page.getNextPageToken()); + assertNull(page.getNextPage()); + assertEquals(0, ImmutableList.copyOf(page.getValues()).size()); + assertEquals(1, source.calls.size()); + } + + @Test + public void testIterableWithNoTenants() throws FirebaseAuthException { + ListTenantsResponse response = new ListTenantsResponse( + ImmutableList.of(), + ListTenantsPage.END_OF_LIST); + TestTenantSource source = new TestTenantSource(response); + ListTenantsPage page = new ListTenantsPage.PageFactory(source).create(); + for (Tenant tenant : page.iterateAll()) { + fail("Should not be able to iterate, but got: " + tenant); + } + assertEquals(1, source.calls.size()); + } + + @Test + public void testIteratorWithNoTenants() throws FirebaseAuthException { + ListTenantsResponse response = new ListTenantsResponse( + ImmutableList.of(), + ListTenantsPage.END_OF_LIST); + TestTenantSource source = new TestTenantSource(response); + + ListTenantsPage page = new ListTenantsPage.PageFactory(source).create(); + Iterator iterator = page.iterateAll().iterator(); + while (iterator.hasNext()) { + fail("Should not be able to iterate"); + } + assertEquals(1, source.calls.size()); + } + + @Test + public void testRemove() throws FirebaseAuthException, IOException { + ListTenantsResponse response = new ListTenantsResponse( + ImmutableList.of(newTenant("tenant1")), + ListTenantsPage.END_OF_LIST); + TestTenantSource source = new TestTenantSource(response); + + ListTenantsPage page = new ListTenantsPage.PageFactory(source).create(); + Iterator iterator = page.iterateAll().iterator(); + while (iterator.hasNext()) { + assertNotNull(iterator.next()); + try { + iterator.remove(); + } catch (UnsupportedOperationException expected) { + // expected + } + } + } + + @Test(expected = NullPointerException.class) + public void testNullSource() { + new ListTenantsPage.PageFactory(null); + } + + @Test + public void testInvalidPageToken() throws IOException { + TestTenantSource source = new TestTenantSource(1); + try { + new ListTenantsPage.PageFactory(source, 1000, ""); + fail("No error thrown for empty page token"); + } catch (IllegalArgumentException expected) { + // expected + } + } + + @Test + public void testInvalidMaxResults() throws IOException { + TestTenantSource source = new TestTenantSource(1); + try { + new ListTenantsPage.PageFactory(source, 1001, ""); + fail("No error thrown for maxResult > 1000"); + } catch (IllegalArgumentException expected) { + // expected + } + + try { + new ListTenantsPage.PageFactory(source, 0, "next"); + fail("No error thrown for maxResult = 0"); + } catch (IllegalArgumentException expected) { + // expected + } + + try { + new ListTenantsPage.PageFactory(source, -1, "next"); + fail("No error thrown for maxResult < 0"); + } catch (IllegalArgumentException expected) { + // expected + } + } + + private static Tenant newTenant(String tenantId) throws IOException { + return Utils.getDefaultJsonFactory().fromString( + String.format("{\"name\":\"%s\"}", tenantId), Tenant.class); + } + + private static class TestTenantSource implements ListTenantsPage.TenantSource { + + private ListTenantsResponse response; + private final List calls = new ArrayList<>(); + + TestTenantSource(int tenantCount) throws IOException { + ImmutableList.Builder tenants = ImmutableList.builder(); + for (int i = 0; i < tenantCount; i++) { + tenants.add(newTenant("tenant" + i)); + } + this.response = new ListTenantsResponse(tenants.build(), ListTenantsPage.END_OF_LIST); + } + + TestTenantSource(ListTenantsResponse response) { + this.response = response; + } + + @Override + public ListTenantsResponse fetch(int maxResults, String pageToken) { + calls.add(pageToken); + return response; + } + } +} diff --git a/src/test/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuthIT.java b/src/test/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuthIT.java new file mode 100644 index 000000000..61a841902 --- /dev/null +++ b/src/test/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuthIT.java @@ -0,0 +1,450 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.auth.multitenancy; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import com.google.api.client.googleapis.util.Utils; +import com.google.api.client.http.GenericUrl; +import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.HttpResponse; +import com.google.api.client.http.HttpTransport; +import com.google.api.client.http.json.JsonHttpContent; +import com.google.api.client.json.GenericJson; +import com.google.api.client.json.JsonFactory; +import com.google.api.client.json.JsonObjectParser; +import com.google.api.core.ApiFuture; +import com.google.api.core.ApiFutureCallback; +import com.google.api.core.ApiFutures; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.firebase.FirebaseApp; +import com.google.firebase.auth.ExportedUserRecord; +import com.google.firebase.auth.FirebaseAuth; +import com.google.firebase.auth.FirebaseAuthException; +import com.google.firebase.auth.FirebaseToken; +import com.google.firebase.auth.ListProviderConfigsPage; +import com.google.firebase.auth.ListUsersPage; +import com.google.firebase.auth.OidcProviderConfig; +import com.google.firebase.auth.ProviderConfigTestUtils; +import com.google.firebase.auth.ProviderConfigTestUtils.TemporaryProviderConfig; +import com.google.firebase.auth.SamlProviderConfig; +import com.google.firebase.auth.UserRecord; +import com.google.firebase.auth.UserTestUtils; +import com.google.firebase.auth.UserTestUtils.RandomUser; +import com.google.firebase.auth.UserTestUtils.TemporaryUser; +import com.google.firebase.internal.Nullable; +import com.google.firebase.testing.IntegrationTestUtils; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Semaphore; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; + +public class TenantAwareFirebaseAuthIT { + + private static final String VERIFY_CUSTOM_TOKEN_URL = + "https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyCustomToken"; + private static final JsonFactory jsonFactory = Utils.getDefaultJsonFactory(); + private static final HttpTransport transport = Utils.getDefaultTransport(); + + private static TenantManager tenantManager; + private static TenantAwareFirebaseAuth tenantAwareAuth; + private static String tenantId; + + @Rule public final TemporaryUser temporaryUser = new TemporaryUser(tenantAwareAuth); + @Rule public final TemporaryProviderConfig temporaryProviderConfig = + new TemporaryProviderConfig(tenantAwareAuth); + + @BeforeClass + public static void setUpClass() throws Exception { + FirebaseApp masterApp = IntegrationTestUtils.ensureDefaultApp(); + tenantManager = FirebaseAuth.getInstance(masterApp).getTenantManager(); + Tenant.CreateRequest tenantCreateRequest = + new Tenant.CreateRequest().setDisplayName("DisplayName"); + tenantId = tenantManager.createTenant(tenantCreateRequest).getTenantId(); + tenantAwareAuth = tenantManager.getAuthForTenant(tenantId); + } + + @AfterClass + public static void tearDownClass() throws Exception { + tenantManager.deleteTenant(tenantId); + } + + @Test + public void testUserLifecycle() throws Exception { + // Create user + UserRecord userRecord = temporaryUser.create(new UserRecord.CreateRequest()); + String uid = userRecord.getUid(); + + // Get user + userRecord = tenantAwareAuth.getUserAsync(userRecord.getUid()).get(); + assertEquals(uid, userRecord.getUid()); + assertEquals(tenantId, userRecord.getTenantId()); + assertNull(userRecord.getDisplayName()); + assertNull(userRecord.getEmail()); + assertNull(userRecord.getPhoneNumber()); + assertNull(userRecord.getPhotoUrl()); + assertFalse(userRecord.isEmailVerified()); + assertFalse(userRecord.isDisabled()); + assertTrue(userRecord.getUserMetadata().getCreationTimestamp() > 0); + assertEquals(0, userRecord.getUserMetadata().getLastSignInTimestamp()); + assertEquals(0, userRecord.getProviderData().length); + assertTrue(userRecord.getCustomClaims().isEmpty()); + + // Update user + RandomUser randomUser = UserTestUtils.generateRandomUserInfo(); + UserRecord.UpdateRequest request = userRecord.updateRequest() + .setDisplayName("Updated Name") + .setEmail(randomUser.getEmail()) + .setPhoneNumber(randomUser.getPhoneNumber()) + .setPhotoUrl("https://example.com/photo.png") + .setEmailVerified(true) + .setPassword("secret"); + userRecord = tenantAwareAuth.updateUserAsync(request).get(); + assertEquals(uid, userRecord.getUid()); + assertEquals(tenantId, userRecord.getTenantId()); + assertEquals("Updated Name", userRecord.getDisplayName()); + assertEquals(randomUser.getEmail(), userRecord.getEmail()); + assertEquals(randomUser.getPhoneNumber(), userRecord.getPhoneNumber()); + assertEquals("https://example.com/photo.png", userRecord.getPhotoUrl()); + assertTrue(userRecord.isEmailVerified()); + assertFalse(userRecord.isDisabled()); + assertEquals(2, userRecord.getProviderData().length); + assertTrue(userRecord.getCustomClaims().isEmpty()); + + // Get user by email + userRecord = tenantAwareAuth.getUserByEmailAsync(userRecord.getEmail()).get(); + assertEquals(uid, userRecord.getUid()); + + // Disable user and remove properties + request = userRecord.updateRequest() + .setPhotoUrl(null) + .setDisplayName(null) + .setPhoneNumber(null) + .setDisabled(true); + userRecord = tenantAwareAuth.updateUserAsync(request).get(); + assertEquals(uid, userRecord.getUid()); + assertEquals(tenantId, userRecord.getTenantId()); + assertNull(userRecord.getDisplayName()); + assertEquals(randomUser.getEmail(), userRecord.getEmail()); + assertNull(userRecord.getPhoneNumber()); + assertNull(userRecord.getPhotoUrl()); + assertTrue(userRecord.isEmailVerified()); + assertTrue(userRecord.isDisabled()); + assertEquals(1, userRecord.getProviderData().length); + assertTrue(userRecord.getCustomClaims().isEmpty()); + + // Delete user + tenantAwareAuth.deleteUserAsync(userRecord.getUid()).get(); + UserTestUtils.assertUserDoesNotExist(tenantAwareAuth, userRecord.getUid()); + } + + @Test + public void testListUsers() throws Exception { + final List uids = new ArrayList<>(); + + for (int i = 0; i < 3; i++) { + UserRecord.CreateRequest createRequest = + new UserRecord.CreateRequest().setPassword("password"); + uids.add(temporaryUser.create(createRequest).getUid()); + } + + // Test list by batches + final AtomicInteger collected = new AtomicInteger(0); + ListUsersPage page = tenantAwareAuth.listUsersAsync(null).get(); + while (page != null) { + for (ExportedUserRecord user : page.getValues()) { + if (uids.contains(user.getUid())) { + collected.incrementAndGet(); + assertNotNull("Missing passwordHash field. A common cause would be " + + "forgetting to add the \"Firebase Authentication Admin\" permission. See " + + "instructions in CONTRIBUTING.md", user.getPasswordHash()); + assertNotNull(user.getPasswordSalt()); + assertEquals(tenantId, user.getTenantId()); + } + } + page = page.getNextPage(); + } + assertEquals(uids.size(), collected.get()); + + // Test iterate all + collected.set(0); + page = tenantAwareAuth.listUsersAsync(null).get(); + for (ExportedUserRecord user : page.iterateAll()) { + if (uids.contains(user.getUid())) { + collected.incrementAndGet(); + assertNotNull(user.getPasswordHash()); + assertNotNull(user.getPasswordSalt()); + assertEquals(tenantId, user.getTenantId()); + } + } + assertEquals(uids.size(), collected.get()); + + // Test iterate async + collected.set(0); + final Semaphore semaphore = new Semaphore(0); + final AtomicReference error = new AtomicReference<>(); + ApiFuture pageFuture = tenantAwareAuth.listUsersAsync(null); + ApiFutures.addCallback(pageFuture, new ApiFutureCallback() { + @Override + public void onFailure(Throwable t) { + error.set(t); + semaphore.release(); + } + + @Override + public void onSuccess(ListUsersPage result) { + for (ExportedUserRecord user : result.iterateAll()) { + if (uids.contains(user.getUid())) { + collected.incrementAndGet(); + assertNotNull(user.getPasswordHash()); + assertNotNull(user.getPasswordSalt()); + assertEquals(tenantId, user.getTenantId()); + } + } + semaphore.release(); + } + }, MoreExecutors.directExecutor()); + semaphore.acquire(); + assertEquals(uids.size(), collected.get()); + assertNull(error.get()); + } + + @Test + public void testCustomToken() throws Exception { + String customToken = tenantAwareAuth.createCustomTokenAsync("user1").get(); + String idToken = signInWithCustomToken(customToken, tenantId); + FirebaseToken decoded = tenantAwareAuth.verifyIdTokenAsync(idToken).get(); + assertEquals("user1", decoded.getUid()); + assertEquals(tenantId, decoded.getTenantId()); + } + + @Test + public void testVerifyTokenWithWrongTenantAwareClient() throws Exception { + String customToken = tenantAwareAuth.createCustomTokenAsync("user").get(); + String idToken = signInWithCustomToken(customToken, tenantId); + + try { + tenantManager.getAuthForTenant("OTHER").verifyIdTokenAsync(idToken).get(); + fail("No error thrown for verifying a token with the wrong tenant-aware client"); + } catch (ExecutionException e) { + assertTrue(e.getCause() instanceof FirebaseAuthException); + assertEquals("tenant-id-mismatch", + ((FirebaseAuthException) e.getCause()).getErrorCode()); + } + } + + @Test + public void testOidcProviderConfigLifecycle() throws Exception { + // Create provider config + String providerId = "oidc.provider-id"; + OidcProviderConfig config = + temporaryProviderConfig.createOidcProviderConfig( + new OidcProviderConfig.CreateRequest() + .setProviderId(providerId) + .setDisplayName("DisplayName") + .setEnabled(true) + .setClientId("ClientId") + .setIssuer("https://oidc.com/issuer")); + assertEquals(providerId, config.getProviderId()); + assertEquals("DisplayName", config.getDisplayName()); + assertEquals("ClientId", config.getClientId()); + assertEquals("https://oidc.com/issuer", config.getIssuer()); + + // Get provider config + config = tenantAwareAuth.getOidcProviderConfigAsync(providerId).get(); + assertEquals(providerId, config.getProviderId()); + assertEquals("DisplayName", config.getDisplayName()); + assertEquals("ClientId", config.getClientId()); + assertEquals("https://oidc.com/issuer", config.getIssuer()); + + // Update provider config + OidcProviderConfig.UpdateRequest updateRequest = + new OidcProviderConfig.UpdateRequest(providerId) + .setDisplayName("NewDisplayName") + .setEnabled(false) + .setClientId("NewClientId") + .setIssuer("https://oidc.com/new-issuer"); + config = tenantAwareAuth.updateOidcProviderConfigAsync(updateRequest).get(); + assertEquals(providerId, config.getProviderId()); + assertEquals("NewDisplayName", config.getDisplayName()); + assertFalse(config.isEnabled()); + assertEquals("NewClientId", config.getClientId()); + assertEquals("https://oidc.com/new-issuer", config.getIssuer()); + + // Delete provider config + temporaryProviderConfig.deleteOidcProviderConfig(providerId); + ProviderConfigTestUtils.assertOidcProviderConfigDoesNotExist(tenantAwareAuth, providerId); + } + + @Test + public void testListOidcProviderConfigs() throws Exception { + final List providerIds = new ArrayList<>(); + + // Create provider configs + for (int i = 0; i < 3; i++) { + String providerId = "oidc.provider-id" + i; + providerIds.add(providerId); + temporaryProviderConfig.createOidcProviderConfig( + new OidcProviderConfig.CreateRequest() + .setProviderId(providerId) + .setClientId("CLIENT_ID") + .setIssuer("https://oidc.com/issuer")); + } + + // List provider configs + // NOTE: We do not need to test all of the different ways we can iterate over the provider + // configs, since this testing is already performed in FirebaseAuthIT with the tenant-agnostic + // tests. + final AtomicInteger collected = new AtomicInteger(0); + ListProviderConfigsPage page = + tenantAwareAuth.listOidcProviderConfigsAsync(null).get(); + for (OidcProviderConfig providerConfig : page.iterateAll()) { + if (providerIds.contains(providerConfig.getProviderId())) { + collected.incrementAndGet(); + assertEquals("CLIENT_ID", providerConfig.getClientId()); + assertEquals("https://oidc.com/issuer", providerConfig.getIssuer()); + } + } + assertEquals(providerIds.size(), collected.get()); + } + + @Test + public void testSamlProviderConfigLifecycle() throws Exception { + // Create provider config + String providerId = "saml.provider-id"; + SamlProviderConfig config = temporaryProviderConfig.createSamlProviderConfig( + new SamlProviderConfig.CreateRequest() + .setProviderId(providerId) + .setDisplayName("DisplayName") + .setEnabled(true) + .setIdpEntityId("IDP_ENTITY_ID") + .setSsoUrl("https://example.com/login") + .addX509Certificate("certificate1") + .addX509Certificate("certificate2") + .setRpEntityId("RP_ENTITY_ID") + .setCallbackUrl("https://projectId.firebaseapp.com/__/auth/handler")); + assertEquals(providerId, config.getProviderId()); + assertEquals("DisplayName", config.getDisplayName()); + assertTrue(config.isEnabled()); + assertEquals("IDP_ENTITY_ID", config.getIdpEntityId()); + assertEquals("https://example.com/login", config.getSsoUrl()); + assertEquals(ImmutableList.of("certificate1", "certificate2"), config.getX509Certificates()); + assertEquals("RP_ENTITY_ID", config.getRpEntityId()); + assertEquals("https://projectId.firebaseapp.com/__/auth/handler", config.getCallbackUrl()); + + config = tenantAwareAuth.getSamlProviderConfig(providerId); + assertEquals(providerId, config.getProviderId()); + assertEquals("DisplayName", config.getDisplayName()); + assertTrue(config.isEnabled()); + assertEquals("IDP_ENTITY_ID", config.getIdpEntityId()); + assertEquals("https://example.com/login", config.getSsoUrl()); + assertEquals(ImmutableList.of("certificate1", "certificate2"), config.getX509Certificates()); + assertEquals("RP_ENTITY_ID", config.getRpEntityId()); + assertEquals("https://projectId.firebaseapp.com/__/auth/handler", config.getCallbackUrl()); + + // Update provider config + SamlProviderConfig.UpdateRequest updateRequest = + new SamlProviderConfig.UpdateRequest(providerId) + .setDisplayName("NewDisplayName") + .setEnabled(false) + .addX509Certificate("certificate"); + config = tenantAwareAuth.updateSamlProviderConfigAsync(updateRequest).get(); + assertEquals(providerId, config.getProviderId()); + assertEquals("NewDisplayName", config.getDisplayName()); + assertFalse(config.isEnabled()); + assertEquals(ImmutableList.of("certificate"), config.getX509Certificates()); + + // Delete provider config + temporaryProviderConfig.deleteSamlProviderConfig(providerId); + ProviderConfigTestUtils.assertSamlProviderConfigDoesNotExist(tenantAwareAuth, providerId); + } + + @Test + public void testListSamlProviderConfigs() throws Exception { + final List providerIds = new ArrayList<>(); + + // Create provider configs + for (int i = 0; i < 3; i++) { + String providerId = "saml.provider-id" + i; + providerIds.add(providerId); + temporaryProviderConfig.createSamlProviderConfig( + new SamlProviderConfig.CreateRequest() + .setProviderId(providerId) + .setIdpEntityId("IDP_ENTITY_ID") + .setSsoUrl("https://example.com/login") + .addX509Certificate("certificate") + .setRpEntityId("RP_ENTITY_ID") + .setCallbackUrl("https://projectId.firebaseapp.com/__/auth/handler")); + } + + // List provider configs + // NOTE: We do not need to test all of the different ways we can iterate over the provider + // configs, since this testing is already performed in FirebaseAuthIT with the tenant-agnostic + // tests. + final AtomicInteger collected = new AtomicInteger(0); + ListProviderConfigsPage page = + tenantAwareAuth.listSamlProviderConfigsAsync(null).get(); + for (SamlProviderConfig config : page.iterateAll()) { + if (providerIds.contains(config.getProviderId())) { + collected.incrementAndGet(); + assertEquals("IDP_ENTITY_ID", config.getIdpEntityId()); + assertEquals("https://example.com/login", config.getSsoUrl()); + assertEquals(ImmutableList.of("certificate"), config.getX509Certificates()); + assertEquals("RP_ENTITY_ID", config.getRpEntityId()); + assertEquals("https://projectId.firebaseapp.com/__/auth/handler", config.getCallbackUrl()); + } + } + assertEquals(providerIds.size(), collected.get()); + } + + private String signInWithCustomToken( + String customToken, @Nullable String tenantId) throws IOException { + final GenericUrl url = new GenericUrl(VERIFY_CUSTOM_TOKEN_URL + "?key=" + + IntegrationTestUtils.getApiKey()); + ImmutableMap.Builder content = ImmutableMap.builder(); + content.put("token", customToken); + content.put("returnSecureToken", true); + if (tenantId != null) { + content.put("tenantId", tenantId); + } + HttpRequest request = transport.createRequestFactory().buildPostRequest(url, + new JsonHttpContent(jsonFactory, content.build())); + request.setParser(new JsonObjectParser(jsonFactory)); + HttpResponse response = request.execute(); + try { + GenericJson json = response.parseAs(GenericJson.class); + return json.get("idToken").toString(); + } finally { + response.disconnect(); + } + } +} diff --git a/src/test/java/com/google/firebase/auth/multitenancy/TenantManagerIT.java b/src/test/java/com/google/firebase/auth/multitenancy/TenantManagerIT.java new file mode 100644 index 000000000..086e25aa2 --- /dev/null +++ b/src/test/java/com/google/firebase/auth/multitenancy/TenantManagerIT.java @@ -0,0 +1,158 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.auth.multitenancy; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import com.google.api.core.ApiFuture; +import com.google.api.core.ApiFutureCallback; +import com.google.api.core.ApiFutures; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.firebase.auth.FirebaseAuth; +import com.google.firebase.auth.FirebaseAuthException; +import com.google.firebase.auth.internal.AuthHttpClient; +import com.google.firebase.testing.IntegrationTestUtils; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Semaphore; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.Test; + +public class TenantManagerIT { + + private static final FirebaseAuth auth = FirebaseAuth.getInstance( + IntegrationTestUtils.ensureDefaultApp()); + + @Test + public void testTenantLifecycle() throws Exception { + TenantManager tenantManager = auth.getTenantManager(); + + // Create tenant + Tenant.CreateRequest createRequest = new Tenant.CreateRequest().setDisplayName("DisplayName"); + Tenant tenant = tenantManager.createTenantAsync(createRequest).get(); + assertEquals("DisplayName", tenant.getDisplayName()); + assertFalse(tenant.isPasswordSignInAllowed()); + assertFalse(tenant.isEmailLinkSignInEnabled()); + String tenantId = tenant.getTenantId(); + + // Get tenant + tenant = tenantManager.getTenantAsync(tenantId).get(); + assertEquals(tenantId, tenant.getTenantId()); + assertEquals("DisplayName", tenant.getDisplayName()); + assertFalse(tenant.isPasswordSignInAllowed()); + assertFalse(tenant.isEmailLinkSignInEnabled()); + + // Update tenant + Tenant.UpdateRequest updateRequest = tenant.updateRequest() + .setDisplayName("UpdatedName") + .setPasswordSignInAllowed(true) + .setEmailLinkSignInEnabled(true); + tenant = tenantManager.updateTenantAsync(updateRequest).get(); + assertEquals(tenantId, tenant.getTenantId()); + assertEquals("UpdatedName", tenant.getDisplayName()); + assertTrue(tenant.isPasswordSignInAllowed()); + assertTrue(tenant.isEmailLinkSignInEnabled()); + + // Delete tenant + tenantManager.deleteTenantAsync(tenant.getTenantId()).get(); + try { + tenantManager.getTenantAsync(tenant.getTenantId()).get(); + fail("No error thrown for getting a deleted tenant"); + } catch (ExecutionException e) { + assertTrue(e.getCause() instanceof FirebaseAuthException); + assertEquals(AuthHttpClient.TENANT_NOT_FOUND_ERROR, + ((FirebaseAuthException) e.getCause()).getErrorCode()); + } + } + + @Test + public void testListTenants() throws Exception { + TenantManager tenantManager = auth.getTenantManager(); + final List tenantIds = new ArrayList<>(); + + try { + for (int i = 0; i < 3; i++) { + Tenant.CreateRequest createRequest = + new Tenant.CreateRequest().setDisplayName("DisplayName" + i); + tenantIds.add(tenantManager.createTenantAsync(createRequest).get().getTenantId()); + } + + // Test list by batches + final AtomicInteger collected = new AtomicInteger(0); + ListTenantsPage page = tenantManager.listTenantsAsync(null).get(); + while (page != null) { + for (Tenant tenant : page.getValues()) { + if (tenantIds.contains(tenant.getTenantId())) { + collected.incrementAndGet(); + assertNotNull(tenant.getDisplayName()); + } + } + page = page.getNextPage(); + } + assertEquals(tenantIds.size(), collected.get()); + + // Test iterate all + collected.set(0); + page = tenantManager.listTenantsAsync(null).get(); + for (Tenant tenant : page.iterateAll()) { + if (tenantIds.contains(tenant.getTenantId())) { + collected.incrementAndGet(); + assertNotNull(tenant.getDisplayName()); + } + } + assertEquals(tenantIds.size(), collected.get()); + + // Test iterate async + collected.set(0); + final Semaphore semaphore = new Semaphore(0); + final AtomicReference error = new AtomicReference<>(); + ApiFuture pageFuture = tenantManager.listTenantsAsync(null); + ApiFutures.addCallback(pageFuture, new ApiFutureCallback() { + @Override + public void onFailure(Throwable t) { + error.set(t); + semaphore.release(); + } + + @Override + public void onSuccess(ListTenantsPage result) { + for (Tenant tenant : result.iterateAll()) { + if (tenantIds.contains(tenant.getTenantId())) { + collected.incrementAndGet(); + assertNotNull(tenant.getDisplayName()); + } + } + semaphore.release(); + } + }, MoreExecutors.directExecutor()); + semaphore.acquire(); + assertEquals(tenantIds.size(), collected.get()); + assertNull(error.get()); + } finally { + for (String tenantId : tenantIds) { + tenantManager.deleteTenantAsync(tenantId).get(); + } + } + } +} diff --git a/src/test/java/com/google/firebase/auth/multitenancy/TenantTest.java b/src/test/java/com/google/firebase/auth/multitenancy/TenantTest.java new file mode 100644 index 000000000..7ea4f2539 --- /dev/null +++ b/src/test/java/com/google/firebase/auth/multitenancy/TenantTest.java @@ -0,0 +1,92 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.auth.multitenancy; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import com.google.api.client.googleapis.util.Utils; +import com.google.api.client.json.JsonFactory; +import java.io.IOException; +import java.util.Map; +import org.junit.Test; + +public class TenantTest { + + private static final JsonFactory JSON_FACTORY = Utils.getDefaultJsonFactory(); + + private static final String TENANT_JSON_STRING = + "{" + + "\"name\":\"projects/project-id/resource/TENANT_ID\"," + + "\"displayName\":\"DISPLAY_NAME\"," + + "\"allowPasswordSignup\":true," + + "\"enableEmailLinkSignin\":false" + + "}"; + + @Test + public void testJsonDeserialization() throws IOException { + Tenant tenant = JSON_FACTORY.fromString(TENANT_JSON_STRING, Tenant.class); + + assertEquals(tenant.getTenantId(), "TENANT_ID"); + assertEquals(tenant.getDisplayName(), "DISPLAY_NAME"); + assertTrue(tenant.isPasswordSignInAllowed()); + assertFalse(tenant.isEmailLinkSignInEnabled()); + } + + @Test + public void testUpdateRequestFromTenant() throws IOException { + Tenant tenant = JSON_FACTORY.fromString(TENANT_JSON_STRING, Tenant.class); + + Tenant.UpdateRequest updateRequest = tenant.updateRequest(); + + assertEquals("TENANT_ID", updateRequest.getTenantId()); + assertTrue(updateRequest.getProperties().isEmpty()); + } + + @Test + public void testUpdateRequestFromTenantId() throws IOException { + Tenant.UpdateRequest updateRequest = new Tenant.UpdateRequest("TENANT_ID"); + updateRequest + .setDisplayName("DISPLAY_NAME") + .setPasswordSignInAllowed(false) + .setEmailLinkSignInEnabled(true); + + assertEquals("TENANT_ID", updateRequest.getTenantId()); + Map properties = updateRequest.getProperties(); + assertEquals(properties.size(), 3); + assertEquals("DISPLAY_NAME", (String) properties.get("displayName")); + assertFalse((boolean) properties.get("allowPasswordSignup")); + assertTrue((boolean) properties.get("enableEmailLinkSignin")); + } + + @Test + public void testCreateRequest() throws IOException { + Tenant.CreateRequest createRequest = new Tenant.CreateRequest(); + createRequest + .setDisplayName("DISPLAY_NAME") + .setPasswordSignInAllowed(false) + .setEmailLinkSignInEnabled(true); + + Map properties = createRequest.getProperties(); + assertEquals(properties.size(), 3); + assertEquals("DISPLAY_NAME", (String) properties.get("displayName")); + assertFalse((boolean) properties.get("allowPasswordSignup")); + assertTrue((boolean) properties.get("enableEmailLinkSignin")); + } +} + diff --git a/src/test/java/com/google/firebase/database/FirebaseDatabaseTest.java b/src/test/java/com/google/firebase/database/FirebaseDatabaseTest.java index d8395035e..ba7816173 100644 --- a/src/test/java/com/google/firebase/database/FirebaseDatabaseTest.java +++ b/src/test/java/com/google/firebase/database/FirebaseDatabaseTest.java @@ -23,6 +23,7 @@ import static org.junit.Assert.assertSame; import static org.junit.Assert.fail; +import com.google.auth.oauth2.GoogleCredentials; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -33,11 +34,12 @@ import com.google.firebase.database.util.EmulatorHelper; import com.google.firebase.testing.ServiceAccount; import com.google.firebase.testing.TestUtils; +import java.io.IOException; import java.util.List; import org.junit.Test; public class FirebaseDatabaseTest { - + private static final FirebaseOptions firebaseOptions = new FirebaseOptions.Builder() .setCredentials(TestUtils.getCertCredential(ServiceAccount.EDITOR.asStream())) @@ -198,7 +200,7 @@ public void testInitAfterAppDelete() { } @Test - public void testDbUrlIsEmulatorUrlWhenSettingOptionsManually() { + public void testDbUrlIsEmulatorUrlWhenSettingOptionsManually() throws IOException { List testCases = ImmutableList.of( // cases where the env var is ignored because the supplied DB URL is a valid emulator URL @@ -235,7 +237,7 @@ public void testDbUrlIsEmulatorUrlWhenSettingOptionsManually() { } @Test - public void testDbUrlIsEmulatorUrlForDbRefWithPath() { + public void testDbUrlIsEmulatorUrlForDbRefWithPath() throws IOException { List testCases = ImmutableList.of( new CustomTestCase("http://my-custom-hosted-emulator.com:80?ns=dummy-ns", diff --git a/src/test/resources/getUser.json b/src/test/resources/getUser.json index 019c665be..3d57f1082 100644 --- a/src/test/resources/getUser.json +++ b/src/test/resources/getUser.json @@ -24,6 +24,7 @@ "validSince" : "1494364393", "disabled" : false, "createdAt" : "1234567890", - "customAttributes" : "{\"admin\": true, \"package\": \"gold\"}" + "customAttributes" : "{\"admin\": true, \"package\": \"gold\"}", + "tenantId": "testTenant" } ] } diff --git a/src/test/resources/listOidc.json b/src/test/resources/listOidc.json new file mode 100644 index 000000000..0c13ea48b --- /dev/null +++ b/src/test/resources/listOidc.json @@ -0,0 +1,15 @@ +{ + "oauthIdpConfigs" : [ { + "name": "projects/projectId/oauthIdpConfigs/oidc.provider-id1", + "displayName" : "DISPLAY_NAME", + "enabled" : true, + "clientId" : "CLIENT_ID", + "issuer" : "https://oidc.com/issuer" + }, { + "name": "projects/projectId/oauthIdpConfigs/oidc.provider-id2", + "displayName" : "DISPLAY_NAME", + "enabled" : true, + "clientId" : "CLIENT_ID", + "issuer" : "https://oidc.com/issuer" + } ] +} diff --git a/src/test/resources/listSaml.json b/src/test/resources/listSaml.json new file mode 100644 index 000000000..64b1e1e36 --- /dev/null +++ b/src/test/resources/listSaml.json @@ -0,0 +1,35 @@ +{ + "inboundSamlConfigs" : [ { + "name": "projects/projectId/inboundSamlConfigs/saml.provider-id1", + "displayName" : "DISPLAY_NAME", + "enabled" : true, + "idpConfig": { + "idpEntityId": "IDP_ENTITY_ID", + "ssoUrl": "https://example.com/login", + "idpCertificates": [ + { "x509Certificate": "certificate1" }, + { "x509Certificate": "certificate2" } + ] + }, + "spConfig": { + "spEntityId": "RP_ENTITY_ID", + "callbackUri": "https://projectId.firebaseapp.com/__/auth/handler" + } + }, { + "name": "projects/projectId/inboundSamlConfigs/saml.provider-id2", + "displayName" : "DISPLAY_NAME", + "enabled" : true, + "idpConfig": { + "idpEntityId": "IDP_ENTITY_ID", + "ssoUrl": "https://example.com/login", + "idpCertificates": [ + { "x509Certificate": "certificate1" }, + { "x509Certificate": "certificate2" } + ] + }, + "spConfig": { + "spEntityId": "RP_ENTITY_ID", + "callbackUri": "https://projectId.firebaseapp.com/__/auth/handler" + } + } ] +} diff --git a/src/test/resources/listTenants.json b/src/test/resources/listTenants.json new file mode 100644 index 000000000..1e2f3a01a --- /dev/null +++ b/src/test/resources/listTenants.json @@ -0,0 +1,17 @@ +{ + "tenants" : [ { + "name" : "TENANT_1", + "displayName" : "DISPLAY_NAME", + "allowPasswordSignup" : true, + "enableEmailLinkSignin" : false, + "disableAuth" : true, + "enableAnonymousUser" : false + }, { + "name" : "TENANT_2", + "displayName" : "DISPLAY_NAME", + "allowPasswordSignup" : true, + "enableEmailLinkSignin" : false, + "disableAuth" : true, + "enableAnonymousUser" : false + } ] +} diff --git a/src/test/resources/listUsers.json b/src/test/resources/listUsers.json index 06d0b34c9..47e169709 100644 --- a/src/test/resources/listUsers.json +++ b/src/test/resources/listUsers.json @@ -24,7 +24,8 @@ "validSince" : "1494364393", "disabled" : false, "createdAt" : "1234567890", - "customAttributes" : "{\"admin\": true, \"package\": \"gold\"}" + "customAttributes" : "{\"admin\": true, \"package\": \"gold\"}", + "tenantId": "testTenant" }, { "localId" : "testuser", "email" : "testuser@example.com", @@ -50,6 +51,7 @@ "validSince" : "1494364393", "disabled" : false, "createdAt" : "1234567890", - "customAttributes" : "{\"admin\": true, \"package\": \"gold\"}" + "customAttributes" : "{\"admin\": true, \"package\": \"gold\"}", + "tenantId": "testTenant" } ] } diff --git a/src/test/resources/oidc.json b/src/test/resources/oidc.json new file mode 100644 index 000000000..e2f1845de --- /dev/null +++ b/src/test/resources/oidc.json @@ -0,0 +1,7 @@ +{ + "name": "projects/projectId/oauthIdpConfigs/oidc.provider-id", + "displayName" : "DISPLAY_NAME", + "enabled" : true, + "clientId" : "CLIENT_ID", + "issuer" : "https://oidc.com/issuer" +} diff --git a/src/test/resources/saml.json b/src/test/resources/saml.json new file mode 100644 index 000000000..ef425b0a8 --- /dev/null +++ b/src/test/resources/saml.json @@ -0,0 +1,17 @@ +{ + "name": "projects/projectId/inboundSamlConfigs/saml.provider-id", + "displayName": "DISPLAY_NAME", + "enabled": true, + "idpConfig": { + "idpEntityId": "IDP_ENTITY_ID", + "ssoUrl": "https://example.com/login", + "idpCertificates": [ + { "x509Certificate": "certificate1" }, + { "x509Certificate": "certificate2" } + ] + }, + "spConfig": { + "spEntityId": "RP_ENTITY_ID", + "callbackUri": "https://projectId.firebaseapp.com/__/auth/handler" + } +} diff --git a/src/test/resources/tenant.json b/src/test/resources/tenant.json new file mode 100644 index 000000000..cc8565f8b --- /dev/null +++ b/src/test/resources/tenant.json @@ -0,0 +1,8 @@ +{ + "name" : "TENANT_1", + "displayName" : "DISPLAY_NAME", + "allowPasswordSignup" : true, + "enableEmailLinkSignin" : false, + "disableAuth" : true, + "enableAnonymousUser" : false +}