Skip to content

Commit

Permalink
Adds support for Firebase Auth session management. (#245)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
bojeil-google authored Apr 5, 2018
1 parent 4d27e90 commit 49d6bcd
Show file tree
Hide file tree
Showing 17 changed files with 1,937 additions and 513 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Unreleased

- [feature] Added the session cookie management APIs for creating and verifying
session cookies, via `auth.createSessionCookie()` and
`auth.verifySessionCookie()`.
- [added] Added the `mutableContent` optional field to the `Aps` type of
the FCM API.
- [added] Added the support for specifying arbitrary custom key-value
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"test": "run-s lint test:unit",
"integration": "run-s build test:integration",
"test:unit": "mocha test/unit/*.spec.ts --compilers ts:ts-node/register",
"test:integration": "mocha test/integration/*.ts --slow 5000 --compilers ts:ts-node/register",
"test:integration": "mocha test/integration/*.ts --slow 5000 --timeout 5000 --compilers ts:ts-node/register",
"test:coverage": "nyc npm run test:unit",
"lint:src": "tslint --format stylish -p tsconfig.json",
"lint:unit": "tslint -c tslint-test.json --format stylish test/unit/*.ts test/unit/**/*.ts",
Expand Down
51 changes: 51 additions & 0 deletions src/auth/auth-api-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ const MAX_DOWNLOAD_ACCOUNT_PAGE_SIZE = 1000;
/** Maximum allowed number of users to batch upload at one time. */
const MAX_UPLOAD_ACCOUNT_BATCH_SIZE = 1000;

/** Minimum allowed session cookie duration in seconds (5 minutes). */
const MIN_SESSION_COOKIE_DURATION_SECS = 5 * 60;

/** Maximum allowed session cookie duration in seconds (2 weeks). */
const MAX_SESSION_COOKIE_DURATION_SECS = 14 * 24 * 60 * 60;


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


/** Instantiates the createSessionCookie endpoint settings. */
export const FIREBASE_AUTH_CREATE_SESSION_COOKIE =
new ApiSettings('createSessionCookie', 'POST')
// Set request validator.
.setRequestValidator((request: any) => {
// Validate the ID token is a non-empty string.
if (!validator.isNonEmptyString(request.idToken)) {
throw new FirebaseAuthError(AuthClientErrorCode.INVALID_ID_TOKEN);
}
// Validate the custom session cookie duration.
if (!validator.isNumber(request.validDuration) ||
request.validDuration < MIN_SESSION_COOKIE_DURATION_SECS ||
request.validDuration > MAX_SESSION_COOKIE_DURATION_SECS) {
throw new FirebaseAuthError(AuthClientErrorCode.INVALID_SESSION_COOKIE_DURATION);
}
})
// Set response validator.
.setResponseValidator((response: any) => {
// Response should always contain the session cookie.
if (!validator.isNonEmptyString(response.sessionCookie)) {
throw new FirebaseAuthError(AuthClientErrorCode.INTERNAL_ERROR);
}
});


/** Instantiates the uploadAccount endpoint settings. */
export const FIREBASE_AUTH_UPLOAD_ACCOUNT = new ApiSettings('uploadAccount', 'POST');

Expand Down Expand Up @@ -421,6 +452,26 @@ export class FirebaseAuthRequestHandler {
this.signedApiRequestHandler = new SignedApiRequestHandler(app);
}

/**
* Creates a new Firebase session cookie with the specified duration that can be used for
* session management (set as a server side session cookie with custom cookie policy).
* The session cookie JWT will have the same payload claims as the provided ID token.
*
* @param {string} idToken The Firebase ID token to exchange for a session cookie.
* @param {number} expiresIn The session cookie duration in milliseconds.
*
* @return {Promise<string>} A promise that resolves on success with the created session cookie.
*/
public createSessionCookie(idToken: string, expiresIn: number): Promise<string> {
const request = {
idToken,
// Convert to seconds.
validDuration: expiresIn / 1000,
};
return this.invokeRequestHandler(FIREBASE_AUTH_CREATE_SESSION_COOKIE, request)
.then((response: any) => response.sessionCookie);
}

/**
* Looks up a user by uid.
*
Expand Down
116 changes: 97 additions & 19 deletions src/auth/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {Certificate} from './credential';
import {FirebaseApp} from '../firebase-app';
import {FirebaseTokenGenerator} from './token-generator';
import {FirebaseAuthRequestHandler} from './auth-api-request';
import {AuthClientErrorCode, FirebaseAuthError} from '../utils/error';
import {AuthClientErrorCode, FirebaseAuthError, ErrorInfo} from '../utils/error';
import {FirebaseServiceInterface, FirebaseServiceInternalsInterface} from '../firebase-service';
import {
UserImportOptions, UserImportRecord, UserImportResult,
Expand Down Expand Up @@ -51,7 +51,7 @@ export interface ListUsersResult {
}


/** Inteface representing a decoded ID token. */
/** Interface representing a decoded ID token. */
export interface DecodedIdToken {
aud: string;
auth_time: number;
Expand All @@ -70,6 +70,12 @@ export interface DecodedIdToken {
}


/** Interface representing the session cookie options. */
export interface SessionCookieOptions {
expiresIn: number;
}


/**
* Auth service bound to the provided app.
*/
Expand Down Expand Up @@ -171,23 +177,9 @@ export class Auth implements FirebaseServiceInterface {
if (!checkRevoked) {
return decodedIdToken;
}
// Get tokens valid after time for the corresponding user.
return this.getUser(decodedIdToken.sub)
.then((user: UserRecord) => {
// If no tokens valid after time available, token is not revoked.
if (user.tokensValidAfterTime) {
// Get the ID token authentication time and convert to milliseconds UTC.
const authTimeUtc = decodedIdToken.auth_time * 1000;
// Get user tokens valid after time in milliseconds UTC.
const validSinceUtc = new Date(user.tokensValidAfterTime).getTime();
// Check if authentication time is older than valid since time.
if (authTimeUtc < validSinceUtc) {
throw new FirebaseAuthError(AuthClientErrorCode.ID_TOKEN_REVOKED);
}
}
// All checks above passed. Return the decoded token.
return decodedIdToken;
});
return this.verifyDecodedJWTNotRevoked(
decodedIdToken,
AuthClientErrorCode.ID_TOKEN_REVOKED);
});
}

