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 demo server (and the Laravel package) use 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