From 32d9b541df8518cade2bf4bbc9cf5a177248cbb2 Mon Sep 17 00:00:00 2001 From: Giannis Katsanos Date: Mon, 26 Feb 2024 16:23:39 +0200 Subject: [PATCH] feat: jwt.Verify fetches the JSON web key In order to reduce friction when manually verifying a session JWT, the jwt.Verify function will fetch the JSON web key if it's not passed in the params. Users can specify the jwks.Client that will be used to invoke the API GET /v1/jwks method. Exported the method which fetches the JSON web key, since it can be re-used by the middleware. It can also be used for consumers who wish to fetch the JSON web key once and cache it. Caching is recommended. --- UPGRADING.md | 12 +++++++- http/middleware.go | 30 +++--------------- jwt/jwt.go | 72 +++++++++++++++++++++++++++++++++++++++---- v2_migration_guide.md | 28 ++++++++++++++++- 4 files changed, 108 insertions(+), 34 deletions(-) diff --git a/UPGRADING.md b/UPGRADING.md index b74e6d0a..576508a8 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -219,5 +219,15 @@ The `clerk.VerifyToken` method in version `v1` of the Clerk Go SDK has been rena The method accepts the same parameters, with two important differences. -- The JSON web key with which the token will be verified is a required parameter. +- You can provide the JSON web key with which the token will be verified. +- If you don't provide the JSON web key, you can provide a jwks.Client that will be used to retrieve it. If you don't provide a jwks.Client, one with default configuration will be used. - The method will not cache the JSON web key. + +```go +sessionToken := "the-clerk-session-jwt" +- client := clerk.NewClient("sk_live_XXXX") +- claims, err := client.VerifyToken(sessionToken) ++ claims, err := jwt.Verify(context.Background(), &jwt.VerifyParams{ ++ Token: sessionToken, ++ }) +``` diff --git a/http/middleware.go b/http/middleware.go index 12b3dd5f..13b58e8f 100644 --- a/http/middleware.go +++ b/http/middleware.go @@ -95,7 +95,10 @@ func getJWK(ctx context.Context, jwksClient *jwks.Client, kid string, clock cler jwk := getCache().Get(kid) if jwk == nil || !getCache().IsValid(kid, clock.Now().UTC()) { var err error - jwk, err = forceGetJWK(ctx, jwksClient, kid) + jwk, err = jwt.GetJSONWebKey(ctx, &jwt.GetJSONWebKeyParams{ + KeyID: kid, + JWKSClient: jwksClient, + }) if err != nil { return nil, err } @@ -104,31 +107,6 @@ func getJWK(ctx context.Context, jwksClient *jwks.Client, kid string, clock cler return jwk, nil } -// Fetch the JSON Web Key Set from the Clerk API and return the JSON -// Web Key corresponding to the provided KeyID. -// A default client will be initialized if the provided jwks.Client -// is nil. -func forceGetJWK(ctx context.Context, jwksClient *jwks.Client, kid string) (*clerk.JSONWebKey, error) { - if jwksClient == nil { - jwksClient = &jwks.Client{ - Backend: clerk.GetBackend(), - } - } - jwks, err := jwksClient.Get(ctx, &jwks.GetParams{}) - if err != nil { - return nil, err - } - if jwks == nil { - return nil, fmt.Errorf("no jwks found") - } - for _, k := range jwks.Keys { - if k != nil && k.KeyID == kid { - return k, nil - } - } - return nil, fmt.Errorf("no jwk key found for kid %s", kid) -} - type AuthorizationParams struct { jwt.VerifyParams // JWKSClient is the jwks.Client that will be used to fetch the diff --git a/jwt/jwt.go b/jwt/jwt.go index 4819995f..fd5aab0f 100644 --- a/jwt/jwt.go +++ b/jwt/jwt.go @@ -9,6 +9,7 @@ import ( "time" "github.com/clerk/clerk-sdk-go/v2" + "github.com/clerk/clerk-sdk-go/v2/jwks" "github.com/go-jose/go-jose/v3/jwt" ) @@ -23,9 +24,18 @@ type CustomClaimsConstructor func(context.Context) any type VerifyParams struct { // Token is the JWT that will be verified. Required. Token string - // JWK the custom JSON Web Key that will be used to verify the - // Token with. Required. + // JWK is the custom JSON Web Key that will be used to verify the + // Token with. + // If the JWK parameter is provided, the Verify method won't + // fetch the JSON Web Key Set and there's no need to provide + // the JWKSClient parameter. JWK *clerk.JSONWebKey + // JWKSClient is a jwks API client that will be used to fetch the + // JSON Web Key Set for verifying the Token with. + // If the JWK parameter is provided, the JWKSClient is not needed. + // If no JWK or JWKSClient is provided, the Verify method will use + // a JWKSClient with the default Backend. + JWKSClient *jwks.Client // Clock can be used to keep track of time and will replace usage of // the [time] package. Pass a custom Clock to control the source of // time or facilitate testing chronologically sensitive flows. @@ -57,15 +67,27 @@ type VerifyParams struct { // Verify verifies a Clerk session JWT and returns the parsed // clerk.SessionClaims. func Verify(ctx context.Context, params *VerifyParams) (*clerk.SessionClaims, error) { + parsedToken, err := jwt.ParseSigned(params.Token) + if err != nil { + return nil, err + } + if len(parsedToken.Headers) == 0 { + return nil, fmt.Errorf("missing JWT headers") + } jwk := params.JWK + if jwk == nil { + jwk, err = GetJSONWebKey(ctx, &GetJSONWebKeyParams{ + KeyID: parsedToken.Headers[0].KeyID, + JWKSClient: params.JWKSClient, + }) + if err != nil { + return nil, err + } + } if jwk == nil { return nil, fmt.Errorf("missing json web key, need to set JWK in the params") } - parsedToken, err := jwt.ParseSigned(params.Token) - if err != nil { - return nil, err - } if parsedToken.Headers[0].Algorithm != jwk.Algorithm { return nil, fmt.Errorf("invalid signing algorithm %s", jwk.Algorithm) } @@ -145,3 +167,41 @@ func Decode(_ context.Context, params *DecodeParams) (*clerk.UnverifiedToken, er } return claims, nil } + +type GetJSONWebKeyParams struct { + // KeyID is the token's 'kid' claim. + KeyID string + // JWKSClient can be used to call the jwks Get Clerk API operation. + JWKSClient *jwks.Client +} + +// GetJSONWebKey fetches the JSON Web Key Set from the Clerk API +// and returns the JSON Web Key corresponding to the provided KeyID. +// A default client will be initialized if the provided JWKSClient +// is nil. +func GetJSONWebKey(ctx context.Context, params *GetJSONWebKeyParams) (*clerk.JSONWebKey, error) { + if params.KeyID == "" { + return nil, fmt.Errorf("missing jwt kid header claim") + } + + jwksClient := params.JWKSClient + if jwksClient == nil { + jwksClient = &jwks.Client{ + Backend: clerk.GetBackend(), + } + } + jwks, err := jwksClient.Get(ctx, &jwks.GetParams{}) + if err != nil { + return nil, err + } + if jwks == nil { + return nil, fmt.Errorf("no jwks found") + } + + for _, k := range jwks.Keys { + if k != nil && k.KeyID == params.KeyID { + return k, nil + } + } + return nil, fmt.Errorf("missing json web key") +} diff --git a/v2_migration_guide.md b/v2_migration_guide.md index 4a51e3df..d713f6e8 100644 --- a/v2_migration_guide.md +++ b/v2_migration_guide.md @@ -340,12 +340,38 @@ The method accepts the same parameters, with two important differences. In the `v1` version, the `clerk.VerifyToken` method would trigger an HTTP request to the Clerk Backend API to fetch the JSON web key and would cache its value for one hour. -The new `jwt.Verify` method that is included in `v2` accepts the JSON web key as a required parameter. It is up +The new `jwt.Verify` method that is included in `v2` accepts the JSON web key that is used to verify the token. It is up to the caller to get access to the key and pass it to `jwt.Verify`. Please note that both HTTP middleware functions, `WithHeaderAuthorization` and `RequireHeaderAuthorization` will cache the JSON web key by default. +You can fetch the JSON web key with the `jwt.GetJSONWebKey` method. + +```go +ctx := context.Background() +token := "the-clerk-session-jwt" +decoded, err := jwt.Decode(ctx, &jwt.DecodeParams{Token: token}) +if err != nil { + panic(err) +} + +// Fetch the JSON web key for your instance. +// It is advised to cache the JSON web key until your instance secret +// key changes. +jwk, err := jwt.GetJSONWebKey(ctx, &jwt.GetJSONWebKeyParams{ + KeyID: decoded.KeyID, +}) +if err != nil { + panic(err) +} +claims, err := jwt.Verify(ctx, &jwt.VerifyParams{ + Token: token, + JWK: jwk, +}) +``` + +If you don't have access to the JSON web key, you can ## Feedback and omissions Please let us know about your experience upgrading to the `v2` version of the Clerk Go SDK.