Security

Security Model

Security is implemented via Spring Security in SecurityConfig (src/main/java/com/friendly/ftconfigsservice/utils/config/SecurityConfig.java).

Key properties:

  • Stateless sessions (SessionCreationPolicy.STATELESS)

  • CSRF enabled with cookie-based repository (CookieCsrfTokenRepository.withHttpOnlyFalse())

  • Authentication: JWT access token read from HTTP-only cookie with Authorization: Bearer … fallback (JwtAuthenticationFilter)

Roles and Authorities

The service uses three roles (Role enum in src/main/java/com/friendly/ftconfigsservice/auth/model/Role.java):

  • VIEWERconfigs.read

  • EDITORconfigs.read, configs.write

  • ADMINconfigs.read, configs.write, users.manage

Authorization is enforced using authorities (not only ROLE_*).

Password validation rules

All password fields enforce the following constraints:

  • Length: 8—​64 characters

  • Complexity: must contain at least one lowercase letter, one uppercase letter, one digit, and one of @$!%*?&

The 64-character upper limit is enforced on all password inputs to prevent BCrypt denial-of-service (BCrypt processes at most 72 bytes; excessively long inputs waste CPU time before being truncated).

Specific endpoint constraints:

DTO Field Validation

LoginRequest

password

@Size(min=8, max=64)

FirstLoginRequest

temporaryPassword

@Size(max=64) (BCrypt DoS protection)

ChangePasswordRequest

currentPassword

@Size(max=64) (BCrypt DoS protection)

User secret delivery

In offline mail mode (see Configuration — Delivery modes), admin user-management endpoints return secrets directly in the API response instead of emailing them:

  • Create user (POST /api/admin/users) and reset password (POST /api/admin/users/{id}/reset-password) return the temporary password in the response body with passwordDelivery=RETURNED.

  • User deletion request (POST /api/admin/users/{id}/deletion/request) issues no OTP/token and returns otpRequired=false; the account is deleted in the /deletion/confirm step, which is called without a token/OTP (gated by OTP delivery being unavailable).

This is also the runtime fallback under AUTO when SMTP is configured but the server is unreachable: create/reset degrade to passwordDelivery=RETURNED, and user deletion degrades to the no-OTP path. To make the tokenless confirm safe, it is gated by MailDeliveryStatus.isOtpConfirmationAvailable(), which is false when configured offline or when a live SMTP connectivity probe fails — so a tokenless confirm is rejected (HTTP 400, "Deletion token is required") whenever OTP delivery is actually working, and accepted only when it is not.

Returning a temporary password (or deleting without OTP) is acceptable because:

  • the endpoints are admin-only (@PreAuthorize("hasAuthority('users.manage')"));

  • traffic is served over HTTPS;

  • the temporary password forces a password change on next login (passwordChangeRequired=true); and

  • the temporary password is never written to logs.

In online mode the deletion OTP is emailed to the initiating admin, so that admin must have an email address; otherwise the request fails with error code USER_DELETION_EMAIL_REQUIRED. This email check is skipped in offline mode (configured or degraded at runtime).

API contract (breaking for the frontend)

Offline mode and the runtime fallback changed the response bodies of the admin user-management endpoints. HTTP status codes are unchanged; only the bodies changed.

Endpoint Response body

POST /api/admin/users

CreateUserResponse: { user: UserSummaryDto, temporaryPassword: string|null, passwordDelivery: "EMAIL"|"RETURNED" }. Previously the body was the UserSummaryDto at the root. temporaryPassword is non-null only when passwordDelivery=RETURNED. UserSummaryDto fields: id, username, email, role, enabled, passwordChangeRequired, locale, lastLoginAt.

POST /api/admin/users/{id}/reset-password

ResetPasswordResponse: { temporaryPassword: string|null, passwordDelivery: "EMAIL"|"RETURNED" }. Previously returned an empty 200 body.

POST /api/admin/users/{id}/deletion/request

UserDeletionRequestResponse gained otpRequired. Online: { token, expiresAt, otpLength, attemptLimit, otpRequired: true }. Offline (including AUTO with SMTP unreachable): { token: null, expiresAt: null, otpLength: 0, attemptLimit: 0, otpRequired: false }. No deletion happens here in either case.

POST /api/admin/users/{id}/deletion/confirm

Body { token, otp }204; performs the deletion. Online: send token + otp. Offline (otpRequired=false): send both as null — the account is deleted directly (allowed only while OTP delivery is unavailable). token and otp are now optional (no longer bean-validated).

The deletion flow is implemented as a router (UserDeletionService) plus an audited bean (UserDeletionOperations). The deletion always happens in the confirm step. Audit operation types: an online request emits CREATE / context UserDeletionIntent, while every actual deletion — both the offline no-OTP confirm and the online OTP-confirmed delete — emits DELETE / context UserDeletion.

Dead code removed

The following registration-related code has been removed as dead code:

  • RegisterRequest DTO

  • RegisterResponse DTO

  • AuthService.register() method

  • /auth/register endpoint removed from SecurityConfig public path list

Registration is now handled exclusively through the admin user-creation flow in UserAdminService.

Endpoint Access Rules (high level)

Configured in SecurityConfig:

  • Public:

    • GET /csrf and GET /configs-service/csrf

    • GET /environment/snapshot

    • POST /auth/login, POST /auth/refresh-token, POST /auth/first-login

    • /actuator/, /v3/api-docs/, /swagger-ui/**

  • Admin-only:

    • /api/admin/**, /auth/users/role require users.manage

  • Configuration domains:

    • GET on domain paths requires configs.read

    • POST/PUT/PATCH/DELETE on domain paths requires configs.write

JWT Details

JwtService (src/main/java/com/friendly/ftconfigsservice/utils/security/JwtService.java) signs tokens using an HMAC key decoded from Base64:

  • jwt.secret is decoded via io.jsonwebtoken.io.Decoders.BASE64.

  • Access token lifetime: jwt.expiration (ms).

  • Refresh token lifetime: jwt.refresh-token.expiration (ms).

Tokens are issued on login and stored in cookies by CookieUtil (ACCESS_TOKEN, REFRESH_TOKEN).

Bootstrap Admin

AdminBootstrap provisions the first admin user on startup when:

  • no existing ADMIN role user exists, and

  • ft-configs.bootstrap.admin.username and ft-configs.bootstrap.admin.password are set.

The bootstrap account is created with passwordChangeRequired=true; the rotation flow is handled by POST /auth/first-login (AuthService.completeFirstLogin).

Security Boundary: Hazelcast Consumers

Runtime consumers that read Hazelcast directly (ACS, northbound-api, provisionportal, serviceapi, angular backend) bypass REST auth entication and authorization completely.

Operational implication:

  • Treat Hazelcast network access as production-critical security boundary (network policies, firewall rules, cluster auth/TLS if configured).