mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-15 04:18:03 +00:00
Add new providers.
This commit is contained in:
@@ -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()
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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"},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user