-
Notifications
You must be signed in to change notification settings - Fork 391
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
Closed
Closed
Changes from 4 commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
8044f7d
chore: Move Token verification logic to util
lahirumaramba 6719cfa
Moved Auth related items to token-verifier-util
lahirumaramba 913ce37
Change the value errors in FirebaseTokenVerifier constructor to Error…
lahirumaramba bba0b02
Move the error type test into verifyJWT() tests
lahirumaramba 27e0e6f
Merge expired error code to erro code config
lahirumaramba 5d3597a
Fixed copyright years
lahirumaramba ac28a55
Remove all Auth dependencies from the token verifier
lahirumaramba File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
/*! | ||
* 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. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
import { FirebaseApp } from '../firebase-app'; | ||
import { AuthClientErrorCode, ErrorCodeConfig, FirebaseAuthError } from '../utils/error'; | ||
import { FirebaseTokenInfo, FirebaseTokenVerifier } from '../utils/token-verifier'; | ||
|
||
const ALGORITHM_RS256 = 'RS256'; | ||
|
||
// URL containing the public keys for the Google certs (whose private keys are used to sign Firebase | ||
// Auth ID tokens) | ||
const CLIENT_CERT_URL = 'https://www.googleapis.com/robot/v1/metadata/x509/[email protected]'; | ||
|
||
// URL containing the public keys for Firebase session cookies. This will be updated to a different URL soon. | ||
const SESSION_COOKIE_CERT_URL = 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/publicKeys'; | ||
|
||
/** Error codes that matches the FirebaseAuthError type */ | ||
const AUTH_ERROR_CODE_CONFIG: ErrorCodeConfig = { | ||
invalidArg: AuthClientErrorCode.INVALID_ARGUMENT, | ||
invalidCredential: AuthClientErrorCode.INVALID_CREDENTIAL, | ||
internalError: AuthClientErrorCode.INTERNAL_ERROR, | ||
} | ||
|
||
/** User facing token information related to the Firebase ID token. */ | ||
export const ID_TOKEN_INFO: FirebaseTokenInfo = { | ||
url: 'https://firebase.google.com/docs/auth/admin/verify-id-tokens', | ||
verifyApiName: 'verifyIdToken()', | ||
jwtName: 'Firebase ID token', | ||
shortName: 'ID token', | ||
expiredErrorCode: AuthClientErrorCode.ID_TOKEN_EXPIRED, | ||
errorCodeConfig: AUTH_ERROR_CODE_CONFIG, | ||
hiranya911 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
errorType: FirebaseAuthError, | ||
}; | ||
|
||
/** User facing token information related to the Firebase session cookie. */ | ||
export const SESSION_COOKIE_INFO: FirebaseTokenInfo = { | ||
url: 'https://firebase.google.com/docs/auth/admin/manage-cookies', | ||
verifyApiName: 'verifySessionCookie()', | ||
jwtName: 'Firebase session cookie', | ||
shortName: 'session cookie', | ||
expiredErrorCode: AuthClientErrorCode.SESSION_COOKIE_EXPIRED, | ||
errorCodeConfig: AUTH_ERROR_CODE_CONFIG, | ||
errorType: FirebaseAuthError, | ||
}; | ||
|
||
/** | ||
* Creates a new FirebaseTokenVerifier to verify Firebase ID tokens. | ||
* | ||
* @param {FirebaseApp} app Firebase app instance. | ||
* @return {FirebaseTokenVerifier} | ||
*/ | ||
export function createIdTokenVerifier(app: FirebaseApp): FirebaseTokenVerifier { | ||
return new FirebaseTokenVerifier( | ||
CLIENT_CERT_URL, | ||
ALGORITHM_RS256, | ||
'https://securetoken.google.com/', | ||
ID_TOKEN_INFO, | ||
app | ||
); | ||
} | ||
|
||
/** | ||
* Creates a new FirebaseTokenVerifier to verify Firebase session cookies. | ||
* | ||
* @param {FirebaseApp} app Firebase app instance. | ||
* @return {FirebaseTokenVerifier} | ||
*/ | ||
export function createSessionCookieVerifier(app: FirebaseApp): FirebaseTokenVerifier { | ||
return new FirebaseTokenVerifier( | ||
SESSION_COOKIE_CERT_URL, | ||
ALGORITHM_RS256, | ||
'https://session.firebase.google.com/', | ||
SESSION_COOKIE_INFO, | ||
app | ||
); | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,46 +14,19 @@ | |
* limitations under the License. | ||
*/ | ||
|
||
import { AuthClientErrorCode, FirebaseAuthError, ErrorInfo } from '../utils/error'; | ||
import * as util from '../utils/index'; | ||
import * as validator from '../utils/validator'; | ||
import * as util from './index'; | ||
import * as validator from './validator'; | ||
import * as jwt from 'jsonwebtoken'; | ||
import { HttpClient, HttpRequestConfig, HttpError } from '../utils/api-request'; | ||
import { HttpClient, HttpRequestConfig, HttpError } from './api-request'; | ||
import { FirebaseApp } from '../firebase-app'; | ||
import { auth } from './index'; | ||
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'; | ||
|
||
export const ALGORITHM_RS256 = 'RS256'; | ||
|
||
// URL containing the public keys for the Google certs (whose private keys are used to sign Firebase | ||
// Auth ID tokens) | ||
const CLIENT_CERT_URL = 'https://www.googleapis.com/robot/v1/metadata/x509/[email protected]'; | ||
|
||
// URL containing the public keys for Firebase session cookies. This will be updated to a different URL soon. | ||
const SESSION_COOKIE_CERT_URL = 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/publicKeys'; | ||
|
||
/** User facing token information related to the Firebase ID token. */ | ||
export const ID_TOKEN_INFO: FirebaseTokenInfo = { | ||
url: 'https://firebase.google.com/docs/auth/admin/verify-id-tokens', | ||
verifyApiName: 'verifyIdToken()', | ||
jwtName: 'Firebase ID token', | ||
shortName: 'ID token', | ||
expiredErrorCode: AuthClientErrorCode.ID_TOKEN_EXPIRED, | ||
}; | ||
|
||
/** User facing token information related to the Firebase session cookie. */ | ||
export const SESSION_COOKIE_INFO: FirebaseTokenInfo = { | ||
url: 'https://firebase.google.com/docs/auth/admin/manage-cookies', | ||
verifyApiName: 'verifySessionCookie()', | ||
jwtName: 'Firebase session cookie', | ||
shortName: 'session cookie', | ||
expiredErrorCode: AuthClientErrorCode.SESSION_COOKIE_EXPIRED, | ||
}; | ||
|
||
/** Interface that defines token related user facing information. */ | ||
export interface FirebaseTokenInfo { | ||
/** Documentation URL. */ | ||
|
@@ -66,6 +39,10 @@ export interface FirebaseTokenInfo { | |
shortName: string; | ||
/** JWT Expiration error code. */ | ||
expiredErrorCode: ErrorInfo; | ||
/** Error code config of the public error type. */ | ||
errorCodeConfig: ErrorCodeConfig; | ||
/** Public error type. */ | ||
errorType: new (info: ErrorInfo, message?: string) => PrefixedFirebaseError; | ||
} | ||
|
||
/** | ||
|
@@ -81,50 +58,24 @@ export class FirebaseTokenVerifier { | |
private readonly app: FirebaseApp) { | ||
|
||
if (!validator.isURL(clientCertUrl)) { | ||
throw new FirebaseAuthError( | ||
AuthClientErrorCode.INVALID_ARGUMENT, | ||
'The provided public client certificate URL is an invalid URL.', | ||
); | ||
throw new Error('The provided public client certificate URL is an invalid URL.'); | ||
} else if (!validator.isNonEmptyString(algorithm)) { | ||
throw new FirebaseAuthError( | ||
AuthClientErrorCode.INVALID_ARGUMENT, | ||
'The provided JWT algorithm is an empty string.', | ||
); | ||
throw new Error('The provided JWT algorithm is an empty string.'); | ||
} else if (!validator.isURL(issuer)) { | ||
throw new FirebaseAuthError( | ||
AuthClientErrorCode.INVALID_ARGUMENT, | ||
'The provided JWT issuer is an invalid URL.', | ||
); | ||
throw new Error('The provided JWT issuer is an invalid URL.'); | ||
} else if (!validator.isNonNullObject(tokenInfo)) { | ||
throw new FirebaseAuthError( | ||
AuthClientErrorCode.INVALID_ARGUMENT, | ||
'The provided JWT information is not an object or null.', | ||
); | ||
throw new Error('The provided JWT information is not an object or null.'); | ||
} else if (!validator.isURL(tokenInfo.url)) { | ||
throw new FirebaseAuthError( | ||
AuthClientErrorCode.INVALID_ARGUMENT, | ||
'The provided JWT verification documentation URL is invalid.', | ||
); | ||
throw new Error('The provided JWT verification documentation URL is invalid.'); | ||
} else if (!validator.isNonEmptyString(tokenInfo.verifyApiName)) { | ||
throw new FirebaseAuthError( | ||
AuthClientErrorCode.INVALID_ARGUMENT, | ||
'The JWT verify API name must be a non-empty string.', | ||
); | ||
throw new Error('The JWT verify API name must be a non-empty string.'); | ||
} else if (!validator.isNonEmptyString(tokenInfo.jwtName)) { | ||
throw new FirebaseAuthError( | ||
AuthClientErrorCode.INVALID_ARGUMENT, | ||
'The JWT public full name must be a non-empty string.', | ||
); | ||
throw new Error('The JWT public full name must be a non-empty string.'); | ||
} else if (!validator.isNonEmptyString(tokenInfo.shortName)) { | ||
throw new FirebaseAuthError( | ||
AuthClientErrorCode.INVALID_ARGUMENT, | ||
'The JWT public short name must be a non-empty string.', | ||
); | ||
} else if (!validator.isNonNullObject(tokenInfo.expiredErrorCode) || !('code' in tokenInfo.expiredErrorCode)) { | ||
throw new FirebaseAuthError( | ||
AuthClientErrorCode.INVALID_ARGUMENT, | ||
'The JWT expiration error code must be a non-null ErrorInfo object.', | ||
); | ||
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.'); | ||
} | ||
this.shortNameArticle = tokenInfo.shortName.charAt(0).match(/[aeiou]/i) ? 'an' : 'a'; | ||
|
||
|
@@ -141,8 +92,8 @@ export class FirebaseTokenVerifier { | |
*/ | ||
public verifyJWT(jwtToken: string, isEmulator = false): Promise<DecodedIdToken> { | ||
if (!validator.isString(jwtToken)) { | ||
throw new FirebaseAuthError( | ||
AuthClientErrorCode.INVALID_ARGUMENT, | ||
throw new this.tokenInfo.errorType( | ||
this.tokenInfo.errorCodeConfig.invalidArg, | ||
`First argument to ${this.tokenInfo.verifyApiName} must be a ${this.tokenInfo.jwtName} string.`, | ||
); | ||
} | ||
|
@@ -159,8 +110,8 @@ export class FirebaseTokenVerifier { | |
isEmulator: boolean | ||
): Promise<DecodedIdToken> { | ||
if (!validator.isNonEmptyString(projectId)) { | ||
throw new FirebaseAuthError( | ||
AuthClientErrorCode.INVALID_CREDENTIAL, | ||
throw new this.tokenInfo.errorType( | ||
this.tokenInfo.errorCodeConfig.invalidCredential, | ||
'Must initialize app with a cert credential or set your Firebase project ID as the ' + | ||
`GOOGLE_CLOUD_PROJECT environment variable to call ${this.tokenInfo.verifyApiName}.`, | ||
); | ||
|
@@ -217,7 +168,7 @@ export class FirebaseTokenVerifier { | |
verifyJwtTokenDocsMessage; | ||
} | ||
if (errorMessage) { | ||
return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_ARGUMENT, errorMessage)); | ||
return Promise.reject(new this.tokenInfo.errorType(this.tokenInfo.errorCodeConfig.invalidArg, errorMessage)); | ||
} | ||
|
||
if (isEmulator) { | ||
|
@@ -228,8 +179,8 @@ export class FirebaseTokenVerifier { | |
return this.fetchPublicKeys().then((publicKeys) => { | ||
if (!Object.prototype.hasOwnProperty.call(publicKeys, header.kid)) { | ||
return Promise.reject( | ||
new FirebaseAuthError( | ||
AuthClientErrorCode.INVALID_ARGUMENT, | ||
new this.tokenInfo.errorType( | ||
this.tokenInfo.errorCodeConfig.invalidArg, | ||
`${this.tokenInfo.jwtName} has "kid" claim which does not correspond to a known public key. ` + | ||
`Most likely the ${this.tokenInfo.shortName} is expired, so get a fresh token from your ` + | ||
'client app and try again.', | ||
|
@@ -264,12 +215,12 @@ export class FirebaseTokenVerifier { | |
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}).` + | ||
verifyJwtTokenDocsMessage; | ||
return reject(new FirebaseAuthError(this.tokenInfo.expiredErrorCode, errorMessage)); | ||
return reject(new this.tokenInfo.errorType(this.tokenInfo.expiredErrorCode, errorMessage)); | ||
} else if (error.name === 'JsonWebTokenError') { | ||
const errorMessage = `${this.tokenInfo.jwtName} has invalid signature.` + verifyJwtTokenDocsMessage; | ||
return reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_ARGUMENT, errorMessage)); | ||
return reject(new this.tokenInfo.errorType(this.tokenInfo.errorCodeConfig.invalidArg, errorMessage)); | ||
} | ||
return reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_ARGUMENT, error.message)); | ||
return reject(new this.tokenInfo.errorType(this.tokenInfo.errorCodeConfig.invalidArg, error.message)); | ||
} else { | ||
const decodedIdToken = (decodedToken as DecodedIdToken); | ||
decodedIdToken.uid = decodedIdToken.sub; | ||
|
@@ -329,41 +280,9 @@ export class FirebaseTokenVerifier { | |
} else { | ||
errorMessage += `${resp.text}`; | ||
} | ||
throw new FirebaseAuthError(AuthClientErrorCode.INTERNAL_ERROR, errorMessage); | ||
throw new this.tokenInfo.errorType(this.tokenInfo.errorCodeConfig.internalError, errorMessage); | ||
} | ||
throw err; | ||
}); | ||
} | ||
} | ||
|
||
/** | ||
* Creates a new FirebaseTokenVerifier to verify Firebase ID tokens. | ||
* | ||
* @param {FirebaseApp} app Firebase app instance. | ||
* @return {FirebaseTokenVerifier} | ||
*/ | ||
export function createIdTokenVerifier(app: FirebaseApp): FirebaseTokenVerifier { | ||
return new FirebaseTokenVerifier( | ||
CLIENT_CERT_URL, | ||
ALGORITHM_RS256, | ||
'https://securetoken.google.com/', | ||
ID_TOKEN_INFO, | ||
app | ||
); | ||
} | ||
|
||
/** | ||
* Creates a new FirebaseTokenVerifier to verify Firebase session cookies. | ||
* | ||
* @param {FirebaseApp} app Firebase app instance. | ||
* @return {FirebaseTokenVerifier} | ||
*/ | ||
export function createSessionCookieVerifier(app: FirebaseApp): FirebaseTokenVerifier { | ||
return new FirebaseTokenVerifier( | ||
SESSION_COOKIE_CERT_URL, | ||
ALGORITHM_RS256, | ||
'https://session.firebase.google.com/', | ||
SESSION_COOKIE_INFO, | ||
app | ||
); | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.