auth-code-listener.ts
services/oauth/auth-code-listener.ts
212
Lines
6650
Bytes
1
Exports
7
Imports
10
Keywords
What this is
This page documents one file from the repository and includes its full source so you can read it without leaving the docs site.
Beginner explanation
This file is one piece of the larger system. Its name, directory, imports, and exports show where it fits. Start by reading the exports and related files first.
How it is used
Start from the exports list and related files. Those are the easiest clues for where this file fits into the system.
Expert explanation
Architecturally, this file intersects with integrations. It contains 212 lines, 7 detected imports, and 1 detected exports.
Important relationships
Detected exports
AuthCodeListener
Keywords
portvoidpendingresponseprivatelocalserverserverrejectoauthredirectscopes
Detected imports
httphttpnetsrc/services/analytics/index.js../../constants/oauth.js../../utils/log.js./client.js
Source notes
This page embeds the full file contents. Small or leaf files are still indexed honestly instead of being over-explained.
Full source
import type { IncomingMessage, ServerResponse } from 'http'
import { createServer, type Server } from 'http'
import type { AddressInfo } from 'net'
import { logEvent } from 'src/services/analytics/index.js'
import { getOauthConfig } from '../../constants/oauth.js'
import { logError } from '../../utils/log.js'
import { shouldUseClaudeAIAuth } from './client.js'
/**
* Temporary localhost HTTP server that listens for OAuth authorization code redirects.
*
* When the user authorizes in their browser, the OAuth provider redirects to:
* http://localhost:[port]/callback?code=AUTH_CODE&state=STATE
*
* This server captures that redirect and extracts the auth code.
* Note: This is NOT an OAuth server - it's just a redirect capture mechanism.
*/
export class AuthCodeListener {
private localServer: Server
private port: number = 0
private promiseResolver: ((authorizationCode: string) => void) | null = null
private promiseRejecter: ((error: Error) => void) | null = null
private expectedState: string | null = null // State parameter for CSRF protection
private pendingResponse: ServerResponse | null = null // Response object for final redirect
private callbackPath: string // Configurable callback path
constructor(callbackPath: string = '/callback') {
this.localServer = createServer()
this.callbackPath = callbackPath
}
/**
* Starts listening on an OS-assigned port and returns the port number.
* This avoids race conditions by keeping the server open until it's used.
* @param port Optional specific port to use. If not provided, uses OS-assigned port.
*/
async start(port?: number): Promise<number> {
return new Promise((resolve, reject) => {
this.localServer.once('error', err => {
reject(
new Error(`Failed to start OAuth callback server: ${err.message}`),
)
})
// Listen on specified port or 0 to let the OS assign an available port
this.localServer.listen(port ?? 0, 'localhost', () => {
const address = this.localServer.address() as AddressInfo
this.port = address.port
resolve(this.port)
})
})
}
getPort(): number {
return this.port
}
hasPendingResponse(): boolean {
return this.pendingResponse !== null
}
async waitForAuthorization(
state: string,
onReady: () => Promise<void>,
): Promise<string> {
return new Promise<string>((resolve, reject) => {
this.promiseResolver = resolve
this.promiseRejecter = reject
this.expectedState = state
this.startLocalListener(onReady)
})
}
/**
* Completes the OAuth flow by redirecting the user's browser to a success page.
* Different success pages are shown based on the granted scopes.
* @param scopes The OAuth scopes that were granted
* @param customHandler Optional custom handler to serve response instead of redirecting
*/
handleSuccessRedirect(
scopes: string[],
customHandler?: (res: ServerResponse, scopes: string[]) => void,
): void {
if (!this.pendingResponse) return
// If custom handler provided, use it instead of default redirect
if (customHandler) {
customHandler(this.pendingResponse, scopes)
this.pendingResponse = null
logEvent('tengu_oauth_automatic_redirect', { custom_handler: true })
return
}
// Default behavior: Choose success page based on granted permissions
const successUrl = shouldUseClaudeAIAuth(scopes)
? getOauthConfig().CLAUDEAI_SUCCESS_URL
: getOauthConfig().CONSOLE_SUCCESS_URL
// Send browser to success page
this.pendingResponse.writeHead(302, { Location: successUrl })
this.pendingResponse.end()
this.pendingResponse = null
logEvent('tengu_oauth_automatic_redirect', {})
}
/**
* Handles error case by sending a redirect to the appropriate success page with an error indicator,
* ensuring the browser flow is completed properly.
*/
handleErrorRedirect(): void {
if (!this.pendingResponse) return
// TODO: swap to a different url once we have an error page
const errorUrl = getOauthConfig().CLAUDEAI_SUCCESS_URL
// Send browser to error page
this.pendingResponse.writeHead(302, { Location: errorUrl })
this.pendingResponse.end()
this.pendingResponse = null
logEvent('tengu_oauth_automatic_redirect_error', {})
}
private startLocalListener(onReady: () => Promise<void>): void {
// Server is already created and listening, just set up handlers
this.localServer.on('request', this.handleRedirect.bind(this))
this.localServer.on('error', this.handleError.bind(this))
// Server is already listening, so we can call onReady immediately
void onReady()
}
private handleRedirect(req: IncomingMessage, res: ServerResponse): void {
const parsedUrl = new URL(
req.url || '',
`http://${req.headers.host || 'localhost'}`,
)
if (parsedUrl.pathname !== this.callbackPath) {
res.writeHead(404)
res.end()
return
}
const authCode = parsedUrl.searchParams.get('code') ?? undefined
const state = parsedUrl.searchParams.get('state') ?? undefined
this.validateAndRespond(authCode, state, res)
}
private validateAndRespond(
authCode: string | undefined,
state: string | undefined,
res: ServerResponse,
): void {
if (!authCode) {
res.writeHead(400)
res.end('Authorization code not found')
this.reject(new Error('No authorization code received'))
return
}
if (state !== this.expectedState) {
res.writeHead(400)
res.end('Invalid state parameter')
this.reject(new Error('Invalid state parameter'))
return
}
// Store the response for later redirect
this.pendingResponse = res
this.resolve(authCode)
}
private handleError(err: Error): void {
logError(err)
this.close()
this.reject(err)
}
private resolve(authorizationCode: string): void {
if (this.promiseResolver) {
this.promiseResolver(authorizationCode)
this.promiseResolver = null
this.promiseRejecter = null
}
}
private reject(error: Error): void {
if (this.promiseRejecter) {
this.promiseRejecter(error)
this.promiseResolver = null
this.promiseRejecter = null
}
}
close(): void {
// If we have a pending response, send a redirect before closing
if (this.pendingResponse) {
this.handleErrorRedirect()
}
if (this.localServer) {
// Remove all listeners to prevent memory leaks
this.localServer.removeAllListeners()
this.localServer.close()
}
}
}