하루들 OAuth 인증 전체 흐름.excalidraw
⚠ Switch to EXCALIDRAW VIEW in the MORE OPTIONS menu of this document. ⚠
Excalidraw Data
Text Elements
- OAuth login 시작 ^t0000003
브라우저 이동과 OAuth 인가 요청을 연결하기 위해 임시 Session을 사용 ^t0000004
- OAuth callback ^t0000007
Kakao가 확인한 신원을 하루들 내부 사용자와 Refresh Token으로 연결 ^t0000008
- Access Token 발급 ^t0000011
Refresh Cookie와 CSRF Token을 사용해 하루들 JWT 발급 ^t0000012
- 인증 API 호출 ^t0000015
Bearer JWT를 검증해 Authentication을 만들고 인가 후 API 실행 ^t0000016
하루들 서비스 OAuth -> JWT 흐름 ^t0000017
브라우저가 Redirect & Cookie를 담당 프론트엔드 서버는 브라우저 안에서 실행되어야하는 HTML, CSS, JavaScript를 전달해주는 역할을 한다. React의 JSX/TSX 코드는 Vite(?) 가 JavaScript로 변환하며, 브라우저는 그 JavaScript를 실행한다. (틀릴 수 있음) ^t0000018
Browser ^t0000020
frontend ^t0000022
Server: Spring Boot ^t0000024
Kakao ^t0000026
-
카카오 로그인 버튼 클릭 ^t0000032
-
로그인 시작 URL로 이동 ^t0000034
-
GET /oauth2/authorization/kakao ^t0000036
Server 내부 ^t0000038
인가 요청 생성: state + 요청을 Session에 저장 ^t0000039
-
302 Kakao 인가 URL + Set-Cookie: JSESSIONID ^t0000041
-
GET Kakao authorize (state 포함) ^t0000043
-
Kakao 로그인 & 동의 화면 ^t0000045
-
로그인 및 정보 제공 동의 ^t0000047
-
302 callback?code&state ^t0000049
-
GET /login/oauth2/code/kakao · code + state + JSESSIONID 이게 302 콜백으로 받은 거여서 서버로 다시 리다이렉트 된 것! ^t0000051
Spring Security ^t0000053
Session에 저장한 state 검증 ^t0000054
-
POST /oauth/token 및 code 교환 ^t0000056
-
Kakao Access Token ^t0000058
-
GET /v2/user/me ^t0000060
-
Kakao subject + profile ^t0000062
Server 내부 ^t0000064
OAuth2AuthenticationToken 생성 & JPA:
- User/OAuthAccount 조회 및 생성
- Refresh Token 원본 생성
- Refresh Token 해시를 DB에 저장 ^t0000065
-
302 Frontend callback · Refresh HttpOnly Cookie + XSRF Cookie ^t0000067
-
/auth/callback 진입 ^t0000069
-
GET /api/v1/auth/csrf ^t0000071
-
200 CSRF Token ^t0000073
-
POST /api/v1/auth/refresh + RT Cookie + X-XSRF-TOKEN ^t0000075
Server 내부 ^t0000077
CSRF Token 검증 기존 Refresh Token 폐기 새 Refresh Token 해시 저장 하루들 Access Token JWT 발급 ^t0000078
- 200 Access Token + Set-Cookie: 새 Refresh Token ^t0000080
프론트엔드 상태 ^t0000082
Access Token을 메모리에 저장 ^t0000083
- GET /api/v1/me · Authorization: Bearer 하루들 JWT ^t0000085
Server 내부 ^t0000087
API SecurityFilterChain 선택 (STATELESS) JwtDecoder: 서명·만료·issuer·audience 검증
Authentication -> SecurityContext 인가 조건 확인 + JPA User 조회 ^t0000088
- 200 사용자 정보 ^t0000090
실패 흐름 ^t0000092
토큰 없음&변조&만료 → 401 Unauthorized 인증은 됐지만 권한 부족 → 403 Forbidden ^t0000093
JSESSIONID는 OAuth 임시 상태용 하루들 API 인증은 Bearer JWT만 사용 ^t0000096
로그인 시작 URL? ^2LfgTfUr
onClick={() ≥ { window.location.assign('/oauth2/authorization/kakao'); }} ^UfoXUPrE
-
브라우저는 현재 접속중인 도메인의 해당 경로로 이동 ex) https://www.harudle.com/oauth2/authorization/kakao
-
Frontend Nginx는 /oauth2 경로를 backend:8080으로 프록시를 해줌
-
SpringSecurityBackend가 302 리다이렉트를 통해
https://kauth.kakao.com/oauth/authorize?..... 로 이동시킴 이렇게 하면 브라우저 화면이 kakao 로그인 화면으로 이동하는 구조 ^OeDbIWa5
참고: Nginx 설정: ^QB7w8gS4
location ~ ^/(api|oauth2|login/oauth2)/ { proxy_pass http://backend:8080; } ^hNxONosi
백엔드가 브라우저를 kakao로 보내기 전에
내가 시작한 로그인 요청! 이라는 증거를 서버에다가 잠시 저장해두어야함! ^Pv9HKqGw
spring Security는 하루들 kakao 설정을 통해
이런 kakao URL을 만들어줌. 이 URL이 바로 OAuth 인가 요청 이렇게 하면 카카오는 우리 서버에 인가 코드를 포함한 callback URL로 리다이렉트
^0xGt37Ps
https://kauth.kakao.com/oauth/authorize ?response_type=code &client_id=하루들_REST_API_KEY &redirect_uri=https://www.harudle.com/login/oauth2/code/kakao &scope=profile_nickname &state=임의의_랜덤값 ^o8RLLYGA
여기에 state가 있는 이유는?
우리가 서버에서 state = abcdef를 만들었다고 가정하자. 그러면 카카오한테 보낼때 state를 담아 보냄 + 우리 서버 세션에다가 잠시 state도 보관
이제 저 링크로 들어간 사람들은 직접 kakao 로그인을 함 (필수, 선택 사항 체크하고 로그인) 그러면 카카오는 브라우저한데 이제 하루들 callback 주소로 이동해야지!! 라고 302 응답을 줌 그러면 브라우저는 302 리다이렉트를 받았으니까 우리 백엔드 서버로 이동
이제 백엔드 서버는 Session에 저장된 state랑 kakao Callback으로 돌아온 state가 같은지를 비교
일치 -> 내가 시작한 로그인이구나! 불일치 -> 의심스러운 요청이구나…! -> 반려!
즉, 이 callback이 동일한 브라우저 Session에서 조금 전에 시작한 OAuth 인가 요청과 대응하는가? 를 검증한다. 쉽게 말하자면 Code를 바꿔끼는 경우를 방지해준다. ^wrc1R8Gm
이게 우리가 따로 카카오에서 설정해두는 kakao Callback 주소다! ^YOYpwid2
Session 데이터 서버의 HTTP Session에 저장 ^EZza4kCn
Server Session └─ session ID: XYZ789 └─ OAuth 인가 요청 ├─ clientId ├─ redirectUri ├─ scope └─ state: abcdef ^OXqyApS6
브라우저에는 Sessino을 찾기 위한 아이디만 쿠키로 전달한다. 서버에서는 Set-Cookie: JSESSIONID=XYZ789 로 설정 ^cLPWAXHc
따라서 로그인 시작 시 실제 상황 ^KT0pZVQQ
Server
- OAuth 요청 + state = abcdef 저장
- Session ID는 xyz789
Browser
- JSESSIONID = xyz789 저장
- state=abcdef가 포함된 카카오 로그인 URL을 따라서 카카오로 이동 ^WbmUph50
요약:
state는 사용자 신원을 증명하는 값이 아니고, Spring Security가 OAuth 요청마다 생성하는 임의의 값이다.
callback이 같은 브라우저 Session에서 시작한 인가 요청과 짝이 맞는지 확인한다.
이를 통해 로그인 CSRF나 위조된 callback 요청을 방어한다. ^X6GBQmIc
OAuth 2.0 이란? ^7WiXEEqi
OIDC란? ^LybTCDMJ
OAuth란 사용자가 자신의 비밀번호를 다른 서비스에 알려주지 않아도, 자신의 정보에 접근할 수 있는 권한을 일부 넘겨주는 방법! ^TEGxLF5B
EX) 사용자: 카카오야, 하루들이 내 kakao 닉네임 조회하도록 허용할게 ^KT7pgNBy
사용자는 하루들에 kakao 비밀번호를 알려주는 대신 kakao 가 하루들 서버에 제한된 권한을 가진 Access Token을 발행해줌 ^zl5uyXVB
그래서 OAuth 표준의 목적은 Authentication(인증) 이 아닌, Authroization (인가) 이다! ^DDcu4QoV
OAuth 1.0에서는 각 요청에 대해 서명을 붙임.
핵심이자 장점은 요청 자체를 서명한다는 것.
근데 구현이 상당하게 복잡했음! ^CwIcyCwi
OAuth1.0, 1.0a 은 레거시 ^pFJjuVmR
code는 뭔가용? ^Rp1C4FXi
정확히는 Authorization Code
이는 Access Token으로 교환하기 위한 짧고 일회성인 교환권 ^v8LBla30
이게 왜 있어야 하냐면,,,
자, 결국 사람이 카카오 로그인을 누르면, kakao는 다시 서버로 콜백을 해줘야함. 근데 그 사이에 브라우저를 경유함. 즉, kakao는 302(리다이렉트)를 담아 브라우저로 주면 브라우저가 서버 URL로 이동하는 것
근데 만일 Access Token을 Authorization Code 없이 바로 제공한다면 브라우저나, JS가 그 Access Token을 확인할 수 있다는 위험이 있음.
즉, 애플리케이션 서버는 AccessToken을 바로 받는 것이 아닌 Authorization Code를 받고 이를 다시 KAkao 서버로 보내 Access token을 발급받는 것이다.
따라서 카카오 서버와 애플리케이션 서버가 통신할 수 있도록 하여 Access Token을 안전하게 발급받도록 도와줌. ^oPQu4URS
똑같이 Access token이나, Authroization Code나 둘 다 노출되어도 위험한 거 아닌가 싶을 수 있지만,
짧은 시간만 유효하며, 한 번만 사용할수 있고, client-secret 등이 있어야만 교환이 가능하기 때문에 Access Token의 노출 위험을 줄여주는 것이다. ^cefnnzO8
그래서 OAuth 자체가 로그인을 완성하는 것이 아닌, OAuth로 신뢰할 수 있는 kakao 사용자 정보를 얻어오고 하루들이 자체적으로 회원을 찾아 로그인 시키는 것이다. ^kUF18lGO
OIDC(Open ID Connect) ^cJvFXxCq
OAuth2.0 위에 사용자 인증 기능을 표준화해서 추가한 것이다.
^7JpmHzZ8
OAuth 2.0
- openid scope
- ID Token
- 사용자 식별 규칙 = OpenID Connect ^r3xwtSwU
OIDC 요청에는 반드시 openid scope가 포함, 토큰 응답에는 ID Token이 포함 ^HKdgrFVX
ID Token이란 kakao가 하루들에게 전달하는 서명된 사용자 인증 확인서이다. ^up6PAkGI
{ "iss": "이 토큰을 발급한 서버", "sub": "Kakao 내부의 사용자 고유 ID", "aud": "이 토큰을 사용할 하루들 Client ID", "iat": "발급 시각", "exp": "만료 시각", "nonce": "인증 요청과 토큰의 연결값" } ^WfHMQp56
하루들 서버는 ID Token의 서명과 정보들을 통해 오 카카오가 실제로 이 유저를 인증을 한 다음에 아이디 토큰을 하루들에게 발급해준 거구나! ^0mSFIMcH
하루들 서비스에서는 OIDC를 사용하지 않았음
OIDC는 OAuth2.0에 인증 기능을 추가한 표준이며 openid socpe ID Token을 통해 로그인 사용자 신원을 전달한다.
ODIC를 사용하면 인가코드로 Access Token을 가지고 올 때 동시에 ID Token도 가지고 오는 것
ID Token이 있으니까 사실 UserInfo를 별도로 호출하지 않아도 됨. 만약 프로필 정보가 ID Token으로 부족하면 Access Token으로 사용자 정보를 추가적으로 가지고 오면 됨. ^DtXAyZmY
한 줄 요약: OIDC는 OAuth2.0에 로그인한 사용자가 누구인지를 증명하는 ID Token을 추가한 인증 표준이다. ^XRtIwy40
브라우저가 하루들 callback주소로 돌아온 상태 ^up7VVy3S
GET /login/oauth2/code/kakao ?code=CODE-777 &state=STATE-123
Cookie: JSESSIONID=SESSION-V ^VwHlyUm6
Session에 저장했던 state랑 callback의 state 비교 Session에 저장된 state = state123 callback으로 돌아온 state = state123
일치 -> 계속 진행 불일치 -> 로그인 중단 ^LWkQ8bDS
이 Callback이 현재 브라우저 Session에서 시작한 OAuth 요청과 짝이 맞다는 것 까지는 확인 됨. ^D46EdTTL
Authroization Code -> access token 발급 ^TnHXinsS
POST: https://kauth.kakao.com/oauth/token ^E5jN1wvD
grant_type=authorization_code client_id=하루들_REST_API_KEY client_secret=하루들_CLIENT_SECRET redirect_uri=https://www.harudle.com/login/oauth2/code/kakao code=CODE-777 ^RCKS8OL7
이러면 Kakao Acess Token 이 발급됨 (harudle access token과 다름) ^zzz3FcMu
OAuth2AuthenticationToken ├─ registrationId: kakao ├─ principal │ ├─ id: 123456789 │ └─ nickname: 라이언 └─ authorities ^f3ErWF6l
Spring Security는 카카오 사용자 정보를 기반으로 인증 객체를 생성 ^uBVeRbWM
얘는 서버 메모리 내에서 kakao 사용자 정보 조회까지 성공했음을 표시하는 java 객체 ^PE9XHMRM
기존 사용자 ^iV7DbfYs
처음 로그인한 사용자 ^GE6Ntlc0
기존 회원이니까 OAuthAccoun가 존재할거고, 그 계정의 User를 가져오면 됨. ^aMTgdiP0
OAuthAccount ├─ provider: KAKAO ├─ providerSubject: 123456789 └─ userId: 하루들 사용자 UUID ^INpvze01
그리고 삭제된 사용자인지를 판단하고 로그인 정보를 갱신 ^05269aBE
연결된 계정이 없으므로, 새로 생성해야한다. ^Jf0WRICd
User 생성
≥ OAuthAccount 생성
≥ kakao id와 하루들 User 연결 ^nWXSoo1S
이제 여기부터는 하루들 애플리케이션의 자체 로그인 정책임 ^1MnUeDTZ
Refresh Token 생성 ^SVmIoJvt
raw Refresh Token: 예측하기 어려운 긴 임의의 값 ^rh2BOXgj
Refresh token을 다음 302 응답에서 브라우저의 HttpOnly Cookie에 저장
Refresh Token 해시 -> 하루들 서버 DB에 저장 ^ICsHJ1uE
해시 해야하는 이유는 DB가 털리면 저장된 값을 그대로 Refresh Token처럼 사용하는 것을 방지하기 위해서 ^GgBITI5X
그렇게 해서 Refresh Token cookie랑 CSRF Token cookie로 브라우저에 전달 ^mVGC7nMw
CSRF 토큰 ^DukDM1Jc
만일 사용자가 로그인한 상황에서 evil.com에 접속했다고 하자. 더해서 evil.com의 내부 코드에는 ^Xs9Spf0k
POST https://harudle.com/api/v1/auth/logout ^ntl59zdq
이 있다고 가정해보자. ^bow5jYvQ
이렇게 되는 경우에 브라우저 쿠키에 refresh token이 존재하다보니, 사용자가 원치 않는 요청을 보낼 수 있음. ^mczOhEZq
SameSite = Lax도 일반적인 크로스사이트 POST를 차단하지만, CSRF 토큰을 통해 더 엄격히 보안을 챙기자. ^a77SwCHo
쿠키뿐만 아니라, 하루들 프론트엔드만 얻을 수 있는 CSRF Token을 요청 헤더에도 넣자
정상 요청은 Refresh Token과 X-XSRF-TOKEN Header 두 가지를 모두 보내야함. 사용자 정의 헤더를 넣는건 SOP와 CORS로 인해 어려움 ^llc4cHon