clerk / clerk-sdk-go

Access the Clerk Backend API from Go
MIT License
79 stars 19 forks source link

VerifyToken and getJWK without middleware in v2 #277

Open sonatard opened 5 months ago

sonatard commented 5 months ago

I am currently working on migrating to v2.

But we cannot use this middleware directly as we want to implement other things inside the Middleware. https://github.com/clerk/clerk-sdk-go/blob/39301db1dd08e6196e35666ed8f9cb338befb92a/http/middleware.go#L38-L84

However, we want to implement jwt.VerifyToken and getJWK. Since getJWK is not public, we cannot use it as is. We can achieve the same thing by copying the implementation of getJWK, but how does the Clerk team recommend implementing it in such cases?

gkats commented 5 months ago

Hi @sonatard, happy to assist but I'm not sure I fully understand what you're trying to achieve. Sorry for that!

The WithWithHeaderAuthorization middleware currently has a specific function. It gets the bearer token from the "Authorization" header and verifies that it's a valid Clerk issued token with a valid session.

In order to verify the token's validity, it fetches the JSON Web key using the Clerk Backend API.

If you already have the JSON Web Key, you can pass it as an option when using the middleware.

WithHeaderAuthorization(JSONWebKey(theKey))

If you want to control the JSON web key fetching with a configurable client, you can pass a jwks.Client as an option

WithHeaderAuthorization(JWKSClient(theClient))

Both options above are also available as jwt.VerifyParams if you need to call jwt.Verify directly.

In order to retrieve the JSON Web Key that you need, you need first retrieve the JSON Web Key Set for your instance and then filter the results to get the key you need.

You can use the JWKS Clerk Backend API operation with the jwks package to fetch the JSON Web Key Set.

If you don't mind me asking, what's your use-case for needing to re-implement the getJWK function?

sonatard commented 5 months ago

The reason I want to create my own middleware is because I want to perform different authentication based on headers within a single middleware.

func (a *AuthenticationMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()
    switch r.Header.Get(httputil.HeaderClient) {
    case httputil.HeaderClientAdmin:
        // Clerk Auth
    case httputil.HeaderClientOther:
        // Other auth
    default:
        panic("unreachable")
    }
}
sonatard commented 5 months ago

You can use the JWKS Clerk Backend API operation with the jwks package to fetch the JSON Web Key Set.

The current jwks package does not expose caching functionality. I need to implement my own caching mechanism. That means I have to create my own getJWK function.

Or if a function like this was exposed, I could implement the middleware myself.

func Verify(ctx context.Context, token string, opts ...clerkhttp.AuthorizationOption) (*clerk.SessionClaims, error) {
    decoded, err := jwt.Decode(ctx, &jwt.DecodeParams{Token: token})
    if err != nil {
        return nil, err
    }

    params := &clerkhttp.AuthorizationParams{}
    for _, opt := range opts {
        err := opt(params)
        if err != nil {
            return nil, err
        }
    }
    if params.Clock == nil {
        params.Clock = clerk.NewClock()
    }
    if params.JWK == nil {
        params.JWK, err = getJWK(ctx, params.JWKSClient, decoded.KeyID, params.Clock)
        if err != nil {
            return nil, err
        }
    }
    params.Token = token
    claims, err := jwt.Verify(ctx, &params.VerifyParams)
    if err != nil {
        return nil, err
    }
    return claims, nil
}
func WithHeaderAuthorization(opts ...clerkhttp.AuthorizationOption) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            ctx := r.Context()
            switch r.Header.Get(httputil.HeaderClient) {
            case httputil.HeaderClientAdmin:
                authorization := strings.TrimSpace(r.Header.Get("Authorization"))
                if authorization == "" {
                    next.ServeHTTP(w, r)
                    return
                }
                token := strings.TrimPrefix(authorization, "Bearer ")
                claims, err := verify(ctx, token, opts...)
                if err != nil {
                    w.WriteHeader(http.StatusUnauthorized)
                    return
                }
                newCtx := clerk.ContextWithSessionClaims(ctx, claims)
                next.ServeHTTP(w, r.WithContext(newCtx))
            case httputil.HeaderClientOther:
                // Other auth
            }
        })
    }
}

I would be happy if functions focused more on pure functionality were provided rather than functions tied to middleware.

gkats commented 5 months ago

Hi @sonatard thanks for the detailed explanation! It helps to see what the use case is and how we can better support it.

The current jwks package does not expose caching functionality.

This was an intentional design choice. No other endpoint supports caching. Users are free to add a caching layer of their own if they choose to.

because I want to perform different authentication based on headers within a single middleware.

I guess the problem is that the WithHeaderAuthorization middleware will stop the middleware chain and respond with 401 Unauthorized.

With the current state of things would something like the following solve your problem?

WithHeaderAuthorization(WithCustomAuthorization(handler))

func WithCustomAuthorization() {
  switch r.Header.Get(httputil.HeaderClient) {
   case httputil.HeaderClientOther:
     // handle this case first
   // ...  
   // add other known cases here
   // ...
   default:
     // Let the next middleware take over. 
     // Next middleware will be the Clerk middleware which will check the 
     // httputil.HeaderClientAdmin case
     next.ServeHTTP(w, r)
  }
}

If I'm not mistaken, it looks like you could also call the Clerk middleware from inside your custom auth middleware depending on the case, once you read the httputil.HeaderClient header.

Alternatively, we could potentially add an option to bypass the default middleware behavior which responds with 401 Unauthorized and let the consumer declare the function to run upon failed authentication.

WithHeaderAuthorization(OnFailure(customFailureFn))(handler)

func customFailureFn() {
  // you can declare custom functionality that's 
  // going to be executed whenever Clerk authentication 
  // fails.
}

Would that work?

sonatard commented 5 months ago

Yes, I think it is possible to implement it. However, I don't want the middleware layers to increase. When the official Clerk v2 SDK is released, I plan to implement Verify using the jwx's jwk package and jwt package.The jwx's jwk package has a cache layer. And then I will compare it with the method you suggested.