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

Adding in alpha interface for blocking token verification #1635

Merged
merged 8 commits into from
Apr 19, 2022
Merged
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 */
lahirumaramba marked this conversation as resolved.
Show resolved Hide resolved
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.`,
lahirumaramba marked this conversation as resolved.
Show resolved Hide resolved
);
}

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