🌱 Infra/KeyCloak

인증 및 권한 프로토콜 OAuth 2.0, OpenID Connect, SAML 그리고 Zero Trust에 대하여

mini_world 2024. 11. 12. 22:03
목차 접기

이 포스팅에서는 자세한 내용 보다는 개념맛보기👅 수준의 내용입니다.

 

 

📌 OAuth 2.0, OpenID Connect, SAML 간단 개념 정리

 

  • OAuth 2.0: 권한 부여에 중점을 둔 프로토콜
  • OpenID Connect: OAuth 2.0을 확장하여 인증 레이어 추가
  • SAML: 엔터프라이즈 환경에 적합한 XML 기반 인증 프로토콜

 

구분 OAuth 2.0 OpenID Connect SAML
목적 권한 부여(Authorization) 사용자 인증(Authentication) 사용자 인증(Authentication)
데이터 형식 JSON JSON XML
사용 사례 API 인증, 모바일 앱 소셜 로그인, API 인증
모바일 앱, SPA, API 기반 서비스에 적합
기업 SSO, 웹 애플리케이션 인증
전통적인 웹 애플리케이션에 적합
복잡성 중간 중간
REST API 호출, JWT 처리로 비교적 단순
높음
XML 처리, 복잡한 리다이렉션 로직 필요
보안특징 Access Token, 
Refresh Token 사용
ID Token(JWT) 추가 사용 디지털 서명, 암호화 지원
통신방식 Authorization Code, 
Access Token 교환
[서버 간 직접 통신 가능, API 기반 통신]
Client ←→ Authorization Server
- 서버 간 직접 통신
- REST/JSON 기반 통신
- Token 기반 인증
[브라우저 중심의 리디렉션 기반 통신]
SP ↔ Browser ↔ IdP
- 모든 통신이 브라우저를 통해 이루어짐
- XML 기반의 메시지
- 리다이렉션을 통한 통신
표준화 기구 IETF OpenID Foundation OASIS

 


 

📌 OAuth 2.0

OAuth 2.0 은 액세스토큰(AccessToken)을 통해 API나 보호된 리소스에 접근할 수 있도록 하는 "권한부여(Authorization)"를 위한 프로토콜이다.  (The OAuth 2.0 Authorization Framework

The OAuth 2.0 Authorization Framework.txt
0.16MB

- 주요개념

리소스 소유자
(Resource Owner)
자원에 대한 권한을 가진 사용자
클라이언트
(Client)
사용자의 승인을 받아 보호된 자원에 접근하려는 애플리케이션
사용자의 권한을 대신 받아오고, 액세스 토큰을 관리함
리소스 서버
(Resource Server)
보호된 리소스를 제공하는 서버
인증서버
(Authorization Server)
클라이언트가 사용자를 대신해 리소스 서버에 접근할 수 있는 액세스 토큰을 발급하는 서버
액세스 토큰
(Access Token)
리소스 서버에 대한 접근 권한을 나타내는 토큰 (클라이언트가 리소스 서버에 요청할 때 사용함)

 

- 주요 권한 부여 유형(Grant Types)

* 참고:
- https://alexbilbie.github.io/guide-to-oauth-2-grants/
- https://oauth.net/2/grant-types/
- https://www.2e.co.kr/news/articleView.html?idxno=208594

Authorization Code
Grant
승인 코드 유형
주로 서버간 통신에서 사용된다. 
사용자가 애플리케이션에 로그인한 후 인증 서버는 Authorization Code를 클라이언트에게 전송한다.
클라이언트는 이 코드를 사용해 인증 서버로부터 Access Token을 요청한다.
보안성이 높음 (Access Token이 브라우저를 통해 노출되지 않음).
OpenID Connect는 이 중 주로 Authorization Code Grant를 확장하여 사용한다.
Client Credentials
Grant
앱인증 유형
사용자없이 어플리케이션 자체의 인증만 필요한 경우 (e.g. 어플리케이션이 리소스 오너인 경우)사용하며,
주로 백엔드 시스템 간의 API 호출과 같은 서버 간 통신에서 사용된다.
클라이언트 애플리케이션이 자체 자격 증명을 사용하여 인증 서버에서 Access Token을 요청한다.
Device Authorization 
Grant
디바이스 인증 유형
디스플레이나 키보드가 없는 디바이스(예: 스마트 TV, IoT 기기 등)가 사용자 인증을 처리해야 할 때 사용한다.
디바이스는 사용자에게 별도의 인증 페이지(URL)와 인증 코드를 제공하고, 사용자는 이 정보를 브라우저나 스마트폰에서 입력하여 인증을 완료한다.
예) 스마트 TV에서 유튜브 앱에 로그인하고 싶을 때, TV 화면에 "https://example.com/device"와 인증 코드를 표시함
Implicit Grant
암묵적 유형 
(사용 ❌❌)
주로 SPA(Single Page Application) 에서 사용된다. 
클라이언트는 Authorization Code를 요청하는 대신, 사용자가 로그인하면 바로 Access Token을 받는다.
인증 과정이 간단하지만 Access Token이 URL로 노출되어 보안성이 낮아 사용하지 않는다.
Resource Owner Password Credentials Grant
사용자 비밀번호 인증 유형
(사용 ❌❌)
사용자가 앱 상으로 ID/비밀번호로 로그인하여 인증하고 권한 부여에 동의하면 접근 토큰을 발급하는 유형이다.
사용자 자격 증명이 클라이언트에게 노출되기 때문에 보안 위험이 높아 자사 서비스나 매우 신뢰할 수 있는 애플리케이션에서만 제한적으로 사용한다.


- OAuth  2.0 기본 흐름

 

  • (A): 클라이언트(Client)가 리소스 소유자(Resource Owner)에게 인증 요청을 보냄
  • (B): 리소스 소유자(Resource Owner)가 클라이언트(Client)에게 인증 승인 부여 (Authorization Grant).
  • (C): 클라이언트(Client)는 이 인증 승인을 권한 서버(Authorization Server)에 전달하여 액세스 토큰 요청
  • (D): 권한 서버는(Authorization Server) 클라이언트(Client)에게 액세스 토큰 반환
  • (E): 클라이언트(Client)는 리소스 서버(Resource Server)에 액세스 토큰을 사용하여 보호된 리소스에 접근
  • (F): 리소스 서버가 요청된 보호된 리소스를 클라이언트에게 반환

 

 


- OAuth  2.0 인증 예시 (철수의 구글드라이브)

+-----------+                                                                        
|  Client   |                                     +------------------+
| (포토북 앱) |  ----(A) Authorization Request----> |  Resource Owner  |
|           |  <----(B) Authorization Grant-----  |    (철수)         |
|           |                                     +------------------+
|           |                                      
|           |                                     +------------------+
|           |  ----(C) Authorization Grant----->  | Authorization    |
|           |  <----(D) Access Token-----------   |     Server       |
|           |                                     |  (구글 OAuth)     |
|           |                                     +------------------+
|           |                                     
|           |                                     +------------------+
|           |  ----(E) Access Token------------>  | Resource Server  |
|           |  <----(F) Protected Resource------  |  (구글 드라이브)    |
+-----------+                                     +------------------+

 

  • (A) Authorization Request
    • 포토북 앱은 철수에게 "구글 드라이브의 사진을 가져와도 되겠습니까?"라는 요청을 보낸다.
    • 철수는 "허용" 버튼을 클릭한다.
  • (B) Authorization Grant
    • 철수가 "허용"을 클릭하면, 구글 OAuth 서버는 포토북 앱에 인증 코드를 반환한다.
    • 인증 코드는 철수가 접근을 허용했다는 증명이다.
  • (C) Authorization Grant
    • 포토북 앱은 이 인증 코드를 구글 OAuth 서버에 보내며, "액세스 토큰을 주세요!"라고 요청한다.
  • (D) Access Token
    • 구글 OAuth 서버는 인증 코드를 검증한 후, 포토북 앱에 "액세스 토큰"을 발급한다.
    • 이 토큰은 포토북 앱이 구글 드라이브에 접근할 수 있는 권한을 부여한다.
  • (E) Access Token
    • 포토북 앱은 구글 드라이브 서버에 액세스 토큰을 포함한 요청을 보낸다.
    • "이 토큰으로 철수의 사진을 가져오겠습니다!"라는 의미이다.
  • (F) Protected Resource
    • 구글 드라이브 서버는 액세스 토큰의 유효성을 확인한 후, 철수의 사진 파일을 포토북 앱에 반환한다.

 

 

- OAuth  2.0 Confidential Client / Public Client 개념 이해

[참고] What's the difference between Confidential and Public clients? - OAuth in Five Minutes

OAuth 2.0에서 클라이언트를 "기밀 클라이언트"와 "공용 클라이언트"로 나누는 이유보안 수준이 각기 다르기 때문이며,
보안이 취약한 환경에서는 공용 클라이언트의 토큰이 쉽게 노출될 수 있기 때문에 별도의 보안 강화 장치가 필요하다.

  • 기밀 클라이언트(Confidential Client)
    • 클라이언트가 보안된 환경에서 실행되므로, 민감한 정보를 안전하게 보관할 수 있는 클라이언트
    • 예시. 쇼핑 앱의 백엔드 서버 (.env, $key=)
    • 보통 외부와는 연결되지 않고, 자신의 서버 내부에서 민감한 정보(클라이언트 비밀, 토큰 등)를 안전하게 보관하는 클라이언트를 말한다.
  • 공용 클라이언트(Public Client)
    • 클라이언트가 보안되지 않은 환경에서 실행되므로, 민감한 정보를 안전하게 보관할 수 없는 클라이언트
    • 예시. 모바일 앱 또는 웹 브라우저 SPA
    • 개인의 스마트폰이나 PC에서 실행되는데, 디컴파일되거나 네트워크에서 데이터가 탈취될 가능성이 있다.

즉, 공용 클라이언트의 경우 PKCE(Proof Key for Code Exchange) 같은 보안 방식을 통해 인증 코드가 탈취되더라도 공격자가 사용할 수 없도록 보호해야한다.

 

- PKCE (Proof Key for Code Exchange) 란? ( 픽시(Pixy)- 라고 읽는다 😎)

공용 클라이언트의 경우 인증 코드를 탈취당하면, 이를 공격자가 다른 클라이언트에서 재사용할 위험이 있다.
PKCE는 이 문제를 해결하기 위한 보안 강화 방법이다.

  • 작동 원리:
    PKCE는 인증 요청과 인증 코드 교환 시, 코드 검증 값 (code_challenge/code_verifier) 을 추가하여 인증 코드의 안전성을 보장한다.
    • code_verifier (원본 값)
    • code_challenge (verifier의 해시값/단방향 해시로, 역산이 불가능)
  • PKCE를 사용하지 않는 시나리오
    1. 정상 앱: 인증 요청 (client_id + redirect_uri)
    2. 악성 앱: 이 요청을 가로챔
    3. 악성 앱: 가로챈 정보로 인증 서버에 접근
    4. 인증 서버: 코드 발급
    5. 악성 앱: 발급받은 코드로 액세스 토큰 획득 😈
  • PKCE를 사용하는 시나리오
    1. 정상 앱: 랜덤한 code_verifier 생성 (예: "abc123xyz789")
    2. 정상 앱: code_verifier를 해시하여 code_challenge 생성
    3. 정상 앱: 인증 요청 시 code_challenge 포함해서 전송
    4. 인증 서버: code_challenge 저장
    5. 정상 앱: 액세스 토큰 요청 시 원본 code_verifier 전송
    6. 인증 서버: code_verifier를 해시해서 저장된 challenge와 비교
  • PKCE가 포함된 다이어그램
    • 클라이언트가 랜덤한 code_verifier 생성
    • code_verifier를 SHA256으로 해시하여 code_challenge 생성
    • 인증 요청 시 code_challenge 포함하여 전송
    • 인증 서버는 code_challenge 저장
    • 인증 코드를 클라이언트에게 반
    • 클라이언트는 토큰 요청 시 원본 code_verifier 전송
    • 서버는 받은 verifier로 challenge를 생성하여 저장된 값과 비교
    • 검증 성공 시 액세스 토큰 발급
    • 클라이언트는 액세스 토큰으로 API 요청
    • 보호된 리소스 응답
+--------+                  +-----------+              +-----------+
|        |                  |           |              |           |
| Client |                  | Auth      |              | Resource  |
|        |                  | Server    |              | Server    |
+--------+                  +-----------+              +-----------+
    |                            |                           |
    | 1. Generate verifier       |                           |
    | code_verifier="abc123"     |                           |
    |                            |                           |
    | 2. Generate challenge      |                           |
    | challenge=hash(verifier)    |                           |
    |                            |                           |
    | 3. Auth Request            |                           |
    |--------------------------->|                           |
    | client_id, challenge       |                           |
    |                            |                           |
    |                            | 4. Store challenge        |
    |                            | Show auth screen          |
    |                            |                           |
    | 5. Auth Response           |                           |
    |<---------------------------|                           |
    | auth_code                  |                           |
    |                            |                           |
    | 6. Token Request           |                           |
    |--------------------------->|                           |
    | auth_code, verifier        |                           |
    |                            |                           |
    |                            | 7. Verify:                |
    |                            | hash(verifier) == challenge|
    |                            |                           |
    | 8. Token Response          |                           |
    |<---------------------------|                           |
    | access_token               |                           |
    |                            |                           |
    | 9. API Request             |                           |
    |------------------------------------------------->|    |
    | Bearer access_token        |                           |
    |                            |                           |
    | 10. Resource Response      |                           |
    |<-------------------------------------------------|    |
    | Data                       |                           |
    |                            |                           |
  1.  

 

- Bearer Tokens 🎫  (전달 방식)

OAuth2.0은 접근토큰 유형이나 사용방법에 대해 정의되어있지 않으며,
Bearer Token은 현재 가장 일반적으로 사용되는 접근토근유형이다.

Bearer Token은 "소지자 토큰"이라고도 한다.  (현금처럼, 누구든 이 토큰을 가지고 있으면 사용할 수 있다)

// Bearer Token 사용 예시
fetch('https://api.example.com/data', {
    headers: {
        'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIs...'
    }
});
  • 특징
    • 단순히 토큰을 제시하는 것만으로 인증된다. 탈취되면 누구나 사용 가능하다. (위험!)
    •  반적으로 JWT 형태로 구현
    • Authorization Header를 통해 전송되며, 인코딩처리된 페이로드(body) 또는 쿼리 파라미터로 전달된다.
      주의할점은, Bearer토큰을 쿼리 파라미터로 전송하는것은 그 자체로 보안취약점이기때문에 사용하지 않아야한다.
       

 

- Token Introspection 🔍

토큰의 현재 상태와 유효성을 확인하는 프로세스를 말한다.

// Token Introspection 요청 예시
const response = await fetch('https://auth-server.com/introspect', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/x-www-form-urlencoded'
    },
    body: 'token=eyJhbGciOiJIUzI1NiIs...'
});

// 응답 예시
{
    "active": true,
    "client_id": "123456",
    "username": "jsmith",
    "scope": "read write",
    "exp": 1589391341
}
  • 특징
    • 토큰이 유효한지 실시간 확인 가능하다,
    • 토큰의 상세 정보 조회 가능하다.
    • 중앙 집중식 토큰 관리 가능하다.

 

- Token Revocation 🚫

발급된 토큰을 무효화하는 프로세스를 말한다.
사용자 로그아웃, 보안 침해 발생, 권한 변경, 비밀번호 변경 등의 상황에서 사용할 수 있다.

// Token Revocation 요청 예시
fetch('https://auth-server.com/revoke', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/x-www-form-urlencoded'
    },
    body: 'token=eyJhbGciOiJIUzI1NiIs...'
});

 

- Bearer Tokens / Token Introspection/  Token Revocation 개념 관계 및 코드샘플

[Bearer Token]
      │
      ├──> [Token Introspection]
      │     - 토큰 유효성 확인
      │     - 토큰 정보 조회
      │
      └──> [Token Revocation]
            - 토큰 무효화
            - 접근 권한 회수
class TokenService {
    // Bearer 토큰 생성
    async createBearerToken(user) {
        return jwt.sign({ userId: user.id }, 'secret', { expiresIn: '1h' });
    }

    // 토큰 검사 (Introspection)
    async introspectToken(token) {
        try {
            const decoded = jwt.verify(token, 'secret');
            const isRevoked = await this.checkIfTokenRevoked(token);
            
            return {
                active: !isRevoked,
                ...decoded
            };
        } catch (error) {
            return { active: false };
        }
    }

    // 토큰 폐기 (Revocation)
    async revokeToken(token) {
        await redis.set(`revoked:${token}`, 'true');
        await redis.expire(`revoked:${token}`, 24 * 60 * 60); // 24시간 유지
    }
}

 


📌 OpenID Connect

사용자 인증(Authentication)을 위한 프로토콜로 시작되었으나 현재는 OAuth 2.0을 기반으로 한 사용자 인증 프로토콜이다. 

OpenID Connect는, OAuth 2.0 프로토콜 위에 구축된 Identity Layer이다.
OAuth 2.0이 권한 부여(Authorization)에 중점을 둔다면, OpenID Connect는 인증(Authentication)을 추가한 프로토콜이다.

