Skip to content

Commit 7fbdfd7

Browse files
committed
merge
2 parents 6943b90 + 3e5c57d commit 7fbdfd7

22 files changed

Lines changed: 1344 additions & 158 deletions

.github/workflows/nightly.yml

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# Copyright 2021 Google Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
name: Nightly Builds
16+
17+
on:
18+
# Runs every day at 06:10 AM (PT) and 08:10 PM (PT) / 04:10 AM (UTC) and 02:10 PM (UTC)
19+
# or on 'firebase_nightly_build' repository dispatch event.
20+
schedule:
21+
- cron: "10 4,14 * * *"
22+
repository_dispatch:
23+
types: [firebase_nightly_build]
24+
25+
jobs:
26+
nightly:
27+
28+
runs-on: ubuntu-latest
29+
30+
steps:
31+
- name: Checkout source for staging
32+
uses: actions/checkout@v2
33+
with:
34+
ref: ${{ github.event.client_payload.ref || github.ref }}
35+
36+
- name: Set up JDK 1.7
37+
uses: actions/setup-java@v1
38+
with:
39+
java-version: 1.7
40+
41+
- name: Compile, test and package
42+
run: ./.github/scripts/package_artifacts.sh
43+
env:
44+
FIREBASE_SERVICE_ACCT_KEY: ${{ secrets.FIREBASE_SERVICE_ACCT_KEY }}
45+
FIREBASE_API_KEY: ${{ secrets.FIREBASE_API_KEY }}
46+
47+
# Attach the packaged artifacts to the workflow output. These can be manually
48+
# downloaded for later inspection if necessary.
49+
- name: Archive artifacts
50+
uses: actions/upload-artifact@v1
51+
with:
52+
name: dist
53+
path: dist
54+
55+
- name: Send email on failure
56+
if: failure()
57+
uses: firebase/firebase-admin-node/.github/actions/send-email@master
58+
with:
59+
api-key: ${{ secrets.OSS_BOT_MAILGUN_KEY }}
60+
domain: ${{ secrets.OSS_BOT_MAILGUN_DOMAIN }}
61+
from: 'GitHub <admin-github@${{ secrets.OSS_BOT_MAILGUN_DOMAIN }}>'
62+
to: ${{ secrets.FIREBASE_ADMIN_GITHUB_EMAIL }}
63+
subject: 'Nightly build ${{github.run_id}} of ${{github.repository}} failed!'
64+
html: >
65+
<b>Nightly workflow ${{github.run_id}} failed on: ${{github.repository}}</b>
66+
<br /><br />Navigate to the
67+
<a href="https://github.com/firebase/firebase-admin-java/actions/runs/${{github.run_id}}">failed workflow</a>.
68+
continue-on-error: true
69+
70+
- name: Send email on cancelled
71+
if: cancelled()
72+
uses: firebase/firebase-admin-node/.github/actions/send-email@master
73+
with:
74+
api-key: ${{ secrets.OSS_BOT_MAILGUN_KEY }}
75+
domain: ${{ secrets.OSS_BOT_MAILGUN_DOMAIN }}
76+
from: 'GitHub <admin-github@${{ secrets.OSS_BOT_MAILGUN_DOMAIN }}>'
77+
to: ${{ secrets.FIREBASE_ADMIN_GITHUB_EMAIL }}
78+
subject: 'Nightly build ${{github.run_id}} of ${{github.repository}} cancelled!'
79+
html: >
80+
<b>Nightly workflow ${{github.run_id}} cancelled on: ${{github.repository}}</b>
81+
<br /><br />Navigate to the
82+
<a href="https://github.com/firebase/firebase-admin-java/actions/runs/${{github.run_id}}">cancelled workflow</a>.
83+
continue-on-error: true

src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import static com.google.common.base.Preconditions.checkArgument;
2020
import static com.google.common.base.Preconditions.checkNotNull;
21+
import static com.google.firebase.auth.internal.Utils.isEmulatorMode;
2122

