diff --git a/openapi/hello_test.go b/openapi/hello_test.go new file mode 100644 index 00000000..ab7783c8 --- /dev/null +++ b/openapi/hello_test.go @@ -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) + }) + } +} diff --git a/openapi/oauth.go b/openapi/oauth.go index dc9fd240..df8cabc4 100644 --- a/openapi/oauth.go +++ b/openapi/oauth.go @@ -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) +} diff --git a/openapi/oauth_test.go b/openapi/oauth_test.go new file mode 100644 index 00000000..764d6c82 --- /dev/null +++ b/openapi/oauth_test.go @@ -0,0 +1 @@ +package openapi diff --git a/openapi/openapi_test.go b/openapi/openapi_test.go index f40bb863..54a1374a 100644 --- a/openapi/openapi_test.go +++ b/openapi/openapi_test.go @@ -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:") } diff --git a/openapi/response.go b/openapi/response.go new file mode 100644 index 00000000..80878373 --- /dev/null +++ b/openapi/response.go @@ -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) +}