Vous avez probablement déjà entendu parler de la récente version de Flama 1.7, qui a apporté de nouvelles fonctionnalités intéressantes pour vous aider dans le développement et la production de vos API ML. Cet article est précisément consacré à l'un des principaux points forts de cette version : Prise en charge de l'authentification JWT. Mais, avant d'entrer dans les détails avec un exemple pratique, nous vous recommandons de garder à l'esprit les ressources suivantes (et de vous familiariser avec elles si ce n'est pas déjà fait) :
Commençons maintenant avec la nouvelle fonctionnalité et voyons comment vous pouvez sécuriser vos points de terminaison d'API avec une authentification basée sur des jetons sur les en-têtes ou les cookies.
Ce message est structuré comme suit :
Si vous êtes déjà familier avec le concept de JSON Web Token (JWT) et son fonctionnement, n'hésitez pas à ignorer cette section et à passer directement à Implémentation de l'authentification JWT avec Flama. Sinon, nous allons essayer de vous fournir une explication succincte de ce qu'est JWT et pourquoi il est si utile à des fins d'autorisation.
Nous pouvons commencer par la définition officielle donnée dans le document RFC 7519 :
JSON Web Token (JWT) est un moyen de représentation compact et sécurisé pour les URL
prétend être transféré entre deux parties. Les réclamations dans un JWT
sont codés en tant qu'objet JSON utilisé comme charge utile d'un JSON
Structure de signature Web (JWS) ou comme texte brut d'un site Web JSON
Structure de cryptage (JWE), permettant aux réclamations d'être numériques
signé ou protégé en intégrité avec un code d'authentification de message
(MAC) et/ou crypté.
Donc, en termes simples, JWT est un standard ouvert qui définit un moyen de transmettre des informations entre deux parties sous la forme d'un objet JSON. Les informations transmises sont signées numériquement pour garantir leur intégrité et leur authenticité. C'est pourquoi l'un des principaux cas d'utilisation de JWT concerne les autorisations.
Un flux d'authentification prototypique basé sur JWT ressemblerait à ceci :
Cependant, les jetons JWT ne sont pas seulement utiles pour identifier les utilisateurs, mais ils peuvent également être utilisés pour partager des informations entre différents services de manière sécurisée via la charge utile du jeton. De plus, étant donné que la signature du jeton est calculée à l'aide de l'en-tête et de la charge utile, le destinataire peut vérifier l'intégrité du jeton et s'assurer qu'il n'a pas été altéré pendant le transport.
Un JWT est représenté comme une séquence de parties sécurisées pour les URL séparées par des points (.), chaque partie contenant un objet JSON codé en base64url. Le nombre de parties dans le JWT peut varier en fonction de la représentation utilisée, soit JWS (JSON Web Signature) RFC-7515, soit JWE (JSON Web Encryption) RFC-7516.
À partir de maintenant, nous pouvons supposer que nous utiliserons JWS, qui est la représentation la plus courante des jetons JWT. Dans ce cas, un jeton JWT se compose de trois parties :
Ainsi, la syntaxe d'un prototype JWS JSON utilisant la sérialisation JWS JSON aplatie est la suivante (pour plus d'informations, voir RFC-7515 Sec. 7.2.2) :
{ "payload":"<payload contents>", "header":<header contents>, "signature":"<signature contents>" }
Dans la sérialisation compacte JWS, le JTW est représenté par la concaténation :
BASE64URL(UTF8(JWS Header)) || '.' || BASE64URL(JWS Payload) || '.' || BASE64URL(JWS Signature)
Un exemple de jeton JWT ressemblerait à ceci (tiré de l'un des tests Flama ici) :
eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJkYXRhIjogeyJmb28iOiAiYmFyIn0sICJpYXQiOiAwfQ==.J3zdedMZSFNOimstjJat0V28rM_b1UU62XCp9dg_5kg=
Pour un objet JWT général, l'en-tête peut contenir une variété de champs (par exemple, décrivant les opérations cryptographiques appliquées au jeton) selon que le JWT est un JWS ou un JWE. Il existe cependant certains champs qui sont communs aux deux cas, et aussi les plus couramment utilisés :
The payload contains the claims (information) that the token is carrying. The claims are represented as a JSON object, and they can be divided into three categories, according to the standard RFC-7519 Sec. 4:
The signature is used to ensure the integrity of the token and to verify that the sender of the token is who it claims to be. The signature is calculated using the header and the payload, and it is generated using the algorithm specified in the header. The signature is then appended to the token to create the final JWT.
Having introduced the concept of JWT, its potential applications, besides a prototypical authentication flow, we can now move on to the implementation of a JWT-authenticated API using Flama. But, before we start, if you need to review the basics on how to create a simple API with flama, or how to run the API once you already have the code ready, then you might want to check out the quick start guide. There, you'll find the fundamental concepts and steps required to follow this post. Now, without further ado, let's get started with the implementation.
Our first step is to create our development environment, and install all required dependencies for this project. The good thing is that for this example we only need to install flama to have all the necessary tools to implement JWT authentication. We'll be using poetry to manage our dependencies, but you can also use pip if you prefer:
poetry add flama[full]
If you want to know how we typically organise our projects, have a look at our previous post here, where we explain in detail how to set up a python project with poetry, and the project folder structure we usually follow.
Let's start with a simple application that has a single public endpoint. This endpoint will return a brief description of the API.
from flama import Flama app = Flama( title="JWT protected API", version="1.0.0", description="JWT Authentication with Flama ?", docs="/docs/", ) @app.get("/public/info/", name="public-info") def info(): """ tags: - Public summary: Ping description: Returns a brief description of the API responses: 200: description: Successful ping. """ return {"title": app.schema.title, "description": app.schema.description, "public": True}
If you want to run this application, you can save the code above in a file called main.py under the src folder, and then run the following command (remember to have the poetry environment activated, otherwise you'll need to prefix the command with poetry run):
flama run --server-reload src.main:app INFO: Started server process [3267] INFO: Waiting for application startup. INFO: Application startup complete. INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
where the --server-reload flag is optional and is used to reload the server automatically when the code changes. This is very useful during development, but you can remove it if you don't need it. For a full list of the available options, you can run flama run --help, or check the documentation.
Ok, now that we have our base application running, let's add a new endpoint that requires authentication. To do this, we'll need to use the following flama functionality:
Let's proceed and modify our base application to include the JWT authentication as intended, and then we'll explain the code in more detail.
import uuid from flama import Flama from flama.authentication import AuthenticationMiddleware, JWTComponent from flama.middleware import Middleware JWT_SECRET_KEY = uuid.UUID(int=0).bytes # The secret key used to signed the token JWT_HEADER_KEY = "Authorization" # Authorization header identifie JWT_HEADER_PREFIX = "Bearer" # Bearer prefix JWT_ALGORITHM = "HS256" # Algorithm used to sign the token JWT_TOKEN_EXPIRATION = 300 # 5 minutes in seconds JWT_REFRESH_EXPIRATION = 2592000 # 30 days in seconds JWT_ACCESS_COOKIE_KEY = "access_token" JWT_REFRESH_COOKIE_KEY = "refresh_token" app = Flama( title="JWT-protected API", version="1.0.0", description="JWT Authentication with Flama ?", docs="/docs/", ) app.add_component( JWTComponent( secret=JWT_SECRET_KEY, header_key=JWT_HEADER_KEY, header_prefix=JWT_HEADER_PREFIX, cookie_key=JWT_ACCESS_COOKIE_KEY, ) ) app.add_middleware( Middleware(AuthenticationMiddleware), ) # The same code as before here # ...
Although we've decided to add the component and middleware after the initialisation of the app, you can also add them directly to the Flama constructor by passing the components and middlewares arguments:
app = Flama( title="JWT-protected API", version="1.0.0", description="JWT Authentication with Flama ?", docs="/docs/", components=[ JWTComponent( secret=JWT_SECRET_KEY, header_key=JWT_HEADER_KEY, header_prefix=JWT_HEADER_PREFIX, cookie_key=JWT_ACCESS_COOKIE_KEY, ) ], middlewares=[ Middleware(AuthenticationMiddleware), ] )
This is just a matter of preference, and both ways are completely valid.
With the modifications introduced above, we can proceed to add a new (and JWT protected) endpoint. However, before we do that, let's briefly explain in more detail the functionality we've just introduced, namely components and middlewares.
As you might've already noticed, whenever we create a new application we're instantiating a Flama object. As the application grows, as is the case right now, also grows the need to add more dependencies to it. Without dependency injection (DI), this would mean that the Flama class would have to create and manage all its dependencies internally. This would make the class tightly coupled to specific implementations and harder to test or modify. With DI the dependencies are provided to the class from the outside, which decouples the class from specific implementations, making it more flexible and easier to test. And, this is where components come into play in flama.
In flama, a Component is a building block for dependency injection. It encapsulates logic that determines how specific dependencies should be resolved when the application runs. Components can be thought of as self-contained units responsible for providing the necessary resources that the application's constituents need. Each Component in flama has a unique identity, making it easy to look up and inject the correct dependency during execution. Components are highly flexible, allowing you to handle various types of dependencies, whether they're simple data types, complex objects, or asynchronous operations. There are several built-in components in flama, although in this post we're going to exclusively focus on the JWTComponent, which (as all others) derives from the Component class.
The JWTComponent contains all the information and logic necessary for extracting a JWT from either the headers or cookies of an incoming request, decoding it, and then validating its authenticity. The component is initialised with the following parameters:
In the code above, we've initialised the JWTComponent with some dummy values for the secret key, header key, header prefix, and cookie key. In a real-world scenario, you should replace these values with your own secret key and identifiers.
Middleware is a crucial concept that acts as a processing layer between the incoming requests from clients and the responses sent by the server. In simpler terms, middleware functions as a gatekeeper or intermediary that can inspect, modify, or act on requests before they reach the core logic of your application, and also on the responses before they are sent back to the client.
In flama, middleware is used to handle various tasks that need to occur before a request is processed or after a response is generated. In this particular case, the task we want to handle is the authentication of incoming requests using JWT. To achieve this, we're going to use the built-in class AuthenticationMiddleware. This middleware is designed to ensure that only authorised users can access certain parts of your application. It works by intercepting incoming requests, checking for the necessary credentials (such as a valid JWT token), and then allowing or denying access based on the user's permissions which are encoded in the token.
Here’s how it works:
By now, we should have a pretty solid understanding on what our code does, and how it does it. Nevertheless, we still need to see what we have to do to add a protected endpoint to our application. Let's do it:
@app.get("/private/info/", name="private-info", tags={"permissions": ["my-permission-name"]}) def protected_info(): """ tags: - Private summary: Ping description: Returns a brief description of the API responses: 200: description: Successful ping. """ return {"title": app.schema.title, "description": app.schema.description, "public": False}
And that's it! We've added a new endpoint that requires authentication to access. The functionality is exactly the same as the previous endpoint, but this time we've added the tags parameter to the @app.get decorator. The tag parameter can be used to specify additional metadata for an endpoint. But, if we use the special key permissions whilst using the AuthenticationMiddleware, we can specify the permissions required to access the endpoint. In this case, we've set the permissions to ["my-permission-name"], which means that only users with the permission my-permission-name will be able to access this endpoint. If a user tries to access the endpoint without the required permission, they will receive a 403 Forbidden response.
If we put all the pieces together, we should have a fully functional application that has a public endpoint and a private endpoint that requires authentication. The full code should look something like this:
import uuid from flama import Flama from flama.authentication import AuthenticationMiddleware, JWTComponent from flama.middleware import Middleware JWT_SECRET_KEY = uuid.UUID(int=0).bytes # The secret key used to signed the token JWT_HEADER_KEY = "Authorization" # Authorization header identifie JWT_HEADER_PREFIX = "Bearer" # Bearer prefix JWT_ALGORITHM = "HS256" # Algorithm used to sign the token JWT_TOKEN_EXPIRATION = 300 # 5 minutes in seconds JWT_REFRESH_EXPIRATION = 2592000 # 30 days in seconds JWT_ACCESS_COOKIE_KEY = "access_token" JWT_REFRESH_COOKIE_KEY = "refresh_token" app = Flama( title="JWT-protected API", version="1.0.0", description="JWT Authentication with Flama ?", docs="/docs/", ) app.add_component( JWTComponent( secret=JWT_SECRET_KEY, header_key=JWT_HEADER_KEY, header_prefix=JWT_HEADER_PREFIX, cookie_key=JWT_ACCESS_COOKIE_KEY, ) ) app.add_middleware( Middleware(AuthenticationMiddleware), ) @app.get("/public/info/", name="public-info") def info(): """ tags: - Public summary: Info description: Returns a brief description of the API responses: 200: description: Successful ping. """ return {"title": app.schema.title, "description": app.schema.description, "public": True} @app.get("/private/info/", name="private-info", tags={"permissions": ["my-permission-name"]}) def protected_info(): """ tags: - Private summary: Info description: Returns a brief description of the API responses: 200: description: Successful ping. """ return {"title": app.schema.title, "description": app.schema.description, "public": False}
Running the application as before, we should see the following output:
flama run --server-reload src.main:app INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) INFO: Started reloader process [48145] using WatchFiles INFO: Started server process [48149] INFO: Waiting for application startup. INFO: Application startup complete.
If we navigate with our favourite browser to http://127.0.0.1:8000/docs/ we should see the documentation of our API as shown below:
As we can already expect, if we send a request to the public endpoint /public/info/ we should receive a successful response:
curl --request GET \ --url http://localhost:8000/public/info/ \ --header 'Accept: application/json' {"title":"JWT protected API","description":"JWT Authentication with Flama ?","public":true}
However, if we try to access the private endpoint /private/info/ without providing a valid JWT token, we should receive a 401 Unauthorized response:
curl --request GET \ --url http://localhost:8000/private/info/ \ --header 'Accept: application/json' {"status_code":401,"detail":"Unauthorized","error":null}%
Thus, we can say that the JWT authentication is working as expected, and only users with a valid JWT token will be able to access the private endpoint.
As we've just seen, we have a private endpoint which requires a valid JWT token to be accessed. But, how do we get this token? The answer is simple: we need to create a login endpoint that will authenticate the user and return a valid JWT token. To do this, we're going to define the schemas for the input and output of the login endpoint (feel free to use your own schemas if you prefer, for instance, adding more fields to the input schema such as username or email):
import uuid import typing import pydantic class User(pydantic.BaseModel): id: uuid.UUID password: typing.Optional[str] = None class UserToken(pydantic.BaseModel): id: uuid.UUID token: str
Now, let's add the login endpoint to our application:
import http from flama import types from flama.authentication.jwt import JWT from flama.http import APIResponse @app.post("/auth/", name="signin") def signin(user: types.Schema[User]) -> types.Schema[UserToken]: """ tags: - Public summary: Authenticate description: Returns a user token to access protected endpoints responses: 200: description: Successful ping. """ token = ( JWT( header={"alg": JWT_ALGORITHM}, payload={ "iss": "vortico", "data": {"id": str(user["id"]), "permissions": ["my-permission-name"]}, }, ) .encode(JWT_SECRET_KEY) .decode() ) return APIResponse( status_code=http.HTTPStatus.OK, schema=types.Schema[UserToken], content={"id": str(user["id"]), "token": token} )
In this code snippet, we've defined a new endpoint /auth/ that receives a User object as input and returns a UserToken object as output. The User object contains the id of the user and the password (which is optional for this example, since we're not going to be comparing it with any stored password). The UserToken object contains the id of the user and the generated token that will be used to authenticate the user in the private endpoints. The token is generated using the JWT class, and contains the permissions granted to the user, in this case, the permission my-permission-name, which will allow the user to access the private endpoint /private/info/.
Now, let's test the login endpoint to see if it's working as expected. For this, we can proceed via the /docs/ page:
Or, we can use curl to send a request to the login endpoint:
curl --request POST \ --url http://localhost:8000/auth/ \ --header 'Accept: application/json' \ --header 'Content-Type: application/json' \ --data '{ "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "password": "string" }'
which should return a response similar to this:
{"id":"497f6eca-6276-4993-bfeb-53cbbbba6f08","token":"eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJkYXRhIjogeyJpZCI6ICI0OTdmNmVjYS02Mjc2LTQ5OTMtYmZlYi01M2NiYmJiYTZmMDgiLCAicGVybWlzc2lvbnMiOiBbIm15LXBlcm1pc3Npb24tbmFtZSJdfSwgImlhdCI6IDE3MjU5ODk5NzQsICJpc3MiOiAidm9ydGljbyJ9.vwwgqahgtALckMAzQHWpNDNwhS8E4KAGwNiFcqEZ_04="}
That string is the JWT token that we can use to authenticate the user in the private endpoint. Let's try to access the private endpoint using the token:
curl --request GET \ --url http://localhost:8000/private/info/ \ --header 'Accept: application/json' \ --header 'Authorization: Bearer eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJkYXRhIjogeyJpZCI6ICI0OTdmNmVjYS02Mjc2LTQ5OTMtYmZlYi01M2NiYmJiYTZmMDgiLCAicGVybWlzc2lvbnMiOiBbIm15LXBlcm1pc3Npb24tbmFtZSJdfSwgImlhdCI6IDE3MjU5ODk5NzQsICJpc3MiOiAidm9ydGljbyJ9.vwwgqahgtALckMAzQHWpNDNwhS8E4KAGwNiFcqEZ_04='
which now returns a successful response:
{"title":"JWT protected API","description":"JWT Authentication with Flama ?","public":false}
And that's it! We've successfully implemented JWT authentication in our Flama application. We now have a public endpoint that can be accessed without authentication, a private endpoint that requires a valid JWT token to be accessed, and a login endpoint that generates a valid JWT token for the user.
The privatisation of some endpoints (even all at some instances) is a common requirement in many applications, even more so when dealing with sensitive data as is often the case in Machine Learning APIs which process personal or financial information, to name some examples. In this post, we've covered the fundamentals of token-based authentication for APIs, and how this can be implemented without much of a hassle using the new features introduced in the latest release of flama (introduced in a previous post). Thanks to the JWTComponent and AuthenticationMiddleware, we can secure our API endpoints and control the access to them based on the permissions granted to the user, and all this with just a few modifications to our base unprotected application.
We hope you've found this post useful, and that you're now ready to implement JWT authentication in your own flama applications. If you have any questions or comments, feel free to reach out to us. We're always happy to help!
Stay tuned for more posts on flama and other exciting topics in the world of AI and software development. Until next time!
If you like what we do, there is a free and easy way to support our work. Gift us a ⭐ at Flama.
GitHub ⭐'s mean a world to us, and give us the sweetest fuel to keep working on it to help others on its journey to build robust Machine Learning APIs.
You can also follow us on ?, where we share our latest news and updates, besides interesting threads on AI, software development, and much more.
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!