TypeScript with Express
Cover Page
Back-end Page
To verify Google’s ID Token, first add the Google API python client as a dependency:
server$ pnpm install google-auth-library
This should add the following line to the dependencies block of your package.json:
"google-auth-library": "^10.6.1",
handlers.ts
Change to your chatterd folder and edit handlers.ts:
server$ cd ~/reactive/chatterd
server$ vi handlers.ts
Add createHash to your import from crypto at the top of handlers.ts:
import { createHash, randomUUID } from 'crypto'
and the following new import:
import { OAuth2Client, type TokenPayload } from 'google-auth-library'
Next add the following two new types:
type AuthChatt = {
chatterID: string
message: string
}
type Chatter = {
clientID: string
idToken: string
}
Then add the following adduser() function:
export async function adduser(req: Request, res: Response) {
let chatter: Chatter = req.body
const now = Math.ceil(Date.now()/1000) // secs since epoch (1/1/70, 00:00:00 UTC)
let idinfo: TokenPayload | undefined
try {
const client = new OAuth2Client()
idinfo = (await client.verifyIdToken({
idToken: chatter.idToken,
audience: chatter.clientID,
})).getPayload()
} catch (error) {
logClientErr(res, HttpStatus.UNAUTHORIZED, `Unauthorized | ${error}`)
}
const username = idinfo?.['name'] ?? "Profile NA"
// 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
const backendSecret = "allhappyfamilies" // or server's private key
const nonce = now.toString()
const hashable = chatter.idToken + backendSecret + nonce
const chatterID = createHash('sha256').update(hashable.trim()).digest('hex')
const lifetime = Math.min((idinfo?.['exp'] ?? now+1800) - now + 1, 300) // secs, up to 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:
try {
await chatterDB`DELETE FROM chatters WHERE ${now} > expiration`
await chatterDB`INSERT INTO chatters (chatterid, username, expiration) VALUES (${chatterID}, ${username}, ${now + lifetime})`
res.json({'username': username, 'chatterID': chatterID, 'lifetime': lifetime})
} catch (error){
logServerErr(res, `${error as PostgresError}`)
}
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.ts. 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. Otherwise, the registered username corresponding to chatterID
is retrieved from the table. Note: chatterIDs are unique in the chatters table.
export async function postauth(req: Request, res: Response) {
let chatt: AuthChatt = req.body
try {
const row = await chatterDB`SELECT username, expiration FROM chatters WHERE chatterID = ${chatt.chatterID}`
if (!row.length || Math.floor(Date.now() / 1000) > row[0]?.expiration) {
logClientErr(res, HttpStatus.UNAUTHORIZED ,'Unauthorized')
return
}
// insert chatt
} catch (error) {
logServerErr(res, `${error as PostgresError}`)
}
}
Insert the chatt into the chatts table under the retrieved username. Replace // insert chatt with:
try {
await chatterDB`INSERT INTO chatts (name, message, id) VALUES (${row[0]?.username}, ${chatt.message}, ${randomUUID()})`
res.json({})
} catch (error) {
logClientErr(res, HttpStatus.BAD_REQUEST, `${error as PostgresError}`)
}
We will be using the original getchatts() from the chatter lab without modification.
We’re done with handlers.ts. Save and exit the file.
main.ts
Edit the file main.ts:
server$ vi main.ts
To the Express app instantiation, add the following new routes for
the new API endpoints /adduser and /postauth:
.post('/adduser/', handlers.adduser)
.post('/postauth/', handlers.postauth)
We’re done with main.ts. Save and exit the file.
Build and test run
To build your server, transpile TypeScript into JavaScript:
server$ npx tsgo
![]()
TypeScript is a compiled language, like C/C++ and unlike JavaScript and Python, which are an interpreted languages. This means you must run npx tsgo each and every time you made changes to your code, for the changes to show up when you run node.
To run your server:
server$ sudo node main.js
# 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 |