> ## Documentation Index
> Fetch the complete documentation index at: https://docs.publica.la/llms.txt
> Use this file to discover all available pages before exploring further.

# OpenID Connect (OIDC)

## Overview

The OpenID Connect integration delegates user authentication from a Publica.la store to an external Identity Provider (IdP) that you already operate (Keycloak, Auth0, Okta, Azure AD, an in-house OIDC server, or any standards-compliant provider).

Your IdP authenticates the end user; Publica.la consumes the IdP response, provisions or links the local account, and starts a session in the store. The user never sees a Publica.la login form.

```mermaid
sequenceDiagram
    participant User
    participant Store as Publica.la Store
    participant IdP as Your IdP

    User->>Store: Clicks "Log in"
    Store->>Store: Terminates any active session
    Store->>IdP: Redirect to authorization endpoint
    IdP->>User: Renders login form
    User->>IdP: Submits credentials
    IdP->>Store: Redirect back with code / payload
    Store->>IdP: Exchanges code, validates ID token
    Store->>Store: Creates or links user (email + external_id)
    Store->>User: Session established, lands on library
```

**Not the right fit?** Consider [Auth Token](./auth-token.md) when you can sign your own JWT, [SAML 2.0](./auth-saml.md) for enterprise SSO, or [LTI](./auth-lti-integrations.md) for LMS contexts.

---

## When to use OIDC

Use this integration when:

