GitHub App & OAuth tokens
Static Token credentials never expire, but GitHub App installation tokens and OAuth access tokens do. GithubAppToken and OauthToken mint, cache and refresh the access token transparently — reads, writes and batches all benefit, with secrets redacted from any serialization. The App JWT is a real RS256 JWS signed through crypto-for-laravel — no third-party JWT library.
GitHub App installation tokens
Configure the app once in .env:
GITHUB_APP_ID=123456
GITHUB_APP_INSTALLATION_ID=7654321
GITHUB_APP_PRIVATE_KEY=/path/to/app-private-key.pem
GITHUB_APP_SLUG=acme-botWith the app id, installation id and private key all configured, Git::github() builds a GithubAppToken automatically — with any of the three missing it falls back to the static token, or to no credential — and Git::credentials(ProviderName::Github) returns that same credential — ->accessToken() on it reads the live installation token, minting it when the cache is cold. Or build one explicitly:
use RoundlyConsulting\Git\Dto\Credentials\{GithubAppToken, OauthToken};
use RoundlyConsulting\Git\Enums\ProviderName;
$github = Git::github(GithubAppToken::for(
appId: config('git.providers.github.app.id'),
installationId: config('git.providers.github.app.installation_id'),
privateKey: config('git.providers.github.app.private_key'), // PEM string or file path
));
$github->repositories(); // installation token minted, cached to expiry, reusedApp and installation ids must be numeric — anything else throws InvalidCredentialsException before it can reach a cache key. Use a shared cache store (not the array driver) so minted tokens persist across requests.
OAuth access tokens
// The OAuth client (client_id, client_secret, token_url) is static per provider, so
// forProvider() takes it from git.providers.github.oauth.* and asks only for the
// per-user half.
$github = Git::github(OauthToken::forProvider(
ProviderName::Github,
accessToken: $access, refreshToken: $refresh, expiresAt: $expiresAt,
));
// Pass all five to OauthToken::for() to bypass config entirely.
$gitlab = Git::gitlab(OauthToken::for(
accessToken: $access,
refreshToken: $refresh,
clientId: 'your-client-id',
clientSecret: 'your-client-secret',
tokenUrl: 'https://gitlab.com/oauth/token',
expiresAt: $expiresAt,
));forProvider() throws InvalidCredentialsException naming the missing key when client_id, client_secret or token_url isn’t configured.
Persisting a rotated refresh token
If you store OAuth credentials, listen for OauthTokenRefreshed. Providers that rotate refresh tokens invalidate the old one the moment they issue a new one. The new token goes into the token cache, whose entry expires with the access token — so once that hour is up, the only copy left anywhere is the one you persisted, and it is dead:
use Illuminate\Support\Facades\Event;
use RoundlyConsulting\Git\Events\OauthTokenRefreshed;
Event::listen(OauthTokenRefreshed::class, function (OauthTokenRefreshed $event) {
if ($event->rotated()) {
$connection->update([
'refresh_token' => $event->refreshToken,
'access_token' => $event->accessToken,
'expires_at' => $event->expiresAt,
]);
}
});The event fires on every refresh, rotated or not; rotated() is the flag that says the stored value has to change. Do not log the event whole — it carries live tokens by design.
Concurrent refreshes
Concurrent workers refresh once: on a cache store that supports locks (Redis, database, file, array, …) the refresh runs under a lock, and a worker that waited for it uses the token the holder just refreshed instead of presenting the spent refresh token again — so a rotating provider never answers invalid_grant to the loser of the race, and the event fires once.
Show your open-source love
This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.
More ways to support, including cryptoBy donating, you agree to our donation terms.
Want this built into your product?
We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.