10

Im using dgrijalva/jwt-go & lestrrat-go/jwx. What im trying to achive is validate wso2 jwt using jwks.

the token(expired token):

const tokenStr = `eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6ImI2TnozUDJwMHg1QWpfWENsUmhrVDFzNlNIQSJ9.eyJodHRwOlwvXC93c28yLm9yZ1wvY2xhaW1zXC9hcHBsaWNhdGlvbnRpZXIiOiJVbmxpbWl0ZWQiLCJodHRwOlwvXC93c28yLm9yZ1wvY2xhaW1zXC9rZXl0eXBlIjoiUFJPRFVDVElPTiIsImh0dHA6XC9cL3dzbzIub3JnXC9jbGFpbXNcL3ZlcnNpb24iOiIxLjAiLCJpc3MiOiJ3c28yLm9yZ1wvcHJvZHVjdHNcL2FtIiwiaHR0cDpcL1wvd3NvMi5vcmdcL2NsYWltc1wvYXBwbGljYXRpb25uYW1lIjoiVGFseW9uIiwiaHR0cDpcL1wvd3NvMi5vcmdcL2NsYWltc1wvZW5kdXNlciI6IkZEQkBjYXJib24uc3VwZXIiLCJodHRwOlwvXC93c28yLm9yZ1wvY2xhaW1zXC9lbmR1c2VyVGVuYW50SWQiOiItMTIzNCIsImh0dHA6XC9cL3dzbzIub3JnXC9jbGFpbXNcL3N1YnNjcmliZXIiOiJGREIiLCJodHRwOlwvXC93c28yLm9yZ1wvY2xhaW1zXC90aWVyIjoiR29sZCIsImh0dHA6XC9cL3dzbzIub3JnXC9jbGFpbXNcL2FwcGxpY2F0aW9uaWQiOiIxNDU2IiwiaHR0cDpcL1wvd3NvMi5vcmdcL2NsYWltc1wvdXNlcnR5cGUiOiJBUFBMSUNBVElPTiIsImV4cCI6MTU4OTQ2NjI0MSwiaHR0cDpcL1wvd3NvMi5vcmdcL2NsYWltc1wvYXBpY29udGV4dCI6IlwvY3VycmVudC1hY2NvdW50XC9jaGVxdWVzXC9hdXRvbWF0aWMtZGVwb3NpdHNcL2F0bVwvMS4wIn0=.K1iPtdXiuicuDPaLC6Exw/7UpJVW6Uy1tPpJlfZ29Vqs9M1zR00JpKxvymQMAzbD0GHlXPPsZmhDxOn0WMAPfr1Xi8tiruTLXNbwUPJ/SOovt+zK4JGtrydhc4iv2EROhMUk2uwJUb4DFjqKZRhBvtCW7fRtdtI9yJL4W4OK8Ld90yOb97usPjEPz8S4E4uNrb5lE2rLzIp+EaPwA232lDkhS8gGPIKdlLG1IdEfQ4cFU1VIplvWoHzprF9mGR0ahT2QGgmGE3AcBfkURk8VzIKDG/UcBA9eHu3XGg28j3OvIXWwJhd7Hi+jTqvggi0hplao8ElvjNBw/wNy2UO9WA==`

the jwks:

{"keys":[{"kty":"RSA","e":"AQAB","use":"sig","kid":"MjhhMDk2N2M2NGEwMzgzYjk2OTI3YzdmMGVhOGYxNjI2OTc5Y2Y2MQ","alg":"RS256","n":"zZU9xSgK77PbtkjJgD2Vmmv6_QNe8B54eyOV0k5K2UwuSnhv9RyRA3aL7gDN-qkANemHw3H_4Tc5SKIMltVIYdWlOMW_2m3gDBOODjc1bE-WXEWX6nQkLAOkoFrGW3bgW8TFxfuwgZVTlb6cYkSyiwc5ueFV2xNqo96Qf7nm5E7KZ2QDTkSlNMdW-jIVHMKjuEsy_gtYMaEYrwk5N7VoiYwePaF3I0_g4G2tIrKTLb8DvHApsN1h-s7jMCQFBrY4vCf3RBlYULr4Nz7u8G2NL_L9vURSCU2V2A8rYRkoZoZwk3a3AyJiqeC4T_1rmb8XdrgeFHB5bzXZ7EI0TObhlw"}]}

most of the examples iv'e seen out there uses 'kid' and are not relevant because my token header doesn't have it, it has 'x5t' field..

and i must note one more thing it seems my signature is base64 encoded and not base64 url encoded (it pretty much messes the usage of Parse method). i have tried using jwt.Parse() i have tried manually encrypt header and payload sha256 and than RS256 and base64 but none showed success.

things i have tried:

const tokenString = `..`
func main() {
    t, err := jwt.Parse(tokenStr,  func(t *jwt.Token) (interface{}, error) {
        return []byte("b6Nz3P2p0x5Aj_XClRhkT1s6SHA"), nil
    })
}
1

3 Answers 3

11

The main problem from the original question is that the jwt.KeyFunc is expecting a Go type that represents a public cryptography key, not the JWT's kid.

I've had a very similar use case recently so I read through some RFCs and authored this package: github.com/MicahParks/keyfunc.

It allows you to use the most popular JWT package, github.com/golang-jwt/jwt/v4, (formerly github.com/dgrijalva/jwt-go), to parse tokens. It can also automatically live reload the contents of your JWKS in a background goroutine.

Using your JWKS and JWT, here are two examples. The first will load the JWKS from a remote URL via HTTPS. The second will load it from static JSON.

From JWKS hosted via HTTPS

package main

import (
    "context"
    "log"
    "time"

    "github.com/golang-jwt/jwt/v4"

    "github.com/MicahParks/keyfunc"
)

func main() {
    // Get the JWKS URL.
    //
    // This is a sample JWKS service. Visit https://jwks-service.appspot.com/ and grab a token to test this example.
    jwksURL := "https://jwks-service.appspot.com/.well-known/jwks.json"

    // Create a context that, when cancelled, ends the JWKS background refresh goroutine.
    ctx, cancel := context.WithCancel(context.Background())

    // Create the keyfunc options. Use an error handler that logs. Refresh the JWKS when a JWT signed by an unknown KID
    // is found or at the specified interval. Rate limit these refreshes. Timeout the initial JWKS refresh request after
    // 10 seconds. This timeout is also used to create the initial context.Context for keyfunc.Get.
    options := keyfunc.Options{
        Ctx: ctx,
        RefreshErrorHandler: func(err error) {
            log.Printf("There was an error with the jwt.Keyfunc\nError: %s", err.Error())
        },
        RefreshInterval:   time.Hour,
        RefreshRateLimit:  time.Minute * 5,
        RefreshTimeout:    time.Second * 10,
        RefreshUnknownKID: true,
    }

    // Create the JWKS from the resource at the given URL.
    jwks, err := keyfunc.Get(jwksURL, options)
    if err != nil {
        log.Fatalf("Failed to create JWKS from resource at the given URL.\nError: %s", err.Error())
    }

    // Get a JWT to parse.
    jwtB64 := "eyJraWQiOiJlZThkNjI2ZCIsInR5cCI6IkpXVCIsImFsZyI6IlJTMjU2In0.eyJzdWIiOiJXZWlkb25nIiwiYXVkIjoiVGFzaHVhbiIsImlzcyI6Imp3a3Mtc2VydmljZS5hcHBzcG90LmNvbSIsImlhdCI6MTYzMTM2OTk1NSwianRpIjoiNDY2M2E5MTAtZWU2MC00NzcwLTgxNjktY2I3NDdiMDljZjU0In0.LwD65d5h6U_2Xco81EClMa_1WIW4xXZl8o4b7WzY_7OgPD2tNlByxvGDzP7bKYA9Gj--1mi4Q4li4CAnKJkaHRYB17baC0H5P9lKMPuA6AnChTzLafY6yf-YadA7DmakCtIl7FNcFQQL2DXmh6gS9J6TluFoCIXj83MqETbDWpL28o3XAD_05UP8VLQzH2XzyqWKi97mOuvz-GsDp9mhBYQUgN3csNXt2v2l-bUPWe19SftNej0cxddyGu06tXUtaS6K0oe0TTbaqc3hmfEiu5G0J8U6ztTUMwXkBvaknE640NPgMQJqBaey0E4u0txYgyvMvvxfwtcOrDRYqYPBnA"

    // Parse the JWT.
    token, err := jwt.Parse(jwtB64, jwks.Keyfunc)
    if err != nil {
        log.Fatalf("Failed to parse the JWT.\nError: %s", err.Error())
    }

    // Check if the token is valid.
    if !token.Valid {
        log.Fatalf("The token is not valid.")
    }
    log.Println("The token is valid.")

    // End the background refresh goroutine when it's no longer needed.
    cancel()

    // This will be ineffectual because the line above this canceled the parent context.Context.
    // This method call is idempotent similar to context.CancelFunc.
    jwks.EndBackground()
}

From JWKS as JSON

package main

import (
    "encoding/json"
    "log"

    "github.com/golang-jwt/jwt/v4"

    "github.com/MicahParks/keyfunc"
)

func main() {
    // Get the JWKS as JSON.
    jwksJSON := json.RawMessage(`{"keys":[{"kty":"RSA","e":"AQAB","kid":"ee8d626d","n":"gRda5b0pkgTytDuLrRnNSYhvfMIyM0ASq2ZggY4dVe12JV8N7lyXilyqLKleD-2lziivvzE8O8CdIC2vUf0tBD7VuMyldnZruSEZWCuKJPdgKgy9yPpShmD2NyhbwQIAbievGMJIp_JMwz8MkdY5pzhPECGNgCEtUAmsrrctP5V8HuxaxGt9bb-DdPXkYWXW3MPMSlVpGZ5GiIeTABxqYNG2MSoYeQ9x8O3y488jbassTqxExI_4w9MBQBJR9HIXjWrrrenCcDlMY71rzkbdj3mmcn9xMq2vB5OhfHyHTihbUPLSm83aFWSuW9lE7ogMc93XnrB8evIAk6VfsYlS9Q"},{"kty":"EC","crv":"P-256","kid":"711d48d1","x":"tfXCoBU-wXemeQCkME1gMZWK0-UECCHIkedASZR0t-Q","y":"9xzYtnKQdiQJHCtGwpZWF21eP1fy5x4wC822rCilmBw"},{"kty":"EC","crv":"P-384","kid":"d52c9829","x":"tFx6ev6eLs9sNfdyndn4OgbhV6gPFVn7Ul0VD5vwuplJLbIYeFLI6T42tTaE5_Q4","y":"A0gzB8TqxPX7xMzyHH_FXkYG2iROANH_kQxBovSeus6l_QSyqYlipWpBy9BhY9dz"},{"kty":"RSA","e":"AQAB","kid":"ecac72e5","n":"nLbnTvZAUxdmuAbDDUNAfha6mw0fri3UpV2w1PxilflBuSnXJhzo532-YQITogoanMjy_sQ8kHUhZYHVRR6vLZRBBbl-hP8XWiCe4wwioy7Ey3TiIUYfW-SD6I42XbLt5o-47IR0j5YDXxnX2UU7-UgR_kITBeLDfk0rSp4B0GUhPbP5IDItS0MHHDDS3lhvJomxgEfoNrp0K0Fz_s0K33hfOqc2hD1tSkX-3oDTQVRMF4Nxax3NNw8-ahw6HNMlXlwWfXodgRMvj9pcz8xUYa3C5IlPlZkMumeNCFx1qds6K_eYcU0ss91DdbhhE8amRX1FsnBJNMRUkA5i45xkOIx15rQN230zzh0p71jvtx7wYRr5pdMlwxV0T9Ck5PCmx-GzFazA2X6DJ0Xnn1-cXkRoZHFj_8Mba1dUrNz-NWEk83uW5KT-ZEbX7nzGXtayKWmGb873a8aYPqIsp6bQ_-eRBd8TDT2g9HuPyPr5VKa1p33xKaohz4DGy3t1Qpy3UWnbPXUlh5dLWPKz-TcS9FP5gFhWVo-ZhU03Pn6P34OxHmXGWyQao18dQGqzgD4e9vY3rLhfcjVZJYNlWY2InsNwbYS-DnienPf1ws-miLeXxNKG3tFydoQzHwyOxG6Wc-HBfzL_hOvxINKQamvPasaYWl1LWznMps6elKCgKDc"},{"kty":"EC","crv":"P-521","kid":"c570888f","x":"AHNpXq0J7rikNRlwhaMYDD8LGVAVJzNJ-jEPksUIn2LB2LCdNRzfAhgbxdQcWT9ktlc9M1EhmTLccEqfnWdGL9G1","y":"AfHPUW3GYzzqbTczcYR0nYMVMFVrYsUxv4uiuSNV_XRN3Jf8zeYbbOLJv4S3bUytO7qHY8bfZxPxR9nn3BBTf5ol"}]}`)

    // Create the JWKS from the resource at the given URL.
    jwks, err := keyfunc.NewJSON(jwksJSON)
    if err != nil {
        log.Fatalf("Failed to create JWKS from JSON.\nError: %s", err.Error())
    }

    // Get a JWT to parse.
    jwtB64 := "eyJraWQiOiJlZThkNjI2ZCIsInR5cCI6IkpXVCIsImFsZyI6IlJTMjU2In0.eyJzdWIiOiJXZWlkb25nIiwiYXVkIjoiVGFzaHVhbiIsImlzcyI6Imp3a3Mtc2VydmljZS5hcHBzcG90LmNvbSIsImlhdCI6MTYzMTM2OTk1NSwianRpIjoiNDY2M2E5MTAtZWU2MC00NzcwLTgxNjktY2I3NDdiMDljZjU0In0.LwD65d5h6U_2Xco81EClMa_1WIW4xXZl8o4b7WzY_7OgPD2tNlByxvGDzP7bKYA9Gj--1mi4Q4li4CAnKJkaHRYB17baC0H5P9lKMPuA6AnChTzLafY6yf-YadA7DmakCtIl7FNcFQQL2DXmh6gS9J6TluFoCIXj83MqETbDWpL28o3XAD_05UP8VLQzH2XzyqWKi97mOuvz-GsDp9mhBYQUgN3csNXt2v2l-bUPWe19SftNej0cxddyGu06tXUtaS6K0oe0TTbaqc3hmfEiu5G0J8U6ztTUMwXkBvaknE640NPgMQJqBaey0E4u0txYgyvMvvxfwtcOrDRYqYPBnA"

    // Parse the JWT.
    token, err := jwt.Parse(jwtB64, jwks.Keyfunc)
    if err != nil {
        log.Fatalf("Failed to parse the JWT.\nError: %s", err.Error())
    }

    // Check if the token is valid.
    if !token.Valid {
        log.Fatalf("The token is not valid.")
    }
    log.Println("The token is valid.")
}
6

