Overview
pykrete is a self-hosted identity provider. Its core is an OpenID Connect provider and OAuth 2.0 authorization server. Around that it adds passkey sign-in, an account dashboard, self-service organizations, applications and APIs, a read-only LDAP server with per-app passwords, end-to-end encryption keys for apps, and HTTP hooks for custom logic.
Generated codes and secrets carry a DELTA- prefix (DELTA-CODE-..., DELTA-CLIENT-SECRET-...,
DELTA-API-KEY-...), named after the first deployment, delta.rocks.
Features
| Area | What you get | Details |
|---|---|---|
OpenID Connect / OAuth 2.0 |
Discovery, authorization code with PKCE ( |
|
Sign-in |
Username plus password, or passkeys (WebAuthn with user verification required; discoverable passkeys allow sign-in without typing a username). Every sign-in session starts with a small proof-of-work challenge solved by the browser. There is no public sign-up: an administrator creates accounts. Optional second factor after a password: authenticator app (TOTP), YubiKey (Yubico OTP, checked locally or with YubiCloud) or recovery codes. |
|
Scopes, roles, consent |
Built-in OIDC scopes, per-API scopes, roles that grant scopes, stored consent, pairwise or public subject identifiers. |
|
Account dashboard |
|
|
Self-service |
Users and organizations own clients (applications), audiences (APIs) and roles, and manage them from the
dashboard or the |
|
Organizations |
Members, organization roles, invitations, per-application access, an active organization in tokens. |
|
HTTP hooks |
The backend calls |
|
LDAP |
Optional LDAPS listener with a read-only directory per application and per-app passwords. |
|
App encryption keys |
Per-user key material unlocked in the browser by a password or a passkey (PRF); apps receive keys through
|
|
Backend API |
Clerk-style REST subset under |
|
Admin API |
GraphQL at |
|
SAML |
Not supported. |
|
Forward / proxy auth |
Not supported. |
|
Email delivery |
Built-in SMTP for verification and invitation emails. The frontend renders the content, so templates can be customized. |
Components
| Component | Runs as | Role |
|---|---|---|
Backend |
|
Listens on |
freshfront |
Deno server from |
Renders all HTML: |
PostgreSQL |
database server |
All state: users, credentials, sessions, clients, audiences, tokens, organizations, API keys, images. |
PKCS#11 token |
SoftHSM2, a hardware HSM or a smartcard |
The token signing keys and the refresh-token encryption key are generated and used inside the token; the backend talks to it through a PKCS#11 module. Required. |
LDAP listener |
part of the |
Optional LDAPS server, enabled by the |
Who handles which path
The backend is the only entry point. Put the TLS terminator in front of it and nothing else.
- Handled by the backend
-
/.well-known/openid-configuration,/.well-known/jwks.json,/jwks,/oauth/token,/oauth/introspect,/oauth/revoke,/device-auth,/register,/register/{client_id},/userinfo,/verify,/control(WebSocket),/graphql,/openapi.json,/api/v1/...,/images/{id},/resources/...,/proxy-auth,/api/v2/jobs/users-exports(not available yet). - Proxied to freshfront
-
Everything else, including
/authorize,/oauth/logout,/code,/account/...,/graphiqland/docs/.... Request and response bodies are streamed; WebSocket upgrades are passed through. - Blocked at the backend
-
/hooksand/hooks/...return 404 at the backend (checked after path normalization, so/./hooksor/x/../hooksdo not slip through). The backend itself POSTs hook calls to freshfront over the same upstream connection, withAuthorization: Bearer <hooks_secret>.
GET /graphql redirects to /graphiql. Responses generated by the backend get a strict
Content-Security-Policy: default-src 'none'; ...; proxied freshfront responses keep freshfront's own CSP.
Request flow
A relying party signing a user in with the authorization code flow:
-
The app sends the browser to
/authorize. The backend has no route for it, so freshfront renders the page. -
The page opens the
/controlWebSocket on the backend and sends the authorization request plus the session cookie. All sign-in logic runs on that socket: proof-of-work, identification (username or discoverable passkey), credential check, consent, organization choice. The backend sends each step as a message and the page renders the matching form. -
When done, the backend tells the page to navigate to the app's
redirect_uriwith the code. -
The app exchanges the code at
/oauth/token. Theclaimshook may add claims at this point. -
The app validates the ID token against
/jwks(or/.well-known/jwks.json) and may call/userinfo.
Dashboard pages work the same way: /account is itself an OIDC client (see
startup). freshfront signs the user in with PKCE, keeps the access
token in an HttpOnly cookie scoped to /account, and renders pages by calling the backend's /graphql with
it.
Glossary
- Issuer
-
https://<domain>/(orhttp://withouttls_terminated), with a trailing slash; on this serverhttps://auth.infra.delta.rocks/. All endpoint URLs in discovery are built from thedomainconfig key, not from the request'sHostheader. - User
-
A person. Id
Uplus 32 alphanumerics. Has a username history (sign-in names), a profile (name, picture), emails (verified or not), credentials, public and private metadata, and roles. - Credential
-
What a user proves at sign-in. Stored as "challenges":
password,webauthn(passkey) andpassword_pending(a one-time registration code that the user turns into a password). - Session
-
Browser sign-in state. The browser keeps a cookie encrypted with a key derived from
session_secret; it holds a random session id that the database maps to the user and the credentials already passed. Listed at/account/sessions. - Client (application)
-
An OAuth/OIDC client. Id
Cplus 32 alphanumerics. Public or confidential, with optional secret, PKCE, redirect URLs, post-logout redirect URLs, allowed grant types, allowed and required scopes, subject type and organization usage. Owned by a user, an organization, or nobody (system clients). - Audience (API)
-
A resource server that access tokens are issued for. Id
Aplus 32 alphanumerics, plus a unique identifier (usually a URL). Defines its own scopes and the access token signature algorithm (RS256orES256). The server itself is the "self" audience; its identifier is the issuer and it carries theadmin:graphql,accountandorg:*scopes. - Scope
-
A permission requested in
scope. Built-in scopes areopenid,offline_access,profile,emailandorganizations; audiences add their own;app_key:<id>scopes request encryption keys. - Role
-
A named set of audience scopes. Granted globally (for example
role:admin) or inside an organization (role:org-admin,role:org-member, or roles owned by the organization). A token only gets an audience scope if one of the user's roles grants it (the self audience'saccountscope is open to every user) and the client is allowed to request it. - Organization
-
A group of users. Id
Oplus 32 alphanumerics, plus a unique slug. Members hold organization roles. Clients opt in to organizations, and a token can carry one active organization. - Consent (grant)
-
A user's stored approval for a client to use a set of scopes. Asked for on first use and listed at
/account/applications. - Access token
-
A JWT (
RS256orES256, chosen by the audience) withiss,sub,aud,azp,scope,iat,exp. Valid for one hour. Also recorded in the database, which is what/userinfo,/graphql, introspection and revocation check. - ID token
-
A JWT for the client (
audis the client id), issued whenopenidis requested. Signed withRS256orES256as configured on the client. - Refresh token
-
Issued with
offline_access. An encrypted JWE that rotates on every use; replaying an old one revokes the whole chain. - Hook
-
An HTTP callback from the backend to freshfront (
POST /hooks/<name>) for custom claims, email delivery and similar logic.