본문으로 건너뛰기

인증 서비스

이 페이지는 등록, 세션 발급, JWT 검증, 프로필 관리, OAuth2 로그인을 담당하는 tenant-aware 인증 모듈 lumie-backend/modules/auth의 레퍼런스입니다.

소스 경로

Path역할
lumie-backend/modules/auth/src/main/java/com/lumie/auth/adapter/in/web/{AuthController,OAuth2Controller}.java공개 HTTP surface
lumie-backend/modules/auth/src/main/java/com/lumie/auth/application/service/{AuthRegistrationService,AuthSessionService,AuthQueryService,UserProfileService,OAuth2AuthService}.java등록, 로그인, refresh, OAuth2, profile의 주요 use case
lumie-backend/modules/auth/src/main/java/com/lumie/auth/application/service/LoginSessionHelper.java공통 token 발급, Redis session 저장, 디바이스 카테고리별 단일 세션 강제
lumie-backend/modules/auth/src/main/java/com/lumie/auth/adapter/in/security/JwtAuthenticationFilter.javabearer token 또는 lumie_access_token cookie에서 JWT 추출
lumie-backend/modules/auth/src/main/java/com/lumie/auth/adapter/out/persistence/RedisTokenRepository.javaRedis에 refresh token, blacklist, session 저장
lumie-backend/modules/auth/src/main/java/com/lumie/auth/adapter/in/internal/AuthServiceAdapter.java다른 모듈에 공개되는 프로세스 내부 auth 계약
lumie-backend/modules/auth/src/main/java/com/lumie/auth/domain/entity/User.javatenant 범위 users 엔티티
lumie-backend/app/src/main/java/com/lumie/app/config/{SecurityConfig,internal/InternalHmacAuthFilter}.javaauth 모듈이 참여하는 앱 수준 security 규칙과 내부 HMAC 보호
lumie-backend/app/src/main/resources/db/migration/public/{V2__create_users_table,V22__introduce_owner_directory,V12__slim_users_to_owner_directory}.sqltenant user와 root-entry owner lookup을 위한 핵심 auth 스키마 변경

공개 인터페이스

Endpoint목적
POST /v1/register기존 tenant 내부의 student self-registration
POST /v1/register/ownerOWNER 등록과 완전히 새로운 tenant 생성
POST /v1/login, POST /v1/refresh, POST /v1/logout, POST /v1/logout-all세션 lifecycle
GET /v1/me, PATCH /v1/me, POST /v1/me/password, PATCH /v1/me/avatar프로필, 비밀번호, avatar 관리
GET /v1/me/sessions, DELETE /v1/me/sessions/{sid}, DELETE /v1/me/sessions세션 조회와 revoke
GET /v1/oauth2/{provider}/authorize, GET /v1/oauth2/{provider}/callbackgoogle, kakao, naver용 OAuth2 로그인

컨트롤러는 등록, 로그인, refresh, OAuth2 callback 완료 시 lumie_access_tokenlumie_refresh_token cookie를 설정합니다.

내부 인터페이스와 의존성

Surface역할
lumie-backend/libs/internal-api/src/main/java/com/lumie/auth/api/AuthService.javatoken 검증, user 조회, user 생성, 비밀번호 재설정, login ID 변경, avatar seed 조회를 위한 프로세스 내부 계약
lumie-backend/libs/internal-api/src/main/java/com/lumie/auth/api/OwnerRegisteredEvent.javadownstream 모듈이 OWNER 연결 레코드를 bootstrap하는 데 쓰는 AFTER_COMMIT event
TenantService등록 시 tenant 생성과 로그인 시 tenant 검증에 모두 사용
Redis token/session storerefresh token, token blacklist 항목, 사용자별 session metadata를 PostgreSQL 밖에 저장

집계와 상태

Entity or store참고
UserRLS 아래의 tenant 범위 user row
AuthTokenJPA가 아니라 Redis에 저장되는 refresh-token value object
Redis sessionstenant와 session ID를 키로 하고 device category와 token JTI를 포함하는 JSON session metadata
owner_directorytenant context가 아직 없을 때 OWNER의 tenant를 해석하는 root-entry lookup table

런타임 흐름

계약 참고 사항

로그인 경로는 실제로 두 가지 진입 방식을 지원합니다. 기존 tenant context가 있는 포털 진입 요청과, 먼저 tenant context를 찾아야 하는 root-entry OWNER 로그인입니다.

공개된 internal API에는 현재 눈에 띄는 필드 불일치도 하나 있습니다.

// 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는 두 번째 필드 이름을 email로 정의하지만, AuthServiceAdapter는 현재 그 자리에 userLoginId를 채웁니다. 인터페이스가 수정되기 전까지 호출자는 이 필드를 반드시 이메일이라고 가정하지 말고 로그인 식별자로 취급해야 합니다.

예시 계약

이 예시는 AuthController, LoginRequest, AuthResponse, UserResponse에서 직접 가져왔습니다.

로그인

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,
"tenantOnboardingCompletedAt": null,
"avatarSeed": "seed-1"
}
}

본문에는 JWT가 들어가지 않습니다. AuthController.buildAuthResponse(...)가 두 token을 모두 cookie에 기록하고, 응답 본문에는 만료 정보와 user만 돌려줍니다.

토큰 갱신

POST /v1/refresh는 cookie 전용입니다. AuthController.refresh(...)는 JSON body를 무시하고 cookie에서 lumie_refresh_token을 읽습니다.

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
}

실패, 재시도, 관측성

  • JwtAuthenticationFilterAuthorization: Bearer ... 또는 lumie_access_token cookie를 모두 받아들입니다.
  • refresh(...)는 blacklist 처리된 token을 거부하고 access/refresh token을 함께 회전시킵니다.
  • LoginSessionHelper는 새 token을 발급하기 전에 같은 device category의 기존 session을 revoke하여 디바이스 카테고리당 하나의 활성 세션만 허용합니다.
  • AuthSessionService.resolveLoginTenant(...)는 root-entry와 portal-entry 로그인 모두에서 비활성 tenant를 거부합니다.
  • OWNER 등록은 OWNER user와 tenant가 생성된 뒤 OwnerRegisteredEvent를 발행하여, 예전의 auth-to-staff 동기 의존성을 끊었습니다.
  • /internal/** route는 end-user JWT가 아니라 InternalHmacAuthFilter로 보호됩니다.

검증

cd lumie-backend
./gradlew :modules:auth:test
./gradlew :modules:auth:test --tests '*AuthSessionService*'
./gradlew :modules:auth:test --tests '*AuthRegistrationService*'

예상 성공 신호:

  • Gradle이 BUILD SUCCESSFUL로 종료되고, auth 모듈 테스트가 여전히 로그인, refresh, 등록 경로를 다룹니다.
  • AuthController.refresh(...)가 여전히 lumie_refresh_token만 읽고, AuthResponse.from(TokenResponse)가 refresh 응답에서 usernull로 직렬화합니다.

관련 페이지