In 2023 you can use github.com/lestrrat-go/jwx/v2 with jwk.NewCachedSet to directly parse and validate a JWT string with a automatic downloading, caching and provisioning of JWK keys.

To create a key set from an available URL (code adapted from the library's code examples):

func NewJWKSet(jwkUrl string) jwk.Set {
    jwkCache := jwk.NewCache(context.Background())

    // register a minimum refresh interval for this URL. 
    // when not specified, defaults to Cache-Control and similar resp headers
    err := jwkCache.Register(jwkUrl, jwk.WithMinRefreshInterval(10*time.Minute))
    if err != nil {
        panic("failed to register jwk location")
    }
    
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()

    // fetch once on application startup
    _, err = jwkCache.Refresh(ctx, jwkUrl)
    if err != nil {
        panic("failed to fetch on startup")
    }
    // create the cached key set
    return jwk.NewCachedSet(jwkCache, jwkUrl)
}

Once you have a value of type jwk.Set, you can use it directly in the jwt package's parse functions with the option jwt.WithKeySet:

    keySet := NewJWKSet("https://www.googleapis.com/oauth2/v3/certs")
    _, err := jwt.ParseString(base64Jwt, jwt.WithKeySet(keySet))

BUT this wil fail if the JWK response does not contain the alg field. In that case you can validate by inferring the algorithm from the key. Thanks to mehran75 for the tip:

_, err = jws.Verify(
   []byte(base64Jwt), 
   jws.WithKeySet(keySet, jws.WithInferAlgorithmFromKey(true)),
) 
2
  • 1
    You seem to consider using github.com/lestrrat-go/jwx/v2 an easier or more modern approach than github.com/MicahParks/keyfunc. Can you elaborate/compare? Commented Aug 8, 2023 at 10:05
  • 1
    @OrestisKapar I don't exactly remember, because I wrote this code a while ago, but IIRC MicahParks/keyfunc doesn't have auto-refresh of the JWK. In case of rotating keys, you'd have to write the refresh yourself. lestrrat-go/jwx instead has auto-refresh.
    – blackgreen
    Commented Aug 8, 2023 at 11:06
0

As you might have already noticed, github.com/lestrrat-go/jwx only supports RawURLEncode base64 format. The best solution would be if you could use tokens in that particular format.

As a hacky workaround, you could manually re-encode your token to RawURLEncode format and feed that token to the jwt.ParseString() function:

// token consists of three parts (header, payload, signature) separeted by '.'
stdEncodedParts := strings.Split(tokenStr, ".")
var rawURLEncodedParts []string
for _, part := range stdEncodedParts {
    rawpart, err := base64.StdEncoding.DecodeString(part)
    if err != nil {
        panic(err)
    }
    rawURLEncodedParts = append(rawURLEncodedParts, base64.RawURLEncoding.EncodeToString(rawpart))
}

rawURLEncodedToken := strings.Join(rawURLEncodedParts, ".")
token, err := jwt.ParseString(rawURLEncodedToken)

As for the validation part, it would look something like the code below, although I did not manage to verify your token. Are you sure, you provided the proper verification key? Also note, that you might have to feed a custom fabricated token to jws.VerifyWithJWKSet(), because

  1. it cannot parse the signature in the original StdEncoded format

  2. but at the same time, the signature should have been made on the first half of the original, StdEncoded string thus you cannot provide the re-encoded string as it is.

const jwksStr = `{"keys":[{"kty":"RSA","e":"AQAB","use":"sig","kid":"MjhhMDk2N2M2NGEwMzgzYjk2OTI3YzdmMGVhOGYxNjI2OTc5Y2Y2MQ","alg":"RS256","n":"zZU9xSgK77PbtkjJgD2Vmmv6_QNe8B54eyOV0k5K2UwuSnhv9RyRA3aL7gDN-qkANemHw3H_4Tc5SKIMltVIYdWlOMW_2m3gDBOODjc1bE-WXEWX6nQkLAOkoFrGW3bgW8TFxfuwgZVTlb6cYkSyiwc5ueFV2xNqo96Qf7nm5E7KZ2QDTkSlNMdW-jIVHMKjuEsy_gtYMaEYrwk5N7VoiYwePaF3I0_g4G2tIrKTLb8DvHApsN1h-s7jMCQFBrY4vCf3RBlYULr4Nz7u8G2NL_L9vURSCU2V2A8rYRkoZoZwk3a3AyJiqeC4T_1rmb8XdrgeFHB5bzXZ7EI0TObhlw"}]}`
customTokenStr := stdEncodedParts[0] + "." + stdEncodedParts[1] + "." + rawURLEncodedParts[2]
jwks, err := jwk.ParseString(jwksStr)
if err != nil {
    panic(err)
}
_, err = jws.VerifyWithJWKSet([]byte(customTokenStr), jwks, nil)
3
  • and i double checked the jwks is the right one, is it possible i miss some kind of cert? (.PEM, etc..)
    – Matankila
    Commented May 17, 2020 at 14:33
  • It seems to me that your jwks is parsed correctly and it ends up with a valid RSA public key. Are you sure, the tokenStr you provided is signed by the private key part of your jwk?
    – aliras
    Commented May 17, 2020 at 14:59
  • hi, thanks alot, my problem was that jwks was not the right jwks!!!!!!!!!
    – Matankila
    Commented May 24, 2020 at 7:16

Not the answer you're looking for? Browse other questions tagged or ask your own question.