Adding user authentication to your bot via OAuth
OAuth can be very useful especially in cases where validating the identify of the user is crucial. In the example that follows we walk through how we was able to setup OAuth verification with Github as my platform of choice in our Botpress bot.
This was accomplished with the help of a custom integration for setting up OAuth with Github.
Demo
When the user is authenticated
When the user is unauthenticated
Bot Setup
The bot setup is straightforward. First, the user is prompted to log in by clicking on a link generated by the integration. Once the login is successful, an event is emitted. This event is captured by a trigger node, which then hands off control to an autonomous node.
![](https://files.readme.io/79f803935423d1748634a0fa8674d9985caa8c706235c6e614008253cfedb2a2-image.png)
Integration
The integration has a couple of responsibilities:
- Generating the OAuth link that users will click on.
- Capturing a successful authentication which then emits an event containing the conversation ID and data about the authenticated user.
This is achieved using an action ( here called “Generate OAuth2 URL” ) and the integration’s Botpress webhook handler. The webhook captures successful authentication and emits a Botpress event ( in this case, “User Authenticated” ).
Writing this integration
You can use the Building to bootstrap an integration, then add the following code as a starting point to achieve the above result. Once you get it working, we suggest you modify the integration to fit your purposes.
In integration.definition.ts
we define the events (which bot builders can hook onto in the Studio), and actions which allow for generating the URL that should be displayed to the end user.
import { conversation, IntegrationDefinition, z } from '@botpress/sdk'
import { integrationName } from './package.json'
import { DEFAULT_STORE_KEY } from 'src/store'
export default new IntegrationDefinition({
name: integrationName,
version: '0.0.1',
readme: 'hub.md',
icon: 'icon.svg',
configuration: {
schema: z.object({
clientId: z.string(),
clientSecret: z.string(),
state: z.string().default("secure_random_string").hidden(), // This is used for the authentication handshake, you can change it to a random string.
})
},
events: {
onUserAuthenticated: {
title: 'User Authenticated',
description: 'Fires when a user has been authenticated',
schema: z.object({
userData: z.any(),
conversationIdxx: z.string(),
})
}
},
actions: {
generateUrl: {
title: 'Generate OAuth2 URL',
description: 'Generates an OAuth2 URL to authenticate the user',
input: {
schema: z.object({
conversationId: z.string().default('{{event.conversationId}}').hidden()
})
},
output: {
schema: z.object({
url: z.string()
})
}
}
},
states: {
[DEFAULT_STORE_KEY]: {
type: 'integration',
schema: z.object({
bpWebhookUrl: z.string(),
conversationId: z.string(),
}),
}
}
})
In src/index.ts we have the main logic for generating a shareable sign-up url for Oauth, and a handler method to handle the Oauth response.
import * as sdk from "@botpress/sdk";
import * as bp from ".botpress";
import { Store } from './store'
export default new bp.Integration({
register: async ({ webhookUrl, client, ctx }) => {
const store = new Store(client, ctx);
await store.initialize({
bpWebhookUrl: webhookUrl,
conversationId: "",
})
console.log("Integration registered")
},
unregister: async () => {},
actions: {
generateUrl: async ({ client, input, ctx } ) => {
const store = new Store(client, ctx);
await store.load();
await store.set("conversationId", input.conversationId);
const webhookUrl = await store.get("bpWebhookUrl");
const clientId = ctx.configuration.clientId;
const redirectUri = webhookUrl; // Redirect back to the integration
const state = ctx.configuration.state; // For CSRF protection
const scope = "read:user"; // Request access to read user profile
const githubOAuthUrl = `https://github.com/login/oauth/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(
redirectUri
)}&scope=${scope}&state=${state}`;
return { url: githubOAuthUrl };
},
},
channels: {},
handler: async ({ req, client, ctx }) => {
const store = new Store(client, ctx);
await store.load();
const webhookUrl = await store.get("bpWebhookUrl");
// Construct a URLSearchParams object
const params = new URLSearchParams(req.query);
// Extract query parameters
const code = params.get("code"); // Extract 'code'
const sec_string = params.get("state"); // Extract 'state'
// Verify the `state` to protect against CSRF
if (sec_string !== ctx.configuration.state) {
return { status: 403, message: "Invalid state" };
}
// Exchange the code for an access token
const tokenResponse: any = await fetch("https://github.com/login/oauth/access_token", {
method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" },
body: JSON.stringify({
client_id: ctx.configuration.clientId,
client_secret: ctx.configuration.clientSecret,
code,
redirect_uri: webhookUrl,
}),
}).then((res) => res.json());
// Fetch user data
const userData: any = await fetch("https://api.github.com/user", {
headers: {
Authorization: `Bearer ${tokenResponse.access_token}`,
},
}).then((res) => res.json());
// Send a message to the user
const conversationId = await store.get("conversationId");
await client.createEvent({
type: "onUserAuthenticated",
conversationId: conversationId,
payload: {
conversationIdxx: conversationId,
userData: userData,
}
})
return { status: 200, message: "Success" };
},
});
In src/store.ts we add some logic to be able to save authentication parameters which will help us know who is who when getting a callback request from Oauth.
import * as sdk from "@botpress/sdk";
export const DEFAULT_STORE_KEY = "globalStore";
export class Store {
private client: sdk.IntegrationSpecificClient<any>;
private ctx: sdk.IntegrationContext;
private globalStoreKey: string;
private validKeys: string[] = [];
constructor(
client: sdk.IntegrationSpecificClient<any>,
ctx: sdk.IntegrationContext,
globalStoreKey = DEFAULT_STORE_KEY
) {
this.client = client;
this.ctx = ctx;
this.globalStoreKey = globalStoreKey;
}
private async fetchGlobalStore(): Promise<Record<string, any>> {
const result = await this.client.getState({
type: "integration",
id: this.ctx.integrationId,
name: this.globalStoreKey,
});
return result.state.payload || {}; // Default to an empty object if no state exists
}
private async updateGlobalStore(store: Record<string, any>): Promise<void> {
console.log('updating store', store)
await this.client.setState({
type: "integration",
id: this.ctx.integrationId,
name: this.globalStoreKey,
payload: store,
});
}
async initialize(initialState: Record<string, any>): Promise<void> {
this.validKeys = Object.keys(initialState);
await this.updateGlobalStore(initialState);
}
async load(): Promise<void> {
const store = await this.fetchGlobalStore();
this.validKeys = Object.keys(store);
}
private validateKey(key: string): void {
if (!this.validKeys.includes(key)) {
throw new Error(`Key ${key} is not a valid key, valid keys are: ${this.validKeys.join(", ")}`);
}
}
async set(key: string, value: any): Promise<void> {
this.validateKey(key);
const store = await this.fetchGlobalStore();
console.log('got store', store);
store[key] = value; // Update the key in the global store
await this.updateGlobalStore(store); // Save the updated store back
}
async get(key: string): Promise<any> {
this.validateKey(key);
const store = await this.fetchGlobalStore();
return store[key]; // Retrieve the value for the key
}
}
After adding this code, deploy it to the bot you want to work on, and configure it using Github parameters (or whatever platform you'd like to port this to) and hit "Save Configuration".
If you'd like to try out the Github example above, you can use this link to create the credentials :
https://github.com/settings/developers#oauth-apps
The "Authorization callback URL" should be your webhook URL, located in the configuration page of the integration. There is no need to enable the device flow.
That's it, now you can publish your bot, and try it out. Once that works, feel free to use this code as a starting point for your Oauth Integration.
Updated 15 days ago