Identity with a clear lifetime.
Password authentication, revocable database sessions and explicit role/resource authorization.
Provision an account
There is no default account, public registration or admin console. Apply migrations, then create an account from a private password file or secret-manager pipe. Passwords require at least 12 characters, are limited to 1,024 bytes, and are hashed with Argon2id. The CLI prints an account ID and email, never the password.
php bin/icom migrate
php bin/icom user:create owner@example.com admin --password-stdin < /private/password.txt
php bin/icom user:roles ACCOUNT_ID member,editor
php bin/icom user:disable ACCOUNT_ID
php bin/icom sessions:pruneBrowser login flow
Use same-origin HTTPS requests. First obtain a CSRF token/cookie, then POST email/password to /auth/login. An unenrolled account receives the HttpOnly, Secure, SameSite=Strict __Host-icom_session cookie. An MFA account receives HTTP 202 and must finish /auth/challenge before any session is issued. GET /auth/me requires it; POST /auth/logout revokes it and expires the cookie. Both POST routes require CSRF protection. The account login form is available at /account/login.
// Run in your same-origin application code, with form-provided credentials.
const csrf = await fetch('/api/csrf').then(r => r.json());
const response = await fetch('/auth/login', {
method: 'POST',
headers: {'Content-Type': 'application/json', 'X-CSRF-Token': csrf.token},
body: JSON.stringify({email, password})
});
if (response.status === 202) {
// Show your factor form, then POST challenge + code to /auth/challenge.
throw new Error('Complete the MFA challenge before loading the profile');
}
if (!response.ok) throw new Error('Login failed');
const {user} = await fetch('/auth/me').then(r => r.json());Protect a route and its resource
Authentication identifies a user. Gate authorizes an ability using config/permissions.php and an optional policy registered at bootstrap. Passing a resource requires an explicit policy even for an admin wildcard role. Keep tenant/ownership checks in that policy, and pass every protected resource to authorize(). Middleware classes resolve only when reached, so injected request data reflects earlier middleware.
use Icom\Auth\{Gate, Identity};
use Icom\Middleware\Authenticate;
$app->container->get(Gate::class)->define('notes.update',
static fn (Identity $user, array $note): bool =>
$note['owner_id'] === $user->id
);
$app->get('/private/profile', function (Identity $user, Gate $gate) {
$gate->authorize($user, 'profile.read');
return \Icom\Http\Response::json(['user' => $user]);
})->middleware(Authenticate::class);
// Before a note write: $gate->authorize($user, 'notes.update', $note);Session and login controls
Only SHA-256 hashes of random 256-bit session tokens are stored. Login rotates a presented session, with a 12-hour absolute lifetime and 30-minute idle lifetime by default. Disable/password change revokes all sessions; role changes are loaded on the next authenticated request. Login performs a password-hash check for unknown accounts and returns a generic failure. Limits are 10 attempts per connected IP and 20 per normalized account per 5 minutes. Forwarded IP headers are ignored, so deployments behind a proxy must plan an explicit trusted-proxy/gateway policy.
What remains application work
TOTP MFA, one-use recovery codes, administrator-mediated password reset, explicit tenant policies and local audit records are now included; see the MFA and Tenants chapters. Account screens and an opt-in OIDC client are included in 0.4. Real provider onboarding, email verification/delivery, SAML, external audit retention and automatic query scoping remain application work. Schedule sessions:prune and supervise queue workers. Do not treat this password module as a complete enterprise identity product.