Skip to content

fix(security): show a message when password reset answers are throttled - #2325

Merged
hirokiterashima merged 6 commits into
WISE-Community:developfrom
Isaries:fix/student-reset-throttle
Sep 1, 2026
Merged

fix(security): show a message when password reset answers are throttled#2325
hirokiterashima merged 6 commits into
WISE-Community:developfrom
Isaries:fix/student-reset-throttle

Conversation

@Isaries

@Isaries Isaries commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Summary

This pairs with the WISE-API change that throttles the student password reset flow after several incorrect security answers. The server now returns a throttling response code. The security answer step previously ignored any unrecognized response code, so a throttled user saw a blank error with no explanation.

Changes

  • Handle the throttling response code in both the security answer step and the password change step, telling the user to wait or to ask their teacher.
  • Add a unit test that mirrors the existing incorrect answer test.

Paired change

Paired with the WISE-API change (branch fix/student-reset-throttle) that adds the throttling.

@Isaries

Isaries commented Jul 6, 2026

Copy link
Copy Markdown
Contributor Author

Paired server change: WISE-Community/WISE-API#324

@Aaron-Detre Aaron-Detre left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looks good

@hirokiterashima hirokiterashima left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for adding this feature!

Functionality works as described, but I think we should mirror the view/flow that the teacher sees (see below). After too many failed attempts, the form controls are disabled, and we show them a link to the forgot teacher password page to start the flow again. This prevents immediate re-attempts and also adds another extra step/deterrent for the attacker.

Screenshot 2026-07-15 at 4 49 42 PM

@Isaries

Isaries commented Jul 17, 2026

Copy link
Copy Markdown
Contributor Author

The generate-messages check failure here is unrelated to this PR's changes (test-coverage and qlty check both pass).

Root cause: .github/workflows/generate-messages.yml fails for any PR opened from a fork, not just this one. Its actions/checkout@v6 steps use:

ref: ${{ github.head_ref || github.ref_name }}

without specifying repository:. For a cross-repo (fork) PR, github.head_ref is the branch name on the fork, but checkout defaults to looking for that branch in the base repo, which is why the job errors with:

A branch or tag with the name 'fix/student-reset-throttle' could not be found

Since develop-workflows.yml calls this reusable workflow pinned to @develop, it always runs the version currently on develop — so this can't be fixed from within this PR branch, and re-running won't help.

A few other issues in the same file worth fixing while at it:

  • The checkout step right after (no with: args) overwrites the ref specified above.
  • The step labeled "Use Node.js" uses actions/checkout@v6 instead of actions/setup-node@v4 — Node.js is never actually installed by that step.
  • Even with checkout fixed, the final git push uses the base repo's GITHUB_TOKEN, which has no write access to a fork branch, so the auto-commit-messages-back-to-PR flow won't work for fork PRs regardless.

Could a maintainer fix generate-messages.yml on develop (add repository: ${{ github.event.pull_request.head.repo.full_name }} for fork PRs, or skip the auto-commit step when github.event.pull_request.head.repo.fork == true), or override this check to merge?

For context: I checked the last 100 PRs on this repo and this is the only cross-repo (fork) one — everyone else pushes branches directly to this repo, so this code path has apparently never been exercised before.

@Isaries

Isaries commented Aug 31, 2026

Copy link
Copy Markdown
Contributor Author

Pushed one more commit addressing a defect in the previous one.

When I moved the message below the form to add the "Forgot Student Password" link, I dropped the @if (message) guard that used to wrap the paragraph. message is the empty string until something fails, so both cards rendered an empty <p class="warn"> with its own vertical margin on every visit, not just after a throttled attempt.

The guard is restored and each spec now asserts that nothing is drawn there on load. The assertion queries .warn rather than the paragraph itself, so it fails if anything empty is rendered in that position; I checked that it does fail against the unguarded template rather than assuming it would.

The generate-messages check will still fail for the reason described above, which is unrelated to these changes.

@hirokiterashima

Copy link
Copy Markdown
Member

Hi @Isaries, thanks for the latest fix commit.

