Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
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
76 changes: 76 additions & 0 deletions docs/architecture/backend/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,82 @@ export interface M2MTokenResponse {
}
```

## 🆔 Identity Claims: `username` vs `sub`

Two distinct identifiers travel on the OIDC user (`req.oidc.user`), and choosing the wrong one breaks upstream lookups. They are **not** interchangeable.

### What each one is

| Claim | Example | Shape | Source claim(s) |
| ------------------------------ | ---------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| **`sub`** (Auth0 subject) | `auth0\|lguerra` | Provider-prefixed, opaque, globally unique per identity | `user.sub` |
| **`username`** (LFID username) | `lguerra` | Bare LF login handle, no provider prefix | `user['https://sso.linuxfoundation.org/claims/username']`, `user.nickname`, `user.username` |
Comment thread
audigregorie marked this conversation as resolved.
Outdated

- **`sub`** identifies the **Auth0 identity record**. It carries a connection prefix (`auth0|`, `github|`, `samlp|`, …), so the same person can have different `sub` values across connections. Treat it as an opaque token — never parse or display it raw (strip the prefix with `stripAuthPrefix` if you must show it). A few call sites still pass the prefixed `sub` upstream during the migration window — e.g. `badges.controller.ts` resolves verified emails via the auth-service using `getEffectiveSub(req)`.
Comment thread
audigregorie marked this conversation as resolved.
Outdated
- **`username`** identifies the **LF person** by their LFID login handle (bare form, no prefix) and is what most upstream microservices index on going forward. Org role grants (`org-identity.controller.ts`, `org-navigation.service.ts`, `org-role-grants.service.ts`) query `b2b_org_settings` with `tags: ['member:${username}']` where `username` comes from `getEffectiveUsername(req)`. On surveys, `creator_username` holds the bare nickname and `creator_id` is set from the `https://sso.linuxfoundation.org/claims/username` claim (LFXV2-2122).
Comment thread
audigregorie marked this conversation as resolved.
Outdated

### ID token vs access token — where the claims actually live

Auth0 issues **two** JWTs per session, and they carry identity differently. This split is the central complication of the `sub` → `username` migration.

| | **ID token** | **Access token** |
| -------------- | ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| Lives on | `req.oidc.user` (typed as `User`) | `req.oidc.accessToken.access_token`, decoded via `decodeJwtPayload()` into `LfxAccessTokenClaims` |
| `sub` | `user.sub` | `claims.sub` |
| username claim | `https://sso.linuxfoundation.org/claims/username` (plus `nickname` / `username` / `preferred_username`) | `http://lfx.dev/claims/username` — **different namespace** |
| Consumed by | The BFF only (SSR, analytics, persistence, the `getEffective*` helpers) | Forwarded upstream as `Authorization: Bearer` to the Go microservices |

Two consequences the migration depends on:

- **`sub` is the only identifier present in both tokens under the same key.** The username claim is namespaced differently in each (`https://sso.linuxfoundation.org/...` in the ID token vs `http://lfx.dev/...` in the access token), so any code bridging the two must map between namespaces — they are not the same key.
Comment thread
audigregorie marked this conversation as resolved.
Outdated
- **The access token is the only _token_ the BFF forwards upstream; the ID token never leaves the BFF.** `req.bearerToken` is the access token (refreshed in `auth.middleware.ts`, forwarded by `microservice-proxy.service.ts`). Upstream authorizes and matches claim-based identity off the access token's `sub` / `http://lfx.dev/claims/username` — never off the ID token, and never off the value `getEffectiveUsername` returns. (Upstream _can_ still see a username outside the JWT when the BFF passes one explicitly in a request param or payload — e.g. the bare `creator_username` persisted on surveys, or a NATS RPC payload — so any LFID handle a Go service needs must be sent on the wire deliberately, not assumed present in the token claims.) So the migration runs on two tracks: (1) **upstream matching** — what the Go services key on, driven by the access token's claims and Auth0 token config, largely outside this repo; and (2) **BFF-side identity** — the `getEffectiveSub` → `getEffectiveUsername` call-site flips and the front-end claim swap, which read the ID token and are fully in this repo.
Comment thread
audigregorie marked this conversation as resolved.
Outdated

**Worked example — impersonation bridges the namespaces by hand.** Impersonation discards the target's ID token and rebuilds identity entirely from the exchanged **access token**, copying its `http://lfx.dev/claims/username` into every ID-token username slot so both namespaces resolve to the same handle (`server.ts`):

```ts
Object.assign(auth.user, {
sub: targetClaims.sub,
username: targetClaims['http://lfx.dev/claims/username'] || '',
'https://sso.linuxfoundation.org/claims/username': targetClaims['http://lfx.dev/claims/username'] || '',
nickname: targetClaims['http://lfx.dev/claims/username'] || '',
// ...
});
```

> **`getUsernameFromAuth()` naming.** For Authelia tokens it returns `preferred_username`; for Auth0 tokens it falls back to `getEffectiveUsername(req)` (LFXV2-2122). The name is still easy to misread — prefer `getEffectiveUsername` directly when you need the LFID handle.
Comment thread
audigregorie marked this conversation as resolved.
Outdated

### When to use which

| Use case | Use |
| ------------------------------------------------------------------------------------- | ------------ |
| Calling an upstream microservice / query-service API that keys on the LF login handle | **username** |
| Persisting an author/owner/creator (`creator_id`, role grants, changelog viewer) | **username** |
| Analytics / observability user identity (DataDog RUM, OpenFeature targeting key) | **username** |
| Per-caller cache keys for user-scoped data | **username** |
| Anything that must match an Auth0 identity record exactly (rare, provider-specific) | **sub** |
Comment thread
audigregorie marked this conversation as resolved.
Outdated

> **Default to `username`.** `sub` is being phased out of backend identity references — see the migration note below.
Comment thread
audigregorie marked this conversation as resolved.
Outdated

### Server-side helpers (impersonation-aware)

Read identity through the helpers in `apps/lfx-one/src/server/utils/auth-helper.ts`, never directly off `req.oidc.user`. They transparently return the **target** user's identity during impersonation and the session user's otherwise.

| Helper | Returns | Status |
| --------------------------- | ------------------------------------------------------------------ | ----------------------------------------------------------------------------------- |
| `getEffectiveUsername(req)` | Impersonated username or OIDC nickname/username/preferred_username | **Preferred** for all new identity references |
| `getEffectiveSub(req)` | Impersonated sub or OIDC sub | **`@deprecated`** — only for call sites whose upstream still wants the prefixed sub |
| `getEffectiveEmail(req)` | Impersonated email or OIDC email (lowercased) | For email-keyed lookups |

### Migration: `sub` → `username` (LFXV2-1962 / LFXV2-2122)

Backend identity references are migrating from the Auth0 `sub` to the LFID `username`. **LFXV2-2122** (merged in [#912](https://github.com/linuxfoundation/lfx-self-serve/pull/912)) flipped the first wave in this repo:

- **Front-end (ID token):** DataDog RUM `id`, OpenFeature `targetingKey`, and survey `creator_id` now read `https://sso.linuxfoundation.org/claims/username` (OpenFeature no longer falls back to `sub` — existing LaunchDarkly rules keyed on sub values need updating before deploy).
- **BFF call sites:** `getEffectiveSub` → `getEffectiveUsername` in changelog, copilot, org-identity, org-navigation, org-lens-access, and org-membership cache keys; `project.service.ts` uses `resolveEmailToUsername` (not `resolveEmailToSub`) for permission and user-info lookups against the plain-LFID `b2b_org_settings` index.
- **`getEffectiveSub`** is annotated `@deprecated` in `auth-helper.ts` and remains only where upstream still requires the prefixed sub (e.g. badges email lookup via auth-service).
Comment thread
audigregorie marked this conversation as resolved.
Outdated

When adding new code, use `username` unless the specific upstream handler still requires the prefixed sub — and if so, note why inline.

## 🏗 Server-Side Implementation

### Auth Context Injection
Expand Down
12 changes: 7 additions & 5 deletions docs/architecture/backend/impersonation.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,11 +158,13 @@ This is the single choke point — every controller and service uses `req.bearer

Many controllers and services read the user's email/username from `req.oidc.user` for server-side filtering (e.g., "get my meetings"). During impersonation, `req.oidc.user` is still the real user. Three helpers resolve the correct identity:

| Helper | Returns |
| --------------------------- | --------------------------------------------- |
| `getEffectiveEmail(req)` | Impersonated email or OIDC email (lowercased) |
| `getEffectiveUsername(req)` | Impersonated username or OIDC nickname |
| `getEffectiveSub(req)` | Impersonated sub or OIDC sub |
| Helper | Returns | Notes |
| --------------------------- | ------------------------------------------------------------------ | ------------------------------------------------------------------------------------- |
| `getEffectiveEmail(req)` | Impersonated email or OIDC email (lowercased) | Email-keyed lookups |
| `getEffectiveUsername(req)` | Impersonated username or OIDC nickname/username/preferred_username | **Preferred** for identity references (LFID username, e.g. `lguerra`) |
| `getEffectiveSub(req)` | Impersonated sub or OIDC sub | **`@deprecated`** — Auth0 sub (prefixed, e.g. `auth0\|lguerra`); rare remaining sites |
Comment thread
audigregorie marked this conversation as resolved.
Outdated

For the full `username` vs `sub` distinction and the LFXV2-1962 migration, see [`authentication.md`](./authentication.md#-identity-claims-username-vs-sub).
Comment thread
audigregorie marked this conversation as resolved.
Outdated

These check `req.appSession['impersonationUser']` first, falling back to `req.oidc.user`. All controllers/services that filter by user identity use these helpers (meetings, events, committees, votes, surveys, mailing lists, documents, analytics, badges, persona detection).

Expand Down
26 changes: 14 additions & 12 deletions docs/architecture/frontend/feature-flags.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ export class FeatureFlagService {
kind: 'user',
name: user.name || '',
email: user.email || '',
targetingKey: user.preferred_username || user.username || user.sub || '',
targetingKey: user.preferred_username || user.username || user['https://sso.linuxfoundation.org/claims/username'],
Comment thread
audigregorie marked this conversation as resolved.
};

await OpenFeature.setContext(userContext);
Expand Down Expand Up @@ -752,19 +752,21 @@ const userContext: EvaluationContext = {
kind: 'user',
name: user.name || '',
email: user.email || '',
targetingKey: user.preferred_username || user.username || user.sub || '',
targetingKey: user.preferred_username || user.username || user['https://sso.linuxfoundation.org/claims/username'],
Comment thread
audigregorie marked this conversation as resolved.
};
```

> **Identity (LFXV2-2122).** `targetingKey` uses the LFID username claim chain above — not Auth0 `sub`. LaunchDarkly rules keyed on legacy `sub` values stop matching after deploy; update them to LFID usernames. See [Authentication — Identity Claims](../backend/authentication.md#-identity-claims-username-vs-sub).
Comment thread
audigregorie marked this conversation as resolved.
Outdated

**Context Structure:**

| Field | Type | Required | Description |
| ----------------- | ------ | -------- | ----------------------------------- |
| `kind` | string | Yes | Always 'user' for user contexts |
| `targetingKey` | string | Yes | Unique identifier for the user |
| `name` | string | No | Display name for LaunchDarkly UI |
| `email` | string | No | Email for targeting rules |
| Custom attributes | any | No | Additional attributes for targeting |
| Field | Type | Required | Description |
| ----------------- | ------ | -------- | ---------------------------------------------------------------------- |
| `kind` | string | Yes | Always 'user' for user contexts |
| `targetingKey` | string | Yes | LFID username (`preferred_username` → `username` → SSO username claim) |
| `name` | string | No | Display name for LaunchDarkly UI |
| `email` | string | No | Email for targeting rules |
| Custom attributes | any | No | Additional attributes for targeting |

**Targeting Use Cases:**

Expand All @@ -778,7 +780,7 @@ const userContext: EvaluationContext = {
```typescript
const userContext: EvaluationContext = {
kind: 'user',
targetingKey: user.sub,
targetingKey: user.preferred_username || user.username || user['https://sso.linuxfoundation.org/claims/username'],
name: user.name,
email: user.email,
// Custom attributes for targeting
Expand Down Expand Up @@ -1448,8 +1450,8 @@ See [Runtime Configuration Troubleshooting](../../runtime-configuration.md#troub
1. **Missing Targeting Key:**

```typescript
// targetingKey is required for user identification
targetingKey: user.preferred_username || user.username || user.sub || '';
// targetingKey is required — LFID username, not Auth0 sub
targetingKey: user.preferred_username || user.username || user['https://sso.linuxfoundation.org/claims/username'];
Comment thread
audigregorie marked this conversation as resolved.
```

2. **Incorrect Attribute Names:**
Expand Down
Loading