Skip to main content
AwaitIO has no database and no sign-up. The people who can log in are listed in code when you create the app, each with an email and a password hash. They log in on the AwaitIO login page with that email and password, and the server keeps them logged in with a signed cookie.

Setup

Pass a secret and a list of users to new AwaitIO():
app.ts
The app refuses to start when the secret is too short, an email is listed twice or a hash is not one from Create new account.
The hashes belong in your code, next to the users they log in: a hash cannot be turned back into the password. The secret does not: keep it in an environment variable or your secret manager, never in the repository.

Generate a secret

Any random string of at least 32 characters works. This prints one:
Put it in SESSION_SECRET (or wherever your app reads it from).

Create a password hash

The login page has a Create new account link (/access). Someone who should get access fills in their email, name and a password of at least 10 characters, and gets one line to copy:
They send it to you and you paste it into users. The line holds only a hash, so it can go by email or chat; their password never reaches you and AwaitIO stores nothing. The page works without a login, and the server allows a few requests per address before it asks to wait.

Manage users

Users live in your code, so every change is an edit and a restart.
  1. The new user clicks Create new account on the login page and sends you the line it gives them.
  2. Paste it into users.
  3. Restart the server. The user can log in.
  1. The user makes a new line with Create new account, using their new password.
  2. Replace their passwordHash with the one from the line and restart.
Every session of that user ends: the cookies were signed together with the old hash, so they stop working everywhere. Other users stay logged in.
Remove the user from users and restart. Their cookie no longer matches anyone, so the next request returns them to the login page.
Generate a new secret, replace secret and restart. Every existing cookie stops working. Use this when you suspect a cookie or the secret has leaked.

How login works

1

Login page

Without a valid session, the UI shows the login page and every API call except login returns 401.
2

Password check

The server hashes the submitted password with the salt and parameters stored in the user’s hash and compares the result in constant time. An unknown email is checked against a dummy hash, so it takes as long as a wrong password and does not reveal which emails exist.
3

Session cookie

On success the server sets the awaitio_session cookie: the email and an expiry, signed with the secret and the user’s password hash. It is valid for 7 days, then the user logs in again.
4

Every request

The server checks the signature, the expiry and that the user is still listed. If any of it fails, the user is back on the login page.
5

Log out

The Log out button in the header deletes the cookie in that browser.

Nothing is stored on the server

Sessions are stateless: the cookie carries everything the server needs. A restart or a deploy keeps everyone logged in, and there is nothing to clean up. The flip side is that a single session cannot be revoked on its own. To end sessions early, change the user’s password (ends that user’s sessions) or the secret (ends all of them).

Password hashes

Hashes use scrypt with a random 16-byte salt. One hash takes about 100 ms, so guessing passwords from a leaked hash is slow too. The format is scrypt$N$r$p$salt$hash: the cost parameters travel with each hash, so they can be raised in a later version without breaking the hashes already in your configuration.

Too many failed attempts

After 10 failed logins for one email within 15 minutes, that email is locked until the 15 minutes run out and the login page shows Too many failed attempts. A successful login resets the count. The count is kept in memory, so a restart clears it. The cookie is HttpOnly, so scripts on the page cannot read it, and SameSite=Strict, so the browser does not send it with requests that another site starts. It is marked Secure when the request came over HTTPS, directly or through a proxy that sets X-Forwarded-Proto. Every API request that changes something, the login included, must come from the AwaitIO UI itself; requests started by another origin get 403. So another site cannot act as a logged-in user, nor log the browser into its own account.

The user in an action

The handler gets the user who started the run in ctx.user, see User.

Exposing the server

By default app.listen() binds to 127.0.0.1, so only the machine the server runs on can reach it. To serve other people, put it behind a reverse proxy with HTTPS and pass host: '0.0.0.0' to listen() when the proxy runs on another machine or container. Without HTTPS the password and the cookie travel in plain text.