Skip to content

Authentication (Server Perspective)

Summary

How to implement login, JWT tokens, and protected routes. Authentication is optional — you can disable it entirely.

The login flow

1. Client tries to load the menu

GET /menu/main

If the user is not authenticated, return 401 Unauthorized (no body needed).

2. Client fetches the login form

GET /auth/login → ScreenContract (Card)

Return a Card with username and password fields. Set auth_action to the login URL:

json
{
  "layout": "Card",
  "title": "Login",
  "auth_action": "/auth/login",
  "sections": [
    {
      "id": "login",
      "label": "Login",
      "fields": [
        {
          "id": "username",
          "label": "Username",
          "type": "Text",
          "value": ""
        },
        {
          "id": "password",
          "label": "Password",
          "type": "Password",
          "value": ""
        }
      ]
    }
  ]
}

The auth_action field tells the client to submit this form as an AuthRequest instead of a SaveChangeset.

3. Client submits credentials

POST /auth/login
json
{
  "fields": {
    "username": "admin",
    "password": "admin"
  }
}

4. Your server responds

Success:

json
{
  "success": true,
  "token": "eyJhbGciOiJIUzI1NiIs...",
  "error": null,
  "screen": {
    "layout": "Menu",
    "title": "Main Menu",
    "menu": {}
  }
}

Include the main menu ScreenContract in screen so the client can show it immediately without a second request.

Failure:

json
{
  "success": false,
  "token": null,
  "error": "Invalid username or password.",
  "screen": null
}

The client shows the error in the bottom bar and keeps the login form open.

AuthResponse fields

PropertyTypePurpose
successbooleanWhether authentication succeeded
tokenstringJWT token (only on success)
errorstringError message (only on failure)
screenScreenContractInitial screen to show (only on success)

Protecting routes

Add authentication middleware to all routes except:

  • GET /auth/login (public — returns the login form)
  • POST /auth/login (public — accepts credentials)

Your middleware should:

  1. Extract the Authorization: Bearer <token> header
  2. Validate the JWT signature and expiry
  3. Return 401 Unauthorized if the token is missing or invalid
  4. Pass the request through if valid

Token format

The protocol has no opinion on token format. The client stores whatever string the server returns and sends it as a bearer token on every request — it never inspects the value.

The reference server uses JWT (HS256) with these claims: sub (username), display_name, exp, iat, and a 24-hour expiry. You can use JWT, opaque tokens, signed cookies baked into a string, or anything else — as long as your middleware can validate the Authorization: Bearer <token> header and return 401 when it is missing or invalid.

user_display_name

Set user_display_name on ScreenContracts to show the logged-in user's name in the top-right corner of every screen:

json
{
  "user_display_name": "Erik Y."
}

No-auth mode

To disable authentication entirely, skip the auth middleware. The client never sees a 401, so no login screen appears.

In the demo server, set TWO_WEE_AUTH=false.

Token expiry mid-session

If any request returns 401, the client clears its stored token and redirects to the login screen. Any unsaved changes are lost — the dirty state is not preserved across re-login. Design token lifetimes with this in mind; 24 hours is a reasonable default for office use.

Routes

PatternMethodAuthPurpose
/auth/loginGETNoLogin form
/auth/loginPOSTNoSubmit credentials
All other routes*YesProtected

Environment variables (demo server)

VariableDefaultPurpose
TWO_WEE_AUTH"true"Set to "false" to disable auth
TWO_WEE_AUTH_SECRETRandomJWT signing key. Set a stable value in production.
TWO_WEE_APP_NAME"Demo Company"Application name on login screen