Skip to content

Commit

Permalink
feat: jwt.Verify fetches the JSON web key
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
gkats committed Feb 26, 2024
1 parent c1f5081 commit 32d9b54
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 34 deletions.
12 changes: 11 additions & 1 deletion UPGRADING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
+ })
```
30 changes: 4 additions & 26 deletions http/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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
Expand Down
72 changes: 66 additions & 6 deletions jwt/jwt.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand All @@ -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.
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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")
}
28 changes: 27 additions & 1 deletion v2_migration_guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down

0 comments on commit 32d9b54

Please sign in to comment.