How can I validate and get info from a JWT received from Amazon Cognito?
I have setup Google authentication in Cognito, and set the redirect uri to to hit API Gateway, I then receive a code which I POST to this endpoint:
https://docs.aws.amazon.com/cognito/latest/developerguide/token-endpoint.html
To receive the JWT token, in a RS256 format. I am now struggling to validate, and parse the token in Golang. I’ve tried to parse it using jwt-go, but it appears to support HMAC instead by default and read somewhere that they recommend using frontend validation instead. I tried a few other packages and had similar problems.
I came across this answer here: Go Language and Verify JWT but assume the code is outdated as that just says panic: unable to find key
.
jwt.io can easily decode the key, and probably verify too. I’m not sure where the public/secret keys are as Amazon generated the token, but from what I understand I need to use a JWK URL to validate too? I’ve found a few AWS specific solutions, but they all seem to be hundreds of lines long. Surely it isn’t that complicated in Golang is it?
To verify JWT claimsVerify that the token is not expired. The aud claim in an ID token and the client_id claim in an access token should match the app client ID that was created in the Amazon Cognito user pool. The issuer ( iss ) claim should match your user pool.
Authenticating with tokensWhen a user signs into your app, Amazon Cognito verifies the login information. If the login is successful, Amazon Cognito creates a session and returns an ID, access, and refresh token for the authenticated user.
To authenticate a user, a client application must send a JSON Web Token (JWT) in the authorization header of the HTTP request to your backend API. API Gateway validates the token on behalf of your API, so you don't have to add any code in your API to process the authentication.
To verify the signature of an Amazon Cognito JWT, search for the key with a key ID that matches the key ID of the JWT, then use libraries to decode the token and verify the signature. Be sure to also verify that: The token is not expired.
This is what I did with only the latest (v1.0.8
) github.com/lestrrat-go/jwx
. Note that github.com/dgrijalva/jwt-go
does not seem to be maintained anymore and people are forking it to make the updates they need.
package main
import (
...
"github.com/lestrrat-go/jwx/jwk"
"github.com/lestrrat-go/jwx/jwt"
)
...
keyset, err := jwk.Fetch("https://cognito-idp." + region + ".amazonaws.com/" + userPoolID + "/.well-known/jwks.json")
parsedToken, err := jwt.Parse(
bytes.NewReader(token), //token is a []byte
jwt.WithKeySet(keyset),
jwt.WithValidate(true),
jwt.WithIssuer(...),
jwt.WithClaimValue("key", value),
)
//check err as usual
//here you can call methods on the parsedToken to get the claim values
...
Token claim methods
Public keys for Amazon Cognito
As you already guessed, you'll need the public key in order to verify the JWT token.
https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-using-tokens-verifying-a-jwt.html#amazon-cognito-user-pools-using-tokens-step-2
Download and store the corresponding public JSON Web Key (JWK) for your user pool. It is available as part of a JSON Web Key Set (JWKS). You can locate it at https://cognito-idp.{region}.amazonaws.com/{userPoolId}/.well-known/jwks.json
Parse keys and verify token
That JSON file structure is documented in the web, so you could potentially parse that manually, generate the public keys, etc.
But it'd probably be easier to just use a library, for example this one: https://github.com/lestrrat-go/jwx
And then jwt-go to deal with the JWT part: https://github.com/dgrijalva/jwt-go
You can then:
Download and parse the public keys JSON using the first library
keySet, err := jwk.Fetch(THE_COGNITO_URL_DESCRIBED_ABOVE)
When parsing the token with jwt-go, use the "kid" field from the JWT header to find the right key to use
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodRS256); !ok {
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
}
kid, ok := token.Header["kid"].(string)
if !ok {
return nil, errors.New("kid header not found")
}
keys := keySet.LookupKeyID(kid);
if !ok {
return nil, fmt.Errorf("key with specified kid is not present in jwks")
}
var publickey interface{}
err = keys.Raw(&publickey)
if err != nil {
return nil, fmt.Errorf("could not parse pubkey")
}
return publickey, nil
The type assertion in the code provided by eugenioy and Kevin Wydler did not work for me: *jwt.SigningMethodRS256 is not a type
.
*jwt.SigningMethodRS256
was a type in the initial commit. From the second commit on (back in July 2014) it was abstracted and replaced by a global variable (see here).
This following code works for me:
func verify(tokenString string, keySet *jwk.Set) {
tkn, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
if token.Method.Alg() != "RSA256" { // jwa.RS256.String() works as well
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
}
kid, ok := token.Header["kid"].(string)
if !ok {
return nil, errors.New("kid header not found")
}
keys := keySet.LookupKeyID(kid)
if len(keys) == 0 {
return nil, fmt.Errorf("key %v not found", kid)
}
var raw interface{}
return raw, keys[0].Raw(&raw)
})
}
Using the following dependency versions:
github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1
github.com/lestrrat-go/jwx v1.0.4
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With