Skip to content

Commit bc201ee

Browse files
committed
feat: use iam to sign custom tokens when necessary
1 parent 0e7e70a commit bc201ee

File tree

3 files changed

+70
-16
lines changed

3 files changed

+70
-16
lines changed

lib/src/app.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,10 +101,14 @@ class AppOptions {
101101
/// The name of the default Cloud Storage bucket associated with the App.
102102
final String? storageBucket;
103103

104+
/// The client email address of the service account.
105+
final String? serviceAccountId;
106+
104107
AppOptions({
105108
required this.credential,
106109
this.databaseUrl,
107110
this.projectId,
108111
this.storageBucket,
112+
this.serviceAccountId,
109113
});
110114
}

lib/src/auth/token_generator.dart

Lines changed: 65 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
1+
import 'dart:convert';
2+
13
import 'package:clock/clock.dart';
4+
import 'package:collection/collection.dart';
25
import 'package:firebase_admin/firebase_admin.dart';
36
import 'package:firebase_admin/src/auth/credential.dart';
47
import 'package:jose/jose.dart';
8+
import 'package:googleapis/iamcredentials/v1.dart' as iamcredentials;
9+
import 'package:googleapis/iam/v1.dart' as iam;
510

11+
import '../utils/api_request.dart';
612
import '../utils/validator.dart' as validator;
713

814
/// Class for generating different types of Firebase Auth tokens (JWTs).
@@ -14,9 +20,6 @@ class FirebaseTokenGenerator {
1420

1521
FirebaseTokenGenerator(this.app);
1622

17-
Certificate get certificate =>
18-
(app.options.credential as ServiceAccountCredential).certificate;
19-
2023
// List of blacklisted claims which cannot be provided when creating a custom token
2124
static const blacklistedClaims = [
2225
'acr',
@@ -33,15 +36,17 @@ class FirebaseTokenGenerator {
3336
'jti',
3437
'nbf',
3538
'nonce',
39+
'sub',
40+
'firebase',
41+
'user_id',
3642
];
3743

3844
// Audience to use for Firebase Auth Custom tokens
3945
static const firebaseAudience =
4046
'https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit';
4147

42-
/// Creates a new Firebase Auth Custom token.
43-
Future<String> createCustomToken(
44-
String uid, Map<String, dynamic> developerClaims) async {
48+
Map<String, dynamic> _createCustomTokenPayload(String uid,
49+
Map<String, dynamic> developerClaims, String serviceAccountId) {
4550
if (!validator.isUid(uid)) {
4651
throw FirebaseAuthError.invalidArgument(
4752
'First argument to createCustomToken() must be a non-empty string uid.');
@@ -55,21 +60,66 @@ class FirebaseTokenGenerator {
5560
}
5661

5762
var iat = clock.now();
58-
var claims = {
63+
return {
5964
'aud': firebaseAudience,
6065
'iat': iat.millisecondsSinceEpoch ~/ 1000,
6166
'exp': iat.add(Duration(hours: 1)).millisecondsSinceEpoch ~/ 1000,
62-
'iss': certificate.clientEmail,
63-
'sub': certificate.clientEmail,
67+
'iss': serviceAccountId,
68+
'sub': serviceAccountId,
6469
'uid': uid,
65-
...developerClaims
70+
'claims': developerClaims,
6671
};
72+
}
73+
74+
/// Creates a new Firebase Auth Custom token.
75+
Future<String> createCustomToken(
76+
String uid, Map<String, dynamic> developerClaims) async {
77+
var credential = app.options.credential;
78+
// If the SDK was initialized with a service account, use it to sign bytes.
79+
if (credential is ServiceAccountCredential &&
80+
credential.certificate.projectId == app.options.projectId) {
81+
var certificate = credential.certificate;
82+
var claims = _createCustomTokenPayload(
83+
uid, developerClaims, certificate.clientEmail);
6784

68-
var builder = JsonWebSignatureBuilder()
69-
..jsonContent = claims
70-
..setProtectedHeader('typ', 'JWT')
71-
..addRecipient(certificate.privateKey, algorithm: 'RS256');
85+
var builder = JsonWebSignatureBuilder()
86+
..jsonContent = claims
87+
..setProtectedHeader('typ', 'JWT')
88+
..addRecipient(certificate.privateKey, algorithm: 'RS256');
89+
90+
return builder.build().toCompactSerialization();
91+
}
92+
93+
// If the SDK was initialized with a service account email, use it with the IAM service
94+
// to sign bytes.
95+
var serviceAccountId = app.options.serviceAccountId;
96+
97+
if (serviceAccountId == null) {
98+
/// Find a service account id in the project
99+
var iamApi = iam.IamApi(AuthorizedHttpClient(app));
100+
var accounts = await iamApi.projects.serviceAccounts
101+
.list('projects/${app.options.projectId}');
102+
103+
var account = accounts.accounts!.firstWhereOrNull(
104+
(a) => a.email?.startsWith('firebase-adminsdk-') ?? false);
105+
serviceAccountId = account?.email;
106+
}
107+
108+
if (serviceAccountId != null) {
109+
var claims =
110+
_createCustomTokenPayload(uid, developerClaims, serviceAccountId);
111+
var client = iamcredentials.IAMCredentialsApi(AuthorizedHttpClient(app));
112+
113+
var r = await client.projects.serviceAccounts.signJwt(
114+
iamcredentials.SignJwtRequest(
115+
payload: json.encode(claims),
116+
),
117+
'projects/-/serviceAccounts/$serviceAccountId');
118+
119+
return r.signedJwt!;
120+
}
72121

73-
return builder.build().toCompactSerialization();
122+
throw FirebaseAuthError.invalidServiceAccount(
123+
'Failed to determine service account ID. Initialize the SDK with service account credentials or specify a service account ID with iam.serviceAccounts.signBlob permission.');
74124
}
75125
}

lib/src/utils/error.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,7 @@ class FirebaseAuthError extends _PrefixedFirebaseError {
381381

382382
/// Invalid service account.
383383
FirebaseAuthError.invalidServiceAccount([String? message])
384-
: this('invalid-service-account', 'Invalid service account.');
384+
: this('invalid-service-account', message ?? 'Invalid service account.');
385385

386386
/// The provided value for the photoURL user property is invalid. It must be a
387387
/// string URL.

0 commit comments

Comments
 (0)