惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Project Zero
Project Zero
F
Fortinet All Blogs
Recent Announcements
Recent Announcements
云风的 BLOG
云风的 BLOG
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
M
MIT News - Artificial intelligence
S
SegmentFault 最新的问题
Blog — PlanetScale
Blog — PlanetScale
T
Tailwind CSS Blog
WordPress大学
WordPress大学
Engineering at Meta
Engineering at Meta
S
Schneier on Security
N
News and Events Feed by Topic
N
News | PayPal Newsroom
H
Help Net Security
C
CXSECURITY Database RSS Feed - CXSecurity.com
T
The Exploit Database - CXSecurity.com
Attack and Defense Labs
Attack and Defense Labs
博客园 - Franky
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
J
Java Code Geeks
A
About on SuperTechFans
AWS News Blog
AWS News Blog
S
Secure Thoughts
The Cloudflare Blog
Hugging Face - Blog
Hugging Face - Blog
爱范儿
爱范儿
C
Cybersecurity and Infrastructure Security Agency CISA
V2EX - 技术
V2EX - 技术
Recorded Future
Recorded Future
Microsoft Azure Blog
Microsoft Azure Blog
博客园_首页
MyScale Blog
MyScale Blog
Martin Fowler
Martin Fowler
Help Net Security
Help Net Security
人人都是产品经理
人人都是产品经理
Latest news
Latest news
C
Cyber Attacks, Cyber Crime and Cyber Security
大猫的无限游戏
大猫的无限游戏
The Last Watchdog
The Last Watchdog
www.infosecurity-magazine.com
www.infosecurity-magazine.com
月光博客
月光博客
H
Hacker News: Front Page
P
Proofpoint News Feed
N
News and Events Feed by Topic
H
Heimdal Security Blog
L
Lohrmann on Cybersecurity
有赞技术团队
有赞技术团队
L
LangChain Blog
Application and Cybersecurity Blog
Application and Cybersecurity Blog

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
Building an Agent that respects User Permissions — With AWS Bedrock AgentCore and Entra ID
Sumanth P · 2026-05-02 · via DEV Community

A practical guide to building an AI agent that queries ServiceNow as the actual user, not a service account, using AgentCore Identity's On-Behalf-Of token exchange.


The Problem Nobody Talks About

Everyone's building AI agents that talk to enterprise systems. But here's the thing most demos skip over: security.

Picture this. You build an agent that helps employees interact with ServiceNow. Jane asks: "Show me 5 incidents assigned to me." Your agent dutifully queries ServiceNow using a service account, filters by Jane's name, and returns results. Looks great in the demo.

Except that service account can see everything — HR complaints, security investigations, executive escalations. If the LLM gets creative with a query, or someone crafts a clever prompt injection, your agent could surface data Jane was never supposed to see. And when the security team checks the audit trail? All they find is "service-account-bot" made the request. Not helpful.

The fix is straightforward in concept: the agent should access ServiceNow as Jane, with Jane's exact permissions. If she can't see HR incidents when she logs into ServiceNow directly, the agent shouldn't see them either.

That's what the On-Behalf-Of (OBO) token exchange pattern does. And AWS recently added native support for it in Bedrock AgentCore Identity.


What We're Building

