Rust Web App Session Management with AWS
Tags: rust, beginners, aws, webapp
Published: 2019-08-08 13:31:46

---
title: Rust Web App Session Management with AWS
published: true
tags: Rust,beginners,AWS,webapp
canonical_url: https://rust.graystorm.com/2019/08/04/rust-web-app-session-management-with-aws/
cover_image: https://rust.graystorm.com/wp-content/uploads/2019/06/plant-wood-old-rust-equipment-gear-1278587-pxhere.com_.jpg
series: Designing a Full-Featured WebApp with Rust
---

![Printed security pass - an old school session management practice
](https://i1.wp.com/rust.graystorm.com/wp-content/uploads/2019/08/writing-number-sign-key-door-security-1095191-pxhere.com_.jpg?resize=840%2C560&ssl=1)<figcaption>Didn’t someone mention a cookie too!? Where’s my cookie!?</figcaption>

I’ve worked on some more bits with my pretend web application I introduced a [few posts ago](https://rust.graystorm.com/2019/07/13/piecing-together-a-rust-web-application/). Thinking about the technology needed to let users register and later log in, [session management](https://en.wikipedia.org/wiki/Session_(computer_science)) seems like a good first step to take. I would like to store the sessions in Amazon’s [DynamoDB](https://aws.amazon.com/dynamodb/) too, just for fun and experience. I’ve pushed several commits to the [repository](https://github.com/jculverhouse/pinpoint_shooting) lately, so let me go through some of the changes enabling Rust web app session management with AWS DynamoDB.

First, I searched and read up a bit on best practices for session management. I found a [Session Management Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html) from [OWASP](https://www.owasp.org/index.php/Main_Page). Also, more generically, [12 Best Practices for User Account, Authorization, and Password Management](https://cloud.google.com/blog/products/gcp/12-best-practices-for-user-account) on the [Google Cloud Blog](https://cloud.google.com/blog/). I haven’t addressed many of these yet, but I’ll come back to resources like this several times in the future. I was trying to find language-independent whitepapers or guides on web app session management… and something not a decade old. If anyone has a definitive resource for this, please share!

## Rusoto to the Rescue

First, let’s get our web application interfacing with AWS. [Rusoto](https://www.rusoto.org/) is a big set of [crates](https://crates.io/) for using AWS services. Rusoto [Core](https://crates.io/crates/rusoto_core) (of course) and [DynamoDB](https://crates.io/crates/rusoto_dynamodb) are all I need for now, but I am sure I’ll come back for more! To get set up, I created a `sessions` table in my [AWS](https://aws.amazon.com/) console and created a programmatic [IAM role](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles.html) called `pinpointshooting` which has full access to the table. When creating a programmatic role, you are given [credential access keys](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html) which I put into my `.env` file at the root of the `pinpointshooting` project. First, connecting to my AWS account with those access keys is a simple matter of:

```
DynamoDbClient::new(Region::UsEast1)
```

![](https://i2.wp.com/rust.graystorm.com/wp-content/uploads/2019/08/session-cookie-life.jpg?fit=720%2C1024&ssl=1)

I had to think about the process I would use to store the `session id` in a cookie, and when/how it gets created, verified, updated, and deleted. I came up with a simple sketch to make it more clear in my mind. Note that I mention a `cache` both in my sketch and in some of the comments, but I haven’t done anything about that yet.

- When a user arrives with **no** cookie – we need to create a session with some appropriate defaults, write it to the db, and send the cookie in the resulting response.
- If the user arrives **with** a cookie – we need to pull the session from the DB (if it really exists) and do some verification and expiration checking. If everything looks good, we pull the session details into our `struct`. Otherwise, we delete that invalid session data and create a new session like above.
- When the user selects to log out – we again check to make sure the session is valid, but then delete the session, delete the cookie, and log the user out.

Here are those steps above, as I have them in code at this point!

## Step 1: Search for or Create the Session

```
// Check for sessid cookie and verify session or create new
// session to use - either way, return the session struct

pub fn get_or_setup_session(cookies: &mut Cookies) -> Session {
    let applogger = &LOGGING.logger;
    let dynamodb = connect_dynamodb();

    // if we can pull sessid from a cookie and validate it,
    // pull session from cache or from storage and return
    if let Some(cookie) = cookies.get_private("sessid") {
        debug!(applogger, "Cookie found, verifying"; "sessid" => cookie.value());
        // verify from dynamodb, update session with last-access if good
        if let Some(mut session) = 
        verify_session_in_ddb(&dynamodb, &cookie.value().to_string()) {
            save_session_to_ddb(&dynamodb, &mut session);
            return session;
        }
     }
     // otherwise, start a new, empty session to use for this user
    let mut hasher = Sha256::new();
    let randstr: String = thread_rng()
        .sample_iter(&Alphanumeric)
        .take(256)
        .collect();
    hasher.input(randstr);
    let sessid = format!("{:x}", hasher.result());
    cookies.add_private(Cookie::new("sessid", sessid.clone()));
    let mut session = Session {
        sessid: sessid.clone(),
        ..Default::default(),
    };
    save_session_to_ddb(&dynamodb, &mut session);
    session
}
```

This will check the (private) cookie `sessid`, if it came along with the http request, and try to fetch it from DynamoDB and then verify it (see below). If that succeeds, we update the session (with an updated `last-access` timestamp) and return the `session struct`. Otherwise, we create a new `session id`, with some appropriate defaults for now, by getting a hash of a random string of characters. Hrm, I should make sure I didn’t just happen to intersect with an existing `sessid` at this point! Anyway, we then just add (or update) the `sessid` cookie to be returned with the http response. Since we are using the [private cookie](https://api.rocket.rs/v0.4/rocket/http/enum.Cookies.html#private-cookies) feature of [Rocket](https://rocket.rs/), the value is actually encrypted. The user can’t see what their real `session id` is or try to manufacture one. Lastly, if we just created a new session, we save it to the session table and return it.

## Step 2: Verify a Session

```
// Search for sessid in dynamodb and verify session if found
// including to see if it has expired
fn verify_session_in_ddb(dynamodb: &DynamoDbClient, sessid: &String) -> Option<Session> {
    let applogger = &LOGGING.logger;
    let av = AttributeValue {
        s: Some(sessid.clone()),
        ..Default::default(),
    };
    let mut key = HashMap::new();
    key.insert("sessid".to_string(), av);
    let get_item_input = GetItemInput {
        table_name: "session".to_string(),
        key: key,
        ..Default::default()
    };
    match dynamodb.get_item(get_item_input).sync() {
        Ok(item_output) => match item_output.item {
            Some(item) => match item.get("session") {
                Some(session) => match &session.s {
                    Some(string) => {
                        let session: Session = serde_json::from_str(&string)
                            .unwrap();
                        match session.last_access {
                            Some(last) => {
                                if last > Utc::now() - 
                                Duration::minutes(CONFIG.sessions.expire) {
                                    Some(session)
                                } else {
                                    debug!(applogger, "Session expired";
                                        "sessid" =\> sessid); 
                                    delete_session_in_ddb(dynamodb, sessid);
                                    None
                                }
                            }
                            None => {
                                debug!(applogger, "'last_access' is blank for stored session"; "sessid" => sessid);
                                delete_session_in_ddb(dynamodb, sessid);
                                None
                            }
                        }
                    }
                    None => {
                        debug!(applogger, "'session' attribute is empty for stored session"; "sessid" => sessid);
                        delete_session_in_ddb(dynamodb, sessid);
                        None
                    }
                },
                None => {
                    debug!(applogger, "No 'session' attribute found for stored session"; "sessid" => sessid);
                    delete_session_in_ddb(dynamodb, sessid);
                    None
                }
            },
            None => {
                debug!(applogger, "Session not found in dynamodb"; "sessid" => sessid);
                None
            }
        },
        Err(e) => {
            crit!(applogger, "Error in dynamodb"; "err" => e.to_string());
            panic!("Error in dynamodb: {}", e.to\_string());
        }
    }
}
```

That’s some deep nesting – I’m still a newbie at `Rust` coding, so there is probably a better way to write this. If I didn’t care about logging the problems, I could probably shorten this by using the “?” operator at the end of each step. Maybe I’ll change that eventually, but for now I need to debugging logs to see that things are working.

This chain tries to retrieve the `session` attribute associated with the `sessid` (session id encoded in the cookie) and for now, just makes sure it isn’t too old. If that simple check is ok, we deserialize and return that `Some(session struct)`. In all other cases, we return `None` and a new session will be generated. For cases where a session was present, but determined to be invalid or expired, we also delete the session from DynamoDB.

## Step 3: Save (or Update) the Session

```
// Write current session to dynamodb, update last-access date/time too
fn save_session_to_ddb(dynamodb: &DynamoDbClient, session: &mut Session) {
    let applogger = &LOGGING.logger;
    session.last_access = Some(Utc::now());
    let sessid_av = AttributeValue {
        s: Some(session.sessid.clone()),
        ..Default::default()
    };
    let session_av = AttributeValue {
        s: Some(serde_json::to_string(&session).unwrap()),
        ..Default::default()
    };
    let mut item = HashMap::new();
    item.insert("sessid".to_string(), sessid_av);
    item.insert("session".to_string(), session_av);
    let put_item_input = PutItemInput {
        table_name: "session".to_string(),
        item: item,
        ..Default::default()
     };
     match dynamodb.put_item(put_item_input).sync() {
         Ok(_) => {}
         Err(e) => {
             crit!(applogger, "Error in dynamodb"; "err" => e.to_string());
             panic!("Error in dynamodb: {}", e.to_string());
         }
     };
}
```

Here, we simply take the session data we are passed, update the `last_access` to `now()` and then write the `serialized session struct` to DynamoDB. Also note, I’m storing the session id inside the session data itself. I’m not sure yet if this is handy duplication, a security concern, or irrelevant.

## Step 4: Clean-up After Ourselves

![Repairman cleaning up his work, but did anyone check his security badge!?](https://i1.wp.com/rust.graystorm.com/wp-content/uploads/2019/08/work-man-person-repair-profession-dad-713854-pxhere.com_.jpg?resize=512%2C342&ssl=1)<figcaption>Hey! Are you a security concern??</figcaption>

Just for completeness, here is the function to drop a session from DynamoDB once we have determined it is invalid (expired).

```
// Delete session from dynamodb
fn delete_session_in_ddb(dynamodb: &DynamoDbClient, sessid: &String) {
    let applogger = &LOGGING.logger;
    let av = AttributeValue {
        s: Some(sessid.clone()),
        ..Default::default()
    };
    let mut key = HashMap::new();
    key.insert("sessid".to_string(), av);
    let delete_item_input = DeleteItemInput {
        table_name: "session".to_string(),
        key: key,
        ..Default::default()
    };
    match dynamodb.delete_item(delete_item_input).sync() {
        Ok(_) => {
            debug!(applogger, "Deleted invalid session from ddb";
                "sessid" => sessid);
        }
        Err(e) => {
            crit!(applogger, "Error in dynamodb"; "err" => e.to_string());
            panic!("Error in dynamodb: {}", e.to_string());
        }
    };
}
```

Lots, lots, lots more to do – but I’m having fun with this. It might go faster if I actually had a plan for what this web app actually does. I mean, I have an idea, but I haven’t mocked up a single page so I’m going slow and just enjoying hacking out `Rust` code as I go! Thanks for coming along with me!

The post [Rust Web App Session Management with AWS](https://rust.graystorm.com/2019/08/04/rust-web-app-session-management-with-aws/) appeared first on [Learning Rust](https://rust.graystorm.com).