Skip to content

Commit cd213e0

Browse files
authored
Support for creating custom tokens without service account credentials (firebase#183)
* Ability to sign tokens without a service account credential * Temp fixes for unit tests * Cleaned up the new JWT sign logic * Cleaned up the new JWT sign logic * Updated changelog * Moved token issuer to CyptoSigner interface * Modifications to the token sign protocol (go/firebase-admin-sign) * Caching the token factory after first call * Updated documentation * Merged with master * Updated API docs * Fixing style error * Updated error message * Updated documentation * Renamed serviceAccount to serviceAccountId * Fixed lint error * Added snippet; updated error message to sync with documentation * Minor API doc updates
1 parent b5919df commit cd213e0

File tree

11 files changed

+652
-82
lines changed

11 files changed

+652
-82
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Unreleased
22

3+
- [added] Implemented the ability to create custom tokens without
4+
service account credentials.
5+
- [added] Added the `setServiceAccount()` method to the
6+
`FirebaseOptions.Builder` API.
37
- [added] The SDK can now read the Firebase/GCP project ID from both
48
`GCLOUD_PROJECT` and `GOOGLE_CLOUD_PROJECT` environment variables.
59

src/main/java/com/google/firebase/FirebaseOptions.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ public final class FirebaseOptions {
6060
private final GoogleCredentials credentials;
6161
private final Map<String, Object> databaseAuthVariableOverride;
6262
private final String projectId;
63+
private final String serviceAccountId;
6364
private final HttpTransport httpTransport;
6465
private final int connectTimeout;
6566
private final int readTimeout;
@@ -77,6 +78,11 @@ private FirebaseOptions(@NonNull FirebaseOptions.Builder builder) {
7778
checkArgument(!builder.storageBucket.startsWith("gs://"),
7879
"StorageBucket must not include 'gs://' prefix.");
7980
}
81+
if (!Strings.isNullOrEmpty(builder.serviceAccountId)) {
82+
this.serviceAccountId = builder.serviceAccountId;
83+
} else {
84+
this.serviceAccountId = null;
85+
}
8086
this.storageBucket = builder.storageBucket;
8187
this.httpTransport = checkNotNull(builder.httpTransport,
8288
"FirebaseOptions must be initialized with a non-null HttpTransport.");
@@ -131,6 +137,16 @@ public String getProjectId() {
131137
return projectId;
132138
}
133139

140+
/**
141+
* Returns the client email address of the service account.
142+
*
143+
* @return The client email of the service account set via
144+
* {@link Builder#setServiceAccountId(String)}
145+
*/
146+
public String getServiceAccountId() {
147+
return serviceAccountId;
148+
}
149+
134150
/**
135151
* Returns the <code>HttpTransport</code> used to call remote HTTP endpoints. This transport is
136152
* used by all services of the SDK, except for FirebaseDatabase.
@@ -192,6 +208,9 @@ public static final class Builder {
192208

193209
@Key("storageBucket")
194210
private String storageBucket;
211+
212+
@Key("serviceAccountId")
213+
private String serviceAccountId;
195214

196215
private GoogleCredentials credentials;
197216
private HttpTransport httpTransport = Utils.getDefaultTransport();
@@ -310,6 +329,24 @@ public Builder setProjectId(@NonNull String projectId) {
310329
return this;
311330
}
312331

332+
/**
333+
* Sets the client email address of the service account that should be associated with an app.
334+
*
335+
* <p>This is used to <a href="https://firebase.google.com/docs/auth/admin/create-custom-tokens">
336+
* create custom auth tokens</a> when service account credentials are not available. The client
337+
* email address of a service account can be found in the {@code client_email} field of the
338+
* service account JSON.
339+
*
340+
* @param serviceAccountId A service account email address string.
341+
* @return This <code>Builder</code> instance is returned so subsequent calls can be chained.
342+
*/
343+
public Builder setServiceAccountId(@NonNull String serviceAccountId) {
344+
checkArgument(!Strings.isNullOrEmpty(serviceAccountId),
345+
"Service account ID must not be null or empty");
346+
this.serviceAccountId = serviceAccountId;
347+
return this;
348+
}
349+
313350
/**
314351
* Sets the <code>HttpTransport</code> used to make remote HTTP calls. A reasonable default
315352
* is used if not explicitly set. The transport specified by calling this method is

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

Lines changed: 47 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,6 @@
2323
import com.google.api.client.json.JsonFactory;
2424
import com.google.api.client.util.Clock;
2525
import com.google.api.core.ApiFuture;
26-
import com.google.auth.oauth2.GoogleCredentials;
27-
import com.google.auth.oauth2.ServiceAccountCredentials;
2826
import com.google.common.annotations.VisibleForTesting;
2927
import com.google.common.base.Strings;
3028
import com.google.firebase.FirebaseApp;
@@ -42,10 +40,10 @@
4240
import com.google.firebase.internal.NonNull;
4341
import com.google.firebase.internal.Nullable;
4442
import java.io.IOException;
45-
import java.security.GeneralSecurityException;
4643
import java.util.List;
4744
import java.util.Map;
4845
import java.util.concurrent.atomic.AtomicBoolean;
46+
import java.util.concurrent.atomic.AtomicReference;
4947

5048
/**
5149
* This class is the entry point for all server-side Firebase Authentication actions.
@@ -65,10 +63,10 @@ public class FirebaseAuth {
6563

6664
private final FirebaseApp firebaseApp;
6765
private final KeyManagers keyManagers;
68-
private final GoogleCredentials credentials;
6966
private final String projectId;
7067
private final JsonFactory jsonFactory;
7168
private final FirebaseUserManager userManager;
69+
private final AtomicReference<FirebaseTokenFactory> tokenFactory;
7270
private final AtomicBoolean destroyed;
7371
private final Object lock;
7472

@@ -85,10 +83,10 @@ private FirebaseAuth(FirebaseApp firebaseApp) {
8583
this.firebaseApp = checkNotNull(firebaseApp);
8684
this.keyManagers = checkNotNull(keyManagers);
8785
this.clock = checkNotNull(clock);
88-
this.credentials = ImplFirebaseTrampolines.getCredentials(firebaseApp);
8986
this.projectId = ImplFirebaseTrampolines.getProjectId(firebaseApp);
9087
this.jsonFactory = firebaseApp.getOptions().getJsonFactory();
9188
this.userManager = new FirebaseUserManager(firebaseApp);
89+
this.tokenFactory = new AtomicReference<>(null);
9290
this.destroyed = new AtomicBoolean(false);
9391
this.lock = new Object();
9492
}
@@ -287,17 +285,31 @@ public String createCustomToken(@NonNull String uid) throws FirebaseAuthExceptio
287285
* <a href="/docs/auth/admin/create-custom-tokens#sign_in_using_custom_tokens_on_clients">signInWithCustomToken</a>
288286
* authentication API.
289287
*
290-
* <p>{@link FirebaseApp} must have been initialized with service account credentials to use
291-
* call this method.
288+
* <p>This method attempts to generate a token using:
289+
* <ol>
290+
* <li>the private key of {@link FirebaseApp}'s service account credentials, if provided at
291+
* initialization.
292+
* <li>the <a href="https://cloud.google.com/iam/reference/rest/v1/projects.serviceAccounts/signBlob">IAM service</a>
293+
* if a service account email was specified via
294+
* {@link com.google.firebase.FirebaseOptions.Builder#setServiceAccountId(String)}.
295+
* <li>the <a href="https://cloud.google.com/appengine/docs/standard/java/appidentity/">App Identity
296+
* service</a> if the code is deployed in the Google App Engine standard environment.
297+
* <li>the <a href="https://cloud.google.com/compute/docs/storing-retrieving-metadata">
298+
* local Metadata server</a> if the code is deployed in a different GCP-managed environment
299+
* like Google Compute Engine.
300+
* </ol>
301+
*
302+
* <p>This method throws an exception when all the above fail.
292303
*
293304
* @param uid The UID to store in the token. This identifies the user to other Firebase services
294305
* (Realtime Database, Firebase Auth, etc.). Should be less than 128 characters.
295306
* @param developerClaims Additional claims to be stored in the token (and made available to
296307
* security rules in Database, Storage, etc.). These must be able to be serialized to JSON
297308
* (e.g. contain only Maps, Arrays, Strings, Booleans, Numbers, etc.)
298309
* @return A Firebase custom token string.
299-
* @throws IllegalArgumentException If the specified uid is null or empty, or if the app has not
300-
* been initialized with service account credentials.
310+
* @throws IllegalArgumentException If the specified uid is null or empty.
311+
* @throws IllegalStateException If the SDK fails to discover a viable approach for signing
312+
* tokens.
301313
* @throws FirebaseAuthException If an error occurs while generating the custom token.
302314
*/
303315
public String createCustomToken(@NonNull String uid,
@@ -342,28 +354,43 @@ private CallableOperation<String, FirebaseAuthException> createCustomTokenOp(
342354
final String uid, final Map<String, Object> developerClaims) {
343355
checkNotDestroyed();
344356
checkArgument(!Strings.isNullOrEmpty(uid), "uid must not be null or empty");
345-
checkArgument(credentials instanceof ServiceAccountCredentials,
346-
"Must initialize FirebaseApp with a service account credential to call "
347-
+ "createCustomToken()");
357+
final FirebaseTokenFactory tokenFactory = ensureTokenFactory();
348358
return new CallableOperation<String, FirebaseAuthException>() {
349359
@Override
350360
public String execute() throws FirebaseAuthException {
351-
final ServiceAccountCredentials serviceAccount = (ServiceAccountCredentials) credentials;
352-
FirebaseTokenFactory tokenFactory = FirebaseTokenFactory.getInstance();
353361
try {
354-
return tokenFactory.createSignedCustomAuthTokenForUser(
355-
uid,
356-
developerClaims,
357-
serviceAccount.getClientEmail(),
358-
serviceAccount.getPrivateKey());
359-
} catch (GeneralSecurityException | IOException e) {
362+
return tokenFactory.createSignedCustomAuthTokenForUser(uid, developerClaims);
363+
} catch (IOException e) {
360364
throw new FirebaseAuthException(ERROR_CUSTOM_TOKEN,
361365
"Failed to generate a custom token", e);
362366
}
363367
}
364368
};
365369
}
366370

371+
private FirebaseTokenFactory ensureTokenFactory() {
372+
FirebaseTokenFactory result = this.tokenFactory.get();
373+
if (result == null) {
374+
synchronized (lock) {
375+
result = this.tokenFactory.get();
376+
if (result == null) {
377+
try {
378+
result = FirebaseTokenFactory.fromApp(firebaseApp, clock);
379+
this.tokenFactory.set(result);
380+
} catch (IOException e) {
381+
throw new IllegalStateException(
382+
"Failed to initialize FirebaseTokenFactory. Make sure to initialize the SDK "
383+
+ "with service account credentials or specify a service account "
384+
+ "ID with iam.serviceAccounts.signBlob permission. Please refer to "
385+
+ "https://firebase.google.com/docs/auth/admin/create-custom-tokens for more "
386+
+ "details on creating custom tokens.", e);
387+
}
388+
}
389+
}
390+
}
391+
return result;
392+
}
393+
367394
/**
368395
* Parses and verifies a Firebase ID Token.
369396
*
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Copyright 2018 Google Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.firebase.auth.internal;
18+
19+
import com.google.firebase.internal.NonNull;
20+
import java.io.IOException;
21+
22+
/**
23+
* Represents an object that can be used to cryptographically sign data. Mainly used for signing
24+
* custom JWT tokens issued to Firebase users.
25+
*
26+
* <p>See {@link com.google.firebase.auth.FirebaseAuth#createCustomToken(String)}.
27+
*/
28+
interface CryptoSigner {
29+
30+
/**
31+
* Signs the given payload.
32+
*
33+
* @param payload Data to be signed
34+
* @return Signature as a byte array
35+
* @throws IOException If an error occurs during signing
36+
*/
37+
@NonNull
38+
byte[] sign(@NonNull byte[] payload) throws IOException;
39+
40+
/**
41+
* Returns the client email of the service account used to sign payloads.
42+
*
43+
* @return A service account client email
44+
*/
45+
@NonNull
46+
String getAccount();
47+
}

0 commit comments

Comments
 (0)