add HTTP Request to TokenVerifier (#404)

The request might be needed to verify the token.

Fixes #403.
This commit is contained in:
Jonathan Amsterdam 2025-09-03 16:23:42 -04:00 • committed by GitHub
commit d0c5943f25
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 16 additions and 13 deletions

View file

@ -29,7 +29,8 @@ var ErrOAuth = errors.New("oauth error")
// A TokenVerifier checks the validity of a bearer token, and extracts information
// from it. If verification fails, it should return an error that unwraps to ErrInvalidToken.
type TokenVerifier func(ctx context.Context, token string) (*TokenInfo, error)
// The HTTP request is provided in case verifying the token involves checking it.
type TokenVerifier func(ctx context.Context, token string, req *http.Request) (*TokenInfo, error)
// RequireBearerTokenOptions are options for [RequireBearerToken].
type RequireBearerTokenOptions struct {
@ -55,6 +56,8 @@ func TokenInfoFromContext(ctx context.Context) *TokenInfo {
// If verification succeeds, the [TokenInfo] is added to the request's context and the request proceeds.
// If verification fails, the request fails with a 401 Unauthenticated, and the WWW-Authenticate header
// is populated to enable [protected resource metadata].
//
//
// [protected resource metadata]: https://datatracker.ietf.org/doc/rfc9728
func RequireBearerToken(verifier TokenVerifier, opts *RequireBearerTokenOptions) func(http.Handler) http.Handler {
@ -62,7 +65,7 @@ func RequireBearerToken(verifier TokenVerifier, opts *RequireBearerTokenOptions)
return func(handler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tokenInfo, errmsg, code := verify(r.Context(), verifier, opts, r.Header.Get("Authorization"))
tokenInfo, errmsg, code := verify(r, verifier, opts)
if code != 0 {
if code == http.StatusUnauthorized || code == http.StatusForbidden {
if opts != nil && opts.ResourceMetadataURL != "" {
@ -78,15 +81,16 @@ func RequireBearerToken(verifier TokenVerifier, opts *RequireBearerTokenOptions)
}
}
func verify(ctx context.Context, verifier TokenVerifier, opts *RequireBearerTokenOptions, authHeader string) (_ *TokenInfo, errmsg string, code int) {
func verify(req *http.Request, verifier TokenVerifier, opts *RequireBearerTokenOptions) (_ *TokenInfo, errmsg string, code int) {
// Extract bearer token.
authHeader := req.Header.Get("Authorization")
fields := strings.Fields(authHeader)
if len(fields) != 2 || strings.ToLower(fields[0]) != "bearer" {
return nil, "no bearer token", http.StatusUnauthorized
}
// Verify the token and get information from it.
tokenInfo, err := verifier(ctx, fields[1])
tokenInfo, err := verifier(req.Context(), fields[1], req)
if err != nil {
if errors.Is(err, ErrInvalidToken) {
return nil, err.Error(), http.StatusUnauthorized
@ -94,8 +98,6 @@ func verify(ctx context.Context, verifier TokenVerifier, opts *RequireBearerToke
if errors.Is(err, ErrOAuth) {
return nil, err.Error(), http.StatusBadRequest
}
// Investigate how that works.
// See typescript-sdk/src/server/auth/middleware/bearerAuth.ts.
return nil, err.Error(), http.StatusInternalServerError
}

View file

@ -7,13 +7,13 @@ package auth
import (
"context"
"errors"
"net/http"
"testing"
"time"
)
func TestVerify(t *testing.T) {
ctx := context.Background()
verifier := func(_ context.Context, token string) (*TokenInfo, error) {
verifier := func(_ context.Context, token string, _ *http.Request) (*TokenInfo, error) {
switch token {
case "valid":
return &TokenInfo{Expiration: time.Now().Add(time.Hour)}, nil
@ -67,7 +67,9 @@ func TestVerify(t *testing.T) {
},
} {
t.Run(tt.name, func(t *testing.T) {
_, gotMsg, gotCode := verify(ctx, verifier, tt.opts, tt.header)
_, gotMsg, gotCode := verify(&http.Request{
Header: http.Header{"Authorization": {tt.header}},
}, verifier, tt.opts)
if gotMsg != tt.wantMsg || gotCode != tt.wantCode {
t.Errorf("got (%q, %d), want (%q, %d)", gotMsg, gotCode, tt.wantMsg, tt.wantCode)
}

View file

@ -83,7 +83,7 @@ func generateToken(userID string, scopes []string, expiresIn time.Duration) (str
// verifyJWT verifies JWT tokens and returns TokenInfo for the auth middleware.
// This function implements the TokenVerifier interface required by auth.RequireBearerToken.
func verifyJWT(ctx context.Context, tokenString string) (*auth.TokenInfo, error) {
func verifyJWT(ctx context.Context, tokenString string, _ *http.Request) (*auth.TokenInfo, error) {
// Parse and validate the JWT token.
token, err := jwt.ParseWithClaims(tokenString, &JWTClaims{}, func(token *jwt.Token) (any, error) {
// Verify the signing method is HMAC.
@ -92,7 +92,6 @@ func verifyJWT(ctx context.Context, tokenString string) (*auth.TokenInfo, error)
}
return jwtSecret, nil
})
if err != nil {
// Return standard error for invalid tokens.
return nil, fmt.Errorf("%w: %v", auth.ErrInvalidToken, err)
@ -111,7 +110,7 @@ func verifyJWT(ctx context.Context, tokenString string) (*auth.TokenInfo, error)
// verifyAPIKey verifies API keys and returns TokenInfo for the auth middleware.
// This function implements the TokenVerifier interface required by auth.RequireBearerToken.
func verifyAPIKey(ctx context.Context, apiKey string) (*auth.TokenInfo, error) {
func verifyAPIKey(ctx context.Context, apiKey string, _ *http.Request) (*auth.TokenInfo, error) {
// Look up the API key in our storage.
key, exists := apiKeys[apiKey]
if !exists {

View file

@ -1270,7 +1270,7 @@ func TestTokenInfo(t *testing.T) {
AddTool(server, &Tool{Name: "tokenInfo", Description: "return token info"}, tokenInfo)
streamHandler := NewStreamableHTTPHandler(func(req *http.Request) *Server { return server }, nil)
verifier := func(context.Context, string) (*auth.TokenInfo, error) {
verifier := func(context.Context, string, *http.Request) (*auth.TokenInfo, error) {
return &auth.TokenInfo{
Scopes: []string{"scope"},
// Expiration is far, far in the future.