# Python Flask/Django 연동

Keycloak은 OpenID Connect와 SAML 기반의 인증·권한 부여 관리 플랫폼이다. Python 환경에서도 Flask, Django 같은 대표적인 웹 프레임워크와 연동하여 인증을 처리할 수 있다. Keycloak에서 제공하는 OIDC 방식을 사용하면, 별도의 인증 로직 없이도 토큰 검증을 통한 안전한 접근 제어를 구현할 수 있다. 다음에서는 Keycloak에서 클라이언트를 설정하고 Flask와 Django 애플리케이션을 연동하는 과정을 설명한다.

### Keycloak 클라이언트 설정

1. **Realm 생성** Keycloak에 접속 후 새로운 Realm을 생성한다. 이 Realm은 Python 웹 애플리케이션들이 참조할 인증 공간이다.
2. **클라이언트(Client) 생성**
   * Keycloak Admin Console에서 ‘Clients’ 메뉴를 연다.
   * ‘Create client’를 선택하고, 클라이언트 ID를 입력한다. 예: `python-flask-client`
   * ‘OpenID Connect’를 선택한다.
   * ‘Confidential’ 또는 ‘Public’ 유형을 선택한다.
     * 백엔드 서버가 토큰을 안전하게 관리할 수 있는 구조라면 일반적으로 ‘Confidential’을 사용한다.
   * ‘Root URL’ 혹은 ‘Valid Redirect URIs’에 Flask 또는 Django 애플리케이션의 리다이렉트 URI를 정확히 등록한다. 예: `http://localhost:5000/*`, `http://localhost:8000/*`
3. **비밀정보(Secret) 확인**
   * ‘Credentials’ 탭에서 ‘Client secret’을 확인한다.
   * Flask나 Django 설정 파일에 클라이언트 ID, 클라이언트 비밀정보, Keycloak 서버 주소, Realm 이름 등을 환경 변수로 저장하거나 설정 파일을 통해 안전하게 보관한다.
4. **OIDC 설정값 확인**
   * Realm > ‘OpenID Endpoints’ 혹은 ‘Well-known configuration’을 확인하여 `authorization_endpoint`, `token_endpoint`, `userinfo_endpoint`, `end_session_endpoint` 등을 확인한다.
   * Flask/Django에서 라이브러리를 통해 자동으로 참조할 수 있으나, 설정 파일 혹은 환경 변수에 직접 기술할 수도 있다.

### Flask 연동

#### 라이브러리 선택

Flask 애플리케이션에 Keycloak 연동을 위해서는 다음과 같은 라이브러리를 활용할 수 있다.

* **python-keycloak**: Keycloak Admin REST API와 함께 OpenID Connect 클라이언트 기능을 제공한다.
* **Authlib**: 범용 OAuth/OIDC 라이브러리로 Keycloak OIDC 인증 흐름에 적용 가능하다.
* **keycloak-flask**: Flask에 특화된 Keycloak 인증 처리를 지원한다(서드파티 라이브러리).

여기서는 `python-keycloak` 예시를 간단히 살펴본다.

#### 예시 코드 구조

1. **설치**

   ```bash
   pip install flask python-keycloak
   ```
2. **프로젝트 구조 예시**

   ```
   my_flask_app/
   ├─ app.py
   ├─ requirements.txt
   └─ config.py
   ```
3. **config.py 예시**

   ```python
   KEYCLOAK_SERVER_URL = "http://localhost:8080/auth/"
   KEYCLOAK_REALM = "myrealm"
   KEYCLOAK_CLIENT_ID = "python-flask-client"
   KEYCLOAK_CLIENT_SECRET = "비밀정보"
   KEYCLOAK_REDIRECT_URI = "http://localhost:5000/callback"
   ```
