Skip to content

Conversation

colerogers
Copy link
Contributor

@colerogers colerogers commented Feb 3, 2022

Adds most of the common code pieces for auth blocking functions. A lot of this functionality is re-implemented and refactored from the gcip blocking function library.

Since this is a pretty large change, I can try and split this into separate PRs if needed.

@colerogers colerogers changed the title Add new identity specific common functions Add identity common items for new functionality Feb 3, 2022
@colerogers colerogers requested review from inlined and taeold February 3, 2022 16:44
it('should return false on a bad project', () => {
expect(
identity.isAuthorizedCloudFunctionURL(
`https://us-central1-${PROJECT}-old.cloudfunctions.net/function-1`,
Copy link
Member

Choose a reason for hiding this comment

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

What's wrong with this? Because the project and URL don't match?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, we expect the url to be in the form https://${location}-${project}.cloudfunctions.net/..., but the given url does not match with the given project name, so we should return false.

const rawUserInfo = {
name: 'John Doe',
granted_scopes:
'openid https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile',
Copy link
Member

Choose a reason for hiding this comment

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

What? This isn't a list? weird...

authType: 'USER',
resource: {
service: 'identitytoolkit.googleapis.com',
name: 'projects/project-id/tenants/TENANT_ID',
Copy link
Member

Choose a reason for hiding this comment

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

Should we verify that GCIP is always going to have one handler for all of auth and not have a handler per tenant? I would assume that the latter would make sense, but they know their product better than us obv.

Copy link
Contributor Author

@colerogers colerogers Feb 8, 2022

Choose a reason for hiding this comment

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

According to their public docs, functions are registered at the project level (one handler per project). If the customer wishes to filter on users registering/logging in under a specific tenant, they can use the user.tenantId field inside the function.

* Verifies the jwt using the 'jwt' library and decodes the token with the public keys
* Throws an error if the event types do not match
*/
function verifyAndDecodeJWT(
Copy link
Member

Choose a reason for hiding this comment

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

OOC, how do you plan to loosen the verification requirements for the emulator in the future?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was planning on checking for an environment variable, but until that decision gets made, I added in a function shouldVerifyJWT() to determine if we need to verify the JWT. Right now, it will always return true.

return async (req: express.Request, res: express.Response): Promise<void> => {
try {
const projectId = process.env.GCLOUD_PROJECT;
const publicKeys = await fetchPublicKeys();
Copy link
Member

Choose a reason for hiding this comment

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

IIRC these keys rotate very infrequently. Can we keep a cache and compare against a key (e.g. exp) to know whether we need to refresh the cache? We might also need some retry logic if the key rotated early. It would be nice to cut a request out of the amortized cost of a function when users are interactively waiting for completion

Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor Author

@colerogers colerogers Feb 10, 2022

Choose a reason for hiding this comment

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

Added a key cache by performing a similar technique that the admin SDK uses except without using a Class.

In the cache-control header, we can parse for a max-age property in order to figure out when the keys needs to be refreshed.

I also added in a single retry to verification that will force refresh the keys.

return async (req: express.Request, res: express.Response): Promise<void> => {
try {
const projectId = process.env.GCLOUD_PROJECT;
const publicKeys = await fetchPublicKeys();
Copy link
Contributor

Choose a reason for hiding this comment

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

const userRecord = parseUserRecord(decodedJWT.user_record);
const authEventContext = parseAuthEventContext(decodedJWT, projectId);
const authResponse =
(await handler(userRecord, authEventContext)) || undefined;
Copy link
Contributor

Choose a reason for hiding this comment

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

nit* I'm not sure what the difference between (await handler(userRecord, authEventContext)) vs (await handler(userRecord, authEventContext)) || undefined is. Are we trying to make sure any falsy values are re-assigned to undefined?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The handler can have a return type of void and we can't use void in boolean checks (like in getUpdateMask) so assigning it to undefined seemed like the right approach here. Curious if you have a better approach?

@colerogers colerogers requested review from inlined and taeold February 10, 2022 17:44
@colerogers colerogers marked this pull request as ready for review February 10, 2022 17:44
) {
return async (req: express.Request, res: express.Response): Promise<void> => {
try {
const projectId = process.env.GCLOUD_PROJECT;
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm a little worried that projectId can be 2 things across GCP:

human reable projectId = 'danielylee-gcp-project'
numeric project number = '1928371928'

Our API guidelines suggests that all Google APIs support both as project identifier interchangeably.

IIUC, GCLOUD_PROJECT env var is set by our CLI and will most likely be the human-readable one. Is there a guideline from the auth team that they will also use this type always?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The identity platform request that's sent uses the human readable project id. I'll have to check with them if they plan on changing that.

return async (req: express.Request, res: express.Response): Promise<void> => {
try {
const projectId = process.env.GCLOUD_PROJECT;
validRequest(req);
Copy link
Contributor

Choose a reason for hiding this comment

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

nit* validateRequest to make it consistent w/ other function name like validateAuthResponse.

One overall sense I got reading the code is that I wasn't sure if a function call like this would throw an error or not. I think one way to improve this issue would've been to consolidate logic for throwing HTTP error within this function, e.g.

      if (!isValidRequest(req)) {
        logger.error('Invalid request, unable to process: ' + e.message);
        throw new HttpsError('invalid-argument', 'Bad Request');
      }

Not meant to be a comment that blocks PR - just a point I wanted to discuss as team.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's a fair point, I re-wrote it to match what we do in common/https

@shauryaaher
Copy link

Please merge this PR.

@inlined
Copy link
Member

inlined commented Mar 28, 2022

What is the plan for this PR?

@colerogers
Copy link
Contributor Author

@inlined plan was to wait for the Admin SDK changes, merge them in here, and complete the PR. Since this is taking much longer than anticipated, do you recommend closing this and re-opening a PR later?

@colerogers colerogers merged commit e08a71d into master Mar 30, 2022
@colerogers colerogers deleted the colerogers.auth-add-new-common-items branch March 30, 2022 22:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants