Skip to content

Polaris for PHP

Self-hosted authentication, MFA, organizations and RBAC for any PHP application. Framework-free core, PSR-15 for HTTP, PDO for storage, one object graph and no container.

PHP Framework-agnostic Multi-tenant MFA License

Polaris gives an application a complete identity stack it owns: email-verified registration, password login with Argon2id, JWT access tokens with rotating refresh tokens and theft detection, multi-factor authentication (TOTP by QR, SMS, email, recovery codes, step-up), multi-tenant organizations with role-based access control, an append-only audit log and PSR-14 events. The HTTP contract is 52 endpoints declared in packages/core/api/**/*.yaml; every response is contract-frozen by a recorded fixture suite.

The name is the idea: Polaris is the fixed star your application’s identity navigates by.


PackageNamespaceWhat it is
polaris/corePolaris\The domain: identity, MFA, tokens, organizations, authorization, the schema, the endpoints, the manifest, the wiring. Depends only on PSR interfaces and a few libraries (lcobucci/jwt, spomky-labs/otphp, endroid/qr-code, symfony/uid, symfony/yaml).
polaris/psr15Polaris\Psr15\Router, middleware stack and request handler for any PSR-15 host (Slim, Mezzio, Laravel or Symfony through their PSR bridges).
polaris/pdoPolaris\Pdo\The database adapter for PostgreSQL, MySQL and SQLite, the DDL exporter and the schema inspector.
polaris/testingPolaris\Testing\The in-memory database adapter for tests of applications built on Polaris.
polaris/cliPolaris\Cli\bin/polaris: schema:export, schema:diff, manifest (JSON, OpenAPI 3.1), doctor.
polaris/laravelPolaris\Laravel\Laravel 13: service provider, the endpoints as routes, the polaris guard, mail bridge, polaris:* artisan commands.
polaris/symfonyPolaris\Symfony\Symfony 7.4 / 8: bundle, the endpoints as routes, a firewall authenticator, mail bridge, polaris:* console commands.
polaris/yiiPolaris\Yii\Yii 3: config plugin, the endpoints as routes, an authentication method, mail bridge, polaris:* console commands.
polaris/auditPolaris\Audit\A plugin: every Polaris event becomes a catalogued, redacted, append-only record, queried per user and per organization, streamed to sinks and to each organization’s drains, pruned by policy, optionally hash-chained.
polaris/adminPolaris\Admin\A plugin: the operator API over users, sessions, MFA, organizations, audit drains and statistics, for admin users and API keys, every action audited.
polaris/messagingPolaris\Messaging\A plugin: the verification, reset, invitation, one-time-code and security messages from templates overridable per instance and per organization, translated, rate-limited, sent through the channel you choose (Symfony Mailer, PHPMailer, Twilio, Vonage, a log).
polaris/sentinelPolaris\Sentinel\A plugin: a local risk engine on sign-up, sign-in, password reset and code sends; signals score an attempt, a policy allows, challenges or blocks, observe mode records without enforcing.
polaris/ssoPolaris\Sso\A plugin: per-organization SAML 2.0 and OpenID Connect providers, verified domains, just-in-time users and memberships, single logout in both directions, the organization’s own self-service routes.
polaris/scimPolaris\Scim\A plugin: a SCIM 2.0 server per organization; a directory provisions members and roles with a connection token the organization creates and rotates itself.
polaris/passwordlessPolaris\Passwordless\A plugin: magic links, email one-time codes (sign-in, email verification, password reset), phone codes for a verified second contact, one-time tokens that hand a session to another device; each ends in core’s login envelope behind core’s MFA gate.
polaris/usernamePolaris\Username\A plugin: usernames unique without regard to case, and sign-in with a username or the email through core’s password path.
polaris/anonymousPolaris\Anonymous\A plugin: guest sessions, their conversion into the account the guest signs up for through a host hook, and the pruning of unconverted guests.
polaris/multi-sessionPolaris\MultiSession\A plugin: several signed-in accounts on one device, switching between them without signing in again, signing one out, the device’s last sign-in method.
polaris/socialPolaris\Social\A plugin: OAuth 2.0 / OpenID Connect sign-in through Google, Apple, Microsoft, GitHub and fifteen more providers or any OAuth 2 server, account linking under a policy, Google One Tap, provider tokens on the user’s behalf, an OAuth proxy for preview origins.
polaris/passkeyPolaris\Passkey\A plugin: WebAuthn passkeys, registration and discoverable sign-in with conditional UI, each passkey also a core MFA factor verified through core’s own MFA routes.
polaris/api-keysPolaris\ApiKeys\A plugin: API keys owned by users and organizations with a subset of the owner’s permissions, their own rate limit, expiry, rotation with a grace window and revocation; a key authenticates every Polaris route as its owner.
polaris/oauth-providerPolaris\OAuth\A plugin: Polaris as an OAuth 2.1 / OpenID Connect provider; authorization code with PKCE, refresh rotation, client credentials, the device flow, CIBA, token exchange, DPoP, dynamic registration, client ID metadata documents, discovery; scopes are permissions.
polaris/mcpPolaris\Mcp\A plugin: Polaris and the Model Context Protocol. McpProtectedResource protects any PHP MCP server with Polaris-issued tokens (RFC 9728 metadata, insufficient_scope step-up, DPoP, local keys or a remote issuer); Polaris itself is an MCP server whose tools are its routes and CLI commands, listed per principal, over POST /mcp and bin/polaris mcp.
polaris/agentsPolaris\Agents\A plugin: AI agents as principals. Registration and host enrolment, delegated (token exchange with act) and autonomous (client credentials, private_key_jwt) agents, signed request JWTs, capability grants with constraints and budgets, approvals by device code, CIBA or in-app, signed receipt chains, a kill switch; Agent Auth Protocol compatible discovery.
polaris/agentPolaris\Agent\The PHP agent SDK: discover, register or enrol, request grants and wait for approval, sign request JWTs, call capabilities, verify receipts.
@polaris-auth/client (npm)packages/client-tsThe TypeScript client generated from the manifest: openapi-fetch typed by the 52 core endpoints plus the plugins’ namespaces (client.audit, client.admin, client.sentinel, client.sso, client.scim, client.passwordless, client.username, client.anonymous, client.multiSession, client.social, client.passkey, client.apiKeys, client.oauth, client.mcp, client.agents), request bodies, data and error checked at compile time.
@polaris-auth/agent (npm)packages/agent-tsThe TypeScript agent SDK, Node and edge runtimes, no dependencies: the same as polaris/agent.

This repository is the monorepo; each package is published to its own read-only repository for Composer (twenty-five univeros/polaris.* repositories; the client and the agent SDK go to npm).


The fastest way to see everything is the Slim demo:

Terminal window
cd examples/slim
composer install
bin/setup # .env, RS256 keys, SQLite schema, permission catalog
bin/walkthrough.sh # register → verify → login → TOTP → MFA login → organization

The whole integration is examples/slim/src/bootstrap.php:

$polaris = Polaris::create(new Config(
secrets: EnvironmentConfig::secrets(), // APP_KEY, AUTH_JWT_* from the environment
auth: EnvironmentConfig::auth(), // issuer, audience, feature flags
database: new PdoAdapter($pdo), // PostgreSQL, MySQL or SQLite
mailer: $mailer, // OtpMailerInterface: verification and OTP emails
sms: $sms, // SmsSenderInterface
dispatcher: $dispatcher, // PSR-14; subscribe $polaris->listeners()
));
$pipeline = new Pipeline($polaris->graph(), $responseFactory);
$pipeline->middleware(); // ordered PSR-15 middleware for your stack
$pipeline->handler(); // the PSR-15 handler serving every route in packages/core/api/

Every port has a working default (in-memory cache, log mailer and SMS sender, system clock, libsodium encrypter, PSR-3 metrics); pass your own to replace it. plugins: [...] adds packages built on the plugin contract (docs/plugins): their tables, routes, services, listeners and permissions join core’s, in every host.

