Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Cover image for Rust: actix-web JSON Web Token authentication.
Be Hai Nguyen
Be Hai Nguyen

Posted on

     

Rust: actix-web JSON Web Token authentication.

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
Enter fullscreen modeExit fullscreen mode

Theactix-web learning application mentioned above has been discussed in the following nine previous posts:

  1. Rust web application: MySQL server, sqlx, actix-web and tera.
  2. Rust: learning actix-web middleware 01.
  3. Rust: retrofit integration tests to an existing actix-web application.
  4. Rust: adding actix-session and actix-identity to an existing actix-web application.
  5. Rust: actix-web endpoints which accept bothapplication/x-www-form-urlencoded andapplication/json content types.
  6. Rust: simple actix-web email-password login and request authentication using middleware.
  7. Rust: actix-web get SSL/HTTPS for localhost.
  8. Rust: actix-web CORS, Cookies and AJAX calls.
  9. 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
Enter fullscreen modeExit fullscreen mode

-- Note the tagv0.9.0.

Table of contents

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 impliesSECONDS_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 impliesSECONDS_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:

  1. Replace the currentactix-identity::Identity login with the newaccess token.
  2. Always send the newaccess 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:

  1. Only send theaccess token to clientsonce if the content type of the login request isapplication/json.
  2. Then users of anAPI-like server or aservice will only have oneaccess token until it expires. They will need to log in again to obtain a new token.
  3. Still replace the currentactix-identity::Identity login with the newaccess 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
Enter fullscreen modeExit fullscreen mode

That is, theaccess token used duringrequest authentication is in the format:

Bearer. + the proper JSON Web Token
Enter fullscreen modeExit fullscreen mode

For example:

Bearer.eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6ImNoaXJzdGlhbi5rb2JsaWNrLjEwMDA0QGdtYWlsLmNvbSIsImlhdCI6MTcwODU1OTcwNywiZXhwIjoxNzA4NTYxNTA3LCJsYXN0X2FjdGl2ZSI6MTcwODU1OTcwN30.CN-whQ0rWW8IuLPVTF7qprk4-GgtK1JSJqp3C8X-ytE
Enter fullscreen modeExit fullscreen mode

❶ 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 ☆
Enter fullscreen modeExit fullscreen mode

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 theiat,email,exp, andlast_active fields.

Additionally, there are two main functions:

  1. 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.
  2. 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:

  1. 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.
  2. pub fn make_bearer_token -- a wrapper function that creates a“Bearer” token from a given token.
  3. 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();...
Enter fullscreen modeExit fullscreen mode

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();...
Enter fullscreen modeExit fullscreen mode

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>}
Enter fullscreen modeExit fullscreen mode

Thestruct TokenStatus represents the status of theaccess token for the current request:


⓷ 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:

  1. Replaces the currentactix-identity::Identity login with the new updated token, asdiscussed earlier.
  2. 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:

  1. Update the call tofn verify_valid_access_token(request: &ServiceRequest) -> TokenStatus; the return value is nowstruct TokenStatus.
  2. If the token is in error, call the closureunauthorised_token() to return the error response. The request is then completed.
  3. If the request is from anauthenticated 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 tokenis 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.rsnext 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:

  1. Functionpub fn mock_access_token(&self, secs_valid_for: u64) -> String now returns a correctly formatted“Bearer” token. Please note the new parametersecs_valid_for.
  2. New functionpub fn jwt_secret_key() -> String
  3. New functionpub 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.
  4. Rewrotepub fn assert_access_token_in_header(response: &reqwest::Response, email: &str) andpub fn assert_access_token_in_cookie(response: &reqwest::Response, email: &str).
  5. Updatedpub 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:

  1. Call the functionpub fn mock_access_token(&self, secs_valid_for: u64) -> String with the new parametersecs_valid_for.
  2. 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)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

I have been a software develper for around 20 years. I am looking forward to work with Python. My other blogs:https://behainguyen.wordpress.com/https://behai-nguyen.github.io/
  • Location
    Melbourne, Australia
  • Education
    Master of Applied Science in Computer Science, RMIT, Victoria, Australia
  • Work
    Software Developer
  • Joined

More fromBe Hai Nguyen

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp