Skip to content

ArcadeAI/cisco-duo-arcade

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

9 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Arcade × Cisco Duo — Agent Authorization Demo

The picture. Your team's AI assistant can already read the CRM. A rep asks it to "pull up my customers" — done in seconds. Then they ask for the defence-sector accounts, the most sensitive records you have. An agent that's merely logged in would hand them right over.

This demo makes that moment safe. The agent can use the CRM freely, but the instant a request touches sensitive data, Cisco Duo pushes a real-time approval to the right person's phone — and the records only move once it's approved. Deny it, and the agent is stopped cold.

The shift: the identity and policy you already trust in Duo now govern what your agents can do — every login, every scope, every sensitive action — and Arcade enforces it on every tool call, with no new authorization stack to build. This repo is that, end to end: a Duo-protected CRM, an Arcade MCP server, and the contextual step-up that turns "the agent is logged in" into "the agent is authorized, right now, for this."

Cisco Duo is the central place for your access policies. Arcade enforces them in real time, on every agent tool call.

This repo is a working demo of identity-governed AI agents: Duo owns who can do what, and Arcade — the runtime for MCP tools — makes the agent obey it, in two layers:

  1. Login + per-tool scopes (OIDC). Duo SSO logs the user in (with MFA), Arcade brokers the token, and each MCP tool requires a specific Duo-managed scope.
  2. Contextual step-up MFA (out-of-band). An Arcade Pre-Execution Hook inspects each tool call and, when the request is sensitive (e.g. "show me my defence customers"), triggers a real-time Duo Push via the Duo Auth API before the tool is allowed to run.

Agent triggering contextual step-up MFA


What this unlocks (the art of the possible)

Pairing Cisco Duo (identity + policy) with Arcade (the MCP runtime that brokers auth and runs tools) lets you give agents real, governed access to enterprise systems — without rebuilding auth:

  • Agents inherit your existing Duo policies. The same identities, groups, MFA, and device/risk rules that protect your apps now protect what an agent can do.
  • Per-tool authorization, managed in Duo. Each tool maps to a Duo scope; granting/revoking a scope per user or group instantly changes which tools that user's agent can execute.
  • Real-time, content-aware step-up. Authorization isn't just "who you are" at login — Arcade can require a fresh Duo Push based on what the request is doing (sensitive query, write vs. read, a flagged record), out-of-band, mid-conversation.
  • Revocable and auditable. Revoke in Duo → the agent loses access on the next call. Every tool call flows through Duo's controls and Arcade's policy hooks.
  • No token ever touches the model. Arcade injects credentials at execution time; the LLM and MCP client never see them.

The demo makes this concrete with a tiny Duo-protected CRM and an agent that reads it.


Solution at a glance

Solution architecture: agent -> Arcade (MCP server + pre-execution hook) -> crm_api -> Cisco Duo (OIDC + Auth API)

Full diagrams of both layers: docs/architecture.md.


Table of contents


Repo layout

.
├── README.md
├── docs/
│   ├── architecture.md            # two-layer architecture + diagrams
│   └── images/                    # screenshots used throughout
├── crm_api/                       # Duo-protected CRM (FastAPI) + the contextual-access hook
│   ├── main.py                    # endpoints, scope enforcement, POST /hooks/pre
│   ├── duo_auth.py                # token validation via Duo token introspection
│   ├── stepup.py                  # sensitivity check + Duo Auth API push (duo_client)
│   ├── seed.py                    # in-memory demo contacts (incl. a defence-sector one)
│   ├── requirements.txt
│   └── .env.example
└── crm_mcp/                        # Arcade MCP server (the tools the agent calls)
    ├── pyproject.toml
    ├── .env.example
    └── src/crm_mcp/server.py      # tools, each requires_auth=OAuth2(id="cisco-duo-oidc", ...)

Prerequisites

  • A Cisco Duo account with Duo SSO enabled (Essentials / Advantage / Premier).
  • An Arcade account + API key — https://api.arcade.dev/dashboard.
  • Python 3.10+, uv, the Arcade CLI (uv tool install arcade-mcp).
  • ngrok (or equivalent) to expose the local CRM + hook over HTTPS.
  • A smartphone with Duo Mobile for push approvals.

Setup

Throughout, the Arcade-side steps link to the official Arcade docs rather than duplicating them. The Duo steps and the step-up MFA mechanics are documented here in full, since that's the part this demo adds.

1. Cisco Duo — overview (two apps)

In the Duo Admin Panel (https://admin.duosecurity.comApplications → Application Catalog) you create two separate apps — same account, different types/credentials:

Duo app Purpose Credentials
OAuth 2.1 / OIDC – Single Sign-On Login + scoped tokens (Layer 1) Client ID / Secret + OIDC endpoints
Auth API (entry labeled 2FA) Programmatic Duo Push for step-up (Layer 2) ikey / skey / API host

2. Duo — create the OAuth / OIDC app (login)

  1. Application Catalog → search OIDC → choose "OAuth 2.1 / OIDC – Single Sign-On" (SSO label) → Protect.
  2. General: enable grant type Authorization Code (+ Refresh Token if you want silent refresh). Leave Sign-In Redirect URLs empty for now.
  3. Metadata: copy the Issuer, Client ID, Client Secret. Issuer pattern:
    https://sso-<tenant>.sso.duosecurity.com/oauth2/<app_id>
    
    Endpoints derive from it: /authorize, /token, /userinfo, /token_introspection.
  4. User access → Enable for all users (the default Disable for all users blocks everyone).
  5. Confirm the app Policy requires two-factor authentication at login.

→ Arcade's own write-up of this provider: Cisco Duo auth provider and the generic OAuth 2.0 provider reference.

3. Duo — scopes & access policy

  1. Scopes tab → Add Custom Scope — create crm.contacts.read, crm.contacts.write, crm.deals.read. Duo requires each custom scope to map at least one claim (any benign mapping works — the CRM reads the granted scope list, not these claims). Keep openid, profile, email enabled.
  2. Clients tab → Confidential client scopes — add all six so the client may request them.
  3. Access Policy → Scope Authorization — grant scopes per user / per group. This is the dial: revoke here and the matching tool stops working.

Duo Access Policy — scope authorization

4. Duo — create the Auth API app (step-up MFA)

  1. Application Catalog → search Auth API → choose the 2FA-labeled entry → Protect.
  2. Copy the Integration key (ikey), Secret key (skey), API hostname (api-<tenant>.duosecurity.com).
  3. User access → Enable for all users (otherwise the API returns "Your account does not have access to this application").
  4. The user you push to must be enrolled with Duo Mobile activated (a phone number isn't enough — activate Duo Mobile so the device is push-capable). Note that Duo username → it becomes DUO_STEPUP_USERNAME.

Duo Auth API reference: https://duo.com/docs/authapi (the /preauth and /auth endpoints, and pushinfo).

5. Run the CRM API + expose it

cd crm_api
uv venv .venv && uv pip install --python .venv/bin/python -r requirements.txt
cp .env.example .env          # fill in per "Environment variables" below
.venv/bin/uvicorn main:app --port 8000
ngrok http 8000               # note the https URL

Smoke-test without Duo: set DEV_ALLOW_INSECURE=true, then curl localhost:8000/healthz and curl localhost:8000/hooks/health. Set it back to false for the real flow.

6. Arcade — configure the OAuth provider

Dashboard → OAuth → Add OAuth Provider → OAuth 2.0: ID cisco-duo-oidc, the Duo Client ID/Secret, Authorization URL <issuer>/authorize, Token URL <issuer>/token, scopes openid profile email, PKCE enabled (S256). Save, then paste Arcade's generated Redirect URL into the Duo OIDC app's Sign-In Redirect URLs.

→ Steps: Configure a custom OAuth 2.0 provider.

Arcade custom OAuth provider for Cisco Duo

7. Arcade — deploy the MCP server + gateway

cd crm_mcp
cp .env.example .env          # CRM_API_BASE_URL = your public CRM (ngrok) URL
arcade login
arcade deploy -e src/crm_mcp/server.py

Then create an MCP Gateway and connect your agent.

→ Docs: Build an MCP server · Add user authorization to tools · Deploy · MCP Gateways.

Deployed Duo-protected MCP server in Arcade

8. Arcade — configure the Contextual Access hook

Dashboard → Contextual Access → create a Logic Extension with a Pre-Execution Hook:

  • Endpoint: https://<your-public-host>/hooks/pre
  • Auth: Bearer = your HOOK_BEARER_TOKEN (same value as crm_api/.env)
  • Timeout: ~75s ⚠️ (a synchronous Duo Push can take up to ~60s; the 5s default blocks the call before you can approve)
  • Failure mode: fail closed · Scope: the duo_crm project / tools

→ Docs: Contextual Access · Webhook API reference.

The hook fires on every tool call. If its endpoint is unreachable, all CRM tools fail with "extension endpoint not found" — keep the CRM + tunnel running.


Run the demo

In your agent:

  1. "List my CRM contacts" → returns everyone immediately (no step-up).
  2. "Search my CRM for defence customers" → the hook flags the sensitive query → Duo Push to your phone → approve → Riley Vance @ Aegis Defence Systems. Deny → the tool is blocked.

Scope demo: revoke crm.deals.read from your user in Duo → list_deals returns 403; re-grant → it works again.


Step-up MFA — what the user sees

When a tool call is flagged sensitive, Arcade's Pre-Execution Hook calls crm_api's /hooks/pre, which sends a synchronous Duo Push through the Auth API (factor=push) and waits for the decision. The push carries the request context via Duo's pushinfo parameter, so the approver sees exactly what the agent is trying to do:

Duo Mobile step-up push notification

In this example the Duo Mobile prompt shows:

  • Title: Sensitive CRM access: Auth API
  • App: Arcade.dev · User: demouser
  • Context (pushinfo): from: Acme CRM · tool: ListContacts · query: defence
  • Actions: Approve / Deny

The flow:

  1. Agent calls a tool (e.g. list_contacts(query="defence")).
  2. Arcade invokes the Pre-Execution Hook → POST /hooks/pre with the tool name, inputs, and user context.
  3. The hook checks the inputs against SENSITIVE_QUERY_TERMS. Not sensitive → returns {"code":"OK"} immediately (no push). Sensitive → continues.
  4. The hook preauths the Duo user, then sends a Duo Push with pushinfo (from, tool, query) and blocks until the user responds.
  5. Approve → hook returns {"code":"OK"} → the tool runs. Deny / timeout → returns {"code":"CHECK_FAILED", "error_message": ...} → Arcade blocks the call and the agent surfaces the reason.

This is out-of-band, content-aware step-up: it layers on top of the static scopes from login, and the decision is driven by what the request is doing, not just who's asking. Which terms trip it is configurable via SENSITIVE_QUERY_TERMS; the Duo username pushed to is DUO_STEPUP_USERNAME.


Environment variables

crm_api/.env

# --- Duo OIDC app (login / token validation) ---
DUO_OIDC_ISSUER=https://sso-<tenant>.sso.duosecurity.com/oauth2/<app_id>
DUO_CLIENT_ID=<oidc app client id>
DUO_CLIENT_SECRET=<oidc app client secret>
DUO_INTROSPECTION_URL=                 # optional; defaults to <issuer>/token_introspection

# --- Contextual step-up MFA (Duo Auth API) ---
DUO_AUTH_IKEY=<auth api integration key>
DUO_AUTH_SKEY=<auth api secret key>
DUO_AUTH_HOST=api-<tenant>.duosecurity.com
DUO_STEPUP_USERNAME=<enrolled Duo username to push to>
HOOK_BEARER_TOKEN=<random secret; must match the Arcade Logic Extension bearer>
SENSITIVE_QUERY_TERMS=defence,defense,classified,military,intelligence

# --- Local testing only ---
DEV_ALLOW_INSECURE=false               # true = skip token validation (CRM only)
DEV_SCOPES=openid profile email crm.contacts.read crm.contacts.write crm.deals.read
STEPUP_DEV_MODE=                       # "allow"/"deny" = simulate the push without real Duo

crm_mcp/.env

CRM_API_BASE_URL=https://<your-public-host>   # the ngrok https URL of the CRM

.env files are git-ignored. Use arcade secret set / the Dashboard for the deployed MCP's secrets.


How it works

  • Token validation & scopes (Layer 1): crm_api/duo_auth.py calls Duo token introspection (authenticated with the OIDC client creds) to validate the bearer token and read the granted scope. crm_api/main.py maps each endpoint to a required scope → 403 if missing. Duo decides what's granted; the CRM enforces it.
  • Contextual step-up (Layer 2): crm_api/main.py exposes POST /hooks/pre (Arcade's hook contract). crm_api/stepup.py runs the sensitivity check and the Duo Auth API push via duo_client.
  • MCP server: each tool declares requires_auth=OAuth2(id="cisco-duo-oidc", scopes=[...]) and forwards the Arcade-injected token to the CRM — no auth code; Arcade brokers the Duo OAuth flow.

Troubleshooting

Symptom Cause / fix
Duo: "Must specify both 'code_challenge' and 'code_challenge_method'" Enable PKCE (S256) on the Arcade provider — Duo OAuth 2.1 requires it.
Duo: "no scopes … configured for the specified Relying Party" Add the scopes to the client's Confidential client scopes (Clients tab).
Login link 403 / no access OIDC app User access = Disable for all users — set Enable for all users.
Step-up: "Your account does not have access to this application" Enable User access on the Auth API app.
Step-up: 400 no capable device / preauth enroll DUO_STEPUP_USERNAME has no activated Duo Mobile push device — enroll/activate it.
Push never arrives DUO_STEPUP_USERNAME isn't the enrolled Duo username (often not the email — check Admin → Users).
Agent: "extension endpoint not found" The CRM/hook or tunnel is down, or the Logic Extension URL no longer matches the tunnel.
Blocked before you can approve Arcade hook timeout too low — raise to ~75s.
Tool returns 403 "Missing required scope" Expected when the user lacks that scope in Duo. Grant it (and re-authorize).

Production notes

  • Run the contextual-access hook as its own service (not co-located in the CRM), behind stable HTTPS with Bearer or mTLS.
  • Prefer async Duo Push (async=1 + /auth_status polling) bounded under the Arcade hook timeout, instead of a long synchronous call.
  • Map the Arcade user identity to the Duo username deterministically (the demo uses a single DUO_STEPUP_USERNAME).
  • Consider Verified Duo Push / risk-based policies for higher-assurance step-up.
  • Treat all DUO_* keys and HOOK_BEARER_TOKEN as secrets.

Tests

A dev-mode smoke suite exercises both authorization layers without contacting Duo. Token validation runs in DEV_ALLOW_INSECURE mode and the step-up is simulated with STEPUP_DEV_MODE, so the tests cover the per-tool 403 and the step-up approve and deny paths offline.

cd crm_api
uv venv .venv && uv pip install --python .venv/bin/python -r requirements-dev.txt
.venv/bin/pytest

Arcade documentation


License

This project is released under the MIT License. See LICENSE.

About

Policy enforcement demo using Arcade and Cisco Duo.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages