Arrow keys to move, Enter to open, Escape to close.

  1. Docs
  2. EAuth
  3. Security

Security

Built against RFC 9700, the OAuth 2.0 security best current practice from January 2025, and the March 2026 update draft.

What is not implemented

Three things are absent rather than disabled, so no configuration can bring them back:

  • The implicit grant. Deprecated. It returns tokens in a URL fragment, where they land in browser history and referrer headers.
  • The password grant. Deprecated. It requires your application to handle the user's password, which defeats the purpose of a redirect flow.
  • PKCE method plain. Permitted by RFC 7636, but it offers no protection when the request can be observed, which is the case it exists to cover.

What is enforced

PKCE with S256, always. There is no code path that issues a token without it, on public and confidential clients alike.

Exact redirect URI matching, compared in constant time. No wildcards, no prefixes, no normalisation. Every relaxation of this rule has produced a real account takeover somewhere.

Authorization codes live sixty seconds, are single use, and are bound to the client, the redirect URI and the PKCE challenge. Presenting one twice revokes every token issued from it, because a replay means somebody else saw it.

Refresh tokens rotate on every use. Presenting an already-rotated token revokes the whole chain. The legitimate client and a thief cannot both hold the newest token, so an old one arriving is evidence of a leak.

The iss parameter on every authorization response, per RFC 9207.

Limits on every door, failing closed. Every attempt is counted before it is checked, so a burst of parallel requests gets no more guesses than a slow one. Sign-in is counted per address and per account: five wrong passwords for one account from one address, or twenty from anywhere, pause it for up to fifteen minutes. The second step allows five wrong codes in fifteen minutes and twenty in a day, per account. A browser that has signed in to an account before keeps a small allowance of its own while the account is paused for everybody else, so guessing cannot lock the owner out. /authorize takes sixty requests a minute per address, /token the limit in your application's settings (120 a minute by default, up to 600 there, more on request), counted only once your client has authenticated; for a public client, which has no secret, per address as well. If the counter cannot be reached, these refuse rather than let requests through. A refusal is a 429 with Retry-After; the token endpoint answers it as temporarily_unavailable.

No page says whether an address has an account. A wrong address and a wrong password get the same message at sign-in, a password reset answers the same either way, and so does registration: both cases show a page saying a mail is on its way, and only the mail, to the address itself, says whether it confirms a new account or that the address had one already.

Registering somebody else's address gets nothing. Until an address is confirmed, its account's password, passkeys and two-factor were chosen by whoever filled in the form, who need not own the address. Confirming in the browser that registered, or in one signed in to the account, keeps them. Confirming anywhere else asks the person who opened the mail to choose the password, removes everything set up before and signs out every device, so nobody who registered an address they do not read can sign in once its owner confirms it. A password reset of an unconfirmed account does the same. Until it is confirmed, an account can sign in to its account page, to send a new link, and no application gets a token for it.

The console and the dashboard ask for a second factor. Whoever can change an application, its secrets or its API keys signs in with an authenticator app as well as a password or passkey. An account without one is taken to set it up first, and comes back to the console when it is done.

Every form carries a token bound to the session, and must come from the sign-in host itself: the consent screen, the account page, sign-out.

Sign-out needs proof. end_session_endpoint is OpenID Connect RP-Initiated Logout. Send the ID token you hold as id_token_hint, with client_id and a registered post_logout_redirect_uri, and the session ends and the person returns to you with your state. Without a hint that names the signed-in person, EAuth asks them first, so no page can sign anybody out with a link, and afterwards it shows its own sign-in page rather than an address nobody vouched for.

How credentials are stored

Value Stored as
Passwords Argon2id, 64 MiB, t=3, p=2
Refresh tokens, codes, recovery codes SHA-256 digest only
Client secrets SHA-256 digest only
Invitation, confirmation and reset links SHA-256 digest only
Token signing keys AES-256-GCM, key held outside the database

Nothing stored as a digest can be read back, including by us. A client secret shown once at creation is genuinely the only time it exists in readable form. Signing keys are decrypted only in the server's memory, to sign; a copy of the database holds them as ciphertext.

How long things are kept

Data Kept
An account, its password hash, name and second factor Until it is deleted: 30 days after its owner asks, from the account page, with everything that is only its own. An account not signed in to for 24 months is written to twice, 30 days apart, and deleted 30 days after the second mail unless it signs in
Sessions and refresh tokens, each with the browser and platform it was started from ("Firefox on Windows", never the User-Agent header itself) and the country Until they expire; a revoked one until it would have expired, so a stolen copy presented later is recognised and ends the rest
Authorization codes An hour past their 60 seconds
Confirmation and reset links A week after they were used or expired
Invitations 30 days after they were accepted or expired
The token-request log, with the country and time of each sign-in 12 months
The audit log 12 months
Webhook deliveries, with their payloads 30 days, the time they can be replayed

A sweep in every server removes what is due once an hour.

What we have not done

No independent security audit has been performed. We state this in the terms as well, because the alternative is letting you assume otherwise. When one is completed, the result is published here regardless of outcome.

Reporting a vulnerability

Write to security@elchi.dev, in German or English. Every host under elchi.dev names the same address in its /.well-known/security.txt.

A report we can act on says where, meaning the address or the endpoint, what you did, what you saw, and what somebody could do with it. Steps we can repeat are worth more than the output of a scanner.

What you can expect from us:

  • A reply within three working days, from a person who has read the report.
  • Once we can reproduce it, the date we expect it fixed, and word when the fix is live.
  • No legal action against research that keeps to the rules below.

The rules:

  • Use your own accounts, and applications you registered yourself. The console lets anyone register one.
  • Do not read, change or delete data that is not yours. If you reach some, stop there and tell us what you saw.
  • No load tests, no denial of service, no spam, no social engineering of our team, no physical access.
  • Give us time before you publish: ninety days, or less once the fix is live and we have agreed a date.

There is no reward programme. In scope is every host under elchi.dev that answers with a security.txt.

7 min read