- Improve health check logic in CI workflows for both HTTP and gRPC readiness of the Tai service, ensuring clearer error reporting if the service fails to start. - Update the OAuth Device Flow implementation to support additional claims during device authorization, enhancing the flexibility of the authorization process. - Refactor the `AuthorizeDevice` method to accept extra claims, allowing for more detailed user context during authorization. - Introduce a new utility function to extract bearer tokens from requests, streamlining token handling across the OpenAPI service. These changes enhance the robustness of service readiness checks and improve the OAuth device authorization flow, contributing to a more reliable and flexible authentication mechanism.
309 lines
8.7 KiB
Go
309 lines
8.7 KiB
Go
package oauth
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/yaoapp/gou/store"
|
|
"github.com/yaoapp/yao/openapi/oauth/providers/client"
|
|
"github.com/yaoapp/yao/openapi/oauth/providers/user"
|
|
"github.com/yaoapp/yao/openapi/oauth/types"
|
|
"github.com/yaoapp/yao/share"
|
|
)
|
|
|
|
// OAuth is the global OAuth service
|
|
var OAuth *Service = nil
|
|
|
|
// Service OAuth service
|
|
type Service struct {
|
|
config *Config
|
|
store store.Store
|
|
cache store.Store
|
|
userProvider types.UserProvider
|
|
clientProvider types.ClientProvider
|
|
prefix string
|
|
// Signing certificates for JWT token signing and verification
|
|
signingCerts *SigningCertificates
|
|
}
|
|
|
|
// Config OAuth service configuration
|
|
type Config struct {
|
|
// Core storage interface
|
|
Store store.Store `json:"-"`
|
|
|
|
// Cache store
|
|
Cache store.Store `json:"-"`
|
|
|
|
// User provider interface
|
|
UserProvider types.UserProvider `json:"-"`
|
|
|
|
// Client provider interface
|
|
ClientProvider types.ClientProvider `json:"-"`
|
|
|
|
// Certificate and key management
|
|
Signing types.SigningConfig `json:"signing"`
|
|
|
|
// Token management settings
|
|
Token types.TokenConfig `json:"token"`
|
|
|
|
// Security configuration
|
|
Security types.SecurityConfig `json:"security"`
|
|
|
|
// Default client settings
|
|
Client types.ClientConfig `json:"client"`
|
|
|
|
// Feature flags
|
|
Features FeatureFlags `json:"features"`
|
|
|
|
// OAuth server metadata
|
|
IssuerURL string `json:"issuer_url"` // JWT token issuer URL
|
|
BaseURL string `json:"base_url"` // API route prefix (e.g. "/v1")
|
|
}
|
|
|
|
// FeatureFlags represents feature toggle configuration
|
|
type FeatureFlags struct {
|
|
// OAuth 2.1 features
|
|
OAuth21Enabled bool `json:"oauth21_enabled"`
|
|
PKCEEnforced bool `json:"pkce_enforced"`
|
|
RefreshTokenRotationEnabled bool `json:"refresh_token_rotation_enabled"`
|
|
|
|
// Advanced features
|
|
DeviceFlowEnabled bool `json:"device_flow_enabled"`
|
|
TokenExchangeEnabled bool `json:"token_exchange_enabled"`
|
|
PushedAuthorizationEnabled bool `json:"pushed_authorization_enabled"`
|
|
DynamicClientRegistrationEnabled bool `json:"dynamic_client_registration_enabled"`
|
|
|
|
// MCP features
|
|
MCPComplianceEnabled bool `json:"mcp_compliance_enabled"`
|
|
ResourceParameterEnabled bool `json:"resource_parameter_enabled"`
|
|
|
|
// Security features
|
|
TokenBindingEnabled bool `json:"token_binding_enabled"`
|
|
MTLSEnabled bool `json:"mtls_enabled"`
|
|
DPoPEnabled bool `json:"dpop_enabled"`
|
|
|
|
// Experimental features
|
|
JWTIntrospectionEnabled bool `json:"jwt_introspection_enabled"`
|
|
TokenRevocationEnabled bool `json:"token_revocation_enabled"`
|
|
UserInfoJWTEnabled bool `json:"userinfo_jwt_enabled"`
|
|
}
|
|
|
|
// NewService creates a new OAuth service with the given configuration
|
|
func NewService(config *Config) (*Service, error) {
|
|
if config == nil {
|
|
return nil, types.ErrInvalidConfiguration
|
|
}
|
|
|
|
// Set default values if not provided
|
|
if err := setConfigDefaults(config); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Validate configuration
|
|
if err := validateConfig(config); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// 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: keyPrefix,
|
|
Model: "__yao.user",
|
|
Cache: config.Cache,
|
|
})
|
|
}
|
|
|
|
// Use ClientProvider from config, or create a default one if not provided
|
|
clientProvider := config.ClientProvider
|
|
if clientProvider == nil {
|
|
var err error
|
|
clientProvider, err = client.NewDefaultClient(&client.DefaultClientOptions{
|
|
Prefix: keyPrefix,
|
|
Store: config.Store,
|
|
Cache: config.Cache,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// Load Certificates
|
|
signingCerts, err := LoadSigningCertificates(&config.Signing)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to load signing certificates: %w", err)
|
|
}
|
|
|
|
// Validate the loaded certificates
|
|
if err := signingCerts.ValidateCertificate(); err != nil {
|
|
return nil, fmt.Errorf("certificate validation failed: %w", err)
|
|
}
|
|
|
|
service := &Service{
|
|
config: config,
|
|
store: config.Store,
|
|
cache: config.Cache,
|
|
userProvider: userProvider,
|
|
clientProvider: clientProvider,
|
|
prefix: keyPrefix,
|
|
signingCerts: signingCerts,
|
|
}
|
|
|
|
// Set the global OAuth service
|
|
OAuth = service
|
|
return service, nil
|
|
}
|
|
|
|
// GetConfig returns the service configuration
|
|
func (s *Service) GetConfig() *Config {
|
|
return s.config
|
|
}
|
|
|
|
// GetUserProvider returns the user provider for the service
|
|
func (s *Service) GetUserProvider() (types.UserProvider, error) {
|
|
return s.userProvider, nil
|
|
}
|
|
|
|
// GetClientProvider returns the client provider for the service
|
|
func (s *Service) GetClientProvider() types.ClientProvider {
|
|
return s.clientProvider
|
|
}
|
|
|
|
// GetCache returns the cache for the service
|
|
func (s *Service) GetCache() store.Store {
|
|
return s.cache
|
|
}
|
|
|
|
// GetStore returns the store for the service
|
|
func (s *Service) GetStore() store.Store {
|
|
return s.store
|
|
}
|
|
|
|
// GetKeyPrefix returns the key prefix used for store keys (e.g. "yao_:")
|
|
func (s *Service) GetKeyPrefix() string {
|
|
return s.prefix
|
|
}
|
|
|
|
// GetSecurityConfig returns the security configuration for the service
|
|
func (s *Service) GetSecurityConfig() types.SecurityConfig {
|
|
if s.config == nil {
|
|
return types.SecurityConfig{}
|
|
}
|
|
return s.config.Security
|
|
}
|
|
|
|
// setConfigDefaults sets default values for configuration
|
|
func setConfigDefaults(config *Config) error {
|
|
// Certificate defaults
|
|
if config.Signing.SigningAlgorithm == "" {
|
|
config.Signing.SigningAlgorithm = "RS256"
|
|
}
|
|
|
|
// Token defaults
|
|
if config.Token.AccessTokenLifetime == 0 {
|
|
config.Token.AccessTokenLifetime = time.Hour
|
|
}
|
|
if config.Token.RefreshTokenLifetime == 0 {
|
|
config.Token.RefreshTokenLifetime = 24 * time.Hour
|
|
}
|
|
if config.Token.AuthorizationCodeLifetime == 0 {
|
|
config.Token.AuthorizationCodeLifetime = 10 * time.Minute
|
|
}
|
|
if config.Token.DeviceCodeLifetime == 0 {
|
|
config.Token.DeviceCodeLifetime = 15 * time.Minute
|
|
}
|
|
if config.Token.DeviceCodeLength == 0 {
|
|
config.Token.DeviceCodeLength = 8
|
|
}
|
|
if config.Token.UserCodeLength == 0 {
|
|
config.Token.UserCodeLength = 8
|
|
}
|
|
if config.Token.DeviceCodeInterval == 0 {
|
|
config.Token.DeviceCodeInterval = 5 * time.Second
|
|
}
|
|
if config.Token.AccessTokenFormat == "" {
|
|
config.Token.AccessTokenFormat = "jwt"
|
|
}
|
|
if config.Token.RefreshTokenFormat == "" {
|
|
config.Token.RefreshTokenFormat = "opaque"
|
|
}
|
|
|
|
// Security defaults
|
|
if len(config.Security.PKCECodeChallengeMethod) == 0 {
|
|
config.Security.PKCECodeChallengeMethod = []string{"S256"}
|
|
}
|
|
if config.Security.PKCECodeVerifierLength == 0 {
|
|
config.Security.PKCECodeVerifierLength = 128
|
|
}
|
|
if config.Security.StateParameterLifetime == 0 {
|
|
config.Security.StateParameterLifetime = 10 * time.Minute
|
|
}
|
|
if config.Security.StateParameterLength == 0 {
|
|
config.Security.StateParameterLength = 32
|
|
}
|
|
|
|
// Client defaults
|
|
if config.Client.DefaultClientType == "" {
|
|
config.Client.DefaultClientType = "confidential"
|
|
}
|
|
if config.Client.DefaultTokenEndpointAuthMethod == "" {
|
|
config.Client.DefaultTokenEndpointAuthMethod = "client_secret_basic"
|
|
}
|
|
if len(config.Client.DefaultGrantTypes) == 0 {
|
|
config.Client.DefaultGrantTypes = []string{"authorization_code", "refresh_token"}
|
|
}
|
|
if len(config.Client.DefaultResponseTypes) == 0 {
|
|
config.Client.DefaultResponseTypes = []string{"code"}
|
|
}
|
|
if config.Client.ClientIDLength == 0 {
|
|
config.Client.ClientIDLength = 32
|
|
}
|
|
if config.Client.ClientSecretLength == 0 {
|
|
config.Client.ClientSecretLength = 64
|
|
}
|
|
|
|
// Feature flags defaults - enable OAuth 2.1 features by default
|
|
config.Features.OAuth21Enabled = true
|
|
config.Features.PKCEEnforced = true
|
|
config.Features.RefreshTokenRotationEnabled = true
|
|
config.Features.DeviceFlowEnabled = true
|
|
config.Features.DynamicClientRegistrationEnabled = true
|
|
|
|
return nil
|
|
}
|
|
|
|
// validateConfig validates the configuration
|
|
func validateConfig(config *Config) error {
|
|
if config.Store == nil {
|
|
return types.ErrStoreMissing
|
|
}
|
|
|
|
// Validate issuer URL
|
|
if config.IssuerURL == "" {
|
|
return types.ErrIssuerURLMissing
|
|
}
|
|
|
|
// Certificate configuration validation
|
|
// If both cert and key paths are provided, they must both exist or be empty
|
|
certPathProvided := config.Signing.SigningCertPath != ""
|
|
keyPathProvided := config.Signing.SigningKeyPath != ""
|
|
|
|
if certPathProvided != keyPathProvided {
|
|
return types.ErrCertificateMissing // Both paths must be provided together or not at all
|
|
}
|
|
|
|
// If paths are not provided, temporary certificates will be generated automatically
|
|
|
|
// Validate token configuration
|
|
if config.Token.AccessTokenLifetime <= 0 {
|
|
return types.ErrInvalidTokenLifetime
|
|
}
|
|
|
|
// Validate security configuration
|
|
if config.Security.PKCERequired && len(config.Security.PKCECodeChallengeMethod) == 0 {
|
|
return types.ErrPKCEConfigurationInvalid
|
|
}
|
|
|
|
return nil
|
|
}
|