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 (S256, plain), refresh tokens, device authorization (RFC 8628, partial), userinfo, JWKS, RP-initiated logout, introspection (RFC 7662), revocation (RFC 7009), dynamic client registration (RFC 7591). The implicit, hybrid and client_credentials grants are not supported.

OpenID Connect

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.

Two-factor authentication

Scopes, roles, consent

Built-in OIDC scopes, per-API scopes, roles that grant scopes, stored consent, pairwise or public subject identifiers.

Scopes and claims

Account dashboard

/account: profile, emails, sign-in methods, sessions, pending sign-ins, organizations, applications, access tokens, app passwords, API keys, E2EE keys, roles, and the clients and audiences you own.

Account dashboard

Self-service

Users and organizations own clients (applications), audiences (APIs) and roles, and manage them from the dashboard or the manage GraphQL API.

Self-service

Organizations

Members, organization roles, invitations, per-application access, an active organization in tokens.

Organizations

HTTP hooks

The backend calls identity, claims and the email rendering hooks implemented by the frontend.

Hooks

LDAP

Optional LDAPS listener with a read-only directory per application and per-app passwords.

LDAP

App encryption keys

Per-user key material unlocked in the browser by a password or a passkey (PRF); apps receive keys through app_key:<id> scopes.

App keys

Backend API

Clerk-style REST subset under /api/v1, API keys.

Backend API

Admin API

GraphQL at /graphql, an in-browser console at /graphiql, "admin mode" in the dashboard.

Administration

SAML

Not supported.

SAML and proxy auth

Forward / proxy auth

Not supported.

SAML and proxy auth

Email delivery

Built-in SMTP for verification and invitation emails. The frontend renders the content, so templates can be customized.

Configuration, Hooks

Components

Component Runs as Role

Backend

pykrete binary

Listens on 127.0.0.1:3000 by default (plain HTTP). Serves the OIDC/OAuth endpoints, JWKS, /graphql, /api/v1 and the /control WebSocket that drives sign-in, consent and logout. Proxies every other path to freshfront. Calls hooks. Runs the optional LDAP listener in the same process.

freshfront

Deno server from freshfront/ (Preact SSR)

Renders all HTML: /authorize, /code, /oauth/logout, /v2/logout, /invite, /add-passkey, /account/..., /graphiql, /docs/.... Implements the hooks under /hooks/.... Calls the backend over HTTP for GraphQL and token exchange. It is an example frontend you are expected to adapt (freshfront/README.md).

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 pykrete process

Optional LDAPS server, enabled by the ldap config section.

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/..., /graphiql and /docs/.... Request and response bodies are streamed; WebSocket upgrades are passed through.

Blocked at the backend

/hooks and /hooks/... return 404 at the backend (checked after path normalization, so /./hooks or /x/../hooks do not slip through). The backend itself POSTs hook calls to freshfront over the same upstream connection, with Authorization: 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:

PostgreSQL freshfront Backend App Browser PostgreSQL freshfront Backend App Browser open app redirect to /authorize?client_id=... GET /authorize proxy (path not handled by backend) sign-in page WebSocket /control with query and session cookie proof-of-work, username, passkey or password, consent session, credentials, consent, authorization code POST /hooks/identity (if hooks enabled) FinishedRedirect to redirect_uri?code=... GET redirect_uri?code=... POST /oauth/token POST /hooks/claims (if hooks enabled) access_token, id_token, refresh_token GET /userinfo, or verify JWTs with /jwks
  1. The app sends the browser to /authorize. The backend has no route for it, so freshfront renders the page.

  2. The page opens the /control WebSocket 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.

  3. When done, the backend tells the page to navigate to the app's redirect_uri with the code.

  4. The app exchanges the code at /oauth/token. The claims hook may add claims at this point.

  5. 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>/ (or http:// without tls_terminated), with a trailing slash; on this server https://auth.infra.delta.rocks/. All endpoint URLs in discovery are built from the domain config key, not from the request's Host header.

User

A person. Id U plus 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) and password_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 C plus 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 A plus 32 alphanumerics, plus a unique identifier (usually a URL). Defines its own scopes and the access token signature algorithm (RS256 or ES256). The server itself is the "self" audience; its identifier is the issuer and it carries the admin:graphql, account and org:* scopes.

Scope

A permission requested in scope. Built-in scopes are openid, offline_access, profile, email and organizations; 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's account scope is open to every user) and the client is allowed to request it.

Organization

A group of users. Id O plus 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 (RS256 or ES256, chosen by the audience) with iss, 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 (aud is the client id), issued when openid is requested. Signed with RS256 or ES256 as 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.