-
Notifications
You must be signed in to change notification settings - Fork 392
Allow createCustomToken() to work with tenant-aware auth #708
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
Conversation
5e6cb16
to
8b02f7a
Compare
src/auth/token-generator.ts
Outdated
return this.createCustomTokenInternal(uid, tenantId, developerClaims); | ||
} | ||
|
||
private createCustomTokenInternal( |
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.
Alternatively to having two separate createCustomToken*() methods, we could have one with an options object, i.e.:
interface CreateCustomTokenOptions {
tenantId?: string;
developerClaims?: {[key: string]: any};
}
public createCustomToken(uid: string, options?: CreateCustomTokenOptions) { ... }
@@ -293,22 +294,22 @@ describe('FirebaseTokenGenerator', () => { | |||
it('should throw given no uid', () => { | |||
expect(() => { | |||
(tokenGenerator as any).createCustomToken(); | |||
}).to.throw('First argument to createCustomToken() must be a non-empty string uid'); | |||
}).to.throw(FirebaseAuthError).with.property('code', 'auth/argument-error'); |
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.
I've switched many of the error check tests from 'verify the error message' to 'verify the error code'. Personally, I prefer just verifying the code, as the error message itself often changes which makes the tests a little fragile. But this is hardly a universal opinion. I'm happy to add the error message back in if you'd prefer.
(Obviously, these changes in particular were caused by me tweaking the error message slightly and having a bunch of tests fail as a result.)
@@ -466,4 +467,215 @@ describe('FirebaseTokenGenerator', () => { | |||
}); | |||
}); | |||
}); | |||
|
|||
describe('createCustomTokenWithTenantId()', () => { |
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.
These tests largely duplicate the createCustomToken() tests above, but validate that the path with tenantIds also works. Roughly half of these tests are pretty low (though not quite no) value and I'd be happy to remove them. i.e. the tests that validate the uid handling is correct, even when a tennantid is provided, e.g. 'should throw given an empty string uid'.
Opinions welcome.
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.
You can follow a similar approach to other tests for common API used in a tenant and non-tenant context where shared code is used. This avoids the duplication and makes it easier to make changes in the future.
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.
You mean like this?
AUTH_CONFIGS.forEach((testConfig) => { |
The problem here is that:
a) We have a TokenGenerator (rather than an Auth object) and the TokenGenerator is not tenant aware.
b) The API for creating a custom token differs based on whether you want a tenantid included or not.
This problem is the more general problem of testing method overloads; I'm not aware of a great solution. Here's the obvious:
a) Just duplicate the tests. (i.e. as currently implemented)
b) Mock the implementation method and just verify that createCustomToken() pipes it's arguments through successfully. Possibly more pragmatic, but requires you to peek at the implementation (and to assume it won't change.)
In our case, we could also:
c) Make TokenGenerator tenant aware. Then we could use the same approach as for Auth instances. It seems a little odd... but I think it's workable with some refactoring around how the Auth instance creates its TokenGenerator.
d) Iterate over a list of functions, like this (which is pretty ugly):
const createTokenFns = [
(uid: string, tenantId: string, developerClaims?: {[key: string]: any}): Promise<string> => {
return tokenGenerator.createCustomToken(string, developerClaims);
}
(uid: string, tenantId: string, developerClaims?: {[key: string]: any}): Promise<string> => {
return tokenGenerator.createCustomTokenWithTenantId(string, tenantId, developerClaims);
}];
createTokensFns.forEach((fn) => {
describe("create custom token, possibly tenant aware", () => {
it('should work', () => {
expect(fn('uid', 'tenantid_that_might_be_ignored')).should.eventually.be.fulfilled;
});
});
});
I don't like (d); I mildly don't like (b). (c) doesn't seem so bad though. wdyt?
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.
TokenGenerator
is created per Auth
and TenantAwareAuth
instance. The tenant ID in that case does not change after creation (could be set in the constructor). TokenGenerator
has only one public method createCustomToken
that mints tokens that are tenant aware. Every call of that should also set the tenant ID in the minted JWT. So maybe (c) is a good choice. But I am not sure how much work is involved in this. Do you want to explore and make an assessment?
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 not too much at all. Done.
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.
Thanks for putting this together. Took a quick look. Will take a more thorough look tomorrow.
src/auth/token-generator.ts
Outdated
/** | ||
* Creates a new Firebase Auth Custom token. | ||
* | ||
* @param {string} uid The user ID to use for the generated Firebase Auth Custom 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.
I know this is the current pattern but we should move away from re-stating the types, eg. string
in the javadocs.
This is according to the internal typescript readability guideline.
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.
Very happy to do this. (Done.) Obviously, I've only done this for the methods I've touched in this PR (createCustomToken*).
test/unit/auth/auth.spec.ts
Outdated
}); | ||
expect(() => { | ||
mockCredentialAuth.createCustomToken(mocks.uid, mocks.developerClaims); | ||
}).not.to.throw; |
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.
Seems to be a mismatch with the test description (should throw if...)
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.
Huh; that's odd. Seems to go back to #285 which is literally called 'Support for creating custom tokens without service account credentials'. So the test was adjusted as part of that, but the test description was not. I've fixed it.
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.
But wait! That's wrong too, since this test can't call "the IAM service in the cloud when the service account credentials are not provided." So instead of throwing immediately, it now returns a failed future. Fixed (for real this time) by ensuring it does fail, but not immediately. (Might be worth checking with @hiranya911 to see if this captures the original intent.)
@@ -466,4 +467,215 @@ describe('FirebaseTokenGenerator', () => { | |||
}); | |||
}); | |||
}); | |||
|
|||
describe('createCustomTokenWithTenantId()', () => { |
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.
You can follow a similar approach to other tests for common API used in a tenant and non-tenant context where shared code is used. This avoids the duplication and makes it easier to make changes in the future.
Cuts down on the test duplication.
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.
Latest changes look good. Just some minor comments.
constructor(app: FirebaseApp, protected readonly authRequestHandler: T) { | ||
const cryptoSigner = cryptoSignerFromApp(app); | ||
this.tokenGenerator = new FirebaseTokenGenerator(cryptoSigner); | ||
constructor(app: FirebaseApp, protected readonly authRequestHandler: T, tokenGenerator?: FirebaseTokenGenerator) { |
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.
We should update the javadocs with the right number of parameters
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. (Also removed {types})
src/auth/token-generator.ts
Outdated
if (!validator.isNonNullObject(signer)) { | ||
throw new FirebaseAuthError( | ||
AuthClientErrorCode.INVALID_CREDENTIAL, | ||
'INTERNAL ASSERT: Must provide a CryptoSigner to use FirebaseTokenGenerator.', | ||
); | ||
} | ||
if (tenantId !== undefined && !validator.isNonEmptyString(tenantId)) { |
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: typeof tenantId !== 'undefined'
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/token-generator.ts
Outdated
if (tenantId !== undefined && !validator.isNonEmptyString(tenantId)) { | ||
throw new FirebaseAuthError( | ||
AuthClientErrorCode.INVALID_ARGUMENT, | ||
'`tenantId` argument must be a non-empty string uid.'); |
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.
Maybe remove uid
mention as it may confuse the developer since we use it to refer to the user unique identifier?
tenantId
argument must be a non-empty string.
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. (I had copied an error message from somewhere else and adjusted it, but didn't remove the uid part.)
expect(() => { | ||
tokenGenerator.createCustomToken(uid); | ||
}).not.to.throw(); | ||
const tokenGeneratorConfigs = [{ |
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.
You can also declare an interface for 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.
I don't think that's necessary; the type checker will still enforce everything via it's type deduction. (And I verified that, because I initially got it wrong.) If you want it anyways for doc purposes, or any other reason, I'm happy to add it in though.
test/integration/auth.spec.ts
Outdated
|
||
it('createCustomToken() mints a JWT that can be used to sign in tenant users', async () => { | ||
try { | ||
firebase.auth!().tenantId = createdTenantId; |
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.
Style guide does not seem to recommend non-nullability assertions without a reason.
https://g3doc.corp.google.com/javascript/typescript/g3doc/styleguide.md?cl=head#type-and-non-nullability-assertions
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 allows it when the reason is "obvious". So I'd argue:
a) It's "obvious" that this is present because we've imported '@firebase/auth'.
b) It's overly verbose to comment each case (or to expect(firebase.auth).to.be.ok
) each time.)
c) Worst case, something goes wrong and the test fails. Which is exactly what we'd want (albeit, the cause might not be as obvious as ideal.)
A possible compromise: It's reasonably obvious that this is the client auth instance, but not perfectly obvious. We could make a helper, and that helper could do the expect dance since it wouldn't clutter the tests. The helper would make it more obvious that this is the client instance (rather than admin.auth), so it'd serve that purpose too. (Done.)
test/integration/auth.spec.ts
Outdated
@@ -91,6 +91,10 @@ function randomOidcProviderId(): string { | |||
return 'oidc.' + generateRandomString(10, false).toLowerCase(); | |||
} | |||
|
|||
function client_auth(): FirebaseAuth { |
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 use camel case clientAuth
for function names here and elsewhere.
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.
No description provided.