Contents

Case Study - Event-Driven Identity Provisioning

This case study describes how a large multi-campus university system replaced a decade of legacy sign-on PeopleCode with an event-driven, asynchronous provisioning pipeline. The redesign moved every side-effect – account creation, role reconciliation, entitlement updates, email sync – off the login path and onto a local-to-local Integration Broker queue fed by inbound webhooks from an external identity manager.

The population is non-trivial: roughly 73,000 active students across four campuses, plus staff, applicants, and parents. The system of record for identity is Oracle IDM. Authentication is SAML via an Appsian/Pathlock firewall product. Boomi sits between PeopleSoft and IDM as a thin HTTP proxy.

This case study does not re-cover the mechanics of local-to-local async queues – see Async Services for Concurrent Processing for the generic pattern. Here, the focus is on what’s specific to inbound event-driven provisioning: webhook receivers, just-in-time reconciliation, producer-agnostic queues, and the deduplication logic you need when a fast-moving upstream system emits bursts of events for the same subject.

The Legacy Problem

In the legacy design, sign-on PeopleCode did everything. On every first login, PeopleCode read SAML headers from the Appsian firewall, then made an outbound LDAP bind to Oracle IDM during the login transaction to fetch the user’s roles, campus affiliation, and entitlements. If IDM answered, the account was provisioned on the fly into PSOPRDEFN, PSROLEUSER, and PS_OPR_DEF_TBL_CS, roles were hard-coded in PeopleCode by institution, and the login completed. If IDM didn’t answer, the user couldn’t log in.

  sequenceDiagram
    actor User
    participant SSO as SSO Server
    participant Login as PS Login Page
    participant SOPC as Signon PeopleCode
    participant LDAP as LDAP / IDM
    participant Sec as Security Tables

    User->>SSO: Login PeopleSoft
    SSO-->>User: Redirect with SAML token
    User->>Login: SAML token
    Login->>Login: Validate token
    Login->>SOPC: Pass SAML assertions
    Note over SOPC,Sec: Provisioning during login blocks authentication
    SOPC->>LDAP: Fetch roles / affiliation
    LDAP-->>SOPC: User attributes
    SOPC->>Sec: Create / update account
    SOPC->>SOPC: Resolve OPRID
    SOPC-->>User: PS_TOKEN cookie

Three problems compounded over the years:

  1. Login latency was tied to IDM availability. Any IDM hiccup cascaded into failed logins.
  2. Provisioning logic was locked inside sign-on PeopleCode. Campus-specific role mappings and permission lists were hard-coded alongside the authentication flow. Changes required testing the entire login path, which in turn required coordination with security, campus leads, and the Appsian team.
  3. There was no deprovisioning. Roles were added at login but never removed. A student who lost an affiliation in IDM kept their PeopleSoft roles until someone noticed.

Four parallel authentication paths had accumulated – native PeopleSoft password, SAML/IDM, mobile LDAP, and a deprecated legacy LDAP – each with its own provisioning quirks. Each was a cost center.

The Redesign: Separate Authentication from Provisioning

The guiding principle of the redesign is simple: the login path does no I/O to external systems and manipulates no security data. Sign-on PeopleCode’s only job is to resolve an OPRID from the SAML assertion. Every lifecycle side-effect is moved to an asynchronous pipeline that runs independently.

  sequenceDiagram
    actor User
   participant SSO as SSO Server
   participant Login as PS Login Page
   participant SOPC as Signon PeopleCode

   User->>SSO: Login PeopleSoft
   SSO-->>User: Redirect with SAML token
   User->>Login: SAML token
   Login->>Login: Validate token
   Login->>SOPC: Pass SAML assertions
   SOPC->>SOPC: Resolve OPRID from PSOPRDEFN
   SOPC-->>User: PS_TOKEN cookie
   Note over SOPC: No external calls. No security mutation.

The new SAML sign-on PeopleCode (implemented as an Application Class plugged into the Appsian user-mapping framework) is roughly:

method LookupPeopleSoftUserID
   /+ &inNameID as String +/
   /+ Returns String +/

   &emplid   = RTrim(%Request.GetHeader(&c.SAMLAttributeEMPLID));
   &idmGUID  = RTrim(%Request.GetHeader(&c.SAMLAttributeGUID));
   &idmCampus = RTrim(%Request.GetHeader(&c.SAMLAttributeCampus));

   Local Record &recOperDefn = CreateRecord(Record.PSOPRDEFN);
   &recOperDefn.OPRID.Value = &c.OPRIDPrefix | &emplid;

   If &recOperDefn.SelectByKey() And &recOperDefn.ACCTLOCK.Value = 0 Then
      If &emplid = &recOperDefn.EMPLID.Value Then
         &resolvedOPRID = &recOperDefn.OPRID.Value;
      End-If;
   End-If;

   Return &resolvedOPRID;
end-method;

That’s the whole happy path: read three SAML headers, key into PSOPRDEFN, return the OPRID. No LDAP bind. No outbound HTTP. No role mutation. If the account isn’t provisioned, the login fails fast and the provisioning pipeline – running independently – will have created it (or will create it shortly) via an IDM event.

The trade-off is explicit and worth stating: a brand-new user who authenticates before their provisioning event has been processed will see a login failure. In production this gap is measured in seconds. The design accepts that narrow race window as the cost of never blocking a login on IDM availability.

Architecture

The provisioning pipeline has four pieces:

  1. Webhook receiver – a REST-provided service operation that accepts IDM events.
  2. Staging tableZ_IDM_EVENTS, one row per event.
  3. Local-to-local async queue – service operation Z_IDM_EVENT_ASYNC.
  4. OnNotify handler – does the actual reconciliation against IDM.
  flowchart LR
    subgraph IDMSide["Identity Management"]
        IDM["IDM\n(system of record)"]
        IDMEvents["IDM Event Push"]
        IDMAPI["IDM User GET API"]
        IDM --> IDMEvents
        IDM --> IDMAPI
    end

    subgraph BoomiSide["Boomi (HTTP proxy)"]
        BoomiIn["Event forwarder"]
        BoomiOut["User GET proxy"]
    end

    subgraph PSSide["PeopleSoft"]
        Webhook["Webhook Receiver\n(REST service op)"]
        Staging["Staging Table\nZ_IDM_EVENTS"]
        Queue["Async Queue\n(local-to-local)"]
        Worker["OnNotify Handler"]
        OPRID["PSOPRDEFN"]
        Roles["PSROLEUSER"]
        Prefs["PS_OPR_DEF_TBL_CS"]
        Email["PS_EMAIL_ADDRESSES"]
        ExtID["PS_EXTERNAL_SYSTEM"]
        QryTool["Query Event Creator"]
        CompEvents["Component Save-Events"]
    end

    IDMEvents --> BoomiIn
    BoomiIn --> Webhook
    Webhook --> Staging
    Webhook --> Queue
    Queue --> Worker
    Worker --> BoomiOut
    BoomiOut --> IDMAPI
    Worker --> OPRID
    Worker --> Roles
    Worker --> Prefs
    Worker --> Email
    Worker --> ExtID
    Worker --> Staging
    QryTool --> Webhook
    CompEvents --> Webhook

The sequence is worth drawing out explicitly because it surfaces the design’s central property: the event producer never waits on PeopleSoft to do any real work.

  sequenceDiagram
    participant IDM as Oracle IDM

   participant Boomi
   participant Webhook as PS Webhook
   participant Table as Z_IDM_EVENTS
   participant Queue as IB Queue
   participant Handler as OnNotify Handler

   IDM->>Boomi: POST event (GUID)
   Boomi->>Webhook: POST Z_IDM_EVENT.v1
   Webhook->>Table: INSERT status=NEW
   Webhook->>Queue: IntBroker.Publish
   Webhook-->>Boomi: 200 eventID
   Boomi-->>IDM: 200
   Note over Queue,Handler: ...later, on a handler thread...
   Queue->>Handler: OnNotify eventID
   Handler->>Table: lookup payload
   Handler->>Boomi: GET StudentProfile
   Boomi->>IDM: GET getuserprofile
   IDM-->>Boomi: userProfile + entitlements
   Boomi-->>Handler: JSON
   Handler->>Handler: reconcile PSOPRDEFN, PSROLEUSER, etc.
   Handler->>Table: status=COMP + log

