- 
          
 - 
                Notifications
    
You must be signed in to change notification settings  - Fork 310
 
Feat: add auth subroutes capabilities #1056
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | 
|---|---|---|
| 
          
            
          
           | 
    @@ -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. | ||
| 
     | 
||
| --- | ||
| <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) | ||
| 
         There was a problem hiding this comment. Choose a reason for hiding this commentThe 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  There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, actually it is! 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
| 
          
            
          
           | 
    ||
| Original file line number | Diff line number | Diff line change | 
|---|---|---|
| 
          
            
          
           | 
    @@ -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 | ||
| 
          
            
          
           | 
    @@ -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]: | ||
| 
        
          
        
         | 
    @@ -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
    
   
  There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @IA-PieroCV , I believe  There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @sansyrox @IA-PieroCV I was also thinking to create new PR to provide auth_required functionality for all routes under a single router  | 
||
| app.configure_authentication(BasicAuthHandler(token_getter=BearerGetter())) | ||
| 
     | 
||
| app.start(port=8080, _check_port=False) | ||
| 
     | 
||
| 
     | 
||
| 
          
            
          
           | 
    ||
| 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" | 
There was a problem hiding this comment.
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?