# fix/sso-refresh-session-restore 설명 문서 ## 1. 문서 목적 이 문서는 backend 프로젝트 `api`의 `fix/sso-refresh-session-restore` 브랜치에서 반영한 **SSO 세션 복원 및 토큰 응답 보강** 변경을 설명합니다. 핵심 목적은 다음 두 가지입니다. - `ALIST_SSO` 쿠키가 없더라도, 유효한 `refreshToken`이 있으면 SSO 세션을 복원할 수 있게 한다. - frontend direct API 흐름에서 필요한 `accessToken` 값을 `/auth/access`, `/auth/refresh` 응답 body로도 내려준다. --- ## 2. 배경 문제 기존 흐름에서는 SSO 관련 엔드포인트가 주로 `ALIST_SSO` 쿠키를 기준으로 로그인 상태를 판단했습니다. 이 구조에서는 다음 문제가 있었습니다. 1. 브라우저에 `refreshToken`은 남아 있지만 `ALIST_SSO` 쿠키가 사라진 경우 - `/sso/loginChecked` - `/sso/authorize` - `/auth/access` 에서 로그인 상태를 복원하지 못했습니다. 2. frontend가 direct API 흐름으로 변경되면서 - `POST /auth/access` - `POST /auth/refresh` 응답 body의 `accessToken`을 직접 사용하도록 바뀌었는데, backend 응답은 cookie만 세팅하고 body에는 토큰을 충분히 내려주지 않았습니다. 즉, **refreshToken 기반 세션 복원**과 **frontend 계약(body accessToken)** 이 동시에 필요했습니다. --- ## 3. 변경 대상 파일 이 브랜치의 핵심 변경 파일은 아래 7개입니다. - `src/main/java/com/alist/api/modules/auth/AuthController.java` - `src/main/java/com/alist/api/modules/auth/SsoController.java` - `src/main/java/com/alist/api/modules/auth/dto/SsoAuthorizeDto.java` - `src/main/java/com/alist/api/modules/auth/service/AuthService.java` - `src/main/java/com/alist/api/modules/auth/service/SsoService.java` - `src/main/java/com/alist/api/modules/auth/vo/LoginTokenVo.java` - `src/main/resources/mapper/auth/LoginMapper.xml` --- ## 4. 핵심 변경 요약 ### 4-1. `refreshToken` 기반 SSO 세션 복원 추가 `SsoService`에 아래 역할이 추가되었습니다. - `loginChecked(String ssoSessionId, String refreshToken)` - `resolveOrRestoreSsoSessionId(String ssoSessionId, String refreshToken)` - `restoreSsoSessionIfNeeded(String ssoSessionId, String refreshToken)` - `loadSsoSession(String ssoSessionId)` 동작 방식은 아래와 같습니다. 1. 먼저 기존 `ALIST_SSO` 쿠키 값(`ssoSessionId`)이 있으면 그대로 사용합니다. 2. `ssoSessionId`가 없으면 `refreshToken`으로 사용자 토큰 정보를 조회합니다. 3. 유효한 사용자가 확인되면 - 기존 최신 SSO 세션이 있으면 TTL을 연장해서 재사용하고 - 없으면 Redis에 새로운 SSO 세션을 다시 생성합니다. 4. 이후 `loginChecked`, `authorize`, `access`는 복원된 SSO 세션 기준으로 정상 동작합니다. 즉, **SSO 쿠키가 비어 있어도 refreshToken만 유효하면 공통 로그인 상태를 다시 살릴 수 있게 변경**되었습니다. --- ### 4-2. `/auth/access` 응답 강화 `AuthController.access()` 변경 내용: - `@CookieValue(name = "refreshToken", required = false)`를 추가하여 refreshToken을 입력으로 받습니다. - `ssoService.loginChecked(ssoSessionId, refreshToken)`을 사용해 SSO 세션을 복원 가능하게 했습니다. - 세션 복원 결과에 `ssoSessionId`가 있으면 `ALIST_SSO` 쿠키를 다시 발급합니다. - 응답 body에 아래 값을 포함합니다. - `isAccessToken` - `userIdx` - `userId` - `userRole` - `accessToken` 즉, `/auth/access`는 이제 **토큰 쿠키 발급 + frontend용 JSON body accessToken 제공**을 같이 수행합니다. --- ### 4-3. `/auth/refresh` 응답 강화 `AuthController.refresh()` 변경 내용: - 기존처럼 `accessToken`, `refreshToken` 쿠키는 재발급합니다. - 추가로 응답 body에 아래 값을 내려줍니다. - `refreshed: true` - `accessToken` 이 변경으로 frontend는 refresh 성공 후 cookie에만 의존하지 않고, **응답 body의 accessToken으로 즉시 service-local 토큰을 다시 기록**할 수 있습니다. --- ### 4-4. `/sso/loginChecked` 개선 `SsoController.loginChecked()` 변경 내용: - `refreshToken` cookie를 함께 입력으로 받습니다. - `ssoService.loginChecked(ssoSessionId, refreshToken)`을 사용합니다. - access/refresh token 존재 여부를 기존처럼 함께 반환합니다. - SSO 세션이 복원되면 `ALIST_SSO` 쿠키를 다시 써줍니다. 즉, `/sso/loginChecked`는 더 이상 단순 조회가 아니라 **필요 시 세션 복원까지 수행하는 확인 엔드포인트**가 되었습니다. --- ### 4-5. `/sso/authorize` 개선 `SsoController.authorize()`와 `SsoService.authorize()` 변경 내용: - `refreshToken` cookie를 함께 전달받아 세션 복원에 사용합니다. - authorize 성공 시 `SsoAuthorizeDto`에 `ssoSessionId`를 실어주고, - controller에서 해당 값을 기준으로 `ALIST_SSO` 쿠키를 재발급합니다. 즉, 사용자가 이미 refreshToken을 가지고 있다면 **SSO 쿠키가 비어 있어도 authorize 진입 시 다시 공통 로그인 상태를 회복**할 수 있습니다. --- ### 4-6. refreshToken 조회용 Auth 계층 보강 `AuthService`와 DB 조회 쪽에는 아래 보강이 들어갔습니다. - `findUserTokenByRefreshToken(String refreshToken)` 추가 - `LoginTokenVo`에 `userIdx`, `userId` 필드 추가 - `LoginMapper.xml`의 `selectUserTokenByUserTokenIdx`가 `user_idx`, `user_id`, `user_role`, `refresh_token`을 함께 조회 이 변경은 SSO 세션 복원 시, 단순 token 유효성 확인을 넘어서 **어떤 사용자의 세션을 복구해야 하는지 식별하기 위해 필요**합니다. --- ## 5. 변경 후 기대 동작 ### 시나리오 A. `ALIST_SSO` 없음 + `refreshToken` 유효 - `POST /auth/access` - SSO 세션 복원 성공 - `ALIST_SSO` 재발급 - `accessToken` / `refreshToken` 재발급 - body에 `accessToken` 포함 ### 시나리오 B. `GET /sso/loginChecked` - 기존에는 로그인 false로 끝날 수 있었던 상황에서 - 이제 refreshToken이 유효하면 loggedIn true로 복원 가능 ### 시나리오 C. `GET /sso/authorize` - 기존에는 `ALIST_SSO` 없으면 `/login`으로 fallback - 이제 refreshToken으로 세션 복원 가능하면 정상 authorize 진행 ### 시나리오 D. `POST /auth/refresh` - cookie 재발급뿐 아니라 body에도 `accessToken` 제공 - frontend가 바로 service-local accessToken을 다시 쓸 수 있음 --- ## 6. frontend와의 계약 변화 이 브랜치는 frontend의 direct API 흐름과 맞물려 있습니다. frontend는 현재 아래 계약을 기대합니다. - `POST /auth/access` 응답 body에 `accessToken` - `POST /auth/refresh` 응답 body에 `accessToken` 따라서 이 브랜치 변경은 단순 backend 내부 개선이 아니라, **frontend의 direct login / callback / refresh 흐름을 안정적으로 지원하는 계약 변경**이기도 합니다. --- ## 7. 운영상 의미 이 변경을 적용하면 다음이 좋아집니다. - SSO 쿠키 유실 상황에서 세션 복원력이 높아짐 - cross-domain authorize 진입 시 로그인 유지가 더 안정적임 - frontend가 refresh 후 토큰 재반영을 더 단순하게 처리할 수 있음 - `ALIST_SSO`와 `refreshToken`을 각각의 역할에 맞게 유지하면서도 사용자 체감 로그인 끊김을 줄일 수 있음 --- ## 8. 검증 포인트 배포 또는 MR 검토 시 아래를 확인하는 것이 좋습니다. 1. `POST /auth/access` - `ALIST_SSO` 없이 `refreshToken`만 있을 때 200 또는 정상 복원되는지 - response body에 `accessToken`이 포함되는지 2. `POST /auth/refresh` - response body에 `accessToken`이 포함되는지 3. `GET /sso/loginChecked` - `refreshToken`만으로 `loggedIn=true`가 가능한지 4. `GET /sso/authorize` - `ALIST_SSO`가 없더라도 refreshToken 기반으로 redirect가 이어지는지 5. Redis 상태 - `alist:sso:userIdx:*` - `alist:sso:session:*` 키가 복원/갱신되는지 --- ## 9. 한 줄 요약 `fix/sso-refresh-session-restore` 브랜치는 **refreshToken만 남아 있는 상황에서도 SSO 세션을 복원하고, `/auth/access` 및 `/auth/refresh`가 frontend가 바로 사용할 수 있는 `accessToken`을 응답 body로 반환하도록 보강한 변경**입니다.