Everything above the dashed note happens in the HTTP round-trip. Everything below it happens independently on an application-server thread. The two halves share nothing except a row in Z_IDM_EVENTS.

The Webhook Receiver

The receiver is a REST-provided service operation with one responsibility: persist the event and acknowledge. Here is an illustrative call:

POST https://{host}/PSIGW/RESTListeningConnector/{node}/Z_IDM_EVENT.v1/
Authorization: Basic {token}
Content-Type: application/json

{
  "userProfile": {
    "userLogin": "6998789647",
    "userISISID": "00827280"
  }
}

The response is a bare acknowledgment:

HTTP/1.1 200 OK
Content-Type: application/json

{
  "eventID": "0abe91b5-6324-42b3-808b-8ca8a051d72e",
  "message": "The event was queued."
}

Receiver logic, in order:

  1. Parse and validate the payload – enough to confirm there’s a business key (IDM GUID, EMPLID, or OPRID).
  2. Insert a row into Z_IDM_EVENTS with status NEW and the raw JSON.
  3. Publish to the Z_IDM_EVENT_ASYNC local-to-local async service operation with just the event ID.
  4. Return the event ID.

The receiver is deliberately thin. If the only way to reliably acknowledge an event is to keep the acknowledgment path as close to zero work as possible, then that’s where the work shouldn’t be.

The Staging Table

Z_IDM_EVENTS is a business-level log that lives alongside – not inside – Integration Broker’s message monitor. It mirrors the pattern from the generic async-processing case study, with identity-specific fields added for audit:

Field Purpose
Z_IDM_EVENT_ID Unique event ID (UUID)
IBTRANSACTIONID Links to IB message monitor
Z_IDM_EVENT_STAT NEW, QUED, COMP, ERR, WARN, CANC
Z_EVENT_PAYLOAD Raw inbound JSON
Z_IDM_DATA IDM GET response captured at processing time
Z_EVENT_LOG Long-form processing log
Z_OPRID_CREATED, Z_ROLES_ADDED, Z_ROLES_REMOVED Audit counters
Z_DRY_RUN Event was processed without writes
Z_IDM_EVENT_SRC What created the event (IDM, query tool, save-event, etc.)
QRYNAME, PROCESS_INSTANCE Populated when the event came from the query-event-creator

The Async Handler

The Z_IDM_EVENT_ASYNC OnNotify handler is where all real work happens. Its first action on waking up is the most important design choice in the entire pipeline: it ignores the event payload and re-fetches current IDM state.

method OnNotify
   /+ &_MSG as Message +/

   Local string &eventId = %This.GetEventIdFromMessage(&_MSG);

   /* Dedup check -- see next section */
   If %This.NewerEventExistsForSameSubject(&eventId) Then
      %This.LogEvent(&eventId, "CANC", "Duplicate Cancelled");
      Return;
   End-If;

   /* Pull authoritative state from IDM via Boomi, keyed by EMPLID */
   Local string &emplid = %This.GetEmplidFromEventRow(&eventId);
   Local string &idmJson = %This.FetchIdmUserProfile(&emplid);

   /* Reconcile -- system of record is always IDM at the moment of work */
   %This.ReconcileOPRID(&emplid, &idmJson);
   %This.ReconcileRoles(&emplid, &idmJson);
   %This.ReconcileUserDefaults(&emplid, &idmJson);
   %This.ReconcileEmails(&emplid, &idmJson);
   %This.ReconcileExternalIdGuid(&emplid, &idmJson);

   %This.LogEvent(&eventId, "COMP", %This.BuildAuditLog());

end-method;

The outbound GET hits a Boomi proxy that forwards to IDM:

GET {baseURL}/ws/rest/IDM_SA/StudentProfile/{emplid}
Authorization: Basic ...

HTTP/1.1 200 OK
Content-Type: application/json

{
  "userProfile": {
    "userLogin": "4476900471",
    "userISISID": "01183164",
    "primaryPL": "SA9SSSTU1",
    "processProfilePL": "SA9SSPRCSPRFL",
    "navigatorHomePL": "",
    "rowSecurityPL": "",
    "campusEmailA": "jdoe@student.example.edu",
    "campusEmailB": "",
    "campusEmailC": ""
  },
  "entitlements": ["SA9_Self_Service_Student"]
}

