Skip to content

feat: add GitHub integration endpoints for token exchange and installation checks#162

Open
catrielmuller wants to merge 3 commits intomainfrom
catrielmuller/github-app-token
Open

feat: add GitHub integration endpoints for token exchange and installation checks#162
catrielmuller wants to merge 3 commits intomainfrom
catrielmuller/github-app-token

Conversation

@catrielmuller
Copy link

Summary

Implements three new GitHub integration endpoints to support the Kilo GitHub Action and CLI.

New Endpoints

  1. POST /api/integrations/github/exchange-token - Exchanges GitHub Actions OIDC tokens for GitHub App installation tokens (production flow)
  2. POST /api/integrations/github/exchange-token-with-pat - Exchanges GitHub PATs for installation tokens (dev/testing flow)
  3. GET /api/integrations/github/check-installation - Checks if KiloConnect is installed (CLI polling)

Implementation Details

OIDC Token Exchange (Production)

  • Verifies GitHub Actions OIDC JWT against GitHub's public JWKS endpoint
  • Validates issuer (https://token.actions.githubusercontent.com) and audience (kilo-github-action)
  • Extracts repository owner from JWT claims
  • Looks up GitHub App installation and generates installation token
  • No user authentication required (machine-to-machine)

PAT Token Exchange (Development)

  • Validates PAT by calling GitHub API to verify repository access
  • Looks up GitHub App installation for the owner
  • Generates installation token
  • Available in all environments

Installation Check (CLI)

  • Public endpoint for CLI to poll installation status
  • Returns { installation: true/false } based on database lookup
  • Used by kilo github install command

Files Changed

  • src/app/api/integrations/github/exchange-token/route.ts (new) - 48 lines
  • src/app/api/integrations/github/exchange-token-with-pat/route.ts (new) - 64 lines
  • src/app/api/integrations/github/check-installation/route.ts (new) - 24 lines
  • src/lib/integrations/platforms/github/oidc.ts (new) - 47 lines
  • src/lib/integrations/db/platform-integrations.ts (modified) - added findGitHubIntegrationByAccountLogin()

GitHub Action Migration Required

The GitHub Action in /home/catrielmuller/Dev/Kilo-Org/kilo/github/index.ts needs URL updates:

Old URLs:

  • https://api.kilo.ai/exchange_github_app_token
  • https://api.kilo.ai/exchange_github_app_token_with_pat

New URLs:

  • https://api.kilo.ai/api/integrations/github/exchange-token
  • https://api.kilo.ai/api/integrations/github/exchange-token-with-pat

Testing

OIDC Endpoint

Requires GitHub Actions workflow with id-token: write permission.

PAT Endpoint

curl -X POST https://api.kilo.ai/api/integrations/github/exchange-token-with-pat \
  -H "Authorization: Bearer $GITHUB_PAT" \
  -H "Content-Type: application/json" \
  -d '{"owner": "kilo-org", "repo": "cloud"}'

Installation Check

curl "https://api.kilo.ai/api/integrations/github/check-installation?owner=kilo-org"

Dependencies

  • jose - JWT verification and JWKS handling (already in lockfile)
  • @octokit/rest - GitHub API client (existing)
  • Existing GitHub App infrastructure

…ation checks

- Add POST /api/integrations/github/exchange-token for OIDC token exchange
- Add POST /api/integrations/github/exchange-token-with-pat for PAT token exchange
- Add GET /api/integrations/github/check-installation for CLI polling
- Implement OIDC JWT verification using GitHub's JWKS endpoint
- Add findGitHubIntegrationByAccountLogin() helper function
- Support both production (OIDC) and development (PAT) authentication flows

export async function GET(request: NextRequest) {
try {
const owner = request.nextUrl.searchParams.get('owner');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Unauthenticated endpoint can be used to enumerate GitHub App installations

This GET route accepts an arbitrary owner and returns whether an active integration exists. If this is reachable from the browser, it can leak which orgs/users have the GitHub App installed (useful for targeted abuse). Consider requiring an authenticated Kilo session and/or restricting to internal callers, plus rate limiting.

return NextResponse.json({ error: 'Missing owner or repo in request body' }, { status: 400 });
}

const octokit = new Octokit({ auth: pat });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CRITICAL: PAT-based token exchange needs stronger guardrails

This endpoint lets any caller who can present any GitHub PAT (with repo access) mint a GitHub App installation token for the owner. That is a large privilege escalation surface if the route is accessible outside a tightly controlled environment. Strongly consider enforcing Kilo-side auth + authorization, restricting to server-to-server usage, and/or limiting which repos/owners/workflows are allowed to exchange.

try {
await octokit.rest.repos.get({ owner, repo });
} catch (error) {
console.error('PAT validation failed:', error);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Logging raw Octokit errors may leak sensitive request details

console.error(..., error) can include rich error objects (request metadata, headers) depending on the thrown type. Since this code handles PATs, prefer logging a sanitized subset (e.g., status + message) and rely on Sentry scrubbing for the rest.


const payload = await verifyGitHubOIDCToken(oidcToken, 'kilo-github-action');

const repositoryOwner = payload.repository_owner;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: OIDC payload is used without runtime validation/normalization

payload.repository_owner is assumed to be a valid login string and is used directly for the DB lookup. If the claim is missing/unexpected or differs in casing, this will either throw later or incorrectly return 404. Consider validating required claims (e.g., repository_owner, repository) and normalizing login casing before calling the DB query.

audience: expectedAudience,
});

return payload as GitHubOIDCTokenPayload;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Unsafe cast of JWT payload can hide missing/incorrect claims

jwtVerify() returns an untyped payload. Casting with as GitHubOIDCTokenPayload skips runtime validation, so downstream code can assume properties exist when they do not. Consider validating with a schema (e.g., Zod) and returning a typed, validated object.

.where(
and(
eq(platform_integrations.platform, PLATFORM.GITHUB),
eq(platform_integrations.platform_account_login, accountLogin),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: GitHub logins are case-insensitive; exact-match lookup may fail

platform_account_login comparisons are case-sensitive in many DB collations. If logins are stored with different casing than the incoming accountLogin (e.g., from OIDC repository_owner), this can produce false negatives. Consider normalizing to lowercase at write time and query time (or using a case-insensitive comparison).

@kiloconnect
Copy link
Contributor

kiloconnect bot commented Feb 12, 2026

Code Review Summary

Status: 11 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 1
WARNING 10
SUGGESTION 0
Issue Details (click to expand)

CRITICAL

File Line Issue
src/app/api/integrations/github/exchange-token-with-pat/route.ts 28 PAT-based token exchange needs stronger guardrails

WARNING

File Line Issue
src/app/api/integrations/github/check-installation/route.ts 7 Unauthenticated endpoint can be used to enumerate installations
src/app/api/integrations/github/check-installation/route.ts 20 Returning raw exception messages can leak internals
src/app/api/integrations/github/exchange-token-with-pat/route.ts 35 Logging raw Octokit errors may leak sensitive request details
src/app/api/integrations/github/exchange-token-with-pat/route.ts 64 Scope installation token using canonical repository name (repoData.full_name)
src/app/api/integrations/github/exchange-token-with-pat/route.ts 73 Returning raw exception messages can leak internals
src/app/api/integrations/github/exchange-token/route.ts 19 OIDC payload is used without runtime validation
src/app/api/integrations/github/exchange-token/route.ts 20 repository_owner_id type mismatch can cause false mismatches
src/app/api/integrations/github/exchange-token/route.ts 85 Returning raw exception messages can leak internals
src/lib/integrations/platforms/github/oidc.ts 42 Unsafe cast of JWT payload can hide missing/incorrect fields
Other Observations (not in diff)

Issues found in unchanged code that cannot receive inline comments:

File Line Issue
src/lib/integrations/db/platform-integrations.ts N/A GitHub logins are case-insensitive; exact-match lookups can fail
Files Reviewed (7 files)

Fix these issues in Kilo Cloud

- Fix case-insensitive account lookup in findGitHubIntegrationByAccountLogin
- Add repository_id and repository_visibility to OIDC token payload
- Add repository-scoped token generation to generateGitHubInstallationToken
- Validate OIDC repository_owner_id matches stored platform_account_id
- Validate OIDC repository is in installation scope (for selected repos)
- Scope OIDC tokens to requesting repository only
- Verify PAT has write permissions (not just read) before token exchange
- Scope PAT tokens to validated repository only (fixes privilege escalation)
- Add audit logging for all token exchanges

Fixes high-severity privilege escalation where read-only PAT with access to
one repo could obtain write access to all repos in the installation.
@catrielmuller
Copy link
Author

Security Improvements Added

After a comprehensive security review, I've added critical security hardening to all three GitHub integration endpoints:

🔒 Security Fixes

High Severity - Privilege Escalation Fixed

PAT Endpoint (): Fixed a privilege escalation vulnerability where a PAT with read access to a single repository could be exchanged for an installation token with write access to all repositories in the installation.

Mitigations:

  • ✅ Verify PAT has write permissions (not just read) before issuing token
  • ✅ Scope installation token to only the validated repository
  • ✅ Add audit logging for all PAT token exchanges

Medium Severity - OIDC Validation Hardened

OIDC Endpoint (): Added multiple validation layers to prevent edge cases and potential abuse.

Improvements:

  • ✅ Validate repository_owner_id matches stored platform_account_id
  • ✅ Validate repository is in installation scope (for selected repos)
  • ✅ Scope installation token to requesting repository only
  • ✅ Add audit logging for all OIDC token exchanges

Low Severity - Case Sensitivity Fixed

Database Query: Fixed case-sensitive comparison on case-insensitive GitHub usernames/org names.

Fix:

  • ✅ Use LOWER() SQL function for case-insensitive account lookup
  • Prevents lookup failures when stored value is Kilo-Org but query is kilo-org

📊 Changes Summary

5 files modified, +72 lines added:

  1. src/lib/integrations/platforms/github/adapter.ts - Add repository-scoped token generation
  2. src/lib/integrations/platforms/github/oidc.ts - Add missing OIDC claims
  3. src/lib/integrations/db/platform-integrations.ts - Fix case-insensitive lookup
  4. src/app/api/integrations/github/exchange-token/route.ts - Harden OIDC validation
  5. src/app/api/integrations/github/exchange-token-with-pat/route.ts - Fix privilege escalation

✅ Backward Compatibility

All changes are backward compatible:

  • OIDC workflows continue to work (now with scoped tokens)
  • PAT users with write access continue to work (read-only PATs now correctly rejected)
  • Other callers of generateGitHubInstallationToken() unaffected
  • Case-insensitive lookup improves reliability

🧪 Testing Recommendations

  1. Test OIDC endpoint with a workflow in a repo with KiloConnect installed
  2. Test PAT endpoint with read-only PAT (should reject with 403)
  3. Test case sensitivity with different casing (e.g., kilo-org vs Kilo-Org)
  4. Verify token scoping - tokens should only access the specific repository

const payload = await verifyGitHubOIDCToken(oidcToken, 'kilo-github-action');

const repositoryOwner = payload.repository_owner;
const repositoryOwnerId = payload.repository_owner_id;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: repository_owner_id type mismatch can cause false "owner mismatch" rejections

GitHub OIDC claims like repository_owner_id are typically numeric in the JWT payload; after jose parsing, this may be a number while integration.platform_account_id is stored as a string. The strict inequality check can then fail even when the IDs represent the same value, returning 403 unexpectedly.

Suggested change
const repositoryOwnerId = payload.repository_owner_id;
const repositoryOwnerId = String(payload.repository_owner_id);

GitHub OIDC claims may be parsed as numbers by jose, but platform_account_id
is stored as text in the database. Convert repositoryOwnerId to string to
prevent false 403 rejections due to type mismatch (e.g., 12345 !== '12345').
@catrielmuller
Copy link
Author

Type Mismatch Fix

Fixed a type comparison issue in the OIDC owner ID validation:

Problem: GitHub OIDC repository_owner_id claims may be parsed as numbers by the jose library, while platform_account_id is stored as text in the database. The strict inequality check would fail even when the IDs represent the same value (e.g., 12345 !== '12345').

Solution: Normalize repositoryOwnerId to string using String() before comparison.

Commit: e184c96

console.error('GitHub installation check failed:', error);
Sentry.captureException(error);
return NextResponse.json(
{ error: error instanceof Error ? error.message : 'Internal server error' },
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Returning raw exception messages can leak internals

This response exposes error.message to the caller, which may include sensitive details (upstream errors, stack context, etc.). Prefer a generic error message in the HTTP response and rely on Sentry/server logs for diagnostics.

Suggested change
{ error: error instanceof Error ? error.message : 'Internal server error' },
{ error: 'Internal server error' },

const { token } = await generateGitHubInstallationToken(
integration.platform_installation_id,
integration.github_app_type || 'standard',
[`${owner}/${repo}`]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Scope installation token using canonical repository name

repositoryNames is built from user-provided owner/repo. Using the canonical name from GitHub (repoData.full_name) avoids casing/canonicalization mismatches and reduces the chance the installation-token request fails unexpectedly.

Suggested change
[`${owner}/${repo}`]
[repoData.full_name]

Sentry.captureException(error);

return NextResponse.json(
{ error: error instanceof Error ? error.message : 'Internal server error' },
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Returning raw exception messages can leak internals

This 500 response exposes error.message to the caller. Prefer a generic message in the response and keep details in Sentry/server logs.

Suggested change
{ error: error instanceof Error ? error.message : 'Internal server error' },
{ error: 'Internal server error' },

}

return NextResponse.json(
{ error: error instanceof Error ? error.message : 'Internal server error' },
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Returning raw exception messages can leak internals

This 500 response exposes error.message to the caller. Prefer a generic message in the response and keep details in Sentry/server logs.

Suggested change
{ error: error instanceof Error ? error.message : 'Internal server error' },
{ error: 'Internal server error' },

import { findGitHubIntegrationByAccountLogin } from '@/lib/integrations/db/platform-integrations';
import * as Sentry from '@sentry/nextjs';

export async function GET(request: NextRequest) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you give a little more info on:

  1. Why we need to check the installation in the first place
  2. Why we need to check by owner?

Shouldn't an authenticated user be required here? And then we can simply query their own `platform_integrations?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jrf0110 It's to support the kilo github install and kilo github run commands of the CLI, so the CLI needs an endpoint to check if the installation ends to continue with the generation of the .github/workflow files.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants