Skip to content

Commit 4397974

Browse files
authored
Merge pull request #232 from python-ellar/drop_global_guard
fix: Drop Global Guard as Provider
2 parents 2022260 + a8613a8 commit 4397974

File tree

22 files changed

+500
-365
lines changed

22 files changed

+500
-365
lines changed
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
# **Authentication Schemes Strategy**
2+
3+
Authentication scheme is another strategy for identifying the user who is using the application. The difference between it and
4+
and Guard strategy is your identification executed at middleware layer when processing incoming request while guard execution
5+
happens just before route function is executed.
6+
7+
Ellar provides `BaseAuthenticationHandler` contract which defines what is required to set up any authentication strategy.
8+
We are going to make some modifications on the existing project to see how we can achieve the same result and to show how authentication handlers in ellar.
9+
10+
### Creating a JWT Authentication Handler
11+
Just like AuthGuard, we need to create its equivalent. But first we need to create a `auth_scheme.py` at the root level
12+
of your application for us to define a `JWTAuthentication` handler.
13+
14+
15+
```python title='prject_name/auth_scheme.py' linenums='1'
16+
import typing as t
17+
from ellar.common.serializer.guard import (
18+
HTTPAuthorizationCredentials,
19+
)
20+
from ellar.auth import UserIdentity
21+
from ellar.auth.handlers import HttpBearerAuthenticationHandler
22+
from ellar.common import IHostContext
23+
from ellar.di import injectable
24+
from ellar_jwt import JWTService
25+
26+
27+
@injectable
28+
class JWTAuthentication(HttpBearerAuthenticationHandler):
29+
def __init__(self, jwt_service: JWTService) -> None:
30+
self.jwt_service = jwt_service
31+
32+
async def authentication_handler(
33+
self,
34+
context: IHostContext,
35+
credentials: HTTPAuthorizationCredentials,
36+
) -> t.Optional[t.Any]:
37+
# this function will be called by Identity Middleware but only when a `Bearer token` is found on the header request
38+
try:
39+
data = await self.jwt_service.decode_async(credentials.credentials)
40+
return UserIdentity(auth_type=self.scheme, **data)
41+
except Exception as ex:
42+
# if we cant identity the user or token has expired, we return None.
43+
return None
44+
```
45+
46+
Let us make `JWTAuthentication` Handler available for ellar to use as shown below
47+
48+
```python title='project_name.server.py' linenums='1'
49+
import os
50+
from ellar.common.constants import ELLAR_CONFIG_MODULE
51+
from ellar.app import AppFactory, use_authentication_schemes
52+
from ellar.core import LazyModuleImport as lazyLoad
53+
from .auth_scheme import JWTAuthentication
54+
55+
56+
application = AppFactory.create_from_app_module(
57+
lazyLoad('project_name.root_module:ApplicationModule'),
58+
config_module=os.environ.get(
59+
ELLAR_CONFIG_MODULE, "project_name.config:DevelopmentConfig"
60+
),
61+
)
62+
use_authentication_schemes(JWTAuthentication)
63+
```
64+
Unlike guards, Authentication handlers are registered global by default as shown in the above illustration.
65+
Also, we need to remove `GlobalGuard` registration we did in `AuthModule`,
66+
so that we don't have too user identification checks.
67+
68+
!!!note
69+
In the above illustration, we added JWTAuthentication as a type.
70+
This means DI will create JWTAuthentication instance.
71+
We can use this method because we want `JWTService` to be injected when instantiating `JWTAuthentication`.
72+
But if you don't have any need for DI injection, you can use the below.
73+
```python
74+
...
75+
application.add_authentication_schemes(JWTAuthentication())
76+
## OR
77+
## use_authentication_schemes(JWTAuthentication())
78+
```
79+
80+
We need
81+
to refactor auth controller and mark `refresh_token` and `sign_in` function as public routes
82+
by using `SkipAuth` decorator from `ellar.auth` package.
83+
84+
```python title='auth/controller.py' linenums='1'
85+
from ellar.common import Controller, ControllerBase, post, Body, get
86+
from ellar.auth import SkipAuth, AuthenticationRequired
87+
from ellar.openapi import ApiTags
88+
from .services import AuthService
89+
90+
91+
@AuthenticationRequired('JWTAuthentication')
92+
@Controller
93+
@ApiTags(name='Authentication', description='User Authentication Endpoints')
94+
class AuthController(ControllerBase):
95+
def __init__(self, auth_service: AuthService) -> None:
96+
self.auth_service = auth_service
97+
98+
@post("/login")
99+
@SkipAuth()
100+
async def sign_in(self, username: Body[str], password: Body[str]):
101+
return await self.auth_service.sign_in(username=username, password=password)
102+
103+
@get("/profile")
104+
async def get_profile(self):
105+
return self.context.user
106+
107+
@SkipAuth()
108+
@post("/refresh")
109+
async def refresh_token(self, payload: str = Body(embed=True)):
110+
return await self.auth_service.refresh_token(payload)
111+
112+
113+
```
114+
In the above illustration,
115+
we decorated AuthController with `@AuthenticationRequired('JWTAuthentication')`
116+
to ensure we have authenticated user before executing any route function and,
117+
we passed in `JWTAuthentication` as a parameter,
118+
which will be used in openapi doc to define the controller routes security scheme.
119+
120+
It is importance to note that when using `AuthenticationHandler` approach,
121+
that you have
122+
to always use `AuthenticationRequired` decorator on route functions or controller
123+
that needs protected from anonymous users.
124+
125+
But if you have a single form of authentication,
126+
you can register `AuthenticatedRequiredGuard` from `eellar.auth.guard` module globally
127+
just like we did in [applying guard globally](./guard-strategy.md#apply-authguard-globally)
128+
129+
```python title='auth/module.py' linenums='1'
130+
from datetime import timedelta
131+
132+
from ellar.app import use_global_guards
133+
from ellar.auth.guards import AuthenticatedRequiredGuard
134+
from ellar.common import Module
135+
from ellar.core import ModuleBase, LazyModuleImport as lazyLoad
136+
from ellar_jwt import JWTModule
137+
138+
from .controllers import AuthController
139+
from .services import AuthService
140+
141+
## Registers AuthenticatedRequiredGuard to the GLOBAL GUARDS
142+
use_global_guards(AuthenticatedRequiredGuard('JWTAuthentication', []))
143+
144+
@Module(
145+
modules=[
146+
lazyLoad('project_name.users.module:UserModule'),
147+
JWTModule.setup(
148+
signing_secret_key="my_poor_secret_key_lol", lifetime=timedelta(minutes=5)
149+
),
150+
],
151+
controllers=[AuthController],
152+
providers=[AuthService],
153+
)
154+
class AuthModule(ModuleBase):
155+
"""
156+
Auth Module
157+
"""
158+
```
159+
160+
Still having the server running, we can test as before
161+
162+
```shell
163+
$ # GET /auth/profile
164+
$ curl http://localhost:8000/auth/profile
165+
{"detail":"Forbidden"} # status_code=403
166+
167+
$ # POST /auth/login
168+
$ curl -X POST http://localhost:8000/auth/login -d '{"username": "john", "password": "password"}' -H "Content-Type: application/json"
169+
{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2OTg3OTE0OTE..."}
170+
171+
$ # GET /profile using access_token returned from previous step as bearer code
172+
$ curl http://localhost:8000/auth/profile -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2Vybm..."
173+
{"exp":1698793558,"iat":1698793258,"jti":"e96e94c5c3ef4fbbbd7c2468eb64534b","sub":1,"user_id":1,"username":"john", "id":null,"auth_type":"bearer"}
174+
175+
```
176+
Source Code to this example is [here](https://github.com/python-ellar/ellar/tree/main/examples/04-auth-with-handlers)

0 commit comments

Comments
 (0)