In Laravel the same graph comes from config/polaris.php and Laravel’s own services:

Terminal window
composer require polaris/laravel
php artisan polaris:install && php artisan migrate # config, tables, permission catalog

then Route::middleware('auth:polaris') protects your routes with Polaris access tokens (examples/laravel is the complete host).

In Symfony, register Polaris\Symfony\PolarisBundle, describe the same graph under polaris: in config/packages/polaris.yaml, import the routes with type: polaris, and put Polaris\Symfony\Security\PolarisAuthenticator on a firewall (examples/symfony is the complete host).

In Yii 3, polaris/yii is a yiisoft/config plugin: set the polaris params, and the routes, the definitions, the polaris/authentication middleware and the polaris:* commands are there (examples/yii is the complete host).

From a browser or Node, @polaris-auth/client (packages/client-ts) is the same contract typed: createClient({ baseUrl, token }) over openapi-fetch, generated from polaris manifest --format=openapi and drift-checked in CI. Beside the 52 core endpoints it carries the plugins’ routes as namespaces (client.audit, client.admin, client.sentinel, client.sso, client.scim).


AreaWhat you get
AuthenticationRegister, email verification, password login, /auth/me, logout / logout-all
TokensAsymmetric JWT access tokens (RS256/EdDSA) + opaque rotating refresh tokens with reuse detection; JWKS endpoint
SessionsPer-device session list, individual + global revocation
MFA / OTPTOTP (QR), SMS OTP, email OTP, recovery codes, login-MFA gate, step-up
PasswordsArgon2id, policy enforcement, breached-password hook, reset & change (logout-everywhere)
Multi-tenant RBACOrganizations, memberships, roles, permissions, invitations, org switching
AuthorizationDeclarative per-endpoint permissions + a programmatic Gate
SecurityRate limiting, account lockout, anti-enumeration, audit log, key rotation
OpsPSR-14 domain events, notification fan-out, transient-row pruning, metrics
PluginsPackages on the plugin contract (docs/plugins): the operator API (polaris/admin), the audit store and drains (polaris/audit), messaging channels and templates (polaris/messaging), the risk engine (polaris/sentinel), SSO (polaris/sso), SCIM (polaris/scim), passwordless sign-in (polaris/passwordless), usernames (polaris/username), guest sessions (polaris/anonymous), several accounts per device (polaris/multi-session), social sign-in (polaris/social), passkeys (polaris/passkey), API keys (polaris/api-keys), the OAuth 2.1 / OpenID Connect provider (polaris/oauth-provider), MCP (polaris/mcp), agents (polaris/agents; SDKs polaris/agent and @polaris-auth/agent)

HTTP core/api/**/*.yaml the manifest: method, path, auth, rate limit, effect, input rules, endpoint class
Polaris\Psr15 RouteMiddleware → ClientContext → rate limits → token / MFA-ticket auth → step-up → denylist → authorization → RequestHandler
Polaris\Http Endpoint(Input): Result — parse, call a service, map the outcome
Domain Identity, Mfa, Token, Authorization services, transactional, emitting PSR-14 events
Polaris\Contract ports: DatabaseAdapter, repositories, tokens, encrypter, mailer, SMS, rate store, metrics, …
Persistence Polaris\Schema the tables as data; Polaris\Repository over any DatabaseAdapter (polaris/pdo, polaris/testing)
Wiring Polaris\Wiring\Graph every service built explicitly from a Config; no container

Login returns a short-lived JWT access token plus a rotating refresh token; presenting an already-rotated refresh token is treated as theft and revokes the entire token family.


A representative slice (full catalog in docs/auth/api-reference.md; bin/polaris manifest --format=openapi renders the OpenAPI 3.1 document):