A Strands-based AI agent running on AgentCore Runtime that:

  1. Authenticates users through Microsoft Entra ID (the company's SSO)
  2. Uses AgentCore Identity's native OBO exchange to swap the user's token for a ServiceNow-scoped token
  3. Calls the ServiceNow REST API carrying the user's delegated identity
  4. Returns only what ServiceNow's ACLs allow that specific user to see

No service accounts. No broad permissions. No custom token exchange code.


The Architecture

The end-to-end flow: user authenticates via Entra ID, AgentCore validates the JWT, AgentCore Identity performs the OBO exchange, and the agent calls ServiceNow with the user's delegated token.

Two tokens are in play:

  • Token A: Issued by Entra ID when Jane logs in. Scoped to the Agent App. Proves Jane's identity but doesn't grant access to ServiceNow.
  • Token B: Issued by Entra ID through the OBO exchange. Scoped to ServiceNow. Still carries Jane's identity. This is what ServiceNow sees.

The agent never holds a service account credential. It never sees data Jane isn't allowed to see. And ServiceNow's audit log shows jane.smith@company.com made the query — not a bot.


How AgentCore Identity OBO Works

Before this feature, you had two options for outbound auth in AgentCore Identity:

  • M2M (client credentials): The agent authenticates as itself. No user identity. ServiceNow sees "the bot."
  • USER_FEDERATION (3-legged OAuth): The user gets redirected to a browser to consent. Preserves identity but requires interactive login.

Neither worked for OBO, where you need to silently exchange an existing user token for a downstream-scoped token — no browser redirect, no consent prompt.

AgentCore Identity now supports a third option: ON_BEHALF_OF_TOKEN_EXCHANGE. When you configure a credential provider with JWT_AUTHORIZATION_GRANT, it sends the inbound JWT as an assertion with grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer. That's exactly what Entra ID's OBO endpoint expects.

The service handles the heavy lifting:

  • Takes the inbound JWT (Token A) from the agent
  • Combines it with the client credentials stored in the credential provider
  • Sends the exchange request to Entra ID's token endpoint
  • Caches the resulting Token B in the Token Vault
  • Returns Token B to the agent

You don't touch the inbound token or manage client secrets in your code.

Important caveat: At the time of writing, the ON_BEHALF_OF_TOKEN_EXCHANGE flow is supported at the API level (GetResourceOauth2Token with oauth2Flow=ON_BEHALF_OF_TOKEN_EXCHANGE), but the Python SDK's @requires_access_token decorator still only accepts "M2M" and "USER_FEDERATION" as auth_flow values. Until the SDK catches up, you'll need to call the IdentityClient directly or use the AWS CLI/boto3 data plane API. The code examples below show both approaches.


Step-by-Step Implementation

Prerequisites

  • A Microsoft Entra ID tenant you can administer
  • A ServiceNow instance with admin access to configure OAuth inbound integrations
  • AWS account with Bedrock model access (Claude Sonnet or Haiku)
  • AgentCore CLI: pip install bedrock-agentcore-starter-toolkit

Part 1: Entra ID and ServiceNow Configuration

Before touching any AWS or agent code, you need to wire up the identity chain between Entra ID and ServiceNow. This involves two Entra ID app registrations and an inbound integration in ServiceNow. (Credit to Greg Biegel's walkthrough for documenting the ServiceNow side of this setup.)

Step 1a: Create the ServiceNow API Connector App Registration (Entra ID)

This app registration represents ServiceNow as a resource in Entra ID. Its client ID becomes the audience claim in Token B.

  1. In the Azure PortalEntra IDApp registrationsNew registration
  2. Name it something like ServiceNow API Connector
  3. After creation, note the Application (client) ID — this is your API_CONNECTOR_CLIENT_ID
  4. Go to Expose an APISet the Application ID URI (e.g., api://{API_CONNECTOR_CLIENT_ID})
  5. Add a scope → name it user_impersonation, set "Who can consent" to "Admins and users", and enable it

Step 1b: Create the Agent App Registration (Entra ID)

This app registration represents your AI agent. Its client ID becomes the audience claim in Token A.

  1. App registrationsNew registration → name it ServiceNow Agent
  2. Note the Application (client) ID — this is your AGENT_APP_CLIENT_ID
  3. Go to Certificates & secretsNew client secret → save the secret value
  4. Go to API permissionsAdd a permissionMy APIs → select ServiceNow API Connector → check user_impersonationAdd permissions
  5. Click Grant admin consent for your tenant

This authorizes the Agent App to perform OBO exchanges for the ServiceNow API Connector scope.

Step 1c: Configure ServiceNow to Accept Entra ID Tokens

This is the part most tutorials skip. Without it, ServiceNow won't know what to do with Token B.

  1. Create an Inbound Integration

    • In ServiceNow admin: System OAuthInbound IntegrationsNew
    • Set the Client ID to your API_CONNECTOR_CLIENT_ID from Step 1a
  2. Create an OIDC Provider Configuration

    • Choose to create a new OIDC provider
    • Set the OIDC Metadata URL to:
     https://login.microsoftonline.com/{TENANT_ID}/v2.0/.well-known/openid-configuration
    
  • Map the user claim: preferred_usernameEmail field in ServiceNow
  • Uncheck JTI verification if your Entra ID tenant doesn't include it in tokens
  1. Create an Auth Scope

    • Name it (e.g., APIConnector)
    • Limit the scope to the Table API — least privilege
  2. Verify user provisioning

    • Entra ID users need matching sys_user records in ServiceNow
    • The email field in sys_user must match the preferred_username claim from Entra ID

Part 2: AWS AgentCore Configuration

Step 2a: Create the OAuth2 Credential Provider

This registers Entra ID as the OAuth provider and configures the OBO exchange. The clientId and clientSecret come from the Agent App Registration (Step 1b):

aws bedrock-agentcore-control create-oauth2-credential-provider \
  --cli-input-json '{
    "name": "EntraIdServiceNow",
    "credentialProviderVendor": "CustomOauth2",
    "oauth2ProviderConfigInput": {
      "customOauth2ProviderConfig": {
        "oauthDiscovery": {
          "discoveryUrl": "https://login.microsoftonline.com/YOUR_TENANT_ID/v2.0/.well-known/openid-configuration"
        },
        "clientId": "YOUR_AGENT_APP_CLIENT_ID",
        "clientSecret": "YOUR_AGENT_APP_CLIENT_SECRET",
        "onBehalfOfTokenExchangeConfig": {
          "grantType": "JWT_AUTHORIZATION_GRANT"
        }
      }
    }
  }'

Enter fullscreen mode Exit fullscreen mode

This single command stores the client secret securely, configures the jwt-bearer grant type, and sets up the Token Vault for caching.

Step 2b: Create a Workload Identity

aws bedrock-agentcore-control create-workload-identity \
  --name "servicenow-agent-workload"

Enter fullscreen mode Exit fullscreen mode

Step 2c: Create the Agent Project

agentcore create --non-interactive \
  --project-name ServiceNowAgent \
  --template basic \
  --agent-framework Strands \
  --model-provider Bedrock

cd ServiceNowAgent

Enter fullscreen mode Exit fullscreen mode


Part 3: Write and Deploy the Agent

Step 3a: Write the Agent Code

Replace the generated src/main.py. Since the @requires_access_token decorator doesn't yet support ON_BEHALF_OF_TOKEN_EXCHANGE as an auth_flow value, we call the IdentityClient directly:

"""
ServiceNow Agent with AgentCore Identity OBO.

Inbound:  Custom JWT Authorizer (Entra ID)
Outbound: ON_BEHALF_OF_TOKEN_EXCHANGE via IdentityClient
"""

import os
import contextvars
import json
import logging

import httpx
from strands import Agent, tool
from strands.models import BedrockModel
from bedrock_agentcore.runtime import BedrockAgentCoreApp, BedrockAgentCoreContext
from bedrock_agentcore.services.identity import IdentityClient

app = BedrockAgentCoreApp()
log = logging.getLogger(__name__)

SNOW_INSTANCE_URL = os.environ.get("SNOW_INSTANCE_URL", "")
MODEL_ID = os.environ.get("MODEL_ID", "us.anthropic.claude-sonnet-4-20250514-v1:0")
AWS_REGION = os.environ.get("AWS_REGION", "us-east-1")

# Request-scoped token storage
snow_token_var: contextvars.ContextVar[str] = contextvars.ContextVar(
    "snow_token", default=""
)

identity_client = IdentityClient(region=AWS_REGION)


async def fetch_snow_token() -> None:
    """
    Perform OBO exchange via AgentCore Identity.

    Gets the workload access token (which wraps the inbound JWT),
    then calls GetResourceOauth2Token with ON_BEHALF_OF_TOKEN_EXCHANGE.
    """
    workload_token = BedrockAgentCoreContext.get_workload_access_token()
    if not workload_token:
        raise RuntimeError("No workload access token. Is inbound JWT auth configured?")

    token_b = await identity_client.get_token(
        provider_name="EntraIdServiceNow",
        scopes=["api://YOUR_API_CONNECTOR_CLIENT_ID/user_impersonation"],
        agent_identity_token=workload_token,
        auth_flow="ON_BEHALF_OF_TOKEN_EXCHANGE",
    )
    snow_token_var.set(token_b)


# -- ServiceNow REST client --

def snow_request(method, endpoint, params=None, json_body=None):
    token = snow_token_var.get()
    if not token:
        return {"error": "No ServiceNow token available."}
    try:
        resp = httpx.request(
            method,
            f"{SNOW_INSTANCE_URL}/api/now/{endpoint}",
            headers={
                "Authorization": f"Bearer {token}",
                "Content-Type": "application/json",
                "Accept": "application/json",
            },
            params=params,
            json=json_body,
            timeout=30,
        )
        resp.raise_for_status()
        return resp.json()
    except httpx.HTTPStatusError as exc:
        status = exc.response.status_code
        if status == 401:
            return {"error": "Token expired. Please re-authenticate."}
        if status == 403:
            return {"error": "Access denied for this operation."}
        return {"error": f"ServiceNow error: {status}"}
    except Exception as exc:
        return {"error": str(exc)}


# -- Tools --

@tool
def query_incidents(
    query: str = "",
    limit: int = 10,
    fields: str = "number,short_description,priority,state,assigned_to",
) -> str:
    """Search for incidents in ServiceNow."""
    return json.dumps(snow_request("GET", "table/incident", params={
        "sysparm_query": query,
        "sysparm_limit": limit,
        "sysparm_fields": fields,
    }), indent=2)


@tool
def get_incident(number: str) -> str:
    """Get details of a specific incident by number."""
    return json.dumps(snow_request("GET", "table/incident", params={
        "sysparm_query": f"number={number}",
        "sysparm_limit": 1,
    }), indent=2)


@tool
def update_incident(sys_id: str, updates: dict) -> str:
    """Update an existing incident."""
    return json.dumps(
        snow_request("PATCH", f"table/incident/{sys_id}", json_body=updates),
        indent=2,
    )


# -- Agent entrypoint --

@app.entrypoint
async def invoke(payload, context):
    try:
        await fetch_snow_token()
    except Exception as exc:
        log.error(f"OBO exchange failed: {exc}")
        yield "Authentication error: could not obtain ServiceNow token."
        return

    agent = Agent(
        model=BedrockModel(model_id=MODEL_ID),
        tools=[query_incidents, get_incident, update_incident],
        system_prompt=(
            "You are a ServiceNow assistant. You help users query and manage "
            "their incidents. You only see records the user is authorized to "
            "access. Be concise."
        ),
    )

    stream = agent.stream_async(payload.get("prompt"))
    async for event in stream:
        if "data" in event and isinstance(event["data"], str):
            yield event["data"]


if __name__ == "__main__":
    app.run()

Enter fullscreen mode Exit fullscreen mode

When the SDK adds ON_BEHALF_OF_TOKEN_EXCHANGE to the decorator, the fetch_snow_token function collapses to five lines with @requires_access_token. The API-level support is already there — it's just the Python decorator that hasn't caught up yet.

Step 3b: Deploy

agentcore configure --entrypoint src/main.py --non-interactive

agentcore deploy -y \
  --env SNOW_INSTANCE_URL=https://YOUR_INSTANCE.service-now.com \
  --env MODEL_ID=us.anthropic.claude-sonnet-4-20250514-v1:0

Enter fullscreen mode Exit fullscreen mode

Step 3c: Configure Inbound JWT Authorizer

aws bedrock-agentcore update-agent-runtime \
  --agent-runtime-id YOUR_RUNTIME_ID \
  --authorizer-configuration '{
    "customJWTAuthorizer": {
      "discoveryUrl": "https://login.microsoftonline.com/YOUR_TENANT_ID/v2.0/.well-known/openid-configuration",
      "allowedClients": ["YOUR_AGENT_APP_CLIENT_ID"],
      "allowedAudiences": ["api://YOUR_AGENT_APP_CLIENT_ID"]
    }
  }'

Enter fullscreen mode Exit fullscreen mode

Step 3d: Update IAM Permissions

aws iam put-role-policy \
  --role-name YOUR_AGENT_EXECUTION_ROLE \
  --policy-name AgentCoreIdentityOBO \
  --policy-document '{
    "Version": "2012-10-17",
    "Statement": [{
      "Effect": "Allow",
      "Action": [
        "bedrock-agentcore:GetResourceOauth2Token",
        "bedrock-agentcore:GetWorkloadAccessTokenForJwt",
        "secretsmanager:GetSecretValue"
      ],
      "Resource": [
        "arn:aws:bedrock-agentcore:REGION:ACCOUNT:token-vault/default/oauth2credentialprovider/EntraIdServiceNow",
        "arn:aws:bedrock-agentcore:REGION:ACCOUNT:workload-identity-directory/default/workload-identity/*",
        "arn:aws:bedrock-agentcore:REGION:ACCOUNT:workload-identity-directory/default",
        "arn:aws:bedrock-agentcore:REGION:ACCOUNT:token-vault/default",
        "arn:aws:secretsmanager:REGION:ACCOUNT:secret:bedrock-agentcore-identity!default/oauth2/EntraIdServiceNow*"
      ]
    }]
  }'

