Guanta authenticated widget example

Guanta

This framework-free PHP application demonstrates how a customer application can give a standard Guanta chat widget the verified identity of a logged-in user.

The important part of this example is the server-to-server identity exchange. The application continues to own login, passwords, sessions, user records, and authorization. Guanta receives only the trusted user identifier and display name that the application backend chooses to assert.

The included CSV login is intentionally small and is not a production authentication system. It exists only so the complete integration can be run and inspected without a framework or database.

The same guide is also available as a standalone HTML document.

What this example demonstrates

The question “Who am I?” makes the identity boundary easy to see, but an AI response must not be used as an application authorization decision.

Authentication responsibilities

The customer application remains the identity provider for its own users:

Customer application Guanta
Authenticates the user Trusts an assertion made by the authenticated customer backend
Stores passwords, SSO identities, or other login credentials Never receives the user's password or password hash
Maintains the application session and creates an opaque ID for each successful login Creates and securely resumes a separate widget conversation session
Chooses a stable external user ID, display name, and per-login context ID Binds the conversation to that exact asserted identity and login context
Decides what the user may do in the customer application Makes the trusted identity available to the configured Guanta agent

Guanta authentication does not replace the customer's login system. It connects an already-authenticated customer session to a Guanta widget session.

Tenant, agent, widget, and credential relationship

Each customer integration is isolated inside a Guanta tenant:

Guanta tenant
└── Agent
    └── Widget interface
        ├── Public widget ID
        ├── Allowed customer origins
        └── Widget identity credentials
            ├── Customer production backend
            └── Customer staging backend

A credential created in one tenant cannot authenticate against another tenant. A credential for one widget interface cannot issue a usable identity token for a different widget.

It is normal to create multiple credentials for the same widget—for example, separate credentials for staging and production, or for two independently deployed customer backends. This makes rotation and revocation safer.

End-to-end flow

Authenticated widget identity flow

The token is created only when the widget is about to initialize. Creating it when the page first renders could allow it to expire before the visitor opens the widget.

Values supplied by Guanta

The environment-variable names below are conventions used by this example. A customer may use different names in their own application.

Example variable Scope Secret? Source
GUANTA_BASE_URL Tenant and environment No The customer's Guanta tenant URL
GUANTA_WIDGET_PUBLIC_ID Widget interface No The widget loader snippet in the interface Playground
GUANTA_WIDGET_IDENTITY_CREDENTIAL Widget interface and customer backend Yes Created in the interface's Widget Identity Credentials section and shown once
GUANTA_WIDGET_LOADER_INTEGRITY Published widget-loader version No The widget loader snippet in the interface Playground

In the Guanta tenant UI:

  1. Open Agents → Interfaces and select the widget interface.
  2. Open its Playground and use Copy HTML to obtain the loader URL, integrity value, and widget public ID.
  3. Open Widget Identity Credentials for the same interface.
  4. Create a named credential for the customer backend and optionally set an expiry.
  5. Confirm the tenant-user password and copy the complete credential when it is displayed.
  6. Store the credential immediately in the customer application's secret manager. Guanta shows the complete value only once.

The widget's allowed origins must include the customer application's exact origin, including scheme and port where applicable.

Requirements

Local setup

Copy the environment template:

cp .env.example .env

Configure the application and Guanta values:

APP_URL=http://127.0.0.1:8088
APP_SESSION_SECURE=false

GUANTA_BASE_URL=https://your-tenant.example
GUANTA_WIDGET_PUBLIC_ID=your-widget-public-id
GUANTA_WIDGET_IDENTITY_CREDENTIAL=wic_your-credential-id.your-secret
GUANTA_WIDGET_LOADER_INTEGRITY=sha384-your-published-integrity-value

GUANTA_WIDGET_IDENTITY_CREDENTIAL is sensitive. Keep it only in the ignored .env file for local development or in the deployment platform's secret store. Never place it in HTML, JavaScript, source control, screenshots, logs, analytics, or browser storage.

Start the local application:

./scripts/run.sh

Then open http://127.0.0.1:8088.

The PHP built-in server is for local demonstration only. Use a supported web server and PHP runtime for an internet-facing deployment.

Demo users

The committed CSV contains password hashes rather than plaintext passwords. The three local demo identities are:

External ID Email Full name Password availability
1 jtorras@guanta.ai Jordi T Guanta Distributed separately
2 jordi@torras.ai Jordi Torras Distributed separately
3 user@example.com George Towers B_timu_bicu_fila_4315

The public user@example.com credentials are prefilled in the login form so a customer can test the authenticated flow immediately. All demo credentials are for this example only and must not be reused for real accounts.

For a real integration, replace the CSV authentication with the application's existing session, SSO, OAuth, OpenID Connect, Laravel, Symfony, or other authentication mechanism. The Guanta exchange begins only after that system has authenticated the request.

Standard widget embed

The page uses the ordinary Guanta widget loader with one additional attribute pointing to a customer-owned endpoint:

<script
  src="https://your-tenant.example/widget/embed-loader.v1.1.0.js"
  integrity="sha384-your-published-integrity-value"
  crossorigin="anonymous"
  data-public-id="your-widget-public-id"
  data-identity-token-url="/widget-identity.php"
  defer>
</script>

data-identity-token-url must resolve to the customer page's own origin. The loader calls it with POST when the widget initializes and includes the customer application's normal same-origin session cookie.

The endpoint must determine the current user and login context exclusively from the trusted server-side session. It must never accept external_user_id, full_name, or login_context_id from browser input.

When no application user is logged in, the endpoint returns anonymous state:

{"identity_token":""}

When a user is logged in, it returns the one-time token received from Guanta:

{"identity_token":"wit_<token-id>.<one-time-secret>"}

Responses containing identity information must use Cache-Control: no-store and should not be logged.

Identity-token API

The customer backend creates a token by calling the tenant-specific endpoint:

POST /api/v1/widget-identity-tokens HTTP/1.1
Host: your-tenant.example
Authorization: Bearer wic_<credential-id>.<secret>
Accept: application/json
Content-Type: application/json

{
  "widget_public_id": "your-widget-public-id",
  "external_user_id": "stable-customer-user-id",
  "full_name": "Customer User",
  "login_context_id": "opaque-id-for-this-login-session"
}

Successful response:

HTTP/1.1 201 Created
Cache-Control: no-store, private
Content-Type: application/json

{
  "identity_token": "wit_<token-id>.<one-time-secret>",
  "widget_public_id": "your-widget-public-id",
  "expires_at": "2026-08-14T12:00:00+00:00"
}

Field guidance:

The API accepts an external user ID and login context ID of up to 128 characters and a full name of up to 191 characters. The login context is optional for backward compatibility, but authenticated conversations without it are intentionally non-resumable.

Typical error responses:

Status Meaning
401 The identity credential is missing or invalid
403 The credential is not authorized for the widget, or the interface is unavailable
422 A required identity field is missing or invalid
429 The credential exceeded the identity-token rate limit

Treat any unsuccessful response as an authenticated initialization failure. Do not silently downgrade a logged-in visitor to anonymous, because that can hide configuration or availability problems.

How the PHP example implements it

  1. public/index.php authenticates a demo user and stores the user's ID plus a newly generated opaque login context in the PHP session.
  2. The widget loader calls public/widget-identity.php when the widget opens.
  3. widget-identity.php reads the current user from the PHP session.
  4. src/app.php sends the server-to-server request with the user assertion and login context to the tenant's identity-token API.
  5. The same-origin endpoint returns the resulting one-time token to the widget loader.
  6. The widget consumes the fresh token while creating or securely resuming the Guanta conversation session.
  7. The loader stores only Guanta's opaque resume token and a boolean visibility preference in first-party sessionStorage, so conversation and open/closed state survive refreshes and full-page links within the same browser tab.

The example's identity endpoint additionally requires a POST request marked with X-Requested-With: GuantaWidget. This is a defense-in-depth check, not a replacement for session security, same-origin controls, or CSRF protections appropriate to the customer's application.

Conversation continuity requirements

Keeping the customer logged in is necessary, but it is not sufficient to restore a widget conversation. The same customer user may log out and later log in again, so Guanta must be able to distinguish one login session from the next. Otherwise, an old browser-held resume credential could incorrectly reopen a conversation from an earlier login.

The following pieces work together:

Component Owner Requirement
Authenticated application session Customer backend Identifies the current user on every request to the same-origin identity endpoint
Stable external user ID Customer backend Remains the same for the customer account; it is not sufficient by itself to resume a conversation
Per-login login_context_id Customer backend Generated after each successful login, stored in the server-side session, kept unchanged during navigation, and replaced after logout or the next login
Fresh identity token Customer backend and Guanta API Created from the current user and login context for every widget initialization; it remains short-lived and single-use
Loader version 1.1 or later Customer page Included with the same Guanta tenant URL, widget public ID, and same-origin identity endpoint on every page
Conversation resume token Guanta loader Stored automatically as an opaque wrt_... value in first-party sessionStorage; customer code should not create, inspect, transmit, or persist it elsewhere
Stable widget scope Customer page and Guanta The tenant, widget public ID, and allowed page origin must continue to match
Browser tab storage Browser sessionStorage must remain available; it normally survives reloads and same-tab navigation but is separate in a new tab

During ordinary full-page navigation:

  1. The old page leaves Guanta's opaque resume token and visibility preference in the browser tab's sessionStorage.
  2. The new page loads the same version 1.1 widget configuration.
  3. The loader calls the customer-owned identity endpoint again.
  4. The customer backend reads the current user and the unchanged login_context_id from its server-side session and requests a new one-time identity token.
  5. The loader sends the fresh identity token and the previously issued opaque resume token to Guanta.
  6. Guanta restores the conversation only after the widget, origin, external user, login context, active session, and expiry all match. It then returns a refreshed opaque resume token.
  7. If Guanta confirms that the same conversation resumed, the loader also restores the tab's saved open/closed widget state.

