Rust with axum

Cover Page

Back-end Page

Change to your chatterd folder and edit Cargo.toml:

server$ cd ~/reactive/chatterd
server$ vi Cargo.toml

and add the following dependencies:

# . . .
hex = "0.4.3"
openidconnect = "4.0.1"
rustls = "0.23.35"
sha2 = "0.11.0-rc.3"

Save and exit Cargo.toml.

handlers.rs

Edit handlers.rs:

server$ vi src/handlers.rs

Add the following additional crates/modules at the top of your src/handlers.rs:

use openidconnect::{
    core::{CoreClient, CoreIdToken},
    reqwest::Client,
    AuthUrl, ClientId, EndUserName, IdToken, IssuerUrl, JsonWebKeySet, JsonWebKeySetUrl, LocalizedClaim, Nonce,
};
use sha2::{digest::Update, Digest, Sha256};

then add the following crates/modules in the use std::{} block:

use std::{
    // . . .
    cmp::min,
    str::FromStr,
};

and Utc to your chrono block:

use chrono::{ DateTime, Local, Utc };

Then add the following two new structs:

#[derive(Debug, Deserialize)]
pub struct Chatter {
    clientID: String,
    idToken: String,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct AuthChatt {
    chatterID: String,
    message: String,
}

Now add the adduser() function:

pub async fn adduser(
    State(appState): State<AppState>,
    ConnectInfo(clientIP): ConnectInfo<SocketAddr>,
    Json(chatter): Json<Chatter>,
) -> Result<Json<Value>, (StatusCode, String)> {
    let client = CoreClient::new(
        ClientId::new(chatter.clientID),
        IssuerUrl::new("https://accounts.google.com".to_string()).unwrap(),
        JsonWebKeySet::fetch_async(
            &JsonWebKeySetUrl::new("https://www.googleapis.com/oauth2/v3/certs".to_string())
                .unwrap(),
            &Client::new(), // cannot refuse redirection!
        )
        .await
        .unwrap(),
    )
    .set_auth_uri(
        AuthUrl::new("https://accounts.google.com/o/oauth2/v2/auth".to_string()).unwrap(),
    );

    let idToken: CoreIdToken = IdToken::from_str(&chatter.idToken).unwrap();

    let idInfo = idToken
        .claims(
            &client.id_token_verifier().allow_any_alg(),
            |_: Option<&Nonce>| Ok(()),
        )
        .map_err(|err| {
            logClientErr(
                &clientIP,
                StatusCode::UNAUTHORIZED,
                err.to_string(),
            )
        })?;

    let profileNA = EndUserName::new("Profile NA".to_string());
    let claimNA = LocalizedClaim::new();
    let username = idInfo
        .name()
        .unwrap_or(&claimNA)
        .get(None)
        .unwrap_or(&profileNA)
        .as_str();

    // compute chatterID
    
}

The function adduser() first receives a POST request containing a clientID and idToken from the front end. It uses Google’s idtoken package to verify the user’s idToken, passing along the clientID as required by Google. The verification process checks that idToken hasn’t expired and is valid. If the token is invalid or has expired, a 401, “Unauthorized” HTTP error is returned to the front end. If idToken is verified, the user’s name registered with the idToken is returned to the front end.

Next, the function computes a chatterID for the new user. The chatterID is computed as a SHA256 one-way hash of the idToken, a server’s secret, and the current time stamp. The function also assigns a lifetime to the chatterID. The lifetime is set to be no more than the remaining lifetime of the idToken, so always less than the total expected lifetime of the idToken. During the lifetime of a chatterID, the user does not need to check the freshness of their idToken with Google. Replace // compute chatterID with:

    // Compute chatterID
    let now = Utc::now().timestamp();
    let hash = Sha256::new()
        .chain(chatter.idToken + "ifyougiveamouse" + &now.to_string())
        .finalize();
    let chatterID = hex::encode(hash);

    let exp = idInfo.expiration().timestamp();
    let lifetime = min((exp - now) + 1, 300);   // secs, max 1800, idToken lifetime

    // add to database
    

During testing, setting lifetime to 1 minute allows faster triggering of the various use cases. Longer lifetime leads to less frequent prompting for user to sign in again, but also leaves open a larger window of vulnerability.

The chatterID, the user’s registered name obtained from the idToken, and the chatterID’s lifetime are then entered into the chatters table. At the same time, we take this oppotunity to do some house keeping to remove all expired chatterIDs from the database. Replace // add to database with:

