Authentication¶
ICOSYS uses JWT tokens delivered as httpOnly cookies for browser sessions and API keys for service-to-service and administrative endpoints. All services share the same security filter chain.
Authentication Methods¶
| Method | Transport | Use Case |
|---|---|---|
| JWT Cookie | Set-Cookie: ICOSYS_SESSION |
React SPA (browser) |
| JWT Bearer | Authorization: Bearer <token> |
Postman, service-to-service |
| API Key | X-API-Key: <key> |
Session management, admin, logs |
Login Flow¶
1. Authenticate¶
curl -X POST http://localhost:8010/icglb/services/api/auth/login \
-H "Content-Type: application/json" \
-H "X-API-Key: <api-key>" \
-d '{
"userCode": "admin",
"password": "secret",
"languageCode": "en",
"captchaToken": "optional-recaptcha-v3-token"
}'
| Field | Type | Required | Description |
|---|---|---|---|
userCode |
string |
Yes | Username |
password |
string |
Yes | Plain-text password (validated against BCrypt hash) |
languageCode |
string |
Yes | en or tr — sets error message language |
captchaToken |
string |
No | Google reCAPTCHA v3 token |
2. Response¶
{
"success": true,
"token": "",
"userCode": "admin",
"source": "DB",
"expiresAt": "2026-02-16T02:00:00Z",
"superUser": false,
"userProcess": {
"id": 123,
"userCode": "admin",
"userDescription": "System Admin",
"userProcessSelects": [
{
"accountNo": "ACC-2024-001",
"accountName": "ACME Corp",
"crProcessId": 1
}
]
}
}
Token Field
The token field in the response body is empty because the JWT is delivered
as an httpOnly cookie. Postman users should extract it from the
Set-Cookie response header.
3. Cookie Set by Server¶
| Attribute | Dev | Production |
|---|---|---|
HttpOnly |
true |
true |
Secure |
false |
true |
SameSite |
Lax |
Lax |
Max-Age |
28800s (8h) | Configurable |
Path |
/ |
/ |
4. Frontend Stores User Metadata¶
The React app stores non-sensitive user data in localStorage:
localStorage.setItem("icosys_auth_state", "authenticated")
localStorage.setItem("icosys_user", JSON.stringify({
userCode: "admin",
superUser: false,
accounts: [...],
activeAccountNo: "ACC-2024-001",
crProcessId: 1,
roles: ["ROLE_ADMIN", "ROLE_BPM_VIEW"]
}))
The JWT itself is never accessible to JavaScript — it lives only in the httpOnly cookie.
Session Validation¶
On every page load, the frontend calls /api/auth/me to verify the session:
curl http://localhost:8010/icglb/services/api/auth/me \
-H "Cookie: ICOSYS_SESSION=eyJhbGciOi..."
{
"success": true,
"userCode": "admin",
"superUser": false,
"userProcess": { ... }
}
{
"status": "ERROR",
"error_code": "IC-SYS-1001",
"message": "Session expired"
}
On 401, the frontend clears localStorage and redirects to /login.
Account Switching¶
Users can belong to multiple accounts. Switching does not require re-login:
curl -X POST http://localhost:8010/icglb/services/api/auth/switch-account \
-H "Content-Type: application/json" \
-H "Cookie: ICOSYS_SESSION=eyJhbGciOi..." \
-d '{ "accountNo": "ACC-2024-002" }'
The server issues a new JWT cookie with updated claims (account context).
The frontend re-fetches user metadata with the new crProcessId.
Password Change¶
curl -X POST http://localhost:8010/icglb/services/api/auth/change-password \
-H "Content-Type: application/json" \
-H "Cookie: ICOSYS_SESSION=eyJhbGciOi..." \
-d '{
"currentPassword": "oldSecret",
"newPassword": "newSecret123"
}'
Returns 204 No Content on success. All existing JWT tokens for this user are
invalidated via tokenVersion increment — every other session is forced to
re-login.
Logout¶
curl -X POST http://localhost:8010/icglb/services/api/auth/logout \
-H "Cookie: ICOSYS_SESSION=eyJhbGciOi..."
The server:
- Closes the session log entry
- Clears the
ICOSYS_SESSIONcookie viaSet-CookiewithMax-Age=0
Returns 204 No Content.
JWT Token Structure¶
Claims¶
{
"sub": "admin",
"userProcessId": 123,
"userCode": "admin",
"userDescription": "System Admin",
"superUser": false,
"tokenVersion": 1,
"iat": 1708088400,
"exp": 1708117200
}
| Claim | Type | Description |
|---|---|---|
sub |
string |
Username (subject) |
userProcessId |
long |
User–account link ID |
userCode |
string |
Username (duplicate for convenience) |
userDescription |
string |
Display name |
superUser |
boolean |
Bypass all role checks |
tokenVersion |
int |
Incremented on password change |
iat |
long |
Issued at (Unix timestamp) |
exp |
long |
Expiration (Unix timestamp) |
Signing¶
| Property | Value |
|---|---|
| Algorithm | HS512 |
| Secret | 64+ character hex string |
| Library | JJWT 0.12.5 |
| Expiration | 8 hours (dev), configurable |
Token Version
The backend checks tokenVersion against the database on every request.
If the user changes their password, tokenVersion is incremented and all
existing tokens are immediately invalid — no waiting for expiration.
Security Filter Chain¶
Requests pass through these filters in order:
Request
│
▼
┌──────────────────┐
│ IpSecurityFilter │ IP whitelist/blacklist (CIDR)
└────────┬─────────┘
▼
┌──────────────────┐
│ ApiKeyFilter │ Validates X-API-Key header
└────────┬─────────┘
▼
┌──────────────────┐
│ RateLimitFilter │ Bucket4j token-bucket per IP
└────────┬─────────┘
▼
┌──────────────────┐
│ JwtAuthFilter │ Extracts JWT from cookie or header
└────────┬─────────┘
▼
┌──────────────────┐
│ Spring Security │ RBAC, method-level @Secured
└────────┬─────────┘
▼
Controller
IP Security Filter¶
Validates client IP against a configurable whitelist/blacklist with CIDR support.
ims.security.ip-filter.enabled=false
ims.security.ip-filter.allowed-ips=192.168.1.0/24,127.0.0.1,::1
Disabled by default in development. Returns 403 Forbidden for blocked IPs.
API Key Filter¶
Protects administrative endpoints. The key is sent in the X-API-Key header.
ims.security.api-key=${ICGLB_SECURITY_API_KEY:dev-secret-key-change-in-production-abc123xyz}
Protected endpoint patterns:
| Pattern | Description |
|---|---|
/api/auth/login |
Login endpoint |
/api/session/** |
Session management |
/api/logs/** |
Exception log queries |
/api/admin/** |
Config monitoring |
Rate Limit Filter¶
Per-IP rate limiting using Bucket4j token-bucket algorithm.
| Endpoint Category | Limit | Refill |
|---|---|---|
Public (/actuator, /api/health) |
60 req/min | Gradual |
| API Key protected | 100 req/min | Gradual |
| JWT protected (all other) | 200 req/min | Gradual |
Exceeding the limit returns:
{
"status": 429,
"error": "Too Many Requests",
"message": "Rate limit exceeded. Maximum 200 requests per minute allowed.",
"retryAfter": 60
}
JWT Authentication Filter¶
Extracts the JWT from two sources (in order):
- Cookie:
ICOSYS_SESSION(browser SPA) - Header:
Authorization: Bearer <token>(Postman, services)
If valid, sets SecurityContext.authentication with the user's roles.
If invalid or expired, returns 401 Unauthorized.
Endpoint Security Categories¶
| Category | Auth Required | Examples |
|---|---|---|
| Public | None | /actuator/health, /api/health |
| Public (DMS) | None | /api/dms-share-link/public/{token} |
| API Key | X-API-Key header |
/api/auth/login, /api/session/**, /api/logs/** |
| JWT | Cookie or Bearer | All CRUD endpoints, business logic |
CORS Configuration¶
| Environment | Allowed Origins | Credentials |
|---|---|---|
| Development | http://localhost:8080, http://localhost:5174 |
true |
| Test | https://test.icosys.com |
true |
| Production | https://app.icosys.com, https://admin.icosys.com |
true |
cors.allowed-origins=http://localhost:8080,http://localhost:5174
cors.allowed-methods=GET,POST,PUT,DELETE,OPTIONS
cors.allowed-headers=*
Vite Proxy Bypass
In development, the Vite dev server proxies API requests to backend ports. Since the browser sees same-origin requests, CORS is effectively bypassed. CORS configuration only matters for direct browser-to-backend calls.
Frontend Integration¶
Axios Instance Configuration¶
const client = axios.create({
baseURL: "/icglb/services",
withCredentials: true, // Send httpOnly cookies
headers: {
"Content-Type": "application/json",
"X-API-Key": import.meta.env.VITE_API_KEY,
"X-Project-Code": "ICOSYS",
"Accept-Language": localStorage.getItem("locale") ?? "tr"
}
})
401 Interceptor¶
client.interceptors.response.use(
response => response,
error => {
if (error.response?.status === 401) {
localStorage.removeItem("icosys_user")
localStorage.removeItem("icosys_auth_state")
window.location.href = "/login"
}
return Promise.reject(error)
}
)
Testing with Postman¶
Step 1 — Login¶
POST http://localhost:8010/icglb/services/api/auth/login
Headers:
Content-Type: application/json
X-API-Key: dev-secret-key-change-in-production-abc123xyz
Body:
{ "userCode": "admin", "password": "secret", "languageCode": "en" }
Step 2 — Copy Cookie¶
From the response Set-Cookie header, copy the ICOSYS_SESSION value.
Step 3 — Use in Requests¶
Postman Auto-Cookie
Enable Settings → Automatically follow redirects and Send cookies in Postman. After login, the cookie is stored and sent automatically.
Environment Variables¶
| Variable | Service | Description |
|---|---|---|
JWT_SECRET |
All | HS512 signing key (64+ hex chars) |
ICGLB_SECURITY_API_KEY |
All | API key for protected endpoints |
DMS_URL_SECRET |
DMS | HMAC secret for signed download URLs |
openssl rand -hex 32 # JWT_SECRET (64 chars)
openssl rand -hex 16 # API keys (32 chars)
What's Next?¶
- Error Codes — IC-* error registry and error response format
- REST API — Full endpoint reference for all services