Skip to content

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
wants to merge 7 commits into from

Conversation

lahirumaramba
Copy link
Member

@lahirumaramba lahirumaramba commented Mar 8, 2021

  • Move FirebaseTokenVerifier to util
  • Include error type and codes to FirebaseTokenInfo to throw public error types from FirebaseTokenVerifier

@lahirumaramba lahirumaramba added the release:stage Stage a release candidate label Mar 8, 2021
Copy link
Contributor

@hiranya911 hiranya911 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haven't looked at everything, but looks reasonable. Let's start off with some feedback on the high-level design.

Copy link
Contributor

@hiranya911 hiranya911 left a 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 suggestions for cleaning up the PR.

@lahirumaramba
Copy link
Member Author

  • Merged the expired error code to error code config
  • Added validation for the new error code config and error type
  • Added unit tests for new validations

Copy link
Contributor

@hiranya911 hiranya911 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for making the changes. Looks mostly good. Couple of points to revise:

  1. There's one remaining reference to auth from the utils token verifier.
  2. Unit tests could use a bit more attention.

@@ -213,9 +216,9 @@ 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 (auth/${this.tokenInfo.errorCodeConfig.expiredError.code}).` +
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a reference to auth here. Should probably get rid of it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh good catch! I actually ended up finding more references to Auth. Ended up using generics in the verify token functions and moved the code to set the user id alias to one level up to auth.ts.
Let me know what you think. Thanks!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re: the error code prefix. I just removed the auth prefix, but the error code feels a bit redundant here as the error code will also be included in the public error type.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is something that will get cleaned up once we fix error handling for this SDK. For now this solution is adequate.

});
});

const invalidErrorCodeConfigs = [null, NaN, 0, 1, true, false, [], {}, { a: 1 }, _.noop, '', 'test'];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests now belong in utils.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved the tests to test/utils

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's not move the whole file. It still contains many auth-specific tests.

.then((decodedIdToken: DecodedIdToken) => {
decodedIdToken.uid = decodedIdToken.sub;
Copy link
Member Author

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 uid alias here. Another way is to add helper functions, verifySessionCookie() and verifyIdToken(), to auth/token-verifier. Let me know your thoughts.

Copy link
Contributor

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.TokenVerifier as AuthTokenVerifier, and add the logic there?

* @return {Promise<T>} A promise fulfilled with the decoded claims of the Firebase Auth ID
* token.
*/
public verifyJWT<T extends object>(jwtToken: string, isEmulator = false): Promise<T> {
Copy link
Member Author

@lahirumaramba lahirumaramba Mar 11, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hiranya911 :
To make the type check tighter we can introduce a base type for returned tokens...

interface FirebaseToken {
  // registered JWT claims
  iss: string
  aud: string
}

interface DecodedIdToken extends FirebaseToken {
...
}

public verifyJWT<T extends FirebaseToken>

Let me know your thoughts. Thanks!

Copy link
Contributor

@hiranya911 hiranya911 Mar 11, 2021

Choose a reason for hiding this comment

The 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.

// in utils
interface DecodedToken {
  iss: string;
  aud: string;
  sub: string;
}

// in auth
interface DecodedIdToken {
  iss: string;
  aud: string;
  sub: string;
  uid: string;
}

// at callsite
const decodedToken: DecodedToken = await tokenVerifier.verifyToken();
const result: DecodedIdToken = {...decodedToken, uid: decodedToken.sub};

Copy link
Contributor

@hiranya911 hiranya911 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets find a more typesafe way to handle the returns. Plus separation of tests between auth and utils.

.then((decodedIdToken: DecodedIdToken) => {
decodedIdToken.uid = decodedIdToken.sub;
Copy link
Contributor

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.TokenVerifier as AuthTokenVerifier, and add the logic there?

} 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)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typeof errorType === 'function' also asserts that errorType !== null. So the 2nd part of the conjunction is redundant.

* token.
*/
public verifyJWT(jwtToken: string, isEmulator = false): Promise<DecodedIdToken> {
public verifyJWT<T extends object>(jwtToken: string, isEmulator = false): Promise<T> {
Copy link
Contributor

Choose a reason for hiding this comment

The 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 auth/token-verifier.

const decodedIdToken = (decodedToken as DecodedIdToken);
decodedIdToken.uid = decodedIdToken.sub;
resolve(decodedIdToken);
resolve(decodedToken as T);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not particularly typesafe.

'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.');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ErrorCodeConfig?

@@ -30,10 +30,12 @@ import LegacyFirebaseTokenGenerator = require('firebase-token-generator');
import * as mocks from '../../resources/mocks';
import { FirebaseTokenGenerator, ServiceAccountSigner } from '../../../src/auth/token-generator';
import * as verifier from '../../../src/utils/token-verifier';
import * as verifierUtil from '../../../src/auth/token-verifier-util';
import {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry if I wasn't clear. But I didn't mean moving this entire file into utils. Some tests in this file are still very auth-specific, and therefore should be in auth. But the test cases that specifically test for the utils.token-verifier, should be moved into a new test module (like the invalidErrorTypes test cases).

I think a good rule of thumb is if a test requires an import from auth, then it probably belongs in auth.

@lahirumaramba
Copy link
Member Author

Closing this as we decided to change the overall approach and design of the refactor. This work is replaced by #1204

@lahirumaramba lahirumaramba deleted the lm-refactor-verify branch March 24, 2021 23:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
release:stage Stage a release candidate
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants