
In thesixth post of ouractix-web learning application, we implemented a basic email-password login process with a placeholder for atoken
. In this post, we will implement a comprehensive JSON Web Token (JWT)-based authentication system. We will utilise thejsonwebtoken crate, which we havepreviously studied.
🚀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
Theactix-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 theninth post above. 🚀 To get the code of thisninth post, please use the following command:
git clone -b v0.9.0 https://github.com/behai-nguyen/rust_web_01.git
-- Note the tagv0.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 thejsonwebtoken crate, as detailed in the post titledRust: JSON Web Token -- some investigative studies on crate jsonwebtoken. The JWT implementation in this post is based on the specifications discussed in thesecond 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, beforestudying the jsonwebtoken crate, we conducted research on thejwt-simple crate, as discussed in the post titledRust: 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 thesecond example section mentioned earlier.
🦀Since the expiry time is updated, we generate a newaccess token
. Here's what we do with the new token:
- Replace the currentactix-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 thelogin process.
We generate a newaccess 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 anAPI-like server
or aservice
, users are required to include a validaccess token
in the requestauthorization
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 anapplication server
, we simply replace the currentactix-identity::Identity login with the newaccess 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 problemsas described in the previous section forAPI-like servers
orservices
. Since clients always have access to theauthorization
response header and cookie, they can use this token with different client tools to send requests, effectively treating the application as anAPI-like server
or aservice
.
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 clientsonce 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 currentactix-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 theactix-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 inRFC 6750, section2.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, theaccess token
used duringrequest authentication
is in the format:
Bearer. + the proper JSON Web Token
For example:
Bearer.eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6ImNoaXJzdGlhbi5rb2JsaWNrLjEwMDA0QGdtYWlsLmNvbSIsImlhdCI6MTcwODU1OTcwNywiZXhwIjoxNzA4NTYxNTA3LCJsYXN0X2FjdGl2ZSI6MTcwODU1OTcwN30.CN-whQ0rWW8IuLPVTF7qprk4-GgtK1JSJqp3C8X-ytE
❶ Theaccess token
included in the requestauthorization
header must adhere to the "Bearer" token format.
❷ Similarly, theaccess token
set for theactix-identity::Identity login is also a "Bearer" token.
🦀 However, theaccess token
sent to clients via the response header and the response cookieauthorization
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 modulesrc/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 thesecond 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 thejsonwebtoken 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 currentlogin process, atstep 4, we note:
...// TO_DO: Work in progress -- future implementations will formalise access token.letaccess_token=&selected_login.email;// https://docs.rs/actix-identity/latest/actix_identity/// Attach a verified user identity to the active sessionIdentity::login(&request.extensions(),String::from(access_token)).unwrap();...
This part of the login process handlerpub async fn login(request: HttpRequest, app_state: web::Data<super::AppState>, body: Bytes) -> Either<impl Responder, HttpResponse>
is updated to:
...letaccess_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 sessionIdentity::login(&request.extensions(),String::from(make_bearer_token(&access_token))).unwrap();...
Please note the call tomake_bearer_token
, which adheres toThe “Bearer” Token Scheme.
This update would take care of theapplication server
case. In the case of anAPI-like server
or aservice
, users are required to include a validaccess token
in the requestauthorization
header,as mentioned, so we don't need to do anything.
The next task is to update therequest authentication
process. This update occurs in thesrc/auth_middleware.rs and thesrc/lib.rs modules.
The Updated Request Authentication Process
The updated requestrequest authentication
involves changes to both thesrc/auth_middleware.rs andsrc/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 thesrc/auth_middleware.rs module serves as therequest 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 therequest authentication
process works with JWT. Please refer to the documentation sectionHow This Middleware Works for more details.
⓶ Newstruct TokenStatus
:
structTokenStatus{is_logged_in:bool,payload:Option<JWTPayload>,api_status:Option<ApiStatus>}
Thestruct TokenStatus
represents the status of theaccess token
for the current request:
- When there is no token,
is_logged_in
is set tofalse
to indicate that the request comes from anunauthenticated 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 functionfn 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 isstruct TokenStatus
, whose fields are set based on the rulesdiscussed previously.
⓸ The new helper functionfn 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 theJWTPayload
instance in thetoken_status
parameter to create the updatedaccess token
. Then, it:
- Replaces the currentactix-identity::Identity login with the new updated token, asdiscussed earlier.
- Attaches the updated token todev::ServiceRequest'sdev::Extensions by callingfn 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 theUnauthorized() method onHttpResponse to return a JSON serialisation ofApiStatus
.
Note the calls to remove theserver-side per-request cookiesredirect-message
andoriginal-content-type
.
⓺ Update thefn 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 theprevious 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 updatedaccess token
is then sent to the client via both the response header and the response cookieauthorization
.
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 updatedsrc/lib.rs
next adhoc middleware. Its functionality is straightforward. It queries the currentdev::ServiceRequest'sdev::Extensions for aString, which represents the updated token. If found, it sets theServiceResponseauthorization
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 inthis section andthis section, we were unable to effectively implement the logout functionality in the application. This will remain unresolved until we implement theproposed solutions and integrate blacklisting.
-- For the time being, we will retain the current logout process unchanged.
Once blacklisting is implemented, therequest authentication
process will need to validate theaccess 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 sectionThe 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 moduletests/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 thetests/test_handlers.rs and thetests/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 thetests/common.rs module.
Concluding Remarks
It has been an interesting process for me as I delved into the world ofactix-web adhoc middleware. While the code may seem simple at first glance, I encountered some problems along the way andsought assistance to overcome them.
I anticipated the problems, as described inthis section andthis 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 theproposed 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:
Top comments(0)
For further actions, you may consider blocking this person and/orreporting abuse