diff --git a/README.md b/README.md
index 4d71883..d2761a9 100644
--- a/README.md
+++ b/README.md
@@ -57,66 +57,21 @@ The LFX v2 Auth Service operates as a NATS-based microservice that responds to r
### Available Operations
-The service provides the following groups of operations:
-
-#### Email Lookup Operations
-Look up users by their email addresses to retrieve usernames or subject identifiers.
-
-**Subjects:**
-- `lfx.auth-service.email_to_username` - Look up username by email
-- `lfx.auth-service.email_to_sub` - Look up subject identifier by email
-
-**[View Email Lookup Documentation](docs/subjects/email_lookups.md)**
-
----
-
-#### User Metadata Operations
-Retrieve and update user profile metadata using various input types (JWT tokens, subject identifiers, or usernames).
-
-**Subjects:**
-- `lfx.auth-service.user_metadata.read` - Retrieve user metadata
-- `lfx.auth-service.user_metadata.update` - Update user profile
-
-**[View User Metadata Documentation](docs/subjects/user_metadata.md)**
-
----
-
-#### 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/subjects/user_emails.md)** - **Note:** Currently only supported for Authelia
-
----
-
-#### Email Verification Flow
-Two-step verification flow for verifying ownership of alternate email addresses.
-
-**Subjects:**
-- `lfx.auth-service.email_linking.send_verification` - Send OTP to email
-- `lfx.auth-service.email_linking.verify` - Verify email with OTP
-
-**[View Email Verification Documentation](docs/subjects/email_verification.md)** - Includes complete flow diagram
-
----
-
-#### Identity Linking
-Link verified identities (such as verified email addresses) to user accounts.
-
-**Subjects:**
-- `lfx.auth-service.user_identity.link` - Link verified identity to user
-
-**[View Identity Linking Documentation](docs/subjects/identity_linking.md)**
-
----
-
-#### Indexer Contract
-
-Documents what data this service sends to the indexer service (currently none).
-
-**[View Indexer Contract](docs/indexer-contract.md)**
+The service exposes NATS request/reply operations grouped by area. Each link
+has the full reference (subjects, payloads, examples):
+
+- **[Email Lookups](docs/subjects/email_lookups.md)** — look up a user by email
+- **[Username Lookups](docs/subjects/username_lookups.md)** — look up a subject identifier by username
+- **[User Metadata](docs/subjects/user_metadata.md)** — read and update user profile metadata
+- **[User Emails](docs/subjects/user_emails.md)** — read emails and set the primary email
+- **[Email Verification](docs/subjects/email_verification.md)** — passwordless OTP verification of alternate emails
+- **[Identity Linking](docs/subjects/identity_linking.md)** — link, unlink, and list identities
+- **[Password Management](docs/subjects/password_management.md)** — change password and send reset links
+- **[Impersonation](docs/subjects/impersonation.md)** — exchange a token to act as another user
+- **[Aliases](docs/subjects/alias.md)** — claim a system-managed alias email
+- **[Indexer Contract](docs/indexer-contract.md)** — data sent to the indexer service (currently none)
+
+For end-to-end authentication flows, see **[Auth Flows](docs/auth-flows/README.md)**.
---
diff --git a/docs/auth-flows/A-auth-service-m2m-profile-lookup.md b/docs/auth-flows/A-auth-service-m2m-profile-lookup.md
index c8fdc73..6f11532 100644
--- a/docs/auth-flows/A-auth-service-m2m-profile-lookup.md
+++ b/docs/auth-flows/A-auth-service-m2m-profile-lookup.md
@@ -23,7 +23,7 @@ sequenceDiagram
alt Token not in cache or expired
AuthSvc->>Auth0: A1: POST /oauth2/token
[client_credentials grant]
w/ Auth Service M2M client credentials
aud=auth0_mgmt
- Auth0-->>AuthSvc: A2: access_token_m2m_read
(read:users)
*NOT update:users*
+ Auth0-->>AuthSvc: A2: access_token_m2m_read
(read:users used in Flow A)
end
AuthSvc->>Auth0: A3: Check emails,
read profiles using
access_token_m2m_read
diff --git a/docs/auth-flows/D-social-identity-linking.md b/docs/auth-flows/D-social-identity-linking.md
index 0edd3b0..4d68ff7 100644
--- a/docs/auth-flows/D-social-identity-linking.md
+++ b/docs/auth-flows/D-social-identity-linking.md
@@ -35,11 +35,11 @@ sequenceDiagram
else id_token_social sub is a social identity (e.g. google-oauth2|, github|)
Note over SSR: SSR uses
access_token_mgmt_self from Flow C
(Management API token)
- SSR->>NATS: D5: Publish link request
with access_token_mgmt_self + id_token_social
+ SSR->>NATS: D5: Publish to lfx.auth-service.user_identity.link
{ user.auth_token: access_token_mgmt_self,
link_with.identity_token: id_token_social }
Note over NATS,AuthSvc: Auth Service subscribed to NATS subject
NATS->>AuthSvc: Deliver request
- AuthSvc->>Auth0Mgmt: Link social identity
using access_token_mgmt_self
w/ id_token_social claims
+ AuthSvc->>Auth0Mgmt: POST /api/v2/users/{id}/identities
with access_token_mgmt_self
(update:current_user_identities)
+ id_token_social
Auth0Mgmt-->>AuthSvc: Identity linked successfully
AuthSvc->>NATS: Publish response
diff --git a/docs/auth-flows/E-passwordless-email-linking.md b/docs/auth-flows/E-passwordless-email-linking.md
index 11b360b..6fd5990 100644
--- a/docs/auth-flows/E-passwordless-email-linking.md
+++ b/docs/auth-flows/E-passwordless-email-linking.md
@@ -19,7 +19,7 @@ sequenceDiagram
Browser->>SSR: User clicks "Add Email"
and enters new email address
- SSR->>NATS: E1: Publish passwordless
start request with email
+ SSR->>NATS: E1: Publish to lfx.auth-service.email_linking.send_verification
with email
Note over NATS,AuthSvc: Auth Service subscribed to NATS subject
NATS->>AuthSvc: Deliver request
@@ -38,7 +38,7 @@ sequenceDiagram
Note over SSR: SSR already has
access_token_mgmt_self from Flow C
(Management API token)
- SSR->>NATS: E3: Publish verification request
with code + access_token_mgmt_self
+ SSR->>NATS: E3: Publish to lfx.auth-service.email_linking.verify
with code + access_token_mgmt_self
NATS->>AuthSvc: Deliver request
AuthSvc->>Auth0: E4: POST /oauth2/token
[passwordless grant]
w/ "LFX One Profile" client credentials
username=new_email@example.com
otp=verification_code
[NO audience]
diff --git a/docs/auth-flows/README.md b/docs/auth-flows/README.md
index da1b80d..0d73205 100644
--- a/docs/auth-flows/README.md
+++ b/docs/auth-flows/README.md
@@ -29,15 +29,15 @@ The main server-side rendering client used for user authentication. This is a re
A **regular web application** client (`app_type: regular_web`) used for Auth0 Management API access and passwordless flows. This client uses the authorization code flow for obtaining Management API access tokens that allow users to update their own profiles and link identities. It also supports the passwordless OTP grant type (`http://auth0.com/oauth/grant-type/passwordless/otp`) for email verification flows. This client implements a dual authentication pattern where users first authenticate with the main LFX One client, then use this client to obtain additional access tokens for specific audiences (Management API) or perform passwordless verification. Used in Flows C, D, and E.
### LFX V2 Auth Service M2M Client
-A **machine-to-machine (M2M)** client named "LFX V2 Auth Service" that uses the client credentials grant type. This client has restricted permissions with only `read:users` scope (but **not** `update:users`) for the Auth0 Management API. It is used exclusively by the Auth Service to perform read-only operations such as profile lookups and checking email-to-username mappings. Used exclusively in Flow A.
+A **machine-to-machine (M2M)** client named "LFX V2 Auth Service" that uses the client credentials grant type. This client is granted the `create:users`, `read:users`, `update:users`, and `delete:users` scopes for the Auth0 Management API. In Flow A it performs read-only operations such as profile lookups and checking email-to-username mappings; the broader scopes support additional Auth Service management use cases.
## Token Overview
| Token | Audience | Scope | Used For |
|-------|----------|-------|----------|
-| `access_token_m2m_read` | `auth0_mgmt` | `read:users` | Auth Service reading user profiles (Flow A) |
+| `access_token_m2m_read` | `auth0_mgmt` | `create:users`, `read:users`, `update:users`, `delete:users` (Flow A uses `read:users`) | Auth Service reading user profiles (Flow A) |
| `access_token_lfxv2` | `lfxv2` | LFX v2 API | Calling LFX v2 API endpoints (Flow B) |
-| `access_token_mgmt_self` | `auth0_mgmt` | `update:users`* | User updating their own profile (Flow C, D, E) |
+| `access_token_mgmt_self` | `auth0_mgmt` | `update:current_user_metadata` (Flow C) / `update:current_user_identities` (Flow D, E) | User self-service: profile update (C); identity linking (D, E) |
| `access_token_social` | (default) | N/A | Ignored - returned from social auth (Flow D) |
| `access_token_pwdless` | (default) | N/A | Ignored - returned from passwordless (Flow E) |
| `id_token_user` | N/A | N/A | User identity from main login (Flow B) |
@@ -45,6 +45,8 @@ A **machine-to-machine (M2M)** client named "LFX V2 Auth Service" that uses the
| `id_token_social` | N/A | N/A | Social provider identity (Flow D) |
| `id_token_pwdless` | N/A | N/A | Passwordless email identity (Flow E) |
+> **Note:** `auth0_mgmt` and `lfxv2` are conceptual audience labels used throughout these docs. The actual JWT `aud` claim is the Auth0 Management API URL (`https://{domain}/api/v2/`) for `auth0_mgmt`, and the configured LFX v2 API audience for `lfxv2`.
+
## Key Architecture Patterns
### NATS Pub/Sub
@@ -68,7 +70,7 @@ All Auth0 Management API calls are abstracted through the Auth Service, which co
## Security Considerations
-1. **Principle of Least Privilege**: Auth Service M2M client only has `read:users`, not `update:users`
+1. **M2M Client Scopes**: The Auth Service M2M client is granted full Management API user scopes (`create:users`, `read:users`, `update:users`, `delete:users`); Flow A performs only read operations
2. **Subject Validation**: Flow C validates token subjects match before allowing profile updates
3. **Email Verification**: Flow E validates the email in `id_token_pwdless` matches the requested email before linking
4. **Token Scoping**: Each access token is scoped to specific audiences and permissions
diff --git a/docs/alias.md b/docs/subjects/alias.md
similarity index 98%
rename from docs/alias.md
rename to docs/subjects/alias.md
index 7bb7e58..71c3e83 100644
--- a/docs/alias.md
+++ b/docs/subjects/alias.md
@@ -162,7 +162,7 @@ These scopes are provisioned via the companion `auth0-terraform` repository —
- The domain is caller-supplied and validated against the server-side allow-list — there is no implicit default.
- Each user may hold at most one alias **per allowed domain**. With multiple domains in the allow-list, a user could in principle claim one alias on each (e.g. `jane@linux.com` and `jane@dev.lfx.example`).
- A successful claim is immutable from the user's perspective: only an administrator with direct Auth0 access can flip `app_metadata.system_managed` and remove the alias.
-- For detailed Auth0-specific behavior and limitations, see: [`../internal/infrastructure/auth0/README.md`](../internal/infrastructure/auth0/README.md)
+- For detailed Auth0-specific behavior and limitations, see: [`../../internal/infrastructure/auth0/README.md`](../../internal/infrastructure/auth0/README.md)
---
diff --git a/docs/subjects/email_verification.md b/docs/subjects/email_verification.md
index 4a6a33f..ff0c1a4 100644
--- a/docs/subjects/email_verification.md
+++ b/docs/subjects/email_verification.md
@@ -129,10 +129,12 @@ The service sends a one-time password (OTP) to the provided email address and re
```json
{
"success": false,
- "error": "alternate email already linked"
+ "error": "email already linked"
}
```
+> **Note:** The `error` text is a human-readable diagnostic, **not** a stable contract — the exact wording can vary by provider (for example, the mock provider returns `alternate email already linked` for this same condition). Consumers should branch on the `success: false` flag, not on the exact error string.
+
**Error Reply (Invalid Email):**
```json
{
@@ -207,10 +209,12 @@ The returned token is an authentication token that can be used to link the verif
```json
{
"success": false,
- "error": "alternate email already linked"
+ "error": "email already linked"
}
```
+> **Note:** The `error` text is a human-readable diagnostic, **not** a stable contract — the exact wording can vary by provider (for example, the mock provider returns `alternate email already linked` for this same condition). Consumers should branch on the `success: false` flag, not on the exact error string.
+
**Error Reply (Invalid Request):**
```json
{
@@ -263,3 +267,5 @@ The Mock flow is fully self-contained — no NATS KV or SMTP is involved:
2. **Verify OTP** (`email_linking.verify`): Compares the submitted code against the in-memory entry. On success, generates an **internal ID token** with `sub: "email|"` — identical sub format to the Authelia flow.
3. **Link identity** (`user_identity.link`): Same `email|` dispatch — the verified email is appended to the user's `alternate_emails` in the in-memory store.
+> The Mock provider returns `alternate email already linked` (rather than `email already linked`) for an already-linked address — see the note under the error replies above.
+
diff --git a/docs/impersonation.md b/docs/subjects/impersonation.md
similarity index 100%
rename from docs/impersonation.md
rename to docs/subjects/impersonation.md
diff --git a/docs/password_management.md b/docs/subjects/password_management.md
similarity index 94%
rename from docs/password_management.md
rename to docs/subjects/password_management.md
index 6bfdb22..818a76b 100644
--- a/docs/password_management.md
+++ b/docs/subjects/password_management.md
@@ -93,7 +93,7 @@ nats request lfx.auth-service.password.update \
- Only supported for Auth0 username-password connection accounts (`auth0|` prefix); social login accounts (Google, GitHub, etc.) cannot use this endpoint
- The current password is validated via Auth0's Resource Owner Password Grant before the update is applied
- Password requirements (minimum length, complexity) are enforced by Auth0 and may cause the update to fail if not met
-- For detailed Auth0-specific behavior, see: [`../internal/infrastructure/auth0/README.md`](../internal/infrastructure/auth0/README.md)
+- For detailed Auth0-specific behavior, see: [`../../internal/infrastructure/auth0/README.md`](../../internal/infrastructure/auth0/README.md)
### Provider Support
@@ -167,7 +167,7 @@ nats request lfx.auth-service.password.reset_link \
- Email delivery is handled by Auth0 via the `/dbconnections/change_password` endpoint
- The reset link expires according to Auth0 tenant configuration (typically 24 hours)
- This operation succeeds even if the user's account uses a social login provider; Auth0 will handle the email gracefully
-- For detailed Auth0-specific behavior, see: [`../internal/infrastructure/auth0/README.md`](../internal/infrastructure/auth0/README.md)
+- For detailed Auth0-specific behavior, see: [`../../internal/infrastructure/auth0/README.md`](../../internal/infrastructure/auth0/README.md)
### Provider Support
diff --git a/docs/subjects/user_emails.md b/docs/subjects/user_emails.md
index db438ba..94f72f1 100644
--- a/docs/subjects/user_emails.md
+++ b/docs/subjects/user_emails.md
@@ -23,7 +23,11 @@ To retrieve user email addresses (both primary and alternate emails), send a NAT
### Request Fields
-- `user.auth_token` (string, required): A valid JWT token identifying the authenticated user
+- `user.auth_token` (string, required): Identifies the user to read emails for. Despite the name, this field accepts either:
+ - a **JWT token** (Auth0) or **Authelia token**, which is validated before the subject identifier is extracted from the verified claims; or
+ - a **subject identifier** (canonical user ID) — an Auth0 `sub` containing `|` (e.g. `auth0|123456789`) or an Authelia UUID — used directly (no token verification). For Auth0, the service uses its M2M Management API token to read user details once the subject is known.
+
+> **⚠️ Authorization:** The subject-identifier form performs **no token verification** — any caller able to publish to this subject can read any user's emails by supplying their `sub`/UUID. This operation is therefore intended for **trusted internal services only**; the NATS message bus is not exposed to end users, and the calling service is responsible for authorizing the requesting principal before invoking it. For end-user-initiated requests, pass the JWT/Authelia **token** form so the service verifies the caller from the signed claims.
### Reply
@@ -92,7 +96,11 @@ The `alternate_emails` array contains every email identity linked to the user fr
### Example using NATS CLI
```bash
+# Using a JWT token
nats request lfx.auth-service.user_emails.read '{"user":{"auth_token":"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."}}'
+
+# Using a subject identifier (Auth0 sub with "|", or an Authelia UUID)
+nats request lfx.auth-service.user_emails.read '{"user":{"auth_token":"auth0|123456789"}}'
```
### Example Response Processing
diff --git a/docs/username_lookups.md b/docs/subjects/username_lookups.md
similarity index 91%
rename from docs/username_lookups.md
rename to docs/subjects/username_lookups.md
index 826a159..c4478a5 100644
--- a/docs/username_lookups.md
+++ b/docs/subjects/username_lookups.md
@@ -50,4 +50,4 @@ nats request lfx.auth-service.username_to_sub zephyr.stormwind
- Leading/trailing whitespace in the request payload is trimmed automatically
- The service works with Auth0, Authelia, and mock repositories based on configuration
- The returned subject identifier is the canonical user identifier used throughout the system
-- For Authelia-specific SUB identifier details and how they are populated, see: [`../internal/infrastructure/authelia/README.md`](../internal/infrastructure/authelia/README.md)
+- For Authelia-specific SUB identifier details and how they are populated, see: [`../../internal/infrastructure/authelia/README.md`](../../internal/infrastructure/authelia/README.md)