# OpenID Connect and OAuth 2.1 (https://docs.idra.zone/docs/admin/oidc)

Connect apps to IdRa's authorization server with OIDC and OAuth 2.1: client types, tokens, claims, discovery and custom APIs.

IdRa is an OpenID Connect provider. Each IdRa organization has one issuer:

```
https://<your IdRa address>/api/auth
```

| Endpoint | Path |
| --- | --- |
| Discovery | `/api/auth/.well-known/openid-configuration` |
| OAuth metadata (RFC 8414) | `/.well-known/oauth-authorization-server/api/auth` |
| Authorize | `/api/auth/oauth2/authorize` |
| Token | `/api/auth/oauth2/token` |
| UserInfo | `/api/auth/oauth2/userinfo` |
| JWKS | `/api/auth/jwks` |
| Introspection | `/api/auth/oauth2/introspect` |
| Device authorization | `/api/auth/device/code` (users approve at `/device`) |

Most libraries only need the issuer: they read the rest from discovery.

## Create an OIDC app

**Applications → Create App Integration → OIDC - OpenID Connect**, then pick the application type:

| Type | Client | Grants |
| --- | --- | --- |
| **Web Application** | Confidential (client secret) | Authorization code (+ PKCE), refresh token |
| **Single-Page Application** | Public, PKCE required | Authorization code, refresh token |
| **Native Application** | Public, PKCE required, no secret | Authorization code, refresh token |
| **API Services** | Confidential | Client credentials only |

Single-page apps on another origin can call the token, userinfo, revoke, introspect, JWKS and discovery endpoints: they answer CORS requests from any origin, without cookies.

Native apps redirect to a private-use scheme (`com.example.app:/callback`), to `http` on `127.0.0.1`, `[::1]` or `localhost`, or to `https` on a public host. Web and single-page apps need `https` on a public host, or for local development `http` on `localhost`, `127.0.0.1` or `[::1]` with a port (`http://localhost:3000/callback`). `javascript:`, `data:` and plain `http` on other hosts are refused for all of them.

Redirect URIs match exactly, port included. A native app registered without a port (`http://127.0.0.1/callback`) may use any port, for apps that pick one when they start. PKCE stays required while a web app has a loopback URI. Remove loopback URIs once an app goes to production.

Enter the **App integration name**, the **Sign-in redirect URIs** (one per line, exact match) and optionally an **Initiate login URI** (where the My Apps tile points). The client secret is shown **once**; **Generate new secret** on the Sign On tab replaces it.

Creating an app opens its **Sign On** tab, and **Assign this app to me** (on by default) lets you test it right away. **Connect your app** on that tab has the issuer, Client ID, discovery URL, redirect URIs and an `.env` snippet to copy, and **Test sign-in**, which signs you in and returns to the first redirect URI with a code. Only your app's own sign-in can exchange a code for tokens.

The tab also lets you change:

- **Grant types:** Authorization Code, Refresh Token, Client Credentials, Device Authorization.
- **Client authentication:** HTTP Basic (`client_secret_basic`), request body (`client_secret_post`) or **Private key JWT** (`private_key_jwt`, see below).
- **Sign-in redirect URIs**, **Sign-out redirect URIs** and a **Back-channel logout URI** (https).
- **Client credentials scopes**, **Require user consent** (off for new apps) and **Require PKCE as additional verification** (on by default; always on for SPAs and native apps).
- **Require DPoP-bound tokens:** see [below](#dpop).

Then [assign](https://docs.idra.zone/docs/admin/applications#assignments) the app to people or groups.

## Private key JWT

Instead of a secret, the app signs a short-lived JWT with its private key, and IdRa checks it against the public keys you give it. On the **Sign On** tab, pick **Private key JWT**, then either paste a **JWKS** or enter a **JWKS URL** (https). IdRa reads the URL when you save, every 5 minutes as assertions arrive, and within 30 seconds of an assertion naming a new `kid`. If the URL stays unreachable for 10 minutes, the app can't authenticate until it's back.

Send `client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer` and `client_assertion` to the token endpoint. The JWT needs `iss` and `sub` set to the client ID, `aud` set to the token endpoint or the issuer, a unique `jti` (each one works once) and an `exp` at most 5 minutes away.

The app's old client secret stops working when you switch. Switching back issues a new secret, shown once.

## DPoP

With **Require DPoP-bound tokens** on, the token endpoint refuses requests without a [DPoP](https://datatracker.ietf.org/doc/html/rfc9449) proof, and tokens are bound to the proof's key: `token_type` is `DPoP`, and using or refreshing a token needs a proof from that same key. Apps that send a proof without this setting get bound tokens too.

## Access after sign-in

Refresh tokens and approved device codes are checked again each time they're used: the app's assignment, blocked zones, ThreatInsight, the Global Session Policy, the app's sign-on policy and the API access policies of its resources. A denial returns an error and is logged as `app.access`; the token isn't consumed, so it works again if access is restored.

Nobody is present at a refresh to answer a prompt, so:

- A rule that asks for stronger assurance doesn't block a refresh: that was settled when the grant was issued.
- The session lifetime doesn't end a grant. Refresh tokens have their own lifetime.
- Network zones see whoever calls the token endpoint. For a web app, that's the app's server, not the person's network, so an "allow only from this zone" rule also has to cover that server.
- Device conditions see the session's device while that session lives, and no device after it ends.

A device code's API access rules run at the token request, when there's a user to evaluate them for.

## Tokens and claims

- PKCE uses `S256` only. Implicit and password grants are refused.
- Tokens are signed with RS256; keys are published at the JWKS endpoint and rotate under **Settings → Keys** (old tokens stay valid until they expire).
- Scopes: `openid`, `profile`, `email`, `offline_access` (refresh tokens), `groups`, plus custom scopes from **Security → API**.
- `groups` adds a `groups` claim (group names) to the ID token, UserInfo and access token. `profile` adds `name`, `preferred_username` (the email) and `updated_at`.
- When a person is deactivated or signed out everywhere, refresh tokens are revoked and apps with a back-channel logout URI receive a logout token. Issued JWT access tokens stay valid until they expire.

## Example: generic OIDC client

```ts
// any OIDC library; values from the app's Sign On tab
const config = {
  issuer: 'https://idra.example.com/api/auth',
  clientId: '<client id>',
  clientSecret: '<client secret>',
  redirectUri: 'https://app.example.com/callback',
  scope: 'openid profile email groups',
};
```

## Custom APIs (authorization servers)

**Security → API** defines authorization servers for your own APIs: an audience, scopes, static claims and access policies that decide which clients and groups may get which scopes. The token preview shows what a given client and user would receive. All authorization servers share the issuer above; the API is told apart by the token's `aud`.