# Xpensely Server — API Reference > Last updated: 2026-05-09 · Branch: `feature/security-hardening` ## Table of Contents 1. [Overview](#1-overview) 2. [Authentication](#2-authentication) 3. [Rate Limiting](#3-rate-limiting) 4. [Endpoints](#4-endpoints) - 4.1 [Home](#41-home) - 4.2 [Users](#42-users) - 4.3 [Expense Lists](#43-expense-lists) 5. [Data Models](#5-data-models) 6. [Error Handling](#6-error-handling) 7. [Recent Changes — `feature/security-hardening`](#7-recent-changes) --- ## 1. Overview Xpensely Server is a Spring Boot REST API that manages shared expense lists for pairs of users. It uses Google OAuth2 JWT tokens for authentication. All protected endpoints require a valid Bearer token in the `Authorization` header. **Base URL (local dev):** `http://localhost:8080` **Content-Type:** `application/json` for all request and response bodies. **Public endpoints (no auth required):** | Method | Path | Description | |--------|------|-------------| | GET | `/` | Health check — returns `"Welcome"` | | POST | `/api/users/createUser` | Register a new user | | GET | `/api/users/byName` | Look up a user by username | All other endpoints require authentication (see [Section 2](#2-authentication)). ## 2. Authentication The server uses **OAuth2 Resource Server** authentication with **Google ID JWT tokens**. ### How it works 1. The client authenticates with Google and receives a Google ID JWT. 2. Every protected API request must include this token in the header: ``` Authorization: Bearer ``` 3. The server validates the JWT signature and extracts the `sub` claim (Google User ID). 4. The `sub` value is used to look up the registered `AppUser` in the database via `AuthenticatedUserResolver`. 5. If no `AppUser` exists for that Google ID, the request is rejected with **403 Forbidden**. ### Test profile When the application runs under the `test` Spring profile (`-Dspring.profiles.active=test`), **all security is disabled** — every endpoint is accessible without a token. This is used for automated tests only. ### User registration flow Before a user can call any protected endpoint they must first be registered: 1. Authenticate with Google to obtain a Google ID JWT. 2. Call `POST /api/users/createUser` with the JWT's `sub` value as `googleId` and a chosen `username`. 3. All subsequent protected calls use the same JWT — the server resolves the caller automatically. ## 3. Rate Limiting All requests pass through a `RateLimitFilter` (implemented with **Bucket4j**). | Setting | Value | |---------|-------| | Limit | 60 requests per minute | | Window | Rolling 1-minute bucket | | Key (authenticated) | JWT `sub` claim (Google User ID) | | Key (unauthenticated) | `X-Forwarded-For` header, falling back to remote IP | When the limit is exceeded the server responds with: ``` HTTP 429 Too Many Requests ``` No `Retry-After` header is currently returned. Clients should back off and retry after 60 seconds. > **Note:** Rate limiting applies in the `!test` profile only. Tests run without rate limiting. ## 4. Endpoints ### 4.1 Home #### `GET /` Health check. No authentication required. **Response:** `200 OK` ``` Welcome ``` --- ### 4.2 Users Base path: `/api/users` --- #### `POST /api/users/createUser` — Register a user **Auth required:** No **Request body:** ```json { "username": "alice", "googleId": "118400012345678901234" } ``` | Field | Type | Constraints | |-------|------|-------------| | `username` | String | Required. 3–30 chars. Pattern: `^[a-zA-Z0-9_.\-]+$` | | `googleId` | String | Required. Non-blank. Must match the JWT `sub` from Google. | **Success response:** `200 OK` — returns the created [AppUser](#appuser) object. **Error responses:** | Status | Condition | |--------|-----------| | 400 | Validation failure (field errors returned as `{"fieldName": "message"}`) | | 409 | `username` already taken | --- #### `GET /api/users` — Get user by ID **Auth required:** Yes **Query params:** | Param | Type | Required | Description | |-------|------|----------|-------------| | `id` | Long | Yes | Database ID of the user | **Success response:** `200 OK` — returns [AppUser](#appuser). **Error responses:** | Status | Condition | |--------|-----------| | 404 | No user found for `id` | --- #### `GET /api/users/byName` — Get user by username **Auth required:** No **Query params:** | Param | Type | Required | Description | |-------|------|----------|-------------| | `username` | String | Yes | Exact username (case-sensitive) | **Success response:** `200 OK` — returns [AppUser](#appuser). **Error responses:** | Status | Condition | |--------|-----------| | 404 | No user found for `username` | --- #### `GET /api/users/byGoogleId` — Get user by Google ID **Auth required:** Yes **Query params:** | Param | Type | Required | Description | |-------|------|----------|-------------| | `id` | String | Yes | Google `sub` claim | **Success response:** `200 OK` — returns [AppUser](#appuser). **Error responses:** | Status | Condition | |--------|-----------| | 404 | No user found for that Google ID | --- #### `DELETE /api/users` — Delete a user **Auth required:** Yes **Query params:** | Param | Type | Required | Description | |-------|------|----------|-------------| | `id` | Long | Yes | Database ID of the user to delete | **Success response:** `200 OK` — returns the deleted [AppUser](#appuser). **Error responses:** | Status | Condition | |--------|-----------| | 404 | No user found for `id` | --- ### 4.3 Expense Lists _TODO_ ## 5. Data Models _TODO_ ## 6. Error Handling _TODO_ ## 7. Recent Changes — `feature/security-hardening` _TODO_