MethodPathPurpose
POST/auth/registerCreate account, send verification
POST/auth/loginPassword login → tokens or MFA challenge
POST/auth/token/refreshRotate refresh → new access + refresh
POST/auth/mfa/totp/enrollStart TOTP enrollment → secret + QR
POST/auth/mfa/verifyComplete MFA → tokens
GET/auth/sessionsList active devices/sessions
POST/orgsCreate an organization (creator → owner)
POST/orgs/{id}/invitesInvite a member
POST/auth/switch-orgSwitch active org → re-scoped token

Routes are relative; mount the handler under whatever prefix your application uses (Pipeline takes a path prefix).


Three factor types, one uniform verification flow:

  • TOTP (authenticator app) — RFC 6238 via spomky-labs/otphp; enrolled by scanning a QR code (otpauth:// provisioning URI). Secrets are encrypted at rest (XChaCha20-Poly1305).
  • SMS OTP — 6-digit codes delivered through your SmsSenderInterface.
  • Email OTP — 6-digit codes delivered through your OtpMailerInterface.
  • Recovery codes — 10 single-use codes, hashed at rest.
  • Step-up — sensitive operations (password change, removing a factor, deleting an org or a user) require a recent strong authentication.

Details in docs/auth/mfa-otp.md.


Identity is global; authority is scoped to an organization:

User ──< Membership >── Organization
└──< roles >── permissions (per-org; system roles when org is null)

Org creators become owner; admin/member templates are seeded per org and fully customizable. The access token carries the active org and resolved roles, so authorization is mostly stateless; the authorization middleware enforces per-endpoint permissions and a Gate handles the rules permissions can’t express (last-owner protection, role hierarchy). Cross-tenant access is denied by design. See docs/auth/rbac.md.


Polaris follows established standards — JWT (RFC 7519), JWKS (RFC 7517), TOTP (RFC 6238), OAuth 2.0 refresh semantics + Security BCP (RFC 9700), and OWASP ASVS for password storage. Secrets are never stored in plaintext (hashed or encrypted at rest), comparisons are constant-time, and signing keys are asymmetric with kid-based rotation. Full threat model in docs/auth/security.md.


The schema is data (Polaris\Schema), so there are no migrations to carry:

Terminal window
bin/polaris schema:export --target=sql:postgres > schema.sql # also sql:mysql, sql:sqlite
bin/polaris schema:diff --dsn='pgsql:host=…;dbname=…' # what a live database is missing
bin/polaris doctor --dsn=… # secrets, keys, manifest, connectivity, schema

polaris/pdo runs on PostgreSQL, MySQL and SQLite; the adapter conformance suite runs on all of them plus the in-memory adapter.


The specification lives in docs/auth/:

DocContents
READMEOverview and goals
data-modelModels, tables, relationships
flowsRegister, login, refresh rotation, sessions, password
mfa-otpTOTP/QR, SMS, email, recovery, step-up
rbacOrgs, memberships, roles, permissions, guard
api-referenceFull endpoint catalog + error format
securityThreat model, crypto, key management
configurationConfig schema, env, deps
eventsPSR-14 domain events
testingTest strategy + acceptance criteria

The extraction from the Univeros module, with every decision taken, is in docs/extraction/. Agent-oriented orientation is in AGENT.md.


Terminal window
composer install
composer qa # phpcs, phpstan, forbidden-import check, phpunit

Without DB_CONNECTION the database tests run on in-memory SQLite (the whole suite in about a minute); CI runs them against PostgreSQL. The functional suite replays 184 recorded 1.0 fixtures through the PSR-15 pipeline, so every response is contract-frozen.


Issues and pull requests are welcome on github.com/univeros/polaris-core. Please read AGENT.md first, follow the conventions (strict types, immutability, small files, tests-first), and run composer qa before opening a PR.


MIT, see LICENSE. Polaris for PHP is created and maintained by 2am.tech.