Identity your application owns. Not a server you have to trust.

SESAME is a headless authentication and authorization engine. It opens no port and ships no UI — your application owns every listener and drives the engine over a versioned protocol on stdin and stdout. Sessions, tokens, policy, and audit stay inside your own stack, in any of 10 languages.

go install github.com/d31ma/sesame/cmd/sesame@latest copy
✓ No network listener ✓ One direct dependency ✓ Default deny, always ✓ Apache-2.0
84
protocol operations
10
language SDKs
1
direct dependency
0
ports opened
What it does

One place for who, what, and why.

Principals, credentials, sessions, tokens, policy, and audit — with the decision boundary in the same engine as the identities it decides about.

Authenticate

Passkeys first, passwords properly

WebAuthn passkeys with sign-counter clone detection, Argon2id passwords with a parameter-upgrade path, TOTP, and single-use recovery codes. A code is spent durably, so a replay fails inside its own window and across a restart.

Authorize

Decisions you can explain

Default deny over roles, grants, groups, and bounded context conditions. Every decision returns a stable reason and the policy version it was made under, so an audit answers "why" and not just "no".

Single sign-on

OIDC provider, with PKCE mandatory

Authorization code flow, rotating refresh families that revoke the whole family on reuse, discovery, introspection, revocation, consent, and RP-initiated logout. There is no grant-types field, so implicit and password can never be switched on.

Federate

Inbound OIDC and SAML 2.0

Bring identities from an existing provider. SAML verification refuses every ambiguous document rather than choosing between readings, which is what defeats signature wrapping.

Provision

SCIM 2.0 users and groups

A directory can create, patch, and deprovision principals and group membership. Reconciliation is idempotent, because directories re-send full state rather than diffs.

Account for

A ledger, not a log file

Every security-relevant change is a hash-chained event on FYLO. Projections are rebuildable, snapshots are verified, and revocation survives a restart with a defined propagation bound.

How it fits

A subprocess, not a service.

Most identity servers ask you to run one more thing on the network and trust it with your users. SESAME is a binary your application starts. It has no listener to expose, no admin console to leak, and no origin to be confused about.

Your application Owns every listener, TLS boundary, route, and middleware chain. Nothing about that changes.
SDK shim One dependency-free file. Owns the subprocess, timeouts, cancellation, and typed errors — never a security decision.
SESAME engine NDJSON on stdin and stdout. Every protocol decision, every policy evaluation, every audit record.
FYLO Documents are authoritative. Indexes, caches, and projections are derived and rebuildable.

The architecture is enforced, not documented: a test inspects the linked dependency graph of the shipped binary and fails the build if an HTTP package, a template engine, or an unexpected module ever appears in it.

In your language

Ten languages, one engine.

Copy one file into your project. It spawns the engine, speaks the protocol, and gets out of the way. There is no registry to depend on and no lockfile entry to resolve — and with SESAME_DEPLOYMENT set, no path in your code either.

// SESAME_BINARY and SESAME_DEPLOYMENT come from the environment.
client, err := sesame.Start(ctx, sesame.Options{})
if err != nil {
    log.Fatal(err)
}
defer client.Close()

decision, err := client.Decide(ctx, sesame.DecisionRequest{
    TenantID:    tenantID,
    PrincipalID: principalID,
    Action:      "doc:read",
    Resource:    "project:alpha",
}, nil)

fmt.Println(decision.Decision, decision.ReasonCode)
// deny deny_no_grant
Features

Everything it does. And everything it does not.

A security project that overstates itself is worse than one that ships less. On the left is what exists — each of them with code, negative-path tests, operator documentation, and recovery evidence behind it. On the right is what does not exist yet, written down here rather than left for you to find out in production.

OAuth 2.0 / OIDC provider

Authorization code flow with mandatory PKCE, rotating refresh families with reuse detection, discovery, introspection, revocation, consent, and RP-initiated logout.

Device grant, PAR, and DPoP

RFC 8628 for inputs a browser cannot reach, RFC 9126 to move the authorization request onto an authenticated back channel, and RFC 9449 to bind an access token to a key its holder proves per request.

Authentication

Argon2id passwords with a parameter-upgrade path, TOTP with durable replay prevention, single-use recovery codes, and WebAuthn passkeys with clone detection.

Authorization

Deterministic default-deny decisions over roles, grants, groups, and bounded context conditions, with stable reasons and a versioned policy snapshot.

Federation and provisioning

Inbound OIDC federation, SCIM 2.0 users and groups, and inbound SAML 2.0 with an in-tree XML canonicalizer that refuses every ambiguous document.

Durability

A hash-chained security ledger on FYLO with rebuildable projections, verified snapshots, and restart evidence run against a real FYLO runtime.

Ten drop-in clients

One dependency-free file per language, each holding the whole operation surface. A contract test resolves every operation against every shim, so a capability cannot ship in one language and quietly miss another.

OpenID certification

The provider surface is driven over real TLS by a relying party in test/interop — discovery, PKCE, ID-token claims, single-use codes. Certification is self-certification against the OpenID Foundation suite, and that submission has not been made, so SESAME is not certified.

SAML interoperability

Proven end to end against a real Keycloak in test/interop — which is how a namespace defect no fixture had ever produced was found and fixed. Okta, Entra ID, Google Workspace, and Shibboleth are still unproven; treat any provider not listed as untested.

Production support

This is a developer preview. A repeatable restore, upgrade/rollback, and soak evidence runner exists, but no release has passed its 72-hour native gate or an independent security review. No version is supported for production use.

Every attack case in SESAME's adversarial suite runs against a real compiled binary over the shipped protocol, against a real deployment with real keys — because a defence that only exists inside a package boundary is not a defence.

Read the code before you trust it.

SESAME is Apache-2.0 and every security claim on this page points at a test you can run. Start with the project plan — it records what is proven, what is deliberately not, and why.

← Docs Guide

MFA and step-up

A second factor attaches to the same authentication transaction a password does, and raises the session's assurance from password to mfa. Authorization policy can then require that level for individual actions.

TOTP

Enrolment is two steps on purpose. totp_enroll issues the shared secret; the factor does not exist until totp_activate proves an authenticator app actually holds it. A half-finished enrolment therefore cannot lock anyone out.

# 1. Enrol. The secret is returned once; render the URI as a QR code.
enrolled = client.totp_enroll(principal_id, "Example App")
# {"secret": "...", "provisioning_uri": "otpauth://totp/Example%20App:..."}

# 2. Activate by proving an authenticator app holds it. Until this
# succeeds the factor does not exist, so a half-finished enrolment
# cannot lock anyone out.
client.totp_activate(principal_id, code_from_the_app)

# 3. At login, after the password.
tx = client.authn_begin(tenant_id, "email", "alice@example.com")
client.authn_verify_password(tx["transaction_id"], password)
state = client.authn_verify_totp(tx["transaction_id"], code_from_the_app)
# {"state": "awaiting_factor", "assurance": "mfa", "attempts_left": 3}

# The session completed from this transaction carries assurance "mfa".
A code is spent when it is used — including by activation.

A successful code durably consumes its 30-second time step. That is what makes replay fail inside the code's own window and across a restart, and it has one consequence worth knowing while you are building: the code you activate with cannot then be used to log in until the next window. In a test, wait for the next time step rather than reusing the code — the second attempt is refused and assurance stays at password.

The shared secret has to be readable to compute the expected code, so unlike a password it cannot be a one-way hash. It is sealed with AES-256-GCM under the deployment's secrets.key, which lives outside every FYLO document. Without that key the engine fails closed rather than falling back to plaintext.

Recovery codes

Ten single-use codes, shown once, stored only as SHA-256 digests. Reissuing retires the previous set, so a leaked list is revoked by handing out a new one rather than by deleting anything.

# Ten single-use codes, returned once, stored only as digests.
# Reissuing retires the previous set, so a leaked list is revoked
# by handing out a new one rather than by deleting anything.
issued = client.recovery_codes_issue(principal_id)

# At login, in place of the second factor.
tx = client.authn_begin(tenant_id, "email", "alice@example.com")
client.authn_verify_password(tx["transaction_id"], password)
state = client.authn_verify_recovery_code(tx["transaction_id"], issued["codes"][0])
# {"assurance": "mfa", "attempts_left": 3}

# That code is now spent — durably, so a restart cannot revive it.

A recovery code is a second factor, not a first one. Presenting one before any first factor is refused — and the refusal does not spend the code, so an attacker cannot burn somebody's recovery set by guessing at it.

Passkeys

WebAuthn passkeys register against a principal and are verified against a challenge carried on the transaction. A passkey with user verification satisfies mfa on its own, with no prior factor — the authenticator already proved possession and user presence.

# Registration: the challenge is issued by the engine, never by the browser.
options = client.passkey_register_begin(principal_id)
credential = client.passkey_register_finish(
    principal_id, attestation_object, client_data_json)

# Login: the challenge lives on the transaction, so it cannot be replayed
# against a different attempt.
tx = client.authn_begin(tenant_id, "email", "alice@example.com")
options = client.passkey_options(tx["transaction_id"])
state = client.authn_verify_passkey(
    tx["transaction_id"], credential_id,
    authenticator_data, client_data_json, signature)

Only none attestation and ES256 are accepted, and the sign counter is checked on every assertion: a counter that goes backwards indicates a cloned authenticator and is refused.

Requiring a second factor for one action

Assurance is not just recorded — it is enforceable. Attach a condition on session.assurance to a permission, then pass the session rather than the principal when you ask.

# Pass the session instead of a principal. The engine verifies it and
# derives context under the reserved "session." prefix, so a caller cannot
# assert its own assurance level.
decision = client.decide({
    "tenant_id": tenant_id,
    "session_id": session["session_id"],
    "session_secret": session["session_secret"],
    "action": "billing:change",
    "resource": "account:acme",
})
# A permission conditioned on session.assurance == "mfa" denies a
# password-only session with reason_code "deny_no_grant".

The engine verifies the session and derives the context itself under the reserved session. prefix. A caller-supplied key with that prefix is rejected, not ignored — that refusal is what makes the condition worth trusting. An unusable session denies with deny_session_invalid rather than falling back to a lower assurance.

What to read next