AWS Cognito token validation with Go

3 min. read


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")
}

Docs to read and tools to use:

6. May 2020
Posted in Gists
Tomas
Tomas

Software developer, lives in Zilina, Slovakia. Fan of modern web technologies, digitalization, cloud and education. Also co-owner of a small coffee shop - Il Doppio coffee