Skip to main content
Loom integrates with GitHub at multiple levels: OAuth authentication for user login, GitHub App for repository access, and API clients for code search and introspection.

GitHub OAuth Authentication

Use GitHub OAuth to authenticate users with their GitHub accounts.

Setup

1

Create OAuth App

Register an OAuth application in your GitHub Developer Settings:
  • Application name: Loom (or your deployment name)
  • Homepage URL: https://your-domain.com
  • Authorization callback URL: https://your-domain.com/auth/github/callback
2

Configure Environment

LOOM_SERVER_GITHUB_CLIENT_ID=Iv1.abc123def456
LOOM_SERVER_GITHUB_CLIENT_SECRET=abc123...
LOOM_SERVER_GITHUB_REDIRECT_URI=https://your-domain.com/auth/github/callback
3

Initialize Client

use loom_server_auth_github::{GitHubOAuthClient, GitHubOAuthConfig};

let config = GitHubOAuthConfig::from_env()?;
let client = GitHubOAuthClient::new(config);

OAuth Flow

Generate the authorization URL and redirect the user:
// loom-server-auth-github/src/lib.rs:355
let state = generate_random_state();  // CSRF protection
let auth_url = client.authorization_url(&state);

// Redirect user to auth_url
// Store state server-side for validation
URL format:
https://github.com/login/oauth/authorize
  ?client_id=Iv1.abc123def456
  &redirect_uri=https://your-domain.com/auth/github/callback
  &scope=user:email+read:user
  &state=random-csrf-token

Scopes

Default scopes:
  • user:email - Read all email addresses (including private)
  • read:user - Read user profile information
Optional scopes:
let config = GitHubOAuthConfig {
    client_id: "...".to_string(),
    client_secret: SecretString::new("..."),
    redirect_uri: "...".to_string(),
    scopes: vec![
        "user:email".to_string(),
        "read:user".to_string(),
        "repo".to_string(),  // Access private repositories
        "read:org".to_string(),  // Read organization membership
    ],
};
Always use verified emails. Only trust emails where verified: true. Unverified emails can be set by anyone and should not be used for authentication.

Response Types

// User profile
pub struct GitHubUser {
    pub id: i64,              // Stable identifier (use this)
    pub login: String,        // Username (can change)
    pub name: Option<String>, // Display name
    pub email: Option<String>,  // Public email (often null)
    pub avatar_url: Option<String>,
}

// Email address
pub struct GitHubEmail {
    pub email: String,
    pub primary: bool,    // User's primary email
    pub verified: bool,   // Verified by GitHub
}

// Token response
pub struct GitHubTokenResponse {
    pub access_token: SecretString,  // Never logged
    pub token_type: String,  // "bearer"
    pub scope: String,       // Granted scopes
}

GitHub App Integration

GitHub Apps provide fine-grained access to repositories with enhanced security and better rate limits than OAuth apps.

Setup

1

Create GitHub App

Create a GitHub App in your organization or personal account:
  1. Go to Settings → Developer settings → GitHub Apps → New GitHub App
  2. Configure:
    • Name: Loom
    • Homepage URL: https://your-domain.com
    • Webhook URL: https://your-domain.com/webhooks/github
    • Webhook secret: Generate a random secret
  3. Set permissions:
    • Repository contents: Read
    • Repository metadata: Read
    • Issues: Read & Write (if needed)
  4. Download the private key (.pem file)
2

Configure Application

LOOM_SERVER_GITHUB_APP_ID=123456
LOOM_SERVER_GITHUB_APP_PRIVATE_KEY_PATH=/path/to/private-key.pem
LOOM_SERVER_GITHUB_APP_WEBHOOK_SECRET=your-webhook-secret
Or provide the private key directly:
LOOM_SERVER_GITHUB_APP_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\n..."

Authentication

GitHub Apps use JWT-based authentication:
use loom_server_github_app::{GithubAppClient, GithubAppConfig};

let config = GithubAppConfig::from_env()?;
let client = GithubAppClient::new(config)?;

// Get installation access token
let installation_id = 12345678;  // From webhook or API
let token = client.get_installation_token(installation_id).await?;

// Use token for API requests (automatically refreshed)
JWT generation:
// loom-server-github-app/src/jwt.rs
use jsonwebtoken::{encode, Header, EncodingKey, Algorithm};

#[derive(Serialize)]
struct Claims {
    iat: i64,  // Issued at
    exp: i64,  // Expires at (max 10 minutes)
    iss: String,  // App ID
}

pub fn create_jwt(app_id: &str, private_key: &str) -> Result<String, JwtError> {
    let now = Utc::now().timestamp();
    let claims = Claims {
        iat: now - 60,  // 60 seconds in the past (clock skew)
        exp: now + 600,  // 10 minutes in the future
        iss: app_id.to_string(),
    };
    
    let header = Header::new(Algorithm::RS256);
    let key = EncodingKey::from_rsa_pem(private_key.as_bytes())?;
    
    Ok(encode(&header, &claims, &key)?)
}
Search code across repositories the app has access to:
use loom_server_github_app::{CodeSearchRequest, CodeSearchResponse};

let request = CodeSearchRequest {
    query: "language:rust trait LlmClient".to_string(),
    per_page: Some(30),
    page: Some(1),
};

let response: CodeSearchResponse = client.search_code(&request, installation_id).await?;

