Skip to content
Open
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
137 changes: 137 additions & 0 deletions docs_src/src/pages/documentation/api_reference/views.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,143 @@ The only thing to remember is that you need to add the subrouter to the main rou
</Col>
</Row>

### Authentication on Subrouters

Batman asked Robyn how to secure his routes. Robyn explained that authentication can be added to subrouters in three ways, giving developers flexibility to decide the scope and precedence of the authentication.

> **Important**: If any of these methods are used, the authentication for the endpoint is set to `False` unless explicitly overridden.

---
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need this. Right?

<Row>
<Col>
#### 1. Authentication at the Endpoint Level

Authentication can be applied directly to a specific endpoint using the decorator for that endpoint. This method has the **highest precedence**, meaning it overrides any authentication settings defined at the router or subrouter level.
</Col>
<Col sticky>
<CodeGroup title="Request" tag="GET" label="/secured , /unsecured">
```python {{ title: 'untyped' }}
from robyn import Robyn, SubRouter

app = Robyn(__file__)

sub_router = SubRouter("/auth_example")

@sub_router.get("/secured", auth_required=True)
def secured(request):
return "This endpoint is secured with authentication!"

@sub_router.get("/unsecured", auth_required=False)
def unsecured(request):
return "This endpoint does not require authentication!"

app.include_router(sub_router)
```

```python {{ title: 'typed' }}
from robyn import Robyn, SubRouter, Request

app = Robyn(__file__)

sub_router = SubRouter("/auth_example")

@sub_router.get("/secured", auth_required=True)
def secured(request : Request) -> str:
return "This endpoint is secured with authentication!"

@sub_router.get("/unsecured", auth_required=False)
def unsecured(request : Request) -> str:
return "This endpoint does not require authentication!"

app.include_router(sub_router)
```
</CodeGroup>
</Col>
</Row>


<Row>
<Col>
#### 2. Authentication via `include_router`
When including a subrouter into the main router, you can specify an authentication flag. This setting applies to all endpoints in the subrouter unless overridden at the endpoint level.
</Col>
<Col sticky>
<CodeGroup title="Request" tag="GET" label="/default_secured">
```python {{ title: 'untyped' }}
from robyn import Robyn, SubRouter

app = Robyn(__file__)

sub_router = SubRouter("/auth_example")

@sub_router.get("/default_secured")
def default_secured(request):
return "Authentication is set by include_router!"

app.include_router(sub_router, auth_required=True)
```

```python {{ title: 'typed' }}
from robyn import Robyn, SubRouter, Request

app = Robyn(__file__)

sub_router = SubRouter("/auth_example")

@sub_router.get("/default_secured")
def default_secured(request : Request) -> str:
return "Authentication is set by include_router!"

app.include_router(sub_router, auth_required=True)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @IA-PieroCV 👋

Thank you for the PR 😄 Overall I like the theme of the PR but will have to a review of the code.

However, why do we need an auth_required flag in include_router? Is it not the same as the SubRouter Class?

Copy link
Author

@IA-PieroCV IA-PieroCV Nov 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, actually it is!
This is just to give developers options for including auth_required due to their own criteria.
Of course we can remove any of the auth_required instance level parameters, from include_router or the SubRoute object instance.

However, include_router have higher precedence. My logic here is because developers face this method more often than the instantiation of the SubRoute. The same logic for endpoint decorators.

Copy link
Member

@sansyrox sansyrox Dec 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@IA-PieroCV , apologies for the late revert. But, I think this is a redundancy. If someone wants to allow auth, they can do it the subrouter class. I'd suggest that we remove it from here.

Let me know if you have anymore thoughts 😄

```
</CodeGroup>
</Col>
</Row>

