Skip to content

Accept a username as a password-login identifier - #76

Merged
imanimanyara merged 3 commits into
mainfrom
feat/username-password-login
Nov 13, 2026
Merged

imanimanyara merged 3 commits into
mainfrom
feat/username-password-login

Conversation

@imanimanyara

Copy link
Copy Markdown
Member

Summary

Password login read the email input and looked the account up on the email column, so an
account that signs in with a username could not sign in at all. This names the columns instead.

// config/laranail/authkit.php — the shipped default
'login' => [
    'field'       => 'email',
    'identifiers' => ['email'],
],

Add 'username' to identifiers to accept either. field is the request input the identifier
arrives in; it stays email so existing forms and API clients are untouched.

The open design decision, closed

The issue asks whether username login should be an independently enabled feature or a configurable
email-or-username identifier. It is a configurable identifier.

A separate feature flag means a second login path — its own action, request and controller — kept
in step with the first for ever, and an abstraction whose every branch has exactly one
implementation. One path reading an ordered list is the plain version, and email-only remains the
default either way.

Three things worth reviewing

Validation is derived from the identifier list, not configured beside it. With email alone the
identifier is still validated as an address — byte-identical to before. Any wider set drops that
rule, because a username is not an address and validating one as an address rejects every username
before the lookup runs. Deriving it means a rule cannot describe a mode the lookup does not
implement.

The lookup is one single-column query per identifier, not one or query. EloquentUserProvider
would accept a closure credential expressing the or, but DatabaseUserProvider and any provider
an application has substituted need not — and going through the provider at all is pointless if the
call only works for one of them.

The password is checked per candidate, not only against the first row found. Nothing stops one
account's username from equalling another's address; checking each candidate keeps the username's
owner able to sign in, and makes a failed attempt cost the same number of hash comparisons whichever
column the identifier belongs to.

Who owns the column

The application. AuthKit resolves whatever user model is configured and does not own the users
table, so it ships no migration for one — adding username, its uniqueness constraint and its
registration validation is the application's job. That is the question the issue asks to be
documented, and it is now in docs/login.md.

The test harness adds the column in workbench/, not in a package migration, which makes that claim
executable: if the package ever started depending on a column it does not create, the suite would be
the only place that column existed.

CheckEmailExists is deliberately not widened — answering it for usernames would add a
username-enumeration surface nothing here asks for.

Verification

Suite 91 passed, 195 assertions (10 new)
laranail-pint --test passed
Coverage asked for by the issue email login, username login, unknown identifier, throttling — all present

Proven red before being relied on, three separate ways:

Break Result
Hardcode the lookup back to the email column 2 failed
Always validate the identifier as an address 1 failed
Widen the shipped default to ['email', 'username'] 3 failed
Restore 10 passed

Nothing renames. AttemptEmailPasswordLogin, its request, its interface, its abstract controller
and FindUserByEmail keep their names and behaviour; only the identifier set widens, and only when
configured. Renaming five public classes is a separate decision and would make this diff
unreviewable.

Closes #65.

Password login read the `email` input and looked the account up on the `email`
column, so an account that signs in with a username could not sign in at all.

Name the columns instead. `laranail.authkit.login.identifiers` lists the user
columns an identifier is matched against, in order, and `login.field` names the
request input it arrives in. Both default to email, so a form or API client
written before this keeps working untouched.

Validation is derived from that same list rather than configured beside it: with
email alone the identifier is still validated as an address, and any wider set
drops that rule, because a username is not an address and validating one as an
address rejects every username before the lookup runs.

The lookup is one single-column query per identifier rather than one `or` query.
EloquentUserProvider would take a closure credential expressing the `or`, but
DatabaseUserProvider and any provider an application has substituted need not --
and going through the provider is pointless if the call only works for one of
them. The password is checked per candidate, so a username that happens to equal
another account's address does not lock its owner out.
Ten cases: the four the issue asks for -- email login, username login, an
identifier matching no account, and throttling -- plus the wrong-password case,
the derived validation rules in both modes, and the collision where one
account's username equals another's address.

Two of them exist to pin the default rather than the feature: email is the only
identifier shipped, and a username is refused until one is configured. Widening
the default fails them.

The `username` column lives in the workbench, not in a package migration, which
is the claim the docs make made executable: AuthKit does not own the users table,
so if it ever started depending on a column it does not create, the test harness
would be the only place that column existed.

Proven red three ways before being relied on: hardcoding the lookup back to the
email column fails 2, always validating the identifier as an address fails 1, and
widening the shipped default fails 3.
Records the answer the issue asks for: the username column, its uniqueness
constraint and its registration validation belong to the application, because
AuthKit resolves whatever user model is configured and does not own the users
table.

Also states the two things a reader would otherwise have to infer -- that
identifier spaces may overlap and which account wins, and that CheckEmailExists
is deliberately not widened, since answering it for usernames would add an
enumeration surface nothing asks for.
Copilot AI balanced review requested due to automatic review settings November 13, 2026 12:27

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

@imanimanyara
imanimanyara merged commit 0b2fd31 into main Nov 13, 2026
7 checks passed
@imanimanyara
imanimanyara deleted the feat/username-password-login branch November 13, 2026 12:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support username-based password login

2 participants