<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>김신건의 로그 - 위키 / auth-security</title><description>&apos;auth-security&apos; 카테고리 위키 페이지 최신 업데이트</description><link>https://shinkeonkim.com/</link><language>ko-KR</language><item><title>[Auth] OAuth 2.0 / 2.1: Authorization Code + PKCE</title><link>https://shinkeonkim.com/wiki/auth-security/oauth2/</link><guid isPermaLink="true">https://shinkeonkim.com/wiki/auth-security/oauth2/</guid><pubDate>Thu, 16 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;정의&quot;&gt;정의&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;OAuth 2.0&lt;/strong&gt; (RFC 6749, 2012) 은 &lt;em&gt;제3자 앱이 사용자 동의로 자원에 접근&lt;/em&gt; 하게 하는 &lt;em&gt;권한 위임 프레임워크&lt;/em&gt;. &lt;em&gt;인증 (authentication)&lt;/em&gt; 이 아니라 &lt;em&gt;인가 (authorization)&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;OAuth 2.1&lt;/strong&gt; 은 진행 중인 draft. 옛 flow 정리 + PKCE 의무화.&lt;/p&gt;
&lt;h2 id=&quot;핵심-4-역할&quot;&gt;핵심 4 역할&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart LR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    User[&quot;Resource Owner&amp;#x3C;br/&gt;사용자&quot;] --&gt;|동의| Auth[&quot;Authorization Server&amp;#x3C;br/&gt;예: Google&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Client[&quot;Client App&amp;#x3C;br/&gt;예: 우리 앱&quot;] --&gt;|토큰 요청| Auth&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Auth --&gt;|access_token| Client&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Client --&gt;|&quot;토큰 + 요청&quot;| Resource[&quot;Resource Server&amp;#x3C;br/&gt;예: Google Drive API&quot;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;authorization-code-flow--pkce-표준&quot;&gt;Authorization Code Flow + PKCE (표준)&lt;/h2&gt;
&lt;p&gt;가장 일반적이고 OAuth 2.1 에서 &lt;em&gt;유일하게 권장&lt;/em&gt; 되는 사용자 흐름.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;sequenceDiagram&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    autonumber&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant U as User&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant C as Client&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant Auth as Auth Server&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant API as Resource Server&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    C-&gt;&gt;C: code_verifier (랜덤 43-128자) 생성&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    C-&gt;&gt;C: code_challenge = SHA256(verifier) -&gt; base64url&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    C-&gt;&gt;U: 브라우저 -&gt; Auth 로 redirect&amp;#x3C;br/&gt;?response_type=code&amp;#x26;client_id=...&amp;#x3C;br/&gt;&amp;#x26;code_challenge=...&amp;#x26;code_challenge_method=S256&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    U-&gt;&gt;Auth: 로그인 + 동의&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Auth--&gt;&gt;U: redirect to Client&amp;#x3C;br/&gt;?code=ABC&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    U--&gt;&gt;C: code=ABC 전달&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    C-&gt;&gt;Auth: POST /token&amp;#x3C;br/&gt;code=ABC&amp;#x26;code_verifier=&amp;#x3C;원본&gt;&amp;#x26;client_id=...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Auth-&gt;&gt;Auth: SHA256(verifier) == challenge?&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Auth--&gt;&gt;C: { access_token, refresh_token }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    C-&gt;&gt;API: Authorization: Bearer &amp;#x3C;token&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    API--&gt;&gt;C: 자원&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;pkce-가-왜-필수&quot;&gt;PKCE 가 왜 필수?&lt;/h3&gt;
&lt;p&gt;OAuth 2.0 의 옛날 &lt;em&gt;implicit flow&lt;/em&gt; + &lt;em&gt;public client (모바일 / SPA)&lt;/em&gt; 에서 &lt;em&gt;intercept 공격&lt;/em&gt;. &lt;em&gt;code_verifier&lt;/em&gt; 가 &lt;em&gt;동적 공유 secret&lt;/em&gt; 처럼 작용.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]
&lt;em&gt;모든 OAuth 클라이언트는 PKCE 필수&lt;/em&gt;. 옛 라이브러리도 &lt;em&gt;반드시 최신 버전&lt;/em&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;다른-flow&quot;&gt;다른 Flow&lt;/h2&gt;








































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Flow&lt;/th&gt;&lt;th&gt;사용&lt;/th&gt;&lt;th&gt;상태&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Authorization Code + PKCE&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;사용자 로그인 (웹/모바일)&lt;/td&gt;&lt;td&gt;&lt;em&gt;권장&lt;/em&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Client Credentials&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;서버 대 서버 (사용자 없음)&lt;/td&gt;&lt;td&gt;권장&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Device Authorization&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;TV / CLI / 헤드리스&lt;/td&gt;&lt;td&gt;권장&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Refresh Token&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;access_token 갱신&lt;/td&gt;&lt;td&gt;권장&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Implicit&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;(옛 SPA)&lt;/td&gt;&lt;td&gt;&lt;em&gt;폐기 (OAuth 2.1)&lt;/em&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Resource Owner Password&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;password 직접 전달&lt;/td&gt;&lt;td&gt;&lt;em&gt;폐기 (OAuth 2.1)&lt;/em&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h3 id=&quot;client-credentials-서버끼리&quot;&gt;Client Credentials (서버끼리)&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;sequenceDiagram&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Service A-&gt;&gt;Auth: POST /token&amp;#x3C;br/&gt;client_id + client_secret&amp;#x3C;br/&gt;grant_type=client_credentials&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Auth--&gt;&gt;Service A: { access_token, expires_in }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Service A-&gt;&gt;Service B: Bearer &amp;#x3C;token&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Service B--&gt;&gt;Service A: 자원&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;device-flow&quot;&gt;Device Flow&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;sequenceDiagram&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Device-&gt;&gt;Auth: POST /device/code&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Auth--&gt;&gt;Device: { device_code, user_code, verification_uri }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Device-&gt;&gt;User: &quot;다른 기기로 https://x.com/code 가서 ABC123 입력&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    User-&gt;&gt;Auth: 브라우저에서 ABC123 입력 + 동의&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    loop poll&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        Device-&gt;&gt;Auth: POST /token (device_code)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        Auth--&gt;&gt;Device: 200 (성공) 또는 authorization_pending&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    end&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Apple TV, Roku, GitHub CLI (&lt;code&gt;gh auth login&lt;/code&gt;)&lt;/em&gt; 같은 &lt;em&gt;키보드 없는 기기&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id=&quot;oauth-20-vs-oauth-21&quot;&gt;OAuth 2.0 vs OAuth 2.1&lt;/h2&gt;








































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;항목&lt;/th&gt;&lt;th&gt;2.0&lt;/th&gt;&lt;th&gt;2.1&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Implicit flow&lt;/td&gt;&lt;td&gt;허용&lt;/td&gt;&lt;td&gt;&lt;em&gt;제거&lt;/em&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Resource owner password&lt;/td&gt;&lt;td&gt;허용&lt;/td&gt;&lt;td&gt;&lt;em&gt;제거&lt;/em&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;PKCE&lt;/td&gt;&lt;td&gt;권장&lt;/td&gt;&lt;td&gt;&lt;em&gt;필수&lt;/em&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Bearer token in URL&lt;/td&gt;&lt;td&gt;허용 (?token=)&lt;/td&gt;&lt;td&gt;&lt;em&gt;제거&lt;/em&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Refresh token rotation&lt;/td&gt;&lt;td&gt;옵션&lt;/td&gt;&lt;td&gt;&lt;em&gt;권장&lt;/em&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;HTTPS&lt;/td&gt;&lt;td&gt;권장&lt;/td&gt;&lt;td&gt;&lt;em&gt;필수&lt;/em&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;scope&quot;&gt;Scope&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;scope=read:files write:profile&lt;/code&gt; 같이 &lt;em&gt;권한 범위 명시&lt;/em&gt;. Auth Server 가 사용자 동의 화면에 표시.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;scope=openid profile email     # OpenID Connect&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;scope=read:items write:orders  # 자원 권한&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;state-parameter-csrf-방어&quot;&gt;State Parameter (CSRF 방어)&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;GET /authorize?...&amp;#x26;state=&amp;#x3C;random&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Auth 가 &lt;em&gt;같은 state 를 redirect 에 포함&lt;/em&gt;. 다른 사이트가 &lt;em&gt;위조한 redirect&lt;/em&gt; 를 클라이언트가 &lt;em&gt;거절&lt;/em&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!CAUTION]
&lt;em&gt;state 검증 누락&lt;/em&gt; = CSRF 공격에 OAuth 흐름 위조 가능. 모든 클라이언트 필수.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;pkce-구현-예시&quot;&gt;PKCE 구현 예시&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;typescript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// TypeScript (browser)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;async&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; function&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; generatePKCE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;() {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; verifier&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; crypto.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;randomUUID&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;().&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;replace&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#DBEDFF&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;g&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    +&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; crypto.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;randomUUID&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;().&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;replace&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#DBEDFF&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;g&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);   &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// 64자&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; encoder&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; TextEncoder&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; data&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; encoder.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;encode&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(verifier);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; digest&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; crypto.subtle.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;digest&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;SHA-256&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, data);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; challenge&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; btoa&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(String.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;fromCharCode&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;...new&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; Uint8Array&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(digest)))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    .&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;replace&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D;font-weight:bold&quot;&gt;\+&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;g&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;-&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;).&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;replace&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D;font-weight:bold&quot;&gt;\/&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;g&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;_&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;).&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;replace&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#DBEDFF&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;+$&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; { verifier, challenge };&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// 사용 예&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; { &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;verifier&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;challenge&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; } &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; generatePKCE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;sessionStorage.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;setItem&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;pkce_verifier&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, verifier);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; authUrl&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; URL&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;https://auth.example.com/authorize&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;authUrl.searchParams.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;set&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;response_type&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;code&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;authUrl.searchParams.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;set&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;client_id&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;CLIENT_ID&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;authUrl.searchParams.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;set&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;redirect_uri&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;REDIRECT_URI&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;authUrl.searchParams.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;set&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;code_challenge&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, challenge);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;authUrl.searchParams.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;set&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;code_challenge_method&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;S256&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;authUrl.searchParams.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;set&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;state&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, crypto.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;randomUUID&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;());&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;window.location.href &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; authUrl.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;toString&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;();&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;토큰-수명과-갱신&quot;&gt;토큰 수명과 갱신&lt;/h2&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;토큰&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;수명&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;용도&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;access_token&lt;/code&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;5-60분 (짧을수록 좋음)&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;자원 접근에 사용&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;refresh_token&lt;/code&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;수일-수개월&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;access_token 재발급&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;id_token&lt;/code&gt; (OIDC)&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;보통 access_token 과 동일&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;사용자 정보 포함 JWT&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Refresh Token Rotation&lt;/strong&gt; (OAuth 2.1 권장):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;refresh_token 을 한 번 사용하면 새 refresh_token 발급, 이전 무효화&lt;/li&gt;
&lt;li&gt;탈취 탐지: 만료된 refresh_token 재사용 시 경보 + 세션 전체 무효화&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;POST /token&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;grant_type=refresh_token&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;refresh_token=&amp;#x3C;old_token&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;-&gt; { access_token: &quot;...&quot;, refresh_token: &quot;&amp;#x3C;new_token&gt;&quot;, expires_in: 3600 }&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;token-introspection--revocation&quot;&gt;Token Introspection / Revocation&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;# RFC 7662: Token Introspection&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;POST /introspect&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;token=&amp;#x3C;opaque_token&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;-&gt; { active: true, sub: &quot;user123&quot;, scope: &quot;read&quot;, exp: 1234567890 }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;# RFC 7009: Token Revocation (로그아웃)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;POST /revoke&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;token=&amp;#x3C;refresh_token&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;token_type_hint=refresh_token&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;opaque token (불투명 토큰) 은 서버 DB 조회 필요. [[jwt|JWT]] 는 서명 검증으로 로컬 검증 가능하지만 revoke 가 어려움.&lt;/p&gt;
&lt;h2 id=&quot;jwt-vs-opaque-token&quot;&gt;JWT vs Opaque Token&lt;/h2&gt;






























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;JWT&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Opaque Token&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;검증&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;로컬 (서명 확인)&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;서버 introspect 필요&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;Revocation&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;어려움 (TTL 만료 대기)&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;즉시 (DB 삭제)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;크기&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;큰 편 (클레임 포함)&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;짧음&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;용도&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;API Gateway, stateless&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Auth Server 중심&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;oauth-vs-openid-connect&quot;&gt;OAuth vs OpenID Connect&lt;/h2&gt;






























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th&gt;OAuth 2.0&lt;/th&gt;&lt;th&gt;OIDC&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;목적&lt;/td&gt;&lt;td&gt;&lt;em&gt;권한 위임&lt;/em&gt; (자원 접근)&lt;/td&gt;&lt;td&gt;&lt;em&gt;인증&lt;/em&gt; (누구인지)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;토큰&lt;/td&gt;&lt;td&gt;access_token&lt;/td&gt;&lt;td&gt;+ &lt;em&gt;id_token&lt;/em&gt; (JWT)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;User info&lt;/td&gt;&lt;td&gt;자원 API 로 별도 조회&lt;/td&gt;&lt;td&gt;id_token 안에&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;표준화&lt;/td&gt;&lt;td&gt;기본&lt;/td&gt;&lt;td&gt;OAuth 2.0 위&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;자세한 건 [[openid-connect|OpenID Connect]] 참고.&lt;/p&gt;
&lt;h2 id=&quot;흔한-함정&quot;&gt;흔한 함정&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;redirect_uri 검증 부재&lt;/strong&gt;: open redirect 공격으로 &lt;em&gt;코드 탈취&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;state 미사용&lt;/strong&gt;: CSRF.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PKCE 없이 public client&lt;/strong&gt;: &lt;em&gt;intercept 공격&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;access_token 을 URL 에&lt;/strong&gt;: 로그 / referrer 로 노출.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;refresh_token 영구&lt;/strong&gt;: 탈취 시 영구 침해. &lt;em&gt;rotation&lt;/em&gt; (한 번 쓰면 새 발급 + 옛 무효화).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;scope 최소화 무시&lt;/strong&gt;: 필요한 scope 만 요청 (principle of least privilege).&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;보안-체크리스트&quot;&gt;보안 체크리스트&lt;/h2&gt;
&lt;p&gt;구현 전 점검:&lt;/p&gt;









































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;항목&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;세부 사항&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;PKCE&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;모든 public client (SPA, 모바일) 에 적용&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;redirect_uri&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;정확한 값만 서버에 등록, prefix 매칭 금지&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;state&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;요청마다 새 random 값, callback 에서 검증&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;access_token 저장&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;localStorage&lt;/code&gt; 보다 메모리 또는 HttpOnly cookie&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;refresh_token 저장&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;HttpOnly cookie (XSS 접근 불가)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;HTTPS&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Auth Server 및 Client 모두 필수&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;scope&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;최소 필요 scope 만 요청&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;token rotation&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;refresh_token 사용 후 즉시 교체&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;실전-라이브러리&quot;&gt;실전 라이브러리&lt;/h2&gt;





































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;플랫폼&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;라이브러리&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;SPA (React/Vue)&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;@auth0/auth0-react&lt;/code&gt;, &lt;code&gt;oidc-client-ts&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;Next.js&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;next-auth&lt;/code&gt; (Auth.js v5)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;Node.js 서버&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;passport-oauth2&lt;/code&gt;, &lt;code&gt;openid-client&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;Spring&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;spring-security-oauth2-client&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;Python&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;authlib&lt;/code&gt;, &lt;code&gt;python-social-auth&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;Mobile (iOS)&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;AppAuth-iOS&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;Mobile (Android)&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;AppAuth-Android&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;관련-위키&quot;&gt;관련 위키&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[jwt|JWT]]&lt;/li&gt;
&lt;li&gt;[[openid-connect|OpenID Connect]]&lt;/li&gt;
&lt;li&gt;[[saml|SAML]]&lt;/li&gt;
&lt;li&gt;[[mtls|mTLS]]&lt;/li&gt;
&lt;li&gt;[[csrf|CSRF]]&lt;/li&gt;
&lt;li&gt;[[session-cookie|Session Cookie]] - cookie 기반 세션과 토큰 방식 비교&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>oauth</category><category>auth</category><category>security</category><category>backend</category><author>koa (김신건)</author></item><item><title>[Auth] CSRF: 위조 요청 공격과 방어</title><link>https://shinkeonkim.com/wiki/auth-security/csrf/</link><guid isPermaLink="true">https://shinkeonkim.com/wiki/auth-security/csrf/</guid><pubDate>Wed, 15 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;정의&quot;&gt;정의&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;CSRF (Cross-Site Request Forgery)&lt;/strong&gt; 는 &lt;em&gt;사용자가 로그인된 상태&lt;/em&gt; 에서 &lt;em&gt;공격자가 만든 페이지&lt;/em&gt; 가 &lt;em&gt;피해자 브라우저로 위조 요청&lt;/em&gt; 을 보내는 공격. 쿠키 기반 인증의 &lt;em&gt;큰 약점&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id=&quot;csrf-vs-xss&quot;&gt;CSRF vs XSS&lt;/h2&gt;



































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;항목&lt;/th&gt;&lt;th&gt;CSRF&lt;/th&gt;&lt;th&gt;XSS&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;공격 경로&lt;/td&gt;&lt;td&gt;브라우저 자동 쿠키 첨부 악용&lt;/td&gt;&lt;td&gt;피해 사이트에 스크립트 주입&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;피해 사이트 코드 조작&lt;/td&gt;&lt;td&gt;불필요&lt;/td&gt;&lt;td&gt;필요&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;피해자 의도&lt;/td&gt;&lt;td&gt;피해자가 요청을 모름&lt;/td&gt;&lt;td&gt;피해자 브라우저에서 코드 실행&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;방어 포인트&lt;/td&gt;&lt;td&gt;서버: 요청 출처 검증&lt;/td&gt;&lt;td&gt;서버: 출력 인코딩&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;쿠키 의존&lt;/td&gt;&lt;td&gt;쿠키 기반 세션에 한정&lt;/td&gt;&lt;td&gt;어떤 인증 방식도 영향&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;공격-시나리오&quot;&gt;공격 시나리오&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;sequenceDiagram&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    autonumber&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant V as Victim&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant Bank as bank.com&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant Evil as evil.com&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    V-&gt;&gt;Bank: 로그인&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Bank--&gt;&gt;V: Set-Cookie: session=ABC&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    V-&gt;&gt;Evil: 다른 탭에서 evil.com 방문&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Evil--&gt;&gt;V: HTML with &amp;#x3C;form action=&quot;bank.com/transfer&quot;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Note over V: HTML 안의 form 자동 submit (JS)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    V-&gt;&gt;Bank: POST /transfer&amp;#x3C;br/&gt;(자동 첨부 cookie: session=ABC)&amp;#x3C;br/&gt;amount=1000&amp;#x26;to=attacker&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Bank-&gt;&gt;Bank: 인증된 세션 → 정상 처리&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Bank--&gt;&gt;V: 200 OK (이미 이체 됨)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;핵심: &lt;em&gt;브라우저가 쿠키를 자동 첨부&lt;/em&gt; 한다는 점을 &lt;em&gt;악용&lt;/em&gt;. &lt;em&gt;XSS 와 다름&lt;/em&gt;. CSRF 는 &lt;em&gt;공격자가 페이지에 코드 주입 못 해도 가능&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id=&quot;4가지-방어-패턴&quot;&gt;4가지 방어 패턴&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart TD&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q{방어 패턴}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q --&gt; A[&quot;1. SameSite Cookie&amp;#x3C;br/&gt;(가장 단순)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q --&gt; B[&quot;2. CSRF Token&amp;#x3C;br/&gt;(Synchronizer)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q --&gt; C[&quot;3. Double Submit Cookie&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q --&gt; D[&quot;4. Custom Header&quot;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;1-samesite-cookie&quot;&gt;1. SameSite Cookie&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;http&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;Set-Cookie&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; SID=abc; HttpOnly; Secure; SameSite=Lax&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;SameSite=Lax&lt;/code&gt; 가 cross-site form POST 차단. &lt;em&gt;2020 부터 Chrome 의 기본값&lt;/em&gt;.&lt;/p&gt;





























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;SameSite 값&lt;/th&gt;&lt;th&gt;Cross-site GET&lt;/th&gt;&lt;th&gt;Cross-site POST&lt;/th&gt;&lt;th&gt;설명&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;Strict&lt;/code&gt;&lt;/td&gt;&lt;td&gt;차단&lt;/td&gt;&lt;td&gt;차단&lt;/td&gt;&lt;td&gt;완전 격리, 외부 링크 클릭도 차단&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;Lax&lt;/code&gt;&lt;/td&gt;&lt;td&gt;top-level 허용&lt;/td&gt;&lt;td&gt;차단&lt;/td&gt;&lt;td&gt;기본값, 대부분의 CSRF 차단&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;None&lt;/code&gt;&lt;/td&gt;&lt;td&gt;허용&lt;/td&gt;&lt;td&gt;허용&lt;/td&gt;&lt;td&gt;&lt;code&gt;Secure&lt;/code&gt; 필수, iframe / 3rd-party 용&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]
&lt;strong&gt;SameSite=Lax 만으로도 대부분의 CSRF 차단&lt;/strong&gt;. &lt;em&gt;옛 브라우저 호환&lt;/em&gt; 이 필요하면 추가 패턴.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;2-csrf-token-synchronizer-token&quot;&gt;2. CSRF Token (Synchronizer Token)&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;sequenceDiagram&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    C-&gt;&gt;Server: GET /form&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Server-&gt;&gt;Server: token = random()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Server--&gt;&gt;C: HTML + hidden input _csrf=token (session 에 token 저장)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    C-&gt;&gt;Server: POST /transfer&amp;#x3C;br/&gt;_csrf=token + body&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Server-&gt;&gt;Server: session 의 token == 받은 token?&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Server--&gt;&gt;C: OK / 거절&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;각 폼/요청마다 고유 token&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;session 에 &lt;em&gt;서버 측 저장&lt;/em&gt; + 폼 hidden field.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Rails, Django, Spring&lt;/em&gt; 기본 적용.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;3-double-submit-cookie&quot;&gt;3. Double Submit Cookie&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;http&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;GET&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; /form&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;→ Set-Cookie: csrf_token=ABC&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;→ HTML: &amp;#x3C;input name=&quot;_csrf&quot; value=&quot;ABC&quot;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;POST&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; /transfer&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;→ Cookie: csrf_token=ABC&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;→ Body: _csrf=ABC&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;Server&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; cookie 의 csrf_token == body 의 _csrf ?&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;서버 측 저장 없이&lt;/em&gt; 동작.&lt;/li&gt;
&lt;li&gt;단점: subdomain 의 &lt;em&gt;cookie 침해&lt;/em&gt; 시 우회.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;4-custom-header-xhrfetch-한정&quot;&gt;4. Custom Header (XHR/Fetch 한정)&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;fetch&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;/api/transfer&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  method: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;POST&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  headers: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;    &apos;X-Requested-With&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;XMLHttpRequest&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;    &apos;Content-Type&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;application/json&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  body: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;JSON&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;stringify&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;({&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;...&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Simple Request 가 아니므로 CORS preflight 발생&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;다른 origin 이면 preflight 거절 → 위조 요청 &lt;em&gt;애초에 불가&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;SPA + API&lt;/em&gt; 의 &lt;em&gt;깔끔한 패턴&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;origin--referer-헤더-검증&quot;&gt;Origin / Referer 헤더 검증&lt;/h2&gt;
&lt;p&gt;서버에서 요청 출처를 직접 확인하는 보조 방어:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# 서버 측 Origin 검증 예시&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; check_origin&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(request):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    origin &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; request.headers.get(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;Origin&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    referer &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; request.headers.get(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;Referer&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    allowed &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;https://myapp.com&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;https://www.myapp.com&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; origin:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; origin &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; allowed&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; referer:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; any&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(referer.startswith(o) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;for&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; o &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; allowed)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; False&lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;  # 둘 다 없으면 거절&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]
&lt;em&gt;Referer 는 브라우저 설정 / 프록시로 제거될 수 있음&lt;/em&gt;. Origin + CSRF Token 을 &lt;em&gt;같이&lt;/em&gt; 사용 권장. Referer 만으로는 불충분.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;프레임워크-기본-내장&quot;&gt;프레임워크 기본 내장&lt;/h2&gt;





























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;프레임워크&lt;/th&gt;&lt;th&gt;CSRF 방어&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Django&lt;/td&gt;&lt;td&gt;&lt;code&gt;CsrfViewMiddleware&lt;/code&gt; 기본 활성화 (&lt;code&gt;{% csrf_token %}&lt;/code&gt; 태그)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Rails&lt;/td&gt;&lt;td&gt;&lt;code&gt;protect_from_forgery :exception&lt;/code&gt; 기본&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Spring Security&lt;/td&gt;&lt;td&gt;&lt;code&gt;csrf()&lt;/code&gt; 기본 활성화 (JWT 사용 시 disable)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Laravel&lt;/td&gt;&lt;td&gt;&lt;code&gt;VerifyCsrfToken&lt;/code&gt; 미들웨어 기본&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Express&lt;/td&gt;&lt;td&gt;&lt;code&gt;csrf&lt;/code&gt; 패키지 별도 설치 필요&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;서버 렌더링 프레임워크는 기본 내장&lt;/em&gt;. SPA 는 별도 설정 필요.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;cors-와-csrf-관계&quot;&gt;CORS 와 CSRF 관계&lt;/h2&gt;
&lt;p&gt;CORS 와 CSRF 는 &lt;em&gt;다른 문제&lt;/em&gt;. 혼동 주의.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart LR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    CORS[&quot;CORS&amp;#x3C;br/&gt;(Cross-Origin Resource Sharing)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    CSRF[&quot;CSRF&amp;#x3C;br/&gt;(Cross-Site Request Forgery)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    CORS --&gt;|&quot;브라우저가 cross-origin 요청 허용 여부&quot;| Browser[&quot;브라우저 정책&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    CSRF --&gt;|&quot;인증된 사용자 대신 위조 요청&quot;| Attack[&quot;공격 방어&quot;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;






























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;항목&lt;/th&gt;&lt;th&gt;CORS&lt;/th&gt;&lt;th&gt;CSRF&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;목적&lt;/td&gt;&lt;td&gt;다른 origin 의 JS 가 응답 읽기 허용&lt;/td&gt;&lt;td&gt;위조 요청 자체를 차단&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;방어 대상&lt;/td&gt;&lt;td&gt;응답 데이터 노출&lt;/td&gt;&lt;td&gt;상태 변경 요청 위조&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Simple Request&lt;/td&gt;&lt;td&gt;preflight 없음 → CORS 우회 가능&lt;/td&gt;&lt;td&gt;CSRF 공격 가능&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;관계&lt;/td&gt;&lt;td&gt;CORS 허용해도 CSRF 방어 별도 필요&lt;/td&gt;&lt;td&gt;CSRF 방어해도 CORS 설정 별도 필요&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]
&lt;em&gt;CORS 를 허용했다고 CSRF 가 방어되는 것이 아님&lt;/em&gt;. Simple Request (form POST) 는 preflight 없이 전송되어 CORS 와 무관하게 CSRF 공격 가능.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;어디에-어떤-방어&quot;&gt;어디에 어떤 방어?&lt;/h2&gt;





























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;환경&lt;/th&gt;&lt;th&gt;권장&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;클래식 서버 렌더링 (Rails, Django)&lt;/td&gt;&lt;td&gt;&lt;strong&gt;CSRF token + SameSite=Lax&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;SPA + REST API&lt;/td&gt;&lt;td&gt;&lt;strong&gt;Custom header + SameSite=Strict&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;API + JWT in &lt;code&gt;Authorization&lt;/code&gt; 헤더&lt;/td&gt;&lt;td&gt;&lt;em&gt;대부분 영향 없음&lt;/em&gt; (cookie 안 씀)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;모바일 앱 (자체 HTTP 라이브러리)&lt;/td&gt;&lt;td&gt;&lt;em&gt;영향 없음&lt;/em&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;3rd-party 통합 (iframe)&lt;/td&gt;&lt;td&gt;&lt;strong&gt;SameSite=None + Secure&lt;/strong&gt; + token&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;현대-브라우저-현황-2024&quot;&gt;현대 브라우저 현황 (2024+)&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart TD&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Chrome[&quot;Chrome 80+&amp;#x3C;br/&gt;SameSite=Lax 기본&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Firefox[&quot;Firefox 79+&amp;#x3C;br/&gt;SameSite=Lax 기본&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Safari[&quot;Safari 13.1+&amp;#x3C;br/&gt;SameSite 지원&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Old[&quot;IE / 구형 브라우저&amp;#x3C;br/&gt;SameSite 미지원&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Chrome --&gt;|&quot;대부분 CSRF 자동 차단&quot;| Safe[&quot;안전&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Firefox --&gt;|&quot;대부분 CSRF 자동 차단&quot;| Safe&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Safari --&gt;|&quot;대부분 CSRF 자동 차단&quot;| Safe&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Old --&gt;|&quot;추가 방어 필요&quot;| Extra[&quot;CSRF Token 필수&quot;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;






























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;브라우저&lt;/th&gt;&lt;th&gt;SameSite=Lax 기본&lt;/th&gt;&lt;th&gt;비고&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Chrome 80+&lt;/td&gt;&lt;td&gt;적용&lt;/td&gt;&lt;td&gt;2020년 2월부터&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Firefox 79+&lt;/td&gt;&lt;td&gt;적용&lt;/td&gt;&lt;td&gt;2020년 7월부터&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Safari 13.1+&lt;/td&gt;&lt;td&gt;적용&lt;/td&gt;&lt;td&gt;2020년 3월부터&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;IE 11&lt;/td&gt;&lt;td&gt;미지원&lt;/td&gt;&lt;td&gt;레거시 환경은 CSRF Token 필수&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;현대 브라우저 환경에서는 SameSite=Lax 기본값으로 대부분 방어&lt;/em&gt;. 레거시 지원 필요 시 CSRF Token 병행.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;get-의-위험성&quot;&gt;GET 의 위험성&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;html&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;img&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; src&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;https://bank.com/transfer?to=attacker&amp;#x26;amount=1000&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!CAUTION]
&lt;em&gt;상태 변경 GET&lt;/em&gt; 은 &lt;em&gt;CSRF 의 가장 흔한 함정&lt;/em&gt;. &lt;em&gt;모든 변경 요청은 POST/PUT/DELETE 만&lt;/em&gt;. &lt;em&gt;GET 은 idempotent + read-only&lt;/em&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;토큰-구현-시-주의사항&quot;&gt;토큰 구현 시 주의사항&lt;/h2&gt;
&lt;p&gt;CSRF Token 을 직접 구현할 때:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; secrets&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; hmac&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; hashlib&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; generate_csrf_token&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(session_id: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, secret: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) -&gt; &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    # 세션 ID 와 서버 secret 으로 HMAC 생성&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    # 단순 random 보다 세션 바인딩이 더 안전&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    random_part &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; secrets.token_hex(&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;16&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    mac &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; hmac.new(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        secret.encode(),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        f&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;session_id&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;random_part&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.encode(),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        hashlib.sha256,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    ).hexdigest()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; f&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;random_part&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;mac&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;단순 &lt;code&gt;random()&lt;/code&gt; 토큰&lt;/em&gt; 은 서버 재시작 시 세션과 분리될 수 있음. &lt;em&gt;HMAC 기반 토큰&lt;/em&gt; 이 더 안전하고 stateless 구현 가능.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;흔한-함정&quot;&gt;흔한 함정&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;SameSite=None&lt;/code&gt; 인데 &lt;code&gt;Secure&lt;/code&gt; 없음&lt;/strong&gt; = 브라우저가 거절. 옛 코드 깨짐.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CSRF token 을 &lt;em&gt;모든 요청에서 동일&lt;/em&gt;&lt;/strong&gt; = &lt;em&gt;하나만 탈취&lt;/em&gt; 되면 모든 요청 위조. &lt;em&gt;세션 단위 회전&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GET 의 &lt;em&gt;상태 변경&lt;/em&gt;&lt;/strong&gt; = SameSite=Lax 가 top-level GET 허용 → CSRF.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Multipart form 의 &lt;em&gt;Content-Type&lt;/em&gt;&lt;/strong&gt; = simple request 라 &lt;em&gt;preflight 없음&lt;/em&gt;. Custom header 방어가 &lt;em&gt;부분적&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;JWT + cookie 혼합&lt;/strong&gt; = Authorization 헤더 JWT 는 안전하지만 &lt;em&gt;cookie 에 저장된 JWT&lt;/em&gt; 는 CSRF 위험.&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;관련-위키&quot;&gt;관련 위키&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[CORS]]&lt;/li&gt;
&lt;li&gt;[[Session Cookie]]&lt;/li&gt;
&lt;li&gt;[[JWT]]&lt;/li&gt;
&lt;li&gt;[[XSS]] (관련 공격)&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>csrf</category><category>security</category><category>auth</category><category>browser</category><category>backend</category><author>koa (김신건)</author></item><item><title>[Auth] JWT: 구조, 서명, 만료, refresh token</title><link>https://shinkeonkim.com/wiki/auth-security/jwt/</link><guid isPermaLink="true">https://shinkeonkim.com/wiki/auth-security/jwt/</guid><pubDate>Wed, 15 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;정의&quot;&gt;정의&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;JWT (JSON Web Token)&lt;/strong&gt; (&lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc7519&quot;&gt;RFC 7519&lt;/a&gt;) 은 &lt;em&gt;base64url 인코딩된 JSON + 서명&lt;/em&gt; 형태의 &lt;em&gt;self-contained token&lt;/em&gt;. 서버가 &lt;em&gt;세션 저장 없이 클레임 검증&lt;/em&gt; 가능.&lt;/p&gt;
&lt;h2 id=&quot;jws-vs-jwe-두-가지-형태&quot;&gt;JWS vs JWE: 두 가지 형태&lt;/h2&gt;























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;형태&lt;/th&gt;&lt;th&gt;풀네임&lt;/th&gt;&lt;th&gt;기능&lt;/th&gt;&lt;th&gt;사용&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;JWS&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;JSON Web Signature&lt;/td&gt;&lt;td&gt;서명 (무결성 보장)&lt;/td&gt;&lt;td&gt;대부분의 JWT (인증, 인가)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;JWE&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;JSON Web Encryption&lt;/td&gt;&lt;td&gt;암호화 (기밀성 보장)&lt;/td&gt;&lt;td&gt;민감 정보 페이로드 필요 시&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;JWS: header.payload.signature           (페이로드 가시)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;JWE: header.key.iv.ciphertext.tag       (페이로드 암호화)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;일반적으로 “JWT” 는 JWS 를 의미. &lt;em&gt;페이로드는 암호화 아님&lt;/em&gt; (base64 인코딩만).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;구조-3-부분&quot;&gt;구조: 3 부분&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0MiIsIm5hbWUiOiJrb2EiLCJleHAiOjE3MjAxMjM0NTZ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;↑ header                                  ↑ payload                                            ↑ signature&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;





















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;부분&lt;/th&gt;&lt;th&gt;형식&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Header&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;{ &quot;alg&quot;: &quot;HS256&quot;, &quot;typ&quot;: &quot;JWT&quot; }&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Payload&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;{ &quot;sub&quot;: &quot;42&quot;, &quot;name&quot;: &quot;koa&quot;, &quot;exp&quot;: 1720123456 }&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Signature&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;HMAC-SHA256(base64(header) + ”.” + base64(payload), secret)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;표준-클레임&quot;&gt;표준 클레임&lt;/h2&gt;





































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;클레임&lt;/th&gt;&lt;th&gt;의미&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;iss&lt;/code&gt;&lt;/td&gt;&lt;td&gt;issuer&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;sub&lt;/code&gt;&lt;/td&gt;&lt;td&gt;subject (user ID)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;aud&lt;/code&gt;&lt;/td&gt;&lt;td&gt;audience&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;exp&lt;/code&gt;&lt;/td&gt;&lt;td&gt;만료 시각 (Unix epoch)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;nbf&lt;/code&gt;&lt;/td&gt;&lt;td&gt;not before&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;iat&lt;/code&gt;&lt;/td&gt;&lt;td&gt;issued at&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;jti&lt;/code&gt;&lt;/td&gt;&lt;td&gt;JWT ID (unique, replay 방지)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;서명-알고리즘-hs256-vs-rs256&quot;&gt;서명 알고리즘: HS256 vs RS256&lt;/h2&gt;









































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Alg&lt;/th&gt;&lt;th&gt;종류&lt;/th&gt;&lt;th&gt;키&lt;/th&gt;&lt;th&gt;비고&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;HS256/384/512&lt;/code&gt;&lt;/td&gt;&lt;td&gt;HMAC (대칭)&lt;/td&gt;&lt;td&gt;공유 secret&lt;/td&gt;&lt;td&gt;양측이 같은 키. 내부 서비스&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;RS256/384/512&lt;/code&gt;&lt;/td&gt;&lt;td&gt;RSA (비대칭)&lt;/td&gt;&lt;td&gt;private (sign) / public (verify)&lt;/td&gt;&lt;td&gt;&lt;em&gt;권장&lt;/em&gt;. 공개키 배포 가능&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;ES256/384/512&lt;/code&gt;&lt;/td&gt;&lt;td&gt;ECDSA&lt;/td&gt;&lt;td&gt;private / public&lt;/td&gt;&lt;td&gt;짧고 빠름&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;EdDSA&lt;/code&gt; (Ed25519)&lt;/td&gt;&lt;td&gt;EdDSA&lt;/td&gt;&lt;td&gt;private / public&lt;/td&gt;&lt;td&gt;가장 빠름, 안전&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;none&lt;/code&gt;&lt;/td&gt;&lt;td&gt;서명 없음&lt;/td&gt;&lt;td&gt;-&lt;/td&gt;&lt;td&gt;&lt;strong&gt;절대 사용 금지&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;sequenceDiagram&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant Auth as AuthServer&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant API as APIServer&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant Client as Client&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Note over Auth,API: RS256 키 배포 (1회)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Auth-&gt;&gt;API: 공개키 (JWKS endpoint or 환경변수)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Note over Client,API: 인증 흐름&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Client-&gt;&gt;Auth: POST /login&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Auth-&gt;&gt;Auth: JWT 생성 (private key 로 서명)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Auth--&gt;&gt;Client: access_token (JWT)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Client-&gt;&gt;API: GET /resource (Authorization: Bearer JWT)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    API-&gt;&gt;API: 공개키로 서명 검증 (Auth 서버 접속 없음)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    API--&gt;&gt;Client: 200 OK&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]
&lt;em&gt;RS256&lt;/em&gt;: Auth Server 만 &lt;em&gt;private key&lt;/em&gt; 보유. API Server 는 &lt;em&gt;public key&lt;/em&gt; 로 검증. &lt;em&gt;public key 유출돼도 토큰 위조 불가&lt;/em&gt;. HS256 은 secret 공유 필요 → &lt;em&gt;서비스 수 늘어날수록 위험&lt;/em&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;흐름&quot;&gt;흐름&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;sequenceDiagram&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    autonumber&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant C as Client&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant Auth as AuthServer&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant API as APIServer&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    C-&gt;&gt;Auth: POST /login (id, password)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Auth-&gt;&gt;Auth: 검증 + JWT 생성&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Auth--&gt;&gt;C: { access_token, refresh_token }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    C-&gt;&gt;API: GET /me (Authorization: Bearer JWT)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    API-&gt;&gt;API: JWT 검증 (서명 + exp)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    API--&gt;&gt;C: 200 OK&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Note over C: 만료 임박&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    C-&gt;&gt;Auth: POST /refresh (refresh_token)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Auth--&gt;&gt;C: 새 access_token&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;refresh-token-rotation-패턴&quot;&gt;Refresh Token Rotation 패턴&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;sequenceDiagram&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant C as Client&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant Auth as AuthServer&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    C-&gt;&gt;Auth: POST /refresh (RT=abc)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Auth-&gt;&gt;Auth: RT 검증, 새 AT + 새 RT 발급&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Auth-&gt;&gt;Auth: 구 RT(abc) 무효화&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Auth--&gt;&gt;C: { access_token, refresh_token: xyz }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Note over C,Auth: 탈취된 RT 로 재시도&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    C-&gt;&gt;Auth: POST /refresh (RT=abc 재사용 시도)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Auth-&gt;&gt;Auth: abc 무효화 확인&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Auth--&gt;&gt;C: 401 Unauthorized&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Auth-&gt;&gt;Auth: 계정 전체 세션 강제 종료 (재사용 감지)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;RT rotation&lt;/em&gt;: 매 refresh 마다 새 RT 발급 + 구 RT 즉시 무효화. 재사용 시 &lt;em&gt;탈취 감지&lt;/em&gt; → &lt;em&gt;전체 세션 폐기&lt;/em&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;access-token-vs-refresh-token&quot;&gt;Access Token vs Refresh Token&lt;/h2&gt;



































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;항목&lt;/th&gt;&lt;th&gt;Access Token&lt;/th&gt;&lt;th&gt;Refresh Token&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;수명&lt;/td&gt;&lt;td&gt;&lt;em&gt;짧음&lt;/em&gt; (5분 ~ 1시간)&lt;/td&gt;&lt;td&gt;&lt;em&gt;길음&lt;/em&gt; (7일 ~ 30일)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;저장&lt;/td&gt;&lt;td&gt;메모리 (브라우저 SPA)&lt;/td&gt;&lt;td&gt;HttpOnly cookie 또는 안전한 저장&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;매 요청 전송&lt;/td&gt;&lt;td&gt;&lt;em&gt;예&lt;/em&gt;&lt;/td&gt;&lt;td&gt;갱신 시만&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;탈취 영향&lt;/td&gt;&lt;td&gt;짧은 시간 피해&lt;/td&gt;&lt;td&gt;큰 피해, &lt;em&gt;반드시 HttpOnly&lt;/em&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Revoke&lt;/td&gt;&lt;td&gt;만료까지 어려움&lt;/td&gt;&lt;td&gt;DB 에 저장 + revoke 가능&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]
&lt;em&gt;Access token 을 localStorage 에 저장&lt;/em&gt; 하면 XSS 시 즉시 탈취. &lt;em&gt;메모리 + 새로고침 시 refresh&lt;/em&gt; 패턴이 정통.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;algnone-취약점&quot;&gt;alg=none 취약점&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# 취약한 라이브러리 예시 (개념도)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; jwt&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# 공격자가 만든 페이로드&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;malicious_header &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;alg&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;none&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;typ&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;JWT&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;malicious_payload &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;sub&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;1&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;role&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;admin&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;exp&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;9999999999&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# base64 인코딩 후 서명 없이 전송&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;token &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; base64(malicious_header) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;.&quot;&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; +&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; base64(malicious_payload) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;.&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# → 취약 라이브러리는 서명 검증 없이 통과&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;방어&lt;/strong&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# 안전한 사용법: 허용 alg 화이트리스트&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;decoded &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; jwt.decode(token, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;PUBLIC_KEY&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;algorithms&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;RS256&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;])  &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# alg 명시&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# algorithms=[&quot;RS256&quot;] 에 없는 알고리즘은 거절&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]
&lt;em&gt;&lt;code&gt;alg: none&lt;/code&gt;&lt;/em&gt; 을 받는 라이브러리는 &lt;em&gt;치명적 취약점&lt;/em&gt;. 항상 &lt;em&gt;허용 alg 화이트리스트&lt;/em&gt;. 사용 라이브러리의 CVE 이력 확인.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;revoke-전략&quot;&gt;Revoke 전략&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart TD&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q{revoke 필요}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q --&gt; A[&quot;짧은 exp + refresh&amp;#x3C;br/&gt;대부분의 경우&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q --&gt; B[&quot;Blacklist (Redis)&amp;#x3C;br/&gt;jti 등록&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q --&gt; C[&quot;Versioning&amp;#x3C;br/&gt;user.token_version 비교&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    A --&gt; Best[추천]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    B --&gt; Heavy[캐시 부담]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    C --&gt; Sync[DB 한 번 조회 필요]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Redis Blacklist 구현&lt;/strong&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;typescript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// 로그아웃 시 jti 를 blacklist 에 등록&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;async&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; function&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; logout&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;jti&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; string&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;exp&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; number&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; ttl&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; exp &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; Math.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;floor&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(Date.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;now&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;() &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 1000&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  await&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; redis.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;setex&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;`blacklist:${&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;jti&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;}`&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, ttl, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;1&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// 검증 미들웨어&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;async&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; function&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; verifyToken&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;token&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; string&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; payload&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; jwt.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;verify&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(token, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;PUBLIC_KEY&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, { algorithms: [&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;RS256&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;] });&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; isBlacklisted&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; redis.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;exists&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;`blacklist:${&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;payload&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;jti&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;}`&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (isBlacklisted) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;throw&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; Error&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;Token revoked&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; payload;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;xss--csrf-와의-관계&quot;&gt;XSS / CSRF 와의 관계&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart TD&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Store{저장 위치}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Store --&gt; A[localStorage] --&gt;|XSS 시 탈취| Bad1[X]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Store --&gt; B[&quot;메모리 + HttpOnly cookie refresh&quot;] --&gt;|XSS 영향 적음| Good[O]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Store --&gt; C[&quot;HttpOnly cookie + SameSite=Strict&quot;] --&gt;|CSRF 안전 + XSS 안전| Best[권장]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]
&lt;em&gt;Cookie 기반&lt;/em&gt; 이면 &lt;em&gt;CSRF 방어&lt;/em&gt; 필요 (SameSite=Strict 또는 CSRF token). &lt;em&gt;Bearer header&lt;/em&gt; 면 &lt;em&gt;XSS 방어&lt;/em&gt; 가 중요.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;stateless-vs-stateful&quot;&gt;Stateless vs Stateful&lt;/h2&gt;



































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th&gt;Session (stateful)&lt;/th&gt;&lt;th&gt;JWT (stateless)&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;서버 저장소&lt;/td&gt;&lt;td&gt;필수&lt;/td&gt;&lt;td&gt;불필요&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Revoke&lt;/td&gt;&lt;td&gt;즉시&lt;/td&gt;&lt;td&gt;만료까지 어려움&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;스케일&lt;/td&gt;&lt;td&gt;sticky session 필요&lt;/td&gt;&lt;td&gt;&lt;em&gt;수평 확장 자유&lt;/em&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;페이로드&lt;/td&gt;&lt;td&gt;session ID 만 (작음)&lt;/td&gt;&lt;td&gt;&lt;em&gt;전체 클레임&lt;/em&gt; (큼)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;갱신&lt;/td&gt;&lt;td&gt;서버 갱신&lt;/td&gt;&lt;td&gt;새 JWT 발급&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;[!CAUTION]
&lt;em&gt;“JWT 가 항상 더 좋다”&lt;/em&gt; 는 미신. &lt;em&gt;작은 monolith&lt;/em&gt; 에서는 &lt;em&gt;session 이 더 단순 + 즉시 revoke 가능&lt;/em&gt;. JWT 는 &lt;em&gt;분산 + 마이크로서비스 + 모바일 SDK&lt;/em&gt; 에서 진가.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;흔한-함정&quot;&gt;흔한 함정&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;alg: none&lt;/code&gt; 허용 라이브러리&lt;/strong&gt; = 옛 라이브러리 일부. 즉시 업데이트.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;HS256 vs RS256 혼동&lt;/strong&gt; = 공개키를 secret 으로 &lt;em&gt;오해해 서명&lt;/em&gt; 받으면 &lt;em&gt;모든 토큰 위조 가능&lt;/em&gt; (CVE 다수).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;만료 너무 김&lt;/strong&gt; = 탈취 시 피해 큼. 15분 ~ 1시간 권장.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;secret 이 짧음&lt;/strong&gt; = &lt;code&gt;secret&lt;/code&gt; 같이 무차별 공격 가능한 키. &lt;em&gt;32+ 바이트 랜덤&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;민감 정보 페이로드&lt;/strong&gt; = JWT 는 &lt;em&gt;암호화 아님&lt;/em&gt; (base64 인코딩). password / 카드 번호 &lt;em&gt;절대 금지&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Refresh token localStorage 저장&lt;/strong&gt; = XSS 취약. &lt;em&gt;반드시 HttpOnly cookie&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RT rotation 미적용&lt;/strong&gt; = 탈취된 RT 무한 재사용 가능.&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;jwt-크기와-성능-고려사항&quot;&gt;JWT 크기와 성능 고려사항&lt;/h2&gt;
&lt;p&gt;JWT 는 &lt;em&gt;매 요청 헤더에 포함&lt;/em&gt;. 크기가 커지면 &lt;em&gt;네트워크 오버헤드&lt;/em&gt;:&lt;/p&gt;





















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;클레임 수&lt;/th&gt;&lt;th&gt;대략 크기 (HS256)&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;최소 (sub, exp, iat)&lt;/td&gt;&lt;td&gt;~150 bytes&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;일반 (10개 클레임)&lt;/td&gt;&lt;td&gt;~300-400 bytes&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;과도 (role, permission 배열 포함)&lt;/td&gt;&lt;td&gt;1KB+&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;클레임 최소화&lt;/em&gt;: role 이름 대신 &lt;em&gt;role ID&lt;/em&gt;, permission 배열 대신 &lt;em&gt;scope string&lt;/em&gt; 으로 압축. 큰 권한 데이터는 &lt;em&gt;DB 조회&lt;/em&gt;로.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;jwe-암호화-변형&quot;&gt;JWE (암호화 변형)&lt;/h2&gt;
&lt;p&gt;JWT 가 &lt;em&gt;서명만&lt;/em&gt; 이면 &lt;em&gt;내용 노출&lt;/em&gt;. &lt;strong&gt;JWE&lt;/strong&gt; 는 &lt;em&gt;암호화&lt;/em&gt; 까지. 그러나 &lt;em&gt;느리고 복잡&lt;/em&gt;. 대부분의 경우 &lt;em&gt;민감 정보를 페이로드에 안 넣고 JWS (서명만) 만으로 충분&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id=&quot;관련-위키&quot;&gt;관련 위키&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[OAuth2]]&lt;/li&gt;
&lt;li&gt;[[OpenID Connect]]&lt;/li&gt;
&lt;li&gt;[[Session Cookie]]&lt;/li&gt;
&lt;li&gt;[[CORS]]&lt;/li&gt;
&lt;li&gt;[[CSRF]]&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>jwt</category><category>auth</category><category>security</category><category>backend</category><author>koa (김신건)</author></item><item><title>[Auth] OpenID Connect (OIDC): OAuth 위의 인증 레이어</title><link>https://shinkeonkim.com/wiki/auth-security/openid-connect/</link><guid isPermaLink="true">https://shinkeonkim.com/wiki/auth-security/openid-connect/</guid><pubDate>Wed, 15 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;정의&quot;&gt;정의&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;OpenID Connect (OIDC)&lt;/strong&gt; 는 &lt;em&gt;OAuth 2.0 위에 얹은 인증 (authentication) 표준&lt;/em&gt;. OAuth 가 &lt;em&gt;권한 위임 (authorization)&lt;/em&gt;, OIDC 가 &lt;em&gt;“누구인지”&lt;/em&gt; 알려준다.&lt;/p&gt;
&lt;p&gt;핵심 추가:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;id_token&lt;/code&gt;&lt;/strong&gt; (JWT): 사용자 식별 정보&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;/userinfo&lt;/code&gt;&lt;/strong&gt; endpoint: 추가 프로필 정보&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discovery&lt;/strong&gt;: &lt;code&gt;.well-known/openid-configuration&lt;/code&gt; 으로 자동 설정&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Scope &lt;code&gt;openid&lt;/code&gt;&lt;/strong&gt;: OIDC 활성화 신호&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;사용-시나리오&quot;&gt;사용 시나리오&lt;/h2&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;상황&lt;/th&gt;&lt;th&gt;OIDC 역할&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;SPA / 모바일 앱 로그인&lt;/td&gt;&lt;td&gt;Authorization Code + PKCE&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;기업 SSO&lt;/td&gt;&lt;td&gt;Okta / Keycloak → 여러 앱 자동 인증&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;소셜 로그인&lt;/td&gt;&lt;td&gt;Google / Kakao → 내 앱에서 id_token 검증&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;K8s API 인증&lt;/td&gt;&lt;td&gt;OIDC Provider 와 kube-apiserver 연동&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;흐름&quot;&gt;흐름&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;sequenceDiagram&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    autonumber&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant U as User&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant App as Client App&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant Idp as OIDC Provider&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Note over App: scope=openid profile email&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    App-&gt;&gt;U: redirect to Idp&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    U-&gt;&gt;Idp: 로그인 + 동의&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Idp--&gt;&gt;App: code (Authorization Code)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    App-&gt;&gt;Idp: POST /token&amp;#x3C;br/&gt;code + verifier&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Idp--&gt;&gt;App: { access_token, id_token, refresh_token }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    App-&gt;&gt;App: id_token (JWT) 검증&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    App-&gt;&gt;Idp: GET /userinfo (옵션)&amp;#x3C;br/&gt;Bearer access_token&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Idp--&gt;&gt;App: { sub, name, email, picture, ... }&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;pkce-상세-흐름&quot;&gt;PKCE 상세 흐름&lt;/h2&gt;
&lt;p&gt;PKCE (Proof Key for Code Exchange) 는 &lt;em&gt;Authorization Code 탈취 방지&lt;/em&gt;. &lt;em&gt;공개 클라이언트&lt;/em&gt; (SPA, 모바일) 필수.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;sequenceDiagram&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    autonumber&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant App as Client&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant Idp as OIDC Provider&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    App-&gt;&gt;App: code_verifier = random 43-128자&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    App-&gt;&gt;App: code_challenge = BASE64URL(SHA256(verifier))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    App-&gt;&gt;Idp: GET /auth?code_challenge=XXX&amp;#x26;code_challenge_method=S256&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Idp--&gt;&gt;App: redirect + code&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    App-&gt;&gt;Idp: POST /token&amp;#x3C;br/&gt;code + code_verifier&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Idp-&gt;&gt;Idp: SHA256(verifier) == code_challenge?&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Idp--&gt;&gt;App: tokens&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;code 만 탈취해도 verifier 없이는 token 교환 불가&lt;/em&gt;. SPA 에서 PKCE 없는 Authorization Code Flow 는 &lt;em&gt;안전하지 않음&lt;/em&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;id_token-의-표준-클레임&quot;&gt;id_token 의 표준 클레임&lt;/h2&gt;





















































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;클레임&lt;/th&gt;&lt;th&gt;의미&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;iss&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Issuer (Idp URL)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;sub&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Subject (사용자 ID, &lt;em&gt;영구 식별자&lt;/em&gt;)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;aud&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Audience (Client ID)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;exp&lt;/code&gt;&lt;/td&gt;&lt;td&gt;만료&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;iat&lt;/code&gt;&lt;/td&gt;&lt;td&gt;발급 시각&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;auth_time&lt;/code&gt;&lt;/td&gt;&lt;td&gt;사용자가 &lt;em&gt;마지막으로 인증한&lt;/em&gt; 시각&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;nonce&lt;/code&gt;&lt;/td&gt;&lt;td&gt;replay 방지&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;acr&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Authentication Context Class Reference&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;amr&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Authentication Methods (&lt;code&gt;[&quot;pwd&quot;, &quot;mfa&quot;]&lt;/code&gt;)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;azp&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Authorized Party&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;프로필&lt;/td&gt;&lt;td&gt;&lt;code&gt;name&lt;/code&gt;, &lt;code&gt;given_name&lt;/code&gt;, &lt;code&gt;family_name&lt;/code&gt;, &lt;code&gt;email&lt;/code&gt;, &lt;code&gt;email_verified&lt;/code&gt;, &lt;code&gt;picture&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;id_token-검증-절차&quot;&gt;id_token 검증 절차&lt;/h2&gt;
&lt;p&gt;id_token 은 &lt;em&gt;단순히 받는 것으로 충분하지 않음&lt;/em&gt;. 반드시 서버에서 검증.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart TD&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    T[&quot;id_token 수신&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    T --&gt; V1[&quot;1. JWT 서명 검증&amp;#x3C;br/&gt;(JWKS 에서 키 가져오기)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    V1 --&gt; V2[&quot;2. iss 검증&amp;#x3C;br/&gt;(기대 Idp URL?)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    V2 --&gt; V3[&quot;3. aud 검증&amp;#x3C;br/&gt;(내 Client ID?)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    V3 --&gt; V4[&quot;4. exp 검증&amp;#x3C;br/&gt;(만료 안 됐나?)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    V4 --&gt; V5[&quot;5. nonce 검증&amp;#x3C;br/&gt;(replay 방지)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    V5 --&gt; OK[&quot;인증 완료&quot;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# python-jose 예시&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; jose &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; jwt&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;payload &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; jwt.decode(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    id_token,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    jwks,           &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# JWKS 에서 가져온 공개키&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;    algorithms&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;RS256&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;    audience&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;my-client-id&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;    issuer&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;https://accounts.google.com&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# payload[&quot;sub&quot;] 가 사용자 영구 ID&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;discovery-well-knownopenid-configuration&quot;&gt;Discovery: &lt;code&gt;.well-known/openid-configuration&lt;/code&gt;&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;curl&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; https://accounts.google.com/.well-known/openid-configuration&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;  &quot;issuer&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;https://accounts.google.com&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;  &quot;authorization_endpoint&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;https://accounts.google.com/o/oauth2/v2/auth&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;  &quot;token_endpoint&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;https://oauth2.googleapis.com/token&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;  &quot;userinfo_endpoint&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;https://openidconnect.googleapis.com/v1/userinfo&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;  &quot;jwks_uri&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;https://www.googleapis.com/oauth2/v3/certs&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;  &quot;scopes_supported&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;openid&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;email&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;profile&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;  &quot;response_types_supported&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;code&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;token&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;id_token&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FDAEB7;font-style:italic&quot;&gt;...&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;  &quot;id_token_signing_alg_values_supported&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;RS256&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FDAEB7;font-style:italic&quot;&gt;  ...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;클라이언트는 &lt;em&gt;Idp URL&lt;/em&gt; 하나만 알면 &lt;em&gt;모든 endpoint 자동 발견&lt;/em&gt;. 신규 OIDC Provider 통합이 &lt;em&gt;1줄&lt;/em&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;jwks-json-web-key-set&quot;&gt;JWKS (JSON Web Key Set)&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;curl&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; https://www.googleapis.com/oauth2/v3/certs&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;  &quot;keys&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;      &quot;kty&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;RSA&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;      &quot;alg&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;RS256&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;      &quot;use&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;sig&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;      &quot;kid&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;abc123&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;      &quot;n&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;...&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;      &quot;e&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;AQAB&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  ]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;Idp 가 &lt;em&gt;공개키 공개&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;클라이언트가 id_token 의 &lt;code&gt;kid&lt;/code&gt; 로 &lt;em&gt;해당 키 찾아 검증&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;키 회전&lt;/em&gt; 시 &lt;em&gt;복수 키 동시 공존&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;oidc-흐름-종류&quot;&gt;OIDC 흐름 종류&lt;/h2&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Flow&lt;/th&gt;&lt;th&gt;응답 타입&lt;/th&gt;&lt;th&gt;사용&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Authorization Code Flow + PKCE&lt;/td&gt;&lt;td&gt;&lt;code&gt;response_type=code&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;em&gt;권장&lt;/em&gt; (모든 클라이언트)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Implicit Flow&lt;/td&gt;&lt;td&gt;&lt;code&gt;response_type=id_token token&lt;/code&gt;&lt;/td&gt;&lt;td&gt;폐기&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Hybrid Flow&lt;/td&gt;&lt;td&gt;&lt;code&gt;response_type=code id_token&lt;/code&gt;&lt;/td&gt;&lt;td&gt;일부 경우&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;sso-single-sign-on&quot;&gt;SSO (Single Sign-On)&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart LR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    User --&gt; App1[App 1]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    User --&gt; App2[App 2]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    User --&gt; App3[App 3]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    App1 --&gt; Idp[OIDC Provider]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    App2 --&gt; Idp&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    App3 --&gt; Idp&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Idp --&gt; Login[중앙 로그인 1회]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;한 번 로그인 → 여러 앱 &lt;em&gt;자동 인증&lt;/em&gt;. &lt;em&gt;기업 SSO&lt;/em&gt; (Okta, Auth0, Keycloak) 의 토대.&lt;/p&gt;
&lt;h2 id=&quot;로그아웃-흐름&quot;&gt;로그아웃 흐름&lt;/h2&gt;
&lt;p&gt;OIDC 는 &lt;em&gt;로그아웃&lt;/em&gt; 도 표준화. 세 가지 방식:&lt;/p&gt;





















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;방식&lt;/th&gt;&lt;th&gt;설명&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;RP-Initiated Logout&lt;/td&gt;&lt;td&gt;앱이 Idp 에 로그아웃 요청. Idp 세션 종료&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Front-Channel Logout&lt;/td&gt;&lt;td&gt;Idp 가 모든 연결 앱에 로그아웃 알림 (iframe)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Back-Channel Logout&lt;/td&gt;&lt;td&gt;Idp 가 서버-서버로 로그아웃 알림 (더 안정)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# RP-Initiated Logout&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;GET&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; https://idp.example.com/logout?&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  id_token_hint&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;id_token&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&gt;&amp;#x26;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  post_logout_redirect_uri&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;https://myapp.com/&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]
&lt;em&gt;로컬 세션만 지우는 것&lt;/em&gt; 은 완전한 로그아웃이 아님. &lt;em&gt;Idp 세션도 종료&lt;/em&gt; 해야 SSO 전체에 영향.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;k8s--oidc-통합&quot;&gt;K8s + OIDC 통합&lt;/h2&gt;
&lt;p&gt;kube-apiserver 에 OIDC Provider 를 연결해 &lt;em&gt;kubectl 사용자 인증&lt;/em&gt; 에 활용:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# kube-apiserver 플래그&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;--oidc-issuer-url&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;https://accounts.google.com&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;--oidc-client-id&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;my-k8s-client&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;--oidc-username-claim&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;email&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;--oidc-groups-claim&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;groups&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# kubeconfig 에 OIDC 토큰 설정&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;kubectl&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; config&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; set-credentials&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; alice&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;  --auth-provider=oidc&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;  --auth-provider-arg=idp-issuer-url=https://accounts.google.com&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;  --auth-provider-arg=client-id=my-k8s-client&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;  --auth-provider-arg=id-token=&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;id_token&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;OIDC 로 인증된 사용자 email / group 을 &lt;em&gt;RBAC 의 subject&lt;/em&gt; 로 사용. &lt;em&gt;중앙 IdP 로 K8s 접근 제어&lt;/em&gt; 통합.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;사용-라이브러리&quot;&gt;사용 라이브러리&lt;/h2&gt;





























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;언어&lt;/th&gt;&lt;th&gt;라이브러리&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Node&lt;/td&gt;&lt;td&gt;passport-openidconnect, openid-client&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Python&lt;/td&gt;&lt;td&gt;authlib, python-jose&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Java&lt;/td&gt;&lt;td&gt;spring-security-oauth2-client, Nimbus JOSE&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Go&lt;/td&gt;&lt;td&gt;go-oidc, oidc-mw&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Ruby&lt;/td&gt;&lt;td&gt;omniauth-openid-connect&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;oidc-vs-oauth-vs-saml&quot;&gt;OIDC vs OAuth vs SAML&lt;/h2&gt;









































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th&gt;OAuth 2.0&lt;/th&gt;&lt;th&gt;OIDC&lt;/th&gt;&lt;th&gt;SAML 2.0&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;목적&lt;/td&gt;&lt;td&gt;권한 위임&lt;/td&gt;&lt;td&gt;인증&lt;/td&gt;&lt;td&gt;인증 + 권한&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;토큰&lt;/td&gt;&lt;td&gt;access_token&lt;/td&gt;&lt;td&gt;+ id_token (JWT)&lt;/td&gt;&lt;td&gt;SAML Assertion (XML)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;페이로드&lt;/td&gt;&lt;td&gt;bearer&lt;/td&gt;&lt;td&gt;JWT&lt;/td&gt;&lt;td&gt;XML&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;모바일 친화&lt;/td&gt;&lt;td&gt;좋음&lt;/td&gt;&lt;td&gt;&lt;em&gt;최고&lt;/em&gt;&lt;/td&gt;&lt;td&gt;떨어짐&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;기업&lt;/td&gt;&lt;td&gt;일부&lt;/td&gt;&lt;td&gt;모던 표준&lt;/td&gt;&lt;td&gt;옛 표준, &lt;em&gt;여전히 많음&lt;/em&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;흔한-함정&quot;&gt;흔한 함정&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;id_token 만 신뢰&lt;/strong&gt; = &lt;code&gt;aud&lt;/code&gt;, &lt;code&gt;iss&lt;/code&gt;, &lt;code&gt;exp&lt;/code&gt;, &lt;code&gt;nonce&lt;/code&gt; 모두 검증해야 함.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;JWKS &lt;em&gt;매 요청&lt;/em&gt; fetch&lt;/strong&gt; = 캐싱 필요 (보통 24h). 단 &lt;em&gt;key rotation&lt;/em&gt; 시 빠르게 갱신.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;sub 변경&lt;/strong&gt; = 같은 Idp 의 같은 사용자는 &lt;em&gt;&lt;code&gt;sub&lt;/code&gt; 가 영구 동일&lt;/em&gt;. 다른 Idp 의 같은 이메일은 &lt;em&gt;다른 sub&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;email 만 보고 사용자 식별&lt;/strong&gt; = 이메일 변경 가능. &lt;em&gt;항상 &lt;code&gt;sub&lt;/code&gt; 로 매핑&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Implicit Flow 아직 사용&lt;/strong&gt; = 폐기된 방식. &lt;em&gt;PKCE 있는 Authorization Code Flow&lt;/em&gt; 로 전환.&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;관련-위키&quot;&gt;관련 위키&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[OAuth2]]&lt;/li&gt;
&lt;li&gt;[[JWT]]&lt;/li&gt;
&lt;li&gt;[[SAML]]&lt;/li&gt;
&lt;li&gt;[[Session Cookie]]&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>oidc</category><category>oauth</category><category>auth</category><category>sso</category><category>backend</category><author>koa (김신건)</author></item><item><title>[Auth] SAML 2.0: XML 기반 기업 SSO</title><link>https://shinkeonkim.com/wiki/auth-security/saml/</link><guid isPermaLink="true">https://shinkeonkim.com/wiki/auth-security/saml/</guid><pubDate>Wed, 15 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;정의&quot;&gt;정의&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;SAML 2.0&lt;/strong&gt; (Security Assertion Markup Language) 는 &lt;em&gt;XML 기반 기업 SSO&lt;/em&gt; 표준. 2005년 OASIS 표준화. 모바일 친화도 / 단순성에서 OIDC 에 밀리지만 &lt;em&gt;기업 / SaaS 통합의 옛 표준&lt;/em&gt; 으로 여전히 광범위.&lt;/p&gt;
&lt;h2 id=&quot;핵심-역할&quot;&gt;핵심 역할&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart LR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    User --&gt; SP[&quot;SP (Service Provider)&amp;#x3C;br/&gt;예: Salesforce&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    User --&gt; IdP[&quot;IdP (Identity Provider)&amp;#x3C;br/&gt;예: Okta&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    SP --&gt; IdP&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    IdP --&gt; SP&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;




















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;역할&lt;/th&gt;&lt;th&gt;설명&lt;/th&gt;&lt;th&gt;예시&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;SP&lt;/strong&gt; (Service Provider)&lt;/td&gt;&lt;td&gt;서비스를 제공, IdP 에 인증 위임&lt;/td&gt;&lt;td&gt;Salesforce, Slack, GitHub Enterprise&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;IdP&lt;/strong&gt; (Identity Provider)&lt;/td&gt;&lt;td&gt;신원 확인 + Assertion 발급&lt;/td&gt;&lt;td&gt;Okta, Azure AD, Google Workspace&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;sp-initiated-sso-흐름&quot;&gt;SP-initiated SSO 흐름&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;sequenceDiagram&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    autonumber&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant U as User&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant SP as ServiceProvider&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant IdP as IdentityProvider&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    U-&gt;&gt;SP: GET /app (로그인 안 됨)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    SP-&gt;&gt;U: 302 Redirect to IdP (SAMLRequest base64+deflate)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    U-&gt;&gt;IdP: SAMLRequest 전달&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    IdP-&gt;&gt;U: 로그인 화면 표시&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    U-&gt;&gt;IdP: id + password 입력&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    IdP-&gt;&gt;IdP: SAML Response 생성 (Assertion + XML 서명)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    IdP-&gt;&gt;U: HTML form 자동 submit (SAMLResponse)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    U-&gt;&gt;SP: POST /acs (SAMLResponse)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    SP-&gt;&gt;SP: Assertion 검증 (서명, 유효시간, Audience)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    SP--&gt;&gt;U: 세션 시작, 애플리케이션 접근 허용&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;SP-initiated = &lt;em&gt;서비스 (SaaS) 가 먼저 redirect&lt;/em&gt;. 가장 일반적.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;idp-initiated-sso-흐름&quot;&gt;IdP-initiated SSO 흐름&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;sequenceDiagram&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    autonumber&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant U as User&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant IdP as IdentityProvider&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant SP as ServiceProvider&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    U-&gt;&gt;IdP: IdP 포털에서 앱 아이콘 클릭&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    IdP-&gt;&gt;IdP: SAML Response 생성 (InResponseTo 없음)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    IdP-&gt;&gt;U: HTML form 자동 submit (SAMLResponse)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    U-&gt;&gt;SP: POST /acs (SAMLResponse)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    SP-&gt;&gt;SP: Assertion 검증 (InResponseTo 검증 생략됨)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    SP--&gt;&gt;U: 세션 시작&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]
IdP-initiated 는 &lt;code&gt;InResponseTo&lt;/code&gt; 가 없어 &lt;em&gt;CSRF replay 공격에 취약&lt;/em&gt;. SP 가 허용 여부 명시 설정 필요.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;http-binding-비교&quot;&gt;HTTP Binding 비교&lt;/h2&gt;




















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Binding&lt;/th&gt;&lt;th&gt;전송 방법&lt;/th&gt;&lt;th&gt;특징&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;HTTP-Redirect&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;URL query string (deflate + base64)&lt;/td&gt;&lt;td&gt;GET 요청, &lt;em&gt;URL 길이 한계&lt;/em&gt; (~2KB)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;HTTP-POST&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;HTML form hidden field (base64)&lt;/td&gt;&lt;td&gt;POST 요청, &lt;em&gt;크기 제한 없음&lt;/em&gt;, &lt;em&gt;XML 서명 전송 가능&lt;/em&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;AuthnRequest&lt;/strong&gt; 는 대부분 HTTP-Redirect. &lt;strong&gt;SAMLResponse&lt;/strong&gt; 는 반드시 HTTP-POST (서명 포함 크기 때문).&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;http&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# HTTP-Redirect (AuthnRequest)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;GET&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; /sso?SAMLRequest=nJJBj9MwEIXv...&amp;#x26;SigAlg=RSA-SHA256&amp;#x26;Signature=abc...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# HTTP-POST (SAMLResponse)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;POST&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; /acs&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;Content-Type&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; application/x-www-form-urlencoded&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;SAMLResponse=PHNhbWxwO...&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;saml-assertion-xml&quot;&gt;SAML Assertion (XML)&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;xml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;saml:Assertion&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; ID&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;...&quot;&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; IssueInstant&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;2026-06-25T12:00:00Z&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;saml:Issuer&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;https://idp.example.com&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;saml:Issuer&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;ds:Signature&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;...&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;ds:Signature&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;saml:Subject&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;saml:NameID&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; Format&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;...:emailAddress&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;koa@example.com&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;saml:NameID&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;saml:SubjectConfirmation&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; Method&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;bearer&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;saml:SubjectConfirmationData&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;        Recipient&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;https://sp.example.com/acs&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;        NotOnOrAfter&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;2026-06-25T12:05:00Z&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;        InResponseTo&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;&amp;#x3C;원래 AuthnRequest ID&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    &amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;saml:SubjectConfirmation&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  &amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;saml:Subject&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;saml:Conditions&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;    NotBefore&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;2026-06-25T12:00:00Z&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;    NotOnOrAfter&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;2026-06-25T12:05:00Z&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;saml:AudienceRestriction&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;saml:Audience&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;https://sp.example.com&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;saml:Audience&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    &amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;saml:AudienceRestriction&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  &amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;saml:Conditions&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;saml:AuthnStatement&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; AuthnInstant&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;2026-06-25T12:00:00Z&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;saml:AuthnContext&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;saml:AuthnContextClassRef&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      &amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;saml:AuthnContextClassRef&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    &amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;saml:AuthnContext&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  &amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;saml:AuthnStatement&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;saml:AttributeStatement&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;saml:Attribute&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; Name&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;email&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;saml:AttributeValue&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;koa@example.com&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;saml:AttributeValue&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    &amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;saml:Attribute&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;saml:Attribute&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; Name&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;department&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;saml:AttributeValue&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;engineering&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;saml:AttributeValue&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    &amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;saml:Attribute&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  &amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;saml:AttributeStatement&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;saml:Assertion&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;metadata-교환&quot;&gt;Metadata 교환&lt;/h2&gt;
&lt;p&gt;SP 와 IdP 가 &lt;em&gt;XML metadata 파일&lt;/em&gt; 로 사전 합의:&lt;/p&gt;



































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;항목&lt;/th&gt;&lt;th&gt;SP metadata&lt;/th&gt;&lt;th&gt;IdP metadata&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Entity ID&lt;/td&gt;&lt;td&gt;&lt;code&gt;https://sp.example.com&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;https://idp.example.com&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;ACS URL&lt;/td&gt;&lt;td&gt;&lt;code&gt;https://sp.example.com/acs&lt;/code&gt;&lt;/td&gt;&lt;td&gt;-&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;SSO URL&lt;/td&gt;&lt;td&gt;-&lt;/td&gt;&lt;td&gt;&lt;code&gt;https://idp.example.com/sso&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;SLO URL&lt;/td&gt;&lt;td&gt;logout&lt;/td&gt;&lt;td&gt;logout&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Certificate&lt;/td&gt;&lt;td&gt;서명 검증용 공개키&lt;/td&gt;&lt;td&gt;동일&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;대부분의 SaaS 는 &lt;em&gt;URL 한 줄로 metadata 자동 교환&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id=&quot;slo-single-logout&quot;&gt;SLO (Single Logout)&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;sequenceDiagram&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant U as User&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant SP as ServiceProvider&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant IdP as IdentityProvider&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    U-&gt;&gt;SP: POST /logout&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    SP-&gt;&gt;U: Redirect to IdP (LogoutRequest)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    U-&gt;&gt;IdP: LogoutRequest 전달&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    IdP-&gt;&gt;IdP: 세션 종료, 다른 SP 에 LogoutRequest 전파&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    IdP--&gt;&gt;U: LogoutResponse (redirect to SP)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    U-&gt;&gt;SP: LogoutResponse 전달&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    SP--&gt;&gt;U: 로그아웃 완료&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;SLO 는 &lt;em&gt;모든 SP 의 세션을 한 번에 종료&lt;/em&gt;. 구현이 복잡해 &lt;em&gt;미구현 SP 도 많음&lt;/em&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;saml-vs-oidc-선택&quot;&gt;SAML vs OIDC 선택&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart TD&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q1{타겟 사용자}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q1 --&gt;|기업 직원 SSO| Q2{기존 IdP?}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q1 --&gt;|소비자 / 모바일| OIDC[OIDC / OAuth2 권장]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q2 --&gt;|Okta, Azure AD, Ping| Q3{새 앱?}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q2 --&gt;|Google, GitHub| OIDC&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q3 --&gt;|예| OIDC_ENT[&quot;OIDC 권장&amp;#x3C;br/&gt;(단순, 모바일 지원)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q3 --&gt;|기존 SAML SP 통합| SAML[SAML 유지]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;


















































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;항목&lt;/th&gt;&lt;th&gt;SAML 2.0&lt;/th&gt;&lt;th&gt;OIDC&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;출시&lt;/td&gt;&lt;td&gt;2005&lt;/td&gt;&lt;td&gt;2014&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;페이로드&lt;/td&gt;&lt;td&gt;XML&lt;/td&gt;&lt;td&gt;JSON (JWT)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;크기&lt;/td&gt;&lt;td&gt;큼 (수 KB)&lt;/td&gt;&lt;td&gt;작음&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;모바일&lt;/td&gt;&lt;td&gt;떨어짐&lt;/td&gt;&lt;td&gt;&lt;em&gt;우수&lt;/em&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;디버깅&lt;/td&gt;&lt;td&gt;복잡 (XML 서명)&lt;/td&gt;&lt;td&gt;단순&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;기업 SSO&lt;/td&gt;&lt;td&gt;&lt;em&gt;전통적&lt;/em&gt;&lt;/td&gt;&lt;td&gt;부상 중&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;MFA / step-up&lt;/td&gt;&lt;td&gt;가능 (acr)&lt;/td&gt;&lt;td&gt;가능 (acr)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;사용자 의도&lt;/td&gt;&lt;td&gt;토큰 발급 후 &lt;em&gt;세션&lt;/em&gt;&lt;/td&gt;&lt;td&gt;토큰 자체 &lt;em&gt;반복 사용&lt;/em&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]
&lt;em&gt;2026 시점&lt;/em&gt;: 모바일 / 소비자 앱 = OIDC. &lt;em&gt;기업 SaaS 통합&lt;/em&gt; (Workday, Salesforce, ServiceNow, Slack 등) = 여전히 SAML 이 &lt;em&gt;압도적&lt;/em&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;보안-assertion-검증-체크리스트&quot;&gt;보안: Assertion 검증 체크리스트&lt;/h2&gt;

































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;검증&lt;/th&gt;&lt;th&gt;의미&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Signature&lt;/td&gt;&lt;td&gt;XML-DSig 로 IdP 공개키 검증&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Audience&lt;/td&gt;&lt;td&gt;&lt;em&gt;내가 그 audience 인지&lt;/em&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;NotBefore / NotOnOrAfter&lt;/td&gt;&lt;td&gt;유효 시간&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Issuer&lt;/td&gt;&lt;td&gt;신뢰하는 IdP 인지&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;InResponseTo&lt;/td&gt;&lt;td&gt;원래 보낸 AuthnRequest ID 와 일치&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Replay&lt;/td&gt;&lt;td&gt;NotOnOrAfter 까지 &lt;em&gt;받은 ID 중복 거절&lt;/em&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;xsw-xml-signature-wrapping-공격&quot;&gt;XSW (XML Signature Wrapping) 공격&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;XML 서명 검증 구현 버그&lt;/em&gt;를 이용해 서명된 영역과 실제 처리 영역을 다르게 만드는 공격:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;xml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;&amp;#x3C;!-- 공격자가 조작한 Assertion (개념도) --&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;saml:Assertion&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; ID&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;evil&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; ...&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;  &amp;#x3C;!-- 공격자가 원하는 내용 (서명 없음) --&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;saml:NameID&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;admin@company.com&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;saml:NameID&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;  &amp;#x3C;!-- 원본 서명된 Assertion 은 다른 위치에 숨겨짐 --&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;saml:Assertion&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;방어&lt;/strong&gt;: &lt;em&gt;검증된 XML 라이브러리&lt;/em&gt; 사용. Assertion ID 의 canonicalization 엄격 검증. &lt;em&gt;자체 XML 파서 구현 금지&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id=&quot;단점--함정&quot;&gt;단점 / 함정&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;XML Canonicalization 의 함정&lt;/strong&gt; = &lt;em&gt;공백 처리&lt;/em&gt; 까지 정확해야 서명 검증. 라이브러리 버그 다수 (XSW 공격).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;시계 어긋남&lt;/strong&gt; = NotBefore / NotOnOrAfter 검증에서 &lt;em&gt;수십 초 clock skew&lt;/em&gt; 가 문제. NTP 필수.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;메시지 크기&lt;/strong&gt; = HTTP-Redirect 으로 보내는 인코딩된 SAMLRequest 가 URL 길이 한계 초과 가능. HTTP-POST binding 사용.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Assertion 의 &lt;em&gt;audience&lt;/em&gt; 검증 누락&lt;/strong&gt; = &lt;em&gt;다른 SP 의 assertion&lt;/em&gt; 이 재사용 가능.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IdP-initiated CSRF&lt;/strong&gt; = InResponseTo 없으므로 &lt;em&gt;replay 가능&lt;/em&gt;. SP 에서 허용 여부 명시.&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;okta-설정-예시&quot;&gt;Okta 설정 예시&lt;/h2&gt;
&lt;p&gt;실제 SaaS 통합에서 SP 설정 예시:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;yaml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# Okta 앱 설정 (개념도)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;SingleSignOnURL&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;https://app.example.com/auth/saml/callback&lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;  # ACS URL&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;Audience URI (Entity ID)&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;https://app.example.com&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;NameID format&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;EmailAddress&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;Attribute Statements&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  - &lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;email&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;user.email&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  - &lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;firstName&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;user.firstName&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  - &lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;lastName&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;user.lastName&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  - &lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;groups&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;user.groups&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# python3-saml 라이브러리로 Assertion 검증&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; onelogin.saml2.auth &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; OneLogin_Saml2_Auth&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; prepare_django_request&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(request):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;        &quot;https&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;on&quot;&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; request.is_secure() &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;else&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;off&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;        &quot;http_host&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: request.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;META&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;HTTP_HOST&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;        &quot;script_name&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: request.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;META&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;PATH_INFO&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;        &quot;post_data&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: request.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;POST&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.dict(),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; acs&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(request):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    req &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; prepare_django_request(request)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    auth &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; OneLogin_Saml2_Auth(req, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;custom_base_path&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;settings.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;SAML_FOLDER&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    auth.process_response()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    errors &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; auth.get_errors()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; not&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; errors:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        user_email &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; auth.get_nameid()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        attributes &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; auth.get_attributes()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;        # 세션 생성 로직&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;관련-위키&quot;&gt;관련 위키&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[OAuth2]]&lt;/li&gt;
&lt;li&gt;[[OpenID Connect]]&lt;/li&gt;
&lt;li&gt;[[JWT]]&lt;/li&gt;
&lt;li&gt;[[Session Cookie]]&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>saml</category><category>sso</category><category>enterprise</category><category>auth</category><category>security</category><author>koa (김신건)</author></item><item><title>[Auth] Session Cookie: HttpOnly, SameSite, Secure flag</title><link>https://shinkeonkim.com/wiki/auth-security/session-cookie/</link><guid isPermaLink="true">https://shinkeonkim.com/wiki/auth-security/session-cookie/</guid><pubDate>Wed, 15 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;정의&quot;&gt;정의&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Session Cookie&lt;/strong&gt; 는 &lt;em&gt;서버가 발급한 임의 ID 를 쿠키에 보관&lt;/em&gt;. 매 요청 자동 첨부. &lt;em&gt;서버 측 세션 store&lt;/em&gt; (Redis, DB) 와 짝.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]
&lt;em&gt;JWT 와의 비교&lt;/em&gt;: session = &lt;em&gt;stateful&lt;/em&gt;, JWT = &lt;em&gt;stateless&lt;/em&gt;. 작은-중간 모놀리스에서는 &lt;em&gt;session 이 더 단순 + 즉시 revoke 가능&lt;/em&gt;. 분산 환경에서는 JWT.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;쿠키-속성&quot;&gt;쿠키 속성&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;http&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;Set-Cookie&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; SID=abc123; Path=/; Domain=example.com;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            Secure; HttpOnly; SameSite=Lax;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            Max-Age=3600&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;









































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;속성&lt;/th&gt;&lt;th&gt;의미&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;HttpOnly&lt;/code&gt;&lt;/td&gt;&lt;td&gt;JS 에서 접근 불가 (XSS 방어)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;Secure&lt;/code&gt;&lt;/td&gt;&lt;td&gt;HTTPS 만&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;SameSite=Strict|Lax|None&lt;/code&gt;&lt;/td&gt;&lt;td&gt;cross-site 동작 제어&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;Path&lt;/code&gt;&lt;/td&gt;&lt;td&gt;URL prefix&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;Domain&lt;/code&gt;&lt;/td&gt;&lt;td&gt;도메인 범위&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;Max-Age&lt;/code&gt; / &lt;code&gt;Expires&lt;/code&gt;&lt;/td&gt;&lt;td&gt;만료&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;Partitioned&lt;/code&gt; &lt;em&gt;(2024+)&lt;/em&gt;&lt;/td&gt;&lt;td&gt;3rd-party cookie 격리&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;__Host-&lt;/code&gt; prefix&lt;/td&gt;&lt;td&gt;Secure + Path=/ + Domain 없음 강제&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;http-세션-전체-흐름&quot;&gt;HTTP 세션 전체 흐름&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;sequenceDiagram&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    autonumber&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant C as Client&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant W as WebServer&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant S as SessionStore&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    C-&gt;&gt;W: POST /login (id, password)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    W-&gt;&gt;S: session.set(SID, userId and expiresAt)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    W--&gt;&gt;C: 200 OK, Set-Cookie: SID=abc123, HttpOnly, Secure&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    C-&gt;&gt;W: GET /profile (Cookie: SID=abc123)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    W-&gt;&gt;S: session.get(SID=abc123)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    S--&gt;&gt;W: userId=42, expiresAt=...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    W--&gt;&gt;C: 200 OK (profile data)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Note over C,W: 로그아웃&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    C-&gt;&gt;W: POST /logout (Cookie: SID=abc123)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    W-&gt;&gt;S: session.delete(SID=abc123)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    W--&gt;&gt;C: 200 OK, SID cleared (Max-Age=0)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;로그아웃은 &lt;em&gt;반드시 서버 store 도 삭제&lt;/em&gt;. 쿠키만 지우면 &lt;em&gt;SID 재사용 가능&lt;/em&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;samesite-3-모드&quot;&gt;SameSite 3 모드&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart TB&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Strict[&quot;Strict&amp;#x3C;br/&gt;같은 site 만 cookie 전송&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Lax[&quot;Lax (기본)&amp;#x3C;br/&gt;top-level GET 만 허용&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    None[&quot;None&amp;#x3C;br/&gt;모든 cross-site 허용 (+Secure 필수)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Strict --&gt; S1[&quot;가장 안전&amp;#x3C;br/&gt;외부 링크 접근 시 로그인 풀림&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Lax --&gt; L1[&quot;보안 + UX 균형&amp;#x3C;br/&gt;대부분 권장&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    None --&gt; N1[&quot;3rd-party 통합&amp;#x3C;br/&gt;(예: 임베드 위젯)&quot;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;









































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th&gt;Strict&lt;/th&gt;&lt;th&gt;Lax&lt;/th&gt;&lt;th&gt;None&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;직접 입력 (주소창)&lt;/td&gt;&lt;td&gt;O&lt;/td&gt;&lt;td&gt;O&lt;/td&gt;&lt;td&gt;O&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;외부 사이트 링크 클릭&lt;/td&gt;&lt;td&gt;X&lt;/td&gt;&lt;td&gt;O (top-level GET)&lt;/td&gt;&lt;td&gt;O&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;외부 폼 POST&lt;/td&gt;&lt;td&gt;X&lt;/td&gt;&lt;td&gt;X&lt;/td&gt;&lt;td&gt;O&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;&amp;#x3C;img src=&quot;other.com&quot;&gt;&lt;/code&gt;&lt;/td&gt;&lt;td&gt;X&lt;/td&gt;&lt;td&gt;X&lt;/td&gt;&lt;td&gt;O&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;iframe POST&lt;/td&gt;&lt;td&gt;X&lt;/td&gt;&lt;td&gt;X&lt;/td&gt;&lt;td&gt;O&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;2020 부터 &lt;em&gt;Chrome 의 기본값 = Lax&lt;/em&gt;. 명시하지 않은 옛 쿠키도 &lt;em&gt;Lax&lt;/em&gt; 적용.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;csrf-공격과-방어&quot;&gt;CSRF 공격과 방어&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;sequenceDiagram&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    autonumber&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant V as Victim&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant A as AttackerSite&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant B as BankSite&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    V-&gt;&gt;B: 로그인 (SID=abc 쿠키 발급)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    V-&gt;&gt;A: 악성 사이트 방문&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    A--&gt;&gt;V: &quot;&amp;#x3C;form action=&apos;bank.com/transfer&apos; method=&apos;POST&apos;&gt; 자동 submit&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    V-&gt;&gt;B: POST /transfer (Cookie: SID=abc 자동 첨부, amount=100000)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Note over B: CSRF 취약하면 정상 처리됨&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;방어 전략&lt;/strong&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;SameSite=Lax/Strict&lt;/strong&gt;: 외부 POST 요청 시 쿠키 전송 차단&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CSRF Token&lt;/strong&gt;: 서버가 생성한 토큰을 폼에 hidden 필드로 삽입, 요청 시 검증&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Double Submit Cookie&lt;/strong&gt;: 쿠키와 헤더에 같은 토큰 → origin 확인 효과&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Referer / Origin 헤더 검증&lt;/strong&gt;: 신뢰할 수 없는 origin 차단&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;html&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;&amp;#x3C;!-- CSRF token 폼 예시 --&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;form&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; method&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;POST&quot;&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; action&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;/transfer&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;input&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; type&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;hidden&quot;&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; name&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;_csrf&quot;&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; value&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;{{ csrf_token }}&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;input&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; type&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;number&quot;&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; name&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;amount&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;button&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; type&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;submit&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;송금&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;button&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;form&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;session-store-패턴&quot;&gt;Session Store 패턴&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart LR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    C[Client] --&gt;|SID=abc| Web[Web App 1]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    C2[Client] --&gt;|SID=abc| Web2[Web App 2]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Web --&gt; Store[(Session Store&amp;#x3C;br/&gt;Redis)]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Web2 --&gt; Store&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;세션 store 옵션:&lt;/p&gt;





























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Store&lt;/th&gt;&lt;th&gt;특징&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;메모리&lt;/td&gt;&lt;td&gt;단일 노드, 재시작 시 손실&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;파일&lt;/td&gt;&lt;td&gt;단일 노드&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Redis&lt;/td&gt;&lt;td&gt;&lt;em&gt;분산 표준&lt;/em&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;DB (Postgres / MySQL)&lt;/td&gt;&lt;td&gt;단순 / 큰 부담&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;클라이언트 (encrypted cookie)&lt;/td&gt;&lt;td&gt;&lt;em&gt;stateless 처럼&lt;/em&gt;. Express의 cookie-session, Rails encrypted cookie&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;구현-예시-nodejs--express&quot;&gt;구현 예시 (Node.js / Express)&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;typescript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; redisClient&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; createClient&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;({ url: process.env.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;REDIS_URL&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; });&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;await&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; redisClient.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;connect&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;app.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;use&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;session&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  store: &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;new&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; RedisStore&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;({ client: redisClient }),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  secret: process.env.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;SESSION_SECRET&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,  &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// 32+ bytes random&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  resave: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;false&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  saveUninitialized: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;false&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  name: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;__Host-SID&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,  &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// __Host- prefix 강제 보안&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  cookie: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    httpOnly: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;true&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    secure: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;true&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,           &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// HTTPS only&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    sameSite: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;lax&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,        &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// CSRF 완화&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    maxAge: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;1000&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; *&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 60&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; *&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 30&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// 30분&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}));&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;cookie-prefix-__host--__secure-&quot;&gt;Cookie Prefix (&lt;code&gt;__Host-&lt;/code&gt;, &lt;code&gt;__Secure-&lt;/code&gt;)&lt;/h2&gt;




















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Prefix&lt;/th&gt;&lt;th&gt;강제 속성&lt;/th&gt;&lt;th&gt;효과&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;__Host-&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Secure + Path=/ + Domain 없음&lt;/td&gt;&lt;td&gt;&lt;em&gt;가장 강력&lt;/em&gt;. 서브도메인 격리&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;__Secure-&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Secure 만&lt;/td&gt;&lt;td&gt;HTTPS 전송 보장&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;http&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;Set-Cookie&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; __Host-SID=abc123; Path=/; Secure; HttpOnly; SameSite=Lax&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;__Host-&lt;/code&gt; 사용 시 Domain 속성을 설정하면 &lt;em&gt;브라우저가 쿠키를 무시&lt;/em&gt;. Path=/ 강제.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;stateless-vs-stateful&quot;&gt;Stateless vs Stateful&lt;/h2&gt;



































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;항목&lt;/th&gt;&lt;th&gt;Session (cookie)&lt;/th&gt;&lt;th&gt;JWT&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;서버 store&lt;/td&gt;&lt;td&gt;필수&lt;/td&gt;&lt;td&gt;불필요&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Revoke&lt;/td&gt;&lt;td&gt;&lt;em&gt;즉시 (DB 삭제)&lt;/em&gt;&lt;/td&gt;&lt;td&gt;만료까지 어려움&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Cookie 자동 첨부&lt;/td&gt;&lt;td&gt;&lt;em&gt;예&lt;/em&gt;&lt;/td&gt;&lt;td&gt;Bearer header 수동&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;CSRF&lt;/td&gt;&lt;td&gt;&lt;em&gt;위험&lt;/em&gt; (SameSite 로 완화)&lt;/td&gt;&lt;td&gt;&lt;em&gt;덜 위험&lt;/em&gt; (헤더면)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;XSS&lt;/td&gt;&lt;td&gt;&lt;em&gt;덜 위험&lt;/em&gt; (HttpOnly)&lt;/td&gt;&lt;td&gt;localStorage 면 &lt;em&gt;위험&lt;/em&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;session-fixation&quot;&gt;Session Fixation&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;sequenceDiagram&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant Att as Attacker&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant S as Site&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant V as Victim&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Att-&gt;&gt;S: GET /login (SID=ATTACK123)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    S--&gt;&gt;Att: 응답&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Att-&gt;&gt;V: &quot;이 링크로 로그인해&quot; (SID=ATTACK123 박힌 링크)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    V-&gt;&gt;S: 로그인 + SID=ATTACK123 유지&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    S-&gt;&gt;S: 인증 성공, SID=ATTACK123 가 권한 가짐&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Att-&gt;&gt;S: SID=ATTACK123 으로 접근&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Note over Att,S: Victim 의 권한 행사&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;방어&lt;/strong&gt;: &lt;em&gt;로그인 직후 새 SID 발급&lt;/em&gt;. 옛 SID 무효화.&lt;/p&gt;
&lt;h2 id=&quot;보안-체크리스트&quot;&gt;보안 체크리스트&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;✓ HttpOnly                (XSS 방어)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;✓ Secure                  (HTTPS 만)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;✓ SameSite=Lax 이상       (CSRF 완화)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;✓ session 만료 짧게        (15-60분 inactive timeout)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;✓ session rotation        (privilege escalation 시 새 SID)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;✓ session_id 충분히 랜덤   (128+ bits)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;✓ 로그아웃 시 server-side 삭제&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;✓ __Host- 또는 __Secure- prefix 사용&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;✓ CSRF token 또는 SameSite=Strict&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;흔한-함정&quot;&gt;흔한 함정&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;HttpOnly&lt;/code&gt; 없음&lt;/strong&gt; = XSS 1줄로 세션 탈취.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Secure&lt;/code&gt; 없음&lt;/strong&gt; = HTTP 구간에서 &lt;em&gt;평문 노출&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;SameSite&lt;/code&gt; 미설정&lt;/strong&gt; = 옛 브라우저는 None 처럼 동작 → CSRF.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;세션 &lt;em&gt;영구&lt;/em&gt;&lt;/strong&gt; = Max-Age 없이 영구 쿠키. 사용자 비활성 후에도 위험.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;로그아웃이 &lt;em&gt;cookie 만 삭제&lt;/em&gt;&lt;/strong&gt; = server-side store 에 그대로. 토큰이 &lt;em&gt;재사용 가능&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Session Fixation 미방어&lt;/strong&gt; = 로그인 후 SID 교체 안 하면 &lt;em&gt;공격자가 사전에 SID 심기 가능&lt;/em&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;partitioned-cookie-chips-2024&quot;&gt;Partitioned Cookie (CHIPS, 2024+)&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;CHIPS (Cookies Having Independent Partitioned State)&lt;/strong&gt;: 3rd-party 쿠키 퇴장 이후 임베드 시나리오를 위한 대안.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;http&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;Set-Cookie&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; __Host-widget_session=xyz; Secure; HttpOnly; SameSite=None; Partitioned&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;쿠키가 &lt;em&gt;top-level site 별로 격리&lt;/em&gt;. &lt;code&gt;a.com&lt;/code&gt; 에서 설정한 &lt;code&gt;widget.com&lt;/code&gt; 쿠키는 &lt;code&gt;b.com&lt;/code&gt; 에서 보이지 않음&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Chrome 118+ 기본 지원&lt;/em&gt;, Firefox 지원 예정&lt;/li&gt;
&lt;li&gt;기존 &lt;code&gt;SameSite=None&lt;/code&gt; 임베드 쿠키의 &lt;em&gt;미래 대안&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;redis-세션-만료-패턴&quot;&gt;Redis 세션 만료 패턴&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;typescript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// 비활성 만료: 활동 시마다 TTL 갱신&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;async&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; function&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; touchSession&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;sid&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; string&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; INACTIVE_TIMEOUT&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 30&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; *&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 60&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;; &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// 30분&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  await&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; redis.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;expire&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;`session:${&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;sid&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;}`&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;INACTIVE_TIMEOUT&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// 절대 만료: 최초 설정 후 고정&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;async&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; function&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; setSession&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;sid&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; string&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;data&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; object&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; ABSOLUTE_TIMEOUT&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 8&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; *&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 60&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; *&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 60&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;; &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// 8시간&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  await&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; redis.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;setex&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;`session:${&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;sid&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;}`&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;ABSOLUTE_TIMEOUT&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;JSON&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;stringify&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(data));&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;비활성 timeout + 절대 timeout 을 모두 적용&lt;/em&gt;하는 것이 정통. 비활성으로는 짧게 (30분), 절대값으로는 길게 (8시간).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;관련-위키&quot;&gt;관련 위키&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[JWT]]&lt;/li&gt;
&lt;li&gt;[[CSRF]]&lt;/li&gt;
&lt;li&gt;[[CORS]]&lt;/li&gt;
&lt;li&gt;[[Redis Cache Patterns]]&lt;/li&gt;
&lt;li&gt;[[Sticky Session]]&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>session</category><category>cookie</category><category>auth</category><category>security</category><category>backend</category><author>koa (김신건)</author></item><item><title>[Auth] mTLS (Mutual TLS): 양방향 인증서 인증</title><link>https://shinkeonkim.com/wiki/auth-security/mtls/</link><guid isPermaLink="true">https://shinkeonkim.com/wiki/auth-security/mtls/</guid><pubDate>Thu, 25 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;정의&quot;&gt;정의&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;mTLS (Mutual TLS)&lt;/strong&gt; 는 &lt;em&gt;서버뿐 아니라 클라이언트도 TLS 인증서로 자기 신분 증명&lt;/em&gt;. 일반 TLS = &lt;em&gt;서버 인증만&lt;/em&gt;, mTLS = &lt;em&gt;양방향&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;활용: &lt;em&gt;서비스 메시 (Istio, Linkerd)&lt;/em&gt;, &lt;em&gt;기업 내부 API&lt;/em&gt;, &lt;em&gt;금융 / B2B 통합&lt;/em&gt;, &lt;em&gt;IoT 기기&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id=&quot;tls-vs-mtls-흐름&quot;&gt;TLS vs mTLS 흐름&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;sequenceDiagram&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    autonumber&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant C as Client&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant S as Server&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    rect rgb(220,240,220)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Note over C,S: 일반 TLS&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    C-&gt;&gt;S: ClientHello&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    S--&gt;&gt;C: ServerHello + Certificate&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    C-&gt;&gt;C: 서버 인증서 검증&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    C--&gt;&gt;S: Finished&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    end&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    rect rgb(255,235,220)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Note over C,S: mTLS&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    C-&gt;&gt;S: ClientHello&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    S--&gt;&gt;C: ServerHello + Certificate&amp;#x3C;br/&gt;+ CertificateRequest&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    C-&gt;&gt;S: ClientCertificate + Finished&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    S-&gt;&gt;S: 클라이언트 인증서 검증&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    S--&gt;&gt;C: Finished&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    end&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;인증서-발급-흐름&quot;&gt;인증서 발급 흐름&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart LR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    CA[Root CA] --&gt;|sign| IntCA[Intermediate CA]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    IntCA --&gt;|sign| ServerCert[Server Cert]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    IntCA --&gt;|sign| ClientCert[Client Cert]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Client --&gt;|보유| ClientCert&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Server --&gt;|보유| ServerCert&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;자세한 인증서 체인은 [[TLS]] 참고.&lt;/p&gt;
&lt;h2 id=&quot;사용-시나리오&quot;&gt;사용 시나리오&lt;/h2&gt;
&lt;h3 id=&quot;1-service-mesh-istio-linkerd-consul&quot;&gt;1. Service Mesh (Istio, Linkerd, Consul)&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart LR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    subgraph A[Service A]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        AApp[App]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        AProxy[Sidecar Envoy]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    end&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    subgraph B[Service B]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        BProxy[Sidecar Envoy]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        BApp[App]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    end&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    AApp --&gt;|plain HTTP| AProxy&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    AProxy --&gt;|mTLS auto| BProxy&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    BProxy --&gt;|plain HTTP| BApp&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;애플리케이션이 mTLS 를 신경 쓰지 않음&lt;/em&gt;. Sidecar 가 자동.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Zero Trust&lt;/em&gt; 네트워크의 토대.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;2-spiffespire-워크로드-신원&quot;&gt;2. SPIFFE/SPIRE: 워크로드 신원&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;SPIFFE ID = spiffe://example.com/ns/prod/sa/web&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;쿠버네티스 ServiceAccount → &lt;em&gt;SVID (SPIFFE Verifiable Identity Document)&lt;/em&gt; → 짧은 mTLS 인증서.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]
&lt;em&gt;수동 인증서 발급/배포 없이&lt;/em&gt; SPIRE 가 &lt;em&gt;자동 회전&lt;/em&gt;. 회전 주기는 &lt;em&gt;시간 단위&lt;/em&gt;. 침해 시 영향 최소화.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;3-기업-vpn--zero-trust&quot;&gt;3. 기업 VPN / Zero Trust&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;BeyondCorp 류 &lt;em&gt;디바이스 인증&lt;/em&gt; + 사용자 인증.&lt;/li&gt;
&lt;li&gt;기기 인증서 + 사용자 OIDC.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;4-iot--디바이스&quot;&gt;4. IoT / 디바이스&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;수백만 디바이스&lt;/em&gt; 각자 인증서.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;제조 시점에 발급&lt;/em&gt; (factory cert).&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;인증서-회전&quot;&gt;인증서 회전&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;gantt&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    title 인증서 라이프사이클&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    dateFormat YYYY-MM-DD&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    section Cert&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    유효기간 1: 2026-01-01, 90d&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    유효기간 2 (회전):  active, 2026-03-15, 90d&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    유효기간 3 (회전):  2026-06-01, 90d&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;회전 주기&lt;/th&gt;&lt;th&gt;적합&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;1년&lt;/td&gt;&lt;td&gt;legacy 시스템&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;90일&lt;/td&gt;&lt;td&gt;Let’s Encrypt 표준&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;24시간&lt;/td&gt;&lt;td&gt;짧은 SVID (SPIFFE)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;1시간&lt;/td&gt;&lt;td&gt;고보안 환경&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;짧을수록 &lt;em&gt;침해 영향 최소화&lt;/em&gt;. &lt;em&gt;자동화 없으면 운영 불가&lt;/em&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;oauth--mtls-rfc-8705&quot;&gt;OAuth + mTLS (RFC 8705)&lt;/h2&gt;
&lt;p&gt;OAuth 의 &lt;em&gt;client_credentials&lt;/em&gt; 에서 &lt;em&gt;client_secret 대신 client cert&lt;/em&gt;. &lt;em&gt;큰 기업 / 금융 API&lt;/em&gt;.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;sequenceDiagram&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Service A-&gt;&gt;Auth: POST /token&amp;#x3C;br/&gt;(mTLS client cert)&amp;#x3C;br/&gt;grant_type=client_credentials&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Auth-&gt;&gt;Auth: cert 검증&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Auth--&gt;&gt;Service A: access_token (cnf claim 에 cert hash)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Service A-&gt;&gt;Service B: Bearer &amp;#x3C;token&gt;&amp;#x3C;br/&gt;(mTLS, 같은 cert)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Service B-&gt;&gt;Service B: token 의 cnf == cert hash?&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Service B--&gt;&gt;Service A: 자원&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;흔한-함정&quot;&gt;흔한 함정&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;인증서 만료 사고&lt;/strong&gt; = 자동화 없으면 &lt;em&gt;언젠가는 만료&lt;/em&gt; → 서비스 다운. &lt;em&gt;모니터링 + 자동 갱신&lt;/em&gt; 필수.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;인증서 회전과 사용 &lt;em&gt;동시&lt;/em&gt;&lt;/strong&gt; = 회전 직후 옛 인증서로 검증 실패. &lt;em&gt;grace period&lt;/em&gt; 필요.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CRL (Certificate Revocation List) 의 &lt;em&gt;체크 누락&lt;/em&gt;&lt;/strong&gt; = 침해 인증서가 &lt;em&gt;수개월 유효&lt;/em&gt;. 짧은 인증서 (SPIFFE) 가 &lt;em&gt;현실적 답&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;mTLS 끝점에 &lt;em&gt;L7 LB&lt;/em&gt;&lt;/strong&gt; = TLS 종료 시점에 &lt;em&gt;클라이언트 인증서 정보 손실&lt;/em&gt;. &lt;code&gt;X-Forwarded-Client-Cert&lt;/code&gt; 같은 헤더로 &lt;em&gt;전달&lt;/em&gt; 또는 &lt;em&gt;passthrough LB&lt;/em&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;인증서-형식&quot;&gt;인증서 형식&lt;/h2&gt;





























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;형식&lt;/th&gt;&lt;th&gt;의미&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;.crt&lt;/code&gt; / &lt;code&gt;.pem&lt;/code&gt;&lt;/td&gt;&lt;td&gt;base64 인코딩 X.509 (텍스트)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;.der&lt;/code&gt;&lt;/td&gt;&lt;td&gt;binary DER&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;.p7b&lt;/code&gt; / &lt;code&gt;.p7c&lt;/code&gt;&lt;/td&gt;&lt;td&gt;PKCS#7 (체인 포함)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;.pfx&lt;/code&gt; / &lt;code&gt;.p12&lt;/code&gt;&lt;/td&gt;&lt;td&gt;PKCS#12 (인증서 + private key)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;.key&lt;/code&gt;&lt;/td&gt;&lt;td&gt;private key 만&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;mtls-vs-대안-비교&quot;&gt;mTLS vs 대안 비교&lt;/h2&gt;









































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;방식&lt;/th&gt;&lt;th&gt;강도&lt;/th&gt;&lt;th&gt;복잡도&lt;/th&gt;&lt;th&gt;주요 용도&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;mTLS&lt;/td&gt;&lt;td&gt;최강 (인증서 기반)&lt;/td&gt;&lt;td&gt;높음&lt;/td&gt;&lt;td&gt;서비스 간 Zero Trust&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;API Key&lt;/td&gt;&lt;td&gt;중간 (공유 비밀)&lt;/td&gt;&lt;td&gt;낮음&lt;/td&gt;&lt;td&gt;외부 API 파트너&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;JWT Bearer&lt;/td&gt;&lt;td&gt;중간 (서명 검증)&lt;/td&gt;&lt;td&gt;낮음&lt;/td&gt;&lt;td&gt;유저 인증, OIDC&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;HMAC 서명&lt;/td&gt;&lt;td&gt;중간 (요청 무결성)&lt;/td&gt;&lt;td&gt;중간&lt;/td&gt;&lt;td&gt;Webhook&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;IP Whitelist&lt;/td&gt;&lt;td&gt;낮음 (위조 가능)&lt;/td&gt;&lt;td&gt;낮음&lt;/td&gt;&lt;td&gt;최후 수단&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]
mTLS 는 &lt;em&gt;인증서 유효 + 개인키 보유&lt;/em&gt;를 동시에 증명. API Key 는 &lt;em&gt;탈취 시 즉시 재사용 가능&lt;/em&gt;. 고보안 서비스 간 통신에는 mTLS.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;ocsp-vs-crl-vs-단기-인증서&quot;&gt;OCSP vs CRL vs 단기 인증서&lt;/h2&gt;
&lt;p&gt;인증서 &lt;em&gt;폐지 확인&lt;/em&gt; 3가지 방법:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart TD&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Verify[&quot;인증서 유효성 확인&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Verify --&gt; CRL[&quot;CRL 다운로드&amp;#x3C;br/&gt;전체 폐지 목록&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Verify --&gt; OCSP[&quot;OCSP 단건 조회&amp;#x3C;br/&gt;실시간 CA 확인&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Verify --&gt; Short[&quot;단기 인증서&amp;#x3C;br/&gt;만료로 자동 폐지&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    CRL --&gt; R1[&quot;큰 파일, 갱신 지연&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    OCSP --&gt; R2[&quot;실시간, CA 의존성&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Short --&gt; R3[&quot;자동화 필수, 권장 방식&quot;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;






























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;방식&lt;/th&gt;&lt;th&gt;확인 방법&lt;/th&gt;&lt;th&gt;단점&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;CRL&lt;/td&gt;&lt;td&gt;CA 발행 폐지 목록 다운로드&lt;/td&gt;&lt;td&gt;큰 파일, 갱신 주기 지연&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;OCSP&lt;/td&gt;&lt;td&gt;실시간 CA 단건 조회&lt;/td&gt;&lt;td&gt;CA 다운 시 검증 불가 (Fail-Open 위험)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;OCSP Stapling&lt;/td&gt;&lt;td&gt;서버가 미리 조회, 인증서에 첨부&lt;/td&gt;&lt;td&gt;서버 설정 필요&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;단기 인증서&lt;/td&gt;&lt;td&gt;짧은 유효기간 (SPIFFE)&lt;/td&gt;&lt;td&gt;자동화 없으면 운영 불가&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;디버깅-openssl-s_client&quot;&gt;디버깅: openssl s_client&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# 서버 인증서 확인&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;openssl&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; s_client&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -connect&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; example.com:443&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -showcerts&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# mTLS: 클라이언트 인증서 제시&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;openssl&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; s_client&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;  -connect&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; api.example.com:443&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;  -cert&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; client.crt&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;  -key&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; client.key&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;  -CAfile&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; ca.crt&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# ALPN 협상 확인&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;openssl&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; s_client&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -alpn&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; h2&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -connect&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; example.com:443&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; 2&gt;&amp;#x26;1&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; |&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; grep&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; ALPN&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;주요 확인 포인트:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Verify return code: 0 (ok)&lt;/code&gt; = 검증 성공&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SSL-Session:&lt;/code&gt; 안의 Protocol 및 Cipher Suite&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Peer certificate:&lt;/code&gt; 서버 인증서 Subject / Issuer&lt;/li&gt;
&lt;li&gt;&lt;code&gt;No client certificate CA names sent&lt;/code&gt; = 서버가 클라이언트 인증서 요구 안 함&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;istio-peerauthentication-mtls-in-k8s&quot;&gt;Istio PeerAuthentication (mTLS in K8s)&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart LR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    subgraph SvcA[&quot;Service A Pod&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        AppA[App]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        ProxyA[&quot;Envoy Proxy&amp;#x3C;br/&gt;자동 mTLS&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    end&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    subgraph SvcB[&quot;Service B Pod&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        ProxyB[&quot;Envoy Proxy&amp;#x3C;br/&gt;자동 mTLS&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        AppB[App]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    end&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    AppA --&gt;|&quot;plain HTTP&quot;| ProxyA&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    ProxyA --&gt;|&quot;mTLS (SPIFFE)&quot;| ProxyB&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    ProxyB --&gt;|&quot;plain HTTP&quot;| AppB&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;yaml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# PeerAuthentication: 네임스페이스 전체 STRICT mTLS 강제&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;apiVersion&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;security.istio.io/v1beta1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;kind&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;PeerAuthentication&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;metadata&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;  name&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;default&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;  namespace&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;production&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;spec&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;  mtls&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;    mode&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;STRICT&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;





















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;모드&lt;/th&gt;&lt;th&gt;동작&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;PERMISSIVE&lt;/code&gt;&lt;/td&gt;&lt;td&gt;mTLS + plain 혼용 (마이그레이션 중)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;STRICT&lt;/code&gt;&lt;/td&gt;&lt;td&gt;mTLS 강제 (권장 최종 상태)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;DISABLE&lt;/code&gt;&lt;/td&gt;&lt;td&gt;mTLS 비활성&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
&lt;code&gt;PERMISSIVE&lt;/code&gt; 로 시작 → 모니터링으로 확인 → &lt;code&gt;STRICT&lt;/code&gt; 로 전환. 한 번에 STRICT 하면 레거시 서비스 차단 위험.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;구현-체크리스트&quot;&gt;구현 체크리스트&lt;/h2&gt;
&lt;ul class=&quot;contains-task-list&quot;&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; CA 인프라: 독립 Root CA, Intermediate CA 분리&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 인증서 만료 모니터링: Datadog, Prometheus alert&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 자동 갱신: SPIRE / cert-manager&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; grace period: 이전 인증서 30일 중복 신뢰&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; CRL 체크: OCSP Stapling 또는 단기 인증서&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; L7 LB 고려: passthrough 또는 &lt;code&gt;X-Forwarded-Client-Cert&lt;/code&gt;&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 폐기 절차: 침해 시 즉시 폐기 + 신규 발급 SLA&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;관련-위키&quot;&gt;관련 위키&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[TLS]]&lt;/li&gt;
&lt;li&gt;[[OAuth2]]&lt;/li&gt;
&lt;li&gt;[[Service Mesh]] (Istio, Linkerd)&lt;/li&gt;
&lt;li&gt;[[Kubernetes RBAC]]&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>mtls</category><category>tls</category><category>security</category><category>service-mesh</category><category>backend</category><author>koa (김신건)</author></item></channel></rss>