Authenticate end users with your own identity provider authentication
By default, every action in your app is public. Any LLM platform with your MCP server URL can call those actions, and your handler cannot identify the end user.
Turn on authentication when an action needs to know which end user is asking — for example, to return their orders, entitlements, or account details. The LLM platform signs the user in with your identity provider (IdP), sends the resulting access token with every call, and your handler receives the verified identity.
In this guide: Learn how to connect your identity provider and choose which actions require sign-in. Use the verified identity in your handlers, deploy the app, and test sign-in and protected actions.
Authentication setup is an advanced task. Complete Infrastructure setup, Create actions, and Deploy your app first.
How it works
You bring your own identity provider. Your deployed app is only an OAuth 2.1 resource server — it verifies tokens that your authorization server issued. It never issues tokens, and Adobe never stores your client ID or client secret.
┌── Your identity provider ───────────────────────────────────────────────┐
│ Authorization server — you own it │
│ Issues access tokens, holds the user directory, defines the scopes │
└─────────────────────────────────────────────────────────────────────────┘
▲ 2 user signs in, platform gets an access token
│ │
│ 1 platform discovers your │ 3 every tools/call carries
│ authorization server from │ Authorization: Bearer <token>
│ your app's metadata ▼
┌── LLM platform (ChatGPT, Claude, …) ────────────────────────────────────┐
└─────────────────────────────────────────────────────────────────────────┘
│
▼
┌── Your LLM App on Adobe I/O Runtime ────────────────────────────────────┐
│ Resource server — verifies the token's signature, issuer, audience, │
│ and expiry, then enforces the auth mode you set for each action │
│ │
│ Your handler reads the verified identity from its second argument │
└─────────────────────────────────────────────────────────────────────────┘
Configure authentication per environment. Stage and Production hold independent settings. Verify the configuration against a development IdP tenant before enabling authentication on Production.
Before you begin
- An OAuth 2.1 or OpenID Connect identity provider that issues JWT access tokens signed with an asymmetric algorithm. Opaque tokens and HMAC-signed tokens are not supported. See Token requirements.
- Administrator access to that identity provider to register an API and a client.
- Your app deployed at least once to the environment you are configuring. The deployed MCP server URL is the value your tokens must be scoped to.
Copy the resource identifier
Your app’s resource identifier is its MCP server URL. Every access token your identity provider issues for the app must name that exact URL as its audience. This audience binding prevents use of a token issued for another service.
- Open the App Detail page.
- Scroll to Test the app.
- Under the environment you are configuring, select Copy URL.
Keep the copied MCP server URL for the identity provider configuration in the next step. Paste the URL rather than retyping it. The audience check requires an exact string match, including the path. A single character difference causes every token to fail validation.
Configure your identity provider
The steps vary by provider. Configure the following four items.
- Register your app as an API (resource). Set its identifier — the value the provider puts in the token’s
audclaim — to the copied MCP server URL. Providers commonly label this field Identifier or Audience. Use an identifier unique to the app, not a generic value such asapi, to prevent use of tokens issued for another service. - Define the scopes that protect your actions, for example
orders:readorprofile:read. Use one scope per meaningful permission so each action requests only the access it needs. - Support PKCE. LLM platforms send a
code_challengewithcode_challenge_method=S256on every authorization request, so your authorization server must support S256 PKCE and advertise"code_challenge_methods_supported": ["S256"]in its metadata. - Choose how the LLM platform identifies its OAuth client. Use Client ID Metadata Documents (CIMD) if your provider supports the platform’s requirements, dynamic client registration (DCR), or a pre-registered OAuth client. Supply a client ID and secret only when your chosen method requires them. Register the exact redirect URI documented or displayed by the platform; for Claude’s hosted surfaces that is
https://claude.ai/api/mcp/auth_callback. ChatGPT may use a stable or connector-specific callback depending on the provider’s advertised capabilities, so copy its displayed URI rather than guessing. See the current ChatGPT authentication documentation and Claude authentication documentation.
Turn on authentication
-
In the left navigation, select Settings, then open the Authentication tab.
-
In Workspace, choose Stage or Production.
-
Turn on Enable authentication.
-
Under Core settings, enter:
- Issuer — your identity provider’s HTTPS issuer URL, which is also the value it puts in each token’s
issclaim. The platform requires this URL and publishes the issuer as the app’s authorization server for LLM client discovery. Each app supports one identity provider. - Scopes supported — every scope this app’s actions are allowed to require. Mirror the scopes you defined in your identity provider.
- Issuer — your identity provider’s HTTPS issuer URL, which is also the value it puts in each token’s
-
Advanced settings is optional. Set JWKS URI only when your signing keys are not where your authorization server’s metadata advertises them; otherwise the app discovers them automatically.
-
Select Save.
For field requirements, see Authentication settings.
Choose an auth mode for each action
When you turn on Enable authentication, every action currently set to None changes to Required. Under Per-action configuration, review that assignment and set the mode each action needs:
Actions already set to Required or Optional keep their existing mode.
For a Required or Optional action, add the Scopes it needs. Every scope must already appear in Scopes supported so the app advertises each required permission to LLM platforms. The platform blocks saving until you resolve any scope mismatch.
Scopes supported controls the available scopes. Removing a scope from that list immediately removes the scope from every action that required it. Add each scope to the list before assigning the scope to an action.
Selecting Require auth on all actions changes None actions to Required, but preserves actions already set to Optional. To require sign-in for every action, change any remaining Optional rows to Required individually. Clearing the checkbox returns every action to None.
Select Save when you are done. Auth mode and scope changes save alongside the app-level settings.
ChatGPT supports mixed auth modes within one app and applies each action’s mode individually. Public actions do not prompt for sign-in; protected actions prompt as needed.
Claude also supports mixed apps through lazy authentication. With Sign in when needed selected during connector setup, public actions remain usable anonymously and the platform asks for sign-in when a Required action is called. An app whose actions are all Required requires sign-in before use. See LLM platform behavior for the distinction between a required action and an optional handler requesting sign-in.
Deploy the change
Authentication changes take effect on this app’s next deploy. Deploy the app again to the environment you configured. See Deploy your app.
Your MCP server URL does not change. Refresh the connection’s metadata and verify that its authentication settings match the app’s new configuration. A platform may require you to reconnect or recreate the connection when its authentication method changes.
Read the identity in your handler
A verified identity reaches your handler as the second argument whenever the caller sends a valid token, regardless of the action’s auth mode. An Optional action can use that identity to personalize results when available and return an anonymous result otherwise.
Use getAuthenticatedUser to read the signed-in user:
const { getAuthenticatedUser } = require('@adobe/llm-apps-runtime');
module.exports = async ({ orderId }, extra) => {
const userId = getAuthenticatedUser(extra);
if (!userId) {
return { content: [{ type: 'text', text: 'Sign in to see your orders.' }] };
}
const order = await fetchOrderForUser(userId, orderId);
return {
content: [{ type: 'text', text: `Order ${order.id} is ${order.status}.` }],
structuredContent: order
};
};
The runtime verifies the token for you. For a Required action, it blocks every call that lacks a valid token with the listed scopes. The handler runs only for an authorized caller. Use hasScope to choose handler behavior based on a permission — for example, in an Optional action.
To request sign-in during a conversation, return extra.challengeAuth() from the handler. The helper is available only on Optional actions:
module.exports = async ({ signIn }, extra) => {
if (signIn && !extra.authInfo) {
return extra.challengeAuth({
error: 'invalid_token',
errorDescription: 'Sign in to see member pricing.'
});
}
return {
content: [{
type: 'text',
text: extra.authInfo ? await memberDeals() : await publicDeals()
}]
};
};
Use an explicit input parameter, such as signIn in this example, to decide when to request authentication. Do not infer that decision from the user’s wording.
Set error to match the reported condition. Use invalid_token when the caller has no valid session and needs to sign in, as in the example above. Use insufficient_scope when the caller is signed in but the token lacks a required scope. Each LLM platform chooses the prompt wording and how to respond to the error code. Send the code that accurately describes the condition.
Request authentication only when the required identity is missing, as the !extra.authInfo check does in this example. Signing in does not resolve an unconditional challenge: the handler requests authentication again on every call.
extra.challengeAuth() returns a tool-result challenge for ChatGPT. Claude instead needs an HTTP authentication challenge before the handler runs. For an operation that requires sign-in across supported platforms, use a Required action. Keep a separate None action for the public experience if needed.Keep the identity server-side. Pass only the data the widget needs into structuredContent. Never include an access token in structuredContent. See Customize a generated handler.
For the full contract, see Handler auth API.
Test the protected app
Your existing plugin or connector uses the updated configuration after deployment. To create a new connection, follow the instructions for your platform:
ChatGPT
In the New Plugin dialog, set Authentication to match how you configured the app’s actions:
An Optional action always accepts anonymous calls, so an app that contains one is never fully gated — choose Mixed even if every action is set to Optional. Only Required refuses unauthenticated callers.
See Test the ChatGPT plugin for the rest of the dialog.
Claude
Add the custom connector and review its authentication settings. For a mixed public/protected app, choose Sign in when needed when available. To request sign-in before connector use, select Sign in now. The dialog varies by account or organization; see Test the Claude connector.
Verify
- The platform redirects you to your own identity provider’s sign-in page.
- A protected action returns user-specific data after sign-in.
- A protected action prompts you to sign in when you are signed out.
- On a mixed app, an action set to None still responds without signing in. In Claude, verify the public action with Sign in when needed.
- After sign-in, retry the protected action and confirm that the runtime enforces the expected permissions.
If sign-in does not start, or a token is rejected, see Troubleshooting.
Security guidance
- Grant the narrowest scope that each action needs. Do not reuse one broad scope across every action.
- Keep your client secret in your identity provider and on the LLM platform’s connector configuration. Never put it in action metadata, handler code, widget JavaScript, or source control.
- Treat token claims as input from an external system. Validate values from
authInfo.extrabefore using those values in a query. - Authorize as well as authenticate. A valid token proves the user’s identity, not permission to read a particular record. Check ownership in your handler before returning data.
- Do not log tokens, complete claim sets, or user identifiers.
- Return safe errors. Do not surface upstream identity provider responses or stack traces to the user.
- Set up and verify Stage against a non-production identity provider tenant before enabling authentication on Production.
What’s next
- Authentication reference — fields, token requirements, and platform behavior.
- Customize a generated handler — call a protected upstream API from a handler.
- Deploy your app.