Skip to main contentArrow Right

Table of Contents

The majority of access requests in large organizations today come from non-human identities (NHIs): services, agents, API clients, and machine-to-machine tokens with real privileges. Oasis Security estimates the ratio of non-human to human identities has already reached 20:1, and that number is only climbing.

These identities touch production data, initiate transactions, and make decisions on behalf of users. But unlike humans, NHIs lack visibility. Typical human sessions start and end cleanly, but agentic identities operate continuously. They dynamically register, request scopes, call APIs, refresh tokens, and can even persist after the authorizing user disappears. 

The NHI lifecycle can feel like a black box, which is why auditing and monitoring agentic identities has become essential. Descope’s agentic identity control plane offers visibility that will help you investigate, respond, and harden your systems. Through granular audit events, developers and operators can observe a discernable feedback loop: when new apps are registered, when consents expand, when external connections are made, and when rules are violated. 

This blog walks through:

  • How the Agentic Identity Hub handles the lifecycle of an agentic identity.

  • How Descope audit events give developers and operators insight into every step of that lifecycle.

  • A concrete technical example: a Model Context Protocol (MCP) server integrating with Snowflake, demonstrating how the Agentic Identity Hub secures automation at scale.

The lifecycle of an agentic identity

To understand the audit surface, we first need to understand the constructs that define an agent’s lifecycle:

  1. Registration: The agent is created and introduced into your system. It needs to identify itself and request permissions.

  2. Consent: A user or tenant admin must approve what the agent is allowed to do on their behalf by granting scopes.

  3. Connectivity: The agent connects to external systems to carry out its tasks.

  4. Termination: When the agent is no longer needed, its credentials must be revoked and its access retired.

Agentic Identity Lifecycle Slide-min
Fig: Agentic identity lifecycle

These stages are common across industries and implementations, whether you’re building in-house automations or externally accessible MCP servers. Descope’s Agentic Identity Hub provides constructs that map directly to this lifecycle:

Inbound Apps

An Inbound App represents an agent or application that authenticates into your system. Each Inbound App requests scopes and requires consent to act on behalf of a user or tenant. They’re dynamically registered (via Dynamic Client Registration (DCR)) or manually provisioned, and they’re auditable from the moment they appear.

Outbound Apps

Outbound Apps represent secure connections to external services, like Stripe, Salesforce, or Slack. Once configured, outbound apps act as a token vault for API keys and OAuth tokens. Clients (inbound apps or agents) never have to handle raw credentials. Instead, they request access to the stored tokens during runtime.

For example, a Stripe Outbound App might hold a tenant’s Stripe API Key. An MCP client automating refunds would dynamically request access to that token to call the Stripe API, but only if policies allow.

Agentic Identity Control Plane

Rules are the binding fabric between Inbound and Outbound Apps. They define who (which Inbound App) can access what (which Outbound App token) under what conditions. These rules enforce least privilege dynamically, ensuring each identity only gets what it needs to do its job.

Each of these pieces is dynamic. New Inbound Apps often appear via dynamic client registration (DCR). Consents are granted, expanded, and revoked. Outbound connections are created, refreshed, and expired. Rules evolve as policies change.

This fluidity is a feature, but it also creates volatility, and auditing is the stabilizing counterweight. By recording each change with context, Descope ensures you can replay the history of identity events, diagnose issues, and prevent silent drift.

Also read: Introducing the Agentic Identity Control Plane

Why monitoring and auditing matter

The scale of non-human identity usage creates three security and operational concerns:

  1. Instruction correctness. Is the agent doing what the user or tenant actually asked? Or is it improvising because of a bad configuration, outdated consent, or even malicious injection?

  2. Scope minimization. Does the agent have the minimal scopes required? Over-provisioned scopes create massive risk surfaces.

  3. Feedback closure. Humans create natural audit trails: log in, take action, log out. Agents don’t. Without monitoring, an agent could run continuously, changing behavior over time without visibility.

Descope’s research shows 57% of CIAM decision makers are worried about AI agents sharing sensitive data with unauthorized users. Robust monitoring and auditing helps close that loop.

Auditing across the agentic AI lifecycle

With the lifecycle in mind, let’s look at how Descope’s auditing framework provides visibility at every stage.

Stage 1: Registration

When a new agent is registered, you need to be able to answer the most basic question: “Who is connecting to my system?”

  • Agents often self-register dynamically via DCR. These events are logged (ThirdPartyApplicationCreated), showing app metadata, requested scopes, and verification status.

  • Offboarding is equally important. If an inbound app is deleted unexpectedly, Descope records a ThirdPartyApplicationDeleted event, giving teams immediate insight into whether offboarding was intentional or accidental.

Without these registration logs, agents could silently connect or disconnect, creating blind spots in your security posture.

Stage 2: Consent management

Once registered, agents request scopes and require user consent to act. Auditing consent evolution is critical. For example, if a payroll integration requests read:payroll, but later an admin modifies consent to include write:payroll: “Who approved that escalation?

  • If a payroll integration first requests read:payroll but later an admin adds write:payroll, the ThirdPartyAppConsentModified event captures who approved that escalation.

  • Historical consent logs help teams prove that sensitive access was explicitly authorized. For example, a healthcare SaaS platform integrating with Snowflake may need to show that no agent accessed tables containing PHI without explicit tenant consent. During a HIPAA audit, Descope’s logs can demonstrate that the session:role:PHI_ACCESS scope was only granted after a tenant admin’s approval, and later revoked when the integration was offboarded.

Consent drift is where over-provisioning risks emerge. Without logs, you won’t see these changes happening over time.

Stage 3: Tool calling

Once an agent is active, it often needs to connect with third party integrations. An agent running inside your system may call APIs from Stripe or GitHub, or a tool from another MCP server. Auditing outbound connections means you can answer:

  • When was a connection to an external system added or removed?

  • Did a change in outbound configuration coincide with unexpected agent activity?

This level of traceability becomes crucial during incident response.

Stage 4: Policy enforcement and violations

Here’s where things get especially interesting. Descope’s policy engine not only enforces rules, but also logs when an agent tries to do something it shouldn’t. This is when configuration, consent, or role mappings drift in a way that makes the agent believe it can act, but the policy engine blocks it.

Here’s an example:

  • A user consents to read:transactions and write:transactions.

  • The user later switches companies and no longer has the FinanceAdmin role.

  • The agent, unaware of this role change, later attempts to write:transactions.

  • The policy engine denies the request, and logs OutboundAppAccessControlAccessDenied.

This tells you two things:

  1. The agent was misconfigured (or manipulated) enough to attempt an unauthorized action.

  2. The policy engine successfully blocked it, preventing damage.

Without logging, you wouldn’t know that something in your configuration stack caused an agent to overreach.

Example: Building an MCP server with Snowflake Tools

To make this concrete, let’s walk through a full example:

We’ll build an MCP server that provides analytics and reporting for enterprises. One of its tools will securely connect to Snowflake to execute SQL queries while ensuring strong authentication, least-privilege access, and complete auditing.

Initialize an MCP server with Descope authentication

We’ll start by securing our MCP server so only authorized clients can access its tools. Using the Descope MCP Express SDK, we’ll add a middleware layer that validates MCP client tokens:

import "dotenv/config";
import express from "express";
import {
  descopeMcpAuthRouter,
  descopeMcpBearerAuth,
} from "@descope/mcp-express";

const app = express();

// Add the MCP authentication router
app.use(descopeMcpAuthRouter());

// Protect all /mcp routes with Descope MCP Bearer auth
app.use(["/mcp"], descopeMcpBearerAuth());

// Start the MCP server
app.listen(3000, () => {
  console.log("MCP server listening on port 3000");
});

This ensures that every request to /mcp routes is authenticated and carries a valid token issued by Descope.

Create Inbound App and consent flow

Next, we’ll configure our Descope project to enable the MCP client/agent to register dynamically:

  • Approved scopes: scopes such as query:write and table:read.

  • User consent: Tenant admins explicitly approve which roles the MCP agent may assume when executing Snowflake queries. You can tie scopes to user roles, so only a System Admin can delegate access to the query:write scope.

Fig: Configuring Dynamic Client Registration settings in Descope
Fig: Configuring Dynamic Client Registration settings in Descope

This consent flow ensures the MCP server only gains access to the scopes explicitly authorized by the tenant, enforcing least-privilege access.

  1. Create and connect Outbound App to Snowflake

Instead of storing raw Snowflake credentials, the MCP server delegates access through Descope’s Outbound App Connect flow action:

  • Configure Snowflake as an Outbound App in Descope, using the Snowflake Outbound App Template with the refresh_token and session:role:SYSADMIN scopes

  • Define Access Control Rules that tie the MCP agent, user roles, and Snowflake scopes together

Fig: Setting up Outbound App configuration in Descope
Fig: Setting up Outbound App configuration in Descope

We will define an access control rule dictating that dynamically registered agents can access tokens with the session:role:SYSADMIN scope only if they are acting on behalf of users with the System Admin role. This user role can be assigned directly in Descope, or mapped from an SSO group when the user authenticates with their SSO provider, before they provide consent.

Fig: Creating an access control rule
Fig: Creating an access control rule

During the consent flow, after authenticating and delegating access to the agent:

  • The admin logs in via Snowflake’s OAuth screen.

  • The admin selects which Snowflake scopes to grant.

  • Descope receives and securely stores OAuth tokens for these scopes.

Fig: The consent flow
Fig: The consent flow

Add tool calling to MCP server

At runtime, the MCP server retrieves an Outbound App Token for Snowflake when the agent calls its tool. Here’s an example MCP tool that enforces authorization, ensuring that the agent is authenticated and holds the query:write scope:

 server.tool(
    "execute-snowflake-query", // Tool name
    "Execute a SQL query on Snowflake", // Tool description
    {
      // Parameters:
      query: z.string().describe("SQL query to execute"),
      database: z.string().describe("Target database name"),
    },
    async ({ query, database }, { authInfo }) => {
      // Require authentication
      if (!authInfo) {
        return {
          content: [{ type: "text", text: "Authentication required" }],
        };
      }

      // Validate required scope
      const scopes = Array.isArray(authInfo.scopes) ? authInfo.scopes : [];
      if (!scopes.includes('query:write')) {
        return {
          content: [{ type: "text", text: "Missing required scope: query:write" }],
        };
      }

      try {
        // Get Snowflake token and execute query
        const snowflakeToken = await getSnowflakeToken(authToken, authInfo);
        const results = await executeSnowflakeQuery(snowflakeToken, query, database);

        // Return formatted results
        return {
          content: [
            {
              type: "text",
              text: JSON.stringify(results, null, 2),
            },
          ],
        };
      } catch (error) {
        // Log and return user-friendly error message
        console.error("Error executing Snowflake query:", error);
        return {
          content: [
            {
              type: "text",
              text: "Failed to execute Snowflake query: " + (error instanceof Error ? error.message : String(error)),
            },
          ],
        };
      }
    }
  );

The getSnowflakeToken() function exchanges the agent’s Inbound App token for a Snowflake token:

async function getSnowflakeToken(userAuthToken: string, authInfo: AuthInfo): Promise<string> {
  console.log("Getting Snowflake token via exchangeToken helper");

  try {
    // Initialize Descope SDK with project ID from environment
    console.log("Initializing Descope SDK...");
    const descope = DescopeSDK({
      projectId: process.env.DESCOPE_PROJECT_ID || "",
    });

    // Validate session and extract user info
    console.log("Validating session...");
    const sessionInfo: AuthenticationInfo = await descope.validateSession(
      userAuthToken
    );
    
    // Get user ID from session token
    const userId = sessionInfo.token.sub;
    if (!userId) {
      throw new Error("Failed to get Snowflake token: No user ID found");
    }
    
    // Exchange Descope token for Snowflake token
    const snowflakeToken = await exchangeToken(
      userAuthToken,
      "snowflake-app", // App identifier in Descope
      userId
    );
    return snowflakeToken;
  } catch (error) {
    console.error("Error getting Snowflake token with outbound app token:", error);
    throw error;
  }
}

If the request violates any Outbound App Access Control Policies, Descope denies it at this stage.

