Chapter 11: Securing and Adding an API and JSON

Two of the most important aspects of a web application are authentication and authorization. In this chapter, we are going to learn how to implement simple authentication and authorization systems. After we have created these systems, we are going to learn how to create a simple Application Programming Interface (API) and how to protect the API endpoint using a JSON Web Token (JWT).

At the end of this chapter, you will be able to create an authentication system, with functionality such as logging in and logging out and setting access rights for logged-in users. You will also be able to create an API server and know how to secure the API endpoints.

In this chapter, we are going to cover these main topics:

  • Authenticating users
  • Authorizing users
  • Handling JSON
  • Protecting the API with a JWT

Technical requirements

For this chapter, we have the usual requirements: a Rust compiler, a text editor, a web browser, and a PostgreSQL database server, along with the FFmpeg command line. We are going to learn about JSON and APIs in this chapter. Install cURL or any other HTTP testing client.

You can find the source code for this chapter at https://github.com/PacktPublishing/Rust-Web-Development-with-Rocket/tree/main/Chapter11.

Authenticating users

One of the most common tasks of a web application is handling registration and logging in. By logging in, users can tell the web server that they really are who they say they are.

We already created a sign-up system when we implemented CRUD for the user model. Now, let's implement a login system using the existing user model.

The idea for login is simple: the user can fill in their username and password. The application then verifies that the username and password are valid. After that, the application can generate a cookie with the user's information and return the cookie to the web browser. Every time there's a request from the browser, the cookie is sent back from the browser to the server, and we validate the content of the cookie.

To make sure we don't have to implement the cookie for every request, we can create a request guard that validates the cookie automatically if we use the request guard in a route handling function.

Let's start implementing a user login system by following these steps:

  1. Create a request guard to handle user authentication cookies. We can organize request guards in the same place to make it easier if we want to add new request guards. In src/lib.rs, add a new module:

    pub mod guards;

  2. Then, create a folder called src/guards. Inside src/guards, add a file called src/guards/mod.rs. Add a new module in this new file:

    pub mod auth;

  3. After that, create a new file called src/guards/auth.rs.
  4. Create a struct to handle user authentication cookies. Let's name the struct CurrentUser. In src/guards/auth.rs, add a struct to store the User information:

    use crate::fairings::db::DBConnection;

    use crate::models::user::User;

    use rocket::http::Status;

    use rocket::request::{FromRequest, Outcome, Request};

    use rocket::serde::Serialize;

    use rocket_db_pools::{sqlx::Acquire, Connection};

    #[derive(Serialize)]

    pub struct CurrentUser {

        pub user: User,

    }

  5. Define a constant that will be used as a key for the cookie to store the user's universally unique identifier (UUID):

    pub const LOGIN_COOKIE_NAME: &str = "user_uuid";

  6. Implement the FromRequest trait for CurrentUser to make the struct a request guard. Add the implementation skeleton as follows:

    #[rocket::async_trait]

    impl<'r> FromRequest<'r> for CurrentUser {

        type Error = ();

        async fn from_request(req: &'r Request<'_>) ->

        Outcome<Self, Self::Error> {

        }

    }

  7. Inside the from_request function, define an error that will be returned if something goes wrong:

    let error = Outcome::Failure((Status::Unauthorized, ()));

  8. Get the cookie from the request, and extract the UUID from the cookie as follows:

    let parsed_cookie = req.cookies().get_private(LOGIN_COOKIE_NAME);

    if parsed_cookie.is_none() {

        return error;

    }

    let cookie = parsed_cookie.unwrap();

    let uuid = cookie.value();

  9. We want to get a connection to the database to find the user information. We can obtain another request guard (such as Connection<DBConnection>) inside a request guard implementation. Add the following lines:

    let parsed_db = req.guard::<Connection<DBConnection>>().await;

    if !parsed_db.is_success() {

        return error;

    }

    let mut db = parsed_db.unwrap();

    let parsed_connection = db.acquire().await;

    if parsed_connection.is_err() {

        return error;

    }

    let connection = parsed_connection.unwrap();

  10. Find and return the user. Add the following lines:

    let found_user = User::find(connection, uuid).await;

    if found_user.is_err() {

        return error;

    }

    let user = found_user.unwrap();

    Outcome::Success(CurrentUser { user })

  11. Next, we want to implement the login itself. We will create a like sessions/new route to get the page for the login, a sessions/create route to send the username and password for login, and a sessions/delete route for logging out. Before implementing those routes, let's create a template for the login. In src/views, add a new folder called sessions. Then, create a file called src/views/sessions/new.html.tera. Append the following lines into the file:

    {% extends "template" %}

    {% block body %}

      <form accept-charset="UTF-8" action="login"

      autocomplete="off" method="POST">

        <input type="hidden" name="authenticity_token"

        value="{{ csrf_token }}"/>

        <fieldset>

          <legend>Login</legend>

          <div class="row">

            <div class="col-sm-12 col-md-3">

              <label for="username">Username:</label>

            </div>

            <div class="col-sm-12 col-md">

              <input name="username" type="text" value=""

              />

            </div>

          </div>

          <div class="row">

            <div class="col-sm-12 col-md-3">

              <label for="password">Password:</label>

            </div>

            <div class="col-sm-12 col-md">

              <input name="password" type="password" />

            </div>

          </div>

          <button type="submit" value="Submit">

          Submit</button>

        </fieldset>

      </form>

    {% endblock %}

  12. In src/models/user.rs, add a struct for the login information:

    #[derive(FromForm)]

    pub struct Login<'r> {

        pub username: &'r str,

        pub password: &'r str,

        pub authenticity_token: &'r str,

    }

  13. Staying in the same file, we want to create a method for the User struct to be able to find the user from the database based on the login username information, and verify whether the login password is correct or not. After verifying that the password is correct by using the update method, it is time to refactor this. Create a new function to verify passwords:

    fn verify_password(ag: &Argon2, reference: &str, password: &str) -> Result<(), OurError> {

        let reference_hash = PasswordHash::new(

        reference).map_err(|e| {

            OurError::new_internal_server_error(

            String::from("Input error"), Some(

            Box::new(e)))

        })?;

        Ok(ag

            .verify_password(password.as_bytes(),

            &reference_hash)

            .map_err(|e| {

                OurError::new_internal_server_error(

                    String::from("Cannot verify

                    password"),

                    Some(Box::new(e)),

                )

            })?)

    }

  14. Change the update method from these lines:

    let old_password_hash = PasswordHash::new(&old_user.password_hash).map_err(|e| {

        OurError::new_internal_server_error(

        String::from("Input error"), Some(Box::new(e)))

    })?;

    let argon2 = Argon2::default();

    argon2

        .verify_password(user.old_password.as_bytes(),

        &old_password_hash)

        .map_err(|e| {

            OurError::new_internal_server_error(

                String::from("Cannot confirm old

                password"),

                Some(Box::new(e)),

            )

        })?;

And, change it to the following lines:

let argon2 = Argon2::default();

verify_password(&argon2, &old_user.password_hash, user.old_password)?;

  1. Create a method to find a user based on the login username. Inside the impl User block, add the following method:

    pub async fn find_by_login<'r>(

        connection: &mut PgConnection,

        login: &'r Login<'r>,

    ) -> Result<Self, OurError> {

        let query_str = "SELECT * FROM users WHERE

        username = $1";

        let user = sqlx::query_as::<_, Self>(query_str)

            .bind(&login.username)

            .fetch_one(connection)

            .await

            .map_err(OurError::from_sqlx_error)?;

        let argon2 = Argon2::default();

        verify_password(&argon2, &user.password_hash,

        &login.password)?;

        Ok(user)

    }

  2. Now, implement routes for handling login. Create a new mod in src/routes/mod.rs:

    pub mod session;

Then, create a new file called src/routes/session.rs.

  1. In src/routes/session.rs, create a route handling function called new. We want the function to serve the rendered template for the login that we created earlier. Add the following lines:

    use super::HtmlResponse;

    use crate::fairings::csrf::Token as CsrfToken;

    use rocket::request::FlashMessage;

    use rocket_dyn_templates::{context, Template};

    #[get("/login", format = "text/html")]

    pub async fn new<'r>(flash: Option<FlashMessage<'_>>, csrf_token: CsrfToken) -> HtmlResponse {

        let flash_string = flash

            .map(|fl| format!("{}", fl.message()))

            .unwrap_or_else(|| "".to_string());

        let context = context! {

            flash: flash_string,

            csrf_token: csrf_token,

        };

        Ok(Template::render("sessions/new", context))

    }

  2. Then, create a new function called create. In this function, we want to find the user and verify the password with the password hash in the database. If everything goes well, set the cookie with the user information. Append the following lines:

    use crate::fairings::db::DBConnection;

    use crate::guards::auth::LOGIN_COOKIE_NAME;

    use crate::models::user::{Login, User};

    use rocket::form::{Contextual, Form};

    use rocket::http::{Cookie, CookieJar};

    use rocket::response::{Flash, Redirect};

    use rocket_db_pools::{sqlx::Acquire, Connection};

    ...

    #[post("/login", format = "application/x-www-form-urlencoded", data = "<login_context>")]

    pub async fn create<'r>(

        mut db: Connection<DBConnection>,

        login_context: Form<Contextual<'r, Login<'r>>>,

        csrf_token: CsrfToken,

        cookies: &CookieJar<'_>,

    ) -> Result<Flash<Redirect>, Flash<Redirect>> {

        let login_error = || Flash::error(

        Redirect::to("/login"), "Cannot login");

        if login_context.value.is_none() {

            return Err(login_error());

        }

        let login = login_context.value.as_ref().unwrap();

        csrf_token

            .verify(&login.authenticity_token)

            .map_err(|_| login_error())?;

        let connection = db.acquire().await.map_err(|_|

        login_error())?;

        let user = User::find_by_login(connection, login)

            .await

            .map_err(|_| login_error())?;

        cookies.add_private(Cookie::new(LOGIN_COOKIE_NAME,

        user.uuid.to_string()));

        Ok(Flash::success(Redirect::to("/users"), "Login

        successfully"))

    }

  3. Finally, create a function called delete. We will use this function as a route for logging out. Append the following lines:

    #[post("/logout", format = "application/x-www-form-urlencoded")]

    pub async fn delete(cookies: &CookieJar<'_>) -> Flash<Redirect> {

        cookies.remove_private(

        Cookie::named(LOGIN_COOKIE_NAME));

        Flash::success(Redirect::to("/users"), "Logout

        successfully")

    }

  4. Add session::new, session::create, and session::delete into src/main.rs:

    use our_application::routes::{self, post, session, user};

    ...

    async fn rocket() -> Rocket<Build> {

        ...

        routes![

        ...

            session::new,

            session::create,

            session::delete,

        ]

        ...

    }

  5. Now, we can use CurrentUser to ensure that only logged-in users can have access to some endpoints in our application. In src/routes/user.rs, remove the routine to find the user in the edit endpoint. Delete the following lines:

    pub async fn edit_user(

        mut db: Connection<DBConnection>,

        ...

    ) -> HtmlResponse {

        let connection = db

            .acquire()

            .await

            .map_err(|_| Status::InternalServerError)?;

        let user = User::find(connection,

        uuid).await.map_err(|e| e.status)?;

        ...

    }

  6. Then, add CurrentUser to the route that requires a logged-in user as follows:

    use crate::guards::auth::CurrentUser;

    ...

    pub async fn edit_user(...

        current_user: CurrentUser,

    ) -> HtmlResponse {

        ...

        let context = context! {

            form_url: format!("/users/{}", uuid),

            ...

            user: &current_user.user,

            current_user: &current_user,

            ...

        };

        ...

    }

    ...

    pub async fn update_user<'r>(...

        current_user: CurrentUser,

    ) -> Result<Flash<Redirect>, Flash<Redirect>> {

        ...

        match user_value.method {

            "PUT" => put_user(db, uuid, user_context,

            csrf_token, current_user).await,

            "PATCH" => patch_user(db, uuid, user_context,

            csrf_token, current_user).await,

            ...

        }

    }

    ...

    pub async fn put_user<'r>(...

        _current_user: CurrentUser,

    ) -> Result<Flash<Redirect>, Flash<Redirect>> {...}

    ...

    pub async fn patch_user<'r>(...

        current_user: CurrentUser,

    ) -> Result<Flash<Redirect>, Flash<Redirect>> {

        put_user(db, uuid, user_context, csrf_token,

        current_user).await

    }

    ...

    pub async fn delete_user_entry_point(...

        current_user: CurrentUser,

    ) -> Result<Flash<Redirect>, Flash<Redirect>> {

        delete_user(db, uuid, current_user).await

    }

    ...

    pub async fn delete_user(...

        _current_user: CurrentUser,

    ) -> Result<Flash<Redirect>, Flash<Redirect>> {...}

  7. Finally, protect the endpoint in src/routes/post.rs as well. Only logged-in users can upload and delete the post, so modify the code into the following:

    crate::guards::auth::CurrentUser;

    ...

    pub async fn create_post<'r>(...

        _current_user: CurrentUser,

    ) -> Result<Flash<Redirect>, Flash<Redirect>> {...}

    ...

    pub async fn delete_post(...

        _current_user: CurrentUser,

    ) -> Result<Flash<Redirect>, Flash<Redirect>> {...}