Expand Down Expand Up @@ -371,4 +363,90 @@ export class Auth implements FirebaseServiceInterface {
users: UserImportRecord[], options?: UserImportOptions): Promise<UserImportResult> {
return this.authRequestHandler.uploadAccount(users, options);
}

/**
* Creates a new Firebase session cookie with the specified options that can be used for
* session management (set as a server side session cookie with custom cookie policy).
* The session cookie JWT will have the same payload claims as the provided ID token.
*
* @param {string} idToken The Firebase ID token to exchange for a session cookie.
* @param {SessionCookieOptions} sessionCookieOptions The session cookie options which includes
* custom session duration.
*
* @return {Promise<string>} A promise that resolves on success with the created session cookie.
*/
public createSessionCookie(
idToken: string, sessionCookieOptions: SessionCookieOptions): Promise<string> {
// Return rejected promise if expiresIn is not available.
if (!validator.isNonNullObject(sessionCookieOptions) ||
!validator.isNumber(sessionCookieOptions.expiresIn)) {
return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_SESSION_COOKIE_DURATION));
}
return this.authRequestHandler.createSessionCookie(
idToken, sessionCookieOptions.expiresIn);
}

/**
* Verifies a Firebase session cookie. Returns a Promise with the tokens claims. Rejects
* the promise if the token could not be verified. If checkRevoked is set to true,
* verifies if the session corresponding to the session cookie was revoked. If the corresponding
* user's session was invalidated, an auth/session-cookie-revoked error is thrown. If not
* specified the check is not performed.
*
* @param {string} sessionCookie The session cookie to verify.
* @param {boolean=} checkRevoked Whether to check if the session cookie is revoked.
* @return {Promise<DecodedIdToken>} A Promise that will be fulfilled after a successful
* verification.
*/
public verifySessionCookie(
sessionCookie: string, checkRevoked: boolean = false): Promise<DecodedIdToken> {
if (typeof this.tokenGenerator_ === 'undefined') {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_CREDENTIAL,
'Must initialize app with a cert credential or set your Firebase project ID as the ' +
'GCLOUD_PROJECT environment variable to call auth().verifySessionCookie().',
);
}
return this.tokenGenerator_.verifySessionCookie(sessionCookie)
.then((decodedIdToken: DecodedIdToken) => {
// Whether to check if the token was revoked.
if (!checkRevoked) {
return decodedIdToken;
}
return this.verifyDecodedJWTNotRevoked(
decodedIdToken,
AuthClientErrorCode.SESSION_COOKIE_REVOKED);
});
}

/**
* Verifies the decoded Firebase issued JWT is not revoked. Returns a promise that resolves
* with the decoded claims on success. Rejects the promise with revocation error if revoked.
*
* @param {DecodedIdToken} decodedIdToken The JWT's decoded claims.
* @param {ErrorInfo} revocationErrorInfo The revocation error info to throw on revocation
* detection.
* @return {Promise<DecodedIdToken>} A Promise that will be fulfilled after a successful
* verification.
*/
private verifyDecodedJWTNotRevoked(
decodedIdToken: DecodedIdToken, revocationErrorInfo: ErrorInfo): Promise<DecodedIdToken> {
// Get tokens valid after time for the corresponding user.
return this.getUser(decodedIdToken.sub)
.then((user: UserRecord) => {
// If no tokens valid after time available, token is not revoked.
if (user.tokensValidAfterTime) {
// Get the ID token authentication time and convert to milliseconds UTC.
const authTimeUtc = decodedIdToken.auth_time * 1000;
// Get user tokens valid after time in milliseconds UTC.
const validSinceUtc = new Date(user.tokensValidAfterTime).getTime();
// Check if authentication time is older than valid since time.
if (authTimeUtc < validSinceUtc) {
throw new FirebaseAuthError(revocationErrorInfo);
}
}
// All checks above passed. Return the decoded token.
return decodedIdToken;
});
}
}
Loading

0 comments on commit 49d6bcd

Please sign in to comment.