Skip to content

Add remember-me to sign-in - #55

Merged
malinfossum merged 7 commits into
mainfrom
feat/remember-me
Aug 25, 2026
Merged

Add remember-me to sign-in#55
malinfossum merged 7 commits into
mainfrom
feat/remember-me

Conversation

@malinfossum

Copy link
Copy Markdown
Member

Stacked on #54 — base branch is fix/auth-input-class, so this diff shows only remember-me. GitHub retargets it to main when #54 merges. Review #54 first; the order matches the Plan 6 design's Depends on.

Closes the promise two comments have carried since Plan 4: the hard-coded isPersistent: false in AuthEndpoints.cs and the cookie-config comment in Program.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

LoginRequest gains RememberMe with a default of false, and the login handler passes it to PasswordSignInAsync. A client that omits the field — every caller written before this — still gets a session cookie. Opting in has to be explicit.

isPersistent changes only whether the cookie carries 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. 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 test255 passed, 0 failed (253 on fix/auth-input-class, +2 here).
  • Both assertions live in RealCookieAuthTests, the real-cookie suite — the test scheme cannot see a Set-Cookie at all, so a test-scheme version would pass while testing nothing.
  • Remember_me_issues_a_cookie_that_survives_the_browser verified red with isPersistent reverted to false (1 failed / 1 passed) — the session-cookie assertion correctly stayed green.
  • Browser, on the running app: with the box ticked the model posts {"email":…,"password":…,"rememberMe":true}, the generic 401 still renders, and the box is still ticked afterwards.
  • Measured at 375×812 and 1280×900: row 44px tall (311px / 480px wide), box 24×24, implicit label resolves to "Keep me signed in", tab order email → password → remember → Sign in.

What to check

  • RememberMe = false default — dropping it would change what every existing caller gets. Without_remember_me_the_cookie_dies_with_the_browser omits the field on purpose to guard exactly that.
  • The two assertions are on expires= in Set-Cookie, matched case-insensitively.
  • Whether seven sliding days is the right ceiling for a remembered session, or whether remember-me should get its own longer ExpiresUtc.
  • Plan 6 inherits one assertion from here: change-password reissues the cookie and must preserve persistence. Nothing in this PR can guard that yet.

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.

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.
@malinfossum

Copy link
Copy Markdown
Member Author

CI note: ci.yml only fires on pull_request into main, and this PR's base is fix/auth-input-class, so no check is attached to it yet. Ran it manually on the branch instead — run 32455802349 is green on ubuntu + postgres:17-alpine.

When #54 merges, GitHub retargets this PR to main and the required Build & test check runs on its own.

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.
Base automatically changed from fix/auth-input-class to main August 25, 2026 10:37
@malinfossum
malinfossum merged commit 8b3ed72 into main Aug 25, 2026
1 check passed
@malinfossum
malinfossum deleted the feat/remember-me branch August 25, 2026 10:41
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.

2 participants