Before we implemented authentication, we could edit and delete any user or post. Now try editing or deleting something without logging in. Then, try logging in and deleting and editing.

One problem still exists: after logging in, users can edit and delete other users' information. We will learn how to prevent this problem by implementing authorization in the next section.

Authorizing users

Authentication and authorization are two of the main concepts of information security. If authentication is a way to prove that an entity is who they say they are, then authorization is a way to give rights to the entity. One entity might be able to modify some resources, one entity might be able to modify all resources, one entity might only be able to see limited resources, and so on.

In the previous section, we implemented authentication concepts such as login and CurrentUser; now it's time to implement authorization. The idea is that we make sure logged-in users can only modify their own information and posts.

Please keep in mind that this example is very simple. In more advanced information security, there are more advanced concepts, such as role-based access control. For example, we can create a role called admin, we can set a certain user as admin, and admin can do everything without restrictions.

Let's try implementing simple authorization by following these steps:

  1. Add a simple method for CurrentUser to compare its instance with a UUID. Append the following lines in src/guards/auth.rs:

    impl CurrentUser {

        pub fn is(&self, uuid: &str) -> bool {

            self.user.uuid.to_string() == uuid

        }

        pub fn is_not(&self, uuid: &str) -> bool {

            !self.is(uuid)

        }

    }

  2. Add a new type of error as well. Add a new method in src/errors/our_error.rs in the impl OurError {} block:

    pub fn new_unauthorized_error(debug: Option<Box<dyn Error>>) -> Self {

        Self::new_error_with_status(Status::Unauthorized,

        String::from("unauthorized"), debug)

    }

  3. We can check the CurrentUser instance on the templates to control the flow of the application. For example, if there's no CurrentUser instance, we show the link to sign up and log in. If there is a CurrentUser instance, we show the link to log out. Let's modify the Tera template. Edit src/views/template.html.tera and append the following lines:

    <body>

      <header>

        <a href="/users" class="button">Home</a>

        {% if current_user %}

          <form accept-charset="UTF-8" action="/logout"

          autocomplete="off" method="POST" id="logout"  

          class="hidden"></form>

          <button type="submit" value="Submit" form="

          logout">Logout</button>

        {% else %}

          <a href="/login" class="button">Login</a>

          <a href="/users/new" class="button">Signup</a>

        {% endif %}

      </header>

      <div class="container">

  4. Edit src/views/users/index.html.tera and remove the following line:

    <a href="/users/new" class="button">New user</a>

