Files
authentik/internal/outpost/proxyv2/application/auth_test.go
Dominic R 3353db0d7f outpost/proxyv2: more tests, fix pg password with spaces, and existing session on restart (#18211)
* outpost/proxyv2: handle PostgreSQL passwords with spaces and special characters

And modify / add some more tests and a bit of refactoring

* Potential fix for code scanning alert no. 268: Disabled TLS certificate check

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Signed-off-by: Dominic R <dominic@sdko.org>

* Revert "Potential fix for code scanning alert no. 268: Disabled TLS certificate check"

This reverts commit ead227a272.

* wip

* fix incorrect status code in error response

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Dominic R <dominic@sdko.org>
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2025-12-11 14:25:41 +00:00

364 lines
12 KiB
Go

package application
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/gorilla/sessions"
"github.com/mitchellh/mapstructure"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"goauthentik.io/internal/outpost/proxyv2/constants"
"goauthentik.io/internal/outpost/proxyv2/types"
)
// TestClaimsJSONSerialization tests that Claims can be serialized to JSON and back
func TestClaimsJSONSerialization(t *testing.T) {
claims := types.Claims{
Sub: "user-id-123",
Exp: 1234567890,
Email: "test@example.com",
Verified: true,
Name: "Test User",
PreferredUsername: "testuser",
Groups: []string{"admin", "user"},
Entitlements: []string{"read", "write"},
Sid: "session-id-456",
Proxy: &types.ProxyClaims{
UserAttributes: map[string]any{
"custom_field": "custom_value",
"department": "engineering",
},
BackendOverride: "custom-backend",
HostHeader: "example.com",
IsSuperuser: true,
},
RawToken: "raw.jwt.token",
}
// Serialize to JSON
jsonData, err := json.Marshal(claims)
require.NoError(t, err)
// Deserialize back
var parsedClaims types.Claims
err = json.Unmarshal(jsonData, &parsedClaims)
require.NoError(t, err)
// Verify all fields
assert.Equal(t, claims.Sub, parsedClaims.Sub)
assert.Equal(t, claims.Exp, parsedClaims.Exp)
assert.Equal(t, claims.Email, parsedClaims.Email)
assert.Equal(t, claims.Verified, parsedClaims.Verified)
assert.Equal(t, claims.Name, parsedClaims.Name)
assert.Equal(t, claims.PreferredUsername, parsedClaims.PreferredUsername)
assert.Equal(t, claims.Groups, parsedClaims.Groups)
assert.Equal(t, claims.Entitlements, parsedClaims.Entitlements)
assert.Equal(t, claims.Sid, parsedClaims.Sid)
// RawToken has no json tag, so it's serialized using the field name
assert.Equal(t, claims.RawToken, parsedClaims.RawToken)
// Verify proxy claims
require.NotNil(t, parsedClaims.Proxy)
assert.Equal(t, claims.Proxy.BackendOverride, parsedClaims.Proxy.BackendOverride)
assert.Equal(t, claims.Proxy.HostHeader, parsedClaims.Proxy.HostHeader)
assert.Equal(t, claims.Proxy.IsSuperuser, parsedClaims.Proxy.IsSuperuser)
assert.Equal(t, "custom_value", parsedClaims.Proxy.UserAttributes["custom_field"])
assert.Equal(t, "engineering", parsedClaims.Proxy.UserAttributes["department"])
}
// TestClaimsMapSerialization tests that Claims stored as map[string]any can be converted back
func TestClaimsMapSerialization(t *testing.T) {
// Simulate how claims are stored in session as map (like from PostgreSQL JSONB)
claimsMap := map[string]any{
"sub": "user-id-123",
"exp": float64(1234567890), // json numbers become float64
"email": "test@example.com",
"email_verified": true,
"name": "Test User",
"preferred_username": "testuser",
"groups": []any{"admin", "user"},
"entitlements": []any{"read", "write"},
"sid": "session-id-456",
"ak_proxy": map[string]any{
"user_attributes": map[string]any{
"custom_field": "custom_value",
},
"backend_override": "custom-backend",
"host_header": "example.com",
"is_superuser": true,
},
"raw_token": "not-a-real-token",
}
// Convert map to Claims using mapstructure marshaling (like getClaimsFromSession does)
var claims types.Claims
err := mapstructure.Decode(claimsMap, &claims)
require.NoError(t, err)
// Verify fields
assert.Equal(t, "user-id-123", claims.Sub)
assert.Equal(t, 1234567890, claims.Exp)
assert.Equal(t, "test@example.com", claims.Email)
assert.True(t, claims.Verified)
assert.Equal(t, "Test User", claims.Name)
assert.Equal(t, "testuser", claims.PreferredUsername)
assert.Equal(t, []string{"admin", "user"}, claims.Groups)
assert.Equal(t, []string{"read", "write"}, claims.Entitlements)
assert.Equal(t, "session-id-456", claims.Sid)
assert.Equal(t, "not-a-real-token", claims.RawToken)
// Verify proxy claims
require.NotNil(t, claims.Proxy)
assert.Equal(t, "custom-backend", claims.Proxy.BackendOverride)
assert.Equal(t, "example.com", claims.Proxy.HostHeader)
assert.True(t, claims.Proxy.IsSuperuser)
assert.Equal(t, "custom_value", claims.Proxy.UserAttributes["custom_field"])
}
// TestClaimsMinimalFields tests that Claims work with minimal required fields
func TestClaimsMinimalFields(t *testing.T) {
claimsMap := map[string]any{
"sub": "user-id-123",
"exp": float64(1234567890),
}
jsonData, err := json.Marshal(claimsMap)
require.NoError(t, err)
var claims types.Claims
err = json.Unmarshal(jsonData, &claims)
require.NoError(t, err)
assert.Equal(t, "user-id-123", claims.Sub)
assert.Equal(t, 1234567890, claims.Exp)
assert.Empty(t, claims.Email)
assert.Empty(t, claims.Name)
assert.Empty(t, claims.Groups)
assert.Nil(t, claims.Proxy)
}
// TestClaimsWithEmptyArrays tests that empty arrays are handled correctly
func TestClaimsWithEmptyArrays(t *testing.T) {
claimsMap := map[string]any{
"sub": "user-id-123",
"exp": float64(1234567890),
"groups": []any{},
"entitlements": []any{},
}
jsonData, err := json.Marshal(claimsMap)
require.NoError(t, err)
var claims types.Claims
err = json.Unmarshal(jsonData, &claims)
require.NoError(t, err)
assert.Equal(t, "user-id-123", claims.Sub)
assert.NotNil(t, claims.Groups)
assert.NotNil(t, claims.Entitlements)
assert.Len(t, claims.Groups, 0)
assert.Len(t, claims.Entitlements, 0)
}
// TestClaimsWithNullProxyClaims tests that null proxy claims don't cause issues
func TestClaimsWithNullProxyClaims(t *testing.T) {
claimsMap := map[string]any{
"sub": "user-id-123",
"exp": float64(1234567890),
"ak_proxy": nil,
}
jsonData, err := json.Marshal(claimsMap)
require.NoError(t, err)
var claims types.Claims
err = json.Unmarshal(jsonData, &claims)
require.NoError(t, err)
assert.Equal(t, "user-id-123", claims.Sub)
assert.Nil(t, claims.Proxy)
}
// TestGetClaimsFromSession_Success tests successful retrieval of claims from session
// uses a mock session that returns claims as map[string]any to simulate
// how PostgreSQL storage deserializes JSONB data
func TestGetClaimsFromSession_Success(t *testing.T) {
// Create a custom mock store that returns claims as map
store := &mockMapSessionStore{
claimsMap: map[string]any{
"sub": "user-id-123",
"exp": float64(1234567890),
"email": "test@example.com",
"email_verified": true,
"preferred_username": "testuser",
"groups": []any{"admin", "user"},
},
}
app := &Application{
sessions: store,
}
req := httptest.NewRequest("GET", "/", nil)
// Test getClaimsFromSession
claims := app.getClaimsFromSession(nil, req)
require.NotNil(t, claims)
assert.Equal(t, "user-id-123", claims.Sub)
assert.Equal(t, 1234567890, claims.Exp)
assert.Equal(t, "test@example.com", claims.Email)
assert.True(t, claims.Verified)
assert.Equal(t, "testuser", claims.PreferredUsername)
assert.Equal(t, []string{"admin", "user"}, claims.Groups)
}
// mockMapSessionStore is a mock session store that returns claims as map[string]any
type mockMapSessionStore struct {
claimsMap map[string]any
}
func (m *mockMapSessionStore) Get(r *http.Request, name string) (*sessions.Session, error) {
session := sessions.NewSession(m, name)
if m.claimsMap != nil {
session.Values[constants.SessionClaims] = m.claimsMap
}
return session, nil
}
func (m *mockMapSessionStore) New(r *http.Request, name string) (*sessions.Session, error) {
return m.Get(r, name)
}
func (m *mockMapSessionStore) Save(r *http.Request, w http.ResponseWriter, s *sessions.Session) error {
return nil
}
// TestGetClaimsFromSession_NoSession tests behavior when no session exists
func TestGetClaimsFromSession_NoSession(t *testing.T) {
store := &mockMapSessionStore{
claimsMap: nil, // No claims
}
app := &Application{
sessions: store,
}
req := httptest.NewRequest("GET", "/", nil)
claims := app.getClaimsFromSession(nil, req)
assert.Nil(t, claims)
}
// TestGetClaimsFromSession_NoClaims tests behavior when session exists but has no claims
func TestGetClaimsFromSession_NoClaims(t *testing.T) {
store := &mockMapSessionStore{
claimsMap: nil, // No claims in session
}
app := &Application{
sessions: store,
}
req := httptest.NewRequest("GET", "/", nil)
claims := app.getClaimsFromSession(nil, req)
assert.Nil(t, claims)
}
// TestGetClaimsFromSession_InvalidClaimsType tests behavior when claims have wrong type
func TestGetClaimsFromSession_InvalidClaimsType(t *testing.T) {
store := &mockInvalidClaimsStore{}
app := &Application{
sessions: store,
}
req := httptest.NewRequest("GET", "/", nil)
claims := app.getClaimsFromSession(nil, req)
assert.Nil(t, claims)
}
// mockInvalidClaimsStore returns claims as invalid type (string)
type mockInvalidClaimsStore struct{}
func (m *mockInvalidClaimsStore) Get(r *http.Request, name string) (*sessions.Session, error) {
session := sessions.NewSession(m, name)
session.Values[constants.SessionClaims] = "invalid-string-value"
return session, nil
}
func (m *mockInvalidClaimsStore) New(r *http.Request, name string) (*sessions.Session, error) {
return m.Get(r, name)
}
func (m *mockInvalidClaimsStore) Save(r *http.Request, w http.ResponseWriter, s *sessions.Session) error {
return nil
}
// TestClaimsRoundTrip tests full round trip: save Claims, retrieve as map, convert back to Claims
func TestClaimsRoundTrip(t *testing.T) {
originalClaims := types.Claims{
Sub: "user-id-789",
Exp: 1234567890,
Email: "roundtrip@example.com",
Verified: true,
Name: "Round Trip User",
PreferredUsername: "roundtripuser",
Groups: []string{"group1", "group2", "group3"},
Entitlements: []string{"ent1", "ent2"},
Sid: "session-789",
Proxy: &types.ProxyClaims{
UserAttributes: map[string]any{
"attr1": "value1",
"attr2": float64(42),
"attr3": true,
},
BackendOverride: "backend",
HostHeader: "host.example.com",
IsSuperuser: false,
},
}
// Step 1: Serialize Claims to JSON (simulating storage)
jsonData, err := json.Marshal(originalClaims)
require.NoError(t, err)
// Step 2: Deserialize to map[string]any (simulating PostgreSQL load)
var claimsMap map[string]any
err = json.Unmarshal(jsonData, &claimsMap)
require.NoError(t, err)
// Step 3: Convert map back to Claims (simulating getClaimsFromSession)
jsonData2, err := json.Marshal(claimsMap)
require.NoError(t, err)
var retrievedClaims types.Claims
err = json.Unmarshal(jsonData2, &retrievedClaims)
require.NoError(t, err)
// Verify all fields match
assert.Equal(t, originalClaims.Sub, retrievedClaims.Sub)
assert.Equal(t, originalClaims.Exp, retrievedClaims.Exp)
assert.Equal(t, originalClaims.Email, retrievedClaims.Email)
assert.Equal(t, originalClaims.Verified, retrievedClaims.Verified)
assert.Equal(t, originalClaims.Name, retrievedClaims.Name)
assert.Equal(t, originalClaims.PreferredUsername, retrievedClaims.PreferredUsername)
assert.Equal(t, originalClaims.Groups, retrievedClaims.Groups)
assert.Equal(t, originalClaims.Entitlements, retrievedClaims.Entitlements)
assert.Equal(t, originalClaims.Sid, retrievedClaims.Sid)
require.NotNil(t, retrievedClaims.Proxy)
assert.Equal(t, originalClaims.Proxy.BackendOverride, retrievedClaims.Proxy.BackendOverride)
assert.Equal(t, originalClaims.Proxy.HostHeader, retrievedClaims.Proxy.HostHeader)
assert.Equal(t, originalClaims.Proxy.IsSuperuser, retrievedClaims.Proxy.IsSuperuser)
assert.Equal(t, "value1", retrievedClaims.Proxy.UserAttributes["attr1"])
assert.Equal(t, float64(42), retrievedClaims.Proxy.UserAttributes["attr2"])
assert.Equal(t, true, retrievedClaims.Proxy.UserAttributes["attr3"])
}