Skip to content

Commit 49d6bcd

Browse files
Adds support for Firebase Auth session management. (#245)
* Adds support for Firebase Auth session management. This adds 2 new APIs: admin.auth().createSessionCookie(idToken: string, sessionCookieOptions: SessionCookieOptions): Promise<string> admin.auth().verifySessionCookie(sessionCookie: string, checkRevoked?: boolean): Promise<DecodedIdToken> Refactored token generator and split token verification to a new class FirebaseTokenVerifier so it can be used to also verify session cookies. Kept the same error handling for backward compatibility. In the process, ported the same tests to token verifier. Updated token generator ID token and session cookie verification to check token verifier is called underneath with the expected parameters. Added integration tests to test all common flows for session cookie creation and verification. Added mocks for session cookie JWTs.
1 parent 4d27e90 commit 49d6bcd

17 files changed

+1937
-513
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Unreleased
22

3+
- [feature] Added the session cookie management APIs for creating and verifying
4+
session cookies, via `auth.createSessionCookie()` and
5+
`auth.verifySessionCookie()`.
36
- [added] Added the `mutableContent` optional field to the `Aps` type of
47
the FCM API.
58
- [added] Added the support for specifying arbitrary custom key-value

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"test": "run-s lint test:unit",
1212
"integration": "run-s build test:integration",
1313
"test:unit": "mocha test/unit/*.spec.ts --compilers ts:ts-node/register",
14-
"test:integration": "mocha test/integration/*.ts --slow 5000 --compilers ts:ts-node/register",
14+
"test:integration": "mocha test/integration/*.ts --slow 5000 --timeout 5000 --compilers ts:ts-node/register",
1515
"test:coverage": "nyc npm run test:unit",
1616
"lint:src": "tslint --format stylish -p tsconfig.json",
1717
"lint:unit": "tslint -c tslint-test.json --format stylish test/unit/*.ts test/unit/**/*.ts",

src/auth/auth-api-request.ts

+51
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,12 @@ const MAX_DOWNLOAD_ACCOUNT_PAGE_SIZE = 1000;
5959
/** Maximum allowed number of users to batch upload at one time. */
6060
const MAX_UPLOAD_ACCOUNT_BATCH_SIZE = 1000;
6161

62+
/** Minimum allowed session cookie duration in seconds (5 minutes). */
63+
const MIN_SESSION_COOKIE_DURATION_SECS = 5 * 60;
64+
65+
/** Maximum allowed session cookie duration in seconds (2 weeks). */
66+
const MAX_SESSION_COOKIE_DURATION_SECS = 14 * 24 * 60 * 60;
67+
6268

6369
/**
6470
* Validates a providerUserInfo object. All unsupported parameters
@@ -287,6 +293,31 @@ function validateCreateEditRequest(request: any, uploadAccountRequest: boolean =
287293
}
288294

289295

296+
/** Instantiates the createSessionCookie endpoint settings. */
297+
export const FIREBASE_AUTH_CREATE_SESSION_COOKIE =
298+
new ApiSettings('createSessionCookie', 'POST')
299+
// Set request validator.
300+
.setRequestValidator((request: any) => {
301+
// Validate the ID token is a non-empty string.
302+
if (!validator.isNonEmptyString(request.idToken)) {
303+
throw new FirebaseAuthError(AuthClientErrorCode.INVALID_ID_TOKEN);
304+
}
305+
// Validate the custom session cookie duration.
306+
if (!validator.isNumber(request.validDuration) ||
307+
request.validDuration < MIN_SESSION_COOKIE_DURATION_SECS ||
308+
request.validDuration > MAX_SESSION_COOKIE_DURATION_SECS) {
309+
throw new FirebaseAuthError(AuthClientErrorCode.INVALID_SESSION_COOKIE_DURATION);
310+
}
311+
})
312+
// Set response validator.
313+
.setResponseValidator((response: any) => {
314+
// Response should always contain the session cookie.
315+
if (!validator.isNonEmptyString(response.sessionCookie)) {
316+
throw new FirebaseAuthError(AuthClientErrorCode.INTERNAL_ERROR);
317+
}
318+
});
319+
320+
290321
/** Instantiates the uploadAccount endpoint settings. */
291322
export const FIREBASE_AUTH_UPLOAD_ACCOUNT = new ApiSettings('uploadAccount', 'POST');
292323

@@ -421,6 +452,26 @@ export class FirebaseAuthRequestHandler {
421452
this.signedApiRequestHandler = new SignedApiRequestHandler(app);
422453
}
423454

455+
/**
456+
* Creates a new Firebase session cookie with the specified duration that can be used for
457+
* session management (set as a server side session cookie with custom cookie policy).
458+
* The session cookie JWT will have the same payload claims as the provided ID token.
459+
*
460+
* @param {string} idToken The Firebase ID token to exchange for a session cookie.
461+
* @param {number} expiresIn The session cookie duration in milliseconds.
462+
*
463+
* @return {Promise<string>} A promise that resolves on success with the created session cookie.
464+
*/
465+
public createSessionCookie(idToken: string, expiresIn: number): Promise<string> {
466+
const request = {
467+
idToken,
468+
// Convert to seconds.
469+
validDuration: expiresIn / 1000,
470+
};
471+
return this.invokeRequestHandler(FIREBASE_AUTH_CREATE_SESSION_COOKIE, request)
472+
.then((response: any) => response.sessionCookie);
473+
}
474+
424475
/**
425476
* Looks up a user by uid.
426477
*

src/auth/auth.ts

+97-19
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {Certificate} from './credential';
1919
import {FirebaseApp} from '../firebase-app';
2020
import {FirebaseTokenGenerator} from './token-generator';
2121
import {FirebaseAuthRequestHandler} from './auth-api-request';
22-
import {AuthClientErrorCode, FirebaseAuthError} from '../utils/error';
22+
import {AuthClientErrorCode, FirebaseAuthError, ErrorInfo} from '../utils/error';
2323
import {FirebaseServiceInterface, FirebaseServiceInternalsInterface} from '../firebase-service';
2424
import {
2525
UserImportOptions, UserImportRecord, UserImportResult,
@@ -51,7 +51,7 @@ export interface ListUsersResult {
5151
}
5252

5353

54-
/** Inteface representing a decoded ID token. */
54+
/** Interface representing a decoded ID token. */
5555
export interface DecodedIdToken {
5656
aud: string;
5757
auth_time: number;
@@ -70,6 +70,12 @@ export interface DecodedIdToken {
7070
}
7171

7272

73+
/** Interface representing the session cookie options. */
74+
export interface SessionCookieOptions {
75+
expiresIn: number;
76+
}
77+
78+
7379
/**
7480
* Auth service bound to the provided app.
7581
*/
@@ -171,23 +177,9 @@ export class Auth implements FirebaseServiceInterface {
171177
if (!checkRevoked) {
172178
return decodedIdToken;
173179
}
174-
// Get tokens valid after time for the corresponding user.
175-
return this.getUser(decodedIdToken.sub)
176-
.then((user: UserRecord) => {
177-
// If no tokens valid after time available, token is not revoked.
178-
if (user.tokensValidAfterTime) {
179-
// Get the ID token authentication time and convert to milliseconds UTC.
180-
const authTimeUtc = decodedIdToken.auth_time * 1000;
181-
// Get user tokens valid after time in milliseconds UTC.
182-
const validSinceUtc = new Date(user.tokensValidAfterTime).getTime();
183-
// Check if authentication time is older than valid since time.
184-
if (authTimeUtc < validSinceUtc) {
185-
throw new FirebaseAuthError(AuthClientErrorCode.ID_TOKEN_REVOKED);
186-
}
187-
}
188-
// All checks above passed. Return the decoded token.
189-
return decodedIdToken;
190-
});
180+
return this.verifyDecodedJWTNotRevoked(
181+
decodedIdToken,
182+
AuthClientErrorCode.ID_TOKEN_REVOKED);
191183
});
192184
}
193185

@@ -371,4 +363,90 @@ export class Auth implements FirebaseServiceInterface {
371363
users: UserImportRecord[], options?: UserImportOptions): Promise<UserImportResult> {
372364
return this.authRequestHandler.uploadAccount(users, options);
373365
}
366+
367+
/**
368+
* Creates a new Firebase session cookie with the specified options that can be used for
369+
* session management (set as a server side session cookie with custom cookie policy).
370+
* The session cookie JWT will have the same payload claims as the provided ID token.
371+
*
372+
* @param {string} idToken The Firebase ID token to exchange for a session cookie.
373+
* @param {SessionCookieOptions} sessionCookieOptions The session cookie options which includes
374+
* custom session duration.
375+
*
376+
* @return {Promise<string>} A promise that resolves on success with the created session cookie.
377+
*/
378+
public createSessionCookie(
379+
idToken: string, sessionCookieOptions: SessionCookieOptions): Promise<string> {
380+
// Return rejected promise if expiresIn is not available.
381+
if (!validator.isNonNullObject(sessionCookieOptions) ||
382+
!validator.isNumber(sessionCookieOptions.expiresIn)) {
383+
return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_SESSION_COOKIE_DURATION));
384+
}
385+
return this.authRequestHandler.createSessionCookie(
386+
idToken, sessionCookieOptions.expiresIn);
387+
}
388+
389+
/**
390+
* Verifies a Firebase session cookie. Returns a Promise with the tokens claims. Rejects
391+
* the promise if the token could not be verified. If checkRevoked is set to true,
392+
* verifies if the session corresponding to the session cookie was revoked. If the corresponding
393+
* user's session was invalidated, an auth/session-cookie-revoked error is thrown. If not
394+
* specified the check is not performed.
395+
*
396+
* @param {string} sessionCookie The session cookie to verify.
397+
* @param {boolean=} checkRevoked Whether to check if the session cookie is revoked.
398+
* @return {Promise<DecodedIdToken>} A Promise that will be fulfilled after a successful
399+
* verification.
400+
*/
401+
public verifySessionCookie(
402+
sessionCookie: string, checkRevoked: boolean = false): Promise<DecodedIdToken> {
403+
if (typeof this.tokenGenerator_ === 'undefined') {
404+
throw new FirebaseAuthError(
405+
AuthClientErrorCode.INVALID_CREDENTIAL,
406+
'Must initialize app with a cert credential or set your Firebase project ID as the ' +
407+
'GCLOUD_PROJECT environment variable to call auth().verifySessionCookie().',
408+
);
409+
}
410+
return this.tokenGenerator_.verifySessionCookie(sessionCookie)
411+
.then((decodedIdToken: DecodedIdToken) => {
412+
// Whether to check if the token was revoked.
413+
if (!checkRevoked) {
414+
return decodedIdToken;
415+
}
416+
return this.verifyDecodedJWTNotRevoked(
417+
decodedIdToken,
418+
AuthClientErrorCode.SESSION_COOKIE_REVOKED);
419+
});
420+
}
421+
422+
/**
423+
* Verifies the decoded Firebase issued JWT is not revoked. Returns a promise that resolves
424+
* with the decoded claims on success. Rejects the promise with revocation error if revoked.
425+
*
426+
* @param {DecodedIdToken} decodedIdToken The JWT's decoded claims.
427+
* @param {ErrorInfo} revocationErrorInfo The revocation error info to throw on revocation
428+
* detection.
429+
* @return {Promise<DecodedIdToken>} A Promise that will be fulfilled after a successful
430+
* verification.
431+
*/
432+
private verifyDecodedJWTNotRevoked(
433+
decodedIdToken: DecodedIdToken, revocationErrorInfo: ErrorInfo): Promise<DecodedIdToken> {
434+
// Get tokens valid after time for the corresponding user.
435+
return this.getUser(decodedIdToken.sub)
436+
.then((user: UserRecord) => {
437+
// If no tokens valid after time available, token is not revoked.
438+
if (user.tokensValidAfterTime) {
439+
// Get the ID token authentication time and convert to milliseconds UTC.
440+
const authTimeUtc = decodedIdToken.auth_time * 1000;
441+
// Get user tokens valid after time in milliseconds UTC.
442+
const validSinceUtc = new Date(user.tokensValidAfterTime).getTime();
443+
// Check if authentication time is older than valid since time.
444+
if (authTimeUtc < validSinceUtc) {
445+
throw new FirebaseAuthError(revocationErrorInfo);
446+
}
447+
}
448+
// All checks above passed. Return the decoded token.
449+
return decodedIdToken;
450+
});
451+
}
374452
}

0 commit comments

Comments
 (0)