Help

Secure Services Sharepoint

Airgentic Help

SharePoint Integration

This guide covers the additional steps required when hosting the Airgentic secure widget on a SharePoint Online intranet. It builds on the general Adding the Secure Widget to Your Site guide — complete those steps first, then follow the SharePoint-specific guidance below.


Overview

SharePoint Online has security restrictions that affect how external scripts are loaded and executed. The key things to be aware of are:

  1. Content Security Policy (CSP) — SharePoint enforces a Content Security Policy that blocks external scripts unless explicitly trusted.
  2. Custom scripts are restricted — Modern SharePoint pages do not allow arbitrary <script> tags. You cannot simply paste the Airgentic embed code into a page editor.
  3. SPFx is the supported integration — Airgentic ships an SPFx web part (inline Service Hub where you place it) and an optional Application Customizer (global hover launcher on all pages when enabled tenant-wide).

How the web part works

The Airgentic SPFx web part acts as a placement anchor for the inline Service Hub. When you add the web part to a SharePoint page:

  • The Service Hub appears exactly where you place the web part (in your chosen section/column)
  • You can add multiple Airgentic web parts to the same page if needed — each gets its own mount point
  • The web part handles loading the Airgentic script and mounting the Service Hub into its location

This design means customers control exactly where the Service Hub appears by placing the web part in the desired location on each page.


Step 1: Add Airgentic as a trusted script source

SharePoint Online's Content Security Policy blocks external scripts that are not explicitly trusted. Your SharePoint administrator must add the Airgentic script domain as a trusted source.

  1. Sign in to the SharePoint Admin Center and navigate to SharePoint settings. The admin center URL is typically https://<your-tenant>-admin.sharepoint.com.
  2. Look for script or CDN settings — for example, SettingsScript management or similar. The exact location varies depending on your Microsoft 365 version and admin center layout.
  3. Add the following domain as a trusted script source:
https://chat.airgentic.com
  1. Save the change. It may take a few minutes to propagate.

Note: If you cannot find this setting in the admin center UI, your SharePoint administrator may need to configure it via PowerShell or the SharePoint API. Microsoft's documentation on allowing custom scripts provides additional guidance.

Without this step, the Airgentic widget script will be blocked by the browser and the widget will not load.


Step 2: Deploy the Airgentic SPFx web part

On modern SharePoint pages, you cannot insert a <script> tag directly. Instead, you need a SharePoint Framework (SPFx) web part that loads the Airgentic script.

Download the pre-built package

Airgentic provides a ready-to-deploy SPFx package. No build step required — just download, upload to your App Catalog, and configure.

Download airgentic-webpart.sppkg

