Skip to main content

OAuth Integration System

This document describes the OAuth integration system that enables secure connections between Openlane and third-party services like GitHub, Slack, and other OAuth providers.

Overview

The OAuth integration system provides a secure, organization-scoped mechanism for users to authorize Openlane to access their third-party accounts (GitHub, Slack, etc.). This enables features like:

  • Repository access and management via GitHub
  • Team communication through Slack
  • Automated workflows across integrated services
  • Secure API calls on behalf of users

Key Features

  • Organization-Scoped: All tokens are stored per organization, ensuring proper data isolation
  • Secure Token Storage: OAuth tokens encrypted and stored using the Hush secrets management system
  • Token Lifecycle Management: Automatic refresh handling and expiry detection
  • Invalidation Detection: Proactive detection of revoked authorizations
  • Provider Extensibility: Easy addition of new OAuth providers

Architecture

System Components

OAuth Flow Sequence

OAuth Flow

1. Initiate OAuth Flow

Endpoint: POST /v1/integrations/oauth/start

{
"provider": "github",
"scopes": ["repo", "user:email"],
"redirectUri": "https://app.openlane.io/integrations/callback"
}

Process:

  1. Validate authenticated user and organization context
  2. Generate cryptographically secure state parameter containing:
    • Organization ID
    • Provider name
    • Random entropy (16 bytes)
    • Base64 URL-encoded for safety
  3. Build OAuth authorization URL with appropriate scopes
  4. Return authorization URL for client redirect

Security Considerations:

  • State parameter prevents CSRF attacks
  • Organization context embedded in state ensures proper scoping
  • Random entropy prevents state guessing attacks
  • HTTPS required for all redirects

2. OAuth Provider Authorization

User is redirected to the OAuth provider (e.g., github.com/login/oauth/authorize) where they:

  1. Review requested permissions
  2. Grant or deny authorization
  3. Are redirected back to Openlane with authorization code

3. Handle OAuth Callback

Endpoint: POST /v1/integrations/oauth/callback

{
"provider": "github",
"code": "gho_16C7e42F292c6912E7710c838347Ae178B4a",
"state": "eyJvcmdJRCI6IjAxSE..."
}

Process:

  1. State Validation: Decode and verify state parameter
    • Extract organization ID and provider
    • Validate format and authenticity
  2. Code Exchange: Exchange authorization code for tokens
    • Use provider's token endpoint
    • Obtain access token, refresh token, and expiry
  3. Token Validation: Verify token with provider API
    • Make test API call to validate token
    • Extract user information from provider
  4. Secure Storage: Store tokens in Hush secrets system
    • Encrypt tokens at rest
    • Associate with integration entity
    • Store provider user metadata

Security Model

Authentication & Authorization

  • User Authentication: All OAuth flows require authenticated Openlane users
  • Organization Scoping: Tokens are always associated with user's current organization
  • Privacy Bypass: System operations use privacy bypass for secure token storage
  • Audit Trail: All token operations logged for security monitoring

Token Security

Storage Security

  • Encryption at Rest: All tokens encrypted using Hush secrets system
  • Access Control: Tokens only accessible by organization members
  • Immutable Updates: Token updates require delete/recreate pattern
  • Secret Rotation: Support for token refresh and rotation

Transmission Security

  • HTTPS Only: All OAuth communications over TLS
  • State Parameters: CSRF protection through cryptographic state
  • Secure Redirects: Validated redirect URIs prevent open redirect attacks
  • Token Masking: Tokens never exposed in logs or error messages

Provider-Specific Security

GitHub
// Default scopes for API access
Scopes: []string{"read:user", "user:email", "repo"}

// Token validation
GET https://api.github.com/user
Authorization: token {access_token}
Slack (when configured)
// Default scopes for team communication
Scopes: []string{"channels:read", "chat:write", "users:read"}

// Token validation
POST https://slack.com/api/auth.test
Authorization: Bearer {access_token}

Token Management

Token Storage Schema

Tokens are stored in the Hush secrets system with the following structure:

Integration Entity:
├── id: "01HX..."
├── owner_id: "{organization_id}"
├── name: "GitHub Integration (username)"
├── kind: "github"
└── description: "OAuth integration with GitHub for {username}"

Associated Hush Secrets:
├── {provider}_access_token: "encrypted_access_token"
├── {provider}_refresh_token: "encrypted_refresh_token"
├── {provider}_expires_at: "2024-12-31T23:59:59Z"
├── {provider}_provider_user_id: "12345"
├── {provider}_provider_username: "johndoe"
└── {provider}_provider_email: "john@example.com"

Token Lifecycle

Creation

  1. OAuth callback successfully processed
  2. Integration entity created in database
  3. Tokens encrypted and stored in Hush
  4. Provider user metadata cached

Refresh

// Automatic refresh when tokens expire
func (h *Handler) RefreshIntegrationToken(ctx context.Context, orgID, provider string) error {
// 1. Retrieve current tokens
// 2. Use refresh token to get new access token
// 3. Validate new token with provider
// 4. Update stored tokens atomically
// 5. Return fresh token data
}

Deletion

  1. Remove all associated Hush secrets
  2. Delete integration entity
  3. Audit log deletion event

Token Invalidation Detection

Proactive Detection Mechanisms

1. API Call Monitoring

Every API call made with stored tokens includes response monitoring:

// Example: GitHub API call with invalidation detection
func (s *GitHubService) makeAPICall(token string, endpoint string) (*Response, error) {
resp, err := http.Get(endpoint, headers{"Authorization": "token " + token})

switch resp.StatusCode {
case 401:
// Token invalid - mark integration as invalid
s.markIntegrationInvalid("github", "Token unauthorized")
return nil, ErrTokenInvalid
case 403:
if isRateLimited(resp) {
// Rate limited - temporary issue
return nil, ErrRateLimit
}
// Forbidden - possibly revoked permissions
s.markIntegrationSuspicious("github", "API access forbidden")
}

return resp, err
}

2. Periodic Token Validation

Background service that validates tokens periodically:

// Runs every 4 hours to validate active integrations
func (s *IntegrationService) validateActiveTokens() {
integrations := s.getActiveIntegrations()

for _, integration := range integrations {
if err := s.validateIntegrationToken(integration); err != nil {
s.handleTokenInvalidation(integration, err)
}
}
}

User Notification System

UI Indicators

Integration status displayed in real-time:

interface IntegrationStatus {
provider: string;
connected: boolean;
status: 'connected' | 'expired' | 'invalid' | 'warning';
tokenValid: boolean;
tokenExpired: boolean;
lastValidated: Date;
message: string;
actions: ('reconnect' | 'refresh' | 'configure')[];
}

Status Messages

{
"github": {
"status": "invalid",
"message": "GitHub integration has been revoked. Please reconnect to continue using GitHub features.",
"actions": ["reconnect"],
"lastError": "Token validation failed with 401 Unauthorized",
"detectedAt": "2024-01-15T10:30:00Z"
}
}

API Endpoints

OAuth Flow Endpoints

Start OAuth Flow

POST /v1/integrations/oauth/start
Content-Type: application/json
Authorization: Bearer {user_session_token}

{
"provider": "github",
"scopes": ["repo", "gist"],
"redirectUri": "https://app.openlane.io/callback"
}

Response:

{
"success": true,
"authUrl": "https://github.com/login/oauth/authorize?client_id=...",
"state": "eyJvcmdJRCI6..."
}

Handle OAuth Callback

POST /v1/integrations/oauth/callback
Content-Type: application/json

{
"provider": "github",
"code": "gho_16C7e42F292c6912E7710c838347Ae178B4a",
"state": "eyJvcmdJRCI6..."
}

Response:

{
"success": true,
"message": "Successfully connected GitHub integration",
"integration": {
"id": "01HX...",
"name": "GitHub Integration (johndoe)",
"provider": "github"
}
}

Token Management Endpoints

Get Integration Token

GET /v1/integrations/{provider}/token
Authorization: Bearer {user_session_token}

Check Integration Status

GET /v1/integrations/{provider}/status
Authorization: Bearer {user_session_token}

Refresh Integration Token

POST /v1/integrations/{provider}/refresh
Authorization: Bearer {user_session_token}

List All Integrations

GET /v1/integrations
Authorization: Bearer {user_session_token}

Delete Integration

DELETE /v1/integrations/{integration_id}
Authorization: Bearer {user_session_token}

External System Integration

GitHub Integration

Configuration

type IntegrationProviderConfig struct {
ClientID string `json:"clientId" koanf:"clientId"`
ClientSecret string `json:"clientSecret" koanf:"clientSecret"`
ClientEndpoint string `json:"clientEndpoint" koanf:"clientEndpoint"`
Scopes []string `json:"scopes" koanf:"scopes"`
}

Default Scopes

  • read:user - Read user profile information
  • user:email - Access user email addresses
  • repo - Full repository access (read/write)

Adding New Providers

To add a new OAuth provider:

  1. Add Provider Configuration:
// Add to IntegrationOauthProviderConfig
type IntegrationOauthProviderConfig struct {
// ... existing providers
NewProvider IntegrationProviderConfig `json:"newprovider" koanf:"newprovider"`
}
  1. Implement Provider Interface:
func (h *Handler) getNewProviderConfig() *oauth2.Config {
return &oauth2.Config{
ClientID: h.IntegrationOauthProvider.NewProvider.ClientID,
ClientSecret: h.IntegrationOauthProvider.NewProvider.ClientSecret,
RedirectURL: fmt.Sprintf("%s/v1/integrations/oauth/callback",
h.IntegrationOauthProvider.NewProvider.ClientEndpoint),
Endpoint: newprovider.Endpoint,
Scopes: h.IntegrationOauthProvider.NewProvider.Scopes,
}
}
  1. Update Provider Registry:
func (h *Handler) getIntegrationProviders() map[string]IntegrationProvider {
providers := map[string]IntegrationProvider{
// existing providers...
"newprovider": {
Name: "newprovider",
Config: h.getNewProviderConfig(),
Validate: func(ctx context.Context, token *oauth2.Token) (*IntegrationUserInfo, error) {
return h.validateNewProviderToken(ctx, token)
},
},
}
return providers
}

Configuration

OAuth App Setup

Before using the OAuth integration system, you must create OAuth applications with each provider and configure the server with the credentials.

GitHub App Configuration

Important: The OAuth integration system uses separate OAuth configuration from social login.

  1. Create or Update GitHub OAuth App:

    • Go to GitHub Settings → Developer settings → OAuth Apps
    • You can either:
      • Option A: Create a new OAuth app specifically for integrations
      • Option B: Update your existing OAuth app (used for social login) to include both callback URLs
    • Authorization callback URLs should include:
      http://localhost:17608/v1/integrations/oauth/callback  # For integrations
      # And if sharing with social login:
      http://localhost:17608/v1/github/callback # For social login
  2. Integration-Specific Configuration:

The system uses a separate configuration section for integrations:

# config.yaml - Integration OAuth configuration (separate from social login)
integrationOauthProvider:
github:
clientId: "your_github_client_id" # Can be same as social login
clientSecret: "your_github_client_secret" # Can be same as social login
clientEndpoint: "http://localhost:17608" # Base URL for callbacks
scopes: ["read:user", "user:email", "repo"] # Extended scopes for API access
slack:
clientId: "your_slack_client_id" # Slack app configuration
clientSecret: "your_slack_client_secret"
clientEndpoint: "http://localhost:17608"
scopes: ["channels:read", "chat:write", "users:read"]

Key Differences from Social Login:

  • Separate configuration section: integrationOauthProvider vs auth.providers
  • Different callback URL: /v1/integrations/oauth/callback vs /v1/github/callback
  • Extended scopes: Includes repo for API access vs basic profile scopes
  • Different token storage: Organization-scoped in Hush vs session-based
  • Different use case: Long-term API access vs short-term user authentication

Environment Variables (Alternative)

# Integration OAuth Provider Configuration (separate from social login)
INTEGRATION_OAUTH_GITHUB_CLIENT_ID=your_github_client_id
INTEGRATION_OAUTH_GITHUB_CLIENT_SECRET=your_github_client_secret
INTEGRATION_OAUTH_GITHUB_CLIENT_ENDPOINT=http://localhost:17608

# Slack Integration
INTEGRATION_OAUTH_SLACK_CLIENT_ID=your_slack_client_id
INTEGRATION_OAUTH_SLACK_CLIENT_SECRET=your_slack_client_secret
INTEGRATION_OAUTH_SLACK_CLIENT_ENDPOINT=http://localhost:17608

# Security Settings
OAUTH_STATE_EXPIRY=300 # State parameter expiry in seconds
OAUTH_TOKEN_VALIDATION_INTERVAL=14400 # 4 hours

Security Note: Client Credentials

OAuth client secrets are NEVER exposed to the frontend. The HTML testing interface works by:

  1. Frontend calls server: POST /v1/integrations/oauth/start
  2. Server uses stored credentials: Creates OAuth URL with server-side client ID
  3. Server returns OAuth URL: Frontend redirects user to provider
  4. Provider redirects back: With authorization code to server callback
  5. Server exchanges code: Using server-side client secret

This ensures that:

  • Client secrets remain secure on the server
  • Credentials can be rotated without frontend changes
  • No sensitive data transmitted to browsers
  • Proper separation of concerns

Testing

Test Environment Setup

Prerequisites

Before testing OAuth integrations, you need:

  1. GitHub OAuth App (for GitHub integration testing):

    • Create at: https://github.com/settings/applications/new
    • Application name: "Openlane Local Development - Integrations"
    • Homepage URL: http://localhost:17608
    • Authorization callback URL: http://localhost:17608/v1/integrations/oauth/callback
    • Copy the Client ID and Client Secret
  2. Server Configuration:

# Set environment variables or update config.yaml
export INTEGRATION_OAUTH_GITHUB_CLIENT_ID="your_github_client_id"
export INTEGRATION_OAUTH_GITHUB_CLIENT_SECRET="your_github_client_secret"
export INTEGRATION_OAUTH_GITHUB_CLIENT_ENDPOINT="http://localhost:17608"

Or in your config.yaml:

integrationOauthProvider:
github:
clientId: "your_github_client_id"
clientSecret: "your_github_client_secret"
clientEndpoint: "http://localhost:17608"

Start Development Environment

# Start full development stack with OAuth configuration
task run-dev

# This starts:
# - Core API server on http://localhost:17608
# - Database with seeded data
# - OAuth handlers configured with your GitHub app
# - All necessary services

OAuth Testing Interface

Navigate to: http://localhost:17608/pkg/testutils/integrations/index.html

Important: The testing interface does NOT contain OAuth credentials. It works by:

  1. Making API calls to your local server (localhost:17608)
  2. Server uses its configured OAuth credentials
  3. Server returns OAuth URLs for redirection
  4. No sensitive credentials exposed to browser

Features:

  • Authentication Status: Shows current user login state
  • Integration Management: Start OAuth flows, check status, get tokens
  • API Testing: Test GitHub/Slack API calls with stored tokens
  • Activity Logging: Real-time log of all actions and responses

Testing Workflow

  1. Authenticate: Login to your local Openlane instance
  2. Configure OAuth App: Ensure GitHub OAuth app is configured on server
  3. Test Integration: Click "Connect GitHub" to start OAuth flow
  4. Verify Storage: Check that tokens are stored and accessible
  5. Test API Calls: Use stored tokens for GitHub API calls

Troubleshooting

Common Issues

"Invalid OAuth State Parameter"

Cause: State parameter validation failed Solutions:

  • Check that state hasn't expired (5 minute default)
  • Verify organization context hasn't changed
  • Ensure HTTPS is used for all redirects

"Provider Not Configured"

Cause: OAuth provider configuration missing Solutions:

  • Verify environment variables are set
  • Check OAuth app configuration in provider settings
  • Ensure redirect URIs match exactly

"Token Validation Failed"

Cause: Provider rejected the token Solutions:

  • Check if user revoked access in provider settings
  • Verify OAuth app hasn't been deleted/suspended
  • Attempt token refresh if refresh token available

"Integration Not Found"

Cause: Integration deleted or organization mismatch Solutions:

  • Verify user is in correct organization
  • Check if integration was manually deleted
  • Re-run OAuth flow to recreate integration

For additional support or questions about the OAuth integration system, please refer to the development team or create an issue in the repository.