-
Notifications
You must be signed in to change notification settings - Fork 408
chore: Move token verification logic to util #1189
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
Changes from 3 commits
8044f7d
6719cfa
913ce37
bba0b02
27e0e6f
5d3597a
ac28a55
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,5 @@ | ||
| /*! | ||
| * Copyright 2018 Google Inc. | ||
| * Copyright 2021 Google Inc. | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
|
|
@@ -20,9 +20,6 @@ import * as jwt from 'jsonwebtoken'; | |
| import { HttpClient, HttpRequestConfig, HttpError } from './api-request'; | ||
| import { FirebaseApp } from '../firebase-app'; | ||
| import { ErrorCodeConfig, ErrorInfo, PrefixedFirebaseError } from './error'; | ||
| import { auth } from '../auth/index'; | ||
|
|
||
| import DecodedIdToken = auth.DecodedIdToken; | ||
|
|
||
| // Audience to use for Firebase Auth Custom tokens | ||
| const FIREBASE_AUDIENCE = 'https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit'; | ||
|
|
@@ -37,8 +34,6 @@ export interface FirebaseTokenInfo { | |
| jwtName: string; | ||
| /** The JWT short name. */ | ||
| shortName: string; | ||
| /** JWT Expiration error code. */ | ||
| expiredErrorCode: ErrorInfo; | ||
| /** Error code config of the public error type. */ | ||
| errorCodeConfig: ErrorCodeConfig; | ||
| /** Public error type. */ | ||
|
|
@@ -73,24 +68,30 @@ export class FirebaseTokenVerifier { | |
| throw new Error('The JWT public full name must be a non-empty string.'); | ||
| } else if (!validator.isNonEmptyString(tokenInfo.shortName)) { | ||
| throw new Error('The JWT public short name must be a non-empty string.'); | ||
| } else if (!validator.isNonNullObject(tokenInfo.expiredErrorCode) || | ||
| !('code' in tokenInfo.expiredErrorCode)) { | ||
| throw new Error('The JWT expiration error code must be a non-null ErrorInfo object.'); | ||
| } else if (!(typeof tokenInfo.errorType === 'function' && tokenInfo.errorType !== null)) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| throw new Error('The provided error type must be a non-null PrefixedFirebaseError type.'); | ||
| } else if (!validator.isNonNullObject(tokenInfo.errorCodeConfig) || | ||
| !('invalidArg' in tokenInfo.errorCodeConfig || | ||
| 'invalidCredential' in tokenInfo.errorCodeConfig || | ||
| 'internalError' in tokenInfo.errorCodeConfig || | ||
| 'expiredError' in tokenInfo.errorCodeConfig)) { | ||
| throw new Error('The provided error code config must be a non-null ErrorCodeInfo object.'); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ErrorCodeConfig? |
||
| } | ||
| this.shortNameArticle = tokenInfo.shortName.charAt(0).match(/[aeiou]/i) ? 'an' : 'a'; | ||
|
|
||
| // For backward compatibility, the project ID is validated in the verification call. | ||
| } | ||
|
|
||
| /** | ||
| * Verifies the format and signature of a Firebase Auth JWT token. | ||
| * Verifies the format and signature of a Firebase JWT token. | ||
| * | ||
| * @param {string} jwtToken The Firebase Auth JWT token to verify. | ||
| * @param {boolean=} isEmulator Whether to accept Auth Emulator tokens. | ||
| * @return {Promise<DecodedIdToken>} A promise fulfilled with the decoded claims of the Firebase Auth ID | ||
| * @template T The returned type of the decoded token. | ||
| * @param {string} jwtToken The Firebase JWT token to verify. | ||
| * @param {boolean=} isEmulator Whether to accept Emulator tokens. | ||
| * @return {Promise<T>} A promise fulfilled with the decoded claims of the Firebase Auth ID | ||
| * token. | ||
| */ | ||
| public verifyJWT(jwtToken: string, isEmulator = false): Promise<DecodedIdToken> { | ||
| public verifyJWT<T extends object>(jwtToken: string, isEmulator = false): Promise<T> { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @hiranya911 : Let me know your thoughts. Thanks! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, this looks good to me. But I don't think we need a generic at all. Just make auth extend the TokenVerifier class, and transform the FirebaseToken into DecodedIdToken there. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is breaking the type safety a bit. We should probably introduce a new interface for the return type of this class. And then convert it into an auth specific return type in |
||
| if (!validator.isString(jwtToken)) { | ||
| throw new this.tokenInfo.errorType( | ||
| this.tokenInfo.errorCodeConfig.invalidArg, | ||
|
|
@@ -100,15 +101,15 @@ export class FirebaseTokenVerifier { | |
|
|
||
| return util.findProjectId(this.app) | ||
| .then((projectId) => { | ||
| return this.verifyJWTWithProjectId(jwtToken, projectId, isEmulator); | ||
| return this.verifyJWTWithProjectId<T>(jwtToken, projectId, isEmulator); | ||
| }); | ||
| } | ||
|
|
||
| private verifyJWTWithProjectId( | ||
| private verifyJWTWithProjectId<T extends object>( | ||
| jwtToken: string, | ||
| projectId: string | null, | ||
| isEmulator: boolean | ||
| ): Promise<DecodedIdToken> { | ||
| ): Promise<T> { | ||
| if (!validator.isNonEmptyString(projectId)) { | ||
| throw new this.tokenInfo.errorType( | ||
| this.tokenInfo.errorCodeConfig.invalidCredential, | ||
|
|
@@ -173,7 +174,7 @@ export class FirebaseTokenVerifier { | |
|
|
||
| if (isEmulator) { | ||
| // Signature checks skipped for emulator; no need to fetch public keys. | ||
| return this.verifyJwtSignatureWithKey(jwtToken, null); | ||
| return this.verifyJwtSignatureWithKey<T>(jwtToken, null); | ||
| } | ||
|
|
||
| return this.fetchPublicKeys().then((publicKeys) => { | ||
|
|
@@ -187,20 +188,23 @@ export class FirebaseTokenVerifier { | |
| ), | ||
| ); | ||
| } else { | ||
| return this.verifyJwtSignatureWithKey(jwtToken, publicKeys[header.kid]); | ||
| return this.verifyJwtSignatureWithKey<T>(jwtToken, publicKeys[header.kid]); | ||
| } | ||
|
|
||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * Verifies the JWT signature using the provided public key. | ||
| * | ||
| * @template T The returned type of the decoded token. | ||
| * @param {string} jwtToken The JWT token to verify. | ||
| * @param {string} publicKey The public key certificate. | ||
| * @return {Promise<DecodedIdToken>} A promise that resolves with the decoded JWT claims on successful | ||
| * @return {Promise<T>} A promise that resolves with the decoded JWT claims on successful | ||
| * verification. | ||
| */ | ||
| private verifyJwtSignatureWithKey(jwtToken: string, publicKey: string | null): Promise<DecodedIdToken> { | ||
| private verifyJwtSignatureWithKey<T extends object>(jwtToken: string, | ||
| publicKey: string | null): Promise<T> { | ||
| const verifyJwtTokenDocsMessage = ` See ${this.tokenInfo.url} ` + | ||
| `for details on how to retrieve ${this.shortNameArticle} ${this.tokenInfo.shortName}.`; | ||
| return new Promise((resolve, reject) => { | ||
|
|
@@ -213,18 +217,16 @@ export class FirebaseTokenVerifier { | |
| if (error) { | ||
| if (error.name === 'TokenExpiredError') { | ||
| const errorMessage = `${this.tokenInfo.jwtName} has expired. Get a fresh ${this.tokenInfo.shortName}` + | ||
| ` from your client app and try again (auth/${this.tokenInfo.expiredErrorCode.code}).` + | ||
| ` from your client app and try again (${this.tokenInfo.errorCodeConfig.expiredError.code}).` + | ||
| verifyJwtTokenDocsMessage; | ||
| return reject(new this.tokenInfo.errorType(this.tokenInfo.expiredErrorCode, errorMessage)); | ||
| return reject(new this.tokenInfo.errorType(this.tokenInfo.errorCodeConfig.expiredError, errorMessage)); | ||
| } else if (error.name === 'JsonWebTokenError') { | ||
| const errorMessage = `${this.tokenInfo.jwtName} has invalid signature.` + verifyJwtTokenDocsMessage; | ||
| return reject(new this.tokenInfo.errorType(this.tokenInfo.errorCodeConfig.invalidArg, errorMessage)); | ||
| } | ||
| return reject(new this.tokenInfo.errorType(this.tokenInfo.errorCodeConfig.invalidArg, error.message)); | ||
| } else { | ||
| const decodedIdToken = (decodedToken as DecodedIdToken); | ||
| decodedIdToken.uid = decodedIdToken.sub; | ||
| resolve(decodedIdToken); | ||
| resolve(decodedToken as T); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not particularly typesafe. |
||
| } | ||
| }); | ||
| }); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moved the code to set the
uidalias here. Another way is to add helper functions,verifySessionCookie()andverifyIdToken(), toauth/token-verifier. Let me know your thoughts.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about extending the
util.TokenVerifierasAuthTokenVerifier, and add the logic there?