mirror of
https://github.com/goauthentik/authentik
synced 2026-04-27 09:57:31 +02:00
* 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>
364 lines
12 KiB
Go
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"])
|
|
}
|