Repo: darkedges/pingfederate-graph-broker*
Background jobs and AI agents increasingly need to answer questions like "who is in this group?" or "find users starting with Nick". The data lives in Microsoft Entra ID, behind Microsoft Graph. The easy answers are both bad:
- Hand the agent a Microsoft token. Now the agent (and its logs, prompts and tool outputs) can leak a credential that works against Graph.
- Give the agent app-wide permissions. Now it can read the whole directory, not just what a given user is willing to share.
This project takes a third path: a directory broker in front of Graph. Entra tokens stay on the backend. Agents receive directory data only.
It is a runnable Go starter for PingFederate 13.1, delegated Microsoft Graph access and background agents. It is a single-instance starter, not a production-ready identity platform. More on that in part 5.
See it in action
Two permission systems, deliberately separate
The key design idea is that two different things authorise a read:
| Layer | What it authorises | Who grants it |
|---|---|---|
| Microsoft delegated consent | The broker may call Graph as this user | The user, via Entra |
| Broker delegation | This specific agent client may use that connection for these operations until this time | The user, via the broker |
A PingFederate client-credentials token on its own does not select or authorise anyone's connection. An agent must present a PF token and a delegation ID.
The architecture
flowchart TD
P[Portal backend] -->|User token and link intent| B[Directory broker]
P -->|Browser connection flow| F[PingFederate 13.1]
F -->|OIDC code flow| E[Microsoft Entra ID]
B -->|Introspection and attribute pickup| F
B -->|Refresh grant| E
A[Agent backend] -->|PF token and delegation ID| B
B -->|Read-only requests| G[Microsoft Graph]
- Portal backend: where the user signs in via PingFederate and starts the connection flow.
- PingFederate 13.1: authenticates callers, runs the upstream OIDC login to Entra, and couriers the resulting token response to the broker.
- Broker: stores the encrypted delegated tokens, validates delegations, and performs read-only Graph calls.
- Agent backend: calls the broker with a PF token and a delegation ID. It never sees a Microsoft token.
What the agent gets
Only three read operations exist:
directory.find_usersdirectory.find_groupsdirectory.list_group_members
And only these Graph delegated scopes are accepted: User.ReadBasic.All and GroupMember.Read.All (plus OIDC scopes and User.Read). Anything broader is rejected.
An agent call looks like this:
curl -H "Authorization: Bearer ${PF_AGENT_TOKEN}" \
"https://directory-broker.example.com/v1/delegations/${DELEGATION_ID}/users?prefix=Nick"
Principles that shaped the code
- No token-returning endpoint. There is no public API that hands out Microsoft tokens.
-
No generic Graph proxy. No raw
$filter,$selector URLs from callers. - Identity by canonical subject, never by email or UPN.
- Standard library only. No third-party Go dependencies.
Try it without any accounts
docker compose -f compose.demo.yaml up --build -d
# open http://127.0.0.1:8097
The demo simulates PingFederate, Entra and Graph locally, so you can see the whole flow with no credentials.
In the next part: how PingFederate acts as a courier for the Entra token response using the Reference ID adapter.
Top comments (0)