diff --git a/docs/en/docs/references/injector.md b/docs/en/docs/references/injector.md
index 9bd6a8e7..7e442eae 100644
--- a/docs/en/docs/references/injector.md
+++ b/docs/en/docs/references/injector.md
@@ -24,8 +24,6 @@ from esmerald import Inject, Injects, Factory, DiderectInjects
- "!^__call__"
- "!^__eq__"
-::: esmerald.DirectInjects
-
::: esmerald.Factory
options:
filters:
diff --git a/docs/en/docs/security/index.md b/docs/en/docs/security/index.md
new file mode 100644
index 00000000..7d73179d
--- /dev/null
+++ b/docs/en/docs/security/index.md
@@ -0,0 +1,80 @@
+# Security
+
+Security, authentication, and authorization can be approached in various ways.
+
+These topics are often considered complex and challenging to master.
+
+In many frameworks and systems, managing security and authentication requires significant effort,
+often accounting for 50% or more of the total codebase.
+
+**Esmerald**, however, offers a range of tools that simplify handling security.
+These tools enable you to implement secure systems quickly, efficiently,
+and in compliance with standards—without needing to dive deeply into all the technical specifications of security.
+
+Before diving in, let’s explore a few key concepts.
+
+## Quick Note
+
+If you don't need to worry about these concepts, terms and terminologies or you are simply familiar with those, you can
+jump directly to the next sections.
+
+## OAuth
+
+OAuth2 is a comprehensive specification that outlines multiple methods for managing authentication and authorization.
+
+It is designed to handle a wide range of complex scenarios.
+
+One of its key features is enabling authentication through a "third party."
+
+This is the foundation for systems that offer options like "Sign via Facebook",
+"Sign in using Google" or "Login via GitHub".
+
+### OAuth (first version)
+
+OAuth 1 was an earlier version of the OAuth specification, significantly different from OAuth2.
+It was more complex because it included detailed requirements for encrypting communication.
+
+Today, OAuth 1 is rarely used or popular.
+
+In contrast, OAuth2 simplifies this aspect by not defining how to encrypt communication.
+Instead, it assumes that your application is served over HTTPS, ensuring secure communication by
+relying on the encryption provided by the protocol.
+
+## OpenID Connect
+
+OpenID Connect is a specification built on top of **OAuth2**.
+
+It extends OAuth2 by addressing ambiguities in the original specification, aiming to improve interoperability across systems.
+
+For instance, Google login leverages OpenID Connect, which operates on top of OAuth2.
+
+However, Facebook login does not support OpenID Connect and instead uses its own customized version of OAuth2.
+
+## OpenID (not "OpenID Connect")
+
+There was also an earlier specification called "OpenID," which aimed to address the same challenges as **OpenID Connect**.
+However, it was not built on OAuth2 and functioned as a completely separate system.
+
+Unlike OpenID Connect, OpenID did not achieve widespread adoption and is rarely used today.
+
+## The OpenAPI
+
+Did you know that OpenAPI in the past was called **Swagger**? You probably did and this is we still have the *swagger ui*
+and the constant use of that name.
+
+**Esmerald** provides a native integration with **OpenAPI** as well with its automatic documentation generation.
+
+Why this? Well, using the OpenAPI specification it can also take advantage of the standard-based tools for security.
+
+The following `security schemes` are supported by OpenAPI:
+
+* `apiKey` - An application specific key that can come from:
+ * Cookie parameter
+ * Header parameter
+ * Header parameter
+* `http` - The standard HTTP authentication system that includes:
+ * `bearer` - An header `Authorization` followed by a value of `Bearer` with the corresponding token.
+ * HTTP Basic Auth
+ * HTTP Digest
+
+OpenAPI also supports the previously mentioned `OAuth2` and `OpenID Connect`.
diff --git a/docs/en/docs/security/interaction.md b/docs/en/docs/security/interaction.md
new file mode 100644
index 00000000..2e2d47a8
--- /dev/null
+++ b/docs/en/docs/security/interaction.md
@@ -0,0 +1,111 @@
+# Interaction & Next Steps
+
+In the [previous chapter](./introduction.md), the security system—based on **Esmerald's** dependency injection system was providing the `path operation function` with a `token` as a `str`.
+
+This token was extracted from the `Authorization` header of the incoming request. The security system automatically handled this, so the function didn't need to worry about how the token was retrieved. The function simply received the token as a string, which it could then use for further processing, such as verifying the token's validity or checking user permissions.
+
+```python hl_lines="9-10"
+{!> ../../../docs_src/security/app.py !}
+```
+
+That’s still not very useful as it is.
+
+Let’s enhance it by returning the current user instead.
+
+## Create a user model
+
+By creating a `user` model you can use `Pydantic`, msgspec or whatever you want since Esmerald supports the [encoders](../encoders.md)
+making it versatile enough for your needs.
+
+For ths example, let us use the native Pydantic support.
+
+```python
+{!> ../../../docs_src/security/enhance.py !}
+```
+
+## The `get_current_user` dependency
+
+Let's create a dependency called `get_current_user`.
+
+And remember, dependencies can have sub-dependencies, right?
+
+```python hl_lines="17"
+{!> ../../../docs_src/security/enhance.py !}
+```
+
+The `get_current_user` dependency will depend on the same `oauth2_scheme` we created earlier.
+
+Just like we did before in the *path operation* itself, our new `get_current_user` dependency will receive a `token` as a `str` from the `oauth2_scheme` sub-dependency.
+
+!!! Warning
+ You can see a `Security` object there in the sub-dependency, right? Well, yes, that `Security` object that depends
+ of the `scheme` can only be called using this object.
+
+ In other words, when a sub-dependency is a `oauth2_scheme` type of thing or any security related, **you must** use the `Security` object.
+
+ This special object once its declared, **Esmerald** will know what to do with it and make sure it can be executed
+ properly.
+
+ Esmerald dependency system is extremely powerful and extremely versatile and therefore some special objects dedicated
+ to this security approach were added to make our lives simples.
+
+## Get the user
+
+The `get_current_user` dependency will use a (fake) utility function we created. This function takes the token as a `str` and returns our Pydantic `User` model.
+
+```python hl_lines="13-14"
+{!> ../../../docs_src/security/enhance.py !}
+```
+
+## Inject the current user
+
+Now, we can use the `Inject` and `Injects` with our `get_current_user` dependency in the *path operation*. This is part
+of the special Esmerlad dependency inject system that is also multi layered. You can read again about the
+[dependency injection with Esmerald](../dependencies.md).
+
+```python hl_lines="27"
+{!> ../../../docs_src/security/enhance.py !}
+```
+
+Notice that we declare the type of `current_user` as the Pydantic model `User`.
+
+This ensures that we get type checking and auto-completion support inside the function, making development smoother and more error-free.
+
+Now, you can directly access the current user in the *path operation functions* and handle the security mechanisms at the **Dependency Injection** level, using `Depends`.
+
+You can use any model or data for your security requirements (in this case, a Pydantic model `User`), but you're not limited to a specific data model, class, or type.
+
+For example:
+- Want to use an `id` and `email` instead of a `username` in your model? No problem, just use the same tools.
+- Prefer a `str` or a `dict`? Or perhaps a database class model instance directly? It all works seamlessly.
+- If you have bots, robots, or other systems logging in instead of users, and they only need an access token, that's fine too.
+
+You can use any model, class, or database structure that fits your application's needs. **Esmerald**'s dependency injection system makes it easy and flexible for all cases.
+
+## Code size so far
+
+This example might seem a bit verbose, but remember, we're combining security, data models, utility functions, and *path operations* in the same file.
+
+Here’s the key takeaway:
+
+The security and dependency injection setup is written **once**.
+
+You can make it as complex as you need, but it only needs to be defined in one place. The beauty of **Esmerald** is its flexibility—whether simple or complex, you only write this logic once.
+
+And once it's set up, you can reuse it across **thousands of endpoints** (*path operations*).
+
+All of these endpoints (or any portion of them) can take advantage of the same dependencies or any others you create.
+
+Even with thousands of *path operations*, many of them can be as simple as just a few lines of code.
+
+```python hl_lines="27"
+{!> ../../../docs_src/security/enhance.py !}
+```
+
+Remember that Esmerald has a flexible dependency injection system and the lines can be cut by a lot avoiding repetition.
+
+You can now access the current user directly in your *path operation function*.
+
+We're already halfway there.
+
+Next, we just need to add a *path operation* that allows the user/client to send their `username` and `password` to get the token. That will be our next step.
diff --git a/docs/en/docs/security/introduction.md b/docs/en/docs/security/introduction.md
new file mode 100644
index 00000000..ae877ee5
--- /dev/null
+++ b/docs/en/docs/security/introduction.md
@@ -0,0 +1,180 @@
+# First Introduction
+
+Let's imagine that you have your backend API in some domain.
+
+And you have a frontend in another domain or in a different path of the same domain (or in a mobile application).
+
+And you want to have a way for the frontend to authenticate with the backend, using a username and password.
+
+We can use OAuth2 to build that with **Esmerald**.
+
+But let's save you the time of reading the full long specification just to find those little pieces of information you need.
+
+Let's use the tools provided by **Esmerald** to handle security.
+
+## Let us dig in
+
+We will be doing and explaining at the same time what is what.
+
+## Create an `app.py`
+
+You can copy the following code into an `app.py` or any file at your choice.
+
+```python
+{!> ../../../docs_src/security/app.py !}
+```
+
+## Run it
+
+You can now run the file using, for example, `uvicorn` and it can be like this:
+
+```shell
+$ uvicorn app:app
+```
+
+## Verify it
+
+To check if the endpoint is properly configured and working, you can access the OpenAPI documentation at
+[http://127.0.0.1:8000/docs/swagger](http://127.0.0.1:8000/docs/swagger).
+
+You should be able to see something like this:
+
+
+
+!!! Tip
+ As you can see, you already have a brand new shiny **Authorize** button at the top of the page.
+ The same is applied to the path operation that contains a lock icon as well.
+
+If you click the **Authorize** button, you will be able to see the type of login to type a `username`, `password` and
+other fields as well.
+
+Lets check and click it!
+
+
+
+!!! Note
+ Typing anything in the form won't make it work, yet. Step by step we will get there, no worries.
+
+This isn't the frontend interface intended for end users. Instead, it serves as a powerful, interactive tool for documenting your API.
+
+It’s useful for the frontend team (which might also be you), for third-party applications and systems, and even for your own use.
+You can rely on it to debug, review, and test your application efficiently.
+
+## The `password` flow
+
+Now, let’s take a step back and clarify what this all means.
+
+The `password` "flow" is one of the methods (or "flows") defined in OAuth2 for managing security and authentication.
+
+OAuth2 was originally designed to separate the backend or API from the server responsible for user authentication.
+
+However, in this scenario, the same Esmerald application will handle both the API and the authentication process.
+
+Let’s examine it from this simplified perspective:
+
+Here’s how the password "flow" works step by step:
+
+1. **User Login**: The user enters their `username` and `password` in the frontend and submits the form by hitting `Enter`.
+
+2. **Frontend Request**: The frontend (running in the user’s browser) sends the `username` and `password` to a specific URL on the API, typically defined with `tokenUrl="token"`.
+
+3. **API Validation**:
+ - The API verifies the provided `username` and `password`.
+ - If valid, it responds with a "token."
+ - A **token** is essentially a string containing information that can later be used to authenticate the user.
+ - Tokens usually have an expiration time:
+ - After expiration, the user must log in again.
+ - This limits the risk if the token is stolen since it won’t work indefinitely (in most cases).
+
+4. **Token Storage**: The frontend temporarily stores the token securely.
+
+5. **Navigating the App**: When the user navigates to another section of the web app, the frontend may need to fetch additional data from the API.
+
+6. **Authenticated API Requests**:
+ - To access protected endpoints, the frontend includes an `Authorization` header in its request.
+ - The header’s value is `Bearer ` followed by the token.
+ - For example, if the token is `foobar`, the `Authorization` header would look like this:
+
+ ```plaintext
+ Authorization: Bearer foobar
+ ```
+
+## **Esmerald** `OAuth2PasswordBearer`
+
+**Esmerald** offers various tools, at different levels of abstraction, to implement security features.
+
+In this example, we’ll use **OAuth2** with the **Password** flow, utilizing a **Bearer** token. To do this, we’ll use the `OAuth2PasswordBearer` class.
+
+!!! info
+
+ A "bearer" token isn’t the only option for authentication. However, it’s the most suitable for our use case and often the best choice for most scenarios.
+
+ Unless you’re an OAuth2 expert and know of another option that better fits your needs, **Esmerald** gives you the flexibility to implement other options as well.
+
+ When creating an instance of the `OAuth2PasswordBearer` class, we provide the `tokenUrl` parameter. This specifies the URL that the frontend (running in the user's browser) will use to send the `username` and `password` in order to obtain the token.
+
+When we create an instance of the `OAuth2PasswordBearer` class, we provide the `tokenUrl` parameter. This URL is where the client (the frontend running in the user's browser) will send the `username` and `password` in order to obtain a token.
+
+```python hl_lines="6"
+{!> ../../../docs_src/security/app.py !}
+```
+
+!!! Tip
+ Here, `tokenUrl="token"` refers to a relative URL, `token`, which we haven’t created yet. Since it’s a relative URL, it’s equivalent to `./token`.
+
+ This means that if your API is hosted at `https://example.com/`, the full URL would be `https://example.com/token`. If your API is at `https://example.com/api/v1/`, then the full URL would be `https://example.com/api/v1/token`.
+
+ Using a relative URL is important, as it ensures your application continues to function correctly, even in more advanced scenarios, like when running **Behind a Proxy**.
+
+This parameter doesn’t automatically create the `/token` endpoint or path operation. Instead, it simply declares that the URL `/token` will be the endpoint that the client should use to obtain the token.
+
+This information is then used in OpenAPI and displayed in the interactive API documentation, guiding the client on where to send the request for the token.
+
+We will create the actual path operation for this endpoint shortly.
+
+The `oauth2_scheme` variable is an instance of the `OAuth2PasswordBearer` class, but it is also a "callable" object.
+
+This means that you can use it as a function, like this:
+
+```Python
+oauth2_scheme(some, parameters)
+```
+
+When called, it will handle the extraction of the token from the request, typically from the `Authorization` header.
+
+So, it can be used with `Inject()` and `Injects()`.
+
+### Use it
+
+Now you can pass that `oauth2_scheme` in a dependency with `Inject` and `Injects` natively from Esmerald.
+
+```python hl_lines="9-10"
+{!> ../../../docs_src/security/app.py !}
+```
+
+The `security` in the handler is what allows the OpenAPI specification to understand what needs to go in the **Authorize**.
+
+This dependency will provide a `str` that gets assigned to the `token` parameter of the *path operation function*.
+
+**Esmerald** will automatically recognize this dependency and use it to define a "security scheme" in the OpenAPI schema. This also makes the security scheme visible in the automatic API documentation, helping both developers and users understand how authentication works for the API.
+
+!!! info
+ **Esmerald** knows it can use the `OAuth2PasswordBearer` class (declared as a dependency) to define the security scheme in OpenAPI because `OAuth2PasswordBearer` inherits from `esmerald.security.oauth2.OAuth2`, which, in turn, inherits from `esmerald.security.base.SecurityBase`.
+
+ All security utilities that integrate with OpenAPI and the automatic API documentation inherit from `SecurityBase`. This inheritance structure allows **Esmerald** to automatically recognize and integrate these security features into the OpenAPI schema, ensuring they are properly displayed in the API docs.
+
+## What does it do
+
+**Esmerald** will automatically look for the `Authorization` header in the request, check if it contains a value starting with `Bearer ` followed by a token, and return that token as a `str`.
+
+If it doesn't find an `Authorization` header or if the value doesn't contain a valid `Bearer` token, **Esmerald** will immediately respond with a `401 Unauthorized` error.
+
+You don't need to manually check for the token or handle the error yourself, **Esmerald** ensures that if your function is executed, the `token` parameter will always contain a valid `str`.
+
+You can even test this behavior in the interactive documentation to see how it works in action.
+
+
+
+That's correct! At this stage, we're not verifying the validity of the token yet. We're simply extracting it from the `Authorization` header and passing it as a string to the path operation function.
+
+This is an important first step, as it lays the groundwork for authentication. Later, you can implement the logic to validate the token (e.g., checking its signature, expiration, etc.). But for now, this setup ensures that the token is correctly extracted and available for further use.
diff --git a/docs/en/docs/security/oauth-jwt.md b/docs/en/docs/security/oauth-jwt.md
new file mode 100644
index 00000000..963ce4be
--- /dev/null
+++ b/docs/en/docs/security/oauth-jwt.md
@@ -0,0 +1,243 @@
+# OAuth2 with Password, Bearer with JWT tokens
+
+Now that we’ve outlined the security flow, let’s secure the application using JWT tokens and secure password hashing.
+
+The following code is production-ready. You can store hashed passwords in your database and integrate it into your application.
+
+We’ll build on the foundation from the previous chapter and enhance it further.
+
+## What is the JWT
+
+JWT extends for *JSON Web Token* and it is widely adopted and used to secure systems around the world.
+
+JWT is also a standard and quite lengthy.
+
+```json
+eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
+```
+
+!!! Info
+ The previous example was extracted from [https://jwt.io/](https://jwt.io) if you decide to play around
+ and see what you can do with it.
+
+
+JWT tokens are not encrypted, meaning their contents can be read if intercepted. However, they are signed, ensuring you can verify that the token was issued by you and hasn't been tampered with.
+
+This allows you to issue a token with a set expiration, for example, one week. If the user returns the next day with the token, you can verify they are still logged into your system. After the token expires, the user will no longer be authorized and must log in again to obtain a new one.
+
+If someone attempts to modify the token, such as changing the expiration date, the signature validation will fail, exposing the tampering attempt.
+
+## Installing `PyJWT`
+
+The following examples will be assuming that you don't know about anything although,
+**Esmerald also comes with [JWT integration](../configurations/jwt.md)** and there are details how to leverage it.
+
+You will be required to install some additional libraries when using the following examples but summarizing it, you
+can also achieve the same results by running:
+
+```shell
+$ pip install esmerald[jwt]
+```
+
+!!! Warning
+ It is strongly advised to use virtual environments to isolate your packages from the core system ones and avoiding to break them by accident.
+
+
+To use digital signature algorithms like RSA or ECDSA, make sure to install the `cryptography` library by adding the `pyjwt[crypto]` dependency.
+
+For more details, refer to the [PyJWT Installation Documentation](https://pyjwt.readthedocs.io/en/stable/installation.html).
+
+Now it is time to install `PyJWT`.
+
+```shell
+$ pip install pyjwt
+```
+
+## Password Hashing
+
+Hashing involves transforming content (such as a password) into a seemingly random sequence of bytes (a string) that resembles gibberish.
+
+The same input (e.g., the same password) will always produce the same hashed output. However, the process is one-way, meaning you cannot reverse the hash to recover the original content.
+
+### Why hashing is important
+
+If your database is compromised, the attacker will only have access to hashed passwords, not the plaintext ones.
+
+This prevents the thief from directly using the passwords on other systems, which is critical since many users reuse the same password across multiple platforms.
+
+An example of hashing is what Django (and **Esmerald**) offer, the **PBKDF2** (Password-Based Key Derivation Function 1 and 2).
+
+To help us with this, we will be using `passlib`.
+
+## Installing `passlib`
+
+PassLib is an excellent Python library for managing password hashing.
+
+It supports a variety of secure hashing algorithms and provides utilities for working with them.
+
+The recommended algorithm is **Bcrypt**, known for its robust security features.
+
+```shell
+$ pip install passlib[bcrypt]
+```
+
+!!! Tip
+ PassLib allows you to configure it to read passwords hashed by frameworks like Django, Flask security plugins, and others.
+
+ This enables scenarios such as sharing a database between a Django application and a Esmerald application or gradually migrating a Django
+ application to Esmerald.
+
+ Users can seamlessly log in from either application, ensuring compatibility and a smooth transition.
+
+## Hashing and verification of the passwords
+
+This can be achived by importing everything that is needed from `passlib` package.
+
+Create a PassLib "context" to handle password hashing and verification.
+
+!!! Tip
+ The PassLib context supports multiple hashing algorithms, including deprecated ones, enabling you to verify old hashes while using a secure algorithm like Bcrypt for new passwords.
+
+ This allows compatibility with existing systems (e.g., verifying Django-generated passwords) while ensuring stronger security for newly hashed passwords—all within the same application.
+
+Create a utility function to hash a user's password, another to check if a given password matches the stored hash, and a third to authenticate the user and return their details.
+
+```python hl_lines="6 29 64-65 68-69 77-81"
+{!> ../../../docs_src/security/hash/app.py !}
+```
+
+!!! Check
+ In the new (fake) database, `fake_users_db`, the hashed password will appear as a string like this: `"$2a$12$KplebFTPwFcgGQosJgI4De0PyB2AoRCSxasxHpFoYZPp6uQV/xLzm"`. You can test the username `janedoe` and the
+ password `hashsecret` against this value and confirm it is correct using any online platform dedicated to this.
+
+## Handling JWT Tokens
+
+Import the necessary modules.
+
+Generate a random secret key to sign the JWT tokens.
+
+Use the following command to generate a secure random secret key:
+
+```shell
+$ openssl rand -hex 32
+```
+
+Here’s a clearer and more concise version of the instructions:
+
+1. Copy the output of the random secret key generation into the `SECRET_KEY` variable (do not use the example key).
+2. Create a variable `ALGORITHM` and set it to `"HS256"`, the algorithm used for signing the JWT token.
+3. Define a variable for the token’s expiration time.
+4. Define a Pydantic model to use for the response in the token endpoint.
+5. Create a utility function to generate a new access token.
+
+```python hl_lines="4 5 24-26 44-46 84-88"
+{!> ../../../docs_src/security/hash/app.py !}
+```
+
+## Dependencies Update
+
+Update `get_current_user` to accept the same token as before, but now use JWT tokens.
+
+Decode the received token, verify its validity, and return the current user. If the token is invalid or a user is disabled, immediately raise an HTTP error.
+
+```python hl_lines="91-108"
+{!> ../../../docs_src/security/hash/app.py !}
+```
+
+## Update the `/token` handler
+
+Create a `timedelta` object for the token's expiration time.
+
+Generate a valid JWT access token and return it.
+
+```python hl_lines="111-128"
+{!> ../../../docs_src/security/hash/app.py !}
+```
+
+### The technicalities of the subject `sub`
+
+The JWT specification includes a `sub` key, which represents the subject of the token. Although optional, it is often used to store the user's unique identifier.
+
+JWTs can be used for more than just identifying users. For example, you might use them to represent entities like a "car" or a "blog post." You can then assign specific permissions to these entities, such as "drive" for the car or "edit" for the blog post. By issuing a JWT to a user or bot, they can perform actions (e.g., drive the car or edit the blog post) without needing an account, relying solely on the JWT generated by your API.
+
+In more complex scenarios, multiple entities might share the same identifier, such as "foo" representing a user, a car, and a blog post. To prevent ID collisions, you can prefix the `sub` value. For instance, to distinguish a user named "johndoe," the `sub` value could be `username:johndoe`.
+
+The key point is that the `sub` key should contain a unique identifier across the entire application and must be a string.
+
+## Time to verify it
+
+Start the server and navigate to the documentation at [http://127.0.0.1:8000/docs/swagger](http://127.0.0.1:8000/docs).
+
+You should see a similar interface like the following:
+
+
+
+Click the **Authorize** button and use the following credentials:
+
+* **User**: `janedoe`
+* **Password**: `hashsecret`.
+
+
+
+Now it time to call the endpoint `/users/me` and you should get a response like the following:
+
+```json
+{
+ "username": "janedoe",
+ "email": "janedoe@example.com",
+ "full_name": "Jane Doe",
+ "disabled": false
+}
+```
+
+
+
+When you open the developer tools, you’ll notice that the data sent includes only the JWT token. The password is sent only in the initial request to authenticate the user and obtain the access token. After that, the password is not transmitted in subsequent requests.
+
+## Advanced usage with `scopes`
+
+OAuth2 defines "scopes" to specify permissions.
+
+These scopes can be included in a JWT token to restrict access.
+
+You can provide this token to a user or a third party to interact with your API under these restrictions.
+
+Advanced usage of JWT tokens often involves scopes, which define specific permissions or actions that the token holder is authorized to perform. Scopes allow more fine-grained control over what users or entities can do within your application.
+
+### Example of Using Scopes in JWT:
+
+1. **Define Scopes**: Scopes are typically added to the payload of the JWT token. For instance, a user might have the scope `read:posts` for viewing posts or `write:posts` for creating new posts.
+
+2. **Include Scopes in JWT**: When generating a token, include the relevant scopes in the payload. For example:
+
+ ```python
+ jwt_payload = {
+ "sub": "username:johndoe",
+ "scopes": ["read:posts", "write:posts"]
+ }
+ ```
+
+3. **Check Scopes During Authorization**: In your API, when processing requests, you can check if the JWT token includes the necessary scopes for the requested action.
+
+ Example of checking the `write:posts` scope:
+
+ ```python
+ def has_scope(required_scope: str, token_scopes: list) -> bool:
+ return required_scope in token_scopes
+
+ token_scopes = decoded_token.get("scopes", [])
+ if not has_scope("write:posts", token_scopes):
+ raise HTTPException(status_code=403, detail="Permission denied")
+ ```
+
+4. **Scope-Based Authorization**: You can use scopes to authorize access to specific resources. For example, only users with the `admin` scope might be allowed to delete posts, while users with `read:posts` can only view them.
+
+5. **Scope Granularity**: Scopes can be used to manage access on different levels, such as at the API, user, or resource level, giving you fine-grained control over who can do what within your application.
+
+By using scopes in JWT, you can enhance security and implement role-based access control (RBAC) or permission-based access control for more complex use cases.
+
+## Notes
+
+These step by step guides were inspired by **FastAPI** great work of providing simple and yet effective examples for everyone to understand.
+
+Esmerald adopts a different implementation internally but with the same purposes as any other framework to achieve that.
diff --git a/docs/en/docs/security/simple-oauth2.md b/docs/en/docs/security/simple-oauth2.md
new file mode 100644
index 00000000..b53bd30c
--- /dev/null
+++ b/docs/en/docs/security/simple-oauth2.md
@@ -0,0 +1,242 @@
+# OAuth2 with Password and Bearer
+
+Now, let's build upon the [previous chapter](./interaction.md) and add the missing parts to complete the security flow.
+
+The following examples were inspired by the same examples of FastAPI so it is normal if you feel familiar. The reson for
+that its to make sure you don't need to have a new learning curve in terms of understanding and flow.
+
+## The `username` and `password`
+
+We’re going to use **Esmerald** security utilities to handle the `username` and `password`.
+
+According to the OAuth2 specification, when using the "password flow" (which we are using), the client/user must send `username` and `password` fields as form data.
+
+The specification requires these fields to be named exactly as `username` and `password` so names like `user-name` or `email` won’t work in this case.
+
+However, don’t worry, you can display these fields however you like in the frontend, and your database models can use different names if needed.
+
+But for the login *path operation*, we need to follow these names to stay compliant with the specification (and to ensure compatibility with tools like the integrated API documentation).
+
+Additionally, the spec specifies that the `username` and `password` should be sent as form data, so **no JSON** here.
+
+### The `scope`
+
+The specification also allows the client to send another form field, `scope`.
+
+The field name must be `scope` (in singular), but it is actually a string containing "scopes" separated by spaces.
+
+Each "scope" is a single string without spaces, and they are typically used to define specific security permissions. For example:
+
+- `users:read` or `users:write` are common scopes.
+- `instagram_basic` is used by Facebook/Instagram.
+- `https://www.googleapis.com/auth/drive` is used by Google.
+
+These scopes help specify the level of access or permissions the user or client is requesting.
+
+!!! Info
+ In OAuth2, a "scope" is simply a string that declares a specific permission required.
+
+ It doesn't matter if the string includes other characters like `:` or if it's a URL.
+
+ These details are implementation-specific, but for OAuth2, scopes are just strings.
+
+## The operation to get the `username` and `password`
+
+Let us use the Esmerald built-ins to perform this operation.
+
+### OAuth2PasswordRequestForm
+
+First, import `OAuth2PasswordRequestForm`, and use it as a dependency with `Security`, `Inject` and `Injects` in the *path operation* for `/token`:
+
+```python hl_lines="5"
+{!> ../../../docs_src/security/post.py !}
+```
+
+!!! Note
+ The `Inject` and `Injects()` are what makes Esmerald dependency injection quite unique and layer based.
+
+The `OAuth2PasswordRequestForm` is a class dependency that defines a form body containing the following fields:
+
+- The `username`.
+- The `password`.
+- An optional `scope` field, which is a single string made up of multiple strings separated by spaces.
+- An optional `grant_type`.
+
+!!! Tip
+ According to the OAuth2 specification, the `grant_type` field is *required* and must have a fixed value of `password`. However, `OAuth2PasswordRequestForm` does not enforce this requirement.
+
+ If you need to strictly enforce the `grant_type` field, you can use `OAuth2PasswordRequestFormStrict` instead of `OAuth2PasswordRequestForm`.
+
+- An optional `client_id` (not needed for our example).
+- An optional `client_secret` (also not needed for our example).
+
+!!! Info
+ The `OAuth2PasswordRequestForm` is not a special class in **Esmerald**, unlike `OAuth2PasswordBearer`.
+
+ `OAuth2PasswordBearer` informs **Esmerald** that it represents a security scheme, which is why it gets added as such to the OpenAPI schema.
+
+ In contrast, `OAuth2PasswordRequestForm` is simply a convenience class dependency. You could have written it yourself or declared the `Form` parameters directly.
+
+ Since it's a common use case, **Esmerald** provides this class out of the box to make your work easier.
+
+## The form data
+
+!!! Tip
+ The instance of the `OAuth2PasswordRequestForm` dependency class won’t have a `scope` attribute containing the long string separated by spaces. Instead, it will have a `scopes` attribute, which is a list of individual strings representing each scope sent.
+
+ Although we’re not using `scopes` in this example, the functionality is available if you need it.
+
+Retrieve the user data from the (fake) database using the `username` from the form field.
+
+If no user is found, raise an `HTTPException` with the message: **"Incorrect username or password"**.
+
+```python hl_lines="4 79-81"
+{!> ../../../docs_src/security/post.py !}
+```
+
+### Checking the Password
+
+Now that we have the user data from our database, we need to verify the password.
+
+First, we will place the user data into the Pydantic `UserDB` model.
+
+Since storing plaintext passwords is unsafe, we'll use a (fake) password hashing system for verification.
+
+If the passwords don’t match, we'll return the same error as before.
+
+#### What is Password Hashing?
+
+Hashing transforms a value (like a password) into a seemingly random sequence of bytes (a string) that looks like gibberish.
+
+- Providing the same input (password) always produces the same hash.
+- However, it is a one-way process. You cannot reverse a hash back to the original password.
+
+##### Why Use Password Hashing?
+
+If your database is compromised, the attacker won't have access to the user's plaintext passwords—only the hashes.
+
+This protects users because the attacker cannot reuse their passwords on other systems (a common risk since many people reuse passwords).
+
+```python hl_lines="82-85"
+{!> ../../../docs_src/security/post.py !}
+```
+
+#### About the `**user_dict`
+
+`UserDB(**user_dict)` means:
+
+It takes the keys and values from the `user_dict` and passes them directly as key-value arguments to the `UserDB` constructor. This is equivalent to:
+
+```python
+UserDB(
+ username=user_dict["username"],
+ email=user_dict["email"],
+ full_name=user_dict["full_name"],
+ disabled=user_dict["disabled"],
+ hashed_password=user_dict["hashed_password"],
+)
+```
+
+## Returning the Token
+
+The response from the `token` endpoint should be a JSON object containing:
+
+- A `token_type`. Since we're using "Bearer" tokens, it should be set to `"bearer"`.
+- An `access_token`, which is a string containing the actual token.
+
+In this simplified example, we'll just return the `username` as the token (though this is insecure).
+
+!!! Tip
+ In the next chapter, we'll implement a secure version using password hashing and JSON Web Tokens (JWT).
+ But for now, let's focus on the key details.
+
+```python hl_lines="87"
+{!> ../../../docs_src/security/post.py !}
+```
+
+!!! Info
+ According to the spec, the response should include a JSON with an `access_token` and a `token_type`, as shown in this example.
+
+ This is something you must implement in your code, ensuring the correct use of these JSON keys.
+
+ It's almost the only part you need to manage manually to comply with the specifications. For everything else, **Esmerald** takes care of it for you.
+
+## Updating the Dependencies
+
+Now, let's update our dependencies.
+
+We want to retrieve the `current_user` **only** if the user is active. To do this, we will create a new dependency, `get_current_active_user`, which will rely on `get_current_user` as a sub-dependency.
+
+Both dependencies will raise an HTTP error if the user doesn't exist or if the user is inactive.
+
+With this update, the endpoint will only return a user if the user exists, is authenticated correctly, and is active.
+
+```python hl_lines="54-65"
+{!> ../../../docs_src/security/post.py !}
+```
+
+!!! Info
+ The additional `WWW-Authenticate` header with the value `Bearer` is part of the OAuth2 specification.
+
+ Any HTTP error with a status code 401 "UNAUTHORIZED" should include this header. For bearer tokens (like in our case), the header's value should be `Bearer`.
+
+ While you can technically omit this header and it will still function, including it ensures compliance with the specification. Additionally, some tools may expect and use this header, either now or in the future, which could be helpful for you or your users.
+
+ That's the advantage of following standards.
+
+## Go ahead and test it
+
+Open the OpenAPI documentation and check it out: [http://localhost:8000/docs/swagger](http://localhost:8000/docs/swagger).
+
+### Authenticate
+
+Click the **Authorize** button and use the following credentials:
+
+* **User**: `janedoe`
+* **Password**: `secret`.
+
+
+
+After pressing the authenticate, you should be able to see something like this:
+
+
+
+### Get the data
+
+Now it is time to test and get the data using the `GET` method provided in the examples `/users/me`.
+
+You will get a payload similar to this:
+
+```json
+{
+ "username": "johndoe",
+ "email": "johndoe@example.com",
+ "full_name": "John Doe",
+ "disabled": false,
+ "hashed_password": "fakehashedsecret"
+}
+```
+
+
+
+Now, if you logout by clicking in the logout icon, you should receive a 401.
+
+
+
+
+## Inactive users
+
+Now you can try with an inactive user and see what happens.
+
+* **User**: `peter`
+* **Password**: `secret2`.
+
+You should have an error like this:
+
+```json
+{
+ "detail": "Inactive user"
+}
+```
+
+As you can see, we have now implemented a simple and yet effective authentication.
diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml
index d5dbbcc1..fddf304c 100644
--- a/docs/en/mkdocs.yml
+++ b/docs/en/mkdocs.yml
@@ -6,240 +6,246 @@ theme:
custom_dir: ../en/overrides
language: en
palette:
- - scheme: default
- primary: green
- accent: orange
- media: '(prefers-color-scheme: light)'
- toggle:
- icon: material/lightbulb
- name: Switch to dark mode
- - scheme: slate
- media: '(prefers-color-scheme: dark)'
- primary: green
- accent: orange
- toggle:
- icon: material/lightbulb-outline
- name: Switch to light mode
+ - scheme: default
+ primary: green
+ accent: orange
+ media: "(prefers-color-scheme: light)"
+ toggle:
+ icon: material/lightbulb
+ name: Switch to dark mode
+ - scheme: slate
+ media: "(prefers-color-scheme: dark)"
+ primary: green
+ accent: orange
+ toggle:
+ icon: material/lightbulb-outline
+ name: Switch to light mode
favicon: statics/images/favicon.ico
logo: statics/images/logo-white.svg
features:
- - search.suggest
- - search.highlight
- - content.tabs.link
- - content.code.copy
- - content.code.annotate
- - content.tooltips
- - content.code.select
- - navigation.indexes
- - navigation.path
- - navigation.tabs
+ - search.suggest
+ - search.highlight
+ - content.tabs.link
+ - content.code.copy
+ - content.code.annotate
+ - content.tooltips
+ - content.code.select
+ - navigation.indexes
+ - navigation.path
+ - navigation.tabs
repo_name: dymmond/esmerald
repo_url: https://github.com/dymmond/esmerald
-edit_uri: ''
+edit_uri: ""
plugins:
-- search
-- meta-descriptions:
- export_csv: false
- quiet: false
- enable_checks: false
- min_length: 50
- max_length: 160
- trim: false
-- mkdocstrings:
- handlers:
- python:
- options:
- extensions:
- - griffe_typingdoc
- show_root_heading: true
- show_if_no_docstring: true
- preload_modules:
- - httpx
- - lilya
- - a2wsgi
- inherited_members: true
- members_order: source
- separate_signature: true
- unwrap_annotated: true
- filters:
- - '!^_'
- merge_init_into_class: true
- docstring_section_style: spacy
- signature_crossrefs: true
- show_symbol_type_heading: true
- show_symbol_type_toc: true
+ - search
+ - meta-descriptions:
+ export_csv: false
+ quiet: false
+ enable_checks: false
+ min_length: 50
+ max_length: 160
+ trim: false
+ - mkdocstrings:
+ handlers:
+ python:
+ options:
+ extensions:
+ - griffe_typingdoc
+ show_root_heading: true
+ show_if_no_docstring: true
+ preload_modules:
+ - httpx
+ - lilya
+ - a2wsgi
+ inherited_members: true
+ members_order: source
+ separate_signature: true
+ unwrap_annotated: true
+ filters:
+ - "!^_"
+ merge_init_into_class: true
+ docstring_section_style: spacy
+ signature_crossrefs: true
+ show_symbol_type_heading: true
+ show_symbol_type_toc: true
markdown_extensions:
-- attr_list
-- toc:
- permalink: true
-- mdx_include:
- base_path: docs
-- admonition
-- extra
-- pymdownx.superfences:
- custom_fences:
- - name: mermaid
- class: mermaid
- format: !!python/name:pymdownx.superfences.fence_code_format ''
-- pymdownx.tabbed:
- alternate_style: true
-- md_in_html
+ - attr_list
+ - toc:
+ permalink: true
+ - mdx_include:
+ base_path: docs
+ - admonition
+ - extra
+ - pymdownx.superfences:
+ custom_fences:
+ - name: mermaid
+ class: mermaid
+ format: !!python/name:pymdownx.superfences.fence_code_format ""
+ - pymdownx.tabbed:
+ alternate_style: true
+ - md_in_html
nav:
-- index.md
-- Application:
- - application/index.md
- - Esmerald: application/applications.md
- - application/levels.md
- - application/settings.md
- - Configurations:
- - configurations/index.md
- - configurations/cors.md
- - configurations/csrf.md
- - configurations/session.md
- - configurations/staticfiles.md
- - configurations/template.md
- - configurations/jwt.md
- - configurations/scheduler.md
- - configurations/openapi/config.md
-- Features:
- - features/index.md
- - Routing:
- - routing/index.md
- - routing/router.md
- - routing/routes.md
- - routing/handlers.md
- - routing/apiview.md
- - routing/webhooks.md
- - interceptors.md
- - permissions.md
- - middleware/middleware.md
- - dependencies.md
- - exceptions.md
- - exception-handlers.md
- - extensions.md
- - password-hashers.md
- - requests.md
- - context.md
- - responses.md
- - encoders.md
- - msgspec.md
- - background-tasks.md
- - lifespan-events.md
- - protocols.md
- - Advanced & Useful:
- - extras/index.md
- - extras/path-params.md
- - extras/query-params.md
- - extras/request-data.md
- - extras/upload-files.md
- - extras/forms.md
- - extras/body-fields.md
- - extras/header-fields.md
- - extras/cookie-fields.md
- - Scheduler:
- - scheduler/index.md
- - Asyncz:
- - scheduler/scheduler.md
- - scheduler/handler.md
- - Management & Directives:
- - directives/index.md
- - directives/discovery.md
- - directives/directives.md
- - directives/custom-directives.md
- - directives/shell.md
-- Database Integrations:
- - databases/index.md
- - Saffier:
- - databases/saffier/motivation.md
- - databases/saffier/models.md
- - Middleware:
- - databases/saffier/middleware.md
- - databases/saffier/example.md
- - Edgy:
- - databases/edgy/motivation.md
- - databases/edgy/models.md
- - Middleware:
- - databases/edgy/middleware.md
- - databases/edgy/example.md
- - Mongoz:
- - databases/mongoz/motivation.md
- - databases/mongoz/documents.md
- - Middleware:
- - databases/mongoz/middleware.md
- - databases/mongoz/example.md
-- openapi.md
-- Extras:
- - wsgi.md
- - testclient.md
- - Deployment:
- - deployment/index.md
- - Intro: deployment/intro.md
- - Using docker: deployment/docker.md
- - external.md
-- API Reference:
- - references/index.md
- - references/esmerald.md
- - references/application/settings.md
- - references/configurations/cors.md
- - references/configurations/csrf.md
- - references/configurations/session.md
- - references/configurations/static_files.md
- - references/configurations/template.md
- - references/configurations/jwt.md
- - references/configurations/openapi.md
- - references/background.md
- - references/routing/router.md
- - references/routing/gateway.md
- - references/routing/websocketgateway.md
- - references/routing/webhookgateway.md
- - references/routing/include.md
- - references/routing/view.md
- - references/routing/handlers.md
- - references/interceptors.md
- - references/permissions.md
- - references/middleware/baseauth.md
- - references/middleware/middlewares.md
- - references/extensions.md
- - references/pluggables.md
- - references/exceptions.md
- - references/request.md
- - references/context.md
- - references/responses/response.md
- - references/responses/json-response.md
- - references/responses/template-response.md
- - references/responses/orjson-response.md
- - references/responses/ujson-response.md
- - references/responses/json.md
- - references/responses/file.md
- - references/responses/redirect.md
- - references/responses/stream.md
- - references/responses/template.md
- - references/responses/orjson.md
- - references/responses/ujson.md
- - references/responses/openapi-response.md
- - references/websockets.md
- - references/injector.md
- - references/uploadfile.md
- - references/status-codes.md
- - references/test-client.md
-- About:
- - about.md
- - sponsorship.md
- - esmerald-people.md
- - examples.md
- - contributing.md
-- release-notes.md
+ - index.md
+ - Application:
+ - application/index.md
+ - Esmerald: application/applications.md
+ - application/levels.md
+ - application/settings.md
+ - Configurations:
+ - configurations/index.md
+ - configurations/cors.md
+ - configurations/csrf.md
+ - configurations/session.md
+ - configurations/staticfiles.md
+ - configurations/template.md
+ - configurations/jwt.md
+ - configurations/scheduler.md
+ - configurations/openapi/config.md
+ - Features:
+ - features/index.md
+ - Routing:
+ - routing/index.md
+ - routing/router.md
+ - routing/routes.md
+ - routing/handlers.md
+ - routing/apiview.md
+ - routing/webhooks.md
+ - interceptors.md
+ - permissions.md
+ - middleware/middleware.md
+ - dependencies.md
+ - exceptions.md
+ - exception-handlers.md
+ - extensions.md
+ - password-hashers.md
+ - requests.md
+ - context.md
+ - responses.md
+ - encoders.md
+ - msgspec.md
+ - background-tasks.md
+ - lifespan-events.md
+ - protocols.md
+ - Security:
+ - security/index.md
+ - security/introduction.md
+ - security/interaction.md
+ - security/simple-oauth2.md
+ - security/oauth-jwt.md
+ - Advanced & Useful:
+ - extras/index.md
+ - extras/path-params.md
+ - extras/query-params.md
+ - extras/request-data.md
+ - extras/upload-files.md
+ - extras/forms.md
+ - extras/body-fields.md
+ - extras/header-fields.md
+ - extras/cookie-fields.md
+ - Scheduler:
+ - scheduler/index.md
+ - Asyncz:
+ - scheduler/scheduler.md
+ - scheduler/handler.md
+ - Management & Directives:
+ - directives/index.md
+ - directives/discovery.md
+ - directives/directives.md
+ - directives/custom-directives.md
+ - directives/shell.md
+ - Database Integrations:
+ - databases/index.md
+ - Saffier:
+ - databases/saffier/motivation.md
+ - databases/saffier/models.md
+ - Middleware:
+ - databases/saffier/middleware.md
+ - databases/saffier/example.md
+ - Edgy:
+ - databases/edgy/motivation.md
+ - databases/edgy/models.md
+ - Middleware:
+ - databases/edgy/middleware.md
+ - databases/edgy/example.md
+ - Mongoz:
+ - databases/mongoz/motivation.md
+ - databases/mongoz/documents.md
+ - Middleware:
+ - databases/mongoz/middleware.md
+ - databases/mongoz/example.md
+ - openapi.md
+ - Extras:
+ - wsgi.md
+ - testclient.md
+ - Deployment:
+ - deployment/index.md
+ - Intro: deployment/intro.md
+ - Using docker: deployment/docker.md
+ - external.md
+ - API Reference:
+ - references/index.md
+ - references/esmerald.md
+ - references/application/settings.md
+ - references/configurations/cors.md
+ - references/configurations/csrf.md
+ - references/configurations/session.md
+ - references/configurations/static_files.md
+ - references/configurations/template.md
+ - references/configurations/jwt.md
+ - references/configurations/openapi.md
+ - references/background.md
+ - references/routing/router.md
+ - references/routing/gateway.md
+ - references/routing/websocketgateway.md
+ - references/routing/webhookgateway.md
+ - references/routing/include.md
+ - references/routing/view.md
+ - references/routing/handlers.md
+ - references/interceptors.md
+ - references/permissions.md
+ - references/middleware/baseauth.md
+ - references/middleware/middlewares.md
+ - references/extensions.md
+ - references/pluggables.md
+ - references/exceptions.md
+ - references/request.md
+ - references/context.md
+ - references/responses/response.md
+ - references/responses/json-response.md
+ - references/responses/template-response.md
+ - references/responses/orjson-response.md
+ - references/responses/ujson-response.md
+ - references/responses/json.md
+ - references/responses/file.md
+ - references/responses/redirect.md
+ - references/responses/stream.md
+ - references/responses/template.md
+ - references/responses/orjson.md
+ - references/responses/ujson.md
+ - references/responses/openapi-response.md
+ - references/websockets.md
+ - references/injector.md
+ - references/uploadfile.md
+ - references/status-codes.md
+ - references/test-client.md
+ - About:
+ - about.md
+ - sponsorship.md
+ - esmerald-people.md
+ - examples.md
+ - contributing.md
+ - release-notes.md
extra_css:
-- statics/css/extra.css
-- statics/css/custom.css
+ - statics/css/extra.css
+ - statics/css/custom.css
extra:
analytics:
provider: google
property: G-CNBVBB90NT
alternate:
- - link: /
- name: en - English
- - link: /ru/
- name: ru - русский язык
+ - link: /
+ name: en - English
+ - link: /ru/
+ name: ru - русский язык
hooks:
-- ../../scripts/hooks.py
+ - ../../scripts/hooks.py
diff --git a/docs_src/security/app.py b/docs_src/security/app.py
new file mode 100644
index 00000000..ed3c1628
--- /dev/null
+++ b/docs_src/security/app.py
@@ -0,0 +1,18 @@
+from typing import Any, Dict
+
+from esmerald import Inject, Injects, Esmerald, get, Gateway
+from esmerald.security.oauth2 import OAuth2PasswordBearer
+
+oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
+
+
+@get("/items", dependencies={"token": Inject(oauth2_scheme)}, security=[oauth2_scheme])
+async def get_items(token: str = Injects()) -> Dict[str, Any]:
+ return {"token": token}
+
+
+app = Esmerald(
+ routes=[
+ Gateway(handler=get_items),
+ ]
+)
diff --git a/docs_src/security/enhance.py b/docs_src/security/enhance.py
new file mode 100644
index 00000000..7ae5f97c
--- /dev/null
+++ b/docs_src/security/enhance.py
@@ -0,0 +1,28 @@
+from esmerald import Inject, Injects, get, Security
+from esmerald.security.oauth2 import OAuth2PasswordBearer
+from pydantic import BaseModel
+
+oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
+
+
+class User(BaseModel):
+ username: str
+ email: str | None = None
+
+
+def fake_decode_token(token):
+ return User(username=token + "fakedecoded", email="john@example.com")
+
+
+async def get_current_user(token: str = Security(oauth2_scheme)):
+ user = fake_decode_token(token)
+ return user
+
+
+@get(
+ "/users/me",
+ dependencies={"current_user": Inject(get_current_user)},
+ security=[oauth2_scheme],
+)
+async def users_me(current_user: User = Injects()) -> User:
+ return current_user
diff --git a/docs_src/security/hash/app.py b/docs_src/security/hash/app.py
new file mode 100644
index 00000000..b7cc6a4e
--- /dev/null
+++ b/docs_src/security/hash/app.py
@@ -0,0 +1,157 @@
+from datetime import datetime, timedelta, timezone
+from typing import Dict, List
+
+import jwt
+from jwt.exceptions import InvalidTokenError
+from passlib.context import CryptContext
+from pydantic import BaseModel
+
+from esmerald import (
+ Esmerald,
+ Gateway,
+ HTTPException,
+ Inject,
+ Injects,
+ Security,
+ get,
+ post,
+ status,
+)
+from esmerald.params import Form
+from esmerald.security.oauth2 import OAuth2PasswordBearer, OAuth2PasswordRequestForm
+
+
+SECRET_KEY = "adec4de83525abdd446b258d0df8a3cc151ee65e95ae8b8ccf51b643df71afcf"
+ALGORITHM = "HS256"
+ACCESS_TOKEN_EXPIRE_MINUTES = 30
+
+# Pasword context
+password_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
+
+oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/token")
+
+fake_users_db = {
+ "janedoe": {
+ "username": "janedoe",
+ "full_name": "Jane Doe",
+ "email": "janedoe@example.com",
+ "hashed_password": "$2a$12$KplebFTPwFcgGQosJgI4De0PyB2AoRCSxasxHpFoYZPp6uQV/xLzm",
+ "disabled": False,
+ }
+}
+
+
+class Token(BaseModel):
+ access_token: str
+ token_type: str
+
+
+class TokenData(BaseModel):
+ username: str | None = None
+
+
+class User(BaseModel):
+ username: str
+ email: str | None = None
+ full_name: str | None = None
+ disabled: bool | None = None
+
+
+class UserDB(User):
+ hashed_password: str
+
+
+def verify_password(plain_password: str, hashed_password: str) -> bool:
+ return password_context.verify(plain_password, hashed_password)
+
+
+def get_password_hash(password: str) -> str:
+ return password_context.hash(password)
+
+
+def get_user(db: Dict[str, Dict[str, str]], username: str) -> User | None:
+ user_dict = db.get(username)
+ return User(**user_dict) if user_dict else None
+
+
+def authenticate_user(fake_db, username: str, password: str) -> UserDB | None:
+ user = get_user(fake_db, username)
+ if user and verify_password(password, user.hashed_password):
+ return user
+ return None
+
+
+def create_access_token(data: dict, expires_delta: timedelta | None = None) -> str:
+ to_encode = data.copy()
+ expire = datetime.now(timezone.utc) + (expires_delta or timedelta(minutes=15))
+ to_encode.update({"exp": expire})
+ return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
+
+
+async def get_current_user(token: str = Security(oauth2_scheme)) -> User:
+ credentials_exception = HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Could not validate credentials",
+ headers={"WWW-Authenticate": "Bearer"},
+ )
+ try:
+ payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
+ username: str = payload.get("sub")
+ if not username:
+ raise credentials_exception
+ except (InvalidTokenError, KeyError):
+ raise credentials_exception
+
+ user = get_user(fake_users_db, username)
+ if not user or user.disabled:
+ raise HTTPException(status_code=400, detail="Inactive user")
+ return user
+
+
+@post(
+ "/token",
+ dependencies={"form_data": Inject(OAuth2PasswordRequestForm)},
+ security=[oauth2_scheme],
+)
+async def login(form_data: OAuth2PasswordRequestForm = Form()) -> Dict[str, str]:
+ user = authenticate_user(fake_users_db, form_data.username, form_data.password)
+ if not user:
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Incorrect username or password",
+ headers={"WWW-Authenticate": "Bearer"},
+ )
+ access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
+ access_token = create_access_token(
+ data={"sub": user.username}, expires_delta=access_token_expires
+ )
+ return Token(access_token=access_token, token_type="bearer")
+
+
+@get(
+ "/users/me",
+ dependencies={"current_user": Inject(get_current_user)},
+ security=[oauth2_scheme],
+)
+async def me(
+ current_user: User = Injects(),
+) -> User:
+ return current_user
+
+
+@get(
+ "/users/me/items",
+ dependencies={"current_user": Inject(get_current_user)},
+ security=[oauth2_scheme],
+)
+async def get_user_items(current_user: User = Injects()) -> List[Dict[str, str]]:
+ return [{"item_id": "Foo", "owner": current_user.username}]
+
+
+app = Esmerald(
+ routes=[
+ Gateway(handler=login),
+ Gateway(handler=me),
+ Gateway(handler=get_user_items),
+ ],
+)
diff --git a/docs_src/security/post.py b/docs_src/security/post.py
new file mode 100644
index 00000000..ea54d930
--- /dev/null
+++ b/docs_src/security/post.py
@@ -0,0 +1,98 @@
+from typing import Dict
+from pydantic import BaseModel
+
+from esmerald import Esmerald, Gateway, HTTPException, Inject, Injects, Security, get, post, status
+from esmerald.security.oauth2 import OAuth2PasswordBearer, OAuth2PasswordRequestForm
+
+
+oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
+
+fake_users_db = {
+ "janedoe": {
+ "username": "janedoe",
+ "full_name": "Jane Doe",
+ "email": "janedoe@example.com",
+ "hashed_password": "fakehashedsecret",
+ "disabled": False,
+ },
+ "peter": {
+ "username": "peter",
+ "full_name": "Peter Parker",
+ "email": "pparker@example.com",
+ "hashed_password": "fakehashedsecret2",
+ "disabled": True,
+ },
+}
+
+
+def fake_hash_password(password: str):
+ return "fakehashed" + password
+
+
+class User(BaseModel):
+ username: str
+ email: str | None = None
+ full_name: str | None = None
+ disabled: bool | None = None
+
+
+class UserDB(User):
+ hashed_password: str
+
+
+def get_user(db, username: str):
+ if username in db:
+ user_dict = db[username]
+ return UserDB(**user_dict)
+
+
+def fake_decode_token(token: str):
+ user = get_user(fake_users_db, token)
+ return user
+
+
+async def get_current_user(token: str = Security(oauth2_scheme)):
+ user = fake_decode_token(token)
+ if user.disabled:
+ raise HTTPException(status_code=400, detail="Inactive user")
+
+ if not user:
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Invalid authentication credentials",
+ headers={"WWW-Authenticate": "Bearer"},
+ )
+ return user
+
+
+@post(
+ "/token",
+ dependencies={"form_data": Inject(OAuth2PasswordRequestForm)},
+ security=[oauth2_scheme],
+)
+async def login(form_data: OAuth2PasswordRequestForm = Injects()) -> Dict[str, str]:
+ user_dict = fake_users_db.get(form_data.username)
+ if not user_dict:
+ raise HTTPException(status_code=400, detail="Incorrect username or password")
+ user = UserDB(**user_dict)
+ hashed_password = fake_hash_password(form_data.password)
+ if not hashed_password == user.hashed_password:
+ raise HTTPException(status_code=400, detail="Incorrect username or password")
+
+ return {"access_token": user.username, "token_type": "bearer"}
+
+
+@get("/users/me", dependencies={"current_user": Inject(get_current_user)})
+async def read_users_me(
+ current_user: User = Injects(),
+) -> User:
+ return current_user
+
+
+app = Esmerald(
+ routes=[
+ Gateway(handler=login),
+ Gateway(handler=read_users_me),
+ ],
+ debug=True,
+)
diff --git a/esmerald/__init__.py b/esmerald/__init__.py
index f2fbdf54..1a6806f2 100644
--- a/esmerald/__init__.py
+++ b/esmerald/__init__.py
@@ -23,7 +23,7 @@
ValidationErrorException,
)
from .interceptors.interceptor import EsmeraldInterceptor
-from .param_functions import DirectInjects
+from .param_functions import Security
from .params import Body, Cookie, File, Form, Header, Injects, Param, Path, Query
from .permissions import AllowAny, BasePermission, DenyAll
from .pluggables import Extension, Pluggable
@@ -62,7 +62,6 @@
"Cookie",
"DaoProtocol",
"DenyAll",
- "DirectInjects",
"Esmerald",
"EsmeraldAPISettings",
"EsmeraldInterceptor",
@@ -93,6 +92,7 @@
"Request",
"Response",
"Router",
+ "Security",
"ServiceUnavailable",
"SessionConfig",
"SimpleAPIView",
diff --git a/esmerald/injector.py b/esmerald/injector.py
index f0dd12db..0ca379a9 100644
--- a/esmerald/injector.py
+++ b/esmerald/injector.py
@@ -81,3 +81,13 @@ def __eq__(self, other: Any) -> bool:
and other.use_cache == self.use_cache
and other.value == self.value
)
+
+ def __hash__(self) -> int:
+ values: Dict[str, Any] = {}
+ for key, value in self.__dict__.items():
+ values[key] = None
+ if isinstance(value, (list, set)):
+ values[key] = tuple(value)
+ else:
+ values[key] = value
+ return hash((type(self),) + tuple(values))
diff --git a/esmerald/openapi/enums.py b/esmerald/openapi/enums.py
index 2e88e887..e4498b51 100644
--- a/esmerald/openapi/enums.py
+++ b/esmerald/openapi/enums.py
@@ -16,12 +16,24 @@ class SecuritySchemeType(BaseEnum):
mutualTLS = "mutualTLS"
openIdConnect = "openIdConnect"
+ def __str__(self) -> str:
+ return self.value
+
+ def __repr__(self) -> str:
+ return str(self)
+
class APIKeyIn(BaseEnum):
query = "query"
header = "header"
cookie = "cookie"
+ def __str__(self) -> str:
+ return self.value
+
+ def __repr__(self) -> str:
+ return str(self)
+
class Header(BaseEnum):
authorization = "Authorization"
diff --git a/esmerald/openapi/models.py b/esmerald/openapi/models.py
index 1cb1e276..494b392f 100644
--- a/esmerald/openapi/models.py
+++ b/esmerald/openapi/models.py
@@ -81,6 +81,7 @@ class SecurityBase(BaseModel):
type: SecuritySchemeType = Field(alias="type")
description: Optional[str] = None
+ scheme_name: Optional[str] = None
SecuritySchemeUnion = Union[APIKey, HTTPBase, OAuth2, OpenIdConnect, HTTPBearer]
@@ -93,7 +94,7 @@ class Components(BaseModel):
examples: Optional[Dict[str, Union[Example, Reference]]] = None
requestBodies: Optional[Dict[str, Union[RequestBody, Reference]]] = None
headers: Optional[Dict[str, Union[Header, Reference]]] = None
- securitySchemes: Optional[Dict[str, Union[SecurityScheme, Reference]]] = None
+ securitySchemes: Optional[Dict[str, Union[SecurityScheme, Reference, Dict[str, Any]]]] = None
links: Optional[Dict[str, Union[Link, Reference]]] = None
callbacks: Optional[Dict[str, Union[Dict[str, PathItem], Reference, Any]]] = None
pathItems: Optional[Dict[str, Union[PathItem, Reference]]] = None
diff --git a/esmerald/openapi/openapi.py b/esmerald/openapi/openapi.py
index febdf896..489720fb 100644
--- a/esmerald/openapi/openapi.py
+++ b/esmerald/openapi/openapi.py
@@ -49,8 +49,12 @@
)
from esmerald.params import Param
from esmerald.routing import gateways, router
-from esmerald.routing._internal import convert_annotation_to_pydantic_model
+from esmerald.routing._internal import (
+ convert_annotation_to_pydantic_model,
+)
+from esmerald.security.oauth2.oauth import SecurityBase
from esmerald.typing import Undefined
+from esmerald.utils.dependencies import is_security_scheme
from esmerald.utils.helpers import is_class_and_subclass, is_union
ADDITIONAL_TYPES = ["bool", "list", "dict"]
@@ -66,11 +70,15 @@ def get_flat_params(route: Union[router.HTTPHandler, Any], body_fields: List[str
cookie_params = [param.field_info for param in route.transformer.get_cookie_params()]
header_params = [param.field_info for param in route.transformer.get_header_params()]
+ handler_dependencies = set(route.get_dependencies().keys())
handler_query_params = [
param
for param in route.transformer.get_query_params()
if param.field_alias not in route.body_encoder_fields.keys()
+ and not param.is_security
+ and param.field_alias not in handler_dependencies
]
+
query_params = []
for param in handler_query_params:
is_union_or_optional = is_union(param.field_info.annotation)
@@ -78,19 +86,25 @@ def get_flat_params(route: Union[router.HTTPHandler, Any], body_fields: List[str
if param.field_info.alias in body_fields:
continue
+ if param.is_security:
+ continue
+
# Making sure all the optional and union types are included
if is_union_or_optional:
- # field_info = should_skip_json_schema(param.field_info)
- query_params.append(param.field_info)
+ if not is_security_scheme(param.field_info.default):
+ query_params.append(param.field_info)
else:
- if isinstance(param.field_info.annotation, _GenericAlias):
+ if isinstance(param.field_info.annotation, _GenericAlias) and not is_security_scheme(
+ param.field_info.default
+ ):
query_params.append(param.field_info)
elif (
param.field_info.annotation.__class__.__name__ in TRANSFORMER_TYPES_KEYS
or param.field_info.annotation.__name__ in TRANSFORMER_TYPES_KEYS
):
- query_params.append(param.field_info)
+ if not is_security_scheme(param.field_info.default):
+ query_params.append(param.field_info)
return path_params + query_params + cookie_params + header_params
@@ -106,15 +120,15 @@ def get_openapi_security_schemes(schemes: Any) -> Tuple[dict, list]:
if inspect.isclass(security_requirement):
security_requirement = security_requirement()
- if not isinstance(security_requirement, SecurityScheme):
+ if not isinstance(security_requirement, (SecurityScheme, SecurityBase)):
raise ValueError(
"Security schemes must subclass from `esmerald.openapi.models.SecurityScheme`"
)
+
security_definition = security_requirement.model_dump(by_alias=True, exclude_none=True)
security_name = security_requirement.scheme_name
security_definitions[security_name] = security_definition
operation_security.append({security_name: security_requirement})
-
return security_definitions, operation_security
diff --git a/esmerald/openapi/schemas/v3_1_0/security_scheme.py b/esmerald/openapi/schemas/v3_1_0/security_scheme.py
index 007c99a6..8939ca0c 100644
--- a/esmerald/openapi/schemas/v3_1_0/security_scheme.py
+++ b/esmerald/openapi/schemas/v3_1_0/security_scheme.py
@@ -71,6 +71,11 @@ class SecurityScheme(BaseModel):
**REQUIRED** for `openIdConnect`. OpenId Connect URL to discover OAuth2 configuration values.
This MUST be in the form of a URL. The OpenID Connect standard requires the use of TLS.
"""
+ model: Optional[BaseModel] = None
+ """
+ An optional model to be used for the security scheme.
+ """
+
model_config = ConfigDict(
extra="ignore",
populate_by_name=True,
@@ -95,7 +100,7 @@ class SecurityScheme(BaseModel):
{
"type": "openIdConnect",
"openIdConnectUrl": "openIdConnect",
- }, # issue #5: allow relative path
+ },
]
},
)
diff --git a/esmerald/openapi/security/api_key/base.py b/esmerald/openapi/security/api_key/base.py
index cdfecdab..c6f8e183 100644
--- a/esmerald/openapi/security/api_key/base.py
+++ b/esmerald/openapi/security/api_key/base.py
@@ -1,73 +1,14 @@
-from typing import Any, Literal, Optional
+from esmerald.security.api_key import (
+ APIKeyInCookie as BaseAPIKeyInCookie,
+ APIKeyInHeader as BaseAPIKeyInHeader,
+ APIKeyInQuery as BaseAPIKeyInQuery,
+)
-from esmerald.openapi.enums import APIKeyIn, SecuritySchemeType
-from esmerald.openapi.security.base import HTTPBase
+class APIKeyInQuery(BaseAPIKeyInQuery): ...
-class APIKeyInQuery(HTTPBase):
- def __init__(
- self,
- *,
- type_: Literal[
- "apiKey", "http", "mutualTLS", "oauth2", "openIdConnect"
- ] = SecuritySchemeType.apiKey.value,
- scheme_name: Optional[str] = None,
- description: Optional[str] = None,
- in_: Optional[Literal["query", "header", "cookie"]] = APIKeyIn.query.value,
- name: Optional[str] = None,
- **kwargs: Any,
- ):
- super().__init__(
- type_=type_,
- description=description,
- name=name,
- in_=in_,
- scheme_name=scheme_name or self.__class__.__name__,
- **kwargs,
- )
+class APIKeyInHeader(BaseAPIKeyInHeader): ...
-class APIKeyInHeader(HTTPBase):
- def __init__(
- self,
- *,
- type_: Literal[
- "apiKey", "http", "mutualTLS", "oauth2", "openIdConnect"
- ] = SecuritySchemeType.apiKey.value,
- scheme_name: Optional[str] = None,
- description: Optional[str] = None,
- in_: Optional[Literal["query", "header", "cookie"]] = APIKeyIn.header.value,
- name: Optional[str] = None,
- **kwargs: Any,
- ):
- super().__init__(
- type_=type_,
- description=description,
- name=name,
- in_=in_,
- scheme_name=scheme_name or self.__class__.__name__,
- **kwargs,
- )
-
-class APIKeyInCookie(HTTPBase):
- def __init__(
- self,
- *,
- type_: Literal[
- "apiKey", "http", "mutualTLS", "oauth2", "openIdConnect"
- ] = SecuritySchemeType.apiKey.value,
- scheme_name: Optional[str] = None,
- description: Optional[str] = None,
- in_: Optional[Literal["query", "header", "cookie"]] = APIKeyIn.cookie.value,
- name: Optional[str] = None,
- **kwargs: Any,
- ):
- super().__init__(
- type_=type_,
- description=description,
- name=name,
- in_=in_,
- scheme_name=scheme_name or self.__class__.__name__,
- **kwargs,
- )
+class APIKeyInCookie(BaseAPIKeyInCookie): ...
diff --git a/esmerald/openapi/security/base.py b/esmerald/openapi/security/base.py
index 3007aac2..4976db43 100644
--- a/esmerald/openapi/security/base.py
+++ b/esmerald/openapi/security/base.py
@@ -1,38 +1,4 @@
-from typing import Any, Literal, Optional, Union
+from esmerald.security.http import HTTPBase as BaseHTTP
-from pydantic import AnyUrl, ConfigDict
-from esmerald.openapi.models import SecurityScheme
-
-
-class HTTPBase(SecurityScheme):
- """
- Base for all HTTP security headers.
- """
-
- model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
-
- def __init__(
- self,
- *,
- type_: Optional[Literal["apiKey", "http", "mutualTLS", "oauth2", "openIdConnect"]] = None,
- bearerFormat: Optional[str] = None,
- scheme_name: Optional[str] = None,
- description: Optional[str] = None,
- in_: Optional[Literal["query", "header", "cookie"]] = None,
- name: Optional[str] = None,
- scheme: Optional[str] = None,
- openIdConnectUrl: Optional[Union[AnyUrl, str]] = None,
- **kwargs: Any,
- ):
- super().__init__( # type: ignore
- type=type_,
- bearerFormat=bearerFormat,
- description=description,
- name=name,
- security_scheme_in=in_,
- scheme_name=scheme_name,
- scheme=scheme,
- openIdConnectUrl=openIdConnectUrl,
- **kwargs,
- )
+class HTTPBase(BaseHTTP): ...
diff --git a/esmerald/openapi/security/http/base.py b/esmerald/openapi/security/http/base.py
index 69de655a..c15b8fa3 100644
--- a/esmerald/openapi/security/http/base.py
+++ b/esmerald/openapi/security/http/base.py
@@ -1,85 +1,10 @@
-from typing import Any, Literal, Optional
+from esmerald.security.http import HTTPBase
-from esmerald.openapi.enums import APIKeyIn, Header, SecuritySchemeType
-from esmerald.openapi.security.base import HTTPBase
+class Basic(HTTPBase): ...
-class Basic(HTTPBase):
- def __init__(
- self,
- *,
- type_: Literal[
- "apiKey", "http", "mutualTLS", "oauth2", "openIdConnect"
- ] = SecuritySchemeType.http.value,
- bearerFormat: Optional[str] = None,
- scheme_name: Optional[str] = None,
- description: Optional[str] = None,
- in_: Optional[Literal["query", "header", "cookie"]] = APIKeyIn.header.value,
- name: Optional[str] = None,
- scheme: Optional[str] = None,
- **kwargs: Any,
- ):
- super().__init__(
- type_=type_,
- bearerFormat=bearerFormat,
- description=description,
- name=name or "Basic",
- in_=in_,
- scheme=scheme or "basic",
- scheme_name=scheme_name or self.__class__.__name__,
- **kwargs,
- )
+class Bearer(HTTPBase): ...
-class Bearer(HTTPBase):
- def __init__(
- self,
- *,
- type_: Literal[
- "apiKey", "http", "mutualTLS", "oauth2", "openIdConnect"
- ] = SecuritySchemeType.http.value,
- bearerFormat: Optional[str] = None,
- scheme_name: Optional[str] = None,
- description: Optional[str] = None,
- in_: Optional[Literal["query", "header", "cookie"]] = APIKeyIn.header.value,
- name: Optional[str] = None,
- scheme: Optional[str] = None,
- **kwargs: Any,
- ):
- super().__init__(
- type_=type_,
- bearerFormat=bearerFormat,
- description=description,
- name=name or Header.authorization,
- in_=in_,
- scheme=scheme or "bearer",
- scheme_name=scheme_name or self.__class__.__name__,
- **kwargs,
- )
-
-class Digest(HTTPBase):
- def __init__(
- self,
- *,
- type_: Literal[
- "apiKey", "http", "mutualTLS", "oauth2", "openIdConnect"
- ] = SecuritySchemeType.http.value,
- bearerFormat: Optional[str] = None,
- scheme_name: Optional[str] = None,
- description: Optional[str] = None,
- in_: Optional[Literal["query", "header", "cookie"]] = APIKeyIn.header.value,
- name: Optional[str] = None,
- scheme: Optional[str] = None,
- **kwargs: Any,
- ):
- super().__init__(
- type_=type_,
- bearerFormat=bearerFormat,
- description=description,
- name=name or Header.authorization,
- in_=in_,
- scheme=scheme or "digest",
- scheme_name=scheme_name or self.__class__.__name__,
- **kwargs,
- )
+class Digest(HTTPBase): ...
diff --git a/esmerald/openapi/security/oauth2/base.py b/esmerald/openapi/security/oauth2/base.py
index ad4ccaf3..6761c759 100644
--- a/esmerald/openapi/security/oauth2/base.py
+++ b/esmerald/openapi/security/oauth2/base.py
@@ -1,49 +1,4 @@
-from typing import Any, Dict, Literal, Optional, Union
+from esmerald.security.oauth2.oauth import OAuth2 as BaseOAuth2
-from esmerald.openapi.enums import SecuritySchemeType
-from esmerald.openapi.models import OAuthFlows
-from esmerald.openapi.security.base import HTTPBase
-
-class OAuth2(HTTPBase):
- """
- The OAuth2 scheme.
-
- For every parameter of the OAuthFlows, expects a OAuthFlow object type.
-
- Example:
- implicit: Optional[OAuthFlow] = OAuthFlow()
- password: Optional[OAuthFlow] = OAuthFlow()
- clientCredentials: Optional[OAuthFlow] = OAuthFlow()
- authorizationCode: Optional[OAuthFlow] = OAuthFlow()
-
- flows: OAuthFlows(
- implicit=implicit,
- password=password,
- clientCredentials=clientCredentials,
- authorizationCode=authorizationCode,
- )
- """
-
- def __init__(
- self,
- *,
- type_: Literal[
- "apiKey", "http", "mutualTLS", "oauth2", "openIdConnect"
- ] = SecuritySchemeType.oauth2.value,
- scheme_name: Optional[str] = None,
- description: Optional[str] = None,
- name: Optional[str] = None,
- flows: Union[OAuthFlows, Dict[str, Dict[str, Any]]] = OAuthFlows(),
- **kwargs: Any,
- ):
- extra: Dict[Any, Any] = {}
- extra["flows"] = flows
- extra.update(kwargs)
- super().__init__(
- type_=type_,
- description=description,
- name=name,
- scheme_name=scheme_name or self.__class__.__name__,
- **extra,
- )
+class OAuth2(BaseOAuth2): ...
diff --git a/esmerald/openapi/security/openid_connect/base.py b/esmerald/openapi/security/openid_connect/base.py
index 706976cc..7c66a9f8 100644
--- a/esmerald/openapi/security/openid_connect/base.py
+++ b/esmerald/openapi/security/openid_connect/base.py
@@ -1,27 +1,4 @@
-from typing import Any, Literal, Optional, Union
+from esmerald.security.open_id import OpenIdConnect as BaseOpenIdConnect
-from pydantic import AnyUrl
-from esmerald.openapi.enums import SecuritySchemeType
-from esmerald.openapi.security.base import HTTPBase
-
-
-class OpenIdConnect(HTTPBase):
- def __init__(
- self,
- *,
- type_: Literal[
- "apiKey", "http", "mutualTLS", "oauth2", "openIdConnect"
- ] = SecuritySchemeType.openIdConnect.value,
- openIdConnectUrl: Optional[Union[AnyUrl, str]] = None,
- scheme_name: Optional[str] = None,
- description: Optional[str] = None,
- **kwargs: Any,
- ):
- super().__init__(
- type_=type_,
- description=description,
- scheme_name=scheme_name or self.__class__.__name__,
- openIdConnectUrl=openIdConnectUrl,
- **kwargs,
- )
+class OpenIdConnect(BaseOpenIdConnect): ...
diff --git a/esmerald/param_functions.py b/esmerald/param_functions.py
index db449026..b7c1c6d4 100644
--- a/esmerald/param_functions.py
+++ b/esmerald/param_functions.py
@@ -1,16 +1,94 @@
-from typing import Any, Callable, Optional
+from typing import Any, Callable, List, Optional, Sequence, Union
-from esmerald.params import DirectInject
+from pydantic.fields import AliasChoices, AliasPath
+from esmerald import params
+from esmerald.enums import EncodingType
+from esmerald.typing import Undefined
-def DirectInjects(
+_PyUndefined: Any = Undefined
+
+
+def Security(
dependency: Optional[Callable[..., Any]] = None,
*,
+ scopes: Optional[Sequence[str]] = None,
use_cache: bool = True,
- allow_none: bool = True,
) -> Any:
"""
This function should be only called if Inject/Injects is not used in the dependencies.
This is a simple wrapper of the classic Inject().
"""
- return DirectInject(dependency=dependency, use_cache=use_cache, allow_none=allow_none)
+ return params.Security(dependency=dependency, scopes=scopes, use_cache=use_cache)
+
+
+def Requires(
+ dependency: Optional[Callable[..., Any]] = None,
+ *,
+ use_cache: bool = True,
+) -> Any:
+ """
+ This function should be only called if Inject/Injects is not used in the dependencies.
+ This is a simple wrapper of the classic Depends().
+ """
+ return params.Requires(dependency=dependency, use_cache=use_cache)
+
+def Form(
+ default: Any = _PyUndefined,
+ *,
+ annotation: Optional[Any] = None,
+ default_factory: Optional[Callable[..., Any]] = _PyUndefined,
+ allow_none: Optional[bool] = True,
+ media_type: Union[str, EncodingType] = EncodingType.URL_ENCODED,
+ content_encoding: Optional[str] = None,
+ alias: Optional[str] = None,
+ alias_priority: Optional[int] = _PyUndefined,
+ title: Optional[str] = None,
+ description: Optional[str] = None,
+ gt: Optional[float] = None,
+ ge: Optional[float] = None,
+ lt: Optional[float] = None,
+ le: Optional[float] = None,
+ min_length: Optional[int] = None,
+ max_length: Optional[int] = None,
+ pattern: Optional[str] = None,
+ examples: Optional[List[Any]] = None,
+ validation_alias: Optional[Union[str, AliasPath, AliasChoices]] = None,
+ discriminator: Optional[str] = None,
+ max_digits: Optional[int] = _PyUndefined,
+ strict: Optional[bool] = _PyUndefined,
+ frozen: Optional[bool] = None,
+ validate_default: bool = True,
+ init_var: bool = True,
+ kw_only: bool = True,
+ include_in_schema: bool = True,
+) -> Any:
+ return params.Form(
+ default=default,
+ annotation=annotation,
+ default_factory=default_factory,
+ allow_none=allow_none,
+ media_type=media_type,
+ content_encoding=content_encoding,
+ alias=alias,
+ alias_priority=alias_priority,
+ title=title,
+ description=description,
+ gt=gt,
+ ge=ge,
+ lt=lt,
+ le=le,
+ min_length=min_length,
+ max_length=max_length,
+ pattern=pattern,
+ examples=examples,
+ validation_alias=validation_alias,
+ discriminator=discriminator,
+ max_digits=max_digits,
+ strict=strict,
+ frozen=frozen,
+ validate_default=validate_default,
+ init_var=init_var,
+ kw_only=kw_only,
+ include_in_schema=include_in_schema,
+ )
diff --git a/esmerald/params.py b/esmerald/params.py
index b1a4b097..65b27ab6 100644
--- a/esmerald/params.py
+++ b/esmerald/params.py
@@ -1,7 +1,5 @@
-# from dataclasses import dataclass
-from typing import Any, Callable, Dict, List, Optional, Union
+from typing import Any, Callable, Dict, List, Optional, Sequence, Union
-from pydantic.dataclasses import dataclass
from pydantic.fields import AliasChoices, AliasPath, FieldInfo
from esmerald.enums import EncodingType, ParamType
@@ -428,6 +426,7 @@ def __init__(
init_var: bool = True,
kw_only: bool = True,
include_in_schema: bool = True,
+ json_schema_extra: Optional[Dict[str, Any]] = None,
) -> None:
extra: Dict[str, Any] = {}
self.media_type = media_type
@@ -441,6 +440,8 @@ def __init__(
extra.update(embed=embed)
extra.update(allow_none=allow_none)
+ json_schema_extra = extra if json_schema_extra is None else json_schema_extra.update(extra)
+
super().__init__(
default=default,
default_factory=default_factory,
@@ -459,7 +460,7 @@ def __init__(
min_length=min_length,
max_length=max_length,
pattern=pattern,
- json_schema_extra=extra,
+ json_schema_extra=json_schema_extra,
validate_default=validate_default,
validation_alias=validation_alias,
discriminator=discriminator,
@@ -629,25 +630,67 @@ def __init__(
super().__init__(default=default, json_schema_extra=self.extra)
-@dataclass
-class DirectInject: # pragma: no cover
+class Requires:
+ """
+ A class that represents a requirement with an optional dependency and caching behavior.
+
+ Attributes:
+ dependency (Optional[Callable[..., Any]]): An optional callable that represents the dependency.
+ use_cache (bool): A flag indicating whether to use caching for the dependency. Defaults to True.
+
+ Methods:
+ __repr__(): Returns a string representation of the Requires instance.
+ """
+
+ def __init__(self, dependency: Optional[Callable[..., Any]] = None, *, use_cache: bool = True):
+ """
+ Initializes a Requires instance.
+
+ Args:
+ dependency (Optional[Callable[..., Any]]): An optional callable that represents the dependency.
+ use_cache (bool): A flag indicating whether to use caching for the dependency. Defaults to True.
+ """
+
+ """
+ Returns a string representation of the Requires instance.
+
+ Returns:
+ str: A string representation of the Requires instance.
+ """
+ self.dependency = dependency
+ self.use_cache = use_cache
+
+ def __repr__(self) -> str:
+ attr = getattr(self.dependency, "__name__", type(self.dependency).__name__)
+ cache = "" if self.use_cache else ", use_cache=False"
+ return f"{self.__class__.__name__}({attr}{cache})"
+
+
+class Security(Requires):
+ """
+ A class used to represent security requirements for a particular operation.
+
+ Attributes:
+ ----------
+ dependency : Optional[Callable[..., Any]]
+ A callable that represents the dependency required for security.
+ scopes : Optional[Sequence[str]]
+ A sequence of scopes required for the security. Defaults to an empty list.
+ use_cache : bool
+ A flag indicating whether to use cache. Defaults to True.
+
+ Methods:
+ -------
+ __init__(self, dependency: Optional[Callable[..., Any]] = None, *, scopes: Optional[Sequence[str]] = None, use_cache: bool = True)
+ Initializes the Security class with the given dependency, scopes, and use_cache flag.
+ """
+
def __init__(
self,
dependency: Optional[Callable[..., Any]] = None,
*,
+ scopes: Optional[Sequence[str]] = None,
use_cache: bool = True,
- allow_none: bool = True,
- ) -> None:
- self.dependency = dependency
- self.use_cache = use_cache
- self.allow_none = allow_none
-
- def __hash__(self) -> int:
- values: Dict[str, Any] = {}
- for key, value in self.__dict__.items():
- values[key] = None
- if isinstance(value, (list, set)):
- values[key] = tuple(value)
- else:
- values[key] = value
- return hash((type(self),) + tuple(values))
+ ):
+ super().__init__(dependency=dependency, use_cache=use_cache)
+ self.scopes = scopes or []
diff --git a/esmerald/routing/_internal.py b/esmerald/routing/_internal.py
index a4e6c07a..9bede24c 100644
--- a/esmerald/routing/_internal.py
+++ b/esmerald/routing/_internal.py
@@ -19,7 +19,7 @@
from esmerald.routing.router import HTTPHandler, WebhookHandler
-def get_base_annotations(base_annotation: Any) -> Dict[str, Any]:
+def get_base_annotations(base_annotation: Any, is_class: bool = False) -> Dict[str, Any]:
"""
Returns the annotations of the base class.
@@ -30,7 +30,12 @@ def get_base_annotations(base_annotation: Any) -> Dict[str, Any]:
Dict[str, Any]: The annotations of the base class.
"""
base_annotations: Dict[str, Any] = {}
- for base in base_annotation.__bases__:
+ if not is_class:
+ bases = base_annotation.__bases__
+ else:
+ bases = base_annotation.__class__.__bases__
+
+ for base in bases:
base_annotations.update(**get_base_annotations(base))
if hasattr(base, "__annotations__"):
for name, annotation in base.__annotations__.items():
@@ -38,6 +43,50 @@ def get_base_annotations(base_annotation: Any) -> Dict[str, Any]:
return base_annotations
+def convert_object_annotation_to_pydantic_model(field_annotation: Any) -> BaseModel:
+ """
+ Converts any annotation of the body into a Pydantic
+ base model.
+
+ This is used for OpenAPI representation purposes only.
+
+ Esmerald will try internally to convert the model into a Pydantic BaseModel,
+ this will serve as representation of the model in the documentation but internally,
+ it will use the native type to validate the data being sent and parsed in the
+ payload/data field.
+
+ Encoders are not supported in the OpenAPI representation, this is because the encoders
+ are unique to Esmerald and are not part of the OpenAPI specification. This is why
+ we convert the encoders into a Pydantic model for OpenAPI representation purposes only.
+ """
+ annotation_args = get_args(field_annotation)
+ if isinstance(field_annotation, _GenericAlias):
+ annotations = tuple(convert_annotation_to_pydantic_model(arg) for arg in annotation_args)
+ field_annotation.__args__ = annotations
+ return cast(BaseModel, field_annotation)
+
+ field_definitions: Dict[str, Any] = {}
+
+ # Get any possible annotations from the base classes
+ base_annotations: Dict[str, Any] = {**get_base_annotations(field_annotation, is_class=True)}
+ field_annotations = {
+ **base_annotations,
+ **field_annotation.__annotations__,
+ }
+ for name, annotation in field_annotations.items():
+ field_definitions[name] = (annotation, ...)
+
+ if inspect.isclass(field_annotation):
+ name = field_annotation.__name__
+ else:
+ name = field_annotation.__class__.__name__
+
+ return cast(
+ BaseModel,
+ create_model(name, __config__={"arbitrary_types_allowed": True}, **field_definitions),
+ )
+
+
def convert_annotation_to_pydantic_model(field_annotation: Any) -> Any:
"""
Converts any annotation of the body into a Pydantic
@@ -278,12 +327,15 @@ def body_encoder_fields(self) -> Dict[str, FieldInfo]:
"""
# Making sure the dependencies are not met as body fields for OpenAPI representation
handler_dependencies = set(self.get_dependencies().keys())
+ security_dependencies = set(self.transformer.get_security_params().keys())
# Getting everything else that is not considered a dependency
body_encoder_fields = {
name: field
for name, field in self.signature_model.model_fields.items()
- if is_body_encoder(field.annotation) and name not in handler_dependencies
+ if is_body_encoder(field.annotation)
+ and name not in handler_dependencies
+ and name not in security_dependencies
}
return body_encoder_fields
diff --git a/esmerald/routing/base.py b/esmerald/routing/base.py
index ff1d9880..4942bc00 100644
--- a/esmerald/routing/base.py
+++ b/esmerald/routing/base.py
@@ -223,7 +223,7 @@ async def _get_response_data(
dependency=dependency, connection=request, **kwargs
)
- parsed_kwargs = signature_model.parse_values_for_connection(
+ parsed_kwargs = await signature_model.parse_values_for_connection(
connection=request, **kwargs
)
else:
diff --git a/esmerald/routing/router.py b/esmerald/routing/router.py
index ab566b07..9546a038 100644
--- a/esmerald/routing/router.py
+++ b/esmerald/routing/router.py
@@ -2448,7 +2448,7 @@ async def get_kwargs(self, websocket: WebSocket) -> Any:
kwargs[dependency.key] = await self.websocket_parameter_model.get_dependencies(
dependency=dependency, connection=websocket, **kwargs
)
- return signature_model.parse_values_for_connection(connection=websocket, **kwargs)
+ return await signature_model.parse_values_for_connection(connection=websocket, **kwargs)
class Include(LilyaInclude):
diff --git a/esmerald/security/api_key/__init__.py b/esmerald/security/api_key/__init__.py
new file mode 100644
index 00000000..ceba86c5
--- /dev/null
+++ b/esmerald/security/api_key/__init__.py
@@ -0,0 +1,3 @@
+from .api_key import APIKeyInCookie, APIKeyInHeader, APIKeyInQuery
+
+__all__ = ["APIKeyInCookie", "APIKeyInHeader", "APIKeyInQuery"]
diff --git a/esmerald/security/api_key/api_key.py b/esmerald/security/api_key/api_key.py
new file mode 100644
index 00000000..c27270a8
--- /dev/null
+++ b/esmerald/security/api_key/api_key.py
@@ -0,0 +1,212 @@
+from typing import Union, cast
+
+from lilya.exceptions import HTTPException
+from lilya.status import HTTP_403_FORBIDDEN
+from pydantic import BaseModel
+from typing_extensions import Annotated, Doc
+
+from esmerald.openapi.models import APIKey, APIKeyIn
+from esmerald.requests import Request
+from esmerald.security.base import SecurityBase
+
+
+class APIKeyBase(SecurityBase):
+ __model__: Union[BaseModel, None] = None
+
+
+class APIKeyInQuery(APIKeyBase):
+ """
+ API key authentication using a query parameter.
+
+ Defines the query parameter name for the API key and integrates it into the OpenAPI documentation.
+ Extracts the key value from the query parameter and provides it as the dependency result.
+
+ ## Usage
+
+ Create an instance and use it as a dependency in `Inject()`.
+
+ The dependency result will be a string containing the key value after using the `Injects()`.
+
+ ## Example
+
+ ```python
+ from esmerald import Esmerald, Gateway, get, Inject, Injects
+ from esmerald.security.api_key import APIKeyInQuery
+
+ query_scheme = APIKeyInQuery(name="api_key")
+
+ @get("/items/", dependencies={"api_key": Inject(query_scheme)})
+ async def read_items(api_key: str = Injects()) -> dict[str, str]:
+ return {"api_key": api_key}
+ ```
+ """
+
+ def __init__(
+ self,
+ *,
+ name: Annotated[str, Doc("Name of the query parameter.")],
+ scheme_name: Annotated[
+ Union[str, None],
+ Doc("Name of the security scheme, shown in OpenAPI documentation."),
+ ] = None,
+ description: Annotated[
+ Union[str, None],
+ Doc("Description of the security scheme, shown in OpenAPI documentation."),
+ ] = None,
+ auto_error: Annotated[
+ bool,
+ Doc(
+ "If True, raises an error if the query parameter is missing. "
+ "If False, returns None when the query parameter is missing."
+ ),
+ ] = True,
+ ):
+ model: APIKey = APIKey(
+ **{"in": APIKeyIn.query.value}, # type: ignore[arg-type]
+ name=name,
+ description=description,
+ )
+ super().__init__(**model.model_dump())
+ self.__model__ = model
+ self.scheme_name = scheme_name or self.__class__.__name__
+ self.__auto_error__ = auto_error
+
+ async def __call__(self, request: Request) -> Union[str, None]:
+ api_key = request.query_params.get(self.__model__.name)
+ if api_key:
+ return cast(str, api_key)
+ if self.__auto_error__:
+ raise HTTPException(status_code=HTTP_403_FORBIDDEN, detail="Not authenticated")
+ return None
+
+
+class APIKeyInHeader(APIKeyBase):
+ """
+ API key authentication using a header parameter.
+
+ Defines the header parameter name for the API key and integrates it into the OpenAPI documentation.
+ Extracts the key value from the header parameter and provides it as the dependency result.
+
+ ## Usage
+
+ Create an instance and use it as a dependency in `Inject()`.
+
+ The dependency result will be a string containing the key value after using the `Injects()`.
+
+ ## Example
+
+ ```python
+ from esmerald import Esmerald, Gateway, get, Inject, Injects
+ from esmerald.security.api_key import APIKeyInHeader
+
+ header_scheme = APIKeyInHeader(name="x-key")
+
+ @get("/items/", dependencies={"api_key": Inject(header_scheme)})
+ async def read_items(api_key: str = Injects()) -> dict[str, str]:
+ return {"api_key": api_key}
+ ```
+ """
+
+ def __init__(
+ self,
+ *,
+ name: Annotated[str, Doc("The name of the header parameter.")],
+ scheme_name: Annotated[
+ Union[str, None],
+ Doc("The name of the security scheme to be shown in the OpenAPI documentation."),
+ ] = None,
+ description: Annotated[
+ Union[str, None],
+ Doc("A description of the security scheme to be shown in the OpenAPI documentation."),
+ ] = None,
+ auto_error: Annotated[
+ bool,
+ Doc(
+ "If True, an error is raised if the header is missing. "
+ "If False, None is returned when the header is missing."
+ ),
+ ] = True,
+ ):
+ model: APIKey = APIKey(
+ **{"in": APIKeyIn.header.value}, # type: ignore[arg-type]
+ name=name,
+ description=description,
+ )
+ super().__init__(**model.model_dump())
+ self.__model__ = model
+ self.scheme_name = scheme_name or self.__class__.__name__
+ self.__auto_error__ = auto_error
+
+ async def __call__(self, request: Request) -> Union[str, None]:
+ api_key = request.headers.get(self.__model__.name)
+ if api_key:
+ return cast(str, api_key)
+ if self.__auto_error__:
+ raise HTTPException(status_code=HTTP_403_FORBIDDEN, detail="Not authenticated")
+ return None
+
+
+class APIKeyInCookie(APIKeyBase):
+ """
+ API key authentication using a cookie parameter.
+
+ Defines the cookie parameter name for the API key and integrates it into the OpenAPI documentation.
+ Extracts the key value from the cookie parameter and provides it as the dependency result.
+
+ ## Usage
+
+ Create an instance and use it as a dependency in `Inject()`.
+
+ The dependency result will be a string containing the key value after using the `Injects()`.
+
+ ## Example
+
+ ```python
+ from esmerald import Esmerald, Gateway, get, Inject, Injects
+ from esmerald.security.api_key import APIKeyInCookie
+
+ cookie_scheme = APIKeyInCookie(name="session")
+
+ @get("/items/", dependencies={"api_key": Inject(cookie_scheme)})
+ async def read_items(api_key: str = Injects()) -> dict[str, str]:
+ return {"api_key": api_key}
+ ```
+ """
+
+ def __init__(
+ self,
+ *,
+ name: Annotated[str, Doc("The name of the cookie parameter.")],
+ scheme_name: Annotated[
+ Union[str, None],
+ Doc("The name of the security scheme to be shown in the OpenAPI documentation."),
+ ] = None,
+ description: Annotated[
+ Union[str, None],
+ Doc("A description of the security scheme to be shown in the OpenAPI documentation."),
+ ] = None,
+ auto_error: Annotated[
+ bool,
+ Doc(
+ "If True, an error is raised if the cookie is missing. "
+ "If False, None is returned when the cookie is missing."
+ ),
+ ] = True,
+ ):
+ model: APIKey = APIKey(
+ **{"in": APIKeyIn.cookie.value}, # type: ignore[arg-type]
+ name=name,
+ description=description,
+ )
+ super().__init__(**model.model_dump())
+ self.__model__ = model
+ self.scheme_name = scheme_name or self.__class__.__name__
+ self.__auto_error__ = auto_error
+
+ async def __call__(self, request: Request) -> Union[str, None]:
+ api_key = request.cookies.get(self.__model__.name)
+ if api_key:
+ return api_key
+ if self.__auto_error__:
+ raise HTTPException(status_code=HTTP_403_FORBIDDEN, detail="Not authenticated")
+ return None
diff --git a/esmerald/security/base.py b/esmerald/security/base.py
new file mode 100644
index 00000000..1aaf48cf
--- /dev/null
+++ b/esmerald/security/base.py
@@ -0,0 +1,33 @@
+from typing import Optional
+
+from esmerald.openapi.models import (
+ SecurityScheme,
+)
+
+
+class SecurityBase(SecurityScheme):
+ scheme_name: Optional[str] = None
+ """
+ An optional name for the security scheme.
+ """
+ __auto_error__: bool = False
+ """
+ A flag to indicate if automatic error handling should be enabled.
+ """
+ __is_security__: bool = True
+ """A flag to indicate that this is a security scheme. """
+
+
+class HttpSecurityBase(SecurityScheme):
+ scheme_name: Optional[str] = None
+ """
+ An optional name for the security scheme.
+ """
+ realm: Optional[str] = None
+ """
+ An optional realm for the security scheme.
+ """
+ __auto_error__: bool = False
+ """
+ A flag to indicate if automatic error handling should be enabled.
+ """
diff --git a/esmerald/security/http/__init__.py b/esmerald/security/http/__init__.py
new file mode 100644
index 00000000..b46c3067
--- /dev/null
+++ b/esmerald/security/http/__init__.py
@@ -0,0 +1,17 @@
+from .http import (
+ HTTPAuthorizationCredentials,
+ HTTPBase,
+ HTTPBasic,
+ HTTPBasicCredentials,
+ HTTPBearer,
+ HTTPDigest,
+)
+
+__all__ = [
+ "HTTPBase",
+ "HTTPBasic",
+ "HTTPBearer",
+ "HTTPDigest",
+ "HTTPAuthorizationCredentials",
+ "HTTPBasicCredentials",
+]
diff --git a/esmerald/security/http/http.py b/esmerald/security/http/http.py
new file mode 100644
index 00000000..3faf51bc
--- /dev/null
+++ b/esmerald/security/http/http.py
@@ -0,0 +1,279 @@
+import binascii
+from base64 import b64decode
+from typing import Any, Optional, Union
+
+from lilya.requests import Request
+from lilya.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN
+from pydantic import BaseModel
+from typing_extensions import Annotated, Doc
+
+from esmerald.exceptions import HTTPException
+from esmerald.openapi.models import HTTPBase as HTTPBaseModel, HTTPBearer as HTTPBearerModel
+from esmerald.security.base import HttpSecurityBase
+from esmerald.security.utils import get_authorization_scheme_param
+
+
+class HTTPBasicCredentials(BaseModel):
+ """
+ Represents HTTP Basic credentials.
+
+ Attributes:
+ username (str): The username.
+ password (str): The password.
+ """
+
+ username: Annotated[str, Doc("The username for HTTP Basic authentication.")]
+ password: Annotated[str, Doc("The password for HTTP Basic authentication.")]
+
+
+class HTTPAuthorizationCredentials(BaseModel):
+ """
+ Represents HTTP authorization credentials.
+
+ Attributes:
+ scheme (str): The authorization scheme (e.g., "Bearer").
+ credentials (str): The authorization credentials (e.g., token).
+ """
+
+ scheme: Annotated[str, Doc("The authorization scheme extracted from the header.")]
+ credentials: Annotated[str, Doc("The authorization credentials extracted from the header.")]
+
+
+class HTTPBase(HttpSecurityBase):
+ def __init__(
+ self,
+ *,
+ scheme: str,
+ scheme_name: Union[str, None] = None,
+ description: Union[str, None] = None,
+ auto_error: bool = True,
+ **kwargs: Any,
+ ):
+ """
+ Base class for HTTP security schemes.
+
+ Args:
+ scheme (str): The security scheme (e.g., "basic", "bearer").
+ scheme_name (str, optional): The name of the security scheme.
+ description (str, optional): Description of the security scheme.
+ auto_error (bool, optional): Whether to automatically raise an error if authentication fails.
+ """
+ model = HTTPBaseModel(scheme=scheme, description=description)
+ model_dump = {**model.model_dump(), **kwargs}
+ super().__init__(**model_dump)
+ self.scheme_name = scheme_name or self.__class__.__name__
+ self.__auto_error__ = auto_error
+
+ async def __call__(self, request: Request) -> Optional[HTTPAuthorizationCredentials]:
+ authorization = request.headers.get("Authorization")
+ if not authorization:
+ if self.__auto_error__:
+ raise HTTPException(status_code=HTTP_403_FORBIDDEN, detail="Not authenticated")
+ return None
+
+ scheme, credentials = get_authorization_scheme_param(authorization)
+ if not (scheme and credentials):
+ if self.__auto_error__:
+ raise HTTPException(
+ status_code=HTTP_403_FORBIDDEN, detail="Invalid authentication credentials"
+ )
+ return None
+
+ return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials)
+
+
+class HTTPBasic(HTTPBase):
+ """
+ HTTP Basic authentication.
+
+ Use this class as a dependency to enforce HTTP Basic authentication.
+
+ ## Example:
+
+ ```python
+ from typing import Any
+
+ from esmerald import Gateway, Inject, Injects, get
+ from esmerald.security.http import HTTPBasic, HTTPBasicCredentials
+ from esmerald.testclient import create_client
+
+ security = HTTPBasic()
+
+ @app.get("/users/me", security=[security], dependencies={"credentials": Inject(security)}))
+ def get_current_user(credentials: HTTPBasicCredentials = Injects()):
+ return {"username": credentials.username, "password": credentials.password}
+ ```
+ """
+
+ def __init__(
+ self,
+ *,
+ scheme_name: Annotated[Union[str, None], Doc("The name of the security scheme.")] = None,
+ realm: Annotated[Union[str, None], Doc("The HTTP Basic authentication realm.")] = None,
+ description: Annotated[
+ Union[str, None], Doc("Description of the security scheme.")
+ ] = None,
+ auto_error: Annotated[
+ bool,
+ Doc(
+ "Whether to automatically raise an error if authentication fails. "
+ "If set to False, the dependency result will be None when authentication is not provided."
+ ),
+ ] = True,
+ ):
+ model = HTTPBaseModel(scheme="basic", description=description)
+ super().__init__(**model.model_dump())
+ self.scheme_name = scheme_name or self.__class__.__name__
+ self.realm = realm
+ self.__auto_error__ = auto_error
+
+ async def __call__(self, request: Request) -> Optional[HTTPBasicCredentials]:
+ authorization = request.headers.get("Authorization")
+ scheme, param = get_authorization_scheme_param(authorization)
+
+ unauthorized_headers = {
+ "WWW-Authenticate": f'Basic realm="{self.realm}"' if self.realm else "Basic"
+ }
+
+ if not authorization or scheme.lower() != "basic":
+ if self.__auto_error__:
+ raise HTTPException(
+ status_code=HTTP_401_UNAUTHORIZED,
+ detail="Not authenticated",
+ headers=unauthorized_headers,
+ )
+ return None
+
+ try:
+ data = b64decode(param).decode("ascii")
+ username, separator, password = data.partition(":")
+ if not separator:
+ raise ValueError("Invalid credentials format")
+ except (ValueError, UnicodeDecodeError, binascii.Error):
+ raise HTTPException(
+ status_code=HTTP_401_UNAUTHORIZED,
+ detail="Invalid authentication credentials",
+ headers=unauthorized_headers,
+ ) from None
+
+ return HTTPBasicCredentials(username=username, password=password)
+
+
+class HTTPBearer(HTTPBase):
+ """
+ HTTP Bearer token authentication.
+
+ Use this class as a dependency to enforce HTTP Bearer token authentication.
+
+ ## Example
+
+ ```python
+ from typing import Any
+
+ from esmerald import Inject, Injects, get
+ from esmerald.security.http import HTTPAuthorizationCredentials, HTTPBearer
+
+ security = HTTPBearer()
+
+ @app.get("/users/me")
+ def get_current_user(credentials: HTTPAuthorizationCredentials = Injects()) -> Any::
+ return {"scheme": credentials.scheme, "credentials": credentials.credentials}
+ ```
+ """
+
+ def __init__(
+ self,
+ *,
+ bearerFormat: Annotated[Union[str, None], Doc("The format of the Bearer token.")] = None,
+ scheme_name: Annotated[Union[str, None], Doc("The name of the security scheme.")] = None,
+ description: Annotated[
+ Union[str, None], Doc("Description of the security scheme.")
+ ] = None,
+ auto_error: Annotated[
+ bool,
+ Doc(
+ "Whether to automatically raise an error if authentication fails. "
+ "If set to False, the dependency result will be None when authentication is not provided."
+ ),
+ ] = True,
+ ):
+ model = HTTPBearerModel(bearerFormat=bearerFormat, description=description)
+ super().__init__(**model.model_dump())
+ self.scheme_name = scheme_name or self.__class__.__name__
+ self.__auto_error__ = auto_error
+
+ async def __call__(self, request: Request) -> Optional[HTTPAuthorizationCredentials]:
+ authorization = request.headers.get("Authorization")
+ if not authorization:
+ if self.__auto_error__:
+ raise HTTPException(status_code=HTTP_403_FORBIDDEN, detail="Not authenticated")
+ return None
+
+ scheme, credentials = get_authorization_scheme_param(authorization)
+ if not (scheme and credentials) or scheme.lower() != "bearer":
+ if self.__auto_error__:
+ raise HTTPException(
+ status_code=HTTP_403_FORBIDDEN,
+ detail="Invalid authentication credentials",
+ )
+ return None
+
+ return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials)
+
+
+class HTTPDigest(HTTPBase):
+ """
+ HTTP Digest authentication.
+
+ Use this class as a dependency to enforce HTTP Digest authentication.
+
+ ## Example:
+
+ ```python
+ from typing import Any
+
+ from esmerald import Inject, Injects, get
+ from esmerald.security.http import HTTPAuthorizationCredentials, HTTPDigest
+
+ security = HTTPDigest()
+
+ @get("/users/me", security=[security], dependencies={"credentials": Inject(security)})
+ def get_current_user(credentials: HTTPAuthorizationCredentials = Injects()) -> Any:
+ return {"scheme": credentials.scheme, "credentials": credentials.credentials}
+ ```
+ """
+
+ def __init__(
+ self,
+ *,
+ scheme_name: Annotated[Union[str, None], Doc("The name of the security scheme.")] = None,
+ description: Annotated[
+ Union[str, None], Doc("Description of the security scheme.")
+ ] = None,
+ auto_error: Annotated[
+ bool,
+ Doc(
+ "Whether to automatically raise an error if authentication fails. "
+ "If set to False, the dependency result will be None when authentication is not provided."
+ ),
+ ] = True,
+ ):
+ model = HTTPBaseModel(scheme="digest", description=description)
+ super().__init__(**model.model_dump())
+ self.scheme_name = scheme_name or self.__class__.__name__
+ self.__auto_error__ = auto_error
+
+ async def __call__(self, request: Request) -> Optional[HTTPAuthorizationCredentials]:
+ authorization = request.headers.get("Authorization")
+ scheme, credentials = get_authorization_scheme_param(authorization)
+ if not (authorization and scheme and credentials):
+ if self.__auto_error__:
+ raise HTTPException(status_code=HTTP_403_FORBIDDEN, detail="Not authenticated")
+ else:
+ return None
+ if scheme.lower() != "digest":
+ raise HTTPException(
+ status_code=HTTP_403_FORBIDDEN,
+ detail="Invalid authentication credentials",
+ )
+ return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials)
diff --git a/esmerald/security/oauth2/__init__.py b/esmerald/security/oauth2/__init__.py
new file mode 100644
index 00000000..f3d1d82f
--- /dev/null
+++ b/esmerald/security/oauth2/__init__.py
@@ -0,0 +1,15 @@
+from .oauth import (
+ OAuth2,
+ OAuth2AuthorizationCodeBearer,
+ OAuth2PasswordBearer,
+ OAuth2PasswordRequestForm,
+ OAuth2PasswordRequestFormStrict,
+)
+
+__all__ = [
+ "OAuth2",
+ "OAuth2AuthorizationCodeBearer",
+ "OAuth2PasswordBearer",
+ "OAuth2PasswordRequestForm",
+ "OAuth2PasswordRequestFormStrict",
+]
diff --git a/esmerald/security/oauth2/oauth.py b/esmerald/security/oauth2/oauth.py
new file mode 100644
index 00000000..e44d2679
--- /dev/null
+++ b/esmerald/security/oauth2/oauth.py
@@ -0,0 +1,660 @@
+from typing import Any, Dict, List, Optional, Union, cast
+
+from lilya.requests import Request
+from lilya.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN
+from pydantic import BaseModel, field_validator
+from typing_extensions import Annotated, Doc
+
+from esmerald.exceptions import HTTPException
+from esmerald.openapi.models import (
+ OAuth2 as OAuth2Model,
+ OAuthFlows as OAuthFlowsModel,
+)
+from esmerald.param_functions import Form
+from esmerald.security.base import SecurityBase as SecurityBase
+from esmerald.security.utils import get_authorization_scheme_param
+
+
+class OAuth2PasswordRequestForm(BaseModel):
+ """
+ This is a dependency class to collect the `username` and `password` as form data
+ for an OAuth2 password flow.
+
+ The OAuth2 specification dictates that for a password flow the data should be
+ collected using form data (instead of JSON) and that it should have the specific
+ fields `username` and `password`.
+
+ All the initialization parameters are extracted from the request.
+
+ Read more about it in the
+ [Esmerald docs for Simple OAuth2 with Password and Bearer](https://esmerald.dev/tutorial/security/simple-oauth2/).
+
+ ## Example
+
+ ```python
+ from typing import Annotated
+
+ from esmerald import Esmerald, Gateway, Inject, Injects, post
+ from esmerald.security.oauth2 import OAuth2PasswordRequestForm
+
+
+ @post("/login", dependencies={"form_data": Inject(OAuth2PasswordRequestForm)})
+ def login(form_data: Annotated[OAuth2PasswordRequestForm, Injects()]) -> dict:
+ data = {}
+ data["scopes"] = []
+ for scope in form_data.scopes:
+ data["scopes"].append(scope)
+ if form_data.client_id:
+ data["client_id"] = form_data.client_id
+ if form_data.client_secret:
+ data["client_secret"] = form_data.client_secret
+ return data
+
+
+ app = Esmerald(
+ routes=[
+ Gateway(handler=login),
+ ]
+ )
+ ```
+
+ Note that for OAuth2 the scope `items:read` is a single scope in an opaque string.
+ You could have custom internal logic to separate it by colon characters (`:`) or
+ similar, and get the two parts `items` and `read`. Many applications do that to
+ group and organize permissions, you could do it as well in your application, just
+ know that that it is application specific, it's not part of the specification.
+ """
+
+ grant_type: Annotated[
+ Union[str, None],
+ Form(pattern="password"),
+ Doc(
+ """
+ Specifies the OAuth2 grant type.
+
+ Per the OAuth2 specification, this value is required and must be set
+ to the fixed string "password" when using the password grant flow.
+ However, this class allows flexibility and does not enforce this
+ restriction. To enforce the "password" value strictly, consider using
+ the `OAuth2PasswordRequestFormStrict` dependency.
+ """
+ ),
+ ] = None
+ username: Annotated[
+ str,
+ Form(),
+ Doc(
+ """
+ The username of the user for OAuth2 authentication.
+
+ According to the OAuth2 specification, this field must be named
+ `username`, as it is used to identify the user during the
+ authentication process.
+ """
+ ),
+ ]
+ password: Annotated[
+ str,
+ Form(),
+ Doc(
+ """
+ The password of the user for OAuth2 authentication.
+
+ Per the OAuth2 spec, this field must also use the name `password`.
+ It is required for authentication to validate the provided username.
+ """
+ ),
+ ]
+ scopes: Annotated[
+ Union[str, List[str]],
+ Form(),
+ Doc(
+ """
+ A single string containing one or more scopes, space-separated.
+
+ Each scope represents a permission requested by the application.
+ Scopes help specify fine-grained access control, enabling the client
+ to request only the permissions it needs. For example, the following
+ string:
+
+ ```python
+ "items:read items:write users:read profile openid"
+ ```
+
+ represents multiple scopes:
+
+ * `items:read`
+ * `items:write`
+ * `users:read`
+ * `profile`
+ * `openid`
+ """
+ ),
+ ] = []
+ client_id: Annotated[
+ Union[str, None],
+ Form(),
+ Doc(
+ """
+ Optional client identifier used to identify the client application.
+
+ If provided, `client_id` can be sent as part of the form data.
+ Although the OAuth2 specification recommends sending both `client_id`
+ and `client_secret` via HTTP Basic authentication headers, some APIs
+ accept these values in the form fields for flexibility.
+ """
+ ),
+ ] = None
+ client_secret: Annotated[
+ Union[str, None],
+ Form(),
+ Doc(
+ """
+ Optional client secret for authenticating the client application.
+
+ If a `client_secret` is required (along with `client_id`), it can be
+ included in the form data. However, the OAuth2 spec advises sending
+ both `client_id` and `client_secret` using HTTP Basic authentication
+ headers for security.
+ """
+ ),
+ ] = None
+
+ @field_validator("scopes", mode="before")
+ @classmethod
+ def validate_scopes(cls, value: Union[str, List[str]]) -> Any:
+ if isinstance(value, str) and len(value) == 0:
+ return []
+ if isinstance(value, str):
+ return value.split(" ")
+ return value
+
+
+class OAuth2PasswordRequestFormStrict(OAuth2PasswordRequestForm):
+ """
+ This is a dependency class to collect the `username` and `password` as form data
+ for an OAuth2 password flow.
+
+ The OAuth2 specification dictates that for a password flow the data should be
+ collected using form data (instead of JSON) and that it should have the specific
+ fields `username` and `password`.
+
+ All the initialization parameters are extracted from the request.
+
+ The only difference between `OAuth2PasswordRequestFormStrict` and
+ `OAuth2PasswordRequestForm` is that `OAuth2PasswordRequestFormStrict` requires the
+ client to send the form field `grant_type` with the value `"password"`, which
+ is required in the OAuth2 specification (it seems that for no particular reason),
+ while for `OAuth2PasswordRequestForm` `grant_type` is optional.
+
+ ## Example
+
+ ```python
+ from typing import Annotated
+
+ from esmerald import Esmerald, Gateway, Inject, Injects, post
+ from esmerald.security.oauth2 import OAuth2PasswordRequestForm
+
+
+ @post("/login", dependencies={"form_data": Inject(OAuth2PasswordRequestForm)})
+ def login(form_data: Annotated[OAuth2PasswordRequestForm, Injects()]) -> dict:
+ data = {}
+ data["scopes"] = []
+ for scope in form_data.scopes:
+ data["scopes"].append(scope)
+ if form_data.client_id:
+ data["client_id"] = form_data.client_id
+ if form_data.client_secret:
+ data["client_secret"] = form_data.client_secret
+ return data
+
+
+ app = Esmerald(
+ routes=[
+ Gateway(handler=login),
+ ]
+ )
+ ```
+
+ Note that for OAuth2 the scope `items:read` is a single scope in an opaque string.
+ You could have custom internal logic to separate it by colon caracters (`:`) or
+ similar, and get the two parts `items` and `read`. Many applications do that to
+ group and organize permissions, you could do it as well in your application, just
+ know that that it is application specific, it's not part of the specification.
+
+
+ grant_type: the OAuth2 spec says it is required and MUST be the fixed string "password".
+ This dependency is strict about it. If you want to be permissive, use instead the
+ OAuth2PasswordRequestForm dependency class.
+ username: username string. The OAuth2 spec requires the exact field name "username".
+ password: password string. The OAuth2 spec requires the exact field name "password".
+ scope: Optional string. Several scopes (each one a string) separated by spaces. E.g.
+ "items:read items:write users:read profile openid"
+ client_id: optional string. OAuth2 recommends sending the client_id and client_secret (if any)
+ using HTTP Basic auth, as: client_id:client_secret
+ client_secret: optional string. OAuth2 recommends sending the client_id and client_secret (if any)
+ using HTTP Basic auth, as: client_id:client_secret
+ """
+
+ def __init__(
+ self,
+ grant_type: Annotated[
+ str,
+ Form(pattern="password"),
+ Doc(
+ """
+ The OAuth2 spec says it is required and MUST be the fixed string
+ "password". This dependency is strict about it. If you want to be
+ permissive, use instead the `OAuth2PasswordRequestForm` dependency
+ class.
+ """
+ ),
+ ],
+ username: Annotated[
+ str,
+ Form(),
+ Doc(
+ """
+ `username` string. The OAuth2 spec requires the exact field name
+ `username`.
+ """
+ ),
+ ],
+ password: Annotated[
+ str,
+ Form(),
+ Doc(
+ """
+ `password` string. The OAuth2 spec requires the exact field name
+ `password".
+ """
+ ),
+ ],
+ scope: Annotated[
+ str,
+ Form(),
+ Doc(
+ """
+ A single string with actually several scopes separated by spaces. Each
+ scope is also a string.
+
+ For example, a single string with:
+
+ ```python
+ "items:read items:write users:read profile openid"
+ ````
+
+ would represent the scopes:
+
+ * `items:read`
+ * `items:write`
+ * `users:read`
+ * `profile`
+ * `openid`
+ """
+ ),
+ ] = "",
+ client_id: Annotated[
+ Union[str, None],
+ Form(),
+ Doc(
+ """
+ If there's a `client_id`, it can be sent as part of the form fields.
+ But the OAuth2 specification recommends sending the `client_id` and
+ `client_secret` (if any) using HTTP Basic auth.
+ """
+ ),
+ ] = None,
+ client_secret: Annotated[
+ Union[str, None],
+ Form(),
+ Doc(
+ """
+ If there's a `client_password` (and a `client_id`), they can be sent
+ as part of the form fields. But the OAuth2 specification recommends
+ sending the `client_id` and `client_secret` (if any) using HTTP Basic
+ auth.
+ """
+ ),
+ ] = None,
+ ) -> None:
+ super().__init__(
+ grant_type=grant_type,
+ username=username,
+ password=password,
+ scopes=scope,
+ client_id=client_id,
+ client_secret=client_secret,
+ )
+
+
+class OAuth2(SecurityBase):
+ """
+ This is the base class for OAuth2 authentication, an instance of it would be used
+ as a dependency. All other OAuth2 classes inherit from it and customize it for
+ each OAuth2 flow.
+
+ You normally would not create a new class inheriting from it but use one of the
+ existing subclasses, and maybe compose them if you want to support multiple flows.
+ """
+
+ def __init__(
+ self,
+ *,
+ flows: Annotated[
+ Union[OAuthFlowsModel, Dict[str, Dict[str, Any]]],
+ Doc(
+ """
+ The dictionary containing the OAuth2 flows.
+ """
+ ),
+ ] = OAuthFlowsModel(),
+ scheme_name: Annotated[
+ Optional[str],
+ Doc(
+ """
+ The name of the Security scheme.
+
+ It will be in the OpenAPI documentation.
+ """
+ ),
+ ] = None,
+ description: Annotated[
+ Optional[str],
+ Doc(
+ """
+ Security scheme description.
+
+ It will be in the OpenAPI documentation.
+ """
+ ),
+ ] = None,
+ auto_error: Annotated[
+ bool,
+ Doc(
+ """
+ By default, if no HTTP Authorization header is provided, which is required for
+ OAuth2 authentication, the request will automatically be canceled and
+ an error will be sent to the client.
+
+ If `auto_error` is set to `False`, when the HTTP Authorization header
+ is not available, instead of erroring out, the dependency result will
+ be `None`.
+
+ This is useful when you want to have optional authentication.
+
+ It is also useful when you want to have authentication that can be
+ provided in one of multiple optional ways (for example, with OAuth2
+ or in a cookie).
+ """
+ ),
+ ] = True,
+ ) -> None:
+ model = OAuth2Model(
+ flows=cast(OAuthFlowsModel, flows), scheme=scheme_name, description=description
+ )
+ super().__init__(**model.model_dump())
+ self.scheme_name = scheme_name or self.__class__.__name__
+ self.__auto_error__ = auto_error
+
+ async def __call__(self, request: Request) -> Any:
+ authorization = request.headers.get("Authorization")
+
+ if authorization:
+ return authorization
+
+ if self.__auto_error__:
+ raise HTTPException(status_code=HTTP_403_FORBIDDEN, detail="Not authenticated")
+
+ return None
+
+
+class OAuth2PasswordBearer(OAuth2):
+ """
+ This class is typically used as a dependency in path operations.
+
+ Args:
+ tokenUrl (str): The URL to obtain the OAuth2 token. This should be the path
+ operation that has `OAuth2PasswordRequestForm` as a dependency.
+ scheme_name (Optional[str], optional): The security scheme name. This will appear
+ in the OpenAPI documentation. Defaults to None.
+ scopes (Optional[Dict[str, str]], optional): The OAuth2 scopes required by the
+ path operations using this dependency.
+ Defaults to None.
+ description (Optional[str], optional): The security scheme description. This will
+ appear in the OpenAPI documentation. Defaults to None.
+ auto_error (bool, optional): If set to True (default), the request will automatically
+ be canceled and an error sent to the client if no HTTP
+ Authorization header is provided. If set to False, the
+ dependency result will be None when the HTTP Authorization
+ header is not available, allowing for optional authentication.
+
+ Methods:
+ __call__(request: Request) -> Optional[str]: Extracts and returns the bearer token
+ from the request's Authorization header.
+ Raises an HTTP 401 error if authentication
+ fails and `auto_error` is True.
+
+ OAuth2 flow for authentication using a bearer token obtained with a password.
+ An instance of it would be used as a dependency.
+ """
+
+ def __init__(
+ self,
+ tokenUrl: Annotated[
+ str,
+ Doc(
+ """
+ The endpoint URL used to obtain the OAuth2 token.
+
+ This URL should point to the *path operation* that includes
+ `OAuth2PasswordRequestForm` as a dependency, facilitating user
+ authentication and token retrieval within the OAuth2 framework.
+ """
+ ),
+ ],
+ scheme_name: Annotated[
+ Optional[str],
+ Doc(
+ """
+ The name of the security scheme.
+
+ This value will be displayed in the OpenAPI documentation,
+ identifying the OAuth2 security scheme associated with this
+ configuration.
+ """
+ ),
+ ] = None,
+ scopes: Annotated[
+ Optional[Dict[str, str]],
+ Doc(
+ """
+ A dictionary of OAuth2 scopes associated with this configuration.
+
+ Scopes define specific permissions that the *path operations* require,
+ enabling fine-grained access control. The dictionary should use
+ scope names as keys and their descriptions as values, aiding in
+ understanding each scope's purpose.
+ """
+ ),
+ ] = None,
+ description: Annotated[
+ Optional[str],
+ Doc(
+ """
+ A description of the security scheme.
+
+ This description will be included in the OpenAPI documentation,
+ providing users with an overview of the OAuth2 security scheme's
+ purpose and any additional information relevant to this configuration.
+ """
+ ),
+ ] = None,
+ auto_error: Annotated[
+ bool,
+ Doc(
+ """
+ Flag to control automatic error response when authorization fails.
+
+ If `True` (default), the application will automatically cancel the
+ request and return an error response if the HTTP Authorization header
+ is missing or invalid. Setting `auto_error` to `False` allows the
+ request to proceed without authentication, returning `None` for the
+ dependency result, which is useful in cases where authentication
+ should be optional or supported through multiple methods (e.g.,
+ OAuth2 or cookies).
+ """
+ ),
+ ] = True,
+ ):
+ if not scopes:
+ scopes = {}
+ flows = OAuthFlowsModel(password=cast(Any, {"tokenUrl": tokenUrl, "scopes": scopes}))
+ super().__init__(
+ flows=flows,
+ scheme_name=scheme_name,
+ description=description,
+ auto_error=auto_error,
+ )
+
+ async def __call__(self, request: Request) -> Optional[str]:
+ authorization = request.headers.get("Authorization")
+ scheme, param = get_authorization_scheme_param(authorization)
+
+ if authorization and scheme.lower() == "bearer":
+ return param
+
+ if self.__auto_error__:
+ raise HTTPException(
+ status_code=HTTP_401_UNAUTHORIZED,
+ detail="Not authenticated",
+ headers={"WWW-Authenticate": "Bearer"},
+ )
+
+ return None
+
+
+class OAuth2AuthorizationCodeBearer(OAuth2):
+ """
+ Implements the OAuth2 authorization code flow for obtaining a bearer token.
+
+ This class is used to handle authentication by exchanging an authorization
+ code for an access token. An instance of `OAuth2AuthorizationCodeBearer` can
+ be used as a dependency to secure endpoint access, ensuring users are
+ authenticated via an OAuth2 authorization code flow.
+ """
+
+ def __init__(
+ self,
+ authorizationUrl: str,
+ tokenUrl: Annotated[
+ str,
+ Doc(
+ """
+ The URL endpoint to exchange the authorization code for an OAuth2 access token.
+
+ This URL should point to the token endpoint in the OAuth2 provider's
+ API, enabling users to obtain a bearer token after the authorization
+ code is provided.
+ """
+ ),
+ ],
+ refreshUrl: Annotated[
+ Optional[str],
+ Doc(
+ """
+ Optional URL endpoint for refreshing the OAuth2 access token.
+
+ When provided, this URL allows users to renew an expired access token
+ without re-authenticating, improving usability and security. This
+ endpoint is part of the OAuth2 provider's API.
+ """
+ ),
+ ] = None,
+ scheme_name: Annotated[
+ Optional[str],
+ Doc(
+ """
+ The name of the OAuth2 security scheme.
+
+ This name will be displayed in the OpenAPI documentation, identifying
+ the authorization method used for API access.
+ """
+ ),
+ ] = None,
+ scopes: Annotated[
+ Optional[Dict[str, str]],
+ Doc(
+ """
+ Dictionary of OAuth2 scopes required for API access.
+
+ Scopes represent permissions requested by the application for specific
+ actions or data access. Each scope is represented by a key-value pair
+ where the key is the scope identifier, and the value is a brief
+ description of the scope's purpose.
+ """
+ ),
+ ] = None,
+ description: Annotated[
+ Optional[str],
+ Doc(
+ """
+ Description of the OAuth2 security scheme for documentation purposes.
+
+ This text will appear in the OpenAPI documentation, providing context
+ on the authentication method used by this scheme and any relevant
+ details.
+ """
+ ),
+ ] = None,
+ auto_error: Annotated[
+ bool,
+ Doc(
+ """
+ Determines if the request should automatically error on authentication failure.
+
+ If set to `True` (default), requests without a valid Authorization
+ header will automatically return an error response. If `False`, the
+ request can continue without authentication, returning `None` as the
+ dependency result. This option is useful for optional authentication
+ or for scenarios where multiple authentication methods are permitted
+ (e.g., OAuth2 or session-based tokens).
+ """
+ ),
+ ] = True,
+ ):
+ if not scopes:
+ scopes = {}
+ flows = OAuthFlowsModel(
+ authorizationCode=cast(
+ Any,
+ {
+ "authorizationUrl": authorizationUrl,
+ "tokenUrl": tokenUrl,
+ "refreshUrl": refreshUrl,
+ "scopes": scopes,
+ },
+ )
+ )
+ super().__init__(
+ flows=flows,
+ scheme_name=scheme_name,
+ description=description,
+ auto_error=auto_error,
+ )
+
+ async def __call__(self, request: Request) -> Optional[str]:
+ authorization = request.headers.get("Authorization")
+ scheme, param = get_authorization_scheme_param(authorization)
+
+ if authorization and scheme.lower() == "bearer":
+ return param
+
+ if self.__auto_error__:
+ raise HTTPException(
+ status_code=HTTP_401_UNAUTHORIZED,
+ detail="Not authenticated",
+ headers={"WWW-Authenticate": "Bearer"},
+ )
+
+ return None
diff --git a/esmerald/security/open_id/__init__.py b/esmerald/security/open_id/__init__.py
new file mode 100644
index 00000000..e302d729
--- /dev/null
+++ b/esmerald/security/open_id/__init__.py
@@ -0,0 +1,3 @@
+from .openid_connect import OpenIdConnect
+
+__all__ = ["OpenIdConnect"]
diff --git a/esmerald/security/open_id/openid_connect.py b/esmerald/security/open_id/openid_connect.py
new file mode 100644
index 00000000..ad6bea66
--- /dev/null
+++ b/esmerald/security/open_id/openid_connect.py
@@ -0,0 +1,70 @@
+from typing import Any, Optional
+
+from lilya.exceptions import HTTPException
+from lilya.requests import Request
+from lilya.status import HTTP_403_FORBIDDEN
+from typing_extensions import Annotated, Doc
+
+from esmerald.openapi.models import OpenIdConnect as OpenIdConnectModel
+from esmerald.security.base import SecurityBase as SecurityBase
+
+
+class OpenIdConnect(SecurityBase):
+ def __init__(
+ self,
+ *,
+ openIdConnectUrl: Annotated[
+ str,
+ Doc(
+ """
+ The OpenID Connect URL.
+ """
+ ),
+ ],
+ scheme_name: Annotated[
+ Optional[str],
+ Doc(
+ """
+ The name of the security scheme.
+ """
+ ),
+ ] = None,
+ description: Annotated[
+ Optional[str],
+ Doc(
+ """
+ A description of the security scheme.
+ """
+ ),
+ ] = None,
+ auto_error: Annotated[
+ bool,
+ Doc(
+ """
+ Determines the behavior when the HTTP Authorization header is missing.
+
+ If set to `True` (default), the request will be automatically canceled and an error will be sent to the client if the header is not provided.
+
+ If set to `False`, the dependency result will be `None` when the header is not available, allowing for optional authentication or multiple authentication methods (e.g., OpenID Connect or a cookie).
+ """
+ ),
+ ] = True,
+ ):
+ model = OpenIdConnectModel(
+ openIdConnectUrl=openIdConnectUrl, description=description, scheme=scheme_name
+ )
+ model_dump = model.model_dump()
+ super().__init__(**model_dump)
+ self.scheme_name = scheme_name or self.__class__.__name__
+ self.__auto_error__ = auto_error
+
+ async def __call__(self, request: Request) -> Any:
+ authorization = request.headers.get("Authorization")
+
+ if authorization:
+ return authorization
+
+ if self.__auto_error__:
+ raise HTTPException(status_code=HTTP_403_FORBIDDEN, detail="Not authenticated")
+
+ return None
diff --git a/esmerald/security/utils.py b/esmerald/security/utils.py
index 6e92f553..54902cb7 100644
--- a/esmerald/security/utils.py
+++ b/esmerald/security/utils.py
@@ -1,4 +1,5 @@
from datetime import datetime, timezone
+from typing import Optional, Tuple
def convert_time(date: datetime) -> datetime:
@@ -8,3 +9,12 @@ def convert_time(date: datetime) -> datetime:
if date.tzinfo is not None:
date.astimezone(timezone.utc)
return date.replace(microsecond=0)
+
+
+def get_authorization_scheme_param(
+ authorization_header_value: Optional[str],
+) -> Tuple[str, str]:
+ if not authorization_header_value:
+ return "", ""
+ scheme, _, param = authorization_header_value.partition(" ")
+ return scheme, param
diff --git a/esmerald/transformers/model.py b/esmerald/transformers/model.py
index 24ef16c4..a8b12887 100644
--- a/esmerald/transformers/model.py
+++ b/esmerald/transformers/model.py
@@ -5,7 +5,7 @@
from esmerald.context import Context
from esmerald.enums import EncodingType, ParamType
from esmerald.exceptions import ImproperlyConfigured
-from esmerald.params import Body
+from esmerald.params import Body, Security
from esmerald.parsers import ArbitraryExtraBaseModel, parse_form_data
from esmerald.requests import Request
from esmerald.transformers.signature import SignatureModel
@@ -17,7 +17,9 @@
get_signature,
merge_sets,
)
+from esmerald.typing import Undefined
from esmerald.utils.constants import CONTEXT, DATA, PAYLOAD, RESERVED_KWARGS
+from esmerald.utils.dependencies import is_security_scheme
from esmerald.utils.schema import is_field_optional
if TYPE_CHECKING:
@@ -27,6 +29,7 @@
MEDIA_TYPES = [EncodingType.MULTI_PART, EncodingType.URL_ENCODED]
MappingUnion = Mapping[Union[int, str], Any]
+PydanticUndefined = Undefined
class TransformerModel(ArbitraryExtraBaseModel):
@@ -115,6 +118,15 @@ def get_header_params(self) -> Set[ParamSetting]:
"""
return self.headers
+ def get_security_params(self) -> Dict[str, ParamSetting]:
+ """
+ Get header parameters.
+
+ Returns:
+ Set[ParamSetting]: Set of header parameters.
+ """
+ return {field.field_name: field for field in self.get_query_params() if field.is_security}
+
def to_kwargs(
self,
connection: Union["WebSocket", "Request"],
@@ -193,8 +205,22 @@ def get_request_context(
"""
return Context(__handler__=handler, __request__=request)
+ async def get_for_security_dependencies(
+ self, connection: Union["Request", "WebSocket"], kwargs: Any
+ ) -> Any:
+ """
+ Checks if the class has security dependencies.
+
+ Returns:
+ bool: True if security dependencies are present, False otherwise.
+ """
+ for name, dependency in kwargs.items():
+ if isinstance(dependency, Security):
+ kwargs[name] = await dependency.dependency(connection)
+ return kwargs
+
async def get_dependencies(
- self, dependency: Dependency, connection: Union["WebSocket", Request], **kwargs: Any
+ self, dependency: Dependency, connection: Union["WebSocket", "Request"], **kwargs: Any
) -> Any:
"""
Get dependencies asynchronously.
@@ -212,7 +238,11 @@ async def get_dependencies(
kwargs[_dependency.key] = await self.get_dependencies(
dependency=_dependency, connection=connection, **kwargs
)
- dependency_kwargs = signature_model.parse_values_for_connection(
+
+ if kwargs:
+ kwargs = await self.get_for_security_dependencies(connection, kwargs)
+
+ dependency_kwargs = await signature_model.parse_values_for_connection(
connection=connection, **kwargs
)
return await dependency.inject(**dependency_kwargs)
@@ -226,7 +256,7 @@ def to_dict(self) -> Dict[str, Any]:
"""
return {
"cookies": [param.dict() for param in self.cookies],
- "dependencies": [dep.dict() for dep in self.dependencies],
+ "dependencies": [dep.model_dump() for dep in self.dependencies],
"form_data": self.form_data,
"headers": [param.dict() for param in self.headers],
"path_params": [param.dict() for param in self.path_params],
@@ -330,7 +360,7 @@ def handle_reserved_kwargs(
return {**reserved_kwargs, **path_params, **query_params, **headers, **cookies}
-def dependency_tree(key: str, dependencies: "Dependencies") -> Dependency:
+def dependency_tree(key: str, dependencies: "Dependencies", first_run: bool = True) -> Dependency:
"""
Recursively build a dependency tree starting from a given key.
@@ -341,13 +371,17 @@ def dependency_tree(key: str, dependencies: "Dependencies") -> Dependency:
Returns:
Dependency: Constructed dependency tree starting from the specified key.
"""
+
inject = dependencies[key]
- dependency_keys = [key for key in get_signature(inject).model_fields if key in dependencies]
+ inject_signature = get_signature(inject)
+ dependency_keys = [key for key in inject_signature.model_fields if key in dependencies]
+
return Dependency(
key=key,
inject=inject,
dependencies=[
- dependency_tree(key=key, dependencies=dependencies) for key in dependency_keys
+ dependency_tree(key=key, dependencies=dependencies, first_run=False)
+ for key in dependency_keys
],
)
@@ -381,12 +415,14 @@ def get_parameter_settings(
for field_name, model_field in signature_fields.items():
if field_name not in ignored_keys:
allow_none = getattr(model_field, "allow_none", True)
+ is_security = is_security_scheme(model_field.default)
parameter_definitions.add(
create_parameter_setting(
allow_none=allow_none,
field_name=field_name,
field_info=model_field,
path_parameters=path_parameters,
+ is_security=is_security,
)
)
diff --git a/esmerald/transformers/signature.py b/esmerald/transformers/signature.py
index f48cc411..0b18e0e4 100644
--- a/esmerald/transformers/signature.py
+++ b/esmerald/transformers/signature.py
@@ -18,6 +18,7 @@
from orjson import loads
from pydantic import ValidationError, create_model
+from pydantic.fields import FieldInfo
from esmerald.encoders import LILYA_ENCODER_TYPES, Encoder
from esmerald.exceptions import (
@@ -31,8 +32,9 @@
from esmerald.transformers.constants import CLASS_SPECIAL_WORDS, UNDEFINED, VALIDATION_NAMES
from esmerald.transformers.utils import get_connection_info, get_field_definition_from_param
from esmerald.typing import Undefined
-from esmerald.utils.dependency import is_dependency_field, should_skip_dependency_validation
+from esmerald.utils.constants import IS_DEPENDENCY, SKIP_VALIDATION
from esmerald.utils.helpers import is_optional_union
+from esmerald.utils.schema import extract_arguments
from esmerald.websockets import WebSocket
if TYPE_CHECKING:
@@ -59,6 +61,16 @@ def is_server_error(error: Any, klass: Type["SignatureModel"]) -> bool:
return False
+def is_dependency_field(val: Any) -> bool:
+ json_schema_extra = getattr(val, "json_schema_extra", None) or {}
+ return bool(isinstance(val, FieldInfo) and bool(json_schema_extra.get(IS_DEPENDENCY)))
+
+
+def should_skip_dependency_validation(val: Any) -> bool:
+ json_schema_extra = getattr(val, "json_schema_extra", None) or {}
+ return bool(is_dependency_field(val) and json_schema_extra.get(SKIP_VALIDATION))
+
+
class Parameter(ArbitraryBaseModel):
"""
Represents a function parameter with associated metadata.
@@ -142,7 +154,7 @@ class SignatureModel(ArbitraryBaseModel):
encoders: ClassVar[Dict["Encoder", Any]]
@classmethod
- def parse_encoders(cls, kwargs: Dict[str, Any]) -> Dict[str, Any]:
+ async def parse_encoders(cls, kwargs: Dict[str, Any]) -> Dict[str, Any]:
"""
Parses the kwargs into a proper structure for the encoder itself.
@@ -178,12 +190,21 @@ def encode_value(encoder: "Encoder", annotation: Any, value: Any) -> Any:
encoder_info: Dict[str, "Encoder"] = cls.encoders[key] # type: ignore
encoder: "Encoder" = encoder_info["encoder"]
annotation = encoder_info["annotation"]
+
+ if is_optional_union(annotation) and not value:
+ kwargs[key] = None
+ continue
+
+ if is_optional_union(annotation) and value:
+ decoded_list = extract_arguments(annotation)
+ annotation = decoded_list[0] # type: ignore
+
kwargs[key] = encode_value(encoder, annotation, value)
return kwargs
@classmethod
- def parse_values_for_connection(
+ async def parse_values_for_connection(
cls, connection: Union[Request, WebSocket], **kwargs: Dict[str, Any]
) -> Any:
"""
@@ -203,7 +224,7 @@ def parse_values_for_connection(
"""
try:
if cls.encoders:
- kwargs = cls.parse_encoders(kwargs)
+ kwargs = await cls.parse_encoders(kwargs)
signature = cls(**kwargs)
values = {}
for key in cls.model_fields:
diff --git a/esmerald/transformers/utils.py b/esmerald/transformers/utils.py
index 6619005f..c6d1a32f 100644
--- a/esmerald/transformers/utils.py
+++ b/esmerald/transformers/utils.py
@@ -40,6 +40,7 @@ class ParamSetting(NamedTuple):
is_required: bool
param_type: ParamType
field_info: FieldInfo
+ is_security: bool = False
class Dependency(HashableBaseModel, ArbitraryExtraBaseModel):
@@ -111,6 +112,7 @@ def create_parameter_setting(
field_info: FieldInfo,
field_name: str,
path_parameters: Set[str],
+ is_security: bool,
) -> ParamSetting:
"""
Create a setting definition for a parameter.
@@ -161,6 +163,7 @@ def create_parameter_setting(
field_name=field_name,
field_info=param,
is_required=is_required and (default_value is None and not allow_none),
+ is_security=is_security,
)
return param_settings
diff --git a/esmerald/typing.py b/esmerald/typing.py
index 7ec862b6..a70a2f31 100644
--- a/esmerald/typing.py
+++ b/esmerald/typing.py
@@ -1,4 +1,4 @@
-from typing import Any, Callable, Type, TypeVar
+from typing import Any, Callable, Dict, Set, Type, TypeVar, Union
from pydantic_core import PydanticUndefined
@@ -12,3 +12,4 @@ class Void:
VoidType = Type[Void]
AnyCallable = Callable[..., Any]
Undefined = PydanticUndefined
+IncEx = Union[Set[int], Set[str], Dict[int, Any], Dict[str, Any]]
diff --git a/esmerald/utils/dependencies.py b/esmerald/utils/dependencies.py
new file mode 100644
index 00000000..3c46d908
--- /dev/null
+++ b/esmerald/utils/dependencies.py
@@ -0,0 +1,29 @@
+from typing import Any
+
+from esmerald import params
+from esmerald.utils.helpers import is_class_and_subclass
+
+
+def is_requires_scheme(param: Any) -> bool:
+ """
+ Checks if the object is a security scheme.
+ """
+ return is_class_and_subclass(param, params.Requires)
+
+
+def is_security_scheme(param: Any) -> bool:
+ """
+ Checks if the object is a security scheme.
+ """
+ if not param:
+ return False
+ return isinstance(param, params.Security)
+
+
+def is_inject(param: Any) -> bool:
+ """
+ Checks if the object is an Inject.
+ """
+ from esmerald.injector import Inject
+
+ return isinstance(param, Inject)
diff --git a/esmerald/utils/dependency.py b/esmerald/utils/dependency.py
deleted file mode 100644
index effd96d0..00000000
--- a/esmerald/utils/dependency.py
+++ /dev/null
@@ -1,15 +0,0 @@
-from typing import Any
-
-from pydantic.fields import FieldInfo
-
-from esmerald.utils.constants import IS_DEPENDENCY, SKIP_VALIDATION
-
-
-def is_dependency_field(val: Any) -> bool:
- json_schema_extra = getattr(val, "json_schema_extra", None) or {}
- return bool(isinstance(val, FieldInfo) and bool(json_schema_extra.get(IS_DEPENDENCY)))
-
-
-def should_skip_dependency_validation(val: Any) -> bool:
- json_schema_extra = getattr(val, "json_schema_extra", None) or {}
- return bool(is_dependency_field(val) and json_schema_extra.get(SKIP_VALIDATION))
diff --git a/esmerald/utils/schema.py b/esmerald/utils/schema.py
index 80ab8610..1ce25c50 100644
--- a/esmerald/utils/schema.py
+++ b/esmerald/utils/schema.py
@@ -52,3 +52,28 @@ def should_skip_json_schema(field_info: Union[FieldInfo, Any]) -> FieldInfo:
arguments = tuple(arguments) # type: ignore
field_info.annotation = Union[arguments]
return field_info
+
+
+def extract_arguments(
+ param: Union[Any, None] = None, argument_list: Union[List[Any], None] = None
+) -> List[Type[type]]:
+ """
+ Recursively extracts unique types from a parameter's type annotation.
+
+ Args:
+ param (Union[Parameter, None], optional): The parameter with type annotation to extract from.
+ argument_list (Union[List[Any], None], optional): The list of arguments extracted so far.
+
+ Returns:
+ List[Type[type]]: A list of unique types extracted from the parameter's type annotation.
+ """
+ arguments: List[Any] = list(argument_list) if argument_list is not None else []
+ args = get_args(param)
+
+ for arg in args:
+ if isinstance(arg, _GenericAlias):
+ arguments.extend(extract_arguments(param=arg, argument_list=arguments))
+ else:
+ if arg not in arguments:
+ arguments.append(arg)
+ return arguments
diff --git a/pyproject.toml b/pyproject.toml
index ca4e7652..b1a61876 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -41,9 +41,9 @@ dependencies = [
"email-validator >=2.2.0,<3.0.0",
"itsdangerous>=2.1.2,<3.0.0",
"jinja2>=3.1.2,<4.0.0",
- "lilya>=0.11.2",
+ "lilya>=0.11.6",
"loguru>=0.7.0,<0.8.0",
- "pydantic>=2.9.1,<3.0.0",
+ "pydantic>=2.10,<3.0.0",
"pydantic-settings>=2.0.0,<3.0.0",
"python-multipart>=0.0.7",
"python-slugify>=8.0.4,<10.0.0",
@@ -103,7 +103,7 @@ testing = [
"ujson>=5.7.0,<6",
"anyio[trio]>=3.6.2,<5.0.0",
"brotli>=1.0.9,<2.0.0",
- "edgy[postgres]>=0.16.0",
+ "edgy[postgres]>=0.23.3",
"databasez>=0.9.7",
"flask>=1.1.2,<4.0.0",
"freezegun>=1.2.2,<2.0.0",
diff --git a/tests/_security/open_api_classes/test_security_all.py b/tests/_security/open_api_classes/test_security_all.py
deleted file mode 100644
index 1db991e0..00000000
--- a/tests/_security/open_api_classes/test_security_all.py
+++ /dev/null
@@ -1,281 +0,0 @@
-from typing import Dict, List, Union
-
-from pydantic import BaseModel
-
-from esmerald import APIView, Gateway, JSONResponse, get
-from esmerald.openapi.security.api_key import APIKeyInCookie, APIKeyInHeader, APIKeyInQuery
-from esmerald.openapi.security.http import Basic, Bearer, Digest
-from esmerald.testclient import create_client
-from tests.settings import TestSettings
-
-
-class Error(BaseModel):
- status: int
- detail: str
-
-
-class CustomResponse(BaseModel):
- status: str
- title: str
- errors: List[Error]
-
-
-class JsonResponse(JSONResponse):
- media_type: str = "application/vnd.api+json"
-
-
-class Item(BaseModel):
- sku: Union[int, str]
-
-
-def test_security_api_key_in_cookie():
- class TestAPI(APIView):
- security = [
- Basic,
- Basic(),
- Bearer,
- Bearer(),
- Digest,
- Digest(),
- APIKeyInHeader,
- APIKeyInHeader(),
- APIKeyInHeader(name="X_TOKEN_HEADER"),
- APIKeyInQuery,
- APIKeyInQuery(),
- APIKeyInQuery(name="X_TOKEN_QUERY"),
- APIKeyInCookie,
- APIKeyInCookie(),
- APIKeyInCookie(name="X_TOKEN_COOKIE"),
- ]
-
- @get("/{pk:int}", response_class=JsonResponse)
- def read_people(self, pk: int) -> Dict[str, str]:
- """ """
-
- with create_client(
- routes=[Gateway(handler=TestAPI)],
- enable_openapi=True,
- include_in_schema=True,
- settings_module=TestSettings,
- ) as client:
- response = client.get("/openapi.json")
-
- assert response.json() == {
- "openapi": "3.1.0",
- "info": {
- "title": "Esmerald",
- "summary": "Esmerald application",
- "description": "Highly scalable, performant, easy to learn and for every application.",
- "contact": {"name": "admin", "email": "admin@myapp.com"},
- "version": client.app.version,
- },
- "servers": [{"url": "/"}],
- "paths": {
- "/{pk}": {
- "get": {
- "summary": "Read People",
- "description": "",
- "operationId": "testapi_read_people__pk__get",
- "deprecated": False,
- "security": [
- {
- "Basic": {
- "type": "http",
- "name": "Basic",
- "in": "header",
- "scheme": "basic",
- "scheme_name": "Basic",
- }
- },
- {
- "Basic": {
- "type": "http",
- "name": "Basic",
- "in": "header",
- "scheme": "basic",
- "scheme_name": "Basic",
- }
- },
- {
- "Bearer": {
- "type": "http",
- "name": "Authorization",
- "in": "header",
- "scheme": "bearer",
- "scheme_name": "Bearer",
- }
- },
- {
- "Bearer": {
- "type": "http",
- "name": "Authorization",
- "in": "header",
- "scheme": "bearer",
- "scheme_name": "Bearer",
- }
- },
- {
- "Digest": {
- "type": "http",
- "name": "Authorization",
- "in": "header",
- "scheme": "digest",
- "scheme_name": "Digest",
- }
- },
- {
- "Digest": {
- "type": "http",
- "name": "Authorization",
- "in": "header",
- "scheme": "digest",
- "scheme_name": "Digest",
- }
- },
- {
- "APIKeyInHeader": {
- "type": "apiKey",
- "in": "header",
- "scheme_name": "APIKeyInHeader",
- }
- },
- {
- "APIKeyInHeader": {
- "type": "apiKey",
- "in": "header",
- "scheme_name": "APIKeyInHeader",
- }
- },
- {
- "APIKeyInHeader": {
- "type": "apiKey",
- "name": "X_TOKEN_HEADER",
- "in": "header",
- "scheme_name": "APIKeyInHeader",
- }
- },
- {
- "APIKeyInQuery": {
- "type": "apiKey",
- "in": "query",
- "scheme_name": "APIKeyInQuery",
- }
- },
- {
- "APIKeyInQuery": {
- "type": "apiKey",
- "in": "query",
- "scheme_name": "APIKeyInQuery",
- }
- },
- {
- "APIKeyInQuery": {
- "type": "apiKey",
- "name": "X_TOKEN_QUERY",
- "in": "query",
- "scheme_name": "APIKeyInQuery",
- }
- },
- {
- "APIKeyInCookie": {
- "type": "apiKey",
- "in": "cookie",
- "scheme_name": "APIKeyInCookie",
- }
- },
- {
- "APIKeyInCookie": {
- "type": "apiKey",
- "in": "cookie",
- "scheme_name": "APIKeyInCookie",
- }
- },
- {
- "APIKeyInCookie": {
- "type": "apiKey",
- "name": "X_TOKEN_COOKIE",
- "in": "cookie",
- "scheme_name": "APIKeyInCookie",
- }
- },
- ],
- "parameters": [
- {
- "name": "pk",
- "in": "path",
- "required": True,
- "deprecated": False,
- "allowEmptyValue": False,
- "allowReserved": False,
- "schema": {"type": "integer", "title": "Pk"},
- }
- ],
- "responses": {
- "200": {
- "description": "Successful response",
- "content": {
- "application/vnd.api+json": {"schema": {"type": "string"}}
- },
- },
- "422": {
- "description": "Validation Error",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/HTTPValidationError"
- }
- }
- },
- },
- },
- }
- }
- },
- "components": {
- "schemas": {
- "HTTPValidationError": {
- "properties": {
- "detail": {
- "items": {"$ref": "#/components/schemas/ValidationError"},
- "type": "array",
- "title": "Detail",
- }
- },
- "type": "object",
- "title": "HTTPValidationError",
- },
- "ValidationError": {
- "properties": {
- "loc": {
- "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]},
- "type": "array",
- "title": "Location",
- },
- "msg": {"type": "string", "title": "Message"},
- "type": {"type": "string", "title": "Error Type"},
- },
- "type": "object",
- "required": ["loc", "msg", "type"],
- "title": "ValidationError",
- },
- },
- "securitySchemes": {
- "Basic": {"type": "http", "name": "Basic", "in": "header", "scheme": "basic"},
- "Bearer": {
- "type": "http",
- "name": "Authorization",
- "in": "header",
- "scheme": "bearer",
- },
- "Digest": {
- "type": "http",
- "name": "Authorization",
- "in": "header",
- "scheme": "digest",
- },
- "APIKeyInHeader": {"type": "apiKey", "name": "X_TOKEN_HEADER", "in": "header"},
- "APIKeyInQuery": {"type": "apiKey", "name": "X_TOKEN_QUERY", "in": "query"},
- "APIKeyInCookie": {"type": "apiKey", "name": "X_TOKEN_COOKIE", "in": "cookie"},
- },
- },
- }
diff --git a/tests/_security/open_api_classes/test_security_api_key_in_cookie.py b/tests/_security/open_api_classes/test_security_api_key_in_cookie.py
deleted file mode 100644
index 76e88002..00000000
--- a/tests/_security/open_api_classes/test_security_api_key_in_cookie.py
+++ /dev/null
@@ -1,162 +0,0 @@
-from typing import Dict, List, Union
-
-import pytest
-from pydantic import BaseModel
-
-from esmerald import APIView, Gateway, JSONResponse, get
-from esmerald.openapi.security.api_key import APIKeyInCookie
-from esmerald.testclient import create_client
-from tests.settings import TestSettings
-
-
-class Error(BaseModel):
- status: int
- detail: str
-
-
-class CustomResponse(BaseModel):
- status: str
- title: str
- errors: List[Error]
-
-
-class JsonResponse(JSONResponse):
- media_type: str = "application/vnd.api+json"
-
-
-class Item(BaseModel):
- sku: Union[int, str]
-
-
-@pytest.mark.parametrize("auth", [APIKeyInCookie, APIKeyInCookie()])
-def test_security_api_key_in_cookie(auth):
- class TestAPIView(APIView):
- security = [auth]
-
- @get(
- response_class=JsonResponse,
- )
- def read_people(self) -> Dict[str, str]:
- """ """
-
- with create_client(
- routes=[Gateway(handler=TestAPIView)],
- enable_openapi=True,
- include_in_schema=True,
- settings_module=TestSettings,
- ) as client:
- response = client.get("/openapi.json")
- assert response.json() == {
- "openapi": "3.1.0",
- "info": {
- "title": "Esmerald",
- "summary": "Esmerald application",
- "description": "Highly scalable, performant, easy to learn and for every application.",
- "contact": {"name": "admin", "email": "admin@myapp.com"},
- "version": client.app.version,
- },
- "servers": [{"url": "/"}],
- "paths": {
- "/": {
- "get": {
- "summary": "Read People",
- "description": "",
- "operationId": "testapiview_read_people__get",
- "deprecated": False,
- "security": [
- {
- "APIKeyInCookie": {
- "type": "apiKey",
- "in": "cookie",
- "scheme_name": "APIKeyInCookie",
- }
- }
- ],
- "responses": {
- "200": {
- "description": "Successful response",
- "content": {
- "application/vnd.api+json": {"schema": {"type": "string"}}
- },
- }
- },
- }
- }
- },
- "components": {
- "securitySchemes": {"APIKeyInCookie": {"type": "apiKey", "in": "cookie"}}
- },
- }
-
-
-@pytest.mark.parametrize(
- "token,value",
- [
- (APIKeyInCookie(name="X_API_TOKEN"), "X_API_TOKEN"),
- (APIKeyInCookie(name="X_TOKEN"), "X_TOKEN"),
- (APIKeyInCookie(name="test"), "test"),
- ],
- ids=["x-api-token", "x-token", "test"],
-)
-def test_security_api_key_in_cookie_value(token, value):
-
- class TestAPIView(APIView):
- security = [token]
-
- @get(
- response_class=JsonResponse,
- )
- def read_people(self) -> Dict[str, str]:
- """ """
-
- with create_client(
- routes=[Gateway(handler=TestAPIView)],
- enable_openapi=True,
- include_in_schema=True,
- ) as client:
- response = client.get("/openapi.json")
-
- assert response.json() == {
- "openapi": "3.1.0",
- "info": {
- "title": "Esmerald",
- "summary": "Esmerald application",
- "description": "Highly scalable, performant, easy to learn and for every application.",
- "contact": {"name": "admin", "email": "admin@myapp.com"},
- "version": client.app.version,
- },
- "servers": [{"url": "/"}],
- "paths": {
- "/": {
- "get": {
- "summary": "Read People",
- "description": "",
- "operationId": "testapiview_read_people__get",
- "deprecated": False,
- "security": [
- {
- "APIKeyInCookie": {
- "type": "apiKey",
- "name": value,
- "in": "cookie",
- "scheme_name": "APIKeyInCookie",
- }
- }
- ],
- "responses": {
- "200": {
- "description": "Successful response",
- "content": {
- "application/vnd.api+json": {"schema": {"type": "string"}}
- },
- }
- },
- }
- }
- },
- "components": {
- "securitySchemes": {
- "APIKeyInCookie": {"type": "apiKey", "name": value, "in": "cookie"}
- }
- },
- }
diff --git a/tests/_security/open_api_classes/test_security_api_key_in_header.py b/tests/_security/open_api_classes/test_security_api_key_in_header.py
deleted file mode 100644
index 302c8a91..00000000
--- a/tests/_security/open_api_classes/test_security_api_key_in_header.py
+++ /dev/null
@@ -1,164 +0,0 @@
-from typing import Dict, List, Union
-
-import pytest
-from pydantic import BaseModel
-
-from esmerald import APIView, Gateway, JSONResponse, get
-from esmerald.openapi.security.api_key import APIKeyInHeader
-from esmerald.testclient import create_client
-from tests.settings import TestSettings
-
-
-class Error(BaseModel):
- status: int
- detail: str
-
-
-class CustomResponse(BaseModel):
- status: str
- title: str
- errors: List[Error]
-
-
-class JsonResponse(JSONResponse):
- media_type: str = "application/vnd.api+json"
-
-
-class Item(BaseModel):
- sku: Union[int, str]
-
-
-@pytest.mark.parametrize("auth", [APIKeyInHeader, APIKeyInHeader()])
-def test_security_api_key_in_header(auth):
-
- class TestAPIView(APIView):
- security = [auth]
-
- @get(
- response_class=JsonResponse,
- )
- def read_people(self) -> Dict[str, str]:
- """ """
-
- with create_client(
- routes=[Gateway(handler=TestAPIView)],
- enable_openapi=True,
- include_in_schema=True,
- settings_module=TestSettings(),
- ) as client:
- response = client.get("/openapi.json")
-
- assert response.json() == {
- "openapi": "3.1.0",
- "info": {
- "title": "Esmerald",
- "summary": "Esmerald application",
- "description": "Highly scalable, performant, easy to learn and for every application.",
- "contact": {"name": "admin", "email": "admin@myapp.com"},
- "version": client.app.version,
- },
- "servers": [{"url": "/"}],
- "paths": {
- "/": {
- "get": {
- "summary": "Read People",
- "description": "",
- "operationId": "testapiview_read_people__get",
- "deprecated": False,
- "security": [
- {
- "APIKeyInHeader": {
- "type": "apiKey",
- "in": "header",
- "scheme_name": "APIKeyInHeader",
- }
- }
- ],
- "responses": {
- "200": {
- "description": "Successful response",
- "content": {
- "application/vnd.api+json": {"schema": {"type": "string"}}
- },
- }
- },
- }
- }
- },
- "components": {
- "securitySchemes": {"APIKeyInHeader": {"type": "apiKey", "in": "header"}}
- },
- }
-
-
-@pytest.mark.parametrize(
- "token,value",
- [
- (APIKeyInHeader(name="X_API_TOKEN"), "X_API_TOKEN"),
- (APIKeyInHeader(name="X_TOKEN"), "X_TOKEN"),
- (APIKeyInHeader(name="test"), "test"),
- ],
- ids=["x-api-token", "x-token", "test"],
-)
-def test_security_api_key_header_value(token, value):
-
- class TestAPIView(APIView):
- security = [token]
-
- @get(
- response_class=JsonResponse,
- )
- def read_people(self) -> Dict[str, str]:
- """ """
-
- with create_client(
- routes=[Gateway(handler=TestAPIView)],
- enable_openapi=True,
- include_in_schema=True,
- ) as client:
- response = client.get("/openapi.json")
-
- assert response.json() == {
- "openapi": "3.1.0",
- "info": {
- "title": "Esmerald",
- "summary": "Esmerald application",
- "description": "Highly scalable, performant, easy to learn and for every application.",
- "contact": {"name": "admin", "email": "admin@myapp.com"},
- "version": client.app.version,
- },
- "servers": [{"url": "/"}],
- "paths": {
- "/": {
- "get": {
- "summary": "Read People",
- "description": "",
- "operationId": "testapiview_read_people__get",
- "deprecated": False,
- "security": [
- {
- "APIKeyInHeader": {
- "type": "apiKey",
- "name": value,
- "in": "header",
- "scheme_name": "APIKeyInHeader",
- }
- }
- ],
- "responses": {
- "200": {
- "description": "Successful response",
- "content": {
- "application/vnd.api+json": {"schema": {"type": "string"}}
- },
- }
- },
- }
- }
- },
- "components": {
- "securitySchemes": {
- "APIKeyInHeader": {"type": "apiKey", "name": value, "in": "header"}
- }
- },
- }
diff --git a/tests/_security/open_api_classes/test_security_api_key_in_query.py b/tests/_security/open_api_classes/test_security_api_key_in_query.py
deleted file mode 100644
index 79433e25..00000000
--- a/tests/_security/open_api_classes/test_security_api_key_in_query.py
+++ /dev/null
@@ -1,162 +0,0 @@
-from typing import Dict, List, Union
-
-import pytest
-from pydantic import BaseModel
-
-from esmerald import APIView, Gateway, JSONResponse, get
-from esmerald.openapi.security.api_key import APIKeyInQuery
-from esmerald.testclient import create_client
-from tests.settings import TestSettings
-
-
-class Error(BaseModel):
- status: int
- detail: str
-
-
-class CustomResponse(BaseModel):
- status: str
- title: str
- errors: List[Error]
-
-
-class JsonResponse(JSONResponse):
- media_type: str = "application/vnd.api+json"
-
-
-class Item(BaseModel):
- sku: Union[int, str]
-
-
-@pytest.mark.parametrize("auth", [APIKeyInQuery, APIKeyInQuery()])
-def test_security_api_key_in_query(auth):
- class TestAPIView(APIView):
- security = [auth]
-
- @get(
- response_class=JsonResponse,
- )
- def read_people(self) -> Dict[str, str]:
- """ """
-
- with create_client(
- routes=[Gateway(handler=TestAPIView)],
- enable_openapi=True,
- include_in_schema=True,
- settings_module=TestSettings,
- ) as client:
- response = client.get("/openapi.json")
-
- assert response.json() == {
- "openapi": "3.1.0",
- "info": {
- "title": "Esmerald",
- "summary": "Esmerald application",
- "description": "Highly scalable, performant, easy to learn and for every application.",
- "contact": {"name": "admin", "email": "admin@myapp.com"},
- "version": client.app.version,
- },
- "servers": [{"url": "/"}],
- "paths": {
- "/": {
- "get": {
- "summary": "Read People",
- "description": "",
- "operationId": "testapiview_read_people__get",
- "deprecated": False,
- "security": [
- {
- "APIKeyInQuery": {
- "type": "apiKey",
- "in": "query",
- "scheme_name": "APIKeyInQuery",
- }
- }
- ],
- "responses": {
- "200": {
- "description": "Successful response",
- "content": {
- "application/vnd.api+json": {"schema": {"type": "string"}}
- },
- }
- },
- }
- }
- },
- "components": {
- "securitySchemes": {"APIKeyInQuery": {"type": "apiKey", "in": "query"}}
- },
- }
-
-
-@pytest.mark.parametrize(
- "token,value",
- [
- (APIKeyInQuery(name="Authorization"), "Authorization"),
- (APIKeyInQuery(name="X_TOKEN"), "X_TOKEN"),
- (APIKeyInQuery(name="test"), "test"),
- ],
- ids=["x-api-token", "x-token", "test"],
-)
-def test_security_api_key_in_query_value(token, value):
- class TestAPIView(APIView):
- security = [token]
-
- @get(
- response_class=JsonResponse,
- )
- def read_people() -> Dict[str, str]:
- """ """
-
- with create_client(
- routes=[Gateway(handler=TestAPIView)],
- enable_openapi=True,
- include_in_schema=True,
- ) as client:
- response = client.get("/openapi.json")
-
- assert response.json() == {
- "openapi": "3.1.0",
- "info": {
- "title": "Esmerald",
- "summary": "Esmerald application",
- "description": "Highly scalable, performant, easy to learn and for every application.",
- "contact": {"name": "admin", "email": "admin@myapp.com"},
- "version": client.app.version,
- },
- "servers": [{"url": "/"}],
- "paths": {
- "/": {
- "get": {
- "summary": "Read People",
- "description": "",
- "operationId": "testapiview_read_people__get",
- "deprecated": False,
- "security": [
- {
- "APIKeyInQuery": {
- "type": "apiKey",
- "name": value,
- "in": "query",
- "scheme_name": "APIKeyInQuery",
- }
- }
- ],
- "responses": {
- "200": {
- "description": "Successful response",
- "content": {
- "application/vnd.api+json": {"schema": {"type": "string"}}
- },
- }
- },
- }
- }
- },
- "components": {
- "securitySchemes": {
- "APIKeyInQuery": {"type": "apiKey", "name": value, "in": "query"}
- }
- },
- }
diff --git a/tests/_security/open_api_classes/test_security_basic.py b/tests/_security/open_api_classes/test_security_basic.py
deleted file mode 100644
index c6cbc948..00000000
--- a/tests/_security/open_api_classes/test_security_basic.py
+++ /dev/null
@@ -1,158 +0,0 @@
-from typing import Dict, List, Union
-
-import pytest
-from pydantic import BaseModel
-
-from esmerald import APIView, Gateway, JSONResponse, get
-from esmerald.openapi.security.http import Basic
-from esmerald.testclient import create_client
-from tests.settings import TestSettings
-
-
-class Error(BaseModel):
- status: int
- detail: str
-
-
-class CustomResponse(BaseModel):
- status: str
- title: str
- errors: List[Error]
-
-
-class JsonResponse(JSONResponse):
- media_type: str = "application/vnd.api+json"
-
-
-class Item(BaseModel):
- sku: Union[int, str]
-
-
-@get("/item/{id}")
-async def read_item(id: str) -> None:
- """ """
-
-
-@pytest.mark.parametrize("auth", [Basic, Basic()])
-def test_security_basic(auth):
- class TestAPIView(APIView):
- security = [auth]
-
- @get(
- response_class=JsonResponse,
- )
- def read_people(self) -> Dict[str, str]:
- """ """
-
- with create_client(
- routes=[Gateway(handler=read_item), Gateway(handler=TestAPIView)],
- enable_openapi=True,
- include_in_schema=True,
- settings_module=TestSettings(),
- ) as client:
- response = client.get("/openapi.json")
-
- assert response.json() == {
- "openapi": "3.1.0",
- "info": {
- "title": "Esmerald",
- "summary": "Esmerald application",
- "description": "Highly scalable, performant, easy to learn and for every application.",
- "contact": {"name": "admin", "email": "admin@myapp.com"},
- "version": client.app.version,
- },
- "servers": [{"url": "/"}],
- "paths": {
- "/item/{id}": {
- "get": {
- "summary": "Read Item",
- "description": "",
- "operationId": "read_item_item__id__get",
- "parameters": [
- {
- "name": "id",
- "in": "path",
- "required": True,
- "deprecated": False,
- "allowEmptyValue": False,
- "allowReserved": False,
- "schema": {"type": "string", "title": "Id"},
- }
- ],
- "responses": {
- "200": {"description": "Successful response"},
- "422": {
- "description": "Validation Error",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/HTTPValidationError"
- }
- }
- },
- },
- },
- "deprecated": False,
- }
- },
- "/": {
- "get": {
- "summary": "Read People",
- "description": "",
- "operationId": "testapiview_read_people__get",
- "deprecated": False,
- "security": [
- {
- "Basic": {
- "type": "http",
- "name": "Basic",
- "in": "header",
- "scheme": "basic",
- "scheme_name": "Basic",
- }
- }
- ],
- "responses": {
- "200": {
- "description": "Successful response",
- "content": {
- "application/vnd.api+json": {"schema": {"type": "string"}}
- },
- }
- },
- }
- },
- },
- "components": {
- "schemas": {
- "HTTPValidationError": {
- "properties": {
- "detail": {
- "items": {"$ref": "#/components/schemas/ValidationError"},
- "type": "array",
- "title": "Detail",
- }
- },
- "type": "object",
- "title": "HTTPValidationError",
- },
- "ValidationError": {
- "properties": {
- "loc": {
- "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]},
- "type": "array",
- "title": "Location",
- },
- "msg": {"type": "string", "title": "Message"},
- "type": {"type": "string", "title": "Error Type"},
- },
- "type": "object",
- "required": ["loc", "msg", "type"],
- "title": "ValidationError",
- },
- },
- "securitySchemes": {
- "Basic": {"type": "http", "name": "Basic", "in": "header", "scheme": "basic"}
- },
- },
- }
diff --git a/tests/_security/open_api_classes/test_security_digest.py b/tests/_security/open_api_classes/test_security_digest.py
deleted file mode 100644
index e9fb0f6f..00000000
--- a/tests/_security/open_api_classes/test_security_digest.py
+++ /dev/null
@@ -1,163 +0,0 @@
-from typing import Dict, List, Union
-
-import pytest
-from pydantic import BaseModel
-
-from esmerald import APIView, Gateway, JSONResponse, get
-from esmerald.openapi.security.http import Digest
-from esmerald.testclient import create_client
-from tests.settings import TestSettings
-
-
-class Error(BaseModel):
- status: int
- detail: str
-
-
-class CustomResponse(BaseModel):
- status: str
- title: str
- errors: List[Error]
-
-
-class JsonResponse(JSONResponse):
- media_type: str = "application/vnd.api+json"
-
-
-class Item(BaseModel):
- sku: Union[int, str]
-
-
-@get("/item/{id}")
-async def read_item(id: str) -> None:
- """ """
-
-
-@pytest.mark.parametrize("auth", [Digest, Digest()])
-def test_security_digest(auth):
- class TestAPIView(APIView):
- security = [auth]
-
- @get(
- response_class=JsonResponse,
- )
- def read_people(self) -> Dict[str, str]:
- """ """
-
- with create_client(
- routes=[Gateway(handler=read_item), Gateway(handler=TestAPIView)],
- enable_openapi=True,
- include_in_schema=True,
- settings_module=TestSettings(),
- ) as client:
- response = client.get("/openapi.json")
-
- assert response.json() == {
- "openapi": "3.1.0",
- "info": {
- "title": "Esmerald",
- "summary": "Esmerald application",
- "description": "Highly scalable, performant, easy to learn and for every application.",
- "contact": {"name": "admin", "email": "admin@myapp.com"},
- "version": client.app.version,
- },
- "servers": [{"url": "/"}],
- "paths": {
- "/item/{id}": {
- "get": {
- "summary": "Read Item",
- "description": "",
- "operationId": "read_item_item__id__get",
- "parameters": [
- {
- "name": "id",
- "in": "path",
- "required": True,
- "deprecated": False,
- "allowEmptyValue": False,
- "allowReserved": False,
- "schema": {"type": "string", "title": "Id"},
- }
- ],
- "responses": {
- "200": {"description": "Successful response"},
- "422": {
- "description": "Validation Error",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/HTTPValidationError"
- }
- }
- },
- },
- },
- "deprecated": False,
- }
- },
- "/": {
- "get": {
- "summary": "Read People",
- "description": "",
- "operationId": "testapiview_read_people__get",
- "deprecated": False,
- "security": [
- {
- "Digest": {
- "type": "http",
- "name": "Authorization",
- "in": "header",
- "scheme": "digest",
- "scheme_name": "Digest",
- }
- }
- ],
- "responses": {
- "200": {
- "description": "Successful response",
- "content": {
- "application/vnd.api+json": {"schema": {"type": "string"}}
- },
- }
- },
- }
- },
- },
- "components": {
- "schemas": {
- "HTTPValidationError": {
- "properties": {
- "detail": {
- "items": {"$ref": "#/components/schemas/ValidationError"},
- "type": "array",
- "title": "Detail",
- }
- },
- "type": "object",
- "title": "HTTPValidationError",
- },
- "ValidationError": {
- "properties": {
- "loc": {
- "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]},
- "type": "array",
- "title": "Location",
- },
- "msg": {"type": "string", "title": "Message"},
- "type": {"type": "string", "title": "Error Type"},
- },
- "type": "object",
- "required": ["loc", "msg", "type"],
- "title": "ValidationError",
- },
- },
- "securitySchemes": {
- "Digest": {
- "type": "http",
- "name": "Authorization",
- "in": "header",
- "scheme": "digest",
- }
- },
- },
- }
diff --git a/tests/_security/open_api_classes/test_security_token_bearer.py b/tests/_security/open_api_classes/test_security_token_bearer.py
deleted file mode 100644
index 1ef7a026..00000000
--- a/tests/_security/open_api_classes/test_security_token_bearer.py
+++ /dev/null
@@ -1,163 +0,0 @@
-from typing import Dict, List, Union
-
-import pytest
-from pydantic import BaseModel
-
-from esmerald import APIView, Gateway, JSONResponse, get
-from esmerald.openapi.security.http import Bearer
-from esmerald.testclient import create_client
-from tests.settings import TestSettings
-
-
-class Error(BaseModel):
- status: int
- detail: str
-
-
-class CustomResponse(BaseModel):
- status: str
- title: str
- errors: List[Error]
-
-
-class JsonResponse(JSONResponse):
- media_type: str = "application/vnd.api+json"
-
-
-class Item(BaseModel):
- sku: Union[int, str]
-
-
-@get("/item/{id}")
-async def read_item(id: str) -> None:
- """ """
-
-
-@pytest.mark.parametrize("auth", [Bearer, Bearer()])
-def test_security_token_bearer(auth):
- class TestAPIView(APIView):
- security = [auth]
-
- @get(
- response_class=JsonResponse,
- )
- def read_people(self) -> Dict[str, str]:
- """ """
-
- with create_client(
- routes=[Gateway(handler=read_item), Gateway(handler=TestAPIView)],
- enable_openapi=True,
- include_in_schema=True,
- settings_module=TestSettings,
- ) as client:
- response = client.get("/openapi.json")
-
- assert response.json() == {
- "openapi": "3.1.0",
- "info": {
- "title": "Esmerald",
- "summary": "Esmerald application",
- "description": "Highly scalable, performant, easy to learn and for every application.",
- "contact": {"name": "admin", "email": "admin@myapp.com"},
- "version": client.app.version,
- },
- "servers": [{"url": "/"}],
- "paths": {
- "/item/{id}": {
- "get": {
- "summary": "Read Item",
- "description": "",
- "operationId": "read_item_item__id__get",
- "parameters": [
- {
- "name": "id",
- "in": "path",
- "required": True,
- "deprecated": False,
- "allowEmptyValue": False,
- "allowReserved": False,
- "schema": {"type": "string", "title": "Id"},
- }
- ],
- "responses": {
- "200": {"description": "Successful response"},
- "422": {
- "description": "Validation Error",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/HTTPValidationError"
- }
- }
- },
- },
- },
- "deprecated": False,
- }
- },
- "/": {
- "get": {
- "summary": "Read People",
- "description": "",
- "operationId": "testapiview_read_people__get",
- "deprecated": False,
- "security": [
- {
- "Bearer": {
- "type": "http",
- "name": "Authorization",
- "in": "header",
- "scheme": "bearer",
- "scheme_name": "Bearer",
- }
- }
- ],
- "responses": {
- "200": {
- "description": "Successful response",
- "content": {
- "application/vnd.api+json": {"schema": {"type": "string"}}
- },
- }
- },
- }
- },
- },
- "components": {
- "schemas": {
- "HTTPValidationError": {
- "properties": {
- "detail": {
- "items": {"$ref": "#/components/schemas/ValidationError"},
- "type": "array",
- "title": "Detail",
- }
- },
- "type": "object",
- "title": "HTTPValidationError",
- },
- "ValidationError": {
- "properties": {
- "loc": {
- "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]},
- "type": "array",
- "title": "Location",
- },
- "msg": {"type": "string", "title": "Message"},
- "type": {"type": "string", "title": "Error Type"},
- },
- "type": "object",
- "required": ["loc", "msg", "type"],
- "title": "ValidationError",
- },
- },
- "securitySchemes": {
- "Bearer": {
- "type": "http",
- "name": "Authorization",
- "in": "header",
- "scheme": "bearer",
- }
- },
- },
- }
diff --git a/tests/_security/openapi_normal/test_security_all.py b/tests/_security/openapi_normal/test_security_all.py
deleted file mode 100644
index 7c8c9da9..00000000
--- a/tests/_security/openapi_normal/test_security_all.py
+++ /dev/null
@@ -1,250 +0,0 @@
-from typing import Dict, List, Union
-
-from pydantic import BaseModel
-
-from esmerald import Gateway, JSONResponse, get
-from esmerald.openapi.security.api_key import APIKeyInCookie, APIKeyInHeader, APIKeyInQuery
-from esmerald.openapi.security.http import Basic, Bearer, Digest
-from esmerald.testclient import create_client
-from tests.settings import TestSettings
-
-
-class Error(BaseModel):
- status: int
- detail: str
-
-
-class CustomResponse(BaseModel):
- status: str
- title: str
- errors: List[Error]
-
-
-class JsonResponse(JSONResponse):
- media_type: str = "application/vnd.api+json"
-
-
-class Item(BaseModel):
- sku: Union[int, str]
-
-
-def test_security_api_key_in_cookie():
- @get(
- response_class=JsonResponse,
- security=[
- Basic,
- Basic(),
- Bearer,
- Bearer(),
- Digest,
- Digest(),
- APIKeyInHeader,
- APIKeyInHeader(),
- APIKeyInHeader(name="X_TOKEN_HEADER"),
- APIKeyInQuery,
- APIKeyInQuery(),
- APIKeyInQuery(name="X_TOKEN_QUERY"),
- APIKeyInCookie,
- APIKeyInCookie(),
- APIKeyInCookie(name="X_TOKEN_COOKIE"),
- ],
- )
- def read_people() -> Dict[str, str]:
- """ """
-
- with create_client(
- routes=[Gateway(handler=read_people)],
- enable_openapi=True,
- include_in_schema=True,
- settings_module=TestSettings,
- ) as client:
- response = client.get("/openapi.json")
-
- assert response.json() == {
- "openapi": "3.1.0",
- "info": {
- "title": "Esmerald",
- "summary": "Esmerald application",
- "description": "Highly scalable, performant, easy to learn and for every application.",
- "contact": {"name": "admin", "email": "admin@myapp.com"},
- "version": client.app.version,
- },
- "servers": [{"url": "/"}],
- "paths": {
- "/": {
- "get": {
- "summary": "Read People",
- "description": "",
- "operationId": "read_people__get",
- "deprecated": False,
- "security": [
- {
- "Basic": {
- "type": "http",
- "name": "Basic",
- "in": "header",
- "scheme": "basic",
- "scheme_name": "Basic",
- }
- },
- {
- "Basic": {
- "type": "http",
- "name": "Basic",
- "in": "header",
- "scheme": "basic",
- "scheme_name": "Basic",
- }
- },
- {
- "Bearer": {
- "type": "http",
- "name": "Authorization",
- "in": "header",
- "scheme": "bearer",
- "scheme_name": "Bearer",
- }
- },
- {
- "Bearer": {
- "type": "http",
- "name": "Authorization",
- "in": "header",
- "scheme": "bearer",
- "scheme_name": "Bearer",
- }
- },
- {
- "Digest": {
- "type": "http",
- "name": "Authorization",
- "in": "header",
- "scheme": "digest",
- "scheme_name": "Digest",
- }
- },
- {
- "Digest": {
- "type": "http",
- "name": "Authorization",
- "in": "header",
- "scheme": "digest",
- "scheme_name": "Digest",
- }
- },
- {
- "APIKeyInHeader": {
- "type": "apiKey",
- "in": "header",
- "scheme_name": "APIKeyInHeader",
- }
- },
- {
- "APIKeyInHeader": {
- "type": "apiKey",
- "in": "header",
- "scheme_name": "APIKeyInHeader",
- }
- },
- {
- "APIKeyInHeader": {
- "type": "apiKey",
- "name": "X_TOKEN_HEADER",
- "in": "header",
- "scheme_name": "APIKeyInHeader",
- }
- },
- {
- "APIKeyInQuery": {
- "type": "apiKey",
- "in": "query",
- "scheme_name": "APIKeyInQuery",
- }
- },
- {
- "APIKeyInQuery": {
- "type": "apiKey",
- "in": "query",
- "scheme_name": "APIKeyInQuery",
- }
- },
- {
- "APIKeyInQuery": {
- "type": "apiKey",
- "name": "X_TOKEN_QUERY",
- "in": "query",
- "scheme_name": "APIKeyInQuery",
- }
- },
- {
- "APIKeyInCookie": {
- "type": "apiKey",
- "in": "cookie",
- "scheme_name": "APIKeyInCookie",
- }
- },
- {
- "APIKeyInCookie": {
- "type": "apiKey",
- "in": "cookie",
- "scheme_name": "APIKeyInCookie",
- }
- },
- {
- "APIKeyInCookie": {
- "type": "apiKey",
- "name": "X_TOKEN_COOKIE",
- "in": "cookie",
- "scheme_name": "APIKeyInCookie",
- }
- },
- ],
- "responses": {
- "200": {
- "description": "Successful response",
- "content": {
- "application/vnd.api+json": {"schema": {"type": "string"}}
- },
- }
- },
- }
- }
- },
- "components": {
- "securitySchemes": {
- "Basic": {
- "type": "http",
- "name": "Basic",
- "in": "header",
- "scheme": "basic",
- },
- "Bearer": {
- "type": "http",
- "name": "Authorization",
- "in": "header",
- "scheme": "bearer",
- },
- "Digest": {
- "type": "http",
- "name": "Authorization",
- "in": "header",
- "scheme": "digest",
- },
- "APIKeyInHeader": {
- "type": "apiKey",
- "name": "X_TOKEN_HEADER",
- "in": "header",
- },
- "APIKeyInQuery": {
- "type": "apiKey",
- "name": "X_TOKEN_QUERY",
- "in": "query",
- },
- "APIKeyInCookie": {
- "type": "apiKey",
- "name": "X_TOKEN_COOKIE",
- "in": "cookie",
- },
- }
- },
- }
diff --git a/tests/_security/openapi_normal/test_security_api_key_in_cookie.py b/tests/_security/openapi_normal/test_security_api_key_in_cookie.py
deleted file mode 100644
index b36590af..00000000
--- a/tests/_security/openapi_normal/test_security_api_key_in_cookie.py
+++ /dev/null
@@ -1,156 +0,0 @@
-from typing import Dict, List, Union
-
-import pytest
-from pydantic import BaseModel
-
-from esmerald import Gateway, JSONResponse, get
-from esmerald.openapi.security.api_key import APIKeyInCookie
-from esmerald.testclient import create_client
-from tests.settings import TestSettings
-
-
-class Error(BaseModel):
- status: int
- detail: str
-
-
-class CustomResponse(BaseModel):
- status: str
- title: str
- errors: List[Error]
-
-
-class JsonResponse(JSONResponse):
- media_type: str = "application/vnd.api+json"
-
-
-class Item(BaseModel):
- sku: Union[int, str]
-
-
-@pytest.mark.parametrize("auth", [APIKeyInCookie, APIKeyInCookie()])
-def test_security_api_key_in_cookie(auth):
- @get(
- response_class=JsonResponse,
- security=[auth],
- )
- def read_people() -> Dict[str, str]:
- """ """
-
- with create_client(
- routes=[Gateway(handler=read_people)],
- enable_openapi=True,
- include_in_schema=True,
- settings_module=TestSettings,
- ) as client:
- response = client.get("/openapi.json")
- assert response.json() == {
- "openapi": "3.1.0",
- "info": {
- "title": "Esmerald",
- "summary": "Esmerald application",
- "description": "Highly scalable, performant, easy to learn and for every application.",
- "contact": {"name": "admin", "email": "admin@myapp.com"},
- "version": client.app.version,
- },
- "servers": [{"url": "/"}],
- "paths": {
- "/": {
- "get": {
- "summary": "Read People",
- "description": "",
- "operationId": "read_people__get",
- "deprecated": False,
- "security": [
- {
- "APIKeyInCookie": {
- "type": "apiKey",
- "in": "cookie",
- "scheme_name": "APIKeyInCookie",
- }
- }
- ],
- "responses": {
- "200": {
- "description": "Successful response",
- "content": {
- "application/vnd.api+json": {"schema": {"type": "string"}}
- },
- }
- },
- }
- }
- },
- "components": {
- "securitySchemes": {"APIKeyInCookie": {"type": "apiKey", "in": "cookie"}}
- },
- }
-
-
-@pytest.mark.parametrize(
- "token,value",
- [
- (APIKeyInCookie(name="X_API_TOKEN"), "X_API_TOKEN"),
- (APIKeyInCookie(name="X_TOKEN"), "X_TOKEN"),
- (APIKeyInCookie(name="test"), "test"),
- ],
- ids=["x-api-token", "x-token", "test"],
-)
-def test_security_api_key_in_cookie_value(token, value):
- @get(
- response_class=JsonResponse,
- security=[token],
- )
- def read_people() -> Dict[str, str]:
- """ """
-
- with create_client(
- routes=[Gateway(handler=read_people)],
- enable_openapi=True,
- include_in_schema=True,
- ) as client:
- response = client.get("/openapi.json")
- assert response.json() == {
- "openapi": "3.1.0",
- "info": {
- "title": "Esmerald",
- "summary": "Esmerald application",
- "description": "Highly scalable, performant, easy to learn and for every application.",
- "contact": {"name": "admin", "email": "admin@myapp.com"},
- "version": client.app.version,
- },
- "servers": [{"url": "/"}],
- "paths": {
- "/": {
- "get": {
- "summary": "Read People",
- "description": "",
- "operationId": "read_people__get",
- "deprecated": False,
- "security": [
- {
- "APIKeyInCookie": {
- "type": "apiKey",
- "name": value,
- "in": "cookie",
- "scheme_name": "APIKeyInCookie",
- }
- }
- ],
- "responses": {
- "200": {
- "description": "Successful response",
- "content": {
- "application/vnd.api+json": {"schema": {"type": "string"}}
- },
- }
- },
- }
- }
- },
- "components": {
- "securitySchemes": {
- "APIKeyInCookie": {"type": "apiKey", "name": value, "in": "cookie"}
- }
- },
- }
diff --git a/tests/_security/openapi_normal/test_security_api_key_in_header.py b/tests/_security/openapi_normal/test_security_api_key_in_header.py
deleted file mode 100644
index 823672bc..00000000
--- a/tests/_security/openapi_normal/test_security_api_key_in_header.py
+++ /dev/null
@@ -1,156 +0,0 @@
-from typing import Dict, List, Union
-
-import pytest
-from pydantic import BaseModel
-
-from esmerald import Gateway, JSONResponse, get
-from esmerald.openapi.security.api_key import APIKeyInHeader
-from esmerald.testclient import create_client
-from tests.settings import TestSettings
-
-
-class Error(BaseModel):
- status: int
- detail: str
-
-
-class CustomResponse(BaseModel):
- status: str
- title: str
- errors: List[Error]
-
-
-class JsonResponse(JSONResponse):
- media_type: str = "application/vnd.api+json"
-
-
-class Item(BaseModel):
- sku: Union[int, str]
-
-
-@pytest.mark.parametrize("auth", [APIKeyInHeader, APIKeyInHeader()])
-def test_security_api_key_in_header(auth):
- @get(
- response_class=JsonResponse,
- security=[auth],
- )
- def read_people() -> Dict[str, str]:
- """ """
-
- with create_client(
- routes=[Gateway(handler=read_people)],
- enable_openapi=True,
- include_in_schema=True,
- settings_module=TestSettings(),
- ) as client:
- response = client.get("/openapi.json")
- assert response.json() == {
- "openapi": "3.1.0",
- "info": {
- "title": "Esmerald",
- "summary": "Esmerald application",
- "description": "Highly scalable, performant, easy to learn and for every application.",
- "contact": {"name": "admin", "email": "admin@myapp.com"},
- "version": client.app.version,
- },
- "servers": [{"url": "/"}],
- "paths": {
- "/": {
- "get": {
- "summary": "Read People",
- "description": "",
- "operationId": "read_people__get",
- "deprecated": False,
- "security": [
- {
- "APIKeyInHeader": {
- "type": "apiKey",
- "in": "header",
- "scheme_name": "APIKeyInHeader",
- }
- }
- ],
- "responses": {
- "200": {
- "description": "Successful response",
- "content": {
- "application/vnd.api+json": {"schema": {"type": "string"}}
- },
- }
- },
- }
- }
- },
- "components": {
- "securitySchemes": {"APIKeyInHeader": {"type": "apiKey", "in": "header"}}
- },
- }
-
-
-@pytest.mark.parametrize(
- "token,value",
- [
- (APIKeyInHeader(name="X_API_TOKEN"), "X_API_TOKEN"),
- (APIKeyInHeader(name="X_TOKEN"), "X_TOKEN"),
- (APIKeyInHeader(name="test"), "test"),
- ],
- ids=["x-api-token", "x-token", "test"],
-)
-def test_security_api_key_header_value(token, value):
- @get(
- response_class=JsonResponse,
- security=[token],
- )
- def read_people() -> Dict[str, str]:
- """ """
-
- with create_client(
- routes=[Gateway(handler=read_people)],
- enable_openapi=True,
- include_in_schema=True,
- ) as client:
- response = client.get("/openapi.json")
- assert response.json() == {
- "openapi": "3.1.0",
- "info": {
- "title": "Esmerald",
- "summary": "Esmerald application",
- "description": "Highly scalable, performant, easy to learn and for every application.",
- "contact": {"name": "admin", "email": "admin@myapp.com"},
- "version": client.app.version,
- },
- "servers": [{"url": "/"}],
- "paths": {
- "/": {
- "get": {
- "summary": "Read People",
- "description": "",
- "operationId": "read_people__get",
- "deprecated": False,
- "security": [
- {
- "APIKeyInHeader": {
- "type": "apiKey",
- "name": value,
- "in": "header",
- "scheme_name": "APIKeyInHeader",
- }
- }
- ],
- "responses": {
- "200": {
- "description": "Successful response",
- "content": {
- "application/vnd.api+json": {"schema": {"type": "string"}}
- },
- }
- },
- }
- }
- },
- "components": {
- "securitySchemes": {
- "APIKeyInHeader": {"type": "apiKey", "name": value, "in": "header"}
- }
- },
- }
diff --git a/tests/_security/openapi_normal/test_security_api_key_in_query.py b/tests/_security/openapi_normal/test_security_api_key_in_query.py
deleted file mode 100644
index bf1f4122..00000000
--- a/tests/_security/openapi_normal/test_security_api_key_in_query.py
+++ /dev/null
@@ -1,156 +0,0 @@
-from typing import Dict, List, Union
-
-import pytest
-from pydantic import BaseModel
-
-from esmerald import Gateway, JSONResponse, get
-from esmerald.openapi.security.api_key import APIKeyInQuery
-from esmerald.testclient import create_client
-from tests.settings import TestSettings
-
-
-class Error(BaseModel):
- status: int
- detail: str
-
-
-class CustomResponse(BaseModel):
- status: str
- title: str
- errors: List[Error]
-
-
-class JsonResponse(JSONResponse):
- media_type: str = "application/vnd.api+json"
-
-
-class Item(BaseModel):
- sku: Union[int, str]
-
-
-@pytest.mark.parametrize("auth", [APIKeyInQuery, APIKeyInQuery()])
-def test_security_api_key_in_query(auth):
- @get(
- response_class=JsonResponse,
- security=[auth],
- )
- def read_people() -> Dict[str, str]:
- """ """
-
- with create_client(
- routes=[Gateway(handler=read_people)],
- enable_openapi=True,
- include_in_schema=True,
- settings_module=TestSettings,
- ) as client:
- response = client.get("/openapi.json")
- assert response.json() == {
- "openapi": "3.1.0",
- "info": {
- "title": "Esmerald",
- "summary": "Esmerald application",
- "description": "Highly scalable, performant, easy to learn and for every application.",
- "contact": {"name": "admin", "email": "admin@myapp.com"},
- "version": client.app.version,
- },
- "servers": [{"url": "/"}],
- "paths": {
- "/": {
- "get": {
- "summary": "Read People",
- "description": "",
- "operationId": "read_people__get",
- "deprecated": False,
- "security": [
- {
- "APIKeyInQuery": {
- "type": "apiKey",
- "in": "query",
- "scheme_name": "APIKeyInQuery",
- }
- }
- ],
- "responses": {
- "200": {
- "description": "Successful response",
- "content": {
- "application/vnd.api+json": {"schema": {"type": "string"}}
- },
- }
- },
- }
- }
- },
- "components": {
- "securitySchemes": {"APIKeyInQuery": {"type": "apiKey", "in": "query"}}
- },
- }
-
-
-@pytest.mark.parametrize(
- "token,value",
- [
- (APIKeyInQuery(name="Authorization"), "Authorization"),
- (APIKeyInQuery(name="X_TOKEN"), "X_TOKEN"),
- (APIKeyInQuery(name="test"), "test"),
- ],
- ids=["x-api-token", "x-token", "test"],
-)
-def test_security_api_key_in_query_value(token, value):
- @get(
- response_class=JsonResponse,
- security=[token],
- )
- def read_people() -> Dict[str, str]:
- """ """
-
- with create_client(
- routes=[Gateway(handler=read_people)],
- enable_openapi=True,
- include_in_schema=True,
- ) as client:
- response = client.get("/openapi.json")
- assert response.json() == {
- "openapi": "3.1.0",
- "info": {
- "title": "Esmerald",
- "summary": "Esmerald application",
- "description": "Highly scalable, performant, easy to learn and for every application.",
- "contact": {"name": "admin", "email": "admin@myapp.com"},
- "version": client.app.version,
- },
- "servers": [{"url": "/"}],
- "paths": {
- "/": {
- "get": {
- "summary": "Read People",
- "description": "",
- "operationId": "read_people__get",
- "deprecated": False,
- "security": [
- {
- "APIKeyInQuery": {
- "type": "apiKey",
- "name": value,
- "in": "query",
- "scheme_name": "APIKeyInQuery",
- }
- }
- ],
- "responses": {
- "200": {
- "description": "Successful response",
- "content": {
- "application/vnd.api+json": {"schema": {"type": "string"}}
- },
- }
- },
- }
- }
- },
- "components": {
- "securitySchemes": {
- "APIKeyInQuery": {"type": "apiKey", "name": value, "in": "query"}
- }
- },
- }
diff --git a/tests/_security/openapi_normal/test_security_basic.py b/tests/_security/openapi_normal/test_security_basic.py
deleted file mode 100644
index 6315b12b..00000000
--- a/tests/_security/openapi_normal/test_security_basic.py
+++ /dev/null
@@ -1,161 +0,0 @@
-from typing import Dict, List, Union
-
-import pytest
-from pydantic import BaseModel
-
-from esmerald import Gateway, JSONResponse, get
-from esmerald.openapi.security.http import Basic
-from esmerald.testclient import create_client
-from tests.settings import TestSettings
-
-
-class Error(BaseModel):
- status: int
- detail: str
-
-
-class CustomResponse(BaseModel):
- status: str
- title: str
- errors: List[Error]
-
-
-class JsonResponse(JSONResponse):
- media_type: str = "application/vnd.api+json"
-
-
-class Item(BaseModel):
- sku: Union[int, str]
-
-
-@get("/item/{id}")
-async def read_item(id: str) -> None:
- """ """
-
-
-@pytest.mark.parametrize("auth", [Basic, Basic()])
-def test_security_basic(auth):
- @get(
- response_class=JsonResponse,
- security=[auth],
- )
- def read_people() -> Dict[str, str]:
- """ """
-
- with create_client(
- routes=[Gateway(handler=read_item), Gateway(handler=read_people)],
- enable_openapi=True,
- include_in_schema=True,
- settings_module=TestSettings(),
- ) as client:
- response = client.get("/openapi.json")
-
- assert response.json() == {
- "openapi": "3.1.0",
- "info": {
- "title": "Esmerald",
- "summary": "Esmerald application",
- "description": "Highly scalable, performant, easy to learn and for every application.",
- "contact": {"name": "admin", "email": "admin@myapp.com"},
- "version": client.app.version,
- },
- "servers": [{"url": "/"}],
- "paths": {
- "/item/{id}": {
- "get": {
- "summary": "Read Item",
- "description": "",
- "operationId": "read_item_item__id__get",
- "parameters": [
- {
- "name": "id",
- "in": "path",
- "required": True,
- "deprecated": False,
- "allowEmptyValue": False,
- "allowReserved": False,
- "schema": {"type": "string", "title": "Id"},
- }
- ],
- "responses": {
- "200": {"description": "Successful response"},
- "422": {
- "description": "Validation Error",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/HTTPValidationError"
- }
- }
- },
- },
- },
- "deprecated": False,
- }
- },
- "/": {
- "get": {
- "summary": "Read People",
- "description": "",
- "operationId": "read_people__get",
- "deprecated": False,
- "security": [
- {
- "Basic": {
- "type": "http",
- "name": "Basic",
- "in": "header",
- "scheme": "basic",
- "scheme_name": "Basic",
- }
- }
- ],
- "responses": {
- "200": {
- "description": "Successful response",
- "content": {
- "application/vnd.api+json": {"schema": {"type": "string"}}
- },
- }
- },
- }
- },
- },
- "components": {
- "schemas": {
- "HTTPValidationError": {
- "properties": {
- "detail": {
- "items": {"$ref": "#/components/schemas/ValidationError"},
- "type": "array",
- "title": "Detail",
- }
- },
- "type": "object",
- "title": "HTTPValidationError",
- },
- "ValidationError": {
- "properties": {
- "loc": {
- "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]},
- "type": "array",
- "title": "Location",
- },
- "msg": {"type": "string", "title": "Message"},
- "type": {"type": "string", "title": "Error Type"},
- },
- "type": "object",
- "required": ["loc", "msg", "type"],
- "title": "ValidationError",
- },
- },
- "securitySchemes": {
- "Basic": {
- "type": "http",
- "name": "Basic",
- "in": "header",
- "scheme": "basic",
- }
- },
- },
- }
diff --git a/tests/_security/openapi_normal/test_security_digest.py b/tests/_security/openapi_normal/test_security_digest.py
deleted file mode 100644
index d9c12819..00000000
--- a/tests/_security/openapi_normal/test_security_digest.py
+++ /dev/null
@@ -1,160 +0,0 @@
-from typing import Dict, List, Union
-
-import pytest
-from pydantic import BaseModel
-
-from esmerald import Gateway, JSONResponse, get
-from esmerald.openapi.security.http import Digest
-from esmerald.testclient import create_client
-from tests.settings import TestSettings
-
-
-class Error(BaseModel):
- status: int
- detail: str
-
-
-class CustomResponse(BaseModel):
- status: str
- title: str
- errors: List[Error]
-
-
-class JsonResponse(JSONResponse):
- media_type: str = "application/vnd.api+json"
-
-
-class Item(BaseModel):
- sku: Union[int, str]
-
-
-@get("/item/{id}")
-async def read_item(id: str) -> None:
- """ """
-
-
-@pytest.mark.parametrize("auth", [Digest, Digest()])
-def test_security_digest(auth):
- @get(
- response_class=JsonResponse,
- security=[auth],
- )
- def read_people() -> Dict[str, str]:
- """ """
-
- with create_client(
- routes=[Gateway(handler=read_item), Gateway(handler=read_people)],
- enable_openapi=True,
- include_in_schema=True,
- settings_module=TestSettings(),
- ) as client:
- response = client.get("/openapi.json")
- assert response.json() == {
- "openapi": "3.1.0",
- "info": {
- "title": "Esmerald",
- "summary": "Esmerald application",
- "description": "Highly scalable, performant, easy to learn and for every application.",
- "contact": {"name": "admin", "email": "admin@myapp.com"},
- "version": client.app.version,
- },
- "servers": [{"url": "/"}],
- "paths": {
- "/item/{id}": {
- "get": {
- "summary": "Read Item",
- "description": "",
- "operationId": "read_item_item__id__get",
- "parameters": [
- {
- "name": "id",
- "in": "path",
- "required": True,
- "deprecated": False,
- "allowEmptyValue": False,
- "allowReserved": False,
- "schema": {"type": "string", "title": "Id"},
- }
- ],
- "responses": {
- "200": {"description": "Successful response"},
- "422": {
- "description": "Validation Error",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/HTTPValidationError"
- }
- }
- },
- },
- },
- "deprecated": False,
- }
- },
- "/": {
- "get": {
- "summary": "Read People",
- "description": "",
- "operationId": "read_people__get",
- "deprecated": False,
- "security": [
- {
- "Digest": {
- "type": "http",
- "name": "Authorization",
- "in": "header",
- "scheme": "digest",
- "scheme_name": "Digest",
- }
- }
- ],
- "responses": {
- "200": {
- "description": "Successful response",
- "content": {
- "application/vnd.api+json": {"schema": {"type": "string"}}
- },
- }
- },
- }
- },
- },
- "components": {
- "schemas": {
- "HTTPValidationError": {
- "properties": {
- "detail": {
- "items": {"$ref": "#/components/schemas/ValidationError"},
- "type": "array",
- "title": "Detail",
- }
- },
- "type": "object",
- "title": "HTTPValidationError",
- },
- "ValidationError": {
- "properties": {
- "loc": {
- "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]},
- "type": "array",
- "title": "Location",
- },
- "msg": {"type": "string", "title": "Message"},
- "type": {"type": "string", "title": "Error Type"},
- },
- "type": "object",
- "required": ["loc", "msg", "type"],
- "title": "ValidationError",
- },
- },
- "securitySchemes": {
- "Digest": {
- "type": "http",
- "name": "Authorization",
- "in": "header",
- "scheme": "digest",
- }
- },
- },
- }
diff --git a/tests/_security/openapi_normal/test_security_token_bearer.py b/tests/_security/openapi_normal/test_security_token_bearer.py
deleted file mode 100644
index 153795ab..00000000
--- a/tests/_security/openapi_normal/test_security_token_bearer.py
+++ /dev/null
@@ -1,161 +0,0 @@
-from typing import Dict, List, Union
-
-import pytest
-from pydantic import BaseModel
-
-from esmerald import Gateway, JSONResponse, get
-from esmerald.openapi.security.http import Bearer
-from esmerald.testclient import create_client
-from tests.settings import TestSettings
-
-
-class Error(BaseModel):
- status: int
- detail: str
-
-
-class CustomResponse(BaseModel):
- status: str
- title: str
- errors: List[Error]
-
-
-class JsonResponse(JSONResponse):
- media_type: str = "application/vnd.api+json"
-
-
-class Item(BaseModel):
- sku: Union[int, str]
-
-
-@get("/item/{id}")
-async def read_item(id: str) -> None:
- """ """
-
-
-@pytest.mark.parametrize("auth", [Bearer, Bearer()])
-def test_security_token_bearer(auth):
- @get(
- response_class=JsonResponse,
- security=[auth],
- )
- def read_people() -> Dict[str, str]:
- """ """
-
- with create_client(
- routes=[Gateway(handler=read_item), Gateway(handler=read_people)],
- enable_openapi=True,
- include_in_schema=True,
- settings_module=TestSettings,
- ) as client:
- response = client.get("/openapi.json")
-
- assert response.json() == {
- "openapi": "3.1.0",
- "info": {
- "title": "Esmerald",
- "summary": "Esmerald application",
- "description": "Highly scalable, performant, easy to learn and for every application.",
- "contact": {"name": "admin", "email": "admin@myapp.com"},
- "version": client.app.version,
- },
- "servers": [{"url": "/"}],
- "paths": {
- "/item/{id}": {
- "get": {
- "summary": "Read Item",
- "description": "",
- "operationId": "read_item_item__id__get",
- "parameters": [
- {
- "name": "id",
- "in": "path",
- "required": True,
- "deprecated": False,
- "allowEmptyValue": False,
- "allowReserved": False,
- "schema": {"type": "string", "title": "Id"},
- }
- ],
- "responses": {
- "200": {"description": "Successful response"},
- "422": {
- "description": "Validation Error",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/HTTPValidationError"
- }
- }
- },
- },
- },
- "deprecated": False,
- }
- },
- "/": {
- "get": {
- "summary": "Read People",
- "description": "",
- "operationId": "read_people__get",
- "deprecated": False,
- "security": [
- {
- "Bearer": {
- "type": "http",
- "name": "Authorization",
- "in": "header",
- "scheme": "bearer",
- "scheme_name": "Bearer",
- }
- }
- ],
- "responses": {
- "200": {
- "description": "Successful response",
- "content": {
- "application/vnd.api+json": {"schema": {"type": "string"}}
- },
- }
- },
- }
- },
- },
- "components": {
- "schemas": {
- "HTTPValidationError": {
- "properties": {
- "detail": {
- "items": {"$ref": "#/components/schemas/ValidationError"},
- "type": "array",
- "title": "Detail",
- }
- },
- "type": "object",
- "title": "HTTPValidationError",
- },
- "ValidationError": {
- "properties": {
- "loc": {
- "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]},
- "type": "array",
- "title": "Location",
- },
- "msg": {"type": "string", "title": "Message"},
- "type": {"type": "string", "title": "Error Type"},
- },
- "type": "object",
- "required": ["loc", "msg", "type"],
- "title": "ValidationError",
- },
- },
- "securitySchemes": {
- "Bearer": {
- "type": "http",
- "name": "Authorization",
- "in": "header",
- "scheme": "bearer",
- }
- },
- },
- }
diff --git a/tests/dependencies/test_injects_with_fastapi_examples.py b/tests/dependencies/test_injects_with_fastapi_examples.py
index 3a050834..b0fe40d5 100644
--- a/tests/dependencies/test_injects_with_fastapi_examples.py
+++ b/tests/dependencies/test_injects_with_fastapi_examples.py
@@ -3,7 +3,7 @@
import pytest
from esmerald import Esmerald, Gateway, get
-from esmerald.param_functions import DirectInjects
+from esmerald.param_functions import Requires
from esmerald.testclient import EsmeraldTestClient
@@ -49,53 +49,53 @@ async def asynchronous_gen(self, value: str) -> AsyncGenerator[str, None]:
@get("/callable-dependency")
-async def get_callable_dependency(value: str = DirectInjects(callable_dependency)) -> str:
+async def get_callable_dependency(value: str = Requires(callable_dependency)) -> str:
return value
@get("/callable-gen-dependency")
-async def get_callable_gen_dependency(value: str = DirectInjects(callable_gen_dependency)) -> str:
+async def get_callable_gen_dependency(value: str = Requires(callable_gen_dependency)) -> str:
return value
@get("/async-callable-dependency")
async def get_async_callable_dependency(
- value: str = DirectInjects(async_callable_dependency),
+ value: str = Requires(async_callable_dependency),
) -> str:
return value
@get("/async-callable-gen-dependency")
async def get_async_callable_gen_dependency(
- value: str = DirectInjects(async_callable_gen_dependency),
+ value: str = Requires(async_callable_gen_dependency),
) -> str:
return value
@get("/synchronous-method-dependency")
async def get_synchronous_method_dependency(
- value: str = DirectInjects(methods_dependency.synchronous),
+ value: str = Requires(methods_dependency.synchronous),
) -> str:
return value
@get("/synchronous-method-gen-dependency")
async def get_synchronous_method_gen_dependency(
- value: str = DirectInjects(methods_dependency.synchronous_gen),
+ value: str = Requires(methods_dependency.synchronous_gen),
) -> str:
return value
@get("/asynchronous-method-dependency")
async def get_asynchronous_method_dependency(
- value: str = DirectInjects(methods_dependency.asynchronous),
+ value: str = Requires(methods_dependency.asynchronous),
) -> str:
return value
@get("/asynchronous-method-gen-dependency")
async def get_asynchronous_method_gen_dependency(
- value: str = DirectInjects(methods_dependency.asynchronous_gen),
+ value: str = Requires(methods_dependency.asynchronous_gen),
) -> str:
return value
diff --git a/tests/encoding/test_encoder_optional.py b/tests/encoding/test_encoder_optional.py
new file mode 100644
index 00000000..d4f95309
--- /dev/null
+++ b/tests/encoding/test_encoder_optional.py
@@ -0,0 +1,90 @@
+from typing import Any, Optional, Union
+
+from pydantic import BaseModel
+
+from esmerald import Gateway, post
+from esmerald.testclient import create_client
+
+
+class User(BaseModel):
+ username: str
+
+
+@post("/optional")
+async def create(data: Optional[User]) -> Any:
+ return data if data else {}
+
+
+def test_optional():
+ with create_client(routes=[Gateway(handler=create)]) as client:
+ response = client.post("/optional", json={"username": "test"})
+ assert response.status_code == 201
+ assert response.json() == {"username": "test"}
+
+ response = client.post("/optional", json={})
+ assert response.status_code == 201
+ assert response.json() == {}
+
+ response = client.post("/optional")
+ assert response.status_code == 201
+ assert response.json() == {}
+
+
+@post("/union")
+async def create_union(data: Union[User, None]) -> Any:
+ return data if data else {}
+
+
+def test_union():
+ with create_client(routes=[Gateway(handler=create_union)]) as client:
+ response = client.post("/union", json={"username": "test"})
+ assert response.status_code == 201
+ assert response.json() == {"username": "test"}
+
+ response = client.post("/union", json={})
+ assert response.status_code == 201
+ assert response.json() == {}
+
+ response = client.post("/union")
+ assert response.status_code == 201
+ assert response.json() == {}
+
+
+@post("/optional-one")
+async def create_one(test: Optional[User]) -> Any:
+ return test if test else {}
+
+
+def test_optional_one():
+ with create_client(routes=[Gateway(handler=create_one)]) as client:
+ response = client.post("/optional-one", json={"test": {"username": "test"}})
+ assert response.status_code == 201
+ assert response.json() == {"username": "test"}
+
+ response = client.post("/optional-one", json={})
+ assert response.status_code == 201
+ assert response.json() == {}
+
+ response = client.post("/optional-one")
+ assert response.status_code == 201
+ assert response.json() == {}
+
+
+@post("/union-one")
+async def create_union_one(test: Union[User, None]) -> Any:
+ return test if test else {}
+
+
+def test_union_one():
+ with create_client(routes=[Gateway(handler=create_union_one)]) as client:
+ response = client.post("/union-one", json={"test": {"username": "test"}})
+ assert response.status_code == 201
+ assert response.json() == {"username": "test"}
+
+ response = client.post("/union-one", json={})
+ assert response.status_code == 201
+ assert response.json() == {}
+
+ response = client.post("/union-one")
+ assert response.status_code == 201
+ assert response.json() == {}
diff --git a/tests/routing/test_syntax_enum.py b/tests/routing/test_syntax_enum.py
index 76d5a984..a20c8c8c 100644
--- a/tests/routing/test_syntax_enum.py
+++ b/tests/routing/test_syntax_enum.py
@@ -20,14 +20,12 @@ async def item(item_type: ItemType) -> JSONResponse:
def test_syntax():
with create_client(routes=[Gateway(handler=item)]) as client:
-
response = client.get("/item/sold")
assert response.json() == {"item_type": "sold"}
def test_syntax_fail():
with create_client(routes=[Gateway(handler=item)]) as client:
-
response = client.get("/item/test")
assert response.status_code == 400
diff --git a/tests/_security/__init__.py b/tests/security/http/__init__.py
similarity index 100%
rename from tests/_security/__init__.py
rename to tests/security/http/__init__.py
diff --git a/tests/security/http/test_security_api_key_cookie.py b/tests/security/http/test_security_api_key_cookie.py
new file mode 100644
index 00000000..6f5e7bd6
--- /dev/null
+++ b/tests/security/http/test_security_api_key_cookie.py
@@ -0,0 +1,99 @@
+from typing import Any
+
+from pydantic import BaseModel
+
+from esmerald import Gateway, Inject, Injects, Security, get
+from esmerald.security.api_key import APIKeyInCookie
+from esmerald.testclient import create_client
+
+api_key = APIKeyInCookie(name="key")
+
+
+class User(BaseModel):
+ username: str
+
+
+def get_current_user(oauth_header: str = Security(api_key)):
+ user = User(username=oauth_header)
+ return user
+
+
+@get("/users/me", dependencies={"current_user": Inject(get_current_user)}, security=[api_key])
+def read_current_user(current_user: User = Injects()) -> Any:
+ return current_user
+
+
+def test_security_api_key():
+ with create_client(
+ routes=[
+ Gateway(handler=read_current_user),
+ ],
+ ) as client:
+ response = client.get("/users/me", cookies={"key": "secret"})
+ assert response.status_code == 200, response.text
+ assert response.json() == {"username": "secret"}
+
+
+def test_security_api_key_no_key():
+ with create_client(
+ routes=[
+ Gateway(handler=read_current_user),
+ ],
+ ) as client:
+ response = client.get("/users/me")
+ assert response.status_code == 403, response.text
+ assert response.json() == {"detail": "Not authenticated"}
+
+
+def test_openapi_schema():
+ with create_client(
+ routes=[
+ Gateway(handler=read_current_user),
+ ],
+ enable_openapi=True,
+ ) as client:
+ response = client.get("/openapi.json")
+ assert response.status_code == 200, response.text
+
+ assert response.json() == {
+ "openapi": "3.1.0",
+ "info": {
+ "title": "Esmerald",
+ "summary": "Esmerald application",
+ "description": "Highly scalable, performant, easy to learn and for every application.",
+ "contact": {"name": "admin", "email": "admin@myapp.com"},
+ "version": client.app.version,
+ },
+ "servers": [{"url": "/"}],
+ "paths": {
+ "/users/me": {
+ "get": {
+ "summary": "Read Current User",
+ "description": "",
+ "operationId": "read_current_user_users_me_get",
+ "deprecated": False,
+ "security": [
+ {
+ "APIKeyInCookie": {
+ "type": "apiKey",
+ "name": "key",
+ "in": "cookie",
+ "scheme_name": "APIKeyInCookie",
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {"application/json": {"schema": {"type": "string"}}},
+ }
+ },
+ }
+ }
+ },
+ "components": {
+ "securitySchemes": {
+ "APIKeyInCookie": {"type": "apiKey", "name": "key", "in": "cookie"}
+ }
+ },
+ }
diff --git a/tests/security/http/test_security_api_key_cookie_description.py b/tests/security/http/test_security_api_key_cookie_description.py
new file mode 100644
index 00000000..a23f3759
--- /dev/null
+++ b/tests/security/http/test_security_api_key_cookie_description.py
@@ -0,0 +1,105 @@
+from typing import Any
+
+from pydantic import BaseModel
+
+from esmerald import Gateway, Inject, Injects, Security, get
+from esmerald.security.api_key import APIKeyInCookie
+from esmerald.testclient import create_client
+
+api_key = APIKeyInCookie(name="key", description="An API Cookie Key")
+
+
+class User(BaseModel):
+ username: str
+
+
+def get_current_user(oauth_header: str = Security(api_key)):
+ user = User(username=oauth_header)
+ return user
+
+
+@get("/users/me", security=[api_key], dependencies={"current_user": Inject(get_current_user)})
+def read_current_user(current_user: User = Injects()) -> Any:
+ return current_user
+
+
+def test_security_api_key():
+ with create_client(
+ routes=[
+ Gateway(handler=read_current_user),
+ ],
+ ) as client:
+ response = client.get("/users/me", cookies={"key": "secret"})
+ assert response.status_code == 200, response.text
+ assert response.json() == {"username": "secret"}
+
+
+def test_security_api_key_no_key():
+ with create_client(
+ routes=[
+ Gateway(handler=read_current_user),
+ ],
+ ) as client:
+ response = client.get("/users/me")
+ assert response.status_code == 403, response.text
+ assert response.json() == {"detail": "Not authenticated"}
+
+
+def test_openapi_schema():
+ with create_client(
+ routes=[
+ Gateway(handler=read_current_user),
+ ],
+ enable_openapi=True,
+ ) as client:
+ response = client.get("/openapi.json")
+ assert response.status_code == 200, response.text
+
+ assert response.json() == {
+ "openapi": "3.1.0",
+ "info": {
+ "title": "Esmerald",
+ "summary": "Esmerald application",
+ "description": "Highly scalable, performant, easy to learn and for every application.",
+ "contact": {"name": "admin", "email": "admin@myapp.com"},
+ "version": client.app.version,
+ },
+ "servers": [{"url": "/"}],
+ "paths": {
+ "/users/me": {
+ "get": {
+ "summary": "Read Current User",
+ "description": "",
+ "operationId": "read_current_user_users_me_get",
+ "deprecated": False,
+ "security": [
+ {
+ "APIKeyInCookie": {
+ "type": "apiKey",
+ "description": "An API Cookie Key",
+ "name": "key",
+ "in": "cookie",
+ "scheme_name": "APIKeyInCookie",
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {"application/json": {"schema": {"type": "string"}}},
+ }
+ },
+ }
+ }
+ },
+ "components": {
+ "securitySchemes": {
+ "APIKeyInCookie": {
+ "type": "apiKey",
+ "description": "An API Cookie Key",
+ "name": "key",
+ "in": "cookie",
+ }
+ }
+ },
+ }
diff --git a/tests/security/http/test_security_api_key_cookie_optional.py b/tests/security/http/test_security_api_key_cookie_optional.py
new file mode 100644
index 00000000..62f9e0c6
--- /dev/null
+++ b/tests/security/http/test_security_api_key_cookie_optional.py
@@ -0,0 +1,104 @@
+from typing import Any, Optional
+
+from pydantic import BaseModel
+
+from esmerald import Gateway, Inject, Injects, Security, get
+from esmerald.security.api_key import APIKeyInCookie
+from esmerald.testclient import create_client
+
+api_key = APIKeyInCookie(name="key", auto_error=False)
+
+
+class User(BaseModel):
+ username: str
+
+
+def get_current_user(oauth_header: Optional[str] = Security(api_key)):
+ if oauth_header is None:
+ return None
+ user = User(username=oauth_header)
+ return user
+
+
+@get("/users/me", security=[api_key], dependencies={"current_user": Inject(get_current_user)})
+def read_current_user(current_user: Optional[User] = Injects()) -> Any:
+ if current_user is None:
+ return {"msg": "Create an account first"}
+ else:
+ return current_user
+
+
+def test_security_api_key():
+ with create_client(
+ routes=[
+ Gateway(handler=read_current_user),
+ ],
+ ) as client:
+ response = client.get("/users/me", cookies={"key": "secret"})
+ assert response.status_code == 200, response.text
+ assert response.json() == {"username": "secret"}
+
+
+def test_security_api_key_no_key():
+ with create_client(
+ routes=[
+ Gateway(handler=read_current_user),
+ ],
+ ) as client:
+ response = client.get("/users/me")
+ assert response.status_code == 200, response.text
+ assert response.json() == {"msg": "Create an account first"}
+
+
+def test_openapi_schema():
+ with create_client(
+ routes=[
+ Gateway(handler=read_current_user),
+ ],
+ enable_openapi=True,
+ ) as client:
+ response = client.get("/openapi.json")
+ assert response.status_code == 200, response.text
+
+ assert response.json() == {
+ "openapi": "3.1.0",
+ "info": {
+ "title": "Esmerald",
+ "summary": "Esmerald application",
+ "description": "Highly scalable, performant, easy to learn and for every application.",
+ "contact": {"name": "admin", "email": "admin@myapp.com"},
+ "version": client.app.version,
+ },
+ "servers": [{"url": "/"}],
+ "paths": {
+ "/users/me": {
+ "get": {
+ "summary": "Read Current User",
+ "description": "",
+ "operationId": "read_current_user_users_me_get",
+ "deprecated": False,
+ "security": [
+ {
+ "APIKeyInCookie": {
+ "type": "apiKey",
+ "name": "key",
+ "in": "cookie",
+ "scheme_name": "APIKeyInCookie",
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {"application/json": {"schema": {"type": "string"}}},
+ }
+ },
+ }
+ }
+ },
+ "components": {
+ "securitySchemes": {
+ "APIKeyInCookie": {"type": "apiKey", "name": "key", "in": "cookie"}
+ }
+ },
+ }
diff --git a/tests/security/http/test_security_api_key_header.py b/tests/security/http/test_security_api_key_header.py
new file mode 100644
index 00000000..4d4773cb
--- /dev/null
+++ b/tests/security/http/test_security_api_key_header.py
@@ -0,0 +1,104 @@
+from typing import Any
+
+from pydantic import BaseModel
+
+from esmerald import Gateway, Inject, Injects, Security, get
+from esmerald.security.api_key import APIKeyInHeader
+from esmerald.testclient import create_client
+
+api_key = APIKeyInHeader(name="key")
+
+
+class User(BaseModel):
+ username: str
+
+
+def get_current_user(oauth_header: str = Security(api_key)):
+ if oauth_header is None:
+ return None
+ user = User(username=oauth_header)
+ return user
+
+
+@get("/users/me", security=[api_key], dependencies={"current_user": Inject(get_current_user)})
+def read_current_user(current_user: User = Injects()) -> Any:
+ if current_user is None:
+ return {"msg": "Create an account first"}
+ else:
+ return current_user
+
+
+def test_security_api_key():
+ with create_client(
+ routes=[
+ Gateway(handler=read_current_user),
+ ],
+ ) as client:
+ response = client.get("/users/me", headers={"key": "secret"})
+ assert response.status_code == 200, response.text
+ assert response.json() == {"username": "secret"}
+
+
+def test_security_api_key_no_key():
+ with create_client(
+ routes=[
+ Gateway(handler=read_current_user),
+ ],
+ ) as client:
+ response = client.get("/users/me")
+ assert response.status_code == 403, response.text
+ assert response.json() == {"detail": "Not authenticated"}
+
+
+def test_openapi_schema():
+ with create_client(
+ routes=[
+ Gateway(handler=read_current_user),
+ ],
+ enable_openapi=True,
+ ) as client:
+ response = client.get("/openapi.json")
+ assert response.status_code == 200, response.text
+
+ assert response.json() == {
+ "openapi": "3.1.0",
+ "info": {
+ "title": "Esmerald",
+ "summary": "Esmerald application",
+ "description": "Highly scalable, performant, easy to learn and for every application.",
+ "contact": {"name": "admin", "email": "admin@myapp.com"},
+ "version": client.app.version,
+ },
+ "servers": [{"url": "/"}],
+ "paths": {
+ "/users/me": {
+ "get": {
+ "summary": "Read Current User",
+ "description": "",
+ "operationId": "read_current_user_users_me_get",
+ "deprecated": False,
+ "security": [
+ {
+ "APIKeyInHeader": {
+ "type": "apiKey",
+ "name": "key",
+ "in": "header",
+ "scheme_name": "APIKeyInHeader",
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {"application/json": {"schema": {"type": "string"}}},
+ }
+ },
+ }
+ }
+ },
+ "components": {
+ "securitySchemes": {
+ "APIKeyInHeader": {"type": "apiKey", "name": "key", "in": "header"}
+ }
+ },
+ }
diff --git a/tests/security/http/test_security_api_key_header_description.py b/tests/security/http/test_security_api_key_header_description.py
new file mode 100644
index 00000000..e5ed2b12
--- /dev/null
+++ b/tests/security/http/test_security_api_key_header_description.py
@@ -0,0 +1,105 @@
+from typing import Any
+
+from pydantic import BaseModel
+
+from esmerald import Gateway, Inject, Injects, Security, get
+from esmerald.security.api_key import APIKeyInHeader
+from esmerald.testclient import create_client
+
+api_key = APIKeyInHeader(name="key", description="An API Key Header")
+
+
+class User(BaseModel):
+ username: str
+
+
+def get_current_user(oauth_header: str = Security(api_key)):
+ user = User(username=oauth_header)
+ return user
+
+
+@get("/users/me", dependencies={"current_user": Inject(get_current_user)}, security=[api_key])
+def read_current_user(current_user: User = Injects()) -> Any:
+ return current_user
+
+
+def test_security_api_key():
+ with create_client(
+ routes=[
+ Gateway(handler=read_current_user),
+ ],
+ ) as client:
+ response = client.get("/users/me", headers={"key": "secret"})
+ assert response.status_code == 200, response.text
+ assert response.json() == {"username": "secret"}
+
+
+def test_security_api_key_no_key():
+ with create_client(
+ routes=[
+ Gateway(handler=read_current_user),
+ ],
+ ) as client:
+ response = client.get("/users/me")
+ assert response.status_code == 403, response.text
+ assert response.json() == {"detail": "Not authenticated"}
+
+
+def test_openapi_schema():
+ with create_client(
+ routes=[
+ Gateway(handler=read_current_user),
+ ],
+ enable_openapi=True,
+ ) as client:
+ response = client.get("/openapi.json")
+ assert response.status_code == 200, response.text
+
+ assert response.json() == {
+ "openapi": "3.1.0",
+ "info": {
+ "title": "Esmerald",
+ "summary": "Esmerald application",
+ "description": "Highly scalable, performant, easy to learn and for every application.",
+ "contact": {"name": "admin", "email": "admin@myapp.com"},
+ "version": client.app.version,
+ },
+ "servers": [{"url": "/"}],
+ "paths": {
+ "/users/me": {
+ "get": {
+ "summary": "Read Current User",
+ "description": "",
+ "operationId": "read_current_user_users_me_get",
+ "deprecated": False,
+ "security": [
+ {
+ "APIKeyInHeader": {
+ "type": "apiKey",
+ "description": "An API Key Header",
+ "name": "key",
+ "in": "header",
+ "scheme_name": "APIKeyInHeader",
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {"application/json": {"schema": {"type": "string"}}},
+ }
+ },
+ }
+ }
+ },
+ "components": {
+ "securitySchemes": {
+ "APIKeyInHeader": {
+ "type": "apiKey",
+ "description": "An API Key Header",
+ "name": "key",
+ "in": "header",
+ }
+ }
+ },
+ }
diff --git a/tests/security/http/test_security_api_key_header_optional.py b/tests/security/http/test_security_api_key_header_optional.py
new file mode 100644
index 00000000..6d1dbc4b
--- /dev/null
+++ b/tests/security/http/test_security_api_key_header_optional.py
@@ -0,0 +1,104 @@
+from typing import Any, Optional
+
+from pydantic import BaseModel
+
+from esmerald import Gateway, Inject, Injects, Security, get
+from esmerald.security.api_key import APIKeyInHeader
+from esmerald.testclient import create_client
+
+api_key = APIKeyInHeader(name="key", auto_error=False)
+
+
+class User(BaseModel):
+ username: str
+
+
+def get_current_user(oauth_header: Optional[str] = Security(api_key)):
+ if oauth_header is None:
+ return None
+ user = User(username=oauth_header)
+ return user
+
+
+@get("/users/me", security=[api_key], dependencies={"current_user": Inject(get_current_user)})
+def read_current_user(current_user: Optional[User] = Injects()) -> Any:
+ if current_user is None:
+ return {"msg": "Create an account first"}
+ else:
+ return current_user
+
+
+def test_security_api_key():
+ with create_client(
+ routes=[
+ Gateway(handler=read_current_user),
+ ],
+ ) as client:
+ response = client.get("/users/me", headers={"key": "secret"})
+ assert response.status_code == 200, response.text
+ assert response.json() == {"username": "secret"}
+
+
+def test_security_api_key_no_key():
+ with create_client(
+ routes=[
+ Gateway(handler=read_current_user),
+ ],
+ ) as client:
+ response = client.get("/users/me")
+ assert response.status_code == 200, response.text
+ assert response.json() == {"msg": "Create an account first"}
+
+
+def test_openapi_schema():
+ with create_client(
+ routes=[
+ Gateway(handler=read_current_user),
+ ],
+ enable_openapi=True,
+ ) as client:
+ response = client.get("/openapi.json")
+ assert response.status_code == 200, response.text
+
+ assert response.json() == {
+ "openapi": "3.1.0",
+ "info": {
+ "title": "Esmerald",
+ "summary": "Esmerald application",
+ "description": "Highly scalable, performant, easy to learn and for every application.",
+ "contact": {"name": "admin", "email": "admin@myapp.com"},
+ "version": client.app.version,
+ },
+ "servers": [{"url": "/"}],
+ "paths": {
+ "/users/me": {
+ "get": {
+ "summary": "Read Current User",
+ "description": "",
+ "operationId": "read_current_user_users_me_get",
+ "deprecated": False,
+ "security": [
+ {
+ "APIKeyInHeader": {
+ "type": "apiKey",
+ "name": "key",
+ "in": "header",
+ "scheme_name": "APIKeyInHeader",
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {"application/json": {"schema": {"type": "string"}}},
+ }
+ },
+ }
+ }
+ },
+ "components": {
+ "securitySchemes": {
+ "APIKeyInHeader": {"type": "apiKey", "name": "key", "in": "header"}
+ }
+ },
+ }
diff --git a/tests/security/http/test_security_api_key_query.py b/tests/security/http/test_security_api_key_query.py
new file mode 100644
index 00000000..249326a7
--- /dev/null
+++ b/tests/security/http/test_security_api_key_query.py
@@ -0,0 +1,104 @@
+from typing import Any
+
+from pydantic import BaseModel
+
+from esmerald import Gateway, Inject, Injects, Security, get
+from esmerald.security.api_key import APIKeyInQuery
+from esmerald.testclient import create_client
+
+api_key = APIKeyInQuery(name="key")
+
+
+class User(BaseModel):
+ username: str
+
+
+def get_current_user(oauth_header: str = Security(api_key)):
+ if oauth_header is None:
+ return None
+ user = User(username=oauth_header)
+ return user
+
+
+@get("/users/me", dependencies={"current_user": Inject(get_current_user)}, security=[api_key])
+def read_current_user(current_user: User = Injects()) -> Any:
+ if current_user is None:
+ return {"msg": "Create an account first"}
+ else:
+ return current_user
+
+
+def test_security_api_key():
+ with create_client(
+ routes=[
+ Gateway(handler=read_current_user),
+ ],
+ ) as client:
+ response = client.get("/users/me?key=secret")
+ assert response.status_code == 200, response.text
+ assert response.json() == {"username": "secret"}
+
+
+def test_security_api_key_no_key():
+ with create_client(
+ routes=[
+ Gateway(handler=read_current_user),
+ ],
+ ) as client:
+ response = client.get("/users/me")
+ assert response.status_code == 403, response.text
+ assert response.json() == {"detail": "Not authenticated"}
+
+
+def test_openapi_schema():
+ with create_client(
+ routes=[
+ Gateway(handler=read_current_user),
+ ],
+ enable_openapi=True,
+ ) as client:
+ response = client.get("/openapi.json")
+ assert response.status_code == 200, response.text
+
+ assert response.json() == {
+ "openapi": "3.1.0",
+ "info": {
+ "title": "Esmerald",
+ "summary": "Esmerald application",
+ "description": "Highly scalable, performant, easy to learn and for every application.",
+ "contact": {"name": "admin", "email": "admin@myapp.com"},
+ "version": client.app.version,
+ },
+ "servers": [{"url": "/"}],
+ "paths": {
+ "/users/me": {
+ "get": {
+ "summary": "Read Current User",
+ "description": "",
+ "operationId": "read_current_user_users_me_get",
+ "deprecated": False,
+ "security": [
+ {
+ "APIKeyInQuery": {
+ "type": "apiKey",
+ "name": "key",
+ "in": "query",
+ "scheme_name": "APIKeyInQuery",
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {"application/json": {"schema": {"type": "string"}}},
+ }
+ },
+ }
+ }
+ },
+ "components": {
+ "securitySchemes": {
+ "APIKeyInQuery": {"type": "apiKey", "name": "key", "in": "query"}
+ }
+ },
+ }
diff --git a/tests/security/http/test_security_api_key_query_description.py b/tests/security/http/test_security_api_key_query_description.py
new file mode 100644
index 00000000..a1ef40ed
--- /dev/null
+++ b/tests/security/http/test_security_api_key_query_description.py
@@ -0,0 +1,110 @@
+from typing import Any
+
+from pydantic import BaseModel
+
+from esmerald import Gateway, Include, Inject, Injects, Security, get
+from esmerald.security.api_key import APIKeyInQuery
+from esmerald.testclient import create_client
+
+api_key = APIKeyInQuery(name="key", description="API Key Query")
+
+
+class User(BaseModel):
+ username: str
+
+
+def get_current_user(oauth_header: str = Security(api_key)):
+ if oauth_header is None:
+ return None
+ user = User(username=oauth_header)
+ return user
+
+
+@get("/users/me", dependencies={"current_user": Inject(get_current_user)})
+def read_current_user(current_user: User = Injects()) -> Any:
+ if current_user is None:
+ return {"msg": "Create an account first"}
+ else:
+ return current_user
+
+
+def test_security_api_key():
+ with create_client(
+ routes=[
+ Gateway(handler=read_current_user),
+ ],
+ ) as client:
+ response = client.get("/users/me?key=secret")
+ assert response.status_code == 200, response.text
+ assert response.json() == {"username": "secret"}
+
+
+def test_security_api_key_no_key():
+ with create_client(
+ routes=[
+ Gateway(handler=read_current_user),
+ ],
+ ) as client:
+ response = client.get("/users/me")
+ assert response.status_code == 403, response.text
+ assert response.json() == {"detail": "Not authenticated"}
+
+
+def test_openapi_schema():
+ with create_client(
+ routes=[
+ Include(routes=[Gateway(handler=read_current_user)], security=[api_key]),
+ ],
+ enable_openapi=True,
+ ) as client:
+ response = client.get("/openapi.json")
+
+ assert response.status_code == 200, response.text
+ assert response.json() == {
+ "openapi": "3.1.0",
+ "info": {
+ "title": "Esmerald",
+ "summary": "Esmerald application",
+ "description": "Highly scalable, performant, easy to learn and for every application.",
+ "contact": {"name": "admin", "email": "admin@myapp.com"},
+ "version": client.app.version,
+ },
+ "servers": [{"url": "/"}],
+ "paths": {
+ "/users/me": {
+ "get": {
+ "summary": "Read Current User",
+ "description": "",
+ "operationId": "read_current_user_users_me_get",
+ "deprecated": False,
+ "security": [
+ {
+ "APIKeyInQuery": {
+ "type": "apiKey",
+ "description": "API Key Query",
+ "name": "key",
+ "in": "query",
+ "scheme_name": "APIKeyInQuery",
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {"application/json": {"schema": {"type": "string"}}},
+ }
+ },
+ }
+ }
+ },
+ "components": {
+ "securitySchemes": {
+ "APIKeyInQuery": {
+ "type": "apiKey",
+ "description": "API Key Query",
+ "name": "key",
+ "in": "query",
+ }
+ }
+ },
+ }
diff --git a/tests/security/http/test_security_api_key_query_optional.py b/tests/security/http/test_security_api_key_query_optional.py
new file mode 100644
index 00000000..fa2dec7a
--- /dev/null
+++ b/tests/security/http/test_security_api_key_query_optional.py
@@ -0,0 +1,91 @@
+from typing import Any, Optional
+
+from pydantic import BaseModel
+
+from esmerald import Gateway, Inject, Injects, Security, get
+from esmerald.security.api_key import APIKeyInQuery
+from esmerald.testclient import create_client
+
+api_key = APIKeyInQuery(name="key", auto_error=False)
+
+
+class User(BaseModel):
+ username: str
+
+
+def get_current_user(oauth_header: Optional[str] = Security(api_key)):
+ if oauth_header is None:
+ return None
+ user = User(username=oauth_header)
+ return user
+
+
+@get("/users/me", dependencies={"current_user": Inject(get_current_user)}, security=[api_key])
+def read_current_user(current_user: Optional[User] = Injects()) -> Any:
+ if current_user is None:
+ return {"msg": "Create an account first"}
+ else:
+ return current_user
+
+
+def test_security_api_key():
+ with create_client(routes=[Gateway(handler=read_current_user)]) as client:
+ response = client.get("/users/me?key=secret")
+ assert response.status_code == 200, response.text
+ assert response.json() == {"username": "secret"}
+
+
+def test_security_api_key_no_key():
+ with create_client(routes=[Gateway(handler=read_current_user)]) as client:
+ response = client.get("/users/me")
+ assert response.status_code == 200, response.text
+ assert response.json() == {"msg": "Create an account first"}
+
+
+def test_openapi_schema():
+ with create_client(routes=[Gateway(handler=read_current_user)], enable_openapi=True) as client:
+ response = client.get("/openapi.json")
+ assert response.status_code == 200, response.text
+
+ assert response.json() == {
+ "openapi": "3.1.0",
+ "info": {
+ "title": "Esmerald",
+ "summary": "Esmerald application",
+ "description": "Highly scalable, performant, easy to learn and for every application.",
+ "contact": {"name": "admin", "email": "admin@myapp.com"},
+ "version": client.app.version,
+ },
+ "servers": [{"url": "/"}],
+ "paths": {
+ "/users/me": {
+ "get": {
+ "summary": "Read Current User",
+ "description": "",
+ "operationId": "read_current_user_users_me_get",
+ "deprecated": False,
+ "security": [
+ {
+ "APIKeyInQuery": {
+ "type": "apiKey",
+ "name": "key",
+ "in": "query",
+ "scheme_name": "APIKeyInQuery",
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {"application/json": {"schema": {"type": "string"}}},
+ }
+ },
+ }
+ }
+ },
+ "components": {
+ "securitySchemes": {
+ "APIKeyInQuery": {"type": "apiKey", "name": "key", "in": "query"}
+ }
+ },
+ }
diff --git a/tests/security/http/test_security_http_base.py b/tests/security/http/test_security_http_base.py
new file mode 100644
index 00000000..2ceb3ee8
--- /dev/null
+++ b/tests/security/http/test_security_http_base.py
@@ -0,0 +1,78 @@
+from typing import Any
+
+from esmerald import Gateway, Inject, Injects, get
+from esmerald.security.http import HTTPAuthorizationCredentials, HTTPBase
+from esmerald.testclient import create_client
+
+security = HTTPBase(scheme="Other")
+
+
+@get(
+ "/users/me",
+ dependencies={"credentials": Inject(security)},
+ security=[security],
+)
+def read_current_user(
+ credentials: HTTPAuthorizationCredentials = Injects(),
+) -> Any:
+ if credentials is None:
+ return {"msg": "Create an account first"}
+ return {"scheme": credentials.scheme, "credentials": credentials.credentials}
+
+
+def test_security_http_base():
+ with create_client(routes=[Gateway(handler=read_current_user)]) as client:
+ response = client.get("/users/me", headers={"Authorization": "Other foobar"})
+ assert response.status_code == 200, response.text
+ assert response.json() == {"scheme": "Other", "credentials": "foobar"}
+
+
+def test_security_http_base_no_credentials():
+ with create_client(routes=[Gateway(handler=read_current_user)]) as client:
+ response = client.get("/users/me")
+ assert response.status_code == 403, response.text
+ assert response.json() == {"detail": "Not authenticated"}
+
+
+def test_openapi_schema():
+ with create_client(routes=[Gateway(handler=read_current_user)], enable_openapi=True) as client:
+ response = client.get("/openapi.json")
+ assert response.status_code == 200, response.text
+
+ assert response.json() == {
+ "openapi": "3.1.0",
+ "info": {
+ "title": "Esmerald",
+ "summary": "Esmerald application",
+ "description": "Highly scalable, performant, easy to learn and for every application.",
+ "contact": {"name": "admin", "email": "admin@myapp.com"},
+ "version": client.app.version,
+ },
+ "servers": [{"url": "/"}],
+ "paths": {
+ "/users/me": {
+ "get": {
+ "summary": "Read Current User",
+ "description": "",
+ "operationId": "read_current_user_users_me_get",
+ "deprecated": False,
+ "security": [
+ {
+ "HTTPBase": {
+ "type": "http",
+ "scheme": "Other",
+ "scheme_name": "HTTPBase",
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {"application/json": {"schema": {"type": "string"}}},
+ }
+ },
+ }
+ }
+ },
+ "components": {"securitySchemes": {"HTTPBase": {"type": "http", "scheme": "Other"}}},
+ }
diff --git a/tests/security/http/test_security_http_base_description.py b/tests/security/http/test_security_http_base_description.py
new file mode 100644
index 00000000..0c298de1
--- /dev/null
+++ b/tests/security/http/test_security_http_base_description.py
@@ -0,0 +1,79 @@
+from typing import Any
+
+from esmerald import Gateway, Inject, Injects, get
+from esmerald.security.http import HTTPAuthorizationCredentials, HTTPBase
+from esmerald.testclient import create_client
+
+security = HTTPBase(scheme="Other", description="Other Security Scheme")
+
+
+@get("/users/me", dependencies={"credentials": Inject(security)}, security=[security])
+def read_current_user(credentials: HTTPAuthorizationCredentials = Injects()) -> Any:
+ return {"scheme": credentials.scheme, "credentials": credentials.credentials}
+
+
+def xtest_security_http_base():
+ with create_client(routes=[Gateway(handler=read_current_user)]) as client:
+ response = client.get("/users/me", headers={"Authorization": "Other foobar"})
+ assert response.status_code == 200, response.text
+ assert response.json() == {"scheme": "Other", "credentials": "foobar"}
+
+
+def test_security_http_base_no_credentials():
+ with create_client(routes=[Gateway(handler=read_current_user)]) as client:
+ response = client.get("/users/me")
+ assert response.status_code == 403, response.text
+ assert response.json() == {"detail": "Not authenticated"}
+
+
+def test_openapi_schema():
+ with create_client(routes=[Gateway(handler=read_current_user)], enable_openapi=True) as client:
+ response = client.get("/openapi.json")
+ assert response.status_code == 200, response.text
+
+ assert response.json() == {
+ "openapi": "3.1.0",
+ "info": {
+ "title": "Esmerald",
+ "summary": "Esmerald application",
+ "description": "Highly scalable, performant, easy to learn and for every application.",
+ "contact": {"name": "admin", "email": "admin@myapp.com"},
+ "version": client.app.version,
+ },
+ "servers": [{"url": "/"}],
+ "paths": {
+ "/users/me": {
+ "get": {
+ "summary": "Read Current User",
+ "description": "",
+ "operationId": "read_current_user_users_me_get",
+ "deprecated": False,
+ "security": [
+ {
+ "HTTPBase": {
+ "type": "http",
+ "description": "Other Security Scheme",
+ "scheme": "Other",
+ "scheme_name": "HTTPBase",
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {"application/json": {"schema": {"type": "string"}}},
+ }
+ },
+ }
+ }
+ },
+ "components": {
+ "securitySchemes": {
+ "HTTPBase": {
+ "type": "http",
+ "description": "Other Security Scheme",
+ "scheme": "Other",
+ }
+ }
+ },
+ }
diff --git a/tests/security/http/test_security_http_base_optional.py b/tests/security/http/test_security_http_base_optional.py
new file mode 100644
index 00000000..97e97b71
--- /dev/null
+++ b/tests/security/http/test_security_http_base_optional.py
@@ -0,0 +1,78 @@
+from typing import Any, Optional
+
+from esmerald import Gateway, Inject, Injects, get
+from esmerald.security.http import HTTPAuthorizationCredentials, HTTPBase
+from esmerald.testclient import create_client
+
+security = HTTPBase(scheme="Other", auto_error=False)
+
+
+@get(
+ "/users/me",
+ dependencies={"credentials": Inject(security)},
+ security=[security],
+)
+def read_current_user(
+ credentials: Optional[HTTPAuthorizationCredentials] = Injects(),
+) -> Any:
+ if credentials is None:
+ return {"msg": "Create an account first"}
+ return {"scheme": credentials.scheme, "credentials": credentials.credentials}
+
+
+def test_security_http_base():
+ with create_client(routes=[Gateway(handler=read_current_user)]) as client:
+ response = client.get("/users/me", headers={"Authorization": "Other foobar"})
+ assert response.status_code == 200, response.text
+ assert response.json() == {"scheme": "Other", "credentials": "foobar"}
+
+
+def test_security_http_base_no_credentials():
+ with create_client(routes=[Gateway(handler=read_current_user)]) as client:
+ response = client.get("/users/me")
+ assert response.status_code == 200, response.text
+ assert response.json() == {"msg": "Create an account first"}
+
+
+def test_openapi_schema():
+ with create_client(routes=[Gateway(handler=read_current_user)], enable_openapi=True) as client:
+ response = client.get("/openapi.json")
+ assert response.status_code == 200, response.text
+
+ assert response.json() == {
+ "openapi": "3.1.0",
+ "info": {
+ "title": "Esmerald",
+ "summary": "Esmerald application",
+ "description": "Highly scalable, performant, easy to learn and for every application.",
+ "contact": {"name": "admin", "email": "admin@myapp.com"},
+ "version": client.app.version,
+ },
+ "servers": [{"url": "/"}],
+ "paths": {
+ "/users/me": {
+ "get": {
+ "summary": "Read Current User",
+ "description": "",
+ "operationId": "read_current_user_users_me_get",
+ "deprecated": False,
+ "security": [
+ {
+ "HTTPBase": {
+ "type": "http",
+ "scheme": "Other",
+ "scheme_name": "HTTPBase",
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {"application/json": {"schema": {"type": "string"}}},
+ }
+ },
+ }
+ }
+ },
+ "components": {"securitySchemes": {"HTTPBase": {"type": "http", "scheme": "Other"}}},
+ }
diff --git a/tests/security/http/test_security_http_basic_optional.py b/tests/security/http/test_security_http_basic_optional.py
new file mode 100644
index 00000000..993c0f43
--- /dev/null
+++ b/tests/security/http/test_security_http_basic_optional.py
@@ -0,0 +1,100 @@
+from base64 import b64encode
+from typing import Any, Optional
+
+from esmerald import Gateway, Inject, Injects, get
+from esmerald.security.http import HTTPBasic, HTTPBasicCredentials
+from esmerald.testclient import create_client
+
+security = HTTPBasic(auto_error=False)
+
+
+@get(
+ "/users/me",
+ security=[security],
+ dependencies={"credentials": Inject(security)},
+)
+def read_current_user(credentials: Optional[HTTPBasicCredentials] = Injects()) -> Any:
+ if credentials is None:
+ return {"msg": "Create an account first"}
+ return {"username": credentials.username, "password": credentials.password}
+
+
+def test_security_http_basic():
+ with create_client(
+ routes=[
+ Gateway(handler=read_current_user),
+ ],
+ ) as client:
+ response = client.get("/users/me", auth=("john", "secret"))
+ assert response.status_code == 200, response.text
+ assert response.json() == {"username": "john", "password": "secret"}
+
+
+def test_security_http_basic_no_credentials():
+ with create_client(routes=[Gateway(handler=read_current_user)]) as client:
+ response = client.get("/users/me")
+ assert response.status_code == 200, response.text
+ assert response.json() == {"msg": "Create an account first"}
+
+
+def test_security_http_basic_invalid_credentials():
+ with create_client(routes=[Gateway(handler=read_current_user)]) as client:
+ response = client.get("/users/me", headers={"Authorization": "Basic notabase64token"})
+ assert response.status_code == 401, response.text
+ assert response.headers["WWW-Authenticate"] == "Basic"
+ assert response.json() == {"detail": "Invalid authentication credentials"}
+
+
+def test_security_http_basic_non_basic_credentials():
+ payload = b64encode(b"johnsecret").decode("ascii")
+ auth_header = f"Basic {payload}"
+
+ with create_client(routes=[Gateway(handler=read_current_user)]) as client:
+ response = client.get("/users/me", headers={"Authorization": auth_header})
+ assert response.status_code == 401, response.text
+ assert response.headers["WWW-Authenticate"] == "Basic"
+ assert response.json() == {"detail": "Invalid authentication credentials"}
+
+
+def test_openapi_schema():
+ with create_client(routes=[Gateway(handler=read_current_user)], enable_openapi=True) as client:
+ response = client.get("/openapi.json")
+ assert response.status_code == 200, response.text
+
+ assert response.json() == {
+ "openapi": "3.1.0",
+ "info": {
+ "title": "Esmerald",
+ "summary": "Esmerald application",
+ "description": "Highly scalable, performant, easy to learn and for every application.",
+ "contact": {"name": "admin", "email": "admin@myapp.com"},
+ "version": client.app.version,
+ },
+ "servers": [{"url": "/"}],
+ "paths": {
+ "/users/me": {
+ "get": {
+ "summary": "Read Current User",
+ "description": "",
+ "operationId": "read_current_user_users_me_get",
+ "deprecated": False,
+ "security": [
+ {
+ "HTTPBasic": {
+ "type": "http",
+ "scheme": "basic",
+ "scheme_name": "HTTPBasic",
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {"application/json": {"schema": {"type": "string"}}},
+ }
+ },
+ }
+ }
+ },
+ "components": {"securitySchemes": {"HTTPBasic": {"type": "http", "scheme": "basic"}}},
+ }
diff --git a/tests/security/http/test_security_http_basic_realm.py b/tests/security/http/test_security_http_basic_realm.py
new file mode 100644
index 00000000..e020c2d8
--- /dev/null
+++ b/tests/security/http/test_security_http_basic_realm.py
@@ -0,0 +1,112 @@
+from base64 import b64encode
+from typing import Any
+
+from esmerald import Gateway, Inject, Injects, get
+from esmerald.security.http import HTTPBasic, HTTPBasicCredentials
+from esmerald.testclient import create_client
+
+security = HTTPBasic(realm="simple")
+
+
+@get("/users/me", security=[security], dependencies={"credentials": Inject(security)})
+def read_current_user(credentials: HTTPBasicCredentials = Injects()) -> Any:
+ return {"username": credentials.username, "password": credentials.password}
+
+
+def test_security_http_basic():
+ with create_client(
+ routes=[
+ Gateway(handler=read_current_user),
+ ],
+ ) as client:
+ response = client.get("/users/me", auth=("john", "secret"))
+ assert response.status_code == 200, response.text
+ assert response.json() == {"username": "john", "password": "secret"}
+
+
+def test_security_http_basic_no_credentials():
+ with create_client(
+ routes=[
+ Gateway(handler=read_current_user),
+ ],
+ ) as client:
+ response = client.get("/users/me")
+ assert response.json() == {"detail": "Not authenticated"}
+ assert response.status_code == 401, response.text
+ assert response.headers["WWW-Authenticate"] == 'Basic realm="simple"'
+
+
+def test_security_http_basic_invalid_credentials():
+ with create_client(
+ routes=[
+ Gateway(handler=read_current_user),
+ ],
+ ) as client:
+ response = client.get("/users/me", headers={"Authorization": "Basic notabase64token"})
+ assert response.status_code == 401, response.text
+ assert response.headers["WWW-Authenticate"] == 'Basic realm="simple"'
+ assert response.json() == {"detail": "Invalid authentication credentials"}
+
+
+def test_security_http_basic_non_basic_credentials():
+ payload = b64encode(b"johnsecret").decode("ascii")
+ auth_header = f"Basic {payload}"
+
+ with create_client(
+ routes=[
+ Gateway(handler=read_current_user),
+ ],
+ ) as client:
+ response = client.get("/users/me", headers={"Authorization": auth_header})
+ assert response.status_code == 401, response.text
+ assert response.headers["WWW-Authenticate"] == 'Basic realm="simple"'
+ assert response.json() == {"detail": "Invalid authentication credentials"}
+
+
+def test_openapi_schema():
+ with create_client(
+ routes=[
+ Gateway(handler=read_current_user),
+ ],
+ ) as client:
+ response = client.get("/openapi.json")
+ assert response.status_code == 200, response.text
+
+ assert response.json() == {
+ "openapi": "3.1.0",
+ "info": {
+ "title": "Esmerald",
+ "summary": "Esmerald application",
+ "description": "Highly scalable, performant, easy to learn and for every application.",
+ "contact": {"name": "admin", "email": "admin@myapp.com"},
+ "version": client.app.version,
+ },
+ "servers": [{"url": "/"}],
+ "paths": {
+ "/users/me": {
+ "get": {
+ "summary": "Read Current User",
+ "description": "",
+ "operationId": "read_current_user_users_me_get",
+ "deprecated": False,
+ "security": [
+ {
+ "HTTPBasic": {
+ "type": "http",
+ "scheme": "basic",
+ "scheme_name": "HTTPBasic",
+ "realm": "simple",
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {"application/json": {"schema": {"type": "string"}}},
+ }
+ },
+ }
+ }
+ },
+ "components": {"securitySchemes": {"HTTPBasic": {"type": "http", "scheme": "basic"}}},
+ }
diff --git a/tests/security/http/test_security_http_basic_realm_description.py b/tests/security/http/test_security_http_basic_realm_description.py
new file mode 100644
index 00000000..629a873e
--- /dev/null
+++ b/tests/security/http/test_security_http_basic_realm_description.py
@@ -0,0 +1,105 @@
+from base64 import b64encode
+from typing import Any
+
+from esmerald import Gateway, Inject, Injects, get
+from esmerald.security.http import HTTPBasic, HTTPBasicCredentials
+from esmerald.testclient import create_client
+
+security = HTTPBasic(realm="simple", description="HTTPBasic scheme")
+
+
+@get("/users/me", security=[security], dependencies={"credentials": Inject(security)})
+def read_current_user(credentials: HTTPBasicCredentials = Injects()) -> Any:
+ return {"username": credentials.username, "password": credentials.password}
+
+
+def test_security_http_basic():
+ with create_client(
+ routes=[
+ Gateway(handler=read_current_user),
+ ],
+ ) as client:
+ response = client.get("/users/me", auth=("john", "secret"))
+ assert response.status_code == 200, response.text
+ assert response.json() == {"username": "john", "password": "secret"}
+
+
+def test_security_http_basic_no_credentials():
+ with create_client(routes=[Gateway(handler=read_current_user)]) as client:
+ response = client.get("/users/me")
+ assert response.json() == {"detail": "Not authenticated"}
+ assert response.status_code == 401, response.text
+ assert response.headers["WWW-Authenticate"] == 'Basic realm="simple"'
+
+
+def test_security_http_basic_invalid_credentials():
+ with create_client(routes=[Gateway(handler=read_current_user)]) as client:
+ response = client.get("/users/me", headers={"Authorization": "Basic notabase64token"})
+ assert response.status_code == 401, response.text
+ assert response.headers["WWW-Authenticate"] == 'Basic realm="simple"'
+ assert response.json() == {"detail": "Invalid authentication credentials"}
+
+
+def test_security_http_basic_non_basic_credentials():
+ payload = b64encode(b"johnsecret").decode("ascii")
+ auth_header = f"Basic {payload}"
+
+ with create_client(routes=[Gateway(handler=read_current_user)]) as client:
+ response = client.get("/users/me", headers={"Authorization": auth_header})
+ assert response.status_code == 401, response.text
+ assert response.headers["WWW-Authenticate"] == 'Basic realm="simple"'
+ assert response.json() == {"detail": "Invalid authentication credentials"}
+
+
+def test_openapi_schema():
+ with create_client(routes=[Gateway(handler=read_current_user)]) as client:
+ response = client.get("/openapi.json")
+ assert response.status_code == 200, response.text
+
+ assert response.json() == {
+ "openapi": "3.1.0",
+ "info": {
+ "title": "Esmerald",
+ "summary": "Esmerald application",
+ "description": "Highly scalable, performant, easy to learn and for every application.",
+ "contact": {"name": "admin", "email": "admin@myapp.com"},
+ "version": client.app.version,
+ },
+ "servers": [{"url": "/"}],
+ "paths": {
+ "/users/me": {
+ "get": {
+ "summary": "Read Current User",
+ "description": "",
+ "operationId": "read_current_user_users_me_get",
+ "deprecated": False,
+ "security": [
+ {
+ "HTTPBasic": {
+ "type": "http",
+ "description": "HTTPBasic scheme",
+ "scheme": "basic",
+ "scheme_name": "HTTPBasic",
+ "realm": "simple",
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {"application/json": {"schema": {"type": "string"}}},
+ }
+ },
+ }
+ }
+ },
+ "components": {
+ "securitySchemes": {
+ "HTTPBasic": {
+ "type": "http",
+ "description": "HTTPBasic scheme",
+ "scheme": "basic",
+ }
+ }
+ },
+ }
diff --git a/tests/security/http/test_security_http_bearer.py b/tests/security/http/test_security_http_bearer.py
new file mode 100644
index 00000000..59987102
--- /dev/null
+++ b/tests/security/http/test_security_http_bearer.py
@@ -0,0 +1,83 @@
+from typing import Any
+
+from esmerald import Inject, Injects, get
+from esmerald.security.http import HTTPAuthorizationCredentials, HTTPBearer
+from esmerald.testclient import create_client
+
+security = HTTPBearer()
+
+
+@get(
+ "/users/me",
+ security=[security],
+ dependencies={"credentials": Inject(security)},
+)
+def read_current_user(credentials: HTTPAuthorizationCredentials = Injects()) -> Any:
+ return {"scheme": credentials.scheme, "credentials": credentials.credentials}
+
+
+def test_security_http_bearer():
+ with create_client(routes=[read_current_user]) as client:
+ response = client.get("/users/me", headers={"Authorization": "Bearer foobar"})
+ assert response.status_code == 200, response.text
+ assert response.json() == {"scheme": "Bearer", "credentials": "foobar"}
+
+
+def test_security_http_bearer_no_credentials():
+ with create_client(routes=[read_current_user]) as client:
+ response = client.get("/users/me")
+ assert response.status_code == 403, response.text
+ assert response.json() == {"detail": "Not authenticated"}
+
+
+def test_security_http_bearer_incorrect_scheme_credentials():
+ with create_client(routes=[read_current_user]) as client:
+ response = client.get("/users/me", headers={"Authorization": "Basic notreally"})
+ assert response.status_code == 403, response.text
+ assert response.json() == {"detail": "Invalid authentication credentials"}
+
+
+def test_openapi_schema():
+ with create_client(routes=[read_current_user]) as client:
+ response = client.get("/openapi.json")
+ assert response.status_code == 200, response.text
+
+ assert response.json() == {
+ "openapi": "3.1.0",
+ "info": {
+ "title": "Esmerald",
+ "summary": "Esmerald application",
+ "description": "Highly scalable, performant, easy to learn and for every application.",
+ "contact": {"name": "admin", "email": "admin@myapp.com"},
+ "version": client.app.version,
+ },
+ "servers": [{"url": "/"}],
+ "paths": {
+ "/users/me": {
+ "get": {
+ "summary": "Read Current User",
+ "description": "",
+ "operationId": "read_current_user_users_me_get",
+ "deprecated": False,
+ "security": [
+ {
+ "HTTPBearer": {
+ "type": "http",
+ "scheme": "bearer",
+ "scheme_name": "HTTPBearer",
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {"application/json": {"schema": {"type": "string"}}},
+ }
+ },
+ }
+ }
+ },
+ "components": {
+ "securitySchemes": {"HTTPBearer": {"type": "http", "scheme": "bearer"}}
+ },
+ }
diff --git a/tests/security/http/test_security_http_bearer_description.py b/tests/security/http/test_security_http_bearer_description.py
new file mode 100644
index 00000000..43074eac
--- /dev/null
+++ b/tests/security/http/test_security_http_bearer_description.py
@@ -0,0 +1,90 @@
+from typing import Any
+
+from esmerald import Inject, Injects, get
+from esmerald.security.http import HTTPAuthorizationCredentials, HTTPBearer
+from esmerald.testclient import create_client
+
+security = HTTPBearer(description="HTTP Bearer token scheme")
+
+
+@get(
+ "/users/me",
+ security=[security],
+ dependencies={"credentials": Inject(security)},
+)
+def read_current_user(credentials: HTTPAuthorizationCredentials = Injects()) -> Any:
+ return {"scheme": credentials.scheme, "credentials": credentials.credentials}
+
+
+def test_security_http_bearer():
+ with create_client(routes=[read_current_user]) as client:
+ response = client.get("/users/me", headers={"Authorization": "Bearer foobar"})
+ assert response.status_code == 200, response.text
+ assert response.json() == {"scheme": "Bearer", "credentials": "foobar"}
+
+
+def test_security_http_bearer_no_credentials():
+ with create_client(routes=[read_current_user]) as client:
+ response = client.get("/users/me")
+ assert response.status_code == 403, response.text
+ assert response.json() == {"detail": "Not authenticated"}
+
+
+def test_security_http_bearer_incorrect_scheme_credentials():
+ with create_client(routes=[read_current_user]) as client:
+ response = client.get("/users/me", headers={"Authorization": "Basic notreally"})
+ assert response.status_code == 403, response.text
+ assert response.json() == {"detail": "Invalid authentication credentials"}
+
+
+def test_openapi_schema():
+ with create_client(routes=[read_current_user]) as client:
+ response = client.get("/openapi.json")
+ assert response.status_code == 200, response.text
+
+ assert response.json() == {
+ "openapi": "3.1.0",
+ "info": {
+ "title": "Esmerald",
+ "summary": "Esmerald application",
+ "description": "Highly scalable, performant, easy to learn and for every application.",
+ "contact": {"name": "admin", "email": "admin@myapp.com"},
+ "version": client.app.version,
+ },
+ "servers": [{"url": "/"}],
+ "paths": {
+ "/users/me": {
+ "get": {
+ "summary": "Read Current User",
+ "description": "",
+ "operationId": "read_current_user_users_me_get",
+ "deprecated": False,
+ "security": [
+ {
+ "HTTPBearer": {
+ "type": "http",
+ "description": "HTTP Bearer token scheme",
+ "scheme": "bearer",
+ "scheme_name": "HTTPBearer",
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {"application/json": {"schema": {"type": "string"}}},
+ }
+ },
+ }
+ }
+ },
+ "components": {
+ "securitySchemes": {
+ "HTTPBearer": {
+ "type": "http",
+ "description": "HTTP Bearer token scheme",
+ "scheme": "bearer",
+ }
+ }
+ },
+ }
diff --git a/tests/security/http/test_security_http_bearer_optional.py b/tests/security/http/test_security_http_bearer_optional.py
new file mode 100644
index 00000000..499efa98
--- /dev/null
+++ b/tests/security/http/test_security_http_bearer_optional.py
@@ -0,0 +1,85 @@
+from typing import Any, Optional
+
+from esmerald import Inject, Injects, get
+from esmerald.security.http import HTTPAuthorizationCredentials, HTTPBearer
+from esmerald.testclient import create_client
+
+security = HTTPBearer(auto_error=False)
+
+
+@get(
+ "/users/me",
+ security=[security],
+ dependencies={"credentials": Inject(security)},
+)
+def read_current_user(credentials: Optional[HTTPAuthorizationCredentials] = Injects()) -> Any:
+ if credentials is None:
+ return {"msg": "Create an account first"}
+ return {"scheme": credentials.scheme, "credentials": credentials.credentials}
+
+
+def xtest_security_http_bearer():
+ with create_client(routes=[read_current_user]) as client:
+ response = client.get("/users/me", headers={"Authorization": "Bearer foobar"})
+ assert response.status_code == 200, response.text
+ assert response.json() == {"scheme": "Bearer", "credentials": "foobar"}
+
+
+def test_security_http_bearer_no_credentials():
+ with create_client(routes=[read_current_user]) as client:
+ response = client.get("/users/me")
+ assert response.status_code == 200, response.text
+ assert response.json() == {"msg": "Create an account first"}
+
+
+def test_security_http_bearer_incorrect_scheme_credentials():
+ with create_client(routes=[read_current_user]) as client:
+ response = client.get("/users/me", headers={"Authorization": "Basic notreally"})
+ assert response.status_code == 200, response.text
+ assert response.json() == {"msg": "Create an account first"}
+
+
+def test_openapi_schema():
+ with create_client(routes=[read_current_user]) as client:
+ response = client.get("/openapi.json")
+ assert response.status_code == 200, response.text
+
+ assert response.json() == {
+ "openapi": "3.1.0",
+ "info": {
+ "title": "Esmerald",
+ "summary": "Esmerald application",
+ "description": "Highly scalable, performant, easy to learn and for every application.",
+ "contact": {"name": "admin", "email": "admin@myapp.com"},
+ "version": client.app.version,
+ },
+ "servers": [{"url": "/"}],
+ "paths": {
+ "/users/me": {
+ "get": {
+ "summary": "Read Current User",
+ "description": "",
+ "operationId": "read_current_user_users_me_get",
+ "deprecated": False,
+ "security": [
+ {
+ "HTTPBearer": {
+ "type": "http",
+ "scheme": "bearer",
+ "scheme_name": "HTTPBearer",
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {"application/json": {"schema": {"type": "string"}}},
+ }
+ },
+ }
+ }
+ },
+ "components": {
+ "securitySchemes": {"HTTPBearer": {"type": "http", "scheme": "bearer"}}
+ },
+ }
diff --git a/tests/security/http/test_security_http_digest.py b/tests/security/http/test_security_http_digest.py
new file mode 100644
index 00000000..cca0391d
--- /dev/null
+++ b/tests/security/http/test_security_http_digest.py
@@ -0,0 +1,79 @@
+from typing import Any
+
+from esmerald import Inject, Injects, get
+from esmerald.security.http import HTTPAuthorizationCredentials, HTTPDigest
+from esmerald.testclient import create_client
+
+security = HTTPDigest()
+
+
+@get("/users/me", security=[security], dependencies={"credentials": Inject(security)})
+def read_current_user(credentials: HTTPAuthorizationCredentials = Injects()) -> Any:
+ return {"scheme": credentials.scheme, "credentials": credentials.credentials}
+
+
+def xtest_security_http_digest():
+ with create_client(routes=[read_current_user]) as client:
+ response = client.get("/users/me", headers={"Authorization": "Digest foobar"})
+ assert response.status_code == 200, response.text
+ assert response.json() == {"scheme": "Digest", "credentials": "foobar"}
+
+
+def test_security_http_digest_no_credentials():
+ with create_client(routes=[read_current_user]) as client:
+ response = client.get("/users/me")
+ assert response.status_code == 403, response.text
+ assert response.json() == {"detail": "Not authenticated"}
+
+
+def test_security_http_digest_incorrect_scheme_credentials():
+ with create_client(routes=[read_current_user]) as client:
+ response = client.get("/users/me", headers={"Authorization": "Other invalidauthorization"})
+ assert response.status_code == 403, response.text
+ assert response.json() == {"detail": "Invalid authentication credentials"}
+
+
+def test_openapi_schema():
+ with create_client(routes=[read_current_user]) as client:
+ response = client.get("/openapi.json")
+ assert response.status_code == 200, response.text
+
+ assert response.json() == {
+ "openapi": "3.1.0",
+ "info": {
+ "title": "Esmerald",
+ "summary": "Esmerald application",
+ "description": "Highly scalable, performant, easy to learn and for every application.",
+ "contact": {"name": "admin", "email": "admin@myapp.com"},
+ "version": client.app.version,
+ },
+ "servers": [{"url": "/"}],
+ "paths": {
+ "/users/me": {
+ "get": {
+ "summary": "Read Current User",
+ "description": "",
+ "operationId": "read_current_user_users_me_get",
+ "deprecated": False,
+ "security": [
+ {
+ "HTTPDigest": {
+ "type": "http",
+ "scheme": "digest",
+ "scheme_name": "HTTPDigest",
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {"application/json": {"schema": {"type": "string"}}},
+ }
+ },
+ }
+ }
+ },
+ "components": {
+ "securitySchemes": {"HTTPDigest": {"type": "http", "scheme": "digest"}}
+ },
+ }
diff --git a/tests/security/http/test_security_http_digest_description.py b/tests/security/http/test_security_http_digest_description.py
new file mode 100644
index 00000000..694a9a54
--- /dev/null
+++ b/tests/security/http/test_security_http_digest_description.py
@@ -0,0 +1,86 @@
+from typing import Any
+
+from esmerald import Inject, Injects, get
+from esmerald.security.http import HTTPAuthorizationCredentials, HTTPDigest
+from esmerald.testclient import create_client
+
+security = HTTPDigest(description="HTTPDigest scheme")
+
+
+@get("/users/me", security=[security], dependencies={"credentials": Inject(security)})
+def read_current_user(credentials: HTTPAuthorizationCredentials = Injects()) -> Any:
+ return {"scheme": credentials.scheme, "credentials": credentials.credentials}
+
+
+def test_security_http_digest():
+ with create_client(routes=[read_current_user]) as client:
+ response = client.get("/users/me", headers={"Authorization": "Digest foobar"})
+ assert response.status_code == 200, response.text
+ assert response.json() == {"scheme": "Digest", "credentials": "foobar"}
+
+
+def test_security_http_digest_no_credentials():
+ with create_client(routes=[read_current_user]) as client:
+ response = client.get("/users/me")
+ assert response.status_code == 403, response.text
+ assert response.json() == {"detail": "Not authenticated"}
+
+
+def test_security_http_digest_incorrect_scheme_credentials():
+ with create_client(routes=[read_current_user]) as client:
+ response = client.get("/users/me", headers={"Authorization": "Other invalidauthorization"})
+ assert response.status_code == 403, response.text
+ assert response.json() == {"detail": "Invalid authentication credentials"}
+
+
+def test_openapi_schema():
+ with create_client(routes=[read_current_user]) as client:
+ response = client.get("/openapi.json")
+ assert response.status_code == 200, response.text
+
+ assert response.json() == {
+ "openapi": "3.1.0",
+ "info": {
+ "title": "Esmerald",
+ "summary": "Esmerald application",
+ "description": "Highly scalable, performant, easy to learn and for every application.",
+ "contact": {"name": "admin", "email": "admin@myapp.com"},
+ "version": client.app.version,
+ },
+ "servers": [{"url": "/"}],
+ "paths": {
+ "/users/me": {
+ "get": {
+ "summary": "Read Current User",
+ "description": "",
+ "operationId": "read_current_user_users_me_get",
+ "deprecated": False,
+ "security": [
+ {
+ "HTTPDigest": {
+ "type": "http",
+ "description": "HTTPDigest scheme",
+ "scheme": "digest",
+ "scheme_name": "HTTPDigest",
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {"application/json": {"schema": {"type": "string"}}},
+ }
+ },
+ }
+ }
+ },
+ "components": {
+ "securitySchemes": {
+ "HTTPDigest": {
+ "type": "http",
+ "description": "HTTPDigest scheme",
+ "scheme": "digest",
+ }
+ }
+ },
+ }
diff --git a/tests/security/http/test_security_http_digest_optional.py b/tests/security/http/test_security_http_digest_optional.py
new file mode 100644
index 00000000..c3fd03c7
--- /dev/null
+++ b/tests/security/http/test_security_http_digest_optional.py
@@ -0,0 +1,83 @@
+from typing import Any, Optional
+
+from esmerald import Inject, Injects, get
+from esmerald.security.http import HTTPAuthorizationCredentials, HTTPDigest
+from esmerald.testclient import create_client
+
+security = HTTPDigest(auto_error=False)
+
+
+@get("/users/me", security=[security], dependencies={"credentials": Inject(security)})
+def read_current_user(
+ credentials: Optional[HTTPAuthorizationCredentials] = Injects(),
+) -> Any:
+ if credentials is None:
+ return {"msg": "Create an account first"}
+ return {"scheme": credentials.scheme, "credentials": credentials.credentials}
+
+
+def xtest_security_http_digest():
+ with create_client(routes=[read_current_user]) as client:
+ response = client.get("/users/me", headers={"Authorization": "Digest foobar"})
+ assert response.status_code == 200, response.text
+ assert response.json() == {"scheme": "Digest", "credentials": "foobar"}
+
+
+def test_security_http_digest_no_credentials():
+ with create_client(routes=[read_current_user]) as client:
+ response = client.get("/users/me")
+ assert response.status_code == 200, response.text
+ assert response.json() == {"msg": "Create an account first"}
+
+
+def test_security_http_digest_incorrect_scheme_credentials():
+ with create_client(routes=[read_current_user]) as client:
+ response = client.get("/users/me", headers={"Authorization": "Other invalidauthorization"})
+ assert response.status_code == 403, response.text
+ assert response.json() == {"detail": "Invalid authentication credentials"}
+
+
+def test_openapi_schema():
+ with create_client(routes=[read_current_user]) as client:
+ response = client.get("/openapi.json")
+ assert response.status_code == 200, response.text
+
+ assert response.json() == {
+ "openapi": "3.1.0",
+ "info": {
+ "title": "Esmerald",
+ "summary": "Esmerald application",
+ "description": "Highly scalable, performant, easy to learn and for every application.",
+ "contact": {"name": "admin", "email": "admin@myapp.com"},
+ "version": client.app.version,
+ },
+ "servers": [{"url": "/"}],
+ "paths": {
+ "/users/me": {
+ "get": {
+ "summary": "Read Current User",
+ "description": "",
+ "operationId": "read_current_user_users_me_get",
+ "deprecated": False,
+ "security": [
+ {
+ "HTTPDigest": {
+ "type": "http",
+ "scheme": "digest",
+ "scheme_name": "HTTPDigest",
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {"application/json": {"schema": {"type": "string"}}},
+ }
+ },
+ }
+ }
+ },
+ "components": {
+ "securitySchemes": {"HTTPDigest": {"type": "http", "scheme": "digest"}}
+ },
+ }
diff --git a/tests/_security/open_api_classes/__init__.py b/tests/security/oauth/__init__.py
similarity index 100%
rename from tests/_security/open_api_classes/__init__.py
rename to tests/security/oauth/__init__.py
diff --git a/tests/security/oauth/test_oauth_code_bearer.py b/tests/security/oauth/test_oauth_code_bearer.py
new file mode 100644
index 00000000..77789ce1
--- /dev/null
+++ b/tests/security/oauth/test_oauth_code_bearer.py
@@ -0,0 +1,114 @@
+from typing import Any, Optional
+
+from esmerald import Gateway, Inject, Injects, get
+from esmerald.security.oauth2 import OAuth2AuthorizationCodeBearer
+from esmerald.testclient import create_client
+
+oauth2_scheme = OAuth2AuthorizationCodeBearer(
+ authorizationUrl="authorize", tokenUrl="token", auto_error=True
+)
+
+
+@get("/items", dependencies={"token": Inject(oauth2_scheme)}, security=[oauth2_scheme])
+async def read_items(token: Optional[str] = Injects()) -> dict[str, Any]:
+ return {"token": token}
+
+
+def test_no_token():
+ with create_client(
+ routes=[
+ Gateway(handler=read_items),
+ ],
+ ) as client:
+ response = client.get("/items")
+ assert response.status_code == 401, response.text
+ assert response.json() == {"detail": "Not authenticated"}
+
+
+def test_incorrect_token():
+ with create_client(
+ routes=[
+ Gateway(handler=read_items),
+ ],
+ ) as client:
+ response = client.get("/items", headers={"Authorization": "Non-existent testtoken"})
+ assert response.status_code == 401, response.text
+ assert response.json() == {"detail": "Not authenticated"}
+
+
+def test_token():
+ with create_client(
+ routes=[
+ Gateway(handler=read_items),
+ ],
+ ) as client:
+ response = client.get("/items", headers={"Authorization": "Bearer testtoken"})
+ assert response.status_code == 200, response.text
+ assert response.json() == {"token": "testtoken"}
+
+
+def test_openapi_schema():
+ with create_client(
+ routes=[
+ Gateway(handler=read_items),
+ ],
+ ) as client:
+ response = client.get("/openapi.json")
+ assert response.status_code == 200, response.text
+
+ assert response.json() == {
+ "openapi": "3.1.0",
+ "info": {
+ "title": "Esmerald",
+ "summary": "Esmerald application",
+ "description": "Highly scalable, performant, easy to learn and for every application.",
+ "contact": {"name": "admin", "email": "admin@myapp.com"},
+ "version": client.app.version,
+ },
+ "servers": [{"url": "/"}],
+ "paths": {
+ "/items": {
+ "get": {
+ "summary": "Read Items",
+ "description": "",
+ "operationId": "read_items_items_get",
+ "deprecated": False,
+ "security": [
+ {
+ "OAuth2AuthorizationCodeBearer": {
+ "type": "oauth2",
+ "flows": {
+ "authorizationCode": {
+ "authorizationUrl": "authorize",
+ "tokenUrl": "token",
+ "scopes": {},
+ }
+ },
+ "scheme_name": "OAuth2AuthorizationCodeBearer",
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {"application/json": {"schema": {"type": "string"}}},
+ }
+ },
+ }
+ }
+ },
+ "components": {
+ "securitySchemes": {
+ "OAuth2AuthorizationCodeBearer": {
+ "type": "oauth2",
+ "flows": {
+ "authorizationCode": {
+ "authorizationUrl": "authorize",
+ "tokenUrl": "token",
+ "scopes": {},
+ }
+ },
+ }
+ }
+ },
+ }
diff --git a/tests/security/oauth/test_oauth_code_bearer_desc.py b/tests/security/oauth/test_oauth_code_bearer_desc.py
new file mode 100644
index 00000000..11f349b4
--- /dev/null
+++ b/tests/security/oauth/test_oauth_code_bearer_desc.py
@@ -0,0 +1,119 @@
+from typing import Any, Optional
+
+from esmerald import Gateway, Inject, Injects, get
+from esmerald.security.oauth2 import OAuth2AuthorizationCodeBearer
+from esmerald.testclient import create_client
+
+oauth2_scheme = OAuth2AuthorizationCodeBearer(
+ authorizationUrl="authorize",
+ tokenUrl="token",
+ description="OAuth2 Code Bearer",
+ auto_error=True,
+)
+
+
+@get("/items", dependencies={"token": Inject(oauth2_scheme)}, security=[oauth2_scheme])
+async def read_items(token: Optional[str] = Injects()) -> dict[str, Any]:
+ return {"token": token}
+
+
+def test_no_token():
+ with create_client(
+ routes=[
+ Gateway(handler=read_items),
+ ],
+ ) as client:
+ response = client.get("/items")
+ assert response.status_code == 401, response.text
+ assert response.json() == {"detail": "Not authenticated"}
+
+
+def test_incorrect_token():
+ with create_client(
+ routes=[
+ Gateway(handler=read_items),
+ ],
+ ) as client:
+ response = client.get("/items", headers={"Authorization": "Non-existent testtoken"})
+ assert response.status_code == 401, response.text
+ assert response.json() == {"detail": "Not authenticated"}
+
+
+def test_token():
+ with create_client(
+ routes=[
+ Gateway(handler=read_items),
+ ],
+ ) as client:
+ response = client.get("/items", headers={"Authorization": "Bearer testtoken"})
+ assert response.status_code == 200, response.text
+ assert response.json() == {"token": "testtoken"}
+
+
+def test_openapi_schema():
+ with create_client(
+ routes=[
+ Gateway(handler=read_items),
+ ],
+ ) as client:
+ response = client.get("/openapi.json")
+ assert response.status_code == 200, response.text
+
+ assert response.json() == {
+ "openapi": "3.1.0",
+ "info": {
+ "title": "Esmerald",
+ "summary": "Esmerald application",
+ "description": "Highly scalable, performant, easy to learn and for every application.",
+ "contact": {"name": "admin", "email": "admin@myapp.com"},
+ "version": client.app.version,
+ },
+ "servers": [{"url": "/"}],
+ "paths": {
+ "/items": {
+ "get": {
+ "summary": "Read Items",
+ "description": "",
+ "operationId": "read_items_items_get",
+ "deprecated": False,
+ "security": [
+ {
+ "OAuth2AuthorizationCodeBearer": {
+ "type": "oauth2",
+ "description": "OAuth2 Code Bearer",
+ "flows": {
+ "authorizationCode": {
+ "authorizationUrl": "authorize",
+ "tokenUrl": "token",
+ "scopes": {},
+ }
+ },
+ "scheme_name": "OAuth2AuthorizationCodeBearer",
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {"application/json": {"schema": {"type": "string"}}},
+ }
+ },
+ }
+ }
+ },
+ "components": {
+ "securitySchemes": {
+ "OAuth2AuthorizationCodeBearer": {
+ "type": "oauth2",
+ "description": "OAuth2 Code Bearer",
+ "flows": {
+ "authorizationCode": {
+ "authorizationUrl": "authorize",
+ "tokenUrl": "token",
+ "scopes": {},
+ }
+ },
+ }
+ }
+ },
+ }
diff --git a/tests/security/oauth/test_security_oauth2_optional_desc.py b/tests/security/oauth/test_security_oauth2_optional_desc.py
new file mode 100644
index 00000000..901714e4
--- /dev/null
+++ b/tests/security/oauth/test_security_oauth2_optional_desc.py
@@ -0,0 +1,167 @@
+from typing import Any, Dict, Optional, Union
+
+from pydantic import BaseModel, __version__
+
+from esmerald import Gateway, Inject, Injects, Security, get, post
+from esmerald.security.oauth2 import OAuth2, OAuth2PasswordRequestFormStrict
+from esmerald.testclient import create_client
+
+pydantic_version = ".".join(__version__.split(".")[:2])
+
+reusable_oauth2 = OAuth2(
+ flows={
+ "password": {
+ "tokenUrl": "token",
+ "scopes": {"read:users": "Read the users", "write:users": "Create users"},
+ }
+ },
+ description="OAuth2 security scheme",
+ auto_error=False,
+)
+
+
+class User(BaseModel):
+ username: str
+
+
+def get_current_user(oauth_header: Union[str, Any] = Security(reusable_oauth2)):
+ if oauth_header is None:
+ return None
+ user = User(username=oauth_header)
+ return user
+
+
+@post(
+ "/login",
+ security=[reusable_oauth2],
+ dependencies={"form_data": Inject(OAuth2PasswordRequestFormStrict)},
+)
+def login(form_data: OAuth2PasswordRequestFormStrict = Injects()) -> Dict[str, Any]:
+ return form_data
+
+
+@get(
+ "/users/me",
+ dependencies={"current_user": Inject(get_current_user)},
+ security=[reusable_oauth2],
+)
+def read_users_me(current_user: Optional[User] = Injects()) -> Dict[str, Any]:
+ if current_user is None:
+ return {"msg": "Create an account first"}
+ return current_user
+
+
+def test_security_oauth2():
+ with create_client(
+ routes=[Gateway(handler=read_users_me)], security=[reusable_oauth2]
+ ) as client:
+ response = client.get("/users/me", headers={"Authorization": "Bearer footokenbar"})
+ assert response.status_code == 200, response.text
+ assert response.json() == {"username": "Bearer footokenbar"}
+
+
+def test_security_oauth2_password_other_header():
+ with create_client(
+ routes=[Gateway(handler=read_users_me)], security=[reusable_oauth2]
+ ) as client:
+ response = client.get("/users/me", headers={"Authorization": "Other footokenbar"})
+ assert response.status_code == 200, response.text
+ assert response.json() == {"username": "Other footokenbar"}
+
+
+def test_security_oauth2_password_bearer_no_header():
+ with create_client(
+ routes=[Gateway(handler=read_users_me)], security=[reusable_oauth2]
+ ) as client:
+ response = client.get("/users/me")
+ assert response.status_code == 200, response.text
+ assert response.json() == {"msg": "Create an account first"}
+
+
+def test_strict_login_None():
+ with create_client(routes=[Gateway(handler=login)], security=[reusable_oauth2]) as client:
+ response = client.post("/login", data=None)
+ assert response.status_code == 400
+ assert response.json() == {
+ "detail": "Validation failed for http://testserver/login with method POST.",
+ "errors": [
+ {
+ "type": "string_type",
+ "loc": ["grant_type"],
+ "msg": "Input should be a valid string",
+ "input": None,
+ "url": f"https://errors.pydantic.dev/{pydantic_version}/v/string_type",
+ },
+ {
+ "type": "string_type",
+ "loc": ["username"],
+ "msg": "Input should be a valid string",
+ "input": None,
+ "url": f"https://errors.pydantic.dev/{pydantic_version}/v/string_type",
+ },
+ {
+ "type": "string_type",
+ "loc": ["password"],
+ "msg": "Input should be a valid string",
+ "input": None,
+ "url": f"https://errors.pydantic.dev/{pydantic_version}/v/string_type",
+ },
+ ],
+ }
+
+
+def test_strict_login_no_grant_type():
+ with create_client(routes=[Gateway(handler=login)], security=[reusable_oauth2]) as client:
+ response = client.post("/login", json={"username": "johndoe", "password": "secret"})
+ assert response.status_code == 400
+ assert response.json() == {
+ "detail": "Validation failed for http://testserver/login with method POST.",
+ "errors": [
+ {
+ "type": "string_type",
+ "loc": ["grant_type"],
+ "msg": "Input should be a valid string",
+ "input": None,
+ "url": f"https://errors.pydantic.dev/{pydantic_version}/v/string_type",
+ }
+ ],
+ }
+
+
+def test_strict_login_incorrect_grant_type():
+ with create_client(routes=[Gateway(handler=login)], security=[reusable_oauth2]) as client:
+ response = client.post(
+ "/login",
+ json={"username": "johndoe", "password": "secret", "grant_type": "incorrect"},
+ )
+ assert response.status_code == 400
+ assert response.json() == {
+ "detail": "Validation failed for http://testserver/login with method POST.",
+ "errors": [
+ {
+ "type": "string_pattern_mismatch",
+ "loc": ["grant_type"],
+ "msg": "String should match pattern 'password'",
+ "input": "incorrect",
+ "ctx": {"pattern": "password"},
+ "url": f"https://errors.pydantic.dev/{pydantic_version}/v/string_pattern_mismatch",
+ }
+ ],
+ }
+
+
+def test_strict_login_correct_correct_grant_type():
+ with create_client(routes=[Gateway(handler=login)], security=[reusable_oauth2]) as client:
+ response = client.post(
+ "/login",
+ json={"username": "johndoe", "password": "secret", "grant_type": "password"},
+ )
+ assert response.status_code == 201, response.text
+ assert response.json() == {
+ "grant_type": "password",
+ "username": "johndoe",
+ "password": "secret",
+ "scopes": [],
+ "client_id": None,
+ "client_secret": None,
+ }
diff --git a/tests/security/oauth/test_security_oauth2_password_bearer_optional.py b/tests/security/oauth/test_security_oauth2_password_bearer_optional.py
new file mode 100644
index 00000000..2c7da034
--- /dev/null
+++ b/tests/security/oauth/test_security_oauth2_password_bearer_optional.py
@@ -0,0 +1,103 @@
+from typing import Any, Dict, Optional
+
+from esmerald import Gateway, Inject, Injects, get
+from esmerald.security.oauth2 import OAuth2PasswordBearer
+from esmerald.testclient import create_client
+
+oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/token", auto_error=False)
+
+
+@get("/items/", security=[oauth2_scheme], dependencies={"token": Inject(oauth2_scheme)})
+async def read_items(token: Optional[str] = Injects()) -> Dict[str, Any]:
+ if token is None:
+ return {"msg": "Create an account first"}
+ return {"token": token}
+
+
+def test_no_token():
+ with create_client(
+ routes=[
+ Gateway(handler=read_items),
+ ],
+ ) as client:
+ response = client.get("/items")
+ assert response.status_code == 200, response.text
+ assert response.json() == {"msg": "Create an account first"}
+
+
+def test_token():
+ with create_client(
+ routes=[
+ Gateway(handler=read_items),
+ ],
+ ) as client:
+ response = client.get("/items", headers={"Authorization": "Bearer testtoken"})
+ assert response.status_code == 200, response.text
+ assert response.json() == {"token": "testtoken"}
+
+
+def test_incorrect_token():
+ with create_client(
+ routes=[
+ Gateway(handler=read_items),
+ ],
+ ) as client:
+ response = client.get("/items", headers={"Authorization": "Notexistent testtoken"})
+ assert response.status_code == 200, response.text
+ assert response.json() == {"msg": "Create an account first"}
+
+
+def test_openapi_schema():
+ with create_client(
+ routes=[
+ Gateway(handler=read_items),
+ ],
+ enable_openapi=True,
+ ) as client:
+ response = client.get("/openapi.json")
+ assert response.status_code == 200, response.text
+
+ assert response.json() == {
+ "openapi": "3.1.0",
+ "info": {
+ "title": "Esmerald",
+ "summary": "Esmerald application",
+ "description": "Highly scalable, performant, easy to learn and for every application.",
+ "contact": {"name": "admin", "email": "admin@myapp.com"},
+ "version": client.app.version,
+ },
+ "servers": [{"url": "/"}],
+ "paths": {
+ "/items": {
+ "get": {
+ "summary": "Read Items",
+ "description": "",
+ "operationId": "read_items_items_get",
+ "deprecated": False,
+ "security": [
+ {
+ "OAuth2PasswordBearer": {
+ "type": "oauth2",
+ "flows": {"password": {"tokenUrl": "/token", "scopes": {}}},
+ "scheme_name": "OAuth2PasswordBearer",
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {"application/json": {"schema": {"type": "string"}}},
+ }
+ },
+ }
+ }
+ },
+ "components": {
+ "securitySchemes": {
+ "OAuth2PasswordBearer": {
+ "type": "oauth2",
+ "flows": {"password": {"tokenUrl": "/token", "scopes": {}}},
+ }
+ }
+ },
+ }
diff --git a/tests/security/oauth/test_security_oauth2_password_bearer_optional_desc.py b/tests/security/oauth/test_security_oauth2_password_bearer_optional_desc.py
new file mode 100644
index 00000000..9f1b46ec
--- /dev/null
+++ b/tests/security/oauth/test_security_oauth2_password_bearer_optional_desc.py
@@ -0,0 +1,107 @@
+from typing import Any, Dict, Optional
+
+from esmerald import Gateway, Inject, Injects, get
+from esmerald.security.oauth2 import OAuth2PasswordBearer
+from esmerald.testclient import create_client
+
+oauth2_scheme = OAuth2PasswordBearer(
+ tokenUrl="/token", description="OAuth2PasswordBearer security scheme", auto_error=False
+)
+
+
+@get("/items/", security=[oauth2_scheme], dependencies={"token": Inject(oauth2_scheme)})
+async def read_items(token: Optional[str] = Injects()) -> Dict[str, Any]:
+ if token is None:
+ return {"msg": "Create an account first"}
+ return {"token": token}
+
+
+def test_no_token():
+ with create_client(
+ routes=[
+ Gateway(handler=read_items),
+ ],
+ ) as client:
+ response = client.get("/items")
+ assert response.status_code == 200, response.text
+ assert response.json() == {"msg": "Create an account first"}
+
+
+def test_token():
+ with create_client(
+ routes=[
+ Gateway(handler=read_items),
+ ],
+ ) as client:
+ response = client.get("/items", headers={"Authorization": "Bearer testtoken"})
+ assert response.status_code == 200, response.text
+ assert response.json() == {"token": "testtoken"}
+
+
+def test_incorrect_token():
+ with create_client(
+ routes=[
+ Gateway(handler=read_items),
+ ],
+ ) as client:
+ response = client.get("/items", headers={"Authorization": "Notexistent testtoken"})
+ assert response.status_code == 200, response.text
+ assert response.json() == {"msg": "Create an account first"}
+
+
+def test_openapi_schema():
+ with create_client(
+ routes=[
+ Gateway(handler=read_items),
+ ],
+ enable_openapi=True,
+ ) as client:
+ response = client.get("/openapi.json")
+ assert response.status_code == 200, response.text
+
+ assert response.json() == {
+ "openapi": "3.1.0",
+ "info": {
+ "title": "Esmerald",
+ "summary": "Esmerald application",
+ "description": "Highly scalable, performant, easy to learn and for every application.",
+ "contact": {"name": "admin", "email": "admin@myapp.com"},
+ "version": client.app.version,
+ },
+ "servers": [{"url": "/"}],
+ "paths": {
+ "/items": {
+ "get": {
+ "summary": "Read Items",
+ "description": "",
+ "operationId": "read_items_items_get",
+ "deprecated": False,
+ "security": [
+ {
+ "OAuth2PasswordBearer": {
+ "type": "oauth2",
+ "description": "OAuth2PasswordBearer security scheme",
+ "flows": {"password": {"tokenUrl": "/token", "scopes": {}}},
+ "scheme_name": "OAuth2PasswordBearer",
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {"application/json": {"schema": {"type": "string"}}},
+ }
+ },
+ }
+ }
+ },
+ "components": {
+ "securitySchemes": {
+ "OAuth2PasswordBearer": {
+ "type": "oauth2",
+ "description": "OAuth2PasswordBearer security scheme",
+ "flows": {"password": {"tokenUrl": "/token", "scopes": {}}},
+ }
+ }
+ },
+ }
diff --git a/tests/_security/openapi_normal/__init__.py b/tests/security/openid/__init__.py
similarity index 100%
rename from tests/_security/openapi_normal/__init__.py
rename to tests/security/openid/__init__.py
diff --git a/tests/security/openid/test_security_openid_connect.py b/tests/security/openid/test_security_openid_connect.py
new file mode 100644
index 00000000..3d75acac
--- /dev/null
+++ b/tests/security/openid/test_security_openid_connect.py
@@ -0,0 +1,112 @@
+from typing import Any
+
+from pydantic import BaseModel
+
+from esmerald import Gateway, Inject, Injects, Security, get
+from esmerald.security.open_id import OpenIdConnect
+from esmerald.testclient import create_client
+
+oid = OpenIdConnect(openIdConnectUrl="/openid")
+
+
+class User(BaseModel):
+ username: str
+
+
+def get_current_user(oauth_header: str = Security(oid)):
+ user = User(username=oauth_header)
+ return user
+
+
+@get("/users/me", security=[oid], dependencies={"current_user": Inject(get_current_user)})
+def read_current_user(current_user: User = Injects()) -> Any:
+ return current_user
+
+
+def test_security_oauth2():
+ with create_client(
+ routes=[
+ Gateway(handler=read_current_user),
+ ],
+ enable_openapi=True,
+ ) as client:
+ response = client.get("/users/me", headers={"Authorization": "Bearer footokenbar"})
+ assert response.status_code == 200, response.text
+ assert response.json() == {"username": "Bearer footokenbar"}
+
+
+def test_security_oauth2_password_other_header():
+ with create_client(
+ routes=[
+ Gateway(handler=read_current_user),
+ ],
+ enable_openapi=True,
+ ) as client:
+ response = client.get("/users/me", headers={"Authorization": "Other footokenbar"})
+ assert response.status_code == 200, response.text
+ assert response.json() == {"username": "Other footokenbar"}
+
+
+def test_security_oauth2_password_bearer_no_header():
+ with create_client(
+ routes=[
+ Gateway(handler=read_current_user),
+ ],
+ enable_openapi=True,
+ ) as client:
+ response = client.get("/users/me")
+ assert response.status_code == 403, response.text
+ assert response.json() == {"detail": "Not authenticated"}
+
+
+def test_openapi_schema():
+ with create_client(
+ routes=[
+ Gateway(handler=read_current_user),
+ ],
+ enable_openapi=True,
+ ) as client:
+ response = client.get("/openapi.json")
+ assert response.status_code == 200, response.text
+
+ assert response.json() == {
+ "openapi": "3.1.0",
+ "info": {
+ "title": "Esmerald",
+ "summary": "Esmerald application",
+ "description": "Highly scalable, performant, easy to learn and for every application.",
+ "contact": {"name": "admin", "email": "admin@myapp.com"},
+ "version": client.app.version,
+ },
+ "servers": [{"url": "/"}],
+ "paths": {
+ "/users/me": {
+ "get": {
+ "summary": "Read Current User",
+ "description": "",
+ "operationId": "read_current_user_users_me_get",
+ "deprecated": False,
+ "security": [
+ {
+ "OpenIdConnect": {
+ "type": "openIdConnect",
+ "openIdConnectUrl": "/openid",
+ "scheme_name": "OpenIdConnect",
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {"application/json": {"schema": {"type": "string"}}},
+ }
+ },
+ }
+ }
+ },
+ "components": {
+ "securitySchemes": {
+ "OpenIdConnect": {"type": "openIdConnect", "openIdConnectUrl": "/openid"}
+ }
+ },
+ }
diff --git a/tests/security/openid/test_security_openid_connect_description.py b/tests/security/openid/test_security_openid_connect_description.py
new file mode 100644
index 00000000..5a3b496c
--- /dev/null
+++ b/tests/security/openid/test_security_openid_connect_description.py
@@ -0,0 +1,117 @@
+from typing import Any
+
+from pydantic import BaseModel
+
+from esmerald import Gateway, Inject, Injects, Security, get
+from esmerald.security.open_id import OpenIdConnect
+from esmerald.testclient import create_client
+
+oid = OpenIdConnect(openIdConnectUrl="/openid", description="OpenIdConnect security scheme")
+
+
+class User(BaseModel):
+ username: str
+
+
+def get_current_user(oauth_header: str = Security(oid)):
+ user = User(username=oauth_header)
+ return user
+
+
+@get("/users/me", security=[oid], dependencies={"current_user": Inject(get_current_user)})
+def read_current_user(current_user: User = Injects()) -> Any:
+ return current_user
+
+
+def test_security_oauth2():
+ with create_client(
+ routes=[
+ Gateway(handler=read_current_user),
+ ],
+ enable_openapi=True,
+ ) as client:
+ response = client.get("/users/me", headers={"Authorization": "Bearer footokenbar"})
+ assert response.status_code == 200, response.text
+ assert response.json() == {"username": "Bearer footokenbar"}
+
+
+def test_security_oauth2_password_other_header():
+ with create_client(
+ routes=[
+ Gateway(handler=read_current_user),
+ ],
+ enable_openapi=True,
+ ) as client:
+ response = client.get("/users/me", headers={"Authorization": "Other footokenbar"})
+ assert response.status_code == 200, response.text
+ assert response.json() == {"username": "Other footokenbar"}
+
+
+def test_security_oauth2_password_bearer_no_header():
+ with create_client(
+ routes=[
+ Gateway(handler=read_current_user),
+ ],
+ enable_openapi=True,
+ ) as client:
+ response = client.get("/users/me")
+ assert response.status_code == 403, response.text
+ assert response.json() == {"detail": "Not authenticated"}
+
+
+def test_openapi_schema():
+ with create_client(
+ routes=[
+ Gateway(handler=read_current_user),
+ ],
+ enable_openapi=True,
+ ) as client:
+ response = client.get("/openapi.json")
+ assert response.status_code == 200, response.text
+
+ assert response.json() == {
+ "openapi": "3.1.0",
+ "info": {
+ "title": "Esmerald",
+ "summary": "Esmerald application",
+ "description": "Highly scalable, performant, easy to learn and for every application.",
+ "contact": {"name": "admin", "email": "admin@myapp.com"},
+ "version": client.app.version,
+ },
+ "servers": [{"url": "/"}],
+ "paths": {
+ "/users/me": {
+ "get": {
+ "summary": "Read Current User",
+ "description": "",
+ "operationId": "read_current_user_users_me_get",
+ "deprecated": False,
+ "security": [
+ {
+ "OpenIdConnect": {
+ "type": "openIdConnect",
+ "description": "OpenIdConnect security scheme",
+ "openIdConnectUrl": "/openid",
+ "scheme_name": "OpenIdConnect",
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {"application/json": {"schema": {"type": "string"}}},
+ }
+ },
+ }
+ }
+ },
+ "components": {
+ "securitySchemes": {
+ "OpenIdConnect": {
+ "type": "openIdConnect",
+ "description": "OpenIdConnect security scheme",
+ "openIdConnectUrl": "/openid",
+ }
+ }
+ },
+ }
diff --git a/tests/security/openid/test_security_openid_connect_optional.py b/tests/security/openid/test_security_openid_connect_optional.py
new file mode 100644
index 00000000..2437ddb4
--- /dev/null
+++ b/tests/security/openid/test_security_openid_connect_optional.py
@@ -0,0 +1,102 @@
+from typing import Any, Optional
+
+from pydantic import BaseModel
+
+from esmerald import Gateway, Inject, Injects, Security, get
+from esmerald.security.open_id import OpenIdConnect
+from esmerald.testclient import create_client
+
+oid = OpenIdConnect(openIdConnectUrl="/openid", auto_error=False)
+
+
+class User(BaseModel):
+ username: str
+
+
+def get_current_user(oauth_header: Optional[str] = Security(oid)):
+ if oauth_header is None:
+ return None
+ user = User(username=oauth_header)
+ return user
+
+
+@get("/users/me", dependencies={"current_user": Inject(get_current_user)})
+def read_current_user(current_user: Optional[User] = Injects()) -> Any:
+ if current_user is None:
+ return {"msg": "Create an account first"}
+ return current_user
+
+
+def test_security_oauth2():
+ with create_client(
+ routes=[
+ Gateway(handler=read_current_user),
+ ],
+ enable_openapi=True,
+ ) as client:
+ response = client.get("/users/me", headers={"Authorization": "Bearer footokenbar"})
+ assert response.status_code == 200, response.text
+ assert response.json() == {"username": "Bearer footokenbar"}
+
+
+def test_security_oauth2_password_other_header():
+ with create_client(
+ routes=[
+ Gateway(handler=read_current_user),
+ ],
+ enable_openapi=True,
+ ) as client:
+ response = client.get("/users/me", headers={"Authorization": "Other footokenbar"})
+ assert response.status_code == 200, response.text
+ assert response.json() == {"username": "Other footokenbar"}
+
+
+def test_security_oauth2_password_bearer_no_header():
+ with create_client(
+ routes=[
+ Gateway(handler=read_current_user),
+ ],
+ enable_openapi=True,
+ ) as client:
+ response = client.get("/users/me")
+ assert response.status_code == 200, response.text
+ assert response.json() == {"msg": "Create an account first"}
+
+
+def test_openapi_schema():
+ with create_client(
+ routes=[
+ Gateway(handler=read_current_user),
+ ],
+ enable_openapi=True,
+ ) as client:
+ response = client.get("/openapi.json")
+
+ assert response.status_code == 200, response.text
+ assert response.json() == {
+ "openapi": "3.1.0",
+ "info": {
+ "title": "Esmerald",
+ "summary": "Esmerald application",
+ "description": "Highly scalable, performant, easy to learn and for every application.",
+ "contact": {"name": "admin", "email": "admin@myapp.com"},
+ "version": client.app.version,
+ },
+ "servers": [{"url": "/"}],
+ "paths": {
+ "/users/me": {
+ "get": {
+ "summary": "Read Current User",
+ "description": "",
+ "operationId": "read_current_user_users_me_get",
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ "content": {"application/json": {"schema": {"type": "string"}}},
+ }
+ },
+ "deprecated": False,
+ }
+ }
+ },
+ }