Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Oauth Server Bundle Laravel Package

klapaudius/oauth-server-bundle

A Symfony bundle that turns your application into an OAuth 2.0 authorization server, handling token issuance, client and scope management, and grant flows so you can secure APIs and third-party access without building the protocol yourself.

View on GitHub
Deep Wiki
Context7

Getting Started

This release is about making your authorization server a correct OpenID Connect provider, not just an OAuth 2.0 one. The package already issues access tokens; 6.0.0 lets you carry the nonce from the authorization request and the user's real auth_time through to the token request so you can put them in the id_token.

Start here, in this order:

  1. AuthCodeInterface — the contract now has setNonce() / setAuthTime() / getters. Read it first; it defines everything the rest of the flow passes around.
  2. OAuthStorage::createAuthCode() — the single write path for auth codes. Its signature now accepts the nonce, and it persists both fields.
  3. The Doctrine mappings (orm, mongodb, couchdb) — new nullable nonce and auth_time columns/fields. Run your schema diff before anything else.
  4. Your consent controller and its Blade form — the package ships the hidden-field round-trip, but you own the view that renders it.

Minimal upgrade steps:

composer require <vendor>/<package>:^6.0
php artisan vendor:publish --tag=<package>-migrations   # if you publish them
php artisan doctrine:schema:update --dump-sql           # inspect before applying
php artisan cache:clear                                 # doctrine metadata cache

First use case worth wiring end-to-end: log in a user, hit /authorize?nonce=abc123, consent, exchange the code at /token, and assert the resulting id_token contains "nonce": "abc123" and a plausible integer auth_time. That single test proves the whole release works in your app.

// Token endpoint / id_token claim assembly
$authCode = $this->storage->getAuthCode($code);

$claims = [
    'sub'       => $user->getAuthIdentifier(),
    'nonce'     => $authCode->getNonce(),      // null if the RP never sent one
    'auth_time' => $authCode->getAuthTime(),   // int, NumericDate
];

Implementation Patterns

Capture the nonce at /authorize, not at /token. The nonce is a property of the authorization request. Read it off the incoming request in your authorize controller and stash it in the session alongside the pending authorization, so it survives whatever redirects and re-prompts happen before consent.

$request->session()->put('oidc.pending', [
    'client_id'    => $request->input('client_id'),
    'redirect_uri' => $request->input('redirect_uri'),
    'state'        => $request->input('state'),
    'nonce'        => $request->input('nonce'),
    'auth_time'    => $request->session()->get('auth.authenticated_at'),
]);

Stamp auth_time at authentication, not at authorization. The release notes are explicit: the library deliberately leaves auth_time to the application, because only you know when the user actually presented credentials. Set it in your LoginController/LoginResponse and keep it in the session for the life of that session. Do not update it on every request, and do not update it when a user merely re-consents — auth_time is "when did they authenticate", and max_age handling depends on it being stable.

// LoginController@store
Auth::attempt($credentials);
$request->session()->put('auth.authenticated_at', time());

Consent round-trip. If you use the bundled hidden field, your consent Blade just needs the field rendered inside the form. If you have a custom consent screen (Inertia, Livewire, SPA), you must reproduce it yourself — put the nonce in a hidden input or in the session, but never in a URL query string that lands in access logs.

<form method="POST" action="{{ route('oauth.consent.approve') }}">
    @csrf
    <input type="hidden" name="auth_code" value="{{ $pendingCode }}">
    <input type="hidden" name="nonce" value="{{ $pending['nonce'] }}">
    <button type="submit">Approve</button>
</form>

Build the id_token claims from the auth code, at token time. Keep claim assembly in one place — a dedicated IdTokenClaimsBuilder or a listener on your token-issuance event — so nonce and auth_time are handled consistently for the authorization-code grant. Don't scatter the logic across controllers.

class IdTokenClaims
{
    public function build(AuthCodeInterface $code, User $user): array
    {
        return array_filter([
            'iss'       => config('app.url'),
            'sub'       => (string) $user->getAuthIdentifier(),
            'aud'       => $code->getClient()->getIdentifier(),
            'iat'       => time(),
            'exp'       => time() + 300,
            'nonce'     => $code->getNonce(),
            'auth_time' => $code->getAuthTime(),
        ], fn ($v) => $v !== null);
    }
}

Note the array_filter — nonce is legitimately absent for clients that didn't send one. Emitting "nonce": null is wrong; omitting the claim is correct. auth_time should always be present once you're stamping it.

Keep the two grants separate. This release targets the authorization-code flow. If you also support implicit or hybrid, nonce is mandatory there and must be validated as present at /authorize. Don't let the now-optional-feeling nullable column lull you into skipping that validation.

Testing. Use a fixed clock or Carbon::setTestNow() so auth_time is deterministic. Assert round-tripping specifically: nonce survives the consent POST, and a code minted with no nonce yields an id_token without the nonce claim.

Gotchas and Tips

AuthCodeInterface grew two methods — that is a real BC break for implementors. If you have your own AuthCode class (common when you need custom metadata on codes), PHP will fatal on the missing methods after upgrading. Add setNonce()/setAuthTime() and their getters before you touch anything else. This is why it's 6.0.0.

auth_time is an integer NumericDate, not a datetime. Passing a Carbon or a formatted string will produce a non-conformant id_token that strict RPs (and jose-based validators) reject. Cast at the boundary: (int) $authenticatedAt->timestamp.

Clear the Doctrine metadata cache after the schema update. New nullable fields silently won't hydrate if the ORM is still working from cached mapping metadata — you'll see getNonce() returning null even though the row has a value. php artisan cache:clear plus your metadata cache driver, and if you run doctrine:migrations:diff, review the generated SQL for the three mapping types; MongoDB and CouchDB won't get DDL, but the document shape changes and existing documents simply lack the fields (which is fine — they're nullable).

Existing auth codes have null in both columns. Short-lived, so usually a non-issue, but if you have a long auth_code TTL and in-flight codes during deploy, be ready for getAuthTime() returning null. Either emit the claim conditionally or flush pending codes on deploy.

Never trust a nonce or auth_time from the token request. Both must come off the stored auth code. If you're reading $request->input('nonce') in your token controller, an attacker can inject claims. The whole point of persisting them on the code is to remove that temptation.

max_age and prompt=login interact with auth_time. Once you start emitting auth_time, RPs will begin sending max_age. You now need to compare max_age against the session's authentication timestamp at /authorize and force re-authentication when it's stale — and reset auth.authenticated_at when you do. Emitting the claim without honouring max_age is a half-implementation that auditors will flag.

Watch the consent hidden field in logs. It's a short-lived, single-use value, but if your request logging middleware dumps POST bodies it'll end up in your log aggregator. Exclude it, or better, keep the nonce in the session and skip the hidden field entirely if your consent flow doesn't leave the server.

Debugging tip: log the auth code entity's nonce/auth_time at three points — immediately after createAuthCode(), after the consent POST resolves the code, and at /token before claim assembly. A null at point two means your hidden field or session key is misnamed; a null at point three means you're fetching a different code record than you created.

Extension points: if you need extra OIDC claims (acr, amr, azp), the cleanest route is a custom claims builder or a decorator around your storage's createAuthCode(), rather than editing the Doctrine mapping. The mapping XML is package-owned and will be overwritten on upgrade.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings