# OpenID Connect

## What it is

OpenID Connect is a standard way for an application to learn who a person is
from a service that has just signed them in. Its specification calls it a
simple identity layer on top of OAuth 2.0, and that is how it works: the same
redirects, endpoints and tokens as OAuth, plus one more token, one more endpoint
and the rules for checking them. It is usually shortened to OIDC. The core
specification became final in February 2014; the current text, which folds in
its second set of corrections, is dated 15 December 2023 and is maintained by a
working group of the OpenID Foundation.

OAuth on its own answers the question "what may this application do?" It hands
the application an access token meant for an API, and the application is not
supposed to read it. OpenID Connect adds the question "who signed in?" and
answers it with an ID token: a signed JSON Web Token, addressed to the
application itself, that states who the person is, who vouches for that, when
it was issued and which application it is meant for.

The specification renames the OAuth roles for this job. The OpenID Provider
(OP) is an OAuth authorization server that authenticates people and issues ID
tokens. The Relying Party (RP) is the OAuth client that needs to know who the
person is. The person is the End-User. A request becomes an OpenID Connect
request by putting `openid` in its scope, and that value is required: "If the
openid scope value is not present, the behavior is entirely unspecified."

Further scope values ask for profile data. `profile` covers name, nickname,
picture, locale and similar claims, `email` adds `email` and `email_verified`,
and `address` and `phone` do what they say. The application can fetch those
claims from the provider's UserInfo endpoint, an ordinary OAuth-protected API
called with the access token.

There are three ways to run it. The authorization code flow sends only a
short-lived code through the browser and collects the tokens server to server;
the implicit flow returns tokens straight in the redirect; the hybrid flow
mixes the two. A companion specification, OpenID Connect Discovery 1.0, lets
the application look up every endpoint and signing key from a single address.

## Why it matters

The reason to adopt it is single sign-on. With one identity provider in front of
many applications, people keep one set of credentials, the applications store
none, and the policy that decides how someone signs in, from password rules to
a second factor, is set once at the provider.
Disabling an account at the provider stops it from starting a new session in any
application that relies on it, which is what makes offboarding a single step
rather than a list.

It also closes a hole that teams fall into when they build login on plain
OAuth. RFC 6749 warns that in the implicit flow a public client has no way to
tell which client an access token was issued to, so one that assumes only the
resource owner can hand it a valid token is open to impersonation: an attacker
holding a token issued to another application can present it and be taken for
the person behind it. An ID token names the application it is for in its `aud`
claim, and the application checks it.

Because it is a standard, the work transfers. The specification promises
profile information in an interoperable manner, so the code that signs people in
through one provider can sign them in through another, and with Discovery the
application learns the provider's endpoints and signing keys from its issuer URL
instead of from hand-copied settings.

Finally, OpenID Connect gives each person a stable identifier. The specification
is explicit that the pair of `iss` (who issued the token) and `sub` (the subject
identifier) is the only claim pair an application can rely on as a stable
identifier for a person. The `sub` value is unique within the issuer, never
reassigned and at most 255 ASCII characters. A provider may even issue a
different `sub` to each application, a pairwise identifier, so that
applications cannot correlate a person's activities without permission.

## What it looks like in practice

Take a web application registered with an OpenID Provider at
`https://server.example.com` under the client ID `s6BhdRkqt3`; the values below
come from the specification's own examples. The application starts by reading
the provider's configuration from the well-known address under the issuer:

```http
GET /.well-known/openid-configuration HTTP/1.1
Host: server.example.com
```

The answer is a JSON document. A shortened version:

```json
{
  "issuer": "https://server.example.com",
  "authorization_endpoint": "https://server.example.com/connect/authorize",
  "token_endpoint": "https://server.example.com/connect/token",
  "userinfo_endpoint": "https://server.example.com/connect/userinfo",
  "jwks_uri": "https://server.example.com/jwks.json",
  "scopes_supported": ["openid", "profile", "email", "address", "phone"],
  "id_token_signing_alg_values_supported": ["RS256", "ES256", "HS256"]
}
```

Discovery requires the `issuer` in that document to be identical to the issuer
URL the application put in front of the well-known path, and the application
stops if it differs. Then it sends the person's browser to the authorization
endpoint with the code flow, the `openid` scope and two random values it has
stored against the browser session: `state` for the redirect and `nonce` for
the ID token. A public client would add the PKCE parameters as well, which
RFC 9700 makes mandatory for it; they are left out here to keep lines short.

```http
GET /connect/authorize?response_type=code&client_id=s6BhdRkqt3
    &redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb
    &scope=openid%20profile%20email
    &state=af0ifjsldkj&nonce=n-0S6_WzA2Mj HTTP/1.1
Host: server.example.com
```

The provider signs the person in, sends the browser back with a code, and the
application redeems the code at the token endpoint exactly as in plain OAuth.
The difference is in the answer, which now carries an `id_token` next to the
access token (shortened here):

```json
{
  "access_token": "SlAV32hkKG",
  "token_type": "Bearer",
  "refresh_token": "8xLOxBtZp8",
  "expires_in": 3600,
  "id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjFlOWdkazcifQ.ewogImlzc..."
}
```

The ID token is three base64url parts separated by dots. The first decodes to
`{"alg":"RS256","kid":"1e9gdk7"}`: the signing algorithm and the ID of the key
in the provider's published key set. The middle part holds the claims, and the
specification's example reads:

```json
{
  "iss": "https://server.example.com",
  "sub": "24400320",
  "aud": "s6BhdRkqt3",
  "nonce": "n-0S6_WzA2Mj",
  "exp": 1311281970,
  "iat": 1311280970,
  "auth_time": 1311280969,
  "acr": "urn:mace:incommon:iap:silver"
}
```

Before the application trusts a word of it, it validates the token, and the
specification lists the steps. The `iss` must match the provider's issuer
exactly. The `aud` must contain the application's own client ID and no audience
it does not trust. The signature must verify with the provider's keys from
`jwks_uri`, using the algorithm in the header; when the token arrived straight
from the token endpoint over TLS, as here, checking the server's TLS
certificate may stand in for the signature check. The current time must be
before `exp`. The `nonce` must equal the value the application sent. Only then
does it sign the person in, keyed on `https://server.example.com` plus
`24400320`, and use the access token to fetch email and profile claims from the
UserInfo endpoint if it needs them.

A single provider for many systems is as much an organisational change as a
technical one. One of this site's projects,
[access management across M365 and SSO](/portfolio/led-access-management-across-m365-and-sso/),
consolidated identities and access across M365, 1Password, Red Hat SSO and OKTA
SSO, enforced least privilege, and standardised onboarding and offboarding so
that access was revoked promptly when someone moved on.

## Where it goes wrong

**The email address is used as the account key.** It is the obvious choice and
the wrong one. The specification says a provider may reuse an email address for
a different person over time, and that `email`, `phone_number`,
`preferred_username` and `name` must not be used as unique identifiers. An
application that looks people up by email can hand one person's account to the
next owner of that address. Store `iss` and `sub` together as the key, keep the
email as an attribute, and before relying on an address for anything, check
that `email_verified` is true.

**An access token is taken as proof of who signed in.** Some applications
receive an access token, call an API with it, and treat the answer as a login.
The token may have been issued to a different application altogether, which is
the impersonation RFC 6749 warns about. Sign people in on the ID token, and
only after confirming that its `aud` names your own client ID.

**The ID token is decoded but never checked.** Reading the claims out of a
token takes a base64url decode and no key at all, and that ease is the trap. A
token whose signature, `iss`, `aud` or `exp` goes unverified can be forged,
belong to another application or have expired. Use a maintained library, give
it the issuer and client ID, and let it run every check in the specification's
list rather than picking the convenient ones.

**The nonce is missing, fixed, or never compared.** Without it, an ID token
captured from one sign-in can be replayed into another. RFC 9700 adds that the
nonce must be specific to one sign-in and bound to the browser that started it.
The specification suggests one way to do that: keep a cryptographically random
value in an HttpOnly session cookie, send its hash as the `nonce`, and compare
the two when the ID token comes back.

**Signing keys are cached for good.** Providers rotate keys, and an application
holding yesterday's key set rejects every token signed with the new one. The
specification expects the provider to publish keys at `jwks_uri` and mark each
token with a `kid`, and the application to fetch the key set again when it
meets a `kid` it does not know. Cache the keys, but treat an unfamiliar `kid` as
a signal to refresh rather than a reason to fail the login.

**Two tenants on one host are treated as one issuer.** A provider can run
several issuers on one host, and the path is part of the issuer identifier: the
subject `1234` at `https://example.com` and the subject `1234` at
`https://example.com/sales` are different people. Store the full issuer string,
compare it exactly, and never trim it to a host name.

## Further reading

- [OpenID Connect Core 1.0](https://openid.net/specs/openid-connect-core-1_0.html)
  — the ID token, the three flows, the standard claims and the validation steps,
  in the edition incorporating errata set 2 of December 2023.
- [OpenID Connect Discovery 1.0](https://openid.net/specs/openid-connect-discovery-1_0.html)
  — the well-known configuration document and the rule that its issuer must
  match the issuer URL it was fetched under.
- [RFC 6749: The OAuth 2.0 Authorization Framework](https://www.rfc-editor.org/rfc/rfc6749)
  — the framework OpenID Connect builds on, with the warning in section 10.16
  about using access tokens to identify people.
- [RFC 7519: JSON Web Token (JWT)](https://www.rfc-editor.org/rfc/rfc7519) — the
  token format every ID token uses.
- [RFC 9700: Best Current Practice for OAuth 2.0 Security](https://www.rfc-editor.org/rfc/rfc9700)
  — current guidance on PKCE, the nonce and the redirect, which applies to
  OpenID Connect as much as to OAuth.

<https://engineer.company/glossary/openid-connect/>
