OAuth2/OIDC and LDAP/AD Integration

OAuth2/OIDC and LDAP/AD Integration

Configuration Guide for IT and Security Teams


Table of Contents

Overview

CodeGraph supports enterprise Single Sign-On (SSO) via OAuth2/OIDC and LDAP/Active Directory integration. Both methods are initialized at application startup and produce SIEM-auditable security events on every authentication attempt.

Key Capabilities

Feature Description
4 OAuth2 providers GitHub, GitLab, Google, Keycloak (OIDC)
LDAP/AD support Active Directory, OpenLDAP with group sync
Auto-provisioning Users created on first login from SSO/LDAP
RBAC mapping LDAP groups map to CodeGraph roles (ADMIN, REVIEWER, ANALYST, VIEWER)
CSRF protection State parameter with server-side validation
SIEM auditing AUTH_SUCCESS / AUTH_FAILURE events on every login
Graceful degradation App starts even if OAuth/LDAP is unavailable

Architecture

Authentication Flow

                     ┌──────────────┐
                     │   Browser /  │
                     │   API Client │
                     └──────┬───────┘
                            │
              ┌─────────────┼─────────────┐
              │             │             │
       ┌──────▼──────┐ ┌───▼────┐ ┌──────▼──────┐
       │  OAuth2     │ │  LDAP  │ │  Local JWT  │
       │  /OIDC      │ │  /AD   │ │  + API Key  │
       └──────┬──────┘ └───┬────┘ └──────┬──────┘
              │             │             │
              └─────────────┼─────────────┘
                            │
                    ┌───────▼───────┐
                    │  User Repo    │
                    │  (find or     │
                    │   create)     │
                    └───────┬───────┘
                            │
                    ┌───────▼───────┐       ┌──────────────┐
                    │  JWT Tokens   │       │  SIEM Event  │
                    │  (access +    │──────►│  Dispatch    │
                    │   refresh)    │       │  (auth.*)    │
                    └───────────────┘       └──────────────┘

Provider Registration

On startup, the application lifespan() initializes both subsystems:

  1. OAuthsetup_oauth_providers() reads OAUTH_* environment variables, registers enabled providers with OAuthManager
  2. LDAPsetup_ldap_authenticator() reads LDAP_* environment variables, creates the singleton LDAPAuthenticator

Both blocks are wrapped in try/except — the application starts normally even if no SSO is configured.


OAuth2/OIDC

Supported Providers

Provider Authorize URL Token URL Scopes
GitHub github.com/login/oauth/authorize github.com/login/oauth/access_token read:user, user:email
GitLab gitlab.com/oauth/authorize (or self-hosted) gitlab.com/oauth/token read_user, openid
Google accounts.google.com/o/oauth2/v2/auth oauth2.googleapis.com/token openid, profile, email
Keycloak {server}/realms/{realm}/protocol/openid-connect/auth .../token openid, profile, email

Configuration

Set environment variables for each provider you want to enable:

# GitHub
export OAUTH_GITHUB_CLIENT_ID="your_github_client_id"
export OAUTH_GITHUB_CLIENT_SECRET="your_github_client_secret"

# Google
export OAUTH_GOOGLE_CLIENT_ID="your_google_client_id"
export OAUTH_GOOGLE_CLIENT_SECRET="your_google_client_secret"

# Keycloak (OIDC)
export OAUTH_KEYCLOAK_SERVER_URL="https://keycloak.corp.example.com"
export OAUTH_KEYCLOAK_REALM="codegraph"
export OAUTH_KEYCLOAK_CLIENT_ID="codegraph-app"
export OAUTH_KEYCLOAK_CLIENT_SECRET="your_keycloak_client_secret"

Providers without CLIENT_ID and CLIENT_SECRET are skipped automatically.

Authorization Flow

1. User    GET /api/v1/auth/oauth/{provider}
2. Server generates CSRF state, stores in memory
3. 302 redirect to provider's authorize URL
4. User authenticates at provider
5. Provider redirects to GET /api/v1/auth/oauth/{provider}/callback?code=...&state=...
6. Server verifies state (CSRF), exchanges code for access token
7. Server fetches user info from provider API
8. Server finds or creates user in database
9. Server returns JWT tokens (access + refresh)

