Skip to content

Commit 2961634

Browse files
committed
Added Auth#createCustomTokenWithOptions to allow token expiry to be specified
1 parent 59f2203 commit 2961634

File tree

5 files changed

+139
-23
lines changed

5 files changed

+139
-23
lines changed

src/auth.d.ts

+35
Original file line numberDiff line numberDiff line change
@@ -828,6 +828,24 @@ export namespace admin.auth {
828828
multiFactor?: admin.auth.MultiFactorUpdateSettings;
829829
}
830830

831+
/**
832+
* Interface representing the custom token options needed for the
833+
* {@link https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#createcustomtoken `createCustomToken()`} method.
834+
*/
835+
interface CustomTokenOptions {
836+
837+
/**
838+
* Optional additional claims to include in the JWT payload.
839+
*/
840+
developerClaims?: { [key: string]: any };
841+
842+
/**
843+
* The JWT expiration in milliseconds. The minimum allowed is X and the maximum allowed is 1 hour.
844+
* Defaults to 1 hour.
845+
*/
846+
expiresIn?: number;
847+
}
848+
831849
/**
832850
* Interface representing the session cookie options needed for the
833851
* {@link https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#createSessionCookie `createSessionCookie()`} method.
@@ -1375,6 +1393,23 @@ export namespace admin.auth {
13751393
*/
13761394
createCustomToken(uid: string, developerClaims?: object): Promise<string>;
13771395

1396+
/**
1397+
* Creates a new Firebase custom token (JWT) that can be sent back to a client
1398+
* device to use to sign in with the client SDKs' `signInWithCustomToken()`
1399+
* methods. (Tenant-aware instances will also embed the tenant ID in the
1400+
* token.)
1401+
*
1402+
* See [Create Custom Tokens](/docs/auth/admin/create-custom-tokens) for code
1403+
* samples and detailed documentation.
1404+
*
1405+
* @param uid The `uid` to use as the custom token's subject.
1406+
* @param {CustomTokenOptions=} options Options to use when creating the JWT.
1407+
*
1408+
* @return A promise fulfilled with a custom token for the
1409+
* provided `uid` and payload.
1410+
*/
1411+
createCustomTokenWithOptions(uid: string, options?: CustomTokenOptions): Promise<string>;
1412+
13781413
/**
13791414
* Creates a new user.
13801415
*

src/auth/auth.ts

+17-2
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
UserIdentifier, isUidIdentifier, isEmailIdentifier, isPhoneIdentifier, isProviderIdentifier,
2020
} from './identifier';
2121
import { FirebaseApp } from '../firebase-app';
22-
import { FirebaseTokenGenerator, cryptoSignerFromApp } from './token-generator';
22+
import { FirebaseTokenGenerator, cryptoSignerFromApp, FirebaseTokenOptions } from './token-generator';
2323
import {
2424
AbstractAuthRequestHandler, AuthRequestHandler, TenantAwareAuthRequestHandler,
2525
} from './auth-api-request';
@@ -111,6 +111,8 @@ export interface DecodedIdToken {
111111
[key: string]: any;
112112
}
113113

114+
/** The public API interface representing the create custom token options. */
115+
export type CustomTokenOptions = FirebaseTokenOptions;
114116