OpenID Connect는 OAuth2.0의 Authorization Code Grant (승인코드유형)을 사용한다.

 

 

많은 대형 서비스 제공업체들이 OIDC Provider(IdP)로 활용되고있다.

- 주요 OIDC Provider

  • Google
  • Microsoft Azure AD
  • Okta
  • AWS Cognito
  • GitHub
  • Facebook

 

- 주요개념
최종 사용자
(End User) 
실제 인증을 수행하는 사용자
Relying Party
(RP)
OpenID Provider를 신뢰하고 사용자 인증을 요청하는 애플리케이션 (예, 로그인페이지)
OpenID Provider
(OP)  
사용자의 신원을 증명하고 ID Token을 발급하는 서버 (예: Google, Keycloak)
ID Token 사용자의 신원 정보를 포함하는 JWT(JSON Web Token) 형식의 토큰
UserInfo
Endpoint 
사용자 프로필 정보를 제공하는 표준화된 REST API

 

- 인증 흐름 유형

* 참고: https://backstage.forgerock.com/docs/am/6/oidc1-guide/

Authorization Code Flow
기본 인증 흐름
가장 일반적이고 안전한 방식 
서버 사이드 애플리케이션에 적합
ID Token과 Access Token 모두 백엔드에서 안전하게 처리
Implicit Flow
암묵적 흐름 
(사용 ❌❌)
SPA와 같은 클라이언트 사이드 애플리케이션에 적합
보안성이 다소 낮음
리다이렉트를 통해 직접 ID Token을 받음
Hybrid Flow
하이브리드 흐름 
Authorization Code Flow와 Implicit Flow의 조합
프론트엔드와 백엔드 모두에서 토큰이 필요한 경우 사용
일부 토큰은 프론트엔드로, 일부는 백엔드로 전달

 

- OpenID Connect의 Authorization Code Flow 다이어그램

+---------+            +--------+            +-----------+
|         |            |        |            |           |
| EndUser |            |   RP   |            |    OP     |
|         |            |        |            |(KeyCloak) |
+---------+            +--------+            +-----------+
    |                      |                      |
    | 1. 서비스 접근          |                      |
    |--------------------->|                      |
    |                      |                      |
    |                      | 2. Auth Request      |
    |                      |--------------------->|
    |                      | scope=openid profile |
    |                      | response_type=code   |
    |                      | client_id, state     |
    |                      |                      |
    | 3. 인증 화면           |                      |
    |<--------------------------------------------|
    |                      |                      |
    | 4. 인증정보 제공        |                      |
    |-------------------------------------------->|
    | (id/password)        |                      |
    |                      |                      |
    |                      | 5. Auth Code         |
    |                      |<---------------------|
    |                      | code=xyz789          |
    |                      |                      |
    |                      | 6. Token Request     |
    |                      |--------------------->|
    |                      | code, client_creds   |
    |                      |                      |
    |                      | 7. Token Response    |
    |                      |<---------------------|
    |                      | id_token             |
    |                      | access_token         |
    |                      |                      |
    |                      | 8. UserInfo Request  |
    |                      |--------------------->|
    |                      | Bearer token         |
    |                      |                      |
    |                      | 9. UserInfo Response |
    |                      |<---------------------|
    |                      | {sub,name,email,...} |
    |                      |                      |
  1. EndUser -> RP
    • 사용자가 서비스(RP) 접근
    • 로그인 필요
  2. RP -> OP
    • 인증 요청 시작
    • OpenID scope 지정
  3. OP -> EndUser : 로그인 화면 표시
  4. EndUser -> OP
    • 사용자 인증 정보 제공
    • 권한 동의
  5. OP -> RP : 인증 코드 전달
  6. RP -> OP : 코드로 토큰 요청
  7. OP -> RP
    • ID Token
    • Access Token
  8. RP -> OP : UserInfo 요청
  9. OP -> RP : 사용자 정보 전달

 

- OAuth 2.0과 OIDC의 주요 차이점 (scope 📋)

클라이언트 초기 요청에 scope 가 추가된다.

OIDC의 Scope는 클라이언트가 요청할 수 있는 사용자 정보의 범위를 정의하고,
각 Scope는 특정 사용자 정보 집합에 대한 접근 권한을 나타낸다.

Scope는 사용자 정보에 대한 표준화된 접근 방식을 제공하며, 클라이언트는 필요한 정보만 요청하고 받을 수 있다.

// OAuth 2.0 응답
{
    "access_token": "eyJ0eXAi...",
    "token_type": "Bearer",
    "expires_in": 3600,
    "refresh_token": "8xLOxBtZp8..."
}

// OIDC 응답 (OAuth 2.0 + α)
{
    "access_token": "eyJ0eXAi...",
    "token_type": "Bearer",
    "expires_in": 3600,
    "refresh_token": "8xLOxBtZp8...",
    "id_token": "eyJ0eXAiOi...",  // 추가됨!
    "scope": "openid profile email" // 특별한 scope
}

 

Scope 사용예시 1.

// Google 로그인 요청 예시
const googleAuth = {
    scope: 'openid profile email',  // 기본 사용자 정보만 요청
    // ...
};

// 받은 정보로 회원가입/로그인 처리
async function handleGoogleLogin(idToken) {
    const decoded = jwt.decode(idToken);
    await createOrUpdateUser({
        email: decoded.email,
        name: decoded.name,
        picture: decoded.picture
    });
}

Scope 사용예시 2.

// 배송 서비스의 경우
const shippingAuth = {
    scope: 'openid profile email address phone',  // 배송에 필요한 모든 정보 요청
    // ...
};