Enter fullscreen mode Exit fullscreen mode

Step 3e: Test

# Without a token — rejected before agent code runs
agentcore invoke '{"prompt": "Show me my incidents"}'
# Expected: AccessDeniedException

# With a valid Entra ID token
agentcore invoke \
  --bearer-token "YOUR_ENTRA_ID_TOKEN" \
  --user-id "jane.smith@company.com" \
  '{"prompt": "Show me 5 incidents assigned to me"}'

Enter fullscreen mode Exit fullscreen mode

What Jane sees:

Incident Description Priority State
INC0012345 VPN connection dropping P2 In Progress
INC0012389 Outlook calendar sync failure P3 New
INC0012401 Laptop BSOD after Windows update P2 In Progress
INC0012455 Shared drive permission error P3 New
INC0012478 Zoom plugin not loading in Teams P4 Assigned

What Jane does not see (blocked by ServiceNow ACLs):

  • INC0012350 — Employee complaint (HR category)
  • INC0012399 — SOC alert (Security category)

These records exist but are invisible to Jane. The agent never receives them — ServiceNow filters before the response leaves the instance.


Tracing the Request

Hop 1: Jane → AgentCore Runtime
The Chat UI attaches Jane's Entra ID token (Token A) as a Bearer header. AgentCore's Custom JWT Authorizer validates it against Entra ID's JWKS endpoint. Invalid tokens are rejected before any agent code runs.