4. **app.py 예시**

   ```python
   from flask import Flask, request, redirect, url_for, session
   from python_keycloak.keycloak_openid import KeycloakOpenID
   from config import (
       KEYCLOAK_SERVER_URL,
       KEYCLOAK_REALM,
       KEYCLOAK_CLIENT_ID,
       KEYCLOAK_CLIENT_SECRET,
       KEYCLOAK_REDIRECT_URI,
   )

   app = Flask(__name__)
   app.secret_key = "some_random_secret_key"

   keycloak_openid = KeycloakOpenID(
       server_url=KEYCLOAK_SERVER_URL,
       client_id=KEYCLOAK_CLIENT_ID,
       realm_name=KEYCLOAK_REALM,
       client_secret_key=KEYCLOAK_CLIENT_SECRET
   )

   @app.route("/")
   def index():
       if "access_token" in session:
           return "로그인 완료. 사용자 정보: " + str(session["user_info"])
       else:
           return "로그인이 필요하다. /login 으로 이동하라."

   @app.route("/login")
   def login():
       auth_url = keycloak_openid.auth_url(
           redirect_uri=KEYCLOAK_REDIRECT_URI,
           scope=["openid", "profile", "email"]
       )
       return redirect(auth_url)

   @app.route("/callback")
   def callback():
       code = request.args.get("code")
       token = keycloak_openid.token(
           grant_type=["authorization_code"],
           code=code,
           redirect_uri=KEYCLOAK_REDIRECT_URI
       )
       session["access_token"] = token["access_token"]
       session["refresh_token"] = token["refresh_token"]
       userinfo = keycloak_openid.userinfo(token["access_token"])
       session["user_info"] = userinfo
       return redirect(url_for("index"))

   if __name__ == "__main__":
       app.run(host="0.0.0.0", port=5000, debug=True)
   ```
5. **동작 원리**
   * `/login`으로 접속하면 Keycloak 로그인 페이지로 리다이렉트된다.
   * 사용자가 로그인하면 Keycloak이 `redirect_uri`로 `authorization_code`를 전달한다.
   * Flask 애플리케이션의 `/callback`에서 코드를 받아 토큰 교환(token exchange)을 수행한다.
   * `access_token`, `refresh_token`, `userinfo` 등을 세션에 저장하여 인증 상태를 유지한다.
   * 보호가 필요한 모든 라우트에서 세션 내 `access_token`을 검사해 인증 여부를 확인한다.

### Django 연동

Django 환경에서도 Keycloak과 OIDC 프로토콜을 통해 연동할 수 있다. Django는 기본적으로 세션과 인증 시스템이 잘 구축되어 있기 때문에, Keycloak에서 받은 토큰을 활용해 사용자 정보를 인증 백엔드 또는 미들웨어에서 처리할 수 있다.

#### 라이브러리 선택

* **django-oidc**
* **Authlib**
* **python-keycloak**

다만, 공식적으로 Django에 최적화된 Keycloak 플러그인은 존재하지 않으므로 범용 OIDC 라이브러리를 활용하는 경우가 많다. 다음은 `Authlib`을 사용하는 간단한 예시를 보여준다.

#### 예시 코드 구조

1. **설치**

   ```bash
   pip install django authlib
   ```
2. **프로젝트 구조 예시**

   ```
   my_django_app/
   ├─ myproject/
   │  ├─ settings.py
   │  ├─ urls.py
   │  ├─ wsgi.py
   │  └─ ...
   └─ manage.py
   ```
3. **settings.py 예시**

   ```python
   import os

   BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
   SECRET_KEY = "some_random_secret_key"
   DEBUG = True

   ALLOWED_HOSTS = ["*"]

   INSTALLED_APPS = [
       "django.contrib.admin",
       "django.contrib.auth",
       "django.contrib.contenttypes",
       "django.contrib.sessions",
       "django.contrib.messages",
       "django.contrib.staticfiles",
       # 필요한 경우 custom app 등록
   ]

   MIDDLEWARE = [
       "django.contrib.sessions.middleware.SessionMiddleware",
       "django.middleware.common.CommonMiddleware",
       "django.middleware.csrf.CsrfViewMiddleware",
       "django.contrib.auth.middleware.AuthenticationMiddleware",
       "django.contrib.messages.middleware.MessageMiddleware",
       "django.middleware.clickjacking.XFrameOptionsMiddleware",
       # 필요 시 OIDC 연동용 미들웨어 추가
   ]

   ROOT_URLCONF = "myproject.urls"

   # Keycloak / OIDC 설정
   KEYCLOAK_SERVER_URL = "http://localhost:8080/auth/"
   KEYCLOAK_REALM = "myrealm"
   KEYCLOAK_CLIENT_ID = "python-django-client"
   KEYCLOAK_CLIENT_SECRET = "비밀정보"
   KEYCLOAK_REDIRECT_URI = "http://localhost:8000/oidc/callback"

   # static, templates 경로 등 일반 Django 설정
   ```
4. **urls.py 예시**

   ```python
   from django.contrib import admin
   from django.urls import path
   from .views import index, login_view, oidc_callback

   urlpatterns = [
       path('admin/', admin.site.urls),
       path('', index, name='index'),
       path('login/', login_view, name='login'),
       path('oidc/callback', oidc_callback, name='oidc_callback'),
   ]
   ```