// 받은 정보로 배송 처리
async function handleShippingInfo(userInfo) {
    await createShippingOrder({
        recipient: userInfo.name,
        address: userInfo.address,
        phone: userInfo.phone_number
    });
}

 

- ID Token  JWT(JSON Web Token)

각 클레임은 토큰의 유효성 검증 (iss, aud, exp, iat), 사용자 식별 (sub), 사용자 정보 표시 (name, email) 목적으로 사용한다.

  • 사용목적
    • 인증(Authentication) 목적
    • 사용자 신원 증명
    • RP가 반드시 검증해야 함
  • 사용범위
    • RP와 OP 사이에서만 사용
    • 표준화된 클레임 필요
// ID Token은 JWT(JSON Web Token) 형식으로 발급되며, 
// 사용자 인증 후 OpenID Provider가 Relying Party(클라이언트)에게 전달하는 사용자 정보를 담고 있다.
{
    // 필수 클레임
    "iss": "https://op.example.com",  // 발급자
    "sub": "user123",                 // 사용자 식별자
    "aud": "client123",               // 클라이언트 ID
    "exp": 1516239022,                // 만료 시간
    "iat": 1516235422,                // 발급 시간

    // 선택적 클레임
    "auth_time": 1516239022,          // 인증 시간
    "nonce": "abc123",                // 재생 공격 방지
    "acr": "urn:mace:incommon:iap:silver", // 인증 문맥 클래스
    "amr": ["pwd"],                   // 인증 방법
    "azp": "client123"                // 인가된 당사자
}

 

- Access Token

  • 사용목적
    • 인가(Authorization) 목적
    • 리소스 접근 권한 표현
    • 리소스 서버마다 다른 요구사항 가능
  • 사용범위
    • 다양한 리소스 서버에서 사용
    • 각 서비스에 맞는 유연한 구조 필요
// Access Token - 포맷 자유로움
// 예시 1: 불투명 토큰
"abcd1234xyz789..."

// 예시 2: JWT 형식
{
    "sub": "user123",
    "scope": ["read", "write"],
    "exp": 1516239022
}

// 예시 3: 커스텀 포맷
{
    "userId": "user123",
    "permissions": ["admin", "user"],
    "customClaim": "value"
}

 

- ID Token VS Access Token

ID Token과 Access Token이 어떻게 다른지, 무엇을 위해 사용하는지 이해하고 넘어가자.
위에 OpenID Connect의 Authorization Code Flow 다이어그램에서
7번. ID Token을 통해서 인증을 받고, 8번. AccessToken을 가지고 userinfo를 요청한거라고 이해할 수 있다.

  ID Token Access Token
목적 인증(Authentication) 인가(Authorization)
질문 "이 사용자가 누구인가?" "이 요청이 허용되는가?"
사용 클라이언트 앱에서 직접 사용 API 요청 시 사용
형식 항상 JWT 자유로움
검증 클라이언트가 반드시 검증 리소스 서버가 검증

 

  • ID Token 
// Access Token - 포맷 자유로움
// 예시 1: 불투명 토큰
"abcd1234xyz789..."

// 예시 2: JWT 형식
{
    "sub": "user123",
    "scope": ["read", "write"],
    "exp": 1516239022
}

// 예시 3: 커스텀 포맷
{
    "userId": "user123",
    "permissions": ["admin", "user"],
    "customClaim": "value"
}
  • Access Token
// Access Token은 "무엇을 할 수 있는지"를 나타냄
// API 호출 시 사용
async function fetchUserData(accessToken) {
    // UserInfo 엔드포인트 호출
    const response = await fetch('https://api.example.com/userinfo', {
        headers: {
            'Authorization': `Bearer ${accessToken}`
        }
    });
    
    // 다른 API 호출
    const orders = await fetch('https://api.example.com/orders', {
        headers: {
            'Authorization': `Bearer ${accessToken}`
        }
    });
}
  • 사용예시
// ----------------------------------------------------------------
// 사용 시나리오
class AuthService {
    async handleAuthenticationFlow(authCode) {
        // 1. 토큰 받기
        const tokens = await fetchTokens(authCode);
        const { id_token, access_token } = tokens;
        
        // 2. ID Token으로 사용자 인증
        if (this.validateIdToken(id_token)) {
            // 로그인 처리
            const userData = jwt.decode(id_token);
            this.setLoggedInUser({
                id: userData.sub,
                name: userData.name
            });
        }
        
        // 3. Access Token으로 추가 정보 요청
        const userDetails = await this.fetchUserDetails(access_token);
        const userOrders = await this.fetchUserOrders(access_token);
    }
}

// ----------------------------------------------------------------
// 실제구현 예시

// 1. 로그인 후 두 토큰 저장
function handleLoginSuccess(tokens) {
    const { id_token, access_token } = tokens;
    
    // ID Token으로 사용자 정보 설정
    const userInfo = jwt.decode(id_token);
    localStorage.setItem('user', JSON.stringify({
        id: userInfo.sub,
        name: userInfo.name,
        email: userInfo.email
    }));
    
    // Access Token 저장
    localStorage.setItem('access_token', access_token);
}

// 2. API 호출 시 Access Token 사용
async function callAPI() {
    const accessToken = localStorage.getItem('access_token');
    
    try {
        const response = await fetch('https://api.example.com/data', {
            headers: {
                'Authorization': `Bearer ${accessToken}`
            }
        });
        
        return await response.json();
    } catch (error) {
        // 토큰 만료 등 처리
        handleAuthError(error);
    }
}

// 3. 사용자 정보 확인 시 ID Token 정보 사용
function getCurrentUser() {
    return JSON.parse(localStorage.getItem('user'));
}

 

- KeyCloak에서 AccessToken으로 JWT를 사용 😎

초창기부터 KeyCloak은 Access Token으로 JWT를 사용해왔으며, JWT를 사용함에 따라 아래와 같은 장점을 가지게 되었다.
(위에 설명된데로 AccessToken은 포멧에 대한 규정이 없다.)

