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:
- Validate authenticated user and organization context
- Generate cryptographically secure state parameter containing:
- Organization ID
- Provider name
- Random entropy (16 bytes)
- Base64 URL-encoded for safety
- Build OAuth authorization URL with appropriate scopes
- 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:
- Review requested permissions
- Grant or deny authorization
- 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:
- State Validation: Decode and verify state parameter
- Extract organization ID and provider
- Validate format and authenticity
- Code Exchange: Exchange authorization code for tokens
- Use provider's token endpoint
- Obtain access token, refresh token, and expiry
- Token Validation: Verify token with provider API
- Make test API call to validate token
- Extract user information from provider
- 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
- OAuth callback successfully processed
- Integration entity created in database
- Tokens encrypted and stored in Hush
- 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
- Remove all associated Hush secrets
- Delete integration entity
- 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 informationuser:email- Access user email addressesrepo- Full repository access (read/write)
Adding New Providers
To add a new OAuth provider:
- Add Provider Configuration:
// Add to IntegrationOauthProviderConfig
type IntegrationOauthProviderConfig struct {
// ... existing providers
NewProvider IntegrationProviderConfig `json:"newprovider" koanf:"newprovider"`
}
- 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,
}
}
- 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.
-
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
-
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:
integrationOauthProvidervsauth.providers - Different callback URL:
/v1/integrations/oauth/callbackvs/v1/github/callback - Extended scopes: Includes
repofor 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:
- Frontend calls server:
POST /v1/integrations/oauth/start - Server uses stored credentials: Creates OAuth URL with server-side client ID
- Server returns OAuth URL: Frontend redirects user to provider
- Provider redirects back: With authorization code to server callback
- 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:
-
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
-
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:
- Making API calls to your local server (
localhost:17608) - Server uses its configured OAuth credentials
- Server returns OAuth URLs for redirection
- 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
- Authenticate: Login to your local Openlane instance
- Configure OAuth App: Ensure GitHub OAuth app is configured on server
- Test Integration: Click "Connect GitHub" to start OAuth flow
- Verify Storage: Check that tokens are stored and accessible
- 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.