Skip to content

Conversation

@Bencheng21
Copy link
Contributor

@Bencheng21 Bencheng21 commented Jan 30, 2026

Description.

see here for the issue.

Fixes

Refresh the app JWT token once it expired.

Test

  1. Run ./baton-github.
image 2. Use the expired accessToken and see if it refreshes the refreshToken. 3. Use the expired jwtToken and see if it refreshes the jwtToken.

Summary by CodeRabbit

  • Refactor
    • Improved GitHub App authentication: more reliable JWT generation and automated refresh for installation tokens, preserving the public API.
  • Bug Fix
    • More consistent validation and error handling during app credential checks, reducing intermittent authentication failures.

@Bencheng21 Bencheng21 requested a review from a team January 30, 2026 23:28
@coderabbitai
Copy link

coderabbitai bot commented Jan 30, 2026

Important

Review skipped

Review was skipped due to path filters

⛔ Files ignored due to path filters (1)
  • .github/workflows/ci.yaml is excluded by none and included by none

CodeRabbit blocks several paths by default. You can override this behavior by explicitly including those paths in the path filters. For example, including **/dist/** will override the default block on the dist directory, by removing the pattern from both the lists.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

  • 🔍 Trigger a full review

Walkthrough

Reworks GitHub App authentication in pkg/connector: introduces JWT generation and a jwtExpiryTime, adds an app JWT refresher TokenSource, changes installation lookup to return installation+error, and refactors token refreshers to use oauth2.TokenSource for JWT and installation tokens.

Changes

Cohort / File(s) Summary
Connector core
pkg/connector/connector.go
Adds jwtExpiryTime, getJWTToken(appID, privateKey), and appJWTTokenRefresher implementing Token(). Replaces raw JWT handling with oauth2.TokenSource composition for app JWTs. Refactors appTokenRefresher to consume a jwt TokenSource and return installation tokens built from that source. Updates findInstallation() to return (*github.Installation, error) and use consistent error wrapping.

Sequence Diagram(s)

sequenceDiagram
  participant Connector as "Connector\n(pkg/connector)"
  participant JWTRefresher as "appJWTTokenRefresher\n(oauth2.TokenSource)"
  participant GitHub as "GitHub API\n(Installations & App Auth)"
  participant AppTokenRef as "appTokenRefresher\n(Token provider)"

  Connector->>JWTRefresher: Request Token()
  JWTRefresher->>GitHub: generate signed JWT (getJWTToken)
  GitHub-->>JWTRefresher: signed JWT + expiry (jwtExpiryTime)
  JWTRefresher-->>Connector: oauth2.Token (app JWT)
  Connector->>GitHub: findInstallation(appID, owner/repo)
  GitHub-->>Connector: Installation ID
  Connector->>AppTokenRef: Request Token() using jwt TokenSource
  AppTokenRef->>GitHub: Exchange app JWT for installation token
  GitHub-->>AppTokenRef: installation token + expiry
  AppTokenRef-->>Connector: oauth2.Token (installation token)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

A rabbit signs a tiny key,
Hops a JWT across the sea 🐇
Refreshes, swaps, then gives a wink —
Tokens fresh before you blink!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title contains a typo ('refersh' instead of 'refresh') but accurately describes the main change: implementing JWT token refresh functionality when app JWT tokens expire.

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

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch ben/refersh_jwtToken

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

@Bencheng21 Bencheng21 force-pushed the ben/refersh_jwtToken branch from 9e6bfb1 to a7c5106 Compare January 30, 2026 23:30
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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pkg/connector/connector.go (1)

219-227: ⚠️ Potential issue | 🟠 Major

Guard against empty org list to avoid panic.

orgLogins[0] will panic when gh.orgs is empty. Align this with New() by requiring exactly one org in app mode.

Suggested fix
 func (gh *GitHub) validateAppCredentials(ctx context.Context) (annotations.Annotations, error) {
 	orgLogins := gh.orgs
-	if len(orgLogins) > 1 {
-		return nil, fmt.Errorf("github-connector: only one org is allowed when using github app")
-	}
+	if len(orgLogins) != 1 {
+		return nil, fmt.Errorf("github-connector: exactly one org is required when using github app")
+	}
 
 	_, err := findInstallation(ctx, gh.appClient, orgLogins[0])
🤖 Fix all issues with AI agents
In `@pkg/connector/connector.go`:
- Around line 399-414: The getJWTToken function should validate that appID is
non-empty before creating the JWT; add an explicit check at the start of
getJWTToken (before calling loadPrivateKeyFromString) that returns a clear error
if appID is empty, so the generated token never contains an empty "iss" claim;
update the function to return that error (e.g., from getJWTToken) and ensure
callers handle it.

Comment on lines +399 to 414
func getJWTToken(appID string, privateKey string) (string, error) {
key, err := loadPrivateKeyFromString(privateKey)
if err != nil {
return "", err
}
now := time.Now()
token, err := jwtv5.NewWithClaims(jwtv5.SigningMethodRS256, jwtv5.MapClaims{
"iat": now.Unix() - 60, // issued at
"exp": now.Add(time.Minute * 10).Unix(), // expires
"iss": ghc.AppId, // GitHub App ID
"iss": appID, // GitHub App ID
}).SignedString(key)
if err != nil {
return "", "", err
return "", err
}
return token, "", nil
return token, nil
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Validate appID before signing.

An empty appID yields an invalid iss claim and opaque auth failures. Add an explicit check.

Suggested fix
 func getJWTToken(appID string, privateKey string) (string, error) {
+	if strings.TrimSpace(appID) == "" {
+		return "", errors.New("github-connector: app ID is required")
+	}
 	key, err := loadPrivateKeyFromString(privateKey)
 	if err != nil {
 		return "", err
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func getJWTToken(appID string, privateKey string) (string, error) {
key, err := loadPrivateKeyFromString(privateKey)
if err != nil {
return "", err
}
now := time.Now()
token, err := jwtv5.NewWithClaims(jwtv5.SigningMethodRS256, jwtv5.MapClaims{
"iat": now.Unix() - 60, // issued at
"exp": now.Add(time.Minute * 10).Unix(), // expires
"iss": ghc.AppId, // GitHub App ID
"iss": appID, // GitHub App ID
}).SignedString(key)
if err != nil {
return "", "", err
return "", err
}
return token, "", nil
return token, nil
}
func getJWTToken(appID string, privateKey string) (string, error) {
if strings.TrimSpace(appID) == "" {
return "", errors.New("github-connector: app ID is required")
}
key, err := loadPrivateKeyFromString(privateKey)
if err != nil {
return "", err
}
now := time.Now()
token, err := jwtv5.NewWithClaims(jwtv5.SigningMethodRS256, jwtv5.MapClaims{
"iat": now.Unix() - 60, // issued at
"exp": now.Add(time.Minute * 10).Unix(), // expires
"iss": appID, // GitHub App ID
}).SignedString(key)
if err != nil {
return "", err
}
return token, nil
}
🤖 Prompt for AI Agents
In `@pkg/connector/connector.go` around lines 399 - 414, The getJWTToken function
should validate that appID is non-empty before creating the JWT; add an explicit
check at the start of getJWTToken (before calling loadPrivateKeyFromString) that
returns a clear error if appID is empty, so the generated token never contains
an empty "iss" claim; update the function to return that error (e.g., from
getJWTToken) and ensure callers handle it.

installation, resp, err := c.Apps.FindOrganizationInstallation(ctx, orgName)
if err != nil {
return nil, nil, err
return nil, wrapGitHubError(err, resp, "github-connector: failed to find installation")
Copy link
Contributor

Choose a reason for hiding this comment

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

this is an unrelated change just to wrap the GitHub error in one place yes?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes

jwtTokenSource: oauth2.ReuseTokenSource(
&oauth2.Token{
AccessToken: jwttoken,
Expiry: time.Now().Add(jwtExpiryTime),
Copy link
Contributor

Choose a reason for hiding this comment

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

so we set an expiration time on the token that is different than on the jwt, because it will expire sooner and lead to a refresh before the token expires. That aligns with the description of recommended library in #115

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The JWT is valid for 10 minutes. It’s better to refresh it a bit earlier to avoid a 401 error interrupting the workflow.

Copy link
Contributor

@jugonzalez12 jugonzalez12 left a comment

Choose a reason for hiding this comment

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

seems reasonable

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: 1

🤖 Fix all issues with AI agents
In `@pkg/connector/connector.go`:
- Around line 225-228: The code indexes orgLogins[0] before ensuring the slice
is non-empty which can panic; update the caller in connector.go to check
len(orgLogins) > 0 before calling findInstallation and return a descriptive
error (e.g., "no org logins provided") when empty, then only call
findInstallation(ctx, gh.appClient, orgLogins[0]) when the guard passes;
reference orgLogins and findInstallation to locate the change and ensure any
returned error is propagated unchanged.
🧹 Nitpick comments (1)
pkg/connector/connector.go (1)

279-307: Reuse the refreshable JWT source for appClient.

Currently, appClient is created with a static JWT token (lines 270-275) but is used in long-running contexts: it's passed to orgBuilder and included in ResourceSyncers (line 110) where it will be used during resource syncing, and it's also used in validateAppCredentials (line 225). Since JWT tokens expire after ~10 minutes, appClient will become invalid if syncing or validation operations extend beyond initialization. The refreshable JWT source introduced at lines 298-307 should be reused for appClient as well, similar to how it's used for the main clients (lines 312-316).

Comment on lines +225 to 228
_, err := findInstallation(ctx, gh.appClient, orgLogins[0])
if err != nil {
return nil, wrapGitHubError(err, resp, "github-connector: failed to retrieve org installation")
return nil, err
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Guard against empty org list before indexing.
Line 225 indexes orgLogins[0]; if the org list is empty (misconfig or future call path), this will panic. Return a clear error instead.

✅ Proposed fix
 func (gh *GitHub) validateAppCredentials(ctx context.Context) (annotations.Annotations, error) {
 	orgLogins := gh.orgs
 	if len(orgLogins) > 1 {
 		return nil, fmt.Errorf("github-connector: only one org is allowed when using github app")
 	}
+	if len(orgLogins) == 0 {
+		return nil, fmt.Errorf("github-connector: org is required when using github app")
+	}
 
 	_, err := findInstallation(ctx, gh.appClient, orgLogins[0])
 	if err != nil {
 		return nil, err
 	}
 	return nil, nil
 }
🤖 Prompt for AI Agents
In `@pkg/connector/connector.go` around lines 225 - 228, The code indexes
orgLogins[0] before ensuring the slice is non-empty which can panic; update the
caller in connector.go to check len(orgLogins) > 0 before calling
findInstallation and return a descriptive error (e.g., "no org logins provided")
when empty, then only call findInstallation(ctx, gh.appClient, orgLogins[0])
when the guard passes; reference orgLogins and findInstallation to locate the
change and ensure any returned error is propagated unchanged.

with:
connector: ./baton-github
baton-entitlement: 'repository:642588514:triage'
baton-entitlement: 'repository:642588514:admin'
Copy link
Contributor Author

@Bencheng21 Bencheng21 Feb 3, 2026

Choose a reason for hiding this comment

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

@laurenleach sleep time doesn't solve the problem, but this line fixes the problem.
The issue is that the user is a member of a team that has been granted access to this repository. Even if we remove the user’s direct access entitlement, they will still be able to access the repo through the team.
I really don't like grant the user with admin entitlement, but we can't do anything if we don't have admin pat token. ( updating these tests requires the resource id )

@Bencheng21 Bencheng21 merged commit 129857d into main Feb 3, 2026
16 of 21 checks passed
@Bencheng21 Bencheng21 deleted the ben/refersh_jwtToken branch February 3, 2026 02:29
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.

5 participants