Skip to content

Commit 8547611

Browse files
authored
Merge pull request #453 from harystyleseze/master
Fix healthchecks, add staging smoke test, add auth and oracle docs (#…
2 parents 8a709d8 + 5171ea0 commit 8547611

4 files changed

Lines changed: 448 additions & 1 deletion

File tree

.github/workflows/staging-deploy.yml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,42 @@ jobs:
3333
if: ${{ secrets.STAGING_KUBECONFIG == '' }}
3434
run: echo "Set STAGING_KUBECONFIG secret to enable automated staging deploy."
3535

36+
smoke-test:
37+
runs-on: ubuntu-latest
38+
environment: staging
39+
needs: deploy-staging
40+
steps:
41+
- name: Check backend health
42+
if: ${{ secrets.STAGING_BACKEND_URL != '' }}
43+
run: |
44+
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" "$STAGING_BACKEND_URL/health")
45+
echo "Backend /health → HTTP $STATUS"
46+
[ "$STATUS" = "200" ]
47+
env:
48+
STAGING_BACKEND_URL: ${{ secrets.STAGING_BACKEND_URL }}
49+
50+
- name: Check frontend
51+
if: ${{ secrets.STAGING_FRONTEND_URL != '' }}
52+
run: |
53+
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" "$STAGING_FRONTEND_URL/")
54+
echo "Frontend / → HTTP $STATUS"
55+
[ "$STATUS" = "200" ]
56+
env:
57+
STAGING_FRONTEND_URL: ${{ secrets.STAGING_FRONTEND_URL }}
58+
59+
- name: Check authenticated path returns 401
60+
if: ${{ secrets.STAGING_BACKEND_URL != '' }}
61+
run: |
62+
STATUS=$(curl -s -o /dev/null -w "%{http_code}" -H "Accept: application/json" "$STAGING_BACKEND_URL/policies")
63+
echo "GET /policies (no auth) → HTTP $STATUS"
64+
[ "$STATUS" = "401" ]
65+
env:
66+
STAGING_BACKEND_URL: ${{ secrets.STAGING_BACKEND_URL }}
67+
68+
- name: Smoke test secrets reminder
69+
if: ${{ secrets.STAGING_BACKEND_URL == '' }}
70+
run: echo "Set STAGING_BACKEND_URL and STAGING_FRONTEND_URL to enable smoke tests."
71+
3672
seed-staging-data:
3773
runs-on: ubuntu-latest
3874
environment: staging

docker-compose.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ services:
1616
interval: 5s
1717
timeout: 5s
1818
retries: 5
19+
start_period: 10s
1920

2021
redis:
2122
image: redis:7-alpine
@@ -26,6 +27,7 @@ services:
2627
interval: 5s
2728
timeout: 3s
2829
retries: 5
30+
start_period: 5s
2931

3032
backend:
3133
build:
@@ -47,6 +49,7 @@ services:
4749
interval: 10s
4850
timeout: 5s
4951
retries: 3
52+
start_period: 30s
5053

5154
frontend:
5255
build:
@@ -60,12 +63,14 @@ services:
6063
env_file:
6164
- .env.docker
6265
depends_on:
63-
- backend
66+
backend:
67+
condition: service_healthy
6468
healthcheck:
6569
test: ["CMD", "curl", "-f", "http://localhost:3000"]
6670
interval: 10s
6771
timeout: 5s
6872
retries: 3
73+
start_period: 30s
6974

7075
volumes:
7176
postgres_data:

docs/AUTH.md

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
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

Comments
 (0)