Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

VerifyToken and getJWK without middleware in v2 #277

Open
sonatard opened this issue Mar 26, 2024 · 5 comments
Open

VerifyToken and getJWK without middleware in v2 #277

sonatard opened this issue Mar 26, 2024 · 5 comments

Comments

@sonatard
Copy link

sonatard commented Mar 26, 2024

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.

func WithHeaderAuthorization(opts ...AuthorizationOption) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
params := &AuthorizationParams{}
for _, opt := range opts {
err := opt(params)
if err != nil {
w.WriteHeader(http.StatusUnauthorized)
return
}
}
if params.Clock == nil {
params.Clock = clerk.NewClock()
}
authorization := strings.TrimSpace(r.Header.Get("Authorization"))
if authorization == "" {
next.ServeHTTP(w, r)
return
}
token := strings.TrimPrefix(authorization, "Bearer ")
decoded, err := jwt.Decode(r.Context(), &jwt.DecodeParams{Token: token})
if err != nil {
next.ServeHTTP(w, r)
return
}
if params.JWK == nil {
params.JWK, err = getJWK(r.Context(), params.JWKSClient, decoded.KeyID, params.Clock)
if err != nil {
w.WriteHeader(http.StatusUnauthorized)
return
}
}
params.Token = token
claims, err := jwt.Verify(r.Context(), &params.VerifyParams)
if err != nil {
w.WriteHeader(http.StatusUnauthorized)
return
}
// Token was verified. Add the session claims to the request context.
newCtx := clerk.ContextWithSessionClaims(r.Context(), claims)
next.ServeHTTP(w, r.WithContext(newCtx))
})
}
}

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
Copy link
Member

gkats commented Mar 27, 2024

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
Copy link
Author

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
Copy link
Author

sonatard commented Mar 28, 2024

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
Copy link
Member

gkats commented Mar 28, 2024

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
Copy link
Author

sonatard commented Mar 29, 2024

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants