ua / okta-oidc
Reusable Okta OIDC authentication package for Laravel applications.
Requires
- php: ^8.2
- illuminate/contracts: ^11.0|^12.0|^13.0
- illuminate/http: ^11.0|^12.0|^13.0
- illuminate/routing: ^11.0|^12.0|^13.0
- illuminate/support: ^11.0|^12.0|^13.0
- laravel/socialite: ^5.16
- socialiteproviders/manager: ^4.7
- socialiteproviders/okta: ^4.5
Requires (Dev)
- larastan/larastan: ^3.9
- orchestra/testbench: ^9.0|^10.0|^11.0
- pestphp/pest: ^3.8|^4.0
- pestphp/pest-plugin-laravel: ^3.2|^4.0
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^10.5|^11.0|^12.0
This package is auto-updated.
Last update: 2026-08-06 13:45:55 UTC
README
A reusable Okta OIDC authentication package for Laravel applications. Provides login, callback, logout, and session-expiry flows out of the box — without forcing any particular local user model on your app.
Requirements
- PHP 8.2+ (PHP 8.3+ for Laravel 13)
- Laravel 11, 12, or 13
Quick Start
1. Install the package
composer require ua/okta-oidc
2. Publish the config
php artisan vendor:publish --tag=okta-oidc-config
3. Add your Okta credentials to .env
OKTA_BASE_URL=https://your-org.okta.com OKTA_CLIENT_ID=your_client_id OKTA_CLIENT_SECRET=your_client_secret OKTA_REDIRECT_URI=https://your-app.com/auth/oidc/callback OKTA_AUTH_SERVER_ID=default
4. Protect your routes
Route::middleware(['okta-oidc.auth'])->group(function () { Route::get('/', HomeController::class); Route::get('/dashboard', DashboardController::class); });
That's it. Unauthenticated users are redirected to Okta, and after login they land back on the page they originally requested.
How It Works
When a user hits a protected route:
- The
okta-oidc.authmiddleware checks for a valid OIDC session - If missing/expired, the user is redirected to Okta to sign in
- After Okta authentication, the callback route:
- Resolves a principal (username, email, etc.) via a
PrincipalResolver - Stores the principal, ID token, and expiration in the session
- Runs a UserBootstrapper to perform any additional setup (session claims, database user, etc.)
- Redirects back to the originally requested page
- Resolves a principal (username, email, etc.) via a
If the state parameter Okta returns does not match the one stored in the session, the
callback cannot be trusted and the user is redirected to the expired page with the
okta_oidc_expired flash flag — the same state the okta-oidc.auth middleware uses for a
dead session. This usually means a stale login tab, a back-button revisit, or a session
that did not survive the round trip.
The expired page is deliberately a terminal destination with a sign-in link, rather than an
automatic bounce back to okta-oidc.login. If the underlying cause is that sessions are not
persisting at all, an automatic retry would loop between the callback and Okta indefinitely,
since Okta still holds an authenticated session and redirects back without any user
interaction.
Routes
The package registers these routes under the auth/oidc prefix (configurable):
| Method | URI | Name | Purpose |
|---|---|---|---|
GET |
/auth/oidc/login |
okta-oidc.login |
Redirect to Okta |
GET |
/auth/oidc/callback |
okta-oidc.callback |
Handle Okta response |
GET|POST |
/auth/oidc/logout |
okta-oidc.logout |
Destroy session + Okta logout |
GET |
/auth/oidc/expired |
okta-oidc.expired |
Session expired page |
GET |
/auth/oidc/logged-out |
okta-oidc.logged-out |
Logout confirmation page |
Principal Resolvers
A PrincipalResolver extracts a user identifier from the OIDC user object returned by Okta. This identifier is stored in the session as the "principal" — typically a username or email.
Built-in Resolvers
| Resolver | Config Value | Behavior | Example Output |
|---|---|---|---|
UsernamePrincipalResolver |
Default | preferred_username local part, lowercased |
jdoe |
UserPrincipalNamePrincipalResolver |
Opt-in | Full preferred_username, lowercased |
jdoe@ua.edu |
EmailPrincipalResolver |
Opt-in | Full email, lowercased | john.doe@ua.edu |
OktaIdPrincipalResolver |
Opt-in | Okta user ID | 00u21yawsni0DL5V51d8 |
To switch resolvers, update config/okta-oidc.php:
use Ua\LaravelOktaOidc\Resolvers\EmailPrincipalResolver; 'principal_resolver' => EmailPrincipalResolver::class,
Creating Your Own
Implement Ua\LaravelOktaOidc\Contracts\PrincipalResolver:
namespace App\Auth; use Illuminate\Http\Request; use Ua\LaravelOktaOidc\Contracts\PrincipalResolver; use Ua\LaravelOktaOidc\Exceptions\OidcAuthenticationException; class CwidPrincipalResolver implements PrincipalResolver { /** * @param \Laravel\Socialite\Two\User $oidcUser */ public function resolve(object $oidcUser, Request $request): string { $cwid = data_get($oidcUser->getRaw(), 'cwid'); if (! filled($cwid)) { throw new OidcAuthenticationException('OIDC user does not have a CWID claim.'); } return $cwid; } }
Then reference it in config:
'principal_resolver' => \App\Auth\CwidPrincipalResolver::class,
User Bootstrappers
A UserBootstrapper runs after authentication to perform app-specific setup — storing claims in the session, creating database records, calling Auth::login(), etc. The bootstrapper is called after the core session keys (principal, ID token, expiration) are already stored.
Built-in Bootstrappers
NullUserBootstrapper
Does nothing. Use this if you only need the core session keys and handle everything else yourself.
'user_bootstrapper' => \Ua\LaravelOktaOidc\Resolvers\NullUserBootstrapper::class,
SessionUserBootstrapper (Default)
Stores configurable OIDC claims into the session. Controlled by the session_claims config:
'session_claims' => [ 'okta.name' => 'getName', // calls $oidcUser->getName() 'okta.email' => 'getEmail', // calls $oidcUser->getEmail() 'okta.raw_claims' => '@raw', // stores $oidcUser->getRaw() (all claims) ],
Accessor types:
| Accessor | Behavior | Example |
|---|---|---|
'getName' |
Calls the method on the Socialite user object | getName(), getEmail() |
'@raw' |
Stores the entire raw OIDC claims array | All JWT claims |
'preferred_username' |
Looks up a key in the raw claims via data_get() |
Supports dot notation like 'address.city' |
After login, access the claims from the session:
session('okta.name'); // "Joey Stowe" session('okta.email'); // "jbstowe@ua.edu" session('okta.raw_claims'); // ['sub' => '00u...', 'preferred_username' => '...', ...]
You can add your own claims to the mapping:
'session_claims' => [ 'okta.name' => 'getName', 'okta.email' => 'getEmail', 'okta.groups' => 'groups', // raw claim key 'okta.department' => 'department', // raw claim key 'okta.raw_claims' => '@raw', ],
EloquentUserBootstrapper
Extends SessionUserBootstrapper — stores session claims and creates/updates a local Eloquent User record, then calls Auth::login(). This gives you full Auth::user() support backed by Laravel's default users table.
'user_bootstrapper' => \Ua\LaravelOktaOidc\Resolvers\EloquentUserBootstrapper::class,
On each login it:
- Stores OIDC claims in the session (inherited from
SessionUserBootstrapper) - Finds or creates a User by email (
firstOrCreate) - Syncs the user's name from Okta
- Sets a random hashed password on first creation (users authenticate via OIDC, not passwords)
- Calls
Auth::login($user)
The User model class is configurable:
'user_model' => App\Models\User::class,
Or via environment variable:
OKTA_OIDC_USER_MODEL=App\Models\User
Guard Hydration (hydrate_guard)
By default, the session-only bootstrappers never touch Laravel's auth guard, so Auth::user() returns null even though the OIDC session is valid. That means guard-based tooling — monitoring like Laravel Nightwatch, Auth::id() in logs, etc. — sees every request as a guest.
If your app has no local users table (or you just don't want database-backed users), enable guard hydration:
'hydrate_guard' => true,
When enabled, the okta-oidc.auth middleware populates the default guard on every request with a valid OIDC session, using a GenericUser built from the session:
id— the principal (e.g. the username)name— from thegetNamesession claim, when storedemail— from thegetEmailsession claim, when stored
This is request-scoped only: Auth::setUser() never writes to the session and no user provider or database lookup is involved. If a real user is already authenticated (e.g. via EloquentUserBootstrapper), hydration is skipped and the real user wins.
Note: With hydration enabled, anything guard-based — the
authmiddleware, policies,Auth::check()— will see the generic user on routes behindokta-oidc.auth. If parts of your app rely onAuth::check()beingfalsefor OIDC-only sessions, leave this off.
Creating Your Own
Implement Ua\LaravelOktaOidc\Contracts\UserBootstrapper:
namespace App\Auth; use App\Models\User; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Ua\LaravelOktaOidc\Contracts\UserBootstrapper; class MyUserBootstrapper implements UserBootstrapper { /** * @param \Laravel\Socialite\Two\User $oidcUser */ public function bootstrap(Request $request, string $principal, object $oidcUser): void { // Store custom session data $request->session()->put('department', data_get($oidcUser->getRaw(), 'department')); // Find or create a local user with custom logic $user = User::firstOrCreate( ['cwid' => data_get($oidcUser->getRaw(), 'cwid')], [ 'name' => $oidcUser->getName(), 'email' => $oidcUser->getEmail(), ], ); Auth::login($user); } }
Or extend SessionUserBootstrapper to keep the session claim behavior and add your own logic on top:
namespace App\Auth; use Illuminate\Http\Request; use Ua\LaravelOktaOidc\Resolvers\SessionUserBootstrapper; class MyUserBootstrapper extends SessionUserBootstrapper { public function bootstrap(Request $request, string $principal, object $oidcUser): void { parent::bootstrap($request, $principal, $oidcUser); // Your additional setup here... } }
Then reference it in config:
'user_bootstrapper' => \App\Auth\MyUserBootstrapper::class,
Session Data
After a successful login, the following session keys are available:
| Session Key | Source | Description |
|---|---|---|
username |
Controller | The resolved principal |
okta.id_token |
Controller | JWT ID token (used for federated logout) |
okta.session_expires_at |
Controller | ISO 8601 expiration timestamp |
okta.name |
SessionUserBootstrapper | User's display name |
okta.email |
SessionUserBootstrapper | User's email address |
okta.raw_claims |
SessionUserBootstrapper | Full array of OIDC claims |
The first three are always set by the controller. The rest depend on your session_claims config and which bootstrapper you're using.
Session keys are configurable:
'session_keys' => [ 'principal' => 'username', 'id_token' => 'okta.id_token', 'expires_at' => 'okta.session_expires_at', ],
Middleware Behavior
The okta-oidc.auth middleware protects routes by checking for a valid OIDC session. It handles expired sessions differently based on the request type:
Branches are evaluated in this order; the first match wins:
| Request Type | Behavior |
|---|---|
Inertia request (X-Inertia: true) |
Returns 409 with X-Inertia-Location header so the Inertia client triggers a full-page navigation. Safe methods point at login (and store the intended URL); unsafe methods point at the expired page. |
| JSON/AJAX request | Returns 419 status with { "message": "...", "reauth_url": "..." } (any HTTP verb) |
| Safe HTML method (GET, HEAD, OPTIONS) | Stores current URL as intended, redirects to login |
| Unsafe HTML method (POST, PUT, DELETE) | Redirects to the expired page — does not replay the request |
This prevents accidental form resubmission after session expiry.
Inertia.js Support
If you're using Inertia.js, the middleware automatically detects Inertia requests via the X-Inertia header and responds with HTTP 409 plus an X-Inertia-Location header. The Inertia client treats this as an external redirect and performs a full-page browser navigation — which is required to handle the redirect chain to Okta correctly. Detection is header-based, so the package does not depend on inertiajs/inertia-laravel.
Federated Logout
By default, logging out clears the local session and redirects to Okta's logout endpoint to end the Okta session. This prevents users from being silently re-authenticated on the next visit.
Disable it if you only want to clear the local session:
'federated_logout' => false,
Configuration Reference
Publish with php artisan vendor:publish --tag=okta-oidc-config.
| Key | Default | Description |
|---|---|---|
driver |
'okta' |
Socialite driver name |
okta.base_url |
env('OKTA_BASE_URL') |
Okta org URL |
okta.client_id |
env('OKTA_CLIENT_ID') |
OAuth client ID |
okta.client_secret |
env('OKTA_CLIENT_SECRET') |
OAuth client secret |
okta.redirect |
env('OKTA_REDIRECT_URI') |
Callback URL |
okta.auth_server_id |
env('OKTA_AUTH_SERVER_ID') |
Auth server ID |
routes.prefix |
'auth/oidc' |
Route prefix |
routes.middleware |
['web'] |
Route middleware |
routes.name_prefix |
'okta-oidc.' |
Route name prefix |
middleware_alias |
'okta-oidc.auth' |
Middleware alias |
scopes |
['openid', 'profile', 'email'] |
OAuth scopes |
principal_resolver |
UsernamePrincipalResolver::class |
Principal resolver class |
user_bootstrapper |
SessionUserBootstrapper::class |
User bootstrapper class |
hydrate_guard |
false |
Populate the auth guard with a request-scoped GenericUser from session claims |
user_model |
env('OKTA_OIDC_USER_MODEL', 'App\\Models\\User') |
Eloquent user model |
session_keys.principal |
'username' |
Session key for principal |
session_keys.id_token |
'okta.id_token' |
Session key for ID token |
session_keys.expires_at |
'okta.session_expires_at' |
Session key for expiry |
session_claims |
See above | Claim-to-session mapping |
redirects.after_login |
'/' |
Redirect after login |
redirects.after_logout |
null |
Redirect after logout |
messages.expired |
Session expired message | Shown on expired page |
messages.logged_out |
Signed out message | Shown on logged-out page |
expired_request_status |
419 |
HTTP status for expired JSON responses |
federated_logout |
true |
Enable Okta federated logout |
Security Notes
- The callback regenerates the session after successful OIDC authentication, preventing session fixation.
- The callback verifies the OAuth
stateparameter; a mismatch is rejected rather than exchanged, and the user is sent to the expired page instead of receiving a 500. - The
return_toparameter is validated to be a same-host URL before being stored as the intended redirect. - Unsafe methods (POST, PUT, DELETE) are never automatically replayed after session expiry.
- Federated logout clears both the local session and the Okta session.