Some additional checks and cleanup
Build LinkLog Development Image / development-image (push) Successful in 10s
Build LinkLog Development Image / development-image (push) Successful in 10s
This commit is contained in:
@@ -69,6 +69,7 @@ Open these URLs:
|
||||
- About: <http://localhost:8000/about>
|
||||
- Admin page: <http://localhost:8000/admin>
|
||||
- Web login: <http://localhost:8000/login>
|
||||
- Token refresh: `POST http://localhost:8000/api/auth/refresh`
|
||||
- Health check: <http://localhost:8000/health>
|
||||
- OpenAPI documentation: <http://localhost:8000/docs>
|
||||
|
||||
@@ -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 `<username> logged in at <backend URL>` 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 `<username> logged in at <backend URL>` 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
|
||||
|
||||
+13
-9
@@ -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 `<all_urls>` 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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
+2
-1
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user