<Row>
<Col>
#### 3. Authentication at SubRouter Instantiation
Finally, you can define authentication at the time of the `SubRouter` instantiation. This acts as the **default authentication** setting for all endpoints in the subrouter unless overridden by the endpoint decorator or during inclusion into the main router.
</Col>
<Col sticky>
<CodeGroup title="Request" tag="GET" label="/inherited_auth">
```python {{ title: 'untyped' }}
from robyn import Robyn, SubRouter

app = Robyn(__file__)

sub_router = SubRouter("/auth_example", auth_required=True)

@sub_router.get("/inherited_auth")
def inherited_auth(request):
return "Authentication is inherited from the SubRouter!"

app.include_router(sub_router)
```

```python {{ title: 'typed' }}
from robyn import Robyn, SubRouter, Request

app = Robyn(__file__)

sub_router = SubRouter("/auth_example", auth_required=True)

@sub_router.get("/inherited_auth")
def inherited_auth(request : Request) -> str:
return "Authentication is inherited from the SubRouter!"

app.include_router(sub_router)
```
</CodeGroup>
</Col>
</Row>
#### Precedence Rules
Authentication precedence in Robyn follows this order:

1. Endpoint-level decorator: If an endpoint explicitly sets `auth_required=True` or `auth_required=False`, this takes priority.
2. `include_router` method: The auth_required parameter in app.include_router applies to all endpoints in the included subrouter unless overridden.
3. `SubRouter` instantiation: The auth_required setting during SubRouter creation acts as the default for all its endpoints unless overridden at the endpoint or include_router level.

## What's next?

Now, that Batman knows how to organise his code, he wants to know how to add dependencies to his code. Robyn tells him that he can add dependencies at the global level, router level and the view level.
Expand Down
20 changes: 17 additions & 3 deletions integration_tests/base_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,15 @@
from collections import defaultdict
from typing import Optional

from integration_tests.subroutes import di_subrouter, sub_router
from integration_tests.subroutes import (
di_subrouter,
sub_router,
auth_subrouter_endpoint,
auth_subrouter_include,
auth_subrouter_instance,
auth_subrouter_include_false,
auth_subrouter_include_true,
)
from integration_tests.views import AsyncView, SyncView
from robyn import Headers, Request, Response, Robyn, WebSocket, WebSocketConnector, jsonify, serve_file, serve_html
from robyn.authentication import AuthenticationHandler, BearerGetter, Identity
Expand Down Expand Up @@ -1090,8 +1098,6 @@ def main():
app.startup_handler(startup_handler)
app.add_view("/sync/view", SyncView)
app.add_view("/async/view", AsyncView)
app.include_router(sub_router)
app.include_router(di_subrouter)

class BasicAuthHandler(AuthenticationHandler):
def authenticate(self, request: Request) -> Optional[Identity]:
Expand All @@ -1103,7 +1109,15 @@ def authenticate(self, request: Request) -> Optional[Identity]:
return Identity(claims={"key": "value"})
return None

app.include_router(sub_router)
app.include_router(di_subrouter)
app.include_router(auth_subrouter_endpoint)
app.include_router(auth_subrouter_include, auth_required=True)
app.include_router(auth_subrouter_instance)
app.include_router(auth_subrouter_include_false, auth_required=False)
app.include_router(auth_subrouter_include_true, auth_required=True)
Comment on lines +1112 to +1118
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@IA-PieroCV , I believe auth_required should either be removed from include_router or the SubRouter() class

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sansyrox @IA-PieroCV
i also think that auth_required should be in SubRouter than in include_router

I was also thinking to create new PR to provide auth_required functionality for all routes under a single router
so user just don't have to set auth_required to every end point, if they want then they can explicitly override the default auth_required setting set by the router, on endpoint itself

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

app.start(port=8080, _check_port=False)


Expand Down
18 changes: 17 additions & 1 deletion integration_tests/subroutes/__init__.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,28 @@
from robyn import SubRouter, WebSocket, jsonify

from .di_subrouter import di_subrouter
from .auth_subroutes import (
auth_subrouter_endpoint,
auth_subrouter_include,
auth_subrouter_instance,
auth_subrouter_include_false,
auth_subrouter_include_true,
)

sub_router = SubRouter(__name__, prefix="/sub_router")

websocket = WebSocket(sub_router, "/ws")

