yao/openapi/oauth.go
Max 517a4cf5b0 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.
2025-07-21 15:34:13 +08:00

429 lines
13 KiB
Go

package openapi
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:
// 1. Discovery endpoints (.well-known) are at the root level, not versioned
// 2. Server metadata correctly returns versioned OAuth endpoint URLs
// 3. MCP clients are configured with the correct base URL for discovery
//
// Example setup:
// - OAuth endpoints: /v1/oauth/authorize, /v1/oauth/token, etc.
// - Discovery endpoints: /.well-known/oauth-authorization-server (root level)
// - MCP client URL: https://server.com/v1/mcp (for MCP protocol)
// - Authorization discovery: https://server.com/.well-known/oauth-authorization-server
func (openapi *OpenAPI) attachOAuth(base *gin.RouterGroup) {
// OAuth Core Endpoints (RFC 6749, OAuth 2.1)
oauth := base.Group("/oauth")
// Authorization endpoint - RFC 6749 Section 3.1
oauth.GET("/authorize", openapi.oauthAuthorize)
oauth.POST("/authorize", openapi.oauthAuthorize) // Support both GET and POST
// Token endpoint - RFC 6749 Section 3.2
oauth.POST("/token", openapi.oauthToken)
// Token revocation endpoint - RFC 7009
oauth.POST("/revoke", openapi.oauthRevoke)
// Token introspection endpoint - RFC 7662
oauth.POST("/introspect", openapi.oauthIntrospect)
// JSON Web Key Set endpoint - RFC 7517
oauth.GET("/jwks", openapi.oauthJWKS)
// UserInfo endpoint - OpenID Connect Core 1.0
oauth.GET("/userinfo", openapi.oauthUserInfo)
oauth.POST("/userinfo", openapi.oauthUserInfo) // Support both GET and POST
// OAuth Extended Endpoints
// Dynamic Client Registration - RFC 7591 (Required by MCP)
oauth.POST("/register", openapi.oauthRegister)
// Client Configuration - RFC 7592
oauth.GET("/register/:client_id", openapi.oauthGetClient)
oauth.PUT("/register/:client_id", openapi.oauthUpdateClient)
oauth.DELETE("/register/:client_id", openapi.oauthDeleteClient)
// Device Authorization Flow - RFC 8628
oauth.POST("/device_authorization", openapi.oauthDeviceAuthorization)
// Pushed Authorization Request - RFC 9126
oauth.POST("/par", openapi.oauthPushedAuthorizationRequest)
// Token Exchange - RFC 8693
oauth.POST("/token_exchange", openapi.oauthTokenExchange)
}
// OAuth Core Endpoints Implementation
// oauthAuthorize handles authorization requests - RFC 6749 Section 3.1
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) {
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) {
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) {
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) {
// 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) {
// 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) {
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) {
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) {
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) {
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) {
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) {
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) {
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)
}