Sign-in Back End

Cover Page

DUE Wed, 02/26, 2 pm

Do NOT start work on your back end until you have worked on the front-end spec to the point where you can obtain an ID Token from Google. You’ll need your ID Token to test your back end.

We extend our Chatter back end to (1) receive a Google ID Token from the front end for authentication and (2) to verify users when they post chatts.

Install updates

Remember to install updates available to your Ubuntu back end. If N in the following notice you see when you ssh to your back-end server is not 0,

N updates can be applied immediately.

run the following:

server$ sudo apt update
server$ sudo apt upgrade

Failure to update your packages could lead to the lab back end not performing correctly and also make you vulnerable to security hacks.

If you see *** System restart required *** when you ssh to your server, please run:

server$ sync
server$ sudo reboot

Your ssh session will be ended at the server. Wait a few minutes for the system to reboot before you ssh to your server again.

Modified Chatter API

We’ll add an adduser API and modify the chatt posting API for the new sign-in related functionalities.

adduser API

When a user signs in and submits their ID Token from Google, adduser will receive the token, make sure it hasn’t expired, generate a new chatterID, store it in the database along with the user’s username (obtained from the ID Token) and the chatterID’s expiration time. The adduser API will then return this chatterID, along with its lifetime, to the user.

API:

/adduser/
<- clientID, idToken
-> chatterID, lifetime 200 OK

The data format adduser expects is:

{
    "clientID": "YOUR_APP'S_CLIENT_ID",
    "idToken": "YOUR_GOOGLE_ID_TOKEN"
}

where YOUR_APP'S_CLIENT_ID and YOUR_GOOGLE_ID_TOKEN are both issued by Google to your front end.

Notice how we don’t do anything with user data upon sign out. In a real-world app, we would need to remove the user from the back end and add a button to revoke Google SignIn on the app.

Posting a chatt API

To post a chatt in this lab requires that chatterID be sent along with each chatt. The back end first verifies that the chatterID exists in the database. If the chatterID is found, the new chatt, along with the user’s username (retrieved from the database) will be added to the chatts database. If it isn’t, an error will be returned to the front end.

API for postauth:

/postauth/
<- chatterID, message
-> {} 200 OK

The data format postauth expects is:

{
    "chatterID": "YOUR_CHATTER_ID",
    "message": "Chitt chatts"
}

where YOUR_CHATTER_ID is the chatterID returned by the adduser API above.

Adding a chatters table to chatterdb

You will be using two tables in this lab: the original chatts table from previous labs and a new chatters table to keep track of authorized users. Both tables will be part of your chatterdb database.

Assuming you’re running psql and already connected to chatterdb, create a chatters table and grant PostgreSQL user chatter access to it by:

CREATE TABLE chatters (chatterid char(256) not null, username varchar(255) not null, expiration bigint not null);
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO chatter;

We will use SHA256 to compute chatterID, so it should be of fixed size, 256 bytes.

Web server

We now add new URL paths and their corresponding handlers to your back-end server.

server$ cd ~/441/chatterd

Go with Echo

Go with Echo

Editing handlers.go

Add the following libraries to the import block at the top of handlers.go:

import (
    "crypto/sha256"
    "fmt"
    "strconv"
    //...
    "github.com/jackc/pgx/v4"
    "google.golang.org/api/idtoken"
)

Next add the following two new structs:

type (
	AuthChatt struct {
		ChatterID string `json:"chatterID"`
		Message   string `json:"message"`
	}
	Chatter struct {
		ClientID string `json:"clientID"`
		IdToken  string `json:"idToken"`
	}
)

Now add the adduser() function:

func adduser(c echo.Context) error {
	var chatter Chatter

	if err := c.Bind(&chatter); err != nil {
		return logClientErr(c, http.StatusUnprocessableEntity, err)
	}

	idinfo, err := idtoken.Validate(background, chatter.IdToken, chatter.ClientID)
	if err != nil {
		return logClientErr(c, http.StatusNetworkAuthenticationRequired, err)
	}

	// get username
	username := "Profile NA"
	name := idinfo.Claims["name"]
	if name != nil {
		username = name.(string)
	}

	// Compute chatterID and add to database
	const backendSecret = "ifyougiveamouse"
	now := time.Now().Unix()
	nonce := strconv.FormatInt(now, 10)
	chatterID := fmt.Sprintf("%x", sha256.Sum256([]byte(chatter.IdToken+backendSecret+nonce)))

	exp := idinfo.Expires
	lifetime := min((exp-now)+1, 60) // secs, up to 1800, idToken lifetime

	_, err = chatterDB.Exec(background, `DELETE FROM chatters WHERE $1 > expiration`, now)
	if err != nil {
		return logServerErr(c, err)
	}

	_, err = chatterDB.Exec(background,
		`INSERT INTO chatters (chatterid, username, expiration) VALUES ($1, $2, $3)`,
		chatterID, username, now+lifetime)
	if err != nil {
		return logServerErr(c, err)
	}

	logOk(c)
	return c.JSON(http.StatusOK, map[string]any{"chatterID": chatterID, "lifetime": lifetime})
}

