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": "octocat@example.com",
"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": "octocat@example.com",
"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