⚠ Switch to EXCALIDRAW VIEW in the MORE OPTIONS menu of this document. ⚠

Excalidraw Data

Text Elements

  1. OAuth login 시작 ^t0000003

브라우저 이동과 OAuth 인가 요청을 연결하기 위해 임시 Session을 사용 ^t0000004

  1. OAuth callback ^t0000007

Kakao가 확인한 신원을 하루들 내부 사용자와 Refresh Token으로 연결 ^t0000008

  1. Access Token 발급 ^t0000011

Refresh Cookie와 CSRF Token을 사용해 하루들 JWT 발급 ^t0000012

  1. 인증 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

  1. 카카오 로그인 버튼 클릭 ^t0000032

  2. 로그인 시작 URL로 이동 ^t0000034

  3. GET /oauth2/authorization/kakao ^t0000036

Server 내부 ^t0000038

인가 요청 생성: state + 요청을 Session에 저장 ^t0000039

  1. 302 Kakao 인가 URL + Set-Cookie: JSESSIONID ^t0000041

  2. GET Kakao authorize (state 포함) ^t0000043

  3. Kakao 로그인 & 동의 화면 ^t0000045

  4. 로그인 및 정보 제공 동의 ^t0000047

  5. 302 callback?code&state ^t0000049

  6. GET /login/oauth2/code/kakao · code + state + JSESSIONID 이게 302 콜백으로 받은 거여서 서버로 다시 리다이렉트 된 것! ^t0000051

Spring Security ^t0000053

Session에 저장한 state 검증 ^t0000054

  1. POST /oauth/token 및 code 교환 ^t0000056

  2. Kakao Access Token ^t0000058

  3. GET /v2/user/me ^t0000060

  4. Kakao subject + profile ^t0000062

Server 내부 ^t0000064

OAuth2AuthenticationToken 생성 & JPA:

  • User/OAuthAccount 조회 및 생성
  • Refresh Token 원본 생성
  • Refresh Token 해시를 DB에 저장 ^t0000065
  1. 302 Frontend callback · Refresh HttpOnly Cookie + XSRF Cookie ^t0000067

  2. /auth/callback 진입 ^t0000069

  3. GET /api/v1/auth/csrf ^t0000071

  4. 200 CSRF Token ^t0000073

  5. POST /api/v1/auth/refresh + RT Cookie + X-XSRF-TOKEN ^t0000075

Server 내부 ^t0000077

CSRF Token 검증 기존 Refresh Token 폐기 새 Refresh Token 해시 저장 하루들 Access Token JWT 발급 ^t0000078

  1. 200 Access Token + Set-Cookie: 새 Refresh Token ^t0000080

프론트엔드 상태 ^t0000082

Access Token을 메모리에 저장 ^t0000083

  1. GET /api/v1/me · Authorization: Bearer 하루들 JWT ^t0000085

Server 내부 ^t0000087

API SecurityFilterChain 선택 (STATELESS) JwtDecoder: 서명·만료·issuer·audience 검증

Authentication -> SecurityContext 인가 조건 확인 + JPA User 조회 ^t0000088

  1. 200 사용자 정보 ^t0000090

실패 흐름 ^t0000092

토큰 없음&변조&만료 → 401 Unauthorized 인증은 됐지만 권한 부족 → 403 Forbidden ^t0000093

JSESSIONID는 OAuth 임시 상태용 하루들 API 인증은 Bearer JWT만 사용 ^t0000096

로그인 시작 URL? ^2LfgTfUr

onClick={() ≥ { window.location.assign('/oauth2/authorization/kakao'); }} ^UfoXUPrE

  1. 브라우저는 현재 접속중인 도메인의 해당 경로로 이동 ex) https://www.harudle.com/oauth2/authorization/kakao

  2. Frontend Nginx는 /oauth2 경로를 backend:8080으로 프록시를 해줌

  3. 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

Built with LogoFlowershow