The function adduser() first receives a POST request containing an ID Token and Client ID from the front end. It uses Google’s idtoken package to verify the user’s ID Token, passing along the Client ID as required by Google. The verification process checks that ID Token hasn’t expired and is valid. If the token is invalid or has expired, a 511, “Network Authentication Required” HTTP error code is returned to the front end.

Next, a new chatterID is computed as a SHA256 one-way hash of the ID Token, a server’s secret, and the current time stamp. A lifetime is assigned to the chatterID. The lifetime should normally be set to be less than the total expected lifetime of the ID Token, and in any case not more than the remaining lifetime of the ID Token. The idea is that during the lifetime of chatterID, the user does not need to check the freshness of their ID Token with Google.

The chatterID, the user’s name obtained from the ID Token, and the chatterID’s lifetime are then entered into the chatters table. At the same time, we do some house keeping and remove all expired chatterIDs from the database.

Finally, the 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.go:

func postauth(c echo.Context) error {
	var chatt AuthChatt
	var err error

	if err = c.Bind(&chatt); err != nil {
		return logClientErr(c, http.StatusUnprocessableEntity, err)
	}

	var username string
	var exp int64
	now := time.Now().Unix()
	err = chatterDB.QueryRow(background, `SELECT username, expiration FROM chatters WHERE chatterID = $1`, chatt.ChatterID).Scan(&username, &exp)
	if err == pgx.ErrNoRows || now > exp {
		return logClientErr(c, http.StatusUnauthorized, err)
	} else if err != nil {
		return logServerErr(c, err)
	}

	_, err = chatterDB.Exec(background, `INSERT INTO chatts (username, message, id) VALUES ($1, $2, gen_random_uuid())`, username, chatt.Message)
	if err != nil {
		return logClientErr(c, http.StatusBadRequest, err)
	}

	logOk(c)
	return c.JSON(http.StatusOK, struct{}{})
}

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” HTTP error code. Otherwise, it retrieves the corresponding username from the table and inserts the chatt into the chatts table with that username. Note: chatterIDs are unique in the chatters table.

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

Save and exit handlers.go.

Routing for new URLs

For the newly added adduser() and postauth() functions, add the following new routes to the routes array in main.go:

var routes = []Route {
    // . . .
    {"POST", "/adduser/", adduser},
    {"POST", "/postauth/", postauth},
}

Save and exit main.go.

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

Since we added new packages, you need to run:

server$ go get

before you can rebuild and restart chatterd:

server$ go build
server$ sudo systemctl restart chatterd

References

JavaScript with Express

JavaScript with Express

To verify Google’s ID Token, first add the Google API python client as a dependency:

server$ npm install google-auth-library

This should add the following line to the dependencies block of your package.json:

    "google-auth-library": "^9.15.1",

Editing handlers.ts

Add the following import at the top of handlers.ts:

import { OAuth2Client, type TokenPayload } from 'google-auth-library'

Next add the following two new interfaces:

interface AuthChatt {
    chatterID: string
    message: string
}

interface Chatter {
    clientID: string
    idToken: string
}

Now add the following adduser() function:

export async function adduser(req: Request, res: Response) {
    let chatter: Chatter
    try {
        chatter = req.body
    } catch (error) {
        res.status(422).json(error)
        return
    }

    // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/now
    const now = Math.ceil(Date.now()/1000) // secs since epoch (1/1/70, 00:00:00 UTC)

    let idinfo: TokenPayload | undefined
    try {
        // https://developers.google.com/identity/sign-in/ios/backend-auth#node.js
        const client = new OAuth2Client() // v. 9.15.0 causes deprecation warning of `punycode` module
        //async function verify() {
        idinfo = (await client.verifyIdToken({
            idToken: chatter.idToken,
            audience: chatter.clientID,
            // Or, if multiple clients access the backend:
            //[CLIENT_ID_1, CLIENT_ID_2, CLIENT_ID_3]
        })).getPayload()
        // If the request specified a Google Workspace domain:
        // const domain = payload['hd'];
    } catch (error) {
        //verify().catch(console.error);
        res.status(511).json(`Network Authorization Required | ${error}`)
    }

    const username = idinfo?.['name'] ?? "Profile NA"

    // Compute chatterID and add to database
    const backendSecret = "allhappyfamilies"   // or server's private key
    const nonce = now.toString()
    const hashable = chatter.idToken + backendSecret + nonce
    // https://futurestud.io/tutorials/node-js-calculate-a-sha256-hash
    const chatterID = createHash('sha256').update(hashable.trim()).digest('hex') //sha256(hashable.trim())

    // Lifetime of chatterID is min of time to idToken expiration
    // (Math.ceil() previously) and target lifetime, which should
    // be less than idToken lifetime (~1 hour).
    const lifetime = Math.min((idinfo?.['exp'] ?? now+1800) - now, 60) // secs, up to 1800, idToken lifetime

    try {
        // clean up db table of expired chatterIDs
        await chatterDB`DELETE FROM chatters WHERE ${now} > expiration`

        // insert new chatterID
        // Ok for chatterID to expire about 1 sec beyond idToken expiration
        await chatterDB`INSERT INTO chatters (chatterid, username, expiration) VALUES (${chatterID}, ${username}, ${now + lifetime})`

        // Return chatterID and its lifetime
        res.json({'chatterID': chatterID, 'lifetime': lifetime})
    } catch (error){
        res.status(500).json(`${error as PostgresError}`)
    }
}

The function adduser() first receives a POST request containing an ID Token and Client ID from the front end. It calls Google’s library to verify the user’s ID Token, passing along the Client ID as required by Google. The verification process checks that ID Token hasn’t expired and is valid. If the token is invalid or has expired, a 511, “Network Authentication Required” HTTP error code is returned to the front end.

Next, a new chatterID is computed as a SHA256 one-way hash of the ID Token, a server’s secret, and the current time stamp. A lifetime is assigned to the chatterID. The lifetime should normally be set to be less than the total expected lifetime of the ID Token, and in any case not more than the remaining lifetime of the ID Token. The idea is that during the lifetime of chatterID, the user does not need to check the freshness of their ID Token with Google.

The chatterID, the user’s name obtained from the ID Token, and the chatterID’s lifetime are then entered into the chatters table. At the same time, we do some house keeping and remove all expired chatterIDs from the database.

Finally, the 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.ts:

export async function postauth(req: Request, res: Response) {
    let chatt: AuthChatt
    try {
        chatt = req.body
    } catch (error) {
        res.status(422).json(error)
        return
    }

    try {
        const row = await chatterDB`SELECT username, expiration FROM chatters WHERE chatterID = ${chatt.chatterID}`

        if (row.length == 0 || Math.floor(Date.now() / 1000) > row[0].expiration) {
            res.status(401).json('Unauthorized')
            return
        }
        try {
            await chatterDB`INSERT INTO chatts (username, message, id) VALUES (${row[0].username}, ${chatt.message}, ${randomUUID()})`
            res.json({})
        } catch (error) {
            res.status(400).json(`${error as PostgresError}`)
        }
    } catch (error) {
        res.status(500).json(`${error as PostgresError}`)
    }
}

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” HTTP error code. Otherwise, it retrieves the corresponding username from the table and inserts the chatt into the chatts table with that username. Note: chatterIDs are unique in the chatters table.

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

Save and exit handlers.ts.

Routing for new URLs

For the newly added adduser() and postauth() functions, add the following new routes to the Express app instantiation in main.ts:

      .post('/adduser/', handlers.adduser)
      .post('/postauth/', handlers.postauth)

Save and exit main.ts. Rebuild and restart chatterd:

server$ npx tsc
server$ sudo systemctl restart chatterd

Node may give the warning, [DEP0040] DeprecationWarning: The punycode module is deprecated. This is due to google-auth-library’s use of punycode. Hopefully it will be upgraded in its next release.

References

Python with Starlette

Python with Starlette

To verify Google’s ID Token, first install the Google API python client in your Python’s virtual environment:

server$ uv add google-api-python-client

Editing handlers.py

Add the following libraries to the import block at the top of handlers.py:

from google.auth.transport import requests
from google.oauth2 import id_token
import hashlib, time

Next add the following two new classes:

@dataclass
class AuthChatt:
    chatterID: str
    message: str

@dataclass
class Chatter:
    clientID: str
    idToken: str

Now add the following adduser() function:

