GitHub OAuth Apps Get Token Rotation and Safer Redirect Controls

GitHub's August 14 OAuth app update adds expiring access tokens, refresh tokens, multiple callback URLs, and explicit wildcard controls.

GitHub's latest OAuth app update is easy to underestimate because it lands in the plumbing layer: tokens, callback URLs, and redirect matching. That is exactly why it matters.

On August 14, 2026, GitHub announced support for expiring access tokens and refresh tokens in OAuth apps, up to 10 callback URLs for OAuth app registrations, and explicit wildcard matching controls for callback URLs across OAuth apps and GitHub Apps. For teams that operate developer tools, CLI integrations, internal dashboards, AI coding assistants, or automation that acts on behalf of GitHub users, this is a real security and maintainability change.

What Happened

GitHub announced three related changes on August 14, 2026.

First, OAuth apps can now use expiring access tokens with refresh tokens. An app can request the short-lived-token pattern for an individual sign-in by adding the offline_access scope to the authorization request. It can also require expiring tokens globally in the app registration once the application has been updated to handle refresh. GitHub's docs say expiring access tokens last eight hours, while refresh tokens expire after six months without use. When the app uses a refresh token, GitHub returns a new access token and a new refresh token, and the old refresh token and old access token stop working.

Short-lived access tokens are enabled by default for new OAuth apps. GitHub notes that maintainers can opt out while updating SDKs or client code that does not yet support refresh tokens. Enabling expiring tokens for an existing app does not automatically expire existing long-lived tokens; users need to sign in again to receive a new token pair.

Second, OAuth apps can register up to 10 callback URLs. GitHub calls these callback URLs in the UI and docs, while much of the OAuth ecosystem calls them redirect URIs. This matters for applications that support production, staging, preview, enterprise, regional, or tenant-specific deployments. Before this change, teams often created duplicate OAuth apps or leaned on broader redirect matching to support multiple environments. Both approaches create operational debt.

Third, wildcard matching is now an explicit per-callback setting for OAuth apps and GitHub Apps. If enabled for https://example.com/path, GitHub's docs say matching can allow subdomains and subdirectories of that callback URL while still requiring the registered host, excluding subdomains, and port to match the callback URL. If disabled, the redirect URL must exactly match the configured callback URL.

There is an important migration detail: apps that had a single callback URL before August 3, 2026 may have wildcard matching enabled to preserve previous behavior. GitHub explicitly recommends disabling wildcard matching if the app does not need it. The August 14 changelog also says these improvements will be included in GitHub Enterprise Server 3.23.

Why It Matters

This update moves GitHub OAuth apps closer to the security baseline that experienced engineers already expect from modern OAuth implementations.

Long-lived bearer tokens are convenient, but they are a poor default for high-value developer accounts. GitHub tokens often unlock source code, private repositories, issues, releases, package registries, deployments, and organization metadata. If one of those tokens leaks into logs, crash reports, browser storage, a compromised dependency, or a copied database snapshot, its lifetime becomes the practical blast radius.

Refresh tokens do not make token theft harmless. A refresh token is powerful and must be protected carefully. But the rotation pattern is materially better than one access token that works indefinitely. The access token has a short useful lifetime. Refresh creates a new pair. Replay of an old refresh token can be detected because the token should already have been invalidated after use.

That lines up with RFC 9700, the OAuth 2.0 Security Best Current Practice published by the IETF in January 2025. RFC 9700 describes refresh tokens as a way to issue shorter-lived access tokens, while warning that refresh tokens are attractive targets because they represent the granted access. For public clients, it recommends replay protection through sender-constrained refresh tokens or refresh token rotation. GitHub's new OAuth app behavior adopts the rotation model.

The redirect URI change matters just as much. Redirect URI validation is one of the places OAuth implementations repeatedly fail in production. RFC 9700 says authorization servers should use exact string matching for registered redirect URIs, except for localhost port handling in native apps. It also documents attacks where loose pattern matching, wildcard bugs, subdomain takeover, or open redirectors can leak authorization codes or tokens.

GitHub's new model is pragmatic. It allows up to 10 explicit callback URLs, which reduces pressure to use broad wildcard behavior just to support normal deployment topology. It also makes wildcard behavior visible and controllable rather than implicit. That is the right tradeoff: give teams exact redirects for common cases, keep wildcard matching for narrow multi-tenant cases, and force maintainers to own the risk when they enable it.

For AI engineering and developer-tooling teams, this is especially relevant. Coding agents, internal MCP servers, repository dashboards, docs generators, CI assistants, and migration tools often use GitHub OAuth because they need user-delegated access. These systems also tend to run across local development, preview apps, hosted SaaS, enterprise installations, and customer-specific domains. Token rotation and explicit callback registration reduce the custom auth machinery those teams need to carry.

How It Works

The web application flow still starts at GitHub's authorization endpoint. The app sends the user to https://github.com/login/oauth/authorize with parameters such as client_id, redirect_uri, scope, state, and, preferably, PKCE parameters.

GitHub's docs now mark several security parameters as strongly recommended. state should be an unguessable value used to protect against CSRF. code_challenge and code_challenge_method enable PKCE, with S256 as the supported method. After the user authorizes the app, GitHub redirects back with a temporary authorization code and the original state. The app then exchanges the code at https://github.com/login/oauth/access_token.

If the app requested offline_access, or if the app registration requires expiring tokens, the token response includes an access token, an expires_in value, a refresh token, and a refresh_token_expires_in value. GitHub's examples use 28800 seconds for the access token lifetime and 15897600 seconds for the refresh token lifetime, corresponding to eight hours and roughly six months.

Refreshing uses the same token endpoint with grant_type=refresh_token and the refresh token. Confidential web applications send their client secret. Device-flow tokens can refresh without a client secret. The scopes on the refreshed access token match the previous token; the refresh request cannot be used to expand access.

The important implementation detail is rotation. Once a refresh token is used, the old refresh token and old access token stop working. Your persistence layer must update the stored refresh token atomically enough that concurrent requests do not fight each other. If two workers try to refresh the same user's token at the same time, one can succeed and invalidate the old token while the other is still holding it. That is not a GitHub-specific problem; it is the normal operational shape of refresh token rotation.

The callback URL behavior is also straightforward but worth being precise about. OAuth app registrations can now contain up to 10 callback URLs. If the authorization request omits redirect_uri, GitHub redirects to the first configured callback URL. In a multi-callback app, that default is a footgun; always send the intended redirect_uri so the authorization code is bound to the environment that initiated the flow.

Wildcard matching is per callback URL. With wildcard matching disabled, the redirect URL must exactly match a configured callback URL. With wildcard matching enabled, GitHub permits matching subdomains and subdirectories according to its documented rules, while rejecting different base domains, different ports, and unrelated paths. This is useful for tightly controlled tenant subdomains. It is risky if any matched subdomain or path can be influenced by users, old DNS records, static hosting buckets, preview environments, or open redirect endpoints.

Example

Imagine a hosted developer tool named CodeMap that reads repository metadata and opens pull-request-aware workspaces. It has these environments:

https://codemap.example.com/auth/github/callback
https://staging.codemap.example.com/auth/github/callback
https://preview.codemap.example.com/auth/github/callback
https://eu.codemap.example.com/auth/github/callback

Before this update, CodeMap's team had two unattractive options. It could create separate GitHub OAuth apps for each environment, which means separate client IDs, secrets, consent screens, installation instructions, and support paths. Or it could rely on broad redirect matching and hope every matched host and path stayed clean forever.

With multiple callback URLs, the team can register the explicit URLs under one OAuth app. Each environment sends its own redirect_uri during authorization, and callback matching can remain exact. That is easier to operate and safer than a wildcard.

For tokens, CodeMap can roll out refresh support incrementally. First, it adds offline_access to a subset of sign-ins or a beta environment. The token storage schema changes from a single access token to something closer to this:

github_user_id
access_token_ciphertext
access_token_expires_at
refresh_token_ciphertext
refresh_token_expires_at
scopes
updated_at

The application refreshes shortly before access-token expiry. It encrypts refresh tokens at rest, redacts token values from logs, and treats refresh as a compare-and-swap operation keyed by the previous refresh token or row version. If refresh fails, the app sends the user through authorization again.

Once this path is stable, CodeMap can enable expiring tokens in its GitHub app registration. New sign-ins receive rotating token pairs by default. Existing long-lived tokens remain valid until users reauthorize, so the team still needs user prompts, telemetry for old-token usage, and a cutoff date if the risk justifies forcing reauth.

Now consider the multi-tenant version:

https://customer-a.codemap.example.com/auth/github/callback
https://customer-b.codemap.example.com/auth/github/callback

Wildcard matching may look tempting. It might be reasonable if CodeMap fully controls every subdomain under codemap.example.com, has automated stale-DNS cleanup, forbids customer-controlled callback paths, and has no open redirectors under the matched route. But if customers can publish content on those subdomains, preview apps create dangling DNS records, or any matched route forwards to arbitrary URLs, wildcard matching is the wrong tool.

My Take

This is a good update because it removes two common excuses for weak OAuth app setups.

The first excuse was token compatibility. Many GitHub OAuth apps were built around durable access tokens because that was the simplest path. Now new OAuth apps default toward expiring access tokens, and existing apps have a runtime opt-in path through offline_access.

The second excuse was redirect sprawl. A lot of redirect looseness is not ideological; it comes from deployment reality. Teams need staging, preview, regional, and tenant URLs. When the platform supports only one callback URL, engineers create duplicate apps or broaden matching. Ten callback URLs will not cover every SaaS topology, but it covers many ordinary cases and makes exact matching much easier to justify.

The part I would not ignore is the legacy wildcard behavior. Making an implicit rule visible is helpful, but it also means maintainers now own the audit. If you operate a GitHub OAuth app or GitHub App, check every callback URL and ask whether wildcard matching is actually needed.

I would also avoid treating refresh tokens as a drop-in field addition. Rotation changes concurrency behavior, failure modes, and incident response. Store refresh tokens like credentials, not session metadata. Encrypt them, scope database access tightly, redact them aggressively, and design the refresh path so two application workers do not accidentally invalidate a healthy session.

For new internal tools, the baseline is clear: authorization code flow, PKCE, state, exact callback URLs, expiring access tokens, refresh token rotation, and no wildcard matching unless there is a documented tenant-domain model that justifies it. For older tools, this GitHub change is a good reason to retire one of the oldest pieces of auth debt in the stack.

Conclusion

GitHub's August 14, 2026 OAuth app update is not just a convenience release. It gives app maintainers a cleaner path to short-lived access tokens, refresh token rotation, explicit multi-environment callback registration, and auditable wildcard behavior.

The safest response is practical: update OAuth client code for refresh tokens, register exact callback URLs for known environments, send redirect_uri explicitly, keep PKCE and state in the flow, and audit wildcard matching on every existing app. The teams that do this work now will have fewer long-lived credentials to chase later and a much clearer redirect boundary around GitHub authorization.

Sources

Enjoying this article?

Subscribe to get new NestJS, Node.js, and backend engineering posts directly in your inbox.

Subscribe for free