ua/okta-oidc

Reusable Okta OIDC authentication package for Laravel applications.

Maintainers

Package info

github.com/uadevteampackages/okta-oidc

pkg:composer/ua/okta-oidc

Statistics

Installs: 86

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.3.0 2026-04-30 21:30 UTC

This package is auto-updated.

Last update: 2026-05-01 15:52:59 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+
  • Laravel 11 or 12

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:

  1. The okta-oidc.auth middleware checks for a valid OIDC session
  2. If missing/expired, the user is redirected to Okta to sign in
  3. 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

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:

  1. Stores OIDC claims in the session (inherited from SessionUserBootstrapper)
  2. Finds or creates a User by email (firstOrCreate)
  3. Syncs the user's name from Okta
  4. Sets a random hashed password on first creation (users authenticate via OIDC, not passwords)
  5. 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:

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
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_to parameter 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.