ADR-0053: Starting a chat session requires team membership or the embed key
ACCEPTED
Context
ADR-0039 required proof of possession on the four session endpoints but left the start endpoint (chat_start_session) alone. There, an authenticated Django session was sufficient on its own: any logged-in OCS user who knew a chatbot's public_id could start a session against any team's chatbot, and only version selection checked team membership. Chatbot public_ids are not secret — they ship in every embed snippet.
Closing that hole by requiring team membership would have broken OCS's own site help widget, which templates/web/app/app_base.html renders for every logged-in user from ChatWidgetConfig. Its users are logged in but are generally not members of the support bot's team, so it worked only because the endpoint accepted a bare session cookie. The config already names the chatbot, so the chatbot's embed key is derivable from it — the widget can present the same proof an external embedder does.
The two credentials then collide. DRF stops at the first authenticator that matches, and SessionAuthentication precedes EmbeddedWidgetAuthentication, so a same-origin widget's cookie always wins and request.auth is never the channel. The embed key has to be consulted by the view rather than inferred from request.auth, and once it is, the channel that owns the resulting session is an open question.
Decision
We will treat being logged in as identity, not as access to every chatbot.
- A session-authenticated caller may start a session only if they belong to the chatbot's team or the request carries that chatbot's widget embed key, validated against the channel's
allowed_domainsexactly asWidgetDomainPermissionvalidates it. - This binds session-authenticated callers only. Anonymous callers are not required to present an embed key. A request carrying no credentials at all may still start a session on any chatbot whose
public_idit knows, exactly as before this ADR. What is being withdrawn is a privilege the Django session cookie was granting on its own — nothing is being added to the anonymous path, which stays governed by the channel's widget auth level (ADR-0045) and the permission classes. - Selecting a chatbot version stays a team-member capability; an embed key does not grant it. An anonymous caller may not select one either, which predates this ADR.
- The embed key authorizes without changing identity: the caller stays the authenticated user, so the participant remains user-linked and session state is still recorded.
- A widget's own channel owns the session whenever that widget's embed key accompanies the request, whether the key authenticated it or merely rode along. Attribution tracks the widget, not the authenticator.
ChatWidgetConfigderives the embed key from its configuredchatbot_idrather than storing one; site config cannot name a chatbot that has no embedded widget channel.
Consequences
- A leaked
public_idno longer lets an arbitrary logged-in user consume another team's chatbot. It does not turnpublic_idinto a secret: the same request without a session cookie is still accepted, so this raises the bar against accidental and in-browser misuse, not against a determined caller. - The anonymous-without-a-key path stays open for now because widgets older than 0.5.1 predate the embed key and cannot send one (
EMBED_KEY_INTRODUCEDinapps/channels/widget_versions.py). Closing it is queued behind the deprecation schedule in that module: the current entry deprecates everything below 0.6.0 with a sunset of 1 October 2026, past which no supported widget lacks a key. Thosesunset_atdates are RFC 8594 declarations of intent rather than enforcement, so dropping the path will be its own change — gated on that date and on the ADR-0045 ratchet having moved live channels offNONE. - The site help widget is no longer a special case: it is an embedded widget that authorizes like any other, so widget version telemetry and the ADR-0045 auth-level ratchet finally see it, and its sessions inherit that channel's ADR-0044 policy.
- Attribution follows the widget rather than the viewer, so a support-team member and an ordinary user on the same page produce one channel and one participant identity, not two.
- Help-widget participants move from
platform=apitoplatform=embedded_widget. BecauseParticipantis keyed on(team, platform, identifier), existing users get a second participant record and their prior conversations stay under the old one. - The support bot's widget channel must list the OCS host in
allowed_domainsin every environment, or the help widget cannot start a session. The admin configuration form refuses a chatbot without a widget channel, but cannot check the domain. - A same-origin embed inside OCS may still run on the session cookie alone, as long as its viewers are members of the chatbot's team.
templates/chatbots/single_chatbot_home.htmlis exactly that — noembed-key, and it picks a version throughopenChatWidget(...), which only a member may do anyway. The site help widget is the outlier that needs a key, because it renders for every logged-in user regardless of team. An embed whose audience is not guaranteed to be team members must carry the key. - Embeds that attach to an already-started session by passing
session-idandsession-token—chatbots/chat/web_chat.htmlandchatbots/components/session_chat_widget_launcher.html— never reachchat_start_session, so nothing here applies to them. Their access is ADR-0039's proof-of-possession, not this ADR's.
Alternatives considered
- Store an
embed_keyfield onChatWidgetConfig— rejected: a copy of a value the chatbot already owns, silently stale after a token rotation. - Order
EmbeddedWidgetAuthenticationbeforeSessionAuthentication— rejected: the key would win over the cookie and downgrade help-widget users to anonymous participants, losing user linkage and the page context that is only saved for authenticated callers. - Accept the key as authorization but keep sessions on the team API channel — rejected: attribution would depend on which credential happened to be consulted, and the site widget would stay invisible to widget telemetry.
- Switch to the widget channel only when the key was needed — rejected: membership short-circuits the authorization check, so staff and non-staff on the same page would land on different channels and different participants.
- Restrict the start endpoint to team members outright — rejected: it breaks the site help widget with no replacement, and the embed key is the proof external embedders already present.
- Require the embed key from anonymous callers in the same change — deferred, not rejected: widgets below 0.5.1 have no key to send, so it would have broken every such embed on deploy. It becomes available once the 0.6.0 sunset above has passed, and is the natural follow-up to this ADR.