mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-14 04:07:46 +00:00
280 lines
6.8 KiB
Go
280 lines
6.8 KiB
Go
//go:build !yaegi
|
|
|
|
package token
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
)
|
|
|
|
// Introspector tests
|
|
func TestNewIntrospector(t *testing.T) {
|
|
introspector := NewIntrospector(
|
|
"client-id",
|
|
"client-secret",
|
|
"https://provider.example.com/introspect",
|
|
&http.Client{},
|
|
&mockLogger{},
|
|
[]string{"groups"},
|
|
[]string{"roles"},
|
|
"",
|
|
)
|
|
|
|
if introspector == nil {
|
|
t.Fatal("Expected NewIntrospector to return non-nil")
|
|
}
|
|
|
|
if introspector.clientID != "client-id" {
|
|
t.Error("Expected clientID to be set")
|
|
}
|
|
|
|
if introspector.clientSecret != "client-secret" {
|
|
t.Error("Expected clientSecret to be set")
|
|
}
|
|
|
|
if introspector.introspectionURL != "https://provider.example.com/introspect" {
|
|
t.Error("Expected introspectionURL to be set")
|
|
}
|
|
|
|
if len(introspector.groupsClaimPath) != 1 || introspector.groupsClaimPath[0] != "groups" {
|
|
t.Error("Expected groupsClaimPath to be set")
|
|
}
|
|
|
|
if len(introspector.rolesClaimPath) != 1 || introspector.rolesClaimPath[0] != "roles" {
|
|
t.Error("Expected rolesClaimPath to be set")
|
|
}
|
|
}
|
|
|
|
func TestIntrospector_IntrospectToken_NoEndpoint(t *testing.T) {
|
|
introspector := NewIntrospector(
|
|
"client-id",
|
|
"client-secret",
|
|
"", // No introspection endpoint
|
|
&http.Client{},
|
|
&mockLogger{},
|
|
nil,
|
|
nil,
|
|
"",
|
|
)
|
|
|
|
_, err := introspector.IntrospectToken("token", "")
|
|
if err == nil {
|
|
t.Error("Expected error when introspection endpoint not configured")
|
|
}
|
|
|
|
if err.Error() != "introspection endpoint not configured" {
|
|
t.Errorf("Expected configuration error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestIntrospector_IntrospectToken_Success(t *testing.T) {
|
|
// Create a test server
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "POST" {
|
|
t.Errorf("Expected POST request, got %s", r.Method)
|
|
}
|
|
|
|
if err := r.ParseForm(); err != nil {
|
|
t.Errorf("Failed to parse form: %v", err)
|
|
}
|
|
|
|
// Verify parameters
|
|
if r.FormValue("token") != "test-token" {
|
|
t.Error("Expected token parameter")
|
|
}
|
|
|
|
if r.FormValue("token_type_hint") != "access_token" {
|
|
t.Error("Expected token_type_hint parameter")
|
|
}
|
|
|
|
if r.FormValue("client_id") != "test-client" {
|
|
t.Error("Expected client_id parameter")
|
|
}
|
|
|
|
// Return valid introspection response
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte(`{
|
|
"active": true,
|
|
"scope": "openid profile email",
|
|
"client_id": "test-client",
|
|
"username": "testuser",
|
|
"token_type": "Bearer",
|
|
"exp": 1234567890,
|
|
"iat": 1234567800,
|
|
"sub": "user123",
|
|
"aud": "test-audience",
|
|
"iss": "https://issuer.example.com",
|
|
"custom_claim": "custom_value"
|
|
}`))
|
|
}))
|
|
defer server.Close()
|
|
|
|
introspector := NewIntrospector(
|
|
"test-client",
|
|
"test-secret",
|
|
server.URL,
|
|
&http.Client{},
|
|
&mockLogger{},
|
|
nil,
|
|
nil,
|
|
"",
|
|
)
|
|
|
|
resp, err := introspector.IntrospectToken("test-token", "access_token")
|
|
if err != nil {
|
|
t.Fatalf("Expected no error, got: %v", err)
|
|
}
|
|
|
|
if !resp.Active {
|
|
t.Error("Expected token to be active")
|
|
}
|
|
|
|
if resp.Scope != "openid profile email" {
|
|
t.Errorf("Expected scope 'openid profile email', got '%s'", resp.Scope)
|
|
}
|
|
|
|
if resp.ClientID != "test-client" {
|
|
t.Errorf("Expected client_id 'test-client', got '%s'", resp.ClientID)
|
|
}
|
|
|
|
if resp.Username != "testuser" {
|
|
t.Errorf("Expected username 'testuser', got '%s'", resp.Username)
|
|
}
|
|
|
|
if resp.TokenType != "Bearer" {
|
|
t.Errorf("Expected token_type 'Bearer', got '%s'", resp.TokenType)
|
|
}
|
|
|
|
// Check extra fields
|
|
if resp.Extra == nil {
|
|
t.Fatal("Expected Extra map to be populated")
|
|
}
|
|
|
|
if val, ok := resp.Extra["custom_claim"]; !ok || val != "custom_value" {
|
|
t.Error("Expected custom_claim in Extra fields")
|
|
}
|
|
|
|
// Standard fields should not be in Extra
|
|
if _, ok := resp.Extra["active"]; ok {
|
|
t.Error("Standard field 'active' should not be in Extra")
|
|
}
|
|
}
|
|
|
|
func TestIntrospector_IntrospectToken_HTTPError(t *testing.T) {
|
|
// Create a test server that returns an error
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
w.Write([]byte(`{"error":"invalid_token"}`))
|
|
}))
|
|
defer server.Close()
|
|
|
|
introspector := NewIntrospector(
|
|
"client-id",
|
|
"client-secret",
|
|
server.URL,
|
|
&http.Client{},
|
|
&mockLogger{},
|
|
nil,
|
|
nil,
|
|
"",
|
|
)
|
|
|
|
_, err := introspector.IntrospectToken("bad-token", "")
|
|
if err == nil {
|
|
t.Error("Expected error for HTTP 401 response")
|
|
}
|
|
}
|
|
|
|
func TestIntrospector_IntrospectToken_InvalidJSON(t *testing.T) {
|
|
// Create a test server that returns invalid JSON
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte(`{invalid json`))
|
|
}))
|
|
defer server.Close()
|
|
|
|
introspector := NewIntrospector(
|
|
"client-id",
|
|
"client-secret",
|
|
server.URL,
|
|
&http.Client{},
|
|
&mockLogger{},
|
|
nil,
|
|
nil,
|
|
"",
|
|
)
|
|
|
|
_, err := introspector.IntrospectToken("token", "")
|
|
if err == nil {
|
|
t.Error("Expected error for invalid JSON response")
|
|
}
|
|
}
|
|
|
|
func TestIntrospector_IntrospectToken_NoTokenTypeHint(t *testing.T) {
|
|
// Test that token_type_hint is optional
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if err := r.ParseForm(); err != nil {
|
|
t.Errorf("Failed to parse form: %v", err)
|
|
}
|
|
|
|
// Verify token_type_hint is not set when empty
|
|
if r.FormValue("token_type_hint") != "" {
|
|
t.Error("Expected no token_type_hint when not provided")
|
|
}
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte(`{"active":true}`))
|
|
}))
|
|
defer server.Close()
|
|
|
|
introspector := NewIntrospector(
|
|
"client-id",
|
|
"client-secret",
|
|
server.URL,
|
|
&http.Client{},
|
|
&mockLogger{},
|
|
nil,
|
|
nil,
|
|
"",
|
|
)
|
|
|
|
_, err := introspector.IntrospectToken("token", "") // Empty token type hint
|
|
if err != nil {
|
|
t.Errorf("Expected no error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestIntrospector_DetectTokenType_IDToken_AudienceString(t *testing.T) {
|
|
_ = NewIntrospector(
|
|
"test-client",
|
|
"client-secret",
|
|
"https://introspect.example.com",
|
|
&http.Client{},
|
|
&mockLogger{},
|
|
nil,
|
|
nil,
|
|
"",
|
|
)
|
|
|
|
// Mock JWT with audience matching client ID
|
|
// Note: parseJWT is a package-level function that we can't easily mock,
|
|
// so this test validates the logic assuming parseJWT works
|
|
// We'll test the DetectTokenType method indirectly
|
|
|
|
// This test would require mocking parseJWT which is complex
|
|
// Skip for now or implement when parseJWT is mockable
|
|
t.Skip("Requires parseJWT mocking - tested indirectly through integration")
|
|
}
|
|
|
|
func TestIntrospector_DetectTokenType_AccessToken_Scope(t *testing.T) {
|
|
// Similar to above - requires parseJWT mocking
|
|
t.Skip("Requires parseJWT mocking - tested indirectly through integration")
|
|
}
|
|
|
|
func TestIntrospector_ExtractGroupsAndRoles(t *testing.T) {
|
|
// Requires parseJWT mocking
|
|
t.Skip("Requires parseJWT mocking - tested indirectly through integration")
|
|
}
|