Hop 2: Agent → AgentCore Identity → Entra ID
The agent calls IdentityClient.get_token() with auth_flow="ON_BEHALF_OF_TOKEN_EXCHANGE". AgentCore Identity takes Token A, combines it with the stored client credentials, and sends a jwt-bearer grant to Entra ID. Entra ID returns Token B — same user, different audience.

Hop 3: Agent → ServiceNow
The query_incidents tool calls ServiceNow's REST API with Authorization: Bearer <Token B>. ServiceNow decodes Token B, matches jane.smith@company.com to a sys_user record, and applies her ACLs. Only her authorized incidents come back.

Hop 4: Back to Jane
The LLM formats the results. Jane sees her 5 incidents. The restricted ones never left ServiceNow.


What This Replaces

Before native OBO support, implementing this pattern meant writing roughly 200 lines of custom code: a Starlette middleware to intercept requests, a function to POST to Entra ID's token endpoint, an in-memory cache with TTL management, Secrets Manager integration for the client secret, and JWT parsing for cache keying.

With native OBO, the credential provider configuration handles the protocol and secrets. The agent code just calls get_token() with the right flow type. When the SDK decorator catches up, it'll be even simpler.


Where Else This Works

OBO isn't specific to ServiceNow. Any downstream API that accepts delegated user tokens works the same way:

  • Microsoft Graph — access email, calendar, or OneDrive as the user
  • Salesforce — query CRM records with the user's permissions
  • Internal APIs — anything behind your Entra ID tenant with JWT auth
  • Multi-hop chains — Agent A calls Agent B calls ServiceNow, all carrying the original user's identity

The requirement is that the downstream service accepts tokens from your IdP and enforces authorization based on the subject claim.


Wrapping Up

Building AI agents that access enterprise data is the easy part. Making sure they respect the same access controls as the humans they serve — that's where it gets interesting.

AgentCore Identity's OBO support takes most of the plumbing off your plate. You configure a credential provider, call the API in your agent, and the platform handles the token exchange and caching. Your agent code stays focused on what matters: helping people get things done.

If you're building agents that touch user-specific data in enterprise systems, bake this pattern in from the start. Retrofitting security after the fact is never fun.


References: