SniffMEET. AuthManager와 SessionManager의 책임 분리하기

작업 배경 AuthManager와 SessionManager는 서로 매우 밀접하게 연관된 작업을 한다. AuthManager는 사용자의 계정 인증을 담당하고 세션을 받아오며, SessionManager는 그 세션을 관리한다. 하지만 기존 코드에서는 세션 관리를 포함한 대부분의 작업을 AuthManager가 처리하고, SessionManager는 세션을 소유하는 역할만 하고 있었다. 역할 분리 기존에는 AuthManager에서 세션 갱신, 복원, 저장, 로드 작업을 처리하고 있었고, SessionManager는 세션의 만료 여부 확인과 세션 객체를 소유하는 역할만 맡고 있었다. // AuthManager 인터페이스 프로토콜 protocol AuthManager { static var shared: AuthManager { get } var authStateSubject: PassthroughSubject<AuthState, Never> { get set } func signInAnonymously() async throws func restoreSession() async throws func refreshSession() async throws func loadTokens() throws } // SessionManager 클래스 final class SessionManager { static let shared = SessionManager() var session: SupabaseSession? var isExpired: Bool { guard let session else { return true } // 세션 만료를 파악할 때는 30초의 여유시간을 줍니다. return Date(timeIntervalSince1970: TimeInterval(session.expiresAt + 30)) < Date() } private init() {} } 그래서 세션 관리는 SessionManager, 인증은 AuthManager가 맡도록 기능을 다시 정의했다. ...

February 10, 2025

Network. 쿠키, 세션

개요 웹 애플리케이션에서 사용자 정보를 저장하고 관리하는 방식, HTTP는 statelessness이기 때문에, 이전 요청에 대한 정보를 기억하지 않는다. 따라서 이 문제를 해결하기 위해 쿠키와 세션이 필요하다. 쿠키 클라이언트에 저장되는 작은 데이터 파일, 서버가 클라이언트에게 데이터를 저장하도록 지시하면, 해당 데이터를 전송하게 됨 용도 사용자 인증 정보 저장 (로그인 상태 유지) 웹사이트 설정 저장 사용자 행동 추적 특징 주기적 전송: 사용자가 특정 사이트에 다시 방문하면 브라우저가 자동으로 쿠키를 서버로 전송함 수명: 유효 기간을 설정할 수 있고, 만료되면 삭제됨, 유효기간이 없으면 브라우저 세션이 종료될 때 삭제 용량: 한 도메인당 쿠키는 4KB 정도로 제한 보안 사용자의 브라우저에 저장되기 때문에 클라이언트에서 조작할 수 있다. 따라서 민감한 데이터는 쿠키에 저장하면 안됨 ...

February 2, 2025

SniffMEET. anon-key와 익명 사용자의 접근 권한 구분

anon-role과 anon-user의 접근 권한 차이 Public Scheme에 있는 테이블을 anon-key로는 접근할 수 있었지만, anon-user의 JWT 토큰으로는 접근하지 못하는 문제가 있었다. 처음에는 둘이 비슷한 범주라고 생각했지만, 실제로는 권한 체계와 접근 방식이 달라서 같은 문제로 볼 수 없었다. 문제를 확인하기 전 생각한 내용 anon-key, anon-role, anon-user의 차이부터 다시 정리했다. anon-key를 이용한 접근은 anon-role로 보고, anon-user는 이메일 주소나 전화번호를 등록하지 않은 유저를 의미한다. 이 경우 role 자체는 authenticated-role이었다. 익명 유저가 접근했을 때 200 OK가 뜨지만 실제 값을 가져오지 못하는 경우도 있었다. 그래서 접근 자체는 허용되지만 RLS에서 막히고 있다고 추정할 수 있었다. ...

November 28, 2024

SniffMEET. Supabase 세션 갱신 요청이 실패한 이유

문제 Supabase 세션 갱신 요청이 제대로 동작하지 않았다. 세션 로직이나 네트워크 레이어 문제처럼 보였지만, 실제 원인은 요청 형식이었다. 당시 메모는 이랬다. 수정 "grant_type": "refreshToken" grant_type 값을 Swift식 camelCase로 보내고 있었다. Supabase가 기대하는 값은 refresh_token이었다. "grant_type": "refresh_token" body도 마찬가지였다. Supabase가 기대하는 key는 refreshToken이 아니라 refresh_token이었다. refresh 요청에서는 기존 access token의 Authorization 헤더도 제거했다. (토큰을 갱신받기 위한 요청인데, 만료된 토큰을 보내는거니까 아무 의미 없다!) body: Data("{ \"refresh_token\": \"\(refreshToken)\" }".utf8) header["Authorization"] = nil refresh 요청은 기존 access token으로 리소스에 접근하는 요청이 아니라, refresh token으로 새 세션을 발급받는 요청이기 때문이다. ...

November 21, 2024

SniffMEET. JWT와 Supabase RLS 정책에 연결하기

Supabase Auth로 로그인하면 클라이언트는 Access Token으로 JWT를 받는다. Supabase는 요청에 포함된 JWT를 검증하고, 그 안의 사용자 정보와 역할을 RLS 정책에 전달한다. JWT의 클레임 JWT는 헤더, 페이로드, 서명으로 구성된다. 각 부분은 Base64URL로 인코딩되지만 암호화되지는 않으므로 페이로드에 비밀 정보를 넣으면 안 된다. 서명은 헤더와 페이로드가 변조되지 않았는지 검증하는 데 사용한다. Supabase의 JWT에는 다음과 같은 클레임이 포함된다. sub: 사용자의 고유 ID role: RLS를 적용할 PostgreSQL 역할 iat: 토큰이 발급된 시각 exp: 토큰이 만료되는 시각 Supabase에서 JWT 사용하기 RLS는 PostgreSQL이 행 단위로 접근을 제한하는 기능이다. Supabase에서는 auth.uid()로 현재 JWT의 sub 값을 가져와 행의 사용자 ID와 비교할 수 있다. ...

November 20, 2024

SniffMEET. 익명 로그인 세션의 생성·복원·갱신 흐름

익명 로그인 생성, 복원, 갱신 로직 구현 JWT, Refresh Token, 세션의 개념이 아직 명확하게 정리되지 않은 상태에서 익명 계정을 먼저 만들게 됐다. 개념이 명확하지 않아서, 세션 관리에 꽤 애를 먹었는데, 앱 재실행 이후에도 익명 계정을 어떻게 이어서 사용할지부터 정리할 필요가 있었다. 생성, 복원, 갱신 로직은 서로 밀접하게 연결되어 있어서, 처음 앱을 실행했을 때와 다시 실행했을 때의 흐름을 먼저 구분해야 했다. 실제 문제 해결 과정 먼저 시점별로 세션 상태를 표로 정리했다. ...

November 14, 2024