async def adduser(request):
    try:
        chatter = Chatter(**(await request.json()))
    except Exception as err:
        print(f'{err=}')
        return JSONResponse('Unprocessable entity', status_code=422)

    now = time.time()                  # secs since epoch (1/1/70, 00:00:00 UTC)

    try:
        # Collect user info from the Google idToken, verify_oauth2_token checks
        # the integrity of idToken and throws a "ValueError" if idToken or
        # clientID is corrupted or if user has been disconnected from Google
        # OAuth (requiring user to log back in to Google).
        # idToken has a lifetime of about 1 hour
        idinfo = id_token.verify_oauth2_token(chatter.idToken, requests.Request(), chatter.clientID)
    except ValueError as err:
        try: # one more time, it has been returning "token used too early" with python backend
            idinfo = id_token.verify_oauth2_token(chatter.idToken, requests.Request(), chatter.clientID)
        except ValueError as err:
            # Invalid or expired token
            print(f'Network Authentication Required: {str(err)}')
            return JSONResponse('Network Authentication Required', status_code=511)

    # get username
    try:
        username = idinfo['name']
    except:
        username = "Profile NA"

    # Compute chatterID and add to database
    backendSecret = "ifyougiveamouse"   # or server's private key
    nonce = str(now)
    hashable = chatter.idToken + backendSecret + nonce
    chatterID = hashlib.sha256(hashable.strip().encode('utf-8')).hexdigest()

    # Lifetime of chatterID is min of time to idToken expiration
    # (int()+1 is just ceil()) and target lifetime, which should
    # be less than idToken lifetime (~1 hour).
    lifetime = min(int(idinfo['exp']-now)+1, 60) # secs, up to 1800, idToken lifetime

    try:
        async with main.server.pool.connection() as connection:
            async with connection.cursor() as cursor:
                # clean up db table of expired chatterIDs
                await cursor.execute('DELETE FROM chatters WHERE %s > expiration;', (now, ))

                # insert new chatterID
                # Ok for chatterID to expire about 1 sec beyond idToken expiration
                await cursor.execute('INSERT INTO chatters (chatterid, username, expiration) VALUES '
                                 '(%s, %s, %s);', (chatterID, username, now+lifetime))
        # Return chatterID and its lifetime
        return JSONResponse({'chatterID': chatterID, 'lifetime': lifetime})
    except Exception as err:
        print(f'{err=}')
        return JSONResponse(f'{type(err).__name__}: {str(err)}', status_code = 500)

For Python-PostgreSQL interaction, see Passing parameters to SQL queries.

The function adduser() first receives a POST request containing an ID Token and Client ID from the front end. It calls Google’s library to verify the user’s ID Token, passing along the Client ID as required by Google. The verification process checks that ID Token hasn’t expired and is valid. If the token is invalid or has expired, a 511, “Network Authentication Required” HTTP error code is returned to the front end.

Next, a new chatterID is computed as a SHA256 one-way hash of the ID Token, a server’s secret, and the current time stamp. A lifetime is assigned to the chatterID. The lifetime should normally be set to be less than the total expected lifetime of the ID Token, and in any case not more than the remaining lifetime of the ID Token. The idea is that during the lifetime of chatterID, the user does not need to check the freshness of their ID Token with Google.

The chatterID, the user’s name obtained from the ID Token, and the chatterID’s lifetime are then entered into the chatters table. At the same time, we do some house keeping and remove all expired chatterIDs from the database.

Finally, the 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.py:

async def postauth(request):
    try:
        # loading raw json (not form-encoded)
        chatt = AuthChatt(**(await request.json()))
    except Exception as err:
        print(f'{err=}')
        return JSONResponse('Unprocessable entity', status_code=422)

    try:
        async with main.server.pool.connection() as connection:
            async with connection.cursor() as cursor:
                await cursor.execute('SELECT username, expiration FROM chatters WHERE chatterID = %s;',
                                 (chatt.chatterID,))

                row = await cursor.fetchone()
                now = time.time()
                if row is None or now > row[1]:
                    # return an error if there is no chatter with that ID
                    return JSONResponse('Unauthorized', status_code=401)

                # else, insert into the chatts table
                await cursor.execute('INSERT INTO chatts (username, message, id) VALUES (%s, %s, gen_random_uuid());',
                                 (row[0], chatt.message))
        return JSONResponse({})
    except StringDataRightTruncation as err:
        print(f'Message too long: {str(err)}')
        return JSONResponse(f'Message too long', status_code = 400)
    except Exception as err:
        print(f'{err=}')
        return JSONResponse(f'{type(err).__name__}: {str(err)}', status_code = 500)

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” HTTP error code. Otherwise, it retrieves the corresponding username from the table and inserts the chatt into the chatts table with that username. Note: chatterIDs are unique in the chatters table.

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

Save and exit handlers.py.

Routing for new URLs

