From d54572a7d1d0b837232a1e04e5e8274cf4a29d7d Mon Sep 17 00:00:00 2001 From: Max Date: Tue, 22 Jul 2025 15:36:41 +0800 Subject: [PATCH] Refactor OAuth response handling and improve content type management - Updated response methods to standardize Content-Type header to "application/json" across OAuth endpoints, enhancing compliance with JSON standards. - Refactored error and success response methods to streamline response generation without unnecessary wrappers, improving clarity and maintainability. - Enhanced security by ensuring all responses include appropriate OAuth security headers, aligning with best practices for sensitive endpoints. - Simplified test assertions for Content-Type in OAuth tests, ensuring consistency in response validation. --- openapi/oauth.go | 104 +++++++++++++--------------- openapi/oauth/guard.go | 8 +++ openapi/oauth_test.go | 9 ++- openapi/oauth_token_test.go | 50 +++----------- openapi/response.go | 133 ++++++------------------------------ 5 files changed, 91 insertions(+), 213 deletions(-) diff --git a/openapi/oauth.go b/openapi/oauth.go index fcaa6b4c..5eddfa59 100644 --- a/openapi/oauth.go +++ b/openapi/oauth.go @@ -123,7 +123,7 @@ func (openapi *OpenAPI) oauthToken(c *gin.Context) { // Validate grant type if grantType == "" { - openapi.respondWithTokenError(c, ErrInvalidRequest) + openapi.respondWithSecureError(c, StatusBadRequest, ErrInvalidRequest) return } @@ -141,7 +141,7 @@ func (openapi *OpenAPI) oauthToken(c *gin.Context) { openapi.handleTokenExchangeGrant(c) default: - openapi.respondWithTokenError(c, ErrUnsupportedGrantType) + openapi.respondWithSecureError(c, StatusBadRequest, ErrUnsupportedGrantType) } } @@ -150,20 +150,20 @@ func (openapi *OpenAPI) handleStandardTokenGrant(c *gin.Context, grantType strin // Extract client credentials from Basic Auth header or form parameters clientID, clientSecret := openapi.extractClientCredentials(c) if clientID == "" { - openapi.respondWithTokenError(c, ErrInvalidClient) + openapi.respondWithSecureError(c, StatusBadRequest, ErrInvalidClient) return } // Validate client credentials using OAuth service oauthService, ok := openapi.OAuth.(*oauth.Service) if !ok { - openapi.respondWithTokenError(c, ErrInvalidClient) + openapi.respondWithSecureError(c, StatusBadRequest, ErrInvalidClient) return } clientInfo, err := oauthService.GetClientProvider().GetClientByCredentials(c, clientID, clientSecret) if err != nil { - openapi.respondWithTokenError(c, ErrInvalidClient) + openapi.respondWithSecureError(c, StatusBadRequest, ErrInvalidClient) return } @@ -179,13 +179,13 @@ func (openapi *OpenAPI) handleStandardTokenGrant(c *gin.Context, grantType strin // Basic validation for authorization code grant if code == "" || redirectURI == "" { - openapi.respondWithTokenError(c, ErrInvalidRequest) + openapi.respondWithSecureError(c, StatusBadRequest, ErrInvalidRequest) return } // Validate that client supports authorization code grant if !openapi.clientSupportsGrantType(clientInfo, types.GrantTypeAuthorizationCode) { - openapi.respondWithTokenError(c, ErrUnauthorizedClient) + openapi.respondWithSecureError(c, StatusUnauthorized, ErrUnauthorizedClient) return } @@ -194,13 +194,13 @@ func (openapi *OpenAPI) handleStandardTokenGrant(c *gin.Context, grantType strin // Basic validation for device code grant if code == "" { - openapi.respondWithTokenError(c, ErrInvalidRequest) + openapi.respondWithSecureError(c, StatusBadRequest, ErrInvalidRequest) return } // Validate that client supports device code grant if !openapi.clientSupportsGrantType(clientInfo, types.GrantTypeDeviceCode) { - openapi.respondWithTokenError(c, ErrUnauthorizedClient) + openapi.respondWithSecureError(c, StatusUnauthorized, ErrUnauthorizedClient) return } @@ -210,7 +210,7 @@ func (openapi *OpenAPI) handleStandardTokenGrant(c *gin.Context, grantType strin // Validate that client supports client credentials grant if !openapi.clientSupportsGrantType(clientInfo, types.GrantTypeClientCredentials) { - openapi.respondWithTokenError(c, ErrUnauthorizedClient) + openapi.respondWithSecureError(c, StatusUnauthorized, ErrUnauthorizedClient) return } } @@ -218,17 +218,17 @@ func (openapi *OpenAPI) handleStandardTokenGrant(c *gin.Context, grantType strin // Call OAuth service to handle the token request token, err := openapi.OAuth.Token(c, grantType, code, clientID, codeVerifier) if err != nil { - // Convert OAuth service error to token error response + // Convert OAuth service error to token error response with security headers if oauthErr, ok := err.(*ErrorResponse); ok { - openapi.respondWithTokenError(c, oauthErr) + openapi.respondWithSecureError(c, StatusBadRequest, oauthErr) } else { - openapi.respondWithTokenError(c, ErrInvalidGrant) + openapi.respondWithSecureError(c, StatusBadRequest, ErrInvalidGrant) } return } - // Return successful token response - openapi.respondWithTokenSuccess(c, token) + // Return successful token response with OAuth security headers (RFC 6749 Section 5.1: MUST set Cache-Control: no-store) + openapi.respondWithSecureSuccess(c, StatusOK, token) } // handleRefreshTokenGrant handles refresh token requests - RFC 6749 Section 6 @@ -236,26 +236,26 @@ func (openapi *OpenAPI) handleRefreshTokenGrant(c *gin.Context) { // Extract client credentials from Basic Auth header or form parameters clientID, clientSecret := openapi.extractClientCredentials(c) if clientID == "" { - openapi.respondWithTokenError(c, ErrInvalidClient) + openapi.respondWithSecureError(c, StatusBadRequest, ErrInvalidClient) return } // Validate client credentials using OAuth service oauthService, ok := openapi.OAuth.(*oauth.Service) if !ok { - openapi.respondWithTokenError(c, ErrInvalidClient) + openapi.respondWithSecureError(c, StatusBadRequest, ErrInvalidClient) return } clientInfo, err := oauthService.GetClientProvider().GetClientByCredentials(c, clientID, clientSecret) if err != nil { - openapi.respondWithTokenError(c, ErrInvalidClient) + openapi.respondWithSecureError(c, StatusBadRequest, ErrInvalidClient) return } // Validate that client supports refresh token grant if !openapi.clientSupportsGrantType(clientInfo, types.GrantTypeRefreshToken) { - openapi.respondWithTokenError(c, ErrUnauthorizedClient) + openapi.respondWithSecureError(c, StatusUnauthorized, ErrUnauthorizedClient) return } @@ -264,7 +264,7 @@ func (openapi *OpenAPI) handleRefreshTokenGrant(c *gin.Context) { // Basic validation if refreshToken == "" { - openapi.respondWithTokenError(c, ErrInvalidRequest) + openapi.respondWithSecureError(c, StatusBadRequest, ErrInvalidRequest) return } @@ -276,17 +276,17 @@ func (openapi *OpenAPI) handleRefreshTokenGrant(c *gin.Context) { refreshResponse, err = openapi.OAuth.RefreshToken(c, refreshToken) } if err != nil { - // Convert OAuth service error to token error response + // Convert OAuth service error to token error response with security headers if oauthErr, ok := err.(*ErrorResponse); ok { - openapi.respondWithTokenError(c, oauthErr) + openapi.respondWithSecureError(c, StatusBadRequest, oauthErr) } else { - openapi.respondWithTokenError(c, ErrInvalidGrant) + openapi.respondWithSecureError(c, StatusBadRequest, ErrInvalidGrant) } return } - // Return successful refresh token response - openapi.respondWithTokenSuccess(c, refreshResponse) + // Return successful refresh token response with security headers + openapi.respondWithSecureSuccess(c, StatusOK, refreshResponse) } // handleTokenExchangeGrant handles token exchange requests - RFC 8693 @@ -298,24 +298,24 @@ func (openapi *OpenAPI) handleTokenExchangeGrant(c *gin.Context) { // Basic validation if subjectToken == "" { - openapi.respondWithTokenError(c, ErrInvalidRequest) + openapi.respondWithSecureError(c, StatusBadRequest, ErrInvalidRequest) return } // Call OAuth service to handle token exchange exchangeResponse, err := openapi.OAuth.TokenExchange(c, subjectToken, subjectTokenType, audience, scope) if err != nil { - // Convert OAuth service error to token error response + // Convert OAuth service error to token error response with security headers if oauthErr, ok := err.(*ErrorResponse); ok { - openapi.respondWithTokenError(c, oauthErr) + openapi.respondWithSecureError(c, StatusBadRequest, oauthErr) } else { - openapi.respondWithTokenError(c, ErrInvalidGrant) + openapi.respondWithSecureError(c, StatusBadRequest, ErrInvalidGrant) } return } - // Return successful token exchange response - openapi.respondWithTokenSuccess(c, exchangeResponse) + // Return successful token exchange response with security headers + openapi.respondWithSecureSuccess(c, StatusOK, exchangeResponse) } // oauthRevoke handles token revocation - RFC 7009 @@ -324,7 +324,7 @@ func (openapi *OpenAPI) oauthRevoke(c *gin.Context) { tokenTypeHint := c.PostForm("token_type_hint") // Optional hint about token type if token == "" { - openapi.respondWithError(c, StatusBadRequest, ErrInvalidRequest) + openapi.respondWithSecureError(c, StatusBadRequest, ErrInvalidRequest) return } @@ -348,18 +348,18 @@ func (openapi *OpenAPI) oauthIntrospect(c *gin.Context) { token := c.PostForm("token") if token == "" { - openapi.respondWithError(c, StatusBadRequest, ErrInvalidRequest) + openapi.respondWithSecureError(c, StatusBadRequest, ErrInvalidRequest) return } // Call OAuth service to introspect the token introspectionResult, err := openapi.OAuth.Introspect(c, token) if err != nil { - // Return inactive token response on error (RFC 7662) + // Return inactive token response on error (RFC 7662) with security headers response := &TokenIntrospectionResponse{ Active: false, } - openapi.respondWithSuccess(c, StatusOK, response) + openapi.respondWithSecureSuccess(c, StatusOK, response) return } @@ -376,7 +376,7 @@ func (openapi *OpenAPI) oauthIntrospect(c *gin.Context) { Audience: introspectionResult.Audience, } - openapi.respondWithSuccess(c, StatusOK, response) + openapi.respondWithSecureSuccess(c, StatusOK, response) } // oauthJWKS returns JSON Web Key Set - RFC 7517 @@ -387,16 +387,8 @@ func (openapi *OpenAPI) oauthJWKS(c *gin.Context) { return } - // RFC 7517 compliance: Return JWKS directly as JSON without wrapper - // Set security headers for JWKS endpoint - c.Header("Cache-Control", "no-store") - c.Header("Pragma", "no-cache") - c.Header("X-Content-Type-Options", "nosniff") - c.Header("X-Frame-Options", "DENY") - c.Header("Referrer-Policy", "no-referrer") - - // Return JWKS directly as per RFC 7517 - c.JSON(StatusOK, jwks) + // RFC 7517 compliance: Return JWKS directly with security headers + openapi.respondWithSecureSuccess(c, StatusOK, jwks) } // oauthUserInfo returns user information - OpenID Connect Core 1.0 @@ -419,24 +411,24 @@ func (openapi *OpenAPI) oauthRegister(c *gin.Context) { var req DynamicClientRegistrationRequest if err := c.ShouldBindJSON(&req); err != nil { - openapi.respondWithError(c, StatusBadRequest, ErrInvalidClientMetadata) + openapi.respondWithSecureError(c, StatusBadRequest, ErrInvalidClientMetadata) return } // Basic validation if len(req.RedirectURIs) == 0 { - openapi.respondWithError(c, StatusBadRequest, ErrMissingRedirectURI) + openapi.respondWithSecureError(c, StatusBadRequest, ErrMissingRedirectURI) return } res, err := openapi.OAuth.DynamicClientRegistration(c, &req) if err != nil { - openapi.respondWithError(c, StatusBadRequest, ErrInvalidClientMetadata) + openapi.respondWithSecureError(c, StatusBadRequest, ErrInvalidClientMetadata) return } - // Return the authorization response directly (RFC 7591 compliant) - openapi.respondWithOAuthDirect(c, StatusCreated, res) + // Return the authorization response with security headers (RFC 7591 compliant, contains client credentials) + openapi.respondWithSecureSuccess(c, StatusCreated, res) } // extractClientCredentials extracts client ID and secret from Basic Auth header or form parameters @@ -532,7 +524,7 @@ func (openapi *OpenAPI) oauthDeviceAuthorization(c *gin.Context) { clientID := c.PostForm("client_id") if clientID == "" { - openapi.respondWithTokenError(c, ErrInvalidRequest) + openapi.respondWithError(c, StatusBadRequest, ErrInvalidRequest) return } @@ -545,7 +537,7 @@ func (openapi *OpenAPI) oauthDeviceAuthorization(c *gin.Context) { Interval: 5, // 5 seconds } - openapi.respondWithTokenSuccess(c, response) + openapi.respondWithSuccess(c, StatusOK, response) } // oauthPushedAuthorizationRequest handles PAR - RFC 9126 @@ -577,13 +569,13 @@ func (openapi *OpenAPI) oauthTokenExchange(c *gin.Context) { grantType := c.PostForm("grant_type") if grantType != types.GrantTypeTokenExchange { - openapi.respondWithTokenError(c, ErrUnsupportedGrantType) + openapi.respondWithError(c, StatusBadRequest, ErrUnsupportedGrantType) return } subjectToken := c.PostForm("subject_token") if subjectToken == "" { - openapi.respondWithTokenError(c, ErrInvalidRequest) + openapi.respondWithError(c, StatusBadRequest, ErrInvalidRequest) return } @@ -595,7 +587,7 @@ func (openapi *OpenAPI) oauthTokenExchange(c *gin.Context) { ExpiresIn: 3600, // 1 hour } - openapi.respondWithTokenSuccess(c, response) + openapi.respondWithSuccess(c, StatusOK, response) } // parseAuthorizationRequest parses and validates authorization request parameters diff --git a/openapi/oauth/guard.go b/openapi/oauth/guard.go index 027df9d1..6875d820 100644 --- a/openapi/oauth/guard.go +++ b/openapi/oauth/guard.go @@ -2,6 +2,7 @@ package oauth import ( "net/http" + "strings" "github.com/gin-gonic/gin" ) @@ -19,4 +20,11 @@ func (s *Service) Guard(c *gin.Context) { } // Validate the token + _, err := s.VerifyToken(strings.TrimPrefix(token, "Bearer ")) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"}) + c.Abort() + return + } + } diff --git a/openapi/oauth_test.go b/openapi/oauth_test.go index 32ce2fa4..c8cf260f 100644 --- a/openapi/oauth_test.go +++ b/openapi/oauth_test.go @@ -69,7 +69,7 @@ func TestOAuthRegister(t *testing.T) { assert.Equal(t, "nosniff", resp.Header.Get("X-Content-Type-Options"), "X-Content-Type-Options header should be set") assert.Equal(t, "DENY", resp.Header.Get("X-Frame-Options"), "X-Frame-Options header should be set") assert.Equal(t, "no-referrer", resp.Header.Get("Referrer-Policy"), "Referrer-Policy header should be set") - assert.Equal(t, "application/json;charset=UTF-8", resp.Header.Get("Content-Type"), "Content-Type header should be set") + assert.Equal(t, "application/json", resp.Header.Get("Content-Type"), "Content-Type header should be set") // Read the complete response body for debugging bodyBytes, _ := io.ReadAll(resp.Body) @@ -363,10 +363,9 @@ func TestOAuthJWKS(t *testing.T) { // Should return 200 OK assert.Equal(t, http.StatusOK, resp.StatusCode) - // Verify Content-Type header (case-insensitive comparison) + // Verify Content-Type header contentType := resp.Header.Get("Content-Type") - assert.Contains(t, contentType, "application/json", "Content-Type should be JSON") - assert.Contains(t, contentType, "charset=utf", "Content-Type should specify charset") + assert.Equal(t, "application/json", contentType, "Content-Type should be application/json") // Verify OAuth 2.1 security headers are present assert.Equal(t, "no-store", resp.Header.Get("Cache-Control"), "Cache-Control header should be set") @@ -465,7 +464,7 @@ func TestOAuthJWKS(t *testing.T) { "X-Content-Type-Options": "nosniff", "X-Frame-Options": "DENY", "Referrer-Policy": "no-referrer", - "Content-Type": "application/json; charset=utf-8", + "Content-Type": "application/json", } for header, expectedValue := range expectedHeaders { diff --git a/openapi/oauth_token_test.go b/openapi/oauth_token_test.go index 88cf9665..1b780a0e 100644 --- a/openapi/oauth_token_test.go +++ b/openapi/oauth_token_test.go @@ -57,7 +57,7 @@ func TestOAuthToken_AuthorizationCode(t *testing.T) { // Verify OAuth 2.1 security headers assert.Equal(t, "no-store", resp.Header.Get("Cache-Control")) assert.Equal(t, "no-cache", resp.Header.Get("Pragma")) - assert.Equal(t, "application/json;charset=UTF-8", resp.Header.Get("Content-Type")) + assert.Equal(t, "application/json", resp.Header.Get("Content-Type")) // Parse response var tokenResp types.Token @@ -614,21 +614,11 @@ func TestOAuthIntrospect(t *testing.T) { body, err := io.ReadAll(resp.Body) assert.NoError(t, err) - // Parse the wrapped response structure - var wrappedResp struct { - Success bool `json:"success"` - Data TokenIntrospectionResponse `json:"data"` - Timestamp string `json:"timestamp"` - } - err = json.Unmarshal(body, &wrappedResp) + // Parse response directly (no wrapper) + var introspectResp TokenIntrospectionResponse + err = json.Unmarshal(body, &introspectResp) assert.NoError(t, err) - // Verify the wrapped response - assert.True(t, wrappedResp.Success) - - // Get the actual introspection data - introspectResp := wrappedResp.Data - // Verify introspection response assert.True(t, introspectResp.Active) assert.Equal(t, client.ClientID, introspectResp.ClientID) @@ -664,21 +654,11 @@ func TestOAuthIntrospect(t *testing.T) { body, err := io.ReadAll(resp.Body) assert.NoError(t, err) - // Parse the wrapped response structure - var wrappedResp struct { - Success bool `json:"success"` - Data TokenIntrospectionResponse `json:"data"` - Timestamp string `json:"timestamp"` - } - err = json.Unmarshal(body, &wrappedResp) + // Parse response directly (no wrapper) + var introspectResp TokenIntrospectionResponse + err = json.Unmarshal(body, &introspectResp) assert.NoError(t, err) - // Verify the wrapped response - assert.True(t, wrappedResp.Success) - - // Get the actual introspection data - introspectResp := wrappedResp.Data - // Should indicate token is inactive assert.False(t, introspectResp.Active) @@ -746,21 +726,11 @@ func TestOAuthIntrospect(t *testing.T) { body, err := io.ReadAll(resp.Body) assert.NoError(t, err) - // Parse the wrapped response structure - var wrappedResp struct { - Success bool `json:"success"` - Data TokenIntrospectionResponse `json:"data"` - Timestamp string `json:"timestamp"` - } - err = json.Unmarshal(body, &wrappedResp) + // Parse response directly (no wrapper) + var introspectResp TokenIntrospectionResponse + err = json.Unmarshal(body, &introspectResp) assert.NoError(t, err) - // Verify the wrapped response - assert.True(t, wrappedResp.Success) - - // Get the actual introspection data - introspectResp := wrappedResp.Data - // Revoked token should be inactive // Note: For JWT tokens, revocation might not be immediately reflected in introspection // since JWT tokens are stateless and contain their own validity information diff --git a/openapi/response.go b/openapi/response.go index 9008e322..0d1eadc3 100644 --- a/openapi/response.go +++ b/openapi/response.go @@ -2,7 +2,6 @@ package openapi import ( "net/http" - "time" "github.com/gin-gonic/gin" "github.com/yaoapp/yao/openapi/oauth/types" @@ -146,15 +145,6 @@ const ( StatusServiceUnavailable = http.StatusServiceUnavailable // 503 - Service temporarily unavailable ) -// StandardResponse represents a standard OAuth API response -type StandardResponse struct { - Success bool `json:"success"` - Data interface{} `json:"data,omitempty"` - Error *ErrorResponse `json:"error,omitempty"` - Timestamp time.Time `json:"timestamp"` - RequestID string `json:"request_id,omitempty"` -} - // setOAuthSecurityHeaders sets standard OAuth 2.0/2.1 security headers // These headers are required by OAuth 2.1 specification for enhanced security func (openapi *OpenAPI) setOAuthSecurityHeaders(c *gin.Context) { @@ -167,66 +157,25 @@ func (openapi *OpenAPI) setOAuthSecurityHeaders(c *gin.Context) { // setJSONContentType sets JSON content type header for OAuth responses func (openapi *OpenAPI) setJSONContentType(c *gin.Context) { - c.Header("Content-Type", "application/json;charset=UTF-8") + c.Header("Content-Type", "application/json") } -// Response helper functions for consistent OAuth responses - -// respondWithSuccess sends a successful OAuth response +// respondWithSuccess sends a successful response (no wrapper, direct data) func (openapi *OpenAPI) respondWithSuccess(c *gin.Context, statusCode int, data interface{}) { - openapi.setOAuthSecurityHeaders(c) - - response := StandardResponse{ - Success: true, - Data: data, - Timestamp: time.Now().UTC(), - RequestID: c.GetString("request_id"), - } - - c.JSON(statusCode, response) + openapi.setJSONContentType(c) + c.JSON(statusCode, data) } -// respondWithError sends an OAuth error response +// respondWithError sends an error response (no wrapper, direct error) func (openapi *OpenAPI) respondWithError(c *gin.Context, statusCode int, err *ErrorResponse) { - openapi.setOAuthSecurityHeaders(c) - - response := StandardResponse{ - Success: false, - Error: err, - Timestamp: time.Now().UTC(), - RequestID: c.GetString("request_id"), - } + openapi.setJSONContentType(c) // Add WWW-Authenticate header for 401 responses if statusCode == StatusUnauthorized { openapi.addWWWAuthenticateHeader(c, err) } - c.JSON(statusCode, response) -} - -// respondWithTokenSuccess sends a successful token response (without wrapper) -// This method is used for OAuth token endpoint responses that must follow RFC 6749 format -func (openapi *OpenAPI) respondWithTokenSuccess(c *gin.Context, token interface{}) { - openapi.setOAuthSecurityHeaders(c) - openapi.setJSONContentType(c) - c.JSON(StatusOK, token) -} - -// respondWithTokenError sends a token endpoint error response (without wrapper) -// This method is used for OAuth token endpoint errors that must follow RFC 6749 format -func (openapi *OpenAPI) respondWithTokenError(c *gin.Context, err *ErrorResponse) { - openapi.setOAuthSecurityHeaders(c) - openapi.setJSONContentType(c) - c.JSON(StatusBadRequest, err) -} - -// respondWithOAuthDirect sends a direct OAuth response without StandardResponse wrapper -// This method is used for endpoints that require RFC-compliant response format (e.g., client registration) -func (openapi *OpenAPI) respondWithOAuthDirect(c *gin.Context, statusCode int, data interface{}) { - openapi.setOAuthSecurityHeaders(c) - openapi.setJSONContentType(c) - c.JSON(statusCode, data) + c.JSON(statusCode, err) } // respondWithAuthorizationError sends an authorization endpoint error via redirect @@ -289,62 +238,22 @@ func (openapi *OpenAPI) addWWWAuthenticateHeader(c *gin.Context, err *ErrorRespo c.Header("WWW-Authenticate", headerValue) } -// Validation helper functions - -// validateRedirectURI validates redirect URI according to RFC 6749 -func (openapi *OpenAPI) validateRedirectURI(redirectURI string, client *ClientInfo) error { - if redirectURI == "" { - return ErrMissingRedirectURI - } - - // Check if redirect URI is registered for the client - for _, registeredURI := range client.RedirectURIs { - if registeredURI == redirectURI { - return nil - } - } - - return ErrInvalidRedirectURI +// respondWithSecureSuccess sends a successful response with OAuth security headers (for sensitive endpoints) +func (openapi *OpenAPI) respondWithSecureSuccess(c *gin.Context, statusCode int, data interface{}) { + openapi.setOAuthSecurityHeaders(c) + openapi.setJSONContentType(c) + c.JSON(statusCode, data) } -// validatePKCE validates PKCE parameters according to RFC 7636 -func (openapi *OpenAPI) validatePKCE(codeChallenge, codeChallengeMethod, codeVerifier string) error { - if codeChallenge == "" { - return ErrMissingCodeChallenge +// respondWithSecureError sends an error response with OAuth security headers (for sensitive endpoints) +func (openapi *OpenAPI) respondWithSecureError(c *gin.Context, statusCode int, err *ErrorResponse) { + openapi.setOAuthSecurityHeaders(c) + openapi.setJSONContentType(c) + + // Add WWW-Authenticate header for 401 responses + if statusCode == StatusUnauthorized { + openapi.addWWWAuthenticateHeader(c, err) } - if codeChallengeMethod != types.CodeChallengeMethodS256 && codeChallengeMethod != types.CodeChallengeMethodPlain { - return ErrInvalidCodeChallenge - } - - // Additional PKCE validation logic would go here - // This is a simplified example - - return nil -} - -// createErrorWithState creates an error response with state parameter -func createErrorWithState(baseError *ErrorResponse, state string) *ErrorResponse { - errorWithState := &ErrorResponse{ - Code: baseError.Code, - ErrorDescription: baseError.ErrorDescription, - ErrorURI: baseError.ErrorURI, - State: state, - } - return errorWithState -} - -// Legacy OAuth 2.1 specific response helpers (deprecated) -// These methods are kept for backward compatibility but should use the unified approach above - -// respondWithOAuth21Error ensures OAuth 2.1 compliance for error responses -// Deprecated: Use respondWithError instead, which now includes all OAuth 2.1 security headers -func (openapi *OpenAPI) respondWithOAuth21Error(c *gin.Context, statusCode int, err *ErrorResponse) { - openapi.respondWithError(c, statusCode, err) -} - -// respondWithOAuth21TokenSuccess ensures OAuth 2.1 compliance for token responses -// Deprecated: Use respondWithTokenSuccess instead, which now includes all OAuth 2.1 security headers -func (openapi *OpenAPI) respondWithOAuth21TokenSuccess(c *gin.Context, token interface{}) { - openapi.respondWithTokenSuccess(c, token) + c.JSON(statusCode, err) }