The response maps cleanly into PeopleSoft security:

  • userLoginPS_EXTERNAL_SYSTEM.EXTERNAL_SYSTEM_ID (the IDM GUID)
  • primaryPLPSOPRDEFN.OPRCLASS
  • processProfilePLPSOPRDEFN.PRCSPRFCLS
  • navigatorHomePLPSOPRDEFN.DEFAULTNAVHP
  • entitlements[]PSROLEUSER rows (full reconciliation: add missing, remove extras)
  • Campus-prefixed email fields → PS_EMAIL_ADDRESSES

If IDM reports "status": "disabled", the handler removes every PeopleSoft role and optionally every university email address – that’s the deprovisioning path the legacy system never had.

Why Refetch? Why Not Trust the Payload?

Two reasons. First, events are notifications, not data: the producer’s only promise is “something may have changed for this subject.” Second, bursts of events for the same subject collapse cleanly when the handler always reads current state – the first handler to win the race converges the record, and later handlers find nothing to do. This is what makes the next section possible.

Event Deduplication

Oracle IDM is a fast-moving system. A student gets admitted, enrolled, and assigned a role – three events, same EMPLID, arriving within seconds. Processing each one independently means three round-trips to IDM and three reconciliation passes, all producing the same final state.

The handler collapses these bursts with a single check at the top of OnNotify:

method NewerEventExistsForSameSubject
   /+ &eventId as String +/
   /+ Returns Boolean +/

   Local SQL &sql = CreateSQL(
     "SELECT 'X' FROM PS_Z_IDM_EVENTS newer, PS_Z_IDM_EVENTS current " |
     "WHERE current.Z_IDM_EVENT_ID = :1 " |
     "  AND newer.EMPLID = current.EMPLID " |
     "  AND newer.SCC_ROW_ADD_DTTM > current.SCC_ROW_ADD_DTTM " |
     "  AND newer.Z_IDM_EVENT_STAT IN ('NEW', 'QUED')",
     &eventId);

   Local string &exists;
   Return &sql.Fetch(&exists);
end-method;

If a newer pending event exists for the same EMPLID, the current event is marked CANC with reason “Duplicate Cancelled” and the handler returns immediately. The newer event will see the latest IDM state when it runs. Ten events for one student during an admissions burst collapse to one piece of real work.

Producer-Agnostic Queue

One of the design’s more subtle wins is that the queue does not care who produced the event. The same Z_IDM_EVENTS table and Z_IDM_EVENT_ASYNC queue accept events from three different producers, all producing identical downstream behavior:

  flowchart LR
    A["IDM Webhook\n(Real-time lifecycle events)"]

B["Query Event Creator\n(App Engine Z_IDMQRYRUN)"]
C["PS Component Save-Events\n(via Event Mapping)"]
Q["Z_IDM_EVENTS\n(staging table)"]
H["Z_IDM_EVENT_ASYNC\n(OnNotify Handler)"]

A --> Q
B --> Q
C --> Q
Q --> H
H --> I
  • IDM webhooks handle the happy path: real-time lifecycle changes in the system of record.
  • The Query Event Creator (Z_IDMQRYRUN App Engine) runs any public PeopleSoft query that returns an EMPLID as its first column and generates events for the returned population. This solves three problems that pure event-driven systems always have: backfill after an outage, periodic audit for drift, and targeted remediation of a known bad subset.
  • PeopleSoft component save-events (via Event Mapping, not traditional customization) can fire an audit when security-sensitive data changes – an OPRID save, an academic program status change, an advisor assignment. This is defensive: if something changes PeopleSoft’s view of a user outside the IDM flow, the reconciler gets a chance to re-assert the correct state.

The third producer matters because identity is not a pure downstream projection of IDM. Faculty status is determined by PS_INSTR_ADVISOR, which IDM doesn’t know about. Applicant-vs-student transitions happen in PS_ACAD_PROG. The handler is authoritative for these decisions; IDM can’t be, because IDM doesn’t have the data.

Operational Tooling