5. **views.py 예시**

   ```python
   from django.shortcuts import redirect, render
   from django.conf import settings
   from django.urls import reverse
   from authlib.integrations.requests_client import OAuth2Session

   def index(request):
       if request.session.get('access_token'):
           userinfo = request.session.get('userinfo')
           return render(request, 'index.html', {'userinfo': userinfo})
       return render(request, 'index.html', {'userinfo': None})

   def login_view(request):
       client = OAuth2Session(
           client_id=settings.KEYCLOAK_CLIENT_ID,
           client_secret=settings.KEYCLOAK_CLIENT_SECRET
       )
       authorization_endpoint = f"{settings.KEYCLOAK_SERVER_URL}realms/{settings.KEYCLOAK_REALM}/protocol/openid-connect/auth"
       redirect_uri = request.build_absolute_uri(reverse('oidc_callback'))
       scope = ["openid", "profile", "email"]

       uri, state = client.create_authorization_url(
           authorization_endpoint,
           redirect_uri=redirect_uri,
           scope=scope
       )
       request.session['oidc_state'] = state
       return redirect(uri)

   def oidc_callback(request):
       client = OAuth2Session(
           client_id=settings.KEYCLOAK_CLIENT_ID,
           client_secret=settings.KEYCLOAK_CLIENT_SECRET
       )
       token_endpoint = f"{settings.KEYCLOAK_SERVER_URL}realms/{settings.KEYCLOAK_REALM}/protocol/openid-connect/token"
       state = request.session.get('oidc_state')
       redirect_uri = request.build_absolute_uri(reverse('oidc_callback'))
       code = request.GET.get('code')
       if code is None:
           return redirect('/')

       token = client.fetch_token(
           token_endpoint,
           code=code,
           redirect_uri=redirect_uri
       )
       request.session['access_token'] = token['access_token']
       request.session['refresh_token'] = token['refresh_token']

       userinfo_endpoint = f"{settings.KEYCLOAK_SERVER_URL}realms/{settings.KEYCLOAK_REALM}/protocol/openid-connect/userinfo"
       resp = client.get(userinfo_endpoint, token=token)
       request.session['userinfo'] = resp.json()

       return redirect('/')
   ```
6. **동작 원리**
   * 사용자가 `/login/`에 접근하면 Keycloak 로그인 화면으로 이동한다.
   * 정상 로그인 후 코드가 `/oidc/callback`으로 전송된다.
   * Django 뷰에서 받은 코드를 토큰으로 교환하고, `access_token`, `userinfo`를 세션에 저장한다.
   * 이후 `request.session`을 통해 인증된 사용자 정보를 확인 가능하다.

### 주의사항

* **HTTPS 사용** 운영 환경에서는 HTTPS를 반드시 사용해야 한다. Keycloak 인증 흐름에서 토큰이 노출되면 심각한 보안 문제가 발생할 수 있다.
* **클라이언트 구분** Flask 애플리케이션과 Django 애플리케이션을 동시에 사용한다면, 각 애플리케이션별로 Keycloak에 별도의 클라이언트를 생성하는 것이 좋다.
* **토큰 검증** 수신한 `access_token`을 백엔드에서 다시 검증하거나 만료 시간을 철저하게 검사하는 로직이 필요할 수 있다. 표준 OIDC 라이브러리를 사용하면 토큰 검증 로직을 자동으로 처리하지만, 커스텀 구현 시에는 토큰의 서명 유효성, 발행 시간, 만료 시간을 점검해야 한다.
* **세션 관리** Django나 Flask에서 기본 제공되는 세션은 서버 세션(cookie 기반)이며, 토큰이 저장된다. 민감 정보가 세션에서 노출되지 않도록 쿠키 보안 설정(HTTPOnly, Secure)을 적용한다.
* **로깅 및 예외 처리** Keycloak 인증 과정에서 발생하는 예외(네트워크 문제, 토큰 교환 실패 등)에 대한 로깅과 예외 처리를 반드시 구현한다.
* **갱신 토큰(refresh\_token)** 인증 세션을 오래 유지해야 한다면 `refresh_token`을 사용하여 만료된 `access_token`을 재발급할 수 있다. Keycloak에서 `refresh_token` 만료 정책을 설정해 둔다.

위와 같은 방식으로 Python Flask, Django 애플리케이션에서도 Keycloak을 통한 인증·권한 관리를 간편하게 적용할 수 있다. OIDC 프로토콜과 Keycloak에서 제공하는 풍부한 기능을 활용하여, 어플리케이션별 인증 로직을 최소화하면서 일관된 보안 정책을 유지하는 것이 중요하다.