for item in response.items {
    println!("File: {} in {}/{}",
        item.name,
        item.repository.full_name,
        item.path
    );
    println!("  URL: {}", item.html_url);
}
Search query syntax:
language:rust trait LlmClient
repo:owner/repo filename:main.rs
org:myorg path:src/ extension:rs
user:username "async fn"
See GitHub Code Search Documentation for complete query syntax.

Repository Introspection

Fetch repository metadata and file contents:
use loom_server_github_app::{RepoInfoRequest, FileContentsRequest};

// Get repository info
let repo = client.get_repository_info(
    &RepoInfoRequest {
        owner: "loom".to_string(),
        repo: "loom".to_string(),
    },
    installation_id
).await?;

println!("Repo: {}", repo.full_name);
println!("Description: {:?}", repo.description);
println!("Stars: {}", repo.stargazers_count);
println!("Language: {:?}", repo.language);
println!("Default branch: {}", repo.default_branch);

// Get file contents
let file = client.get_file_contents(
    &FileContentsRequest {
        owner: "loom".to_string(),
        repo: "loom".to_string(),
        path: "README.md".to_string(),
        reference: Some("main".to_string()),  // branch, tag, or commit SHA
    },
    installation_id
).await?;

let content = file.decode_content()?;  // Base64 decode
println!("README.md:\n{}", content);

Webhook Handling

Verify and process GitHub webhooks:
use loom_server_github_app::verify_webhook_signature;

// In your webhook handler
async fn handle_webhook(
    payload: Vec<u8>,
    signature: &str,  // X-Hub-Signature-256 header
    secret: &str,
) -> Result<(), Error> {
    // Verify signature
    if !verify_webhook_signature(&payload, signature, secret) {
        return Err(Error::InvalidSignature);
    }
    
    // Parse payload
    let event: serde_json::Value = serde_json::from_slice(&payload)?;
    
    // Handle event types
    match event["action"].as_str() {
        Some("created") => { /* Installation created */ }
        Some("deleted") => { /* Installation deleted */ }
        _ => {}
    }
    
    Ok(())
}
Signature verification:
// loom-server-github-app/src/webhook.rs
use hmac::{Hmac, Mac};
use sha2::Sha256;

pub fn verify_webhook_signature(
    payload: &[u8],
    signature: &str,
    secret: &str,
) -> bool {
    // GitHub sends "sha256=<hex>"
    let expected = match signature.strip_prefix("sha256=") {
        Some(sig) => sig,
        None => return false,
    };
    
    // Compute HMAC-SHA256
    let mut mac = Hmac::<Sha256>::new_from_slice(secret.as_bytes())
        .expect("HMAC can take key of any size");
    mac.update(payload);
    
    // Compare (constant-time)
    let computed = hex::encode(mac.finalize().into_bytes());
    computed == expected
}

Installation Management

Track which repositories the app is installed on:
// List installations
let installations = client.list_installations().await?;

for install in installations {
    println!("Installation ID: {}", install.id);
    println!("  Account: {}", install.account.login);
    println!("  Type: {:?}", install.account.account_type);
    println!("  Repos: {:?}", install.repository_selection);
}

// Get installation status
let status = client.get_installation_status(installation_id).await?;
println!("Status: {}", status.status);  // "active" or "suspended"

Rate Limiting

GitHub enforces rate limits for API requests: OAuth (per user):
  • 5,000 requests/hour for authenticated requests
  • 60 requests/hour for unauthenticated requests
GitHub App (per installation):
  • 15,000 requests/hour (3x higher than OAuth)
  • Shared across all users of the installation
Check rate limit:
let response = client.get(...).await?;

if let Some(remaining) = response.headers().get("x-ratelimit-remaining") {
    println!("Remaining: {}", remaining.to_str()?);
}

if let Some(reset) = response.headers().get("x-ratelimit-reset") {
    let timestamp = reset.to_str()?.parse::<i64>()?;
    println!("Resets at: {}", timestamp);
}
GitHub returns 403 Forbidden when rate limited, not 429. Check the X-RateLimit-Remaining header proactively to avoid hitting limits.

Best Practices

Use GitHub Apps

Prefer GitHub Apps over OAuth Apps for repository access. They provide better rate limits, fine-grained permissions, and don’t depend on individual user tokens.

Cache Tokens

Installation tokens are valid for 1 hour. Cache them to avoid unnecessary JWT creation and token exchange calls.

Verify Webhooks

Always verify webhook signatures using HMAC-SHA256. Never trust unverified webhook payloads.

Use Stable IDs

Use user.id (numeric) as the stable identifier, not user.login (can change). Store both for display purposes.

Error Handling

use loom_server_github_app::GithubAppError;

match client.search_code(&request, installation_id).await {
    Ok(results) => { /* ... */ }
    Err(GithubAppError::RateLimitExceeded { reset_at }) => {
        let wait = reset_at - Utc::now();
        println!("Rate limited, retry in {} seconds", wait.num_seconds());
    }
    Err(GithubAppError::NotFound) => {
        println!("Repository or installation not found");
    }
    Err(GithubAppError::Unauthorized) => {
        println!("Invalid credentials or insufficient permissions");
    }
    Err(e) => {
        println!("Error: {}", e);
    }
}

API Reference

See the source for complete type definitions:
  • OAuth Client: crates/loom-server-auth-github/src/lib.rs
  • GitHub App Client: crates/loom-server-github-app/src/client.rs
  • Types: crates/loom-server-github-app/src/types.rs
  • Webhook Verification: crates/loom-server-github-app/src/webhook.rs
GitHub API documentation: https://docs.github.com/en/rest

Build docs developers (and LLMs) love