# Authentication The customer portal API (`dk_customer_portal_api`) uses a **bearer token** issued at login, not Odoo's cookie-based session auth. Every route (except `/api/login`, `/api/register`, and the OAuth/social-login start/callback routes) is registered as `auth="none"` in Odoo and checks the token itself. ## Getting a token ```text POST /api/login Content-Type: application/json { "db": "your_database_name", "login": "user@example.com", "password": "your-password" } ``` On success: ```json { "success": true, "token": "a-64-byte-urlsafe-token", "user": { "id": 14, "name": "Jane Doe", "login": "user@example.com", "is_admin": false } } ``` On failure, the response is `{"success": false, "message": "..."}` with `400` (missing `db`/`login`/`password`), `401` (invalid credentials), or `404` (user record missing after auth). Internally, `/api/login` calls `request.session.authenticate(db, {...})` against Odoo, then generates the token with `secrets.token_urlsafe(64)` and stores it on a `dk.mobile.token` record linked to the authenticated `res.users`. The same token model backs both the React customer portal and the mobile app. ## Using the token Send it as a standard `Authorization: Bearer` header on every subsequent request: ```text GET /api/instances Authorization: Bearer a-64-byte-urlsafe-token ``` Each controller resolves the caller with the same pattern: ```python def _get_authenticated_user(self): auth_header = request.httprequest.headers.get("Authorization") if not auth_header or not auth_header.startswith("Bearer "): return False token = auth_header.replace("Bearer ", "").strip() token_record = request.env["dk.mobile.token"].sudo().search( [("token", "=", token), ("active", "=", True)], limit=1 ) return token_record.user_id if token_record else False ``` If the header is missing, malformed, or the token doesn't match an active `dk.mobile.token` record, the endpoint returns: ```json { "success": false, "message": "Unauthorized" } ``` with HTTP status `401`. :::note Tokens don't currently expire on a fixed schedule — they're deactivated by flipping `active` to `false` on the `dk.mobile.token` record (e.g. on logout or manual revocation). Each token also tracks `last_used`. ::: ## CORS Every controller defines the same `_cors_headers()` helper and only reflects `Access-Control-Allow-Origin` back for an allow-listed set of origins: - `http://localhost:3000`, `http://localhost:3001`, `http://localhost:8069` (local dev) - `https://dishonkadoh.com`, `https://www.dishonkadoh.com` - `https://app.dishonkadoh.com`, `https://erp.dishonkadoh.com` - `https://farmproduction.dishonkadoh.com`, `https://demo.dishonkadoh.com` - `http://odoo.minikube:31906` (local Kubernetes) `Access-Control-Allow-Credentials: true` and `Vary: Origin` are always set. Every route accepts `OPTIONS` for the CORS preflight and returns an empty `200` response before touching authentication logic. :::note Because every route is `auth="none"`, CORS and the bearer check are the only things standing between an anonymous request and the endpoint. If you add a new controller, copy `_cors_headers()`/`_json_response()`/ `_get_authenticated_user()` from an existing one (e.g. `main.py`) rather than reimplementing them — that's the pattern used across every controller. ::: ## Social / OAuth login `google_oauth.py` and `social_auth.py` implement start/callback routes for Google, GitHub, GitLab, and Bitbucket (`/api/auth/` and `/api/auth//callback`), plus `/api/auth/social/exchange` and `/api/auth/google/exchange` to trade a provider code for a `dk.mobile.token` using the same mechanism as `/api/login`. These are separate from the `oauth.py` handoff routes (`/api/oauth/handoff`, `/api/oauth/handoff/consume`), which support handing an authenticated session off between `dishonkadoh.com` properties (e.g. into `erp.dishonkadoh.com`).