mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-19 04:47:48 +00:00
Abstract the provider logic into a separate package.
This commit is contained in:
@@ -0,0 +1,127 @@
|
|||||||
|
package providers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Adapter facilitates communication between the legacy TraefikOIDC struct and the new provider system.
|
||||||
|
type Adapter struct {
|
||||||
|
provider OIDCProvider
|
||||||
|
legacySettings LegacySettings
|
||||||
|
tokenVerifier TokenVerifier
|
||||||
|
tokenCache TokenCache
|
||||||
|
}
|
||||||
|
|
||||||
|
// LegacySettings provides the adapter with access to the original configuration values.
|
||||||
|
type LegacySettings interface {
|
||||||
|
GetIssuerURL() string
|
||||||
|
GetAuthURL() string
|
||||||
|
GetScopes() []string
|
||||||
|
IsPKCEEnabled() bool
|
||||||
|
GetClientID() string
|
||||||
|
GetRefreshGracePeriod() time.Duration
|
||||||
|
IsOverrideScopes() bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAdapter creates a new adapter for a given provider and legacy settings.
|
||||||
|
func NewAdapter(provider OIDCProvider, settings LegacySettings, tokenVerifier TokenVerifier, tokenCache TokenCache) *Adapter {
|
||||||
|
return &Adapter{
|
||||||
|
provider: provider,
|
||||||
|
legacySettings: settings,
|
||||||
|
tokenVerifier: tokenVerifier,
|
||||||
|
tokenCache: tokenCache,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildAuthURL constructs the authentication URL using the adapted provider.
|
||||||
|
func (a *Adapter) BuildAuthURL(redirectURL, state, nonce, codeChallenge string) string {
|
||||||
|
params := url.Values{}
|
||||||
|
params.Set("client_id", a.legacySettings.GetClientID())
|
||||||
|
params.Set("response_type", "code")
|
||||||
|
params.Set("redirect_uri", redirectURL)
|
||||||
|
params.Set("state", state)
|
||||||
|
params.Set("nonce", nonce)
|
||||||
|
|
||||||
|
if a.legacySettings.IsPKCEEnabled() && codeChallenge != "" {
|
||||||
|
params.Set("code_challenge", codeChallenge)
|
||||||
|
params.Set("code_challenge_method", "S256")
|
||||||
|
}
|
||||||
|
|
||||||
|
scopes := a.legacySettings.GetScopes()
|
||||||
|
|
||||||
|
// When overrideScopes is true, use exactly the scopes provided without modification
|
||||||
|
if a.legacySettings.IsOverrideScopes() {
|
||||||
|
// Use scopes as-is, don't let provider add anything
|
||||||
|
finalParams := params
|
||||||
|
finalParams.Set("scope", strings.Join(scopes, " "))
|
||||||
|
|
||||||
|
// For provider-specific parameters, we still need to check the provider type
|
||||||
|
switch a.provider.GetType() {
|
||||||
|
case ProviderTypeGoogle:
|
||||||
|
// Google-specific parameters
|
||||||
|
finalParams.Set("access_type", "offline")
|
||||||
|
finalParams.Set("prompt", "consent")
|
||||||
|
case ProviderTypeAzure:
|
||||||
|
// Azure-specific parameters
|
||||||
|
finalParams.Set("response_mode", "query")
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.buildURLWithParams(a.legacySettings.GetAuthURL(), finalParams)
|
||||||
|
}
|
||||||
|
|
||||||
|
// When overrideScopes is false, let the provider add necessary scopes
|
||||||
|
authParams, err := a.provider.BuildAuthParams(params, scopes)
|
||||||
|
if err != nil {
|
||||||
|
// Log the error appropriately
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
finalParams := authParams.URLValues
|
||||||
|
finalParams.Set("scope", strings.Join(authParams.Scopes, " "))
|
||||||
|
|
||||||
|
// Build the full URL with params
|
||||||
|
return a.buildURLWithParams(a.legacySettings.GetAuthURL(), finalParams)
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildURLWithParams takes a base URL and query parameters and constructs a full URL string.
|
||||||
|
// If the baseURL is relative (doesn't start with http/https), it prepends the scheme and host
|
||||||
|
// from the configured issuerURL.
|
||||||
|
func (a *Adapter) buildURLWithParams(baseURL string, params url.Values) string {
|
||||||
|
if !strings.HasPrefix(baseURL, "http://") && !strings.HasPrefix(baseURL, "https://") {
|
||||||
|
// Relative URL - resolve against issuer URL
|
||||||
|
issuerURLParsed, err := url.Parse(a.legacySettings.GetIssuerURL())
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
baseURLParsed, err := url.Parse(baseURL)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
resolvedURL := issuerURLParsed.ResolveReference(baseURLParsed)
|
||||||
|
resolvedURL.RawQuery = params.Encode()
|
||||||
|
return resolvedURL.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Absolute URL
|
||||||
|
u, err := url.Parse(baseURL)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
u.RawQuery = params.Encode()
|
||||||
|
return u.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateTokens validates tokens using the adapted provider.
|
||||||
|
func (a *Adapter) ValidateTokens(session Session) (*ValidationResult, error) {
|
||||||
|
return a.provider.ValidateTokens(session, a.tokenVerifier, a.tokenCache, a.legacySettings.GetRefreshGracePeriod())
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetType returns the underlying provider's type.
|
||||||
|
func (a *Adapter) GetType() ProviderType {
|
||||||
|
return a.provider.GetType()
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
package providers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AzureProvider encapsulates Azure AD-specific OIDC logic.
|
||||||
|
type AzureProvider struct {
|
||||||
|
*BaseProvider
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAzureProvider creates a new instance of the AzureProvider.
|
||||||
|
func NewAzureProvider() *AzureProvider {
|
||||||
|
return &AzureProvider{
|
||||||
|
BaseProvider: NewBaseProvider(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetType returns the provider's type.
|
||||||
|
func (p *AzureProvider) GetType() ProviderType {
|
||||||
|
return ProviderTypeAzure
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCapabilities returns the specific capabilities of the Azure provider.
|
||||||
|
func (p *AzureProvider) GetCapabilities() ProviderCapabilities {
|
||||||
|
return ProviderCapabilities{
|
||||||
|
SupportsRefreshTokens: true,
|
||||||
|
RequiresOfflineAccessScope: true,
|
||||||
|
PreferredTokenValidation: "access", // Azure AD prefers access token validation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildAuthParams configures Azure-specific authentication parameters.
|
||||||
|
func (p *AzureProvider) BuildAuthParams(baseParams url.Values, scopes []string) (*AuthParams, error) {
|
||||||
|
baseParams.Set("response_mode", "query")
|
||||||
|
|
||||||
|
// Ensure "offline_access" scope is present for refresh tokens
|
||||||
|
hasOfflineAccess := false
|
||||||
|
for _, scope := range scopes {
|
||||||
|
if scope == "offline_access" {
|
||||||
|
hasOfflineAccess = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !hasOfflineAccess {
|
||||||
|
scopes = append(scopes, "offline_access")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &AuthParams{
|
||||||
|
URLValues: baseParams,
|
||||||
|
Scopes: scopes,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateTokens overrides the default token validation to implement Azure-specific logic.
|
||||||
|
// Azure may use access tokens for validation, and this method ensures that behavior is preserved.
|
||||||
|
func (p *AzureProvider) ValidateTokens(session Session, verifier TokenVerifier, tokenCache TokenCache, refreshGracePeriod time.Duration) (*ValidationResult, error) {
|
||||||
|
if !session.GetAuthenticated() {
|
||||||
|
if session.GetRefreshToken() != "" {
|
||||||
|
return &ValidationResult{NeedsRefresh: true}, nil
|
||||||
|
}
|
||||||
|
return &ValidationResult{IsExpired: true}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
accessToken := session.GetAccessToken()
|
||||||
|
idToken := session.GetIDToken()
|
||||||
|
|
||||||
|
if accessToken != "" {
|
||||||
|
if strings.Count(accessToken, ".") == 2 {
|
||||||
|
if err := verifier.VerifyToken(accessToken); err != nil {
|
||||||
|
if idToken != "" {
|
||||||
|
return p.ValidateTokenExpiry(session, idToken, tokenCache, refreshGracePeriod)
|
||||||
|
}
|
||||||
|
if session.GetRefreshToken() != "" {
|
||||||
|
return &ValidationResult{NeedsRefresh: true}, nil
|
||||||
|
}
|
||||||
|
return &ValidationResult{IsExpired: true}, nil
|
||||||
|
}
|
||||||
|
return p.ValidateTokenExpiry(session, accessToken, tokenCache, refreshGracePeriod)
|
||||||
|
}
|
||||||
|
if idToken != "" {
|
||||||
|
return p.ValidateTokenExpiry(session, idToken, tokenCache, refreshGracePeriod)
|
||||||
|
}
|
||||||
|
return &ValidationResult{Authenticated: true}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if idToken != "" {
|
||||||
|
if err := verifier.VerifyToken(idToken); err != nil {
|
||||||
|
if session.GetRefreshToken() != "" {
|
||||||
|
return &ValidationResult{NeedsRefresh: true}, nil
|
||||||
|
}
|
||||||
|
return &ValidationResult{IsExpired: true}, nil
|
||||||
|
}
|
||||||
|
return p.ValidateTokenExpiry(session, idToken, tokenCache, refreshGracePeriod)
|
||||||
|
}
|
||||||
|
|
||||||
|
if session.GetRefreshToken() != "" {
|
||||||
|
return &ValidationResult{NeedsRefresh: true}, nil
|
||||||
|
}
|
||||||
|
return &ValidationResult{IsExpired: true}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateConfig validates Azure-specific configuration requirements.
|
||||||
|
// Azure requires specific tenant configuration and scope handling.
|
||||||
|
func (p *AzureProvider) ValidateConfig() error {
|
||||||
|
// Azure provider validation - ensure we have the necessary configuration
|
||||||
|
// In a real implementation, this might check for tenant ID, proper issuer URL format, etc.
|
||||||
|
return p.BaseProvider.ValidateConfig()
|
||||||
|
}
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
package providers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BaseProvider provides a common foundation for OIDC provider implementations.
|
||||||
|
// It can be embedded in specific provider structs to share common logic.
|
||||||
|
type BaseProvider struct {
|
||||||
|
// Common configuration or dependencies can be added here.
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetType returns the default provider type, which is Generic.
|
||||||
|
// This should be overridden by specific provider implementations.
|
||||||
|
func (p *BaseProvider) GetType() ProviderType {
|
||||||
|
return ProviderTypeGeneric
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCapabilities returns a default set of capabilities for a generic OIDC provider.
|
||||||
|
// This can be overridden by specific providers to declare their unique features.
|
||||||
|
func (p *BaseProvider) GetCapabilities() ProviderCapabilities {
|
||||||
|
return ProviderCapabilities{
|
||||||
|
SupportsRefreshTokens: true,
|
||||||
|
RequiresOfflineAccessScope: true,
|
||||||
|
PreferredTokenValidation: "id",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateTokens provides a default token validation implementation.
|
||||||
|
// This method can be extended or replaced by specific providers.
|
||||||
|
func (p *BaseProvider) ValidateTokens(session Session, verifier TokenVerifier, tokenCache TokenCache, refreshGracePeriod time.Duration) (*ValidationResult, error) {
|
||||||
|
if !session.GetAuthenticated() {
|
||||||
|
if session.GetRefreshToken() != "" {
|
||||||
|
return &ValidationResult{NeedsRefresh: true}, nil
|
||||||
|
}
|
||||||
|
return &ValidationResult{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
accessToken := session.GetAccessToken()
|
||||||
|
if accessToken == "" {
|
||||||
|
if session.GetRefreshToken() != "" {
|
||||||
|
return &ValidationResult{NeedsRefresh: true}, nil
|
||||||
|
}
|
||||||
|
return &ValidationResult{IsExpired: true}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
idToken := session.GetIDToken()
|
||||||
|
if idToken == "" {
|
||||||
|
if session.GetRefreshToken() != "" {
|
||||||
|
return &ValidationResult{Authenticated: true, NeedsRefresh: true}, nil
|
||||||
|
}
|
||||||
|
return &ValidationResult{Authenticated: true}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := verifier.VerifyToken(idToken); err != nil {
|
||||||
|
if strings.Contains(err.Error(), "token has expired") {
|
||||||
|
if session.GetRefreshToken() != "" {
|
||||||
|
return &ValidationResult{NeedsRefresh: true}, nil
|
||||||
|
}
|
||||||
|
return &ValidationResult{IsExpired: true}, nil
|
||||||
|
}
|
||||||
|
if session.GetRefreshToken() != "" {
|
||||||
|
return &ValidationResult{NeedsRefresh: true}, nil
|
||||||
|
}
|
||||||
|
return &ValidationResult{IsExpired: true}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.ValidateTokenExpiry(session, idToken, tokenCache, refreshGracePeriod)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateTokenExpiry provides common token expiry validation logic that can be used by all providers.
|
||||||
|
// This method is now exported so provider implementations can reuse this logic without duplication.
|
||||||
|
func (p *BaseProvider) ValidateTokenExpiry(session Session, token string, tokenCache TokenCache, refreshGracePeriod time.Duration) (*ValidationResult, error) {
|
||||||
|
cachedClaims, found := tokenCache.Get(token)
|
||||||
|
if !found {
|
||||||
|
if session.GetRefreshToken() != "" {
|
||||||
|
return &ValidationResult{NeedsRefresh: true}, nil
|
||||||
|
}
|
||||||
|
return &ValidationResult{IsExpired: true}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
expClaim, ok := cachedClaims["exp"].(float64)
|
||||||
|
if !ok {
|
||||||
|
if session.GetRefreshToken() != "" {
|
||||||
|
return &ValidationResult{NeedsRefresh: true}, nil
|
||||||
|
}
|
||||||
|
return &ValidationResult{IsExpired: true}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
expTime := time.Unix(int64(expClaim), 0)
|
||||||
|
if expTime.Before(time.Now().Add(refreshGracePeriod)) {
|
||||||
|
if session.GetRefreshToken() != "" {
|
||||||
|
return &ValidationResult{Authenticated: true, NeedsRefresh: true}, nil
|
||||||
|
}
|
||||||
|
return &ValidationResult{Authenticated: true}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ValidationResult{Authenticated: true}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildAuthParams provides a default implementation for building authorization parameters.
|
||||||
|
// It includes the "offline_access" scope by default.
|
||||||
|
func (p *BaseProvider) BuildAuthParams(baseParams url.Values, scopes []string) (*AuthParams, error) {
|
||||||
|
// Ensure offline_access is included if not already present
|
||||||
|
hasOfflineAccess := false
|
||||||
|
for _, scope := range scopes {
|
||||||
|
if scope == "offline_access" {
|
||||||
|
hasOfflineAccess = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !hasOfflineAccess {
|
||||||
|
scopes = append(scopes, "offline_access")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &AuthParams{
|
||||||
|
URLValues: baseParams,
|
||||||
|
Scopes: scopes,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleTokenRefresh provides a default implementation for token refresh handling.
|
||||||
|
// By default, it does nothing and assumes the standard token response is sufficient.
|
||||||
|
func (p *BaseProvider) HandleTokenRefresh(tokenData *TokenResult) error {
|
||||||
|
// No provider-specific refresh handling by default.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateConfig provides a default implementation for configuration validation.
|
||||||
|
// By default, it assumes the configuration is valid.
|
||||||
|
func (p *BaseProvider) ValidateConfig() error {
|
||||||
|
// No provider-specific config validation by default.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBaseProvider creates a new BaseProvider.
|
||||||
|
func NewBaseProvider() *BaseProvider {
|
||||||
|
return &BaseProvider{}
|
||||||
|
}
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
package providers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ProviderFactory encapsulates the logic for creating and configuring OIDC providers.
|
||||||
|
type ProviderFactory struct {
|
||||||
|
registry *ProviderRegistry
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewProviderFactory creates a new factory with a pre-configured registry.
|
||||||
|
func NewProviderFactory() *ProviderFactory {
|
||||||
|
registry := NewProviderRegistry()
|
||||||
|
|
||||||
|
// Register all available providers
|
||||||
|
registry.RegisterProvider(NewGenericProvider())
|
||||||
|
registry.RegisterProvider(NewGoogleProvider())
|
||||||
|
registry.RegisterProvider(NewAzureProvider())
|
||||||
|
|
||||||
|
return &ProviderFactory{
|
||||||
|
registry: registry,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateProvider creates and returns the appropriate provider for the given issuer URL.
|
||||||
|
// It automatically detects the provider type and returns a configured instance.
|
||||||
|
func (f *ProviderFactory) CreateProvider(issuerURL string) (OIDCProvider, error) {
|
||||||
|
if issuerURL == "" {
|
||||||
|
return nil, fmt.Errorf("issuer URL cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate URL format
|
||||||
|
if _, err := url.Parse(issuerURL); err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid issuer URL format: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
provider := f.registry.DetectProvider(issuerURL)
|
||||||
|
if provider == nil {
|
||||||
|
return nil, fmt.Errorf("unable to detect provider for issuer URL: %s", issuerURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the provider configuration if it implements config validation
|
||||||
|
if err := provider.ValidateConfig(); err != nil {
|
||||||
|
return nil, fmt.Errorf("provider configuration validation failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return provider, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateProviderByType creates a provider instance for a specific provider type.
|
||||||
|
// This is useful when you want to force a specific provider type regardless of URL.
|
||||||
|
func (f *ProviderFactory) CreateProviderByType(providerType ProviderType) (OIDCProvider, error) {
|
||||||
|
var provider OIDCProvider
|
||||||
|
|
||||||
|
switch providerType {
|
||||||
|
case ProviderTypeGeneric:
|
||||||
|
provider = NewGenericProvider()
|
||||||
|
case ProviderTypeGoogle:
|
||||||
|
provider = NewGoogleProvider()
|
||||||
|
case ProviderTypeAzure:
|
||||||
|
provider = NewAzureProvider()
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported provider type: %d", providerType)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := provider.ValidateConfig(); err != nil {
|
||||||
|
return nil, fmt.Errorf("provider configuration validation failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return provider, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSupportedProviders returns a list of all supported provider types and their detection patterns.
|
||||||
|
func (f *ProviderFactory) GetSupportedProviders() map[ProviderType][]string {
|
||||||
|
return map[ProviderType][]string{
|
||||||
|
ProviderTypeGeneric: {"*"}, // Generic supports any issuer
|
||||||
|
ProviderTypeGoogle: {"accounts.google.com"},
|
||||||
|
ProviderTypeAzure: {"login.microsoftonline.com", "sts.windows.net"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DetectProviderType returns the provider type that would be used for a given issuer URL.
|
||||||
|
// This is useful for diagnostic purposes or UI display.
|
||||||
|
func (f *ProviderFactory) DetectProviderType(issuerURL string) (ProviderType, error) {
|
||||||
|
provider, err := f.CreateProvider(issuerURL)
|
||||||
|
if err != nil {
|
||||||
|
return ProviderTypeGeneric, err
|
||||||
|
}
|
||||||
|
return provider.GetType(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsProviderSupported checks if a given issuer URL is supported by any registered provider.
|
||||||
|
func (f *ProviderFactory) IsProviderSupported(issuerURL string) bool {
|
||||||
|
if issuerURL == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
normalizedURL, err := url.Parse(issuerURL)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
host := strings.ToLower(normalizedURL.Host)
|
||||||
|
supportedProviders := f.GetSupportedProviders()
|
||||||
|
|
||||||
|
for _, patterns := range supportedProviders {
|
||||||
|
for _, pattern := range patterns {
|
||||||
|
if pattern == "*" || strings.Contains(host, strings.ToLower(pattern)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package providers
|
||||||
|
|
||||||
|
// GenericProvider encapsulates standard OIDC logic for any compliant provider.
|
||||||
|
type GenericProvider struct {
|
||||||
|
*BaseProvider
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewGenericProvider creates a new instance of the GenericProvider.
|
||||||
|
func NewGenericProvider() *GenericProvider {
|
||||||
|
return &GenericProvider{
|
||||||
|
BaseProvider: NewBaseProvider(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetType returns the provider's type.
|
||||||
|
func (p *GenericProvider) GetType() ProviderType {
|
||||||
|
return ProviderTypeGeneric
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package providers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GoogleProvider encapsulates Google-specific OIDC logic.
|
||||||
|
type GoogleProvider struct {
|
||||||
|
*BaseProvider
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewGoogleProvider creates a new instance of the GoogleProvider.
|
||||||
|
func NewGoogleProvider() *GoogleProvider {
|
||||||
|
return &GoogleProvider{
|
||||||
|
BaseProvider: NewBaseProvider(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetType returns the provider's type.
|
||||||
|
func (p *GoogleProvider) GetType() ProviderType {
|
||||||
|
return ProviderTypeGoogle
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCapabilities returns the specific capabilities of the Google provider.
|
||||||
|
func (p *GoogleProvider) GetCapabilities() ProviderCapabilities {
|
||||||
|
return ProviderCapabilities{
|
||||||
|
SupportsRefreshTokens: true,
|
||||||
|
RequiresOfflineAccessScope: false, // Google uses access_type=offline instead
|
||||||
|
RequiresPromptConsent: true,
|
||||||
|
PreferredTokenValidation: "id",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildAuthParams configures Google-specific authentication parameters.
|
||||||
|
func (p *GoogleProvider) BuildAuthParams(baseParams url.Values, scopes []string) (*AuthParams, error) {
|
||||||
|
baseParams.Set("access_type", "offline")
|
||||||
|
baseParams.Set("prompt", "consent")
|
||||||
|
|
||||||
|
// Google does not use the "offline_access" scope, so we remove it if present.
|
||||||
|
var filteredScopes []string
|
||||||
|
for _, scope := range scopes {
|
||||||
|
if scope != "offline_access" {
|
||||||
|
filteredScopes = append(filteredScopes, scope)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &AuthParams{
|
||||||
|
URLValues: baseParams,
|
||||||
|
Scopes: filteredScopes,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateConfig validates Google-specific configuration requirements.
|
||||||
|
// Google requires specific scopes and client configuration for proper operation.
|
||||||
|
func (p *GoogleProvider) ValidateConfig() error {
|
||||||
|
// Google provider doesn't require additional validation beyond the base implementation
|
||||||
|
// All Google-specific requirements are handled in BuildAuthParams
|
||||||
|
return p.BaseProvider.ValidateConfig()
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
package providers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TokenVerifier defines the interface for token verification.
|
||||||
|
type TokenVerifier interface {
|
||||||
|
VerifyToken(token string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// TokenCache defines the interface for a token cache.
|
||||||
|
type TokenCache interface {
|
||||||
|
Get(key string) (map[string]interface{}, bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProviderType is an enumeration for identifying different OIDC providers.
|
||||||
|
type ProviderType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ProviderTypeGeneric represents a standard, compliant OIDC provider.
|
||||||
|
ProviderTypeGeneric ProviderType = iota
|
||||||
|
// ProviderTypeGoogle represents Google as the OIDC provider.
|
||||||
|
ProviderTypeGoogle
|
||||||
|
// ProviderTypeAzure represents Microsoft Azure AD as the OIDC provider.
|
||||||
|
ProviderTypeAzure
|
||||||
|
)
|
||||||
|
|
||||||
|
// ProviderCapabilities defines the specific features and behaviors of an OIDC provider.
|
||||||
|
type ProviderCapabilities struct {
|
||||||
|
// SupportsRefreshTokens indicates if the provider issues refresh tokens.
|
||||||
|
SupportsRefreshTokens bool
|
||||||
|
// RequiresOfflineAccessScope indicates if the "offline_access" scope is needed for refresh tokens.
|
||||||
|
RequiresOfflineAccessScope bool
|
||||||
|
// RequiresPromptConsent indicates if "prompt=consent" is needed to ensure a refresh token is issued.
|
||||||
|
RequiresPromptConsent bool
|
||||||
|
// PreferredTokenValidation specifies the recommended token type to validate (e.g., "access" or "id").
|
||||||
|
PreferredTokenValidation string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidationResult holds the outcome of a token validation check.
|
||||||
|
type ValidationResult struct {
|
||||||
|
// Authenticated is true if the token is valid and the user is authenticated.
|
||||||
|
Authenticated bool
|
||||||
|
// NeedsRefresh is true if the token is approaching its expiry and should be refreshed.
|
||||||
|
NeedsRefresh bool
|
||||||
|
// IsExpired is true if the token has expired or is invalid.
|
||||||
|
IsExpired bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthParams contains the provider-specific parameters for building the authorization URL.
|
||||||
|
type AuthParams struct {
|
||||||
|
// URLValues are the query parameters to be added to the authorization URL.
|
||||||
|
URLValues url.Values
|
||||||
|
// Scopes is the list of scopes to be requested.
|
||||||
|
Scopes []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// TokenResult holds the tokens returned by the provider.
|
||||||
|
type TokenResult struct {
|
||||||
|
// IDToken is the OIDC ID token.
|
||||||
|
IDToken string
|
||||||
|
// AccessToken is the OAuth2 access token.
|
||||||
|
AccessToken string
|
||||||
|
// RefreshToken is the OAuth2 refresh token.
|
||||||
|
RefreshToken string
|
||||||
|
}
|
||||||
|
|
||||||
|
// OIDCProvider defines the interface for an OIDC provider implementation.
|
||||||
|
// This abstraction allows for provider-specific logic to be encapsulated.
|
||||||
|
type OIDCProvider interface {
|
||||||
|
// GetType returns the type of the provider (e.g., Google, Azure, Generic).
|
||||||
|
GetType() ProviderType
|
||||||
|
|
||||||
|
// GetCapabilities returns the feature set of the provider.
|
||||||
|
GetCapabilities() ProviderCapabilities
|
||||||
|
|
||||||
|
// ValidateTokens performs token validation according to the provider's specific rules.
|
||||||
|
// It should check the validity of the access and/or ID tokens from the session.
|
||||||
|
ValidateTokens(session Session, verifier TokenVerifier, tokenCache TokenCache, refreshGracePeriod time.Duration) (*ValidationResult, error)
|
||||||
|
|
||||||
|
// BuildAuthParams modifies the authorization URL parameters for the provider.
|
||||||
|
// This can be used to add provider-specific parameters like "access_type" for Google.
|
||||||
|
BuildAuthParams(baseParams url.Values, scopes []string) (*AuthParams, error)
|
||||||
|
|
||||||
|
// HandleTokenRefresh manages the token refresh process for the provider.
|
||||||
|
// It can modify the token request or handle the response as needed.
|
||||||
|
HandleTokenRefresh(tokenData *TokenResult) error
|
||||||
|
|
||||||
|
// ValidateConfig checks if the user's configuration is valid for this provider.
|
||||||
|
ValidateConfig() error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Session represents the session data required by providers for validation.
|
||||||
|
// This interface decouples the providers from the main session management implementation.
|
||||||
|
type Session interface {
|
||||||
|
GetIDToken() string
|
||||||
|
GetAccessToken() string
|
||||||
|
GetRefreshToken() string
|
||||||
|
GetAuthenticated() bool
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
package providers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ProviderRegistry holds and manages the available OIDC provider implementations.
|
||||||
|
// It provides thread-safe access to provider instances and caches detection results.
|
||||||
|
type ProviderRegistry struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
providers []OIDCProvider
|
||||||
|
cache map[string]OIDCProvider
|
||||||
|
typeMap map[ProviderType]OIDCProvider // Maps provider type to instance
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewProviderRegistry creates and initializes a new ProviderRegistry.
|
||||||
|
func NewProviderRegistry() *ProviderRegistry {
|
||||||
|
return &ProviderRegistry{
|
||||||
|
providers: make([]OIDCProvider, 0),
|
||||||
|
cache: make(map[string]OIDCProvider),
|
||||||
|
typeMap: make(map[ProviderType]OIDCProvider),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterProvider adds a new provider to the registry.
|
||||||
|
// It maintains both a list of providers and a type-to-provider mapping for efficient lookups.
|
||||||
|
func (r *ProviderRegistry) RegisterProvider(provider OIDCProvider) {
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
r.providers = append(r.providers, provider)
|
||||||
|
r.typeMap[provider.GetType()] = provider
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetProviderByType returns a provider instance for the specified type.
|
||||||
|
// Returns nil if the provider type is not registered.
|
||||||
|
func (r *ProviderRegistry) GetProviderByType(providerType ProviderType) OIDCProvider {
|
||||||
|
r.mu.RLock()
|
||||||
|
defer r.mu.RUnlock()
|
||||||
|
return r.typeMap[providerType]
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRegisteredProviders returns a slice of all registered provider types.
|
||||||
|
func (r *ProviderRegistry) GetRegisteredProviders() []ProviderType {
|
||||||
|
r.mu.RLock()
|
||||||
|
defer r.mu.RUnlock()
|
||||||
|
|
||||||
|
types := make([]ProviderType, 0, len(r.typeMap))
|
||||||
|
for providerType := range r.typeMap {
|
||||||
|
types = append(types, providerType)
|
||||||
|
}
|
||||||
|
return types
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearCache removes all cached provider detection results.
|
||||||
|
// This can be useful for testing or when provider configuration changes.
|
||||||
|
func (r *ProviderRegistry) ClearCache() {
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
r.cache = make(map[string]OIDCProvider)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DetectProvider determines the most appropriate provider for a given issuer URL.
|
||||||
|
// It iterates through the registered providers and returns the first one that matches.
|
||||||
|
// Detection is based on URL patterns and other provider-specific criteria.
|
||||||
|
func (r *ProviderRegistry) DetectProvider(issuerURL string) OIDCProvider {
|
||||||
|
r.mu.RLock()
|
||||||
|
defer r.mu.RUnlock()
|
||||||
|
|
||||||
|
// Check cache first for performance
|
||||||
|
if provider, found := r.cache[issuerURL]; found {
|
||||||
|
return provider
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize issuer URL for consistent matching
|
||||||
|
normalizedURL, err := url.Parse(issuerURL)
|
||||||
|
if err != nil {
|
||||||
|
// Log error or handle it appropriately
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
host := normalizedURL.Host
|
||||||
|
|
||||||
|
// Iterate through registered providers to find a match
|
||||||
|
for _, p := range r.providers {
|
||||||
|
switch p.GetType() {
|
||||||
|
case ProviderTypeGoogle:
|
||||||
|
if strings.Contains(host, "accounts.google.com") {
|
||||||
|
r.cache[issuerURL] = p
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
case ProviderTypeAzure:
|
||||||
|
if strings.Contains(host, "login.microsoftonline.com") || strings.Contains(host, "sts.windows.net") {
|
||||||
|
r.cache[issuerURL] = p
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to the generic provider if no specific provider is detected
|
||||||
|
for _, p := range r.providers {
|
||||||
|
if p.GetType() == ProviderTypeGeneric {
|
||||||
|
r.cache[issuerURL] = p
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
package providers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ConfigValidator provides common configuration validation utilities for providers.
|
||||||
|
type ConfigValidator struct{}
|
||||||
|
|
||||||
|
// NewConfigValidator creates a new configuration validator.
|
||||||
|
func NewConfigValidator() *ConfigValidator {
|
||||||
|
return &ConfigValidator{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateIssuerURL validates that an issuer URL is properly formatted and accessible.
|
||||||
|
func (v *ConfigValidator) ValidateIssuerURL(issuerURL string) error {
|
||||||
|
if issuerURL == "" {
|
||||||
|
return fmt.Errorf("issuer URL cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedURL, err := url.Parse(issuerURL)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid issuer URL format: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if parsedURL.Scheme == "" {
|
||||||
|
return fmt.Errorf("issuer URL must include scheme (http/https)")
|
||||||
|
}
|
||||||
|
|
||||||
|
if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" {
|
||||||
|
return fmt.Errorf("issuer URL scheme must be http or https")
|
||||||
|
}
|
||||||
|
|
||||||
|
if parsedURL.Host == "" {
|
||||||
|
return fmt.Errorf("issuer URL must include host")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateClientID validates that a client ID is properly formatted.
|
||||||
|
func (v *ConfigValidator) ValidateClientID(clientID string) error {
|
||||||
|
if clientID == "" {
|
||||||
|
return fmt.Errorf("client ID cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(clientID) < 3 {
|
||||||
|
return fmt.Errorf("client ID appears to be too short")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateScopes validates that the provided scopes are reasonable.
|
||||||
|
func (v *ConfigValidator) ValidateScopes(scopes []string) error {
|
||||||
|
if len(scopes) == 0 {
|
||||||
|
return fmt.Errorf("at least one scope must be provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for required OIDC scope
|
||||||
|
hasOpenIDScope := false
|
||||||
|
for _, scope := range scopes {
|
||||||
|
if strings.TrimSpace(scope) == "openid" {
|
||||||
|
hasOpenIDScope = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hasOpenIDScope {
|
||||||
|
return fmt.Errorf("'openid' scope is required for OIDC authentication")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateRedirectURL validates that a redirect URL is properly formatted.
|
||||||
|
func (v *ConfigValidator) ValidateRedirectURL(redirectURL string) error {
|
||||||
|
if redirectURL == "" {
|
||||||
|
return fmt.Errorf("redirect URL cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedURL, err := url.Parse(redirectURL)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid redirect URL format: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if parsedURL.Scheme == "" {
|
||||||
|
return fmt.Errorf("redirect URL must include scheme (http/https)")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateProviderSpecificConfig performs provider-specific validation.
|
||||||
|
func (v *ConfigValidator) ValidateProviderSpecificConfig(provider OIDCProvider, config map[string]interface{}) error {
|
||||||
|
switch provider.GetType() {
|
||||||
|
case ProviderTypeGoogle:
|
||||||
|
return v.validateGoogleConfig(config)
|
||||||
|
case ProviderTypeAzure:
|
||||||
|
return v.validateAzureConfig(config)
|
||||||
|
case ProviderTypeGeneric:
|
||||||
|
return v.validateGenericConfig(config)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unknown provider type: %d", provider.GetType())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateGoogleConfig validates Google-specific configuration.
|
||||||
|
func (v *ConfigValidator) validateGoogleConfig(config map[string]interface{}) error {
|
||||||
|
// Google-specific validation logic
|
||||||
|
if issuerURL, ok := config["issuer_url"].(string); ok {
|
||||||
|
if !strings.Contains(issuerURL, "accounts.google.com") {
|
||||||
|
return fmt.Errorf("google provider requires issuer URL to contain accounts.google.com")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateAzureConfig validates Azure-specific configuration.
|
||||||
|
func (v *ConfigValidator) validateAzureConfig(config map[string]interface{}) error {
|
||||||
|
// Azure-specific validation logic
|
||||||
|
if issuerURL, ok := config["issuer_url"].(string); ok {
|
||||||
|
if !strings.Contains(issuerURL, "login.microsoftonline.com") && !strings.Contains(issuerURL, "sts.windows.net") {
|
||||||
|
return fmt.Errorf("azure provider requires issuer URL to contain login.microsoftonline.com or sts.windows.net")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for tenant ID in the URL
|
||||||
|
if issuerURL, ok := config["issuer_url"].(string); ok {
|
||||||
|
parsedURL, err := url.Parse(issuerURL)
|
||||||
|
if err == nil {
|
||||||
|
pathParts := strings.Split(parsedURL.Path, "/")
|
||||||
|
hasTenantID := false
|
||||||
|
for _, part := range pathParts {
|
||||||
|
// Simple check for GUID-like structure (tenant ID)
|
||||||
|
if len(part) == 36 && strings.Count(part, "-") == 4 {
|
||||||
|
hasTenantID = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !hasTenantID {
|
||||||
|
return fmt.Errorf("azure issuer URL should include tenant ID")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateGenericConfig validates generic OIDC provider configuration.
|
||||||
|
func (v *ConfigValidator) validateGenericConfig(config map[string]interface{}) error {
|
||||||
|
// Generic provider validation - basic checks only
|
||||||
|
return nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user