omniscient / omniauth
Omniauth: who signs in, and by which factors, under any PHP host - the contracts (accounts, the stores of each proof, the second-factor rule, the host's small duties), the conductor (begin, then verify; the attempt, the result), the methods that need no library (password through symfony/password-has
Requires
- php: >=8.2
- psr/cache: ^2.0|^3.0
- psr/clock: ^1.0
- psr/event-dispatcher: ^1.0
- psr/simple-cache: ^2.0|^3.0
- symfony/password-hasher: ^6.4|^7.0|^8.0
Requires (Dev)
- phpunit/phpunit: ^11.0
- symfony/cache: ^6.4|^7.0|^8.0
Suggests
- omniscient/omnibridge: The host contracts: Omnibridge\Contract\Tokens checks a sign-in form's CSRF token; omnibridge/symfony and omnibridge/laravel bridge omniauth to their host
- symfony/cache: A PSR-6 pool or PSR-16 cache for Omniauth\CacheThrottle in plain PHP
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-21 01:31:37 UTC
README
Who signs in, and by which factors, under any PHP host: the methods and their ceremonies (begin, then verify), the second-factor rule, and the result of an attempt - who, what passed, what remains. The host keeps its firewall or guard, the request and the response, its session cookie and its pages; the application keeps its accounts and their proofs in its own tables.
use Omniauth\{ArrayState, CacheThrottle, Context, Omniauth}; use Omniauth\Method\Methods; use Omniauth\Password\PasswordMethod; use Omniauth\EmailCode\EmailCodeMethod; use Omniauth\BackupCode\BackupCodeMethod; use Omniauth\TrustedDevice\TrustedDevices; $omniauth = new Omniauth( accounts: $accounts, // your accounts, behind Contract\Accounts methods: new Methods([ new PasswordMethod($accounts, $passwords), // symfony/password-hasher: Symfony's hashes, >72 bytes included new EmailCodeMethod($accounts, $emailCodes, $sender, $secret), new BackupCodeMethod($backupCodes), new TrustedDevices($accounts, $trustedVersion, $secret), // scheb/2fa's trusted_device cookie ]), state: new ArrayState($_SESSION), // the session, under a framework throttle: new CacheThrottle($pool), // any PSR-6 pool or PSR-16 cache ); $context = new Context(origin: 'https://example.org', host: 'example.org', ip: $_SERVER['REMOTE_ADDR'], trustedDevice: $_COOKIE['trusted_device'] ?? null); $result = $omniauth->verify('password', $context, ['identifier' => 'ringo@example.org', 'password' => '...']); // SecondFactorRequired, $result->offered === [Factor::EmailCode, Factor::BackupCode] $omniauth->begin('email-code', $context); // the code goes out through your Sender $result = $omniauth->verify('email-code', $context, ['code' => '481516', 'trust' => true]); // Authenticated, $result->passed === [Factor::Password, Factor::EmailCode], $result->trustDevice === 'eyJ0eXAiOiJKV1Qi...'
It requires PHP 8.2, four PSR interfaces (cache, simple-cache, clock, event-dispatcher) and
symfony/password-hasher, a standalone component. The methods that carry a library are packages
of their own; the hosts are bridged in omnibridge:
| Package | What |
|---|---|
omniscient/omniauth (here) |
the contracts, the conductor, the methods that need no library: password, sign-in link, code by e-mail, backup codes, trusted devices; events; Testing\ |
omniauth/passkey |
passkeys, over web-auth/webauthn-lib; compatible both ways with web-auth/webauthn-symfony-bundle's records |
omniauth/totp |
authenticator applications, over spomky-labs/otphp, on scheb/2fa's parameters; a code once |
omniauth/oauth |
sign-in through identity providers, with no OAuth library: the state, PKCE and nonce, the linking by sub, the sign-up of an unknown identity |
omniauth/google |
Google, over league/oauth2-google: one package per provider |
| omnibridge/symfony, omnibridge/laravel | an authenticator per firewall; a guard and a user provider |
The name: OmniAuth is also Ruby's well-known sign-in library. This one is PHP, under omniscient/,
and unrelated.
The shape
Omniauth |
the conductor: begin(), verify(), attempt(), abandon(), enrol(), revoke() - one sequence whatever the method: throttle, Verifying, the method, Accounts::check(), the second-factor rule, SignedIn or SignInFailed |
Attempt, Result, Status, Failure, Factor, Context, Proof, Enrolment, Enrolled, Message, PendingIdentity |
the attempt kept between two requests, what it ended with, the stable keys a page translates |
Contract\ |
Accounts; one store per proof (Passwords, LinkTokens, EmailCodes, BackupCodes, TrustedVersion, TotpSecrets, Identities); SecondFactorPolicy; the host's duties (State, Throttle, Sender) |
Method\ |
Method (verify), Challenging (begin first), SecondFactor (enrolled?), Ambient (read from the context), Enrolling, Revocable; Verdict, Challenge, Methods |
Password\, Link\, EmailCode\, BackupCode\, TrustedDevice\ |
the five methods of the core |
EnrolledSecondFactors, ArrayState, CacheThrottle, SystemClock |
the defaults: the rule, the state, the throttle, the clock |
Event\ |
PSR-14: Beginning, Verifying, SecondFactorRequired, SignedIn, SignInFailed, AccountRefused, Revoking, CredentialAdded, CredentialRemoved |
Testing\ |
in-memory stores, a recording sender, a fixed clock, an array cache, stand-ins for a passkey and an authenticator application |
Decisions it carries (Marco, 2026-10-20)
- No second factor after a passkey (QA1), a second factor after an identity provider (QA2):
EnrolledSecondFactors::AFTERis[Password, Link, OAuth]. - The sign-in link: 15 minutes, once, its SHA-256 kept, never its value; a new link at each request, 3 an hour (QA4).
- The code by e-mail kept hashed: an HMAC keyed by the host's secret, bound to the account (QA5).
- The bridges in omnibridge, omniauth only suggested there (QA8).
Documentation
- Installation, in plain PHP
- The contracts
- Password, sign-in link, code by e-mail, backup codes, trusted devices
- The second-factor rule
- Events
- Testing
- Symfony and Laravel
- The Docker harness
Verified, and not
- Verified (
docker compose run --rm omniauth testindocker/, PHP 8.4; every file linted on PHP 8.2): each method's way through and its refusals on the in-memory stores - wrong secret, unknown address answered as a wrong password, expired, used twice, throttled, a banned account, the second factor offered, passed, not offered, its attempt expired; QA1 and QA2 through the default rule; the trusted-device tokens equal to those scheb/2fa 8.6.1'sJwtTokenEncoderwrites for the same input, whole seconds and microseconds, each read by the other (recorded, and live with scheb/2fa installed in the harness); hashes written by symfony/password-hasher 8.1 ("auto": bcrypt, cost 13), one of a 102-byte password thatpassword_verify()alone refuses;docs/installation.mdrun as written; in plain PHP, a whole sign-in by hand loading nothing but omniauth, the PSR interfaces and symfony/password-hasher, and every class of the package loading with them alone (BareTest). - Not verified: an application's own stores (omnibase fills them at step AU4); a real host (the
bridges come with AU6 and AU8); a cookie written by a running scheb/2fa on an application (the
encoder is compared, not a browser's cookie); concurrent tries against
CacheThrottle(read then written, not atomic: a host that needs it exact uses its rate limiter).
Licence
MIT.