This supplemental guide covers how to build custom widgets that integrate with third-party services using Passports — Appspace's mechanism for managing authentication with external providers like Google, Microsoft, Zoom, Slack, and more.
In this article:
Prerequisites:
Before reading this guide, complete the Custom Widget Developer Guide to understand the basics of widget creation, the Widget API, and schema configuration.
What Are Passports?
A Passport in Appspace is a secure authentication connector that allows your widget to communicate with third-party services. Instead of managing OAuth tokens directly in your widget code, Passports handle the entire authentication lifecycle:
-
Administrator connects the Passport (OAuth popup)
-
Appspace stores the credentials securely
-
Your widget calls
callAuthenticatedAPI()— the Console injects the correct auth headers automatically -
Token refresh is handled transparently by Appspace
Your widget code never sees the actual credentials. It only sends API requests by name, and the Console proxies them with the correct authentication.
Two Approaches to Authentication
When your widget needs to talk to a third-party service, you have two options:
| Approach | When to Use | What Happens |
|---|---|---|
| Built-in Passport | The service already has an Appspace Passport (Google, Microsoft, Zoom, etc.) | Administrator selects an existing Passport from a dropdown in the widget configurator |
| Custom (Embedded) Passport | The service is not in Appspace's built-in list, or you need custom OAuth configuration | You bundle a |
Both approaches use the same Widget API method (callAuthenticatedAPI) at runtime. The only difference is how the Passport is provisioned.
Approach 1: Using Built-in Passports
If the third-party service already has an Appspace Passport, your widget can reference it without bundling any passport file. The administrator will select an existing Passport when configuring the widget.
Step 1: Add authentications to schema.json
{
"authentications": [
{
"label": "Google Calendar",
"authenticationType": "passport",
"authKey": "googleCalAuth",
"metadata": {
"passportProvider": "google"
}
}
]
}
The passportProvider value tells the widget configurator which Passports to show in the dropdown. See the Built-in Passport Reference for all available values.
Step 2: Add authenticatedApis to schema.json
{
"authenticatedApis": [
{
"label": "Get Calendar Events",
"name": "getCalendarEvents",
"url": "https://www.googleapis.com/calendar/v3/calendars/primary/events",
"method": "GET",
"authenticationKey": "googleCalAuth",
"headers": {
"Authorization": "Bearer {authorization.googleCalAuth.token}"
}
}
]
}
Step 3: Call the API from your widget
var response = await window.appspace.widgetApi.callAuthenticatedAPI('getCalendarEvents', {
params: { maxResults: '10', orderBy: 'startTime', singleEvents: 'true' }
});
if (response.status === 200 && response.data && response.data.isSuccess) {
var events = response.data.body; // Parsed JSON
}
Important: Only pass params. Do not pass method, headers, or body — these are defined in the schema and handled by the proxy.
What the administrator sees
When configuring the widget, the administrator sees a dropdown listing all Passports matching the passportProvider value. They select one (or create a new one) and connect it.
Approach 2: Bundling a Custom Passport
When your target service is not in the built-in list, or you need specific OAuth scopes, bundle a Passport JSON file inside your widget ZIP.
Step 1: Create the passport JSON file
Create a file in the passport/ directory of your widget project.
OAuth Example — passport/my-api-connector.json:
{
"name": "My API Connector",
"applicationName": "custom",
"authenticationType": "OAuth",
"isCustomApplication": true,
"customApplicationInfo": {
"name": "my-api-connector",
"displayName": "My API Connector",
"description": "OAuth connector for My API service",
"categoryType": "Custom",
"authenticationType": "OAuth",
"parentName": "custom",
"metadata": {
"clientId": "<your-client-id>",
"clientSecret": "<your-client-secret>",
"authenticationUrl": "https://api.example.com/oauth/authorize",
"tokenUrl": "https://api.example.com/oauth/token",
"profileUrl": "https://api.example.com/api/me",
"profileAccountEmailPath": "$.email",
"profileAccountIdPath": "$.id",
"profileAccountNamePath": "$.name",
"scope": "read write"
}
},
"metadata": {}
}
Step 2: Reference in schema.json
Add the passport property to your schema and wire up authentications and authenticatedApis:
{
"passport": {
"filePath": "passport/my-api-connector.json"
},
"authentications": [
{
"label": "My API Authentication",
"authenticationType": "passport",
"authKey": "myApiAuth",
"metadata": {
"passportProvider": "custom:my-api-connector"
}
}
],
"authenticatedApis": [
{
"label": "Get Data",
"name": "getData",
"url": "https://api.example.com/v1/data",
"method": "GET",
"authenticationKey": "myApiAuth",
"headers": {
"Authorization": "Bearer {authorization.myApiAuth.token}"
}
}
]
}
Critical: The passportProvider value must follow the pattern custom:<name> where <name> matches customApplicationInfo.name in your passport JSON file.
Step 3: Include passport in your ZIP
Ensure your packaging script includes the passport/ directory:
// In scripts/package-widget.js
if (fs.existsSync('passport')) {
archive.directory('passport/', 'passport');
}
Or if zipping manually, include the passport/ folder in the ZIP alongside widget.html and schema.json.
What happens on upload
When you upload the widget ZIP to Appspace:
-
The backend reads
schema.jsonand findspassport.filePath -
It extracts the passport JSON from the ZIP
-
It automatically creates a custom Passport in the Appspace passport service
-
The widget configurator shows the passport pre-selected for the administrator
-
The administrator completes the OAuth connection flow
What the administrator sees
Instead of a dropdown, the administrator sees a dedicated "Connect" button for the bundled passport. After connecting (OAuth popup), the button shows the connected status with options to reconnect or disconnect.
Schema Configuration for Authentication
The three schema properties work together:
passport (optional) → Bundles a passport JSON for auto-creation
│
authentications (required) → Declares what authentication the widget needs
│
authenticatedApis (required) → Defines the actual API endpoints to call
The passport property
| Field | Type | Required | Description |
|---|---|---|---|
|
|
| Yes | Relative path to passport JSON inside the widget ZIP |
Only needed for custom (embedded) passports. Omit this entirely when using built-in passports.
The authentications array
Each entry declares one authentication requirement:
| Field | Type | Required | Description |
|---|---|---|---|
|
|
| No | Display label in widget configurator |
|
|
| Yes | Must be |
|
|
| Yes | Unique key to reference this auth (used by |
|
|
| No | Filters which passports are shown. Use |
|
|
| No | Informational: |
|
|
| No | Whether admin-level site permissions are required |
The authenticatedApis array
Each entry defines one API endpoint:
| Field | Type | Required | Description |
|---|---|---|---|
|
|
| Yes | Human-readable label |
|
|
| Yes | Unique name — used in |
|
|
| Yes | Full URL with optional |
|
|
| Yes |
|
|
|
| Yes | Must match an |
|
|
| No | Static headers. Use |
|
|
| No | Static request body for POST/PUT |
Making Authenticated API Calls
Once authentication is configured, making API calls is straightforward:
// Call by the 'name' defined in authenticatedApis
var response = await window.appspace.widgetApi.callAuthenticatedAPI('getData', {
params: {
userId: '12345',
limit: '50'
}
});
// Response structure
// response.status — HTTP status code (e.g., 200)
// response.statusText — HTTP status text (e.g., "OK")
// response.data.statusCode — Backend status code
// response.data.body — Response body (auto-parsed if JSON)
// response.data.headers — Response headers
// response.data.contentType — Response content type
// response.data.isSuccess — Boolean indicating success
if (response.status === 200 && response.data && response.data.isSuccess) {
var data = response.data.body;
// Use the data...
}
Key rules:
-
Only pass
params— do not passmethod,headers, orbody -
The
paramsobject is used for URL template variable substitution and query parameters -
The Console proxy handles authentication header injection, URL construction, and the actual HTTP request
-
If the API returns JSON,
response.data.bodyis automatically parsed
Template Variables in API URLs
API URLs support template variables that are resolved at call time:
{
"url": "https://api.example.com/users/{param.userId}/posts?page={param.page}",
"method": "GET",
"authenticationKey": "myApiAuth"
}
When calling:
await window.appspace.widgetApi.callAuthenticatedAPI('getUserPosts', {
params: { userId: '42', page: '2' }
});
The resolved URL becomes: https://api.example.com/users/42/posts?page=2
Special variables
| Variable Pattern | Source |
|---|---|
|
| From the |
|
| OAuth token for the specified auth key (injected by proxy) |
|
| Current user's ID (injected by host) |
Built-in Passport Reference
| Passport | passportProvider | Auth Type | Category |
|---|---|---|---|
| Google Calendar |
| OAuth | Calendar |
| Google Drive |
| OAuth | Library |
| Google Meet Conferencing |
| OAuth | Conference |
Microsoft
| Passport | passportProvider | Auth Type | Category |
|---|---|---|---|
| Microsoft 365 |
| OAuth | Calendar |
| Microsoft Teams |
| OAuth | Publishing |
| Microsoft Teams Conferencing |
| OAuth | Conference |
| Power BI |
| OAuth | Dashboard |
| SharePoint |
| OAuth | Library |
| SharePoint Publishing |
| OAuth | Publishing |
| Microsoft Viva Engage |
| OAuth | Publishing |
| Microsoft Entra ID |
| OAuth | Org Chart |
| Microsoft 365 Presence |
| OAuth | Presence |
| Microsoft 365 Calendar Permission |
| OAuth | Delegations |
| Appspace SharePoint Intranet |
| OAuth | Library |
Zoom
| Passport | passportProvider | Auth Type | Category |
|---|---|---|---|
| Zoom Meetings |
| OAuth | Library |
| Zoom Conferencing |
| OAuth | Conference |
Webex
| Passport | passportProvider | Auth Type | Category |
|---|---|---|---|
| Webex Teams |
| OAuth | Publishing |
| Webex Meetings |
| OAuth | Library |
| Webex Conferencing |
| OAuth | Conference |
Social & Publishing
| Passport | passportProvider | Auth Type | Category |
|---|---|---|---|
| |
| OAuth | Social |
| |
| OAuth | Social |
| Slack |
| OAuth | Publishing |
| Workplace (Facebook) |
| OAuth | Publishing |
Other Services
| Passport | passportProvider | Auth Type | Category |
|---|---|---|---|
| Salesforce |
| OAuth | Dashboard |
| Tableau |
| Custom Auth | Dashboard |
| ServiceNow |
| OAuth | Library |
| Box |
| OAuth | Library |
Note:
When using a built-in passport, you do not need a passport property or passport/ directory in your widget ZIP. The administrator selects an existing Passport from the dropdown when configuring the widget.
Custom Passport JSON Reference
When bundling a custom passport, the JSON file must follow this structure:
Required Fields
| Field | Value | Description |
|---|---|---|
|
|
| Display name for the passport |
|
|
| Must be |
|
|
| Authentication method |
|
|
| Must be |
|
|
| Short identifier (used in |
|
|
| Human-readable name |
|
|
| Description text |
|
|
| Category (typically |
|
|
| Must match parent |
|
|
| Must be |
|
|
| Auth-specific credentials (see below) |
|
|
| Additional metadata (typically empty) |
OAuth Metadata Fields
| Field | Value | Description |
|---|---|---|
|
| Yes | OAuth application client ID |
|
| Yes | OAuth application client secret |
|
| Yes | OAuth authorization endpoint |
|
| Yes | OAuth token exchange endpoint |
|
| No | User profile endpoint (for account display) |
|
| No | JSONPath to extract email from profile (e.g., |
|
| No | JSONPath to extract user ID (e.g., |
|
| No | JSONPath to extract display name (e.g., |
|
| No | OAuth scopes (space-separated) |
|
| No | Scope separator character (default: |
|
| No | Inherit OAuth URLs from an existing built-in provider |
|
| No |
|
|
| No | Refresh interval in minutes |
Complete Examples
Example 1: Microsoft Graph Widget (Built-in Passport)
schema.json (relevant sections):
{
"authentications": [
{
"label": "Microsoft 365",
"authenticationType": "passport",
"authKey": "msGraph",
"metadata": {
"passportProvider": "microsoft",
"passportType": "oauth2"
}
}
],
"authenticatedApis": [
{
"label": "Get Calendar Events",
"name": "getEvents",
"url": "https://graph.microsoft.com/v1.0/me/calendarview?startDateTime={param.startDate}&endDateTime={param.endDate}",
"method": "GET",
"authenticationKey": "msGraph",
"headers": {
"Authorization": "Bearer {authorization.msGraph.token}"
}
}
]
}
Widget code:
var now = new Date();
var nextWeek = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000);
var response = await window.appspace.widgetApi.callAuthenticatedAPI('getEvents', {
params: {
startDate: now.toISOString(),
endDate: nextWeek.toISOString()
}
});
if (response.status === 200 && response.data && response.data.isSuccess) {
var events = response.data.body.value;
events.forEach(function(event) {
console.log(event.subject, event.start.dateTime);
});
}
Example 2: Custom OAuth Widget (Bundled OAuth Passport)
A widget that integrates with a custom SaaS API using OAuth.
passport/my-saas-connector.json:
{
"name": "My SaaS Platform",
"applicationName": "custom",
"authenticationType": "OAuth",
"isCustomApplication": true,
"customApplicationInfo": {
"name": "my-saas-connector",
"displayName": "My SaaS Platform",
"description": "OAuth connector for My SaaS Platform API",
"categoryType": "Custom",
"authenticationType": "OAuth",
"parentName": "custom",
"metadata": {
"clientId": "your-client-id-here",
"clientSecret": "your-client-secret-here",
"authenticationUrl": "https://auth.my-saas.com/oauth/authorize",
"tokenUrl": "https://auth.my-saas.com/oauth/token",
"profileUrl": "https://api.my-saas.com/v1/me",
"profileAccountEmailPath": "$.email",
"profileAccountIdPath": "$.id",
"profileAccountNamePath": "$.display_name",
"scope": "read:data write:data"
}
},
"metadata": {}
}
Troubleshooting
Passport not appearing in widget configurator
Symptom: No passport dropdown or connect button shown.
Solutions:
-
Ensure
authenticationsarray exists inschema.jsonwith at least one entry -
Verify
authenticationTypeis"passport"(not"custom") -
For built-in passports: verify the
passportProvidervalue matches a known provider -
For bundled passports: verify
passport.filePathpoints to a valid JSON file in the ZIP
"Authentication required" error on API call
Symptom: callAuthenticatedAPI() returns an error.
Solutions:
-
Ensure the administrator has connected the passport
-
Verify
authenticationKeyinauthenticatedApismatches anauthKeyinauthentications -
Check that the passport is active (not expired or revoked)
OAuth connection fails
Symptom: OAuth popup closes without completing.
Solutions:
-
Verify
clientIdandclientSecretin your passport JSON are correct -
Ensure
authenticationUrlandtokenUrlare valid endpoints -
Check that the OAuth app's redirect URI includes your Appspace domain
-
Verify the
scopeis valid for the target API
API call returns unexpected data
Symptom: response.data.body is empty or malformed.
Solutions:
-
Verify the API URL is correct (check template variable substitution)
-
Ensure you're only passing
params— notmethod,headers, orbody -
Check that the OAuth scope includes permission for the requested endpoint
-
Look at
response.data.statusCodeandresponse.data.headersfor clues
Custom passport not auto-created on upload
Symptom: Widget uploads but passport doesn't appear.
Solutions:
-
Verify the
passport/directory is included in the widget ZIP at the root level -
Check that
passport.filePathin schema.json matches the actual file path -
Ensure the passport JSON follows the required structure (all required fields present)
-
Verify
isCustomApplication: trueis set in the passport JSON -
Check that
applicationNameandparentNameare both"custom"
