Skip to content

Commit 2172e38

Browse files
Merge pull request #15 from mauriciozanettisalomao/feat/jira-497-user-data-consumption-backward-compatible
[LFXV2-498, LFXV2-499] User data consumption req/reply - hybrid input support
2 parents f5ac00e + 35ed176 commit 2172e38

File tree

16 files changed

+270
-110
lines changed

16 files changed

+270
-110
lines changed

.gitleaks.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Copyright The Linux Foundation and each contributor to LFX.
2+
# SPDX-License-Identifier: MIT
3+
4+
title = "gitleaks config"
5+
6+
[allowlist]
7+
description = "Allowlist for false positives"
8+
paths = [
9+
"pkg/jwt/parser_test.go"
10+
]

.gitleaksignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Ignore false positives in test files
2+
pkg/jwt/parser_test.go

README.md

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -162,16 +162,43 @@ To retrieve user metadata, send a NATS request to the following subject:
162162
**Subject:** `lfx.auth-service.user_metadata.read`
163163
**Pattern:** Request/Reply
164164

165-
The service takes a token and validates/retrieves user data from the target identity provider based on the `USER_REPOSITORY_TYPE` environment variable configuration.
165+
The service supports a **hybrid approach** for user metadata retrieval, accepting multiple input types and automatically determining the appropriate lookup strategy based on the input format.
166+
167+
#### Hybrid Input Support
168+
169+
The service intelligently handles different input types:
170+
171+
1. **JWT Tokens** (Auth0) or **Authelia Tokens** (Authelia)
172+
2. **Subject Identifiers** (canonical user IDs)
173+
3. **Usernames**
166174

167175
##### Request Payload
168176

169-
The request payload should be a token (no JSON wrapping required):
177+
The request payload can be any of the following formats (no JSON wrapping required):
170178

179+
**JWT Token (Auth0):**
171180
```
172181
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
173182
```
174183

184+
**Subject Identifier:**
185+
```
186+
auth0|123456789
187+
```
188+
189+
**Username:**
190+
```
191+
john.doe
192+
```
193+
194+
##### Lookup Strategy
195+
196+
The service automatically determines the lookup strategy based on input format:
197+
198+
- **Token Strategy**: If input is a JWT/Authelia token, validates the token and extracts the subject identifier
199+
- **Canonical Lookup**: If input contains `|` (pipe character) or is a UUID, treats as subject identifier for direct lookup
200+
- **Username Search**: If input doesn't match above patterns, treats as username for search lookup
201+
175202
##### Reply
176203

177204
The service returns a structured reply with user metadata:
@@ -218,12 +245,19 @@ The service returns a structured reply with user metadata:
218245
##### Example using NATS CLI
219246

220247
```bash
221-
# Retrieve user metadata using token
248+
# Retrieve user metadata using JWT token
222249
nats request lfx.auth-service.user_metadata.read "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
250+
251+
# Retrieve user metadata using subject identifier
252+
nats request lfx.auth-service.user_metadata.read "auth0|123456789"
253+
254+
# Retrieve user metadata using username
255+
nats request lfx.auth-service.user_metadata.read "john.doe"
223256
```
224257

225258
**Important Notes:**
226-
- The service validates the token and extracts user information from the target identity provider
259+
- The service automatically detects input type and applies the appropriate lookup strategy
260+
- JWT tokens are validated for signature and expiration before extracting subject information
227261
- The target identity provider is determined by the `USER_REPOSITORY_TYPE` environment variable
228262
- For detailed Auth0-specific behavior and limitations, see: [`internal/infrastructure/auth0/README.md`](internal/infrastructure/auth0/README.md)
229263
- For detailed Authelia-specific behavior and SUB management, see: [`internal/infrastructure/authelia/README.md`](internal/infrastructure/authelia/README.md)

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.6
8+
version: 0.2.7
99
appVersion: "latest"

internal/infrastructure/auth0/jwt_parser.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/linuxfoundation/lfx-v2-auth-service/pkg/errors"
1515
"github.com/linuxfoundation/lfx-v2-auth-service/pkg/httpclient"
1616
jwtparser "github.com/linuxfoundation/lfx-v2-auth-service/pkg/jwt"
17+
"github.com/linuxfoundation/lfx-v2-auth-service/pkg/redaction"
1718
)
1819

1920
// JWTVerificationConfig holds configuration for JWT signature verification
@@ -61,7 +62,7 @@ func (j *JWTVerificationConfig) JWTVerify(ctx context.Context, token string, req
6162
}
6263

