-
Notifications
You must be signed in to change notification settings - Fork 2.1k
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
feat(core): validate if access and id tokens are valid cognito tokens #13385
Conversation
|
||
return true; | ||
} catch (error) { | ||
return false; |
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.
Do we want to debug log a generic message 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.
We could, though if we really want a log to provide insight of the check failure, we may actually need production log.
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.
Also can we add a TODO item here to remind us that this error should be surfaced eventually.
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 added
): Promise<boolean> { | ||
const userPoolId = this.authConfig?.Cognito?.userPoolId; | ||
const clientId = this.authConfig?.Cognito?.userPoolClientId; | ||
if (!userPoolId || !clientId) return false; |
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 may want to throw a different error here? If not we will just throw InvalidToken and is not appropriate I think.
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.
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
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 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); |
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.
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.
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.
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
395ca9a
to
c3dc5b6
Compare
nit cleanups in progress |
c3dc5b6
to
8bad71e
Compare
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 @ashwinkumar6 for fixing this issue!
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 adding this improvement.
packages/core/src/adapterCore/serverContext/types/KeyValueStorageValidator.ts
Outdated
Show resolved
Hide resolved
packages/core/src/adapterCore/serverContext/types/KeyValueStorageValidator.ts
Outdated
Show resolved
Hide resolved
|
||
return true; | ||
} catch (error) { | ||
return false; |
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.
Also can we add a TODO item here to remind us that this error should be surfaced eventually.
...__tests__/adapterCore/storageFactories/createKeyValueStorageFromCookieStorageAdapter.test.ts
Outdated
Show resolved
Hide resolved
|
||
// TODO: is this correct ? | ||
// make sure userPoolId && clientId are present if token is access/id | ||
if (!userPoolId || !clientId) return false; |
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: we can require these two fields from createTokenValidator
's consumer.
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.
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.
packages/adapter-nextjs/__tests__/utils/createTokenValidator.test.ts
Outdated
Show resolved
Hide resolved
packages/adapter-nextjs/__tests__/utils/createTokenValidator.test.ts
Outdated
Show resolved
Hide resolved
packages/adapter-nextjs/__tests__/utils/createTokenValidator.test.ts
Outdated
Show resolved
Hide resolved
…ageValidator.ts Co-authored-by: Hui Zhao <[email protected]>
…est.ts Co-authored-by: Hui Zhao <[email protected]>
…ageValidator.ts Co-authored-by: Hui Zhao <[email protected]>
if (value && validatorMap?.getItem) { | ||
const isValid = await validatorMap.getItem(key, value); | ||
if (!isValid) return null; | ||
} | ||
|
||
return value; |
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 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
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 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> => { |
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 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.
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.
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
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 @ashwinkumar6 for putting this together only some minor comments. Let me know what you think
Co-authored-by: israx <[email protected]>
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.
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, |
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.
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?
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.
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", |
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.
Sanity check: No concerns with RN or anything (polyfills, etc) here right as this code should only execute on SSR?
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.
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
…o tokens (aws-amplify#13385)" This reverts commit 0b72b32.
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 sideDescription of how you validated changes
Tested locally in Next.js sample app by passing valid and invalid tokens in cookie storage
Checklist
yarn test
passesBy submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.