OAuth 2.0과 OpenID Connect 로그인
사용자가 Keycloak에서 로그인했을 때, 앱은 그 사실을 어떻게 믿고 자기 로그인으로 연결할까? 이 장에서는 브라우저·앱 서버·Keycloak이 무엇을 주고받는지 따라간다.
먼저 각 이름의 목적을 구분하자. OAuth 2.0은 앱에 제한된 API 접근을 맡기는 규칙이고, OIDC는 그 위에 로그인한 사용자를 확인하는 규칙을 더한다. Authorization Code는 일회용 code를 받아 토큰으로 교환하는 절차이며, PKCE는 code만 가로챈 주체가 교환하지 못하게 하는 보호 장치다. 배경부터 읽으려면 기초의 OAuth·OIDC·PKCE가 필요한 이유에서 사진 앱 예제로 이 네 가지를 먼저 구분할 수 있다.
이 장에서 처음 나오는 말3개
OAuth 2.0- 사용자나 서비스가 자격 증명을 직접 건네지 않고 제한된 접근 권한을 위임하는 프레임워크.
OIDCOpenID Connect- OAuth 2.0 위에 사용자 인증 결과와 ID token을 정의한 프로토콜.
PKCEProof Key for Code Exchange- authorization code를 요청한 client만 교환할 수 있게 verifier와 challenge를 묶는 보호 장치.
서버 앱의 로그인 흐름
섹션 제목: “서버 앱의 로그인 흐름”여기서 client는 로그인 결과를 받을 앱이며, 첫 사례의 앱은 서버 측 confidential client다. 즉, 앱의 비밀값을 브라우저에 배포하지 않고 서버에서 보관할 수 있다. 로컬 사용자로 새 로그인하는 정상 경로를 따라가며 단계를 바꾸어 메시지와 객체별 데이터 위치를 확인한다. 각 객체의 역할은 그림 아래에서 읽을 수 있다. 밑줄이 있는 전달 값을 누르면 그림 아래의 용어 설명으로 이동한다. 실제 로그인 요청은 보내지 않는 설명용 모델이다. 모든 통신은 TLS를 전제로 하며, 실제 구현에서는 검증된 OIDC 라이브러리가 요청 연결 확인과 code 교환·토큰 검증을 처리하게 한다.
서버 측 Code + PKCE: 누가 무엇을 보관할까?
각 단계가 끝난 시점의 보관 상태입니다. 강조된 객체 사이의 화살표를 따라 전달 값을 읽으세요. 내부 처리는 같은 객체로 돌아옵니다.
그림에 나오는 값의 뜻
verifier- 로그인마다 앱이 새로 만드는 무작위 비밀값. 이 사례에서는 앱 서버에 보관했다가 code를 token으로 교환할 때 Keycloak에 보낸다. code만 가로챈 사람은 이 값을 모르므로 교환할 수 없다.
challenge- verifier에서 계산해 먼저 보내 두는 대조값. S256은 verifier를 SHA-256으로 해시하고 base64url로 표현하는 방식이다. Keycloak은 나중에 받은 verifier로 같은 값을 계산해, 처음 보낸 challenge와 일치하는지 확인한다.
state- 돌아온 callback이 내가 시작한 로그인 요청의 응답인지 확인하는 무작위 값. 앱이 저장해 두고 인증 요청에 넣으면 Keycloak이 callback에 그대로 돌려준다. 앱은 원래 브라우저의 거래에 저장한 값과 비교한다.
nonce- 받은 ID token이 이번 로그인 요청에 대한 것인지 확인하는 무작위 값. 앱이 인증 요청에 넣으면 Keycloak이 ID token 안에 담아 준다. 앱은 token 검증 때 원래 값과 비교한다. state는 callback을, nonce는 ID token을 연결한다.
구성 요소의 역할
- 브라우저
- redirect를 따라 이동하고 사용자 입력을 Keycloak에 제출한다. 이 사례의 OIDC client는 아니다.
- 앱 서버
- confidential OIDC client. code를 교환하고 로그인 응답을 검증한 뒤 자체 세션을 만든다.
- Keycloak
- 사용자를 인증하고 code와 token을 발급한다. 앱 세션을 만드는 주체와는 다르다.
앱은 새 verifier를 생성하고 S256 challenge를 계산한다. state는 callback을, nonce는 ID token을 원래 로그인 요청과 묶는다. 이 사례는 서버의 임시 로그인 거래를 브라우저와 연결하며, 이때 쓰는 거래 식별용 cookie는 로그인 완료 뒤의 앱 session cookie와 구별한다.
- 브라우저
- 로그인 시작. 임시 거래 식별용 cookie를 사용할 수 있으나 아직 로그인된 앱 세션은 없음.
- 앱 서버
- client secret은 서버 설정에 보관. 브라우저에 연결된 임시 로그인 거래에 verifier·state·nonce 보관. token·앱 로그인 세션은 아직 없음.
- Keycloak
- 로컬 사용자 계정과 client 설정 보유.
로컬 사용자로 처음 로그인하는 정상 경로다. 로그인 화면의 주체는 Keycloak이며 앱은 사용자 비밀번호를 받지 않는다. 화면을 구성하는 정적 자원 요청은 생략한다.
- 브라우저
- Keycloak 로그인 화면과 로그인 진행용 cookie. 앱 로그인 세션·token 없음.
- 앱 서버
- client secret은 서버 설정에 보관. 브라우저에 연결된 임시 로그인 거래에 verifier·state·nonce 보관. token·앱 로그인 세션은 아직 없음.
- Keycloak
- 로컬 사용자 계정과 client 설정 보유. 인증 요청의 challenge(S256)·nonce와 로그인 진행 상태 보관.
비밀번호는 TLS로 Keycloak에 제출되고 로컬 계정의 저장된 credential과 대조된다. 앱 서버로 전달하거나 앱에 보관하지 않는다. 여기서는 추가 MFA나 동의 화면이 필요 없는 설정을 가정한다.
- 브라우저
- 비밀번호를 form으로 제출하는 시점. 앱에 비밀번호나 token을 전달하지 않음.
- 앱 서버
- client secret은 서버 설정에 보관. 브라우저에 연결된 임시 로그인 거래에 verifier·state·nonce 보관. token·앱 로그인 세션은 아직 없음.
- Keycloak
- 로컬 사용자 계정과 client 설정 보유. 사용자 인증 완료, Keycloak 세션 유지. 비밀번호는 인증 입력으로 처리하며 발급 결과에 포함하지 않음.
Keycloak은 짧은 수명의 일회용 code를 만들고 인증 요청의 challenge와 연결한다. code는 token 자체가 아니다. 브라우저는 Keycloak 세션 cookie와 callback URL을 받고 다음 요청을 준비한다.
- 브라우저
- callback URL의 code·state가 잠시 브라우저를 통과. Keycloak cookie 보유. 앱 session cookie·token 없음.
- 앱 서버
- client secret은 서버 설정에 보관. 브라우저에 연결된 임시 로그인 거래에 verifier·state·nonce 보관. token·앱 로그인 세션은 아직 없음.
- Keycloak
- 로컬 사용자 계정과 client 설정 보유. 사용자 인증 완료, Keycloak 세션 유지. 교환 전 code와 challenge의 연결 정보 보유.
앱은 브라우저에 연결된 원래 거래를 찾아 반환된 state를 비교한다. 정상 일치일 때만 다음 교환으로 진행한다. code를 받았다는 사실만으로 사용자가 로그인한 것으로 처리하지 않는다.
- 브라우저
- callback URL의 code·state가 잠시 브라우저를 통과. Keycloak cookie 보유. 앱 session cookie·token 없음.
- 앱 서버
- client secret은 서버 설정에 보관. 브라우저에 연결된 임시 로그인 거래에 verifier·state·nonce 보관. token·앱 로그인 세션은 아직 없음. callback의 code 수신, state 일치 확인.
- Keycloak
- 로컬 사용자 계정과 client 설정 보유. 사용자 인증 완료, Keycloak 세션 유지. 교환 전 code와 challenge의 연결 정보 보유.
앱은 grant_type=authorization_code, code, redirect_uri, verifier를 보내고 이 사례에서는 client secret으로 client를 인증한다. Keycloak은 code의 유효성·client·redirect URI를 확인하고 verifier에서 계산한 challenge를 원래 값과 비교한다. PKCE는 이 거래의 연결 증명이며 client 인증을 대체하지 않는다.
- 브라우저
- callback 응답 대기. 서버 간 요청의 verifier·client secret을 보지 못함.
- 앱 서버
- client secret은 서버 설정에 보관. 브라우저에 연결된 임시 로그인 거래에 verifier·state·nonce 보관. token·앱 로그인 세션은 아직 없음. 교환 요청 전송, token 응답 대기.
- Keycloak
- 로컬 사용자 계정과 client 설정 보유. 사용자 인증 완료, Keycloak 세션 유지. code·verifier·client 인증을 수신하여 교환 조건 확인.
성공 응답의 token은 브라우저가 아니라 앱 서버에 도착한다. 이 사례는 refresh token도 발급되는 설정을 가정한다. code는 소비되어 다시 교환할 수 없고, 앱은 수신한 로그인 응답을 아직 검증해야 한다.
- 브라우저
- callback 응답 대기. token 없음.
- 앱 서버
- client secret·임시 거래 유지. ID·access·refresh token 수신, 로그인 응답 검증 전. 앱 로그인 세션 없음.
- Keycloak
- 로컬 사용자 계정과 client 설정 보유. 사용자 인증 완료, Keycloak 세션 유지. code 소비 완료. token 발급 완료.
앱의 OIDC 라이브러리가 응답을 검증한다. 이 사례에서는 ID token의 서명·issuer·audience·만료·nonce 등을 확인한 뒤 신원을 받아들인다. API가 access token을 검증하고 권한을 판단하는 일은 별도다. 임시 거래를 정리하고 서버 session에 token을 보관한다.
- 브라우저
- callback 응답 대기. 앱 session cookie·token 없음.
- 앱 서버
- client secret 유지. 검증한 신원과 token을 서버 session에 보관. 임시 verifier·state·nonce 정리, code 재사용 안 함.
- Keycloak
- 로컬 사용자 계정과 client 설정 보유. 사용자 인증 완료, Keycloak 세션 유지. code 소비 완료.
11. 브라우저에는 앱 session cookie 전달
브라우저는 HttpOnly·Secure 등 적절한 속성의 앱 session cookie를 받는다. 이는 서버 session을 찾는 식별자이며 ID·access·refresh token과 다르다. 이후 앱 요청은 이 cookie를 사용한다. token을 서버에만 두는 것은 이 구성의 선택이지 모든 OIDC 앱의 보장은 아니다.
- 브라우저
- 앱 session cookie와 별도의 Keycloak cookie 보유. OIDC token·verifier·client secret 없음. callback code는 더 이상 사용하지 않음.
- 앱 서버
- client secret 유지. 앱 session 식별자에 연결된 신원·token 보관. 임시 거래 정리 완료.
- Keycloak
- 로컬 사용자 계정과 client 설정 보유. 사용자 인증 완료, Keycloak 세션 유지. 앱 session cookie를 발급하거나 관리하지 않음.
전체 흐름 보기는 같은 설명을 한 번에 펼친다. 단계 제목의 링크로 특정 순간을 직접 참조할 수 있다. OIDC Code Flow와 PKCE의 verifier·challenge 교환 규칙을 기준으로 요청과 응답을 나눴다. discovery·JWKS 조회, 추가 MFA·동의, 실패 분기는 그림에서 생략했다.
확인해 보자. callback에 code가 도착했는데도 서버가 Keycloak을 다시 호출하는 이유는 무엇일까? code 교환 요청과 앱 session cookie 전달을 비교하면 code는 교환할 증표이고, token은 서버에, 앱 session cookie는 브라우저에 남는다는 차이를 확인할 수 있다.
이 장에서 답할 질문
섹션 제목: “이 장에서 답할 질문”- OAuth 2.0과 OIDC는 무엇이 다른가?
- Authorization Code + PKCE에서 브라우저와 앱 server는 무엇을 주고받는가?
- 서버 없는 SPA는 같은 흐름을 어떻게 수행하는가?
- frontend와 backend가 나뉜 앱은 누가 client가 되는가?
- ID·access·refresh token은 누가 어디에 쓰는가?
OAuth 위에 인증을 얹는다
섹션 제목: “OAuth 위에 인증을 얹는다”OAuth 2.0의 핵심 질문은 “이 client가 어떤 resource에 접근하도록 위임받았는가”다. OIDC는
authorization request에 openid scope를 넣고 ID token·UserInfo·표준 claim을 정의해 “누가
로그인했는가”를 추가한다. OAuth access token만 보고 앱 로그인 정보를 임의로 추측하면 안 된다.
Code+PKCE의 보호 장치
섹션 제목: “Code+PKCE의 보호 장치”Keycloak의 OIDC endpoint 안내는 discovery, authorization, token, JWKS endpoint를 설명한다. 앱은 endpoint를 문자열로 흩어 적기보다 realm의 discovery 문서에서 읽는다.
| 값 | 막으려는 문제 | 확인 주체 |
|---|---|---|
| 정확한 redirect URI | 공격자 callback으로 code가 새는 문제 | Keycloak |
state | 다른 브라우저 요청의 callback을 끼워 넣는 문제 | 앱 |
PKCE S256 | 탈취한 code를 다른 client가 교환하는 문제 | Keycloak |
nonce | 다른 인증의 ID token 재사용·혼동 | 앱의 OIDC 라이브러리 |
public client는 secret을 안전하게 숨길 수 없으므로 PKCE가 특히 중요하다. confidential server-side
앱도 PKCE를 함께 쓰면 code 탈취 방어가 더 명확해진다. 이 실습의 앱 A/B는 confidential client지만
S256을 요구한다.
SPA라면 브라우저가 직접 교환한다
섹션 제목: “SPA라면 브라우저가 직접 교환한다”HTML·CSS·JS 정적 파일로만 배포되는 SPA(Single Page Application)는 서버가 없어 secret을 숨길 곳이 없다. 그래서 public client로 등록하고, 위 흐름에서 앱 server가 하던 일 — authorization URL 생성, code 교환, token 보관 — 을 브라우저의 JS가 직접 한다. 흐름은 같은 Code+PKCE지만 client secret이 없고 PKCE만으로 code 탈취를 막는다.
대신 token이 브라우저에 저장되므로 페이지에 낀 악성 script(XSS)가 token을 읽을 수 있는 노출면이 생기고, refresh token 수명도 짧게 잡는다. 이 실습의 앱 A/B는 서버가 있으므로 이 경로를 쓰지 않고, token을 서버 session에 두는 confidential client를 쓴다. 두 유형의 선택 기준은 Client 연결과 SSO에 있다.
frontend와 backend가 나뉘면 누가 client인가
섹션 제목: “frontend와 backend가 나뉘면 누가 client인가”React 같은 SPA frontend와 별도 backend API로 나뉜 앱도 새 흐름이 필요한 것이 아니다. 둘 중 누가 OIDC client가 되는지를 고르면 위 서버 측 흐름이나 SPA 다이어그램으로 정확히 환원된다. 갈리는 것은 token이 어디에 사는가와 backend의 역할 두 가지다.
| backend가 client (BFF) | frontend가 client (SPA) | |
|---|---|---|
| client 유형 | confidential — 위 서버 측 흐름 | public — 위 SPA 다이어그램 |
| token 위치 | backend session | 브라우저 |
| 브라우저→backend 인증 | session cookie | Bearer access token |
| backend의 역할 | OIDC client + API 대리 호출 | resource server — 검증·인가만 |
BFF(Backend For Frontend)는 backend가 confidential client로 로그인을 대신 처리하고 token을 자기 session에만 두는 구조다. frontend 정적 파일은 로그인에 관여하지 않고, 브라우저는 session cookie로 backend를 호출한다.
token이 브라우저에 전혀 없으므로 XSS로 token을 잃는 노출면이 사라진다. IETF의 브라우저 기반 앱 보안 지침(RFC 10017, BCP 212)은 세 구조를 보안이 강한 순서로 나열하며 BFF를 첫째로 두고, 업무·민감·개인정보 앱에 강하게 권장한다. 이 실습의 앱 A는 HTML까지 직접 주는 server 앱이지만 역할은 BFF와 같다 — access token을 자기 session에 두고 대신 API를 호출한다.
frontend가 client가 되는 쪽을 고르면 backend는 로그인에 관여하지 않는 resource server가 된다. 브라우저가 Bearer로 보낸 access token의 서명·issuer·audience·만료를 검증하고 role로 인가만 하며, 검증 항목은 Access Token 검증이 다룬다.
세 token의 역할
섹션 제목: “세 token의 역할”| token | 받는 사람과 목적 | 쓰면 안 되는 곳 |
|---|---|---|
| ID token | client가 로그인한 사용자와 인증 사건을 확인 | API bearer 권한 판단 |
| access token | API가 서명·issuer·audience·만료와 권한 claim을 검증 | 브라우저 프로필 표시의 유일한 근거 |
| refresh token | client가 새 token을 요청 | API 요청의 bearer credential |
JWT는 암호화된 비밀 상자가 아니라 서명된 claim 묶음인 경우가 많다. 브라우저 console이나 로그에 원문을 남기지 않고, access token은 의도한 API에만 보낸다.
실습에서 확인한 실패
섹션 제목: “실습에서 확인한 실패”앱 A 자동 검증은 올바른 local-user로 callback과 앱 session까지 성공했다. 오답 비밀번호는 callback에 도달하지 않았고, 변조한 state·nonce는 앱이 HTTP 400으로 거부했으며, 미등록 redirect URI는 Keycloak이 거부했다. password grant로 이 흐름을 대체하지 않았다.
Authorization Code + PKCE를 앱 코드와 실제 callback으로 확인하려면 Client를 연결하고 로그인 확인하기로 이어 간다.
- OAuth 2.0은 접근 위임, OIDC는 그 위에 사용자 인증을 정의한다.
- Authorization Code 흐름에서 code는 짧게 브라우저를 지나고, 앱이 PKCE verifier로 token을 교환한다.
- state·nonce·redirect URI·PKCE는 서로 다른 공격 경계를 막는다.
- 로그인에는 ID token, API에는 access token, 갱신에는 refresh token을 쓴다.
- frontend·backend 분리 앱은 backend를 confidential client로 쓰는 BFF가 우선 선택지다.