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.
This commit is contained in:
parent
7e8d4a9ba9
commit
d54572a7d1
5 changed files with 91 additions and 213 deletions
104
openapi/oauth.go
104
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue