authMiddleware validates OAuth2 bearer tokens for MCP requests. On 401, it emits an RFC 9728 / MCP-authorization-spec compliant WWW-Authenticate header pointing at the protected-resource-metadata URL. MCP clients (Claude Code, Cursor, etc.) use this header to bootstrap the OAuth flow without out-of
(next echo.HandlerFunc)
| 91 | // MCP clients (Claude Code, Cursor, etc.) use this header to bootstrap the |
| 92 | // OAuth flow without out-of-band configuration. |
| 93 | func (s *Server) authMiddleware(next echo.HandlerFunc) echo.HandlerFunc { |
| 94 | return func(c *echo.Context) error { |
| 95 | // Extract Authorization header |
| 96 | authHeader := c.Request().Header.Get("Authorization") |
| 97 | if authHeader == "" { |
| 98 | return s.unauthorized(c, "authorization required") |
| 99 | } |
| 100 | |
| 101 | // Validate Bearer format |
| 102 | parts := strings.Fields(authHeader) |
| 103 | if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" { |
| 104 | return s.unauthorized(c, "authorization header format must be Bearer {token}") |
| 105 | } |
| 106 | tokenStr := parts[1] |
| 107 | |
| 108 | // Parse and validate JWT |
| 109 | claims := jwt.MapClaims{} |
| 110 | token, err := jwt.ParseWithClaims(tokenStr, claims, func(t *jwt.Token) (any, error) { |
| 111 | if t.Method.Alg() != jwt.SigningMethodHS256.Name { |
| 112 | return nil, errors.New("invalid token signing method") |
| 113 | } |
| 114 | if kid, ok := t.Header["kid"].(string); ok && kid == "v1" { |
| 115 | return []byte(s.secret), nil |
| 116 | } |
| 117 | return nil, errors.New("invalid token key id") |
| 118 | }) |
| 119 | if err != nil { |
| 120 | if strings.Contains(err.Error(), "expired") { |
| 121 | return s.unauthorized(c, "token expired") |
| 122 | } |
| 123 | return s.unauthorized(c, "invalid token") |
| 124 | } |
| 125 | |
| 126 | if !token.Valid { |
| 127 | return s.unauthorized(c, "invalid token") |
| 128 | } |
| 129 | |
| 130 | // Validate audience - accept both user access tokens and OAuth2 access tokens |
| 131 | aud, ok := claims["aud"] |
| 132 | if !ok { |
| 133 | return s.unauthorized(c, "invalid token: missing audience") |
| 134 | } |
| 135 | |
| 136 | validAudience := false |
| 137 | switch v := aud.(type) { |
| 138 | case string: |
| 139 | validAudience = v == auth.OAuth2AccessTokenAudience || v == auth.AccessTokenAudience |
| 140 | case []any: |
| 141 | for _, a := range v { |
| 142 | if str, ok := a.(string); ok && (str == auth.OAuth2AccessTokenAudience || str == auth.AccessTokenAudience) { |
| 143 | validAudience = true |
| 144 | break |
| 145 | } |
| 146 | } |
| 147 | default: |
| 148 | } |
| 149 | if !validAudience { |
| 150 | return s.unauthorized(c, "invalid token: audience mismatch") |