All insights

Agentforce

Agentforce in an authenticated Experience Cloud portal: identity, and scoping what the agent sees

The same agent that's fine on your public website will answer a logged-in member's question with someone else's data if you deploy it the same way. The fix isn't the chat widget. It's who the agent runs as, and which identity you trust when it queries a record.

Agentforce in an authenticated Experience Cloud portal: identity, and scoping what the agent sees, article illustration

You put the same Agentforce agent behind your public marketing site and inside your logged-in customer portal. On the public site it behaves. In the portal a member asks “what’s the status of my order,” and the agent either can’t answer or, the nightmare case, answers with somebody else’s order.

The reflex is to debug the chat widget. The widget is the same in both places. What changed is that one deployment has a known, logged-in person on the other end, and an agent that treats that person’s identity casually will either refuse to help or leak.

Two facts settle most of the design, so I’ll lead with them. The agent does not run as the logged-in member; it runs as its own service user. And the member’s identity only becomes safe to act on once it’s verified, not when the page hands it over.

Scoping a member’s agent is therefore two separate jobs: cap what the agent’s own user can ever touch, and filter every record query by the verified member, never by a value the browser sent.

The agent runs as its own user, not the member

Putting an agent in a portal inverts how a Lightning page works, and that inversion is what teams miss. When a member opens a record in an Experience Cloud site, they see it through their own user and their own sharing.

When that member talks to an Agentforce Service Agent, the agent acts as a dedicated agent user with its own permission sets, not as the member.

Creating a Service Agent provisions that user for you and assigns it a permission set group, with a deliberately minimal base: out of the box it can do close to nothing, and you grant from there. That default is the right one. It means the agent’s ceiling, the widest set of records it could ever reach, is whatever you explicitly give its user plus what sharing exposes to that user.

The consequence is easy to get wrong in both directions. Grant the agent user broad read on Cases and Contacts so your actions work, and you’ve just made the agent capable of reading every case and every contact, for every member, unless something narrows it per conversation. That narrowing is the second job, and it depends entirely on trusting the right identity.

Pre-chat data is not proof of identity

The easy way to tell the agent who it’s talking to is pre-chat context: the portal drops the member’s ContactId into a hidden field, it flows through the messaging channel into a context variable, and an action uses it to look up that member’s records. It works in the demo. It’s also forgeable.

Hidden pre-chat fields live in the browser, and anything in the browser can be edited. A curious or malicious member can change the ContactId in that payload to a value that isn’t theirs, and if your action queries records by the ContactId it received, the agent cheerfully returns another member’s data.

Pre-chat is good for passing data the agent can use to be helpful. It’s not evidence of who the person is.

There are two ways to make identity trustworthy, and a serious portal uses them together.

Verify identity with a signed token

Salesforce’s token-based user verification for Messaging for In-App and Web is the cryptographic answer. Your server, which already knows who’s logged in, mints a JSON Web Token for the member and hands it to the chat client. Salesforce validates it against a public key you uploaded ahead of time, and only then treats the conversation as belonging to that verified user.

The token has specific requirements worth getting right the first time. It must be signed with RS256 or RS512. The kid in the header has to match the key you registered as a JSON Web Key, the iss has to match the issuer configured on your keyset, and the sub is the member’s identifier, which Salesforce stores on the Messaging End User record.

{
  "sub": "member-87421",
  "iss": "https://portal.example.com",
  "iat": 1757700000,
  "exp": 1757703600
}

On the page, you hand that token to the client after it signals it’s ready, and you do it server-side all the way: the token is minted by your backend for the authenticated session, never assembled in the browser.

// mint the token server-side for the logged-in member, then pass it in
addEventListener('onEmbeddedMessagingReady', () => {
  embeddedservice_bootstrap.userVerificationAPI.setIdentityToken({
    identityTokenType: 'JWT',
    identityToken: tokenFromYourServer
  });
});

One operational detail trips people up: the token lives in the browser’s memory, so you call setIdentityToken again after a page refresh and in every tab where you want the verified conversation to continue. Skip that and a refresh silently drops the member back to unverified.

Filter records by the verified contact, never the supplied one

Verification tells the platform who the person is. The second job is making sure your actions use that verified identity and nothing else when they read records.

In a messaging conversation, the agent has access to context variables drawn from the Messaging Session, and the one that matters here is the verified end-user contact. Your actions should filter on that value. The rule practitioners repeat, because the failure is so costly, is to always use the verified MessagingSession.EndUserContactId from context and never a contact id that came from user-supplied input.

