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/mainIf 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/loginjson
{
"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
| Property | Type | Purpose |
|---|---|---|
success | boolean | Whether authentication succeeded |
token | string | JWT token (only on success) |
error | string | Error message (only on failure) |
screen | ScreenContract | Initial 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:
- Extract the
Authorization: Bearer <token>header - Validate the JWT signature and expiry
- Return
401 Unauthorizedif the token is missing or invalid - 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
| Pattern | Method | Auth | Purpose |
|---|---|---|---|
/auth/login | GET | No | Login form |
/auth/login | POST | No | Submit credentials |
| All other routes | * | Yes | Protected |
Environment variables (demo server)
| Variable | Default | Purpose |
|---|---|---|
TWO_WEE_AUTH | "true" | Set to "false" to disable auth |
TWO_WEE_AUTH_SECRET | Random | JWT signing key. Set a stable value in production. |
TWO_WEE_APP_NAME | "Demo Company" | Application name on login screen |