2223
import com.google.api.client.json.JsonFactory;
2324
import com.google.api.client.util.Clock;
@@ -309,7 +310,7 @@ protected FirebaseToken execute() throws FirebaseAuthException {
309310
@VisibleForTesting
310311
FirebaseTokenVerifier getIdTokenVerifier(boolean checkRevoked) {
311312
FirebaseTokenVerifier verifier = idTokenVerifier.get();
312-
if (checkRevoked) {
313+
if (checkRevoked || isEmulatorMode()) {
313314
FirebaseUserManager userManager = getUserManager();
314315
verifier = RevocationCheckDecorator.decorateIdTokenVerifier(verifier, userManager);
315316
}
@@ -389,7 +390,7 @@ public FirebaseToken execute() throws FirebaseAuthException {
389390
@VisibleForTesting
390391
FirebaseTokenVerifier getSessionCookieVerifier(boolean checkRevoked) {
391392
FirebaseTokenVerifier verifier = cookieVerifier.get();
392-
if (checkRevoked) {
393+
if (checkRevoked || isEmulatorMode()) {
393394
FirebaseUserManager userManager = getUserManager();
394395
verifier = RevocationCheckDecorator.decorateSessionCookieVerifier(verifier, userManager);
395396
}
@@ -553,6 +554,64 @@ protected UserRecord execute() throws FirebaseAuthException {
553554
};
554555
}
555556

557+
/**
558+
* Gets the user data for the user corresponding to a given provider id.
559+
*
560+
* @param providerId Identifier for the given federated provider, for example,
561+
* "google.com" for the Google provider.
562+
* @param uid The user identifier with the given provider.
563+
* @return A {@link UserRecord} instance.
564+
* @throws IllegalArgumentException If the uid is null or empty, or if
565+
* the providerId is null, empty, or does not belong to a federated provider.
566+
* @throws FirebaseAuthException If an error occurs while retrieving user data.
567+
*/
568+
public UserRecord getUserByProviderUid(
569+
@NonNull String providerId, @NonNull String uid) throws FirebaseAuthException {
570+
return getUserByProviderUidOp(providerId, uid).call();
571+
}
572+
573+
/**
574+
* Gets the user data for the user corresponding to a given provider id.
575+
*
576+
* @param providerId Identifer for the given federated provider, for example,
577+
* "google.com" for the Google provider.
578+
* @param uid The user identifier with the given provider.
579+
* @return An {@code ApiFuture} which will complete successfully with a {@link UserRecord}
580+
* instance. If an error occurs while retrieving user data or if the provider ID and uid
581+
* do not correspond to a user, the future throws a {@link FirebaseAuthException}.
582+
* @throws IllegalArgumentException If the uid is null or empty, or if
583+
* the provider ID is null, empty, or does not belong to a federated provider.
584+
*/
585+
public ApiFuture<UserRecord> getUserByProviderUidAsync(
586+
@NonNull String providerId, @NonNull String uid) {
587+
return getUserByProviderUidOp(providerId, uid).callAsync(firebaseApp);
588+
}
589+
590+
private CallableOperation<UserRecord, FirebaseAuthException> getUserByProviderUidOp(
591+
final String providerId, final String uid) {
592+
checkArgument(!Strings.isNullOrEmpty(providerId), "providerId must not be null or empty");
593+
checkArgument(!Strings.isNullOrEmpty(uid), "uid must not be null or empty");
594+
595+
// Although we don't really advertise it, we want to also handle
596+
// non-federated idps with this call. So if we detect one of them, we'll
597+
// reroute this request appropriately.
598+
if (providerId == "phone") {
599+
return this.getUserByPhoneNumberOp(uid);
600+
} else if (providerId == "email") {
601+
return this.getUserByEmailOp(uid);
602+
}
603+
604+
checkArgument(!providerId.equals("password")
605+
&& !providerId.equals("anonymous"), "providerId must belong to a federated provider");
606+
final FirebaseUserManager userManager = getUserManager();
607+
return new CallableOperation<UserRecord, FirebaseAuthException>() {
608+
@Override
609+
protected UserRecord execute() throws FirebaseAuthException {
610+
return userManager.getUserByProviderUid(providerId, uid);
611+
}
612+
};
613+
}
614+
556615
/**
557616
* Gets a page of users starting from the specified {@code pageToken}. Page size is limited to
558617
* 1000 users.

src/main/java/com/google/firebase/auth/FirebaseTokenVerifierImpl.java

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import com.google.common.base.Joiner;
3030
import com.google.common.base.Strings;
3131
import com.google.firebase.ErrorCode;
32+
import com.google.firebase.auth.internal.Utils;
3233
import com.google.firebase.internal.Nullable;
3334
import java.io.IOException;
3435
import java.math.BigDecimal;
@@ -94,9 +95,12 @@ private FirebaseTokenVerifierImpl(Builder builder) {
9495
*/
9596
@Override
9697
public FirebaseToken verifyToken(String token) throws FirebaseAuthException {
98+
boolean isEmulatorMode = Utils.isEmulatorMode();
9799
IdToken idToken = parse(token);
98-
checkContents(idToken);
99-
checkSignature(idToken);
100+
checkContents(idToken, isEmulatorMode);
101+
if (!isEmulatorMode) {
102+
checkSignature(idToken);
103+
}
100104
FirebaseToken firebaseToken = new FirebaseToken(idToken.getPayload());
101105
checkTenantId(firebaseToken);
102106
return firebaseToken;
@@ -160,17 +164,18 @@ private void checkSignature(IdToken token) throws FirebaseAuthException {
160164
}
161165
}
162166

163-
private void checkContents(final IdToken idToken) throws FirebaseAuthException {
167+
private void checkContents(final IdToken idToken, boolean isEmulatorMode)
168+
throws FirebaseAuthException {
164169
final Header header = idToken.getHeader();
165170
final Payload payload = idToken.getPayload();
166171

167172
final long currentTimeMillis = idTokenVerifier.getClock().currentTimeMillis();
168173
String errorMessage = null;
169174
AuthErrorCode errorCode = invalidTokenErrorCode;
170175

171-
if (header.getKeyId() == null) {
176+
if (!isEmulatorMode && header.getKeyId() == null) {
172177
errorMessage = getErrorForTokenWithoutKid(header, payload);
173-
} else if (!RS256.equals(header.getAlgorithm())) {
178+
} else if (!isEmulatorMode && !RS256.equals(header.getAlgorithm())) {
174179
errorMessage = String.format(
175180
"Firebase %s has incorrect algorithm. Expected \"%s\" but got \"%s\".",
176181
shortName,

src/main/java/com/google/firebase/auth/FirebaseUserManager.java

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
import com.google.firebase.auth.internal.ListOidcProviderConfigsResponse;
4242
import com.google.firebase.auth.internal.ListSamlProviderConfigsResponse;
4343
import com.google.firebase.auth.internal.UploadAccountResponse;
44+
import com.google.firebase.auth.internal.Utils;
4445
import com.google.firebase.internal.ApiClientUtils;
4546
import com.google.firebase.internal.HttpRequestInfo;
4647
import com.google.firebase.internal.NonNull;
@@ -72,6 +73,8 @@ final class FirebaseUserManager {
7273

7374
private static final String ID_TOOLKIT_URL =
7475
"https://identitytoolkit.googleapis.com/%s/projects/%s";
76+
private static final String ID_TOOLKIT_URL_EMULATOR =
77+
"http://%s/identitytoolkit.googleapis.com/%s/projects/%s";
7578

7679
private final String userMgtBaseUrl;
7780
private final String idpConfigMgtBaseUrl;
@@ -85,8 +88,8 @@ private FirebaseUserManager(Builder builder) {
8588
+ "set the project ID explicitly via FirebaseOptions. Alternatively you can also "
8689
+ "set the project ID via the GOOGLE_CLOUD_PROJECT environment variable.");
8790
this.jsonFactory = checkNotNull(builder.jsonFactory, "JsonFactory must not be null");
88-
final String idToolkitUrlV1 = String.format(ID_TOOLKIT_URL, "v1", projectId);
89-
final String idToolkitUrlV2 = String.format(ID_TOOLKIT_URL, "v2", projectId);
91+
final String idToolkitUrlV1 = getIdToolkitUrl(projectId, "v1");
92+
final String idToolkitUrlV2 = getIdToolkitUrl(projectId, "v2");
9093
final String tenantId = builder.tenantId;
9194
if (tenantId == null) {
9295
this.userMgtBaseUrl = idToolkitUrlV1;
@@ -100,6 +103,13 @@ private FirebaseUserManager(Builder builder) {
100103
this.httpClient = new AuthHttpClient(jsonFactory, builder.requestFactory);
101104
}
102105

106+
private String getIdToolkitUrl(String projectId, String version) {
107+
if (Utils.isEmulatorMode()) {
108+
return String.format(ID_TOOLKIT_URL_EMULATOR, Utils.getEmulatorHost(), version, projectId);
109+
}
110+
return String.format(ID_TOOLKIT_URL, version, projectId);
111+
}
112+
103113
@VisibleForTesting
104114
void setInterceptor(HttpResponseInterceptor interceptor) {
105115
httpClient.setInterceptor(interceptor);
@@ -145,6 +155,15 @@ Set<UserRecord> getAccountInfo(@NonNull Collection<UserIdentifier> identifiers)
145155
return results;
146156
}
147157

158+
UserRecord getUserByProviderUid(
159+
String providerId, String uid) throws FirebaseAuthException {
160+
final Map<String, Object> payload = ImmutableMap.<String, Object>of(
161+
"federatedUserId", ImmutableList.of(
162+
ImmutableMap.<String, Object>builder()
163+
.put("rawId", uid).put("providerId", providerId).build()));
164+
return lookupUserAccount(payload, uid);
165+
}
166+
148167
String createUser(UserRecord.CreateRequest request) throws FirebaseAuthException {
149168
GenericJson response = post("/accounts", request.getProperties(), GenericJson.class);
150169
return (String) response.get("localId");

src/main/java/com/google/firebase/auth/UserRecord.java

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,29 @@ public UpdateRequest setPhoneNumber(@Nullable String phone) {
475475
if (phone != null) {
476476
checkPhoneNumber(phone);
477477
}
478+
479+
if (phone == null && properties.containsKey("deleteProvider")) {
480+
Object deleteProvider = properties.get("deleteProvider");
481+
if (deleteProvider != null) {
482+
// Due to java's type erasure, we can't fully check the type. :(
483+
@SuppressWarnings("unchecked")
484+
Iterable<String> deleteProviderIterable = (Iterable<String>)deleteProvider;
485+
486+
// If we've been told to unlink the phone provider both via setting phoneNumber to null
487+
// *and* by setting providersToUnlink to include 'phone', then we'll reject that. Though
488+
// it might also be reasonable to relax this restriction and just unlink it.
489+
for (String dp : deleteProviderIterable) {
490+
if (dp == "phone") {
491+
throw new IllegalArgumentException(
492+
"Both UpdateRequest.setPhoneNumber(null) and "
493+
+ "UpdateRequest.setProvidersToUnlink(['phone']) were set. To unlink from a "
494+
+ "phone provider, only specify UpdateRequest.setPhoneNumber(null).");
495+
496+
}
497+
}
498+
}
499+
}
500+
478501
properties.put("phoneNumber", phone);
479502
return this;
480503
}
@@ -548,6 +571,52 @@ public UpdateRequest setCustomClaims(Map<String,Object> customClaims) {
548571
return this;
549572
}
550573

574+
/**
575+
* Links this user to the specified provider.
576+
*
577+
* <p>Linking a provider to an existing user account does not invalidate the
578+
* refresh token of that account. In other words, the existing account
579+
* would continue to be able to access resources, despite not having used
580+
* the newly linked provider to log in. If you wish to force the user to
581+
* authenticate with this new provider, you need to (a) revoke their
582+
* refresh token (see
583+
* https://firebase.google.com/docs/auth/admin/manage-sessions#revoke_refresh_tokens),
584+
* and (b) ensure no other authentication methods are present on this
585+
* account.
586+
*
587+
* @param providerToLink provider info to be linked to this user\'s account.
588+
*/
589+
public UpdateRequest setProviderToLink(@NonNull UserProvider providerToLink) {
590+
properties.put("linkProviderUserInfo", checkNotNull(providerToLink));
591+
return this;
592+
}
593+
594+
/**
595+
* Unlinks this user from the specified providers.
596+
*
597+
* @param providerIds list of identifiers for the identity providers.
598+
*/
599+
public UpdateRequest setProvidersToUnlink(Iterable<String> providerIds) {
600+
checkNotNull(providerIds);
601+
for (String id : providerIds) {
602+
checkArgument(!Strings.isNullOrEmpty(id), "providerIds must not be null or empty");
603+
604+
if (id == "phone" && properties.containsKey("phoneNumber")
605+
&& properties.get("phoneNumber") == null) {
606+
// If we've been told to unlink the phone provider both via setting phoneNumber to null
607+
// *and* by setting providersToUnlink to include 'phone', then we'll reject that. Though
608+
// it might also be reasonable to relax this restriction and just unlink it.
609+
throw new IllegalArgumentException(
610+
"Both UpdateRequest.setPhoneNumber(null) and "
611+
+ "UpdateRequest.setProvidersToUnlink(['phone']) were set. To unlink from a phone "
612+
+ "provider, only specify UpdateRequest.setPhoneNumber(null).");
613+
}
614+
}
615+
616+
properties.put("deleteProvider", providerIds);
617+
return this;
618+
}
619+
551620
UpdateRequest setValidSince(long epochSeconds) {
552621
checkValidSince(epochSeconds);
553622
properties.put("validSince", epochSeconds);
@@ -569,7 +638,20 @@ Map<String, Object> getProperties(JsonFactory jsonFactory) {
569638
}
570639

571640
if (copy.containsKey("phoneNumber") && copy.get("phoneNumber") == null) {
572-
copy.put("deleteProvider", ImmutableList.of("phone"));
641+
Object deleteProvider = copy.get("deleteProvider");
642+
if (deleteProvider != null) {
643+
// Due to java's type erasure, we can't fully check the type. :(
644+
@SuppressWarnings("unchecked")
645+
Iterable<String> deleteProviderIterable = (Iterable<String>)deleteProvider;
646+
647+
copy.put("deleteProvider", new ImmutableList.Builder<String>()
648+
.addAll(deleteProviderIterable)
649+
.add("phone")
650+
.build());
651+
} else {
652+
copy.put("deleteProvider", ImmutableList.of("phone"));
653+
}
654+
573655
copy.remove("phoneNumber");
574656
}
575657

0 commit comments

Comments
 (0)