diff --git a/README.md b/README.md index a87a8a7..d1cfb30 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,7 @@ Open these URLs: - About: - Admin page: - Web login: +- Token refresh: `POST http://localhost:8000/api/auth/refresh` - Health check: - OpenAPI documentation: @@ -158,12 +159,13 @@ The manifest includes stable Firefox extension metadata and references the packa 4. Choose `webextension/manifest.json`. 5. Open the LinkLog extension options and enter: - Backend URL: the URL of your LinkLog server, such as `http://localhost:8000` - - Username: `alice` + - Email: `alice@example.com` - Password: `secret123` + - One-time password: enter it when OTP is enabled 6. Save the settings and login. 7. Open a webpage, select the LinkLog toolbar button, review the title and URL, add a comment, and submit it. -When the extension settings page has a valid session, it shows ` logged in at ` and a **Sign out** button instead of the login form. Signing out revokes the token and returns the form. +When the extension settings page has a valid session, it shows ` logged in at ` and a **Sign out** button instead of the login form. Access and refresh credentials are kept in Firefox session storage, so a browser restart requires login again. Signing out revokes the token family and returns the form. Temporary extensions are removed when Firefox restarts. Reload the extension from `about:debugging` after changing its files. @@ -261,6 +263,18 @@ curl -X POST http://localhost:8000/api/auth/login \\ -d '{"email":"alice@example.com","password":"secret123"}' ``` +The login response contains a 15-minute access token, a device-bound refresh token, its expiry time, and a `device_id`. Each successful refresh rotates the refresh token. + +Refresh an access token: + +```sh +curl -X POST http://localhost:8000/api/auth/refresh \\ + -H 'Content-Type: application/json' \\ + -d '{"refresh_token":"YOUR_REFRESH_TOKEN","device_id":"YOUR_DEVICE_ID"}' +``` + +Refresh-token reuse or a mismatched device ID returns `401` and revokes the token family. Signing out revokes the token family, while changing the password or completing a password reset revokes all sessions for the user. + Submit a link using the returned access token: ```sh diff --git a/Security-audit.md b/Security-audit.md index 0ca6935..f88cca9 100644 --- a/Security-audit.md +++ b/Security-audit.md @@ -173,15 +173,19 @@ These findings are prioritized below. Severity describes the potential security **Priority:** Medium. -### SA-012: Token lifecycle has unused refresh-token semantics +### SA-012: Token lifecycle has refresh-token rotation and revocation controls -**Severity:** Medium -**Evidence:** `issue_token()` returns a `refresh_token` value, but only the access token is inserted into `tokens`; no refresh endpoint or refresh-token hash is implemented. -**Impact:** Clients may assume the refresh token provides renewal or may store a value that cannot be revoked or used. This complicates session reasoning and can lead to unsafe client fallbacks. Access tokens currently live for the configured default of 30 days. +**Severity:** Medium, remediated in current worktree +**Evidence before remediation:** `issue_token()` returned a `refresh_token` value, but only the access token was inserted into `tokens`; no refresh endpoint or refresh-token hash was implemented. +**Impact:** Clients could assume the refresh token provided renewal or could store a value that could not be revoked or used. This complicated session reasoning and could lead to unsafe client fallbacks. Access tokens previously lived for the configured default of 30 days. -**Recommendation:** Either remove `refresh_token` from the API contract or implement a real refresh-token lifecycle: hash and persist refresh tokens, rotate them on use, detect reuse, bind them to a session/device, expire them separately, and revoke the token family on logout or password change. Reduce access-token lifetime after a real refresh flow is available. +**Current state:** Login and `POST /api/auth/refresh` issue 15-minute access tokens and separately expiring 30-day refresh tokens. Only SHA-256 refresh-token hashes are stored. Each token family is bound to a device ID; a valid refresh token is rotated and the previous family state is revoked. Reuse of a revoked refresh token revokes the complete family. Logout revokes the token family, and password reset deletes all tokens for the user. -**Priority:** Medium. +**Residual impact:** Existing sessions issued before deployment should be rotated, refresh-token reuse should be monitored, and clients must protect the device ID and session storage. Token-family cleanup and retention remain operational improvements. + +**Recommendation:** Keep access tokens short-lived, require device binding on refresh, alert on refresh-token reuse, and retain family revocation on logout and password reset. Never place tokens in URLs or logs. + +**Priority:** Completed in code; session rotation and monitoring remain. ### SA-013: No explicit security headers, CORS policy, or request-size policy @@ -224,7 +228,7 @@ These findings are prioritized below. Severity describes the potential security - **Primary email selection:** Only an already verified alternative address can be promoted to primary. The same user row retains account permissions and active sessions, and the previous primary is retained as a verified alternative. - **Password policy:** New and reset passwords require at least eight characters. This is better than no policy but should be replaced with a longer passphrase-oriented policy and breached-password screening after a proper password hash migration. - **Email verification:** New administrator-created users cannot log in until verified. The setup-created first administrator is marked verified, which is appropriate for bootstrap but should be protected by the setup controls above. -- **Password reset:** Tokens are random, hashed, expiring, single-use, and revoke existing access tokens after reset. Reset-email generation errors are intentionally swallowed to preserve generic login behavior, but this should be paired with server-side monitoring. +- **Password reset:** Tokens are random, hashed, expiring, single-use, and revoke all access and refresh token families after reset. Reset-email generation errors are intentionally swallowed to preserve generic login behavior, but this should be paired with server-side monitoring. - **OTP:** Login enforcement is present and OTP setup requires confirmation. Recovery codes, secret rotation, reauthentication, and encrypted secret storage are missing. - **Authorization:** Admin checks and link ownership checks are present. The last-administrator invariant is enforced for privilege changes and deletion. Add authorization tests for every new destructive endpoint as the API grows. @@ -243,7 +247,7 @@ These findings are prioritized below. Severity describes the potential security - The web API currently uses bearer headers, so browser CSRF risk is lower than with cookie sessions. Keep it that way unless a CSRF token design is added. - Browser local storage is exposed to any script running in the same origin. Keep third-party scripts out of authenticated pages and add a restrictive CSP. - The extension's `` host permission should be reduced if the active-tab workflow is sufficient. Review Mozilla Add-ons policies before publishing signed releases. -- The extension stores access and refresh-token-like values in `browser.storage.local`; implement actual refresh semantics or stop storing/returning unused refresh values. +- The extension stores access and refresh credentials in `browser.storage.session`; it uses the refresh endpoint after access-token expiry and clears session and legacy token material on logout or invalidation. - Extension error messages should not include tokens or full sensitive URLs. ## Deployment Checklist @@ -260,7 +264,7 @@ Before production exposure: - [ ] Define a restrictive CORS policy or leave CORS disabled. - [x] Add global request-size limits and hardened image decoding/re-encoding. - [x] Add OTP recovery codes and a protected recovery workflow. -- [ ] Remove or implement refresh-token behavior. +- [x] Implement refresh-token behavior with hashing, device binding, rotation, reuse detection, and family revocation. - [x] Add security audit events and centralized redacted logging. - [ ] Rotate all credentials and set a unique high-entropy production secret. - [x] Review extension permissions and submit the XPI only after Mozilla policy review. diff --git a/VIBE/CHAT_LOG.md b/VIBE/CHAT_LOG.md index 8e4fd28..97b9e7b 100644 --- a/VIBE/CHAT_LOG.md +++ b/VIBE/CHAT_LOG.md @@ -1,5 +1,17 @@ # Chat Log +### User +Update SA-012 to reflect the actual implementation, document device binding, rotation, reuse detection, family revocation, logout and password-reset revocation, update the checklist and README API examples, and add the refresh endpoint to the documented API surface. + +### Assistant outcome +Reconciled SA-012 and the authentication documentation with the implemented 15-minute access tokens and 30-day device-bound refresh tokens. Documented hashing, rotation, reuse-triggered family revocation, logout and password-reset revocation, added the refresh endpoint and curl example to the README, corrected extension login instructions, and checked off the refresh-token implementation item. + +### User +Replace `LINKLOG_TOKEN_EXPIRY_DAYS` with `LINKLOG_TOKEN_EXPIRY_MINUTES`, add `LINKLOG_REFRESH_TOKEN_EXPIRY_DAYS` to the production Compose environment, and add a CI test comparing Compose variables with Settings. + +### Assistant outcome +Updated production Compose to pass the configured 15-minute access-token and 30-day refresh-token settings. Added a repository test that compares `LINKLOG_*` Compose keys with the environment keys declared by `Settings` and rejects the obsolete days-based access-token variable. + ### User Reduce the size of the font of the greeting somewhat. diff --git a/VIBE/PROMPTS.md b/VIBE/PROMPTS.md index f0cfa43..2d8c7de 100644 --- a/VIBE/PROMPTS.md +++ b/VIBE/PROMPTS.md @@ -197,6 +197,8 @@ 191. Put the email header text to the right of the logo, align it at the top, and add a comma after Hello. 192. Put the email greeting in a separate top-aligned cell to the right of the logo. 193. Reduce the email greeting font size somewhat. +194. Replace LINKLOG_TOKEN_EXPIRY_DAYS with LINKLOG_TOKEN_EXPIRY_MINUTES, add LINKLOG_REFRESH_TOKEN_EXPIRY_DAYS to production Compose, and add a CI configuration consistency test. +195. Update SA-012 and README for the implemented refresh-token lifecycle, revocation behavior, and refresh endpoint. ## Future entries diff --git a/backend/tests/test_configuration.py b/backend/tests/test_configuration.py new file mode 100644 index 0000000..9ca7bbb --- /dev/null +++ b/backend/tests/test_configuration.py @@ -0,0 +1,18 @@ +import re +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[2] + + +def test_production_compose_configuration_matches_settings_environment_keys(): + compose = (ROOT / 'docker-compose.yml').read_text() + settings = (ROOT / 'backend' / 'app' / 'core' / 'config.py').read_text() + database = (ROOT / 'backend' / 'app' / 'database.py').read_text() + + compose_keys = set(re.findall(r'\b(LINKLOG_[A-Z0-9_]+):', compose)) + settings_keys = set(re.findall(r"os\.getenv\('([^']+)'", settings + database)) + + assert {'LINKLOG_TOKEN_EXPIRY_MINUTES', 'LINKLOG_REFRESH_TOKEN_EXPIRY_DAYS'} <= compose_keys + assert compose_keys & settings_keys == compose_keys + assert 'LINKLOG_TOKEN_EXPIRY_DAYS' not in compose_keys \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index c6b7b13..3ad0b91 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,7 +17,8 @@ services: LINKLOG_DATA_ENCRYPTION_KEY: ${LINKLOG_DATA_ENCRYPTION_KEY:?Set LINKLOG_DATA_ENCRYPTION_KEY in .env} LINKLOG_PUBLIC_URL: ${LINKLOG_PUBLIC_URL:-linklog.example.com} LINKLOG_LOG_LEVEL: ${LINKLOG_LOG_LEVEL:-INFO} - LINKLOG_TOKEN_EXPIRY_DAYS: ${LINKLOG_TOKEN_EXPIRY_DAYS:-30} + LINKLOG_TOKEN_EXPIRY_MINUTES: ${LINKLOG_TOKEN_EXPIRY_MINUTES:-15} + LINKLOG_REFRESH_TOKEN_EXPIRY_DAYS: ${LINKLOG_REFRESH_TOKEN_EXPIRY_DAYS:-30} LINKLOG_TRACKING_PARAMS: ${LINKLOG_TRACKING_PARAMS:-} restart: ${APP_RESTART_POLICY:-unless-stopped} healthcheck: