Skip to content

Conversation

@abebars
Copy link
Contributor

@abebars abebars commented Jan 30, 2026

Note

🤖 Generated with Claude Code

Fixes #115

Summary

Replace manual JWT handling with ghinstallation/v2 library to fix token expiration issues on long-running syncs. Also fixes Enterprise SAML handling and GitHub Enterprise URL configuration.

Problem

When using GitHub App authentication, the connector fails with 401 Unauthorized errors after ~10 minutes because:

  1. JWT tokens (10-min max expiry) were generated once at startup
  2. Stored statically in appTokenRefresher.jwttoken
  3. When installation tokens expired (~1 hour), the expired JWT was reused

Additional issues found in production:
4. REST clients not configured with Enterprise URLs for GitHub Enterprise instances
5. Token expiry not set, causing oauth2 to cache tokens indefinitely
6. Enterprise SAML error crashes sync when org-level SAML is disabled in favor of Enterprise SAML

Solution

Use github.com/bradleyfalzon/ghinstallation/v2 - the officially recommended library by go-github that:

  • Automatically regenerates JWTs before they expire
  • Automatically refreshes installation tokens
  • Is thread-safe and battle-tested
  • Supports GitHub Enterprise

Plus additional fixes:

  • Configure WithEnterpriseURLs() on REST clients for GitHub Enterprise
  • Set token expiry from ghinstallation transport for proper oauth2 refresh
  • Handle Enterprise SAML error in hasSAML() gracefully (return false instead of failing)

Changes

  • Add ghinstallation/v2 dependency
  • Replace appTokenRefresher with ghinstallationTokenSource wrapper
  • Configure Enterprise URLs on appClient and ghClient (addresses CodeRabbit feedback)
  • Set token Expiry from transport for oauth2 refresh (addresses CodeRabbit feedback)
  • Handle Enterprise SAML error in hasSAML() to prevent sync failures
  • Add tests for token refresh behavior including expired token scenarios
  • Remove dead code (getClientToken, loadPrivateKeyFromString, appTokenRefresher)

Testing

  • Unit tests pass
  • Token refresh test verifies expired tokens are renewed
  • PAT authentication unchanged and tested
  • Build succeeds
  • Deployed to production - successfully syncing 6,114 repos, 744 teams, 1,561 users

Production Validation

This fix has been deployed and validated in production:

  • Running on EKS with GitHub App authentication
  • Successfully syncing large GitHub organization (6K+ repos)
  • Enterprise SAML handling confirmed working
  • JWT refresh verified over multiple hours

Rollback Plan

Revert PR - PAT authentication is unchanged, only GitHub App auth is affected.

Summary by CodeRabbit

  • New Features

    • GitHub App authentication now uses installation-based tokens with automatic refresh and enterprise URL support; PAT authentication remains supported.
  • Tests

    • Added comprehensive tests covering App and PAT authentication flows, token caching/expiry, refresh behavior, and misconfiguration cases.
  • Chores

    • Updated indirect dependencies to support the enhanced authentication implementations.

✏️ Tip: You can customize this high-level summary in your review settings.

@abebars abebars requested a review from a team January 30, 2026 04:03
@coderabbitai
Copy link

coderabbitai bot commented Jan 30, 2026

Walkthrough

Replaces manual GitHub App JWT handling with ghinstallation-based App/Installation transports and an oauth2 TokenSource; adds indirect GitHub/JWT deps and comprehensive tests for App and PAT authentication, token refresh/expiry, and misconfiguration handling.

Changes

Cohort / File(s) Summary
Dependency Management
go.mod
Added indirect dependencies: github.com/bradleyfalzon/ghinstallation/v2, github.com/golang-jwt/jwt/v4, github.com/google/go-github/v75.
Authentication Implementation
pkg/connector/connector.go
Replaced custom private-key/JWT and manual installation-token refresh with ghinstallation-based App and Installation transports; added ghinstallationTokenSource (oauth2.TokenSource); removed legacy JWT helpers and static JWT reuse; preserved PAT branch and enterprise URL handling; updated error paths.
Authentication Tests
pkg/connector/connector_test.go
Added tests (httptest, in-memory RSA keys) covering ghinstallation token sourcing, caching/auto-refresh, expiry handling, PAT flow, no-auth error, and multi-org App misconfiguration.

Sequence Diagram(s)

sequenceDiagram
    participant App as Application
    participant GHT as ghinstallation<br/>Transport
    participant TS as ghinstallationTokenSource
    participant GH as GitHub API
    participant GQL as GraphQL Client

    App->>GHT: Initialize transport (AppID, AppKey[, EnterpriseURL])
    App->>TS: Wrap GHT as oauth2 TokenSource
    App->>GQL: Create GraphQL client using TS

    GQL->>TS: Request token
    TS->>GHT: Ask for installation token
    GHT->>GH: Use JWT to request installation token
    GH-->>GHT: Return installation token (with expiry)
    GHT-->>TS: Supply oauth2 token
    TS->>TS: Cache token (expiry)
    TS-->>GQL: Return oauth2.Token

    note over GHT,TS: On expiry, GHT regenerates JWT and refreshes installation token automatically
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇 I nudge the keys and watch them spin,

Tokens hop back in when they grow thin,
ghinstallation hums, renews the tune,
No stale JWTs beneath the moon,
Syncs keep dancing past high noon.

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.09% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: use ghinstallation for GitHub App token refresh' accurately summarizes the main change: replacing manual JWT handling with the ghinstallation library to resolve token expiration issues.
Linked Issues check ✅ Passed The pull request successfully addresses all coding requirements from issue #115: replaces manual JWT handling with ghinstallation/v2, configures enterprise URL support, sets token expiry for oauth2 caching, and includes comprehensive tests for token refresh scenarios.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing GitHub App token refresh: dependency addition, connector authentication refactoring, and comprehensive test coverage. No unrelated modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Comment @coderabbitai help to get the list of available commands and usage tips.

@abebars abebars force-pushed the fix/github-app-token-refresh branch from 7248766 to 2d4bfae Compare January 30, 2026 04:08
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@pkg/connector/connector.go`:
- Around line 267-309: The REST clients appClient and ghClient are not
configured for GitHub Enterprise because only appTransport.BaseURL and
installTransport.BaseURL are set; call client.WithEnterpriseURLs(instanceURL) on
both github.Client instances (appClient and ghClient) when instanceURL is
non-empty and not equal to githubDotCom so their BaseURL fields point at the
enterprise API; locate where appClient is created after
ghinstallation.NewAppsTransport and where ghClient is created after
ghinstallation.New and invoke WithEnterpriseURLs using the same instanceURL
logic as used for appTransport/installTransport, matching the pattern used by
newGitHubClient for PAT auth.
- Around line 394-406: The Token() implementation on ghinstallationTokenSource
returns an oauth2.Token with no Expiry so oauth2.ReuseTokenSource treats it as
non-expiring; update Token() to set the token Expiry (e.g., Expiry: time.Now())
so the oauth2 client will re-evaluate and allow ghinstallation.Transport to
refresh tokens. Modify ghinstallationTokenSource.Token to import time, call
g.transport.Token(context.Background()), and return &oauth2.Token{AccessToken:
token, Expiry: time.Now()} (ensuring use of the ghinstallationTokenSource,
Token(), ghinstallation.Transport and oauth2.Token symbols referenced).
🧹 Nitpick comments (1)
pkg/connector/connector_test.go (1)

90-95: Optional: track the skipped GitHub App test.
Consider linking this to an issue or moving it under an integration test tag so it doesn’t stay skipped indefinitely.

abebars and others added 2 commits January 30, 2026 10:47
Replace manual JWT handling with ghinstallation/v2 library to fix
token expiration issues on long-running syncs.

Problem: JWT tokens (10-min expiry) were generated once at startup
and reused, causing 401 errors when installation tokens expired.

Solution: Use ghinstallation which automatically regenerates JWTs
and refreshes installation tokens as needed.

Changes:
- Add ghinstallation/v2 dependency
- Replace appTokenRefresher with ghinstallationTokenSource
- Add tests for token refresh behavior
- Remove dead code (getClientToken, loadPrivateKeyFromString)

Fixes ConductorOne#115

Co-Authored-By: Claude <[email protected]>
- Handle Enterprise SAML error in hasSAML() to prevent sync failures
  when org-level SAML is disabled in favor of Enterprise SAML
- Configure Enterprise URLs on appClient and ghClient for GitHub
  Enterprise instances (addresses CodeRabbit feedback)
- Set token Expiry from ghinstallation transport for proper oauth2
  token refresh behavior (addresses CodeRabbit feedback)

Co-Authored-By: Claude <[email protected]>
@abebars abebars force-pushed the fix/github-app-token-refresh branch from ac67be8 to 1b71e4b Compare January 30, 2026 15:49
- Fix errcheck: handle w.Write return values in tests
- Fix goconst: extract repeated test URL path to constant
- Fix gocritic ifElseChain: add nolint directive (switch inappropriate)
- Fix nilerr: rename variable and add nolint for intentional fallback
- Fix unused: remove unused getInstallationToken function
- Fix gosec G101: add nolint for test constant (not credentials)
- Remove unused io import

Co-Authored-By: Claude <[email protected]>
@dslick
Copy link

dslick commented Jan 30, 2026

We are actively reviewing this and investigating alternative solutions.

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.

GitHub App JWT token expires after 10 minutes, causing 401 on subsequent syncs

2 participants