-
Type:
Bug
-
Resolution: Unresolved
-
Priority:
Minor
-
Component/s: github-oauth-plugin
-
None
-
Environment:Jenkins 2.568.3, github-oauth 685.v53b_070455063, github-api 1.330-492.v3941a_032db_2a_, github.com. Security realm configured with the client ID and secret of a GitHub App that has "User-to-server token expiration" enabled (the default for GitHub Apps created since 2021). Matrix authorization with Overall/Read granted to GitHub org groups only.Jenkins 2.568.3, github-oauth 685.v53b_070455063, github-api 1.330-492.v3941a_032db_2a_, github.com. Security realm configured with the client ID and secret of a GitHub App that has "User-to-server token expiration" enabled (the default for GitHub Apps created since 2021). Matrix authorization with Overall/Read granted to GitHub org groups only.
Symptom
Users authenticate to the UI through GitHub normally. Any client using a Jenkins API token (REST, CLI, mcp-server plugin) works for 8 hours after that UI login, then every request fails with AccessDeniedException3: <user> is missing the Overall/Read permission. The UI session itself keeps working. The failures stop the moment the user loads any UI page, which re-runs the OAuth flow, and start again 8 hours later. With matrix authorization driven by org and team groups this looks to users like a random, daily permission outage. The only server-side trace is an INFO log from GithubAuthenticationToken#loadMyself with GitHub's 401 Bad credentials body, and it does not include the username.
Root cause
GithubSecurityRealm.getAccessToken splits GitHub's token response and returns only access_token, discarding refresh_token and expires_in. The access token is stored via GithubSecretStorage and reused by loadUserByUsername2 for every API-token request. Once GitHub expires it, loadMyself gets 401, caches UNKNOWN_TOKEN for one minute, and the constructor throws UsernameNotFoundException("Token not valid"). loadUserByUsername2 then throws UserMayOrMayNotExistException2, and because the realm deliberately does not fire SecurityListener.loggedIn (PR #87, JENKINS-47113), core has no LastGrantedAuthoritiesProperty to fall back to and grants authenticated only. The plugin has no code path that could use a refresh token.
Steps to reproduce
- Create a GitHub App, leave "User-to-server token expiration" enabled, configure its client ID and secret in the GitHub security realm, grant Overall/Read to an org group only.
- Log in through the UI, create an API token, confirm curl -u user:token .../whoAmI/api/json shows the org authorities.
- Wait 8 hours without loading a UI page. The same curl now returns 403 "missing the Overall/Read permission" and the controller logs loadMyself: Bad credentials.
- Load any UI page. The curl works again.
Workaround
Opt out of "User-to-server token expiration" on the GitHub App (Optional features). New tokens then behave like classic OAuth App tokens, which is what the plugin was designed around.
Suggested fixes, either of which would help
- Store refresh_token and expires_in alongside the access token and refresh before expiry, with a per-user lock, since using a refresh token invalidates both old tokens and concurrent API-token requests are normal.
- Failing that, detect GitHub App client IDs (Iv1. and Iv23 prefixes) and surface an administrative monitor or documentation note that expiring user tokens are not supported.
Related
JENKINS-74925 shows the identical loadMyself 401 log from a GitHub App setup. JENKINS-66292 describes sessions dying "under 8hr". https://github.com/jenkinsci/github-oauth-plugin/pull/87 contains the discussion that disabled the loggedIn event.