Skip to content

Login & logout

Login is owned by AuthenticatedSessionController, wired through two handlers that share the auth/login path. Your app fills the LoginView seam by binding the contract; the package owns validation, credential checks, rate limiting, event dispatch, session regeneration, and the hand-off to the post-login pipeline and multi-factor challenge.

Route name Verb Path Middleware
identity.login GET auth/login web, guest:identity
identity.login.store POST auth/login web, guest:identity, throttle:5,1

GET identity.login renders through the bound LoginView contract. If none is bound, hitting the route throws MissingAuthViewException.

The realm’s methods decides which of password, passkey and social are accepted. A method left out has its routes closed — a hand-built POST gets a 404, not just a login page missing a button — and LoginPrompt::$methods tells the view which to render. Registration and password reset hang off password: both end in a password the realm would not otherwise accept.

The login flow (POST identity.login.store)

Section titled “The login flow (POST identity.login.store)”

The store action is throttled to 5 requests per minute and runs the following steps:

  1. Validate. The username field (config('oidc.login.username'), default email) and password are both required|string.
  2. Normalize. The username is lowercased before it is used as a credential.
  3. Verify credentials against the identity guard’s user provider (retrieveByCredentials + validateCredentials). On failure the request throws a ValidationException with the generic auth.failed message. Passwords are rehashed on login when hashing.rehash_on_login is enabled.
  4. Record the primary factor. The pwd authentication method is recorded (it becomes part of the amr claim — see Multi-factor).
  5. Run the post-login pipeline. A LoginEvent is dispatched through the hooks registered on PostLoginPipeline. If a hook denies the login, the recorded factor is discarded and the request fails with the same generic auth.failed message. Queued id_token/access_token claims from the pipeline are stored on the session.
  6. Branch on MFA (below).
  7. Branch on required actions. Before the session is established, the realm is asked whether the user still owes it anything — a confirmed address, a password inside the rotation window, an enrolled factor. If so the login parks: no session is created, and the user is sent to that action’s screen.
flowchart TD
    A["POST identity.login.store"] --> B["Validate + lowercase username"]
    B --> C{"Credentials valid?"}
    C -- no --> F["Generic auth.failed error"]
    C -- yes --> D["Record pwd factor"]
    D --> E["Post-login pipeline"]
    E -- "deny()" --> F
    E -- ok --> G{"Challengeable factor enrolled?"}
    G -- "no, but MFA required" --> L["Enroll a factor"]
    G -- no --> M
    G -- yes --> I["Stash pending login,<br/>redirect to two-factor challenge"]
    I --> J["Verify factor (adds its amr)"]
    J --> M
    L --> M{"Anything the realm requires?"}
    M -- yes --> N["Action screen"]
    N --> M
    M -- no --> H["Log in + regenerate session"]
    H --> K["Redirect to intended / home"]

If the realm still requires something of the user, the login is not completed: no session is created, a pending record naming the user is stored, and

  • a JSON request receives {"required_actions": ["update_password"]}.
  • a browser request is redirected to the first open action’s screen.

The required actions page covers the screens, the authorization-endpoint re-check, and how to add your own.

When nothing is left to challenge or require, the user is logged in on the identity guard (honoring the remember field), the session is regenerated, and:

  • A JSON request (wantsJson) receives an empty 200 response.
  • A browser request is redirected via redirect()->intended(...) to config('oidc.login.home') (default /dashboard).

After the primary factor succeeds, the package looks up the user’s confirmed, challengeable enrollments, filtered by config('oidc.credentials.challenge_providers') (default ['totp']).

  • If the post-login pipeline called requireMfa() but the user has no challengeable factor, the login is denied.
  • If at least one challengeable enrollment exists, the login is not completed yet. The pending user id, remember flag, and the first enrollment’s provider key and id are stashed on the session, and:
    • A JSON request receives {"two_factor": true}.
    • A browser request is redirected to identity.two-factor.login.

The user finishes authenticating on the two-factor challenge, which performs the actual guard->login() and session regeneration once a factor is verified.

Passwordless login is delegated to laravel/passkeys through two guest routes:

Route name Verb Path Middleware
identity.passkey.login-options GET auth/passkeys/login/options web, guest:identity, throttle:5,1
identity.passkey.login POST auth/passkeys/login web, guest:identity, throttle:5,1

The options endpoint returns the WebAuthn assertion challenge; the browser signs it and posts the credential back to identity.passkey.login to establish the session. Passkey registration (and its RequirePassword gate) lives on the multi-factor page.

The auth engine deliberately ships no identity.logout route — the relying party keeps Laravel’s conventional logout route name for itself. Log the user out of the interactive session with the identity guard:

use Illuminate\Support\Facades\Auth;
Route::post('/logout', function (Request $request) {
Auth::guard(config('oidc.auth.guard'))->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect('/');
})->name('logout');

Terminating the interactive session is separate from OIDC RP-initiated logout at /oauth/logout, which relying parties use to end the OP session and any downstream sessions. That endpoint and its CSRF threat model are documented under Logout.