Skip to main content

AuthCrunch as an OpenID Provider

AuthCrunch can act as an OpenID Provider for applications that sign in its local users. Your application becomes the relying party: it redirects the browser to AuthCrunch, receives an authorization code, and exchanges that code for an ID token and a UserInfo access token.

This is available in caddy-security v1.3.0 / go-authcrunch v1.3.8. It is the opposite direction from connecting an external OIDC provider. The portal adapter selects explicit local realms; external OAuth, SAML and LDAP logins do not supply downstream OIDC authentication proof.

Prepare the issuer and keys​

Choose a stable canonical HTTPS issuer, here https://auth.example.com/auth. Its path must equal the portal mount, without a trailing slash. Changing the issuer changes the identity boundary for relying parties.

Create a dedicated RSA private key, separate from the ordinary portal JWT key:

umask 077
mkdir -p /etc/authcrunch/oidc
chmod 700 /etc/authcrunch/oidc
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:3072 \
-out /etc/authcrunch/oidc/signing.pem
chmod 600 /etc/authcrunch/oidc/signing.pem

Run these provisioning commands with the account responsible for the service's private files. The runtime accepts unencrypted PKCS#8 or PKCS#1 RSA PEM with 2048–8192-bit keys. Parent directories and files must be private and readable by the service. Never reuse the portal access-token key for OIDC.

Create a private random client secret of at least 32 bytes of entropy, store it as OIDC_WEBSITE_SECRET, and configure the same secret in the relying party. Provision the local store before testing and choose its password/MFA policy. The Caddyfile uses {$OIDC_WEBSITE_SECRET} because client registration validates the secret during adaptation. Treat adapted JSON as private: it contains the expanded client credential.

Register a relying application​

assets/conf/apps/oidc/Caddyfile
# AuthCrunch as an OpenID Provider: caddy-security v1.3.0.
{
security {
local identity store localdb {
realm local
path /var/lib/authcrunch/users.json
}

oauth application website {
client_id website-client
client_name "Example website"
client_secret {$OIDC_WEBSITE_SECRET}
token_endpoint_auth_method client_secret_basic
redirect_uri https://app.example.com/oidc/callback
scopes openid profile email
require_pkce yes
}

authentication portal myportal {
enable identity store localdb
oidc provider {
issuer https://auth.example.com/auth
realms local
signing key files /etc/authcrunch/oidc/signing.pem
applications website
session lifetime 28800
token lifetime 300
}
}
}
}

# Scope the consent form's headers to its HTML response and exact callback origin.
(oidc_consent_headers) {
@oidcConsent {
method GET
path /auth/oidc/authorize /auth/oidc/continue
}
header @oidcConsent {
Referrer-Policy same-origin
Content-Security-Policy "default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'self' https://app.example.com"
match {
status 200
header Content-Type "text/html; charset=utf-8"
header Content-Security-Policy "default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'"
header Referrer-Policy no-referrer
}
}
}

auth.example.com {
route {
redir /auth /auth/ 308
import oidc_consent_headers
authenticate /auth/* with myportal
}
}

website is the configuration nickname; website-client is the protocol client ID. applications website explicitly selects that registration for the portal. The example retains consent and requires S256 PKCE. It also scopes consent HTML headers to the exact authorization/continuation paths and callback origin: the released provider's default no-referrer policy can omit Origin on consent POST, and Chrome checks the form redirect against form-action. Keep this header adjustment narrow; change its callback origin when changing the relying party.

Each redirect_uri directive registers one exact callback. Repeat it for another callback; the plural redirect_uris is not Caddyfile syntax. HTTPS callbacks do not support wildcards. Supply explicit credentials on ordinary adaptation, or use a privately provisioned registration revision; do not generate new client credentials on every restart.

Configure the relying party with:

SettingValue in this example
Issuerhttps://auth.example.com/auth
Client IDwebsite-client
Client authenticationclient_secret_basic
Callbackhttps://app.example.com/oidc/callback
Requested scopesopenid profile email
Response type / PKCEcode / S256

Use the relying party's own OIDC integration library to generate state, nonce, and PKCE, redeem the code, and verify the ID token's signature, issuer, audience, expiry and nonce. The resulting application session belongs to that application.

Discovery and token boundaries​

PurposePath relative to the issuer
Discovery/.well-known/openid-configuration
Authorization/oidc/authorize
Token exchange/oidc/token
UserInfo/oidc/userinfo
Public OIDC signing keys/oidc/jwks
Token revocation/oidc/revoke

All ID tokens use RS256. The opaque access token goes to UserInfo as Authorization: Bearer TOKEN; the ID token and ordinary portal access JWT cannot be substituted for it. The legacy portal /.well-known/jwks.json publishes a different key set for ordinary portal JWTs.

The OIDC subject derives from the immutable local record plus its store/realm namespace. Recreating a deleted username creates a different subject. Local roles and internal credential evidence are not exported as UserInfo claims. The adapter reports email_verified: false; a configured email address is not proof of verified ownership.

Consent is required by default. skip_consent yes preapproves a trusted client for its registered scopes; it does not override an explicit prompt=consent. prompt=none returns a protocol error when login or consent is needed. offline_access requires explicit fresh consent and returns a rotating OIDC refresh credential. It is independent of portal refresh.

Public clients set token_endpoint_auth_method none, omit the secret, and must use S256 PKCE. A native client may register an HTTP callback on literal 127.0.0.1 or [::1]; only its valid port may vary at authorization. Redeem the code with the actual authorized URI, including that port. localhost, private mobile schemes and arbitrary callback origins do not gain this exception.

The provider supports code responses using query or form-post, scope-limited claims, authentication-context mappings, and by-value Request Objects using none or registered RS256 keys. Remote request URIs, encrypted Request Objects and advertised RP-initiated logout are not supported. Available protocol support is not a claim of OpenID Foundation certification.

Lifetime, logout, and key rotation​

Default browser sessions last 28800 seconds; ID/access grants last at most 300 seconds and are bounded by the session's remaining lifetime. Codes last 60 seconds. OIDC refresh has its own lifetime and capacity. Current account and credential evidence are checked on authorization, redemption and UserInfo; account disablement or changed security state can invalidate credentials.

Portal logout revokes the OIDC browser session and dependent opaque grants. Already issued ID tokens remain signed statements until expiry; relying parties must end their own application sessions. Refresh/code replay revokes dependent token authority rather than permitting another successful exchange.

State is volatile unless persistence is enabled. Persistence retains completed sessions and grant/replay history, with one active owner. Pending interactive requests restart. Browser affinity alone does not route a relying party's backchannel exchange to the correct process.

For key rotation, list the new private key first and retain old keys later until relying parties can retire old ID tokens. The first key signs; all listed public keys are published. Keep the issuer stable.

Verify the integration​

Test discovery, actual browser login and consent, code/PKCE exchange, independent ID-token verification and UserInfo. Then test an unregistered callback, wrong verifier, code replay, changed account and logout. Confirm that the callback reaches the relying party and all issuer paths reach the unstripped portal mount.