Skip to content

Commit 6b97607

Browse files
authored
Adding in alpha interface for blocking token verification (#1635)
* adding in alpha interface * adding token verifier & unit tests * remove old ref * adding exports * address pr comments
1 parent b01d1a9 commit 6b97607

10 files changed

+660
-10
lines changed

etc/firebase-admin.api.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,10 @@ export namespace auth {
106106
export type CreateRequest = CreateRequest;
107107
// Warning: (ae-forgotten-export) The symbol "CreateTenantRequest" needs to be exported by the entry point default-namespace.d.ts
108108
export type CreateTenantRequest = CreateTenantRequest;
109+
// Warning: (ae-forgotten-export) The symbol "DecodedAuthBlockingToken" needs to be exported by the entry point default-namespace.d.ts
110+
//
111+
// @alpha (undocumented)
112+
export type DecodedAuthBlockingToken = DecodedAuthBlockingToken;
109113
// Warning: (ae-forgotten-export) The symbol "DecodedIdToken" needs to be exported by the entry point default-namespace.d.ts
110114
export type DecodedIdToken = DecodedIdToken;
111115
// Warning: (ae-forgotten-export) The symbol "DeleteUsersResult" needs to be exported by the entry point default-namespace.d.ts

etc/firebase-admin.auth.api.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ export abstract class BaseAuth {
6868
setCustomUserClaims(uid: string, customUserClaims: object | null): Promise<void>;
6969
updateProviderConfig(providerId: string, updatedConfig: UpdateAuthProviderRequest): Promise<AuthProviderConfig>;
7070
updateUser(uid: string, properties: UpdateRequest): Promise<UserRecord>;
71+
// @alpha (undocumented)
72+
_verifyAuthBlockingToken(token: string, audience?: string): Promise<DecodedAuthBlockingToken>;
7173
verifyIdToken(idToken: string, checkRevoked?: boolean): Promise<DecodedIdToken>;
7274
verifySessionCookie(sessionCookie: string, checkRevoked?: boolean): Promise<DecodedIdToken>;
7375
}
@@ -110,6 +112,56 @@ export interface CreateRequest extends UpdateRequest {
110112
// @public
111113
export type CreateTenantRequest = UpdateTenantRequest;
112114

115+
// @alpha (undocumented)
116+
export interface DecodedAuthBlockingToken {
117+
// (undocumented)
118+
[key: string]: any;
119+
// (undocumented)
120+
aud: string;
121+
// (undocumented)
122+
event_id: string;
123+
// (undocumented)
124+
event_type: string;
125+
// (undocumented)
126+
exp: number;
127+
// (undocumented)
128+
iat: number;
129+
// (undocumented)
130+
ip_address: string;
131+
// (undocumented)
132+
iss: string;
133+
// (undocumented)
134+
locale?: string;
135+
// (undocumented)
136+
oauth_access_token?: string;
137+
// (undocumented)
138+
oauth_expires_in?: number;
139+
// (undocumented)
140+
oauth_id_token?: string;
141+
// (undocumented)
142+
oauth_refresh_token?: string;
143+
// (undocumented)
144+
oauth_token_secret?: string;
145+
// (undocumented)
146+
raw_user_info?: string;
147+
// (undocumented)
148+
sign_in_attributes?: {
149+
[key: string]: any;
150+
};
151+
// (undocumented)
152+
sign_in_method?: string;
153+
// (undocumented)
154+
sub: string;
155+
// (undocumented)
156+
tenant_id?: string;
157+
// (undocumented)
158+
user_agent?: string;
159+
// Warning: (ae-forgotten-export) The symbol "DecodedAuthBlockingUserRecord" needs to be exported by the entry point index.d.ts
160+
//
161+
// (undocumented)
162+
user_record?: DecodedAuthBlockingUserRecord;
163+
}
164+
113165
// @public
114166
export interface DecodedIdToken {
115167
[key: string]: any;

src/auth/auth-namespace.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,10 @@ import {
7373
TenantManager as TTenantManager,
7474
} from './tenant-manager';
7575

76-
import { DecodedIdToken as TDecodedIdToken } from './token-verifier';
76+
import {
77+
DecodedIdToken as TDecodedIdToken,
78+
DecodedAuthBlockingToken as TDecodedAuthBlockingToken,
79+
} from './token-verifier';
7780

7881
import {
7982
HashAlgorithmType as THashAlgorithmType,
@@ -173,6 +176,9 @@ export namespace auth {
173176
*/
174177
export type DecodedIdToken = TDecodedIdToken;
175178

179+
/** @alpha */
180+
export type DecodedAuthBlockingToken = TDecodedAuthBlockingToken;
181+
176182
/**
177183
* Type alias to {@link firebase-admin.auth#DeleteUsersResult}.
178184
*/

src/auth/base-auth.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,12 @@ import * as validator from '../utils/validator';
2222
import { AbstractAuthRequestHandler, useEmulator } from './auth-api-request';
2323
import { FirebaseTokenGenerator, EmulatedSigner, handleCryptoSignerError } from './token-generator';
2424
import {
25-
FirebaseTokenVerifier, createSessionCookieVerifier, createIdTokenVerifier,
25+
FirebaseTokenVerifier,
26+
createSessionCookieVerifier,
27+
createIdTokenVerifier,
28+
createAuthBlockingTokenVerifier,
2629
DecodedIdToken,
30+
DecodedAuthBlockingToken,
2731
} from './token-verifier';
2832
import {
2933
AuthProviderConfig, SAMLAuthProviderConfig, AuthProviderConfigFilter, ListProviderConfigResults,
@@ -131,6 +135,8 @@ export abstract class BaseAuth {
131135
/** @internal */
132136
protected readonly idTokenVerifier: FirebaseTokenVerifier;
133137
/** @internal */
138+
protected readonly authBlockingTokenVerifier: FirebaseTokenVerifier;
139+
/** @internal */
134140
protected readonly sessionCookieVerifier: FirebaseTokenVerifier;
135141

136142
/**
@@ -156,6 +162,7 @@ export abstract class BaseAuth {
156162

157163
this.sessionCookieVerifier = createSessionCookieVerifier(app);
158164
this.idTokenVerifier = createIdTokenVerifier(app);
165+
this.authBlockingTokenVerifier = createAuthBlockingTokenVerifier(app);
159166
}
160167

161168
/**
@@ -1055,6 +1062,19 @@ export abstract class BaseAuth {
10551062
return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID));
10561063
}
10571064

1065+
/** @alpha */
1066+
// eslint-disable-next-line @typescript-eslint/naming-convention
1067+
public _verifyAuthBlockingToken(
1068+
token: string,
1069+
audience?: string
1070+
): Promise<DecodedAuthBlockingToken> {
1071+
const isEmulator = useEmulator();
1072+
return this.authBlockingTokenVerifier._verifyAuthBlockingToken(token, isEmulator, audience)
1073+
.then((decodedAuthBlockingToken: DecodedAuthBlockingToken) => {
1074+
return decodedAuthBlockingToken;
1075+
});
1076+
}
1077+
10581078
/**
10591079
* Verifies the decoded Firebase issued JWT is not revoked or disabled. Returns a promise that
10601080
* resolves with the decoded claims on success. Rejects the promise with revocation error if revoked

src/auth/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,10 @@ export {
116116
TenantManager,
117117
} from './tenant-manager';
118118

119-
export { DecodedIdToken } from './token-verifier';
119+
export {
120+
DecodedIdToken,
121+
DecodedAuthBlockingToken
122+
} from './token-verifier';
120123

121124
export {
122125
HashAlgorithmType,

src/auth/token-verifier.ts

Lines changed: 146 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,82 @@ export interface DecodedIdToken {
176176
[key: string]: any;
177177
}
178178

179+
/** @alpha */
180+
export interface DecodedAuthBlockingSharedUserInfo {
181+
uid: string;
182+
display_name?: string;
183+
email?: string;
184+
photo_url?: string;
185+
phone_number?: string;
186+
}
187+
188+
/** @alpha */
189+
export interface DecodedAuthBlockingMetadata {
190+
creation_time?: number;
191+
last_sign_in_time?: number;
192+
}
193+
194+
/** @alpha */
195+
export interface DecodedAuthBlockingUserInfo extends DecodedAuthBlockingSharedUserInfo {
196+
provider_id: string;
197+
}
198+
199+
/** @alpha */
200+
export interface DecodedAuthBlockingMfaInfo {
201+
uid: string;
202+
display_name?: string;
203+
phone_number?: string;
204+
enrollment_time?: string;
205+
factor_id?: string;
206+
}
207+
208+
/** @alpha */
209+
export interface DecodedAuthBlockingEnrolledFactors {
210+
enrolled_factors?: DecodedAuthBlockingMfaInfo[];
211+
}
212+
213+
/** @alpha */
214+
export interface DecodedAuthBlockingUserRecord extends DecodedAuthBlockingSharedUserInfo {
215+
email_verified?: boolean;
216+
disabled?: boolean;
217+
metadata?: DecodedAuthBlockingMetadata;
218+
password_hash?: string;
219+
password_salt?: string;
220+
provider_data?: DecodedAuthBlockingUserInfo[];
221+
multi_factor?: DecodedAuthBlockingEnrolledFactors;
222+
custom_claims?: any;
223+
tokens_valid_after_time?: number;
224+
tenant_id?: string;
225+
[key: string]: any;
226+
}
227+
228+
/** @alpha */
229+
export interface DecodedAuthBlockingToken {
230+
aud: string;
231+
exp: number;
232+
iat: number;
233+
iss: string;
234+
sub: string;
235+
event_id: string;
236+
event_type: string;
237+
ip_address: string;
238+
user_agent?: string;
239+
locale?: string;
240+
sign_in_method?: string;
241+
user_record?: DecodedAuthBlockingUserRecord;
242+
tenant_id?: string;
243+
raw_user_info?: string;
244+
sign_in_attributes?: {
245+
[key: string]: any;
246+
};
247+
oauth_id_token?: string;
248+
oauth_access_token?: string;
249+
oauth_refresh_token?: string;
250+
oauth_token_secret?: string;
251+
oauth_expires_in?: number;
252+
[key: string]: any;
253+
}
254+
179255
// Audience to use for Firebase Auth Custom tokens
180256
const FIREBASE_AUDIENCE = 'https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit';
181257

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

280+
/**
281+
* User facing token information related to the Firebase Auth Blocking token.
282+
*
283+
* @internal
284+
*/
285+
export const AUTH_BLOCKING_TOKEN_INFO: FirebaseTokenInfo = {
286+
url: 'https://cloud.google.com/identity-platform/docs/blocking-functions',
287+
verifyApiName: '_verifyAuthBlockingToken()',
288+
jwtName: 'Firebase Auth Blocking token',
289+
shortName: 'Auth Blocking token',
290+
expiredErrorCode: AuthClientErrorCode.AUTH_BLOCKING_TOKEN_EXPIRED,
291+
};
292+
204293
/**
205294
* User facing token information related to the Firebase session cookie.
206295
*
@@ -320,6 +409,33 @@ export class FirebaseTokenVerifier {
320409
});
321410
}
322411

412+
/** @alpha */
413+
// eslint-disable-next-line @typescript-eslint/naming-convention
414+
public _verifyAuthBlockingToken(
415+
jwtToken: string,
416+
isEmulator: boolean,
417+
audience: string | undefined): Promise<DecodedAuthBlockingToken> {
418+
if (!validator.isString(jwtToken)) {
419+
throw new FirebaseAuthError(
420+
AuthClientErrorCode.INVALID_ARGUMENT,
421+
`First argument to ${this.tokenInfo.verifyApiName} must be a ${this.tokenInfo.jwtName} string.`,
422+
);
423+
}
424+
425+
return this.ensureProjectId()
426+
.then((projectId) => {
427+
if (typeof audience === 'undefined') {
428+
audience = `${projectId}.cloudfunctions.net/`;
429+
}
430+
return this.decodeAndVerify(jwtToken, projectId, isEmulator, audience);
431+
})
432+
.then((decoded) => {
433+
const decodedAuthBlockingToken = decoded.payload as DecodedAuthBlockingToken;
434+
decodedAuthBlockingToken.uid = decodedAuthBlockingToken.sub;
435+
return decodedAuthBlockingToken;
436+
});
437+
}
438+
323439
private ensureProjectId(): Promise<string> {
324440
return util.findProjectId(this.app)
325441
.then((projectId) => {
@@ -334,10 +450,14 @@ export class FirebaseTokenVerifier {
334450
})
335451
}
336452

337-
private decodeAndVerify(token: string, projectId: string, isEmulator: boolean): Promise<DecodedToken> {
453+
private decodeAndVerify(
454+
token: string,
455+
projectId: string,
456+
isEmulator: boolean,
457+
audience?: string): Promise<DecodedToken> {
338458
return this.safeDecode(token)
339459
.then((decodedToken) => {
340-
this.verifyContent(decodedToken, projectId, isEmulator);
460+
this.verifyContent(decodedToken, projectId, isEmulator, audience);
341461
return this.verifySignature(token, isEmulator)
342462
.then(() => decodedToken);
343463
});
@@ -369,7 +489,8 @@ export class FirebaseTokenVerifier {
369489
private verifyContent(
370490
fullDecodedToken: DecodedToken,
371491
projectId: string | null,
372-
isEmulator: boolean): void {
492+
isEmulator: boolean,
493+
audience: string | undefined): void {
373494
const header = fullDecodedToken && fullDecodedToken.header;
374495
const payload = fullDecodedToken && fullDecodedToken.payload;
375496

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

396517
errorMessage += verifyJwtTokenDocsMessage;
397518
} else if (!isEmulator && header.alg !== ALGORITHM_RS256) {
398519
errorMessage = `${this.tokenInfo.jwtName} has incorrect algorithm. Expected "` + ALGORITHM_RS256 + '" but got ' +
399520
'"' + header.alg + '".' + verifyJwtTokenDocsMessage;
400-
} else if (payload.aud !== projectId) {
521+
} else if (typeof audience !== 'undefined' && !(payload.aud as string).includes(audience)) {
401522
errorMessage = `${this.tokenInfo.jwtName} has incorrect "aud" (audience) claim. Expected "` +
402-
projectId + '" but got "' + payload.aud + '".' + projectIdMatchMessage +
523+
audience + '" but got "' + payload.aud + '".' + verifyJwtTokenDocsMessage;
524+
} else if (typeof audience === 'undefined' && payload.aud !== projectId) {
525+
errorMessage = `${this.tokenInfo.jwtName} has incorrect "aud" (audience) claim. Expected "` +
526+
projectId + '" but got "' + payload.aud + '".' + projectIdMatchMessage +
403527
verifyJwtTokenDocsMessage;
404528
} else if (payload.iss !== this.issuer + projectId) {
405529
errorMessage = `${this.tokenInfo.jwtName} has incorrect "iss" (issuer) claim. Expected ` +
@@ -470,6 +594,22 @@ export function createIdTokenVerifier(app: App): FirebaseTokenVerifier {
470594
);
471595
}
472596

597+
/**
598+
* Creates a new FirebaseTokenVerifier to verify Firebase Auth Blocking tokens.
599+
*
600+
* @internal
601+
* @param app - Firebase app instance.
602+
* @returns FirebaseTokenVerifier
603+
*/
604+
export function createAuthBlockingTokenVerifier(app: App): FirebaseTokenVerifier {
605+
return new FirebaseTokenVerifier(
606+
CLIENT_CERT_URL,
607+
'https://securetoken.google.com/',
608+
AUTH_BLOCKING_TOKEN_INFO,
609+
app
610+
);
611+
}
612+
473613
/**
474614
* Creates a new FirebaseTokenVerifier to verify Firebase session cookies.
475615
*

src/utils/error.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,10 @@ export class AppErrorCodes {
359359
* Auth client error codes and their default messages.
360360
*/
361361
export class AuthClientErrorCode {
362+
public static AUTH_BLOCKING_TOKEN_EXPIRED = {
363+
code: 'auth-blocking-token-expired',
364+
message: 'The provided Firebase Auth Blocking token is expired.',
365+
};
362366
public static BILLING_NOT_ENABLED = {
363367
code: 'billing-not-enabled',
364368
message: 'Feature requires billing to be enabled.',

0 commit comments

Comments
 (0)