OpenID Connect (OIDC)
OIDC is the recommended external identity integration. Jiandu uses the Authorization Code flow as a confidential server-side client, adds PKCE/S256, validates issuer, audience, signature, nonce, and authorized party, and then creates its own opaque local session. Provider access and refresh tokens are not retained.
Jiandu discovers the provider at startup. The issuer and every endpoint in its discovery document must be reachable from the Jiandu process over trusted HTTPS—not merely from a user's browser.
1. Fix the provider ID and callback first
Choose a stable lowercase ID such as authentik, keycloak-home, or authelia. It becomes both an
identity namespace and part of the exact callback:
Public origin: https://documents.example.net
Provider ID: authentik
Callback URI: https://documents.example.net/api/v1/auth/oidc/authentik/callback
Register that one callback URI at the provider with exact matching—no wildcard and no alternate host. Renaming the ID later changes the callback and makes the configured provider a different identity namespace, so do not use a display label or server version as the ID.
The provider client needs:
| Provider setting | Jiandu requirement |
|---|---|
| Client type | Confidential/server-side client with a secret |
| Flow/grant | Authorization Code; PKCE S256 must be accepted |
| Redirect URI | Exact provider-specific callback above |
| Issuer | Exact HTTPS issuer returned as iss and by discovery, including any realm/path/trailing slash |
| ID token | Signed token containing a stable sub; name is optional |
| Group admission | The configured claim must be present in the ID token as a string or array of strings |
Jiandu does not use email as an identity key and does not fetch UserInfo to fill missing admission
claims. If a provider only emits groups after a separate groups scope or only from UserInfo,
required_group will deny sign-in. Configure a client-specific ID-token mapper or start with
invitation_only.
2. Store the client secret
Copy the raw client secret to a protected file readable by Jiandu:
umask 077
install -d -m 0700 /etc/jiandu/secrets
read -r -s oidc_client_secret
printf '%s\n' "$oidc_client_secret" > /etc/jiandu/secrets/authentik-client-secret
unset oidc_client_secret
chmod 0600 /etc/jiandu/secrets/authentik-client-secret
Do not put the secret directly in JSON or an environment variable. On Unix, Jiandu requires a
single regular file with exact mode 0600, no symlink/hard link, and no more than 4 KiB.
3. Add the provider to the HTTPS boundary
{
"schema_version": 1,
"server": {
"bind": "127.0.0.1:8077",
"ingress": {
"mode": "https_reverse_proxy",
"public_origin": "https://documents.example.net",
"trusted_proxy_cidrs": ["127.0.0.1/32", "::1/128"]
}
},
"auth": {
"providers": [
{
"kind": "oidc",
"id": "authentik",
"display_name": "Authentik",
"issuer": "https://identity.example.net/application/o/jiandu/",
"client_id": "jiandu",
"client_secret_path": "/etc/jiandu/secrets/authentik-client-secret",
"public_origin": "https://documents.example.net",
"admission_policy": "invitation_only"
}
]
}
}
The provider public_origin must exactly equal server.ingress.public_origin, including a
non-default port. OIDC cannot be enabled in loopback-HTTP mode. Follow reverse proxy and
HTTPS first and keep the Jiandu listener private.
The provider registry is file-only and supports up to 16 unique IDs. The older single-provider
auth.oidc object and JIANDU_OIDC_* variables exist only as a migration path; do not combine them
with auth.providers.
4. Select admission explicitly
Keep the example's invitation_only while validating a provider. To require a claim value on every
sign-in, replace the policy and add both fields:
"admission_policy": "required_group",
"admission_group_claim": "groups",
"admission_group": "jiandu-members"
The claim may be one string or an array of strings. Nested JSON paths are not interpreted, and the value comparison is exact and case-sensitive. Verify the actual signed ID token mapping at the provider; do not infer it from what its administration screen calls a group.
To let every identity at this exact issuer become a Jiandu Member, use:
"admission_policy": "open"
Remove both group fields when using invitation_only or open. Open admission does not import
provider roles, and an existing invitation is neither required nor consumed.
Authentik
Create one OAuth2/OpenID provider associated with a Jiandu application. Use these values:
| Authentik value | Setting |
|---|---|
| Client type | Confidential |
| Redirect URI | https://documents.example.net/api/v1/auth/oidc/authentik/callback (strict/exact) |
| Signing key | An asymmetric certificate/key pair, so Jiandu verifies through the published JWKS |
| Issuer mode | Per-provider (recommended) |
| Application slug | jiandu |
With the normal per-provider issuer mode, configure Jiandu with:
{
"kind": "oidc",
"id": "authentik",
"display_name": "Authentik",
"issuer": "https://identity.example.net/application/o/jiandu/",
"client_id": "<AUTHENTIK-CLIENT-ID>",
"client_secret_path": "/etc/jiandu/secrets/authentik-client-secret",
"public_origin": "https://documents.example.net",
"admission_policy": "invitation_only"
}
The trailing slash in Authentik's per-application issuer is significant. Confirm it from the discovery document rather than constructing a close-looking value. Authentik supports authorization code and PKCE, and its official OAuth2/OIDC provider reference documents issuer modes and exact redirect URI behavior.
Authentik's profile scope mapping can include groups, but Jiandu's admission needs the claim in
the signed ID token used by this client. If you choose required_group, create a provider/client
mapping that emits the intended groups value without relying on UserInfo, test a denied user, and
only then change the Jiandu policy.
Keycloak
Create an OpenID Connect client in the intended realm with this capability set:
| Keycloak value | Setting |
|---|---|
| Client ID | jiandu |
| Client authentication | On (confidential client) |
| Standard Flow | On |
| Implicit Flow / Direct Access Grants / Service accounts | Off unless another consumer needs them |
| Valid Redirect URIs | Exact Jiandu callback, no * |
| Web Origins | https://documents.example.net or leave absent when not required by your policy |
| PKCE method | S256, if enforced by a client policy |
For realm household, the Jiandu entry is:
{
"kind": "oidc",
"id": "keycloak-home",
"display_name": "Keycloak",
"issuer": "https://identity.example.net/realms/household",
"client_id": "jiandu",
"client_secret_path": "/etc/jiandu/secrets/keycloak-client-secret",
"public_origin": "https://documents.example.net",
"admission_policy": "invitation_only"
}
For required-group admission, attach a Group Membership protocol mapper directly to the client
or through a default client scope, set its token claim name to groups, and include it in the ID
token. Decide whether Keycloak emits full group paths: with full paths, the exact Jiandu value may be
/jiandu-members rather than jiandu-members. Keycloak's client documentation
explains confidential clients, Standard Flow, and exact redirect restrictions.
Authelia
Authelia stores a digest of the client secret while Jiandu needs the corresponding raw secret. Save
the raw value in Jiandu's mode-0600 file and put only its Authelia-generated digest below:
identity_providers:
oidc:
clients:
- client_id: jiandu
client_name: Jiandu
client_secret: '<DIGEST-OF-THE-SAME-RAW-SECRET>'
public: false
authorization_policy: two_factor
redirect_uris:
- 'https://documents.example.net/api/v1/auth/oidc/authelia/callback'
scopes:
- openid
- profile
- groups
grant_types:
- authorization_code
response_types:
- code
pkce_challenge_method: S256
token_endpoint_auth_method: client_secret_basic
Then add:
{
"kind": "oidc",
"id": "authelia",
"display_name": "Authelia",
"issuer": "https://identity.example.net",
"client_id": "jiandu",
"client_secret_path": "/etc/jiandu/secrets/authelia-client-secret",
"public_origin": "https://documents.example.net",
"admission_policy": "invitation_only"
}
Use the issuer published by Authelia's discovery document if it differs from the root example. The
allowed groups scope does not by itself prove that Jiandu's ID token will contain a group claim;
retain invitation_only unless the signed token mapping has been verified. See Authelia's
OIDC client reference
for its current secret-digest and PKCE fields.
Validate and diagnose
First check configuration, issuer discovery, and public presentation:
jiandu --config /etc/jiandu/jiandu.json --check-config
curl --fail --silent \
https://identity.example.net/.well-known/openid-configuration | jq '.issuer, .jwks_uri'
curl --fail --silent https://documents.example.net/api/v1/auth/methods \
| jq '.oidcProviders'
Authentik's discovery path is below its per-application issuer; use the discovery URL it documents. After restart, test a complete flow in a clean browser and then test a user who must be denied.
| Symptom | Check |
|---|---|
| Jiandu fails during startup | Secret file rules, DNS/system CA trust, discovery reachability, and exact configured issuer |
| Provider rejects the authorization request | Exact callback URI, client ID, Authorization Code enabled, and PKCE S256 support |
| Token exchange fails | Raw client secret matches provider credential and confidential-client auth method is compatible |
| Callback loops or state fails | Browser used the canonical public origin; proxy preserved Host and cookies; no path-prefix rewriting |
admission_denied | Invitation is valid or the exact claim/value is present in the signed ID token |
| A new duplicate member appears | The provider changed iss/sub or the provider ID was renamed; email/name matching is intentionally not automatic |
Rotate a client secret by updating the provider credential and protected file as one maintenance change, then restart Jiandu and complete a fresh flow. Keep a local recovery method available while the provider is being changed.