Encedo OIDC Provider

HSM-anchored · OpenID Connect Core 1.0 · self-hosted

Identity nobody can steal.
Not even the provider.

Every other identity provider holds the secret that proves who you are. This one holds nothing. Your key is generated inside your Encedo HEM and never leaves it — the server assembles the token, your hardware signs it, and the server is left able to verify a signature it cannot produce.

What the server builds
What your module returns
What the server does with it
What the server can sign alone

Four claims, and what backs each one

The server cannot sign for you

The backend builds signing_input — header and claims, exactly as OIDC requires — and hands it to the browser. The module signs it. The backend verifies the signature and assembles the token. There is no code path in which it produces one, and no configuration that adds one.

Structural, not a policy setting

A database breach exposes nothing usable

No passwords, no hashes, no session secrets, no MFA seeds, no private keys. What is stored per user is a public key, a device certificate and display claims — public by definition, and useless without the hardware that holds the other half. Even email_verified is earned, not typed: it is set only when the enrollment link that reached the mailbox is the one that was used.

Redis only · no SQL, no credential store

No login without the physical device

Every authentication ends at the module, which has to answer — approved on your phone or unlocked with a passphrase. A compromised server can refuse a login. It cannot grant one.

Pending session 120 s · auth code 60 s · PKCE S256

Standard OIDC, no cloud dependency

Discovery, JWKS, UserInfo, RP-initiated logout, PKCE — a relying party integrates the way it integrates anything else, and reads auth_time and amr to learn how the user was authenticated. Nothing phones home: the only outbound call in normal operation is to your own module, on your own network.

Runs air-gapped · Node.js 22 + Redis · tagged releases

OIDC is a standard, so nothing here is written for one application: whatever reads discovery integrates the same way. Tested with Nextcloud, Carbonio CE and GitLab, which authenticate against it in daily use at RKV. Read the security model →

The obvious objection, answered

The device on every login?
Only on the first one.

After one confirmation on the module, your browser keeps the device's short-lived authorization — in that browser, and nowhere else. The next application is one click: the device still signs, the server still verifies, and the server still holds nothing it could reuse. Single sign-on without a session on the server to steal.

Every application learns what happened. The ID Token says when the device was confirmed and whether this sign-in reused it, so a sensitive system can insist on a fresh confirmation while the rest ride on the first one.

auth_time · amr = [hwk] or [hwk, sso] 8 h by default, chosen on the device

  • Who can say no
  • The operator — on or off for the whole provider, and a ceiling on how old an authorization may be reused.
  • Each application — the payroll system asks for the device every time, the wiki does not.
  • Each user — excluded for shared workstations or privileged accounts.
  • The user — decides at sign-in whether this browser remembers at all, and picks the lifetime on the phone.
  • The application, at runtime — standard prompt=login and max_age force a fresh confirmation.
  • Signing out — leaving one application asks whether to end the browser's session too. Nothing happens silently.

What this costs you

Every user needs an Encedo HEM
There is no password to fall back on and no recovery without the device. A lost or replaced module means a new enrollment, which only an administrator can start.
The ID Token is the user's signature, not the provider's
There is no provider key. A token that verifies against jwks_uri proves that the user's module signed it, and the user could sign any payload offline. Trust an ID Token only as the response of POST /token; never accept one from another channel as a bearer credential.
No RS256
The module signs with EdDSA and ES256/384/512. OIDC Core lists RS256 as mandatory for a provider; a relying party wired to it has to be configured for one of these instead.
One key per user in the JWKS
A relying party that caches the JWKS and matches kid strictly will not know a newly enrolled user until its cache expires, typically an hour.
Attestation is recorded, not enforced
At enrollment the device certificate is checked against Encedo's attestation service and the outcome is stored as hw_attested. A failed check is logged, not refused; a system that needs a genuine module has to read the flag. Every item above, in full →

Your own infrastructure

Three commands. One small VM. No SQL, no cloud.

Do not take our word for any of the above — run the provider yourself. It is one Node.js process with a single dependency, and the whole of its state fits in Redis.

# any small Linux box, Node 22+ and Redis 7+ — or the release ZIP
$ git clone --recurse-submodules https://github.com/encedo/encedo-oidc.git && cd encedo-oidc
$ npm ci && cp .env.example .env   # set ISSUER and ADMIN_SECRET
$ npm start
1 process 1 dependency (Redis) 0 secrets at rest Docker multi-tenant template included backup / restore in one command each tagged releases, tests gate every one MIT licence