Skip to main content

OAuth Capability

The OAuth capability implements the OAuth 2.0 authorization code flow for connecting external services (GitHub, Google, etc.) to the Ever Works platform. It manages the full lifecycle: generating authorization URLs, handling callbacks, exchanging codes for tokens, storing credentials, and disconnecting providers.

Source: apps/api/src/plugins-capabilities/oauth/

Architecture

OAuthModule
├── OAuthController -- REST API endpoints
├── OAuthService -- OAuth flow orchestration
├── OAuthFacadeService -- Plugin OAuth operations
├── OAuthTokenRepository -- Token persistence (TypeORM)
└── PluginSettingsService -- OAuth client credentials
@Module({
imports: [FacadesModule, DatabaseModule],
controllers: [OAuthController],
providers: [OAuthService],
exports: [OAuthService]
})
export class OAuthModule {}

The module relies on PluginsModule being registered globally via forRoot() at the application root level for access to PluginSettingsService.

OAuth 2.0 Flow

API Endpoints

All endpoints are under /api/oauth and require JWT authentication.

List Providers

GET /api/oauth/providers
Authorization: Bearer <jwt-token>

Returns all available OAuth providers and configuration status.

Response:

{
"configured": true,
"providers": [{ "id": "github", "name": "GitHub", "enabled": true }]
}

Check Connection

GET /api/oauth/:providerId/connection
Authorization: Bearer <jwt-token>

Checks if the current user has a valid OAuth connection.

Response (connected):

{
"id": "github",
"name": "GitHub",
"enabled": true,
"connected": true,
"username": "octocat",
"email": "[email protected]",
"avatarUrl": "https://avatars.githubusercontent.com/..."
}

Get Authorization URL

GET /api/oauth/:providerId/connect/url?callbackUrl=...&forceConsent=true
Authorization: Bearer <jwt-token>

Generates the OAuth authorization URL for the specified provider. The CSRF state is always server-minted (never client-supplied) and bound to the caller via an HttpOnly ew_oauth_state cookie; it is also returned in the response body so a proxying web tier can mirror it into its own cookie.

Query Parameters:

ParameterTypeRequiredDescription
callbackUrlstringNoCustom redirect URI after authorization
forceConsentstringNoSet to "true" to force re-authorization

Response:

{
"url": "https://github.com/login/oauth/authorize?client_id=...&state=abc123&scope=repo,user",
"state": "abc123def456..."
}

Handle Callback

GET /api/oauth/:providerId/callback/plugins?code=AUTH_CODE&state=STATE
Authorization: Bearer <jwt-token>

Processes the OAuth callback after user authorization.

Query Parameters:

ParameterTypeRequiredDescription
codestringYesAuthorization code from provider
statestringYesCSRF state — must match the ew_oauth_state cookie minted by connect/url (single-use)

Response:

{
"id": "github",
"name": "GitHub",
"enabled": true,
"connected": true,
"username": "octocat",
"email": "[email protected]",
"avatarUrl": "https://avatars.githubusercontent.com/..."
}

Get User Info

GET /api/oauth/:providerId/user
Authorization: Bearer <jwt-token>

Returns the authenticated user's profile from the OAuth provider.

Disconnect Provider

DELETE /api/oauth/:providerId
Authorization: Bearer <jwt-token>

Revokes the OAuth token and removes the stored credentials. Returns 204 No Content.

OAuthService Internals

State Management (CSRF Protection)

The service maintains an in-memory state store with expiration:

private stateStore = new Map<string, { userId: string; expires: Date }>();

State Lifecycle:

  1. Generation: generateState(userId) creates a random 16-byte hex string
  2. Storage: storeState(state, userId) saves with a 10-minute expiration
  3. Verification: verifyState(state, userId) checks existence, user match, and expiration
  4. Cleanup: cleanupExpiredStates() runs on every store operation
// State is valid only for the same user and within 10 minutes
verifyState(state: string, userId: string): boolean {
const stored = this.stateStore.get(state);
if (!stored || stored.userId !== userId || new Date() > stored.expires) {
return false;
}
this.stateStore.delete(state); // One-time use
return true;
}

Token Exchange and Storage

When handling the callback, the service:

  1. Exchanges the authorization code for tokens via OAuthFacadeService
  2. Fetches the user profile using the new access token
  3. Persists all token data via OAuthTokenRepository.upsert():
await this.oauthTokenRepository.upsert({
userId,
provider: providerId,
accessToken: token.accessToken,
refreshToken: token.refreshToken,
tokenType: token.tokenType,
scope: token.scope,
expiresAt, // Computed from token.expiresIn
email: user.email || null,
username: user.username,
metadata: {
oauthUserId: user.id,
name: user.name,
avatarUrl: user.avatarUrl
}
});

OAuth Configuration Resolution

Client credentials (clientId, clientSecret) are resolved from plugin settings:

private async getOAuthConfig(providerId: string, redirectUri?: string) {
const settings = await this.pluginSettingsService.getSettings(providerId, {
includeSecrets: true,
});

return {
clientId: settings?.clientId,
clientSecret: settings?.clientSecret,
redirectUri,
scopes: settings?.scopes,
};
}

This means OAuth credentials are configured as plugin settings, not hardcoded.

Connection Info Interface

interface OAuthConnectionInfo extends OAuthProviderInfo {
connected: boolean;
username?: string;
email?: string;
avatarUrl?: string;
}

Error Handling

ScenarioExceptionMessage
Missing authorization codeBadRequestException"Authorization code is required"
Invalid state parameterBadRequestException"Invalid state parameter"
Missing OAuth credentialsBadRequestException"OAuth credentials not configured for provider: {id}"
No valid tokenBadRequestException"No valid token for provider {id}"
Provider not foundGraceful return{ connected: false }

Configuration

OAuth providers require these plugin settings:

SettingDescription
clientIdOAuth application client ID
clientSecretOAuth application client secret
scopesRequested permission scopes (array)

These are configured per-provider through the plugin settings system, typically via environment variables prefixed with PLUGIN_.

Supported Providers

Plugin IDProviderScopes
githubGitHubrepo, user, configurable

Additional OAuth providers can be added by implementing the IOAuthPlugin interface from @ever-works/plugin.

Source Files

FilePurpose
apps/api/src/plugins-capabilities/oauth/oauth.module.tsModule definition
apps/api/src/plugins-capabilities/oauth/oauth.controller.tsREST API endpoints
apps/api/src/plugins-capabilities/oauth/oauth.service.tsOAuth flow orchestration