Brian Gita / Work / Service platform

Client project · Staging · Private

Incident & Service Management Platform

A service desk for a multi-campus school group, and the single sign-on service it shares with the group's other internal apps.

Role
Sole developer
Service teams
8
Apps on one sign-in
3
Automated tests
105

The problem

Staff and students need help from eight service teams: ICT, facilities, security, transport, student affairs, procurement, finance and administration. Each request needs an owner, a deadline someone is accountable for, and a record of what happened to it.

The group was also building other internal apps, starting with procurement and an executive dashboard. Without a shared sign-in, each app would bring its own passwords, its own MFA and its own way of granting access. That's three times the attack surface, and no single place to check who can reach what.

So the work came in two parts: the service desk, then one sign-on service for all three apps.

Part 1: The service desk

A React front end on a Django REST API. Incidents ("something is broken") and service requests ("I need something") share one ticket model, so SLAs, assignment and reporting work the same for both.

What happens to a ticket

  1. It's raised as an incident, or as a request from the service catalogue, with a priority and an impact scope from "just me" to "entire organisation". The service team comes from the catalogue item, not from whatever the browser sends.
  2. Its SLA deadline is set from the policy for its priority.
  3. It's assigned to the least-loaded member of that service team, who gets an email.
  4. Every change to a field is recorded with who made it and when.
  5. If it misses its deadline, a scheduled job emails about the breach once, and the service team can escalate it to admins.
  6. Once resolved, only the requester can close it and rate it from 1 to 5, once. The database enforces the range.

Who sees what

Admins see everything. Service team members see only their own team's tickets. Staff and students see only tickets they raised. Internal notes are hidden from requesters, and service teams can't reach other teams' notes.

Approvals with separation of duties

Catalogue items that need approval start as pending. Requesters can't approve their own requests, service team members can't approve at all, and whoever approves a request can't also resolve it: a different agent has to. A request that's pending or rejected can't be resolved.

An insights dashboard reports volumes by priority and team, SLA performance, resolution times and average satisfaction.

Part 2: Single sign-on

A separate Django service is the only place where passwords, MFA secrets and sessions live. Apps never see a password. Each one checks tokens itself against the service's published public keys, so the sign-on service isn't called on every request.

PieceHow it works
Access tokenA 10-minute JWT signed with RS256, issued for one app only and kept in browser memory, never in storage.
Refresh tokenRandom, stored only as a hash, and kept in an HttpOnly, Secure, SameSite=Strict cookie. A new one is issued on every use.
Moving between appsA single-use code that expires in 60 seconds and is bound with PKCE to the browser that asked for it.
Second factorAn authenticator app code (TOTP), required for privileged roles and every admin-site user.
Key rotationThe old public key stays published alongside the new one for 15 minutes, so no one is signed out mid-session.

Catching a stolen refresh token

Each refresh token works once. If a token that has already been used turns up again, someone has a copy, so every session for that user is ended. The check is a conditional update, so two requests racing with the same token can't both succeed.

# Conditional UPDATE so two concurrent refreshes can't both succeed.
if not RefreshToken.objects.filter(
    pk=rt.pk, revoked_at__isnull=True
).update(revoked_at=timezone.now()):
    revoke_all(rt.user)
    raise TokenReuse(rt.user)

Security and controls

These are the controls I would test as an IT auditor:

ControlHow it's enforced
Account provisioningHR raises a ticket, a system admin adds roles, and an IT manager approves. Only then is the account created and the invitation sent. Each person is emailed when it's their turn.
Segregation of dutiesNobody can approve a request they raised or submitted, even if they hold both roles. Nobody can promote themselves. Rejections need a reason.
Least privilegeRoles travel in the token. Heads of school can be limited to one campus, and the executive dashboard is open only to the executive team and the board.
Brute forceFive wrong passwords or codes lock the account for 15 minutes, and the admin site shares that count. Sign-in is also rate limited.
No account enumerationAn unknown email gets the same error as a wrong password, and password reset never reveals whether an address exists.
MFA replayEach authenticator code works only once, even within its 30-second window.
Data at restRefresh tokens and handoff codes are stored as SHA-256 hashes, so a copy of the database doesn't give anyone a working session.
Audit trailSign-ins, failures, lockouts, MFA events and invitations are logged with IP address and kept for a year. A nightly job prunes expired tokens.
Revocation"Sign out of all apps" plus deactivating the account ends access within 10 minutes, the life of the last access token.

Decisions worth explaining

Fewer moving parts

The first version used Redis for sign-in rate limits. I moved them into PostgreSQL, which the service already depends on. That's one less service to run, secure and pay for.

A 10-minute access token

Apps verify tokens without calling the sign-on service, which is fast, but it means a revoked user keeps access until their token expires. Ten minutes is the trade-off, and the runbook says so plainly rather than promising instant revocation.

Written for the next developer

Connecting an app means copying one Python module and one JavaScript file. The integration guide is written for junior developers, and the operations guide covers a lost phone, a locked account, a compromised account and key rotation step by step.

Testing

60 tests on the service desk cover SLA deadlines, least-loaded assignment, breach and escalation emails, who can see, close and rate which tickets, and every approval rule, including that an approver can't also resolve.

45 tests on the sign-on service cover cookie flags, origin checks, lockout, refresh-token reuse ending every session, the PKCE handoff, MFA enrolment, and checks of the authenticator-code algorithm against the standard's published test values. They also cover each step of the account approval chain, including that nobody can approve what they raised and that a failed email never loses the request.