Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prepare release #453

Merged
merged 7 commits into from
Dec 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion docs/en/docs/extras/forms.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ from esmerald.params import Form
As [explained here](./request-data.md#request-data), the handler is expecting a `data` field declared and from there
you can pass more details about the form.


## Examples

You can send the form in many different formats, for example:
Expand Down
1 change: 0 additions & 1 deletion docs/en/docs/extras/upload-files.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ These types are the ones that shall be passed to `Body(media_type=...)`.
optional to use `File` and `Form` as the same result can be achieved by using
`Body(media_type=...)`.


## Single file upload

Uploading a single file, you need to type the `data` as [UploadFile](#uploadfile).
Expand Down
13 changes: 11 additions & 2 deletions docs/en/docs/release-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,20 @@ hide:

# Release Notes

## Unreleased
## 3.6.0

### Added

- New [Security](./security/index.md) section with all the explanations how to use the internals of Esmerald.
- Added new `Security` object used for security dependencies using Esmerald `esmerald.security` package.

### Changed

- Updates from python-jose to PyJWT as dependency contrib library.
- Remove OpenAPI security as they where redundant and not 100% compliant with OpenAPI security.
- Allow the new Lilya StaticFiles allowing to provide multiple directories with fallthrough behaviour.
- Deprecate support for Mako.
- Internal code organisation and cleaning.

### Fixed

Expand All @@ -24,7 +33,7 @@ hide:

- Use assigned encoders at requests for json_encoder.
- Allow overwriting the `LILYA_ENCODER_TYPES` for different encoder sets or tests.
- Use more orjson for encoding requests.
- Use more OrJSON for encoding requests.

## 3.5.0

Expand Down
112 changes: 112 additions & 0 deletions docs/en/docs/security/advanced/basic-auth.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# HTTP Basic Auth

For simple scenarios, HTTP Basic Auth can be used.

With HTTP Basic Auth, the application expects a header containing a username and password.

If the header is missing, the application responds with an HTTP 401 "Unauthorized" error and includes a `WWW-Authenticate` header with a value of `Basic` and an optional `realm` parameter.

This prompts the browser to display a login dialog for the username and password. Once entered, the browser automatically sends the credentials in the header.

## Simple HTTP Basic Auth

1. Import `HTTPBasic` and `HTTPBasicCredentials`.
2. Create a security scheme using `HTTPBasic`.
3. Apply this security scheme as a dependency in your path operation.
4. The dependency returns an `HTTPBasicCredentials` object, which includes the provided `username` and `password`.

```python hl_lines="10 20"
{!> ../../../docs_src/security/advanced/basic.py !}
```

When you first open the URL (or click the "Execute" button in the docs), the browser will prompt you for your username and password:

<img src="https://res.cloudinary.com/dymmond/image/upload/v1733928287/esmerald/security/basic_cbkrjk.png" alt="Basic">

## Verify the Username

Here's a more comprehensive example.

Use a dependency to verify if the username and password are correct.

For this, use the Python standard module <a href="https://docs.python.org/3/library/secrets.html" class="external-link" target="_blank">`secrets`</a> to check the username and password.

`secrets.compare_digest()` requires `bytes` or a `str` containing only ASCII characters, meaning it won't work with characters like `ú`, as in `Araújo`.

To handle this, first convert the `username` and `password` to `bytes` by encoding them with UTF-8.

Then use `secrets.compare_digest()` to ensure that `credentials.username` is `"alice123"` and `credentials.password` is `"sunshine"`.

```python
{!> ../../../docs_src/security/advanced/basic_complex.py !}
```

This would be similar to:

```python
if not (credentials.username == "alice123") or not (credentials.password == "sunshine"):
# Return some error
...
```

### Timing Attacks

What exactly is a "timing attack"?

Imagine some attackers are attempting to figure out a valid username and password combination.

They send a request with the username `alice123` and the password `sunshine`.

In your Python application, the logic might look something like this:

```Python
if "alice123" == "charlie_admin" and "sunshine" == "openSesame":
...
```

When Python compares the first character of `alice123` (`a`) with the first character of `charlie_admin` (`c`), it instantly determines that the strings do not match and returns `False`. No further comparisons are needed because the mismatch is already clear. Consequently, your application responds with "Invalid username or password."

Next, the attackers try a different username, such as `charlie_adminx`, with the same password `sunshine`.

Your application logic then processes something like this:

```Python
if "charlie_adminx" == "charlie_admin" and "sunshine" == "openSesame":
...
```

Python will need to compare the entire string `charlie_admi` in both `charlie_adminx` and `charlie_admin` before determining they are not the same. This will take a few extra microseconds to respond with "Invalid username or password."

Here’s the rewritten version with new names and terms:

#### The time to respond helps attackers

Attackers can notice that the server took slightly longer to respond with "Invalid username or password." This indicates that some initial characters in the username might be correct.

They can then try again, refining their guesses, knowing that the correct username is likely closer to `charlie_adminx` than `alice123`.

#### Automated Attacks

Attackers typically don't guess usernames and passwords manually. Instead, they use scripts to automate the process, making thousands or even millions of attempts per second. These scripts can identify one correct character at a time.

By exploiting timing information unintentionally leaked by the application, attackers can eventually determine the correct username and password within minutes or hours.

#### Fix it with `secrets.compare_digest()`

Using `secrets.compare_digest()` in our code ensures that comparing any two strings, such as `charlie_adminx` to `charlie_admin` or `alice123` to `charlie_admin`, takes the same amount of time. This also applies to password comparisons.

By integrating `secrets.compare_digest()` into your application, you can effectively protect against timing attacks.

### Return the error

If the credentials are incorrect, return an `HTTPException` with a status code of 401. This is the same status code used when no credentials are provided. Additionally, include the `WWW-Authenticate` header to prompt the browser to display the login screen again:

```python hl_lines="16-25"
{!> ../../../docs_src/security/advanced/basic_complex.py !}
```

## 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.
166 changes: 166 additions & 0 deletions docs/en/docs/security/advanced/oauth2-scopes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
# OAuth2 Scopes

Esmerald supports OAuth2 scopes, offering a detailed permission system that adheres to the OAuth2 standard. This feature integrates seamlessly into your OpenAPI application and its API documentation.

OAuth2 scopes are widely used by major providers such as Facebook, Google, GitHub, Microsoft, and Twitter. Whenever an application allows you to "log in with" these platforms, it leverages OAuth2 with scopes to define specific permissions.

In this guide, we’ll explore how to manage authentication and authorization using OAuth2 scopes in Esmerald.

!!! Warning

This section delves into advanced concepts, so beginners may prefer to skip it for now.

While OAuth2 scopes aren't mandatory, they offer a structured way to handle permissions, seamlessly integrating with OpenAPI and API documentation. However, it’s crucial to enforce scopes or any other security measures directly in your code.

In many cases, OAuth2 scopes might be excessive. Still, if your application requires them or you're eager to learn, continue reading to explore their implementation and benefits.

## OAuth2 Scopes and OpenAPI

OAuth2 defines "scopes" as a list of space-separated strings representing permissions.

In OpenAPI, you can define "security schemes" that use OAuth2 and declare scopes.

Each scope is a string (without spaces) used to specify security permissions, such as:

* `users:read` or `users:write`
* `instagram_basic` (used by Facebook/Instagram)
* `https://www.googleapis.com/auth/drive` (used by Google)

!!! Info
In OAuth2, a "scope" is simply a string representing a specific permission. The format, such as including characters like `:` or even being a URL, is entirely implementation-specific. From the OAuth2 perspective, these are treated as plain strings without inherent meaning outside their defined context.

## Global View

We’ll quickly review the updates in the main [OAuth2 with Password, Bearer with JWT tokens](../oauth-jwt.md){.internal-link target=_blank}, now enhanced with OAuth2 scopes:

```python hl_lines="7 9 34 97 99-108 114-117 123-129"
{!> ../../../docs_src/security/advanced/app.py !}
```

Next, we’ll break these changes down step by step for better understanding.

## OAuth2 Security Scheme

First, we declare the OAuth2 security scheme with two scopes: `me` and `items`.

The `scopes` parameter is a `dict` with each scope as a key and its description as the value:

```python hl_lines="32-35"
{!> ../../../docs_src/security/advanced/app.py !}
```

These scopes will appear in the API docs when you log in/authorize, allowing you to select which scopes to grant access to: `me` and `items`.

This is similar to granting permissions when logging in with Facebook, Google, GitHub, etc:

<img src="https://res.cloudinary.com/dymmond/image/upload/v1733926056/esmerald/security/scopes_ujzsf9.png" alt="Scopes">

## JWT Token with Scopes

Next, update the token *path operation* to include the requested scopes in the response.

We continue to use `OAuth2PasswordRequestForm`, which has a `scopes` property containing the list of scopes from the request.

These scopes will be included in the JWT token returned.

!!! Danger
To keep things simple, we directly include the received scopes in the token.

In your application, make sure to only include scopes that the user is permitted to have.

```python hl_lines="148"
{!> ../../../docs_src/security/advanced/app.py !}
```

## Declare Scopes in Path Operations and Dependencies

To require the `items` scope for the `/users/me/items/` path operation, use `Security` from `Esmerald`. This works similarly to `Inject`, but includes a `scopes` parameter.

Pass the `get_current_user` dependency function to `Security`, along with the required scopes (in this case, `items`).

!!! Info

You don't need to declare scopes in multiple locations.
This example shows how **Esmerald** manages scopes defined at different levels.

```python hl_lines="97 155 159"
{!> ../../../docs_src/security/advanced/app.py !}
```

## Using `SecurityScopes`

Update the `get_current_user` dependency to use the previously created OAuth2 scheme.

Since this function does not require scopes itself, use `Inject` with `oauth2_scheme`.

Declare a `SecurityScopes` parameter, imported from `esmerald.security.scopes`.

```python hl_lines="20 97"
{!> ../../../docs_src/security/advanced/app.py !}
```

## Using the Scopes

The `scopes` parameter will be of type `SecurityScopes`.

It includes a `scopes` property, which is a list of all the scopes required by itself and any dependencies that use it as a sub-dependency. This might sound confusing, but it will be explained further below.

The `scopes` object also has a `scope_str` attribute, which is a single string containing all the scopes separated by spaces (we will use this later).

We create an `HTTPException` that can be reused (`raise`) at various points.

In this exception, we include the required scopes (if any) as a space-separated string (using `scope_str`). This string is placed in the `WWW-Authenticate` header, as specified by the OAuth2 standard.

```python hl_lines="97 99-108"
{!> ../../../docs_src/security/advanced/app.py !}
```

## Verify `username` and Data Structure

Check the `username` and extract the scopes.

Use a Pydantic model to validate the data, raising an `HTTPException` if validation fails.

Update the `TokenData` Pydantic model to include a `scopes` property.

Ensure the data structure is correct to prevent security issues.

Confirm the user exists, raising an exception if not.

```python hl_lines="55 109-117"
{!> ../../../docs_src/security/advanced/app.py !}
```

## Verify the Scopes

Check that the token includes all necessary scopes. If any required scopes are missing, raise an `HTTPException`.

```python hl_lines="123-129"
{!> ../../../docs_src/security/advanced/app.py !}
```

## More Details about `SecurityScopes`

You can use `SecurityScopes` at any point in the dependency tree.

It will always contain the scopes declared by the current `Security` dependencies and all dependants for that specific *path operation*.

Use `SecurityScopes` to verify token scopes in a central dependency function, with different scope requirements for each *path operation*.

## About Third-Party Integrations

This example demonstrates the OAuth2 "password" flow, which is ideal for logging into your own application using your frontend.

For applications where users connect through third-party providers (like Facebook, Google, GitHub), other OAuth2 flows are more appropriate.

The implicit flow is commonly used, while the authorization code flow is more secure but also more complex.

!!! Note
Authentication providers might use different names for their flows, but they all adhere to the OAuth2 standard.
**Esmerald** provides utilities for all OAuth2 authentication flows in `esmerald.security.oauth2`.

## 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.
2 changes: 2 additions & 0 deletions docs/en/mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,8 @@ nav:
- security/interaction.md
- security/simple-oauth2.md
- security/oauth-jwt.md
- security/advanced/oauth2-scopes.md
- security/advanced/basic-auth.md
- Advanced & Useful:
- extras/index.md
- extras/path-params.md
Expand Down
Loading
Loading