- You already run an OIDC-compliant IdP and want to keep credentials there.
- You need a hosted login form managed by your team (branding, MFA, password policies).
- You want a centralized logout across your ecosystem.
- You operate multiple regional storefronts that should share a single identity layer (see the [multi-tenant pattern](#multi-tenant-stores) below).

If your platform can mint its own signed JWTs and you do not need a separate login form, [Auth Token](./auth-token.md) is simpler.

---

## Setup

1. **Register Publica.la as a client in your IdP.** Create an OIDC client (confidential) with:
   - **Grant type:** `authorization_code` (PKCE recommended).
   - **Redirect URI:** `https://{store-domain}/embeddable-login-ui/{aggregator_id?}` (provided by support during onboarding).
   - **Scopes:** at least `openid email profile`. Add any custom scope needed to expose your stable user identifier.

2. **Contact support** with the following configuration:

   | Parameter               | Description                                                                                        |
   | ----------------------- | -------------------------------------------------------------------------------------------------- |
   | `issuer_url`            | Base URL of your OIDC provider (used to fetch `.well-known/openid-configuration` and JWKS).        |
   | `client_id`             | OIDC client identifier registered in your IdP.                                                     |
   | `client_secret`         | OIDC client secret. Stored encrypted on our side.                                                  |
   | `scopes`                | Scopes to request. Defaults to `openid email profile`.                                             |
   | `external_id_claim`     | Claim that holds the stable user identifier (commonly `sub`).                                      |
   | `email_claim`           | Claim that holds the user email. Defaults to `email`.                                              |
   | `logout_url`            | Where Publica.la should redirect users after they log out of the store.                            |
   | `post_login_url`        | Optional default landing URL after successful login. Defaults to `/library`.                       |
   | `force_session_restart` | If `true`, any active store session is terminated before redirecting to your IdP. Default: `true`. |

3. **Wait for confirmation** from support before pointing real users at the flow. We will provide a sandbox tenant for end-to-end testing.

---

## ID token requirements

Publica.la validates the ID token returned by your IdP against the JWKS published at `{issuer_url}/.well-known/openid-configuration`. Tokens that fail any of the checks below are rejected and the user is redirected to your `logout_url` with an error code.

### Validated claims

| Claim   | Validation                                                                                    |
| ------- | --------------------------------------------------------------------------------------------- |
| `iss`   | Must match the configured `issuer_url`.                                                       |
| `aud`   | Must contain the configured `client_id`.                                                      |
| `exp`   | Must be in the future. Allow up to 60 seconds of clock skew between your IdP and our servers. |
| `iat`   | Must not be more than 5 minutes in the past.                                                  |
| `nonce` | Must match the value Publica.la sent in the authorization request.                            |

### Required user data

The following values are pulled from the ID token or from the `userinfo` endpoint (whichever your IdP populates first):

| Field         | Source claim (default) | Description                                                                                             |
| ------------- | ---------------------- | ------------------------------------------------------------------------------------------------------- |
| `external_id` | `sub`                  | Stable, unique identifier for the user in your IdP. Stored on the Publica.la account and never changes. |
| `email`       | `email`                | User email. Updated on every login if it changes upstream.                                              |
| `name`        | `name`                 | Optional. Used for display only.                                                                        |
| `picture`     | `picture`              | Optional. Stored on first login; not updated afterwards.                                                |

:::caution
Choose an `external_id_claim` that is guaranteed to be stable for the lifetime of the user. `sub` is the recommended default. Avoid email-based identifiers; users can change their email upstream and you will end up with duplicate accounts.
:::

---

## Authentication flow

The flow always runs inside the embeddable login iframe at `/{tenant_id}/embeddable-login-ui/{aggregator_id?}`. The host page (your store frontend or mobile app) hosts the iframe and listens for `postMessage` events.

1. The guest opens the store and clicks **Log in**.
2. Publica.la terminates any existing session (when `force_session_restart` is enabled).
3. The browser is redirected to your IdP authorization endpoint with `response_type=code`, the configured scopes, a fresh `state`, and a `nonce`.
4. The user authenticates on your IdP form.
5. The IdP redirects back to `/{tenant_id}/embeddable-login-ui/{aggregator_id?}` with `code` and `state`.
6. Publica.la exchanges the code for an ID token, validates it against JWKS, and reads the configured claims.
7. The local account is provisioned or linked (see [User management](#user-management)).
8. The iframe emits a `loginSuccess` message to the host:
   - On **web** the host stores an [Auth Token](./auth-token.md) cookie.
   - On **mobile and desktop** apps the host stores the platform JWT directly.
9. The user lands on `post_login_url` (or `/library` by default).

### View detailed authentication flow

```mermaid
flowchart TD
Start([User clicks Log in]) --> ForceLogout{Active session?}
ForceLogout -->|Yes| Terminate[Terminate session]
ForceLogout -->|No| Authorize
Terminate --> Authorize[Redirect to IdP authorize endpoint]
Authorize --> Auth[User authenticates on IdP]
Auth --> Callback[IdP redirects to embeddable-login-ui]
Callback --> Exchange[Exchange code for ID token]
Exchange --> Validate{Token valid?}
Validate -->|No| Error[Redirect to logout_url with error]
Validate -->|Yes| UserCheck{User exists by external_id?}
UserCheck -->|No| Create[Create user with external_id and email]
UserCheck -->|Yes| Update[Update email if changed]
Create --> Login[Emit loginSuccess via postMessage]
Update --> Login
Login --> Landing[Redirect to post_login_url]
```

---

## User management

### New users

When no Publica.la user exists with the incoming `external_id`:

1. A new account is created with `external_id` set to the IdP's identifier.
2. `email`, `name`, and `picture` are populated when present.
3. The user is logged in and redirected to `post_login_url`.

### Returning users

When a user with the same `external_id` already exists:

1. The account is matched by `external_id`.
2. `email` is updated if the IdP now returns a different value.
3. `name` and `picture` are not updated after the first login.

### Email uniqueness

Each email can be attached to a single `external_id`. If your IdP returns an email that already belongs to a different user on Publica.la, authentication fails with an `email-conflict` error. Resolve the conflict in your IdP (merge users, or move the email to the correct identity) before retrying.

---

## Multi-tenant stores

If you operate several regional storefronts that share one IdP, register a single OIDC client and route users to the correct store using an `aggregator_id` segment in the redirect URI:

```
https://{store-domain}/{tenant_id}/embeddable-login-ui/{aggregator_id}
```

`aggregator_id` is a value defined during onboarding that maps to a specific regional tenant. Your IdP returns the user to the central domain (for example `https://www.example.com`), and the platform forwards the request to the right regional store based on `aggregator_id`. Keep the mapping list under version control on your side; support owns the mapping on ours.

---

## Code examples

### Generating the authorization URL

### Reference snippet

```javascript
const params = new URLSearchParams({
response_type: 'code',
client_id: process.env.OIDC_CLIENT_ID,
redirect_uri:
'https://store.example.com/2251799858000009/embeddable-login-ui/',
scope: 'openid email profile',
state: crypto.randomUUID(),
nonce: crypto.randomUUID(),
});
res.redirect(`${process.env.OIDC_ISSUER}/auth?${params.toString()}`);
```

You do not need to implement this yourself in most cases. Publica.la performs the redirect from the embeddable login iframe; the snippet is provided so you can reproduce the request in your IdP logs while debugging.

### Listening for `loginSuccess` in the host

```javascript
window.addEventListener('message', (event) => {
  if (event.origin !== 'https://store.example.com') return;
  if (event.data?.type !== 'loginSuccess') return;

  const { authToken, user } = event.data;
  // Web: persist authToken in a cookie or localStorage
  // App: hand authToken to the native bridge
});
```

---

## Troubleshooting

| Symptom                                | Likely cause                                        | Recommended fix                                                               |
| -------------------------------------- | --------------------------------------------------- | ----------------------------------------------------------------------------- |
| Infinite redirect loop after login     | Wrong `aggregator_id` or redirect URI mismatch      | Verify the URI registered in your IdP matches the one provided by support.    |
| `invalid_token` after callback         | Clock drift between your IdP and our servers        | Sync both sides via NTP. Skew above 60 seconds will reject the token.         |
| Duplicate accounts for the same person | `external_id_claim` is not stable (often email)     | Switch the claim to `sub` (or another stable identifier) and reprocess users. |
| User signs in but lands on `/login`    | `post_login_url` not configured or returns 404      | Set `post_login_url` to an existing route, or remove it to use `/library`.    |
| `email-conflict` error                 | Email already attached to a different `external_id` | Reconcile the duplicate in your IdP before re-attempting.                     |
| `loginSuccess` never fires in the host | Iframe origin not allowlisted on the host page      | Ensure the host validates `event.origin` against the store domain you embed.  |

### Debugging tips

1. Inspect the ID token at [jwt.io](https://jwt.io) to confirm `iss`, `aud`, `exp`, and `nonce`.
2. Pull the discovery document at `{issuer_url}/.well-known/openid-configuration` and confirm `jwks_uri` is reachable from the public internet.
3. Capture the IdP redirect in your browser DevTools. The `state` and `nonce` echoed back must match the ones Publica.la generated.
4. Reproduce the failure on the sandbox tenant before changing production configuration.

---

## Security best practices

1. **Use HTTPS end to end.** Never expose the authorization or callback endpoints over plain HTTP.
2. **Rotate client secrets.** Coordinate rotation with support; we will update the stored credential without downtime.
3. **Enforce short token lifetimes** on the IdP side (ID tokens under 5 minutes is a sensible default).
4. **Pin a stable claim** for `external_id`. Avoid mutable fields like email or username.
5. **Keep clock skew under 60 seconds** between your IdP and our servers.
6. **Validate `event.origin`** on the host page when handling `postMessage` to avoid accepting forged login events.



