About this project
I'm Antalon Jackson Jr, a system administrator that is currently the primary M365 and Azure admin for a SMB environment. I'm working towards diving deeper into infrastructure and systems engineering, and identity is a key part that keeps pulling my attention.
Particularly, onboarding. Since it's such a big part of how a new hire experiences a company, I like to see it work smoothly/without issue. Offboarding matters for a different reason: security and compliance don't really care about anyone's first day, they care that access actually goes away when it should. I've done this in Entra, but I wanted to see how joiner-mover-leaver cycle works within Okta without just watching videos.
What it does
Three operations run against an Okta dev. org: hire, role change and offboarding. Each one calls the Okta API, waits for Okta to push the change into a downstream app over SCIM, then checks what actually landed instead of assuming it worked.
The downstream app is Glasscliff Expenses. A totally not made-up app that provides something real on the other end of the provisioning. It uses Okta for sign-in, writes a session row for each login, and checks that row on every page view. Offboarding someone who is signed in takes their session away about five seconds later, which you can watch on the dashboard.
How it's put together
- Lifecycle API on Azure Functions: hire, transfer, terminate and status, behind an operator key.
- A SCIM 2.0 endpoint that Okta provisions into, which stores every call it receives as evidence.
- Expenses on App Service: OIDC authorization code with PKCE and refresh tokens, sessions in Table Storage.
- This site's dashboard, public and read-only, with people shown as pseudonyms.
- Azure infrastructure in Bicep: managed identities and Key Vault references rather than secrets in app settings, and storage permissions scoped to single tables and containers.
Three things that broke
These were just fun troubleshooting things.
Sign-on passed, but tokens were refused. Okta said "You are not allowed to access this app", which reads like an assignment problem. It wasn't. The System Log showed the sign-on policy returning ALLOW, then app.oauth2.as.authorize failing with no_matching_policy against the default authorization server. App assignment and the authorization server's own access policy are two separate gates, and I'd only set up the first. Custom authorization servers deny by default until a rule covers the client, the grant type and every scope requested.
Ended sessions didn't look ended. My first version of the Expenses session marked its row inactive when someone signed out. But the SCIM code deletes session rows on deactivation, and the offboarding check counts every row left in that user's partition. So my politely retired rows looked like live sessions to the verification step. The fix was to follow the convention that already existed: a session exists or it doesn't.
A client ID with an apostrophe (') at the end. I made a typo
What I may change next
- The offboarding check has a safety net that cleans up any sessions left behind, which is good behaviour but hides whether SCIM did the work. The SCIM step records its own count, so the two need to be read together.
- Upgrade the runtime to Node.js 22.
- Adding more of a showy flair to the dashboard.