JWT를 지원하는 OpenID Connect/OAuth 2.0 라이브러리를 사용하는것을 권장한다. (보안취약점있음)

// -------------------------------------------------------------------
// Access Token으로 일반토큰 (불투명토큰) 사용시
+-----------+                                     +------------------+
|  포토북 앱  |  ----(1) Access Token 전달 ------>   |   구글 드라이브      |
|           |  <---(2) 토큰 검증 요청 ----------     |   (리소스 서버)     |
|           |  ----(3) 사용자 정보 요청 -------->     |                  |
|           |  <---(4) 실제 리소스 응답 ---------     |                  |
+-----------+                                     +------------------+
                          |
                     +------------------+
                     |   구글 OAuth      |
                     | (인증 서버)        |
                     +------------------+
   
// 불투명 토큰 처리 과정 예시
async function handleOpaqueToken() {
    // 총 3번의 네트워크 요청
    const steps = [
        '1. 토큰 검증 요청 ➡️ Keycloak',
        '2. UserInfo 요청 ➡️ Keycloak',
        '3. 실제 리소스 처리'
    ];
    // 처리 시간 = 네트워크 지연 x 3
}


// -------------------------------------------------------------------
// Access Token으로 JWT 사용시
// JWT에 이미 필요한 정보가 포함되어 있어서 추가 요청 없이 바로 리소스 접근 가능
+-----------+                                     +------------------+
|  포토북 앱  |  ---(1) JWT Access Token 전달--->     |   구글 드라이브    |
|           |                                     |                 |
|           |  <---(2) 실제 리소스 응답 --------      |   (리소스 서버)    |
+-----------+                                     +------------------+

// JWT 처리 과정
async function handleJWT() {
    // 총 1번의 네트워크 요청
    const steps = [
        '1. JWT 로컬 검증 (네트워크 요청 없음)',
        '2. 권한 확인 (네트워크 요청 없음)',
        '3. 실제 리소스 처리'
    ];
    // 처리 시간 = 네트워크 지연 x 1
}
  • 자체 포함(Self-contained)
    • 필요한 모든 정보가 토큰 안에 포함
    • 추가 조회 불필요
  • 성능 향상
    • 네트워크 요청 감소
    • 서버 부하 감소
  • 확장성
    • 필요한 정보를 클레임으로 추가 가능
    • 다양한 인증/인가 시나리오 지원

 

 

 

- 표준 클레임 세트

클레임(Claim)은  ID Token에서 사용되며,
"주장" 또는 "명세"라는 의미로, 사용자나 엔티티에 대한 정보를 표현하는 key-value 쌍을 말한다.

// ID Token 필수 클레임
{
    "iss": "https://auth.example.com",  // 토큰 발급자
    "sub": "248289761001",             // 사용자 고유 식별자
    "aud": "client_id",                // 토큰 수신자
    "exp": 1516239022,                 // 만료 시간
    "iat": 1516239022                  // 발급 시간
}

// 프로필 관련 클레임
{
    "name": "John Doe",
    "family_name": "Doe",
    "given_name": "John",
    "middle_name": "William",
    "nickname": "Johnny",
    "preferred_username": "j.doe",
    "profile": "https://example.com/profile",
    "picture": "https://example.com/photo.jpg",
    "website": "http://johndoe.com",
    "gender": "male",
    "birthdate": "1990-01-01",
    "zoneinfo": "Europe/Paris",
    "locale": "en-US",
    "updated_at": 1516239022
}

// 이메일 관련 클레임
{
    "email": "johndoe@example.com",
    "email_verified": true             // 이메일 인증 여부
}

// 주소 관련 클레임
{
    "address": {
        "street_address": "123 Main St",
        "locality": "Anytown",         // 시/군/구
        "region": "State",             // 도/시
        "postal_code": "12345",
        "country": "US"
    }
}

 

- 엔드포인트 정의 (OICD/ KeyCloak)

// OpenID Configuration 엔드포인트
GET /.well-known/openid-configuration

// 응답
{
    "issuer": "https://op.example.com",
    "authorization_endpoint": "https://op.example.com/auth",
    "token_endpoint": "https://op.example.com/token",
    "userinfo_endpoint": "https://op.example.com/userinfo",
    "jwks_uri": "https://op.example.com/jwks",
    "registration_endpoint": "https://op.example.com/register",
    "scopes_supported": ["openid", "profile", "email"],
    "response_types_supported": ["code", "token", "id_token"],
    "grant_types_supported": ["authorization_code", "implicit"],
    "subject_types_supported": ["public", "pairwise"],
    "id_token_signing_alg_values_supported": ["RS256", "ES256"]
}

// Keycloak의 OpenID Configuration 엔드포인트
// 로컬 개발 환경 http://localhost:8080/realms/master/.well-known/openid-configuration
// 실제 환경 https://auth.example.com/realms/my-realm/.well-known/openid-configuration
http(s)://{keycloak-host}/realms/{realm-name}/.well-known/openid-configuration

// 응답예시
{
    "issuer": "http://localhost:8080/realms/master",
    "authorization_endpoint": "http://localhost:8080/realms/master/protocol/openid-connect/auth",
    "token_endpoint": "http://localhost:8080/realms/master/protocol/openid-connect/token",
    "introspection_endpoint": "http://localhost:8080/realms/master/protocol/openid-connect/token/introspect",
    "userinfo_endpoint": "http://localhost:8080/realms/master/protocol/openid-connect/userinfo",
    "end_session_endpoint": "http://localhost:8080/realms/master/protocol/openid-connect/logout",
    "jwks_uri": "http://localhost:8080/realms/master/protocol/openid-connect/certs",
    "check_session_iframe": "http://localhost:8080/realms/master/protocol/openid-connect/login-status-iframe.html",
    "grant_types_supported": [
        "authorization_code",
        "implicit",
        "refresh_token",
        "password",
        "client_credentials"
    ],
    "response_types_supported": [
        "code",
        "none",
        "id_token",
        "token",
        "id_token token",
        "code id_token",
        "code token",
        "code id_token token"
    ],
    "subject_types_supported": ["public"],
    "id_token_signing_alg_values_supported": ["RS256"],
    "userinfo_signing_alg_values_supported": ["RS256"],
    "request_object_signing_alg_values_supported": ["none", "RS256"],
    "response_modes_supported": ["query", "fragment", "form_post"],
    "registration_endpoint": "http://localhost:8080/realms/master/clients-registrations/openid-connect",
    "token_endpoint_auth_methods_supported": [
        "private_key_jwt",
        "client_secret_basic",
        "client_secret_post",
        "tls_client_auth",
        "client_secret_jwt"
    ],
    "token_endpoint_auth_signing_alg_values_supported": ["RS256"],
    "claims_supported": [
        "aud",
        "sub",
        "iss",
        "auth_time",
        "name",
        "given_name",
        "family_name",
        "preferred_username",
        "email",
        "acr"
    ],
    "claim_types_supported": ["normal"],
    "claims_parameter_supported": false,
    "scopes_supported": ["openid", "offline_access", "profile", "email", "address", "phone", "roles", "web-origins"],
    "request_parameter_supported": true,
    "request_uri_parameter_supported": true,
    "require_request_uri_registration": true,
    "code_challenge_methods_supported": ["plain", "S256"],
    "tls_client_certificate_bound_access_tokens": true,
    "revocation_endpoint": "http://localhost:8080/realms/master/protocol/openid-connect/revoke",
    "backchannel_logout_supported": true,
    "backchannel_logout_session_supported": true
}

 

- OIDC의 주요 확장 기능 #1 Discovery 🔍

OP의 엔드포인트와 기능을 자동으로 발견하는 메커니즘

GET /.well-known/openid-configuration

// 응답 예시
{
    "issuer": "https://auth.example.com",
    "authorization_endpoint": "https://auth.example.com/auth",
    "token_endpoint": "https://auth.example.com/token",
    "userinfo_endpoint": "https://auth.example.com/userinfo",
    "jwks_uri": "https://auth.example.com/jwks",
    "registration_endpoint": "https://auth.example.com/register",
    // ... 기타 설정들
}

- OIDC의 주요 확장 기능 #2 Dynamic Registration 📝

클라이언트가 동적으로 OP에 등록하는 기능

// 등록 요청
POST /register HTTP/1.1
{
    "application_type": "web",
    "redirect_uris": [
        "https://client.example.com/callback"
    ],
    "client_name": "My Web App",
    "logo_uri": "https://client.example.com/logo.png",
    "subject_type": "pairwise"
}

// 응답
{
    "client_id": "abc123",
    "client_secret": "xyz789",
    "registration_access_token": "reg-token-123"
}

- OIDC의 주요 확장 기능 #3 Session Management 🔄

브라우저 기반 세션 관리
OpenID 제공자와 최종사용자의 인증 세션을 모니터링 하는 방법과 클라이언트가 로그가웃 방법을 정의함

// 클라이언트 측 구현
class SessionManager {
    checkSession() {
        const iframe = document.createElement('iframe');
        iframe.src = 'https://op.example.com/check-session';
        
        iframe.onload = () => {
            // 세션 상태 확인
            iframe.contentWindow.postMessage(
                { clientId: 'abc123' },
                'https://op.example.com'
            );
        };
    }
    
    handleSessionChange(event) {
        if (event.data.sessionState === 'changed') {
            // 로그아웃 처리
            this.logout();
        }
    }
}

- OIDC의 주요 확장 기능 #4 Front-channel Logout 🚪

브라우저를 통한 로그아웃 (iframe)

<!-- 로그아웃 iframe -->
<iframe 
    src="https://op.example.com/logout?
         client_id=abc123&
         logout_uri=https://client.example.com/logout"
    style="display:none">
</iframe>

<script>
    function handleLogout() {
        // 여러 RP에 로그아웃 알림
        logoutIframes.forEach(iframe => {
            iframe.src = iframe.getAttribute('logout-uri');
        });
    }
</script>

- OIDC의 주요 확장 기능 #5 Back-channel Logout 🔒

서버 간 직접 통신을 통한 로그아웃

// RP의 백채널 로그아웃 엔드포인트
app.post('/backchannel_logout', async (req, res) => {
    const logoutToken = req.body.logout_token;
    
    try {
        // 로그아웃 토큰 검증
        const verified = await verifyLogoutToken(logoutToken);
        
        if (verified) {
            // 해당 사용자 세션 종료
            await terminateUserSession(verified.sub);
            res.status(200).send('OK');
        }
    } catch (error) {
        res.status(400).send('Invalid logout token');
    }
});

 

 

 



📌 SAML

SAML은 웹 기반의 Single Sign-On(SSO) 및 인증을 위한 XML 기반의 표준이다.
주로 기업 환경에서 사용되며, 사용자 인증 정보를 안전하게 교환하는 데 사용한다.


- 주요 개념

최종 사용자 
(End User)
실제 인증을 수행하는 사용자
서비스 제공자 
(Service Provider, SP)
사용자가 접근하려는 애플리케이션 또는 서비스. SAML 인증 요청을 처리 (예, 로그인페이지)
아이덴티티 제공자 
(Identity Provider, IdP)
사용자의 신원을 증명하고 SAML 어설션을 발급하는 서버 (예: ADFS, Okta)
SAML 어설션 
(SAML Assertion)
사용자의 인증 정보를 포함하는 XML 형식의 데이터
SAML 프로토콜 SAML 어설션을 전송하는 데 사용되는 프로토콜.

 

