mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-06-05 22:44:17 +00:00
6efb78b7a8
* Smarter approach to the cookies - Single maxCookieSize = 1400 constant with clear documentation - Combined cookie storage for ~40-45% size reduction - Backward compatible migration from legacy cookies * Tuneup the code.
1003 lines
28 KiB
Go
1003 lines
28 KiB
Go
package traefikoidc
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// TestDynamicClientRegistrarCreation tests creating a new DCR registrar
|
|
func TestDynamicClientRegistrarCreation(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
httpClient *http.Client
|
|
logger *Logger
|
|
dcrConfig *DynamicClientRegistrationConfig
|
|
providerURL string
|
|
}{
|
|
{
|
|
name: "with all parameters",
|
|
httpClient: &http.Client{},
|
|
logger: NewLogger("DEBUG"),
|
|
dcrConfig: &DynamicClientRegistrationConfig{
|
|
Enabled: true,
|
|
ClientMetadata: &ClientRegistrationMetadata{
|
|
RedirectURIs: []string{"https://example.com/callback"},
|
|
ClientName: "Test Client",
|
|
},
|
|
},
|
|
providerURL: "https://example.com",
|
|
},
|
|
{
|
|
name: "with nil logger",
|
|
httpClient: &http.Client{},
|
|
logger: nil,
|
|
dcrConfig: &DynamicClientRegistrationConfig{
|
|
Enabled: true,
|
|
},
|
|
providerURL: "https://example.com",
|
|
},
|
|
{
|
|
name: "with nil config",
|
|
httpClient: &http.Client{},
|
|
logger: NewLogger("DEBUG"),
|
|
dcrConfig: nil,
|
|
providerURL: "https://example.com",
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
registrar := NewDynamicClientRegistrar(tc.httpClient, tc.logger, tc.dcrConfig, tc.providerURL)
|
|
|
|
if registrar == nil {
|
|
t.Fatal("Expected non-nil registrar")
|
|
}
|
|
|
|
if registrar.httpClient != tc.httpClient {
|
|
t.Error("HTTP client not set correctly")
|
|
}
|
|
|
|
if registrar.providerURL != tc.providerURL {
|
|
t.Errorf("Provider URL mismatch: got %s, want %s", registrar.providerURL, tc.providerURL)
|
|
}
|
|
|
|
if registrar.config != tc.dcrConfig {
|
|
t.Error("Config not set correctly")
|
|
}
|
|
|
|
// Logger should never be nil (fallback to no-op logger)
|
|
if registrar.logger == nil {
|
|
t.Error("Logger should not be nil")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestRegisterClientSuccess tests successful client registration
|
|
func TestRegisterClientSuccess(t *testing.T) {
|
|
// Create mock server that returns successful registration response
|
|
expectedClientID := "test-client-id-12345"
|
|
expectedClientSecret := "test-client-secret-67890"
|
|
|
|
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
// Verify request
|
|
if r.Method != http.MethodPost {
|
|
t.Errorf("Expected POST request, got %s", r.Method)
|
|
}
|
|
|
|
if r.Header.Get("Content-Type") != "application/json" {
|
|
t.Errorf("Expected Content-Type: application/json, got %s", r.Header.Get("Content-Type"))
|
|
}
|
|
|
|
// Parse request body
|
|
var reqBody map[string]interface{}
|
|
if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil {
|
|
t.Errorf("Failed to decode request body: %v", err)
|
|
http.Error(w, "Bad request", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Verify redirect_uris is present
|
|
if _, ok := reqBody["redirect_uris"]; !ok {
|
|
t.Error("redirect_uris missing from request")
|
|
}
|
|
|
|
// Return successful response
|
|
resp := ClientRegistrationResponse{
|
|
ClientID: expectedClientID,
|
|
ClientSecret: expectedClientSecret,
|
|
ClientIDIssuedAt: time.Now().Unix(),
|
|
ClientSecretExpiresAt: 0, // Never expires
|
|
RedirectURIs: []string{"https://example.com/callback"},
|
|
ResponseTypes: []string{"code"},
|
|
GrantTypes: []string{"authorization_code", "refresh_token"},
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusCreated)
|
|
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
|
t.Errorf("Failed to encode response: %v", err)
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
|
|
// Create registrar
|
|
dcrConfig := &DynamicClientRegistrationConfig{
|
|
Enabled: true,
|
|
ClientMetadata: &ClientRegistrationMetadata{
|
|
RedirectURIs: []string{"https://example.com/callback"},
|
|
ClientName: "Test Client",
|
|
},
|
|
}
|
|
|
|
registrar := NewDynamicClientRegistrar(
|
|
server.Client(),
|
|
NewLogger("DEBUG"),
|
|
dcrConfig,
|
|
server.URL,
|
|
)
|
|
|
|
// Perform registration
|
|
ctx := context.Background()
|
|
resp, err := registrar.RegisterClient(ctx, server.URL+"/register")
|
|
|
|
if err != nil {
|
|
t.Fatalf("Registration failed: %v", err)
|
|
}
|
|
|
|
if resp.ClientID != expectedClientID {
|
|
t.Errorf("ClientID mismatch: got %s, want %s", resp.ClientID, expectedClientID)
|
|
}
|
|
|
|
if resp.ClientSecret != expectedClientSecret {
|
|
t.Errorf("ClientSecret mismatch: got %s, want %s", resp.ClientSecret, expectedClientSecret)
|
|
}
|
|
|
|
// Verify response is cached
|
|
cached := registrar.GetCachedResponse()
|
|
if cached == nil {
|
|
t.Fatal("Response should be cached")
|
|
}
|
|
if cached.ClientID != expectedClientID {
|
|
t.Errorf("Cached ClientID mismatch: got %s, want %s", cached.ClientID, expectedClientID)
|
|
}
|
|
}
|
|
|
|
// TestRegisterClientWithInitialAccessToken tests registration with an initial access token
|
|
func TestRegisterClientWithInitialAccessToken(t *testing.T) {
|
|
expectedToken := "initial-access-token-12345"
|
|
receivedToken := ""
|
|
|
|
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
// Capture the authorization header
|
|
authHeader := r.Header.Get("Authorization")
|
|
if authHeader != "" {
|
|
receivedToken = authHeader
|
|
}
|
|
|
|
resp := ClientRegistrationResponse{
|
|
ClientID: "test-client-id",
|
|
ClientSecret: "test-client-secret",
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusCreated)
|
|
json.NewEncoder(w).Encode(resp)
|
|
}))
|
|
defer server.Close()
|
|
|
|
dcrConfig := &DynamicClientRegistrationConfig{
|
|
Enabled: true,
|
|
InitialAccessToken: expectedToken,
|
|
ClientMetadata: &ClientRegistrationMetadata{
|
|
RedirectURIs: []string{"https://example.com/callback"},
|
|
},
|
|
}
|
|
|
|
registrar := NewDynamicClientRegistrar(
|
|
server.Client(),
|
|
NewLogger("DEBUG"),
|
|
dcrConfig,
|
|
server.URL,
|
|
)
|
|
|
|
ctx := context.Background()
|
|
_, err := registrar.RegisterClient(ctx, server.URL+"/register")
|
|
|
|
if err != nil {
|
|
t.Fatalf("Registration failed: %v", err)
|
|
}
|
|
|
|
expectedAuthHeader := "Bearer " + expectedToken
|
|
if receivedToken != expectedAuthHeader {
|
|
t.Errorf("Authorization header mismatch: got %s, want %s", receivedToken, expectedAuthHeader)
|
|
}
|
|
}
|
|
|
|
// TestRegisterClientError tests error handling during registration
|
|
func TestRegisterClientError(t *testing.T) {
|
|
tests := []struct {
|
|
serverResponse func(w http.ResponseWriter, r *http.Request)
|
|
name string
|
|
errorContains string
|
|
expectError bool
|
|
}{
|
|
{
|
|
name: "invalid_redirect_uri error",
|
|
serverResponse: func(w http.ResponseWriter, r *http.Request) {
|
|
resp := ClientRegistrationError{
|
|
Error: "invalid_redirect_uri",
|
|
ErrorDescription: "The redirect_uri is not valid",
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
json.NewEncoder(w).Encode(resp)
|
|
},
|
|
expectError: true,
|
|
errorContains: "invalid_redirect_uri",
|
|
},
|
|
{
|
|
name: "invalid_client_metadata error",
|
|
serverResponse: func(w http.ResponseWriter, r *http.Request) {
|
|
resp := ClientRegistrationError{
|
|
Error: "invalid_client_metadata",
|
|
ErrorDescription: "Missing required field",
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
json.NewEncoder(w).Encode(resp)
|
|
},
|
|
expectError: true,
|
|
errorContains: "invalid_client_metadata",
|
|
},
|
|
{
|
|
name: "server error",
|
|
serverResponse: func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
w.Write([]byte("Internal Server Error"))
|
|
},
|
|
expectError: true,
|
|
errorContains: "500",
|
|
},
|
|
{
|
|
name: "missing client_id in response",
|
|
serverResponse: func(w http.ResponseWriter, r *http.Request) {
|
|
resp := map[string]string{
|
|
"client_secret": "some-secret",
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusCreated)
|
|
json.NewEncoder(w).Encode(resp)
|
|
},
|
|
expectError: true,
|
|
errorContains: "missing client_id",
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
server := httptest.NewTLSServer(http.HandlerFunc(tc.serverResponse))
|
|
defer server.Close()
|
|
|
|
dcrConfig := &DynamicClientRegistrationConfig{
|
|
Enabled: true,
|
|
ClientMetadata: &ClientRegistrationMetadata{
|
|
RedirectURIs: []string{"https://example.com/callback"},
|
|
},
|
|
}
|
|
|
|
registrar := NewDynamicClientRegistrar(
|
|
server.Client(),
|
|
NewLogger("DEBUG"),
|
|
dcrConfig,
|
|
server.URL,
|
|
)
|
|
|
|
ctx := context.Background()
|
|
_, err := registrar.RegisterClient(ctx, server.URL+"/register")
|
|
|
|
if tc.expectError {
|
|
if err == nil {
|
|
t.Fatal("Expected error but got nil")
|
|
}
|
|
if tc.errorContains != "" && !stringContains(err.Error(), tc.errorContains) {
|
|
t.Errorf("Error should contain %q, got: %v", tc.errorContains, err)
|
|
}
|
|
} else {
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestRegisterClientDisabled tests that registration fails when not enabled
|
|
func TestRegisterClientDisabled(t *testing.T) {
|
|
tests := []struct {
|
|
dcrConfig *DynamicClientRegistrationConfig
|
|
name string
|
|
}{
|
|
{
|
|
name: "nil config",
|
|
dcrConfig: nil,
|
|
},
|
|
{
|
|
name: "enabled=false",
|
|
dcrConfig: &DynamicClientRegistrationConfig{
|
|
Enabled: false,
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
registrar := NewDynamicClientRegistrar(
|
|
&http.Client{},
|
|
NewLogger("DEBUG"),
|
|
tc.dcrConfig,
|
|
"https://example.com",
|
|
)
|
|
|
|
ctx := context.Background()
|
|
_, err := registrar.RegisterClient(ctx, "https://example.com/register")
|
|
|
|
if err == nil {
|
|
t.Fatal("Expected error when DCR is disabled")
|
|
}
|
|
|
|
if !stringContains(err.Error(), "not enabled") {
|
|
t.Errorf("Error should mention 'not enabled', got: %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestRegisterClientMissingRedirectURIs tests that registration fails without redirect_uris
|
|
func TestRegisterClientMissingRedirectURIs(t *testing.T) {
|
|
dcrConfig := &DynamicClientRegistrationConfig{
|
|
Enabled: true,
|
|
ClientMetadata: &ClientRegistrationMetadata{
|
|
ClientName: "Test Client",
|
|
// Missing RedirectURIs
|
|
},
|
|
}
|
|
|
|
registrar := NewDynamicClientRegistrar(
|
|
&http.Client{},
|
|
NewLogger("DEBUG"),
|
|
dcrConfig,
|
|
"https://example.com",
|
|
)
|
|
|
|
ctx := context.Background()
|
|
_, err := registrar.RegisterClient(ctx, "https://example.com/register")
|
|
|
|
if err == nil {
|
|
t.Fatal("Expected error when redirect_uris is missing")
|
|
}
|
|
|
|
if !stringContains(err.Error(), "redirect_uris") {
|
|
t.Errorf("Error should mention 'redirect_uris', got: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestRegisterClientNoEndpoint tests that registration fails without an endpoint
|
|
func TestRegisterClientNoEndpoint(t *testing.T) {
|
|
dcrConfig := &DynamicClientRegistrationConfig{
|
|
Enabled: true,
|
|
ClientMetadata: &ClientRegistrationMetadata{
|
|
RedirectURIs: []string{"https://example.com/callback"},
|
|
},
|
|
}
|
|
|
|
registrar := NewDynamicClientRegistrar(
|
|
&http.Client{},
|
|
NewLogger("DEBUG"),
|
|
dcrConfig,
|
|
"https://example.com",
|
|
)
|
|
|
|
ctx := context.Background()
|
|
_, err := registrar.RegisterClient(ctx, "") // Empty endpoint
|
|
|
|
if err == nil {
|
|
t.Fatal("Expected error when registration endpoint is missing")
|
|
}
|
|
|
|
if !stringContains(err.Error(), "no registration endpoint") {
|
|
t.Errorf("Error should mention 'no registration endpoint', got: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestRegisterClientHTTPSRequired tests that HTTPS is required for non-localhost endpoints
|
|
func TestRegisterClientHTTPSRequired(t *testing.T) {
|
|
dcrConfig := &DynamicClientRegistrationConfig{
|
|
Enabled: true,
|
|
ClientMetadata: &ClientRegistrationMetadata{
|
|
RedirectURIs: []string{"https://example.com/callback"},
|
|
},
|
|
}
|
|
|
|
registrar := NewDynamicClientRegistrar(
|
|
&http.Client{},
|
|
NewLogger("DEBUG"),
|
|
dcrConfig,
|
|
"https://example.com",
|
|
)
|
|
|
|
ctx := context.Background()
|
|
_, err := registrar.RegisterClient(ctx, "http://example.com/register") // HTTP instead of HTTPS
|
|
|
|
if err == nil {
|
|
t.Fatal("Expected error when using HTTP for non-localhost endpoint")
|
|
}
|
|
|
|
if !stringContains(err.Error(), "HTTPS") {
|
|
t.Errorf("Error should mention 'HTTPS', got: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestRegisterClientCredentialsPersistence tests saving and loading credentials
|
|
func TestRegisterClientCredentialsPersistence(t *testing.T) {
|
|
// Create a temp file for credentials
|
|
tempDir := t.TempDir()
|
|
credentialsFile := filepath.Join(tempDir, "test-credentials.json")
|
|
|
|
expectedClientID := "persisted-client-id"
|
|
expectedClientSecret := "persisted-client-secret"
|
|
|
|
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
resp := ClientRegistrationResponse{
|
|
ClientID: expectedClientID,
|
|
ClientSecret: expectedClientSecret,
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusCreated)
|
|
json.NewEncoder(w).Encode(resp)
|
|
}))
|
|
defer server.Close()
|
|
|
|
dcrConfig := &DynamicClientRegistrationConfig{
|
|
Enabled: true,
|
|
PersistCredentials: true,
|
|
CredentialsFile: credentialsFile,
|
|
ClientMetadata: &ClientRegistrationMetadata{
|
|
RedirectURIs: []string{"https://example.com/callback"},
|
|
},
|
|
}
|
|
|
|
registrar := NewDynamicClientRegistrar(
|
|
server.Client(),
|
|
NewLogger("DEBUG"),
|
|
dcrConfig,
|
|
server.URL,
|
|
)
|
|
|
|
// First registration - should hit the server
|
|
ctx := context.Background()
|
|
resp, err := registrar.RegisterClient(ctx, server.URL+"/register")
|
|
if err != nil {
|
|
t.Fatalf("First registration failed: %v", err)
|
|
}
|
|
|
|
if resp.ClientID != expectedClientID {
|
|
t.Errorf("ClientID mismatch: got %s, want %s", resp.ClientID, expectedClientID)
|
|
}
|
|
|
|
// Verify credentials file was created
|
|
if _, err := os.Stat(credentialsFile); os.IsNotExist(err) {
|
|
t.Fatal("Credentials file was not created")
|
|
}
|
|
|
|
// Create a new registrar to test loading
|
|
registrar2 := NewDynamicClientRegistrar(
|
|
server.Client(),
|
|
NewLogger("DEBUG"),
|
|
dcrConfig,
|
|
server.URL,
|
|
)
|
|
|
|
// Second registration - should load from file
|
|
resp2, err := registrar2.RegisterClient(ctx, server.URL+"/register")
|
|
if err != nil {
|
|
t.Fatalf("Second registration failed: %v", err)
|
|
}
|
|
|
|
if resp2.ClientID != expectedClientID {
|
|
t.Errorf("Loaded ClientID mismatch: got %s, want %s", resp2.ClientID, expectedClientID)
|
|
}
|
|
}
|
|
|
|
// TestCredentialsValidation tests the areCredentialsValid function
|
|
func TestCredentialsValidation(t *testing.T) {
|
|
dcrConfig := &DynamicClientRegistrationConfig{Enabled: true}
|
|
registrar := NewDynamicClientRegistrar(&http.Client{}, NewLogger("DEBUG"), dcrConfig, "https://example.com")
|
|
|
|
tests := []struct {
|
|
response *ClientRegistrationResponse
|
|
name string
|
|
expected bool
|
|
}{
|
|
{
|
|
name: "nil response",
|
|
response: nil,
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "empty client_id",
|
|
response: &ClientRegistrationResponse{
|
|
ClientID: "",
|
|
},
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "valid non-expiring credentials",
|
|
response: &ClientRegistrationResponse{
|
|
ClientID: "test-client-id",
|
|
ClientSecretExpiresAt: 0, // Never expires
|
|
},
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "valid future-expiring credentials",
|
|
response: &ClientRegistrationResponse{
|
|
ClientID: "test-client-id",
|
|
ClientSecretExpiresAt: time.Now().Add(1 * time.Hour).Unix(),
|
|
},
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "expired credentials",
|
|
response: &ClientRegistrationResponse{
|
|
ClientID: "test-client-id",
|
|
ClientSecretExpiresAt: time.Now().Add(-1 * time.Hour).Unix(),
|
|
},
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "about to expire credentials (within 5 min buffer)",
|
|
response: &ClientRegistrationResponse{
|
|
ClientID: "test-client-id",
|
|
ClientSecretExpiresAt: time.Now().Add(2 * time.Minute).Unix(),
|
|
},
|
|
expected: false,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
result := registrar.areCredentialsValid(tc.response)
|
|
if result != tc.expected {
|
|
t.Errorf("areCredentialsValid() = %v, want %v", result, tc.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestBuildRegistrationRequest tests the request body construction
|
|
func TestBuildRegistrationRequest(t *testing.T) {
|
|
tests := []struct {
|
|
metadata *ClientRegistrationMetadata
|
|
expectedFields map[string]interface{}
|
|
name string
|
|
expectError bool
|
|
}{
|
|
{
|
|
name: "minimal metadata",
|
|
metadata: &ClientRegistrationMetadata{
|
|
RedirectURIs: []string{"https://example.com/callback"},
|
|
},
|
|
expectedFields: map[string]interface{}{
|
|
"redirect_uris": []interface{}{"https://example.com/callback"},
|
|
"response_types": []interface{}{"code"},
|
|
"grant_types": []interface{}{"authorization_code", "refresh_token"},
|
|
"token_endpoint_auth_method": "client_secret_basic",
|
|
},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "full metadata",
|
|
metadata: &ClientRegistrationMetadata{
|
|
RedirectURIs: []string{"https://example.com/callback", "https://example.com/callback2"},
|
|
ResponseTypes: []string{"code", "token"},
|
|
GrantTypes: []string{"authorization_code"},
|
|
ApplicationType: "web",
|
|
Contacts: []string{"admin@example.com"},
|
|
ClientName: "My Test Client",
|
|
LogoURI: "https://example.com/logo.png",
|
|
ClientURI: "https://example.com",
|
|
PolicyURI: "https://example.com/privacy",
|
|
TOSURI: "https://example.com/tos",
|
|
SubjectType: "public",
|
|
TokenEndpointAuthMethod: "client_secret_post",
|
|
DefaultMaxAge: 3600,
|
|
RequireAuthTime: true,
|
|
Scope: "openid profile email",
|
|
},
|
|
expectedFields: map[string]interface{}{
|
|
"redirect_uris": []interface{}{"https://example.com/callback", "https://example.com/callback2"},
|
|
"response_types": []interface{}{"code", "token"},
|
|
"grant_types": []interface{}{"authorization_code"},
|
|
"application_type": "web",
|
|
"client_name": "My Test Client",
|
|
"token_endpoint_auth_method": "client_secret_post",
|
|
"default_max_age": float64(3600),
|
|
"require_auth_time": true,
|
|
"scope": "openid profile email",
|
|
},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "missing redirect_uris",
|
|
metadata: &ClientRegistrationMetadata{
|
|
ClientName: "Test Client",
|
|
},
|
|
expectError: true,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
dcrConfig := &DynamicClientRegistrationConfig{
|
|
Enabled: true,
|
|
ClientMetadata: tc.metadata,
|
|
}
|
|
|
|
registrar := NewDynamicClientRegistrar(
|
|
&http.Client{},
|
|
NewLogger("DEBUG"),
|
|
dcrConfig,
|
|
"https://example.com",
|
|
)
|
|
|
|
reqBody, err := registrar.buildRegistrationRequest()
|
|
|
|
if tc.expectError {
|
|
if err == nil {
|
|
t.Fatal("Expected error but got nil")
|
|
}
|
|
return
|
|
}
|
|
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
|
|
var reqData map[string]interface{}
|
|
if err := json.Unmarshal(reqBody, &reqData); err != nil {
|
|
t.Fatalf("Failed to unmarshal request body: %v", err)
|
|
}
|
|
|
|
for field, expectedValue := range tc.expectedFields {
|
|
actualValue, ok := reqData[field]
|
|
if !ok {
|
|
t.Errorf("Missing expected field: %s", field)
|
|
continue
|
|
}
|
|
|
|
// Compare JSON representations for slices
|
|
expectedJSON, _ := json.Marshal(expectedValue)
|
|
actualJSON, _ := json.Marshal(actualValue)
|
|
if string(expectedJSON) != string(actualJSON) {
|
|
t.Errorf("Field %s mismatch: got %v, want %v", field, actualValue, expectedValue)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestProviderMetadataRegistrationEndpoint tests that registration_endpoint is parsed from metadata
|
|
func TestProviderMetadataRegistrationEndpoint(t *testing.T) {
|
|
metadata := &ProviderMetadata{
|
|
Issuer: "https://example.com",
|
|
AuthURL: "https://example.com/authorize",
|
|
TokenURL: "https://example.com/token",
|
|
JWKSURL: "https://example.com/.well-known/jwks.json",
|
|
RegistrationURL: "https://example.com/register",
|
|
}
|
|
|
|
if metadata.RegistrationURL != "https://example.com/register" {
|
|
t.Errorf("RegistrationURL not set correctly: got %s", metadata.RegistrationURL)
|
|
}
|
|
}
|
|
|
|
// TestDCRConfigDefaults tests default configuration values
|
|
func TestDCRConfigDefaults(t *testing.T) {
|
|
dcrConfig := &DynamicClientRegistrationConfig{
|
|
Enabled: true,
|
|
}
|
|
|
|
registrar := NewDynamicClientRegistrar(
|
|
&http.Client{},
|
|
NewLogger("DEBUG"),
|
|
dcrConfig,
|
|
"https://example.com",
|
|
)
|
|
|
|
// Test default credentials file path
|
|
path := registrar.credentialsFilePath()
|
|
if path != "/tmp/oidc-client-credentials.json" {
|
|
t.Errorf("Default credentials file path mismatch: got %s", path)
|
|
}
|
|
|
|
// Test custom credentials file path
|
|
dcrConfig.CredentialsFile = "/custom/path/credentials.json"
|
|
path = registrar.credentialsFilePath()
|
|
if path != "/custom/path/credentials.json" {
|
|
t.Errorf("Custom credentials file path mismatch: got %s", path)
|
|
}
|
|
}
|
|
|
|
// TestUpdateClientRegistration tests the RFC 7592 client update functionality
|
|
func TestUpdateClientRegistration(t *testing.T) {
|
|
updateCalled := false
|
|
|
|
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method == http.MethodPut {
|
|
updateCalled = true
|
|
|
|
// Verify authorization header
|
|
if r.Header.Get("Authorization") == "" {
|
|
t.Error("Missing Authorization header for update")
|
|
}
|
|
|
|
resp := ClientRegistrationResponse{
|
|
ClientID: "updated-client-id",
|
|
ClientSecret: "updated-client-secret",
|
|
RegistrationAccessToken: "new-access-token",
|
|
RegistrationClientURI: r.URL.String(),
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusOK)
|
|
json.NewEncoder(w).Encode(resp)
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
|
|
dcrConfig := &DynamicClientRegistrationConfig{
|
|
Enabled: true,
|
|
ClientMetadata: &ClientRegistrationMetadata{
|
|
RedirectURIs: []string{"https://example.com/callback"},
|
|
},
|
|
}
|
|
|
|
registrar := NewDynamicClientRegistrar(
|
|
server.Client(),
|
|
NewLogger("DEBUG"),
|
|
dcrConfig,
|
|
server.URL,
|
|
)
|
|
|
|
// Set up cached response with management credentials
|
|
registrar.mu.Lock()
|
|
registrar.registrationResponse = &ClientRegistrationResponse{
|
|
ClientID: "original-client-id",
|
|
ClientSecret: "original-client-secret",
|
|
RegistrationAccessToken: "access-token",
|
|
RegistrationClientURI: server.URL + "/register/client123",
|
|
}
|
|
registrar.mu.Unlock()
|
|
|
|
// Perform update
|
|
ctx := context.Background()
|
|
resp, err := registrar.UpdateClientRegistration(ctx)
|
|
|
|
if err != nil {
|
|
t.Fatalf("Update failed: %v", err)
|
|
}
|
|
|
|
if !updateCalled {
|
|
t.Error("Update endpoint was not called")
|
|
}
|
|
|
|
if resp.ClientID != "updated-client-id" {
|
|
t.Errorf("Updated ClientID mismatch: got %s", resp.ClientID)
|
|
}
|
|
}
|
|
|
|
// TestDeleteClientRegistration tests the RFC 7592 client deletion functionality
|
|
func TestDeleteClientRegistration(t *testing.T) {
|
|
deleteCalled := false
|
|
|
|
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method == http.MethodDelete {
|
|
deleteCalled = true
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
|
|
tempDir := t.TempDir()
|
|
credentialsFile := filepath.Join(tempDir, "credentials.json")
|
|
|
|
// Create a credentials file to test deletion
|
|
os.WriteFile(credentialsFile, []byte(`{"client_id":"test"}`), 0600)
|
|
|
|
dcrConfig := &DynamicClientRegistrationConfig{
|
|
Enabled: true,
|
|
PersistCredentials: true,
|
|
CredentialsFile: credentialsFile,
|
|
}
|
|
|
|
registrar := NewDynamicClientRegistrar(
|
|
server.Client(),
|
|
NewLogger("DEBUG"),
|
|
dcrConfig,
|
|
server.URL,
|
|
)
|
|
|
|
// Set up cached response with management credentials
|
|
registrar.mu.Lock()
|
|
registrar.registrationResponse = &ClientRegistrationResponse{
|
|
ClientID: "test-client-id",
|
|
RegistrationAccessToken: "access-token",
|
|
RegistrationClientURI: server.URL + "/register/client123",
|
|
}
|
|
registrar.mu.Unlock()
|
|
|
|
// Perform delete
|
|
ctx := context.Background()
|
|
err := registrar.DeleteClientRegistration(ctx)
|
|
|
|
if err != nil {
|
|
t.Fatalf("Delete failed: %v", err)
|
|
}
|
|
|
|
if !deleteCalled {
|
|
t.Error("Delete endpoint was not called")
|
|
}
|
|
|
|
// Verify cache is cleared
|
|
if registrar.GetCachedResponse() != nil {
|
|
t.Error("Cached response should be cleared after deletion")
|
|
}
|
|
|
|
// Verify credentials file is deleted
|
|
if _, err := os.Stat(credentialsFile); !os.IsNotExist(err) {
|
|
t.Error("Credentials file should be deleted")
|
|
}
|
|
}
|
|
|
|
// TestReadClientRegistration tests the RFC 7592 client read functionality
|
|
func TestReadClientRegistration(t *testing.T) {
|
|
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method == http.MethodGet {
|
|
resp := ClientRegistrationResponse{
|
|
ClientID: "read-client-id",
|
|
ClientSecret: "read-client-secret",
|
|
RedirectURIs: []string{"https://example.com/callback"},
|
|
ResponseTypes: []string{"code"},
|
|
GrantTypes: []string{"authorization_code"},
|
|
ApplicationType: "web",
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusOK)
|
|
json.NewEncoder(w).Encode(resp)
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
|
|
dcrConfig := &DynamicClientRegistrationConfig{Enabled: true}
|
|
|
|
registrar := NewDynamicClientRegistrar(
|
|
server.Client(),
|
|
NewLogger("DEBUG"),
|
|
dcrConfig,
|
|
server.URL,
|
|
)
|
|
|
|
// Set up cached response with management credentials
|
|
registrar.mu.Lock()
|
|
registrar.registrationResponse = &ClientRegistrationResponse{
|
|
ClientID: "original-client-id",
|
|
RegistrationAccessToken: "access-token",
|
|
RegistrationClientURI: server.URL + "/register/client123",
|
|
}
|
|
registrar.mu.Unlock()
|
|
|
|
// Read registration
|
|
ctx := context.Background()
|
|
resp, err := registrar.ReadClientRegistration(ctx)
|
|
|
|
if err != nil {
|
|
t.Fatalf("Read failed: %v", err)
|
|
}
|
|
|
|
if resp.ClientID != "read-client-id" {
|
|
t.Errorf("Read ClientID mismatch: got %s", resp.ClientID)
|
|
}
|
|
}
|
|
|
|
// TestOperationsWithoutCachedResponse tests error handling when no cached response exists
|
|
func TestOperationsWithoutCachedResponse(t *testing.T) {
|
|
dcrConfig := &DynamicClientRegistrationConfig{Enabled: true}
|
|
|
|
registrar := NewDynamicClientRegistrar(
|
|
&http.Client{},
|
|
NewLogger("DEBUG"),
|
|
dcrConfig,
|
|
"https://example.com",
|
|
)
|
|
|
|
ctx := context.Background()
|
|
|
|
// Test Update without cached response
|
|
_, err := registrar.UpdateClientRegistration(ctx)
|
|
if err == nil || !stringContains(err.Error(), "no existing registration") {
|
|
t.Errorf("Update should fail without cached response: %v", err)
|
|
}
|
|
|
|
// Test Read without cached response
|
|
_, err = registrar.ReadClientRegistration(ctx)
|
|
if err == nil || !stringContains(err.Error(), "no existing registration") {
|
|
t.Errorf("Read should fail without cached response: %v", err)
|
|
}
|
|
|
|
// Test Delete without cached response
|
|
err = registrar.DeleteClientRegistration(ctx)
|
|
if err == nil || !stringContains(err.Error(), "no existing registration") {
|
|
t.Errorf("Delete should fail without cached response: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestOperationsWithoutManagementCredentials tests error handling without management URIs
|
|
func TestOperationsWithoutManagementCredentials(t *testing.T) {
|
|
dcrConfig := &DynamicClientRegistrationConfig{Enabled: true}
|
|
|
|
registrar := NewDynamicClientRegistrar(
|
|
&http.Client{},
|
|
NewLogger("DEBUG"),
|
|
dcrConfig,
|
|
"https://example.com",
|
|
)
|
|
|
|
// Set up cached response WITHOUT management credentials
|
|
registrar.mu.Lock()
|
|
registrar.registrationResponse = &ClientRegistrationResponse{
|
|
ClientID: "test-client-id",
|
|
// Missing RegistrationAccessToken and RegistrationClientURI
|
|
}
|
|
registrar.mu.Unlock()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Test Update without management credentials
|
|
_, err := registrar.UpdateClientRegistration(ctx)
|
|
if err == nil || !stringContains(err.Error(), "registration management not supported") {
|
|
t.Errorf("Update should fail without management credentials: %v", err)
|
|
}
|
|
|
|
// Test Read without management credentials
|
|
_, err = registrar.ReadClientRegistration(ctx)
|
|
if err == nil || !stringContains(err.Error(), "registration management not supported") {
|
|
t.Errorf("Read should fail without management credentials: %v", err)
|
|
}
|
|
|
|
// Test Delete without management credentials
|
|
err = registrar.DeleteClientRegistration(ctx)
|
|
if err == nil || !stringContains(err.Error(), "registration management not supported") {
|
|
t.Errorf("Delete should fail without management credentials: %v", err)
|
|
}
|
|
}
|
|
|
|
// stringContains is a helper function to check if a string contains a substring
|
|
func stringContains(s, substr string) bool {
|
|
return len(s) >= len(substr) && (s == substr || len(s) > 0 && stringContainsHelper(s, substr))
|
|
}
|
|
|
|
func stringContainsHelper(s, substr string) bool {
|
|
for i := 0; i <= len(s)-len(substr); i++ {
|
|
if s[i:i+len(substr)] == substr {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|