Maison  >  Article  >  développement back-end  >  Authentification FastAPI avec injection de dépendances

Authentification FastAPI avec injection de dépendances

Patricia Arquette
Patricia Arquetteoriginal
2024-09-24 06:21:09877parcourir

FastAPI Auth with Dependency Injection

FastAPI est un framework Web moderne permettant de créer des API en Python. C'est l'un de mes frameworks Web préférés car il prend en charge les spécifications OpenAPI (ce qui signifie que vous pouvez écrire votre code backend et tout générer à partir de celui-ci) et il prend en charge l'injection de dépendances.

Dans cet article, nous examinerons brièvement le fonctionnement de Depends de FastAPI. Nous verrons ensuite pourquoi cela s’applique si bien à l’authentification et à l’autorisation. Nous le comparerons également au middleware, qui est une autre option courante pour l’authentification. Enfin, nous examinerons quelques modèles d'autorisation plus avancés dans FastAPI.

Qu'est-ce que l'injection de dépendances ?

L'une des fonctionnalités les plus puissantes de FastAPI est sa prise en charge de première classe pour l'injection de dépendances. Nous avons un guide plus long ici, mais regardons un exemple rapide de la façon dont il peut être utilisé.

Disons que nous construisons une API paginée. Chaque appel d'API peut inclure un numéro de page et une taille de page. Maintenant, nous pourrions simplement créer une API et prendre ces paramètres directement :

@app.get("/things/")
async def fetch_things(page_number: int = 0, page_size: int = 100):
    return db.fetch_things(page_number, page_size)

Mais nous souhaitons probablement ajouter une logique de validation afin que personne ne demande le numéro de page -1 ou la taille de page 10 000 000.

@app.get("/things/")
async def fetch_things(page_number: int = 0, page_size: int = 100):
    if page_number < 0:
        raise HTTPException(status_code=400, detail="Invalid page number")
    elif page_size <= 0:
        raise HTTPException(status_code=400, detail="Invalid page size")
    elif page_size > 100:
        raise HTTPException(status_code=400, detail="Page size can be at most 100")
    return db.fetch_things(page_number, page_size)

Et c'est... bien, mais si nous avions 10 ou 100 API qui nécessitaient toutes les mêmes paramètres de pagination, cela deviendrait un peu fastidieux. C'est là qu'intervient l'injection de dépendances : nous pouvons déplacer toute cette logique dans une fonction et injecter cette fonction dans notre API :

async def paging_params_dep(page_number: int = 0, page_size: int = 100):
    if page_number < 0:
        raise HTTPException(status_code=400, detail="Invalid page number")
    elif page_size <= 0:
        raise HTTPException(status_code=400, detail="Invalid page size")
    elif page_size > 100:
        raise HTTPException(status_code=400, detail="Page size can be at most 100")
    return PagingParams(page_number, page_size)

@app.get("/things/")
async def fetch_things(paging_params: PagingParams = Depends(paging_params_dep)):
    return db.fetch_things(paging_params)

@app.get("/other_things/")
async def fetch_other_things(paging_params: PagingParams = Depends(paging_params_dep)):
    return db.fetch_other_things(paging_params)

Cela présente de jolis avantages :

  • Chaque itinéraire emprunté par PagingParams est automatiquement validé et a des valeurs par défaut.

  • C'est moins verbeux et sujet aux erreurs que d'avoir la première ligne de chaque route validate_paging_params(page_number, page_size)

  • Cela fonctionne toujours avec le support OpenAPI de FastAPI - ces paramètres apparaîtront dans vos spécifications OpenAPI.

Qu'est-ce que cela a à voir avec l'authentification ?

Il s'avère que c'est aussi un excellent moyen de modéliser l'authentification ! Imaginez que vous ayez une fonction comme :

async def validate_token(token: str):
    try:
        # This could be JWT validation, looking up a session token in the DB, etc.
        return await get_user_for_token(token)
    except:
        return None

Pour connecter cela à une route API, il suffit de l'envelopper dans une dépendance :

async def require_valid_token_dep(req: Request):
    # This could also be a cookie, x-api-key header, etc.
    token = req.headers["Authorization"]
    user = await validate_token(token)
    if user == None:
        raise HTTPException(status_code=401, detail="Unauthorized")
    return user

Et puis tous nos itinéraires protégés peuvent ajouter cette dépendance :

@app.get("/protected")
async def do_secret_things(user: User = Depends(require_valid_token_dep)):
    # do something with the user

Si l'utilisateur fournit un jeton valide, cette route s'exécutera et l'utilisateur est défini. Dans le cas contraire, un 401 vous sera retourné.

