Repo: darkedges/pingfederate-graph-broker*
Once a user has linked their Entra account, how does an agent get to use it? Two things must both be true on every request: the caller holds a valid PingFederate token of the right kind, and a user-created delegation covers the call.
Introspect every request
The broker calls PF's /as/introspect.oauth2 for every protected request. There is no caching of authorisation results, so a revoked or expired token stops working immediately.
An accepted response looks like this:
{
"active": true,
"token_type": "Bearer",
"iss": "https://pf.example.com",
"aud": "https://directory-broker.example.com",
"exp": 2000000000,
"client_id": "directory-portal",
"sub": "canonical-user-id",
"scope": "broker.connect",
"broker_principal_type": "user"
}
The broker requires an exact iss, an aud equal to its configured audience, token_type of Bearer, plus exp, client_id and scope.
Two callers, two contracts
| Caller | PF scope | Fixed claim | Allowed client IDs |
|---|---|---|---|
| Portal acting for a user | broker.connect |
user |
PF_PORTAL_CLIENT_IDS |
| Background agent | broker.directory.read |
agent |
PF_AGENT_CLIENT_IDS |
broker_principal_type is not a built-in PingFederate claim. It's a contract this broker introduces, set to a fixed value per client and flow in the access token manager. That prevents an agent token from ever being mistaken for a user token, and vice versa.
The portal uses the authorization code flow with PKCE. Agents use client credentials.
Delegations
A user (via the portal) grants an agent a delegation on one of their connections:
POST /v1/connections/{connection}/delegations
Authorization: Bearer <user token>
{
"agent_client_id": "directory-agent",
"operations": ["directory.find_users", "directory.find_groups", "directory.list_group_members"],
"expires_in_seconds": 86400
}
A delegation binds owner, connection, agent client, operations and expiry. Lifetime is between 60 seconds and 7 days. It is re-validated on every request, and the owner can revoke it with DELETE /v1/delegations/{delegation}.
The agent's view
| Method | Path | Operation |
|---|---|---|
| GET | /v1/delegations/{delegation}/users |
directory.find_users |
| GET | /v1/delegations/{delegation}/groups |
directory.find_groups |
| GET | /v1/delegations/{delegation}/groups/{group}/members |
directory.list_group_members |
curl -H "Authorization: Bearer ${PF_AGENT_TOKEN}" \
"https://directory-broker.example.com/v1/delegations/${DELEGATION_ID}/users?prefix=Nick"
The agent never names a user or a connection. The delegation ID resolves to both, and only if the agent's PF client_id matches the one in the delegation.
Graph calls are narrow by construction
- Graph v1.0
GETrequests only - Fixed page size of 25
- Raw
$filter,$selectand URLs from callers are rejected - Paging cursors are opaque, encrypted and authenticated, expire after 15 minutes and are bound to the delegation and route
- Outbound redirects are disabled, so credentials can't follow a redirect
Failure semantics
The broker distinguishes "the grant is gone" from "something is down":
-
invalid_grant, interaction required, scope escalation or a Graph 401 mark the connectionreconnect_requiredand clear its tokens. - Outages and client-credential errors do not erase a valid grant.
- A Graph 403 does not trigger endless refresh attempts.
Silent connections with SAML Bearer Token Exchange
The browser link flow in part 2 needs the user to go through an Entra login. For cases where that interaction isn't wanted, the broker has an optional path (SAML_ENABLED=true, POST /v1/connections/saml) built on PingFederate's token exchange:
- The portal supplies a PF reference token for the signed-in user.
- PF's SAML Bearer Token Exchange issues a SAML 1.1 assertion for that user.
- The assertion is exchanged at Entra for a token for a custom API.
- Entra's on-behalf-of flow turns that into a Microsoft Graph token.
There's no browser redirect and no consent prompt per connection. The trade-off is that the resulting connection is session-limited: there is no refresh token to store, so it lasts as long as the Graph token does.
Two honest caveats. This path is not yet provisioned or proven against a live PF/Entra environment. And in the existing environment inspected, the Graph token for the demonstrated app carried a write permission, which must not be used unchanged: the broker's read-only rule applies to this path too.
Next: how the broker protects the tokens it holds.
Top comments (0)