Basic crud with rust using tide - Adding OAuth
Tags: rust, tide, oauth
Published: 2021-07-19 10:24:06

Hi there! welcome back, in the last [post](https://javierviola.com/post/06-basic-crud-with-rust-using-tide-final-refactor-and-complete-ci-cd/) of this serie we made a `final` refactor and completed the process of `ci/cd` but I found a couple of `bonus` to add that could be useful. In this first bonus post I will be add `OAuth` to our  app to allow users to login and manage their `dinos`.

---

Until now we are not tracking the user that create a `dino`, so everyone can edit or delete `dinos` despite of if they created or not. Now we can introduce the `user` concept and those `dinos` created by an user can only be updated/deleted by that user, we will still supporting create `dinos` without been logged and those will have the same behavior as they have now.

So, our roadmap for this note will be :

- Implement OAuth, so the visitors can authenticate with a provider ( e.g google, github, etc )

- Track the user that create the `dino`, if we have one.

- Add an authorization mechanism for the `update` and `delete` operations.


### Implement OAuth

We will be using `google` as OAuth provider, so before start we need to get your credentials ( here is the [Setting up OAuth 2.0](https://support.google.com/googleapi/answer/6158849) documentation ).

Great, now that we had the credentials we can continue with the implementation. We will using this two examples as `inspiration` for our implementation [tide-tera-example](https://github.com/jmn/tide-tera) and [tide-google-oauth-example](https://github.com/pbzweihander/tide-google-oauth-example).

> Also, @jmn was the one to give me the idea of adding this OAuth bonus to this serie, thanks 🙌 🙌!

Let's started, we need to add the `deps` we will be using, first we will add `oauth2` but we will try the last `alpha` version and with `reqwest` to use with `async/await`.

```toml
oauth2 = { version = "4.0.0-alpha.3", features = ["reqwest"], default-features = false  }
```

Move on and create the basic scaffolding, first we need to create the `auth.rs` file inside the `controllers` directory and put the basic skeleton

```rust
// controllers/auth.rs
use super::*;

use tide::{Request, Result};

pub async fn auth_google(req: Request<State>) -> Result {
    unimplemented!();
}

pub async fn auth_google_authorized(req: Request<State>) -> Result {
    unimplemented!();
}

pub async fn logout(req: Request<State>) -> Result {
    unimplemented!();
}
```

So, we will be handle three routes:

- `/auth/google`  will start the process and redirect the user to authorize our app.

- `/auth/google/authorized`   will handle the callback from the provider ( google in this case ), get the `user info` and create the `session`.

- `/logout`  will destroy the session.


Wait! `session you say`... Yes, we will use cookie based sessions in this example, so we need to add the `Session` middleware to our server.

```rust
// main.rs

async fn server(db_pool: PgPool) -> Server<State> {
 (...)
    app.with(tide::sessions::SessionMiddleware::new(
        tide::sessions::MemoryStore::new(),
        std::env::var("TIDE_SECRET")
            .expect("Please provide a TIDE_SECRET value of at least 32 bytes")
            .as_bytes(),
    ));
```

To keep things simple we will use the builtin `MemoryStore` but it's `recommended` to use one of the [others](https://github.com/http-rs/tide#session-stores) stores in `production`.

Great! let's add also the routes in our `main.rs`

```rust

    // auth
    app.at("/auth/google")
        .get(auth::login)
        .at( "/authorized").get(auth::login_authorized);

    app.at("/logout").get(auth::logout);
```

> Nice! so, now we had a bunch of `env vars` to add, remember to add those to our `.env` file.

We had all the structure in place, now we need to actually implement the OAuth client, let's add a `helper` function to create the `basic client`

```rust
fn make_oauth_google_client() -> tide::Result<BasicClient> {
    let client  =  BasicClient::new(
    ClientId::new(std::env::var("OAUTH_GOOGLE_CLIENT_ID").expect("missing env var OAUTH_GOOGLE_CLIENT_ID")),
Some(ClientSecret::new(std::env::var("OAUTH_GOOGLE_CLIENT_SECRET").expect("missing env var OAUTH_GOOGLE_CLIENT_SECRET"))),
    AuthUrl::new(AUTH_URL.to_string())?,Some(TokenUrl::new(TOKEN_URL.to_string())?),
    )
    .set_redirect_url(RedirectUrl::new(std::env::var("OAUTH_GOOGLE_REDIRECT_URL").expect("missing env var OAUTH_GOOGLE_REDIRECT_URL"))?);

    Ok(client)
}
```

and store in our `State`

```rust
#[derive(Clone, Debug)]
pub struct State {
    db_pool: PgPool,
    tera: Tera,
    oauth_google_client: BasicClient
}

(...)

    let oauth_google_client = make_oauth_google_client().unwrap();

    let state = State { db_pool, tera, oauth_google_client };

    let mut app = tide::with_state(state);
```

Nice! we can move on and work in the `auth` logic.

First, we need to handle the `get` request that start the OAuth flow in our `auth_google` fn. We are getting the `client` from the state and adding the `scope`s we want to use. With the goal of keep the things simple we will only request the `profile` since we will use the `firstName` and the `id` only.

> The rest of the code have the `email` commented in case you also want to use.

```rust
pub async fn auth_google(req: Request<State>) -> Result {
    let client  = &req.state().oauth_google_client;
    let (auth_url, _csrf_token) = client
    .authorize_url(CsrfToken::new_random)
    // Set the desired scopes.
    // .add_scope(Scope::new(AUTH_GOOGLE_SCOPE_EMAIL.to_string()))
    .add_scope(Scope::new(AUTH_GOOGLE_SCOPE_PROFILE.to_string()))
    .url();

    Ok(Redirect::see_other(auth_url).into())
}
```

The last line `redirect` the user to the `google` page to ask the user if grant the request permissions/data to our app.

![image](https://user-images.githubusercontent.com/363911/105925896-abf2c980-601f-11eb-9ff8-8de249f7ed7b.png)


After accept the user will be redirected  to our `callback` fn, so now let's complete the flow to get the user info.

```rust
#[derive(Debug, Deserialize)]
struct AuthRequestQuery {
    code: String,
    state: String,
    scope: String,
}

#[derive(Debug, Deserialize)]
struct UserInfoResponse {
    // email: String,
    id: String,
    given_name: String
}
(...)

pub async fn auth_google_authorized(mut req: Request<State>) -> Result {
    let client  = &req.state().oauth_google_client;
    let query: AuthRequestQuery = req.query()?;
    let token_result = client
        .exchange_code(AuthorizationCode::new(query.code))
        .request_async(async_http_client)
        .await;

    let token_result = match token_result {
        Ok(token) => token,
        Err(_) => return Err(tide::Error::from_str(401, "error"))
    };

    let userinfo: UserInfoResponse = surf::get("https://www.googleapis.com/oauth2/v2/userinfo")
    .header(
        http::headers::AUTHORIZATION,
        format!("Bearer {}", token_result.access_token().secret()),
    )
    .recv_json()
    .await?;

    let session = req.session_mut();
    session.insert("user_name", userinfo.given_name)?;
    session.insert("user_id", userinfo.id)?;

    Ok(Redirect::new("/").into())
```

Again, we are first getting the `client` from the store to `exchange` the auth code that we extract from the query string to the `AuthRequestQuery` struct.
Then, with the code we request the `userInfo` and deserialize the response into the `UserInfoResponse` struct. In the last lines we are storing the `user_id` and `user_name` into the session and redirect the `logged in` user to the home again.

So, let's add the link to `log in` and test this flow...

![image](https://user-images.githubusercontent.com/363911/105931408-e5c8cd80-6029-11eb-8eb5-e659059e9e7e.png)

> Whoops... the flow works but the browser is not sending the right cookie.

After checking this issue, I found this [so answer](https://stackoverflow.com/questions/40781534/chrome-doesnt-send-cookies-after-redirect) that point me to check the `SameSite` policy.  And set the policy to `Lax` make the flow works :-)

```rust
    app.with(tide::sessions::SessionMiddleware::new(
        tide::sessions::MemoryStore::new(),
        std::env::var("TIDE_SECRET")
            .expect("Please provide a TIDE_SECRET value of at least 32 bytes")
            .as_bytes(),
        ).with_same_site_policy(SameSite::Lax)
    );
```

![image](https://user-images.githubusercontent.com/363911/105932738-1f023d00-602c-11eb-9ada-01cee1f63f82.png)


Awesome!!! We just need to implement the `logout` and we will finish the first item of our goals.

```rust
pub async fn logout(mut req: Request<State>) -> Result {
    let session = req.session_mut();
    session.destroy();

    Ok(Redirect::new("/").into())
}
```

Nice, this will handle the `logout` and redirect the user to home again.


#### Save the user, if there is one

The next item in our list is to track the user in the database, for this task we need another field in our db that could be `null` or an `string` that represente the `user_id`.

After adding the field in our `dinos` table, we need to update our `Dino` struct.

```rust
[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Dino {
    id: Uuid,
    name: String,
    weight: i32,
    diet: String,
    user_id: Option<String>
}
```

so, `user_id` is an `Option` that can hold an `String`.  Now we need to add an extra logic to add the `user_id` in our `controller`


```rust
pub async fn create(mut req: Request<State>) -> tide::Result {
    let mut dino: Dino = req.body_json().await?;
    let db_pool = req.state().db_pool.clone();

    let session = req.session();
    match session.get("user_id") {
        Some(id) => dino.user_id = Some(id),
        None => dino.user_id = None
    };

    let row = handlers::dino::create(dino, &db_pool).await?;

    let mut res = Response::new(201);
    res.set_body(Body::from_json(&row)?);
    Ok(res)
}
```

And update the `handler`fns to use the new field

```rust
pub async fn create(dino: Dino, db_pool: &PgPool) -> tide::Result<Dino> {
    let row: Dino = query_as!(
        Dino,
        r#"
        INSERT INTO dinos (id, name, weight, diet, user_id) VALUES
        ($1, $2, $3, $4, $5) returning id as "id!", name, weight, diet, user_id
        "#,
        dino.id,
        dino.name,
        dino.weight,
        dino.diet,
        dino.user_id
    )
    .fetch_one(db_pool)
    .await
    .map_err(|e| Error::new(409, e))?;

    Ok(row)
}

(...)
```

> Now you can login, create a new `dino` and see that the new row has the `user_id` set. Also, if you create a new `dino` without been logged in this column should be `null`.

Nice! second goal done, we are tracking the `id` of the users when they create a new `dino`.

#### Authorize update and delete operations

The last item in our list is restrict the update and delete operations. If a `dino` was created by a logged in user **only** this user is authorized to `delete` or `edit` this `dino`. We will add this `auth` rule in two different places.

First in the `ui`, the edit and delete options should only be available for *public* `dinos` or the ones that the user created.

We can archive this with some logic in the `tera` template

```jinja
                {% if dino.user_id %}
                    {%if user_id != "" and dino.user_id == user_id %}
                        <td><a href="/dinos/{{dino.id}}/edit"> Edit </a></td>
                        <td><a class="delete" data-id="{{dino.id}}"href="#"> Delete </a></td>
                    {% else %}
                        <td></td>
                        <td></td>
                    {% endif %}
                {% else %}
                    <td><a href="/dinos/{{dino.id}}/edit"> Edit </a></td>
                    <td><a class="delete" data-id="{{dino.id}}"href="#"> Delete </a></td>
                {% endif %}
```

![image](https://user-images.githubusercontent.com/363911/105977745-7f1ad280-6070-11eb-9c8f-c740afdeca6b.png)

Nice!! we are half way, we just restrict the `ui` but we also need to check this restriction in the controller.

```rust
//controllers/dino.rs

(...)
    // auth operation
    let session = req.session();
    let user_id: String = session.get("user_id").unwrap_or("".to_string());
    let row = handlers::dino::get(id, &db_pool).await?;
    if let Some(dino) = row {
        if dino.user_id.is_some() && dino.user_id.unwrap() != user_id {
            // 401
            return Ok(Response::new(401));
        }
    }
(...)
```

We are checking that the `user_id` ( if is set ) is the same of the session, and if not we just return a `
401 Unauthorized`.

Nice! we have to add some tests now to check if this works as expected

```rust
    #[async_std::test]
    async fn updatet_dino_create_by_another_user_should_reject_with_401() -> tide::Result<()> {
        dotenv::dotenv().ok();

        let mut dino = Dino {
            id: Uuid::new_v4(),
            name: String::from("test_update"),
            weight: 500,
            diet: String::from("carnivorous"),
            user_id: Some(String::from("123"))
        };

        let db_pool = make_db_pool(&DB_URL).await;

        // create the dino for update
        query!(
            r#"
            INSERT INTO dinos (id, name, weight, diet, user_id) VALUES
            ($1, $2, $3, $4, $5) returning id
            "#,
            dino.id,
            dino.name,
            dino.weight,
            dino.diet,
            dino.user_id
        )
        .fetch_one(&db_pool)
        .await?;

        // change the dino
        dino.name = String::from("updated from test");

        // start the server
        let app = server(db_pool).await;

        let res = surf::Client::with_http_client(app)
            .put(format!("https://example.com/dinos/{}", &dino.id))
            .body(serde_json::to_string(&dino)?)
            .await?;

        assert_eq!(401, res.status());

        Ok(())
    }

    #[async_std::test]
    async fn delete_dino_create_by_another_user_should_reject_with_401() -> tide::Result<()> {
        dotenv::dotenv().ok();

        let dino = Dino {
            id: Uuid::new_v4(),
            name: String::from("test_delete"),
            weight: 500,
            diet: String::from("carnivorous"),
            user_id: Some(String::from("123"))
        };

        let db_pool = make_db_pool(&DB_URL).await;

        // create the dino for delete
        query!(
            r#"
            INSERT INTO dinos (id, name, weight, diet, user_id) VALUES
            ($1, $2, $3, $4, $5) returning id
            "#,
            dino.id,
            dino.name,
            dino.weight,
            dino.diet,
            dino.user_id
        )
        .fetch_one(&db_pool)
        .await?;

        // start the server
        let app = server(db_pool).await;

        let res = surf::Client::with_http_client(app)
            .delete(format!("https://example.com/dinos/{}", &dino.id))
            .await?;

        assert_eq!(401, res.status());

        Ok(())
    }
```

Nice, now we can run those test :)

```bash
cargo test
    Finished test [unoptimized + debuginfo] target(s) in 0.31s
     Running target/debug/deps/tide_basic_crud-b16dbe7e8764d4d3

running 12 tests
test tests::clear ... ok
test tests::create_dino_with_existing_key ... ok
test tests::delete_dino_create_by_another_user_should_reject_with_401 ... ok
test tests::delete_dino ... ok
test tests::create_dino ... ok
test tests::get_dino_non_existing_key ... ok
test tests::get_dino ... ok
test tests::delete_dino_non_existing_key ... ok
test tests::list_dinos ... ok
test tests::updatet_dino_create_by_another_user_should_reject_with_401 ... ok
test tests::update_dino ... ok
test tests::updatet_dino_non_existing_key ... ok

test result: ok. 12 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
```

Awesome!! we just finished the third item in our list.

---
That's all for today... first bonus of this serie, we just implemented OAuth ( with google as provider ) and now users can track his `dinos` :-), you can check the complete code in the [PR](https://github.com/pepoviola/tide-basic-crud/pull/7).
I think there are still room to improve ( like adding a mechanism to authorize api calls ) but we can leave for the next `bonus post`.


As always, I write this as a learning journal and there could be another more elegant and correct way to do it and any feedback is welcome.

Thanks!