The customer application does not need to save messages, copy a Guanta session ID between pages, or expose any conversation identifier in HTML. It should only preserve its own authenticated session and per-login context. Loader version 1.1 handles the Guanta resume credential automatically.

If the identity, login context, widget, origin, expiry, or stored token does not match, Guanta does not restore the earlier authenticated conversation. Login, logout, account switching, and subsequent login therefore begin with a closed, fresh widget conversation.

HTML templates in the example

The demo keeps presentation in the non-public html/ directory and application logic in src/. The three page templates share one layout and use small partial templates for authenticated and anonymous states. The public PHP files remain thin entry points.

Templates contain simple placeholders rather than executable PHP:

<h2>Welcome, {{full_name}}</h2>

src/template.php provides the deliberately small replace_tags() helper. Ordinary replacement values are HTML-escaped by default, so names, email addresses, validation messages, and other data cannot inject markup. Complete HTML fragments may be inserted only through the helper's separate trusted-fragment argument; this example uses that path exclusively for partials that were themselves rendered from files under html/.

The renderer rejects unknown placeholders, invalid placeholder names, non-scalar ordinary values, and template paths outside the html/ directory. Templates contain no PHP. This is intentionally not a general-purpose template engine; production applications should normally use their framework's established templating system and its escaping rules.

Adapting the flow to another backend

The server-side logic is framework-independent:

receive POST from the widget loader
load the current user from the authenticated application session

if there is no current user:
    return { "identity_token": "" }

load the opaque per-login context ID from the same server-side session
call the Guanta tenant API using the server-only credential
send widget_public_id, stable external_user_id, full_name, and login_context_id

if Guanta does not return 201:
    fail the authenticated widget initialization

return the one-time identity_token with no-store cache headers

the standard loader handles the opaque conversation resume token automatically

In a production framework, place the Guanta API call in a server-side service and protect the same-origin identity endpoint with the application's normal session middleware. Never let the browser submit or override the asserted user ID.

Login, logout, and account switching

An identity token is still short-lived and single-use. Conversation continuity comes from a separate Guanta resume token that is valid only alongside a fresh identity token asserting the same user and login context.

The example's Home, Account, and Orders links perform genuine full-page navigation so conversation resumption is visible and easy to test.

Security model

The trusted identity is context for the Guanta agent. It does not grant permissions inside the customer application, and agent output must not be treated as proof of authentication or authorization.

Credential lifecycle

Use separate credentials for separate customer environments or independently operated backends. Give each credential a descriptive name and an expiry where appropriate.

For rotation without interruption:

  1. Create a second credential for the same widget interface.
  2. Store and deploy the new credential to the customer backend.
  3. Verify identity-token creation through the new credential.
  4. Revoke the old credential in Guanta.

Regenerating an existing credential invalidates its previous secret immediately. Revoking a credential also prevents unconsumed tokens issued by that credential from being accepted.

Production checklist

Troubleshooting

Symptom What to check
401 Invalid widget identity credential Confirm the complete credential was copied once, is active, has not expired, and belongs to this tenant
403 Credential is not authorized for this widget Confirm the credential and GUANTA_WIDGET_PUBLIC_ID came from the same widget interface
422 response Confirm widget_public_id, external_user_id, full_name, and any supplied login_context_id are within their limits
429 response Check for repeated initialization loops and the configured rate limit
Example returns 502 from /widget-identity.php Inspect the customer backend's connectivity and Guanta API response without logging credentials or tokens
Widget remains anonymous after login Confirm the customer session cookie reaches the same-origin identity endpoint and that the endpoint returns a token
Token is expired or already used Mint tokens only when the widget opens and never cache or reuse them
Widget does not load Check the loader URL, SRI value, allowed origin, Content Security Policy, and widget public ID
Conversation does not survive page navigation Confirm loader version 1.1 is deployed, the login context stays stable, and sessionStorage is available
Conversation survives but the widget does not reopen Confirm Guanta reports a successful resume and that browser sessionStorage is available in the current tab
Earlier conversation returns after logout/login Confirm the backend generates a new login_context_id on every login and clears it on logout

Relevant files

Verification

Validate the demo users and password hashes:

php tests/check.php

Rebuild the documentation artifacts after editing this README or the Mermaid source:

./scripts/build-docs.sh

For an end-to-end check:

  1. Open the page while logged out and ask the widget “Who am I?”. It should state that no authenticated identity is available.
  2. Log in with one of the separately supplied demo passwords.
  3. Open the new widget session and ask “Who am I?”. It should answer with that user's configured full name.
  4. Log out and confirm that a newly initialized widget no longer knows the previous identity.
  5. Repeat with the second demo user to verify account isolation.

Getting help

If Guanta rejects a correctly formed request or the required tenant values are unavailable, contact your Guanta representative with the tenant name, widget interface name, timestamp, and HTTP status. Do not include credentials or identity-token values.