Recently, I was struggling with “How to verify and validate AWS Cognito user JWT with the Go backend”. I knew only basic concepts of how JWT token works and I couldn’t find any comprehensive guide for implementation. Nevertheless, sometimes its better to do it the hard way, and learn everything by yourself.
Short background
The client is sending user token with every request. API backend is written with go/go-swagger and is using API key authentication. In such scenario, every authenticated request has argument *principal
, holding the client user JWT.
Gist
Here is my github gist:
// AWS Cognito public keys are available at address:
// https://cognito-idp.{region}.amazonaws.com/{userPoolId}/.well-known/jwks.json
publicKeysURL := "https://cognito-idp.{region}.amazonaws.com/{userPoolId}/.well-known/jwks.json"
// Start with downloading public keys information
// The .Fetch method is used from https://github.com/lestrrat-go/jwx package
publicKeySet, err := jwk.Fetch(publicKeysURL)
if err != nil{
log.Printf("failed to parse key: %s", err)
}
// Get JWT as string from *principal
// Token is Base64-encoded JSON that contains user details - called "claims".
// ---
// Token is separated into 3 sections - header, payload and signature
// You can test and validate your token with jwt.io
tokenString := fmt.Sprintf("%s", *principal)
// We want to get details from the JWT: client_id and unique user identifier.
// Let's add client_id. We can verify, if it match our App cliet ID in AWS Cognito User Pool
// We can also add user identifier (f.e. "username") to use it with our App
type AWSCognitoClaims struct{
Client_ID string `json:client_id`
Username string `json:username`
jwt.StandardClaims
}
// JWT Parse - it's actually doing parsing, validation and returns back a token.
// Use .Parse or .ParseWithClaims methods from https://github.com/dgrijalva/jwt-go
token, err := jwt.ParseWithClaims(tokenString, &AWSCognitoClaims{}, func(token *jwt.Token) (interface{}, error){
// Verify if the token was signed with correct signing method
// AWS Cognito is using RSA256 in my case
_, ok := token.Method.(*jwt.SigningMethodRSA);
if !ok {
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
}
// Get "kid" value from token header
// "kid" is shorthand for Key ID
kid, ok := token.Header["kid"].(string)
if !ok {
return nil, errors.New("kid header not found")
}
// Check client_id attribute from the token
claims, ok := token.Claims.(*AWSCognitoClaims)
if !ok {
return nil, errors.New("There is problem to get claims")
}
log.Printf("client_id: %v", claims.Client_ID)
// "kid" must be present in the public keys set
keys := publicKeySet.LookupKeyID(kid);
if len(keys) == 0 {
return nil, fmt.Errorf("key %v not found", kid)
}
// In our case, we are returning only one key = keys[0]
// Return token key as []byte{string} type
var tokenKey interface{}
if err := keys[0].Raw(&tokenKey); err != nil {
return nil, errors.New("failed to create token key")
}
return tokenKey, nil
})
if err != nil{
// This place can throw expiration error
log.Printf("token problem: %s", err)
}
// Check if token is valid
if !token.Valid {
log.Println("token is invalid")
}
Userful links
Docs to read and tools to use:
- AWS article How can I decode and verify the signature of an Amazon Cognito JSON Web Token?
- Library to fetch public keys - https://github.com/lestrrat-go/jwx
- Library to parse token - https://github.com/dgrijalva/jwt-go
- Very helpful stackoverflow advice from eugenioy https://stackoverflow.com/questions/56905995/how-to-verify-a-jwt-token-from-aws-cognito-in-go
- jwt.io documentation site with live token endocing and decoding