    let chatterDB = appState
        .pgpool
        .get()
        .await
        .map_err(|err| logServerErr(&clientIP, err.to_string()))?;

    chatterDB
        .execute("DELETE FROM chatters WHERE $1 > expiration", &[&now])
        .await
        .map_err(|err| logServerErr(&clientIP, err.to_string()))?;

    chatterDB
        .execute(
            "INSERT INTO chatters (chatterid, username, expiration) VALUES ($1, $2, $3)",
            &[&chatterID, &username, &(now + lifetime)],
        )
        .await
        .map_err(|err| logClientErr(&clientIP, StatusCode::NOT_ACCEPTABLE, err.to_string()))?;

    logOk(&clientIP);
    Ok(Json(
        json!({ "username": username, "chatterID": chatterID, "lifetime": lifetime }),
    ))

The registered username, the newly created chatterID and its lifetime are returned to the user as a JSON object.

postauth()

We now add postauth(), which is a modified postchatt(), to your handlers.rs. To post a chatt, the front end sends a POST request containing the user’s chatterID and message. The function postauth() retrieves the record matching chatterID from the chatters table. If chatterID is not found in the chatters table, or if the chatterID has expired, it returns a 401, “Unauthorized” HTTPerror. Otherwise, the registered username corresponding to chatterID is retrieved from the table. Note: chatterIDs are unique in the chatters table.

pub async fn postauth(
    State(appState): State<AppState>,
    ConnectInfo(clientIP): ConnectInfo<SocketAddr>,
    Json(chatt): Json<AuthChatt>,
) -> Result<Json<Value>, (StatusCode, String)> {
    let chatterDB = appState
        .pgpool
        .get()
        .await
        .map_err(|err| logServerErr(&clientIP, err.to_string()))?;

    let row = chatterDB
        .query_one(
            "SELECT username, expiration FROM chatters WHERE chatterID = $1",
            &[&chatt.chatterID],
        )
        .await
        .map_err(|err| logServerErr(&clientIP, err.to_string()))?;

    let username: String = row.get("username");
    let exp: i64 = row.get("expiration");
    let now = Utc::now().timestamp();
    if now > exp {
        Err(logClientErr(
            &clientIP,
            StatusCode::UNAUTHORIZED,
            "Unauthorized".to_string(),
        ))
    } else {
       	// insert chatt
        
    }
}

Insert the chatt into the chatts table under the retrieved username. Replace // insert chatt with:

        chatterDB
            .execute(
                "INSERT INTO chatts (name, message, id) VALUES ($1, $2, gen_random_uuid())",
                &[&username, &chatt.message],
            )
            .await
            .map_err(|err| logClientErr(&clientIP, StatusCode::NOT_ACCEPTABLE, err.to_string()))?;

        logOk(&clientIP);
        Ok(Json(json!({})))

We will be using the original getchatts() from the chatter lab without modification.

We’re done with handlers.rs. Save and exit the file.

main.rs

Edit the file main.rs:

server$ vi src/main.rs

Add the following additional imported crate/module at the top of your src/main.rs:

use rustls::crypto::aws_lc_rs;

Find the router = Router::new() instantiation statement and add the following new routes for the new API endpoints /adduser and /postauth, before the .layer(/*...*/) line:

        .route("/adduser", post(handlers::adduser))
        .route("/postauth", post(handlers::postauth))

Between the router and certkey assignment, add the following to create a “Rustls Crypto Provider,” that OpenIDConnect relies on, using the AWS’ libcrypto library:

    aws_lc_rs::default_provider()
        .install_default()
        .map_err(|err| { eprintln!("{:?}", err); process::exit(1) })
        .unwrap();

We’re done with main.rs. Save and exit the file.

Build and test run

To build your server, first install pkg-config so that cargo can find our installation of openssl.

server$ sudo apt install pkg-config
server$ cargo build --release

:point_right:Rust is a compiled language, like C/C++ and unlike Python, which is an interpreted language. This means you must run cargo build each and every time you made changes to your code, for the changes to show up in your executable.

To run your server:

server$ sudo ./chatterd
# Hit ^C to end the test

The cover back-end spec provides instructions for Testing Signin.

References


Prepared by Benjamin Brengman, Ollie Elmgren, Wendan Jiang, Alexander Wu, and Sugih Jamin Last updated March 13th, 2026