User Provisioning

On first OAuth login, a new user is created automatically:

Field Source
username Provider-specific (GitHub: login, Google: email prefix, Keycloak: preferred_username)
email Provider user info
display_name Provider name field
auth_provider oauth
role Default: ANALYST
external_id Provider’s unique user ID

Subsequent logins match by (provider, external_id) and reuse the existing account.


LDAP/Active Directory

LDAP Configuration

# Required
export LDAP_SERVER="ldaps://ldap.corp.example.com"
export LDAP_BASE_DN="dc=corp,dc=example,dc=com"

# Service account for searches
export LDAP_BIND_USER="cn=codegraph-svc,ou=service-accounts,dc=corp,dc=example,dc=com"
export LDAP_BIND_PASSWORD="service_account_password"

# Search bases
export LDAP_USER_SEARCH_BASE="ou=users,dc=corp,dc=example,dc=com"
export LDAP_GROUP_SEARCH_BASE="ou=groups,dc=corp,dc=example,dc=com"

Full configuration in config.yaml:

auth:
  ldap:
    enabled: true
    server: "ldaps://ldap.corp.example.com"
    port: 636
    use_ssl: true
    base_dn: "dc=corp,dc=example,dc=com"
    user_search_base: "ou=users,dc=corp,dc=example,dc=com"
    group_search_base: "ou=groups,dc=corp,dc=example,dc=com"
    bind_user: "cn=codegraph-svc,ou=service-accounts,dc=corp,dc=example,dc=com"
    bind_password: "${LDAP_BIND_PASSWORD}"
    user_object_class: "person"
    group_object_class: "group"
    username_attribute: "sAMAccountName"
    email_attribute: "mail"
    group_membership_attribute: "memberOf"
    role_mapping:
      "cn=codegraph-admins,ou=groups,dc=corp,dc=example,dc=com": "admin"
      "cn=codegraph-reviewers,ou=groups,dc=corp,dc=example,dc=com": "reviewer"
      "cn=codegraph-analysts,ou=groups,dc=corp,dc=example,dc=com": "analyst"

Group-to-Role Mapping

LDAP groups map to CodeGraph RBAC roles via role_mapping. The authenticator checks the user’s groups in priority order (admin > reviewer > analyst > viewer). First match wins.

LDAP Group CodeGraph Role Permissions
codegraph-admins ADMIN Full access (admin:all)
codegraph-reviewers REVIEWER Code review + analyst permissions
codegraph-analysts ANALYST Query execution + API keys
(no match) ANALYST Default fallback

LDAP User Provisioning

On first LDAP login, a user is created:

Field Source
username LDAP sAMAccountName (or configured attribute)
email LDAP mail attribute
display_name LDAP displayName attribute
ldap_dn Full distinguished name
auth_provider ldap
role From group-to-role mapping

Subsequent logins match by ldap_dn.


SIEM Integration

All authentication attempts (success and failure) generate SIEM events dispatched to configured handlers (Syslog, CEF, LEEF).

Authentication Events

Event Type Severity Trigger
AUTH_SUCCESS 6 (Informational) Successful login via any method
AUTH_FAILURE 4 (Warning) Failed login — wrong credentials, unknown user, provider error

Event Examples

Successful OAuth login:

{
  "event_type": "auth.success",
  "timestamp": "2026-02-26T10:30:00.000Z",
  "severity": 6,
  "message": "OAuth login: ghuser via github",
  "user_id": "550e8400-e29b-41d4-a716-446655440000",
  "ip_address": "10.0.0.50",
  "provider": "github",
  "request_id": "req-abc123"
}

Failed LDAP login:

{
  "event_type": "auth.failure",
  "timestamp": "2026-02-26T10:31:00.000Z",
  "severity": 4,
  "message": "LDAP auth failed: unknown_user",
  "ip_address": "10.0.0.51",
  "request_id": "req-def456"
}

Failed local login (wrong password):

{
  "event_type": "auth.failure",
  "timestamp": "2026-02-26T10:32:00.000Z",
  "severity": 4,
  "message": "Login failed: invalid password for analyst01",
  "user_id": "550e8400-e29b-41d4-a716-446655440001",
  "ip_address": "10.0.0.52",
  "request_id": "req-ghi789"
}

API Reference

OAuth Endpoints

Method Path Description
GET /api/v1/auth/oauth/providers List all OAuth providers and their status
GET /api/v1/auth/oauth/{provider} Start OAuth flow (redirects to provider)
GET /api/v1/auth/oauth/{provider}/callback Handle OAuth callback, return JWT tokens

List providers response:

[
  {"name": "github", "enabled": true, "authorize_url": "https://github.com/login/oauth/authorize?..."},
  {"name": "google", "enabled": false, "authorize_url": null},
  {"name": "gitlab", "enabled": false, "authorize_url": null},
  {"name": "keycloak", "enabled": false, "authorize_url": null}
]

Callback success response:

{
  "access_token": "eyJhbGciOiJIUzI1NiIs...",
  "refresh_token": "eyJhbGciOiJIUzI1NiIs...",
  "token_type": "bearer",
  "expires_in": 1800
}

LDAP Endpoints

Method Path Description
POST /api/v1/auth/ldap Authenticate with LDAP credentials
GET /api/v1/auth/ldap/status Check LDAP connection status

LDAP login request:

{
  "username": "jdoe",
  "password": "corporate_password"
}

LDAP status response (configured):

{
  "enabled": true,
  "available": true,
  "connected": true,
  "server": "ldaps://ldap.corp.example.com"
}

LDAP status response (not configured):

{
  "enabled": false,
  "available": false,
  "message": "LDAP not configured"
}

Startup and Shutdown

Startup Sequence

During application startup (lifespan()), OAuth and LDAP are initialized in order:

# 1. OAuth providers
auth_config = get_auth_config()
oauth_providers = setup_oauth_providers({
    "github": auth_config.oauth.github,
    "google": auth_config.oauth.google,
    "gitlab": auth_config.oauth.gitlab,
    "keycloak": auth_config.oauth.keycloak,
})
# Logs: "OAuth providers initialized: 2" or "No OAuth providers configured"

# 2. LDAP authenticator
ldap = setup_ldap_authenticator(auth_config.ldap)
# Logs: "LDAP authenticator initialized: ldaps://..." or "LDAP not configured"

Shutdown Cleanup

On shutdown, OAuth HTTP clients are closed gracefully:

await get_oauth_manager().close_all()

Graceful Degradation

Both subsystems are optional. If initialization fails (missing library, bad config), the application logs a warning and continues:

WARNING  OAuth initialization skipped: <error>
WARNING  LDAP initialization skipped: <error>

All other endpoints (local JWT login, API keys, query, scenarios) remain fully functional.


Troubleshooting

OAuth providers show enabled: false

Verify environment variables are set:

echo $OAUTH_GITHUB_CLIENT_ID
echo $OAUTH_GITHUB_CLIENT_SECRET

Both CLIENT_ID and CLIENT_SECRET must be non-empty for a provider to register.

OAuth callback returns invalid_state_parameter

The CSRF state token is stored in server memory. This error occurs if: - The state expired (server restarted between redirect and callback) - The state was already consumed (browser refresh) - Multi-instance deployment without shared state storage (use Redis in production)

LDAP returns 503 ldap_not_configured

The LDAP_SERVER environment variable is not set, or the ldap3 library is not installed:

pip install ldap3>=2.9.1

LDAP connection test fails

Check network connectivity and credentials:

# Test LDAP connectivity
ldapsearch -H ldaps://ldap.corp.example.com -x -D "cn=codegraph-svc,..." -w PASSWORD -b "dc=corp,dc=example,dc=com" "(sAMAccountName=testuser)"

No SIEM events appear

Verify SIEM dispatcher is enabled in config.yaml:

siem:
  enabled: true
  handlers:
    - type: syslog
      host: "siem.corp.example.com"
      port: 514

See SIEM Integration for full configuration.

Startup logs show initialization warnings

Check application logs for specific errors:

uvicorn src.api.main:app --log-level debug 2>&1 | grep -i "oauth\|ldap"