// Generic function to exchange auth token for outbound app token using Descope API
async function exchangeToken(
  authToken: string,
  appId: string,
  userId: string
): Promise<string> {
  console.log("Exchanging token for appId:", appId, "userId:", userId);

  try {
    const requestBody: any = {
      appId: appId,
      userId: userId,
    };
    const response = await fetch(
      "https://api.descope.com/v1/mgmt/outbound/app/user/token/latest",
      {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          Authorization: `Bearer ${process.env.DESCOPE_PROJECT_ID}:${authToken}`,
        },
        body: JSON.stringify(requestBody),
      }
    );

    if (!response.ok) {
      throw new Error(
        `Failed to get outbound app token for ${appId}: ${response.status} ${response.statusText}`
      );
    }

    const tokenData = (await response.json()) as {
      token?: { accessToken?: string };
    };
    const accessToken = tokenData.token?.accessToken;
    console.log(`${appId} token received:`, accessToken);

    if (!accessToken) {
      throw new Error(
        `No access token received from Descope outbound app API for ${appId}`
      );
    }

    return accessToken;
  } catch (error) {
    console.error(`Error exchanging token for ${appId}:`, error);
    throw error;
  }
}

Audit the full lifecycle

Descope logs every step in the agent identity lifecycle:

  • ThirdPartyApplicationCreated: When the MCP client is registered.

  • ThirdPartyAppConsentGranted: Which scopes were approved.

  • OutboundAppCreated: When the Snowflake connection was established.

  • OutboundAppAccessControlAccessDenied: When a rogue agent violates a policy.

With this, every access request is verifiable, and unauthorized sensitive actions like running destructive queries are blocked automatically.

Extending visibility with custom audit events

Descope gives you an extensive library of out-of-the-box (OOTB) audit events, covering app registrations, consent changes, token issuance, policy violations, and more. But these events only tell part of the story: they show what access was granted or blocked, not whether the agent successfully executed the action it was authorized to perform.

Custom audit events bridge that gap by letting you emit operational context alongside identity decisions. There are two main ways to do this:

Custom events within Flows

You can configure audit events inside a Descope Flow to record decisions and patterns during authentication or consent flows:

  • Track scope approval trends by comparing requested scopes (inboundAppRegister.scopes) with those approved by a user (form.thirdPartyAppApproveScopes).

  • If you consistently see users deny certain scopes, this may indicate your scopes are too broad or misaligned with user expectations.

  • You can track these trends to refactor scope boundaries or provide better education around why a permission is necessary.

This turns audit logs into a feedback tool that helps refine your consent model over time, reducing friction and building trust.

Custom events from your backend

You can also log events directly from your backend when authorized actions execute — whether they succeed or fail.

  • When your MCP server successfully issues a refund through Stripe, it can send a PaymentRefundSucceeded event.

  • If an automated data sync job completes, it can trigger a DataSyncCompleted event tied to the inbound app and user context that initiated it.

  • A failed execution (InvoiceGenerationFailed) can be logged with error details, enabling teams to correlate failures with authorization history.

This creates a unified log: your audit trail doesn’t just show who requested an action and why it was allowed, it can also show what actually happened.

Streaming and visualization

All OOTB and custom events can be streamed into your existing observability or analytics stack (S3, Datadog, Splunk, or other SIEM tools) where they can be combined with logs from other services. This enables you to:

  • Build dashboards to track agent behavior, consent friction, and action success rates.

  • Correlate identity activity with application telemetry, infrastructure metrics, and threat detection pipelines.

  • Create alerts for suspicious agent actions or unexpected permission changes.

Rather than being a silo, Descope’s audit trail feeds into a broader single source of truth, helping teams unify identity, security, and operational visibility in one place.

Simplify agent auditing with Descope

Agentic identities are multiplying, and their lifecycle is volatile by design. Without visibility, they become risks. With Descope’s agentic control plane, every registration, consent, connection, and violation becomes observable.

Whether it’s an MCP server talking to Stripe, an LLM plugin pulling from Snowflake, or a M2M client invoking healthcare APIs, the principle is the same: you can’t secure what you can’t see.

Descope’s auditing framework built on top of its Agentic Identity Hub gives you the visibility to not just see, but to understand, enforce, and evolve your agentic ecosystem with confidence. Sign up for a free Descope account to get started, or book a demo with our team if you have an active agentic identity project.