__all__ = ["sub_router", "websocket", "di_subrouter"]
__all__ = [
"sub_router",
"websocket",
"di_subrouter",
"auth_subrouter_endpoint",
"auth_subrouter_include",
"auth_subrouter_instance",
"auth_subrouter_include_false",
"auth_subrouter_include_true",
]


@websocket.on("connect")
Expand Down
125 changes: 125 additions & 0 deletions integration_tests/subroutes/auth_subroutes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
from robyn import Request, SubRouter

auth_subrouter_endpoint = SubRouter(__file__, "/auth_subrouter_endpoint")


@auth_subrouter_endpoint.get("/auth_subroute_sync", auth_required=True)
def sync_subrouter_auth_endpoint(request: Request):
assert request.identity is not None
assert request.identity.claims == {"key": "value"}
return "authenticated"


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


auth_subrouter_include = SubRouter(__file__, "/auth_subrouter_include")


@auth_subrouter_include.get("/auth_subroute_sync")
def sync_subrouter_auth_include(request: Request):
assert request.identity is not None
assert request.identity.claims == {"key": "value"}
return "authenticated"


@auth_subrouter_include.get("/auth_subroute_async")
async def async_subrouter_auth_include(request: Request):
assert request.identity is not None
assert request.identity.claims == {"key": "value"}
return "authenticated"


@auth_subrouter_include.get("/noauth_subroute_sync", auth_required=False)
def sync_subrouter_noauth_include(request: Request):
return "bypassed"


@auth_subrouter_include.get("/noauth_subroute_async", auth_required=False)
async def async_subrouter_noauth_include(request: Request):
return "bypassed"


auth_subrouter_instance = SubRouter(__file__, "/auth_subrouter_instance", auth_required=True)


@auth_subrouter_instance.get("/auth_subroute_sync")
def sync_subrouter_auth_instance(request: Request):
assert request.identity is not None
assert request.identity.claims == {"key": "value"}
return "authenticated"


@auth_subrouter_instance.get("/auth_subroute_async")
async def async_subrouter_auth_instance(request: Request):
assert request.identity is not None
assert request.identity.claims == {"key": "value"}
return "authenticated"


@auth_subrouter_instance.get("/noauth_subroute_sync", auth_required=False)
def sync_subrouter_noauth_instance(request: Request):
return "bypassed"


@auth_subrouter_instance.get("/noauth_subroute_async", auth_required=False)
async def async_subrouter_noauth_instance(request: Request):
return "bypassed"


auth_subrouter_include_false = SubRouter(__file__, "/auth_subrouter_include_false", auth_required=True)


@auth_subrouter_include_false.get("/auth_subroute_sync", auth_required=True)
def sync_subrouter_auth_include_false(request: Request):
assert request.identity is not None
assert request.identity.claims == {"key": "value"}
return "authenticated"


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


@auth_subrouter_include_false.get("/noauth_subroute_sync")
def sync_subrouter_noauth_include_false(request: Request):
return "bypassed"


@auth_subrouter_include_false.get("/noauth_subroute_async")
async def async_subrouter_noauth_include_false(request: Request):
return "bypassed"


auth_subrouter_include_true = SubRouter(__file__, "/auth_subrouter_include_true", auth_required=False)


@auth_subrouter_include_true.get("/auth_subroute_sync")
def sync_subrouter_auth_include_true(request: Request):
assert request.identity is not None
assert request.identity.claims == {"key": "value"}
return "authenticated"


@auth_subrouter_include_true.get("/auth_subroute_async")
async def async_subrouter_auth_include_true(request: Request):
assert request.identity is not None
assert request.identity.claims == {"key": "value"}
return "authenticated"


@auth_subrouter_include_true.get("/noauth_subroute_sync", auth_required=False)
def sync_subrouter_noauth_include_true(request: Request):
return "bypassed"


@auth_subrouter_include_true.get("/noauth_subroute_async", auth_required=False)
async def async_subrouter_noauth_include_true(request: Request):
return "bypassed"
Loading