Skip to main content

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 settingJiandu requirement
Client typeConfidential/server-side client with a secret
Flow/grantAuthorization Code; PKCE S256 must be accepted
Redirect URIExact provider-specific callback above
IssuerExact HTTPS issuer returned as iss and by discovery, including any realm/path/trailing slash
ID tokenSigned token containing a stable sub; name is optional
Group admissionThe 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​

/etc/jiandu/jiandu.json
{
"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 valueSetting
Client typeConfidential
Redirect URIhttps://documents.example.net/api/v1/auth/oidc/authentik/callback (strict/exact)
Signing keyAn asymmetric certificate/key pair, so Jiandu verifies through the published JWKS
Issuer modePer-provider (recommended)
Application slugjiandu

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 valueSetting
Client IDjiandu
Client authenticationOn (confidential client)
Standard FlowOn
Implicit Flow / Direct Access Grants / Service accountsOff unless another consumer needs them
Valid Redirect URIsExact Jiandu callback, no *
Web Originshttps://documents.example.net or leave absent when not required by your policy
PKCE methodS256, 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:

configuration.yml
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.

SymptomCheck
Jiandu fails during startupSecret file rules, DNS/system CA trust, discovery reachability, and exact configured issuer
Provider rejects the authorization requestExact callback URI, client ID, Authorization Code enabled, and PKCE S256 support
Token exchange failsRaw client secret matches provider credential and confidential-client auth method is compatible
Callback loops or state failsBrowser used the canonical public origin; proxy preserved Host and cookies; no path-prefix rewriting
admission_deniedInvitation is valid or the exact claim/value is present in the signed ID token
A new duplicate member appearsThe 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.