Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into feat/add-rate-throttling
Browse files Browse the repository at this point in the history
  • Loading branch information
Ido Slonimsky committed Jul 10, 2023
2 parents f81c384 + 4929b44 commit d549470
Show file tree
Hide file tree
Showing 18 changed files with 372 additions and 40 deletions.
5 changes: 3 additions & 2 deletions .github/ISSUE_TEMPLATE/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ contact_links:
url: https://discord.gg/rkERZ5eNU8
about: You can ask and answer questions here.
- name: Documentation
url: https://sansyrox.github.io/robyn/#/
about: The Robyn documentation.
url: https://https://sparckles.github.io/robyn/#/
about: The Robyn documentation.

2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "robyn"
version = "0.34.0"
version = "0.36.0"
authors = ["Sanskar Jethi <[email protected]>"]
edition = "2018"
description = "A web server that is fast!"
Expand Down
29 changes: 29 additions & 0 deletions docs/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,35 @@ async def hello_after_request(response: Response):
print("This won't be executed if user isn't logged in")
```

## Authentication

Robyn provides an easy way to add an authentication middleware to your application. You can then specify `auth_required=True` in your routes to make them accessible only to authenticated users.

```python
@app.get("/auth", auth_required=True)
async def auth(request: Request):
# This route method will only be executed if the user is authenticated
# Otherwise, a 401 response will be returned
return "Hello, world"
```

To add an authentication middleware, you can use the `configure_authentication` method. This method requires an `AuthenticationHandler` object as an argument. This object specifies how to authenticate a user, and uses a `TokenGetter` object to retrieve the token from the request. Robyn does currently provide a `BearerGetter` class that gets the token from the `Authorization` header, using the `Bearer` scheme. Here is an example of a basic authentication handler:

```python
class BasicAuthHandler(AuthenticationHandler):
def authenticate(self, request: Request) -> Optional[Identity]:
token = self.token_getter.get_token(request)
if token == "valid":
return Identity(claims={})
return None

app.configure_authentication(BasicAuthHandler(token_getter=BearerGetter()))
```

Your `authenticate` method should return an `Identity` object if the user is authenticated, or `None` otherwise. The `Identity` object can contain any data you want, and will be accessible in your route methods using the `request.identity` attribute.

Note that this authentication system is basically only using a "before request" middleware under the hood. This means you can overlook it and create your own authentication system using middlewares if you want to. However, Robyn still provide this easy to implement solution that should suit most use cases.

## MultiCore Scaling

To run Robyn across multiple cores, you can use the following command:
Expand Down
4 changes: 2 additions & 2 deletions docs/landing_page/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ <h1>A fast web framework!</h1>
An async web server with the runtime written in Rust.
</p>
<a
href="https://sansyrox.github.io/robyn/#/"
href="https://sparckles.github.io/robyn/"
class="btn btn-success svg-icon"
>
Docs
Expand Down Expand Up @@ -244,7 +244,7 @@ <h2 class="text-center text-md-left">
>
</li>
<li class="list-inline-item">
<a href="https://sansyrox.github.io/robyn/#/">API Docs</a>
<a href="https://sparckles.github.io/robyn/">API Docs</a>
</li>
<li class="list-inline-item">
<a href="https://github.com/sansyrox/robyn/"
Expand Down
28 changes: 28 additions & 0 deletions integration_tests/base_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

import pathlib
from collections import defaultdict
from typing import Optional

from robyn import WS, Robyn, Request, Response, jsonify, serve_file, serve_html
from robyn.authentication import AuthenticationHandler, BearerGetter, Identity
from robyn.templating import JinjaTemplate
from robyn.throttling import RateLimiter

Expand Down Expand Up @@ -693,6 +695,23 @@ async def async_exception_post(_: Request):
raise ValueError("value error")


# ===== Authentication =====


@app.get("/sync/auth", auth_required=True)
def sync_auth(request: Request):
assert request.identity is not None
assert request.identity.claims == {"key": "value"}
return "authenticated"


@app.get("/async/auth", auth_required=True)
async def async_auth(request: Request):
assert request.identity is not None
assert request.identity.claims == {"key": "value"}
return "authenticated"


# ===== Rate Limiting ====

rate_limiter = RateLimiter(calls_limit=3, limit_ttl=60)
Expand Down Expand Up @@ -742,4 +761,13 @@ async def async_rate_post(request: Request):
app.add_view("/sync/view", SyncView)
app.add_view("/async/view", AsyncView)
app.include_router(sub_router)

class BasicAuthHandler(AuthenticationHandler):
def authenticate(self, request: Request) -> Optional[Identity]:
token = self.token_getter.get_token(request)
if token == "valid":
return Identity(claims={"key": "value"})
return None

app.configure_authentication(BasicAuthHandler(token_getter=BearerGetter()))
app.start(port=8080)
42 changes: 42 additions & 0 deletions integration_tests/test_authentication.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import pytest

from helpers.http_methods_helpers import get


@pytest.mark.benchmark
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_valid_authentication(session, function_type: str):
r = get(f"/{function_type}/auth", headers={"Authorization": "Bearer valid"})
assert r.text == "authenticated"


@pytest.mark.benchmark
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_invalid_authentication_token(session, function_type: str):
r = get(
f"/{function_type}/auth",
headers={"Authorization": "Bearer invalid"},
should_check_response=False,
)
assert r.status_code == 401
assert r.headers["WWW-Authenticate"] == "BearerGetter"


@pytest.mark.benchmark
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_invalid_authentication_header(session, function_type: str):
r = get(
f"/{function_type}/auth",
headers={"Authorization": "Bear valid"},
should_check_response=False,
)
assert r.status_code == 401
assert r.headers["WWW-Authenticate"] == "BearerGetter"


@pytest.mark.benchmark
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_invalid_authentication_no_token(session, function_type: str):
r = get(f"/{function_type}/auth", should_check_response=False)
assert r.status_code == 401
assert r.headers["WWW-Authenticate"] == "BearerGetter"
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "maturin"

[project]
name = "robyn"
version = "0.34.0"
version = "0.36.0"
description = "A web server that is fast!"
authors = [{ name = "Sanskar Jethi", email = "[email protected]" }]
dependencies = [
Expand Down
Loading

0 comments on commit d549470

Please sign in to comment.