Skip to main content

Базовая авторизация

Реализуем следующий процесс авторизации:

Делаем страницу с проверкой, авторизован или нет пользователь. Если пользователь не авторизован, показываем кнопку Авторизация. Если авторизован - кнопку Выход. Кнопки отличаются ссылками и текстом.

Будем использовать модуль python-keycloak 

pip install python-keycloak

Адресация серверов

192.168.1.3 web server и ПК, с которого я тестирую работу

192.168.1.195 keycloak server

Настройка keycloak.

Создаем realm для данного эксперимента. Назовем его pythonsimpletest.

В разделе Manage realms - Create

image.png

Теперь создаем клиента. Назовем его clientforsimpletest. Clients - Create client

image.png

 Поскольку этот клиент доверенный и расположен на сервере, то Client authentication включаем, 

image.png

image.png

И тут проявилась первая ошибка. Localhost виден с моего ПК. Поэтому, когда я прописал на сервере keycloak  в разделе Root URL localhost - ничего не заработало. Похоже, что необходимо указывать имена/ip доступные с сервера keycloak а не только с браузера на ПК пользователя. Результирующие настройки клиента:

image.png

Создаем пользователя и задаем ему пароль.

Python клиент

from fastapi import FastAPI, Depends, Request
from fastapi.responses import HTMLResponse, RedirectResponse
from uvicorn import run
from typing import Optional

from keycloak import KeycloakOpenID

app = FastAPI(docs_url=None, redoc_url=None, openapi_url=None)

KEYCLOAK_URL = "http://192.168.1.195:9090/"
REALM_NAME = "pythonsimpletest"
CLIENT_ID = "clientforsimpletest"
CLIENT_SECRET = "Vix6txRyHwt81KyZIpl7O06CxpWMFtib"
REDIRECT_URI = "http://192.168.1.3:8100/auth/callback"

# Инициализация Keycloak клиента
keycloak_openid = KeycloakOpenID(
    server_url=KEYCLOAK_URL,
    client_id=CLIENT_ID,
    realm_name=REALM_NAME,
    client_secret_key=CLIENT_SECRET,
)

   
async def get_current_user_from_cookie(request: Request) -> Optional[dict]:
    """Извлечение и валидация пользователя из cookie"""
    
    # Получаем токен из cookies
    access_token = request.cookies.get("access_token")
    
    if not access_token:
        return None
    
    try:
        # Проверяем токен через Keycloak
        userinfo = keycloak_openid.userinfo(access_token)
        return userinfo
    except Exception as e:
        # Если токен просрочен или невалиден, удаляем его
        # Но здесь мы не можем удалить cookie, это нужно делать в ответе
        return None

def get_login_url():
    """Генерация URL для входа через Keycloak"""
    auth_url = keycloak_openid.auth_url(
        redirect_uri=REDIRECT_URI,
        scope="openid email profile",
        state="random_state_string"  # В реальном приложении используйте генерацию случайной строки
    )
    return auth_url

@app.get("/", response_class=HTMLResponse)
async def simpleauth(request: Request):
    user = await get_current_user_from_cookie(request)
    if not user:
        login_url = get_login_url()
        html_content = f'<!DOCTYPE html><html><body><a href="{login_url}"><button>Авторизация</button></a></body></html>'
    else:
        html_content = '<!DOCTYPE html><html><body><a href="/logout"><button>Выход</button></a></body></html>'
    return HTMLResponse(content=html_content, status_code=200)

@app.get("/auth/callback")
async def auth_callback(code: str):
    """Callback URL для обработки ответа от Keycloak после логина"""
    try:
        # Обмен кода на токены
        token_response = keycloak_openid.token(
            grant_type="authorization_code",
            code=code,
            redirect_uri=REDIRECT_URI
        )
        
        access_token = token_response.get('access_token')
        
        # Создаем редирект с токеном в заголовке (через установку cookie)
        response = RedirectResponse(url="/")
        
        # Устанавливаем токен в cookie (альтернатива Authorization header для браузера)
        response.set_cookie(
            key="access_token",
            value=access_token,
            httponly=True,  # Защита от XSS
            secure=False,   # True для HTTPS
            samesite="lax"
        )
        refresh_token = token_response.get('refresh_token')
        response.set_cookie(
            key="refresh_token",
            value=refresh_token,
            httponly=True,  # Защита от XSS
            secure=False,   # True для HTTPS
            samesite="lax"
        )
        return response
        
    except Exception as e:
        return HTMLResponse(content=f"<h1>Ошибка авторизации: {str(e)}</h1>", status_code=400)

@app.get("/logout")
async def logout(request: Request):
    """Выход из системы"""
    refresh_token = request.cookies.get("refresh_token")
    
    # Логаут через python-keycloak
    if refresh_token:
        try:
            keycloak_openid.logout(refresh_token)
        except Exception as e:
            print(f"Keycloak logout error: {e}")
    response = RedirectResponse(url="/")
    response.delete_cookie("access_token")
    response.delete_cookie("refresh_token")
    return response

if __name__ == '__main__':
    run(app="simple:app", host='0.0.0.0', port=8100, workers=4, log_level='warning')