Skip to content

Commit 2b246bf

Browse files
Merge pull request #11 from mauriciozanettisalomao/feat/lfxv2-493-user-email-to-sub-lookup-authelia
[LFXV2-493] User email to sub lookup - Authelia
2 parents 369086b + 5dae9bf commit 2b246bf

File tree

14 files changed

+435
-71
lines changed

14 files changed

+435
-71
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,8 +149,9 @@ nats request lfx.auth-service.email_to_sub zephyr.stormwind@mythicaltech.io
149149
**Important Notes:**
150150
- This service searches for users by their **primary email** only
151151
- Linked/alternate email addresses are **not** supported for lookup
152-
- The service works with both Auth0 and mock repositories based on configuration
152+
- The service works with Auth0, Authelia, and mock repositories based on configuration
153153
- The returned subject identifier is the canonical user identifier used throughout the system
154+
- For Authelia-specific SUB identifier details and how they are populated, see: [`internal/infrastructure/authelia/README.md`](internal/infrastructure/authelia/README.md)
154155

155156
---
156157

charts/lfx-v2-auth-service/Chart.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@ apiVersion: v2
55
name: lfx-v2-auth-service
66
description: LFX Platform V2 Auth Service chart
77
type: application
8-
version: 0.2.3
8+
version: 0.2.4
99
appVersion: "latest"

charts/lfx-v2-auth-service/values.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,3 +96,5 @@ app:
9696
value: lfx-platform-authelia
9797
AUTHELIA_SECRET_NAME:
9898
value: authelia-users
99+
AUTHELIA_OIDC_USERINFO_URL:
100+
value: https://auth.k8s.orb.local/api/oidc/userinfo

cmd/server/service/providers.go

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -149,11 +149,17 @@ func newUserReaderWriter(ctx context.Context) port.UserReaderWriter {
149149
secretName = "authelia-users"
150150
}
151151

152+
oidcUserInfoURL := os.Getenv(constants.AutheliaOIDCUserInfoURLEnvKey)
153+
if oidcUserInfoURL == "" {
154+
oidcUserInfoURL = "https://auth.k8s.orb.local/api/oidc/userinfo"
155+
}
156+
152157
config := map[string]string{
153-
"configmap-name": configMapName,
154-
"namespace": configMapNamespace,
155-
"daemon-set-name": daemonSetName,
156-
"secret-name": secretName,
158+
"configmap-name": configMapName,
159+
"namespace": configMapNamespace,
160+
"daemon-set-name": daemonSetName,
161+
"secret-name": secretName,
162+
"oidc-userinfo-url": oidcUserInfoURL,
157163
}
158164

159165
// Create Authelia user repository with NATS client for storage

go.mod

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,13 @@ require (
99
github.com/auth0/go-auth0 v1.28.0
1010
github.com/golang-jwt/jwt/v5 v5.3.0
1111
github.com/nats-io/nats.go v1.45.0
12+
github.com/stretchr/testify v1.11.1
13+
go.yaml.in/yaml/v2 v2.4.2
1214
goa.design/clue v1.2.3
1315
goa.design/goa/v3 v3.22.2
16+
golang.org/x/crypto v0.42.0
1417
golang.org/x/oauth2 v0.31.0
18+
golang.org/x/sync v0.17.0
1519
gopkg.in/yaml.v3 v3.0.1
1620
k8s.io/apimachinery v0.34.1
1721
k8s.io/client-go v0.34.1
@@ -56,17 +60,13 @@ require (
5660
github.com/pmezard/go-difflib v1.0.0 // indirect
5761
github.com/segmentio/asm v1.2.0 // indirect
5862
github.com/spf13/pflag v1.0.6 // indirect
59-
github.com/stretchr/testify v1.11.1 // indirect
6063
github.com/x448/float16 v0.8.4 // indirect
6164
go.devnw.com/structs v1.0.0 // indirect
6265
go.opentelemetry.io/otel v1.38.0 // indirect
6366
go.opentelemetry.io/otel/trace v1.38.0 // indirect
64-
go.yaml.in/yaml/v2 v2.4.2 // indirect
6567
go.yaml.in/yaml/v3 v3.0.4 // indirect
66-
golang.org/x/crypto v0.42.0 // indirect
6768
golang.org/x/mod v0.28.0 // indirect
6869
golang.org/x/net v0.44.0 // indirect
69-
golang.org/x/sync v0.17.0 // indirect
7070
golang.org/x/sys v0.36.0 // indirect
7171
golang.org/x/term v0.35.0 // indirect
7272
golang.org/x/text v0.29.0 // indirect

internal/domain/model/user.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,15 @@ func (u User) BuildEmailIndexKey(ctx context.Context) string {
102102
return u.buildIndexKey(ctx, "email", data)
103103
}
104104

105+
// BuildSubIndexKey builds the index key for the sub
106+
func (u User) BuildSubIndexKey(ctx context.Context) string {
107+
data := strings.TrimSpace(strings.ToLower(u.Sub))
108+
if data == "" {
109+
return ""
110+
}
111+
return u.buildIndexKey(ctx, "sub", data)
112+
}
113+
105114
// sanitize sanitizes the user metadata by cleaning up string fields
106115
func (um *UserMetadata) userMetadataSanitize() {
107116
if um.Name != nil {

internal/domain/model/user_test.go

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -802,6 +802,244 @@ func TestUser_BuildEmailIndexKey(t *testing.T) {
802802
}
803803
}
804804

805+
func TestUser_BuildSubIndexKey(t *testing.T) {
806+
tests := []struct {
807+
name string
808+
sub string
809+
expected string
810+
}{
811+
{
812+
name: "valid sub",
813+
sub: "auth0|123456789",
814+
expected: "", // Will be calculated
815+
},
816+
{
817+
name: "empty sub",
818+
sub: "",
819+
expected: "",
820+
},
821+
{
822+
name: "sub with whitespace",
823+
sub: " auth0|123456789 ",
824+
expected: "", // Will be calculated (should be trimmed)
825+
},
826+
{
827+
name: "sub with uppercase",
828+
sub: "AUTH0|123456789",
829+
expected: "", // Will be calculated (should be lowercase)
830+
},
831+
{
832+
name: "sub with mixed case and whitespace",
833+
sub: " Auth0|123456789 ",
834+
expected: "", // Will be calculated (should be trimmed and lowercase)
835+
},
836+
{
837+
name: "only whitespace",
838+
sub: " ",
839+
expected: "",
840+
},
841+
{
842+
name: "google oauth sub",
843+
sub: "google-oauth2|123456789012345678901",
844+
expected: "", // Will be calculated
845+
},
846+
{
847+
name: "github oauth sub",
848+
sub: "github|12345678",
849+
expected: "", // Will be calculated
850+
},
851+
{
852+
name: "sub with special characters",
853+
sub: "provider|user@domain.com",
854+
expected: "", // Will be calculated
855+
},
856+
{
857+
name: "long sub string",
858+
sub: "provider|" + strings.Repeat("a", 100),
859+
expected: "", // Will be calculated
860+
},
861+
}
862+
863+
for _, tt := range tests {
864+
t.Run(tt.name, func(t *testing.T) {
865+
user := User{Sub: tt.sub}
866+
ctx := context.Background()
867+
868+
result := user.BuildSubIndexKey(ctx)
869+
870+
// Calculate expected hash
871+
var expectedHash string
872+
if tt.expected != "" {
873+
expectedHash = tt.expected
874+
} else {
875+
// Check if this is a case where we expect empty string explicitly
876+
normalizedSub := strings.TrimSpace(strings.ToLower(tt.sub))
877+
if normalizedSub == "" {
878+
expectedHash = "" // Empty subs should return empty string
879+
} else {
880+
hash := sha256.Sum256([]byte(normalizedSub))
881+
expectedHash = hex.EncodeToString(hash[:])
882+
}
883+
}
884+
885+
if result != expectedHash {
886+
t.Errorf("BuildSubIndexKey() = %q, want %q", result, expectedHash)
887+
}
888+
889+
// Verify it's valid hex if not empty
890+
if result != "" {
891+
if _, err := hex.DecodeString(result); err != nil {
892+
t.Errorf("BuildSubIndexKey() result is not valid hex: %v", err)
893+
}
894+
// Verify the result is a valid hex string of correct length (64 chars for SHA256)
895+
if len(result) != 64 {
896+
t.Errorf("BuildSubIndexKey() result length = %d, want 64", len(result))
897+
}
898+
}
899+
})
900+
}
901+
}
902+
903+
func TestUser_BuildSubIndexKey_Normalization(t *testing.T) {
904+
// Test that normalization works correctly (trimming and lowercase)
905+
testCases := []struct {
906+
name string
907+
input string
908+
expected string
909+
}{
910+
{
911+
name: "uppercase to lowercase",
912+
input: "AUTH0|123456789",
913+
expected: "auth0|123456789",
914+
},
915+
{
916+
name: "leading and trailing whitespace",
917+
input: " auth0|123456789 ",
918+
expected: "auth0|123456789",
919+
},
920+
{
921+
name: "mixed case with whitespace",
922+
input: " Auth0|User123 ",
923+
expected: "auth0|user123",
924+
},
925+
}
926+
927+
for _, tc := range testCases {
928+
t.Run(tc.name, func(t *testing.T) {
929+
user1 := User{Sub: tc.input}
930+
user2 := User{Sub: tc.expected}
931+
ctx := context.Background()
932+
933+
result1 := user1.BuildSubIndexKey(ctx)
934+
result2 := user2.BuildSubIndexKey(ctx)
935+
936+
if result1 != result2 {
937+
t.Errorf("BuildSubIndexKey() normalization failed: input %q gave %q, expected %q gave %q",
938+
tc.input, result1, tc.expected, result2)
939+
}
940+
941+
// Verify the normalized result matches expected hash
942+
hash := sha256.Sum256([]byte(tc.expected))
943+
expectedHash := hex.EncodeToString(hash[:])
944+
945+
if result1 != expectedHash {
946+
t.Errorf("BuildSubIndexKey() = %q, want %q for normalized input", result1, expectedHash)
947+
}
948+
})
949+
}
950+
}
951+
952+
func TestUser_BuildSubIndexKey_Consistency(t *testing.T) {
953+
// Test that the same input always produces the same output
954+
user := User{Sub: "auth0|123456789"}
955+
ctx := context.Background()
956+
957+
result1 := user.BuildSubIndexKey(ctx)
958+
result2 := user.BuildSubIndexKey(ctx)
959+
960+
if result1 != result2 {
961+
t.Errorf("BuildSubIndexKey() not consistent: first=%q, second=%q", result1, result2)
962+
}
963+
964+
// Verify it's not empty and is valid hex
965+
if result1 == "" {
966+
t.Error("BuildSubIndexKey() returned empty string for valid sub")
967+
}
968+
969+
if _, err := hex.DecodeString(result1); err != nil {
970+
t.Errorf("BuildSubIndexKey() result is not valid hex: %v", err)
971+
}
972+
973+
if len(result1) != 64 {
974+
t.Errorf("BuildSubIndexKey() result length = %d, want 64", len(result1))
975+
}
976+
}
977+
978+
func TestUser_BuildSubIndexKey_EdgeCases(t *testing.T) {
979+
tests := []struct {
980+
name string
981+
sub string
982+
expectEmpty bool
983+
}{
984+
{
985+
name: "nil-like empty string",
986+
sub: "",
987+
expectEmpty: true,
988+
},
989+
{
990+
name: "only spaces",
991+
sub: " ",
992+
expectEmpty: true,
993+
},
994+
{
995+
name: "only tabs",
996+
sub: "\t\t\t",
997+
expectEmpty: true,
998+
},
999+
{
1000+
name: "mixed whitespace",
1001+
sub: " \t \n ",
1002+
expectEmpty: true,
1003+
},
1004+
{
1005+
name: "single character",
1006+
sub: "a",
1007+
expectEmpty: false,
1008+
},
1009+
{
1010+
name: "unicode characters",
1011+
sub: "provider|用户123",
1012+
expectEmpty: false,
1013+
},
1014+
}
1015+
1016+
for _, tt := range tests {
1017+
t.Run(tt.name, func(t *testing.T) {
1018+
user := User{Sub: tt.sub}
1019+
ctx := context.Background()
1020+
1021+
result := user.BuildSubIndexKey(ctx)
1022+
1023+
if tt.expectEmpty {
1024+
if result != "" {
1025+
t.Errorf("BuildSubIndexKey() = %q, expected empty string", result)
1026+
}
1027+
} else {
1028+
if result == "" {
1029+
t.Error("BuildSubIndexKey() returned empty string, expected non-empty")
1030+
}
1031+
// Verify it's valid hex
1032+
if _, err := hex.DecodeString(result); err != nil {
1033+
t.Errorf("BuildSubIndexKey() result is not valid hex: %v", err)
1034+
}
1035+
if len(result) != 64 {
1036+
t.Errorf("BuildSubIndexKey() result length = %d, want 64", len(result))
1037+
}
1038+
}
1039+
})
1040+
}
1041+
}
1042+
8051043
func TestUser_BuildEmailIndexKey_Normalization(t *testing.T) {
8061044
// Test that different representations of the same email produce the same hash
8071045
ctx := context.Background()

internal/infrastructure/authelia/README.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,35 @@ The Authelia integration requires the following configuration parameters:
102102
- NATS server connection details (inherited from main service configuration)
103103
- Key-Value bucket configuration for user data storage
104104

105+
## Subject Identifier (SUB) Management
106+
107+
### SUB Generation and Persistence
108+
109+
The Subject Identifier (SUB) in Authelia is a deterministic UUID that uniquely identifies each user within the system. Key characteristics:
110+
111+
- **Deterministic Generation**: The SUB is a UUID that is consistently generated for each user by Authelia
112+
- **Token-Based Persistence**: To ensure consistent data retrieval from Authelia, the SUB is only persisted when a user is updated using a valid authentication token
113+
- **OIDC UserInfo Endpoint**: The SUB can be retrieved from Authelia's OIDC UserInfo endpoint at `/api/oidc/userinfo` using a valid token
114+
115+
### Token-Based User Updates
116+
117+
When updating user metadata through the auth service, the SUB is populated by accessing Authelia's UserInfo endpoint with the provided token:
118+
119+
```bash
120+
# Example: Update user metadata with token (this populates the SUB)
121+
nats req --server nats://lfx-platform-nats.lfx.svc.cluster.local:4222 "lfx.auth-service.user_metadata.update" '{
122+
"token": "authelia_at_Tx****",
123+
"user_metadata": {
124+
"city": "Metropolis"
125+
}
126+
}'
127+
```
128+
129+
This process ensures that:
130+
- The SUB is retrieved from Authelia's authoritative source
131+
- User data consistency is maintained across the system
132+
- The canonical user identifier is properly established for future lookups
133+
105134
## Security Considerations
106135

107136
- User passwords are automatically generated and stored as bcrypt hashes in ConfigMaps

0 commit comments

Comments
 (0)