본문 바로가기

DevOps/Keycloak

[Keycloak]OIDC를 활용한 사용자 인증 및 로그아웃

개요

OpenID Connect(OIDC)를 통해 애플리케이션과 Keycloak 간의 인증 및 데이터 요청 흐름을 분석하고, 주요 상호작용을 세부적으로 다룹니다.

주제

  • 실습용 OIDC Playground 애플리케이션 실행
  • Discovery 엔드포인트 이해
  • 사용자 인증 흐름 분석
  • ID 토큰 및 UserInfo 활용

배경지식

OIDC에서의 scope 의미와 사용 방법

[정의]

  • scope는 OAuth 2.0 및 OpenID Connect(OIDC)에서 클라이언트가 요청할 수 있는 권한의 범위를 정의하는 매개변수입니다.
  • 이를 통해 클라이언트가 액세스하려는 사용자 정보 또는 API 리소스의 종류를 명시할 수 있습니다. 사용자의 동의를 기반으로 애플리케이션이 접근할 수 있는 데이터 범위를 제어하는 데 사용됩니다.
  • OIDC의 scope는 클라이언트 애플리케이션의 권한을 명확히 정의하고, 사용자 데이터 접근을 통제하는 중요한 요소입니다.

[주요 OIDC scope의 종류]

OIDC에서 scope는 표준적으로 정의된 값과 사용자 지정 값으로 나뉩니다.

  1. 표준 scope:
    • openid:
      • OIDC 요청에서 반드시 포함되어야 하며, 인증 요청임을 나타냅니다.
      • 이 scope가 포함되면 ID 토큰을 클라이언트에 반환합니다.
    • profile:
      • 사용자의 기본 정보에 접근(예: 이름, 성, 프로필 사진).
      • 반환 정보: name, family_name, given_name, middle_name, nickname, preferred_username, profile, picture, website, gender, birthdate, zoneinfo, locale, updated_at.
    • email:
      • 사용자의 이메일 주소와 이메일 확인 상태 반환.
      • 반환 정보: email, email_verified.
    • address:
      • 사용자의 주소 정보 반환.
      • 반환 정보: formatted, street_address, locality, region, postal_code, country.
    • phone:
      • 사용자의 전화번호와 확인 상태 반환.
      • 반환 정보: phone_number, phone_number_verified.
  2. 사용자 정의 scope:
    • 개발자가 API나 리소스를 보호하기 위해 정의한 특정 권한.
    • 예: read:messages, write:messages.

[scope의 사용 방법]

  1. 클라이언트 요청에 포함: 클라이언트는 OIDC 인증 요청 시 필요한 scope를 명시합니다. 이 정보는 Authorization Server(Keycloak 등)에 전달됩니다.
    GET /authorize?response_type=code
        &client_id=example-client
        &redirect_uri=https://example.com/callback
        &scope=openid profile email
        &state=xyz123
    
    • openid: ID 토큰을 요청.
    • profile: 사용자 기본 정보 요청.
    • email: 사용자 이메일 요청.
  2. 예제:
  3. 사용자의 동의:
    • 사용자는 클라이언트가 요청한 scope를 승인(또는 거부)합니다.
    • 사용자가 동의한 scope의 범위 내에서만 클라이언트가 정보에 접근할 수 있습니다.
  4. Access Token 및 ID Token 반환:
    • 승인된 scope는 토큰에 포함됩니다.
    • 클라이언트는 토큰을 기반으로 사용자 정보(UserInfo Endpoint)나 API를 호출.

[유의할 점]

  1. scope 최소화:
    • 꼭 필요한 데이터만 요청해 사용자의 신뢰를 유지.
    • 예: 이메일만 필요한 경우 email만 요청.
  2. 권한 거부 처리:
    • 사용자가 요청한 scope를 승인하지 않을 경우의 처리 로직 필요.
  3. 보안 고려:
    • Access Token과 Refresh Token은 안전한 저장소에 보관.
    • 가능한 짧은 수명(Time-to-Live, TTL) 설정.

OIDC 플레이그라운드 애플리케이션 실행

app.js

var KC_URL = process.env.KC_URL || "<http://10.77.77.41:31180/auth>";

index.html

<label>Issuer</label><input id="input-issuer" value="KC_URL/realms/jeff-realm">

Keycloak

클라이언트 생성:

  • ID: oidc-playground

Discovery

검색 엔드포인트 이해

OIDC Discovery(검색) 명세

  • OIDC Relying Party 라이브러리의 상호 운용성과 가용성 측면에서 모두 중요합니다.
  • OIDC 제공자가 구현 여부를 결정할 수 있는 명세입니다. Keycloak을 포함한 대부분의 OIDC 제공자는 해당 명세를 구현합니다.
  • OIDC Provider에 대한 base URL(issuer URL)을 알기만 하면 신뢰자는 제공자에 대한 유용한 정보를 많이 검색할 수 있습니다.

[OpenID Provider Configuration]

  • authorization_endpoint: 인증 요청을 위한 URL
  • token_endpoint: 토큰 요청
  • introspection_endpoint: 점검 요청
  • userinfo_endpoint: UserInfo 요청
  • grant_types_supported: 지원 가능한 승인 유형 리스트
  • response_types_supported: 지원 가능한 응답 유형 리스트

Authentication

Keycloak을 통해 사용자를 인증하는 가장 보편적인 방법은 OIDC 인가 코드 흐름을 사용하는 것입니다.

  • client_id
    • keycloak에 등록된 애플리케이션 client ID
  • scope
    • default: openid
  • prompt:
    • none: keycloak에 로그인 된 경우만 사용자를 인증
    • login: 재인증을 항상 요청
  • max_age
    • 사용자가 인증을 유지하는 최대 시간(s)

[Authentication Request]

Keycloak 로그인을 하면 다시 애플리케이션으로 리다이렉트되고 인증 응답이 표시됩니다.

  • code: 애플리케이션 ID 토큰과 리프레시 토큰을 얻기 위해 사용되는 인가 코드이다.
  • 애플리케이션 인가 코드를 토큰으로 교환할 수 있다.
code=a596683a-c6b4-4e51-9875-538010ace673.65357d64-2d07-4498-bbc8-46e5af7084d4.af12e150-814d-4911-a43f-0af6873c3446

Token

[Token Request]

  • max_age를 60초로 설정했기에 인증 응답을 수신하고 토큰 요청을 전송하는 데 1분 이상 걸리면 요청이 실패합니다.
  • 인가 코드는 한 번만 유효하므로 2개 이상의 토큰 요청에 포함된 경우 요청이 실패합니다.

[Token Response]

  • access_token: JWS(Json Web Signature) 포맷을 가지는 Keycloak 접근 토큰
  • expires_in: 접근 토큰의 만료 시간
  • refresh_expires_in: 리프레시 토큰의 만료 시간
  • token_type: 접근 토큰의 유형, Keycloak은 항상 Bearer를 사용
  • session_state: Keycloak 사용자의 세션 ID
  • scope: 애플리케이션은 인증 요청에서 Keycloak의 scope를 요청하지만 반환된 토큰의 실제 범위는 요청된 범위와 일치하지 않을 수 있습니다.

ID 토큰 이해하기

  • 포맷: <Header>.<Playload>.<Signature>
  • Header,Playload: Base64URL로 인코딩된 JSON 문서
"id_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJOLXRTZmVjR3d4VkxJMnBmTERVaUI1d2ZUVDBFLWlHS2RUeWRZbHdFWVNrIn0.eyJleHAiOjE3MzI0NjYxMDksImlhdCI6MTczMjQ2NTgwOSwiYXV0aF90aW1lIjoxNzMyNDY1NzgyLCJqdGkiOiJiNmQzYTk5NC05NTA4LTQ4NmMtYjliMi0yM2NiY2MxNTM1NDIiLCJpc3MiOiJodHRwOi8vMTAuNzcuNzcuNDE6MzExODAvcmVhbG1zL2plZmYtcmVhbG0iLCJhdWQiOiJvaWRjLXBsYXlncm91bmQiLCJzdWIiOiJkZTJmODEzYS1jNGRhLTQ0YmYtYmFhMi1iNTRjZTA1OGIyNWIiLCJ0eXAiOiJJRCIsImF6cCI6Im9pZGMtcGxheWdyb3VuZCIsInNpZCI6IjY1MzU3ZDY0LTJkMDctNDQ5OC1iYmM4LTQ2ZTVhZjcwODRkNCIsImF0X2hhc2giOiJvMTJkclZvdkhBWFFkQnVTamhySUtRIiwiYWNyIjoiMSIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwibmFtZSI6IkplZmYgSGFuIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiamVmZiIsImdpdmVuX25hbWUiOiJKZWZmIiwiZmFtaWx5X25hbWUiOiJIYW4iLCJlbWFpbCI6ImplZmZAZXhhbXBsZS5jb20ifQ.B5E3HijFc3kH6QxsSKEJoFFCLjFPyzXbMBKkq5lzMd5q3sQQ2l6WOFSqwSZwXT6y5URoP-zybR_S5Fq76EIJ3dKOrq5w25wevbCg-fehvV5NGmCqsB937qMtHU1_xJEun-BMxxgHK685sEnp5VUkae_C4I4PD42WGRfzJLSdtU7HJQ5NYwVYzvtoC7IGwS1DQbbz7CNt-8HiN53tEm0dvMRkFb8FlRQ-0uHQVe1bwjVeV6EyIIP9-CIJgtWjXTvxiqAVqqW6hTE7u_otgAqHMgQT9JsX04Yox2aesfAzilFYbzy-ILfG1sVNSdEgh-JEgqPE8ld_aR9wn_5HEs8O1g",
  • 일반적으로 ID 토큰은 노출 위험을 줄이기 위해 짧은 지속 시간을 가진다. 이는 애플리케이션이 사용자를 재인증해야 한다는 의미가 아니라 업데이트된 ID 토큰을 획득하는데 사용되는 별도의 리프레시 토큰이 있다는 의미입니다.
  • 리프레시 토큰은 유효 기간이 훨씬 길며 keycloak에서만 직접 사용할 수 있습니다.

[디코딩된 ID Token]

Payload:

  • exp: 토큰 만료 일시
  • unix epoch 시간 변환 사이트 https://www.epochconverter.com/
  • iat: 토큰 발행 일시
  • auth_time: 사용자 최종 인증 일시
  • jti: 토큰의 고유 ID
  • aud: 사용자를 인증하는 신뢰자가 포함된 토큰 사용 주체
  • azp: 토큰 발생 대상
  • sub: 인증된 사용자의 고유 ID.

Refresh

  • 리프레시 응답에 리프레시 토큰도 포함되어 있습니다.
  • 애플리케이션이 추후 ID 토큰을 갱신할 때 업데이트된 리프레시 토큰을 사용하는 것이 중요합니다.
    • 키 순환:
      • Keycloak은 보안을 위해 주기적으로 토큰 서명에 사용하는 암호화 키를 변경하며, 이를 "키 순환"이라고 합니다.
      • 클라이언트는 갱신된 키로 서명된 새 리프레시 토큰을 사용합니다.
    • 유휴 세션: 클라이언트는 유휴 세션 기능을 가지며 리프레시 토큰은 관련 세션보다 더 빨리 만료될 수 있습니다.
    • 리프레시 토큰 노출 탐지: 리프레시 토큰 노출을 탐지하기 위해 Keycloak은 리프레시 토큰의 재사용을 허용하지 않습니다.

[ID Token]

  • exp, iat, jti이 변경된 것을 제외하고 거의 동일한 값을 갇는다.

[토큰 갱신의 이점]

  • 애플리케이션이 재인증을 수행하지 않고 사용자의 정보를 업데이트할 수 있습니다.

사용자 프로파일 업데이트

[사용자의 이메일, 성명 변경 후 리프레시 요청]

email과 name이 변경된걸 확인할 수 있습니다.

[group attribute 추가]

[property 추가]

[Add Client scopes to Clients]

optional client scopes에 해당 클레임을 추가했기 때문에 클라이언트가 해당 범위를 명시적으로 요청해야합니다.(디폴트: 범위에 추가하면 클라이언트에 기본적으로 포함)

클라이언트는 특정 시간에 필요한 정보만 요청할 수 있으며 해당 작업은 사용자가 애플리케이션에 접근하기 위해 동의가 필요할때 유용합니다.

현재는 Send Refresh Request 버튼을 클릭해도 사용자 정의 속성이 ID 토큰에 추가되지 않습니다.

[신규 인증 요청]

  • myclaim scope 포함

ID 토큰에 역할 추가

  • 기본적으로 ID 토큰은 역할을 가지고 있지 않습니다.
  • 기본적으로 모든 역할은 전체 클라이언트에 추가되며 개별 클라이언트의 접근 권한을 제한해야 하는 경우에 적절하지 않습니다.
  • ID토큰은 특정 클라이언트에 대해 사용자를 인증하기 위해서만 사용되기 때문에 큰 영향이 없지만, 다른 서비스에 접근하기 위해 사용되는 접근 토큰의 경우에는 큰 문제가 될 수 있습니다.

[Client Scopes] → [roles] → [Mappers] → [realm reols] → [Add to ID Token enabled]

 

 

[Send Refresh Request] realm_access

ID 토큰에서 realm_access 를 확인할 수 있다.

UserInfo 엔드포인트 호출

  • ID 토큰을 통해 인증된 사용자에 대한 정보를 검색할 수 있을 뿐만 아니라 OIDC 흐름을 통해 획득한 접근 토큰으로 UserInfo 엔드포인트를 호출할 수도 있다.

[UserInfo]

  • 해당 응답은 사용자 속성만 포함하는 단순한 JSON 응답입니다.
  • 클라이언트 범위 및 프로토콜 매퍼를 통해 Keycloak ID 토큰에서 반환하는 정보를 설정할 수 있는것 처럼 UserInfo 엔드포인트에서 반환되는 정보도 설정할 수 있습니다.

[dedicated clinet scopes]

[Add mapper]

[Refresh] → [UserInfo]

사용자 로그아웃 처리

로그아웃 흐름

OIDC 기반 로그아웃 처리의 일반적인 흐름은 다음과 같습니다:

  1. 사용자가 애플리케이션의 로그아웃 버튼을 클릭합니다.
  2. 애플리케이션은 OIDC RP-Initiated Logout 요청을 생성하여 OIDC Provider(Keycloak)와 상호작용합니다.
  3. 사용자는 Keycloak End Session 엔드포인트로 리다이렉트되어 세션 종료 처리가 진행됩니다.

주요 엔드포인트와 매개변수

  • End Session 엔드포인트: OIDC Metadata의 end_session_endpoint에 정의되어 있음.
  • 매개변수:
    • id_token_hint: 발급된 ID 토큰으로 로그아웃할 클라이언트 및 세션 식별.
    • post_logout_redirect_uri: 로그아웃 후 사용자를 리다이렉트할 URI.
    • state: 로그아웃 요청 및 리다이렉트 과정에서 클라이언트 상태 유지.
    • ui_locales: 로그아웃 시 표시할 UI의 지역 설정.

Keycloak의 처리 방식

Keycloak은 End Session 요청을 수신하면 다음 작업을 수행합니다:

  1. 동일한 세션에 속한 모든 클라이언트들에게 로그아웃 신호를 전송.
  2. 세션을 만료시켜 모든 토큰(ID, 접근, 리프레시)을 무효화.

로그아웃 구현 방법

ID 및 접근 토큰 만료를 활용한 로그아웃

OIDC 환경에서 ID 및 접근 토큰의 만료 시간은 자연스러운 로그아웃 상태를 구현하는 데 유용합니다.

[동작 원리]

  • ID 및 접근 토큰은 기본적으로 짧은 유효 기간을 가짐.
  • Keycloak에서 세션이 만료되면 리프레시 토큰도 더 이상 사용할 수 없음.
  • 애플리케이션은 신규 토큰 요청이 실패하면 사용자 로그아웃을 처리.

[장점]

  • 설정 및 구현이 간단.
  • 모든 클라이언트와 애플리케이션에서 동작.

[단점]

  • 모든 애플리케이션에 로그아웃 상태가 반영되기까지 몇 분 소요 가능.
  • 긴 토큰 유효기간을 사용하는 환경에서는 Token Introspection 엔드포인트를 사용해 주기적으로 유효성을 확인해야 함.

OIDC 세션 관리를 활용한 로그아웃 (비추천)

Keycloak은 특수 세션 쿠키를 통해 사용자의 세션 상태를 모니터링합니다.

[동작 원리]

  • Keycloak의 iframe을 사용하여 세션 쿠키 상태를 확인.
  • 세션 상태 변경이 탐지되면 애플리케이션에 이벤트 전송.

[문제점]

  • iframe 의존성:
    • 브라우저의 서드파티 콘텐츠 차단 정책으로 인해 iframe 로드가 실패할 수 있음.
  • Cross-Origin 문제:
    • 애플리케이션이 Keycloak과 다른 도메인에 있을 경우 쿠키 접근 불가.

[결론]

iframe을 사용하는 이 방식은 브라우저 호환성 및 신뢰성 문제로 인해 추천되지 않습니다.

OIDC 백-채널 로그아웃

Keycloak이 로그아웃 요청을 처리할 때, 백-채널 로그아웃 엔드포인트로 등록된 모든 클라이언트에 로그아웃 토큰을 전송합니다.

[동작 원리]

  1. 로그아웃 요청 시 Keycloak은 로그아웃 토큰(서명된 JWT)을 클라이언트에 전송.
  2. 클라이언트는 로그아웃 토큰의 서명을 검증 후 연관된 세션을 종료.

[장점]

  • 서버 사이드 애플리케이션에서 효과적.
  • 사용자 세션을 확실히 종료 가능.

[단점]

  • 상태 유지 클러스터 환경:
    • 동일 세션에 대한 로그아웃 요청을 특정 인스턴스로 라우팅하는 로드 밸런서 설정 필요
    • 하지만 로드 밸런서에서 처리하는 것이 쉽지 않기 때문에 일반적으로 애플리케이션단에 처리
  • 비상태 유지 애플리케이션:
    • 로그아웃 요청 처리 및 세션 만료 동기화가 어려움.

OIDC 프론트-채널 로그아웃

프론트-채널 로그아웃은 OIDC 제공자의 로그아웃 페이지에서 각 클라이언트의 iframe을 렌더링하여 로그아웃을 수행합니다.

[동작 원리]

  • OIDC 로그아웃 페이지가 각 애플리케이션의 프론트-채널 로그아웃 엔드포인트를 호출.
  • 애플리케이션이 로그아웃 작업을 처리.

[문제점]

  • OpenID Provider가 로그아웃 성공 여부를 확인할 방법이 없음.
  • 브라우저 서드파티 콘텐츠 차단, iframe 제한 등의 문제로 신뢰성 낮음.

[결론]

클라이언트 애플리케이션에 로그아웃 신호를 전달하는 용도로는 적합하지만, 신뢰성을 요구하는 환경에는 비추천.

선택

  1. ID 및 접근 토큰 만료: 기본적으로 모든 환경에서 추천.
  2. 백-채널 로그아웃: 서버 사이드 애플리케이션에서 필수적.
  3. 프론트-채널 로그아웃: 클라이언트 애플리케이션의 로그아웃 보조 수단.
  4. 세션 관리 기반 로그아웃: 실시간 로그아웃이 필수적일 때만 제한적으로 사용.