Implement OAuth endpoint logic and enhance test setup

- Added core functionality for OAuth authorization, token handling, and user info retrieval, including error handling and response generation.
- Introduced helper functions for managing different grant types, improving code organization and clarity.
- Enhanced the test setup with a new Prepare function to initialize the OpenAPI test environment and a Clean function for proper resource management.
- Updated tests to ensure the OpenAPI server is correctly loaded and operational during testing.
This commit is contained in:
Max 2025-07-21 15:34:13 +08:00
parent 458391f5b4
commit 517a4cf5b0
5 changed files with 880 additions and 19 deletions

79
openapi/hello_test.go Normal file
View file

@ -0,0 +1,79 @@
package openapi
import (
"encoding/json"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
"github.com/yaoapp/yao/share"
)
func TestHelloWorldHello(t *testing.T) {
serverURL := Prepare(t)
defer Clean()
// Get base URL from server config
baseURL := ""
if Server != nil && Server.Config != nil {
baseURL = Server.Config.BaseURL
}
tests := []struct {
name string
method string
path string
}{
{
name: "GET hello endpoint",
method: "GET",
path: baseURL + "/helloworld/hello",
},
{
name: "POST hello endpoint",
method: "POST",
path: baseURL + "/helloworld/hello",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Make HTTP request
var resp *http.Response
var err error
if tt.method == "GET" {
resp, err = http.Get(serverURL + tt.path)
} else {
resp, err = http.Post(serverURL+tt.path, "application/json", nil)
}
assert.NoError(t, err)
assert.NotNil(t, resp)
defer resp.Body.Close()
// Check status code
assert.Equal(t, http.StatusOK, resp.StatusCode)
// Parse JSON response
var response map[string]interface{}
err = json.NewDecoder(resp.Body).Decode(&response)
assert.NoError(t, err)
// Verify response structure and content
assert.Equal(t, "HELLO, WORLD", response["MESSAGE"])
assert.NotEmpty(t, response["SERVER_TIME"])
assert.Equal(t, share.VERSION, response["VERSION"])
assert.Equal(t, share.PRVERSION, response["PRVERSION"])
assert.Equal(t, share.CUI, response["CUI"])
assert.Equal(t, share.PRCUI, response["PRCUI"])
assert.Equal(t, share.App.Name, response["APP"])
assert.Equal(t, share.App.Version, response["APP_VERSION"])
// Check that SERVER_TIME is a valid timestamp format
serverTime, ok := response["SERVER_TIME"].(string)
assert.True(t, ok)
assert.NotEmpty(t, serverTime)
})
}
}

View file

