Skip to content

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

POST /icglb/services/api/auth/login
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

200 OK
{
  "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.

Set-Cookie: ICOSYS_SESSION=eyJhbGciOi...; Path=/; HttpOnly; SameSite=Lax; Max-Age=28800
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:

GET /icglb/services/api/auth/me
curl http://localhost:8010/icglb/services/api/auth/me \
  -H "Cookie: ICOSYS_SESSION=eyJhbGciOi..."
200 OK — Session valid
{
  "success": true,
  "userCode": "admin",
  "superUser": false,
  "userProcess": { ... }
}
401 Unauthorized — Session expired
{
  "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:

POST /icglb/services/api/auth/switch-account
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

POST /icglb/services/api/auth/change-password
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

POST /icglb/services/api/auth/logout
curl -X POST http://localhost:8010/icglb/services/api/auth/logout \
  -H "Cookie: ICOSYS_SESSION=eyJhbGciOi..."

The server:

  1. Closes the session log entry
  2. Clears the ICOSYS_SESSION cookie via Set-Cookie with Max-Age=0

Returns 204 No Content.


JWT Token Structure

Claims

JWT Payload (decoded)
{
  "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.

application-dev.properties
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.

application-dev.properties
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:

429 Too Many Requests
{
  "status": 429,
  "error": "Too Many Requests",
  "message": "Rate limit exceeded. Maximum 200 requests per minute allowed.",
  "retryAfter": 60
}
Retry-After: 60

JWT Authentication Filter

Extracts the JWT from two sources (in order):

  1. Cookie: ICOSYS_SESSION (browser SPA)
  2. 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
application-dev.properties
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

service-clients.ts (simplified)
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

service-clients.ts (simplified)
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" }

From the response Set-Cookie header, copy the ICOSYS_SESSION value.

Step 3 — Use in Requests

GET http://localhost:8010/icglb/services/api/account/ACC-001
Headers:
  Cookie: ICOSYS_SESSION=eyJhbGciOi...
GET http://localhost:8010/icglb/services/api/account/ACC-001
Headers:
  Authorization: Bearer eyJhbGciOi...

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
Generate secure values
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