ua / okta-oidc
Reusable Okta OIDC authentication package for Laravel applications.
Requires
- php: ^8.2
- illuminate/contracts: ^11.0|^12.0
- illuminate/http: ^11.0|^12.0
- illuminate/routing: ^11.0|^12.0
- illuminate/support: ^11.0|^12.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
- pestphp/pest: ^3.8
- pestphp/pest-plugin-laravel: ^3.2
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^10.5|^11.0
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+
- Laravel 11 or 12
Quick Start
1. Install the package
composer require ua/laravel-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
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 | Email local part, lowercased | jdoe |
EmailPrincipalResolver |
Opt-in | Full email, lowercased | jdoe@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
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:
| Request Type | Behavior |
|---|---|
| Safe method (GET, HEAD, OPTIONS) | Stores current URL as intended, redirects to login |
| Unsafe method (POST, PUT, DELETE) | Redirects to the expired page — does not replay the request |
| JSON/AJAX request | Returns 419 status with { "message": "...", "reauth_url": "..." } |
This prevents accidental form resubmission after session expiry.
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 |
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
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.