To deploy:

  1. Download the .sppkg file using the link above.
  2. Open your SharePoint App Catalog site (typically https://<your-tenant>.sharepoint.com/sites/appcatalog).
  3. Go to Apps for SharePoint and upload the .sppkg file.
  4. When prompted, check Make this solution available to all sites in the organization if you want it available tenant-wide, then click Deploy.
  5. Navigate to a SharePoint page, edit it, and add the Airgentic web part from the web part picker.
  6. Place the web part where you want the Service Hub to appear — the inline Service Hub renders exactly in the web part's location.
  7. Configure the web part by clicking the edit (pencil) icon and entering your Account ID, Service ID, and Redirect URI in the property pane.

Airgentic will provide your Account ID and Service ID.

Placement tips

  • Choose your location carefully — the Service Hub appears where you place the web part. Common choices include a sidebar column, a dedicated section, or a prominent area on your homepage.
  • Multiple web parts — you can add more than one Airgentic web part to the same page if needed. Each instance gets its own unique mount point and they do not interfere with each other.
  • Consistent placement — for a cohesive user experience, consider placing the web part in a similar location across pages where you want the Service Hub available.

Optional: Global hover launcher (all pages)

The same .sppkg includes an Application Customizer that can load the Airgentic hover widget on every modern SharePoint page without adding the web part to each page. It is off until your administrator enables it tenant-wide (see below).

How it works

  • The customizer reads tenant storage entities (three keys you set once with PowerShell).
  • It injects the same Airgentic script used by the web part (#airgentic-script) with data-mode="launcher" for the global launcher.
  • If the web part already loaded the script on a page, the customizer does nothing — there is no duplicate script tag.
  • If tenant keys are missing, the customizer skips loading and the site continues to work; you can still use only the web part with its property pane settings.

1. Set tenant properties (one-time, tenant admin)

Install PnP PowerShell if needed, then run (replace placeholders):

Connect-PnPOnline -Url "https://yourorg-admin.sharepoint.com" -Interactive

Set-PnPStorageEntity -Key "AirgenticAccountId" -Value "your_account_id"
Set-PnPStorageEntity -Key "AirgenticServiceId" -Value "your_service_id"
Set-PnPStorageEntity -Key "AirgenticRedirectUri" -Value "https://yourorg.sharepoint.com/sites/intranet/SitePages/callback.aspx"

Use the same Account ID, Service ID, and Redirect URI values you use for the web part (typically the callback page URL for Redirect URI).

Microsoft’s reference: SharePoint Framework tenant properties.

2. Deploy the package and enable the extension tenant-wide

After uploading airgentic-webpart.sppkg to the App Catalog, register the Application Customizer for the tenant. The component ID is:

c1d2e3f4-a5b6-4789-a012-3456789abcde

Options include:

  • The Tenant Wide Extensions list on your app catalog site, or
  • CLI for Microsoft 365: e.g. m365 spo tenant applicationcustomizer add --title "Airgentic global launcher" --clientSideComponentId c1d2e3f4-a5b6-4789-a012-3456789abcde

It can take several minutes for the extension to apply after the first registration.

Inline Service Hub vs global launcher

Capability Web part Application Customizer
Inline Service Hub in a chosen section/column Yes No
Hover / launcher on many pages without per-page setup No (unless you add web parts everywhere) Yes (when enabled)
Configuration Property pane on each instance Tenant storage entities (shared)

Optional: Search UI integration

If you want to use Airgentic's Search UI (a unified site search + chat overlay), the web part also supports these optional settings:

Setting Description
Search Input ID The ID of your site's search input element (no # prefix)
Search Button ID The ID of your site's search button element (no # prefix)

You can provide one or both. When configured, Airgentic will bind to your existing search elements and activate the Search UI overlay when users interact with them.


Building your own SPFx web part

If your organisation has a SharePoint developer and prefers to build or customise the web part, you can create your own SPFx project. This gives you full control over the web part's behaviour, allows you to integrate it into your existing SharePoint development workflow, and enables customisation such as styling or additional features.

Click to expand: SPFx development instructions and code example #### Architecture The web part is designed as an **inline Service Hub placement anchor**: - Each web part instance creates a unique mount container using `this.instanceId` - Multiple web parts on the same page work independently without conflicts - The Airgentic script is shared across all instances and is not removed when individual web parts are disposed - This design supports coexistence with the **Airgentic Application Customizer** in the same package (`src/extensions/airgenticLoader/`), which can load the global hover launcher using tenant storage entities #### 1. Create a new SPFx project Use **Node.js v22 LTS** (required for SPFx 1.22+). From an empty folder, run:
yo @microsoft/sharepoint
When prompted, choose: - **Web part** as the component type - **No framework** (or React if preferred — the script injection is the same) The generator creates a **Heft-based** project (no gulp). Build and package with the commands in step 3. #### 2. Replace the web part's code Replace the contents of `src/webparts/airgentic/AirgenticWebPart.ts` with the following. Key design points: - **Unique mount ID per instance** — uses `airgentic-${this.instanceId}` so multiple web parts don't conflict - **Idempotent script loading** — checks if the Airgentic runtime or script tag already exists before injecting - **Safe disposal** — only cleans up instance-specific DOM; does not remove the shared script tag
import { Version } from '@microsoft/sp-core-library';
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
import {
  type IPropertyPaneConfiguration,
  PropertyPaneTextField
} from '@microsoft/sp-property-pane';

export interface AirgenticMountConfig {
  target: string | HTMLElement;
  mode: 'service-hub' | 'inline';
  accountId: string;
  serviceId: string;
  authMode: string;
  redirectUri: string;
  searchInputId?: string;
  searchButtonId?: string;
}

export interface AirgenticMountHandle {
  unmount: () => void;
}

declare global {
  interface Window {
    Airgentic?: {
      mount?: (config: AirgenticMountConfig) => AirgenticMountHandle | void;
      unmount?: (target: string | HTMLElement) => void;
      isReady?: boolean;
    };
  }
}

export interface IAirgenticWebPartProps {
  accountId: string;
  serviceId: string;
  redirectUri: string;
  searchInputId: string;
  searchButtonId: string;
}

const RUNTIME_POLL_INTERVAL_MS = 50;
const RUNTIME_WAIT_TIMEOUT_MS = 60000;

export default class AirgenticWebPart extends BaseClientSideWebPart<IAirgenticWebPartProps> {

  private static readonly SCRIPT_ID = 'airgentic-script';
  private static readonly SCRIPT_SRC = 'https://chat.airgentic.com/airgentic-1.4.js';
  private static sharedScriptElementPromise: Promise<void> | undefined;

  private mountId: string = '';
  private mountHandle: AirgenticMountHandle | null = null;
  private thisInstanceInjectedScript: boolean = false;
  private scriptError: boolean = false;
  private lastInitConfigSignature: string = '';
  private renderGeneration: number = 0;

  public render(): void {
    this.mountId = this.getMountId();

    if (!this.isConfigurationValid()) {
      this.renderConfigState(this.getMissingConfigFields());
      return;
    }

    const sig = this.getConfigSignature();
    if (sig !== this.lastInitConfigSignature) {
      this.lastInitConfigSignature = sig;
      this.scriptError = false;
      this.unmountInlineServiceHub();
    }

    if (this.scriptError) {
      this.renderErrorState();
      return;
    }

    this.renderMountContainer();

    const gen = ++this.renderGeneration;
    void this.ensureScriptLoaded()
      .then(() => {
        if (gen !== this.renderGeneration) return;
        return this.mountInlineServiceHub();
      })
      .catch((err: unknown) => {
        if (gen !== this.renderGeneration) return;
        this.scriptError = true;
        const message = err instanceof Error ? err.message : undefined;
        this.renderErrorState(message);
      });
  }

  private getMountId(): string {
    return `airgentic-${this.instanceId}`;
  }

  private getConfigSignature(): string {
    const p = this.properties;
    return [p.accountId, p.serviceId, p.redirectUri, p.searchInputId, p.searchButtonId].join('\u0001');
  }

  private isConfigurationValid(): boolean {
    return this.getMissingConfigFields().length === 0;
  }

  private getMissingConfigFields(): string[] {
    const { accountId, serviceId, redirectUri } = this.properties;
    const missing: string[] = [];
    if (!accountId?.trim()) missing.push('Account ID');
    if (!serviceId?.trim()) missing.push('Service ID');
    if (!redirectUri?.trim()) missing.push('Redirect URI');
    return missing;
  }

  private renderConfigState(missingFields: string[]): void {
    const items = missingFields.map((f) => `<li>${this.escapeHtml(f)}</li>`).join('');
    this.domElement.innerHTML = `
      <div style="padding: 20px; border: 1px solid #c7c7c7; border-radius: 4px; background: #f9f9f9;">
        <p style="margin: 0 0 10px 0; font-weight: 600;">Airgentic Service Hub — configuration required</p>
        <p style="margin: 0; color: #666;">Please configure the web part properties:</p>
        <ul style="margin: 10px 0 0 0; color: #666;">${items}</ul>
        <p style="margin: 10px 0 0 0; color: #666;">Click the edit (pencil) icon on this web part to open the property pane.</p>
      </div>
    `;
  }

  private renderErrorState(message?: string): void {
    const extra = message ? `<p style="margin: 10px 0 0 0; color: #666;">${this.escapeHtml(message)}</p>` : '';
    this.domElement.innerHTML = `
      <div style="padding: 20px; border: 1px solid #d93025; border-radius: 4px; background: #fce8e6;">
        <p style="margin: 0; color: #d93025; font-weight: 600;">Failed to load Airgentic</p>
        <p style="margin: 10px 0 0 0; color: #666;">Please check that chat.airgentic.com is added to your SharePoint trusted script sources.</p>
        ${extra}
      </div>
    `;
  }

  private renderMountContainer(): void {
    this.domElement.innerHTML = `
      <div id="${this.escapeHtml(this.mountId)}" class="airgentic-inline-root">
        <div class="airgentic-loading" style="padding: 20px; text-align: center; color: #666;">
          Loading Airgentic Service Hub...
        </div>
      </div>
    `;
  }

  private escapeHtml(text: string): string {
    return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
  }

  private ensureScriptLoaded(): Promise<void> {
    if (this.isAirgenticRuntimeReady()) return Promise.resolve();

    if (!AirgenticWebPart.sharedScriptElementPromise) {
      const existing = document.getElementById(AirgenticWebPart.SCRIPT_ID) as HTMLScriptElement | null;
      if (existing) {
        AirgenticWebPart.sharedScriptElementPromise = AirgenticWebPart.waitForScriptElementReady(existing);
      } else {
        AirgenticWebPart.sharedScriptElementPromise = new Promise<void>((resolve, reject) => {
          this.injectScript(resolve, reject);
        });
      }
    }

    return AirgenticWebPart.sharedScriptElementPromise
      .then(() => this.waitForAirgenticRuntime())
      .catch((err: unknown) => {
        AirgenticWebPart.sharedScriptElementPromise = undefined;
        throw err;
      });
  }

  private isAirgenticRuntimeReady(): boolean {
    return typeof window.Airgentic !== 'undefined';
  }

  private waitForAirgenticRuntime(): Promise<void> {
    if (this.isAirgenticRuntimeReady()) return Promise.resolve();
    return new Promise((resolve, reject) => {
      const started = Date.now();
      const id = window.setInterval(() => {
        if (this.isAirgenticRuntimeReady()) {
          window.clearInterval(id);
          resolve();
        } else if (Date.now() - started > RUNTIME_WAIT_TIMEOUT_MS) {
          window.clearInterval(id);
          reject(new Error('Timed out waiting for Airgentic runtime.'));
        }
      }, RUNTIME_POLL_INTERVAL_MS);
    });
  }

  private static waitForScriptElementReady(script: HTMLScriptElement): Promise<void> {
    return new Promise((resolve, reject) => {
      if (script.dataset.airgenticSpfxLoaded === 'true') {
        resolve();
        return;
      }

      let settled = false;
      let pollId: number = 0;
      let timeoutId: number = 0;

      const settleOk = (): void => {
        if (settled) return;
        settled = true;
        window.clearInterval(pollId);
        window.clearTimeout(timeoutId);
        script.dataset.airgenticSpfxLoaded = 'true';
        resolve();
      };
      const settleErr = (): void => {
        if (settled) return;
        settled = true;
        window.clearInterval(pollId);
        window.clearTimeout(timeoutId);
        reject(new Error('Airgentic script failed to load.'));
      };

      script.addEventListener('load', settleOk, { once: true });
      script.addEventListener('error', settleErr, { once: true });

      pollId = window.setInterval(() => {
        if (typeof window.Airgentic !== 'undefined') settleOk();
      }, RUNTIME_POLL_INTERVAL_MS);

      timeoutId = window.setTimeout(() => {
        if (!settled && typeof window.Airgentic === 'undefined') settleErr();
      }, RUNTIME_WAIT_TIMEOUT_MS);
    });
  }

  private injectScript(resolve: () => void, reject: (reason?: Error) => void): void {
    const { accountId, serviceId, redirectUri, searchInputId, searchButtonId } = this.properties;

    const script = document.createElement('script');
    script.id = AirgenticWebPart.SCRIPT_ID;
    script.src = AirgenticWebPart.SCRIPT_SRC;
    script.setAttribute('data-account-id', accountId.trim());
    script.setAttribute('data-service-id', serviceId.trim());
    script.setAttribute('data-auth-mode', 'oidc');
    script.setAttribute('data-auth-redirect-uri', redirectUri.trim());
    script.setAttribute('data-target-id', this.mountId);
    script.setAttribute('data-mode', 'inline');

    if (searchInputId?.trim()) script.setAttribute('data-search-input-id', searchInputId.trim());
    if (searchButtonId?.trim()) script.setAttribute('data-search-button-id', searchButtonId.trim());

    this.thisInstanceInjectedScript = true;

    script.onload = () => {
      script.dataset.airgenticSpfxLoaded = 'true';
      resolve();
    };
    script.onerror = () => {
      if (script.parentNode) script.parentNode.removeChild(script);
      this.thisInstanceInjectedScript = false;
      reject(new Error('Airgentic script failed to load.'));
    };

    document.body.appendChild(script);
  }

  private mountInlineServiceHub(): Promise<void> {
    const root = document.getElementById(this.mountId);
    if (!root) {
      this.scriptError = true;
      this.renderErrorState('Mount container was not found.');
      return Promise.resolve();
    }

    const mountFn = window.Airgentic?.mount;
    if (typeof mountFn === 'function') {
      try {
        const result = mountFn.call(window.Airgentic, this.buildMountConfig(root));
        if (result && typeof result.unmount === 'function') this.mountHandle = result;
      } catch (e) {
        this.scriptError = true;
        const msg = e instanceof Error ? e.message : 'Mount failed.';
        this.renderErrorState(msg);
        return Promise.resolve();
      }
      this.clearLoadingPlaceholder(root);
      return Promise.resolve();
    }

    // Legacy fallback: only the injecting instance can use data-target-id on script tag
    if (this.thisInstanceInjectedScript) {
      this.clearLoadingPlaceholder(root);
      return Promise.resolve();
    }

    this.scriptError = true;
    this.renderErrorState(
      'Another Airgentic instance already loaded the script. ' +
      'Update the Airgentic runtime to support window.Airgentic.mount() for multiple inline web parts.'
    );
    return Promise.resolve();
  }

  private buildMountConfig(target: HTMLElement): AirgenticMountConfig {
    const p = this.properties;
    const config: AirgenticMountConfig = {
      target,
      mode: 'service-hub',
      accountId: p.accountId.trim(),
      serviceId: p.serviceId.trim(),
      authMode: 'oidc',
      redirectUri: p.redirectUri.trim()
    };
    if (p.searchInputId?.trim()) config.searchInputId = p.searchInputId.trim();
    if (p.searchButtonId?.trim()) config.searchButtonId = p.searchButtonId.trim();
    return config;
  }

  private clearLoadingPlaceholder(root: HTMLElement): void {
    const loading = root.querySelector('.airgentic-loading');
    if (loading?.parentNode) loading.parentNode.removeChild(loading);
  }

  private unmountInlineServiceHub(): void {
    if (this.mountHandle) {
      try { this.mountHandle.unmount(); } catch { /* ignore */ }
      this.mountHandle = null;
    } else {
      const unmount = window.Airgentic?.unmount;
      const root = this.mountId ? document.getElementById(this.mountId) : null;
      if (typeof unmount === 'function' && root) {
        try { unmount.call(window.Airgentic, root); } catch { /* ignore */ }
      }
    }
  }

  protected onDispose(): void {
    this.unmountInlineServiceHub();
    if (this.domElement) this.domElement.innerHTML = '';
    // Do NOT remove global script — other instances or future global loader may depend on it
    super.onDispose();
  }

  protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
    return {
      pages: [{
        groups: [
          {
            groupName: 'Airgentic Settings',
            groupFields: [
              PropertyPaneTextField('accountId', {
                label: 'Account ID',
                description: 'Your Airgentic account ID (provided by Airgentic)'
              }),
              PropertyPaneTextField('serviceId', {
                label: 'Service ID',
                description: 'Your Airgentic service ID (provided by Airgentic)'
              }),
              PropertyPaneTextField('redirectUri', {
                label: 'Redirect URI',
                description: 'The callback page URL (e.g. https://yourorg.sharepoint.com/sites/intranet/SitePages/callback.aspx)'
              })
            ]
          },
          {
            groupName: 'Search UI (Optional)',
            groupFields: [
              PropertyPaneTextField('searchInputId', {
                label: 'Search Input ID',
                description: 'The ID of your site\'s search input element (no # prefix). Leave blank if not using Search UI.'
              }),
              PropertyPaneTextField('searchButtonId', {
                label: 'Search Button ID',
                description: 'The ID of your site\'s search button element (no # prefix). Leave blank if not using Search UI.'
              })
            ]
          }
        ]
      }]
    };
  }

  protected get dataVersion(): Version {
    return Version.parse('1.0');
  }
}
#### 3. Build and package (SPFx 1.22+ Heft toolchain)
npm install
npx heft build --production
npx heft package-solution --production
#### 4. Deploy Upload the `.sppkg` file from `sharepoint/solution/` to your SharePoint App Catalog. #### 5. Add the web part Add the web part to a SharePoint page, place it where you want the Service Hub to appear, and configure the Account ID, Service ID, and Redirect URI in the web part properties panel.

Alternative: classic pages with custom scripts enabled

If your SharePoint environment uses classic pages and your administrator has enabled custom scripts, you can use a Script Editor web part to paste the embed code directly. However, this approach is being phased out by Microsoft and is not recommended for new deployments.


Step 3: Set up the callback page

The authentication flow requires a callback page — the page your identity provider redirects users to after sign-in. On SharePoint, we recommend creating a dedicated callback page rather than using the same page that hosts the widget.

How authentication works

Understanding the authentication flow helps explain why the callback page is needed:

  1. User starts on Page A — they interact with the Airgentic widget, which detects they need to sign in.
  2. Browser redirects to Microsoft — the user signs in with their organisation credentials.
  3. Microsoft redirects to the callback page — after successful sign-in, Microsoft sends the user to the redirect URI you registered (the callback page), not back to Page A.
  4. Widget completes authentication — the Airgentic widget on the callback page sees the authorisation code in the URL, exchanges it for tokens, and stores the session.
  5. User is now authenticated — the widget is ready to use on the callback page.

The user stays on the callback page after sign-in — they are not automatically returned to the page they started from. This is standard OIDC behaviour.

Why a dedicated callback page?

  • SharePoint pages often have long URLs that vary across site collections. Using one dedicated page avoids redirect URI mismatches.
  • You register a single, stable URL in your identity provider, reducing the risk of configuration errors.
  • The callback page URL must exactly match what's registered in Entra ID — character for character.

How to set it up

  1. Create a new SharePoint page — for example, https://yourorg.sharepoint.com/sites/intranet/SitePages/callback.aspx. You can name it anything (callback, airgentic-login, etc.).
  2. Add the Airgentic web part — edit the page and add the same Airgentic SPFx web part.
  3. Configure the web part — enter your Account ID, Service ID, and set the Redirect URI to this page's own URL.
  4. The page can otherwise be blank — no other content is needed. The widget handles authentication automatically.
  5. Register this URL in Entra ID — add this exact URL as a redirect URI in your app registration (see Registering Airgentic in your Identity Provider).

Configure other pages to use the callback

On every other page where you add the Airgentic web part, set the Redirect URI property to point to your callback page — not to the page itself.

For example, if your callback page is https://yourorg.sharepoint.com/sites/intranet/SitePages/callback.aspx, then:

Page Redirect URI setting
Homepage https://yourorg.sharepoint.com/sites/intranet/SitePages/callback.aspx
HR Portal https://yourorg.sharepoint.com/sites/intranet/SitePages/callback.aspx
Callback page https://yourorg.sharepoint.com/sites/intranet/SitePages/callback.aspx

All pages point to the same callback URL.

Where should users land after sign-in?

Since users stay on the callback page after authentication, consider making it a page where they'd naturally want to use the widget — such as your main intranet homepage — rather than a blank page.

Alternatively, you can create a simple callback page with a message like "You're now signed in. Return to [Homepage] to continue." with a link back.


Step 4: Configure your allowed origin

Your SharePoint origin must be included in the Airgentic service configuration. When you send your details to Airgentic (see Registering Airgentic in your Identity Provider), include your SharePoint origin:

https://yourorg.sharepoint.com

If your organisation uses a custom domain for SharePoint (e.g. https://intranet.yourorg.gov.au), use that instead.


Important considerations

Avoid wrapping the SharePoint page in another iframe

The Airgentic widget itself uses iframes internally — this is normal and works correctly on SharePoint.

However, if the SharePoint page that hosts the widget is loaded inside another iframe (for example, if someone embeds the SharePoint page within Microsoft Teams using an iframe, or wraps it in a third-party portal), the authentication redirect may fail. Identity providers such as Microsoft Entra ID block their sign-in pages from rendering inside nested iframes.

Ensure the SharePoint page with the Airgentic widget is loaded as a top-level page in the browser, not embedded inside another frame.

Microsoft Teams and Viva Connections

If your intranet is accessed through Microsoft Teams or Viva Connections, be aware that these environments can wrap SharePoint pages in ways that interfere with redirect-based authentication. If your users access the intranet through Teams or Viva, test the widget in those environments specifically to confirm the sign-in flow works correctly.

Session storage

The widget stores authentication state in the browser's session storage, scoped to the top-level window. This means:

  • Session storage is shared across tabs within the same browser window, so users typically don't need to sign in again when opening new tabs.
  • However, if a user opens a completely new browser window (not just a new tab), they will need to sign in again.
  • This is normal and expected behaviour.

Group claims with many groups

If your organisation uses group-based authorisation and users belong to a large number of groups (more than 150), Microsoft Entra ID may not include all groups directly in the token. In this case, Entra ID returns a link to the Microsoft Graph API instead. If you plan to use group-based authorisation, let Airgentic know how many groups your users typically belong to so we can ensure the configuration handles this correctly.


Checklist

Step Action
1 SharePoint admin adds https://chat.airgentic.com to trusted script sources
2 Deploy the Airgentic SPFx package (.sppkg) from the App Catalog
3 Create a dedicated callback page with the Airgentic web part
4 Register the callback page URL in your Entra ID app registration
5 Send your SharePoint origin, client ID, client secret, tenant ID, and callback URL to Airgentic
6 Test sign-in and widget functionality
7 If accessing via Teams or Viva, test in those environments too
8 (Optional) For a global hover launcher on all pages: set tenant storage entities and enable the Application Customizer tenant-wide — see Global hover launcher

Troubleshooting

Widget does not appear on the page

  • Check the browser's developer console (F12 → Console) for CSP errors mentioning chat.airgentic.com. If present, the trusted script source has not been added (Step 1).
  • Confirm the SPFx package is deployed and, for the inline Service Hub, that the Airgentic web part is added to the page.
  • If you rely on the global Application Customizer, confirm tenant storage entities are set and the extension is registered tenant-wide; without those, the customizer does not inject the script (the web part can still work on its own).

"Redirect URI mismatch" error after sign-in

  • The callback URL in data-auth-redirect-uri does not exactly match the redirect URI registered in Entra ID. SharePoint page URLs are case-sensitive and include the full path — ensure they match character for character.

Sign-in redirects in a loop

  • This can happen if the callback page does not include the Airgentic widget script/web part. The widget needs to run on the callback page to complete authentication.

Widget works but chat returns "unauthorised"

  • Your account does not match the authorisation rules. Check with Airgentic that your email domain, address, or group membership is included.

If you need help, contact Airgentic support — see Contacting Airgentic.

← Back to Secure Services overview

You have unsaved changes