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 struct
s:
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: chatterID
s 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
.
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: chatterID
s 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
punycodemodule is deprecated.
This is due togoogle-auth-library
’s use ofpunycode
. 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: chatterID
s 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
-
Authenticate with a backend server
Once the mobile client obtained an ID Token from an SSO, it presents the ID Token to a backend server. The backend server must authenticate that ID Token.
- SHA in Python
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 struct
s:
#[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: chatterID
s are unique in the chatters
table.
We will be using the original getchatts()
from the Chatter
lab, with no changes, to retrieve and return chatt
s.
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
- Commit new changes to the local repo with:
server$ cd ~/441/chatterd server$ git commit -am "signin back end"
and push new changes to the remote GitHub repo with:
server$ git push
- If
git push
fails due to new changes made to the remote repo, you will need to rungit pull
first. Then you may have to resolve any conflicts before you cangit push
again.
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 |