DEV Community

DarkEdges
DarkEdges

Posted on

Agents Should Never Hold Your Tokens: A PingFederate to Microsoft Graph Broker

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:

  1. Hand the agent a Microsoft token. Now the agent (and its logs, prompts and tool outputs) can leak a credential that works against Graph.
  2. 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_users
  • directory.find_groups
  • directory.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"
Enter fullscreen mode Exit fullscreen mode

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, $select or 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
Enter fullscreen mode Exit fullscreen mode

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)