Add remember-me to sign-in - #55
Merged
Merged
Conversation
Closes the promise two comments have carried since Plan 4 - the hard-coded
isPersistent: false at AuthEndpoints.cs:246 and the cookie-config comment
in Program.cs. Pulled out of Plan 6 as its own PR so that plan carries two
features instead of three.
One request field, one argument, one checkbox, two assertions, which is
what the Plan 6 design predicted it would be.
LoginRequest gains RememberMe with a default of false, so a client that
omits it - every caller written before this - still gets a session cookie.
Opting in has to be explicit, and one of the two tests omits the field
rather than sending false, because the default is the part that could
regress silently and change what every existing login issues.
What isPersistent changes is only whether the cookie is written with an
Expires date and so survives closing the browser. It does not change how
long the ticket is valid: ExpireTimeSpan bounds both at seven sliding
days. A longer window for remembered sessions specifically would need
per-sign-in AuthenticationProperties.ExpiresUtc, which is a duration
decision rather than a wiring one, so it is not invented here.
The checkbox is a wrapping label, so the words are part of the target: the
box is 1.5rem like the checklist rows, the row keeps the 2.75rem floor.
Measured on the running app at both 375 and 1280: row 44px, box 24x24,
implicit label resolves, tab order is email, password, remember, submit.
The choice is re-rendered from model state. Without that, a mistyped
password would silently drop a box the user had already ticked - the form
is rebuilt from state on every failure.
Verified through the browser as well as the suite: with the box ticked the
model posts {"rememberMe":true}, the generic 401 still renders, and the
box is still ticked afterwards. 255/255 green; the persistent-cookie
assertion was verified red with isPersistent reverted to false, while the
session-cookie one correctly stayed green.
No human pointer has touched the screen - the browser pane's synthetic
clicks do not dispatch, so this is scripted interaction and measurement.
Member
Author
|
CI note: When #54 merges, GitHub retargets this PR to |
Both from Malin's first real click-through of the auth screens, 2026-08-21. Reveal: login, register and reset each get a Show/Hide button after the password field. Register and reset ask for a password of at least twelve characters and login is where a typo costs one of five lockout attempts, so not being able to see what you typed is worst exactly where the stakes are highest. The button sits after the input in the flow rather than floating inside it. Floating is the common pattern and it costs two things: the control either shrinks below 44x44 or overlaps the text a password manager filled in. In the flow it keeps its own target - measured 74 x 44 - and the field still clears the 44px floor at 228 x 51.59 on a 375px screen, with no horizontal overflow. Visibility is view state, not model state, and deliberately so: whether a password is on screen has no business surviving a re-render, and every re-render on these screens follows a failed submit. Verified: reveal, get the generic 401, and the rebuilt field comes back hidden with the button back to "Show". The state is announced through #status rather than left to aria-pressed. The button's accessible name changes with the state, and a name change on its own is not something screen readers reliably speak. Back to sign in: the register "Check your email" screen was a dead end - Malin hit it, and there was no way onwards but the browser's back button. Its sibling, verify's "Check your email" after a resend, had the same gap and gets the same link. 255/255 green - frontend only. Measured on the running app at 375x812: type flips both ways, the typed value survives the toggle, and #status announces "Password shown."/"Password hidden.". Not changed: the auth links are 17px inline text, this one included. That predates this branch and is the same on every auth screen, so it wants its own decision rather than a change smuggled in here.
Malin's call, 2026-08-21: keep them native text links rather than turning them into buttons. WCAG 2.5.8 (AA) asks for 24x24, and exempts a target that sits inline in a sentence because its size is set by the surrounding line-height. "No account yet? Create one." has that excuse. "Forgotten your password?" and the new "Back to sign in" do not - each is alone in its own paragraph - and at 14px they measured 17px high. inline-block plus a quarter-rem of block padding is the whole change: the link still looks and behaves like a link, and its box now measures 30.4px on the running app. Nothing about the type or colour moves.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes the promise two comments have carried since Plan 4: the hard-coded
isPersistent: falseinAuthEndpoints.csand the cookie-config comment inProgram.cs. Pulled out of the Plan 6 design as its own PR so that plan carries two features instead of three.One request field, one argument, one checkbox, two assertions — the size the design predicted.
Server
LoginRequestgainsRememberMewith a default of false, and the login handler passes it toPasswordSignInAsync. A client that omits the field — every caller written before this — still gets a session cookie. Opting in has to be explicit.isPersistentchanges only whether the cookie carries anExpiresdate and so survives closing the browser. It does not change how long the ticket is valid:ExpireTimeSpanbounds both at seven sliding days.A longer window for remembered sessions specifically would need per-sign-in
AuthenticationProperties.ExpiresUtc. That is a duration decision rather than a wiring one, so it is not invented here — say the word if you want remembered sessions to outlive seven idle days.Client
A wrapping
<label class="auth-remember">, so the words are part of the target — box 1.5rem like the checklist rows, row at the 2.75rem floor every other control in the form has.The choice is re-rendered from model state. Without that, a mistyped password would silently drop a box the user had already ticked, because the form is rebuilt from state on every failure.
Verification
dotnet test→ 255 passed, 0 failed (253 onfix/auth-input-class, +2 here).RealCookieAuthTests, the real-cookie suite — the test scheme cannot see aSet-Cookieat all, so a test-scheme version would pass while testing nothing.Remember_me_issues_a_cookie_that_survives_the_browserverified red withisPersistentreverted tofalse(1 failed / 1 passed) — the session-cookie assertion correctly stayed green.{"email":…,"password":…,"rememberMe":true}, the generic 401 still renders, and the box is still ticked afterwards.What to check
RememberMe = falsedefault — dropping it would change what every existing caller gets.Without_remember_me_the_cookie_dies_with_the_browseromits the field on purpose to guard exactly that.expires=inSet-Cookie, matched case-insensitively.ExpiresUtc.Not covered
No human pointer has touched the screen — the browser pane's synthetic clicks do not dispatch, so the above is scripted interaction plus measurement, and the pane would not composite for screenshots.