@ -1,6 +1,9 @@
package openapi
import "github.com/gin-gonic/gin"
import (
"github.com/gin-gonic/gin"
"github.com/yaoapp/yao/openapi/oauth/types"
)
// OAuth handlers
// NOTE: If using versioned paths like /v1/oauth, ensure that:
@ -61,42 +64,366 @@ func (openapi *OpenAPI) attachOAuth(base *gin.RouterGroup) {
// OAuth Core Endpoints Implementation
// oauthAuthorize handles authorization requests - RFC 6749 Section 3.1
func (openapi *OpenAPI) oauthAuthorize(c *gin.Context) {}
func (openapi *OpenAPI) oauthAuthorize(c *gin.Context) {
// Parse and validate authorization request
authReq, err := openapi.parseAuthorizationRequest(c)
if err != nil {
openapi.respondWithAuthorizationError(c, authReq.RedirectURI, err, authReq.State)
return
}
// TODO: Implement full authorization logic
// For now, return server error to indicate not implemented
openapi.respondWithAuthorizationError(c, authReq.RedirectURI, ErrServerError, authReq.State)
}
// oauthToken handles token requests - RFC 6749 Section 3.2
func (openapi *OpenAPI) oauthToken(c *gin.Context) {}
func (openapi *OpenAPI) oauthToken(c *gin.Context) {
grantType := c.PostForm("grant_type")
// Validate grant type
if grantType == "" {
openapi.respondWithTokenError(c, ErrInvalidRequest)
return
}
switch grantType {
case types.GrantTypeAuthorizationCode:
openapi.handleAuthorizationCodeGrant(c)
case types.GrantTypeRefreshToken:
openapi.handleRefreshTokenGrant(c)
case types.GrantTypeClientCredentials:
openapi.handleClientCredentialsGrant(c)
case types.GrantTypeDeviceCode:
openapi.handleDeviceCodeGrant(c)
default:
openapi.respondWithTokenError(c, ErrUnsupportedGrantType)
}
}
// oauthRevoke handles token revocation - RFC 7009
func (openapi *OpenAPI) oauthRevoke(c *gin.Context) {}
func (openapi *OpenAPI) oauthRevoke(c *gin.Context) {
token := c.PostForm("token")
if token == "" {
openapi.respondWithError(c, StatusBadRequest, ErrInvalidRequest)
return
}
// TODO: Implement token revocation logic
c.Status(StatusNoContent)
}
// oauthIntrospect handles token introspection - RFC 7662
func (openapi *OpenAPI) oauthIntrospect(c *gin.Context) {}
func (openapi *OpenAPI) oauthIntrospect(c *gin.Context) {
token := c.PostForm("token")
if token == "" {
openapi.respondWithError(c, StatusBadRequest, ErrInvalidRequest)
return
}
// TODO: Implement token introspection logic
// Return inactive token for now
response := &TokenIntrospectionResponse{
Active: false,
}
openapi.respondWithSuccess(c, StatusOK, response)
}
// oauthJWKS returns JSON Web Key Set - RFC 7517
func (openapi *OpenAPI) oauthJWKS(c *gin.Context) {}
func (openapi *OpenAPI) oauthJWKS(c *gin.Context) {
// TODO: Implement JWKS generation
jwks := &JWKSResponse{
Keys: []JWK{},
}
openapi.respondWithSuccess(c, StatusOK, jwks)
}
// oauthUserInfo returns user information - OpenID Connect Core 1.0
func (openapi *OpenAPI) oauthUserInfo(c *gin.Context) {}
func (openapi *OpenAPI) oauthUserInfo(c *gin.Context) {
// Check for Bearer token in Authorization header
authHeader := c.GetHeader("Authorization")
if authHeader == "" || len(authHeader) < 7 || authHeader[:7] != "Bearer " {
openapi.respondWithError(c, StatusUnauthorized, ErrInvalidToken)
return
}
// TODO: Implement user info retrieval
openapi.respondWithError(c, StatusNotImplemented, ErrServerError)
}
// OAuth Extended Endpoints Implementation
// oauthRegister handles dynamic client registration - RFC 7591
func (openapi *OpenAPI) oauthRegister(c *gin.Context) {}
func (openapi *OpenAPI) oauthRegister(c *gin.Context) {
var req DynamicClientRegistrationRequest
if err := c.ShouldBindJSON(&req); err != nil {
openapi.respondWithError(c, StatusBadRequest, ErrInvalidClientMetadata)
return
}
// Basic validation
if len(req.RedirectURIs) == 0 {
openapi.respondWithError(c, StatusBadRequest, ErrMissingRedirectURI)
return
}
res, err := openapi.OAuth.DynamicClientRegistration(c, &req)
if err != nil {
openapi.respondWithError(c, StatusBadRequest, ErrInvalidClientMetadata)
return
}
// Return the registration response
openapi.respondWithSuccess(c, StatusCreated, res)
}
// oauthGetClient retrieves client configuration - RFC 7592
func (openapi *OpenAPI) oauthGetClient(c *gin.Context) {}
func (openapi *OpenAPI) oauthGetClient(c *gin.Context) {
clientID := c.Param("client_id")
if clientID == "" {
openapi.respondWithError(c, StatusBadRequest, ErrInvalidRequest)
return
}
// TODO: Implement client retrieval logic
openapi.respondWithError(c, StatusNotFound, ErrInvalidClient)
}
// oauthUpdateClient updates client configuration - RFC 7592
func (openapi *OpenAPI) oauthUpdateClient(c *gin.Context) {}
func (openapi *OpenAPI) oauthUpdateClient(c *gin.Context) {
clientID := c.Param("client_id")
if clientID == "" {
openapi.respondWithError(c, StatusBadRequest, ErrInvalidRequest)
return
}
var req DynamicClientRegistrationRequest
if err := c.ShouldBindJSON(&req); err != nil {
openapi.respondWithError(c, StatusBadRequest, ErrInvalidClientMetadata)
return
}
// TODO: Implement client update logic
openapi.respondWithError(c, StatusNotImplemented, ErrServerError)
}
// oauthDeleteClient deletes client configuration - RFC 7592
func (openapi *OpenAPI) oauthDeleteClient(c *gin.Context) {}
func (openapi *OpenAPI) oauthDeleteClient(c *gin.Context) {
clientID := c.Param("client_id")
if clientID == "" {
openapi.respondWithError(c, StatusBadRequest, ErrInvalidRequest)
return
}
// TODO: Implement client deletion logic
c.Status(StatusNoContent)
}
// oauthDeviceAuthorization handles device authorization - RFC 8628
func (openapi *OpenAPI) oauthDeviceAuthorization(c *gin.Context) {}
func (openapi *OpenAPI) oauthDeviceAuthorization(c *gin.Context) {
clientID := c.PostForm("client_id")
if clientID == "" {
openapi.respondWithTokenError(c, ErrInvalidRequest)
return
}
// TODO: Implement device authorization logic
response := &DeviceAuthorizationResponse{
DeviceCode: "generated-device-code",
UserCode: "USER-CODE",
VerificationURI: "https://example.com/device",
ExpiresIn: 900, // 15 minutes
Interval: 5, // 5 seconds
}
openapi.respondWithTokenSuccess(c, response)
}
// oauthPushedAuthorizationRequest handles PAR - RFC 9126
func (openapi *OpenAPI) oauthPushedAuthorizationRequest(c *gin.Context) {}
func (openapi *OpenAPI) oauthPushedAuthorizationRequest(c *gin.Context) {
var req PushedAuthorizationRequest
if err := c.ShouldBind(&req); err != nil {
openapi.respondWithError(c, StatusBadRequest, ErrInvalidRequest)
return
}
// Basic validation
if req.ClientID == "" {
openapi.respondWithError(c, StatusBadRequest, ErrInvalidRequest)
return
}
// TODO: Implement PAR logic
response := &PushedAuthorizationResponse{
RequestURI: "urn:example:bwc4JK-ESC0w8acc191e-Y1LTC2",
ExpiresIn: 60, // 60 seconds
}
openapi.respondWithSuccess(c, StatusCreated, response)
}
// oauthTokenExchange handles token exchange - RFC 8693
func (openapi *OpenAPI) oauthTokenExchange(c *gin.Context) {}
func (openapi *OpenAPI) oauthTokenExchange(c *gin.Context) {
grantType := c.PostForm("grant_type")
if grantType != types.GrantTypeTokenExchange {
openapi.respondWithTokenError(c, ErrUnsupportedGrantType)
return
}
subjectToken := c.PostForm("subject_token")
if subjectToken == "" {
openapi.respondWithTokenError(c, ErrInvalidRequest)
return
}
// TODO: Implement token exchange logic
response := &TokenExchangeResponse{
AccessToken: "exchanged-access-token",
IssuedTokenType: "urn:ietf:params:oauth:token-type:access_token",
TokenType: types.TokenTypeBearer,
ExpiresIn: 3600, // 1 hour
}
openapi.respondWithTokenSuccess(c, response)
}
// Helper functions for token grant handling
func (openapi *OpenAPI) handleAuthorizationCodeGrant(c *gin.Context) {
code := c.PostForm("code")
redirectURI := c.PostForm("redirect_uri")
clientID := c.PostForm("client_id")
// Basic validation
if code == "" || redirectURI == "" || clientID == "" {
openapi.respondWithTokenError(c, ErrInvalidRequest)
return
}
// TODO: Validate authorization code and PKCE
// TODO: Generate tokens
token := &Token{
AccessToken: "generated-access-token",
TokenType: types.TokenTypeBearer,
ExpiresIn: 3600, // 1 hour
RefreshToken: "generated-refresh-token",
Scope: "openid profile email",
}
// Use OAuth 2.1 compliant response
openapi.respondWithOAuth21TokenSuccess(c, token)
}
func (openapi *OpenAPI) handleRefreshTokenGrant(c *gin.Context) {
refreshToken := c.PostForm("refresh_token")
if refreshToken == "" {
openapi.respondWithTokenError(c, ErrInvalidRequest)
return
}
// TODO: Validate refresh token
// TODO: Generate new tokens
response := &RefreshTokenResponse{
AccessToken: "new-access-token",
TokenType: types.TokenTypeBearer,
ExpiresIn: 3600, // 1 hour
RefreshToken: "new-refresh-token", // OAuth 2.1 requires refresh token rotation
Scope: "openid profile email",
}
openapi.respondWithOAuth21TokenSuccess(c, response)
}
func (openapi *OpenAPI) handleClientCredentialsGrant(c *gin.Context) {
// Client authentication is handled by middleware
scope := c.PostForm("scope")
// TODO: Validate client credentials
// TODO: Generate access token
token := &Token{
AccessToken: "client-credentials-token",
TokenType: types.TokenTypeBearer,
ExpiresIn: 3600, // 1 hour
Scope: scope,
}
openapi.respondWithOAuth21TokenSuccess(c, token)
}
func (openapi *OpenAPI) handleDeviceCodeGrant(c *gin.Context) {
deviceCode := c.PostForm("device_code")
if deviceCode == "" {
openapi.respondWithTokenError(c, ErrInvalidRequest)
return
}
// TODO: Check device code status
// For now, return authorization pending
openapi.respondWithTokenError(c, ErrAuthorizationPending)
}
// parseAuthorizationRequest parses and validates authorization request parameters
func (openapi *OpenAPI) parseAuthorizationRequest(c *gin.Context) (*AuthorizationRequest, *ErrorResponse) {
// Parse authorization request parameters from both GET (query) and POST (form) methods
authReq := &AuthorizationRequest{
ClientID: openapi.getParam(c, "client_id"),
ResponseType: openapi.getParam(c, "response_type"),
RedirectURI: openapi.getParam(c, "redirect_uri"),
Scope: openapi.getParam(c, "scope"),
State: openapi.getParam(c, "state"),
CodeChallenge: openapi.getParam(c, "code_challenge"),
CodeChallengeMethod: openapi.getParam(c, "code_challenge_method"),
Resource: openapi.getParam(c, "resource"),
Nonce: openapi.getParam(c, "nonce"),
}
// Basic validation
if authReq.ClientID == "" {
return authReq, ErrInvalidRequest
}
// Validate response_type parameter - RFC 6749 Section 3.1.1
if authReq.ResponseType == "" {
return authReq, ErrInvalidRequest
}
// Check supported response types
switch authReq.ResponseType {
case types.ResponseTypeCode:
// Authorization code flow - supported
case types.ResponseTypeToken:
// Implicit flow - deprecated in OAuth 2.1, return error
return authReq, ErrUnsupportedResponseType
default:
return authReq, ErrUnsupportedResponseType
}
return authReq, nil
}
// getParam gets parameter from both query string (GET) and form data (POST)
// This supports OAuth 2.0 authorization endpoint which can accept both GET and POST requests
func (openapi *OpenAPI) getParam(c *gin.Context, key string) string {
// First try to get from query parameters (GET request)
if value := c.Query(key); value != "" {
return value
}
// Then try to get from POST form data (POST request)
return c.PostForm(key)
}

1
openapi/oauth_test.go Normal file
View file

@ -0,0 +1 @@
package openapi

View file

@ -1,20 +1,170 @@
package openapi
import (
"context"
"fmt"
"net"
"net/http"
"testing"
"time"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"github.com/yaoapp/yao/config"
"github.com/yaoapp/yao/test"
)
func TestLoad(t *testing.T) {
test.Prepare(t, config.Conf)
defer test.Clean()
// testServer holds the test HTTP server instance
var testServer *http.Server
// Prepare initializes the OpenAPI test environment and starts a mock HTTP server.
//
// AI ASSISTANT INSTRUCTIONS:
// All tests in the openapi package MUST use these utility functions for proper test environment setup.
// This is a preparation utility function, NOT an actual test case.
//
// Usage pattern for ALL openapi tests:
//
// func TestYourFunction(t *testing.T) {
// serverURL := Prepare(t)
// defer Clean()
//
// // Get base URL from server config for correct path construction
// baseURL := ""
// if Server != nil && Server.Config != nil {
// baseURL = Server.Config.BaseURL
// }
//
// // Your actual test code here...
// // Use serverURL + baseURL + endpoint to make HTTP requests
// // Example: http.Get(serverURL + baseURL + "/helloworld/hello")
// // The OpenAPI server will be available as the global Server variable
// }
//
// PREREQUISITES:
// Before running any tests in this package, you MUST execute the following command in your terminal:
//
// source $YAO_SOURCE_ROOT/env.local.sh
//
// This loads the required environment variables for the test environment.
//
// WHAT THIS FUNCTION DOES:
// Step 1: Calls test.Prepare(t, config.Conf) to initialize the base Yao test environment
//
// This sets up database connections, configurations, and other core dependencies
//
// Step 2: Calls Load(config.Conf) to initialize the OpenAPI server instance
//
// This creates the global Server variable that contains the Gin router and all endpoints
//
// Step 3: Creates a Gin router and attaches the OpenAPI server to it
//
// The server uses Server.Config.BaseURL as the base path for all endpoints
//
// Step 4: Starts an HTTP server on a random available port (127.0.0.1:xxxxx)
//
// This allows actual HTTP testing of the OpenAPI endpoints
//
// RETURN VALUE:
// Returns the server URL in format "http://127.0.0.1:xxxxx" where xxxxx is the random port
// NOTE: You need to append Server.Config.BaseURL to construct the full endpoint URL
//
// ERROR HANDLING:
// If any step fails, the test will fail immediately with a descriptive error message.
func Prepare(t *testing.T) string {
// Step 1: Initialize base test environment with all Yao dependencies
test.Prepare(t, config.Conf)
// Step 2: Initialize OpenAPI server and make it available globally
_, err := Load(config.Conf)
if err != nil {
t.Fatal(err)
t.Fatalf("Failed to load OpenAPI server: %v", err)
}
assert.NotNil(t, Server)
// Step 3: Create Gin router and attach OpenAPI server
gin.SetMode(gin.TestMode)
router := gin.New()
// Attach the OpenAPI server to the router
if Server != nil {
Server.Attach(router)
}
// Step 4: Start HTTP server on random available port
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("Failed to create listener: %v", err)
}
testServer = &http.Server{
Handler: router,
}
// Start server in background
go func() {
if err := testServer.Serve(listener); err != nil && err != http.ErrServerClosed {
t.Errorf("Failed to start test server: %v", err)
}
}()
// Wait a moment for server to start
time.Sleep(10 * time.Millisecond)
// Return server URL
serverURL := fmt.Sprintf("http://%s", listener.Addr().String())
return serverURL
}
// Clean cleans up the OpenAPI test environment and shuts down the test server.
//
// AI ASSISTANT INSTRUCTIONS:
// This function MUST be called with defer in every test that uses Prepare().
// This is a cleanup utility function, NOT an actual test case.
// Always use: defer Clean()
//
// WHAT THIS FUNCTION DOES:
// Step 1: Gracefully shutdown the HTTP test server if it exists
//
// This ensures all pending requests are completed and resources are freed
//
// Step 2: Reset the global Server variable to nil
//
// This ensures no state leakage between tests and prevents memory leaks
//
// Step 3: Calls test.Clean() to clean up the base test environment
//
// This closes database connections, cleans up temporary files, and resets global state
//
// IMPORTANT NOTES:
// - This function should ALWAYS be called with defer to ensure cleanup happens even if tests panic
// - Proper cleanup prevents test interference and resource leaks
// - The order of cleanup steps is important: HTTP server first, then OpenAPI cleanup, then base cleanup
// - Server shutdown has a 5-second timeout to prevent hanging tests
func Clean() {
// Step 1: Gracefully shutdown the HTTP test server
if testServer != nil {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := testServer.Shutdown(ctx); err != nil {
// Force close if graceful shutdown fails
testServer.Close()
}
testServer = nil
}
// Step 2: Reset OpenAPI server instance to prevent state leakage
Server = nil
// Step 3: Clean up base test environment and all dependencies
test.Clean()
}
func TestLoad(t *testing.T) {
serverURL := Prepare(t)
defer Clean()
assert.NotNil(t, Server)
assert.NotEmpty(t, serverURL)
assert.Contains(t, serverURL, "http://127.0.0.1:")
}

304
openapi/response.go Normal file
View file

@ -0,0 +1,304 @@
package openapi
import (
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/yaoapp/yao/openapi/oauth/types"
)
// Type aliases for OAuth types to simplify usage
type (
// Core response types
ErrorResponse = types.ErrorResponse
Token = types.Token
RefreshTokenResponse = types.RefreshTokenResponse
// Authorization flow types
AuthorizationRequest = types.AuthorizationRequest
AuthorizationResponse = types.AuthorizationResponse
// Client management types
ClientInfo = types.ClientInfo
DynamicClientRegistrationRequest = types.DynamicClientRegistrationRequest
DynamicClientRegistrationResponse = types.DynamicClientRegistrationResponse
// Extended OAuth types
DeviceAuthorizationResponse = types.DeviceAuthorizationResponse
PushedAuthorizationRequest = types.PushedAuthorizationRequest
PushedAuthorizationResponse = types.PushedAuthorizationResponse
TokenExchangeResponse = types.TokenExchangeResponse
TokenIntrospectionResponse = types.TokenIntrospectionResponse
// Discovery types
AuthorizationServerMetadata = types.AuthorizationServerMetadata
ProtectedResourceMetadata = types.ProtectedResourceMetadata
// Security types
WWWAuthenticateChallenge = types.WWWAuthenticateChallenge
JWKSResponse = types.JWKSResponse
JWK = types.JWK
)
// Standard OAuth 2.0/2.1 Error Codes - RFC 6749 Section 5.2
var (
// Authorization endpoint errors - RFC 6749 Section 4.1.2.1
ErrInvalidRequest = &ErrorResponse{Code: types.ErrorInvalidRequest, ErrorDescription: "The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed."}
ErrUnauthorizedClient = &ErrorResponse{Code: types.ErrorUnauthorizedClient, ErrorDescription: "The client is not authorized to request an authorization code using this method."}
ErrAccessDenied = &ErrorResponse{Code: types.ErrorAccessDenied, ErrorDescription: "The resource owner or authorization server denied the request."}
ErrUnsupportedResponseType = &ErrorResponse{Code: types.ErrorUnsupportedResponseType, ErrorDescription: "The authorization server does not support obtaining an authorization code using this method."}
ErrInvalidScope = &ErrorResponse{Code: types.ErrorInvalidScope, ErrorDescription: "The requested scope is invalid, unknown, or malformed."}
ErrServerError = &ErrorResponse{Code: types.ErrorServerError, ErrorDescription: "The authorization server encountered an unexpected condition that prevented it from fulfilling the request."}
ErrTemporarilyUnavailable = &ErrorResponse{Code: types.ErrorTemporarilyUnavailable, ErrorDescription: "The authorization server is currently unable to handle the request due to a temporary overloading or maintenance of the server."}
// Token endpoint errors - RFC 6749 Section 5.2
ErrInvalidClient = &ErrorResponse{Code: types.ErrorInvalidClient, ErrorDescription: "Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method)."}
ErrInvalidGrant = &ErrorResponse{Code: types.ErrorInvalidGrant, ErrorDescription: "The provided authorization grant (e.g., authorization code, resource owner credentials) or refresh token is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client."}
ErrUnsupportedGrantType = &ErrorResponse{Code: types.ErrorUnsupportedGrantType, ErrorDescription: "The authorization grant type is not supported by the authorization server."}
// Token introspection and validation errors - RFC 7662
ErrInvalidToken = &ErrorResponse{Code: types.ErrorInvalidToken, ErrorDescription: "The access token provided is expired, revoked, malformed, or invalid for other reasons."}
ErrInsufficientScope = &ErrorResponse{Code: types.ErrorInsufficientScope, ErrorDescription: "The request requires higher privileges than provided by the access token."}
// Device authorization flow errors - RFC 8628 Section 3.5
ErrAuthorizationPending = &ErrorResponse{Code: types.ErrorAuthorizationPending, ErrorDescription: "The authorization request is still pending as the end user hasn't yet completed the user-interaction steps."}
ErrSlowDown = &ErrorResponse{Code: types.ErrorSlowDown, ErrorDescription: "The client should slow down the polling requests to the token endpoint."}
ErrExpiredToken = &ErrorResponse{Code: types.ErrorExpiredToken, ErrorDescription: "The device_code has expired, and the device authorization session has concluded."}
// Extended error codes for better developer experience
ErrMissingRedirectURI = &ErrorResponse{Code: "missing_redirect_uri", ErrorDescription: "The redirect_uri parameter is required but was not provided."}
ErrInvalidRedirectURI = &ErrorResponse{Code: "invalid_redirect_uri", ErrorDescription: "The redirect_uri parameter value is invalid or not registered for this client."}
ErrMismatchedRedirectURI = &ErrorResponse{Code: "mismatched_redirect_uri", ErrorDescription: "The redirect_uri does not match the one used in the authorization request."}
ErrInvalidCodeVerifier = &ErrorResponse{Code: "invalid_code_verifier", ErrorDescription: "The code_verifier does not match the code_challenge from the authorization request."}
ErrMissingCodeChallenge = &ErrorResponse{Code: "missing_code_challenge", ErrorDescription: "PKCE code_challenge is required but was not provided."}
ErrInvalidCodeChallenge = &ErrorResponse{Code: "invalid_code_challenge", ErrorDescription: "The code_challenge parameter is invalid or uses an unsupported method."}
ErrInvalidClientMetadata = &ErrorResponse{Code: "invalid_client_metadata", ErrorDescription: "The client metadata is invalid or contains unsupported values."}
ErrInvalidSoftwareStatement = &ErrorResponse{Code: "invalid_software_statement", ErrorDescription: "The software statement is invalid or cannot be verified."}
ErrUnapprovedSoftware = &ErrorResponse{Code: "unapproved_software", ErrorDescription: "The software statement represents software that has been replaced or is otherwise invalid."}
// Configuration and service errors
ErrInvalidConfiguration = types.ErrInvalidConfiguration
ErrStoreMissing = types.ErrStoreMissing
ErrIssuerURLMissing = types.ErrIssuerURLMissing
ErrCertificateMissing = types.ErrCertificateMissing
ErrInvalidTokenLifetime = types.ErrInvalidTokenLifetime
ErrPKCEConfigurationInvalid = types.ErrPKCEConfigurationInvalid
)
// Standard HTTP Status Codes for OAuth Responses
const (
// Success responses
StatusOK = http.StatusOK // 200 - Successful token response
StatusCreated = http.StatusCreated // 201 - Successful client registration
StatusNoContent = http.StatusNoContent // 204 - Successful token revocation
// Client error responses
StatusBadRequest = http.StatusBadRequest // 400 - Invalid request parameters
StatusUnauthorized = http.StatusUnauthorized // 401 - Authentication required
StatusForbidden = http.StatusForbidden // 403 - Access denied
StatusNotFound = http.StatusNotFound // 404 - Client or resource not found
StatusMethodNotAllowed = http.StatusMethodNotAllowed // 405 - HTTP method not supported
StatusNotAcceptable = http.StatusNotAcceptable // 406 - Content type not acceptable
StatusConflict = http.StatusConflict // 409 - Client already exists
StatusUnprocessableEntity = http.StatusUnprocessableEntity // 422 - Invalid client metadata
// Server error responses
StatusInternalServerError = http.StatusInternalServerError // 500 - Internal server error
StatusNotImplemented = http.StatusNotImplemented // 501 - Feature not implemented
StatusBadGateway = http.StatusBadGateway // 502 - Bad gateway
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"`
}
// Response helper functions for consistent OAuth responses
// respondWithSuccess sends a successful OAuth response
func (openapi *OpenAPI) respondWithSuccess(c *gin.Context, statusCode int, data interface{}) {
response := StandardResponse{
Success: true,
Data: data,
Timestamp: time.Now().UTC(),
RequestID: c.GetString("request_id"),
}
c.Header("Cache-Control", "no-store")
c.Header("Pragma", "no-cache")
c.JSON(statusCode, response)
}
// respondWithError sends an OAuth error response
func (openapi *OpenAPI) respondWithError(c *gin.Context, statusCode int, err *ErrorResponse) {
response := StandardResponse{
Success: false,
Error: err,
Timestamp: time.Now().UTC(),
RequestID: c.GetString("request_id"),
}
c.Header("Cache-Control", "no-store")
c.Header("Pragma", "no-cache")
// 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)
func (openapi *OpenAPI) respondWithTokenSuccess(c *gin.Context, token interface{}) {
c.Header("Cache-Control", "no-store")
c.Header("Pragma", "no-cache")
c.Header("Content-Type", "application/json;charset=UTF-8")
c.JSON(StatusOK, token)
}
// respondWithTokenError sends a token endpoint error response (without wrapper)
func (openapi *OpenAPI) respondWithTokenError(c *gin.Context, err *ErrorResponse) {
c.Header("Cache-Control", "no-store")
c.Header("Pragma", "no-cache")
c.Header("Content-Type", "application/json;charset=UTF-8")
c.JSON(StatusBadRequest, err)
}
// respondWithAuthorizationError sends an authorization endpoint error via redirect
func (openapi *OpenAPI) respondWithAuthorizationError(c *gin.Context, redirectURI string, err *ErrorResponse, state string) {
// Build error redirect URL
redirectURL := redirectURI
if redirectURL != "" {
separator := "?"
if len(redirectURL) > 0 && redirectURL[len(redirectURL)-1:] == "?" {
separator = "&"
}
redirectURL += separator + "error=" + err.Code
if err.ErrorDescription != "" {
redirectURL += "&error_description=" + err.ErrorDescription
}
if err.ErrorURI != "" {
redirectURL += "&error_uri=" + err.ErrorURI
}
if state != "" {
redirectURL += "&state=" + state
}
c.Redirect(http.StatusFound, redirectURL)
return
}
// Fallback to JSON error response if no redirect URI
openapi.respondWithError(c, StatusBadRequest, err)
}
// addWWWAuthenticateHeader adds appropriate WWW-Authenticate header
func (openapi *OpenAPI) addWWWAuthenticateHeader(c *gin.Context, err *ErrorResponse) {
challenge := &WWWAuthenticateChallenge{
Scheme: types.WWWAuthenticateSchemeBearer,
Realm: "OAuth",
}
if err != nil {
challenge.Error = err.Code
challenge.ErrorDesc = err.ErrorDescription
challenge.ErrorURI = err.ErrorURI
}
// Build WWW-Authenticate header value
headerValue := challenge.Scheme
if challenge.Realm != "" {
headerValue += ` realm="` + challenge.Realm + `"`
}
if challenge.Error != "" {
headerValue += `, error="` + challenge.Error + `"`
}
if challenge.ErrorDesc != "" {
headerValue += `, error_description="` + challenge.ErrorDesc + `"`
}
if challenge.ErrorURI != "" {
headerValue += `, error_uri="` + challenge.ErrorURI + `"`
}
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
}
// 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
}
// OAuth 2.1 specific response helpers
// respondWithOAuth21Error ensures OAuth 2.1 compliance for error responses
func (openapi *OpenAPI) respondWithOAuth21Error(c *gin.Context, statusCode int, err *ErrorResponse) {
// OAuth 2.1 requires additional security headers
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")
openapi.respondWithError(c, statusCode, err)
}
// respondWithOAuth21TokenSuccess ensures OAuth 2.1 compliance for token responses
func (openapi *OpenAPI) respondWithOAuth21TokenSuccess(c *gin.Context, token interface{}) {
// OAuth 2.1 requires additional security headers
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")
c.Header("Content-Type", "application/json;charset=UTF-8")
c.JSON(StatusOK, token)
}