6364
slog.DebugContext(ctx, "JWT signature verification successful",
64-
"user_id", claims.Subject,
65+
"user_id", redaction.Redact(claims.Subject),
6566
"issuer", claims.Issuer,
6667
"audience", claims.Audience,
6768
"expires_at", claims.ExpiresAt,

internal/infrastructure/auth0/jwt_parser_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,7 @@ func TestMetadataLookupWithoutJWTVerificationConfig(t *testing.T) {
359359
{
360360
name: "missing JWT verification config for metadata lookup",
361361
token: "any-token",
362-
expectError: true,
362+
expectError: false, // Now handled as username lookup with M2M token
363363
},
364364
}
365365

internal/infrastructure/auth0/user.go

Lines changed: 57 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"github.com/linuxfoundation/lfx-v2-auth-service/pkg/constants"
1717
"github.com/linuxfoundation/lfx-v2-auth-service/pkg/errors"
1818
"github.com/linuxfoundation/lfx-v2-auth-service/pkg/httpclient"
19+
"github.com/linuxfoundation/lfx-v2-auth-service/pkg/jwt"
1920
"github.com/linuxfoundation/lfx-v2-auth-service/pkg/redaction"
2021
)
2122

@@ -81,6 +82,10 @@ func (u *userReaderWriter) SearchUser(ctx context.Context, user *model.User, cri
8182
}
8283

