DEV Community

DarkEdges
DarkEdges

Posted on

Introspection and Delegations: Authorising Agents Per User, Per Operation

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"
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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 GET requests only
  • Fixed page size of 25
  • Raw $filter, $select and 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 connection reconnect_required and 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:

  1. The portal supplies a PF reference token for the signed-in user.
  2. PF's SAML Bearer Token Exchange issues a SAML 1.1 assertion for that user.
  3. The assertion is exchanged at Entra for a token for a custom API.
  4. 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)