Three pieces of operational UI were built on top of the staging table:

  • Event browser (c/Z_IDM.Z_IDM_EVENTS.GBL) – search by EMPLID, status, date, source; drill into the full processing log; resubmit errored events; cancel queued events.
  • Ad-hoc single-subject reconciliation – trigger one event for one EMPLID, with optional dry run and debug flags. This is what production support uses.
  • Configuration page (c/Z_IDM.Z_IDM_CONFIG.GBL) – feature flags for every write path (update PSOPRDEFN, update/delete emails, update GUID, post-back to IDM) plus a global dry-run switch. Every flag is honored by the handler. This is how risky changes roll out.

A set of canned PeopleSoft queries drives the Query Event Creator on a schedule:

  • Z_IDM_ALERT_ERROR_LAST_DAY – finds ERR events from the last 24 hours and retries them.
  • Z_IDM_STUD_NO_OPRID_NO_EVENT – finds active students in PS_ACAD_PROG with a custom-prefixed OPRID missing and no recent event. This catches the edge case where IDM had a user active before PeopleSoft knew about them, re-enrollment didn’t change IDM state, and no event would otherwise be emitted.

The Query Event Creator is also the go-to tool for full-population audit: write a query that returns every active EMPLID, run it once a quarter, let drift correct itself.

Gotchas and Lessons Learned

Mobile clients can’t send SAML. The native mobile app authenticates via a REST service that accepts Basic auth (email + LDAP password) and returns a PS_TOKEN cookie. The LDAP bind lives in a dedicated PeopleCode module (Z_SIGNON_PCODE:LDAPAuthentication) and is gated by %Request.HTTPMethod being populated – which is true for incoming HTTP requests and false when the Integration Broker itself invokes sign-on PeopleCode. Without that gate, the LDAP path would fire for any IB-initiated authentication and widen the attack surface.

Missing SAML attributes arrive as absent, not blank. The IdP sends nothing – not an empty string – when an attribute can’t be resolved. Code that treats "" as the not-found sentinel will misbehave. Defensive reads:

&idmCampus = RTrim(%Request.GetHeader(&c.SAMLAttributeCampus));
If All(&idmCampus) Then
   /* ... */
End-If;

Faculty logic is not IDM-authoritative. Faculty role eligibility is driven by PS_INSTR_ADVISOR inside PeopleSoft. The reconciler queries this during role reconciliation and adds the appropriate campus-specific application-level security (SCRTY_TBL_INST, SCRTY_TBL_SRVC, SAA_SCRTY_AARPT). If you assume IDM is the sole source of truth for all roles, this case will break.

Applicant-to-student transitions need explicit password clearing. An applicant who matriculates gets a new OPRID through the event pipeline, but their legacy applicant account still exists with a password. That’s a backdoor. The reconciler clears the applicant account’s password when the student account activates.

Dry run is not optional. Every write path honors Z_DRY_RUN. During the eight-week rollout, every change to the reconciliation logic was first proven in dry-run mode against a broad population – the logs show what would have changed without actually changing anything. If you skip this during rollout, you will find out what you missed in production, on real accounts.

Results

After go-live:

  • The login path no longer depends on IDM availability. An IDM outage no longer cascades into failed logins.
  • Sign-on PeopleCode shrank to a single concern: OPRID resolution from SAML. It has not needed a change since.
  • Provisioning logic is centralized in one application package (Z_IDM_INTEGRATION) with configuration tables where roles used to be hard-coded constants.
  • Deprovisioning is event-driven. Accounts disabled in IDM lose their PeopleSoft roles within minutes.
  • The same infrastructure handles IDM events, scheduled drift audits, retries, and component-triggered reconciliation – one pattern, many producers.
  • End-to-end latency from an IDM event to a reconciled PeopleSoft account is typically under a minute in production.

Testing changed as much as anything: changes to reconciliation logic are tested in isolation against the handler and the IDM GET, without involving the login path. What was once a high-risk deployment touching critical authentication code is now a low-risk deployment touching a scoped integration.


Author Info
Chris Malek

Chris Malek s a PeopleTools® Technical Consultant with over two decades of experience working on PeopleSoft enterprise software projects. He is available for consulting engagements.

Work with Chris