Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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
72 changes: 72 additions & 0 deletions docs/architecture/backend/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,78 @@ 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). Some upstream paths still key on the **prefixed `sub`** during the migration window: the member-service `b2b_org_settings` index tags each doc with `member:auth0|<id>` (and `writers.username:auth0|<id>`) and stores the caller's role under `data.writers[].username` in the same prefixed form, so org role lookups resolve identity via `getEffectiveSub` (see `org-identity.controller.ts` / `org-navigation.service.ts`) — the bare nickname form misses every row there.
- **`username`** identifies the **LF person** by their LFID login handle (bare form, no prefix) and is what most upstream microservices index on going forward. For example, on surveys the bare username is persisted as `creator_username`, while the sibling `creator_id` currently stores the `sub` (migrating to username under LFXV2-1962).

### 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
- **Upstream microservices only ever see the access token.** `req.bearerToken` is the access token (refreshed in `auth.middleware.ts`, forwarded by `microservice-proxy.service.ts`); the ID token never leaves the BFF. Upstream authorizes and indexes off the access token's `sub` / `http://lfx.dev/claims/username` — never the value `getEffectiveUsername` returns. 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'] || '',
// ...
});
```

> **Migration landmine.** `getUsernameFromAuth()` in `auth-helper.ts` is misleadingly named: for Authelia tokens it returns `preferred_username`, but for normal Auth0 tokens it falls back to `getEffectiveSub(req)` — i.e. it returns the prefixed `sub`, not a username. It is a concrete `sub`-as-username site to fix under LFXV2-1962.

### 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** 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)

Backend identity references are migrating from the Auth0 `sub` to the LFID `username`. As upstream handlers learn to accept the username, call sites flip from `getEffectiveSub` to `getEffectiveUsername`, and front-end identity references (DataDog RUM `id`, OpenFeature `targetingKey`, survey `creator_id`) will move to the `https://sso.linuxfoundation.org/claims/username` claim instead of `sub` (today they still read `sub` / `preferred_username`).

`getEffectiveSub` remains as a fallback for the migration window and should be treated as deprecated (annotate it `@deprecated` in `auth-helper.ts` as the migration lands). 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** for identity references (LFID username, e.g. `lguerra`) |
| `getEffectiveSub(req)` | Impersonated sub or OIDC sub | **Deprecated** — Auth0 sub (prefixed, e.g. `auth0\|lguerra`); migrating to username |

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
Loading