Find this line:

<a href="/users/edit/{{ user.uuid }}" class="button">Edit User</a>

Modify it into the following lines:

{% if current_user and current_user.user.uuid == user.uuid %}

    <a href="/users/edit/{{user.uuid}}" class="

    button">Edit User</a>

{% endif %}

  1. Edit src/views/users/show.html.tera and find these lines:

    <a href="/users/edit/{{user.uuid}}" class="button">Edit User</a>

    <form accept-charset="UTF-8" action="/users/delete/{{user.uuid}}" autocomplete="off" method="POST" id="deleteUser" class="hidden"></form>

    <button type="submit" value="Submit" form="deleteUser">Delete</button>

And, surround those lines with conditional checking as follows:

{% if current_user and current_user.user.uuid == user.uuid %}

     <a href...

    ...

    </button>

{% endif %}

  1. Next, we want to allow upload only for logged-in users. Find the form lines in src/views/posts/index.html.tera:

    <form action="/users/{{ user.uuid }}/posts" enctype="multipart/form-data" method="POST">

    ...

    </form>

Surround the form lines with the following conditional:

{% if current_user %}

     <form action="/users/{{ user.uuid }}/posts" enctype="multipart/form-data" method="POST">

    ...

    </form>

{% endif %}

  1. Now for the final modification for the template. We want only the owner of the post to be able to delete the post. Find these lines in src/views/posts/show.html.tera:

    <form accept-charset="UTF-8" action="/users/{{user.uuid}}/posts/delete/{{post.uuid}}" autocomplete="off" method="POST" id="deletePost" class="hidden"></form>

    <button type="submit" value="Submit" form="deletePost">Delete</button>

Surround them with the following lines:

{% if current_user and current_user.user.uuid == user.uuid %}

    <form...

    ...

    </button>

{% endif %}

  1. Modify the route handling functions to get the value of current_user. Remember, we can wrap a request guard in Option, such as Option<CurrentUser>. When a route handling function fails to get a CurrentUser instance (for example, there is no logged-in user), it will generate a None variant of Option. We can then pass the instance to a template.

Let's convert route handling functions, starting from src/routes/post.rs. Modify the get_post() function as follows:

pub async fn get_post(...

    current_user: Option<CurrentUser>,

) -> HtmlResponse {

    ...

    let context = context! {user, current_user, post:

    &(post.to_show_post())};

    Ok(Template::render("posts/show", context))

}

  1. Let's do the same thing with the get_posts() function. Modify the function as follows:

    pub async fn get_posts(...

        current_user: Option<CurrentUser>,

    ) -> HtmlResponse {

        let context = context! {

            ...

            current_user,

        };

        Ok(Template::render("posts/index", context))

    }

  2. One thing we can do to secure the create_post() function is to check whether the user uploading the file has the same UUID as user_uuid on the URL. This check is to prevent logged-in attackers from doctoring the request and sending false requests. Put the check in the create_post() function before we do file manipulation, as follows:

    pub async fn create_post<'r>(...

        current_user: CurrentUser,

    ) -> Result<Flash<Redirect>, Flash<Redirect>> {

        ...

        if current_user.is_not(user_uuid) {

            return Err(create_err());

        }

        ...

    }

  3. We can do the same check for the delete_post() function in src/routes/post.rs. We want to prevent unauthorized users from being able to send doctored requests and delete other people's posts. Modify delete_post() as follows:

    pub async fn delete_post(...

        current_user: CurrentUser,

    ) -> Result<Flash<Redirect>, Flash<Redirect>> {

        ...

        if current_user.is_not(user_uuid) {

            return Err(delete_err());

        }

        ...

    }

  4. Try restarting the application, logging in, and seeing whether you can delete other people's posts. Try also modifying src/routes/user.rs by applying the same principle: getting the CurrentUser instance and applying the necessary check, or passing the CurrentUser instance to the template. You can find the full code, including protecting user-related routes, at https://github.com/PacktPublishing/Rust-Web-Development-with-Rocket/tree/main/Chapter11/02Authorization.
  5. One of the most common tasks of a web server is providing APIs, and some APIs must be secured from unwanted usage. We will learn how to serve an API and protect the API endpoint in the next sections.

Handling JSON

One of the common tasks of web applications is handling APIs. APIs can return a lot of different formats, but modern APIs have converged into two common formats: JSON and XML.

Building an endpoint that returns JSON is pretty simple in the Rocket web framework. For handling the request body in JSON format, we can use rocket::serde::json::Json<T> as a data guard. The generic T type must implement the serde::Deserialize trait or else the Rust compiler will refuse to compile.

For responding, we can do the same thing by responding with rocket::serde::json::Json<T>. The generic T type must only implement the serde::Serialize trait when used as a response.

Let's see an example of how to handle JSON requests and responses. We want to create a single API endpoint, /api/users. This endpoint can receive a JSON body similar to the structure of our_application::models::pagination::Pagination, as follows:

{"next":"2022-02-22T22:22:22.222222Z","limit":10}

Follow these steps to implement the API endpoint:

  1. Implement serde::Serialize for OurError. Append these lines into src/errors/our_error.rs:

    use rocket::serde::{Serialize, Serializer};

    use serde::ser::SerializeStruct;

    ...

    impl Serialize for OurError {

        fn serialize<S>(&self, serializer: S) ->

        Result<S::Ok, S::Error>

        where

            S: Serializer,

        {

            let mut state = serializer.

            serialize_struct("OurError", 2)?;

            state.serialize_field("status", &self

            .status.code)?;

            state.serialize_field("message", &self

            .message)?;

            state.end()

        }

    }

  2. We want Pagination to derive Deserialize and to automatically implement the Deserialize trait, as Pagination will be used in the JSON data guard, Json<Pagination>. Because Pagination contains the OurDateTime member, OurDateTime has to implement the Deserialize trait as well. Modify src/models/our_date_time.rs and add the Deserialize derive macro:

    use rocket::serde::{Deserialize, Serialize};

    ...

    #[derive(Debug, sqlx::Type, Clone, Serialize, Deserialize)]

    #[sqlx(transparent)]

    pub struct OurDateTime(pub DateTime<Utc>);

  3. Derive Serialize and Deserialize for Pagination. We also want to derive Serialize because we want to use Pagination as part of the response from the /api/users endpoint. Modify src/models/pagination.rs as follows:

    use rocket::serde::{Deserialize, Serialize};

    ...

    #[derive(FromForm, Serialize, Deserialize)]

    pub struct Pagination {...}

  4. For the User struct, it already derives Serialize automatically, so we can use it in a vector of User. One thing to be fixed is we don't want the password to be included in the resulting JSON. Serde has many macros to control how to generate serialized data from a struct. Append a single macro that will skip the password_hash field. Modify src/models/user.rs:

    pub struct User {

        ...

        #[serde(skip_serializing)]

        pub password_hash: String,

        ...

    }

  5. We want to return the vector of User and Pagination as the resulting JSON. We can create a new struct to wrap those in a field. Append the following lines in src/models/user.rs:

    #[derive(Serialize)]

    pub struct UsersWrapper {

        pub users: Vec<User>,

        #[serde(skip_serializing_if = "Option::is_none")]

        #[serde(default)]

        pub pagination: Option<Pagination>,

    }

Note that we are skipping the pagination field if it's None.

  1. Add a new module in src/routes/mod.rs:

    pub mod api;

Then, create a new file in src/routes/api.rs.

  1. In src/routes/api.rs, add the usual use declarations, models, errors, and database connection:

    use crate::errors::our_error::OurError;

    use crate::fairings::db::DBConnection;

    use crate::models::{

        pagination::Pagination,

        user::{User, UsersWrapper},

    };

    use rocket_db_pools::Connection;

  2. Add a use declaration for rocket::serde::json::Json as well:

    use rocket::serde::json::Json;

  3. Add a route handling function definition to get users:

    #[get("/users", format = "json", data = "<pagination>")]

    pub async fn users(

        mut db: Connection<DBConnection>,

        pagination: Option<Json<Pagination>>,

    ) -> Result<Json<UsersWrapper>, Json<OurError>> {}

  4. Implement the function. In the function, we can get the content of the JSON using the into_inner() method as follows:

    let parsed_pagination = pagination.map(|p| p.into_inner());

  5. Find the users. Append the following lines:

    let (users, new_pagination) = User::find_all(&mut db, parsed_pagination)

        .await

        .map_err(|_| OurError::new_internal_server_

        error(String::from("Internal Error"), None))?;

Because we have implemented the Serialize trait for OurError, we can return the type automatically.

  1. Now, it's time to return UsersWrapper. Append the following lines:

    Ok(Json(UsersWrapper {

        users,

        pagination: new_pagination,

    }))

  2. The last thing to do is to add the route to src/main.rs:

    use our_application::routes::{self, api, post, session, user};

    ...

    .mount("/", ...)

    .mount("/assets", FileServer::from(relative!("static")))

    .mount("/api", routes![api::users])

  3. Try running the application and sending a request to http://127.0.0.1:8000/api/users. We can use any HTTP client, but if we're using cURL, it will be as follows:

    curl -X GET -H "Content-Type: application/json" -d "{"next":"2022-02-22T22:22:22.222222Z","limit":1}" http://127.0.0.1:8000/api/users

The application should return something similar to the following output:

{"users":[{"uuid":"8faa59d6-1079-424a-8eb9-09ceef1969c8","username":"example","email":"[email protected]","description":"example","status":"Inactive","created_at":"2021-11-06T06:09:09.534864Z","updated_at":"2021-11-06T06:09:09.534864Z"}],"pagination":{"next":"2021-11-06T06:09:09.534864Z","limit":1}}

Now that we have finished creating an API endpoint, let's try securing the endpoint in the next section.

Protecting the API with a JWT

One common task we want to do is protect the API endpoints from unauthorized access. There are a lot of reasons why API endpoints have to be protected, such as wanting to protect sensitive data, conducting financial services, or offering subscription services.

In the web browser, we can protect server endpoints by making a session, assigning a cookie to the session, and returning the session to the web browser, but an API client is not always a web browser. API clients can be mobile applications, other web applications, hardware monitors, and many more. This raises the question, how can we protect the API endpoint?

There are a lot of ways to protect the API endpoint, but one industry standard is by using a JWT. According to IETF RFC7519, a JWT is a compact, URL-safe means of representing claims to be transferred between two parties. The claims in a JWT can be either JSON objects or special plaintext representations of said JSON objects.

One flow to use a JWT is as follows:

  1. The client sends an authentication request to the server.
  2. The server responds with a JWT.
  3. The client stores the JWT.
  4. The client uses the stored JWT to send an API request.
  5. The server verifies the JWT and responds accordingly.

