Authentication
JWT Bearer token authentication with refresh token support. Multi-tenant: a single user can belong to multiple firms and switch between them seamlessly.
When to use this module
- User needs to log in →
POST /auth/login - Access token expires → refresh →
POST /auth/refreshtoken - User belongs to multiple firms → switch →
GET /auth/setfirm - User logs out → invalidate session →
POST /auth/logout
Endpoints
| Method | Route | Auth | Description |
|---|---|---|---|
| POST | /api/v1/auth/login | Login → JWT + refresh token | |
| POST | /api/v1/auth/refreshtoken | Exchange refresh token for new JWT | |
| GET | /api/v1/auth/setfirm | Switch active firm context | |
| POST | /api/v1/auth/logout | Invalidate current session |
Authentication Flow
// 1. Login request
Client POST /auth/login { email, password }
→ Server validates credentials (PBKDF2 hash check)
→ Server loads user's firm relations
→ Server generates JWT with claims: UserId, Email, FirmId, RelationId
→ Server creates refresh token in FirmUserRefreshToken table
→ Returns { token, refreshToken, firms[] }
// 2. API calls
Client calls API with Authorization: Bearer <token>
// 3. Token refresh (when expired)
Client POST /auth/refreshtoken { refreshToken }
→ Server validates token ↔ DB record
→ Returns new { token, refreshToken }
// 4. Multi-firm switch (optional)
GET /auth/setfirm?userId=&relationId=&sessionId=
→ Returns new JWT with different FirmId claim
POST
/api/v1/auth/login
Authenticates a user and returns a JWT access token, refresh token, and the list of firms the user belongs to.
This endpoint does not require Authorization header.
Request Body
JSON
{
"email": "admin@company.com",
"password": "SecurePass123!"
}
Request Parameters
| Field | Type | Required | Description |
|---|---|---|---|
email | string | required | User's email address |
password | string | required | User's password (min 8 chars, complexity required) |
Response — 200 OK
JSON
{
"isSuccess": true,
"data": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "d4f8e2a1-9b3c-4e5f-a6d7-8b9c0d1e2f3a",
"expiresIn": 3600,
"user": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"firstName": "John",
"lastName": "Doe",
"email": "admin@company.com",
"userName": "johndoe"
},
"firms": [
{
"firmId": "f1e2d3c4-b5a6-9870-fedc-ba9876543210",
"relationId": "r1e2d3c4-b5a6-9870-fedc-ba9876543210",
"firmName": "Acme Corp",
"sessionId": "s1e2d3c4-b5a6-9870-fedc-ba9876543210"
}
]
},
"message": "Login successful",
"error": null
}
JWT Claims
| Claim | Value |
|---|---|
NameIdentifier | UserId (Guid) |
Email | User email |
FirmId | Active firm ID (Guid) |
RelationId | FirmUserRelation ID (Guid) |
Error Responses
| Status | Error | Cause |
|---|---|---|
| 400 | ValidationError | Email/password missing or malformed |
| 401 | Unauthorized | Invalid credentials or inactive account |
POST
/api/v1/auth/refreshtoken
Exchange an expired JWT for a new access token + refresh token pair. The old refresh token is invalidated.
Request Body
JSON
{
"refreshToken": "d4f8e2a1-9b3c-4e5f-a6d7-8b9c0d1e2f3a"
}
Response — 200 OK
JSON
{
"isSuccess": true,
"data": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "new-refresh-token-guid",
"expiresIn": 3600
},
"message": "Token refreshed successfully"
}
GET
/api/v1/auth/setfirm
Switches the user's active firm context. Returns a new JWT with the selected firm's ID embedded. Use when a user belongs to multiple firms.
Requires
Authorization: Bearer <token> header. Returns a new token — replace the old one in client storage.Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
userId | Guid | required | The user ID (from login response) |
relationId | Guid | required | The FirmUserRelation ID for the target firm |
sessionId | Guid | required | Current session ID (from login response) |
Example Request
HTTP
GET /api/v1/auth/setfirm?userId=a1b2c3d4&relationId=r1e2d3c4&sessionId=s1e2d3c4
Authorization: Bearer <current_token>
POST
/api/v1/auth/logout
Invalidates the current session and refresh token. Subsequent requests with the same refresh token will fail.
After logout, the client should discard both the access token and refresh token from storage.
Response — 200 OK
JSON
{
"isSuccess": true,
"data": null,
"message": "Logged out successfully"
}