More Projects
59 in total
CCS Auth — Single Sign-On
Creative Computing Society · 2023
OAuth2-style single sign-on for society portals. Google is the only identity source, over a hand-rolled authorization-code exchange rather than a library — build the consent URL, take the code back, exchange it, fetch the profile — with per-client registration against whitelisted callback URLs, per-client JWT issuance and crypto-js-encrypted payloads, served by an Express/MongoDB backend with EJS consent pages and a React admin console. An authentication-log collection is modelled and not yet written to.
5,000+ users
Project Details
Creative Computing Society
2023
5,000+ users
Society · Security
The society ran half a dozen separate web apps and every one had grown its own login table. That means six places to get session handling wrong, six copies of the same student's roll number, and no way to answer "who is a core member" without asking six databases. CCS Auth replaces all of it with one identity service the portals delegate to.
The shape of the problem
The obvious fix is a shared users table that every app reads. That fails for two reasons. Half the portals were written by different people in different stacks - Node, Django, PHP - so "share the database" means handing every one of them write access to identity. And the interesting question is never just "who is this"; it is "what is this person allowed to do here", which is a different answer in the merch store than in the judging portal. So the design had to carry authorisation, not only authentication, and it had to hand each portal an answer it could verify on its own without a round trip.
The documents
Four collections, and the split is the design.
- Users carry the Google identity -
googleId,email,profilePic- plus the fields Google does not know:personalEmail,rollNoas a BigInt,branch,yearPassing. The interesting field isroles, an array of{clientId, role}subdocuments. Identity is one document; entitlement is a list on it, keyed by application. - Clients are the relying parties:
ClientName,ClientDomain,ClientEmail,ClientAccessLeveland aClientSecretgenerated from 48 random bytes ofcryptooutput. - Callback URLs are their own collection rather than an array on the client, one row per
{CallbackURL, ClientID}pair, so a portal can register a staging and a production return address and either can be revoked without touching the client document. - Authentication logs are modelled - user id, session id, status, timestamp - and never written to. That is the gap I would close first.
What it provides
- Google as the only identity source, over a hand-rolled authorization-code exchange rather than
a library: build the consent URL with the userinfo email and profile scopes, take the code back,
exchange it at
oauth2.googleapis.com/token, fetch the profile. An earlier generation of the same flow used Passport's Google strategy and still sits in the repo beside it. - Per-client registration. Each relying party is a document with a name, domain, contact email and access level, plus a secret generated from 48 random bytes.
- Callback URL whitelisting. A client passes its id and a return URL; that URL must already exist as a registered row for that client, matched on exact equality. The verified client id, secret and callback are stashed in the session before the user is sent to Google, and cleared the moment the token is minted.
- Tokens signed per client, with the requesting client's own secret and a 6-hour expiry, so a token minted for one portal is verifiable only by that portal. The whole user document goes into the payload, which is why a portal can make an authorisation decision without calling back.
- Per-client roles. The user document carries a
rolesarray of{clientId, role}pairs. First login into a new app auto-provisions a role there; returning users get one appended if they have none for that client. - Cascading cleanup on client deletion - it drops the callback URLs and pulls that client's
roles out of every user document in one pass with a single
updateManyand a$pull, which is referential integrity written by hand because Mongo will not do it for you. - Profile completion before consent. Middleware runs ahead of the Google redirect and serves a form if name, personal email, roll number, branch or passing year is missing. It shipped twice, as a server-rendered EJS page and as a webpack-bundled React form, and the submit handler mints the token and redirects to the stored callback itself rather than bouncing the user back through Google.
- Academic year derived, not stored. A virtual parses the admission year out of the roll number, adjusts by whether the current semester is odd or even, and returns first through fourth year, defaulting to alumni. Storing a year would mean a migration every July; deriving it means the answer is correct the day the semester counter changes.
- A token introspection endpoint at
/auth/google/verify, which decodes the token, loads the user it names, and gates the response on the client secret matching before returning the profile. - A React admin console on Chakra and Tailwind: login, a client grid exposing each client id for pasting into a portal config, add and edit client with a repeatable callback-URL input, a user directory rendering roles grouped by app as badges, and a two-step flow to rewrite a user's roles. Role editing replaces the whole set for that client rather than appending, so a mistake is one edit to undo instead of a growing list.
- A separate admin auth path. The console logs in against a username and password from the
environment and gets a JWT signed with the service's own secret; every
/superAdminroute sits behind a verifier that also checks the decoded username still matches. Society members never touch that path and administrators never touch the Google one.
What it looks like from a portal
The merch store's Django backend authenticates entirely against this. It decodes the token, AES-decrypts the embedded claim, auto-provisions the user keyed on roll number, and recomputes their position and membership flag from the SSO roles on every login. Codeboard does the same. Promoting someone to core in one console changes what they can buy and what they can see everywhere, without a migration.
Deployment is a node:alpine image on port 3001 behind Compose, with the source bind-mounted and
node_modules masked by an anonymous volume so a host install cannot shadow the container's.
What I would change
There is no state parameter and no PKCE on the Google leg, and the finished token is delivered as
a query string on the redirect, which puts it in browser history and any proxy access log. Both are
protocol-level fixes rather than rewrites - bind state to the session, and make the final hop a
short-lived code the portal exchanges server side.
Two more, found on re-reading it. The introspection endpoint compares the secret stored on the token's client against the secret stored on the named client, which is a comparison between two database rows rather than a check that the caller actually holds the secret; it should verify a presented credential. And the express-session store is the default in-memory one, so sessions do not survive a restart and would not be shared across replicas - fine for one container, wrong the moment you scale it. The audit collection sitting there unwritten is the thread that ties all three together: the design anticipated the operational questions and the implementation stopped at the functional ones.
Project Details
Creative Computing Society
2023
5,000+ users
Society · Security