四つのフローとその居場所
한국어 원문으로 표시합니다.
한 줄 요약
공개 클라이언트에는 authorization code + PKCE, 서버 간 통신에는 client credentials. 나머지는 대부분 쓰지 말아야 할 이유가 있는 흐름이다.
왜 이게 필요했나
"구글 로그인 버튼 하나에 왜 이렇게 복잡한 프로토콜이 필요한가"라는 질문이 자연스럽습니다. 답은 이것입니다. 사용자의 비밀번호를 제3자 앱에 주지 않으면서, 그 앱이 사용자를 대신해 제한된 자원에 접근하게 하려는 것입니다.
이 목표를 위해 여러 흐름이 만들어졌고, 시간이 지나며 몇 개는 폐기됐습니다.
어떻게 동작하나
Authorization Code 가 기본이자 표준입니다. 사용자가 인증 서버로 리다이렉트되어 로그인하고, 인가 코드를 받아 앱으로 돌아오고, 앱의 백엔드가 그 코드를 토큰으로 교환합니다. 코드가 브라우저 URL 에 노출되지만 1회용이고 단독으로는 쓸 수 없다는 점이 핵심입니다. 교환에는 클라이언트 시크릿이 필요합니다.
문제는 SPA 와 모바일 앱입니다. 이들은 시크릿을 안전하게 보관할 수 없습니다. 소스에 넣으면 누구나 볼 수 있습니다. 그래서 예전에는 Implicit 흐름 — 코드 없이 토큰을 바로 URL 프래그먼트로 받는 방식 — 을 썼습니다. 토큰이 브라우저 히스토리와 리퍼러에 남는 심각한 문제가 있어 지금은 권장되지 않습니다.
대체가 PKCE(Proof Key for Code Exchange)입니다. 클라이언트가 매 요청마다 무작위 문자열 code_verifier 를 만들고, 그 SHA256 해시를 Base64URL 로 인코딩한 code_challenge 를 인가 요청에 실어 보냅니다. 나중에 코드를 토큰으로 교환할 때 원본 code_verifier 를 제출합니다. 인증 서버는 해시를 다시 계산해 맞춰 봅니다. 공격자가 인가 코드를 가로채도 code_verifier 가 없으면 토큰을 받지 못합니다. 시크릿 없이도 안전해지는 것입니다.
Client Credentials 는 사용자가 개입하지 않는 서버 간 통신용입니다. 배치 작업, 서비스 계정, 내부 API 호출. 사용자 컨텍스트가 없으므로 sub 는 서비스 계정입니다.
Resource Owner Password 는 앱이 사용자의 아이디와 비밀번호를 직접 받아 토큰으로 바꾸는 흐름입니다. OAuth 가 없애려던 바로 그 패턴이라 사실상 폐기됐습니다. 레거시 마이그레이션 외에는 쓰지 않습니다.
현장에서 만나는 모습
토큰 종류별 저장 위치와 수명을 정리하면 이렇습니다.
| 토큰 | 용도 | 수명 | 저장 위치 |
|---|---|---|---|
| Authorization Code | 토큰 교환용 1회성 | 수 초 | URL 파라미터(즉시 소멸) |
| ID Token | 사용자 신원 확인 | 5~60분 | 클라이언트 |
| Access Token | API 접근 권한 | 5~60분 | 메모리 또는 BFF |
| Refresh Token | 액세스 토큰 갱신 | 길다(취소 가능) | httpOnly 쿠키 또는 서버 |
리프레시 토큰을 localStorage 에 두면 안 되는 이유는 명확합니다. XSS 로 자바스크립트가 읽을 수 있고, 리프레시 토큰은 장기 유효라 탈취되면 지속적 접근을 허용합니다.
state 파라미터도 빠뜨리면 안 됩니다. 이것이 없으면 공격자가 자기 계정으로 발급받은 인가 코드를 피해자의 콜백으로 보내 피해자를 공격자 계정에 로그인시킬 수 있습니다.
브라우저 앱에서 토큰을 어디에 두나
흐름을 올바로 골라도 받은 토큰을 어디에 보관하느냐에서 다시 갈린다. 브라우저에서 고를 수 있는 자리는 셋뿐이고 각각 무엇에 약한지가 다르다.
| 자리 | 약한 지점 | 비고 |
|---|---|---|
localStorage |
XSS 로 그대로 읽힌다 | 새로고침에도 남아 편하지만 가장 위험하다 |
| 자바스크립트 메모리 | XSS 에는 여전히 노출, 새로고침하면 사라짐 | 액세스 토큰을 짧게 들고 있기에는 무난 |
httpOnly 쿠키 |
자바스크립트가 못 읽지만 CSRF 를 따로 막아야 한다 | SameSite 와 검증이 함께 필요 |
여기서 나온 실무의 답이 BFF(Backend For Frontend) 다. 토큰을 브라우저에 아예 주지 않고 서버가 들고 있으면서, 브라우저에는 세션 쿠키만 준다. 브라우저의 요청을 서버가 받아 토큰을 붙여 뒤로 전달하는 구조다. 앞의 세 자리가 가진 문제를 한꺼번에 없애는 대신 서버 한 층이 늘어난다.
어느 쪽을 고르든 XSS 가 있으면 대부분 무너진다는 점은 분명히 해 두어야 한다. 스크립트가 실행되면 토큰을 직접 읽지 못하더라도 그 페이지에서 요청을 대신 보낼 수 있기 때문이다. 그래서 토큰 보관 위치를 고민하는 것과 같은 무게로 콘텐츠 보안 정책과 입력 처리에 신경 써야 한다.
로그아웃도 생각보다 간단하지 않다. 앞 모듈에서 본 대로 액세스 토큰은 취소되지 않으므로, 로그아웃은 리프레시 토큰을 무효화하고 클라이언트가 들고 있는 것을 버리는 일에 가깝다. 여러 애플리케이션이 같은 로그인 세션을 공유한다면 한 곳에서 로그아웃했을 때 나머지도 정리되어야 하는데, 그것을 위한 별도의 규약이 있고 앱마다 그것을 구현해 두어야 한다. "로그아웃 버튼을 눌렀으니 끝" 이 아니라, 어디까지 정리되는지를 설계에서 정해 두는 일입니다.
다음 확인에서 볼 것
이 모듈은 개념만 다룹니다. 다음 모듈부터 실제 Keycloak 에서 토큰을 받아 뜯어보고, 그다음 PKCE 흐름을 스크립트로 완주합니다.