- 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.
433 lines
17 KiB
Go
433 lines
17 KiB
Go
package openapi
|
|
|
|
import (
|
|
"errors"
|
|
// "fmt"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
jsoniter "github.com/json-iterator/go"
|
|
"github.com/yaoapp/gou/store"
|
|
"github.com/yaoapp/yao/config"
|
|
"github.com/yaoapp/yao/openapi/oauth"
|
|
"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"
|
|
)
|
|
|
|
// Validate validates the configuration
|
|
func (config *Config) Validate() error {
|
|
if config.Providers == nil {
|
|
return errors.New("providers is required")
|
|
}
|
|
|
|
if config.BaseURL == "" {
|
|
return errors.New("baseurl is required")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// MarshalJSON JSON Marshaler
|
|
func (config *Config) MarshalJSON() ([]byte, error) {
|
|
// Convert config to temporary structure with string duration fields
|
|
tempConfig := TempConfig{
|
|
BaseURL: config.BaseURL,
|
|
Store: config.Store,
|
|
Cache: config.Cache,
|
|
Providers: config.Providers,
|
|
}
|
|
|
|
if config.OAuth != nil {
|
|
tempConfig.OAuth = &TempOAuth{
|
|
IssuerURL: config.OAuth.IssuerURL,
|
|
Features: config.OAuth.Features,
|
|
Signing: TempSigningConfig{
|
|
SigningCertPath: convertAbsoluteToRelativePath(config.OAuth.Signing.SigningCertPath, config.root),
|
|
SigningKeyPath: convertAbsoluteToRelativePath(config.OAuth.Signing.SigningKeyPath, config.root),
|
|
SigningKeyPassword: config.OAuth.Signing.SigningKeyPassword,
|
|
SigningAlgorithm: config.OAuth.Signing.SigningAlgorithm,
|
|
VerificationCerts: config.OAuth.Signing.VerificationCerts,
|
|
MTLSClientCACertPath: convertAbsoluteToRelativePath(config.OAuth.Signing.MTLSClientCACertPath, config.root),
|
|
MTLSEnabled: config.OAuth.Signing.MTLSEnabled,
|
|
CertRotationEnabled: config.OAuth.Signing.CertRotationEnabled,
|
|
CertRotationInterval: formatDuration(config.OAuth.Signing.CertRotationInterval),
|
|
},
|
|
Token: TempTokenConfig{
|
|
AccessTokenLifetime: formatDuration(config.OAuth.Token.AccessTokenLifetime),
|
|
AccessTokenFormat: config.OAuth.Token.AccessTokenFormat,
|
|
AccessTokenSigningAlg: config.OAuth.Token.AccessTokenSigningAlg,
|
|
RefreshTokenLifetime: formatDuration(config.OAuth.Token.RefreshTokenLifetime),
|
|
RefreshTokenRotation: config.OAuth.Token.RefreshTokenRotation,
|
|
RefreshTokenFormat: config.OAuth.Token.RefreshTokenFormat,
|
|
AuthorizationCodeLifetime: formatDuration(config.OAuth.Token.AuthorizationCodeLifetime),
|
|
AuthorizationCodeLength: config.OAuth.Token.AuthorizationCodeLength,
|
|
DeviceCodeLifetime: formatDuration(config.OAuth.Token.DeviceCodeLifetime),
|
|
DeviceCodeLength: config.OAuth.Token.DeviceCodeLength,
|
|
UserCodeLength: config.OAuth.Token.UserCodeLength,
|
|
DeviceCodeInterval: formatDuration(config.OAuth.Token.DeviceCodeInterval),
|
|
TokenBindingEnabled: config.OAuth.Token.TokenBindingEnabled,
|
|
SupportedBindingTypes: config.OAuth.Token.SupportedBindingTypes,
|
|
DefaultAudience: config.OAuth.Token.DefaultAudience,
|
|
AudienceValidationMode: config.OAuth.Token.AudienceValidationMode,
|
|
},
|
|
Security: TempSecurityConfig{
|
|
PKCERequired: config.OAuth.Security.PKCERequired,
|
|
PKCECodeChallengeMethod: config.OAuth.Security.PKCECodeChallengeMethod,
|
|
PKCECodeVerifierLength: config.OAuth.Security.PKCECodeVerifierLength,
|
|
StateParameterRequired: config.OAuth.Security.StateParameterRequired,
|
|
StateParameterLifetime: formatDuration(config.OAuth.Security.StateParameterLifetime),
|
|
StateParameterLength: config.OAuth.Security.StateParameterLength,
|
|
RateLimitEnabled: config.OAuth.Security.RateLimitEnabled,
|
|
RateLimitRequests: config.OAuth.Security.RateLimitRequests,
|
|
RateLimitWindow: formatDuration(config.OAuth.Security.RateLimitWindow),
|
|
RateLimitByClientID: config.OAuth.Security.RateLimitByClientID,
|
|
BruteForceProtectionEnabled: config.OAuth.Security.BruteForceProtectionEnabled,
|
|
MaxFailedAttempts: config.OAuth.Security.MaxFailedAttempts,
|
|
LockoutDuration: formatDuration(config.OAuth.Security.LockoutDuration),
|
|
EncryptionKey: config.OAuth.Security.EncryptionKey,
|
|
EncryptionAlgorithm: config.OAuth.Security.EncryptionAlgorithm,
|
|
IPWhitelist: config.OAuth.Security.IPWhitelist,
|
|
IPBlacklist: config.OAuth.Security.IPBlacklist,
|
|
RequireHTTPS: config.OAuth.Security.RequireHTTPS,
|
|
DisableUnsecureEndpoints: config.OAuth.Security.DisableUnsecureEndpoints,
|
|
SecureCookie: config.OAuth.Security.SecureCookie,
|
|
},
|
|
Client: TempClientConfig{
|
|
DefaultClientType: config.OAuth.Client.DefaultClientType,
|
|
DefaultTokenEndpointAuthMethod: config.OAuth.Client.DefaultTokenEndpointAuthMethod,
|
|
DefaultGrantTypes: config.OAuth.Client.DefaultGrantTypes,
|
|
DefaultResponseTypes: config.OAuth.Client.DefaultResponseTypes,
|
|
DefaultScopes: config.OAuth.Client.DefaultScopes,
|
|
ClientIDLength: config.OAuth.Client.ClientIDLength,
|
|
ClientSecretLength: config.OAuth.Client.ClientSecretLength,
|
|
ClientSecretLifetime: formatDuration(config.OAuth.Client.ClientSecretLifetime),
|
|
DynamicRegistrationEnabled: config.OAuth.Client.DynamicRegistrationEnabled,
|
|
AllowedRedirectURISchemes: config.OAuth.Client.AllowedRedirectURISchemes,
|
|
AllowedRedirectURIHosts: config.OAuth.Client.AllowedRedirectURIHosts,
|
|
ClientCertificateRequired: config.OAuth.Client.ClientCertificateRequired,
|
|
ClientCertificateValidation: config.OAuth.Client.ClientCertificateValidation,
|
|
},
|
|
}
|
|
}
|
|
|
|
return jsoniter.Marshal(tempConfig)
|
|
}
|
|
|
|
// UnmarshalJSON JSON Unmarshaler
|
|
func (config *Config) UnmarshalJSON(data []byte) error {
|
|
var tempConfig TempConfig
|
|
err := jsoniter.Unmarshal(data, &tempConfig)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Convert temporary config to final config
|
|
config.BaseURL = tempConfig.BaseURL
|
|
config.Store = tempConfig.Store
|
|
config.Cache = tempConfig.Cache
|
|
config.Providers = tempConfig.Providers
|
|
|
|
if tempConfig.OAuth != nil {
|
|
config.OAuth = &OAuth{
|
|
IssuerURL: tempConfig.OAuth.IssuerURL,
|
|
Features: tempConfig.OAuth.Features,
|
|
}
|
|
|
|
// fmt.Println("----debug----")
|
|
// fmt.Println("tempConfig.OAuth.IssuerURL", tempConfig.OAuth.IssuerURL)
|
|
// fmt.Println("config.OAuth.IssuerURL", config.OAuth.IssuerURL)
|
|
// fmt.Println("----debug----")
|
|
|
|
// Convert signing config with duration parsing
|
|
config.OAuth.Signing = types.SigningConfig{
|
|
SigningCertPath: tempConfig.OAuth.Signing.SigningCertPath,
|
|
SigningKeyPath: tempConfig.OAuth.Signing.SigningKeyPath,
|
|
SigningKeyPassword: tempConfig.OAuth.Signing.SigningKeyPassword,
|
|
SigningAlgorithm: tempConfig.OAuth.Signing.SigningAlgorithm,
|
|
VerificationCerts: tempConfig.OAuth.Signing.VerificationCerts,
|
|
MTLSClientCACertPath: tempConfig.OAuth.Signing.MTLSClientCACertPath,
|
|
MTLSEnabled: tempConfig.OAuth.Signing.MTLSEnabled,
|
|
CertRotationEnabled: tempConfig.OAuth.Signing.CertRotationEnabled,
|
|
}
|
|
if tempConfig.OAuth.Signing.CertRotationInterval != "" {
|
|
if duration, err := parseDuration(tempConfig.OAuth.Signing.CertRotationInterval); err == nil {
|
|
config.OAuth.Signing.CertRotationInterval = duration
|
|
}
|
|
}
|
|
|
|
// Convert token config with duration parsing
|
|
config.OAuth.Token = types.TokenConfig{
|
|
AccessTokenFormat: tempConfig.OAuth.Token.AccessTokenFormat,
|
|
AccessTokenSigningAlg: tempConfig.OAuth.Token.AccessTokenSigningAlg,
|
|
RefreshTokenRotation: tempConfig.OAuth.Token.RefreshTokenRotation,
|
|
RefreshTokenFormat: tempConfig.OAuth.Token.RefreshTokenFormat,
|
|
AuthorizationCodeLength: tempConfig.OAuth.Token.AuthorizationCodeLength,
|
|
DeviceCodeLength: tempConfig.OAuth.Token.DeviceCodeLength,
|
|
UserCodeLength: tempConfig.OAuth.Token.UserCodeLength,
|
|
TokenBindingEnabled: tempConfig.OAuth.Token.TokenBindingEnabled,
|
|
SupportedBindingTypes: tempConfig.OAuth.Token.SupportedBindingTypes,
|
|
DefaultAudience: tempConfig.OAuth.Token.DefaultAudience,
|
|
AudienceValidationMode: tempConfig.OAuth.Token.AudienceValidationMode,
|
|
}
|
|
if tempConfig.OAuth.Token.AccessTokenLifetime != "" {
|
|
if duration, err := parseDuration(tempConfig.OAuth.Token.AccessTokenLifetime); err == nil {
|
|
config.OAuth.Token.AccessTokenLifetime = duration
|
|
}
|
|
}
|
|
if tempConfig.OAuth.Token.RefreshTokenLifetime != "" {
|
|
if duration, err := parseDuration(tempConfig.OAuth.Token.RefreshTokenLifetime); err == nil {
|
|
config.OAuth.Token.RefreshTokenLifetime = duration
|
|
}
|
|
}
|
|
if tempConfig.OAuth.Token.AuthorizationCodeLifetime != "" {
|
|
if duration, err := parseDuration(tempConfig.OAuth.Token.AuthorizationCodeLifetime); err == nil {
|
|
config.OAuth.Token.AuthorizationCodeLifetime = duration
|
|
}
|
|
}
|
|
if tempConfig.OAuth.Token.DeviceCodeLifetime != "" {
|
|
if duration, err := parseDuration(tempConfig.OAuth.Token.DeviceCodeLifetime); err == nil {
|
|
config.OAuth.Token.DeviceCodeLifetime = duration
|
|
}
|
|
}
|
|
if tempConfig.OAuth.Token.DeviceCodeInterval != "" {
|
|
if duration, err := parseDuration(tempConfig.OAuth.Token.DeviceCodeInterval); err == nil {
|
|
config.OAuth.Token.DeviceCodeInterval = duration
|
|
}
|
|
}
|
|
|
|
// Convert security config with duration parsing
|
|
config.OAuth.Security = types.SecurityConfig{
|
|
PKCERequired: tempConfig.OAuth.Security.PKCERequired,
|
|
PKCECodeChallengeMethod: tempConfig.OAuth.Security.PKCECodeChallengeMethod,
|
|
PKCECodeVerifierLength: tempConfig.OAuth.Security.PKCECodeVerifierLength,
|
|
StateParameterRequired: tempConfig.OAuth.Security.StateParameterRequired,
|
|
StateParameterLength: tempConfig.OAuth.Security.StateParameterLength,
|
|
RateLimitEnabled: tempConfig.OAuth.Security.RateLimitEnabled,
|
|
RateLimitRequests: tempConfig.OAuth.Security.RateLimitRequests,
|
|
RateLimitByClientID: tempConfig.OAuth.Security.RateLimitByClientID,
|
|
BruteForceProtectionEnabled: tempConfig.OAuth.Security.BruteForceProtectionEnabled,
|
|
MaxFailedAttempts: tempConfig.OAuth.Security.MaxFailedAttempts,
|
|
EncryptionKey: tempConfig.OAuth.Security.EncryptionKey,
|
|
EncryptionAlgorithm: tempConfig.OAuth.Security.EncryptionAlgorithm,
|
|
IPWhitelist: tempConfig.OAuth.Security.IPWhitelist,
|
|
IPBlacklist: tempConfig.OAuth.Security.IPBlacklist,
|
|
RequireHTTPS: tempConfig.OAuth.Security.RequireHTTPS,
|
|
DisableUnsecureEndpoints: tempConfig.OAuth.Security.DisableUnsecureEndpoints,
|
|
SecureCookie: tempConfig.OAuth.Security.SecureCookie,
|
|
}
|
|
if tempConfig.OAuth.Security.StateParameterLifetime != "" {
|
|
if duration, err := parseDuration(tempConfig.OAuth.Security.StateParameterLifetime); err == nil {
|
|
config.OAuth.Security.StateParameterLifetime = duration
|
|
}
|
|
}
|
|
if tempConfig.OAuth.Security.RateLimitWindow != "" {
|
|
if duration, err := parseDuration(tempConfig.OAuth.Security.RateLimitWindow); err == nil {
|
|
config.OAuth.Security.RateLimitWindow = duration
|
|
}
|
|
}
|
|
if tempConfig.OAuth.Security.LockoutDuration != "" {
|
|
if duration, err := parseDuration(tempConfig.OAuth.Security.LockoutDuration); err == nil {
|
|
config.OAuth.Security.LockoutDuration = duration
|
|
}
|
|
}
|
|
|
|
// Convert client config with duration parsing
|
|
config.OAuth.Client = types.ClientConfig{
|
|
DefaultClientType: tempConfig.OAuth.Client.DefaultClientType,
|
|
DefaultTokenEndpointAuthMethod: tempConfig.OAuth.Client.DefaultTokenEndpointAuthMethod,
|
|
DefaultGrantTypes: tempConfig.OAuth.Client.DefaultGrantTypes,
|
|
DefaultResponseTypes: tempConfig.OAuth.Client.DefaultResponseTypes,
|
|
DefaultScopes: tempConfig.OAuth.Client.DefaultScopes,
|
|
ClientIDLength: tempConfig.OAuth.Client.ClientIDLength,
|
|
ClientSecretLength: tempConfig.OAuth.Client.ClientSecretLength,
|
|
DynamicRegistrationEnabled: tempConfig.OAuth.Client.DynamicRegistrationEnabled,
|
|
AllowedRedirectURISchemes: tempConfig.OAuth.Client.AllowedRedirectURISchemes,
|
|
AllowedRedirectURIHosts: tempConfig.OAuth.Client.AllowedRedirectURIHosts,
|
|
ClientCertificateRequired: tempConfig.OAuth.Client.ClientCertificateRequired,
|
|
ClientCertificateValidation: tempConfig.OAuth.Client.ClientCertificateValidation,
|
|
}
|
|
if tempConfig.OAuth.Client.ClientSecretLifetime != "" {
|
|
if duration, err := parseDuration(tempConfig.OAuth.Client.ClientSecretLifetime); err == nil {
|
|
config.OAuth.Client.ClientSecretLifetime = duration
|
|
}
|
|
}
|
|
}
|
|
|
|
// Set defaults if needed
|
|
if config.BaseURL == "" {
|
|
config.BaseURL = "/v1"
|
|
}
|
|
|
|
// Format the BaseURL should not have trailing slash
|
|
config.BaseURL = strings.TrimSuffix(config.BaseURL, "/")
|
|
|
|
if config.Cache == "" {
|
|
config.Cache = "__yao.oauth.cache"
|
|
}
|
|
|
|
if config.Store == "" {
|
|
config.Store = "__yao.oauth.store"
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// parseDuration parses a time duration string (e.g., "24h", "1h", "10m") into time.Duration
|
|
func parseDuration(durationStr string) (time.Duration, error) {
|
|
if durationStr == "" || durationStr == "0" || durationStr == "0s" {
|
|
return 0, nil
|
|
}
|
|
return time.ParseDuration(durationStr)
|
|
}
|
|
|
|
// formatDuration converts time.Duration to human-readable string format
|
|
func formatDuration(duration time.Duration) string {
|
|
if duration == 0 {
|
|
return "0s"
|
|
}
|
|
return duration.String()
|
|
}
|
|
|
|
// convertRelativeToAbsolutePath converts relative certificate path to absolute path
|
|
func convertRelativeToAbsolutePath(relativePath, rootPath string) string {
|
|
if relativePath == "" {
|
|
return ""
|
|
}
|
|
// If already absolute path, return as is
|
|
if filepath.IsAbs(relativePath) {
|
|
return relativePath
|
|
}
|
|
// Convert relative path to absolute: Root + "openapi" + "certs" + relativePath
|
|
return filepath.Join(rootPath, "openapi", "certs", relativePath)
|
|
}
|
|
|
|
// convertAbsoluteToRelativePath converts absolute certificate path to relative path
|
|
func convertAbsoluteToRelativePath(absolutePath, rootPath string) string {
|
|
if absolutePath == "" {
|
|
return ""
|
|
}
|
|
// If not absolute path, return as is
|
|
if !filepath.IsAbs(absolutePath) {
|
|
return absolutePath
|
|
}
|
|
|
|
// Remove Root + "openapi" + "certs" prefix
|
|
certBasePath := filepath.Join(rootPath, "openapi", "certs")
|
|
if strings.HasPrefix(absolutePath, certBasePath) {
|
|
relativePath := strings.TrimPrefix(absolutePath, certBasePath)
|
|
// Remove leading separator
|
|
relativePath = strings.TrimPrefix(relativePath, string(filepath.Separator))
|
|
return relativePath
|
|
}
|
|
|
|
// If path doesn't match expected pattern, return as is
|
|
return absolutePath
|
|
}
|
|
|
|
// OAuthConfig converts the configuration to an OAuth configuration
|
|
func (config *Config) OAuthConfig(appConfig config.Config) (*oauth.Config, error) {
|
|
var oauthConfig oauth.Config
|
|
|
|
var prefix string = share.App.GetPrefix()
|
|
var providers *Providers = config.GetProviders()
|
|
|
|
// Store the root path for later use in MarshalJSON
|
|
config.root = appConfig.Root
|
|
|
|
cacheStore, err := store.Get(config.Cache)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
dataStore, err := store.Get(config.Store)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
clientStore, err := store.Get(string(providers.Client))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Create the User provider
|
|
userProvider := user.NewDefaultUser(&user.DefaultUserOptions{
|
|
Prefix: prefix,
|
|
Model: string(providers.User),
|
|
Cache: cacheStore,
|
|
})
|
|
|
|
// Create the Client provider
|
|
clientProvider, err := client.NewDefaultClient(&client.DefaultClientOptions{
|
|
Prefix: prefix,
|
|
Store: clientStore,
|
|
Cache: cacheStore,
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Default OAuth configuration
|
|
if config.OAuth == nil {
|
|
config.OAuth = config.GetDefaultOAuthConfig()
|
|
}
|
|
|
|
// Convert certificate paths from relative to absolute
|
|
signingConfig := config.OAuth.Signing
|
|
signingConfig.SigningCertPath = convertRelativeToAbsolutePath(signingConfig.SigningCertPath, appConfig.Root)
|
|
signingConfig.SigningKeyPath = convertRelativeToAbsolutePath(signingConfig.SigningKeyPath, appConfig.Root)
|
|
signingConfig.MTLSClientCACertPath = convertRelativeToAbsolutePath(signingConfig.MTLSClientCACertPath, appConfig.Root)
|
|
|
|
// Create the OAuth configuration
|
|
oauthConfig = oauth.Config{
|
|
UserProvider: userProvider,
|
|
ClientProvider: clientProvider,
|
|
Cache: cacheStore,
|
|
Store: dataStore,
|
|
IssuerURL: config.OAuth.IssuerURL,
|
|
BaseURL: config.BaseURL,
|
|
Signing: signingConfig, // Use the converted signing config
|
|
Token: config.OAuth.Token,
|
|
Security: config.OAuth.Security,
|
|
Client: config.OAuth.Client,
|
|
Features: config.OAuth.Features,
|
|
}
|
|
|
|
return &oauthConfig, nil
|
|
}
|
|
|
|
// GetDefaultOAuthConfig Get the default OAuth configuration
|
|
func (config *Config) GetDefaultOAuthConfig() *OAuth {
|
|
return &OAuth{
|
|
Signing: types.SigningConfig{},
|
|
Token: types.TokenConfig{},
|
|
Security: types.SecurityConfig{},
|
|
Client: types.ClientConfig{},
|
|
Features: oauth.FeatureFlags{},
|
|
}
|
|
}
|
|
|
|
// GetProviders Get the providers from the configuration
|
|
func (config *Config) GetProviders() *Providers {
|
|
if config.Providers == nil {
|
|
config.Providers = &Providers{
|
|
User: "__yao.user",
|
|
Client: "__yao.oauth.client",
|
|
}
|
|
|
|
return config.Providers
|
|
}
|
|
|
|
if config.Providers.User == "" {
|
|
config.Providers.User = "__yao.user"
|
|
}
|
|
|
|
if config.Providers.Client == "" {
|
|
config.Providers.Client = "__yao.oauth.client"
|
|
}
|
|
|
|
return config.Providers
|
|
}
|