-
Notifications
You must be signed in to change notification settings - Fork 23
Description
Summary
Before starting Phase 1 auth work, we should stop relying on frontend hot reload in the stack and move the UI to a production-style build/serve flow. This ensures E2E and manual testing exercise the same frontend bundle that will be deployed, instead of the current npm run dev HMR setup.
At the same time, we should define and begin implementing the next auth direction for Open-FDD: expiring bearer tokens, a login flow, refresh support, and a single bootstrap-managed app user with a hashed password stored in a file.
Frontend hot reload
Current state
The stack’s frontend container currently runs:
npm run devThat is convenient during active UI work, but once the UI is stable enough, it becomes a mismatch from deployed behavior because HMR/dev server behavior is not the same as a production build.
Goal
Turn off frontend hot reload once the UI is solid enough and before Phase 1 auth begins.
Run the frontend as a production build so:
- manual testing hits the real built bundle
- E2E testing hits the same assets we plan to deploy
- there is no HMR masking runtime issues
- behavior matches production more closely
Expected direction
Switch from dev server mode to one of these:
- build the frontend and serve
frontend/distvia Caddy - or build the frontend and serve static assets from the API
- or otherwise use a static file server, but no HMR/dev mode in the deployed stack
Security / auth – next steps
Current state
Auth is currently based on a single shared bearer token from stack/.env:
-
OFDD_API_KEY -
bootstrap generates a key if missing unless
--no-auth -
frontend uses the same key, such as
VITE_OFDD_API_KEY -
frontend sends:
Authorization: Bearer <key>for HTTP?token=for WebSocket
Limitations of current model:
- single shared secret
- no expiry
- no refresh flow
- no user identity
- frontend and backend share one long-lived credential
Planned direction
1. Expiring bearer tokens
Move to short-lived access tokens with expiry.
Preferred pattern:
- short-lived access token, for example 15–60 minutes
- longer-lived refresh token or session-based refresh flow
Behavior:
- frontend uses access token for API and WebSocket
- when access token expires, frontend calls refresh endpoint
- on refresh failure, user is redirected to login
Implementation notes:
- JWT access tokens are a natural fit using
exp - refresh token may be opaque and stored server-side, or JWT with longer expiry
- token validation must apply to both API and WebSocket access
- revoked or expired tokens must be rejected
2. Add FastAPI auth endpoints
Add auth endpoints for login and refresh.
Planned routes:
POST /auth/loginPOST /auth/refresh
Example login response:
{
"access_token": "...",
"refresh_token": "...",
"expires_in": 3600
}Example refresh response:
{
"access_token": "...",
"expires_in": 3600
}Notes:
- refresh token rotation is preferred if practical
- backend should preserve current bearer validation style, but validate access tokens instead of a static shared key
- WebSocket auth should use the current valid access token
3. Add React login flow
Add a login screen and app-level token handling.
Requirements:
- login page at
/loginor equivalent - successful login stores auth state and redirects into app
- API calls use current access token
- WebSocket uses current access token
- refresh logic happens in one central place
- if refresh fails, clear auth state and redirect to login
Recommended direction:
- central API/auth layer for token injection and refresh retry
- one retry on 401 before forcing logout
4. Bootstrap-managed single app user
Bootstrap should support creating one application user with username/password.
Proposed bootstrap args:
--user openfdd --password <pwd>This single user is the initial UI login account.
Notes:
- for now, keep this as a one-user system
- changing credentials can initially be done by rerunning bootstrap
- a dedicated change-password flow can come later
5. Store hashed password in a file
Do not store plaintext passwords.
Use a proper password hashing scheme:
- Argon2 preferred
- bcrypt acceptable
Proposed storage model:
- file-based only for now
- no database required for single-user auth
- similar in spirit to Node-RED credentials storage
Possible locations:
stack/auth.envstack/.env- or a dedicated auth config file
Example values:
OFDD_APP_USER=openfdd
OFDD_APP_USER_HASH=<argon2 hash>Backend behavior:
- read configured username and password hash at startup
- keep single user credentials in memory for login verification
- on login, verify with Argon2 or bcrypt
- return 401 on failure without leaking whether username exists
6. Decide what happens to OFDD_API_KEY
We need to decide whether to keep a machine/API credential alongside UI login.
Options:
-
Keep
OFDD_API_KEYfor machine-to-machine access- useful for Home Assistant, scripts, and non-browser clients
- React UI uses username/password login flow
-
Remove
OFDD_API_KEY- everything uses the new login/token flow
- simpler long term, but may be less convenient for non-browser integrations
This should be explicitly decided during implementation.
Suggested implementation order
- Disable frontend dev/HMR in the stack and serve a production build
- Add bootstrap support for
--userand--password - Hash password with Argon2 or bcrypt and store it in a file
- Add
POST /auth/login - Add expiring access tokens
- Add
POST /auth/refresh - Add React login page and token management
- Update API and WebSocket auth to use access tokens
- Decide whether
OFDD_API_KEYremains for machine access
Acceptance criteria
Frontend
- frontend is no longer run via
npm run devin the deployed stack - stack serves a built production frontend bundle
- E2E and manual testing target the production-served bundle
- no HMR is present in stack-based testing workflow
Backend auth
- bootstrap can create a single app user with username/password
- plaintext password is never stored
- password hash is stored in a file and loaded by backend at startup
POST /auth/loginissues an access token- access tokens expire
POST /auth/refreshcan issue a new access token- API rejects expired or invalid tokens
- WebSocket rejects expired or invalid tokens
Frontend auth
- React app has a login screen
- frontend stores and uses current access token
- frontend can refresh expired access token automatically
- failed refresh redirects user back to login
Security baseline
- password verification uses Argon2 or bcrypt
- access tokens are short-lived
- auth flow is compatible with HTTPS-first deployment
- refresh token handling is defined clearly before merge
Open questions
- Should
OFDD_API_KEYremain for machine/non-browser clients? - Should refresh tokens be stored in an httpOnly cookie or returned in JSON?
- Should refresh tokens be rotated on every refresh?
- Should the backend use JWTs only, or opaque refresh tokens plus JWT access tokens?
- Should static frontend files be served by Caddy or by FastAPI?
Notes
This issue is intended as the bridge from the current shared-secret bootstrap auth model into a more production-ready Phase 1 auth foundation, while also aligning frontend testing with real deployment behavior.
If you want, I can also turn this into a shorter GitHub issue version or split it into 2–3 separate issues like:
- frontend production build serving,
- backend auth/token system,
- React login flow.