Table of Contents
What Descope adds to Vertex AI
Vertex AI Agent Engine is Google's managed runtime for agents built with the Agent Development Kit (ADK). Google's Agent Identity gives each agent a strongly attested, SPIFFE-based cryptographic identity, with an auth manager that stores API keys, OAuth client credentials, and delegated end-user tokens, and Google Cloud access tokens cryptographically bound to the agent's X.509 certificates. For agents living on Google Cloud and calling Google services, it's an excellent foundation.
But the management plane is Google Cloud, and your entire agent fleet is likely not on Google Cloud. Moreover, the authorization model is Google Cloud IAM: roles granted to identities ahead of time, with the per-user question (should this analyst, through this agent, write to your CRM?) left to your application code. For customer- and workforce-facing auth, Google Identity Platform handles sign-in, but it is not an authorization server for your resources: no client registration, no consent screens, no scope issuance for your APIs, no Dynamic Client Registration (DCR), and no Client-Initiated Backchannel Authentication (CIBA).
Bottom line: Google's stack tells you which workload is calling and what IAM lets it touch in Google Cloud. Deciding per request what each agent may do against your application, for which user, with whose approval, is the layer you still own.
Descope provides that layer: cloud-neutral agent identity management, issuance-time policy, a credential vault that works across every runtime, and an authorization server that covers what Google Identity Platform doesn't.
What Descope adds to Vertex AI
The Descope Agentic Identity Hub provides several capabilities (Policies, Connections, cloud-neutral agent directory) that address specific gaps in Google Identity Platform’s native identity capabilities.
Cloud-neutral agent directory
Google's Agent Identity solves the problem it targets well: each agent gets an attested SPIFFE identity instead of a shared service account, with no long-lived keys and tokens bound to the agent's certificates.
However, what that identity governs is Google's world. The agent identity is a Google Cloud object, managed through Google's control plane, and the tokens bound to it are currency for Google Cloud resources. And the authorization grammar is IAM: roles granted ahead of time, with no evaluation at issuance of whether this user invoking this agent should yield a token carrying this scope against your API.
The Descope Agentic Identity Hub is a single directory for every agent in your fleet: the Vertex AI agent, the Bedrock AgentCore agent, the Foundry agent, the LangGraph prototype. Each entry carries issuance-time policy and a credential vault, and the integration point with any runtime is standard OAuth.

Descope Policies handle agent token requests by evaluating the directory entry, the invoking user’s roles and tenant, and the requested scopes. Policies decide token contents at issuance, before the token exists. An agent can’t escalate by asking, and users can’t delegate what they don’t hold. An agent can't escalate by asking, and users can't delegate what they don't hold. The unit of permission is scope, not the Google Cloud IAM role or the SSO group; those feed the decision as inputs rather than acting as grants. One model pushes per-user enforcement into every API you own; the other centralizes it where the token is born.

One credential manager
Google's Agent Identity auth manager stores API keys, OAuth client credentials, and delegated end-user tokens for its agents, and that's worth crediting: Google reached the same conclusion everyone operating agents reaches, that agents need a credential broker. The differences are scope and policy.
The auth manager serves agents on Google's platform; Descope Connections serves the whole fleet, with the same vault and the same token exchange for the agent on Agent Engine, the one on AgentCore, and the prototype on a laptop. And every exchange is governed by Connections Policies, evaluated when the Descope token is traded for the vaulted credential, so retrieval is an authorization decision, not just a lookup. Nothing long-lived sits in any agent's environment, and the raw secret never enters a model's context.

Descope also handles authentication for users. Federate your corporate IdP through SSO, and agent access dies with the directory account when someone leaves. Passkeys, magic links, one-time passwords (OTP), and social login are available for customer-facing agents. The agent Security Token Service (STS) issues short-lived, delegated tokens carrying user context without user credentials, and Descope Flows handle the consent screens, step-up, multi-factor authentication (MFA), and CIBA out-of-band approvals for headless agents.

Resource authorization beyond Google Identity Platform
Your MCP servers and backend APIs need an authorization server, and Identity Platform isn't one. It authenticates users (it's Google's enterprise evolution of Firebase Auth) and issues ID tokens for sign-in, but it has no client registration for the MCP clients connecting to your servers, no consent flow, no scope model for your APIs, no DCR, and no CIBA. The authorization server in front of your resources is bring-your-own on Google Cloud.