We recently addressed the generate-messages issue in this PR: #2334. You should now see a different error when the GitHub actions run and fails on PRs from forked branches, as this one did here: https://github.com/WISE-Community/WISE-Client/actions/runs/33351721843/job/99673772548?pr=2325

Run echo "Error: src/messages.xlf is out of date. Run 'npm run extract-i18n' and commit the updated file."
Error: src/messages.xlf is out of date. Run 'npm run extract-i18n' and commit the updated file.
Error: Process completed with exit code 1.

Can you please try generating the new messages.xlf file and committing it? Thanks for your patience and understanding!

The server now temporarily blocks the student password reset flow after
several incorrect security answers to prevent brute forcing the answer.
The security answer step previously ignored any unrecognized response
code, so a throttled user saw a blank error and no explanation. Handle
the throttling response code in both the security answer and password
change steps so the user is told to wait or to ask their teacher.
…tled

Showing only a message let a throttled student keep submitting answers.
Mirror the teacher verification code flow: disable the form and show a link
back to the start of the flow, with the warning and the link below the form
where the teacher flow puts them.

Unlike the teacher flow there is no new verification code for a student to
generate, so the message tells them to wait and start again or to ask their
teacher rather than promising the link will unblock them.

The security answer step also had no default branch, so any response code it
did not recognise left the message undefined and the student saw nothing at
all. The server returns invalidUsername from that endpoint when the account
has gone away mid-flow, which reached exactly that dead end.

Both steps now share a base class holding the message state and the lockout,
so the two copies of the response text cannot drift apart.
Moving the message below the form dropped the @if (message) guard that used to
wrap it, and message is the empty string until something fails, so both cards
rendered an empty p.warn with its own vertical margin on every visit.

Restore the guard and assert its absence on load. The assertion queries .warn
rather than the paragraph itself, so it fails if anything empty is drawn there,
and it does fail against the unguarded template.
@Isaries
Isaries force-pushed the fix/student-reset-throttle branch from 5145297 to cdee49a Compare August 31, 2026 23:46
…the answer

The change step carries the security answer in query params, so the student
cannot correct it on that page. Mapping incorrectAnswer to the generic error
left them resubmitting the same rejected answer, and the server now counts each
submission against the limit that locks the reset for ten minutes, so they were
told only that an error occurred until they were locked out.

Announce both messages through a live region that is present before the message
arrives. The paragraph moved below the form to sit next to the link, and the
answer field is disabled while focus is still on it, so nothing was reaching
assistive technology.
@Isaries

Isaries commented Sep 1, 2026

Copy link
Copy Markdown
Contributor Author

Hi @hirokiterashima, thanks for fixing generate-messages, and for pointing me at the new error.

I have regenerated src/messages.xlf with npm run extract-i18n and committed it. In the meantime develop had moved ahead and the regenerated file conflicted, so I also rebased the branch onto develop (9c52a2dac). I resolved the conflict by taking the develop copy of messages.xlf and re-running the extraction on top of it, so the commit adds only the new strings from this PR and does not disturb anyone else's. Relative to develop the file differs by 28 lines that are not linenumber context churn.

While revisiting the branch I also found two things worth fixing, in the last commit:

  1. On the change step, incorrectAnswer was falling through to the generic "An error occurred. Please try again." That was harmless before, but the paired server change (fix(security): throttle failed student password reset answer attempts WISE-API#324) now records a failed attempt in exactly that branch, so each such response spends one of the five attempts and the fifth locks the reset for ten minutes. A student whose answer did not survive the navigation would see only a generic error five times and then be locked out over answers they were never told they were getting wrong. The change step now has its own case for it. Rather than reusing the "please try again" wording from the security step, it disables the form and shows the link back to the start of the flow, since the answer is carried in the query params and cannot be corrected on that page, so resubmitting could only send the same rejected answer again.

  2. The message paragraph now sits below the form, mirroring the teacher flow as you asked, and the answer field is disabled while focus is still on it. With no live region, a screen reader user got no announcement at all when the throttle fired. The paragraph is now wrapped in a <div role="alert"> that is always present, with the @if (message) guard kept on the paragraph itself, so the region exists before the message arrives and no empty paragraph is drawn. Please let me know if you would rather I left this out or handled it differently, since it is the first role="alert" in the codebase.

Both are covered by specs, and I checked that the new assertions fail against the code without these changes rather than assuming they would. ng test --include "src/app/forgot/student/**/*.spec.ts" passes 34 of 34 locally.

One thing I cannot do from my side: the workflow runs on this PR are waiting for maintainer approval, so test-coverage and generate-messages have not run since the push and I cannot confirm the messages.xlf check is green. Could you approve the run when you get a chance?

Also, the earlier review is still marked as changes requested. The form is now disabled and the link back to the Forgot Student Password page is shown after too many failed attempts, matching the teacher flow in your screenshot, so whenever you have time I would appreciate another look. Thanks again for your patience with this one.

@hirokiterashima
hirokiterashima dismissed their stale review September 1, 2026 02:41

My original request has been addressed. Still figuring out the issue with conflicting messages.xlf now.

@hirokiterashima

Copy link
Copy Markdown
Member

@Isaries can you try running npm run extract-i18n again? I ran it locally with your rebased branch and I see more changes to messages.xlf.

The committed file did not match what npm run extract-i18n produces on the
Linux runner, so the generate-messages check failed. Neither cause is
visible from a Windows checkout.

The extractor reports the end line of a multi-line i18n block one higher on
Windows than on Linux. Separately, the equiv-text attributes carry multi-line
source snippets whose CR characters are content rather than line endings, and
core.autocrlf strips them when the file is staged.

Regenerated in a node:24 container and staged with the filter disabled, so
the file is byte-identical to what the workflow regenerates. Most of the diff
is the same correction applied to strings this branch never touched.
@Isaries

Isaries commented Sep 1, 2026

Copy link
Copy Markdown
Contributor Author

Hi @hirokiterashima, thanks for looking again.

I ran npm run extract-i18n and the file did not change, which is what sent me digging. The short version is that I cannot produce the file the check wants from a Windows checkout, and develop's own copy does not match what the current toolchain regenerates either.

I reproduced the job locally in a node:24 container, running npm ci and npm run extract-i18n over git archive HEAD, and diffed the result against what I had committed. Two separate causes:

  1. The extractor reports the end line of a multi-line i18n block one higher on Windows than on Linux. Four linenumber values in the two templates this PR touches were affected. For example the reCAPTCHA paragraph in forgot-student-password-security.component.html has its content on lines 24 to 26 and its closing tag on line 27, and the Linux run correctly emits 24,27 where my Windows run emitted 24,28. I ruled out the Node version by running the same tree under node:22 and node:24, which produce byte-identical output.
  2. Several equiv-text attributes carry multi-line source snippets, so the file legitimately contains 31 CR characters that are content rather than line endings. With core.autocrlf=true the clean filter strips them on git add, so the blob I committed could never match a regeneration. Staging with the filter disabled fixes this.

The file in this PR is now byte-identical to what the workflow regenerates, so the check should pass.

One thing worth raising separately. Regenerating from a pristine develop in the same container produces a file that differs from the committed src/messages.xlf by 812 lines, in the same off-by-one pattern and with the same 31 CR characters missing. That means generate-messages fails on any fork PR regardless of its contents. On a non-fork PR the workflow commits the regenerated file back to the branch, so the discrepancy is absorbed silently and never surfaces.

The practical consequence for this PR is that src/messages.xlf now shows 874 changed lines, of which 812 are that pre-existing drift and only 64 belong to this branch. I am happy to reduce it to just my own strings if you would rather fix develop separately, but that version will not pass the check.

@hirokiterashima
hirokiterashima merged commit 4068228 into WISE-Community:develop Sep 1, 2026
3 of 4 checks passed
@hirokiterashima

Copy link
Copy Markdown
Member

Thanks @Isaries. This PR brought up good points that we need to consider.

@hirokiterashima

Copy link
Copy Markdown
Member

🎉 This PR is included in version 5.238.1 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants