Skip to Content
DocumentationFeaturesOAuth Authentication

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

  1. OAuthWorkflow generates auth URL and shows it to the user
  2. User completes OAuth in browser
  3. Token is exchanged and stored per user per provider
  4. OAuthTokenStore.getValidAccessToken() auto-refreshes expired tokens
  5. Tools return { error: 'unauthorized' } if no token exists
  6. 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