// verifiedContactId is bound from $Context.MessagingSession.EndUserContactId,
// the identity the platform confirmed, not a value the page sent
List<Case> cases = [
    SELECT CaseNumber, Subject, Status
    FROM Case
    WHERE ContactId = :verifiedContactId
    ORDER BY CreatedDate DESC
    LIMIT 10
];

Read together, the two jobs form a belt and braces. The agent user’s permissions and sharing set the outer limit of what any query could return, and the verified-contact filter narrows each conversation to one member. Either one alone fails: broad permissions with no filter leaks across members, and a filter on an untrusted id lets a member read whoever they claim to be.

True end-user-scoped access, where the agent operates strictly inside the member’s own sharing, is possible but takes deliberate configuration. The default model is the agent-user-plus-verified-filter pattern above, and most portals should start there. It’s the same principle behind governing what any grounded agent is allowed to see.

Don’t forget the guest user is still there

A login-required Experience Cloud site still has a guest user. It’s created with the site and it can’t be removed, and its permissions and sharing still apply to anything that touches the site anonymously, including an agent conversation that starts before a member signs in.

Salesforce tightened this hard for a reason. Since Summer ‘20, and enforced from Winter ‘21, the Secure guest user record access setting is on in every org with an Experience Cloud site and can’t be disabled. It forces the guest user’s org-wide defaults to private and removes every path to records except explicit, read-only guest user sharing rules. That default exists because over-permissioned guest users have been one of the most exploited misconfigurations on the platform, and an agent inherits whatever the guest user can see.

So audit the guest user as its own surface. If the agent can be reached anonymously, confirm the guest user’s object permissions and sharing rules expose only public, non-sensitive data, and route anything member-specific behind verification. The security review you’d run on any org applies here with a sharper edge, because the agent turns a quiet guest-user oversight into a conversational one.

A few things that fail without warning

The chain from the portal page to a scoped answer has several links, and each one fails in a way that looks like the agent is broken.

API names have to line up end to end. A pre-chat field, the messaging parameter, the channel mapping, the inbound flow variable, and the Messaging Session field all have to agree, and a single spelling mismatch drops the value without an error. The anonymous Messaging for Web setup has the same plumbing; the authenticated version just adds the verification layer on top, so when data doesn’t arrive, check the names before you suspect the token.

The site’s URL also has to be a trusted, CORS-allowed origin, or the browser blocks the chat calls and the widget fails without a visible error. And because the agent’s base permission set is intentionally tiny, forgetting to grant the agent user read on the objects your actions touch produces actions that simply don’t fire. None of these throw a red banner. All of them look, from the member’s side, like an agent that won’t help.

What to get right before launch

Decide who the agent runs as and write down exactly what its user can touch. Verify the member with a signed token rather than trusting pre-chat. Filter every member-scoped query on the verified contact. And treat the guest user as a live attack surface even though the site requires login.

Get those four right and the same agent that behaves on your marketing site will behave in the portal, where the cost of getting identity wrong isn’t a bad answer, it’s the wrong person’s data.

Understanding the basics

Does an Agentforce agent run as the logged-in Experience Cloud user?

No. An Agentforce Service Agent runs as its own dedicated agent user with its own permission sets, not as the member it’s talking to. The member’s identity is passed into the conversation as context, and the agent’s own permissions plus sharing set the outer limit of what it can access. You scope each conversation to the right member by filtering record queries on the verified contact.

How do you pass a logged-in user’s identity to an Agentforce agent securely?

Use token-based user verification for Messaging for In-App and Web. Your server mints a JWT for the authenticated member, signed with RS256 or RS512, and the page hands it to the chat client with the user verification API after the client is ready. Salesforce validates the token against a public key you registered. Pre-chat fields can carry data but are editable in the browser, so they can’t be trusted as proof of identity on their own.

Why would an agent return another member’s records?

Because an action filtered records on a contact id that came from pre-chat or another client-supplied input, which a member can alter in the browser. Always filter member-scoped queries on the verified MessagingSession.EndUserContactId from context, and keep the agent user’s permissions scoped, so neither an untrusted id nor over-broad access can cross members.

Do I need to secure the guest user on a login-required site?

Yes. Every Experience Cloud site has a guest user that can’t be removed, and its permissions apply to anonymous access, including an agent reached before sign-in. Secure guest user record access has been enforced since Winter ‘21 and forces guest org-wide defaults to private, but you still have to confirm guest sharing rules expose only public data and route anything member-specific behind verification.


Putting an Agentforce agent inside a logged-in portal and making sure it answers for the member in front of it and no one else? Talk to us. Getting identity verified, the agent user scoped, and every query filtered on the right contact is exactly the work that keeps a portal agent from becoming a data-leak.

Keep reading

All insights