Skip to content
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

feat(core): validate if access and id tokens are valid cognito tokens #13385

Merged
merged 12 commits into from
May 21, 2024

Conversation

ashwinkumar6
Copy link
Contributor

@ashwinkumar6 ashwinkumar6 commented May 15, 2024

Description of changes

Add additional validations:
Add isValidCognitoToken util to core to validate if the given access and id tokens are valid using the aws-jwt-verify library on server side

Description of how you validated changes

Tested locally in Next.js sample app by passing valid and invalid tokens in cookie storage

Checklist

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

@ashwinkumar6 ashwinkumar6 marked this pull request as ready for review May 15, 2024 22:34
@ashwinkumar6 ashwinkumar6 requested review from a team as code owners May 15, 2024 22:34

return true;
} catch (error) {
return false;
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we want to debug log a generic message here?

Copy link
Contributor

Choose a reason for hiding this comment

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

We could, though if we really want a log to provide insight of the check failure, we may actually need production log.

Copy link
Contributor

Choose a reason for hiding this comment

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

Also can we add a TODO item here to remind us that this error should be surfaced eventually.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

thanks added

): Promise<boolean> {
const userPoolId = this.authConfig?.Cognito?.userPoolId;
const clientId = this.authConfig?.Cognito?.userPoolClientId;
if (!userPoolId || !clientId) return false;
Copy link
Contributor

Choose a reason for hiding this comment

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

We may want to throw a different error here? If not we will just throw InvalidToken and is not appropriate I think.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Discussed offline, throwing any error here would result in it being swallowed by the token store. Hence we just return null if validation fails when getting an item from the store.

The reason the token store swallows the error and returns null is it intends to mock the Storage mechanism in the browser which also returns null if a key isn't found. Adding a TODO to revisit this, we could also clear the tokens if invalid

Copy link
Contributor

@israx israx left a comment

Choose a reason for hiding this comment

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

The usage of the aws-jwt-verify library to validate the signature of the tokens make sense to me. Based on their code base I see that they have a solid approach to validate Cognito tokens. However we shouldn't validate the tokens at the TokenStore level because it can be overwritten by a custom token provider.

Blocking PR until we make sure there is not a security gap left. It might be worth validating tokens in the server fetchAuthSession API directly. That way we isolate any extra bundle size that is affecting other APIs.

tokenUse: tokenType,
clientId,
});
await verifier.verify(token);
Copy link
Contributor

@israx israx May 16, 2024

Choose a reason for hiding this comment

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

Probably this util might not be needed. The verify method returns the same JWT if the token is valid, otherwise it throws. I think we should rely on that error and let the customer handle it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Discussed async, the error thrown by aws-jwt-verify may contain sensitive information. It's best to not surface the actual error generated by aws-jwt-verify

@ashwinkumar6
Copy link
Contributor Author

nit cleanups in progress

packages/adapter-nextjs/src/utils/validator.ts Outdated Show resolved Hide resolved
packages/adapter-nextjs/src/utils/validator.ts Outdated Show resolved Hide resolved
packages/adapter-nextjs/src/utils/validator.ts Outdated Show resolved Hide resolved
Copy link
Contributor

@israx israx left a comment

Choose a reason for hiding this comment

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

Thanks @ashwinkumar6 for fixing this issue!

@israx israx self-requested a review May 17, 2024 22:00
Copy link
Contributor

@HuiSF HuiSF 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 adding this improvement.


return true;
} catch (error) {
return false;
Copy link
Contributor

Choose a reason for hiding this comment

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

Also can we add a TODO item here to remind us that this error should be surfaced eventually.


// TODO: is this correct ?
// make sure userPoolId && clientId are present if token is access/id
if (!userPoolId || !clientId) return false;
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: we can require these two fields from createTokenValidator's consumer.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If we make userPoolId and clientId required fields in this function, we would need make sure these are passed by it's consumer createRunWithAmplifyServerContext.

It's a bit tricky because if we add assertions in createRunWithAmplifyServerContext, we won't be able to get other keys from Storage if userPoolId or clientId are missing.

Comment on lines +40 to +45
if (value && validatorMap?.getItem) {
const isValid = await validatorMap.getItem(key, value);
if (!isValid) return null;
}

return value;
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if it make sense to move this logic inside the cookieStorageAdapter directly and return null if it is not able to find the cookie

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We had initially moved validation logic inside packages/adapter-nextjs/src/utils/createCookieStorageAdapterFromNextServerContext.ts but this isn't an async function. So in case we throw an error it'll become unhandledRejection.

Hence we made the validationMap optionally injectable into the KeyValueStorage

}): KeyValueStorageMethodValidator => {
return {
// validate access, id tokens
getItem: async (key: string, value: string): Promise<boolean> => {
Copy link
Contributor

Choose a reason for hiding this comment

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

The getItem method implies that it will get something but the return type is a boolean. We could either return the actual item/token and return null if it is invalid. The other option is to change the method name to something more deterministic that implies the validation of the token.

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 good point 🤔,
This function returns a validationMap where the key is name of the function in KeyValueStorage that we want to add validation for (getItem in this case) and the value is the validation function that returns a bool if the validation was successful or now

We could maybe improve the function name to clarity that

Copy link
Contributor

@israx israx left a comment

Choose a reason for hiding this comment

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

Thanks @ashwinkumar6 for putting this together only some minor comments. Let me know what you think

@israx israx self-requested a review May 20, 2024 21:13
Copy link
Contributor

@jimblanc jimblanc left a comment

Choose a reason for hiding this comment

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

Looking good, few questions & nits

@@ -18,6 +21,7 @@ const ONE_YEAR_IN_MS = 365 * 24 * 60 * 60 * 1000;
*/
export const createKeyValueStorageFromCookieStorageAdapter = (
cookieStorageAdapter: CookieStorage.Adapter,
validatorMap?: KeyValueStorageMethodValidator,
Copy link
Contributor

Choose a reason for hiding this comment

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

Given that we have control over this interface & it isn't customer facing, I wonder why even need createTokenValidator with the interface with getItem? Couldn't this just be a function predicate we pass in that ultimately calls isValidCognitoToken and returns true/false?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This would also update a public interface. If customers are creating a custom KeyValueStorageFromCookieStorage they can now pass in a validator also optionally to it (2nd parameter). Since it's a custom KeyValueStorage it's up to them on how they handle the validation.

@@ -54,6 +54,7 @@
"@aws-sdk/types": "3.398.0",
"@smithy/util-hex-encoding": "2.0.0",
"@types/uuid": "^9.0.0",
"aws-jwt-verify": "^4.0.1",
Copy link
Contributor

Choose a reason for hiding this comment

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

Sanity check: No concerns with RN or anything (polyfills, etc) here right as this code should only execute on SSR?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep that's right, the isValidCognitoToken util is added to coreUtils and is used only on the server side for validation. No change on the client side

@ashwinkumar6 ashwinkumar6 merged commit 0b72b32 into aws-amplify:main May 21, 2024
30 checks passed
jimblanc added a commit to jimblanc/amplify-js that referenced this pull request May 22, 2024
jimblanc added a commit that referenced this pull request May 22, 2024
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.

None yet

5 participants