OpenID Connect (OIDC)

Linux Directory as OpenID Connect IdP for app SSO.

OpenID Connect (OIDC)

What OIDC is

OpenID Connect is an identity layer on top of OAuth 2.0. An application (relying party) sends the user to an identity provider (IdP). After the user signs in, the IdP returns:

  • an ID token (JWT) asserting who the user is (sub, name, email, …)
  • usually an access token for calling APIs (e.g. UserInfo)
  • optionally a refresh token to obtain new tokens without re-login

That is how apps like Grafana, Nextcloud, or internal portals do SSO against a central directory — without each app storing passwords.

In lindir, the server is the IdP. Domain users authenticate with their lindir password; apps trust lindir’s tokens.

Apps: step-by-step Grafana and Nextcloud (user_oidc) wiring is in Operator handbook §8. Nextcloud is a natural companion for files, calendar, and contacts.

What lindir implements

Piece Detail
Discovery GET /.well-known/openid-configuration (also /oidc/.well-known/…)
Authorization code GET/POST /oidc/authorize (HTML login form)
Token POST /oidc/token (authorization_code, refresh_token)
UserInfo GET /oidc/userinfo
JWKS GET /oidc/jwks (RSA public key for RS256)
Admin POST/GET /v1/oidc/clients, POST /v1/oidc/clients/revoke
Clients Confidential (client secret) and public ("public": true, no secret)
PKCE Optional for confidential; required for public (S256 / plain)
Signing RS256 ID tokens; key auto-generated at oidcSigningKeyPath

Passkeys / WebAuthn: see WebAuthn.

Config

{
  "issuerUrl": "https://id.example.com",
  "oidcSigningKeyPath": "/var/lib/lindir/oidc-rsa.pem"
}

If issuerUrl is empty, issuer defaults to http://<listenHost>:<listenPort> (0.0.0.0 → 127.0.0.1).
If the PEM file is missing, lindir creates a 2048-bit RSA key (mode 0600) on first start.

Browser-reachable issuer. Discovery advertises authorization_endpoint (and related URLs) from issuerUrl. The user’s browser opens that authorize URL after the app redirects — not only the app’s server. Set issuerUrl to a hostname (and scheme) that humans and browsers can reach. Split-horizon / NAT labs often need the public or host-forwarded URL in issuerUrl, while the app’s server discovers via a private path that returns the same issuer document. If authorize points at an address only the guest can route (for example a hypervisor gateway IP), the browser hangs on “pending.”

Nextcloud and private IdPs. When Nextcloud’s PHP process discovers an IdP on a private/RFC1918 URL, enable allow_local_remote_servers. Register the redirect URI exactly as user_oidc sends it — usually including /index.php (see Operator handbook §8.6).

Register an app

Confidential (default):

curl -s -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"name":"grafana","redirectUris":["https://grafana.example.com/login/generic_oauth"]}' \
  http://127.0.0.1:1289/v1/oidc/clients

Public / SPA (PKCE required):

curl -s -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"name":"spa","public":true,"redirectUris":["https://app.example.com/cb"]}' \
  http://127.0.0.1:1289/v1/oidc/clients

Or via CLI:

lindir-admin oidc client create --name grafana --redirect-uri 'https://grafana/…'
lindir-admin oidc client create --name spa --redirect-uri 'https://spa/cb' --public

Save clientId and clientSecret (secret shown once; empty for public clients). Point the app at:

  • Issuer / discovery: your issuerUrl
  • Auth: /oidc/authorize
  • Token: /oidc/token
  • UserInfo: /oidc/userinfo
  • JWKS: /oidc/jwks
  • Scopes: openid profile email
  • Token auth: client_secret_basic / client_secret_post (confidential) or none + PKCE (public)

Users need a password set (POST /v1/users with password, or POST /v1/users/password).

Refresh tokens

Token responses include refresh_token. Exchange with:

grant_type=refresh_token&refresh_token=…&client_id=…

Refresh tokens are single-use (rotated on each refresh) and expire after 30 days by default.