Skip to content

Commit

Permalink
Adding in alpha interface for blocking token verification (#1635)
Browse files Browse the repository at this point in the history
* adding in alpha interface

* adding token verifier & unit tests

* remove old ref

* adding exports

* address pr comments
  • Loading branch information
colerogers authored Apr 19, 2022
1 parent b01d1a9 commit 6b97607
Show file tree
Hide file tree
Showing 10 changed files with 660 additions and 10 deletions.
4 changes: 4 additions & 0 deletions etc/firebase-admin.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@ export namespace auth {
export type CreateRequest = CreateRequest;
// Warning: (ae-forgotten-export) The symbol "CreateTenantRequest" needs to be exported by the entry point default-namespace.d.ts
export type CreateTenantRequest = CreateTenantRequest;
// Warning: (ae-forgotten-export) The symbol "DecodedAuthBlockingToken" needs to be exported by the entry point default-namespace.d.ts
//
// @alpha (undocumented)
export type DecodedAuthBlockingToken = DecodedAuthBlockingToken;
// Warning: (ae-forgotten-export) The symbol "DecodedIdToken" needs to be exported by the entry point default-namespace.d.ts
export type DecodedIdToken = DecodedIdToken;
// Warning: (ae-forgotten-export) The symbol "DeleteUsersResult" needs to be exported by the entry point default-namespace.d.ts
Expand Down
52 changes: 52 additions & 0 deletions etc/firebase-admin.auth.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ export abstract class BaseAuth {
setCustomUserClaims(uid: string, customUserClaims: object | null): Promise<void>;
updateProviderConfig(providerId: string, updatedConfig: UpdateAuthProviderRequest): Promise<AuthProviderConfig>;
updateUser(uid: string, properties: UpdateRequest): Promise<UserRecord>;
// @alpha (undocumented)
_verifyAuthBlockingToken(token: string, audience?: string): Promise<DecodedAuthBlockingToken>;
verifyIdToken(idToken: string, checkRevoked?: boolean): Promise<DecodedIdToken>;
verifySessionCookie(sessionCookie: string, checkRevoked?: boolean): Promise<DecodedIdToken>;
}
Expand Down Expand Up @@ -110,6 +112,56 @@ export interface CreateRequest extends UpdateRequest {
// @public
export type CreateTenantRequest = UpdateTenantRequest;

// @alpha (undocumented)
export interface DecodedAuthBlockingToken {
// (undocumented)
[key: string]: any;
// (undocumented)
aud: string;
// (undocumented)
event_id: string;
// (undocumented)
event_type: string;
// (undocumented)
exp: number;
// (undocumented)
iat: number;
// (undocumented)
ip_address: string;
// (undocumented)
iss: string;
// (undocumented)
locale?: string;
// (undocumented)
oauth_access_token?: string;
// (undocumented)
oauth_expires_in?: number;
// (undocumented)
oauth_id_token?: string;
// (undocumented)
oauth_refresh_token?: string;
// (undocumented)
oauth_token_secret?: string;
// (undocumented)
raw_user_info?: string;
// (undocumented)
sign_in_attributes?: {
[key: string]: any;
};
// (undocumented)
sign_in_method?: string;
// (undocumented)
sub: string;
// (undocumented)
tenant_id?: string;
// (undocumented)
user_agent?: string;
// Warning: (ae-forgotten-export) The symbol "DecodedAuthBlockingUserRecord" needs to be exported by the entry point index.d.ts
//
// (undocumented)
user_record?: DecodedAuthBlockingUserRecord;
}

// @public
export interface DecodedIdToken {
[key: string]: any;
Expand Down
8 changes: 7 additions & 1 deletion src/auth/auth-namespace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,10 @@ import {
TenantManager as TTenantManager,
} from './tenant-manager';

import { DecodedIdToken as TDecodedIdToken } from './token-verifier';
import {
DecodedIdToken as TDecodedIdToken,
DecodedAuthBlockingToken as TDecodedAuthBlockingToken,
} from './token-verifier';

import {
HashAlgorithmType as THashAlgorithmType,
Expand Down Expand Up @@ -173,6 +176,9 @@ export namespace auth {
*/
export type DecodedIdToken = TDecodedIdToken;

/** @alpha */
export type DecodedAuthBlockingToken = TDecodedAuthBlockingToken;

/**
* Type alias to {@link firebase-admin.auth#DeleteUsersResult}.
*/
Expand Down
22 changes: 21 additions & 1 deletion src/auth/base-auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,12 @@ import * as validator from '../utils/validator';
import { AbstractAuthRequestHandler, useEmulator } from './auth-api-request';
import { FirebaseTokenGenerator, EmulatedSigner, handleCryptoSignerError } from './token-generator';
import {
FirebaseTokenVerifier, createSessionCookieVerifier, createIdTokenVerifier,
FirebaseTokenVerifier,
createSessionCookieVerifier,
createIdTokenVerifier,
createAuthBlockingTokenVerifier,
DecodedIdToken,
DecodedAuthBlockingToken,
} from './token-verifier';
import {
AuthProviderConfig, SAMLAuthProviderConfig, AuthProviderConfigFilter, ListProviderConfigResults,
Expand Down Expand Up @@ -131,6 +135,8 @@ export abstract class BaseAuth {
/** @internal */
protected readonly idTokenVerifier: FirebaseTokenVerifier;
/** @internal */
protected readonly authBlockingTokenVerifier: FirebaseTokenVerifier;
/** @internal */
protected readonly sessionCookieVerifier: FirebaseTokenVerifier;

/**
Expand All @@ -156,6 +162,7 @@ export abstract class BaseAuth {

this.sessionCookieVerifier = createSessionCookieVerifier(app);
this.idTokenVerifier = createIdTokenVerifier(app);
this.authBlockingTokenVerifier = createAuthBlockingTokenVerifier(app);
}

/**
Expand Down Expand Up @@ -1055,6 +1062,19 @@ export abstract class BaseAuth {
return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID));
}

/** @alpha */
// eslint-disable-next-line @typescript-eslint/naming-convention
public _verifyAuthBlockingToken(
token: string,
audience?: string
): Promise<DecodedAuthBlockingToken> {
const isEmulator = useEmulator();
return this.authBlockingTokenVerifier._verifyAuthBlockingToken(token, isEmulator, audience)
.then((decodedAuthBlockingToken: DecodedAuthBlockingToken) => {
return decodedAuthBlockingToken;
});
}

/**
* Verifies the decoded Firebase issued JWT is not revoked or disabled. Returns a promise that
* resolves with the decoded claims on success. Rejects the promise with revocation error if revoked
Expand Down
5 changes: 4 additions & 1 deletion src/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,10 @@ export {
TenantManager,
} from './tenant-manager';

export { DecodedIdToken } from './token-verifier';
export {
DecodedIdToken,
DecodedAuthBlockingToken
} from './token-verifier';

export {
HashAlgorithmType,
Expand Down
152 changes: 146 additions & 6 deletions src/auth/token-verifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,82 @@ export interface DecodedIdToken {
[key: string]: any;
}

/** @alpha */
export interface DecodedAuthBlockingSharedUserInfo {
uid: string;
display_name?: string;
email?: string;
photo_url?: string;
phone_number?: string;
}

/** @alpha */
export interface DecodedAuthBlockingMetadata {
creation_time?: number;
last_sign_in_time?: number;
}

/** @alpha */
export interface DecodedAuthBlockingUserInfo extends DecodedAuthBlockingSharedUserInfo {
provider_id: string;
}

/** @alpha */
export interface DecodedAuthBlockingMfaInfo {
uid: string;
display_name?: string;
phone_number?: string;
enrollment_time?: string;
factor_id?: string;
}

/** @alpha */
export interface DecodedAuthBlockingEnrolledFactors {
enrolled_factors?: DecodedAuthBlockingMfaInfo[];
}

/** @alpha */
export interface DecodedAuthBlockingUserRecord extends DecodedAuthBlockingSharedUserInfo {
email_verified?: boolean;
disabled?: boolean;
metadata?: DecodedAuthBlockingMetadata;
password_hash?: string;
password_salt?: string;
provider_data?: DecodedAuthBlockingUserInfo[];
multi_factor?: DecodedAuthBlockingEnrolledFactors;
custom_claims?: any;
tokens_valid_after_time?: number;
tenant_id?: string;
[key: string]: any;
}

/** @alpha */
export interface DecodedAuthBlockingToken {
aud: string;
exp: number;
iat: number;
iss: string;
sub: string;
event_id: string;
event_type: string;
ip_address: string;
user_agent?: string;
locale?: string;
sign_in_method?: string;
user_record?: DecodedAuthBlockingUserRecord;
tenant_id?: string;
raw_user_info?: string;
sign_in_attributes?: {
[key: string]: any;
};
oauth_id_token?: string;
oauth_access_token?: string;
oauth_refresh_token?: string;
oauth_token_secret?: string;
oauth_expires_in?: number;
[key: string]: any;
}

// Audience to use for Firebase Auth Custom tokens
const FIREBASE_AUDIENCE = 'https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit';

Expand All @@ -201,6 +277,19 @@ export const ID_TOKEN_INFO: FirebaseTokenInfo = {
expiredErrorCode: AuthClientErrorCode.ID_TOKEN_EXPIRED,
};

/**
* User facing token information related to the Firebase Auth Blocking token.
*
* @internal
*/
export const AUTH_BLOCKING_TOKEN_INFO: FirebaseTokenInfo = {
url: 'https://cloud.google.com/identity-platform/docs/blocking-functions',
verifyApiName: '_verifyAuthBlockingToken()',
jwtName: 'Firebase Auth Blocking token',
shortName: 'Auth Blocking token',
expiredErrorCode: AuthClientErrorCode.AUTH_BLOCKING_TOKEN_EXPIRED,
};

/**
* User facing token information related to the Firebase session cookie.
*
Expand Down Expand Up @@ -320,6 +409,33 @@ export class FirebaseTokenVerifier {
});
}

/** @alpha */
// eslint-disable-next-line @typescript-eslint/naming-convention
public _verifyAuthBlockingToken(
jwtToken: string,
isEmulator: boolean,
audience: string | undefined): Promise<DecodedAuthBlockingToken> {
if (!validator.isString(jwtToken)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_ARGUMENT,
`First argument to ${this.tokenInfo.verifyApiName} must be a ${this.tokenInfo.jwtName} string.`,
);
}

return this.ensureProjectId()
.then((projectId) => {
if (typeof audience === 'undefined') {
audience = `${projectId}.cloudfunctions.net/`;
}
return this.decodeAndVerify(jwtToken, projectId, isEmulator, audience);
})
.then((decoded) => {
const decodedAuthBlockingToken = decoded.payload as DecodedAuthBlockingToken;
decodedAuthBlockingToken.uid = decodedAuthBlockingToken.sub;
return decodedAuthBlockingToken;
});
}

private ensureProjectId(): Promise<string> {
return util.findProjectId(this.app)
.then((projectId) => {
Expand All @@ -334,10 +450,14 @@ export class FirebaseTokenVerifier {
})
}

private decodeAndVerify(token: string, projectId: string, isEmulator: boolean): Promise<DecodedToken> {
private decodeAndVerify(
token: string,
projectId: string,
isEmulator: boolean,
audience?: string): Promise<DecodedToken> {
return this.safeDecode(token)
.then((decodedToken) => {
this.verifyContent(decodedToken, projectId, isEmulator);
this.verifyContent(decodedToken, projectId, isEmulator, audience);
return this.verifySignature(token, isEmulator)
.then(() => decodedToken);
});
Expand Down Expand Up @@ -369,7 +489,8 @@ export class FirebaseTokenVerifier {
private verifyContent(
fullDecodedToken: DecodedToken,
projectId: string | null,
isEmulator: boolean): void {
isEmulator: boolean,
audience: string | undefined): void {
const header = fullDecodedToken && fullDecodedToken.header;
const payload = fullDecodedToken && fullDecodedToken.payload;

Expand All @@ -390,16 +511,19 @@ export class FirebaseTokenVerifier {
errorMessage = `${this.tokenInfo.verifyApiName} expects ${this.shortNameArticle} ` +
`${this.tokenInfo.shortName}, but was given a legacy custom token.`;
} else {
errorMessage = 'Firebase ID token has no "kid" claim.';
errorMessage = `${this.tokenInfo.jwtName} has no "kid" claim.`;
}

errorMessage += verifyJwtTokenDocsMessage;
} else if (!isEmulator && header.alg !== ALGORITHM_RS256) {
errorMessage = `${this.tokenInfo.jwtName} has incorrect algorithm. Expected "` + ALGORITHM_RS256 + '" but got ' +
'"' + header.alg + '".' + verifyJwtTokenDocsMessage;
} else if (payload.aud !== projectId) {
} else if (typeof audience !== 'undefined' && !(payload.aud as string).includes(audience)) {
errorMessage = `${this.tokenInfo.jwtName} has incorrect "aud" (audience) claim. Expected "` +
projectId + '" but got "' + payload.aud + '".' + projectIdMatchMessage +
audience + '" but got "' + payload.aud + '".' + verifyJwtTokenDocsMessage;
} else if (typeof audience === 'undefined' && payload.aud !== projectId) {
errorMessage = `${this.tokenInfo.jwtName} has incorrect "aud" (audience) claim. Expected "` +
projectId + '" but got "' + payload.aud + '".' + projectIdMatchMessage +
verifyJwtTokenDocsMessage;
} else if (payload.iss !== this.issuer + projectId) {
errorMessage = `${this.tokenInfo.jwtName} has incorrect "iss" (issuer) claim. Expected ` +
Expand Down Expand Up @@ -470,6 +594,22 @@ export function createIdTokenVerifier(app: App): FirebaseTokenVerifier {
);
}

/**
* Creates a new FirebaseTokenVerifier to verify Firebase Auth Blocking tokens.
*
* @internal
* @param app - Firebase app instance.
* @returns FirebaseTokenVerifier
*/
export function createAuthBlockingTokenVerifier(app: App): FirebaseTokenVerifier {
return new FirebaseTokenVerifier(
CLIENT_CERT_URL,
'https://securetoken.google.com/',
AUTH_BLOCKING_TOKEN_INFO,
app
);
}

/**
* Creates a new FirebaseTokenVerifier to verify Firebase session cookies.
*
Expand Down
4 changes: 4 additions & 0 deletions src/utils/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,10 @@ export class AppErrorCodes {
* Auth client error codes and their default messages.
*/
export class AuthClientErrorCode {
public static AUTH_BLOCKING_TOKEN_EXPIRED = {
code: 'auth-blocking-token-expired',
message: 'The provided Firebase Auth Blocking token is expired.',
};
public static BILLING_NOT_ENABLED = {
code: 'billing-not-enabled',
message: 'Feature requires billing to be enabled.',
Expand Down
Loading

0 comments on commit 6b97607

Please sign in to comment.