Remarque : OpenAPI/Swagger dispose d'un support de première classe pour la spécification des jetons d'authentification, mais vous devez utiliser l'une des classes dédiées pour cela. Au lieu de req.headers["Authorization"], vous pouvez utiliser HTTPBearer(auto_error=False) de fastapi.security qui renvoie un HTTPAuthorizationCredentials.

Middleware vs Dépend pour l'authentification

FastAPI, comme la plupart des frameworks, a un concept de middleware. Votre middleware peut contenir du code qui s'exécutera avant et après une requête. Il peut modifier la demande avant qu'elle n'arrive sur votre itinéraire et il peut modifier la réponse avant qu'elle ne soit renvoyée à l'utilisateur.

Dans de nombreux autres frameworks, le middleware est un endroit très courant pour les contrôles d'authentification. Cependant, cela est souvent dû au fait que le middleware est également chargé d’« injecter » l’utilisateur dans l’itinéraire. Par exemple, un modèle courant dans Express consiste à faire quelque chose comme :

app.get("/protected", authMiddleware, (req, res) => {
    // req.user is set by the middleware
    // as there's no good way to pass in extra information into this route,
    // outside of the request
});

Étant donné que FastAPI a un concept d'injection intégré, vous n'aurez peut-être pas du tout besoin d'utiliser un middleware. J'envisagerais d'utiliser un middleware si vous avez besoin de « rafraîchir » périodiquement vos jetons d'authentification (pour les maintenir en vie) et de définir la réponse sous forme de cookie.

Dans ce cas, vous souhaiterez utiliser request.state pour transmettre les informations du middleware aux routes (et vous pouvez utiliser une dépendance pour valider le request.state si vous le souhaitez).

Sinon, je m'en tiendrai à l'utilisation de Depends car l'utilisateur sera injecté directement dans vos routes sans avoir besoin de passer par request.state.

Autorisation - Multilocation, rôles et autorisations

Si nous appliquons tout ce que nous avons appris jusqu'à présent, l'ajout d'une architecture multi-tenant, de rôles ou d'autorisations peut être assez simple. Disons que nous avons un sous-domaine unique pour chacun de nos clients, nous pouvons créer une dépendance pour ce sous-domaine :

async def tenant_by_subdomain_dep(request: Request) -> Optional[str]:
    # first we get the subdomain from the host header
    host = request.headers.get("host", "")
    parts = host.split(".")
    if len(parts) <= 2:
        raise HTTPException(status_code=404, detail="Not found")
    subdomain = parts[0]

    # then we lookup the tenant by subdomain
    tenant = await lookup_tenant_for_subdomain(subdomain)
    if tenant == None:
        raise HTTPException(status_code=404, detail="Not found")
    return tenant

Nous pouvons combiner cette idée avec nos idées précédentes et créer une nouvelle dépendance « multi-tenant » :

async def get_user_and_tenant_for_token(
    user: User = Depends(require_valid_token_dep),
    tenant: Tenant = Depends(tenant_by_subdomain_dep),
) -> UserAndTenant:
    is_user_in_tenant = await check_user_is_in_tenant(tenant, user)
    if is_user_in_tenant:
        return UserAndTenant(user, tenant)
    raise HTTPException(status_code=403, detail="Forbidden")

Nous pouvons ensuite injecter cette dépendance dans nos routes :

@app.get("/protected")
async def do_secret_things(user_and_tenant: UserAndTenant = Depends(get_user_and_tenant_for_token)):
    # do something with the user and tenant

Et cela finit par faire quelques choses importantes :

  • Vérifier que l'utilisateur dispose d'un token valide

  • Vérifier que l'utilisateur fait une demande vers un sous-domaine valide

  • Vérifier que l'utilisateur doit avoir accès à ce sous-domaine

If any of those invariants aren’t met - an error is returned and our route will never run. We can extend this to include other things like roles & permissions (RBAC) or making sure the user has a certain property set (active paid subscription vs no active subscription).

PropelAuth <3 FastAPI

At PropelAuth, we’re big fans of FastAPI. We have a FastAPI library that will enable you to set up authentication and authorization quickly - including SSO, Enterprise SSO / SAML, SCIM Provisioning, and more.

And it all works with dependencies like the ones you’ve seen above, e.g.:

@app.get("/")
async def root(current_user: User = Depends(auth.require_user)):
    return {"message": f"Hello {current_user.user_id}"}

You can find out more here.

Summary

  • FastAPI's dependency injection provides a powerful way to handle authentication and authorization in web applications.

  • The Depends feature allows for clean, reusable code for validating tokens, checking user permissions, and handling multi-tenancy.

  • Compared to middleware, using dependencies for auth offers more flexibility and direct integration with route functions.

  • Complex authorization scenarios like multi-tenancy and role-based access control can be efficiently implemented using nested dependencies.

  • PropelAuth offers a FastAPI library that simplifies the implementation of advanced authentication and authorization features.

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn