Authentication is an essential part of most applications. It refers to the methods and techniques used to verify the identity of users interacting with your application. There are many different approaches and strategies to handle authentication. The approach taken for any project depends on its particular application requirements. In this section, we shall go through different approaches to authentication in Ellar and how it will suit your authentication requirements.
There are two ways in which user authentication and identification are processed in Ellar:
We have discussed in detail how Guards are used to protect a route and check for user authorizations, but we never really addressed how they can be used for authentication purposes. For this, we are going to illustrate JWT authentication using Guard
Let's flesh out our requirements. For this use case, clients will start by authenticating with a username and password. Once authenticated, the server will issue a JWT that can be sent as a bearer token in an authorization header on subsequent requests to prove authentication. Then, we create a protected route that is accessible only to requests that contain a valid JWT.
Let's start with the first requirement: authenticating a user, then extend that by issuing a JWT. And finally, we'll create a protected route that checks for a valid JWT on the request.
We´ll start by scaffolding an AuthModule with the Ellar CLI tool followed by AuthService and AuthController implementations. We´ll use the AuthService to implement the authentication logic and the AuthController to expose the authentication endpoints.
ellarcreate-moduleauth
Also, the AuthService would need UserService, which encapsulates user operations. Let's also scaffold a user module.
ellarcreate-moduleuser
Now, let's add some implementations to the generated files. For this application, the UserService will be working with a hard-coded list of users with a retrieve one-by-email method. In a real application, you´d build your user model and persistence layer using a library of your choice like SQLAlchemy, Django ORM, Peewee, PonyORM, etc.
In the above example, we have used make_password to hash the password. It is strictly advised you don't save passwords as plain text. In the UsersModule, we need to register the UserService we just created so that it will be injectable in AuthService
user/module.py
1 2 3 4 5 6 7 8 910111213
fromellar.commonimportModulefromellar.coreimportModuleBasefrom.servicesimportUsersService@Module(providers=[UsersService],)classUserModule(ModuleBase):""" User Module """
At this junction, the AuthService returns a Python dictionary object of the user retrieved if the password is correct. But in the real sense, we need a token returned to the client.
For this, we need to install the ellar-jwt package
pipinstallellar-jwt
Let us review and refine our requirements once again:
Allow users to authenticate with username/password, returning a JWT for use in subsequent calls to protected API endpoints. This is almost done. What is left is to write the code that issues a JWT.
Create API routes that are protected based on the presence of a valid JWT as a bearer token
EllarJWT comes with JWTModule and JWTService for encoding and decoding tokens. Let us configure the JWTModule inside AuthModule.
In the above example, we configured JWTModule with very minimal configurations and registered it as a module dependency together with UserModule. Also we have registered AuthController and AuthService to AuthModule as well. With that done, we have completed the AuthModule setup.
Now, let us finish the AuthService by returning a token using JWTService.
At this point, we can run the application to test what we have done so far. To do that, we need to register AuthModule to the ApplicationModule.
project_name/root_module.py
1 2 3 4 5 6 7 8 910111213
fromellar.commonimportModule,exception_handlerfromellar.commonimportIExecutionContext,JSONResponse,Responsefromellar.coreimportModuleBase,LazyModuleImportaslazyLoadfromellar.samples.modulesimportHomeModule@Module(modules=[HomeModule,lazyLoad('project_name.auth.module:AuthModule'),],)classApplicationModule(ModuleBase):@exception_handler(404)defexception_404_handler(cls,ctx:IExecutionContext,exc:Exception)->Response:returnJSONResponse(dict(detail="Resource not found."))
Then restart the server if it is not running.
ellarrunserver--reload
Let us make some requests to the server. Open a terminal window and run the code below:
$# POST to /auth/login
$curl-XPOSThttp://localhost:8000/auth/login-d'{"username": "john", "password": "password"}'-H"Content-Type: application/json"{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2OTg3OTE0OTE..."}
$# Note: above JWT truncated
At this point, we can now comfortably address our final requirement: protecting endpoints by requiring a valid JWT to be present on the request. We will do this by creating an AuthGuard that will be used to guard our routes.
Ensure the app is running, and test the routes using cURL.
$# GET /auth/profile
$curlhttp://localhost:8000/auth/profile
{"detail":"Forbidden"}# status_code=403
$# POST /auth/login
$curl-XPOSThttp://localhost:8000/auth/login-d'{"username": "john", "password": "password"}'-H"Content-Type: application/json"{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2OTg3OTE0OTE..."}
$# GET /profile using access_token returned from previous step as bearer code
$curlhttp://localhost:8000/auth/profile-H"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2Vybm..."{"exp":1698793558,"iat":1698793258,"jti":"e96e94c5c3ef4fbbbd7c2468eb64534b","sub":1,"user_id":1,"username":"john","id":null,"auth_type":"bearer"}
Note in the AuthModule configuration, we configured the JWT to have an expiration of 5 minutes. If you wait 5 minutes after authenticating before attempting a GET/auth/profile request, you'll receive a 401 Unauthorized response. This is because the EllarJWT package automatically checks the JWT for its expiration time, saving you the trouble of doing so in your application.
This was not included in the requirement, but it might be useful to some developers. So, let us address token refresh using EllarJWT. Depending your application, this illustration may vary.
To get this done, we need to edit the sign_in in AuthService to return access_token and refresh_token. We also need to add a refresh_token endpoint to our AuthController.
We have modified the sign_in method and added the refresh_token method to handle refresh token actions. The sign_in method return access_token and refresh_token that expires in 30days.
In situations when you need to protect all your endpoints, you can register AuthGuard as a global guard instead of using the @UseGuards decorator in all your controllers or route functions. However, you also need to implement a mechanism to skip the auth guard for certain route functions that don't require it, such as the sign_in route function.
First, let us register AuthGuard a global guard in AuthModule.
Let us define a mechanism for declaring routes as anonymous or public.
One way to achieve this, is by using the set_metadata decorator. We can set some metadata on those functions, and it can be read in AuthGuard. If the metadata is present, we exit the authentication verification and allow the execution to continue.
We have defined the allow_any metadata decorator in the above illustration and have used Ellar's built-in class Reflector to read the metadata defined at the controller or route function.
We can create an allow_any decorator function that defines a guard metadata on the decorated function to override the global guard
Authentication scheme is another strategy for identifying the user who is using the application. The difference between it and and Guard strategy is your identification executed at middleware layer when processing incoming request while guard execution happens just before route function is executed.
Ellar provides BaseAuthenticationHandler contract which defines what is required to set up any authentication strategy. 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.
Just like AuthGuard, we need to create its equivalent. But first we need to create a auth_scheme.py at the root level of your application for us to define a JWTAuthentication handler.
importtypingastfromellar.common.serializer.guardimport(HTTPAuthorizationCredentials,)fromellar.authimportUserIdentityfromellar.auth.handlersimportHttpBearerAuthenticationHandlerfromellar.commonimportIHostContextfromellar.diimportinjectablefromellar_jwtimportJWTService@injectableclassJWTAuthentication(HttpBearerAuthenticationHandler):def__init__(self,jwt_service:JWTService)->None:self.jwt_service=jwt_serviceasyncdefauthentication_handler(self,context:IHostContext,credentials:HTTPAuthorizationCredentials,)->t.Optional[t.Any]:# this function will be called by Identity Middleware but only when a `Bearer token` is found on the header requesttry:data=awaitself.jwt_service.decode_async(credentials.credentials)returnUserIdentity(auth_type=self.scheme,**data)exceptExceptionasex:# if we cant identity the user or token has expired, we return None.returnNone
Let us make JWTAuthentication Handler available for ellar to use as shown below
Unlike guards, Authentication handlers are registered global by default as shown in the above illustration. Also, we need to remove GlobalGuard registration we did in AuthModule, so that we don't have too user identification checks.
Note
In the above illustration, we added JWTAuthentication as a type. This means DI will create JWTAuthentication instance. We can use this method because we want JWTService to be injected when instantiating JWTAuthentication. But if you don't have any need for DI injection, you can use the below.
In the above illustration, we decorated AuthController with @AuthenticationRequired('JWTAuthentication') to ensure we have authenticated user before executing any route function and, we passed in JWTAuthentication as a parameter, which will be used in openapi doc to define the controller routes security scheme.
It is importance to note that when using AuthenticationHandler approach, that you have to always use AuthenticationRequired decorator on route functions or controller that needs protected from anonymous users.
But if you have a single form of authentication, you can register AuthenticatedRequiredGuard from eellar.auth.guard module globally just like we did in applying guard globally
Still having the server running, we can test as before
$# GET /auth/profile
$curlhttp://localhost:8000/auth/profile
{"detail":"Forbidden"}# status_code=403
$# POST /auth/login
$curl-XPOSThttp://localhost:8000/auth/login-d'{"username": "john", "password": "password"}'-H"Content-Type: application/json"{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2OTg3OTE0OTE..."}
$# GET /profile using access_token returned from previous step as bearer code
$curlhttp://localhost:8000/auth/profile-H"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2Vybm..."{"exp":1698793558,"iat":1698793258,"jti":"e96e94c5c3ef4fbbbd7c2468eb64534b","sub":1,"user_id":1,"username":"john","id":null,"auth_type":"bearer"}