Add new providers.

This commit is contained in:
2025-09-26 23:08:50 +01:00
parent abea624841
commit 7c8917bf6f
17 changed files with 1587 additions and 53 deletions
+72
View File
@@ -0,0 +1,72 @@
package providers
import (
"net/url"
)
// Auth0Provider encapsulates Auth0-specific OIDC logic.
type Auth0Provider struct {
*BaseProvider
}
// NewAuth0Provider creates a new instance of the Auth0Provider.
func NewAuth0Provider() *Auth0Provider {
return &Auth0Provider{
BaseProvider: NewBaseProvider(),
}
}
// GetType returns the provider's type.
func (p *Auth0Provider) GetType() ProviderType {
return ProviderTypeAuth0
}
// GetCapabilities returns the specific capabilities of the Auth0 provider.
func (p *Auth0Provider) GetCapabilities() ProviderCapabilities {
return ProviderCapabilities{
SupportsRefreshTokens: true,
RequiresOfflineAccessScope: true,
RequiresPromptConsent: false,
PreferredTokenValidation: "id", // Auth0 typically uses ID tokens
}
}
// BuildAuthParams configures Auth0-specific authentication parameters.
func (p *Auth0Provider) BuildAuthParams(baseParams url.Values, scopes []string) (*AuthParams, error) {
// Auth0 supports various response types and connection parameters
baseParams.Set("response_type", "code")
// 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")
}
// Ensure openid scope is present
hasOpenID := false
for _, scope := range scopes {
if scope == "openid" {
hasOpenID = true
break
}
}
if !hasOpenID {
scopes = append(scopes, "openid")
}
return &AuthParams{
URLValues: baseParams,
Scopes: scopes,
}, nil
}
// Auth0 requires specific tenant configuration and connection handling.
func (p *Auth0Provider) ValidateConfig() error {
return p.BaseProvider.ValidateConfig()
}
+124
View File
@@ -0,0 +1,124 @@
package providers
import (
"net/url"
"testing"
)
// TestAuth0Provider_NewAuth0Provider tests the constructor
func TestAuth0Provider_NewAuth0Provider(t *testing.T) {
provider := NewAuth0Provider()
if provider == nil {
t.Fatal("Expected provider to be created, got nil")
}
if provider.BaseProvider == nil {
t.Error("BaseProvider should be initialized")
}
}
// TestAuth0Provider_GetType tests provider type
func TestAuth0Provider_GetType(t *testing.T) {
provider := NewAuth0Provider()
if provider.GetType() != ProviderTypeAuth0 {
t.Errorf("Expected ProviderTypeAuth0, got %v", provider.GetType())
}
}
// TestAuth0Provider_GetCapabilities tests Auth0-specific capabilities
func TestAuth0Provider_GetCapabilities(t *testing.T) {
provider := NewAuth0Provider()
capabilities := provider.GetCapabilities()
if !capabilities.SupportsRefreshTokens {
t.Error("Expected SupportsRefreshTokens to be true for Auth0")
}
if !capabilities.RequiresOfflineAccessScope {
t.Error("Expected RequiresOfflineAccessScope to be true for Auth0")
}
if capabilities.RequiresPromptConsent {
t.Error("Expected RequiresPromptConsent to be false for Auth0")
}
if capabilities.PreferredTokenValidation != "id" {
t.Errorf("Expected PreferredTokenValidation 'id', got '%s'", capabilities.PreferredTokenValidation)
}
}
// TestAuth0Provider_BuildAuthParams tests Auth0-specific auth params
func TestAuth0Provider_BuildAuthParams(t *testing.T) {
provider := NewAuth0Provider()
baseParams := url.Values{}
baseParams.Set("client_id", "test_client")
tests := []struct {
name string
scopes []string
expectedScopes []string
}{
{
name: "Add offline_access and openid scopes",
scopes: []string{"profile", "email"},
expectedScopes: []string{"profile", "email", "offline_access", "openid"},
},
{
name: "Keep existing offline_access and openid",
scopes: []string{"openid", "profile", "offline_access", "email"},
expectedScopes: []string{"openid", "profile", "offline_access", "email"},
},
{
name: "Add both scopes when none provided",
scopes: []string{},
expectedScopes: []string{"offline_access", "openid"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
authParams, err := provider.BuildAuthParams(baseParams, tt.scopes)
if err != nil {
t.Errorf("BuildAuthParams failed: %v", err)
return
}
// Check that response_type is set
if authParams.URLValues.Get("response_type") != "code" {
t.Errorf("Expected response_type 'code', got '%s'", authParams.URLValues.Get("response_type"))
}
if len(authParams.Scopes) != len(tt.expectedScopes) {
t.Errorf("Expected %d scopes, got %d. Expected: %v, Got: %v",
len(tt.expectedScopes), len(authParams.Scopes), tt.expectedScopes, authParams.Scopes)
return
}
// Check that all expected scopes are present
for _, expectedScope := range tt.expectedScopes {
found := false
for _, actualScope := range authParams.Scopes {
if actualScope == expectedScope {
found = true
break
}
}
if !found {
t.Errorf("Expected scope '%s' not found in %v", expectedScope, authParams.Scopes)
}
}
})
}
}
// TestAuth0Provider_ValidateConfig tests config validation
func TestAuth0Provider_ValidateConfig(t *testing.T) {
provider := NewAuth0Provider()
err := provider.ValidateConfig()
if err != nil {
t.Errorf("ValidateConfig failed: %v", err)
}
}
+73
View File
@@ -0,0 +1,73 @@
package providers
import (
"net/url"
)
// AWSCognitoProvider encapsulates AWS Cognito-specific OIDC logic.
type AWSCognitoProvider struct {
*BaseProvider
}
// NewAWSCognitoProvider creates a new instance of the AWSCognitoProvider.
func NewAWSCognitoProvider() *AWSCognitoProvider {
return &AWSCognitoProvider{
BaseProvider: NewBaseProvider(),
}
}
// GetType returns the provider's type.
func (p *AWSCognitoProvider) GetType() ProviderType {
return ProviderTypeAWSCognito
}
// GetCapabilities returns the specific capabilities of the AWS Cognito provider.
func (p *AWSCognitoProvider) GetCapabilities() ProviderCapabilities {
return ProviderCapabilities{
SupportsRefreshTokens: true,
RequiresOfflineAccessScope: false, // Cognito doesn't use offline_access scope
RequiresPromptConsent: false,
PreferredTokenValidation: "id", // Cognito typically uses ID tokens
}
}
// BuildAuthParams configures AWS Cognito-specific authentication parameters.
func (p *AWSCognitoProvider) BuildAuthParams(baseParams url.Values, scopes []string) (*AuthParams, error) {
// AWS Cognito supports standard OIDC parameters
baseParams.Set("response_type", "code")
// Remove offline_access scope as Cognito doesn't use it
var filteredScopes []string
for _, scope := range scopes {
if scope != "offline_access" {
filteredScopes = append(filteredScopes, scope)
}
}
// Ensure openid scope is present
hasOpenID := false
for _, scope := range filteredScopes {
if scope == "openid" {
hasOpenID = true
break
}
}
if !hasOpenID {
filteredScopes = append(filteredScopes, "openid")
}
// Default Cognito scopes if none specified
if len(filteredScopes) == 1 && filteredScopes[0] == "openid" {
filteredScopes = append(filteredScopes, "email", "profile")
}
return &AuthParams{
URLValues: baseParams,
Scopes: filteredScopes,
}, nil
}
// AWS Cognito requires user pool and domain configuration.
func (p *AWSCognitoProvider) ValidateConfig() error {
return p.BaseProvider.ValidateConfig()
}
+27 -3
View File
@@ -18,6 +18,12 @@ func NewProviderFactory() *ProviderFactory {
registry.RegisterProvider(NewGenericProvider())
registry.RegisterProvider(NewGoogleProvider())
registry.RegisterProvider(NewAzureProvider())
registry.RegisterProvider(NewGitHubProvider())
registry.RegisterProvider(NewAuth0Provider())
registry.RegisterProvider(NewOktaProvider())
registry.RegisterProvider(NewKeycloakProvider())
registry.RegisterProvider(NewAWSCognitoProvider())
registry.RegisterProvider(NewGitLabProvider())
return &ProviderFactory{
registry: registry,
@@ -65,6 +71,18 @@ func (f *ProviderFactory) CreateProviderByType(providerType ProviderType) (OIDCP
provider = NewGoogleProvider()
case ProviderTypeAzure:
provider = NewAzureProvider()
case ProviderTypeGitHub:
provider = NewGitHubProvider()
case ProviderTypeAuth0:
provider = NewAuth0Provider()
case ProviderTypeOkta:
provider = NewOktaProvider()
case ProviderTypeKeycloak:
provider = NewKeycloakProvider()
case ProviderTypeAWSCognito:
provider = NewAWSCognitoProvider()
case ProviderTypeGitLab:
provider = NewGitLabProvider()
default:
return nil, fmt.Errorf("unsupported provider type: %d", providerType)
}
@@ -79,9 +97,15 @@ func (f *ProviderFactory) CreateProviderByType(providerType ProviderType) (OIDCP
// 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: {"*"},
ProviderTypeGoogle: {"accounts.google.com"},
ProviderTypeAzure: {"login.microsoftonline.com", "sts.windows.net"},
ProviderTypeGeneric: {"*"},
ProviderTypeGoogle: {"accounts.google.com"},
ProviderTypeAzure: {"login.microsoftonline.com", "sts.windows.net"},
ProviderTypeGitHub: {"github.com"},
ProviderTypeAuth0: {".auth0.com"},
ProviderTypeOkta: {".okta.com", ".oktapreview.com", ".okta-emea.com"},
ProviderTypeKeycloak: {"keycloak"},
ProviderTypeAWSCognito: {"cognito-idp", ".amazonaws.com"},
ProviderTypeGitLab: {"gitlab.com"},
}
}
+42
View File
@@ -53,6 +53,48 @@ func TestProviderFactory_CreateProvider(t *testing.T) {
expectedType: ProviderTypeAzure,
wantErr: false,
},
{
name: "GitHub provider",
issuerURL: "https://github.com/login/oauth",
expectedType: ProviderTypeGitHub,
wantErr: false,
},
{
name: "Auth0 provider",
issuerURL: "https://tenant.auth0.com",
expectedType: ProviderTypeAuth0,
wantErr: false,
},
{
name: "Okta provider",
issuerURL: "https://tenant.okta.com",
expectedType: ProviderTypeOkta,
wantErr: false,
},
{
name: "Okta preview provider",
issuerURL: "https://tenant.oktapreview.com",
expectedType: ProviderTypeOkta,
wantErr: false,
},
{
name: "Keycloak provider",
issuerURL: "https://auth.example.com/auth/realms/master",
expectedType: ProviderTypeKeycloak,
wantErr: false,
},
{
name: "AWS Cognito provider",
issuerURL: "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_example",
expectedType: ProviderTypeAWSCognito,
wantErr: false,
},
{
name: "GitLab provider",
issuerURL: "https://gitlab.com/oauth",
expectedType: ProviderTypeGitLab,
wantErr: false,
},
{
name: "Generic provider",
issuerURL: "https://auth.example.com",
+61
View File
@@ -0,0 +1,61 @@
package providers
import (
"net/url"
)
// GitHubProvider encapsulates GitHub-specific OIDC logic.
type GitHubProvider struct {
*BaseProvider
}
// NewGitHubProvider creates a new instance of the GitHubProvider.
func NewGitHubProvider() *GitHubProvider {
return &GitHubProvider{
BaseProvider: NewBaseProvider(),
}
}
// GetType returns the provider's type.
func (p *GitHubProvider) GetType() ProviderType {
return ProviderTypeGitHub
}
// GetCapabilities returns the specific capabilities of the GitHub provider.
// WARNING: GitHub does NOT support OpenID Connect - it's OAuth 2.0 only.
// This provider should only be used for OAuth flows, not OIDC authentication.
func (p *GitHubProvider) GetCapabilities() ProviderCapabilities {
return ProviderCapabilities{
SupportsRefreshTokens: false, // GitHub OAuth apps don't support refresh tokens
RequiresOfflineAccessScope: false, // GitHub doesn't use offline_access
RequiresPromptConsent: false,
PreferredTokenValidation: "access", // GitHub only provides access tokens, no ID tokens
}
}
// BuildAuthParams configures GitHub-specific authentication parameters.
func (p *GitHubProvider) BuildAuthParams(baseParams url.Values, scopes []string) (*AuthParams, error) {
// GitHub doesn't use offline_access scope, so remove it if present
var filteredScopes []string
for _, scope := range scopes {
if scope != "offline_access" {
filteredScopes = append(filteredScopes, scope)
}
}
// If no scopes specified, use default GitHub scopes for OAuth
// Note: GitHub doesn't support 'openid' scope as it's not an OIDC provider
if len(filteredScopes) == 0 {
filteredScopes = []string{"user:email", "read:user"}
}
return &AuthParams{
URLValues: baseParams,
Scopes: filteredScopes,
}, nil
}
// GitHub requires specific configuration for proper operation.
func (p *GitHubProvider) ValidateConfig() error {
return p.BaseProvider.ValidateConfig()
}
+110
View File
@@ -0,0 +1,110 @@
package providers
import (
"net/url"
"testing"
)
// TestGitHubProvider_NewGitHubProvider tests the constructor
func TestGitHubProvider_NewGitHubProvider(t *testing.T) {
provider := NewGitHubProvider()
if provider == nil {
t.Fatal("Expected provider to be created, got nil")
}
if provider.BaseProvider == nil {
t.Error("BaseProvider should be initialized")
}
}
// TestGitHubProvider_GetType tests provider type
func TestGitHubProvider_GetType(t *testing.T) {
provider := NewGitHubProvider()
if provider.GetType() != ProviderTypeGitHub {
t.Errorf("Expected ProviderTypeGitHub, got %v", provider.GetType())
}
}
// TestGitHubProvider_GetCapabilities tests GitHub-specific capabilities
func TestGitHubProvider_GetCapabilities(t *testing.T) {
provider := NewGitHubProvider()
capabilities := provider.GetCapabilities()
if capabilities.SupportsRefreshTokens {
t.Error("Expected SupportsRefreshTokens to be false for GitHub")
}
if capabilities.RequiresOfflineAccessScope {
t.Error("Expected RequiresOfflineAccessScope to be false for GitHub")
}
if capabilities.RequiresPromptConsent {
t.Error("Expected RequiresPromptConsent to be false for GitHub")
}
if capabilities.PreferredTokenValidation != "access" {
t.Errorf("Expected PreferredTokenValidation 'access', got '%s'", capabilities.PreferredTokenValidation)
}
}
// TestGitHubProvider_BuildAuthParams tests GitHub-specific auth params
func TestGitHubProvider_BuildAuthParams(t *testing.T) {
provider := NewGitHubProvider()
baseParams := url.Values{}
baseParams.Set("client_id", "test_client")
tests := []struct {
name string
scopes []string
expectedScopes []string
}{
{
name: "Remove offline_access scope",
scopes: []string{"user:email", "offline_access", "read:user"},
expectedScopes: []string{"user:email", "read:user"},
},
{
name: "Default scopes when none provided",
scopes: []string{},
expectedScopes: []string{"user:email", "read:user"},
},
{
name: "Keep other scopes",
scopes: []string{"user", "repo"},
expectedScopes: []string{"user", "repo"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
authParams, err := provider.BuildAuthParams(baseParams, tt.scopes)
if err != nil {
t.Errorf("BuildAuthParams failed: %v", err)
return
}
if len(authParams.Scopes) != len(tt.expectedScopes) {
t.Errorf("Expected %d scopes, got %d", len(tt.expectedScopes), len(authParams.Scopes))
return
}
for i, scope := range tt.expectedScopes {
if authParams.Scopes[i] != scope {
t.Errorf("Expected scope '%s', got '%s'", scope, authParams.Scopes[i])
}
}
})
}
}
// TestGitHubProvider_ValidateConfig tests config validation
func TestGitHubProvider_ValidateConfig(t *testing.T) {
provider := NewGitHubProvider()
err := provider.ValidateConfig()
if err != nil {
t.Errorf("ValidateConfig failed: %v", err)
}
}
+73
View File
@@ -0,0 +1,73 @@
package providers
import (
"net/url"
)
// GitLabProvider encapsulates GitLab-specific OIDC logic.
type GitLabProvider struct {
*BaseProvider
}
// NewGitLabProvider creates a new instance of the GitLabProvider.
func NewGitLabProvider() *GitLabProvider {
return &GitLabProvider{
BaseProvider: NewBaseProvider(),
}
}
// GetType returns the provider's type.
func (p *GitLabProvider) GetType() ProviderType {
return ProviderTypeGitLab
}
// GetCapabilities returns the specific capabilities of the GitLab provider.
func (p *GitLabProvider) GetCapabilities() ProviderCapabilities {
return ProviderCapabilities{
SupportsRefreshTokens: true,
RequiresOfflineAccessScope: false, // GitLab doesn't use offline_access scope
RequiresPromptConsent: false,
PreferredTokenValidation: "id", // GitLab typically uses ID tokens
}
}
// BuildAuthParams configures GitLab-specific authentication parameters.
func (p *GitLabProvider) BuildAuthParams(baseParams url.Values, scopes []string) (*AuthParams, error) {
// GitLab supports standard OAuth 2.0 parameters
baseParams.Set("response_type", "code")
// Remove offline_access scope as GitLab doesn't use it
var filteredScopes []string
for _, scope := range scopes {
if scope != "offline_access" {
filteredScopes = append(filteredScopes, scope)
}
}
// Ensure openid scope is present for OIDC
hasOpenID := false
for _, scope := range filteredScopes {
if scope == "openid" {
hasOpenID = true
break
}
}
if !hasOpenID {
filteredScopes = append(filteredScopes, "openid")
}
// Default GitLab scopes if none specified
if len(filteredScopes) == 1 && filteredScopes[0] == "openid" {
filteredScopes = append(filteredScopes, "profile", "email")
}
return &AuthParams{
URLValues: baseParams,
Scopes: filteredScopes,
}, nil
}
// GitLab requires application configuration and proper redirect URIs.
func (p *GitLabProvider) ValidateConfig() error {
return p.BaseProvider.ValidateConfig()
}
+2 -2
View File
@@ -24,8 +24,8 @@ func (p *GoogleProvider) GetType() ProviderType {
// GetCapabilities returns the specific capabilities of the Google provider.
func (p *GoogleProvider) GetCapabilities() ProviderCapabilities {
return ProviderCapabilities{
SupportsRefreshTokens: true,
RequiresOfflineAccessScope: false,
SupportsRefreshTokens: true, // Google DOES support refresh tokens
RequiresOfflineAccessScope: false, // Google uses access_type=offline instead
RequiresPromptConsent: true,
PreferredTokenValidation: "id",
}
+6
View File
@@ -25,6 +25,12 @@ const (
ProviderTypeGeneric ProviderType = iota
ProviderTypeGoogle
ProviderTypeAzure
ProviderTypeGitHub
ProviderTypeAuth0
ProviderTypeOkta
ProviderTypeKeycloak
ProviderTypeAWSCognito
ProviderTypeGitLab
)
// ProviderCapabilities defines the specific features and behaviors of an OIDC provider.
+72
View File
@@ -0,0 +1,72 @@
package providers
import (
"net/url"
)
// KeycloakProvider encapsulates Keycloak-specific OIDC logic.
type KeycloakProvider struct {
*BaseProvider
}
// NewKeycloakProvider creates a new instance of the KeycloakProvider.
func NewKeycloakProvider() *KeycloakProvider {
return &KeycloakProvider{
BaseProvider: NewBaseProvider(),
}
}
// GetType returns the provider's type.
func (p *KeycloakProvider) GetType() ProviderType {
return ProviderTypeKeycloak
}
// GetCapabilities returns the specific capabilities of the Keycloak provider.
func (p *KeycloakProvider) GetCapabilities() ProviderCapabilities {
return ProviderCapabilities{
SupportsRefreshTokens: true,
RequiresOfflineAccessScope: true,
RequiresPromptConsent: false,
PreferredTokenValidation: "id", // Keycloak typically uses ID tokens
}
}
// BuildAuthParams configures Keycloak-specific authentication parameters.
func (p *KeycloakProvider) BuildAuthParams(baseParams url.Values, scopes []string) (*AuthParams, error) {
// Keycloak supports standard OIDC parameters
baseParams.Set("response_type", "code")
// 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")
}
// Ensure openid scope is present
hasOpenID := false
for _, scope := range scopes {
if scope == "openid" {
hasOpenID = true
break
}
}
if !hasOpenID {
scopes = append(scopes, "openid")
}
return &AuthParams{
URLValues: baseParams,
Scopes: scopes,
}, nil
}
// Keycloak requires realm and server configuration.
func (p *KeycloakProvider) ValidateConfig() error {
return p.BaseProvider.ValidateConfig()
}
+72
View File
@@ -0,0 +1,72 @@
package providers
import (
"net/url"
)
// OktaProvider encapsulates Okta-specific OIDC logic.
type OktaProvider struct {
*BaseProvider
}
// NewOktaProvider creates a new instance of the OktaProvider.
func NewOktaProvider() *OktaProvider {
return &OktaProvider{
BaseProvider: NewBaseProvider(),
}
}
// GetType returns the provider's type.
func (p *OktaProvider) GetType() ProviderType {
return ProviderTypeOkta
}
// GetCapabilities returns the specific capabilities of the Okta provider.
func (p *OktaProvider) GetCapabilities() ProviderCapabilities {
return ProviderCapabilities{
SupportsRefreshTokens: true,
RequiresOfflineAccessScope: true,
RequiresPromptConsent: false,
PreferredTokenValidation: "id", // Okta primarily uses ID tokens
}
}
// BuildAuthParams configures Okta-specific authentication parameters.
func (p *OktaProvider) BuildAuthParams(baseParams url.Values, scopes []string) (*AuthParams, error) {
// Okta supports various response types
baseParams.Set("response_type", "code")
// 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")
}
// Ensure openid scope is present
hasOpenID := false
for _, scope := range scopes {
if scope == "openid" {
hasOpenID = true
break
}
}
if !hasOpenID {
scopes = append(scopes, "openid")
}
return &AuthParams{
URLValues: baseParams,
Scopes: scopes,
}, nil
}
// Okta requires specific domain configuration and application setup.
func (p *OktaProvider) ValidateConfig() error {
return p.BaseProvider.ValidateConfig()
}
+24
View File
@@ -134,6 +134,30 @@ func (r *ProviderRegistry) detectProviderUnsafe(issuerURL string) OIDCProvider {
if strings.Contains(host, "login.microsoftonline.com") || strings.Contains(host, "sts.windows.net") {
return p
}
case ProviderTypeGitHub:
if strings.Contains(host, "github.com") {
return p
}
case ProviderTypeAuth0:
if strings.Contains(host, ".auth0.com") {
return p
}
case ProviderTypeOkta:
if strings.Contains(host, ".okta.com") || strings.Contains(host, ".oktapreview.com") || strings.Contains(host, ".okta-emea.com") {
return p
}
case ProviderTypeKeycloak:
if strings.Contains(host, "keycloak") || strings.Contains(normalizedURL.Path, "/auth/realms/") {
return p
}
case ProviderTypeAWSCognito:
if strings.Contains(host, "cognito-idp") && strings.Contains(host, ".amazonaws.com") {
return p
}
case ProviderTypeGitLab:
if strings.Contains(host, "gitlab.com") {
return p
}
}
}
+151
View File
@@ -0,0 +1,151 @@
package providers
import (
"fmt"
"strings"
)
// ProviderWarning represents a warning about provider limitations or requirements.
type ProviderWarning struct {
ProviderType ProviderType
Level string // "info", "warning", "error"
Message string
}
// GetProviderWarnings returns warnings about provider-specific limitations.
func GetProviderWarnings(providerType ProviderType) []ProviderWarning {
var warnings []ProviderWarning
switch providerType {
case ProviderTypeGitHub:
warnings = append(warnings, ProviderWarning{
ProviderType: ProviderTypeGitHub,
Level: "warning",
Message: "GitHub uses OAuth 2.0, not OpenID Connect. ID tokens are not available. Use access tokens for API calls only.",
})
warnings = append(warnings, ProviderWarning{
ProviderType: ProviderTypeGitHub,
Level: "info",
Message: "GitHub OAuth apps do not support refresh tokens. Users will need to re-authenticate when tokens expire.",
})
case ProviderTypeAuth0:
warnings = append(warnings, ProviderWarning{
ProviderType: ProviderTypeAuth0,
Level: "info",
Message: "Auth0 requires 'offline_access' scope for refresh tokens. This will be automatically added.",
})
case ProviderTypeOkta:
warnings = append(warnings, ProviderWarning{
ProviderType: ProviderTypeOkta,
Level: "info",
Message: "Okta requires proper application configuration in your Okta admin console for OIDC to work.",
})
case ProviderTypeKeycloak:
warnings = append(warnings, ProviderWarning{
ProviderType: ProviderTypeKeycloak,
Level: "info",
Message: "Keycloak detection is based on URL path '/auth/realms/'. Ensure your issuer URL follows this pattern.",
})
case ProviderTypeAWSCognito:
warnings = append(warnings, ProviderWarning{
ProviderType: ProviderTypeAWSCognito,
Level: "info",
Message: "AWS Cognito uses regional endpoints. Ensure your issuer URL includes the correct region (e.g., cognito-idp.us-east-1.amazonaws.com).",
})
case ProviderTypeGitLab:
warnings = append(warnings, ProviderWarning{
ProviderType: ProviderTypeGitLab,
Level: "info",
Message: "GitLab supports OIDC but requires application registration in GitLab admin settings.",
})
}
return warnings
}
// ValidateProviderCompatibility checks if a provider is suitable for OIDC authentication.
func ValidateProviderCompatibility(providerType ProviderType, requiresOIDC bool) error {
switch providerType {
case ProviderTypeGitHub:
if requiresOIDC {
return fmt.Errorf("GitHub does not support OpenID Connect. It only supports OAuth 2.0. Consider using a different provider for OIDC authentication")
}
return nil
default:
return nil
}
}
// GetProviderRecommendations returns setup recommendations for each provider.
func GetProviderRecommendations(providerType ProviderType) []string {
switch providerType {
case ProviderTypeGitHub:
return []string{
"Register an OAuth App in GitHub Settings > Developer settings > OAuth Apps",
"Use scopes: 'user:email', 'read:user' for basic profile access",
"GitHub tokens expire, plan for re-authentication flow",
}
case ProviderTypeAuth0:
return []string{
"Create an Application in Auth0 Dashboard",
"Set Application Type to 'Regular Web Application'",
"Configure Allowed Callback URLs with your redirect URI",
"Enable 'offline_access' scope for refresh tokens",
}
case ProviderTypeOkta:
return []string{
"Create an App Integration in Okta Admin Console",
"Choose 'OIDC - OpenID Connect' as sign-in method",
"Select 'Web Application' as application type",
"Configure redirect URIs and assign users/groups",
}
case ProviderTypeKeycloak:
return []string{
"Create a Client in your Keycloak realm",
"Set Client Protocol to 'openid-connect'",
"Configure Valid Redirect URIs",
"Ensure issuer URL format: https://your-keycloak/auth/realms/your-realm",
}
case ProviderTypeAWSCognito:
return []string{
"Create a User Pool in AWS Cognito",
"Create an App Client with 'Authorization code grant' enabled",
"Configure App Client settings and callback URLs",
"Use issuer URL format: https://cognito-idp.{region}.amazonaws.com/{userPoolId}",
}
case ProviderTypeGitLab:
return []string{
"Create an Application in GitLab (Admin Area > Applications)",
"Select 'openid', 'profile', 'email' scopes",
"Configure Redirect URI",
"Use issuer URL: https://gitlab.com (for GitLab.com)",
}
default:
return []string{}
}
}
// FormatProviderWarnings formats warnings for display.
func FormatProviderWarnings(warnings []ProviderWarning) string {
if len(warnings) == 0 {
return ""
}
var result strings.Builder
for _, warning := range warnings {
result.WriteString(fmt.Sprintf("[%s] %s\n", strings.ToUpper(warning.Level), warning.Message))
}
return result.String()
}
+195
View File
@@ -0,0 +1,195 @@
package providers
import (
"strings"
"testing"
)
// TestGetProviderWarnings tests that warnings are provided for providers with limitations
func TestGetProviderWarnings(t *testing.T) {
tests := []struct {
name string
providerType ProviderType
expectCount int
checkContent string
}{
{
name: "GitHub has OAuth 2.0 warning",
providerType: ProviderTypeGitHub,
expectCount: 2,
checkContent: "OAuth 2.0",
},
{
name: "Auth0 has offline_access info",
providerType: ProviderTypeAuth0,
expectCount: 1,
checkContent: "offline_access",
},
{
name: "Okta has configuration info",
providerType: ProviderTypeOkta,
expectCount: 1,
checkContent: "admin console",
},
{
name: "AWS Cognito has regional endpoint info",
providerType: ProviderTypeAWSCognito,
expectCount: 1,
checkContent: "regional endpoints",
},
{
name: "Generic provider has no warnings",
providerType: ProviderTypeGeneric,
expectCount: 0,
checkContent: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
warnings := GetProviderWarnings(tt.providerType)
if len(warnings) != tt.expectCount {
t.Errorf("Expected %d warnings, got %d", tt.expectCount, len(warnings))
}
if tt.checkContent != "" {
found := false
for _, warning := range warnings {
if strings.Contains(strings.ToLower(warning.Message), strings.ToLower(tt.checkContent)) {
found = true
break
}
}
if !found {
t.Errorf("Expected warning content '%s' not found", tt.checkContent)
}
}
})
}
}
// TestValidateProviderCompatibility tests OIDC compatibility validation
func TestValidateProviderCompatibility(t *testing.T) {
tests := []struct {
name string
providerType ProviderType
requiresOIDC bool
expectError bool
}{
{
name: "GitHub with OIDC requirement should error",
providerType: ProviderTypeGitHub,
requiresOIDC: true,
expectError: true,
},
{
name: "GitHub without OIDC requirement should pass",
providerType: ProviderTypeGitHub,
requiresOIDC: false,
expectError: false,
},
{
name: "Auth0 with OIDC requirement should pass",
providerType: ProviderTypeAuth0,
requiresOIDC: true,
expectError: false,
},
{
name: "Google with OIDC requirement should pass",
providerType: ProviderTypeGoogle,
requiresOIDC: true,
expectError: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateProviderCompatibility(tt.providerType, tt.requiresOIDC)
if tt.expectError && err == nil {
t.Error("Expected error but got none")
}
if !tt.expectError && err != nil {
t.Errorf("Unexpected error: %v", err)
}
})
}
}
// TestGetProviderRecommendations tests that recommendations are provided
func TestGetProviderRecommendations(t *testing.T) {
tests := []struct {
name string
providerType ProviderType
expectMin int
}{
{
name: "GitHub recommendations",
providerType: ProviderTypeGitHub,
expectMin: 3,
},
{
name: "Auth0 recommendations",
providerType: ProviderTypeAuth0,
expectMin: 3,
},
{
name: "AWS Cognito recommendations",
providerType: ProviderTypeAWSCognito,
expectMin: 3,
},
{
name: "Generic provider no recommendations",
providerType: ProviderTypeGeneric,
expectMin: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
recommendations := GetProviderRecommendations(tt.providerType)
if len(recommendations) < tt.expectMin {
t.Errorf("Expected at least %d recommendations, got %d", tt.expectMin, len(recommendations))
}
})
}
}
// TestFormatProviderWarnings tests warning formatting
func TestFormatProviderWarnings(t *testing.T) {
warnings := []ProviderWarning{
{
ProviderType: ProviderTypeGitHub,
Level: "warning",
Message: "Test warning message",
},
{
ProviderType: ProviderTypeGitHub,
Level: "info",
Message: "Test info message",
},
}
formatted := FormatProviderWarnings(warnings)
if !strings.Contains(formatted, "[WARNING]") {
t.Error("Expected formatted output to contain [WARNING]")
}
if !strings.Contains(formatted, "[INFO]") {
t.Error("Expected formatted output to contain [INFO]")
}
if !strings.Contains(formatted, "Test warning message") {
t.Error("Expected formatted output to contain warning message")
}
// Test empty warnings
emptyFormatted := FormatProviderWarnings([]ProviderWarning{})
if emptyFormatted != "" {
t.Error("Expected empty string for no warnings")
}
}