Skip to content

Commit 99d3214

Browse files
authored
Allow createCustomToken() to work with tenant-aware auth (#708)
1 parent 050cc59 commit 99d3214

File tree

6 files changed

+298
-248
lines changed

6 files changed

+298
-248
lines changed

src/auth/auth.ts

Lines changed: 16 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -98,13 +98,21 @@ export class BaseAuth<T extends AbstractAuthRequestHandler> {
9898
/**
9999
* The BaseAuth class constructor.
100100
*
101-
* @param {T} authRequestHandler The RPC request handler
102-
* for this instance.
101+
* @param app The FirebaseApp to associate with this Auth instance.
102+
* @param authRequestHandler The RPC request handler for this instance.
103+
* @param tokenGenerator Optional token generator. If not specified, a
104+
* (non-tenant-aware) instance will be created. Use this paramter to
105+
* specify a tenant-aware tokenGenerator.
103106
* @constructor
104107
*/
105-
constructor(app: FirebaseApp, protected readonly authRequestHandler: T) {
106-
const cryptoSigner = cryptoSignerFromApp(app);
107-
this.tokenGenerator = new FirebaseTokenGenerator(cryptoSigner);
108+
constructor(app: FirebaseApp, protected readonly authRequestHandler: T, tokenGenerator?: FirebaseTokenGenerator) {
109+
if (tokenGenerator) {
110+
this.tokenGenerator = tokenGenerator;
111+
} else {
112+
const cryptoSigner = cryptoSignerFromApp(app);
113+
this.tokenGenerator = new FirebaseTokenGenerator(cryptoSigner);
114+
}
115+
108116
this.sessionCookieVerifier = createSessionCookieVerifier(app);
109117
this.idTokenVerifier = createIdTokenVerifier(app);
110118
}
@@ -613,26 +621,12 @@ export class TenantAwareAuth extends BaseAuth<TenantAwareAuthRequestHandler> {
613621
* @constructor
614622
*/
615623
constructor(app: FirebaseApp, tenantId: string) {
616-
super(app, new TenantAwareAuthRequestHandler(app, tenantId));
624+
const cryptoSigner = cryptoSignerFromApp(app);
625+
const tokenGenerator = new FirebaseTokenGenerator(cryptoSigner, tenantId);
626+
super(app, new TenantAwareAuthRequestHandler(app, tenantId), tokenGenerator);
617627
utils.addReadonlyGetter(this, 'tenantId', tenantId);
618628
}
619629

620-
/**
621-
* Creates a new custom token that can be sent back to a client to use with
622-
* signInWithCustomToken().
623-
*
624-
* @param {string} uid The uid to use as the JWT subject.
625-
* @param {object=} developerClaims Optional additional claims to include in the JWT payload.
626-
*
627-
* @return {Promise<string>} A JWT for the provided payload.
628-
*/
629-
public createCustomToken(uid: string, developerClaims?: object): Promise<string> {
630-
// This is not yet supported by the Auth server. It is also not yet determined how this will be
631-
// supported.
632-
return Promise.reject(
633-
new FirebaseAuthError(AuthClientErrorCode.UNSUPPORTED_TENANT_OPERATION));
634-
}
635-
636630
/**
637631
* Verifies a JWT auth token. Returns a Promise with the tokens claims. Rejects
638632
* the promise if the token could not be verified. If checkRevoked is set to true,

src/auth/token-generator.ts

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ interface JWTBody {
7474
exp: number;
7575
iss: string;
7676
sub: string;
77+
tenant_id?: string;
7778
}
7879

7980
/**
@@ -247,33 +248,43 @@ export class FirebaseTokenGenerator {
247248

248249
private readonly signer: CryptoSigner;
249250

250-
constructor(signer: CryptoSigner) {
251+
/**
252+
* @param tenantId The tenant ID to use for the generated Firebase Auth
253+
* Custom token. If absent, then no tenant ID claim will be set in the
254+
* resulting JWT.
255+
*/
256+
constructor(signer: CryptoSigner, public readonly tenantId?: string) {
251257
if (!validator.isNonNullObject(signer)) {
252258
throw new FirebaseAuthError(
253259
AuthClientErrorCode.INVALID_CREDENTIAL,
254260
'INTERNAL ASSERT: Must provide a CryptoSigner to use FirebaseTokenGenerator.',
255261
);
256262
}
263+
if (typeof tenantId !== 'undefined' && !validator.isNonEmptyString(tenantId)) {
264+
throw new FirebaseAuthError(
265+
AuthClientErrorCode.INVALID_ARGUMENT,
266+
'`tenantId` argument must be a non-empty string.');
267+
}
257268
this.signer = signer;
258269
}
259270

260271
/**
261272
* Creates a new Firebase Auth Custom token.
262273
*
263-
* @param {string} uid The user ID to use for the generated Firebase Auth Custom token.
264-
* @param {object} [developerClaims] Optional developer claims to include in the generated Firebase
265-
* Auth Custom token.
266-
* @return {Promise<string>} A Promise fulfilled with a Firebase Auth Custom token signed with a
267-
* service account key and containing the provided payload.
274+
* @param uid The user ID to use for the generated Firebase Auth Custom token.
275+
* @param developerClaims Optional developer claims to include in the generated Firebase
276+
* Auth Custom token.
277+
* @return A Promise fulfilled with a Firebase Auth Custom token signed with a
278+
* service account key and containing the provided payload.
268279
*/
269280
public createCustomToken(uid: string, developerClaims?: {[key: string]: any}): Promise<string> {
270281
let errorMessage: string | undefined;
271-
if (typeof uid !== 'string' || uid === '') {
272-
errorMessage = 'First argument to createCustomToken() must be a non-empty string uid.';
282+
if (!validator.isNonEmptyString(uid)) {
283+
errorMessage = '`uid` argument must be a non-empty string uid.';
273284
} else if (uid.length > 128) {
274-
errorMessage = 'First argument to createCustomToken() must a uid with less than or equal to 128 characters.';
285+
errorMessage = '`uid` argument must a uid with less than or equal to 128 characters.';
275286
} else if (!this.isDeveloperClaimsValid_(developerClaims)) {
276-
errorMessage = 'Second argument to createCustomToken() must be an object containing the developer claims.';
287+
errorMessage = '`developerClaims` argument must be a valid, non-null object containing the developer claims.';
277288
}
278289

279290
if (errorMessage) {
@@ -309,6 +320,9 @@ export class FirebaseTokenGenerator {
309320
sub: account,
310321
uid,
311322
};
323+
if (this.tenantId) {
324+
body.tenant_id = this.tenantId;
325+
}
312326
if (Object.keys(claims).length > 0) {
313327
body.claims = claims;
314328
}

src/index.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1445,7 +1445,8 @@ declare namespace admin.auth {
14451445
/**
14461446
* Creates a new Firebase custom token (JWT) that can be sent back to a client
14471447
* device to use to sign in with the client SDKs' `signInWithCustomToken()`
1448-
* methods.
1448+
* methods. (Tenant-aware instances will also embed the tenant ID in the
1449+
* token.)
14491450
*
14501451
* See [Create Custom Tokens](/docs/auth/admin/create-custom-tokens) for code
14511452
* samples and detailed documentation.

test/integration/auth.spec.ts

Lines changed: 40 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import url = require('url');
3030
import * as mocks from '../resources/mocks';
3131
import { AuthProviderConfig } from '../../src/auth/auth-config';
3232
import { deepExtend, deepCopy } from '../../src/utils/deep-copy';
33-
import { User } from '@firebase/auth-types';
33+
import { User, FirebaseAuth } from '@firebase/auth-types';
3434

3535
/* tslint:disable:no-var-requires */
3636
const chalk = require('chalk');
@@ -91,6 +91,10 @@ function randomOidcProviderId(): string {
9191
return 'oidc.' + generateRandomString(10, false).toLowerCase();
9292
}
9393

94+
function clientAuth(): FirebaseAuth {
95+
expect(firebase.auth).to.be.ok;
96+
return firebase.auth!();
97+
}
9498

9599
describe('admin.auth', () => {
96100

@@ -213,7 +217,7 @@ describe('admin.auth', () => {
213217
let currentIdToken: string;
214218
let currentUser: User;
215219
// Sign in with an email and password account.
216-
return firebase.auth!().signInWithEmailAndPassword(mockUserData.email, mockUserData.password)
220+
return clientAuth().signInWithEmailAndPassword(mockUserData.email, mockUserData.password)
217221
.then(({user}) => {
218222
expect(user).to.exist;
219223
currentUser = user!;
@@ -248,7 +252,7 @@ describe('admin.auth', () => {
248252
})
249253
.then(() => {
250254
// New sign-in should succeed.
251-
return firebase.auth!().signInWithEmailAndPassword(
255+
return clientAuth().signInWithEmailAndPassword(
252256
mockUserData.email, mockUserData.password);
253257
})
254258
.then(({user}) => {
@@ -273,7 +277,7 @@ describe('admin.auth', () => {
273277
// Confirm custom claims set on the UserRecord.
274278
expect(userRecord.customClaims).to.deep.equal(customClaims);
275279
expect(userRecord.email).to.exist;
276-
return firebase.auth!().signInWithEmailAndPassword(
280+
return clientAuth().signInWithEmailAndPassword(
277281
userRecord.email!, mockUserData.password);
278282
})
279283
.then(({user}) => {
@@ -302,8 +306,8 @@ describe('admin.auth', () => {
302306
// Custom claims should be cleared.
303307
expect(userRecord.customClaims).to.deep.equal({});
304308
// Force token refresh. All claims should be cleared.
305-
expect(firebase.auth!().currentUser).to.exist;
306-
return firebase.auth!().currentUser!.getIdToken(true);
309+
expect(clientAuth().currentUser).to.exist;
310+
return clientAuth().currentUser!.getIdToken(true);
307311
})
308312
.then((idToken) => {
309313
// Verify ID token contents.
@@ -368,7 +372,7 @@ describe('admin.auth', () => {
368372
isAdmin: true,
369373
})
370374
.then((customToken) => {
371-
return firebase.auth!().signInWithCustomToken(customToken);
375+
return clientAuth().signInWithCustomToken(customToken);
372376
})
373377
.then(({user}) => {
374378
expect(user).to.exist;
@@ -388,7 +392,7 @@ describe('admin.auth', () => {
388392
isAdmin: true,
389393
})
390394
.then((customToken) => {
391-
return firebase.auth!().signInWithCustomToken(customToken);
395+
return clientAuth().signInWithCustomToken(customToken);
392396
})
393397
.then(({user}) => {
394398
expect(user).to.exist;
@@ -426,7 +430,7 @@ describe('admin.auth', () => {
426430

427431
// Sign out after each test.
428432
afterEach(() => {
429-
return firebase.auth!().signOut();
433+
return clientAuth().signOut();
430434
});
431435

432436
// Delete test user at the end of test suite.
@@ -443,10 +447,10 @@ describe('admin.auth', () => {
443447
.then((link) => {
444448
const code = getActionCode(link);
445449
expect(getContinueUrl(link)).equal(actionCodeSettings.url);
446-
return firebase.auth!().confirmPasswordReset(code, newPassword);
450+
return clientAuth().confirmPasswordReset(code, newPassword);
447451
})
448452
.then(() => {
449-
return firebase.auth!().signInWithEmailAndPassword(email, newPassword);
453+
return clientAuth().signInWithEmailAndPassword(email, newPassword);
450454
})
451455
.then((result) => {
452456
expect(result.user).to.exist;
@@ -466,10 +470,10 @@ describe('admin.auth', () => {
466470
.then((link) => {
467471
const code = getActionCode(link);
468472
expect(getContinueUrl(link)).equal(actionCodeSettings.url);
469-
return firebase.auth!().applyActionCode(code);
473+
return clientAuth().applyActionCode(code);
470474
})
471475
.then(() => {
472-
return firebase.auth!().signInWithEmailAndPassword(email, userData.password);
476+
return clientAuth().signInWithEmailAndPassword(email, userData.password);
473477
})
474478
.then((result) => {
475479
expect(result.user).to.exist;
@@ -482,7 +486,7 @@ describe('admin.auth', () => {
482486
return admin.auth().generateSignInWithEmailLink(email, actionCodeSettings)
483487
.then((link) => {
484488
expect(getContinueUrl(link)).equal(actionCodeSettings.url);
485-
return firebase.auth!().signInWithEmailLink(email, link);
489+
return clientAuth().signInWithEmailLink(email, link);
486490
})
487491
.then((result) => {
488492
expect(result.user).to.exist;
@@ -722,6 +726,23 @@ describe('admin.auth', () => {
722726
expect(userRecord.uid).to.equal(createdUserUid);
723727
});
724728
});
729+
730+
it('createCustomToken() mints a JWT that can be used to sign in tenant users', async () => {
731+
try {
732+
clientAuth().tenantId = createdTenantId;
733+
734+
const customToken = await tenantAwareAuth.createCustomToken('uid1');
735+
const {user} = await clientAuth().signInWithCustomToken(customToken);
736+
expect(user).to.not.be.null;
737+
const idToken = await user!.getIdToken();
738+
const token = await tenantAwareAuth.verifyIdToken(idToken);
739+
740+
expect(token.uid).to.equal('uid1');
741+
expect(token.firebase.tenant).to.equal(createdTenantId);
742+
} finally {
743+
clientAuth().tenantId = null;
744+
}
745+
});
725746
});
726747

727748
// Sanity check OIDC/SAML config management API.
@@ -1203,7 +1224,7 @@ describe('admin.auth', () => {
12031224

12041225
it('creates a valid Firebase session cookie', () => {
12051226
return admin.auth().createCustomToken(uid, {admin: true, groupId: '1234'})
1206-
.then((customToken) => firebase.auth!().signInWithCustomToken(customToken))
1227+
.then((customToken) => clientAuth().signInWithCustomToken(customToken))
12071228
.then(({user}) => {
12081229
expect(user).to.exist;
12091230
return user!.getIdToken();
@@ -1239,7 +1260,7 @@ describe('admin.auth', () => {
12391260
it('creates a revocable session cookie', () => {
12401261
let currentSessionCookie: string;
12411262
return admin.auth().createCustomToken(uid2)
1242-
.then((customToken) => firebase.auth!().signInWithCustomToken(customToken))
1263+
.then((customToken) => clientAuth().signInWithCustomToken(customToken))
12431264
.then(({user}) => {
12441265
expect(user).to.exist;
12451266
return user!.getIdToken();
@@ -1266,7 +1287,7 @@ describe('admin.auth', () => {
12661287

12671288
it('fails when called with a revoked ID token', () => {
12681289
return admin.auth().createCustomToken(uid3, {admin: true, groupId: '1234'})
1269-
.then((customToken) => firebase.auth!().signInWithCustomToken(customToken))
1290+
.then((customToken) => clientAuth().signInWithCustomToken(customToken))
12701291
.then(({user}) => {
12711292
expect(user).to.exist;
12721293
return user!.getIdToken();
@@ -1294,7 +1315,7 @@ describe('admin.auth', () => {
12941315

12951316
it('fails when called with a Firebase ID token', () => {
12961317
return admin.auth().createCustomToken(uid)
1297-
.then((customToken) => firebase.auth!().signInWithCustomToken(customToken))
1318+
.then((customToken) => clientAuth().signInWithCustomToken(customToken))
12981319
.then(({user}) => {
12991320
expect(user).to.exist;
13001321
return user!.getIdToken();
@@ -1580,7 +1601,7 @@ function testImportAndSignInUser(
15801601
expect(result.successCount).to.equal(1);
15811602
expect(result.errors.length).to.equal(0);
15821603
// Sign in with an email and password to the imported account.
1583-
return firebase.auth!().signInWithEmailAndPassword(users[0].email, rawPassword);
1604+
return clientAuth().signInWithEmailAndPassword(users[0].email, rawPassword);
15841605
})
15851606
.then(({user}) => {
15861607
// Confirm successful sign-in.

0 commit comments

Comments
 (0)