Auth Service
This is the reference page for lumie-backend/modules/auth, the tenant-aware authentication module that owns registration, session issuance, JWT validation, profile management, and password-based sign-in.
Source Paths
| Path | Role |
|---|---|
lumie-backend/modules/auth/src/main/java/com/lumie/auth/adapter/in/web/AuthController.java | Public HTTP surface |
lumie-backend/modules/auth/src/main/java/com/lumie/auth/application/service/{AuthRegistrationService,AuthSessionService,AuthQueryService,UserProfileService}.java | Main registration, login, refresh, session, and profile use cases |
lumie-backend/modules/auth/src/main/java/com/lumie/auth/application/service/LoginSessionHelper.java | Shared token issuance, Redis session persistence, and single-device-category enforcement |
lumie-backend/modules/auth/src/main/java/com/lumie/auth/domain/vo/SessionRevocationReason.java | Session revocation reason values used when refresh failures need user-facing semantics |
lumie-backend/modules/auth/src/main/java/com/lumie/auth/adapter/in/web/CookieUtils.java | Shared auth-cookie builder for access and refresh session cookies |
lumie-backend/modules/auth/src/main/java/com/lumie/auth/adapter/in/security/JwtAuthenticationFilter.java | JWT extraction from bearer tokens or lumie_access_token cookie |
lumie-backend/modules/auth/src/main/java/com/lumie/auth/adapter/out/persistence/RedisTokenRepository.java | Refresh-token, blacklist, and session storage in Redis |
lumie-backend/modules/auth/src/main/java/com/lumie/auth/adapter/in/internal/AuthServiceAdapter.java | Published in-process auth contract for other modules |
lumie-backend/modules/auth/src/main/java/com/lumie/auth/domain/entity/User.java | Tenant-scoped users entity |
lumie-backend/app/src/main/java/com/lumie/app/config/{SecurityConfig,internal/InternalHmacAuthFilter}.java | App-level security rules and internal HMAC protection that the auth module participates in |
lumie-backend/app/src/main/resources/{application.yaml,application-dev.yml} | Cookie defaults and dev-only SameSite overrides |
lumie-backend/app/src/main/resources/db/migration/public/{V2__create_users_table,V4__federated_identities,V12__slim_users_to_owner_directory,V18__rls_baseline,V22__introduce_owner_directory}.sql | Core auth-facing schema evolution for tenant users, root-entry owner lookup, and historical federated-login artifacts that may still exist in upgraded databases |
Public Surface
| Endpoint | Purpose |
|---|---|
POST /v1/register | Student self-registration inside an existing tenant |
POST /v1/register/owner | Owner registration plus brand-new tenant creation |
POST /v1/login, POST /v1/refresh, POST /v1/logout, POST /v1/logout-all | Session lifecycle |
GET /v1/me, PATCH /v1/me, POST /v1/me/password, PATCH /v1/me/avatar | Profile, password, and avatar management |
GET /v1/me/sessions, DELETE /v1/me/sessions/{sid}, DELETE /v1/me/sessions | Session inspection and revocation |
The controller sets lumie_access_token and lumie_refresh_token cookies on successful registration, login, and refresh.
No GET /v1/oauth2/{provider}/... endpoints remain in AuthController, and the supported runtime contract no longer includes Kakao, Google, or Naver sign-in.
Internal Surface And Dependencies
| Surface | Role |
|---|---|
lumie-backend/libs/internal-api/src/main/java/com/lumie/auth/api/AuthService.java | In-process contract for token validation, user lookup, user creation, password reset, login-ID change, and avatar seed lookup |
lumie-backend/libs/internal-api/src/main/java/com/lumie/auth/api/OwnerRegisteredEvent.java | AFTER_COMMIT event that downstream modules use to bootstrap OWNER-linked records |
TenantService | Used both for registration-time tenant creation and for login-time tenant validation |
| Redis token/session store | Refresh tokens, token blacklist entries, and per-user session metadata are stored outside PostgreSQL |
Aggregate And State
| Entity or store | Notes |
|---|---|
User | Tenant-scoped user row under RLS |
AuthToken | Refresh-token value object persisted in Redis rather than JPA |
| Redis sessions | JSON session metadata keyed by tenant and session ID, including device category and token JTIs |
| Redis revocation reasons | auth:session-revoked:{tenantSlug}:{sid} records why a missing refresh token was revoked while the reason remains useful to the browser |
owner_directory | Root-entry lookup table used to resolve an OWNER's tenant before a tenant context already exists |
Historical Schema Artifacts
Applied databases may still carry legacy federated-login artifacts from earlier migrations, but the supported runtime auth contract no longer depends on them:
federated_identitiesmay still exist fromV4__federated_identities.sqlusers.oauth_providerappears in the early users-table migrations such asV2__create_users_table.sqlandV18__rls_baseline.sql
The runtime no longer reads or writes either artifact. Leave them untouched during the code-removal deploy so already-upgraded databases can keep serving the password-login, refresh, and profile flows without a same-release contract change.
After deploy verification confirms the password-based auth paths are healthy, a later forward-only contract migration can archive or drop the unused table and column. Do not treat untracked cleanup drafts as active migrations until that follow-up is actually authored and landed.
Runtime Flow
Contract Notes
The login path truly supports two entry styles: portal-entry requests with an existing tenant context, and root-entry owner logins that discover tenant context first.
The published internal API also has one notable field mismatch today.
Source anchor: lumie-backend/modules/auth/src/main/java/com/lumie/auth/adapter/in/internal/AuthServiceAdapter.java, AuthServiceAdapter#getUserInfo.
// lumie-backend/modules/auth/src/main/java/com/lumie/auth/adapter/in/internal/AuthServiceAdapter.java
return Optional.of(new UserData(
user.id(), user.userLoginId(), user.name(),
user.role().name(), claims.tenantSlug(), claims.tenantId()
));
AuthService.UserData names its second field email, but AuthServiceAdapter currently fills it with userLoginId. Callers should treat that field as login identity, not guaranteed email, until the interface is corrected.
Example Contracts
These examples come directly from AuthController, LoginRequest, AuthResponse, and UserResponse.
Login
POST /v1/login
Content-Type: application/json
{
"userLoginId": "alice_owner",
"password": "SecretPass123!",
"customId": "acme"
}
HTTP/1.1 200 OK
Set-Cookie: lumie_access_token=<jwt>; HttpOnly; ...
Set-Cookie: lumie_refresh_token=<jwt>; HttpOnly; ...
{
"accessExpiresIn": <seconds>,
"refreshExpiresIn": <seconds>,
"user": {
"id": 42,
"userLoginId": "alice_owner",
"name": "Alice",
"phone": "01000000000",
"email": "alice@example.com",
"role": "OWNER",
"tenantSlug": "acme",
"tenantId": 7,
"tenantCustomId": "acme",
"tenantCustomDomain": "academy.example.com",
"tenantOnboardingCompletedAt": null,
"avatarSeed": "seed-1"
}
}
The body never carries JWTs. AuthController.buildAuthResponse(...) writes both tokens to cookies and returns only expiry metadata plus user.
UserResponse includes tenantCustomId and tenantCustomDomain so the frontend can preserve the correct tenant login surface during auth-boundary redirects even when the current tenant query cache is cold. Those fields are populated by AuthSessionService, AuthQueryService, and UserProfileService from TenantService.TenantData, not from browser input.
Refresh
POST /v1/refresh is cookie-only. AuthController.refresh(...) ignores any JSON body and reads lumie_refresh_token from the cookie.
POST /v1/refresh
Cookie: lumie_refresh_token=<refresh-jwt>
HTTP/1.1 200 OK
Set-Cookie: lumie_access_token=<rotated-jwt>; HttpOnly; ...
Set-Cookie: lumie_refresh_token=<rotated-jwt>; HttpOnly; ...
{
"accessExpiresIn": <seconds>,
"refreshExpiresIn": <seconds>,
"user": null
}
When a previous session was replaced by a new login in the same device category, the old browser's next refresh attempt returns a ProblemDetail response with code: "AUTH_017". Source anchors: AuthSessionService.refresh(...), LoginSessionHelper.enforceSessionLimitByCategory(...), and RedisTokenRepository.findSessionRevocationReason(...).
HTTP/1.1 401 Unauthorized
Content-Type: application/problem+json
{
"type": "urn:lumie:error:auth-017",
"title": "Session replaced by another login",
"status": 401,
"detail": "Session replaced by another login",
"code": "AUTH_017"
}
Failure And Runtime Behavior
JwtAuthenticationFilteraccepts eitherAuthorization: Bearer ...or thelumie_access_tokencookie.refresh(...)rejects blacklisted tokens and rotates both access and refresh tokens together.LoginSessionHelperenforces one active session per device category by revoking prior sessions in the same category before issuing new tokens.- When that category enforcement replaces an existing session,
LoginSessionHelperstoresSESSION_REPLACED_BY_LOGINin Redis before deleting the session metadata.AuthSessionService.refresh(...)maps that reason back toAUTH_017when the old browser presents the now-missing refresh token. AuthSessionService.resolveLoginTenant(...)rejects inactive tenants for both root-entry and portal-entry logins.- Owner registration publishes
OwnerRegisteredEventafter the OWNER user and tenant are created, breaking the old auth-to-staff synchronous dependency. /internal/**routes are protected byInternalHmacAuthFilter, not by end-user JWTs.
Verification
cd lumie-backend
./gradlew :modules:auth:test
./gradlew :modules:auth:test --tests '*AuthControllerMeTest'
./gradlew :modules:tenant:test :modules:notification:test
Expected success signals:
- Gradle exits with
BUILD SUCCESSFUL, and the auth module tests still cover the password-based registration, login, refresh, and/v1/meflows. AuthControllerMeTeststill proves that the controller resolves the authenticated user profile, emits the tenant URL fields onUserResponse, and maps replaced-session refresh failures toAUTH_017.- The tenant and notification module tests prove the updated
TenantService.TenantDatainternal contract still compiles across modules that consume tenant metadata.