Skip to content

Instantly share code, notes, and snippets.

@miguelmota
Created February 11, 2019 21:26
Show Gist options
  • Save miguelmota/06f563756448b0d4ce2ba508b3cbe6e2 to your computer and use it in GitHub Desktop.
Save miguelmota/06f563756448b0d4ce2ba508b3cbe6e2 to your computer and use it in GitHub Desktop.
Golang AWS Cognito Validate JWT token
package auth
import (
"crypto/rsa"
"encoding/base64"
"encoding/binary"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"math/big"
"net/http"
jwt "github.com/dgrijalva/jwt-go"
)
// Auth ...
type Auth struct {
jwk *JWK
jwkURL string
cognitoRegion string
cognitoUserPoolID string
}
// Config ...
type Config struct {
CognitoRegion string
CognitoUserPoolID string
}
// JWK ...
type JWK struct {
Keys []struct {
Alg string `json:"alg"`
E string `json:"e"`
Kid string `json:"kid"`
Kty string `json:"kty"`
N string `json:"n"`
} `json:"keys"`
}
// NewAuth ...
func NewAuth(config *Config) *Auth {
a := &Auth{
cognitoRegion: config.CognitoRegion,
cognitoUserPoolID: config.CognitoUserPoolID,
}
a.jwkURL = fmt.Sprintf("https://cognito-idp.%s.amazonaws.com/%s/.well-known/jwks.json", a.cognitoRegion, a.cognitoUserPoolID)
err := a.CacheJWK()
if err != nil {
log.Fatal(err)
}
return a
}
// CacheJWK ...
func (a *Auth) CacheJWK() error {
req, err := http.NewRequest("GET", a.jwkURL, nil)
if err != nil {
return err
}
req.Header.Add("Accept", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
jwk := new(JWK)
err = json.Unmarshal(body, jwk)
if err != nil {
return err
}
a.jwk = jwk
return nil
}
// ParseJWT ...
func (a *Auth) ParseJWT(tokenString string) (*jwt.Token, error) {
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
key := convertKey(a.jwk.Keys[1].E, a.jwk.Keys[1].N)
return key, nil
})
if err != nil {
return token, err
}
return token, nil
}
// JWK ...
func (a *Auth) JWK() *JWK {
return a.jwk
}
// JWKURL ...
func (a *Auth) JWKURL() string {
return a.jwkURL
}
// https://gist.github.com/MathieuMailhos/361f24316d2de29e8d41e808e0071b13
func convertKey(rawE, rawN string) *rsa.PublicKey {
decodedE, err := base64.RawURLEncoding.DecodeString(rawE)
if err != nil {
panic(err)
}
if len(decodedE) < 4 {
ndata := make([]byte, 4)
copy(ndata[4-len(decodedE):], decodedE)
decodedE = ndata
}
pubKey := &rsa.PublicKey{
N: &big.Int{},
E: int(binary.BigEndian.Uint32(decodedE[:])),
}
decodedN, err := base64.RawURLEncoding.DecodeString(rawN)
if err != nil {
panic(err)
}
pubKey.N.SetBytes(decodedN)
return pubKey
}
package auth
import (
"os"
"testing"
)
func TestCacheJWT(t *testing.T) {
if !(os.Getenv("AWS_COGNITO_USER_POOL_ID") != "" && os.Getenv("AWS_COGNITO_REGION") != "") {
t.Skip("requires AWS Cognito environment variables")
}
auth := NewAuth(&Config{
CognitoRegion: os.Getenv("AWS_COGNITO_REGION"),
CognitoUserPoolID: os.Getenv("AWS_COGNITO_USER_POOL_ID"),
})
err := auth.CacheJWK()
if err != nil {
t.Error(err)
}
jwt := "eyJraWQiOiJlS3lvdytnb1wvXC9yWmtkbGFhRFNOM25jTTREd0xTdFhibks4TTB5b211aE09IiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJjMTcxOGY3OC00ODY5LTRmMmEtYTk2ZS1lYmEwYmJkY2RkMjEiLCJldmVudF9pZCI6IjZmYWMyZGNjLTJlMzUtMTFlOS05NDZjLTZiZDI0YmRlZjFiNiIsInRva2VuX3VzZSI6ImFjY2VzcyIsInNjb3BlIjoiYXdzLmNvZ25pdG8uc2lnbmluLnVzZXIuYWRtaW4iLCJhdXRoX3RpbWUiOjE1NDk5MTQyNjUsImlzcyI6Imh0dHBzOlwvXC9jb2duaXRvLWlkcC51cy13ZXN0LTIuYW1hem9uYXdzLmNvbVwvdXMtd2VzdC0yX0wwVldGSEVueSIsImV4cCI6MTU0OTkxNzg2NSwiaWF0IjoxNTQ5OTE0MjY1LCJqdGkiOiIzMTg0MDdkMC0zZDNhLTQ0NDItOTMyYy1lY2I0MjQ2MzRiYjIiLCJjbGllbnRfaWQiOiI2ZjFzcGI2MzZwdG4wNzRvbjBwZGpnbms4bCIsInVzZXJuYW1lIjoiYzE3MThmNzgtNDg2OS00ZjJhLWE5NmUtZWJhMGJiZGNkZDIxIn0.rJl9mdCrw_lertWhC5RiJcfhRP-xwTYkPLPXmi_NQEO-LtIJ-kwVEvUaZsPnBXku3bWBM3V35jdJloiXclbffl4SDLVkkvU9vzXDETAMaZEzOY1gDVcg4YzNNR4H5kHnl-G-XiN5MajgaWbjohDHTvbPnqgW7e_4qNVXueZv2qfQ8hZ_VcyniNxMGaui-C0_YuR6jdH-T14Wl59Cyf-UFEyli1NZFlmpUQ8QODGMUI12PVFOZiHJIOZ3CQM_Xs-TlRy53RlKGFzf6RQfRm57rJw_zLyJHHnB8DZgbdCRfhNsqZka7ZZUUAlS9aMzdmSc3pPFSJ-hH3p8eFAgB4E71g"
token, err := auth.ParseJWT(jwt)
if err != nil {
t.Error(err)
}
if !token.Valid {
t.Fail()
}
}
@nvcnvn
Copy link