Descope MCP Auth implements the MCP authorization spec with OAuth 2.1, including DCR and Client ID Metadata Documents (CIMD), which allows off-the-shelf clients like Claude Desktop to register themselves. The same project protects backend APIs with resource-level access control, CIBA for async approval, and per-tenant SSO that customers configure themselves.
Why use both Google Agent Identity and Descope
Descope augments the Google stack by adding per-request application authorization, fleet-wide credential management, and the resource authorization server Google leaves to you. Agent Identity keeps doing what it alone can do.
There are a couple of things no external platform can replicate. Only Google can attest what's running inside its own platform: the SPIFFE identity is provisioned by the runtime, can't be impersonated, and binds Google Cloud tokens to the agent's certificates so a stolen token is useless off the workload. And for agents calling Google services, the authorization path is IAM end to end; when your agent queries BigQuery, that's the right system doing its job.
In a model using both Descope and Google, Agent Identity is the runtime's identity layer: workload attestation inside Google and IAM authorization against Google services. Descope is the fleet's directory and policy layer: per-request application authorization, decided at issuance, plus the vault for every credential Google doesn't broker. They meet wherever your application's tokens flow, and neither replaces the other.
The same division applies to auditing. Cloud Audit Logs record every IAM-authorized call, and Agent Engine's logging records user identity and agent identity together for delegated access, which is genuinely ahead of its peers; that infrastructure layer stays. What it can't record is the decision about your application tokens' contents, because IAM roles are granted ahead of time, and the per-user judgment happens downstream in your API.
Descope re-makes that decision at every issuance and logs it with its inputs (user, roles, tenant, agent, scopes granted and refused), so a denied scope is a structured event rather than an absence in your API logs, and a CIBA approval links the approver and the action to the token it produced, in one schema across the whole fleet.
Integrating Descope and Vertex AI
The full flow has three phases: the user authenticates and consents in the browser, the app invokes the agent with the resulting token in session state, and the agent's tools fetch scoped credentials as they work. We'll take them one at a time.
Getting started
To get started building with Descope and Vertex AI:
Create a Descope project. Sign up and copy your Project ID from Settings > Project.
Connect your corporate IdP. In the console, select your tenant under Tenants, then navigate to the SSO Setup Suite Configurations and generate a link for the SSO Setup Suite. Go to that link, and follow the setup guide for your Corporate IdP within the SSO Setup Suite to configure SSO and the user and group attributes so IdP groups map to Descope roles.
Create an Agentic Client. Go to Clients, click + Add Client, and name it. Open the app's Connection Information section and copy three system-generated values you'll use below: Client ID, Client Secret, and Discovery URL. You can tag each by runtime if you want to as well.
Define your Resources. Go to Resources, click + Resource, and choose API or MCP Server. Set the Resource identifier (for example
https://crm.internal.example.com), define its OAuth scopes (crm:read,crm:write), and use Role association to map each scope to the Descope roles allowed to hold it. This mapping is the policy the control plane enforces at token issuance.Configure Connections for third-party and internal services. Go to Outbound Apps, click + Outbound App, and pick from the library (for example Google Calendar) or create a custom connection for an internal API key. Note the Outbound App ID; it's the
app_idyour code passes when fetching credentials.Front your MCP servers with Descope MCP Auth and connect them to your ADK agent through MCPToolset (steps below).
Pass the user's Descope access token into the Agent Engine session state on each invocation, and replace any hardcoded credentials in tool implementations with Connections token fetching or Resource token exchange (code below).
The docs for MCP Auth, Resources, and Connections cover this configuration in more depth.
User authentication and agent invocation

Your app runs a standard OIDC authorization code flow with Descope as the provider. The user authenticates and consents in the browser before invocation; the Using Inbound Apps guide walks through the authorization code flow step by step, with the Authorization Server reference covering the endpoints. For browserless clients, the device code flow fills the same role. CIBA handles scenarios where the agent needs mid-session approval that no prior consent covers.
The Vertex-specific part is what you do with the resulting access token. Agent Engine's own invocation auth stays Google (your app authenticates with its Google credentials), and the Descope token rides into the agent's session state, where every tool can reach it through ToolContext.
Deploy the agent with its own service account. Give each agent a dedicated service account rather than the shared platform default; it's what makes the agent individually identifiable in IAM, in Cloud Audit Logs, and in the Descope federation below.
gcloud iam service-accounts create ops-assistant-agent \
--display-name="Ops Assistant Agent"
gcloud projects add-iam-policy-binding $GCP_PROJECT \
--member="serviceAccount:ops-assistant-agent@$GCP_PROJECT.iam.gserviceaccount.com" \
--role="roles/aiplatform.user"Pass it at deployment (agent_engines.create(..., service_account="ops-assistant-agent@...") or the equivalent flag in adk deploy agent_engine). There's nothing to configure for Agent Identity itself; the SPIFFE identity is provisioned by the platform.
Authorize the invoking app. Your web app's identity (its own service account, or your user credentials in development) needs permission to query the agent:
gcloud projects add-iam-policy-binding $GCP_PROJECT \
--member="serviceAccount:webapp@$GCP_PROJECT.iam.gserviceaccount.com" \
--role="roles/aiplatform.user"With IAM in place, the invocation carries the Descope token in session state:
import vertexai
from vertexai import agent_engines
vertexai.init(project=os.getenv("GCP_PROJECT"), location="us-central1")
remote_agent = agent_engines.get(os.getenv("AGENT_ENGINE_RESOURCE_NAME"))
# After the OIDC callback yields access_token:
session = remote_agent.create_session(
user_id=user_id,
state={"descope_access_token": access_token},
)
for event in remote_agent.stream_query(
user_id=user_id, session_id=session["id"], message=user_prompt,
):
handle(event)import vertexai
from vertexai import agent_engines
vertexai.init(project=os.getenv("GCP_PROJECT"), location="us-central1")
remote_agent = agent_engines.get(os.getenv("AGENT_ENGINE_RESOURCE_NAME"))
# After the OIDC callback yields access_token:
session = remote_agent.create_session(
user_id=user_id,
state={"descope_access_token": access_token},
)
for event in remote_agent.stream_query(
user_id=user_id, session_id=session["id"], message=user_prompt,
):
handle(event)Connecting a Descope-protected MCP server to the agent uses ADK's MCPToolset, with the bearer carried in the connection headers; the server validates it like any bearer.
from google.adk.agents import Agent
from google.adk.tools.mcp_tool.mcp_toolset import MCPToolset, StreamableHTTPConnectionParams
internal_tools = MCPToolset(
connection_params=StreamableHTTPConnectionParams(
url="https://mcp.internal.example.com/mcp",
headers={"Authorization": f"Bearer {descope_access_token}"},
),
)
agent = Agent(
model="gemini-2.5-pro",
name="ops_assistant",
instructions="You help employees with tickets, CRM lookups, and reports.",
tools=[internal_tools],
)Making the agent's Descope identity primary, without a Descope secret. This is optional; the client-secret path works fine. Federation brings you two benefits: no Descope secret exists anywhere in the agent's environment, and the agent's Descope access becomes derived from its Google identity.
This means revoking the agent's service account kills its ability to authenticate to Descope in the same stroke. The agent's runtime can mint a Google-signed OIDC ID token for its service account (issuer https://accounts.google.com) with an audience you choose, and Descope's JWT Bearer grant accepts it as the assertion:
Know which service account the token will represent. It's the one you deployed the agent with above; copy its email and its numeric Unique ID from IAM & Admin > Service Accounts. In the Google-signed ID token, the SA's email appears in the email claim and the unique ID in sub, which is what the trusted-issuer mapping keys on.

On the agent's client in Clients, enable the JWT Bearer grant type, click Manage, click + Add Issuer and add a trusted issuer with Issuer URL
https://accounts.google.com. The Workload Identity docs cover the Google configuration.At runtime, request an ID token from the metadata server (or the IAM Credentials API) with your chosen audience, and present it to Descope:
import requests
METADATA_URL = ("http://metadata.google.internal/computeMetadata/v1/"
"instance/service-accounts/default/identity")
google_id_token = requests.get(
METADATA_URL, params={"audience": "descope-agent-federation"},
headers={"Metadata-Flavor": "Google"},
).text
resp = requests.post(DESCOPE_TOKEN_URL, data={
"grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
"client_id": os.getenv("DESCOPE_CLIENT_ID"), # the agent's Descope client
"assertion": google_id_token,
})
resp.raise_for_status()
descope_agent_token = resp.json()["access_token"]Fetching credentials inside ADK tools

Inside ADK tools, pull the user's token from ToolContext state, then fetch the right credential for the destination. For Descope-protected resources, the tool performs OAuth Token Exchange. The control plane checks the user, agent, and requested scope; a refused scope never becomes a token.
from google.adk.tools import ToolContext
DESCOPE_TOKEN_URL = "https://api.descope.com/oauth2/v1/apps/token"
def exchange_for_resource_token(user_access_token: str, resource: str, scopes: list[str]) -> str:
"""RFC 8693: trade the user's access token for a scoped access token
targeting a Descope-protected resource. Policy is applied at issuance."""
resp = requests.post(DESCOPE_TOKEN_URL, data={
"grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
"client_id": os.getenv("DESCOPE_CLIENT_ID"),
"client_secret": os.getenv("DESCOPE_CLIENT_SECRET"),
"subject_token": user_access_token,
"subject_token_type": "urn:ietf:params:oauth:token-type:access_token",
"resource": resource, # Resource identifier from the console (RFC 8707)
"scope": " ".join(scopes),
})
resp.raise_for_status()
return resp.json()["access_token"]
def query_crm(tool_context: ToolContext) -> dict:
user_access_token = tool_context.state.get("descope_access_token")
crm_token = exchange_for_resource_token(
user_access_token, "https://crm.internal.example.com", ["crm:read"]
)
headers = {"Authorization": f"Bearer {crm_token}"}
return requests.get(f"{os.getenv('CRM_API')}/records", headers=headers).json()For third-party services, OAuth tokens, or static API keys, the tool fetches from the Connections vault with the outbound application SDK.
from descope import DescopeClient
descope_client = DescopeClient(project_id=os.getenv("DESCOPE_PROJECT_ID"))
def get_calendar_information(tool_context: ToolContext) -> dict:
"""Third-party OAuth: fetch the user's vaulted Google Calendar token from Connections."""
user_access_token = tool_context.state.get("descope_access_token")
user = descope_client.validate_session(session_token=user_access_token)
vaulted = descope_client.mgmt.outbound_application.fetch_token(
app_id="google-calendar", # Outbound App ID from the console
user_id=user["sub"],
)
headers = {"Authorization": f"Bearer {vaulted['accessToken']}"}
return requests.get(
"https://www.googleapis.com/calendar/v3/calendars/primary/events",
headers=headers, params={"maxResults": 10, "orderBy": "startTime", "singleEvents": True},
).json()
def call_internal_api(tool_context: ToolContext) -> dict:
"""Static API key: same vault, same fetch, API-key connection."""
user_access_token = tool_context.state.get("descope_access_token")
user = descope_client.validate_session(session_token=user_access_token)
vaulted = descope_client.mgmt.outbound_application.fetch_token(
app_id="internal-reporting-api",
user_id=user["sub"],
)
headers = {"X-API-Key": vaulted["accessToken"]}
return requests.get(f"{os.getenv('INTERNAL_API_URL')}/reports", headers=headers).json()Gating sensitive actions with CIBA

For sensitive actions, CIBA provides out-of-band human approval. This runs as an ADK tool: when the agent decides an elevated operation is needed (an account reset, a payment), the tool blocks until a human approves. Google's stack has no equivalent grant, so natively you would build this with a notification service, an approvals table, and polling logic. Descope delivers the approval as an email link with a roughly three-minute expiration. The initiation endpoint is published in your Inbound App's discovery document as backchannel_authentication_endpoint.
def request_step_up(login_hint: str, binding_message: str) -> dict:
"""CIBA: out-of-band human approval for a sensitive action."""
init = requests.post(BACKCHANNEL_AUTH_URL, data={
"client_id": os.getenv("DESCOPE_CLIENT_ID"),
"client_secret": os.getenv("DESCOPE_CLIENT_SECRET"),
"login_hint": login_hint,
"binding_message": binding_message, # shown to the user with the approval request
"scope": "openid",
}).json()
for _ in range(18): # approval links expire after ~3 minutes
time.sleep(10)
poll = requests.post(DESCOPE_TOKEN_URL, data={
"grant_type": "urn:openid:params:grant-type:ciba",
"client_id": os.getenv("DESCOPE_CLIENT_ID"),
"client_secret": os.getenv("DESCOPE_CLIENT_SECRET"),
"auth_req_id": init["auth_req_id"],
})
if poll.status_code == 200:
return {"status": "approved", "token": poll.json()["access_token"]}
if poll.json().get("error") not in ("authorization_pending", "slow_down"):
return {"status": "denied"}
return {"status": "timeout"}Start building with Descope and Vertex AI
Agent Engine gives your agents a production runtime, and Agent Identity gives them attested credentials for Google Cloud. Descope gives them one identity layer across every cloud they touch, with policy evaluated at token issuance. Together, they separate where an agent runs from what it's allowed to do.
Sign up and explore the docs for Agentic Identity, MCP Auth, Connections, and Resources. Running agents on more than one cloud and want a unified directory for all of them? Reach out to our team or connect with us on our dev community, AuthTown.

