Merge pull request #1029 from trheyi/main

Refactor OAuth response handling and improve content type management
This commit is contained in:
Max 2025-07-22 15:37:11 +08:00 committed by GitHub
commit d9392f14b6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 91 additions and 213 deletions

View file

@ -123,7 +123,7 @@ func (openapi *OpenAPI) oauthToken(c *gin.Context) {
// Validate grant type // Validate grant type
if grantType == "" { if grantType == "" {
openapi.respondWithTokenError(c, ErrInvalidRequest) openapi.respondWithSecureError(c, StatusBadRequest, ErrInvalidRequest)
return return
} }
@ -141,7 +141,7 @@ func (openapi *OpenAPI) oauthToken(c *gin.Context) {
openapi.handleTokenExchangeGrant(c) openapi.handleTokenExchangeGrant(c)
default: 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 // Extract client credentials from Basic Auth header or form parameters
clientID, clientSecret := openapi.extractClientCredentials(c) clientID, clientSecret := openapi.extractClientCredentials(c)
if clientID == "" { if clientID == "" {
openapi.respondWithTokenError(c, ErrInvalidClient) openapi.respondWithSecureError(c, StatusBadRequest, ErrInvalidClient)
return return
} }
// Validate client credentials using OAuth service // Validate client credentials using OAuth service
oauthService, ok := openapi.OAuth.(*oauth.Service) oauthService, ok := openapi.OAuth.(*oauth.Service)
if !ok { if !ok {
openapi.respondWithTokenError(c, ErrInvalidClient) openapi.respondWithSecureError(c, StatusBadRequest, ErrInvalidClient)
return return
} }
clientInfo, err := oauthService.GetClientProvider().GetClientByCredentials(c, clientID, clientSecret) clientInfo, err := oauthService.GetClientProvider().GetClientByCredentials(c, clientID, clientSecret)
if err != nil { if err != nil {
openapi.respondWithTokenError(c, ErrInvalidClient) openapi.respondWithSecureError(c, StatusBadRequest, ErrInvalidClient)
return return
} }
@ -179,13 +179,13 @@ func (openapi *OpenAPI) handleStandardTokenGrant(c *gin.Context, grantType strin
// Basic validation for authorization code grant // Basic validation for authorization code grant
if code == "" || redirectURI == "" { if code == "" || redirectURI == "" {
openapi.respondWithTokenError(c, ErrInvalidRequest) openapi.respondWithSecureError(c, StatusBadRequest, ErrInvalidRequest)
return return
} }
// Validate that client supports authorization code grant // Validate that client supports authorization code grant
if !openapi.clientSupportsGrantType(clientInfo, types.GrantTypeAuthorizationCode) { if !openapi.clientSupportsGrantType(clientInfo, types.GrantTypeAuthorizationCode) {
openapi.respondWithTokenError(c, ErrUnauthorizedClient) openapi.respondWithSecureError(c, StatusUnauthorized, ErrUnauthorizedClient)
return return
} }
@ -194,13 +194,13 @@ func (openapi *OpenAPI) handleStandardTokenGrant(c *gin.Context, grantType strin
// Basic validation for device code grant // Basic validation for device code grant
if code == "" { if code == "" {
openapi.respondWithTokenError(c, ErrInvalidRequest) openapi.respondWithSecureError(c, StatusBadRequest, ErrInvalidRequest)
return return
} }
// Validate that client supports device code grant // Validate that client supports device code grant
if !openapi.clientSupportsGrantType(clientInfo, types.GrantTypeDeviceCode) { if !openapi.clientSupportsGrantType(clientInfo, types.GrantTypeDeviceCode) {
openapi.respondWithTokenError(c, ErrUnauthorizedClient) openapi.respondWithSecureError(c, StatusUnauthorized, ErrUnauthorizedClient)
return return
} }
@ -210,7 +210,7 @@ func (openapi *OpenAPI) handleStandardTokenGrant(c *gin.Context, grantType strin
// Validate that client supports client credentials grant // Validate that client supports client credentials grant
if !openapi.clientSupportsGrantType(clientInfo, types.GrantTypeClientCredentials) { if !openapi.clientSupportsGrantType(clientInfo, types.GrantTypeClientCredentials) {
openapi.respondWithTokenError(c, ErrUnauthorizedClient) openapi.respondWithSecureError(c, StatusUnauthorized, ErrUnauthorizedClient)
return return
} }
} }
@ -218,17 +218,17 @@ func (openapi *OpenAPI) handleStandardTokenGrant(c *gin.Context, grantType strin
// Call OAuth service to handle the token request // Call OAuth service to handle the token request
token, err := openapi.OAuth.Token(c, grantType, code, clientID, codeVerifier) token, err := openapi.OAuth.Token(c, grantType, code, clientID, codeVerifier)
if err != nil { 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 { if oauthErr, ok := err.(*ErrorResponse); ok {
openapi.respondWithTokenError(c, oauthErr) openapi.respondWithSecureError(c, StatusBadRequest, oauthErr)
} else { } else {
openapi.respondWithTokenError(c, ErrInvalidGrant) openapi.respondWithSecureError(c, StatusBadRequest, ErrInvalidGrant)
} }
return return
} }
// Return successful token response // Return successful token response with OAuth security headers (RFC 6749 Section 5.1: MUST set Cache-Control: no-store)
openapi.respondWithTokenSuccess(c, token) openapi.respondWithSecureSuccess(c, StatusOK, token)
} }
// handleRefreshTokenGrant handles refresh token requests - RFC 6749 Section 6 // 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 // Extract client credentials from Basic Auth header or form parameters
clientID, clientSecret := openapi.extractClientCredentials(c) clientID, clientSecret := openapi.extractClientCredentials(c)
if clientID == "" { if clientID == "" {
openapi.respondWithTokenError(c, ErrInvalidClient) openapi.respondWithSecureError(c, StatusBadRequest, ErrInvalidClient)
return return
} }
// Validate client credentials using OAuth service // Validate client credentials using OAuth service
oauthService, ok := openapi.OAuth.(*oauth.Service) oauthService, ok := openapi.OAuth.(*oauth.Service)
if !ok { if !ok {
openapi.respondWithTokenError(c, ErrInvalidClient) openapi.respondWithSecureError(c, StatusBadRequest, ErrInvalidClient)
return return
} }
clientInfo, err := oauthService.GetClientProvider().GetClientByCredentials(c, clientID, clientSecret) clientInfo, err := oauthService.GetClientProvider().GetClientByCredentials(c, clientID, clientSecret)
if err != nil { if err != nil {
openapi.respondWithTokenError(c, ErrInvalidClient) openapi.respondWithSecureError(c, StatusBadRequest, ErrInvalidClient)
return return
} }
// Validate that client supports refresh token grant // Validate that client supports refresh token grant
if !openapi.clientSupportsGrantType(clientInfo, types.GrantTypeRefreshToken) { if !openapi.clientSupportsGrantType(clientInfo, types.GrantTypeRefreshToken) {
openapi.respondWithTokenError(c, ErrUnauthorizedClient) openapi.respondWithSecureError(c, StatusUnauthorized, ErrUnauthorizedClient)
return return
} }
@ -264,7 +264,7 @@ func (openapi *OpenAPI) handleRefreshTokenGrant(c *gin.Context) {
// Basic validation // Basic validation
if refreshToken == "" { if refreshToken == "" {
openapi.respondWithTokenError(c, ErrInvalidRequest) openapi.respondWithSecureError(c, StatusBadRequest, ErrInvalidRequest)
return return
} }
@ -276,17 +276,17 @@ func (openapi *OpenAPI) handleRefreshTokenGrant(c *gin.Context) {
refreshResponse, err = openapi.OAuth.RefreshToken(c, refreshToken) refreshResponse, err = openapi.OAuth.RefreshToken(c, refreshToken)
} }
if err != nil { 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 { if oauthErr, ok := err.(*ErrorResponse); ok {
openapi.respondWithTokenError(c, oauthErr) openapi.respondWithSecureError(c, StatusBadRequest, oauthErr)
} else { } else {
openapi.respondWithTokenError(c, ErrInvalidGrant) openapi.respondWithSecureError(c, StatusBadRequest, ErrInvalidGrant)
} }
return return
} }
// Return successful refresh token response // Return successful refresh token response with security headers
openapi.respondWithTokenSuccess(c, refreshResponse) openapi.respondWithSecureSuccess(c, StatusOK, refreshResponse)
} }
// handleTokenExchangeGrant handles token exchange requests - RFC 8693 // handleTokenExchangeGrant handles token exchange requests - RFC 8693
@ -298,24 +298,24 @@ func (openapi *OpenAPI) handleTokenExchangeGrant(c *gin.Context) {
// Basic validation // Basic validation
if subjectToken == "" { if subjectToken == "" {
openapi.respondWithTokenError(c, ErrInvalidRequest) openapi.respondWithSecureError(c, StatusBadRequest, ErrInvalidRequest)
return return
} }
// Call OAuth service to handle token exchange // Call OAuth service to handle token exchange
exchangeResponse, err := openapi.OAuth.TokenExchange(c, subjectToken, subjectTokenType, audience, scope) exchangeResponse, err := openapi.OAuth.TokenExchange(c, subjectToken, subjectTokenType, audience, scope)
if err != nil { 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 { if oauthErr, ok := err.(*ErrorResponse); ok {
openapi.respondWithTokenError(c, oauthErr) openapi.respondWithSecureError(c, StatusBadRequest, oauthErr)
} else { } else {
openapi.respondWithTokenError(c, ErrInvalidGrant) openapi.respondWithSecureError(c, StatusBadRequest, ErrInvalidGrant)
} }
return return
} }
// Return successful token exchange response // Return successful token exchange response with security headers
openapi.respondWithTokenSuccess(c, exchangeResponse) openapi.respondWithSecureSuccess(c, StatusOK, exchangeResponse)
} }
// oauthRevoke handles token revocation - RFC 7009 // 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 tokenTypeHint := c.PostForm("token_type_hint") // Optional hint about token type
if token == "" { if token == "" {
openapi.respondWithError(c, StatusBadRequest, ErrInvalidRequest) openapi.respondWithSecureError(c, StatusBadRequest, ErrInvalidRequest)
return return
} }
@ -348,18 +348,18 @@ func (openapi *OpenAPI) oauthIntrospect(c *gin.Context) {
token := c.PostForm("token") token := c.PostForm("token")
if token == "" { if token == "" {
openapi.respondWithError(c, StatusBadRequest, ErrInvalidRequest) openapi.respondWithSecureError(c, StatusBadRequest, ErrInvalidRequest)
return return
} }
// Call OAuth service to introspect the token // Call OAuth service to introspect the token
introspectionResult, err := openapi.OAuth.Introspect(c, token) introspectionResult, err := openapi.OAuth.Introspect(c, token)
if err != nil { if err != nil {
// Return inactive token response on error (RFC 7662) // Return inactive token response on error (RFC 7662) with security headers
response := &TokenIntrospectionResponse{ response := &TokenIntrospectionResponse{
Active: false, Active: false,
} }
openapi.respondWithSuccess(c, StatusOK, response) openapi.respondWithSecureSuccess(c, StatusOK, response)
return return
} }
@ -376,7 +376,7 @@ func (openapi *OpenAPI) oauthIntrospect(c *gin.Context) {
Audience: introspectionResult.Audience, Audience: introspectionResult.Audience,
} }
openapi.respondWithSuccess(c, StatusOK, response) openapi.respondWithSecureSuccess(c, StatusOK, response)
} }
// oauthJWKS returns JSON Web Key Set - RFC 7517 // oauthJWKS returns JSON Web Key Set - RFC 7517
@ -387,16 +387,8 @@ func (openapi *OpenAPI) oauthJWKS(c *gin.Context) {
return return
} }
// RFC 7517 compliance: Return JWKS directly as JSON without wrapper // RFC 7517 compliance: Return JWKS directly with security headers
// Set security headers for JWKS endpoint openapi.respondWithSecureSuccess(c, StatusOK, jwks)
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)
} }
// oauthUserInfo returns user information - OpenID Connect Core 1.0 // oauthUserInfo returns user information - OpenID Connect Core 1.0
@ -419,24 +411,24 @@ func (openapi *OpenAPI) oauthRegister(c *gin.Context) {
var req DynamicClientRegistrationRequest var req DynamicClientRegistrationRequest
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
openapi.respondWithError(c, StatusBadRequest, ErrInvalidClientMetadata) openapi.respondWithSecureError(c, StatusBadRequest, ErrInvalidClientMetadata)
return return
} }
// Basic validation // Basic validation
if len(req.RedirectURIs) == 0 { if len(req.RedirectURIs) == 0 {
openapi.respondWithError(c, StatusBadRequest, ErrMissingRedirectURI) openapi.respondWithSecureError(c, StatusBadRequest, ErrMissingRedirectURI)
return return
} }
res, err := openapi.OAuth.DynamicClientRegistration(c, &req) res, err := openapi.OAuth.DynamicClientRegistration(c, &req)
if err != nil { if err != nil {
openapi.respondWithError(c, StatusBadRequest, ErrInvalidClientMetadata) openapi.respondWithSecureError(c, StatusBadRequest, ErrInvalidClientMetadata)
return return
} }
// Return the authorization response directly (RFC 7591 compliant) // Return the authorization response with security headers (RFC 7591 compliant, contains client credentials)
openapi.respondWithOAuthDirect(c, StatusCreated, res) openapi.respondWithSecureSuccess(c, StatusCreated, res)
} }
// extractClientCredentials extracts client ID and secret from Basic Auth header or form parameters // 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") clientID := c.PostForm("client_id")
if clientID == "" { if clientID == "" {
openapi.respondWithTokenError(c, ErrInvalidRequest) openapi.respondWithError(c, StatusBadRequest, ErrInvalidRequest)
return return
} }
@ -545,7 +537,7 @@ func (openapi *OpenAPI) oauthDeviceAuthorization(c *gin.Context) {
Interval: 5, // 5 seconds Interval: 5, // 5 seconds
} }
openapi.respondWithTokenSuccess(c, response) openapi.respondWithSuccess(c, StatusOK, response)
} }
// oauthPushedAuthorizationRequest handles PAR - RFC 9126 // oauthPushedAuthorizationRequest handles PAR - RFC 9126
@ -577,13 +569,13 @@ func (openapi *OpenAPI) oauthTokenExchange(c *gin.Context) {
grantType := c.PostForm("grant_type") grantType := c.PostForm("grant_type")
if grantType != types.GrantTypeTokenExchange { if grantType != types.GrantTypeTokenExchange {
openapi.respondWithTokenError(c, ErrUnsupportedGrantType) openapi.respondWithError(c, StatusBadRequest, ErrUnsupportedGrantType)
return return
} }
subjectToken := c.PostForm("subject_token") subjectToken := c.PostForm("subject_token")
if subjectToken == "" { if subjectToken == "" {
openapi.respondWithTokenError(c, ErrInvalidRequest) openapi.respondWithError(c, StatusBadRequest, ErrInvalidRequest)
return return
} }
@ -595,7 +587,7 @@ func (openapi *OpenAPI) oauthTokenExchange(c *gin.Context) {
ExpiresIn: 3600, // 1 hour ExpiresIn: 3600, // 1 hour
} }
openapi.respondWithTokenSuccess(c, response) openapi.respondWithSuccess(c, StatusOK, response)
} }
// parseAuthorizationRequest parses and validates authorization request parameters // parseAuthorizationRequest parses and validates authorization request parameters

View file

@ -2,6 +2,7 @@ package oauth
import ( import (
"net/http" "net/http"
"strings"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@ -19,4 +20,11 @@ func (s *Service) Guard(c *gin.Context) {
} }
// Validate the token // 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
}
} }

View file

@ -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, "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, "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, "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 // Read the complete response body for debugging
bodyBytes, _ := io.ReadAll(resp.Body) bodyBytes, _ := io.ReadAll(resp.Body)
@ -363,10 +363,9 @@ func TestOAuthJWKS(t *testing.T) {
// Should return 200 OK // Should return 200 OK
assert.Equal(t, http.StatusOK, resp.StatusCode) assert.Equal(t, http.StatusOK, resp.StatusCode)
// Verify Content-Type header (case-insensitive comparison) // Verify Content-Type header
contentType := resp.Header.Get("Content-Type") contentType := resp.Header.Get("Content-Type")
assert.Contains(t, contentType, "application/json", "Content-Type should be JSON") assert.Equal(t, "application/json", contentType, "Content-Type should be application/json")
assert.Contains(t, contentType, "charset=utf", "Content-Type should specify charset")
// Verify OAuth 2.1 security headers are present // Verify OAuth 2.1 security headers are present
assert.Equal(t, "no-store", resp.Header.Get("Cache-Control"), "Cache-Control header should be set") 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-Content-Type-Options": "nosniff",
"X-Frame-Options": "DENY", "X-Frame-Options": "DENY",
"Referrer-Policy": "no-referrer", "Referrer-Policy": "no-referrer",
"Content-Type": "application/json; charset=utf-8", "Content-Type": "application/json",
} }
for header, expectedValue := range expectedHeaders { for header, expectedValue := range expectedHeaders {

View file

@ -57,7 +57,7 @@ func TestOAuthToken_AuthorizationCode(t *testing.T) {
// Verify OAuth 2.1 security headers // Verify OAuth 2.1 security headers
assert.Equal(t, "no-store", resp.Header.Get("Cache-Control")) assert.Equal(t, "no-store", resp.Header.Get("Cache-Control"))
assert.Equal(t, "no-cache", resp.Header.Get("Pragma")) 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 // Parse response
var tokenResp types.Token var tokenResp types.Token
@ -614,21 +614,11 @@ func TestOAuthIntrospect(t *testing.T) {
body, err := io.ReadAll(resp.Body) body, err := io.ReadAll(resp.Body)
assert.NoError(t, err) assert.NoError(t, err)
// Parse the wrapped response structure // Parse response directly (no wrapper)
var wrappedResp struct { var introspectResp TokenIntrospectionResponse
Success bool `json:"success"` err = json.Unmarshal(body, &introspectResp)
Data TokenIntrospectionResponse `json:"data"`
Timestamp string `json:"timestamp"`
}
err = json.Unmarshal(body, &wrappedResp)
assert.NoError(t, err) assert.NoError(t, err)
// Verify the wrapped response
assert.True(t, wrappedResp.Success)
// Get the actual introspection data
introspectResp := wrappedResp.Data
// Verify introspection response // Verify introspection response
assert.True(t, introspectResp.Active) assert.True(t, introspectResp.Active)
assert.Equal(t, client.ClientID, introspectResp.ClientID) assert.Equal(t, client.ClientID, introspectResp.ClientID)
@ -664,21 +654,11 @@ func TestOAuthIntrospect(t *testing.T) {
body, err := io.ReadAll(resp.Body) body, err := io.ReadAll(resp.Body)
assert.NoError(t, err) assert.NoError(t, err)
// Parse the wrapped response structure // Parse response directly (no wrapper)
var wrappedResp struct { var introspectResp TokenIntrospectionResponse
Success bool `json:"success"` err = json.Unmarshal(body, &introspectResp)
Data TokenIntrospectionResponse `json:"data"`
Timestamp string `json:"timestamp"`
}
err = json.Unmarshal(body, &wrappedResp)
assert.NoError(t, err) 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 // Should indicate token is inactive
assert.False(t, introspectResp.Active) assert.False(t, introspectResp.Active)
@ -746,21 +726,11 @@ func TestOAuthIntrospect(t *testing.T) {
body, err := io.ReadAll(resp.Body) body, err := io.ReadAll(resp.Body)
assert.NoError(t, err) assert.NoError(t, err)
// Parse the wrapped response structure // Parse response directly (no wrapper)
var wrappedResp struct { var introspectResp TokenIntrospectionResponse
Success bool `json:"success"` err = json.Unmarshal(body, &introspectResp)
Data TokenIntrospectionResponse `json:"data"`
Timestamp string `json:"timestamp"`
}
err = json.Unmarshal(body, &wrappedResp)
assert.NoError(t, err) 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 // Revoked token should be inactive
// Note: For JWT tokens, revocation might not be immediately reflected in introspection // Note: For JWT tokens, revocation might not be immediately reflected in introspection
// since JWT tokens are stateless and contain their own validity information // since JWT tokens are stateless and contain their own validity information

View file

@ -2,7 +2,6 @@ package openapi
import ( import (
"net/http" "net/http"
"time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/yaoapp/yao/openapi/oauth/types" "github.com/yaoapp/yao/openapi/oauth/types"
@ -146,15 +145,6 @@ const (
StatusServiceUnavailable = http.StatusServiceUnavailable // 503 - Service temporarily unavailable 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 // setOAuthSecurityHeaders sets standard OAuth 2.0/2.1 security headers
// These headers are required by OAuth 2.1 specification for enhanced security // These headers are required by OAuth 2.1 specification for enhanced security
func (openapi *OpenAPI) setOAuthSecurityHeaders(c *gin.Context) { 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 // setJSONContentType sets JSON content type header for OAuth responses
func (openapi *OpenAPI) setJSONContentType(c *gin.Context) { 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 response (no wrapper, direct data)
// respondWithSuccess sends a successful OAuth response
func (openapi *OpenAPI) respondWithSuccess(c *gin.Context, statusCode int, data interface{}) { func (openapi *OpenAPI) respondWithSuccess(c *gin.Context, statusCode int, data interface{}) {
openapi.setOAuthSecurityHeaders(c) openapi.setJSONContentType(c)
c.JSON(statusCode, data)
response := StandardResponse{
Success: true,
Data: data,
Timestamp: time.Now().UTC(),
RequestID: c.GetString("request_id"),
} }
c.JSON(statusCode, response) // respondWithError sends an error response (no wrapper, direct error)
}
// respondWithError sends an OAuth error response
func (openapi *OpenAPI) respondWithError(c *gin.Context, statusCode int, err *ErrorResponse) { func (openapi *OpenAPI) respondWithError(c *gin.Context, statusCode int, err *ErrorResponse) {
openapi.setOAuthSecurityHeaders(c) openapi.setJSONContentType(c)
response := StandardResponse{
Success: false,
Error: err,
Timestamp: time.Now().UTC(),
RequestID: c.GetString("request_id"),
}
// Add WWW-Authenticate header for 401 responses // Add WWW-Authenticate header for 401 responses
if statusCode == StatusUnauthorized { if statusCode == StatusUnauthorized {
openapi.addWWWAuthenticateHeader(c, err) openapi.addWWWAuthenticateHeader(c, err)
} }
c.JSON(statusCode, response) c.JSON(statusCode, err)
}
// 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)
} }
// respondWithAuthorizationError sends an authorization endpoint error via redirect // 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) c.Header("WWW-Authenticate", headerValue)
} }
// Validation helper functions // respondWithSecureSuccess sends a successful response with OAuth security headers (for sensitive endpoints)
func (openapi *OpenAPI) respondWithSecureSuccess(c *gin.Context, statusCode int, data interface{}) {
// validateRedirectURI validates redirect URI according to RFC 6749 openapi.setOAuthSecurityHeaders(c)
func (openapi *OpenAPI) validateRedirectURI(redirectURI string, client *ClientInfo) error { openapi.setJSONContentType(c)
if redirectURI == "" { c.JSON(statusCode, data)
return ErrMissingRedirectURI
} }
// Check if redirect URI is registered for the client // respondWithSecureError sends an error response with OAuth security headers (for sensitive endpoints)
for _, registeredURI := range client.RedirectURIs { func (openapi *OpenAPI) respondWithSecureError(c *gin.Context, statusCode int, err *ErrorResponse) {
if registeredURI == redirectURI { openapi.setOAuthSecurityHeaders(c)
return nil openapi.setJSONContentType(c)
}
// Add WWW-Authenticate header for 401 responses
if statusCode == StatusUnauthorized {
openapi.addWWWAuthenticateHeader(c, err)
} }
return ErrInvalidRedirectURI c.JSON(statusCode, err)
}
// validatePKCE validates PKCE parameters according to RFC 7636
func (openapi *OpenAPI) validatePKCE(codeChallenge, codeChallengeMethod, codeVerifier string) error {
if codeChallenge == "" {
return ErrMissingCodeChallenge
}
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)
} }