- SAML 인증 흐름

  1. 최종 사용자의 서비스 접근: 사용자가 브라우저를 통해 서비스 제공자(SP)의 리소스에 접근을 시도함
  2. SP 인증 흐름 시작
    - SP는 사용자가 인증되지 않았음을 확인함
    - SP-initiated flow(SP 시작 흐름)가 시작됨
  3. SAML 인증 요청 리다이렉트: 👈 <samlp:AuthnRequest> 사용
    SP는 브라우저를 통해 SAML 인증 요청을 IdP로 리다이렉트한다.  
  4. IdP 인증 흐름 시작: IdP-initiated flow(IdP 시작 흐름)가 시작된다.
  5. 사용자 인증 및 SAML 어설션 생성: 👈 <saml:Assertion> 생성
    - IdP는 사용자를 인증하고
    - 인증 성공 시 SAML 어설션을 생성하여 브라우저로 전송한다.
  6. SAML 어설션 전달: 👈 <saml:Assertion> 전달
    브라우저는 IdP로부터 받은 SAML 어설션을 SP로 전달한다.
  7. 보안 컨텍스트 설정: SP는 사용자 인증이 완료되면 보안 컨텍스트를 브라우저로 전송한다.
  8. 리소스 요청: 인증된 사용자가 SP의 리소스를 요청한다.
  9. 리소스 응답: SP는 요청된 리소스로 응답한다.

⭐️⭐️⭐️⭐️⭐️
[Service Provider] ↔ [User's Browser] ↔ [Identity Provider]
특히 SAML 인증 흐름에서 중요한 점은 모든 통신이 사용자의 브라우저를 통해 이루어지며(Browser Agent),
IdP와 SP는 직접 통신하지 않는다
는 것이다. (SAML Browser POST Profile 또는 Web Browser SSO Profile 라고도 함)
이러한 방식은 웹 기반 SSO(Single Sign-On)에 특히 적합하다.

 

- SAML 인증요청 및 어설션 샘플

<!-- SAML 인증 요청 -->
# 서비스 제공자(SP)가 아이덴티티 제공자(IdP)에게 사용자의 인증을 요청하기 위해 생성하는 메시지
<samlp:AuthnRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" 
                   ID="_123456789"
                   Version="2.0"
                   IssueInstant="2023-10-01T00:00:00Z"
                   Destination="https://idp.com/SAML2/SSO">
    <saml:Issuer>https://yourapp.com</saml:Issuer>
    <samlp:NameIDPolicy AllowCreate="true" />
</samlp:AuthnRequest>

<!-- SAML 어선셜 -->
#  IdP가 사용자를 인증한 후, SP에게 전달하는 메시지로, 사용자의 인증 정보를 포함
<saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
                ID="_123456789"
                IssueInstant="2023-10-01T00:00:00Z"
                Version="2.0">
    <saml:Issuer>https://idp.com</saml:Issuer>
    <saml:Subject>
        <saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified">user@example.com</saml:NameID>
        <saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
            <saml:SubjectConfirmationData NotOnOrAfter="2023-10-01T01:00:00Z"
                                          Recipient="https://yourapp.com/SAML/SSO"/>
        </saml:SubjectConfirmation>
    </saml:Subject>
    <saml:Conditions NotBefore="2023-10-01T00:00:00Z"
                     NotOnOrAfter="2023-10-01T01:00:00Z">
        <saml:AudienceRestriction>
            <saml:Audience>https://yourapp.com</saml:Audience>
        </saml:AudienceRestriction>
    </saml:Conditions>
    <saml:AuthnStatement AuthnInstant="2023-10-01T00:00:00Z">
        <saml:AuthnContext>
            <saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml:AuthnContextClassRef>
        </saml:AuthnContext>
    </saml:AuthnStatement>
</saml:Assertion>

 


📌 Zero Trust

"절대 신뢰하지 말고 항상 검증하라(Never Trust, Always Verify)"는 개념이다.

네트워크 내부와 외부 모든 사용자와 장치에 대해 항상 신뢰를 재검증하고 인증하며(접근검증), 
사용자와 장치가 필요한 최소한의 권한만 부여하고(최소권한접근제어),
모든 액세스와 작업을 모니터링하고 로깅하여 이상징후를 분석하고 대응한다.

- 기존 보안과 뭐가 다른가?

기존 보안모델에서는 내부 네트워크는 신뢰한다는 가정으로 설계되었으며, 외부에서부터 접근하는 사용자및 장치에 대한 보안에 집중되어있다. 
Zero Trust는 네트워크 내부와 외부의 구분을 없애고, 모든 접근을 지속적으로 평가하고 검증하는 개념이다.

- 기존 보안모델 vs Zelo Trust 

기준 기존 보안 모델 Zero Trust(Zelo Trust)
보안 경계 내부와 외부로 명확하게 구분 경계가 없으며 모든 접근을 검증
내부 신뢰 수준 내부 네트워크는 신뢰할 수 있음 내부 네트워크도 신뢰하지 않음
접근 제어 네트워크 기반(방화벽, VPN) 사용자, 장치, 컨텍스트 기반 접근 제어, 정책기반
인증 절차 초기 인증만 요구 지속적인 인증 및 정책 평가
위협 탐지 주로 외부 위협 탐지에 집중 내부 및 외부 위협을 모두 지속적으로 모니터링
구현예시 Firewall, VPN, IDS/IPS KeycloakAWS SSO와 같은 IAM 솔루션

즉, 기존 보안 모델은 내부 네트워크의 신뢰를 전제로 하고, 외부에서의 접근을 차단하는 데 중점을 둔다.
Zero Trust(Zelo Trust)는 내부와 외부를 구분하지 않고, 모든 접근 요청을 지속적으로 검증하고 최소 권한 접근을 원칙으로 한다. 

728x90