nvcnvn commented May 27, 2019

Hi,
Any downside of caching the result of convertKey?

@miguelmota
Copy link
Author

@nvcnvn not that I can think of

@Quixotical
Copy link

@miguelmota thank you for posting this, it has been a tremendous help to me

@Quixotical
Copy link

one thing to note for anyone who comes here though is that the JWT package used here is now deprecated, you should instead use https://github.com/golang-jwt/jwt. This code is fully functional using the new repo though

@stream-jdibling
Copy link

stream-jdibling commented Jul 16, 2022

@miguelmota thanks for posting this, it was super helpful!

I did run in to one problem with the code - the KeyFunc is hardcoding using the second JWK, instead of looking up the correct JWK by KID. On line 91:

		key := convertKey(a.jwk.Keys[1].E, a.jwk.Keys[1].N)

This was failing for me, because my JWT happened to use the first KID instead of the second! So I created a new gist that fixes this by looking up the JWK by KID, and also uses https://github.com/golang-jwt/jwt as mentioned by @Quixotical. Feel free to copy if you like: https://gist.github.com/stream-ai-llc/2c27416d01c99cbeea9fdd07d74e8b0f.

But thanks again, this gist really helped!

@davidsteed1
Copy link

davidsteed1 commented Aug 4, 2022

There is a problem with this

key := convertKey(a.jwk.Keys[1].E, a.jwk.Keys[1].N)

Assumes the key is always the last key. You need to match the kid. Replace with

		index := 0
		found := false
		for i, v := range a.jwk.Keys {
			if v.Kid == token.Header["kid"] {
				index = i
				found = true
			}
		}
		if !found {
			return nil, errors.New("Key Not found")
		}
		if token.Method.Alg() != "RS256" {
			return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
		}
		key := convertKey(a.jwk.Keys[index].E, a.jwk.Keys[index].N)

@nabarunchatterjee
Copy link

@miguelmota Thanks. This has been very helpful.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment