Docs / Reference

Security and privacy notes

What signo stores

Everything lives in one SQLite file, signo.db, in DATA_DIR.

Table What it holds How long
users a random ID (the sub apps receive), email, optional name, when the account was created until the account is deleted
passkeys for each passkey: its credential ID, public key, signature counter, transport hints, when it was added and last used until removed or the account is deleted
magic_links for each pending email link: a SHA-256 of its token (never the token), the email, the sign-in it belongs to, its expiry until used, or 15 minutes
oidc_store oidc-provider's records: sessions, consents, sign-ins in progress, authorization codes, access tokens and single-page apps' refresh tokens, plus the registered apps; apps added on the sites page also carry their owner's account ID sessions 14 days, consents 1 year, sign-ins 1 hour, codes 1 minute, access tokens 10 minutes, refresh tokens 14 days at most; apps until removed, or until their owner's account is deleted

Expired rows are deleted every 10 minutes. keys.json beside the database holds the ID-token signing key and the cookie-signing key (see Keys below).

signo stores no passwords. It writes no request logs, no IP addresses and no user agents, and pages load no third-party scripts, fonts or analytics. Rate-limit counters are the only place IP addresses appear; they live in memory and are dropped when their window ends. When SMTP_URL is not set, sign-in links are printed to the console instead of emailed, together with the address they are for; set it in production.

Cookies

Cookie Purpose Lifetime
signo_session who is signed in 14 days
_interaction, _interaction_resume tie the browser to a sign-in in progress only during a sign-in, at most 1 hour

Each has a .sig companion that signs it, as oidc-provider recommends. All are HttpOnly and SameSite=Lax, and Secure over HTTPS. There are no other cookies.

Signing in and recovery

  • Accounts are created only after the person opens a sign-in link sent to their email. Apps therefore always receive email_verified: true, and nobody can create an account in someone else's name.
  • Passkeys are the main way to sign in. They are WebAuthn credentials bound to signo's domain, so another site cannot use them.
  • Email links work once, expire after 15 minutes, and finish the sign-in only in the browser that asked for them. Opening a link shows a "Continue" button and only that button uses the token, so email scanners that open links can't use it up.
  • Recovery: someone who has lost their passkeys asks for an email link, signs in, and adds a passkey on their new device.

Protocol settings

  • Authorization code flow only, with PKCE (S256) required for every app.
  • ID tokens are signed with RS256 and carry email and name, as Google's do. Access tokens last 10 minutes.
  • Redirect URIs must use https; http is allowed only for localhost.
  • Single-page apps get refresh tokens bound to a key their browser keeps (DPoP, RFC 9449), so a copied token is useless. They last at most 14 days from sign-in, end when the person signs out of signo in a browser where they used the app, and aren't replaced when used. A DPoP proof whose time is more than five minutes off gets a nonce from signo to sign instead, so a device with a wrong clock can still sign in.
  • Revoking tokens never removes consent: when someone signs out, or an authorization code is used twice, the tokens go and the consent stays until the person withdraws it.
  • Browsers may call the token endpoint only for single-page apps, which have no secret, and only from the origin of one of the app's callback URLs. Any site's page may call userinfo, from the same origins.
  • Consent is remembered per account and app for up to a year. Signing out ends the session and revokes the tokens of every site used in it, in all of the person's browsers, but keeps the consent; deleting the account removes it.

Sites added by account holders

  • Any account can add up to 10 sites at /account/sites. Only the account that added a site can see it, give it a new secret or remove it.
  • A site added as a single-page app gets no secret, since anyone can read its code. It signs in with PKCE, and its token requests must carry a DPoP proof.
  • The client secret is shown once, when the site is added or given a new secret. signo stores it as given, because oidc-provider compares it on every token request. A new secret replaces the old one at once.
  • Anyone can give a site any name, so the consent screen names the domain the person returns to before listing what the site receives.
  • Callback URLs follow the same rules as apps added with the CLI: https only, except for localhost.
  • Cross-site request forgery: a POST to these pages is refused unless the browser reports it as same-origin (Sec-Fetch-Site, or Origin on browsers without it).
  • A Content-Security-Policy allows scripts only from signo itself and forbids other sites from framing the pages, which blocks clickjacking of the sign-in and consent screens.
  • They are sent with Cache-Control: no-store.
  • The sign-out confirmation is oidc-provider's own form, protected by its own CSRF token.

Rate limits

What Limit
Email links to one address 5 per 15 minutes
Email links from one IP address 30 per 15 minutes
Sign-in attempts from one IP address 60 per 15 minutes
Token requests from one IP address 600 per minute

Counters are kept per signo process and reset on restart. Behind a proxy, the IP address comes from X-Forwarded-For when TRUST_PROXY is set, or from the header named in CLIENT_IP_HEADER for proxies that use their own.

Deleting an account

Deleting an account removes its row in users, its passkeys, its pending email links, all of its sessions, consents, codes and tokens, and the sites it added. SQLite's secure_delete setting overwrites the freed space in the database file. A recently deleted row can survive briefly in SQLite's write-ahead log (signo.db-wal) until the next checkpoint, and in any backups you keep. Apps the person signed in to keep whatever they saved at the time.

Keys

The signing key and cookie key are created on first start and kept in DATA_DIR/keys.json, readable only by signo's user. Back the file up together with the database. If it is lost, everyone is signed out and apps fetch the new public key from /jwks on their next sign-in. v0.1 does not rotate keys automatically.

Moving to a new domain

Passkeys belong to the domain signo runs on. If you move signo to a new domain, people sign in once with an email link and add a passkey again, and each app must be given the new issuer URL.