|
| 1 | +# Auth Flow for Integrators |
| 2 | + |
| 3 | +## Overview |
| 4 | + |
| 5 | +StellarInsure uses Stellar wallet signature verification for authentication. Users prove ownership of a Stellar keypair by signing a message, and the backend issues JWT tokens for subsequent requests. |
| 6 | + |
| 7 | +This is **not** SEP-10. There is no challenge endpoint or structured handshake — the client constructs a message, signs it locally, and sends the signature directly to `/auth/login`. |
| 8 | + |
| 9 | +--- |
| 10 | + |
| 11 | +## Auth Sequence |
| 12 | + |
| 13 | +``` |
| 14 | +1. Client chooses a message to sign (e.g. a timestamped nonce) |
| 15 | + ↓ |
| 16 | +2. Client signs the message with the Stellar private key (Ed25519) |
| 17 | + ↓ |
| 18 | +3. POST /auth/login { stellar_address, message, signature } |
| 19 | + ↓ |
| 20 | +4. Backend verifies the signature against the public key |
| 21 | + ↓ |
| 22 | +5. Backend returns access_token + refresh_token |
| 23 | + ↓ |
| 24 | +6. Client includes access_token in Authorization header for all requests |
| 25 | + ↓ |
| 26 | +7. When access_token expires, POST /auth/refresh to get new tokens |
| 27 | +``` |
| 28 | + |
| 29 | +New users are automatically created on first login — there is no separate registration step required. |
| 30 | + |
| 31 | +--- |
| 32 | + |
| 33 | +## Step 1 — Construct a message |
| 34 | + |
| 35 | +The backend does not enforce a specific message format; it only verifies that the signature matches whatever message is sent. Use a timestamped nonce to prevent replay across sessions: |
| 36 | + |
| 37 | +``` |
| 38 | +Sign in to StellarInsure: <unix_timestamp> |
| 39 | +``` |
| 40 | + |
| 41 | +Example: |
| 42 | +``` |
| 43 | +Sign in to StellarInsure: 1714204800 |
| 44 | +``` |
| 45 | + |
| 46 | +--- |
| 47 | + |
| 48 | +## Step 2 — Sign with Freighter |
| 49 | + |
| 50 | +```javascript |
| 51 | +import freighter from "@stellar/freighter-api"; |
| 52 | + |
| 53 | +const message = `Sign in to StellarInsure: ${Math.floor(Date.now() / 1000)}`; |
| 54 | +const { signedMessage } = await freighter.signMessage(message); |
| 55 | +// signedMessage is a base64-encoded Ed25519 signature |
| 56 | +``` |
| 57 | + |
| 58 | +### Sign with stellar-sdk (server/testing) |
| 59 | + |
| 60 | +```javascript |
| 61 | +import { Keypair } from "@stellar/stellar-sdk"; |
| 62 | + |
| 63 | +const keypair = Keypair.fromSecret("S..."); |
| 64 | +const message = `Sign in to StellarInsure: ${Math.floor(Date.now() / 1000)}`; |
| 65 | +const msgBytes = Buffer.from(message, "utf8"); |
| 66 | +const signature = keypair.sign(msgBytes).toString("base64"); |
| 67 | +``` |
| 68 | + |
| 69 | +```python |
| 70 | +from stellar_sdk import Keypair |
| 71 | +import base64, time |
| 72 | + |
| 73 | +keypair = Keypair.from_secret("S...") |
| 74 | +message = f"Sign in to StellarInsure: {int(time.time())}" |
| 75 | +signature = base64.b64encode(keypair.sign(message.encode())).decode() |
| 76 | +``` |
| 77 | + |
| 78 | +--- |
| 79 | + |
| 80 | +## Step 3 — Login |
| 81 | + |
| 82 | +**Endpoint:** `POST /auth/login` |
| 83 | + |
| 84 | +**Rate limit:** 10 requests / minute per IP |
| 85 | + |
| 86 | +**Request body:** |
| 87 | + |
| 88 | +```json |
| 89 | +{ |
| 90 | + "stellar_address": "GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", |
| 91 | + "message": "Sign in to StellarInsure: 1714204800", |
| 92 | + "signature": "<base64-encoded Ed25519 signature>" |
| 93 | +} |
| 94 | +``` |
| 95 | + |
| 96 | +**curl example:** |
| 97 | + |
| 98 | +```bash |
| 99 | +curl -X POST https://api.example.com/auth/login \ |
| 100 | + -H "Content-Type: application/json" \ |
| 101 | + -d '{ |
| 102 | + "stellar_address": "GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", |
| 103 | + "message": "Sign in to StellarInsure: 1714204800", |
| 104 | + "signature": "ABC123...base64..." |
| 105 | + }' |
| 106 | +``` |
| 107 | + |
| 108 | +**Response (200):** |
| 109 | + |
| 110 | +```json |
| 111 | +{ |
| 112 | + "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", |
| 113 | + "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", |
| 114 | + "token_type": "bearer", |
| 115 | + "expires_in": 1800 |
| 116 | +} |
| 117 | +``` |
| 118 | + |
| 119 | +**Error responses:** |
| 120 | + |
| 121 | +| HTTP | Code | Meaning | |
| 122 | +|------|------|---------| |
| 123 | +| 401 | AUTH_001 | Signature is invalid or does not match the message | |
| 124 | +| 429 | — | Rate limit exceeded | |
| 125 | + |
| 126 | +--- |
| 127 | + |
| 128 | +## Token Lifetimes and Headers |
| 129 | + |
| 130 | +| Token | Lifetime | Usage | |
| 131 | +|-------|----------|-------| |
| 132 | +| `access_token` | 30 minutes (`expires_in: 1800`) | Sent as `Authorization: Bearer <token>` on every request | |
| 133 | +| `refresh_token` | 7 days | Sent to `POST /auth/refresh` to obtain a new token pair | |
| 134 | + |
| 135 | +**JWT claims** (decoded payload): |
| 136 | + |
| 137 | +```json |
| 138 | +{ |
| 139 | + "sub": "42", |
| 140 | + "stellar_address": "GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", |
| 141 | + "exp": 1714206600, |
| 142 | + "type": "access" |
| 143 | +} |
| 144 | +``` |
| 145 | + |
| 146 | +- `sub` — internal user ID (integer as string) |
| 147 | +- `stellar_address` — Stellar public key |
| 148 | +- `exp` — expiry as Unix timestamp |
| 149 | +- `type` — `"access"` or `"refresh"`; the backend rejects access tokens presented to `/auth/refresh` and vice versa |
| 150 | + |
| 151 | +--- |
| 152 | + |
| 153 | +## Step 4 — Authenticated Requests |
| 154 | + |
| 155 | +Include the access token in every request: |
| 156 | + |
| 157 | +```bash |
| 158 | +curl https://api.example.com/policies \ |
| 159 | + -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." |
| 160 | +``` |
| 161 | + |
| 162 | +A missing or invalid token returns: |
| 163 | + |
| 164 | +```json |
| 165 | +HTTP 401 |
| 166 | +{ |
| 167 | + "error": { "code": "AUTH_002", "message": "User not found" } |
| 168 | +} |
| 169 | +``` |
| 170 | + |
| 171 | +An expired token returns HTTP 401 with code `AUTH_004`. |
| 172 | + |
| 173 | +--- |
| 174 | + |
| 175 | +## Step 5 — Refresh Tokens |
| 176 | + |
| 177 | +**Endpoint:** `POST /auth/refresh` |
| 178 | + |
| 179 | +```bash |
| 180 | +curl -X POST https://api.example.com/auth/refresh \ |
| 181 | + -H "Content-Type: application/json" \ |
| 182 | + -d '{"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}' |
| 183 | +``` |
| 184 | + |
| 185 | +**Response (200):** same shape as login — a new `access_token` and `refresh_token`. |
| 186 | + |
| 187 | +**Error:** HTTP 401 if the refresh token is expired or has the wrong type. |
| 188 | + |
| 189 | +When the refresh token itself expires (after 7 days), the user must sign again via `/auth/login`. |
| 190 | + |
| 191 | +--- |
| 192 | + |
| 193 | +## How Signature Verification Works |
| 194 | + |
| 195 | +The backend uses the Stellar Python SDK's `Keypair.verify()`: |
| 196 | + |
| 197 | +```python |
| 198 | +keypair = Keypair.from_public_key(stellar_address) |
| 199 | +keypair.verify( |
| 200 | + data=message.encode("utf-8"), |
| 201 | + signature=base64.b64decode(signature), |
| 202 | +) |
| 203 | +``` |
| 204 | + |
| 205 | +This is a standard Ed25519 signature check. The signature must be a base64-encoded raw Ed25519 signature (64 bytes) over the UTF-8 bytes of the message string. |
| 206 | + |
| 207 | +--- |
| 208 | + |
| 209 | +## Error Code Reference |
| 210 | + |
| 211 | +| Code | HTTP | Description | |
| 212 | +|------|------|-------------| |
| 213 | +| AUTH_001 | 401 | Invalid wallet signature | |
| 214 | +| AUTH_002 | 401 | User not found | |
| 215 | +| AUTH_003 | 400 | User already exists (only relevant on `/auth/register`) | |
| 216 | +| AUTH_004 | 401 | Token expired or invalid | |
| 217 | + |
| 218 | +--- |
| 219 | + |
| 220 | +## Notes for Integrators |
| 221 | + |
| 222 | +- **`/auth/login` vs `/auth/register`:** Both accept the same request body and perform the same signature check. `/auth/login` auto-creates a new user record on first login. You do not need to call `/auth/register` separately. |
| 223 | +- **Session storage:** The reference frontend stores the session in `sessionStorage` under the key `stellarinsure-session` and clears it on tab close. |
| 224 | +- **Logout:** `POST /auth/logout` is a client-side operation only — the backend is stateless and does not invalidate tokens. Discard tokens client-side. |
| 225 | +- **Account deletion:** `DELETE /auth/me` requires re-authentication (send the same body as `/auth/login`). It soft-deletes the account, cancels active policies, and anonymises personal data. |
0 commit comments