Let's try implementing API endpoint protection by following these steps:

  1. Append the required libraries in the Cargo.toml dependencies section:

    hmac = "0.12.1"

    jwt = "0.16.0"

    sha2 = "0.10.2"

  2. We want to use a secret token to sign the JWT token. Add a new entry in Rocket.toml as follows:

    jwt_secret = "fill with your own secret"

  3. Add a new state to store a secret for the token. We want to retrieve the secret when the application creates or verifies JWT. Add the following lines in src/states/mod.rs:

    pub struct JWToken {

        pub secret: String,

    }

  4. Modify src/main.rs to make the application retrieve the secret from the configuration and manage the state:

    use our_application::states::JWToken;

    ...

    struct Config {...

        jwt_secret: String,

    }

    ...

    async fn rocket() -> Rocket<Build> {

        ...

        let config: Config = our_rocket...

        let jwt_secret = JWToken {

            secret: String::from(config.jwt_

            secret.clone()),

        };

        let final_rocket = our_rocket.manage(jwt_secret);

        ...

        final_rocket

    }

  5. Make one struct to hold JSON data that is sent for authentication, and another struct to hold JSON data containing the token to be returned to the client. In src/models/user.rs, add the following use declaration:

    use rocket::serde::{Deserialize, Serialize};

Add the following structs:

#[derive(Deserialize)]

pub struct JWTLogin<'r> {

    pub username: &'r str,

    pub password: &'r str,

}

#[derive(Serialize)]

pub struct Auth {

    pub token: String,

}

  1. Implement a method to verify the username and password for JWTLogin. Add the impl block and method:

    impl<'r> JWTLogin<'r> {

        pub async fn authenticate(

            &self,

            connection: &mut PgConnection,

            secret: &'r str,

        ) -> Result<Auth, OurError> {}

    }

  2. Inside the authenticate() method, add the error closure:

    let auth_error =

        || OurError::new_bad_request_error(

        String::from("Cannot verify password"), None);

  3. Then, find the user according to the username and verify the password:

    let user = User::find_by_login(

        connection,

        &Login {

            username: self.username,

            password: self.password,

            authenticity_token: "",

        },

    )

    .await

    .map_err(|_| auth_error())?;

    verify_password(&Argon2::default(), &user.password_hash, self.password)?;

  4. Add the following use declaration:

    use hmac::{Hmac, Mac};

    use jwt::{SignWithKey};

    use sha2::Sha256;

    use std::collections::BTreeMap;

Continue the following inside authenticate to generate a token from the user's UUID and return the token:

let user_uuid = &user.uuid.to_string();

let key: Hmac<Sha256> =

    Hmac::new_from_slice(secret.as_bytes()

    ).map_err(|_| auth_error())?;

let mut claims = BTreeMap::new();

claims.insert("user_uuid", user_uuid);

let token = claims.sign_with_key(&key).map_err(|_| auth_error())?;

Ok(Auth {

    token: token.as_str().to_string(),

})

  1. Create a function to authenticate. Let's call this function login(). In src/routes/api.rs, add the required use declaration:

    use crate::models::user::{Auth, JWTLogin, User, UsersWrapper};

    use crate::states::JWToken;

    use rocket::State;

    use rocket_db_pools::{sqlx::Acquire, Connection};

  2. Then, add the login() function as follows:

    #[post("/login", format = "json", data = "<jwt_login>")]

    pub async fn login<'r>(

        mut db: Connection<DBConnection>,

        jwt_login: Option<Json<JWTLogin<'r>>>,

        jwt_secret: &State<JWToken>,

    ) -> Result<Json<Auth>, Json<OurError>> {

        let connection = db

            .acquire()

            .await

            .map_err(|_| OurError::new_internal_server_

            error(String::from("Cannot login"), None))?;

        let parsed_jwt_login = jwt_login

            .map(|p| p.into_inner())

            .ok_or_else(|| OurError::new_bad_request_

            error(String::from("Cannot login"), None))?;

        Ok(Json(

            parsed_jwt_login

                .authenticate(connection, &jwt_secret

                .secret)

                .await

                .map_err(|_| OurError::new_internal_

                server_error(String::from("Cannot login"),

                None))?,

        ))

    }

  3. Now that we have created login functionality, the next action is to create a request guard that handles the authorization token in the request header. In src/guards/auth.rs, add the following use declarations:

    use crate::states::JWToken;

    use hmac::{Hmac, Mac};

    use jwt::{Header, Token, VerifyWithKey};

    use sha2::Sha256;

    use std::collections::BTreeMap;

  4. Add a new struct for a request guard called APIUser:

    pub struct APIUser {

        pub user: User,

    }

  5. Implement FromRequest for APIUser. Add the following block:

    #[rocket::async_trait]

    impl<'r> FromRequest<'r> for APIUser {

        type Error = ();

        async fn from_request(req: &'r Request<'_>) ->

        Outcome<Self, Self::Error> {}

    }

  6. Inside from_request(), add the closure to return an error:

    let error = || Outcome::Failure ((Status::Unauthorized, ()));

  7. Get the token from the request header:

    let parsed_header = req.headers().get_one("Authorization");

    if parsed_header.is_none() {

        return error();

    }

    let token_str = parsed_header.unwrap();

  8. Get the secret from state:

    let parsed_secret = req.rocket().state::<JWToken>();

    if parsed_secret.is_none() {

        return error();

    }

    let secret = &parsed_secret.unwrap().secret;

  9. Verify the token and get the user's UUID:

    let parsed_key: Result<Hmac<Sha256>, _> = Hmac::new_from_slice(secret.as_bytes());

    if parsed_key.is_err() {

        return error();

    }

    let key = parsed_key.unwrap();

    let parsed_token: Result<Token<Header, BTreeMap<String, String>, _>, _> = token_str.verify_with_key(&key);

    if parsed_token.is_err() {

        return error();

    }

    let token = parsed_token.unwrap();

    let claims = token.claims();

    let parsed_user_uuid = claims.get("user_uuid");

    if parsed_user_uuid.is_none() {

        return error();

    }

    let user_uuid = parsed_user_uuid.unwrap();

  10. Find the user and return the user data:

    let parsed_db = req.guard::<Connection<DBConnection>>().await;

    if !parsed_db.is_success() {

        return error();

    }

    let mut db = parsed_db.unwrap();

    let parsed_connection = db.acquire().await;

    if parsed_connection.is_err() {

        return error();

    }

    let connection = parsed_connection.unwrap();

    let found_user = User::find(connection, &user_uuid).await;

    if found_user.is_err() {

        return error();

    }

    let user = found_user.unwrap();

    Outcome::Success(APIUser { user })

  11. Finally, add a new protected API endpoint in src/routes/api.rs:

    use crate::guards::auth::APIUser;

    ...

    #[get("/protected_users", format = "json", data = "<pagination>")]

    pub async fn authenticated_users(

        db: Connection<DBConnection>,

        pagination: Option<Json<Pagination>>,

        _authorized_user: APIUser,

    ) -> Result<Json<UsersWrapper>, Json<OurError>> {

        users(db, pagination).await

    }

  12. In src/main.rs, add the routes to Rocket:

    ...

    .mount("/api", routes![api::users, api::login,

    api::authenticated_users])

    ...

Now, try accessing the new endpoint. Here is an example when using the cURL command line:

curl -X GET -H "Content-Type: application/json"

http://127.0.0.1:8000/api/protected_users

The response will be an error. Now try sending a request to get the access token. Here is an example:

curl -X POST -H "Content-Type: application/json"

  -d "{"username":"example", "password": "password"}"

http://127.0.0.1:8000/api/login

There's a token returned, as shown in this example:

{"token":"eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX3V1aWQiOiJmMGMyZDM4Yy0zNjQ5LTRkOWQtYWQ4My0wZGE4ZmZlY2 E2MDgifQ.XJIaKlIfrBEUw_Ho2HTxd7hQkowTzHkx2q_xKy8HMKA"}

Use the token to send the request, as in this example:

curl -X GET -H "Content-Type: application/json" T -H "Content-Type: application/json"

-H "Authorization: eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX3V1aWQiOiJmMGMyZDM4Yy0zNjQ5LTRkOWQtYWQ4My0wZGE4ZmZlY2 E2MDgifQ.XJIaKlIfrBEUw_Ho2HTxd7hQkowTzHkx2q_xKy8HMKA"

http://127.0.0.1:8000/api/protected_users

Then, the correct response will be returned. JWT is a good way to protect API endpoints, so use the technique that we have learned when necessary.

Summary

In this chapter, we learned about authenticating users and then creating a cookie to store logged-in user information. We also introduced CurrentUser as a request guard that works as authorization for certain parts of the application.

After creating authentication and authorization systems, we also learned about API endpoints. We parsed the incoming request body as a request guard in an API and then created an API response.

Finally, we learned a little bit about the JWT and how to use it to protect API endpoints.

In the next chapter, we are going to learn how to test the code that we have created.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.23.130.108