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) ornone+ 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.
