Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,16 @@ Retrieve and update user profile metadata using various input types (JWT tokens,

---

#### User Emails Operations
Retrieve user email addresses (primary and alternate emails) using various input types (JWT tokens, subject identifiers, or usernames).

**Subjects:**
- `lfx.auth-service.user_emails.read` - Retrieve user email addresses

**[View User Emails Documentation](docs/user_emails.md)** - **Note:** Currently only supported for Authelia

---

#### Email Verification Flow
Two-step verification flow for verifying ownership of alternate email addresses.

Expand Down
2 changes: 1 addition & 1 deletion charts/lfx-v2-auth-service/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ apiVersion: v2
name: lfx-v2-auth-service
description: LFX Platform V2 Auth Service chart
type: application
version: 0.3.0
version: 0.3.1
appVersion: "latest"
22 changes: 22 additions & 0 deletions charts/lfx-v2-auth-service/templates/nats-kv-buckets.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Copyright The Linux Foundation and each contributor to LFX.
# SPDX-License-Identifier: MIT

# The following buckets are only for the authelia flow
---
{{- if and .Values.nats.authelia_users_kv_bucket.creation (eq .Values.app.environment.USER_REPOSITORY_TYPE.value "authelia") }}
apiVersion: jetstream.nats.io/v1beta2
Expand All @@ -19,3 +21,23 @@ spec:
maxBytes: {{ .Values.nats.authelia_users_kv_bucket.maxBytes }}
compression: {{ .Values.nats.authelia_users_kv_bucket.compression }}
{{- end }}
---
{{- if and .Values.nats.authelia_email_otp_kv_bucket.creation (eq .Values.app.environment.USER_REPOSITORY_TYPE.value "authelia") }}
apiVersion: jetstream.nats.io/v1beta2
kind: KeyValue
metadata:
name: {{ .Values.nats.authelia_email_otp_kv_bucket.name }}
namespace: {{ .Release.Namespace }}
{{- if .Values.nats.authelia_email_otp_kv_bucket.keep }}
annotations:
"helm.sh/resource-policy": keep
{{- end }}
spec:
bucket: {{ .Values.nats.authelia_email_otp_kv_bucket.name }}
history: {{ .Values.nats.authelia_email_otp_kv_bucket.history }}
storage: {{ .Values.nats.authelia_email_otp_kv_bucket.storage }}
maxValueSize: {{ .Values.nats.authelia_email_otp_kv_bucket.maxValueSize }}
maxBytes: {{ .Values.nats.authelia_email_otp_kv_bucket.maxBytes }}
compression: {{ .Values.nats.authelia_email_otp_kv_bucket.compression }}
ttl: {{ .Values.nats.authelia_email_otp_kv_bucket.ttl }}
{{- end }}
22 changes: 22 additions & 0 deletions charts/lfx-v2-auth-service/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,28 @@ nats:
# compression is a boolean to determine if the KV bucket should be compressed
compression: true

authelia_email_otp_kv_bucket:
# creation is a boolean to determine if the KV bucket should be created via the helm chart.
# set it to false if you want to use an existing KV bucket.
creation: true
# keep is a boolean to determine if the KV bucket should be preserved during helm uninstall
# set it to false if you want the bucket to be deleted when the chart is uninstalled
keep: true
# name is the name of the KV bucket for storing projects
name: authelia-email-otp
# history is the number of history entries to keep for the KV bucket
history: 1
# storage is the storage type for the KV bucket
storage: file
# maxValueSize is the maximum size of a value in the KV bucket
maxValueSize: 1024 # 1KB (sufficient for OTP data)
# maxBytes is the maximum number of bytes in the KV bucket
maxBytes: 524288 # 512KB (smaller for local dev)
# compression is a boolean to determine if the KV bucket should be compressed
compression: true
# ttl is the time-to-live for entries in the bucket (5 minutes for OTPs)
ttl: 5m

# serviceAccount is the configuration for the Kubernetes service account
## This will be used only if the USER_REPOSITORY_TYPE is authelia
serviceAccount:
Expand Down
1 change: 1 addition & 0 deletions cmd/server/service/message_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ func (mhs *MessageHandlerService) HandleMessage(ctx context.Context, msg port.Tr
// user read/write operations
constants.UserMetadataUpdateSubject: mhs.messageHandler.UpdateUser,
constants.UserMetadataReadSubject: mhs.messageHandler.GetUserMetadata,
constants.UserEmailReadSubject: mhs.messageHandler.GetUserEmails,
// lookup operations
constants.UserEmailToUserSubject: mhs.messageHandler.EmailToUsername,
constants.UserEmailToSubSubject: mhs.messageHandler.EmailToSub,
Expand Down
1 change: 1 addition & 0 deletions cmd/server/service/providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ func QueueSubscriptions(ctx context.Context) error {
constants.UserEmailToUserSubject: messageHandlerService.HandleMessage,
constants.UserEmailToSubSubject: messageHandlerService.HandleMessage,
constants.UserMetadataReadSubject: messageHandlerService.HandleMessage,
constants.UserEmailReadSubject: messageHandlerService.HandleMessage,
constants.EmailLinkingSendVerificationSubject: messageHandlerService.HandleMessage,
constants.EmailLinkingVerifySubject: messageHandlerService.HandleMessage,
constants.UserIdentityLinkSubject: messageHandlerService.HandleMessage,
Expand Down
24 changes: 16 additions & 8 deletions docs/identity_linking.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,19 @@ The request payload must be a JSON object containing the user's JWT token and th

```json
{
"user_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"link_with": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
"user": {
"auth_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
},
"link_with": {
"identity_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
}
}
```

### Required Fields

- `user_token`: A JWT access token for the Auth0 Management API with the `update:current_user_identities` scope. The `user_id` will be automatically extracted from the `sub` claim of this token.
- `link_with`: The ID token obtained from the email verification process that contains the verified email identity
- `user.auth_token`: A JWT access token for the Auth0 Management API with the `update:current_user_identities` scope. The `user_id` will be automatically extracted from the `sub` claim of this token.
- `link_with.identity_token`: The ID token obtained from the email verification process that contains the verified email identity

### Reply

Expand Down Expand Up @@ -60,21 +64,25 @@ The service links the verified email identity to the user account without changi
```bash
# Link the verified email identity to the user account
nats request lfx.auth-service.user_identity.link '{
"user_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"link_with": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
"user": {
"auth_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
},
"link_with": {
"identity_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
}
}'

# Expected response: {"success":true,"message":"identity linked successfully"}
```

### Important Notes

- The SSR application must provide the user's JWT token (`user_token`) with the `update:current_user_identities` scope
- The SSR application must provide the user's JWT token (`user.auth_token`) with the `update:current_user_identities` scope
- The Auth Service automatically extracts the `user_id` from the `sub` claim of the user's token
- The Auth Service verifies the JWT token signature and validates the required scope before processing
- The Auth Service uses the **user's token** (not the service's M2M credentials) to call the Auth0 Management API
- This ensures the operation is performed with the user's permissions and does not change their current global session
- The `link_with` field contains the ID token from the email verification process with the verified email information that will be linked to the user account
- The `link_with.identity_token` field contains the ID token from the email verification process with the verified email information that will be linked to the user account
- This feature is **only supported for Auth0**. Authelia and mock implementations do not support this functionality yet.

### Complete Flow
Expand Down
190 changes: 190 additions & 0 deletions docs/user_emails.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
# User Emails Operations

This document describes the NATS subject for retrieving user email addresses.

---

## User Emails Retrieval

To retrieve user email addresses (both primary and alternate emails), send a NATS request to the following subject:

**Subject:** `lfx.auth-service.user_emails.read`
**Pattern:** Request/Reply

The service supports a **hybrid approach** for user email retrieval, accepting multiple input types and automatically determining the appropriate lookup strategy based on the input format.

### Hybrid Input Support

The service intelligently handles different input types:

1. **JWT Tokens** (Auth0) or **Authelia Tokens** (Authelia)
2. **Subject Identifiers** (canonical user IDs)
3. **Usernames**

### Request Payload

The request payload can be any of the following formats (no JSON wrapping required):

**JWT Token (Auth0):**
```
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
```

**Subject Identifier:**
```
auth0|123456789
```

**Username:**
```
john.doe
```

### Lookup Strategy

The service automatically determines the lookup strategy based on input format:

- **Token Strategy**: If input is a JWT/Authelia token, validates the token and extracts the subject identifier
- **Canonical Lookup**: If input contains `|` (pipe character) or is a UUID, treats as subject identifier for direct lookup
- **Username Search**: If input doesn't match above patterns, treats as username for search lookup

### Reply

The service returns a structured reply with user email information:

**Success Reply:**
```json
{
"success": true,
"data": {
"primary_email": "[email protected]",
"alternate_emails": [
{
"email": "[email protected]",
"verified": true
},
{
"email": "[email protected]",
"verified": false
}
]
}
}
```

**Success Reply (No Alternate Emails):**
```json
{
"success": true,
"data": {
"primary_email": "[email protected]",
"alternate_emails": []
}
}
```

**Error Reply (User Not Found):**
```json
{
"success": false,
"error": "user not found"
}
```

**Error Reply (Invalid Token):**
```json
{
"success": false,
"error": "invalid token"
}
```

### Response Fields

- `primary_email` (string): The user's primary email address registered with the identity provider
- `alternate_emails` (array): List of alternate email addresses linked to the user account
- `email` (string): The alternate email address
- `verified` (boolean): Whether the alternate email has been verified

### Example using NATS CLI

```bash
# Retrieve user emails using JWT token
nats request lfx.auth-service.user_emails.read "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."

# Retrieve user emails using subject identifier
nats request lfx.auth-service.user_emails.read "auth0|123456789"

# Retrieve user emails using username
nats request lfx.auth-service.user_emails.read "john.doe"
```

### Example Response Processing

```bash
# Get and format the response
nats request lfx.auth-service.user_emails.read "john.doe" | jq '.'

# Extract only the primary email
nats request lfx.auth-service.user_emails.read "john.doe" | jq -r '.data.primary_email'

# List all verified alternate emails
nats request lfx.auth-service.user_emails.read "john.doe" | jq -r '.data.alternate_emails[] | select(.verified == true) | .email'

# Count total email addresses (primary + alternates)
nats request lfx.auth-service.user_emails.read "john.doe" | jq '.data.alternate_emails | length + 1'
```

**Important Notes:**
- The service automatically detects input type and applies the appropriate lookup strategy
- JWT tokens are validated for signature and expiration before extracting subject information
- The target identity provider is determined by the `USER_REPOSITORY_TYPE` environment variable
- Primary email is always present if the user exists
- Alternate emails array may be empty if the user has not linked any additional email addresses
- Only verified alternate emails should be considered as confirmed user identities
- For detailed Auth0-specific behavior and limitations, see: [`../internal/infrastructure/auth0/README.md`](../internal/infrastructure/auth0/README.md)
- For detailed Authelia-specific behavior and SUB management, see: [`../internal/infrastructure/authelia/README.md`](../internal/infrastructure/authelia/README.md)

---

## Use Cases

### Identity Verification
When you need to verify if a user owns a specific email address:
```bash
# Get all user emails
nats request lfx.auth-service.user_emails.read "john.doe"
```

### Email Communication
When you need to send notifications to all verified user email addresses:
```bash
# Extract all verified emails (primary + verified alternates)
nats request lfx.auth-service.user_emails.read "john.doe" | \
jq -r '(.data.primary_email, (.data.alternate_emails[] | select(.verified == true) | .email))'
```

### Account Recovery
When displaying email options for account recovery:
```bash
# Show all verified email addresses for recovery selection
nats request lfx.auth-service.user_emails.read "auth0|123456789" | \
jq '.data.alternate_emails[] | select(.verified == true)'
```

### Email Uniqueness Check
To check if an email is already associated with a user account, use the email lookup subjects:
- `lfx.auth-service.email_to_username` - Get username from email
- `lfx.auth-service.email_to_sub` - Get user ID from email

See [`email_lookups.md`](email_lookups.md) for more details on these subjects.

---

## Related Subjects

- **Email Lookup**: [`email_lookups.md`](email_lookups.md)
- **Email Verification**: [`email_verification.md`](email_verification.md)
- **User Metadata**: [`user_metadata.md`](user_metadata.md)
- **Identity Linking**: [`identity_linking.md`](identity_linking.md)

39 changes: 36 additions & 3 deletions internal/domain/model/email.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import "net/mail"

// Email represents an email
type Email struct {
OTP string `json:"otp"`
Email string `json:"email"`
EmailVerified bool `json:"email_verified"`
OTP string `json:"otp,omitempty"`
Email string `json:"email"`
Verified bool `json:"verified"`
}

// IsValidEmail checks if the email is valid according to RFC 5322
Expand All @@ -20,3 +20,36 @@ func (e *Email) IsValidEmail() bool {
_, err := mail.ParseAddress(e.Email)
return err == nil
}

// EmailMessage represents an email message to be sent
type EmailMessage struct {
// From is the sender email address
From string
// FromName is the sender name (optional)
FromName string
// To is the recipient email address
To string
// Subject is the email subject
Subject string
// Body is the email body content
Body string
// IsHTML indicates if the body is HTML formatted
IsHTML bool
}

// IsValid checks if the email message has all required fields
func (e *EmailMessage) IsValid() bool {
if e.To == "" || e.Subject == "" || e.Body == "" {
return false
}
// Validate email addresses
if _, err := mail.ParseAddress(e.To); err != nil {
return false
}
if e.From != "" {
if _, err := mail.ParseAddress(e.From); err != nil {
return false
}
}
return true
}
Loading