-
Notifications
You must be signed in to change notification settings - Fork 373
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
Improve token verification logic with Auth Emulator. #1148
Changes from all commits
512fa4f
c5b68d3
2718b2c
0c6eaef
ce83e14
6ba7fcf
159e0b4
136f774
1cbc812
38c537e
263f885
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -79,7 +79,7 @@ export class FirebaseTokenVerifier { | |
constructor(private clientCertUrl: string, private algorithm: jwt.Algorithm, | ||
private issuer: string, private tokenInfo: FirebaseTokenInfo, | ||
private readonly app: FirebaseApp) { | ||
|
||
if (!validator.isURL(clientCertUrl)) { | ||
throw new FirebaseAuthError( | ||
AuthClientErrorCode.INVALID_ARGUMENT, | ||
|
@@ -135,10 +135,11 @@ export class FirebaseTokenVerifier { | |
* Verifies the format and signature of a Firebase Auth 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 | ||
* token. | ||
*/ | ||
public verifyJWT(jwtToken: string): Promise<DecodedIdToken> { | ||
public verifyJWT(jwtToken: string, isEmulator = false): Promise<DecodedIdToken> { | ||
if (!validator.isString(jwtToken)) { | ||
throw new FirebaseAuthError( | ||
AuthClientErrorCode.INVALID_ARGUMENT, | ||
|
@@ -148,19 +149,15 @@ export class FirebaseTokenVerifier { | |
|
||
return util.findProjectId(this.app) | ||
.then((projectId) => { | ||
return this.verifyJWTWithProjectId(jwtToken, projectId); | ||
return this.verifyJWTWithProjectId(jwtToken, projectId, isEmulator); | ||
}); | ||
} | ||
|
||
/** | ||
* Override the JWT signing algorithm. | ||
* @param algorithm the new signing algorithm. | ||
*/ | ||
public setAlgorithm(algorithm: jwt.Algorithm): void { | ||
this.algorithm = algorithm; | ||
} | ||
|
||
private verifyJWTWithProjectId(jwtToken: string, projectId: string | null): Promise<DecodedIdToken> { | ||
private verifyJWTWithProjectId( | ||
jwtToken: string, | ||
projectId: string | null, | ||
isEmulator: boolean | ||
): Promise<DecodedIdToken> { | ||
if (!validator.isNonEmptyString(projectId)) { | ||
throw new FirebaseAuthError( | ||
AuthClientErrorCode.INVALID_CREDENTIAL, | ||
|
@@ -185,7 +182,7 @@ export class FirebaseTokenVerifier { | |
if (!fullDecodedToken) { | ||
errorMessage = `Decoding ${this.tokenInfo.jwtName} failed. Make sure you passed the entire string JWT ` + | ||
`which represents ${this.shortNameArticle} ${this.tokenInfo.shortName}.` + verifyJwtTokenDocsMessage; | ||
} else if (typeof header.kid === 'undefined' && this.algorithm !== 'none') { | ||
} else if (!isEmulator && typeof header.kid === 'undefined') { | ||
const isCustomToken = (payload.aud === FIREBASE_AUDIENCE); | ||
const isLegacyCustomToken = (header.alg === 'HS256' && payload.v === 0 && 'd' in payload && 'uid' in payload.d); | ||
|
||
|
@@ -200,7 +197,7 @@ export class FirebaseTokenVerifier { | |
} | ||
|
||
errorMessage += verifyJwtTokenDocsMessage; | ||
} else if (header.alg !== this.algorithm) { | ||
} else if (!isEmulator && header.alg !== this.algorithm) { | ||
errorMessage = `${this.tokenInfo.jwtName} has incorrect algorithm. Expected "` + this.algorithm + '" but got ' + | ||
'"' + header.alg + '".' + verifyJwtTokenDocsMessage; | ||
} else if (payload.aud !== projectId) { | ||
|
@@ -209,7 +206,7 @@ export class FirebaseTokenVerifier { | |
verifyJwtTokenDocsMessage; | ||
} else if (payload.iss !== this.issuer + projectId) { | ||
errorMessage = `${this.tokenInfo.jwtName} has incorrect "iss" (issuer) claim. Expected ` + | ||
`"${this.issuer}"` + projectId + '" but got "' + | ||
`"${this.issuer}` + projectId + '" but got "' + | ||
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. nit: took me a while staring at this diff because of the mixed use of string templates and string concatenation via |
||
payload.iss + '".' + projectIdMatchMessage + verifyJwtTokenDocsMessage; | ||
} else if (typeof payload.sub !== 'string') { | ||
errorMessage = `${this.tokenInfo.jwtName} has no "sub" (subject) claim.` + verifyJwtTokenDocsMessage; | ||
|
@@ -223,9 +220,8 @@ export class FirebaseTokenVerifier { | |
return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_ARGUMENT, errorMessage)); | ||
} | ||
|
||
// When the algorithm is set to 'none' there will be no signature and therefore we don't check | ||
// the public keys. | ||
if (this.algorithm === 'none') { | ||
if (isEmulator) { | ||
// Signature checks skipped for emulator; no need to fetch public keys. | ||
return this.verifyJwtSignatureWithKey(jwtToken, null); | ||
} | ||
|
||
|
@@ -257,26 +253,29 @@ export class FirebaseTokenVerifier { | |
const verifyJwtTokenDocsMessage = ` See ${this.tokenInfo.url} ` + | ||
`for details on how to retrieve ${this.shortNameArticle} ${this.tokenInfo.shortName}.`; | ||
return new Promise((resolve, reject) => { | ||
jwt.verify(jwtToken, publicKey || '', { | ||
algorithms: [this.algorithm], | ||
}, (error: jwt.VerifyErrors | null, decodedToken: object | undefined) => { | ||
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}).` + | ||
verifyJwtTokenDocsMessage; | ||
return reject(new FirebaseAuthError(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)); | ||
const verifyOptions: jwt.VerifyOptions = {}; | ||
if (publicKey !== null) { | ||
verifyOptions.algorithms = [this.algorithm]; | ||
} | ||
jwt.verify(jwtToken, publicKey || '', verifyOptions, | ||
(error: jwt.VerifyErrors | null, decodedToken: object | undefined) => { | ||
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}).` + | ||
verifyJwtTokenDocsMessage; | ||
return reject(new FirebaseAuthError(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 FirebaseAuthError(AuthClientErrorCode.INVALID_ARGUMENT, error.message)); | ||
} else { | ||
const decodedIdToken = (decodedToken as DecodedIdToken); | ||
decodedIdToken.uid = decodedIdToken.sub; | ||
resolve(decodedIdToken); | ||
} | ||
return reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_ARGUMENT, error.message)); | ||
} else { | ||
const decodedIdToken = (decodedToken as DecodedIdToken); | ||
decodedIdToken.uid = decodedIdToken.sub; | ||
resolve(decodedIdToken); | ||
} | ||
}); | ||
}); | ||
}); | ||
} | ||
|
||
|
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.
@yuchenshi can you give any context around why the
verifyIdToken
method always callsverifyDecodedJWTNotRevokedOrDisabled
if using emulator please?I'm in the position where I'm working with uid's that may/may not exist in Firebase.
I want to verify a token (which succeeds in prod) but the emulator checks whether there's a user and whether its been disabled regardless of the value of
checkRevoked
.If this isn't intended I can create a PR.
TIA
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.
@lprhodes Tokens from the Auth Emulator aren't really "verifiable" since they are not signed (compared to production), so the closest approximation we can do is to ask the Auth Emulator if they exist. It sounds like you have a specific workflow in mind that you want to emulate and please open a new issue with more context (especially why you'd like to verify tokens from non-existent users) and we'll see what we can do.