Rust: actix-web JSON Web Token authentication.
In the sixth post of our actix-web learning application, we implemented a basic email-password login process with a placeholder for a token
. In this post, we will implement a comprehensive JSON Web Token (JWT)-based authentication system. We will utilise the jsonwebtoken crate, which we have previously studied.
π¦ Index of the Complete Series.
Rust: actix-web JSON Web Token authentication. |
π Please note, complete code for this post can be downloaded from GitHub with:
git clone -b v0.10.0 https://github.com/behai-nguyen/rust_web_01.git
The actix-web learning application mentioned above has been discussed in the following nine previous posts:
- Rust web application: MySQL server, sqlx, actix-web and tera.
- Rust: learning actix-web middleware 01.
- Rust: retrofit integration tests to an existing actix-web application.
- Rust: adding actix-session and actix-identity to an existing actix-web application.
- Rust: actix-web endpoints which accept both
application/x-www-form-urlencoded
andapplication/json
content types. - Rust: simple actix-web email-password login and request authentication using middleware.
- Rust: actix-web get SSL/HTTPS for localhost.
- Rust: actix-web CORS, Cookies and AJAX calls.
- Rust: actix-web global extractor error handlers.
The code weβre developing in this post is a continuation of the code from the ninth post above. π To get the code of this ninth post, please use the following command:
git clone -b v0.9.0 https://github.com/behai-nguyen/rust_web_01.git
β Note the tag v0.9.0
.
Table of contents
- Previous Studies on JSON Web Token (JWT)
- Proposed JWT Implementations: Problems and Solutions
- The βBearerβ Token Scheme
- Project Layout
- The Token Utility jwt_utils.rs and Test test_jsonwebtoken.rs Modules
- The Updated Login Process
- The Updated Request Authentication Process
- JWT and Logout
- Updating Integration Tests
- Concluding Remarks
Previous Studies on JSON Web Token (JWT)
As mentioned earlier, we conducted studies on the jsonwebtoken crate, as detailed in the post titled Rust: JSON Web Token β some investigative studies on crate jsonwebtoken. The JWT implementation in this post is based on the specifications discussed in the second example of the aforementioned post, particularly focusing on this specification:
π It should be obvious that: this implementation implies
SECONDS_VALID_FOR
is the duration the token stays valid since last active. It does not mean that after this duration, the token becomes invalid or expired. So long as the client keeps sending requests while the token is valid, it will never expire!
We will provide further details on this specification later in the post. Additionally, before studying the jsonwebtoken crate, we conducted research on the jwt-simple crate, as discussed in the post titled Rust: JSON Web Token β some investigative studies on crate jwt-simple. It would be beneficial to review this post as well, as it covers background information on JWT.
Proposed JWT Implementations: Problems and Solutions
Proposed JWT Implementations
Letβs revisit the specifications outlined in the previous section:
π It should be obvious that: this implementation implies
SECONDS_VALID_FOR
is the duration the token stays valid since last active. It does not mean that after this duration, the token becomes invalid or expired. So long as the client keeps sending requests while the token is valid, it will never expire!
This concept involves extending the expiry time of a valid token every time a request is made. This functionality was demonstrated in the original discussion, specifically in the second example section mentioned earlier.
π¦ Since the expiry time is updated, we generate a new access token
. Hereβs what we do with the new token:
-
Replace the current actix-identity::Identity login with the new
access token
. -
Always send the new
access token
to clients via both the response header and the response cookieauthorization
, as in the login process.
We generate a new access token
based on logic, but it doesnβt necessarily mean the previous ones have expired.β
Problems with the Proposed Implementations
The proposed implementations outlined above present some practical challenges, which we will discuss next.
However, for the sake of learning in this project, we will proceed with the proposed implementations despite the identified issues.
Problems when Used as an API-Server or Service
In an API-like server
or a service
, users are required to include a valid access token
in the request authorization
header. Therefore, if a new token is generated, users should have access to this latest token.
What happens if users simply ignore the new tokens and continue using a previous one that has not yet expired? In such a scenario, request authentication
would still be successful, and the requests would potentially succeed until the old token expires. However, a more serious concern arises if we implement blacklisting. In that case, we would need to blacklist all previous tokens. This would necessitate writing the current access token to a blacklist table for every request, which is impractical.
Problems when Used as an Application Server
When used as an application server
, we simply replace the current actix-identity::Identity login with the new access token
. If we implement blacklisting, we only need to blacklist the last token
π This process makes sense, as we cannot expire a session while a user is still actively using it.
However, we still encounter similar problems as described in the previous section for API-like servers
or services
. Since clients always have access to the authorization
response header and cookie, they can use this token with different client tools to send requests, effectively treating the application as an API-like server
or a service
.
Proposed Solutions
The above problems would disappear, and the actual implementations would be simpler if we adjust the logic slightly:
-
Only send the
access token
to clients once if the content type of the login request isapplication/json
. -
Then users of an
API-like server
or aservice
will only have oneaccess token
until it expires. They will need to log in again to obtain a new token. -
Still replace the current actix-identity::Identity login with the new
access token
. Theapplication server
continues to function as usual. However, since users no longer have access to the token, we only need to manage the one stored in the actix-identity::Identity login.
But as mentioned at the start of this section, we will ignore the problems and, therefore, the solutions for this revision of the code.
The βBearerβ Token Scheme
We adhere to the βBearerβ token scheme as specified in RFC 6750, section 2.1. Authorization Request Header Field:
For example:
GET /resource HTTP/1.1
Host: server.example.com
Authorization: Bearer mF_9.B5f-4.1JqM
That is, the access token
used during request authentication
is in the format:
Bearer. + the proper JSON Web Token
For example:
Bearer.eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6ImNoaXJzdGlhbi5rb2JsaWNrLjEwMDA0QGdtYWlsLmNvbSIsImlhdCI6MTcwODU1OTcwNywiZXhwIjoxNzA4NTYxNTA3LCJsYXN0X2FjdGl2ZSI6MTcwODU1OTcwN30.CN-whQ0rWW8IuLPVTF7qprk4-GgtK1JSJqp3C8X-ytE
βΆ The access token
included in the request authorization
header must adhere to the βBearerβ token format.
β· Similarly, the access token
set for the actix-identity::Identity login is also a βBearerβ token.
π¦ However, the access token
sent to clients via the response header and the response cookie authorization
is always a pure JSON Web Token.
Project Layout
Below is the complete project layout.
β Please note, those marked with β are updated, and those marked with β are new.
.
βββ .env β
βββ Cargo.toml β
βββ cert
β βββ cert-pass.pem
β βββ key-pass-decrypted.pem
β βββ key-pass.pem
βββ migrations
β βββ mysql
β β βββ migrations
β β βββ 20231128234321_emp_email_pwd.down.sql
β β βββ 20231128234321_emp_email_pwd.up.sql
β βββ postgres
β βββ migrations
β βββ 20231130023147_emp_email_pwd.down.sql
β βββ 20231130023147_emp_email_pwd.up.sql
βββ README.md β
βββ src
β βββ auth_handlers.rs β
β βββ auth_middleware.rs β
β βββ bh_libs
β β βββ api_status.rs
β β βββ australian_date.rs
β βββ bh_libs.rs
β βββ config.rs β
β βββ database.rs
β βββ handlers.rs
β βββ helper
β β βββ app_utils.rs β
β β βββ constants.rs β
β β βββ endpoint.rs β
β β βββ jwt_utils.rs β
β β βββ messages.rs β
β βββ helper.rs β
β βββ lib.rs β
β βββ main.rs
β βββ middleware.rs
β βββ models.rs β
βββ templates
β βββ auth
β β βββ home.html
β β βββ login.html
β βββ employees.html
βββ tests
βββ common.rs β
βββ test_auth_handlers.rs β
βββ test_handlers.rs β
βββ test_jsonwebtoken.rs β
The Token Utility jwt_utils.rs and Test test_jsonwebtoken.rs Modules
The Token Utility src/helper/jwt_utils.rs Module
In the module src/helper/jwt_utils.rs, we implement all the JWT management code, which includes the core essential code that somewhat repeats the code already mentioned in the second example:
-
struct JWTPayload
-- represents the JWT payload, where theemail
field uniquely identifies the logged-in user. -
JWTPayload implementation
-- implements some of the required functions and methods:- A function to create a new instance.
-
Methods to update the expiry field (
exp
) and thelast_active
field using seconds, minutes, and hours. -
Four getter methods which return the values of the
iat
,email
,exp
, andlast_active
fields.
Additionally, there are two main functions:
-
pub fn make_token
-- creates a new JWT from anemail
. The parametersecs_valid_for
indicates how many seconds the token is valid for, and the parametersecret_key
is used by the jsonwebtoken crate to encode the token. It creates an instance ofstruct JWTPayload
, and then creates a token using this instance. -
pub fn decode_token
-- decodes a given token. If the token is valid and successfully decoded, it returns the token'sstruct JWTPayload
. Otherwise, it returns anApiStatus
which describes the error.
Other functions are βconvenientβ functions or wrapper functions:
-
pub fn make_token_from_payload
-- creates a JWT from an instance of structstruct JWTPayload
. It is a "convenient" function. We decode the current token, update the extracted payload, then call this function to create an updated token. -
pub fn make_bearer_token
-- a wrapper function that creates a βBearerβ token from a given token. -
pub fn decode_bearer_token
-- a wrapper function that decodes a βBearerβ token.
Please note also the unit test section in this module. There are sufficient tests to cover all functions and methods.
The documentation in the source code should be sufficient to aid in the reading of the code.
The Test tests/test_jsonwebtoken.rs Module
We implement some integration tests for JWT management code. These tests are self-explanatory.
The Updated Login Process
In the current login process, at step 4, we note:
...
// TO_DO: Work in progress -- future implementations will formalise access token.
let access_token = &selected_login.email;
// https://docs.rs/actix-identity/latest/actix_identity/
// Attach a verified user identity to the active session
Identity::login(&request.extensions(), String::from(access_token)).unwrap();
...
This part of the login process handler pub async fn login(request: HttpRequest, app_state: web::Data<super::AppState>, body: Bytes) -> Either<impl Responder, HttpResponse>
is updated to:
...
let access_token = make_token(&selected_login.email,
app_state.cfg.jwt_secret_key.as_ref(), app_state.cfg.jwt_mins_valid_for * 60);
// https://docs.rs/actix-identity/latest/actix_identity/
// Attach a verified user identity to the active session
Identity::login(&request.extensions(), String::from( make_bearer_token(&access_token) )).unwrap();
...
Please note the call to make_bearer_token
, which adheres to The βBearerβ Token Scheme.
This update would take care of the application server
case. In the case of an API-like server
or a service
, users are required to include a valid access token
in the request authorization
header, as mentioned, so we donβt need to do anything.
The next task is to update the request authentication
process. This update occurs in the src/auth_middleware.rs and the src/lib.rs modules.
The Updated Request Authentication Process
The updated request request authentication
involves changes to both the src/auth_middleware.rs and src/lib.rs modules.
This section, How the Request Authentication Process Works, describes the current process.
Code Updated in the src/auth_middleware.rs Module
Please recall that the src/auth_middleware.rs module serves as the request authentication middleware
. We will make some substantial updates within this module.
Although the code has sufficient documentation, we will discuss the updates in the following sections.
β΅ The module documentation has been updated to describe how the request authentication
process works with JWT. Please refer to the documentation section How This Middleware Works for more details.
βΆ New struct TokenStatus
:
struct TokenStatus {
is_logged_in: bool,
payload: Option<JWTPayload>,
api_status: Option<ApiStatus>
}
The struct TokenStatus
represents the status of the access token
for the current request:
-
When there is no token,
is_logged_in
is set tofalse
to indicate that the request comes from an unauthenticated session
. The other two fields are set toNone
, indicating that there is no error. -
When there is a token, we call the
pub fn decode_token(token: &str, secret_key: &[u8]) -> Result<JWTPayload, ApiStatus>
function:-
If token decoding fails or the token has already expired,
is_logged_in
is set tofalse
, andapi_status
is set to the returnedApiStatus
. This indicates an error. -
If token decoding succeeds,
is_logged_in
is set to totrue
, andpayload
is set to the returnedJWTPayload
.
-
If token decoding fails or the token has already expired,
β· The function fn verify_valid_access_token(request: &ServiceRequest) -> TokenStatus
has been completely rewritten, although its purpose remains the same. It checks if the token is present and, if so, decodes it.
The return value of this function is struct TokenStatus
, whose fields are set based on the rules discussed previously.
βΈ The new helper function fn update_and_set_updated_token(request: &ServiceRequest, token_status: TokenStatus)
is called when there is a token and the token is successfully decoded.
It uses the JWTPayload
instance in the token_status
parameter to create the updated access token
. Then, it:
- Replaces the current actix-identity::Identity login with the new updated token, as discussed earlier.
-
Attaches the updated token to dev::ServiceRequest's dev::Extensions by calling fn extensions_mut(&self) -> RefMut<'_, Extensions>.
The next adhoc middleware, discussed in the next section, consumes this extension.
βΉ The new closure, let unauthorised_token = |req: ServiceRequest, api_status: ApiStatus| -> Self::Future
, calls the Unauthorized() method on HttpResponse to return a JSON serialisation of ApiStatus
.
Note the calls to remove the server-side per-request cookies redirect-message
and original-content-type
.
βΊ Update the fn call(&self, request: ServiceRequest) -> Self::Future
function. All groundwork has been completed. The updates to this method are fairly straightforward:
-
Update the call to
fn verify_valid_access_token(request: &ServiceRequest) -> TokenStatus
; the return value is nowstruct TokenStatus
. -
If the token is in error, call the closure
unauthorised_token()
to return the error response. The request is then completed. -
If the request is from an
authenticated session
, meaning we have a token, and the token has been decoded successfully, we make an additional call to the new helper functionfn update_and_set_updated_token(request: &ServiceRequest, token_status: TokenStatus)
, which has been described in the previous section.
The core logic of this method remains unchanged.
Code Updated in the src/lib.rs Module
As mentioned previously, if a valid token is present, an updated token is generated from the current tokenβs payload every time a request occurs. This updated access token
is then sent to the client via both the response header and the response cookie authorization
.
This section describes how the updated token is attached to the request extension so that the next adhoc middleware can pick it up and send it to the clients.
This is the updated src/lib.rs
next adhoc middleware. Its functionality is straightforward. It queries the current dev::ServiceRequestβs dev::Extensions for a String, which represents the updated token. If found, it sets the ServiceResponse authorization
header and cookie with this updated token.
Afterward, it forwards the response. Since it is currently the last middleware in the call stack, the response will be sent directly to the client, completing the request.
JWT and Logout
Due to the issues outlined in this section and this section, we were unable to effectively implement the logout functionality in the application. This will remain unresolved until we implement the proposed solutions and integrate blacklisting.
β For the time being, we will retain the current logout process unchanged.
Once blacklisting is implemented, the request authentication
process will need to validate the access token
against the blacklist table. If the token is found in the blacklist, it will be considered invalid.
Updating Integration Tests
There is a new integration test module as already discussed in section The Test tests/test_jsonwebtoken.rs Module. There is no new integration test added to existing modules.
Some common test code has been updated as a result of implementing JSON Web Token.
β΅ There are several updates in module tests/common.rs:
-
Function
pub fn mock_access_token(&self, secs_valid_for: u64) -> String
now returns a correctly formatted βBearerβ token. Please note the new parametersecs_valid_for
. -
New function
pub fn jwt_secret_key() -> String
-
New function
pub fn assert_token_email(token: &str, email: &str)
. It decodes the parametertoken
, which is expected to always succeed, then tests that the tokenJWTPayload
'semail
value equal to parameteremail
. -
Rewrote
pub fn assert_access_token_in_header(response: &reqwest::Response, email: &str)
andpub fn assert_access_token_in_cookie(response: &reqwest::Response, email: &str)
. -
Updated
pub async fn assert_json_successful_login(response: reqwest::Response, email: &str)
.
βΆ Some minor changes in both the tests/test_handlers.rs and the tests/test_auth_handlers.rs modules:
-
Call the function
pub fn mock_access_token(&self, secs_valid_for: u64) -> String
with the new parametersecs_valid_for
. - Other updates as a result of the updates in the tests/common.rs module.
Concluding Remarks
It has been an interesting process for me as I delved into the world of actix-web adhoc middleware. While the code may seem simple at first glance, I encountered some problems along the way and sought assistance to overcome them.
I anticipated the problems, as described in this section and this section, before diving into the actual coding process. Despite the hurdles, I proceeded with the implementation because I wanted to learn how to set a custom header for all routes before their final response is sent to clients β thatβs the essence of adhoc middleware.
In a future post, I plan to implement the proposed solutions and explore the concept of blacklisting.
I hope you find this post informative and helpful. Thank you for reading. And stay safe, as always.
βΏβΏβΏ
Feature image source:
- https://www.omgubuntu.co.uk/2022/09/ubuntu-2210-kinetic-kudu-default-wallpaper
- https://in.pinterest.com/pin/337277459600111737/
- https://www.rust-lang.org/
- https://www.pngitem.com/download/ibmJoR_rust-language-hd-png-download/