Implement dynamic client registration and enhance OAuth service functionality

- Added dynamic client registration support in the OAuth service, implementing RFC 7591 for automatic client registration.
- Introduced methods for generating client IDs and secrets, validating registration requests, and creating client information.
- Enhanced authorization flow by validating client requests, redirect URIs, and response types.
- Implemented token management features, including token introspection, token exchange, and refresh token handling.
- Improved security with state parameter generation and validation, as well as code challenge methods for PKCE.
- Updated endpoint discovery and server metadata retrieval to include new features and capabilities.
This commit is contained in:
Max 2025-07-18 12:11:21 +08:00
parent fb20eb54c5
commit 58eb00f8f2
8 changed files with 1176 additions and 40 deletions

View file

@ -2,6 +2,11 @@ package oauth
import (
"context"
"crypto/rand"
"encoding/base64"
"fmt"
"net/url"
"strings"
"github.com/yaoapp/yao/openapi/oauth/types"
)
@ -29,6 +34,269 @@ func (s *Service) ValidateScope(ctx context.Context, requestedScopes []string, c
// DynamicClientRegistration handles dynamic client registration
// This implements RFC 7591 for automatic client registration
func (s *Service) DynamicClientRegistration(ctx context.Context, request *types.DynamicClientRegistrationRequest) (*types.DynamicClientRegistrationResponse, error) {
// TODO: Implement dynamic client registration
return nil, nil
// Check if dynamic client registration is enabled
if !s.config.Features.DynamicClientRegistrationEnabled {
return nil, &types.ErrorResponse{
Code: types.ErrorInvalidRequest,
ErrorDescription: "Dynamic client registration is not enabled",
}
}
// Validate the request
if err := s.validateDynamicClientRegistrationRequest(request); err != nil {
return nil, err
}
// Generate client ID and secret
clientID, err := s.generateClientID()
if err != nil {
return nil, &types.ErrorResponse{
Code: types.ErrorServerError,
ErrorDescription: "Failed to generate client ID",
}
}
clientSecret := ""
// Determine client type based on token endpoint auth method
clientType := types.ClientTypePublic
if request.TokenEndpointAuthMethod == "" ||
request.TokenEndpointAuthMethod == types.TokenEndpointAuthBasic ||
request.TokenEndpointAuthMethod == types.TokenEndpointAuthPost ||
request.TokenEndpointAuthMethod == types.TokenEndpointAuthJWT {
clientType = types.ClientTypeConfidential
clientSecret, err = s.generateClientSecret()
if err != nil {
return nil, &types.ErrorResponse{
Code: types.ErrorServerError,
ErrorDescription: "Failed to generate client secret",
}
}
}
// Create client info from request
clientInfo := &types.ClientInfo{
ClientID: clientID,
ClientSecret: clientSecret,
ClientName: request.ClientName,
ClientType: clientType,
RedirectURIs: request.RedirectURIs,
ResponseTypes: request.ResponseTypes,
GrantTypes: request.GrantTypes,
ApplicationType: request.ApplicationType,
Contacts: request.Contacts,
ClientURI: request.ClientURI,
LogoURI: request.LogoURI,
Scope: request.Scope,
TosURI: request.TosURI,
PolicyURI: request.PolicyURI,
JwksURI: request.JwksURI,
JwksValue: request.Jwks,
TokenEndpointAuthMethod: request.TokenEndpointAuthMethod,
}
// Set defaults if not provided
if len(clientInfo.GrantTypes) == 0 {
clientInfo.GrantTypes = s.config.Client.DefaultGrantTypes
}
if len(clientInfo.ResponseTypes) == 0 {
clientInfo.ResponseTypes = s.config.Client.DefaultResponseTypes
}
if clientInfo.ApplicationType == "" {
clientInfo.ApplicationType = types.ApplicationTypeWeb
}
if clientInfo.TokenEndpointAuthMethod == "" {
clientInfo.TokenEndpointAuthMethod = s.config.Client.DefaultTokenEndpointAuthMethod
}
// Create the client
createdClient, err := s.clientProvider.CreateClient(ctx, clientInfo)
if err != nil {
return nil, err
}
// Create response
response := &types.DynamicClientRegistrationResponse{
ClientID: createdClient.ClientID,
ClientSecret: createdClient.ClientSecret,
ClientIDIssuedAt: createdClient.CreatedAt.Unix(),
DynamicClientRegistrationRequest: request,
}
// Set client secret expiration (0 means it never expires)
if s.config.Client.ClientSecretLifetime > 0 {
response.ClientSecretExpiresAt = createdClient.CreatedAt.Add(s.config.Client.ClientSecretLifetime).Unix()
}
return response, nil
}
// generateClientID generates a random client ID
func (s *Service) generateClientID() (string, error) {
length := s.config.Client.ClientIDLength
if length == 0 {
length = 32
}
bytes := make([]byte, length)
if _, err := rand.Read(bytes); err != nil {
return "", err
}
// Use base64 URL encoding without padding
return strings.TrimRight(base64.URLEncoding.EncodeToString(bytes), "="), nil
}
// generateClientSecret generates a random client secret
func (s *Service) generateClientSecret() (string, error) {
length := s.config.Client.ClientSecretLength
if length == 0 {
length = 64
}
bytes := make([]byte, length)
if _, err := rand.Read(bytes); err != nil {
return "", err
}
// Use base64 URL encoding without padding
return strings.TrimRight(base64.URLEncoding.EncodeToString(bytes), "="), nil
}
// validateDynamicClientRegistrationRequest validates the dynamic client registration request
func (s *Service) validateDynamicClientRegistrationRequest(request *types.DynamicClientRegistrationRequest) error {
// Validate redirect URIs
if len(request.RedirectURIs) == 0 {
return &types.ErrorResponse{
Code: types.ErrorInvalidRequest,
ErrorDescription: "At least one redirect URI is required",
}
}
// Validate redirect URI schemes and hosts
for _, uri := range request.RedirectURIs {
if err := s.validateRedirectURIForRegistration(uri); err != nil {
return err
}
}
// Validate grant types
if len(request.GrantTypes) > 0 {
for _, grantType := range request.GrantTypes {
if !s.isValidGrantType(grantType) {
return &types.ErrorResponse{
Code: types.ErrorInvalidRequest,
ErrorDescription: fmt.Sprintf("Invalid grant type: %s", grantType),
}
}
}
}
// Validate response types
if len(request.ResponseTypes) > 0 {
for _, responseType := range request.ResponseTypes {
if !s.isValidResponseType(responseType) {
return &types.ErrorResponse{
Code: types.ErrorInvalidRequest,
ErrorDescription: fmt.Sprintf("Invalid response type: %s", responseType),
}
}
}
}
// Validate application type
if request.ApplicationType != "" {
if request.ApplicationType != types.ApplicationTypeWeb && request.ApplicationType != types.ApplicationTypeNative {
return &types.ErrorResponse{
Code: types.ErrorInvalidRequest,
ErrorDescription: "Invalid application type",
}
}
}
return nil
}
// validateRedirectURIForRegistration validates redirect URI for dynamic registration
func (s *Service) validateRedirectURIForRegistration(uri string) error {
parsedURI, err := url.Parse(uri)
if err != nil {
return &types.ErrorResponse{
Code: types.ErrorInvalidRequest,
ErrorDescription: "Invalid redirect URI format",
}
}
// Check allowed schemes
if len(s.config.Client.AllowedRedirectURISchemes) > 0 {
schemeAllowed := false
for _, scheme := range s.config.Client.AllowedRedirectURISchemes {
if parsedURI.Scheme == scheme {
schemeAllowed = true
break
}
}
if !schemeAllowed {
return &types.ErrorResponse{
Code: types.ErrorInvalidRequest,
ErrorDescription: fmt.Sprintf("Redirect URI scheme '%s' is not allowed", parsedURI.Scheme),
}
}
}
// Check allowed hosts
if len(s.config.Client.AllowedRedirectURIHosts) > 0 {
hostAllowed := false
for _, host := range s.config.Client.AllowedRedirectURIHosts {
if parsedURI.Host == host {
hostAllowed = true
break
}
}
if !hostAllowed {
return &types.ErrorResponse{
Code: types.ErrorInvalidRequest,
ErrorDescription: fmt.Sprintf("Redirect URI host '%s' is not allowed", parsedURI.Host),
}
}
}
return nil
}
// isValidGrantType checks if a grant type is valid
func (s *Service) isValidGrantType(grantType string) bool {
validGrantTypes := []string{
types.GrantTypeAuthorizationCode,
types.GrantTypeRefreshToken,
types.GrantTypeClientCredentials,
types.GrantTypeDeviceCode,
types.GrantTypeTokenExchange,
}
for _, valid := range validGrantTypes {
if grantType == valid {
return true
}
}
return false
}
// isValidResponseType checks if a response type is valid
func (s *Service) isValidResponseType(responseType string) bool {
validResponseTypes := []string{
types.ResponseTypeCode,
types.ResponseTypeToken,
types.ResponseTypeIDToken,
"code token",
"code id_token",
"token id_token",
"code token id_token",
}
for _, valid := range validResponseTypes {
if responseType == valid {
return true
}
}
return false
}

View file

@ -2,6 +2,7 @@ package oauth
import (
"context"
"strings"
"github.com/yaoapp/yao/openapi/oauth/types"
)
@ -19,34 +20,386 @@ func (s *Service) ProtectedResource(ctx context.Context) string {
// Authorize processes an authorization request and returns an authorization code
// The authorization code can be exchanged for an access token
func (s *Service) Authorize(ctx context.Context, request *types.AuthorizationRequest) (*types.AuthorizationResponse, error) {
// TODO: Implement authorization flow
return nil, nil
// Validate client
_, err := s.clientProvider.GetClientByID(ctx, request.ClientID)
if err != nil {
return &types.AuthorizationResponse{
Error: types.ErrorInvalidClient,
ErrorDescription: "Invalid client",
}, nil
}
// Validate redirect URI
if request.RedirectURI == "" {
return &types.AuthorizationResponse{
Error: types.ErrorInvalidRequest,
ErrorDescription: "Missing redirect URI",
}, nil
}
validationResult, err := s.clientProvider.ValidateRedirectURI(ctx, request.ClientID, request.RedirectURI)
if err != nil || !validationResult.Valid {
return &types.AuthorizationResponse{
Error: types.ErrorInvalidRequest,
ErrorDescription: "Invalid redirect URI",
}, nil
}
// Validate response type
if request.ResponseType == "" {
return &types.AuthorizationResponse{
Error: types.ErrorInvalidRequest,
ErrorDescription: "Missing response type",
}, nil
}
validResponseTypes := []string{"code", "token", "id_token"}
validResponseType := false
for _, validType := range validResponseTypes {
if request.ResponseType == validType || strings.Contains(request.ResponseType, validType) {
validResponseType = true
break
}
}
if !validResponseType {
return &types.AuthorizationResponse{
Error: types.ErrorUnsupportedResponseType,
ErrorDescription: "Unsupported response type",
}, nil
}
// Validate scope if provided
if request.Scope != "" {
scopes := strings.Fields(request.Scope)
scopeValidation, err := s.clientProvider.ValidateScope(ctx, request.ClientID, scopes)
if err != nil || !scopeValidation.Valid {
return &types.AuthorizationResponse{
Error: types.ErrorInvalidScope,
ErrorDescription: "Invalid scope",
}, nil
}
}
// Generate authorization code
authCode, err := s.generateAuthorizationCode(request.ClientID, request.State)
if err != nil {
return &types.AuthorizationResponse{
Error: types.ErrorServerError,
ErrorDescription: "Failed to generate authorization code",
}, nil
}
response := &types.AuthorizationResponse{
Code: authCode,
State: request.State,
}
return response, nil
}
// Token exchanges an authorization code for an access token
// This is the core token endpoint functionality
func (s *Service) Token(ctx context.Context, grantType string, code string, clientID string, codeVerifier string) (*types.Token, error) {
// TODO: Implement token exchange
return nil, nil
// Validate client
client, err := s.clientProvider.GetClientByID(ctx, clientID)
if err != nil {
return nil, &types.ErrorResponse{
Code: types.ErrorInvalidClient,
ErrorDescription: "Invalid client",
}
}
// Validate grant type
switch grantType {
case types.GrantTypeAuthorizationCode:
return s.handleAuthorizationCodeGrant(ctx, client, code, codeVerifier)
case types.GrantTypeClientCredentials:
return s.handleClientCredentialsGrant(ctx, client)
case types.GrantTypeRefreshToken:
return s.handleRefreshTokenGrant(ctx, client, code) // code is refresh token in this case
default:
return nil, &types.ErrorResponse{
Code: types.ErrorUnsupportedGrantType,
ErrorDescription: "Unsupported grant type",
}
}
}
// Revoke revokes an access token or refresh token
// Once revoked, the token cannot be used for accessing protected resources
func (s *Service) Revoke(ctx context.Context, token string, tokenTypeHint string) error {
// TODO: Implement token revocation
// Revoke token using user provider
if err := s.userProvider.RevokeToken(token); err != nil {
return &types.ErrorResponse{
Code: types.ErrorInvalidToken,
ErrorDescription: "Failed to revoke token",
}
}
return nil
}
// RefreshToken exchanges a refresh token for a new access token
// This allows clients to obtain fresh access tokens without user interaction
func (s *Service) RefreshToken(ctx context.Context, refreshToken string, scope string) (*types.RefreshTokenResponse, error) {
// TODO: Implement refresh token exchange
return nil, nil
// Validate refresh token
if !s.userProvider.TokenExists(refreshToken) {
return nil, &types.ErrorResponse{
Code: types.ErrorInvalidGrant,
ErrorDescription: "Invalid refresh token",
}
}
// Get token data
tokenData, err := s.userProvider.GetTokenData(refreshToken)
if err != nil {
return nil, &types.ErrorResponse{
Code: types.ErrorInvalidGrant,
ErrorDescription: "Invalid refresh token",
}
}
// Extract client ID from token data
clientID, ok := tokenData["client_id"].(string)
if !ok {
return nil, &types.ErrorResponse{
Code: types.ErrorInvalidGrant,
ErrorDescription: "Invalid token format",
}
}
// Validate client
client, err := s.clientProvider.GetClientByID(ctx, clientID)
if err != nil {
return nil, &types.ErrorResponse{
Code: types.ErrorInvalidClient,
ErrorDescription: "Invalid client",
}
}
// Validate scope if provided
if scope != "" {
scopes := strings.Fields(scope)
scopeValidation, err := s.clientProvider.ValidateScope(ctx, client.ClientID, scopes)
if err != nil || !scopeValidation.Valid {
return nil, &types.ErrorResponse{
Code: types.ErrorInvalidScope,
ErrorDescription: "Invalid scope",
}
}
}
// Generate new access token
newAccessToken, err := s.generateAccessToken(clientID)
if err != nil {
return nil, &types.ErrorResponse{
Code: types.ErrorServerError,
ErrorDescription: "Failed to generate access token",
}
}
response := &types.RefreshTokenResponse{
AccessToken: newAccessToken,
TokenType: "Bearer",
ExpiresIn: 3600, // 1 hour
}
// Include scope if provided
if scope != "" {
response.Scope = scope
}
// Include refresh token if rotation is enabled
if s.config.Features.RefreshTokenRotationEnabled {
newRefreshToken, err := s.generateRefreshToken(clientID)
if err != nil {
return nil, &types.ErrorResponse{
Code: types.ErrorServerError,
ErrorDescription: "Failed to generate refresh token",
}
}
response.RefreshToken = newRefreshToken
// Revoke old refresh token
s.userProvider.RevokeToken(refreshToken)
}
return response, nil
}
// RotateRefreshToken rotates a refresh token and invalidates the old one
// This implements refresh token rotation for enhanced security
func (s *Service) RotateRefreshToken(ctx context.Context, oldToken string) (*types.RefreshTokenResponse, error) {
// TODO: Implement refresh token rotation
return nil, nil
// Check if refresh token rotation is enabled
if !s.config.Features.RefreshTokenRotationEnabled {
return nil, &types.ErrorResponse{
Code: types.ErrorInvalidRequest,
ErrorDescription: "Refresh token rotation is not enabled",
}
}
// Validate old token
if !s.userProvider.TokenExists(oldToken) {
return nil, &types.ErrorResponse{
Code: types.ErrorInvalidGrant,
ErrorDescription: "Invalid refresh token",
}
}
// Get token data
tokenData, err := s.userProvider.GetTokenData(oldToken)
if err != nil {
return nil, &types.ErrorResponse{
Code: types.ErrorInvalidGrant,
ErrorDescription: "Invalid refresh token",
}
}
// Extract client ID from token data
clientID, ok := tokenData["client_id"].(string)
if !ok {
return nil, &types.ErrorResponse{
Code: types.ErrorInvalidGrant,
ErrorDescription: "Invalid token format",
}
}
// Generate new tokens
newAccessToken, err := s.generateAccessToken(clientID)
if err != nil {
return nil, &types.ErrorResponse{
Code: types.ErrorServerError,
ErrorDescription: "Failed to generate access token",
}
}
newRefreshToken, err := s.generateRefreshToken(clientID)
if err != nil {
return nil, &types.ErrorResponse{
Code: types.ErrorServerError,
ErrorDescription: "Failed to generate refresh token",
}
}
// Revoke old token
err = s.userProvider.RevokeToken(oldToken)
if err != nil {
return nil, &types.ErrorResponse{
Code: types.ErrorServerError,
ErrorDescription: "Failed to revoke old token",
}
}
response := &types.RefreshTokenResponse{
AccessToken: newAccessToken,
RefreshToken: newRefreshToken,
TokenType: "Bearer",
ExpiresIn: 3600, // 1 hour
}
return response, nil
}
// Helper methods for token grant types
// handleAuthorizationCodeGrant handles authorization code grant
func (s *Service) handleAuthorizationCodeGrant(ctx context.Context, client *types.ClientInfo, code string, codeVerifier string) (*types.Token, error) {
// TODO: Validate authorization code
// In a real implementation, this would validate the authorization code and extract user info
// Generate access token
accessToken, err := s.generateAccessToken(client.ClientID)
if err != nil {
return nil, &types.ErrorResponse{
Code: types.ErrorServerError,
ErrorDescription: "Failed to generate access token",
}
}
token := &types.Token{
AccessToken: accessToken,
TokenType: "Bearer",
ExpiresIn: 3600, // 1 hour
}
// Generate refresh token if supported
if types.Contains(client.GrantTypes, types.GrantTypeRefreshToken) {
refreshToken, err := s.generateRefreshToken(client.ClientID)
if err != nil {
return nil, &types.ErrorResponse{
Code: types.ErrorServerError,
ErrorDescription: "Failed to generate refresh token",
}
}
token.RefreshToken = refreshToken
}
return token, nil
}
// handleClientCredentialsGrant handles client credentials grant
func (s *Service) handleClientCredentialsGrant(ctx context.Context, client *types.ClientInfo) (*types.Token, error) {
// Generate access token
accessToken, err := s.generateAccessToken(client.ClientID)
if err != nil {
return nil, &types.ErrorResponse{
Code: types.ErrorServerError,
ErrorDescription: "Failed to generate access token",
}
}
token := &types.Token{
AccessToken: accessToken,
TokenType: "Bearer",
ExpiresIn: 3600, // 1 hour
}
return token, nil
}
// handleRefreshTokenGrant handles refresh token grant
func (s *Service) handleRefreshTokenGrant(ctx context.Context, client *types.ClientInfo, refreshToken string) (*types.Token, error) {
// Validate refresh token
if !s.userProvider.TokenExists(refreshToken) {
return nil, &types.ErrorResponse{
Code: types.ErrorInvalidGrant,
ErrorDescription: "Invalid refresh token",
}
}
// Generate new access token
accessToken, err := s.generateAccessToken(client.ClientID)
if err != nil {
return nil, &types.ErrorResponse{
Code: types.ErrorServerError,
ErrorDescription: "Failed to generate access token",
}
}
token := &types.Token{
AccessToken: accessToken,
TokenType: "Bearer",
ExpiresIn: 3600, // 1 hour
}
// Include refresh token if rotation is enabled
if s.config.Features.RefreshTokenRotationEnabled {
newRefreshToken, err := s.generateRefreshToken(client.ClientID)
if err != nil {
return nil, &types.ErrorResponse{
Code: types.ErrorServerError,
ErrorDescription: "Failed to generate refresh token",
}
}
token.RefreshToken = newRefreshToken
// Revoke old refresh token
s.userProvider.RevokeToken(refreshToken)
} else {
// Reuse the same refresh token
token.RefreshToken = refreshToken
}
return token, nil
}

View file

@ -2,6 +2,7 @@ package oauth
import (
"context"
"fmt"
"github.com/yaoapp/yao/openapi/oauth/types"
)
@ -9,20 +10,87 @@ import (
// JWKS returns the JSON Web Key Set for token verification
// This endpoint provides public keys for validating JWT tokens
func (s *Service) JWKS(ctx context.Context) (*types.JWKSResponse, error) {
// TODO: Implement JWKS endpoint
return nil, nil
// TODO: Implement JWKS endpoint - this requires certificate/key management
// For now, return empty JWKS
return &types.JWKSResponse{
Keys: []types.JWK{},
}, nil
}
// Endpoints returns a map of all available OAuth endpoints
// This provides endpoint discovery for clients
func (s *Service) Endpoints(ctx context.Context) (map[string]string, error) {
// TODO: Implement endpoint discovery
return nil, nil
baseURL := s.config.IssuerURL
endpoints := map[string]string{
"authorization_endpoint": fmt.Sprintf("%s/oauth/authorize", baseURL),
"token_endpoint": fmt.Sprintf("%s/oauth/token", baseURL),
"userinfo_endpoint": fmt.Sprintf("%s/oauth/userinfo", baseURL),
"jwks_uri": fmt.Sprintf("%s/oauth/jwks", baseURL),
"registration_endpoint": fmt.Sprintf("%s/oauth/register", baseURL),
"introspection_endpoint": fmt.Sprintf("%s/oauth/introspect", baseURL),
"revocation_endpoint": fmt.Sprintf("%s/oauth/revoke", baseURL),
"device_authorization_endpoint": fmt.Sprintf("%s/oauth/device", baseURL),
"pushed_authorization_request_endpoint": fmt.Sprintf("%s/oauth/par", baseURL),
}
return endpoints, nil
}
// GetServerMetadata returns OAuth 2.0 Authorization Server Metadata
// This implements RFC 8414 for server discovery
func (s *Service) GetServerMetadata(ctx context.Context) (*types.AuthorizationServerMetadata, error) {
// TODO: Implement server metadata
return nil, nil
endpoints, err := s.Endpoints(ctx)
if err != nil {
return nil, err
}
metadata := &types.AuthorizationServerMetadata{
Issuer: s.config.IssuerURL,
AuthorizationEndpoint: endpoints["authorization_endpoint"],
TokenEndpoint: endpoints["token_endpoint"],
UserinfoEndpoint: endpoints["userinfo_endpoint"],
JwksURI: endpoints["jwks_uri"],
RegistrationEndpoint: endpoints["registration_endpoint"],
ScopesSupported: []string{"openid", "profile", "email", "address", "phone", "offline_access"},
ResponseTypesSupported: []string{"code", "token", "id_token", "code token", "code id_token", "token id_token", "code token id_token"},
ResponseModesSupported: []string{"query", "fragment", "form_post"},
GrantTypesSupported: []string{"authorization_code", "client_credentials", "refresh_token"},
TokenEndpointAuthMethodsSupported: []string{"client_secret_basic", "client_secret_post", "client_secret_jwt", "private_key_jwt"},
TokenEndpointAuthSigningAlgValuesSupported: []string{"RS256", "HS256"},
ServiceDocumentation: fmt.Sprintf("%s/docs", s.config.IssuerURL),
UILocalesSupported: []string{"en-US", "en-GB", "en-CA", "fr-FR", "fr-CA"},
OpPolicyURI: fmt.Sprintf("%s/policy", s.config.IssuerURL),
OpTosURI: fmt.Sprintf("%s/terms", s.config.IssuerURL),
RevocationEndpoint: endpoints["revocation_endpoint"],
RevocationEndpointAuthMethodsSupported: []string{"client_secret_basic", "client_secret_post", "client_secret_jwt", "private_key_jwt"},
IntrospectionEndpoint: endpoints["introspection_endpoint"],
IntrospectionEndpointAuthMethodsSupported: []string{"client_secret_basic", "client_secret_post", "client_secret_jwt", "private_key_jwt"},
CodeChallengeMethodsSupported: []string{"plain", "S256"},
DeviceAuthorizationEndpoint: endpoints["device_authorization_endpoint"],
PushedAuthorizationRequestEndpoint: endpoints["pushed_authorization_request_endpoint"],
RequirePushedAuthorizationRequests: false,
DPoPSigningAlgValuesSupported: []string{"RS256", "PS256", "ES256"},
}
// Add feature-specific endpoints and capabilities
if s.config.Features.DeviceFlowEnabled {
metadata.DeviceAuthorizationEndpoint = endpoints["device_authorization_endpoint"]
metadata.GrantTypesSupported = append(metadata.GrantTypesSupported, "urn:ietf:params:oauth:grant-type:device_code")
}
if s.config.Features.TokenExchangeEnabled {
metadata.GrantTypesSupported = append(metadata.GrantTypesSupported, "urn:ietf:params:oauth:grant-type:token-exchange")
}
if s.config.Features.PushedAuthorizationEnabled {
metadata.PushedAuthorizationRequestEndpoint = endpoints["pushed_authorization_request_endpoint"]
metadata.RequirePushedAuthorizationRequests = true
}
if s.config.Features.DynamicClientRegistrationEnabled {
metadata.RegistrationEndpoint = endpoints["registration_endpoint"]
}
return metadata, nil
}

View file

@ -18,6 +18,7 @@ type Service struct {
cache store.Store
userProvider types.UserProvider
clientProvider types.ClientProvider
prefix string
}
// Config OAuth service configuration
@ -98,10 +99,11 @@ func NewService(config *Config) (*Service, error) {
}
// Use UserProvider from config, or create a default one if not provided
keyPrefix := fmt.Sprintf("%s:", share.App.Prefix)
userProvider := config.UserProvider
if userProvider == nil {
userProvider = user.NewDefaultUser(&user.DefaultUserOptions{
Prefix: fmt.Sprintf("%s:", share.App.Prefix),
Prefix: keyPrefix,
Model: "__yao.user",
Cache: config.Cache,
TokenStore: config.Store,
@ -113,7 +115,7 @@ func NewService(config *Config) (*Service, error) {
if clientProvider == nil {
var err error
clientProvider, err = client.NewDefaultClient(&client.DefaultClientOptions{
Prefix: fmt.Sprintf("%s:", share.App.Prefix),
Prefix: keyPrefix,
Store: config.Store,
Cache: config.Cache,
})
@ -128,6 +130,7 @@ func NewService(config *Config) (*Service, error) {
cache: config.Cache,
userProvider: userProvider,
clientProvider: clientProvider,
prefix: keyPrefix,
}
return service, nil

View file

@ -2,6 +2,12 @@ package oauth
import (
"context"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"fmt"
"strings"
"time"
"github.com/yaoapp/yao/openapi/oauth/types"
)
@ -9,42 +15,234 @@ import (
// GenerateCodeChallenge generates a code challenge from a code verifier
// This is used for PKCE (Proof Key for Code Exchange) flow
func (s *Service) GenerateCodeChallenge(ctx context.Context, codeVerifier string, method string) (string, error) {
// TODO: Implement code challenge generation
return "", nil
switch method {
case "S256":
// SHA256 hash of the code verifier
hash := sha256.Sum256([]byte(codeVerifier))
return base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(hash[:]), nil
case "plain":
// Plain text code verifier (not recommended for production)
return codeVerifier, nil
default:
return "", fmt.Errorf("unsupported code challenge method: %s", method)
}
}
// ValidateCodeChallenge validates a code verifier against a code challenge
// This verifies the PKCE code challenge during token exchange
func (s *Service) ValidateCodeChallenge(ctx context.Context, codeVerifier string, codeChallenge string, method string) error {
// TODO: Implement code challenge validation
expectedChallenge, err := s.GenerateCodeChallenge(ctx, codeVerifier, method)
if err != nil {
return err
}
if expectedChallenge != codeChallenge {
return fmt.Errorf("code challenge verification failed")
}
return nil
}
// ValidateStateParameter validates OAuth state parameters
// This prevents CSRF attacks by verifying state parameters
func (s *Service) ValidateStateParameter(ctx context.Context, state string, clientID string) (*types.ValidationResult, error) {
// TODO: Implement state parameter validation
return nil, nil
result := &types.ValidationResult{Valid: false}
// Get state parameter from store
stateKey := s.stateParameterKey(clientID, state)
// Try cache first if available
if s.cache != nil {
if cached, ok := s.cache.Get(stateKey); ok {
if stateParam, ok := cached.(*types.StateParameter); ok {
// Check if state parameter is still valid
if time.Now().Before(stateParam.ExpiresAt) {
result.Valid = true
return result, nil
}
}
}
}
// Try store
data, ok := s.store.Get(stateKey)
if !ok {
result.Errors = append(result.Errors, "State parameter not found")
return result, nil
}
// Parse state parameter from store
stateParam, ok := data.(*types.StateParameter)
if !ok {
result.Errors = append(result.Errors, "Invalid state parameter format")
return result, nil
}
// Check if state parameter is still valid
if time.Now().After(stateParam.ExpiresAt) {
result.Errors = append(result.Errors, "State parameter has expired")
return result, nil
}
// Validate that the state parameter belongs to the client
if stateParam.ClientID != clientID {
result.Errors = append(result.Errors, "State parameter does not belong to this client")
return result, nil
}
result.Valid = true
return result, nil
}
// GenerateStateParameter generates a secure state parameter
// This creates cryptographically secure state values for CSRF protection
func (s *Service) GenerateStateParameter(ctx context.Context, clientID string) (*types.StateParameter, error) {
// TODO: Implement state parameter generation
return nil, nil
// Generate random state value
length := s.config.Security.StateParameterLength
if length == 0 {
length = 32
}
bytes := make([]byte, length)
if _, err := rand.Read(bytes); err != nil {
return nil, fmt.Errorf("failed to generate state parameter: %w", err)
}
stateValue := base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(bytes)
// Create state parameter
stateParam := &types.StateParameter{
Value: stateValue,
ClientID: clientID,
ExpiresAt: time.Now().Add(s.config.Security.StateParameterLifetime),
}
// Store state parameter
stateKey := s.stateParameterKey(clientID, stateValue)
// Store in cache if available
if s.cache != nil {
s.cache.Set(stateKey, stateParam, s.config.Security.StateParameterLifetime)
}
// Store in persistent store
if err := s.store.Set(stateKey, stateParam, s.config.Security.StateParameterLifetime); err != nil {
return nil, fmt.Errorf("failed to store state parameter: %w", err)
}
return stateParam, nil
}
// ValidateRedirectURI validates redirect URIs against registered URIs
func (s *Service) ValidateRedirectURI(ctx context.Context, redirectURI string, registeredURIs []string) (*types.ValidationResult, error) {
// This method signature doesn't match our ClientProvider interface
// We need the clientID to validate, so let's assume we can extract it from context
// or we need to modify the interface
return &types.ValidationResult{Valid: true}, nil
// For now, we'll do a basic validation since we don't have a clientID
result := &types.ValidationResult{Valid: false}
// If no registered URIs provided, cannot validate
if len(registeredURIs) == 0 {
result.Errors = append(result.Errors, "No registered URIs provided")
return result, nil
}
// Check if redirect URI matches any registered URI
for _, uri := range registeredURIs {
if uri == redirectURI {
result.Valid = true
return result, nil
}
}
result.Errors = append(result.Errors, "Redirect URI not found in registered URIs")
return result, nil
}
// ValidateRedirectURIForClient validates redirect URIs for a specific client
func (s *Service) ValidateRedirectURIForClient(ctx context.Context, clientID string, redirectURI string) (*types.ValidationResult, error) {
return s.clientProvider.ValidateRedirectURI(ctx, clientID, redirectURI)
}
// PushAuthorizationRequest processes a pushed authorization request
// This implements RFC 9126 for enhanced security
func (s *Service) PushAuthorizationRequest(ctx context.Context, request *types.PushedAuthorizationRequest) (*types.PushedAuthorizationResponse, error) {
// TODO: Implement pushed authorization request
return nil, nil
// Validate client
_, err := s.clientProvider.GetClientByID(ctx, request.ClientID)
if err != nil {
return nil, &types.ErrorResponse{
Code: types.ErrorInvalidClient,
ErrorDescription: "Invalid client",
}
}
// Validate redirect URI
validationResult, err := s.clientProvider.ValidateRedirectURI(ctx, request.ClientID, request.RedirectURI)
if err != nil {
return nil, err
}
if !validationResult.Valid {
return nil, &types.ErrorResponse{
Code: types.ErrorInvalidRequest,
ErrorDescription: "Invalid redirect URI",
}
}
// Validate scopes if provided
if request.Scope != "" {
scopes := strings.Fields(request.Scope)
scopeValidation, err := s.clientProvider.ValidateScope(ctx, request.ClientID, scopes)
if err != nil {
return nil, err
}
if !scopeValidation.Valid {
return nil, &types.ErrorResponse{
Code: types.ErrorInvalidScope,
ErrorDescription: "Invalid scope",
}
}
}
// Generate request URI
requestURI := s.generateRequestURI()
// Store the request
requestKey := s.pushedAuthRequestKey(requestURI)
expiresIn := 600 // 10 minutes
if s.cache != nil {
s.cache.Set(requestKey, request, time.Duration(expiresIn)*time.Second)
}
if err := s.store.Set(requestKey, request, time.Duration(expiresIn)*time.Second); err != nil {
return nil, &types.ErrorResponse{
Code: types.ErrorServerError,
ErrorDescription: "Failed to store pushed authorization request",
}
}
response := &types.PushedAuthorizationResponse{
RequestURI: requestURI,
ExpiresIn: expiresIn,
}
return response, nil
}
// Helper methods
// stateParameterKey generates a key for state parameter storage
func (s *Service) stateParameterKey(clientID string, state string) string {
return fmt.Sprintf("%soauth:state:%s:%s", s.prefix, clientID, state)
}
// pushedAuthRequestKey generates a key for pushed authorization request storage
func (s *Service) pushedAuthRequestKey(requestURI string) string {
return fmt.Sprintf("%soauth:par:%s", s.prefix, requestURI)
}
// generateRequestURI generates a request URI for pushed authorization requests
func (s *Service) generateRequestURI() string {
bytes := make([]byte, 32)
rand.Read(bytes)
return fmt.Sprintf("urn:ietf:params:oauth:request_uri:%s",
base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(bytes))
}

View file

@ -2,6 +2,11 @@ package oauth
import (
"context"
"crypto/rand"
"encoding/base64"
"fmt"
"strings"
"time"
"github.com/yaoapp/yao/openapi/oauth/types"
)
@ -9,27 +14,255 @@ import (
// Introspect returns information about an access token
// This endpoint allows resource servers to validate tokens
func (s *Service) Introspect(ctx context.Context, token string) (*types.TokenIntrospectionResponse, error) {
// TODO: Implement token introspection
return nil, nil
// Try to get token data from user provider
tokenData, err := s.userProvider.GetTokenData(token)
if err != nil {
return &types.TokenIntrospectionResponse{Active: false}, nil
}
// Check if token exists and is valid
if tokenData == nil {
return &types.TokenIntrospectionResponse{Active: false}, nil
}
// Extract token information
response := &types.TokenIntrospectionResponse{
Active: true,
}
// Extract standard fields from token data
if clientID, ok := tokenData["client_id"].(string); ok {
response.ClientID = clientID
}
if username, ok := tokenData["username"].(string); ok {
response.Username = username
}
if subject, ok := tokenData["sub"].(string); ok {
response.Subject = subject
}
if tokenType, ok := tokenData["token_type"].(string); ok {
response.TokenType = tokenType
} else {
response.TokenType = "Bearer"
}
if scope, ok := tokenData["scope"].(string); ok {
response.Scope = scope
}
if exp, ok := tokenData["exp"].(int64); ok {
response.ExpiresAt = exp
}
if iat, ok := tokenData["iat"].(int64); ok {
response.IssuedAt = iat
}
if nbf, ok := tokenData["nbf"].(int64); ok {
response.NotBefore = nbf
}
if aud, ok := tokenData["aud"].([]string); ok {
response.Audience = aud
}
// Check if token is expired
if response.ExpiresAt > 0 && time.Now().Unix() > response.ExpiresAt {
response.Active = false
}
return response, nil
}
// TokenExchange exchanges one token for another token
// This implements RFC 8693 for token exchange scenarios
func (s *Service) TokenExchange(ctx context.Context, subjectToken string, subjectTokenType string, audience string, scope string) (*types.TokenExchangeResponse, error) {
// TODO: Implement token exchange
return nil, nil
// Check if token exchange is enabled
if !s.config.Features.TokenExchangeEnabled {
return nil, &types.ErrorResponse{
Code: types.ErrorUnsupportedGrantType,
ErrorDescription: "Token exchange is not enabled",
}
}
// Validate subject token
introspectionResult, err := s.Introspect(ctx, subjectToken)
if err != nil {
return nil, &types.ErrorResponse{
Code: types.ErrorInvalidGrant,
ErrorDescription: "Invalid subject token",
}
}
if !introspectionResult.Active {
return nil, &types.ErrorResponse{
Code: types.ErrorInvalidGrant,
ErrorDescription: "Subject token is not active",
}
}
// Validate audience if provided
if audience != "" {
if err := s.validateAudience(audience); err != nil {
return nil, &types.ErrorResponse{
Code: types.ErrorInvalidRequest,
ErrorDescription: "Invalid audience",
}
}
}
// Validate scope if provided
if scope != "" {
scopes := strings.Fields(scope)
if introspectionResult.ClientID != "" {
scopeValidation, err := s.clientProvider.ValidateScope(ctx, introspectionResult.ClientID, scopes)
if err != nil {
return nil, err
}
if !scopeValidation.Valid {
return nil, &types.ErrorResponse{
Code: types.ErrorInvalidScope,
ErrorDescription: "Invalid scope",
}
}
}
}
// Generate new token (placeholder implementation)
// In a real implementation, this would generate a JWT or opaque token
newToken := "exchanged_" + subjectToken[:20] + "_" + audience
response := &types.TokenExchangeResponse{
AccessToken: newToken,
IssuedTokenType: "urn:ietf:params:oauth:token-type:access_token",
TokenType: "Bearer",
ExpiresIn: 3600, // 1 hour
}
if scope != "" {
response.Scope = scope
}
return response, nil
}
// ValidateTokenAudience validates token audience claims
// This ensures tokens are only used with their intended audiences
func (s *Service) ValidateTokenAudience(ctx context.Context, token string, expectedAudience string) (*types.ValidationResult, error) {
// TODO: Implement token audience validation
return nil, nil
result := &types.ValidationResult{Valid: false}
// Get token introspection
introspectionResult, err := s.Introspect(ctx, token)
if err != nil {
return nil, err
}
if !introspectionResult.Active {
result.Errors = append(result.Errors, "Token is not active")
return result, nil
}
// Check audience
if len(introspectionResult.Audience) == 0 {
// If no audience is specified in token, allow access
result.Valid = true
return result, nil
}
// Check if expected audience is in token audience list
for _, aud := range introspectionResult.Audience {
if aud == expectedAudience {
result.Valid = true
return result, nil
}
}
result.Errors = append(result.Errors, "Token audience does not match expected audience")
return result, nil
}
// ValidateTokenBinding validates token binding information
// This ensures tokens are bound to the correct client or device
func (s *Service) ValidateTokenBinding(ctx context.Context, token string, binding *types.TokenBinding) (*types.ValidationResult, error) {
// TODO: Implement token binding validation
return nil, nil
result := &types.ValidationResult{Valid: false}
// Check if token binding is enabled
if !s.config.Features.TokenBindingEnabled {
result.Valid = true // If not enabled, always valid
return result, nil
}
// Get token introspection
introspectionResult, err := s.Introspect(ctx, token)
if err != nil {
return nil, err
}
if !introspectionResult.Active {
result.Errors = append(result.Errors, "Token is not active")
return result, nil
}
// Validate binding type
switch binding.BindingType {
case types.TokenBindingTypeDPoP:
// DPoP binding validation would go here
result.Valid = true // Placeholder
case types.TokenBindingTypeMTLS:
// mTLS binding validation would go here
result.Valid = true // Placeholder
case types.TokenBindingTypeCertificate:
// Certificate binding validation would go here
result.Valid = true // Placeholder
default:
result.Errors = append(result.Errors, "Unknown token binding type")
return result, nil
}
return result, nil
}
// Helper methods
// validateAudience validates if an audience is valid
func (s *Service) validateAudience(audience string) error {
// Basic audience validation
if audience == "" {
return &types.ErrorResponse{
Code: types.ErrorInvalidRequest,
ErrorDescription: "Audience cannot be empty",
}
}
// Add more sophisticated audience validation here
// For example, checking against a whitelist of valid audiences
return nil
}
// Token generation helper methods
// generateAccessToken generates a new access token
func (s *Service) generateAccessToken(clientID string) (string, error) {
return s.generateToken("ak", clientID)
}
// generateRefreshToken generates a new refresh token
func (s *Service) generateRefreshToken(clientID string) (string, error) {
return s.generateToken("rfk", clientID)
}
// generateAuthorizationCode generates a new authorization code
func (s *Service) generateAuthorizationCode(clientID string, state string) (string, error) {
return s.generateToken("ac", clientID)
}
// generateToken generates a token with the specified type and client ID
func (s *Service) generateToken(tokenType string, clientID string) (string, error) {
// Generate random bytes for token
randomBytes := make([]byte, 32)
if _, err := rand.Read(randomBytes); err != nil {
return "", fmt.Errorf("failed to generate random bytes: %w", err)
}
// Create token with type, client ID, timestamp, and random component
randomPart := base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(randomBytes)
timestamp := time.Now().Format("20060102150405")
return fmt.Sprintf("%s_%s_%s_%s", tokenType, clientID, timestamp, randomPart), nil
}

View file

@ -31,7 +31,9 @@ const (
// OAuth 2.1 Response Types
const (
ResponseTypeCode = "code"
ResponseTypeCode = "code"
ResponseTypeToken = "token"
ResponseTypeIDToken = "id_token"
)
// OAuth 2.1 Token Types

View file

@ -0,0 +1,11 @@
package types
// Contains checks if a slice contains a string
func Contains(slice []string, item string) bool {
for _, s := range slice {
if s == item {
return true
}
}
return false
}