For the newly added adduser() and postauth() functions, add the following new routes to the routes array in main.py:

    Route('/postauth/', handlers.postauth, methods=['POST']),
    Route('/adduser/', handlers.adduser, methods=['POST']),

Save and exit main.py and restart chatterd:

server$ sudo systemctl restart chatterd

References

Rust with axum

Rust with axum

Adding dependencies

In your package directory, ~/441/chatterd/, open Cargo.toml and add the following dependencies:

# . . .
hex = "0.4.3"
openidconnect = "4.0.0"
reqwest = "0.12.15"
rustls = "0.23.26"
sha2 = "0.11.0-pre.5"

Save and exit Cargo.toml.

Editing 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,
};

You can choose to “merge” the following with the existing set of chrono module or add it as a separate line. Both work:

use chrono::Utc;

Next, 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(pgpool): State<PGPool>,
    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(),
        )
        .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::NETWORK_AUTHENTICATION_REQUIRED,
                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();
        
    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, 60);

    let chatterDB = 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!({ "chatterID": chatterID, "lifetime": lifetime })))
}

The function adduser() first receives a POST request containing an ID Token and Client ID from the front end. We use the OpenID Connect crate to verify the user’s ID Token, passing along the Client ID as required by Google. The verification process checks that ID Token hasn’t expired and is valid. If the token is invalid or has expired, a 511, “Network Authentication Required” HTTP error code is returned to the front end.

Next, a new chatterID is computed as a SHA256 one-way hash of the ID Token, a server’s secret, and the current time stamp. A lifetime is assigned to the chatterID. The lifetime should normally be set to be less than the total expected lifetime of the ID Token, and in any case not more than the remaining lifetime of the ID Token. The idea is that during the lifetime of chatterID, the user does not need to check the freshness of their ID Token with Google.

The chatterID, the user’s name obtained from the ID Token, and the chatterID’s lifetime are then entered into the chatters table. At the same time, we do some house keeping and remove all expired chatterIDs from the database.

Finally, the 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:

pub async fn postauth(
    State(pgpool): State<PGPool>,
    ConnectInfo(clientIP): ConnectInfo<SocketAddr>,
    Json(chatt): Json<AuthChatt>,
) -> Result<Json<Value>, (StatusCode, String)> {
    let chatterDB = 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 {
        chatterDB
            .execute(
                "INSERT INTO chatts (username, 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!({})))
    }
}

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” HTTP error code. Otherwise, it retrieves the corresponding username from the table and inserts the chatt into the chatts table with that username. Note: chatterIDs are unique in the chatters table.

We will be using the original getchatts() from the Chatter lab, with no changes, to retrieve and return chatts.

Save and exit handlers.rs.

Editing main.rs

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

use rustls::crypto::aws_lc_rs;

For the newly added adduser() and postauth() functions, add the following new routes to the Router instantiation of the router variable in main.rs, 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();

Save and exit main.rs. Rebuild and restart chatterd; but we need to first install pkg-config so that cargo can find our installation of openssl.

server$ sudo apt install pkg-config
server$ cargo build --release
server$ sudo systemctl restart chatterd

References

Testing Sign-in

To test your implementation with Insomnia, you’d need your idToken. Put a debugging breakpoint at the start of your front end’s ChattStore.addUser() function. The idToken parameter passed to ChattStore.addUser() is your idToken.

ID Token on iOS

For iOS, on Xcode, mouse over the idToken variable inside the addUser(_:) function, then click on the i icon on the extreme right of the box that pops up. The idToken will be displayed as plain text on yet another pop up box. Click in the box, then select all and copy (⌘-A, ⌘-C). This is your idToken to be used with Insomnia below.

Create a new Insomnia HTTP POST request to adduser with your Google ID Token (idToken) and your front end’s Google Client ID (clientID):

{
    "clientID": "YOUR_APP'S_CLIENT_ID",
    "idToken": "YOUR_GOOGLE_ID_TOKEN"
}

Adding users with an invalid token results in a 511 error, while a valid Google ID Token will result in a 200 response status and a payload body with a chatterID and its lifetime. You can use this chatterID in place of YOUR_CHATTER_ID below to POST a message to postauth:

{
    "chatterID": "YOUR_CHATTER_ID",
    "message": "Chitt chatts"
}

You should get a 200 server response in return.

That’s it! You’re now all set up on the back end for Chatter! Be sure to confirm that your back end works with your front end.

Submitting your back end

You can now return to complete the front end: iOS.


Prepared for EECS 441 by Benjamin Brengman, Ollie Elmgren, Wendan Jiang, Alexander Wu, Chenglin Li, and Sugih Jamin Last updated: August 21st, 2025