Compare commits

...

1 Commits

Author SHA1 Message Date
abeatrix 0a79676869 Add WorkOS authentication provider integration
- Add @workos-inc/node dependency for SSO authentication
- Create WorkOSAuthProvider class for handling WorkOS auth flows
- Update AuthService to support new provider architecture
- Add configuration options for WorkOS integration
2025-08-27 19:53:54 -07:00
6 changed files with 635 additions and 474 deletions
+356 -466
View File
File diff suppressed because it is too large Load Diff
+3 -2
View File
@@ -426,8 +426,6 @@
"@anthropic-ai/vertex-sdk": "^0.6.4",
"@aws-sdk/client-bedrock-runtime": "^3.840.0",
"@aws-sdk/credential-providers": "^3.840.0",
"@sap-ai-sdk/ai-api": "^1.17.0",
"@sap-ai-sdk/orchestration": "^1.17.0",
"@bufbuild/protobuf": "^2.2.5",
"@cerebras/cerebras_cloud_sdk": "^1.35.0",
"@google-cloud/vertexai": "^1.9.3",
@@ -443,10 +441,13 @@
"@opentelemetry/sdk-trace-node": "^1.30.1",
"@opentelemetry/semantic-conventions": "^1.30.0",
"@playwright/test": "^1.53.2",
"@sap-ai-sdk/ai-api": "^1.17.0",
"@sap-ai-sdk/orchestration": "^1.17.0",
"@sentry/browser": "^9.12.0",
"@streamparser/json": "^0.0.22",
"@types/uuid": "^10.0.0",
"@vscode/codicons": "^0.0.36",
"@workos-inc/node": "^7.69.1",
"archiver": "^7.0.1",
"axios": "^1.8.2",
"cheerio": "^1.0.0",
+16
View File
@@ -16,6 +16,10 @@ interface EnvironmentConfig {
messagingSenderId?: string
appId?: string
}
workos: {
apiKey: string
clientId: string
}
}
function getClineEnv(): Environment {
@@ -42,6 +46,10 @@ function getEnvironmentConfig(env: Environment): EnvironmentConfig {
messagingSenderId: "853479478430",
appId: "1:853479478430:web:2de0dba1c63c3262d4578f",
},
workos: {
apiKey: process.env.WORKOS_API_KEY || "",
clientId: process.env.WORKOS_CLIENT_ID || "",
},
}
case Environment.local:
return {
@@ -53,6 +61,10 @@ function getEnvironmentConfig(env: Environment): EnvironmentConfig {
authDomain: "cline-preview.firebaseapp.com",
projectId: "cline-preview",
},
workos: {
apiKey: process.env.WORKOS_API_KEY || "",
clientId: process.env.WORKOS_CLIENT_ID || "",
},
}
default:
return {
@@ -67,6 +79,10 @@ function getEnvironmentConfig(env: Environment): EnvironmentConfig {
messagingSenderId: "941048379330",
appId: "1:941048379330:web:45058eedeefc5cdfcc485b",
},
workos: {
apiKey: process.env.WORKOS_API_KEY || "",
clientId: process.env.WORKOS_CLIENT_ID || "",
},
}
}
}
+33 -6
View File
@@ -8,6 +8,7 @@ import { HostProvider } from "@/hosts/host-provider"
import { FEATURE_FLAGS } from "@/shared/services/feature-flags/feature-flags"
import { openExternal } from "@/utils/env"
import { FirebaseAuthProvider } from "./providers/FirebaseAuthProvider"
import { WorkOSAuthProvider } from "./providers/WorkOSAuthProvider"
const DefaultClineAccountURI = `${clineEnvConfig.appBaseUrl}/auth`
let authProviders: any[] = []
@@ -19,6 +20,7 @@ export type ServiceConfig = {
const availableAuthProviders = {
firebase: FirebaseAuthProvider,
workos: WorkOSAuthProvider,
// Add other providers here as needed
}
@@ -54,7 +56,7 @@ export class AuthService {
protected _config: ServiceConfig
protected _authenticated: boolean = false
protected _clineAuthInfo: ClineAuthInfo | null = null
protected _provider: { provider: FirebaseAuthProvider } | null = null
protected _provider: { provider: FirebaseAuthProvider | WorkOSAuthProvider } | null = null
protected _activeAuthStatusUpdateSubscriptions = new Set<[Controller, StreamingResponseHandler<AuthState>]>()
protected _controller: Controller
@@ -63,7 +65,8 @@ export class AuthService {
* @param controller - Optional reference to the Controller instance.
*/
protected constructor(controller: Controller) {
const providerName = "firebase"
// Default to firebase for backward compatibility, but can be changed via environment variable
const providerName = process.env.CLINE_AUTH_PROVIDER || "firebase"
this._config = { URI: DefaultClineAccountURI }
// Fetch AuthProviders
@@ -75,6 +78,10 @@ export class AuthService {
name: "firebase",
config: clineEnvConfig.firebase,
},
{
name: "workos",
config: clineEnvConfig.workos,
},
]
// Merge authProviders with availableAuthProviders
@@ -134,6 +141,20 @@ export class AuthService {
this._setProvider(providerName)
}
/**
* Get list of available auth providers
*/
getAvailableProviders(): string[] {
return authProviders.map((provider) => provider.name)
}
/**
* Get current auth provider name
*/
getCurrentProvider(): string | null {
return authProviders.find((provider) => provider.provider === this._provider?.provider)?.name || null
}
async getAuthToken(): Promise<string | null> {
if (!this._clineAuthInfo) {
return null
@@ -194,11 +215,17 @@ export class AuthService {
const callbackHost = await HostProvider.get().getCallbackUri()
const callbackUrl = `${callbackHost}/auth`
// Use URL object for more graceful query construction
const authUrl = new URL(this._config.URI)
authUrl.searchParams.set("callback_url", callbackUrl)
let authUrlString: string
const authUrlString = authUrl.toString()
// Handle WorkOS provider differently
if (this._provider?.provider instanceof WorkOSAuthProvider) {
authUrlString = this._provider.provider.getAuthorizationUrl(callbackUrl)
} else {
// Use URL object for more graceful query construction (Firebase and other providers)
const authUrl = new URL(this._config.URI)
authUrl.searchParams.set("callback_url", callbackUrl)
authUrlString = authUrl.toString()
}
await openExternal(authUrlString)
return String.create({ value: authUrlString })
+84
View File
@@ -0,0 +1,84 @@
# Authentication Providers
This directory contains authentication provider implementations for Cline.
## Available Providers
### Firebase Auth Provider
- **File**: `FirebaseAuthProvider.ts`
- **Description**: Handles authentication using Firebase Auth with Google and GitHub OAuth providers
- **Configuration**: Requires Firebase project configuration (API key, project ID, etc.)
### WorkOS Auth Provider
- **File**: `WorkOSAuthProvider.ts`
- **Description**: Handles authentication using WorkOS AuthKit for enterprise SSO
- **Configuration**: Requires WorkOS API key and client ID
## Configuration
Authentication providers are configured in `src/config.ts`. Each environment (production, staging, local) can have different provider configurations.
### Environment Variables
For WorkOS provider, set the following environment variables:
- `WORKOS_API_KEY`: Your WorkOS API key
- `WORKOS_CLIENT_ID`: Your WorkOS client ID
- `CLINE_AUTH_PROVIDER`: Set to "workos" to use WorkOS as the default provider (optional, defaults to "firebase")
### Example Configuration
```typescript
// In config.ts
workos: {
apiKey: process.env.WORKOS_API_KEY || "",
clientId: process.env.WORKOS_CLIENT_ID || "",
}
```
## Usage
The authentication service automatically loads all configured providers. You can switch between providers using:
```typescript
const authService = AuthService.getInstance()
// Switch to WorkOS provider
authService.authProvider = "workos"
// Get available providers
const providers = authService.getAvailableProviders() // ["firebase", "workos"]
// Get current provider
const current = authService.getCurrentProvider() // "workos"
```
## Adding New Providers
To add a new authentication provider:
1. Create a new provider class that implements the same interface as existing providers:
- `shouldRefreshIdToken(token: string): Promise<boolean>`
- `retrieveClineAuthInfo(controller: Controller): Promise<ClineAuthInfo | null>`
- `signIn(controller: Controller, token: string, provider: string): Promise<ClineAuthInfo | null>`
2. Add the provider to `availableAuthProviders` in `AuthService.ts`
3. Add configuration for the provider in `config.ts`
4. Add the provider to the `authProvidersConfigs` array in the AuthService constructor
## Authentication Flow
1. User initiates authentication via `createAuthRequest()`
2. System opens external browser with provider-specific auth URL
3. User completes authentication with the provider
4. Provider redirects back to Cline with authorization code/token
5. `handleAuthCallback()` processes the callback and exchanges code for tokens
6. User information is retrieved and stored
7. Authentication status is updated across the application
## Token Management
- **Access Tokens**: Short-lived tokens used for API requests
- **Refresh Tokens**: Long-lived tokens stored securely to refresh access tokens
- **Token Refresh**: Automatic refresh when tokens are about to expire (within 5 minutes)
@@ -0,0 +1,143 @@
import { errorService } from "@services/posthog/PostHogClientProvider"
import { WorkOS } from "@workos-inc/node"
import axios from "axios"
import { jwtDecode } from "jwt-decode"
import { clineEnvConfig } from "@/config"
import { Controller } from "@/core/controller"
import type { ClineAccountUserInfo, ClineAuthInfo } from "../AuthService"
export class WorkOSAuthProvider {
private _config: any
private _workos: WorkOS
constructor(config: any) {
this._config = config || {}
this._workos = new WorkOS(this._config.apiKey, {
https: true,
})
}
get config(): any {
return this._config
}
set config(value: any) {
this._config = value
this._workos = new WorkOS(this._config.apiKey, {
https: true,
})
}
async shouldRefreshIdToken(existingIdToken: string): Promise<boolean> {
try {
const decodedToken = jwtDecode(existingIdToken)
const exp = decodedToken.exp || 0
const expirationTime = exp * 1000
const currentTime = Date.now()
const fiveMinutesInMs = 5 * 60 * 1000
if (currentTime > expirationTime - fiveMinutesInMs) {
return true // id token is expired or about to be expired
}
return false
} catch (error) {
console.error("Error checking token expiration:", error)
return true // If we can't decode the token, assume it needs refresh
}
}
/**
* Restores the authentication token using a stored refresh token.
* @param controller - The controller instance for accessing stored secrets.
* @returns {Promise<ClineAuthInfo | null>} A promise that resolves with the authentication info or null.
*/
async retrieveClineAuthInfo(controller: Controller): Promise<ClineAuthInfo | null> {
const refreshToken = controller.stateManager.getSecretKey("clineAccountId")
if (!refreshToken) {
console.error("No stored authentication credential found.")
return null
}
try {
// Use WorkOS to refresh the access token
const { accessToken } = await this._workos.userManagement.authenticateWithRefreshToken({
refreshToken,
clientId: this._config.clientId,
})
// Fetch user info from Cline API using the access token
const userResponse = await axios.get(`${clineEnvConfig.apiBaseUrl}/api/v1/users/me`, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
})
const userInfo: ClineAccountUserInfo = userResponse.data.data
return { idToken: accessToken, userInfo }
} catch (error) {
console.error("WorkOS restore token error", error)
errorService.logMessage("WorkOS restore token error", "error")
errorService.logException(error)
throw error
}
}
/**
* Signs in the user using WorkOS authentication with an authorization code.
* @param controller - The controller instance for storing secrets.
* @param code - The authorization code from WorkOS OAuth flow.
* @param provider - The provider name (should be 'workos').
* @returns {Promise<ClineAuthInfo | null>} A promise that resolves with the authentication info.
*/
async signIn(controller: Controller, code: string, provider: string): Promise<ClineAuthInfo | null> {
if (provider !== "workos") {
throw new Error(`Unsupported provider: ${provider}`)
}
try {
// Exchange authorization code for tokens
const { user, accessToken, refreshToken } = await this._workos.userManagement.authenticateWithCode({
code,
clientId: this._config.clientId,
})
// Store the refresh token in secret storage
try {
controller.stateManager.setSecret("clineAccountId", refreshToken)
} catch (error) {
errorService.logMessage("WorkOS store token error", "error")
errorService.logException(error)
throw error
}
// Map WorkOS user to ClineAccountUserInfo format
const userInfo: ClineAccountUserInfo = {
id: user.id,
email: user.email,
displayName: user.firstName && user.lastName ? `${user.firstName} ${user.lastName}` : user.email,
createdAt: user.createdAt,
organizations: [], // WorkOS organizations would need to be fetched separately if needed
appBaseUrl: clineEnvConfig?.appBaseUrl,
}
return { idToken: accessToken, userInfo }
} catch (error) {
errorService.logMessage("WorkOS sign-in error", "error")
errorService.logException(error)
throw error
}
}
/**
* Generates the WorkOS authorization URL for OAuth flow.
* @param redirectUri - The redirect URI after authentication.
* @returns {string} The authorization URL.
*/
getAuthorizationUrl(redirectUri: string): string {
return this._workos.userManagement.getAuthorizationUrl({
provider: "authkit",
clientId: this._config.clientId,
redirectUri,
})
}
}