API / Minn\Auth

Minn\Auth

Passwords, sessions, cookies, nonces, roles and capabilities. 19 classes, 105 public methods and 21 internals. This page is the public surface; each class name opens the whole class.

ApplicationPasswords final readonly class

public/minn/src/Minn/Auth/ApplicationPasswords.php · 153 lines · Open the class →

Application passwords as the reference stores them: a serialized list in the user's _application_passwords meta, each entry uuid, app_id, name, password (a hash), created, last_used, last_ip. New passwords are 24 characters shown in groups of four; the stored hash is the reference's own ($generic$, FastHash), so a password made on either stack works on the other. Older hashes still verify, as the reference verifies them: phpass (WordPress before 6.8, and the engine before this) and $wp$.

constMETA = '_application_passwords'
new__construct(Minn\Content\Users $users)
methodall(int $userId): array

Every application password of a user, as stored records without the hash's plain text.

@return list<array<string, mixed>>
methodfind(int $userId, string $uuid): ?array

One record by uuid, or null.

@return array<string, mixed>|null
methodcreate(int $userId, string $name, string $appId): array

Mints a password, stores its hash, and returns the record with the plain text once.

@return array{0: array<string, mixed>, 1: string} the stored record and the one-time plaintext
methodrename(int $userId, string $uuid, string $name): ?array

Renames one password; null when the uuid is unknown.

methodtouch(int $userId, string $uuid, string $ip): ?array

Records a use: the time and the address it came from.

methoddelete(int $userId, string $uuid): ?array

Removes one password and returns its record; null when unknown.

methoddeleteAll(int $userId): int

Removes every application password of a user and returns how many.

methodverify(int $userId, string $password): ?array

The record a plaintext (spaces ignored) unlocks for the user, or null.

staticgenerate(): string

Twenty-four letters and digits.

staticchunked(string $plain): string

The plaintext as shown once: groups of four, space separated.

insidechange(), save(), uuid()

AuthCookies final readonly class

public/minn/src/Minn/Auth/AuthCookies.php · 107 lines · Open the class →

The three cookies a sign-in sets: the auth cookie on the admin and plugins paths (the secure variant over HTTPS, keyed off that scheme's salt) and the logged_in cookie on the site root, which is the one REST reads. Every value shares the username|expiration|token|hmac shape.

new__construct(Minn\Db $db, Minn\Auth\Cookie $cookie)
methodhash(): string

The hash suffixed onto every cookie name: md5 of the siteurl option.

methodsettle(Minn\Content\UserRecord $user, int $days, bool $secure, bool $https, string $token = ''): array

The sign-in cookies as the reference settles them (suite login-hooks): their life through auth_cookie_expiration (two days, or fourteen when remembered, the cookies then kept twelve hours past it); the auth cookie's security through secure_auth_cookie, and the logged-in cookie's through secure_logged_in_cookie (secure only over HTTPS); a session made for that life when none is given; then set_auth_cookie and set_logged_in_cookie told the values, and send_auth_cookies asked whether to send them. Signing in always runs under the booted runtime (its sessions are WordPress's), so the hooks always run.

@return array{expiration: int, expire: int, secure: bool, secureLoggedIn: bool, scheme: string, token: string, auth: string, loggedIn: string, send: bool}
methodattach(Minn\Http\Response $response, array $plan): Minn\Http\Response

The response with the settled cookies on it: the auth cookie on the admin and plugins paths, the logged-in cookie on the site root; none when send_auth_cookies said not to.

@param array{expire: int, secure: bool, secureLoggedIn: bool, auth: string, loggedIn: string, send: bool} $plan from settle()
methodclearing(): bool

Whether signing out clears the cookies: clear_auth_cookie told, then send_auth_cookies asked with no cookie in hand, as the reference asks them (suite login-hooks).

methodclear(Minn\Http\Response $response): Minn\Http\Response

The response with every sign-in cookie expired.

staticmint(Minn\Content\UserRecord $user, int $expiration, string $token, string $scheme): string

A cookie value under any scheme's salt (auth, secure_auth, logged_in).

AuthFailure final readonly class

public/minn/src/Minn/Auth/AuthFailure.php · 26 lines · Open the class →

Why a request is not authenticated, as the reference's error code: a missing or invalid cookie, or a good one sent without a nonce, is rest_not_logged_in (the request is simply anonymous); a good cookie with a nonce that does not verify is rest_cookie_invalid_nonce. The reason tells a good cookie sent without its nonce apart: that request is nobody, whoever plugin code then names.

constCOOKIE_WITHOUT_NONCE = 'a sign-in cookie without its nonce'
new__construct(string $code, string $reason = '')
propreadonly string $code
propreadonly string $reason
staticcookieWithoutNonce(): self

A good sign-in cookie sent without the nonce that would let it vouch for its user.

staticnotLoggedIn(): self

No usable session.

staticinvalidNonce(): self

A session whose nonce did not verify.

Authenticated final readonly class

public/minn/src/Minn/Auth/Authenticated.php · 16 lines · Open the class →

A validated session: the user row and the raw session token behind it.

new__construct(Minn\Content\UserRecord $user, string $token, ?array $applicationPassword = NULL)
@param array<string, mixed>|null $applicationPassword the record that authenticated this call, when Basic auth did
propreadonly Minn\Content\UserRecord $user
propreadonly string $token
propreadonly ?array $applicationPassword
methodid(): int

The signed-in user's id.

Authenticator final readonly class

public/minn/src/Minn/Auth/Authenticator.php · 149 lines · Open the class →

Resolves the current user two ways. A page load carries the cookie alone; a REST call must also carry a nonce bound to the same session.

new__construct(Minn\Auth\Cookie $cookie, Minn\Content\Users $users)
staticfromDb(Minn\Db $db): self

An authenticator over the shared database door.

methodsession(array $cookies): Minn\Auth\Authenticated|Minn\Auth\AuthFailure

The session the cookie jar carries, or why there is none.

@param array<string, string> $cookies
methodrest(array $cookies, ?string $nonce): Minn\Auth\Authenticated|Minn\Auth\AuthFailure

The session for a REST call: the cookie, then the nonce. A cookie sent without a nonce is no sign-in at all (the request goes on signed out, as the reference's cookie check makes it); one with a nonce that does not verify is refused (suite auth, checked against the reference).

@param array<string, string> $cookies
methodrestFromRequest(Minn\Http\Request $request): Minn\Auth\Authenticated|Minn\Auth\AuthFailure

A REST caller: the cookie session with its nonce first; failing that, HTTP Basic credentials carrying an application password, which need no nonce. A wrong Basic pair is reported as not logged in.

methodapplicationPassword(Minn\Http\Request $request): ?Minn\Auth\Authenticated

The user an Authorization: Basic header's application password unlocks, with the use recorded.

staticbasicCredentials(Minn\Http\Request $request): ?array

The login and password an Authorization: Basic header carries, or null.

@return array{0: string, 1: string}|null
staticapplicationPasswordsAvailable(Minn\Http\Request $request): bool

Application passwords need HTTPS, unless plugin code says otherwise through the reference's filter.

staticapplicationPasswordsAvailableFor(Minn\Content\UserRecord $user): bool

Whether Basic auth may sign this user in; the runtime's filter has the last word.

@param array<string, mixed> $user
methodlogin(string $username, string $password): ?Minn\Content\UserRecord

Username and password to a user row, an outdated hash replaced as the sign-in succeeds; no session is created here.

Capabilities final readonly class

public/minn/src/Minn/Auth/Capabilities.php · 277 lines · Open the class →

The capability engine: a user's roles from {prefix}capabilities usermeta, what those roles and the user's own capabilities grant, and the meta-capability mapping for edit_post, delete_post, and read_post.

new__construct(Minn\Db $db, Minn\Content\Users $users, Minn\Auth\Roles $roles)
staticfromDb(Minn\Db $db): self

The capability engine over the shared database door.

methodroles(): Minn\Auth\Roles

The role definitions.

methodcapsOf(int $userId): array

The capabilities stored on the user, as stored: role names and the capabilities granted (or taken away) one by one.

@return array<string, mixed>
methodrolesOf(int $userId): array

The role slugs a user holds: each stored capability that names a registered role, whatever it is set to (probe: the reference counts 'editor' => false as a role). One a plugin granted but never registered does not count.

@return list<string> role slugs
methodallcapsOf(int $userId): array

Everything a user holds, as the reference's allcaps: each role's capabilities in turn, then the user's own over them, role names included, so a capability taken away one by one stays away and current_user_can('administrator') is true for an administrator. Values are as stored.

@return array<string, mixed>
methodcan(int $userId, string $capability, ?int $postId = NULL): bool

Does the user hold every primitive the (possibly meta) capability maps to?

methodmap(string $capability, int $userId, ?int $postId = NULL): array

The primitives a capability requires, all of which must be held.

@return list<string>
insidefileModsAllowed(), mapMore(), mapCommentCapability(), mapPostCapability(), mapTermCapability(), fold(), trashedFrom()

FastHash final class

public/minn/src/Minn/Auth/FastHash.php · 23 lines · Open the class →

The reference's hash for high-entropy secrets ("$generic$", WordPress 6.8 and later): a 30-byte keyed BLAKE2b digest (sodium's generic hash, keyed by the scheme's fixed label) in URL-safe base64 without padding. Reset keys and application passwords are stored this way, so a secret minted on either stack verifies on the other. Pinned against hashes the reference made (tests/unit/auth-hashes.php). Without the sodium extension nothing can be hashed or verified this way.

constPREFIX = '$generic$'
staticavailable(): bool

Whether this PHP can make and check these hashes (the sodium extension).

statichash(string $secret): string

The "$generic$" hash of a secret.

staticverify(string $secret, string $hash): bool

Whether a secret matches a "$generic$" hash; false for any other kind of hash.

LoginThrottle final readonly class

public/minn/src/Minn/Auth/LoginThrottle.php · 82 lines · Open the class →

Failed sign-ins per address, so a password guesser meets a wall: twenty failures in fifteen minutes and the address waits out the window. The counters live in the options table (autoload off), one row per address, as "window start:count"; a successful sign-in clears the row. An IPv6 address counts as its /64, the block one subscriber is handed, so stepping through it buys no fresh tries; each failure is counted in one statement, so failures arriving together are all counted.

constLIMIT = 20
constWINDOW = 900
new__construct(Minn\Db $db)
methodretryAfter(string $address, ?int $now = NULL): ?int

Seconds the address must wait, or null when it may try.

methodrecordFailure(string $address, ?int $now = NULL): void

Counts one failed sign-in from an address within the current window.

methodclear(string $address): void

Forgets an address's failures.

staticsweep(Minn\Db $db, int $now): int

Drops counters whose window has passed, at most a thousand in one statement, however many addresses have failed; the number dropped.

insideread(), key()

Nonce final class

public/minn/src/Minn/Auth/Nonce.php · 46 lines · Open the class →

The wp_rest nonce: ten characters of HMAC-md5(tick|wp_rest|uid|token) under the nonce salt, accepted for the current tick and the one before.

staticlifetime(string|int $action): int

How long a nonce for an action lives, in seconds (nonce_life, handed the action as the caller gave it; probe filter-inputs-3). A plugin's filter reaches every nonce the engine makes and checks once plugins have loaded; a lifetime under a second keeps the default.

statictick(string|int $action = -1): float

The current half-life tick a nonce for an action is bound to.

staticcreate(int $userId, string $token, string|int $action = 'wp_rest'): string

A wp_rest nonce for a user and session token at the current tick.

staticverify(string $nonce, int $userId, string $token, string $action = 'wp_rest'): bool

Whether a nonce is valid at this tick or the one before.

staticat(float $tick, int $userId, string $token, string $action): string

The nonce for a given tick.

Password final class

public/minn/src/Minn/Auth/Password.php · 71 lines · Open the class →

The stored password scheme. A modern "$wp$2y$..." value is bcrypt over base64(HMAC-sha384(password, "wp-sha384")) at PHP's default cost (10 up to PHP 8.3, 12 from 8.4) unless the site's options say otherwise; under another algorithm (argon2id) the hash is that algorithm's own. Legacy phpass "$P$" hashes and any other password_hash() value still verify, and a sign-in replaces whatever needsRehash() says is outdated (probe password-rehash).

staticverify(string $password, string $hash): bool

Whether a password matches a stored hash: the "$wp$" scheme, legacy phpass, or any hash password_hash() makes. A password over 4,096 characters never matches and costs no hashing.

statichash(string $password, string $algorithm = Minn\Auth\PASSWORD_BCRYPT, array $options = array ( )): string

A stored hash: bcrypt in the "$wp$" scheme over the pre-hash, or, under any other algorithm, that algorithm's own hash of the password.

@param array<string, mixed> $options password_hash() options (cost, ...); PHP's defaults when empty
staticneedsRehash(string $hash, string $algorithm = Minn\Auth\PASSWORD_BCRYPT, array $options = array ( )): bool

Whether a stored hash is not the one these settings would make: under bcrypt, anything outside the "$wp$" scheme or at another cost; under another algorithm, whatever PHP says is outdated for it.

@param array<string, mixed> $options
staticfragment(string $hash): string

The four characters of the hash the cookie key is derived from: offset 8 for legacy phpass ("$P$") and plain bcrypt ("$2y$"), the LAST four for anything else ("$wp$", argon2, whose offset 8 is the same for every password). Getting this wrong yields a cookie that verifies nowhere while every other check passes, so the branches are pinned by the auth suite and the 2026-10-13 audit's probe.

insideprehash()

PasswordReset final readonly class

public/minn/src/Minn/Auth/PasswordReset.php · 78 lines · Open the class →

Password reset keys in the reference's storage shape: user_activation_key holds "time:hash", the key itself travels in the email link and is good for a day. The hash is the reference's own ($generic$, FastHash), so a link either stack sent works on the other. Older shapes still verify: a phpass hash (WordPress before 6.8) and the engine's former $minn$ HMAC. A matching key stored without its time (or, older still, stored as itself) is expired, as the reference treats it.

constLIFETIME = 86400
new__construct(Minn\Content\Users $users)
methodissue(Minn\Content\UserRecord $user): string

Mints a key, stores its hash, returns the key for the link.

staticmint(): array

A new key and the value that stores it (the time it was issued and its hash), for a caller that saves it itself.

@return array{0: string, 1: string}
methodverify(Minn\Content\UserRecord $user, string $key): bool

True when the key matches the stored hash and has not expired: a day, or what password_reset_expiration makes it.

methodstatus(Minn\Content\UserRecord $user, string $key, int $lifetime = self::LIFETIME): string

"valid", "expired" (a matching key past its lifetime, a day unless given), "unstamped" (a matching key stored without a time, which the reference counts expired after asking password_reset_key_expired), or "invalid".

methodclear(Minn\Content\UserRecord $user): void

Forgets a user's reset key.

insidehash(), matches()

PortableHash final class

public/minn/src/Minn/Auth/PortableHash.php · 86 lines · Open the class →

The portable phpass hash ("$P$"), from the published algorithm: an iteration count character, an eight character salt, and MD5 iterated over salt and password, encoded in phpass's own base64 alphabet. The reference stores it in the post-password cookie (2^13 iterations, and it accepts nothing weaker there), and still verifies it for application passwords and reset keys made before its fast hash, which is why both directions of each are accepted. Its pieces are public for phpass's own class, PasswordHash (probe identity-internals): the base64 encoding, both kinds of salt, and the hash with phpass's failure markers.

constALPHABET = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
constMAX_LENGTH = 4096

The longest password hashed; a longer one is refused before any work, as the reference refuses it.

statichash(string $password, int $countLog2 = 13): string

A portable hash of a password, 2^$countLog2 iterations (the reference's cookies carry 2^13); "*" for one too long.

staticsalt(string $input, int $countLog2): string

A portable setting: "$P$", the count's character, and six bytes of salt encoded.

staticblowfishSalt(string $input, int $countLog2): string

A bcrypt setting: "$2a$", the two-digit cost, and the first sixteen bytes in bcrypt's base64.

staticverify(string $password, string $hash): bool

Whether a password matches a portable hash from the published algorithm.

staticcrypt(string $password, string $setting): string

The hash for a setting; for a setting it cannot use ("$P$" or "$H$", a count from 7 to 30, eight characters of salt) phpass's marker "*0", or "*1" when the setting is itself "*0".

staticencode(string $input, int $count): string

phpass's base64: little-endian groups of three bytes, $count bytes of the input.

RegisteredCaps final class

public/minn/src/Minn/Auth/RegisteredCaps.php · 24 lines · Open the class →

The capability names a plugin's post type or taxonomy registered, read from the runtime's registry once plugins are loaded (probe rest-plugin-caps): a type's cap object (create_posts, edit_posts, publish_posts...) and whether it maps meta capabilities, a taxonomy's term capabilities. A built-in type's too (patterns, menu items, templates), all but posts and pages, whose families the engine knows; null for those and when no runtime is up.

staticofType(string $type): ?WP_Post_Type

A post type's registered object (any but post and page), or null.

staticofTaxonomy(string $taxonomy): ?WP_Taxonomy

A plugin's taxonomy, or null.

Roles final class

public/minn/src/Minn/Auth/Roles.php · 68 lines · Open the class →

Role definitions from the site's {prefix}user_roles option, parsed by a bounded scan of the serialized blob, with the seeded contract copy as the fallback for a fresh install.

new__construct(Minn\Db $db)
staticlevel(string $role): int

The wp_user_level stored beside the capabilities meta.

staticserializeSingle(string $role): string

The {role: true} capabilities meta in its serialized form.

methodforget(): void

Drops the parsed map so the next read sees a rewritten option.

methodall(): array

Every role with its name and capabilities, from the option or the shipped defaults.

staticparse(?string $blob): ?array

Each role is a string key over a two-entry array of "name" and "capabilities"; the capabilities map is {cap: bool}. Null when the blob has another shape, so the caller can fall back.

Salts final class

public/minn/src/Minn/Auth/Salts.php · 41 lines · Open the class →

The site's own secret material, read from the constants wp-config.php defines, so the engine keys off exactly what the install used.

staticconfigured(): bool

True when every key and salt is defined and none is the installer's placeholder.

staticknows(string $scheme): bool

Whether a scheme has a key and salt of its own (auth, secure_auth, logged_in, nonce).

staticfor(string $scheme): string

The key and salt of a scheme joined, as the reference feeds them to its HMACs.

statichash(string $data, string $scheme): string

HMAC-md5 under the scheme's salt: the primitive every cookie and nonce is built from.

Sessions final readonly class

public/minn/src/Minn/Auth/Sessions.php · 173 lines · Open the class →

The session_tokens usermeta store: {sha256(token): {expiration, ip, ua, login}}. Read by a bounded scan of the serialized blob and written by serializing it ourselves, so stored data is never executed.

new__construct(Minn\Content\Users $users)
staticgenerateToken(): string

A 43-character token; only sha256(token) is ever stored or compared.

methodisLive(int $userId, string $token): bool

Whether a session token is still stored and unexpired for a user.

methodcreate(int $userId, int $expiration, string $ip, string $userAgent): string

Creates a session, pruning expired ones, and returns the raw token.

staticplain(string $value): string

A request-supplied field (the address, the user agent) as the store keeps it: the store is read back by shape, so it carries nothing that looks like the shape, and no more than 255 characters.

methoddestroyAll(int $userId): void

Ends every session of the user (a password change).

methoddestroy(int $userId, string $token): bool

Removes one session; true when it existed.

methoddestroyKey(int $userId, string $key): bool

Removes the session stored under a key (the sha256 of its token); true when it existed.

methoddestroyOthers(int $userId, string $keepKey): void

Ends every session of the user except the one stored under the given key.

methodread(int $userId): array

Every stored session of a user, keyed by token hash. Each entry keeps its stored text under raw, so writing the store back leaves sessions this request did not make exactly as they were, including anything a plugin attached to them.

@return array<string, array{expiration: int, ip: string, ua: string, login: int, raw: string}>
staticserialize(array $sessions): string

The map in PHP's serialized form: stored entries as they were read, new ones key by key in the order given.

insideparseEntry(), prune(), write(), entry(), string()

SignIn final readonly class

public/minn/src/Minn/Auth/SignIn.php · 65 lines · Open the class →

The door itself: what a sign-in surface needs beyond checking a password. Sessions are minted here, the three cookies ride out on the response, and the failure counter behind the throttle is kept in one place with the session store and the cookie jar it protects.

new__construct(Minn\Auth\AuthCookies $cookies, Minn\Auth\LoginThrottle $throttle)
methodretryAfter(string $address): ?int

Seconds this address must wait before another attempt, or null when it may try now.

methodrecordFailure(string $address): void

Counts one failed attempt from an address.

methodhash(): string

The cookie-name hash the sign-in cookies, the postpass cookie, and the reset cookie share.

methodestablish(Minn\Http\Response $response, Minn\Content\UserRecord $user, Minn\Http\Request $request): Minn\Http\Response

Signs a user in for two days: a new session, and the response with the cookies attached for the browser session.

methodremember(Minn\Http\Response $response, Minn\Content\UserRecord $user, Minn\Http\Request $request): Minn\Http\Response

Signs a user in for fourteen days, the cookies kept past the browser session: "remember me", and the one-time login link.

methodend(Minn\Http\Response $response, Minn\Auth\Authenticated $session): Minn\Http\Response

Ends one session and clears the cookies from the response, unless send_auth_cookies says not to (AuthCookies::clearing).

methodendAll(Minn\Http\Response $response, int $userId): Minn\Http\Response

Ends every session of a user (a password reset) and clears the cookies from the response.

methodclear(Minn\Http\Response $response): Minn\Http\Response

The response with the sign-in cookies cleared.

insideopen()

TypeCapabilities final readonly class

public/minn/src/Minn/Auth/TypeCapabilities.php · 51 lines · Open the class →

The capability names a post type's permissions are built from. Posts and pages own their families (edit_posts, edit_others_pages, ...). Navigation menus fold every one of them onto edit_theme_options, which is why an editor may read a menu and only an administrator may change one; the reference's own registration says the same thing. A plugin's type answers with the names it registered.

staticof(string $type, string $capability): string

A cap name built from the post/page family, folded when the type folds, or the plugin type's own name for it (probe rest-plugin-caps).

staticcreate(string $type): string

The capability creating a post of the type needs: the edit capability, or a plugin type's create_posts.

staticplural(string $type): string

The capability stem of a type: pages for page, posts for everything else.

staticedit(string $type): string

The edit capability of a type.

staticeditOthers(string $type): string

The edit-others capability of a type.

staticpublish(string $type): string

The publish capability of a type.

staticreadPrivate(string $type): string

The read-private capability of a type.