|
| 1 | +import logging |
| 2 | +from datetime import datetime |
| 3 | +from typing import Annotated, Optional |
| 4 | + |
| 5 | +from fastapi import APIRouter, BackgroundTasks, Form, Header |
| 6 | +from fastapi_sqlalchemy import db |
| 7 | + |
| 8 | +from auth_backend.auth_plugins.email import Email |
| 9 | +from auth_backend.exceptions import OidcGrantTypeClientNotSupported, OidcGrantTypeNotImplementedError |
| 10 | +from auth_backend.models.db import Scope |
| 11 | +from auth_backend.schemas.oidc import PostTokenResponse |
| 12 | +from auth_backend.settings import get_settings |
| 13 | +from auth_backend.utils.jwt import create_jwks |
| 14 | +from auth_backend.utils.oidc_token import OidcGrantType, token_by_client_credentials, token_by_refresh_token |
| 15 | + |
| 16 | + |
| 17 | +settings = get_settings() |
| 18 | +router = APIRouter(prefix="/openid", tags=["OpenID"]) |
| 19 | +logger = logging.getLogger(__name__) |
| 20 | + |
| 21 | + |
| 22 | +@router.get("/.well_known/openid_configuration") |
| 23 | +def openid_configuration(): |
| 24 | + """Конфигурация для подключения OpenID Connect совместимых приложений |
| 25 | +
|
| 26 | + **Attention:** ручка соответствует спецификации не полностью, не все OIDC приложения смогут ей пользоваться |
| 27 | + """ |
| 28 | + return { |
| 29 | + "issuer": f"{settings.APPLICATION_HOST}", |
| 30 | + "token_endpoint": f"{settings.APPLICATION_HOST}/openid/token", |
| 31 | + "userinfo_endpoint": f"{settings.APPLICATION_HOST}/me", |
| 32 | + "jwks_uri": f"{settings.APPLICATION_HOST}/.well-known/jwks", |
| 33 | + "scopes_supported": list(x[0] for x in db.session.query(Scope.name).all()), |
| 34 | + "response_types_supported": ["token"], |
| 35 | + "subject_types_supported": ["public"], |
| 36 | + "id_token_signing_alg_values_supported": ["RS256"], |
| 37 | + "claims_supported": ["sub", "iss", "exp", "iat"], |
| 38 | + "grant_types_supported": [ |
| 39 | + "refresh_token", |
| 40 | + "client_credentials", |
| 41 | + ], |
| 42 | + } |
| 43 | + |
| 44 | + |
| 45 | +@router.get("/.well_known/jwks") |
| 46 | +def jwks(): |
| 47 | + """Публичные ключи для проверки JWT токенов""" |
| 48 | + return {"keys": [create_jwks()]} |
| 49 | + |
| 50 | + |
| 51 | +@router.post("/token") |
| 52 | +async def token( |
| 53 | + background_tasks: BackgroundTasks, |
| 54 | + # Общие OIDC параметры |
| 55 | + grant_type: Annotated[str, Form()], |
| 56 | + client_id: Annotated[str, Form()], # Тут должна быть любая строка, которую проверяем в БД |
| 57 | + client_secret: Annotated[Optional[str], Form()] = None, |
| 58 | + scopes: Annotated[list[str] | None, Form()] = None, |
| 59 | + user_agent: Annotated[str | None, Header()] = None, |
| 60 | + # grant_type=refresh_token |
| 61 | + refresh_token: Annotated[Optional[str], Form()] = None, |
| 62 | + # grant_type=client_credentials |
| 63 | + username: Annotated[Optional[str], Form()] = None, |
| 64 | + password: Annotated[Optional[str], Form()] = None, |
| 65 | +) -> PostTokenResponse: |
| 66 | + """Ручка для получения токена доступа |
| 67 | +
|
| 68 | + ## Позволяет |
| 69 | + - Обменять старый не-JWT токен на новый c таким же набором доступов и таким же сроком давности |
| 70 | + - Обменять JWT токен на новый, если у него есть SESSION_UPDATE_SCOPE |
| 71 | +
|
| 72 | + Потенциально будет позволять: |
| 73 | + - Обменивать Refresh Token на пару Access Token + Refresh Token |
| 74 | + - Обменивать Code (см. Oauth Authorization Code Flow) на пару Access Token + Refresh Token |
| 75 | +
|
| 76 | + ## Параметры: |
| 77 | + Для всех запросов |
| 78 | + - `grant_type` – refresh_token/client_credentials (см. список в `/.well_known/openid_configuration` в поле `grant_types_supported`) |
| 79 | + - `client_id` – строка, по которой проверяется принадлежность к проекту (сейчас только app) |
| 80 | + - `scopes` – список прав для нового токена |
| 81 | +
|
| 82 | + ### `grant_type=refresh_token` |
| 83 | + - refresh_token – токен, выданный этой ручкой или ручкой `/login` в методе авторизации |
| 84 | +
|
| 85 | + ### `grant_type=client_credentials` |
| 86 | + - `username` – логин пользователя |
| 87 | + - `password` – пароль пользователя |
| 88 | + """ |
| 89 | + scopes = scopes or [] |
| 90 | + |
| 91 | + if client_id != 'app': |
| 92 | + raise OidcGrantTypeClientNotSupported(grant_type, client_id) |
| 93 | + if grant_type == OidcGrantType.authorization_code: |
| 94 | + raise OidcGrantTypeNotImplementedError("authorization_code") |
| 95 | + |
| 96 | + # Разные методы обмена токенов |
| 97 | + if grant_type == OidcGrantType.refresh_token: |
| 98 | + new_session = await token_by_refresh_token(refresh_token, scopes) |
| 99 | + elif grant_type == OidcGrantType.client_credentials and Email.is_active(): |
| 100 | + new_session = await token_by_client_credentials(username, password, scopes, user_agent, background_tasks) |
| 101 | + else: |
| 102 | + raise OidcGrantTypeClientNotSupported(grant_type, client_id) |
| 103 | + |
| 104 | + return PostTokenResponse( |
| 105 | + access_token=new_session.token, |
| 106 | + token_type="Bearer", |
| 107 | + expires_in=int((new_session.expires - datetime.utcnow()).total_seconds()), |
| 108 | + refresh_token=new_session.token, |
| 109 | + ) |
0 commit comments