-
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
Adds support for Firebase Auth session management. #245
Conversation
This adds 2 new APIs: admin.auth().createSessionCookie(idToken: string, sessionCookieOptions: SessionCookieOptions): Promise<string> admin.auth().verifySessionCookie(sessionCookie: string, checkRevoked?: boolean): Promise<DecodedIdToken> Refactored token generator and split token verification to a new class FirebaseTokenVerifier so it can be used to also verify session cookies. Kept the same error handling for backward compatibility. In the process, ported the same tests to token verifier. Updated token generator ID token and session cookie verification to check token verifier is called underneath with the expected parameters. Added integration tests to test all common flows for session cookie creation and verification. Added mocks for session cookie JWTs. Fixed error in URL validator.
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.
Looks pretty good. Just a few things to clean up.
src/auth/auth-api-request.ts
Outdated
// Set response validator. | ||
.setResponseValidator((response: any) => { | ||
// Response should always contain the session cookie. | ||
if (!response.sessionCookie || !validator.isNonEmptyString(response.sessionCookie)) { |
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.
isNonEmptyString
check should be sufficient here.
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.
Done
src/auth/auth-api-request.ts
Outdated
* The session cookie JWT will have the same payload claims as the provided ID token. | ||
* | ||
* @param {string} idToken The Firebase ID token to exchange for a session cookie. | ||
* @param {number} expiresIn The session cookie duration. |
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.
Please specify time unit.
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.
Done
src/auth/auth.ts
Outdated
* verification. | ||
*/ | ||
private verifyDecodedJWTNotRevoked( | ||
decodedIdToken: DecodedIdToken, revocationError: FirebaseAuthError): Promise<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.
Nit: Perhaps just take the error code as the second argument? You can instantiate the FirebaseAuthError in this function. It is a little weird to pass an exception as an argument.
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.
Done
src/auth/auth.ts
Outdated
*/ | ||
public createSessionCookie( | ||
idToken: string, sessionCookieOptions: SessionCookieOptions): Promise<string> { | ||
return this.authRequestHandler.createSessionCookie( |
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.
Should we validate the existence of expiresIn here and throw an error? (since it's a required field)
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.
Ok I added a check. Note this will return a rejected promise. We do this for all other APIs here. We don't directly throw an error and return a rejected promise after validation in auth-api-request.ts.
src/auth/auth.ts
Outdated
public createSessionCookie( | ||
idToken: string, sessionCookieOptions: SessionCookieOptions): Promise<string> { | ||
return this.authRequestHandler.createSessionCookie( | ||
idToken, sessionCookieOptions && sessionCookieOptions.expiresIn); |
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.
Why not just sessionCookieOptions.expiresIn
?
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.
There is a risk that if sessionCookieOptions
is passed as undefined, an error is thrown. So we have to keep this.
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.
Ok I changed it since I validate this above.
@@ -318,6 +318,10 @@ export class AuthClientErrorCode { | |||
code: 'claims-too-large', | |||
message: 'Developer claims maximum payload size exceeded.', | |||
}; | |||
public static ID_TOKEN_EXPIRED = { |
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.
Are we referencing these constants anywhere?
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.
The backend error TOKEN_EXPIRED
will map to this client facing error. I think you can see this error in action in the integration tests I added.
test/integration/auth.spec.ts
Outdated
}) | ||
.then((sessionCookie) => admin.auth().verifySessionCookie(sessionCookie)) | ||
.then((decodedIdToken) => { | ||
// Check for expected expiration with +/-2 seconds of variation. |
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.
Lets be on the safe side and use a 5s interval for tests.
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.
Done
test/integration/auth.spec.ts
Outdated
return admin.auth().verifySessionCookie(currentSessionCookie, true) | ||
.should.eventually.be.rejected.and.have.property('code', 'auth/session-cookie-revoked'); | ||
}); | ||
}).timeout(5000); |
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.
Can we drop .timeout(5000)
calls now that we are setting it in package.json?
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.
Done
test/integration/auth.spec.ts
Outdated
}); | ||
}).timeout(5000); | ||
|
||
it('fails when called with an invalid ID token', () => { |
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.
It's probably enough to have tests like these that do not make remote API calls as unit tests.
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.
Ok removed these 2 tests.
.then((resp) => { | ||
throw new Error('Unexpected success'); | ||
}, (error) => { | ||
expect(error).to.deep.equal(expectedError); |
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.
Does the error have some code that we can test here?
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.
The server will return the error code INVALID_ID_TOKEN
which gets mapped to developer facing error AuthClientErrorCode.INVALID_ID_TOKEN
. The deep equal assertion here should test for exact code match.
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.
LGTM 👍
Thanks for making the changes.
Thanks for the prompt review, @hiranya911 ! |
@bojeil-google I forgot to mention this in the code review. Please add an entry to the CHANGELOG file as part of this PR. |
I updated the changelog file. PTAL. |
This adds 2 new APIs:
admin.auth().createSessionCookie(idToken: string, sessionCookieOptions:
SessionCookieOptions): Promise
admin.auth().verifySessionCookie(sessionCookie: string, checkRevoked?:
boolean): Promise
Refactored token generator and split token verification to a new class
FirebaseTokenVerifier so it can be used to also verify session cookies.
Kept the same error handling for backward compatibility.
In the process, ported the same tests to token verifier.
Updated token generator ID token and session cookie verification to
check token verifier is called underneath with the expected parameters.
Added integration tests to test all common flows for session cookie
creation and verification.
Added mocks for session cookie JWTs.
Fixed error in URL validator.
Increased integration test timeout to 5 seconds from the default 2 seconds to accommodate user deletion throttling.