Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds support for Firebase Auth session management. #245

Merged
merged 5 commits into from
Apr 5, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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