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.
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.
Packages
Section titled “Packages”| Package | Namespace | What it is |
|---|---|---|
polaris/core | Polaris\ | 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/psr15 | Polaris\Psr15\ | Router, middleware stack and request handler for any PSR-15 host (Slim, Mezzio, Laravel or Symfony through their PSR bridges). |
polaris/pdo | Polaris\Pdo\ | The database adapter for PostgreSQL, MySQL and SQLite, the DDL exporter and the schema inspector. |
polaris/testing | Polaris\Testing\ | The in-memory database adapter for tests of applications built on Polaris. |
polaris/cli | Polaris\Cli\ | bin/polaris: schema:export, schema:diff, manifest (JSON, OpenAPI 3.1), doctor. |
polaris/laravel | Polaris\Laravel\ | Laravel 13: service provider, the endpoints as routes, the polaris guard, mail bridge, polaris:* artisan commands. |
polaris/symfony | Polaris\Symfony\ | Symfony 7.4 / 8: bundle, the endpoints as routes, a firewall authenticator, mail bridge, polaris:* console commands. |
polaris/yii | Polaris\Yii\ | Yii 3: config plugin, the endpoints as routes, an authentication method, mail bridge, polaris:* console commands. |
polaris/audit | Polaris\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/admin | Polaris\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/messaging | Polaris\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/sentinel | Polaris\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/sso | Polaris\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/scim | Polaris\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/passwordless | Polaris\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/username | Polaris\Username\ | A plugin: usernames unique without regard to case, and sign-in with a username or the email through core’s password path. |
polaris/anonymous | Polaris\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-session | Polaris\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/social | Polaris\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/passkey | Polaris\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-keys | Polaris\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-provider | Polaris\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/mcp | Polaris\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/agents | Polaris\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/agent | Polaris\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-ts | The 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-ts | The 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).
Quick start
Section titled “Quick start”The fastest way to see everything is the Slim demo:
cd examples/slimcomposer installbin/setup # .env, RS256 keys, SQLite schema, permission catalogbin/walkthrough.sh # register → verify → login → TOTP → MFA login → organizationThe 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:
composer require polaris/laravelphp artisan polaris:install && php artisan migrate # config, tables, permission catalogthen 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).
Features
Section titled “Features”| Area | What you get |
|---|---|
| Authentication | Register, email verification, password login, /auth/me, logout / logout-all |
| Tokens | Asymmetric JWT access tokens (RS256/EdDSA) + opaque rotating refresh tokens with reuse detection; JWKS endpoint |
| Sessions | Per-device session list, individual + global revocation |
| MFA / OTP | TOTP (QR), SMS OTP, email OTP, recovery codes, login-MFA gate, step-up |
| Passwords | Argon2id, policy enforcement, breached-password hook, reset & change (logout-everywhere) |
| Multi-tenant RBAC | Organizations, memberships, roles, permissions, invitations, org switching |
| Authorization | Declarative per-endpoint permissions + a programmatic Gate |
| Security | Rate limiting, account lockout, anti-enumeration, audit log, key rotation |
| Ops | PSR-14 domain events, notification fan-out, transient-row pruning, metrics |
| Plugins | Packages 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) |
How it works
Section titled “How it works”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 outcomeDomain 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 containerLogin 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.
API surface
Section titled “API surface”A representative slice (full catalog in
docs/auth/api-reference.md; bin/polaris manifest --format=openapi
renders the OpenAPI 3.1 document):
| Method | Path | Purpose |
|---|---|---|
POST | /auth/register | Create account, send verification |
POST | /auth/login | Password login → tokens or MFA challenge |
POST | /auth/token/refresh | Rotate refresh → new access + refresh |
POST | /auth/mfa/totp/enroll | Start TOTP enrollment → secret + QR |
POST | /auth/mfa/verify | Complete MFA → tokens |
GET | /auth/sessions | List active devices/sessions |
POST | /orgs | Create an organization (creator → owner) |
POST | /orgs/{id}/invites | Invite a member |
POST | /auth/switch-org | Switch active org → re-scoped token |
Routes are relative; mount the handler under whatever prefix your application uses
(Pipeline takes a path prefix).
Multi-factor authentication
Section titled “Multi-factor authentication”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.
Multi-tenant RBAC
Section titled “Multi-tenant RBAC”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.
Security
Section titled “Security”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.
Database
Section titled “Database”The schema is data (Polaris\Schema), so there are no migrations to carry:
bin/polaris schema:export --target=sql:postgres > schema.sql # also sql:mysql, sql:sqlitebin/polaris schema:diff --dsn='pgsql:host=…;dbname=…' # what a live database is missingbin/polaris doctor --dsn=… # secrets, keys, manifest, connectivity, schemapolaris/pdo runs on PostgreSQL, MySQL and SQLite; the adapter conformance suite
runs on all of them plus the in-memory adapter.
Documentation
Section titled “Documentation”The specification lives in docs/auth/:
| Doc | Contents |
|---|---|
| README | Overview and goals |
| data-model | Models, tables, relationships |
| flows | Register, login, refresh rotation, sessions, password |
| mfa-otp | TOTP/QR, SMS, email, recovery, step-up |
| rbac | Orgs, memberships, roles, permissions, guard |
| api-reference | Full endpoint catalog + error format |
| security | Threat model, crypto, key management |
| configuration | Config schema, env, deps |
| events | PSR-14 domain events |
| testing | Test strategy + acceptance criteria |
The extraction from the Univeros module, with every decision taken, is in
docs/extraction/. Agent-oriented orientation is in
AGENT.md.
Testing
Section titled “Testing”composer installcomposer qa # phpcs, phpstan, forbidden-import check, phpunitWithout 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.
Contributing
Section titled “Contributing”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.
License
Section titled “License”MIT, see LICENSE. Polaris for PHP is created and maintained by 2am.tech.