8384
if user.Token == "" {
85+
slog.DebugContext(ctx, "getting M2M token",
86+
"criteria", criteria,
87+
)
88+
8489
m2mToken, errGetToken := u.config.M2MTokenManager.GetToken(ctx)
8590
if errGetToken != nil {
8691
return nil, errors.NewUnexpected("failed to get M2M token", errGetToken)
@@ -162,6 +167,18 @@ func (u *userReaderWriter) GetUser(ctx context.Context, user *model.User) (*mode
162167

163168
slog.DebugContext(ctx, "getting user", "user_id", user.UserID)
164169

170+
if user.Token == "" {
171+
slog.DebugContext(ctx, "getting M2M token",
172+
"user_id", redaction.Redact(user.UserID),
173+
)
174+
175+
m2mToken, errGetToken := u.config.M2MTokenManager.GetToken(ctx)
176+
if errGetToken != nil {
177+
return nil, errors.NewUnexpected("failed to get M2M token", errGetToken)
178+
}
179+
user.Token = m2mToken
180+
}
181+
165182
// If we don't have a user ID, we can't fetch the user
166183
if user.UserID == "" {
167184
return nil, errors.NewValidation("user_id is required to get user")
@@ -207,7 +224,7 @@ func (u *userReaderWriter) GetUser(ctx context.Context, user *model.User) (*mode
207224
}
208225

209226
// MetadataLookup prepares the user for metadata lookup based on the input
210-
// Verifies JWT token with read:current_user scope and extracts user information
227+
// Accepts JWT token, username, or sub
211228
func (u *userReaderWriter) MetadataLookup(ctx context.Context, input string) (*model.User, error) {
212229
// Validate input
213230
input = strings.TrimSpace(input)
@@ -217,37 +234,52 @@ func (u *userReaderWriter) MetadataLookup(ctx context.Context, input string) (*m
217234

218235
slog.DebugContext(ctx, "metadata lookup", "input", redaction.Redact(input))
219236

220-
// Verify JWT token with read scope
221-
if u.config.JWTVerificationConfig == nil {
222-
return nil, errors.NewValidation("JWT verification configuration is required")
223-
}
237+
user := &model.User{}
224238

225-
claims, err := u.config.JWTVerificationConfig.JWTVerify(ctx, input, userReadRequiredScope)
226-
if err != nil {
227-
slog.ErrorContext(ctx, "JWT signature verification failed for metadata lookup",
228-
"error", err,
239+
// First, try to parse as JWT token to extract the sub
240+
if cleanToken, isJWT := jwt.LooksLikeJWT(input); isJWT {
241+
242+
slog.DebugContext(ctx, "jwt strategy", "input", redaction.Redact(input))
243+
244+
// Verify JWT token with read scope
245+
if u.config.JWTVerificationConfig == nil {
246+
return nil, errors.NewValidation("JWT verification configuration is required")
247+
}
248+
249+
claims, err := u.config.JWTVerificationConfig.JWTVerify(ctx, cleanToken, userReadRequiredScope)
250+
if err != nil {
251+
slog.ErrorContext(ctx, "JWT signature verification failed",
252+
"error", err,
253+
)
254+
return nil, err
255+
}
256+
257+
// Successfully verified JWT token
258+
user.Token = cleanToken
259+
user.UserID = claims.Subject
260+
user.Sub = claims.Subject
261+
262+
slog.DebugContext(ctx, "JWT signature verification successful for metadata lookup",
263+
"sub", user.Sub,
229264
)
230-
return nil, err
231-
}
265+
return user, nil
232266

233-
// Clean the token by removing Bearer prefix if present
234-
cleanToken := strings.TrimSpace(input)
235-
parts := strings.Fields(input)
236-
if len(parts) > 1 && strings.EqualFold(parts[0], "Bearer") {
237-
cleanToken = strings.Join(parts[1:], " ")
238267
}
239268

240-
// Create user object with cleaned token and extracted claims
241-
user := &model.User{
242-
Token: cleanToken,
243-
UserID: claims.Subject,
244-
Sub: claims.Subject,
269+
// Determine lookup strategy based on input format
270+
switch {
271+
case strings.Contains(input, "|"):
272+
// Input contains "|", use as sub for canonical lookup
273+
user.UserID = input
274+
slog.DebugContext(ctx, "canonical lookup strategy", "sub", redaction.Redact(input))
275+
276+
default:
277+
// username search
278+
user.Username = input
279+
user.UserID = ""
280+
slog.DebugContext(ctx, "username search strategy", "username", redaction.Redact(input))
245281
}
246282

247-
slog.DebugContext(ctx, "JWT signature verification successful for metadata lookup",
248-
"sub", user.Sub,
249-
)
250-
251283
return user, nil
252284
}
253285

internal/infrastructure/auth0/user_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -697,17 +697,17 @@ func TestUserReaderWriter_MetadataLookup(t *testing.T) {
697697
{
698698
name: "invalid JWT token",
699699
input: "invalid-token",
700-
expectError: true,
700+
expectError: false, // Now handled as username lookup
701701
},
702702
{
703703
name: "non-JWT input",
704704
input: "test@example.com",
705-
expectError: true,
705+
expectError: false, // Now handled as email lookup
706706
},
707707
{
708708
name: "username input",
709709
input: "testuser",
710-
expectError: true,
710+
expectError: false, // Now handled as username lookup
711711
},
712712
{
713713
name: "valid JWT token with read:current_user scope",

internal/infrastructure/authelia/user.go

Lines changed: 30 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"net/http"
1111
"strings"
1212

13+
"github.com/google/uuid"
1314
"github.com/linuxfoundation/lfx-v2-auth-service/internal/domain/model"
1415
"github.com/linuxfoundation/lfx-v2-auth-service/internal/domain/port"
1516
"github.com/linuxfoundation/lfx-v2-auth-service/internal/infrastructure/nats"
@@ -134,37 +135,47 @@ func (a *userReaderWriter) GetUser(ctx context.Context, user *model.User) (*mode
134135
}
135136

136137
// MetadataLookup prepares the user for metadata lookup based on the input
137-
// Returns true if should use canonical lookup, false if should use search
138+
// Accepts Authelia token, username, or sub
138139
func (u *userReaderWriter) MetadataLookup(ctx context.Context, input string) (*model.User, error) {
139140

140141
if input == "" {
141142
return nil, errors.NewValidation("input is required")
142143
}
143144

144-
userInfo, err := u.fetchOIDCUserInfo(ctx, input)
145-
if err != nil {
146-
slog.ErrorContext(ctx, "failed to fetch OIDC userinfo",
147-
"error", err,
148-
)
149-
return nil, err
150-
}
145+
slog.DebugContext(ctx, "metadata lookup", "input", redaction.Redact(input))
151146

152147
user := &model.User{}
153-
if userInfo != nil {
154-
if userInfo.Sub != "" {
155-
slog.DebugContext(ctx, "found sub in OIDC userinfo",
156-
"sub", userInfo.Sub,
157-
)
158-
user.Sub = userInfo.Sub
159-
}
160-
if userInfo.PreferredUsername != "" {
161-
slog.DebugContext(ctx, "found username in OIDC userinfo",
162-
"username", userInfo.PreferredUsername,
148+
149+
// First, try to parse as Authelia token (starts with 'authelia')
150+
if strings.HasPrefix(input, "authelia") {
151+
// Handle Authelia token
152+
userInfo, err := u.fetchOIDCUserInfo(ctx, input)
153+
if err != nil {
154+
slog.ErrorContext(ctx, "failed to fetch OIDC userinfo",
155+
"error", err,
163156
)
164-
user.Username = userInfo.PreferredUsername
157+
return nil, err
165158
}
159+
user.Token = input
160+
user.UserID = userInfo.Sub
161+
user.Sub = userInfo.Sub
162+
user.Username = userInfo.PreferredUsername
163+
return user, nil
164+
}
165+
166+
sub, errParseUUID := uuid.Parse(input)
167+
if errParseUUID == nil {
168+
user.UserID = sub.String()
169+
user.Sub = user.UserID
170+
slog.DebugContext(ctx, "canonical lookup strategy", "sub", redaction.Redact(input))
171+
return user, nil
166172
}
167173

174+
// username search
175+
user.Username = input
176+
user.Sub = input
177+
slog.DebugContext(ctx, "username search strategy", "username", redaction.Redact(input))
178+
168179
return user, nil
169180
}
170181

internal/infrastructure/authelia/user_test.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,7 @@ func TestUserReaderWriter_MetadataLookup(t *testing.T) {
9797
{
9898
name: "invalid token - should fail OIDC fetch",
9999
input: "invalid-token",
100-
expectError: true,
101-
// The actual error message will depend on the OIDC configuration
100+
expectError: false, // Now handled as username lookup
102101
},
103102
}
104103

0 commit comments

Comments
 (0)