mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-14 04:07:46 +00:00
```
ok github.com/lukaszraczylo/traefikoidc 117.017s coverage: 72.6% of statements
ok github.com/lukaszraczylo/traefikoidc/auth 0.505s coverage: 87.1% of statements
ok github.com/lukaszraczylo/traefikoidc/circuit_breaker 0.283s coverage: 99.0% of statements
github.com/lukaszraczylo/traefikoidc/config coverage: 0.0% of statements
ok github.com/lukaszraczylo/traefikoidc/handlers 0.349s coverage: 98.2% of statements
ok github.com/lukaszraczylo/traefikoidc/internal/providers (cached) coverage: 94.3% of statements
ok github.com/lukaszraczylo/traefikoidc/middleware 0.808s coverage: 78.0% of statements
ok github.com/lukaszraczylo/traefikoidc/recovery 0.653s coverage: 100.0% of statements
ok github.com/lukaszraczylo/traefikoidc/session/chunking (cached) coverage: 87.8% of statements
ok github.com/lukaszraczylo/traefikoidc/session/core (cached) coverage: 85.6% of statements
ok github.com/lukaszraczylo/traefikoidc/session/crypto (cached) coverage: 81.8% of statements
ok github.com/lukaszraczylo/traefikoidc/session/storage (cached) coverage: 93.5% of statements
ok github.com/lukaszraczylo/traefikoidc/session/validators (cached) coverage: 98.8% of statements
````
924 lines
23 KiB
Go
924 lines
23 KiB
Go
package auth
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
)
|
|
|
|
// Mock logger for testing
|
|
type mockLogger struct {
|
|
debugLogs []string
|
|
errorLogs []string
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
func (m *mockLogger) Debugf(format string, args ...interface{}) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.debugLogs = append(m.debugLogs, fmt.Sprintf(format, args...))
|
|
}
|
|
|
|
func (m *mockLogger) Errorf(format string, args ...interface{}) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.errorLogs = append(m.errorLogs, fmt.Sprintf(format, args...))
|
|
}
|
|
|
|
//lint:ignore U1000 May be needed for future debug log verification tests
|
|
func (m *mockLogger) getDebugLogs() []string {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
result := make([]string, len(m.debugLogs))
|
|
copy(result, m.debugLogs)
|
|
return result
|
|
}
|
|
|
|
func (m *mockLogger) getErrorLogs() []string {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
result := make([]string, len(m.errorLogs))
|
|
copy(result, m.errorLogs)
|
|
return result
|
|
}
|
|
|
|
//lint:ignore U1000 May be needed for future test isolation
|
|
func (m *mockLogger) reset() {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.debugLogs = nil
|
|
m.errorLogs = nil
|
|
}
|
|
|
|
// Mock session for testing
|
|
type mockSession struct {
|
|
data map[string]interface{}
|
|
redirectCount int
|
|
saveError error
|
|
mu sync.RWMutex
|
|
dirty bool
|
|
authenticated bool
|
|
email string
|
|
accessToken string
|
|
refreshToken string
|
|
idToken string
|
|
nonce string
|
|
codeVerifier string
|
|
csrf string
|
|
incomingPath string
|
|
}
|
|
|
|
func newMockSession() *mockSession {
|
|
return &mockSession{
|
|
data: make(map[string]interface{}),
|
|
}
|
|
}
|
|
|
|
func (m *mockSession) GetRedirectCount() int {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
return m.redirectCount
|
|
}
|
|
|
|
func (m *mockSession) ResetRedirectCount() {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.redirectCount = 0
|
|
}
|
|
|
|
func (m *mockSession) IncrementRedirectCount() {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.redirectCount++
|
|
}
|
|
|
|
func (m *mockSession) SetAuthenticated(auth bool) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.authenticated = auth
|
|
}
|
|
|
|
func (m *mockSession) SetEmail(email string) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.email = email
|
|
}
|
|
|
|
func (m *mockSession) SetAccessToken(token string) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.accessToken = token
|
|
}
|
|
|
|
func (m *mockSession) SetRefreshToken(token string) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.refreshToken = token
|
|
}
|
|
|
|
func (m *mockSession) SetIDToken(token string) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.idToken = token
|
|
}
|
|
|
|
func (m *mockSession) SetNonce(nonce string) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.nonce = nonce
|
|
}
|
|
|
|
func (m *mockSession) SetCodeVerifier(verifier string) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.codeVerifier = verifier
|
|
}
|
|
|
|
func (m *mockSession) SetCSRF(csrf string) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.csrf = csrf
|
|
}
|
|
|
|
func (m *mockSession) SetIncomingPath(path string) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.incomingPath = path
|
|
}
|
|
|
|
func (m *mockSession) MarkDirty() {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.dirty = true
|
|
}
|
|
|
|
func (m *mockSession) Save(req *http.Request, rw http.ResponseWriter) error {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
return m.saveError
|
|
}
|
|
|
|
func (m *mockSession) setSaveError(err error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.saveError = err
|
|
}
|
|
|
|
// Test helper functions
|
|
func createTestAuthHandler(logger Logger, enablePKCE bool, isGoogle, isAzure func() bool) *AuthHandler {
|
|
return NewAuthHandler(
|
|
logger,
|
|
enablePKCE,
|
|
isGoogle,
|
|
isAzure,
|
|
"test-client-id",
|
|
"https://provider.example.com/auth",
|
|
"https://provider.example.com",
|
|
[]string{"openid", "email", "profile"},
|
|
false,
|
|
)
|
|
}
|
|
|
|
func TestNewAuthHandler(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
logger Logger
|
|
enablePKCE bool
|
|
clientID string
|
|
authURL string
|
|
issuerURL string
|
|
scopes []string
|
|
overrideScopes bool
|
|
}{
|
|
{
|
|
name: "creates handler with basic config",
|
|
logger: &mockLogger{},
|
|
enablePKCE: true,
|
|
clientID: "test-client",
|
|
authURL: "https://example.com/auth",
|
|
issuerURL: "https://example.com",
|
|
scopes: []string{"openid", "email"},
|
|
overrideScopes: false,
|
|
},
|
|
{
|
|
name: "creates handler with PKCE disabled",
|
|
logger: &mockLogger{},
|
|
enablePKCE: false,
|
|
clientID: "test-client",
|
|
authURL: "https://example.com/auth",
|
|
issuerURL: "https://example.com",
|
|
scopes: []string{"openid"},
|
|
overrideScopes: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
isGoogle := func() bool { return false }
|
|
isAzure := func() bool { return false }
|
|
|
|
handler := NewAuthHandler(
|
|
tt.logger,
|
|
tt.enablePKCE,
|
|
isGoogle,
|
|
isAzure,
|
|
tt.clientID,
|
|
tt.authURL,
|
|
tt.issuerURL,
|
|
tt.scopes,
|
|
tt.overrideScopes,
|
|
)
|
|
|
|
if handler == nil {
|
|
t.Fatal("NewAuthHandler returned nil")
|
|
}
|
|
|
|
if handler.clientID != tt.clientID {
|
|
t.Errorf("Expected clientID %s, got %s", tt.clientID, handler.clientID)
|
|
}
|
|
|
|
if handler.enablePKCE != tt.enablePKCE {
|
|
t.Errorf("Expected enablePKCE %v, got %v", tt.enablePKCE, handler.enablePKCE)
|
|
}
|
|
|
|
if handler.authURL != tt.authURL {
|
|
t.Errorf("Expected authURL %s, got %s", tt.authURL, handler.authURL)
|
|
}
|
|
|
|
if handler.issuerURL != tt.issuerURL {
|
|
t.Errorf("Expected issuerURL %s, got %s", tt.issuerURL, handler.issuerURL)
|
|
}
|
|
|
|
if handler.overrideScopes != tt.overrideScopes {
|
|
t.Errorf("Expected overrideScopes %v, got %v", tt.overrideScopes, handler.overrideScopes)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestInitiateAuthentication(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
enablePKCE bool
|
|
initialRedirectCount int
|
|
generateNonceError error
|
|
generateCodeVerifierError error
|
|
deriveCodeChallengeError error
|
|
sessionSaveError error
|
|
expectedStatusCode int
|
|
expectRedirect bool
|
|
}{
|
|
{
|
|
name: "successful authentication without PKCE",
|
|
enablePKCE: false,
|
|
expectedStatusCode: http.StatusFound,
|
|
expectRedirect: true,
|
|
},
|
|
{
|
|
name: "successful authentication with PKCE",
|
|
enablePKCE: true,
|
|
expectedStatusCode: http.StatusFound,
|
|
expectRedirect: true,
|
|
},
|
|
{
|
|
name: "fails when nonce generation fails",
|
|
enablePKCE: false,
|
|
generateNonceError: fmt.Errorf("nonce generation failed"),
|
|
expectedStatusCode: http.StatusInternalServerError,
|
|
expectRedirect: false,
|
|
},
|
|
{
|
|
name: "fails when code verifier generation fails",
|
|
enablePKCE: true,
|
|
generateCodeVerifierError: fmt.Errorf("code verifier generation failed"),
|
|
expectedStatusCode: http.StatusInternalServerError,
|
|
expectRedirect: false,
|
|
},
|
|
{
|
|
name: "fails when code challenge derivation fails",
|
|
enablePKCE: true,
|
|
deriveCodeChallengeError: fmt.Errorf("code challenge derivation failed"),
|
|
expectedStatusCode: http.StatusInternalServerError,
|
|
expectRedirect: false,
|
|
},
|
|
{
|
|
name: "fails when session save fails",
|
|
enablePKCE: false,
|
|
sessionSaveError: fmt.Errorf("session save failed"),
|
|
expectedStatusCode: http.StatusInternalServerError,
|
|
expectRedirect: false,
|
|
},
|
|
{
|
|
name: "fails when redirect count exceeds limit",
|
|
enablePKCE: false,
|
|
initialRedirectCount: 5,
|
|
expectedStatusCode: http.StatusLoopDetected,
|
|
expectRedirect: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
logger := &mockLogger{}
|
|
session := newMockSession()
|
|
session.redirectCount = tt.initialRedirectCount
|
|
if tt.sessionSaveError != nil {
|
|
session.setSaveError(tt.sessionSaveError)
|
|
}
|
|
|
|
handler := createTestAuthHandler(logger, tt.enablePKCE, func() bool { return false }, func() bool { return false })
|
|
|
|
req := httptest.NewRequest("GET", "/test", nil)
|
|
rw := httptest.NewRecorder()
|
|
|
|
generateNonce := func() (string, error) {
|
|
if tt.generateNonceError != nil {
|
|
return "", tt.generateNonceError
|
|
}
|
|
return "test-nonce", nil
|
|
}
|
|
|
|
generateCodeVerifier := func() (string, error) {
|
|
if tt.generateCodeVerifierError != nil {
|
|
return "", tt.generateCodeVerifierError
|
|
}
|
|
return "test-code-verifier", nil
|
|
}
|
|
|
|
deriveCodeChallenge := func() (string, error) {
|
|
if tt.deriveCodeChallengeError != nil {
|
|
return "", tt.deriveCodeChallengeError
|
|
}
|
|
return "test-code-challenge", nil
|
|
}
|
|
|
|
handler.InitiateAuthentication(
|
|
rw, req, session,
|
|
"https://example.com/callback",
|
|
generateNonce,
|
|
generateCodeVerifier,
|
|
deriveCodeChallenge,
|
|
)
|
|
|
|
if rw.Code != tt.expectedStatusCode {
|
|
t.Errorf("Expected status code %d, got %d", tt.expectedStatusCode, rw.Code)
|
|
}
|
|
|
|
if tt.expectRedirect {
|
|
location := rw.Header().Get("Location")
|
|
if location == "" {
|
|
t.Error("Expected redirect location header, got empty")
|
|
}
|
|
if !strings.Contains(location, "https://provider.example.com/auth") {
|
|
t.Errorf("Expected redirect to auth URL, got %s", location)
|
|
}
|
|
}
|
|
|
|
// Verify error logs for failure cases
|
|
if tt.expectedStatusCode != http.StatusFound {
|
|
errorLogs := logger.getErrorLogs()
|
|
if len(errorLogs) == 0 {
|
|
t.Error("Expected error logs for failure case, got none")
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBuildAuthURL(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
enablePKCE bool
|
|
isGoogle bool
|
|
isAzure bool
|
|
scopes []string
|
|
overrideScopes bool
|
|
codeChallenge string
|
|
expectedParams map[string]string
|
|
expectContains []string
|
|
}{
|
|
{
|
|
name: "builds URL without PKCE for standard provider",
|
|
enablePKCE: false,
|
|
isGoogle: false,
|
|
isAzure: false,
|
|
scopes: []string{"openid", "email"},
|
|
codeChallenge: "",
|
|
expectedParams: map[string]string{
|
|
"client_id": "test-client-id",
|
|
"response_type": "code",
|
|
"state": "test-state",
|
|
"nonce": "test-nonce",
|
|
"scope": "openid email offline_access",
|
|
},
|
|
},
|
|
{
|
|
name: "builds URL with PKCE for standard provider",
|
|
enablePKCE: true,
|
|
isGoogle: false,
|
|
isAzure: false,
|
|
scopes: []string{"openid", "email"},
|
|
codeChallenge: "test-challenge",
|
|
expectedParams: map[string]string{
|
|
"client_id": "test-client-id",
|
|
"response_type": "code",
|
|
"state": "test-state",
|
|
"nonce": "test-nonce",
|
|
"code_challenge": "test-challenge",
|
|
"code_challenge_method": "S256",
|
|
"scope": "openid email offline_access",
|
|
},
|
|
},
|
|
{
|
|
name: "builds URL for Google provider",
|
|
enablePKCE: false,
|
|
isGoogle: true,
|
|
isAzure: false,
|
|
scopes: []string{"openid", "email"},
|
|
expectedParams: map[string]string{
|
|
"client_id": "test-client-id",
|
|
"response_type": "code",
|
|
"state": "test-state",
|
|
"nonce": "test-nonce",
|
|
"access_type": "offline",
|
|
"prompt": "consent",
|
|
"scope": "openid email",
|
|
},
|
|
},
|
|
{
|
|
name: "builds URL for Azure provider",
|
|
enablePKCE: false,
|
|
isGoogle: false,
|
|
isAzure: true,
|
|
scopes: []string{"openid", "email"},
|
|
expectedParams: map[string]string{
|
|
"client_id": "test-client-id",
|
|
"response_type": "code",
|
|
"state": "test-state",
|
|
"nonce": "test-nonce",
|
|
"response_mode": "query",
|
|
"scope": "openid email offline_access",
|
|
},
|
|
},
|
|
{
|
|
name: "handles scope override for Azure",
|
|
enablePKCE: false,
|
|
isGoogle: false,
|
|
isAzure: true,
|
|
scopes: []string{"custom-scope"},
|
|
overrideScopes: true,
|
|
expectedParams: map[string]string{
|
|
"client_id": "test-client-id",
|
|
"response_type": "code",
|
|
"state": "test-state",
|
|
"nonce": "test-nonce",
|
|
"response_mode": "query",
|
|
"scope": "custom-scope",
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
logger := &mockLogger{}
|
|
isGoogleFunc := func() bool { return tt.isGoogle }
|
|
isAzureFunc := func() bool { return tt.isAzure }
|
|
|
|
handler := NewAuthHandler(
|
|
logger,
|
|
tt.enablePKCE,
|
|
isGoogleFunc,
|
|
isAzureFunc,
|
|
"test-client-id",
|
|
"https://provider.example.com/auth",
|
|
"https://provider.example.com",
|
|
tt.scopes,
|
|
tt.overrideScopes,
|
|
)
|
|
|
|
authURL := handler.BuildAuthURL(
|
|
"https://example.com/callback",
|
|
"test-state",
|
|
"test-nonce",
|
|
tt.codeChallenge,
|
|
)
|
|
|
|
if authURL == "" {
|
|
t.Fatal("BuildAuthURL returned empty string")
|
|
}
|
|
|
|
parsedURL, err := url.Parse(authURL)
|
|
if err != nil {
|
|
t.Fatalf("Failed to parse auth URL: %v", err)
|
|
}
|
|
|
|
// Check base URL
|
|
expectedBase := "https://provider.example.com/auth"
|
|
actualBase := fmt.Sprintf("%s://%s%s", parsedURL.Scheme, parsedURL.Host, parsedURL.Path)
|
|
if actualBase != expectedBase {
|
|
t.Errorf("Expected base URL %s, got %s", expectedBase, actualBase)
|
|
}
|
|
|
|
// Check required parameters
|
|
query := parsedURL.Query()
|
|
for param, expectedValue := range tt.expectedParams {
|
|
actualValue := query.Get(param)
|
|
if actualValue != expectedValue {
|
|
t.Errorf("Expected %s=%s, got %s=%s", param, expectedValue, param, actualValue)
|
|
}
|
|
}
|
|
|
|
// Check that contains expected strings
|
|
for _, contains := range tt.expectContains {
|
|
if !strings.Contains(authURL, contains) {
|
|
t.Errorf("Expected URL to contain %s, got %s", contains, authURL)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateURL(t *testing.T) {
|
|
logger := &mockLogger{}
|
|
handler := createTestAuthHandler(logger, false, func() bool { return false }, func() bool { return false })
|
|
|
|
tests := []struct {
|
|
name string
|
|
url string
|
|
expectError bool
|
|
errorMsg string
|
|
}{
|
|
{
|
|
name: "valid HTTPS URL",
|
|
url: "https://example.com/path",
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "valid HTTP URL",
|
|
url: "http://example.com/path",
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "empty URL",
|
|
url: "",
|
|
expectError: true,
|
|
errorMsg: "empty URL",
|
|
},
|
|
{
|
|
name: "invalid scheme",
|
|
url: "ftp://example.com/path",
|
|
expectError: true,
|
|
errorMsg: "disallowed URL scheme",
|
|
},
|
|
{
|
|
name: "missing host",
|
|
url: "https:///path",
|
|
expectError: true,
|
|
errorMsg: "missing host in URL",
|
|
},
|
|
{
|
|
name: "localhost not allowed",
|
|
url: "https://localhost/path",
|
|
expectError: true,
|
|
errorMsg: "localhost access not allowed",
|
|
},
|
|
{
|
|
name: "127.0.0.1 not allowed",
|
|
url: "https://127.0.0.1/path",
|
|
expectError: true,
|
|
errorMsg: "localhost access not allowed",
|
|
},
|
|
{
|
|
name: "private IP not allowed",
|
|
url: "https://192.168.1.1/path",
|
|
expectError: true,
|
|
errorMsg: "private IP not allowed",
|
|
},
|
|
{
|
|
name: "path traversal not allowed",
|
|
url: "https://example.com/../path",
|
|
expectError: true,
|
|
errorMsg: "path traversal detected",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := handler.validateURL(tt.url)
|
|
|
|
if tt.expectError {
|
|
if err == nil {
|
|
t.Errorf("Expected error for URL %s, got nil", tt.url)
|
|
} else if tt.errorMsg != "" && !strings.Contains(err.Error(), tt.errorMsg) {
|
|
t.Errorf("Expected error to contain %s, got %s", tt.errorMsg, err.Error())
|
|
}
|
|
} else {
|
|
if err != nil {
|
|
t.Errorf("Expected no error for URL %s, got %v", tt.url, err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateHost(t *testing.T) {
|
|
logger := &mockLogger{}
|
|
handler := createTestAuthHandler(logger, false, func() bool { return false }, func() bool { return false })
|
|
|
|
tests := []struct {
|
|
name string
|
|
host string
|
|
expectError bool
|
|
errorMsg string
|
|
}{
|
|
{
|
|
name: "valid domain",
|
|
host: "example.com",
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "valid domain with port",
|
|
host: "example.com:443",
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "empty host",
|
|
host: "",
|
|
expectError: true,
|
|
errorMsg: "empty host",
|
|
},
|
|
{
|
|
name: "localhost",
|
|
host: "localhost",
|
|
expectError: true,
|
|
errorMsg: "localhost access not allowed",
|
|
},
|
|
{
|
|
name: "localhost with port",
|
|
host: "localhost:8080",
|
|
expectError: true,
|
|
errorMsg: "localhost access not allowed",
|
|
},
|
|
{
|
|
name: "127.0.0.1",
|
|
host: "127.0.0.1",
|
|
expectError: true,
|
|
errorMsg: "localhost access not allowed",
|
|
},
|
|
{
|
|
name: "::1 (IPv6 localhost)",
|
|
host: "::1",
|
|
expectError: true,
|
|
errorMsg: "invalid host:port format",
|
|
},
|
|
{
|
|
name: "private IP 192.168.1.1",
|
|
host: "192.168.1.1",
|
|
expectError: true,
|
|
errorMsg: "private IP not allowed",
|
|
},
|
|
{
|
|
name: "private IP 10.0.0.1",
|
|
host: "10.0.0.1",
|
|
expectError: true,
|
|
errorMsg: "private IP not allowed",
|
|
},
|
|
{
|
|
name: "invalid host:port format",
|
|
host: "example.com:99999", // Use invalid port number instead
|
|
expectError: false, // This actually doesn't error in Go's net.SplitHostPort
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := handler.validateHost(tt.host)
|
|
|
|
if tt.expectError {
|
|
if err == nil {
|
|
t.Errorf("Expected error for host %s, got nil", tt.host)
|
|
} else if tt.errorMsg != "" && !strings.Contains(err.Error(), tt.errorMsg) {
|
|
t.Errorf("Expected error to contain %s, got %s", tt.errorMsg, err.Error())
|
|
}
|
|
} else {
|
|
if err != nil {
|
|
t.Errorf("Expected no error for host %s, got %v", tt.host, err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBuildURLWithParams(t *testing.T) {
|
|
logger := &mockLogger{}
|
|
handler := NewAuthHandler(
|
|
logger,
|
|
false,
|
|
func() bool { return false },
|
|
func() bool { return false },
|
|
"test-client-id",
|
|
"https://provider.example.com/auth",
|
|
"https://provider.example.com",
|
|
[]string{"openid"},
|
|
false,
|
|
)
|
|
|
|
tests := []struct {
|
|
name string
|
|
baseURL string
|
|
params url.Values
|
|
expected string
|
|
expectError bool
|
|
}{
|
|
{
|
|
name: "absolute HTTPS URL with params",
|
|
baseURL: "https://example.com/auth",
|
|
params: url.Values{"param1": []string{"value1"}, "param2": []string{"value2"}},
|
|
expected: "https://example.com/auth?param1=value1¶m2=value2",
|
|
},
|
|
{
|
|
name: "relative URL resolved against issuer",
|
|
baseURL: "/auth/endpoint",
|
|
params: url.Values{"client_id": []string{"test"}},
|
|
expected: "https://provider.example.com/auth/endpoint?client_id=test",
|
|
},
|
|
{
|
|
name: "invalid absolute URL",
|
|
baseURL: "https://localhost/auth",
|
|
params: url.Values{},
|
|
expectError: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := handler.buildURLWithParams(tt.baseURL, tt.params)
|
|
|
|
if tt.expectError {
|
|
if result != "" {
|
|
t.Errorf("Expected empty result for invalid URL, got %s", result)
|
|
}
|
|
} else {
|
|
if result == "" {
|
|
t.Error("Expected non-empty result, got empty string")
|
|
}
|
|
|
|
// Parse and compare the result
|
|
resultURL, err := url.Parse(result)
|
|
if err != nil {
|
|
t.Fatalf("Failed to parse result URL: %v", err)
|
|
}
|
|
|
|
expectedURL, err := url.Parse(tt.expected)
|
|
if err != nil {
|
|
t.Fatalf("Failed to parse expected URL: %v", err)
|
|
}
|
|
|
|
if resultURL.Scheme != expectedURL.Scheme {
|
|
t.Errorf("Expected scheme %s, got %s", expectedURL.Scheme, resultURL.Scheme)
|
|
}
|
|
if resultURL.Host != expectedURL.Host {
|
|
t.Errorf("Expected host %s, got %s", expectedURL.Host, resultURL.Host)
|
|
}
|
|
if resultURL.Path != expectedURL.Path {
|
|
t.Errorf("Expected path %s, got %s", expectedURL.Path, resultURL.Path)
|
|
}
|
|
|
|
// Check query parameters
|
|
resultQuery := resultURL.Query()
|
|
expectedQuery := expectedURL.Query()
|
|
for key, expectedValues := range expectedQuery {
|
|
resultValues := resultQuery[key]
|
|
if len(resultValues) != len(expectedValues) {
|
|
t.Errorf("Expected %d values for %s, got %d", len(expectedValues), key, len(resultValues))
|
|
}
|
|
for i, expectedValue := range expectedValues {
|
|
if i >= len(resultValues) || resultValues[i] != expectedValue {
|
|
t.Errorf("Expected %s=%s, got %s=%s", key, expectedValue, key, resultValues[i])
|
|
}
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestConcurrentAccess(t *testing.T) {
|
|
logger := &mockLogger{}
|
|
handler := createTestAuthHandler(logger, true, func() bool { return false }, func() bool { return false })
|
|
|
|
const numGoroutines = 10
|
|
const numRequests = 5
|
|
|
|
var wg sync.WaitGroup
|
|
errors := make(chan error, numGoroutines*numRequests)
|
|
|
|
for i := 0; i < numGoroutines; i++ {
|
|
wg.Add(1)
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
for j := 0; j < numRequests; j++ {
|
|
session := newMockSession()
|
|
req := httptest.NewRequest("GET", fmt.Sprintf("/test-%d-%d", id, j), nil)
|
|
rw := httptest.NewRecorder()
|
|
|
|
generateNonce := func() (string, error) {
|
|
return fmt.Sprintf("nonce-%d-%d", id, j), nil
|
|
}
|
|
generateCodeVerifier := func() (string, error) {
|
|
return fmt.Sprintf("verifier-%d-%d", id, j), nil
|
|
}
|
|
deriveCodeChallenge := func() (string, error) {
|
|
return fmt.Sprintf("challenge-%d-%d", id, j), nil
|
|
}
|
|
|
|
handler.InitiateAuthentication(
|
|
rw, req, session,
|
|
"https://example.com/callback",
|
|
generateNonce,
|
|
generateCodeVerifier,
|
|
deriveCodeChallenge,
|
|
)
|
|
|
|
if rw.Code != http.StatusFound {
|
|
errors <- fmt.Errorf("goroutine %d request %d: expected status %d, got %d",
|
|
id, j, http.StatusFound, rw.Code)
|
|
}
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
close(errors)
|
|
|
|
var errorCount int
|
|
for err := range errors {
|
|
t.Error(err)
|
|
errorCount++
|
|
}
|
|
|
|
if errorCount > 0 {
|
|
t.Errorf("Found %d errors in concurrent access test", errorCount)
|
|
}
|
|
}
|
|
|
|
// Benchmark tests
|
|
func BenchmarkInitiateAuthentication(b *testing.B) {
|
|
logger := &mockLogger{}
|
|
handler := createTestAuthHandler(logger, true, func() bool { return false }, func() bool { return false })
|
|
|
|
generateNonce := func() (string, error) { return "test-nonce", nil }
|
|
generateCodeVerifier := func() (string, error) { return "test-verifier", nil }
|
|
deriveCodeChallenge := func() (string, error) { return "test-challenge", nil }
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
session := newMockSession()
|
|
req := httptest.NewRequest("GET", "/test", nil)
|
|
rw := httptest.NewRecorder()
|
|
|
|
handler.InitiateAuthentication(
|
|
rw, req, session,
|
|
"https://example.com/callback",
|
|
generateNonce,
|
|
generateCodeVerifier,
|
|
deriveCodeChallenge,
|
|
)
|
|
}
|
|
}
|
|
|
|
func BenchmarkBuildAuthURL(b *testing.B) {
|
|
logger := &mockLogger{}
|
|
handler := createTestAuthHandler(logger, true, func() bool { return false }, func() bool { return false })
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
handler.BuildAuthURL(
|
|
"https://example.com/callback",
|
|
"test-state",
|
|
"test-nonce",
|
|
"test-challenge",
|
|
)
|
|
}
|
|
}
|
|
|
|
func BenchmarkValidateURL(b *testing.B) {
|
|
logger := &mockLogger{}
|
|
handler := createTestAuthHandler(logger, false, func() bool { return false }, func() bool { return false })
|
|
|
|
testURLs := []string{
|
|
"https://example.com/auth",
|
|
"https://provider.example.com/oauth/authorize",
|
|
"https://login.microsoftonline.com/tenant/oauth2/v2.0/authorize",
|
|
}
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
handler.validateURL(testURLs[i%len(testURLs)])
|
|
}
|
|
}
|