OAuth Authentication
@loopstack/oauth-module provides a provider-agnostic OAuth 2.0 framework. @loopstack/google-workspace-module adds Google as a provider.
Setup
import { Module } from '@nestjs/common';
import { LoopCoreModule } from '@loopstack/core';
import { GoogleWorkspaceModule } from '@loopstack/google-workspace-module';
@Module({
imports: [LoopCoreModule, GoogleWorkspaceModule],
providers: [MyWorkflow],
exports: [MyWorkflow],
})
export class MyModule {}OAuth as Sub-Workflow
The simplest approach: launch the built-in OAuthWorkflow when authentication is needed.
import {
BaseWorkflow,
CallbackSchema,
Final,
Guard,
Initial,
InjectWorkflow,
Transition,
Workflow,
} from '@loopstack/common';
import { LinkDocument, MarkdownDocument } from '@loopstack/core';
import { OAuthWorkflow } from '@loopstack/oauth-module';
@Workflow({ uiConfig: __dirname + '/calendar.ui.yaml' })
export class CalendarWorkflow extends BaseWorkflow {
@InjectTool() private calendarFetchEvents: CalendarFetchEventsTool;
@InjectWorkflow() private oAuth: OAuthWorkflow;
events?: CalendarEvent[];
requiresAuthentication?: boolean;
@Initial({ to: 'calendar_fetched' })
async fetchEvents(args: { calendarId: string }) {
const result = await this.calendarFetchEvents.call({
calendarId: args.calendarId,
});
this.requiresAuthentication = result.data!.error === 'unauthorized';
this.events = result.data!.events;
}
// If unauthorized -> launch OAuth sub-workflow
@Transition({ from: 'calendar_fetched', to: 'awaiting_auth', priority: 10 })
@Guard('needsAuth')
async authRequired() {
const result = await this.oAuth.run(
{ provider: 'google', scopes: ['https://www.googleapis.com/auth/calendar.readonly'] },
{ alias: 'oAuth', callback: { transition: 'authCompleted' } },
);
await this.repository.save(
LinkDocument,
{
label: 'Google authentication required',
workflowId: result.workflowId,
embed: true,
expanded: true,
},
{ id: `link_${result.workflowId}` },
);
}
needsAuth(): boolean {
return !!this.requiresAuthentication;
}
// After auth -> retry from start
@Transition({ from: 'awaiting_auth', to: 'start', wait: true, schema: CallbackSchema })
async authCompleted(payload: { workflowId: string }) {
await this.repository.save(
LinkDocument,
{
status: 'success',
label: 'Google authentication completed',
workflowId: payload.workflowId,
},
{ id: `link_${payload.workflowId}` },
);
}
// Success -> display results
@Final({ from: 'calendar_fetched' })
async displayResults() {
await this.repository.save(MarkdownDocument, {
markdown: this.render(__dirname + '/templates/summary.md', { events: this.events }),
});
}
}Using Tokens in Custom Tools
import { Inject } from '@nestjs/common';
import { BaseTool, Tool, ToolResult } from '@loopstack/common';
import { OAuthTokenStore } from '@loopstack/oauth-module';
@Tool({
uiConfig: { description: 'Fetches Google Calendar events.' },
schema: z.object({ calendarId: z.string().default('primary') }).strict(),
})
export class CalendarFetchEventsTool extends BaseTool {
@Inject() private tokenStore: OAuthTokenStore;
async call(args: { calendarId: string }): Promise<ToolResult> {
const accessToken = await this.tokenStore.getValidAccessToken(this.ctx.context.userId, 'google');
if (!accessToken) {
return { data: { error: 'unauthorized' } };
}
const response = await fetch(`https://www.googleapis.com/calendar/v3/calendars/${args.calendarId}/events`, {
headers: { Authorization: `Bearer ${accessToken}` },
});
return { data: await response.json() };
}
}Creating a Custom OAuth Provider
import { Injectable, OnModuleInit } from '@nestjs/common';
import { OAuthProviderInterface, OAuthProviderRegistry, OAuthTokenSet } from '@loopstack/oauth-module';
@Injectable()
export class MyOAuthProvider implements OAuthProviderInterface, OnModuleInit {
readonly providerId = 'my-provider';
readonly defaultScopes = ['read', 'write'];
constructor(private registry: OAuthProviderRegistry) {}
onModuleInit() {
this.registry.register(this);
}
buildAuthUrl(scopes: string[], state: string): string {
const params = new URLSearchParams({
client_id: process.env.MY_CLIENT_ID!,
redirect_uri: process.env.MY_REDIRECT_URI!,
scope: scopes.join(' '),
state,
response_type: 'code',
});
return `https://my-provider.com/oauth/authorize?${params}`;
}
async exchangeCode(code: string): Promise<OAuthTokenSet> {
// POST to token endpoint
}
async refreshToken(refreshToken: string): Promise<OAuthTokenSet> {
// POST to refresh endpoint
}
}Environment Variables
GOOGLE_CLIENT_ID=...
GOOGLE_CLIENT_SECRET=...
GOOGLE_OAUTH_REDIRECT_URI=...Token Lifecycle
OAuthWorkflowgenerates auth URL and shows it to the user- User completes OAuth in browser
- Token is exchanged and stored per user per provider
OAuthTokenStore.getValidAccessToken()auto-refreshes expired tokens- Tools return
{ error: 'unauthorized' }if no token exists - Workflow guard detects the error and launches OAuth sub-workflow
Registry References
- google-oauth-example — Google Calendar fetch with OAuth sub-workflow, custom calendar tool, and Google Workspace agent with tool calling
- github-oauth-example — GitHub OAuth integration with repos overview and GitHub agent with 25+ tools
Last updated on