Keycloak Authorization Code Flow — 왜 2단계인가
서비스마다 로그인 화면을 따로 만들 이유가 없어서 인증은 Keycloak 한군데로 모았습니다. 로그인 화면과 계정, 토큰 발급을 대신 맡아 주는 오픈소스 인증 서버(Identity Provider) 입니다. 각 서비스는 비밀번호를 직접 안 들고 있고, Keycloak 이 발급한 토큰만 검증하면 끝입니다.
붙일 때 쓰는 규약은 OAuth2 와 OIDC. OAuth2 는 “이 앱이 사용자를 대신해 뭘 할 수 있는지” 를 다루고, OIDC 는 그 위에 “이 사용자가 누구인지” 를 얹었습니다. 로그인에 필요한 건 뒤쪽이라 둘을 같이 씁니다.
서버 앱이든 SPA 든 지금은 다 Authorization Code Flow 를 씁니다. 문서대로 따라 하면 동작은 하지만, 한 가지가 계속 걸렸습니다. 로그인에 성공했으면 그냥 토큰을 주면 될 텐데 왜 code 를 먼저 주고 그걸 다시 토큰으로 교환하는 2단계일까.
code 는 브라우저까지, 토큰은 서버끼리
브라우저는 로그인 후 code 만 받고 서버가 그 code 를 뒤에서 client_secret 과 함께 토큰으로
바꿉니다.
짧고 1회용인 code
Authorization Code 는 “이 사용자는 인증에 성공했다” 는 1회용 티켓입니다. 유효기간이 매우 짧고(보통 1분) 한 번 쓰면 만료되며, 그 자체엔 사용자 정보나 권한이 안 담겨 있습니다. 오직 토큰 교환 용도.
왜 토큰을 바로 안 주나 — URL 유출
인증이 끝났으면 그 자리에서 토큰을 주면 되겠다 싶었지만, Access Token 을 리다이렉트 URL 에 바로
실으면 새어 나갑니다. URL 에 실린 값은 브라우저 히스토리에 남고, 쿼리스트링으로 실으면 서버와
프록시의 접근 로그에 그대로 찍히는 데다 다음 페이지로 넘어갈 때 Referer 헤더에도 실려
나갑니다.
프래그먼트(#)로 실으면 서버까지는 안 가지만 브라우저 안에서는 그대로 보입니다. 그 페이지에서
도는 스크립트와 확장이 전부 읽을 수 있으니, 토큰을 앞단에 두는 것 자체가 위험합니다.
Access Token 은 안에 roles 와 exp 가 들어 있는 JWT 입니다. API 접근 권한 그 자체라, 새면 그대로 도용당하는 값. URL 에는 짧고 1회용이라 새도 덜 위험한 code 만 싣고, 진짜 토큰은 서버가 뒤에서 받아 갑니다.
code 를 토큰으로 바꿀 때는 서버만 아는 client_secret 도 있어야 해서, code 를 가로챈 쪽이
있어도 secret 없이는 토큰으로 못 바꿉니다. 브라우저(공개된 곳)에는 secret 을 두지 않는다는
원칙과 맞물립니다.
브라우저에서 도는 앱은 secret 을 둘 데가 없으니 이 보호가 통째로 빠지고, 그 자리를 메우는 게
PKCE 입니다. 난수를 하나 만들어 두고(code_verifier) 그걸 SHA-256 해시한 값만 첫 요청에 실어
보냅니다.
GET /realms/<realm>/protocol/openid-connect/auth
?response_type=code
&client_id=<client>
&redirect_uri=<앱 주소>
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
&code_challenge_method=S256
그리고 토큰으로 바꿀 때는 해시가 아니라 원본 난수를 제시하게 합니다.
POST /realms/<realm>/protocol/openid-connect/token
grant_type=authorization_code
code=<받은 code>
code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
서버는 SHA256(code_verifier) 를 다시 계산해서 처음 받아 둔 code_challenge 와 같은지 봅니다.
code 를 가로챈 쪽은 해시만 봤지 원본 난수를 모르니 이 단계를 못 넘깁니다. (값은 RFC 7636 의
예시입니다.)
ID Token vs Access Token — 역할 차이
OIDC 를 얹으면 토큰이 둘로 나뉘고, ID Token 은 누구인가를 담아서 로그인한 사용자의 신원을 알려 주고 Access Token 은 무엇을 할 수 있나를 담아서 API 접근 인가에 씁니다. 신원 확인과 권한 부여를 다른 토큰으로 갈라 둔 겁니다.
2단계인 이유 — 토큰을 URL 에 안 싣기
Authorization Code Flow 가 2단계인 건 토큰을 URL 에 노출하지 않으려고 입니다. 짧고 1회용인
code 만 앞단에 두고 토큰은 뒤에서 교환하고, 서버 앱은 client_secret 으로, secret 을 둘 데가
없는 SPA 는 PKCE 로 그 교환을 지킵니다. “왜 한 번에 안 주지” 의 답은 “URL 은 여러 경로로 샌다”
입니다.
댓글 0