- 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.
429 lines
13 KiB
Go
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)
|
|
}
|