115117
/** Interface representing the session cookie options. */
116118
export interface SessionCookieOptions {
@@ -159,7 +161,20 @@ export class BaseAuth<T extends AbstractAuthRequestHandler> {
159161
* @return {Promise<string>} A JWT for the provided payload.
160162
*/
161163
public createCustomToken(uid: string, developerClaims?: object): Promise<string> {
162-
return this.tokenGenerator.createCustomToken(uid, developerClaims);
164+
return this.createCustomTokenWithOptions(uid, { developerClaims });
165+
}
166+
167+
/**
168+
* Creates a new custom token that can be sent back to a client to use with
169+
* signInWithCustomToken().
170+
*
171+
* @param {string} uid The uid to use as the JWT subject.
172+
* @param {CustomTokenOptions=} options Options to use when creating the JWT.
173+
*
174+
* @return {Promise<string>} A JWT for the provided payload.
175+
*/
176+
public createCustomTokenWithOptions(uid: string, options?: CustomTokenOptions): Promise<string> {
177+
return this.tokenGenerator.createCustomToken(uid, options);
163178
}
164179

165180
/**

src/auth/token-generator.ts

+25-12
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ import { toWebSafeBase64 } from '../utils';
2424

2525

2626
const ALGORITHM_RS256 = 'RS256';
27-
const ONE_HOUR_IN_SECONDS = 60 * 60;
27+
const MIN_JWT_EXPIRES_IN_MS = 1000;
28+
const ONE_HOUR_IN_MS = 60 * 60 * 1000;
2829

2930
// List of blacklisted claims which cannot be provided when creating a custom token
3031
export const BLACKLISTED_CLAIMS = [
@@ -231,6 +232,12 @@ export function cryptoSignerFromApp(app: FirebaseApp): CryptoSigner {
231232
return new IAMSigner(new AuthorizedHttpClient(app), app.options.serviceAccountId);
232233
}
233234

235+
/** Interface representing the create custom token options. */
236+
export interface FirebaseTokenOptions {
237+
developerClaims?: { [key: string]: any };
238+
expiresIn?: number;
239+
}
240+
234241
/**
235242
* Class for generating different types of Firebase Auth tokens (JWTs).
236243
*/
@@ -262,37 +269,43 @@ export class FirebaseTokenGenerator {
262269
* Creates a new Firebase Auth Custom token.
263270
*
264271
* @param uid The user ID to use for the generated Firebase Auth Custom token.
265-
* @param developerClaims Optional developer claims to include in the generated Firebase
266-
* Auth Custom token.
272+
* @param options Options to use when creating the JWT..
267273
* @return A Promise fulfilled with a Firebase Auth Custom token signed with a
268274
* service account key and containing the provided payload.
269275
*/
270-
public createCustomToken(uid: string, developerClaims?: {[key: string]: any}): Promise<string> {
276+
public createCustomToken(uid: string, options?: FirebaseTokenOptions): Promise<string> {
271277
let errorMessage: string | undefined;
272278
if (!validator.isNonEmptyString(uid)) {
273279
errorMessage = '`uid` argument must be a non-empty string uid.';
274280
} else if (uid.length > 128) {
275281
errorMessage = '`uid` argument must a uid with less than or equal to 128 characters.';
276-
} else if (!this.isDeveloperClaimsValid_(developerClaims)) {
277-
errorMessage = '`developerClaims` argument must be a valid, non-null object containing the developer claims.';
282+
} else if (typeof options !== 'undefined' && !validator.isObject(options)) {
283+
errorMessage = '`options` argument must be a valid object.';
284+
} else if (!this.isDeveloperClaimsValid_(options?.developerClaims)) {
285+
errorMessage = '`options.developerClaims` argument must be a valid, non-null object containing ' +
286+
'the developer claims.';
287+
} else if (typeof options?.expiresIn !== 'undefined' && (!validator.isNumber(options.expiresIn) ||
288+
options.expiresIn < MIN_JWT_EXPIRES_IN_MS || options.expiresIn > ONE_HOUR_IN_MS)) {
289+
errorMessage = `\`options.expiresIn\` argument must be a valid number between ${MIN_JWT_EXPIRES_IN_MS} ` +
290+
`and ${ONE_HOUR_IN_MS}.`;
278291
}
279292

280293
if (errorMessage) {
281294
throw new FirebaseAuthError(AuthClientErrorCode.INVALID_ARGUMENT, errorMessage);
282295
}
283296

284-
const claims: {[key: string]: any} = {};
285-
if (typeof developerClaims !== 'undefined') {
286-
for (const key in developerClaims) {
297+
const claims: { [key: string]: any } = {};
298+
if (typeof options?.developerClaims !== 'undefined') {
299+
for (const key in options.developerClaims) {
287300
/* istanbul ignore else */
288-
if (Object.prototype.hasOwnProperty.call(developerClaims, key)) {
301+
if (Object.prototype.hasOwnProperty.call(options.developerClaims, key)) {
289302
if (BLACKLISTED_CLAIMS.indexOf(key) !== -1) {
290303
throw new FirebaseAuthError(
291304
AuthClientErrorCode.INVALID_ARGUMENT,
292305
`Developer claim "${key}" is reserved and cannot be specified.`,
293306
);
294307
}
295-
claims[key] = developerClaims[key];
308+
claims[key] = options.developerClaims[key];
296309
}
297310
}
298311
}
@@ -305,7 +318,7 @@ export class FirebaseTokenGenerator {
305318
const body: JWTBody = {
306319
aud: FIREBASE_AUDIENCE,
307320
iat,
308-
exp: iat + ONE_HOUR_IN_SECONDS,
321+
exp: iat + Math.floor((options?.expiresIn || ONE_HOUR_IN_MS) / 1000),
309322
iss: account,
310323
sub: account,
311324
uid,

src/utils/validator.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ export function isBoolean(value: any): boolean {
6464
* @param {any} value The value to validate.
6565
* @return {boolean} Whether the value is a number or not.
6666
*/
67-
export function isNumber(value: any): boolean {
67+
export function isNumber(value: any): value is number {
6868
return typeof value === 'number' && !isNaN(value);
6969
}
7070

@@ -111,7 +111,7 @@ export function isNonEmptyString(value: any): value is string {
111111
* @param {any} value The value to validate.
112112
* @return {boolean} Whether the value is an object or not.
113113
*/
114-
export function isObject(value: any): boolean {
114+
export function isObject(value: any): value is object {
115115
return typeof value === 'object' && !isArray(value);
116116
}
117117

test/unit/auth/token-generator.spec.ts

+60-7
Original file line numberDiff line numberDiff line change
@@ -349,35 +349,62 @@ describe('FirebaseTokenGenerator', () => {
349349
}).to.throw(FirebaseAuthError).with.property('code', 'auth/argument-error');
350350
});
351351

352+
it('should throw given a non-object options', () => {
353+
const invalidOptions: any[] = [NaN, [], true, false, '', 'a', 0, 1, Infinity, _.noop];
354+
invalidOptions.forEach((opts) => {
355+
expect(() => {
356+
tokenGenerator.createCustomToken(mocks.uid, opts);
357+
}).to.throw(FirebaseAuthError).with.property('code', 'auth/argument-error');
358+
});
359+
});
360+
352361
it('should throw given a non-object developer claims', () => {
353362
const invalidDeveloperClaims: any[] = [null, NaN, [], true, false, '', 'a', 0, 1, Infinity, _.noop];
354363
invalidDeveloperClaims.forEach((invalidDevClaims) => {
355364
expect(() => {
356-
tokenGenerator.createCustomToken(mocks.uid, invalidDevClaims);
365+
tokenGenerator.createCustomToken(mocks.uid, { developerClaims: invalidDevClaims });
357366
}).to.throw(FirebaseAuthError).with.property('code', 'auth/argument-error');
358367
});
359368
});
360369

361370
BLACKLISTED_CLAIMS.forEach((blacklistedClaim) => {
362371
it('should throw given a developer claims object with a blacklisted claim: ' + blacklistedClaim, () => {
363-
const blacklistedDeveloperClaims: {[key: string]: any} = _.clone(mocks.developerClaims);
372+
const blacklistedDeveloperClaims: { [key: string]: any } = _.clone(mocks.developerClaims);
364373
blacklistedDeveloperClaims[blacklistedClaim] = true;
365374
expect(() => {
366-
tokenGenerator.createCustomToken(mocks.uid, blacklistedDeveloperClaims);
375+
tokenGenerator.createCustomToken(mocks.uid, { developerClaims: blacklistedDeveloperClaims });
367376
}).to.throw(FirebaseAuthError, blacklistedClaim).with.property('code', 'auth/argument-error');
368377
});
369378
});
370379

380+
it('should throw given an invalid expiresIn', () => {
381+
const invalidExpiresIns: any[] = [null, NaN, Infinity, _.noop, 0, 999, 3600001];
382+
invalidExpiresIns.forEach((invalidExpiresIn) => {
383+
expect(() => {
384+
tokenGenerator.createCustomToken(mocks.uid, { expiresIn: invalidExpiresIn });
385+
}).to.throw(FirebaseAuthError).with.property('code', 'auth/argument-error');
386+
});
387+
});
388+
371389
it('should be fulfilled given a valid uid and no developer claims', () => {
372390
return tokenGenerator.createCustomToken(mocks.uid);
373391
});
374392

375393
it('should be fulfilled given a valid uid and empty object developer claims', () => {
376-
return tokenGenerator.createCustomToken(mocks.uid, {});
394+
return tokenGenerator.createCustomToken(mocks.uid, { developerClaims: {} });
377395
});
378396

379397
it('should be fulfilled given a valid uid and valid developer claims', () => {
380-
return tokenGenerator.createCustomToken(mocks.uid, mocks.developerClaims);
398+
return tokenGenerator.createCustomToken(mocks.uid, { developerClaims: mocks.developerClaims });
399+
});
400+
401+
it('should be fulfilled given a valid uid, empty object developer claims and valid expiresIn', () => {
402+
return tokenGenerator.createCustomToken(mocks.uid, { developerClaims: {}, expiresIn: 1000 });
403+
});
404+
405+
it('should be fulfilled given a valid uid, valid developer claims and valid expiresIn', () => {
406+
return tokenGenerator
407+
.createCustomToken(mocks.uid, { developerClaims: mocks.developerClaims, expiresIn: 3600000 });
381408
});
382409

383410
it('should be fulfilled with a Firebase Custom JWT', () => {
@@ -412,7 +439,7 @@ describe('FirebaseTokenGenerator', () => {
412439
it('should be fulfilled with a JWT with the developer claims in its decoded payload', () => {
413440
clock = sinon.useFakeTimers(1000);
414441

415-
return tokenGenerator.createCustomToken(mocks.uid, mocks.developerClaims)
442+
return tokenGenerator.createCustomToken(mocks.uid, { developerClaims: mocks.developerClaims })
416443
.then((token) => {
417444
const decoded = jwt.decode(token);
418445

@@ -438,6 +465,32 @@ describe('FirebaseTokenGenerator', () => {
438465
});
439466
});
440467

468+
it('should be fulfilled with a JWT with the expiresIn in its exp payload', () => {
469+
clock = sinon.useFakeTimers(2000);
470+
const expiresIn = 300900
471+
472+
return tokenGenerator.createCustomToken(mocks.uid, { expiresIn })
473+
.then((token) => {
474+
const decoded = jwt.decode(token);
475+
476+
const expected: { [key: string]: any } = {
477+
uid: mocks.uid,
478+
iat: 2,
479+
exp: 302,
480+
aud: FIREBASE_AUDIENCE,
481+
iss: mocks.certificateObject.client_email,
482+
sub: mocks.certificateObject.client_email,
483+
};
484+
485+
if (tokenGenerator.tenantId) {
486+
// eslint-disable-next-line @typescript-eslint/camelcase
487+
expected.tenant_id = tokenGenerator.tenantId;
488+
}
489+
490+
expect(decoded).to.deep.equal(expected);
491+
});
492+
});
493+
441494
it('should be fulfilled with a JWT with the correct header', () => {
442495
clock = sinon.useFakeTimers(1000);
443496

@@ -495,7 +548,7 @@ describe('FirebaseTokenGenerator', () => {
495548
foo: 'bar',
496549
};
497550
const clonedClaims = _.clone(originalClaims);
498-
return tokenGenerator.createCustomToken(mocks.uid, clonedClaims)
551+
return tokenGenerator.createCustomToken(mocks.uid, { developerClaims: clonedClaims })
499552
.then(() => {
500553
expect(originalClaims).to.deep.equal(clonedClaims);
501554
});

0 commit comments

Comments
 (0)