Skip to content

fix: Fixed the forced password-change flow after first login - #4893

Open
Galane-dev wants to merge 5 commits into
shesha-io:releases/0.43from
Galane-dev:users/welcome/fix/update-password-permission-error
Open

fix: Fixed the forced password-change flow after first login#4893
Galane-dev wants to merge 5 commits into
shesha-io:releases/0.43from
Galane-dev:users/welcome/fix/update-password-permission-error

Conversation

@Galane-dev

@Galane-dev Galane-dev commented May 5, 2026

Copy link
Copy Markdown

Enforces RequireChangePassword in the frontend authenticator so users cannot bypass the change-password screen by manually editing the URL.
Clears RequireChangePassword after successful token-based password reset.
Allows authenticated users to call ChangePasswordAsync without requiring the admin Users permission.
Details

Previously, users with RequireChangePassword = true received a valid access token during login and were redirected to the change-password form only by UI logic. If they removed /no-auth/Shesha/change-password?mode=edit from the URL, the app restored the valid token and allowed normal authenticated navigation.

The authenticator now checks requireChangePassword during login redirect and session restore. If the flag is still true, the user is redirected back to the change-password form and the app is not marked as ready.

The backend password reset token flow now clears RequireChangePassword after a successful password update, matching the normal change-password behavior.

ChangePasswordAsync now overrides the class-level Users permission with AnyAuthenticated, so normal logged-in users can change their own password. The method still uses the current session user, validates the existing password, applies password policy validation, and clears RequireChangePassword.

Summary by CodeRabbit

  • New Features
    • Authentication now redirects users to a password-change page when their account requires a password update.
  • Bug Fixes / Behavior Changes
    • Password-reset via token no longer leaves users flagged to be forced to change their password afterward.
    • Logout is now processed locally immediately for a faster sign-out experience.

Galane-dev added 2 commits May 4, 2026 20:20
Redirected users with RequireChangePassword enabled back to change password form during login and protected-route auth checks

That prevents them from bypassing the flow by editing the URL

Also cleared RequireChangePassword after a successful token based password reset
Overrode the UserAppService class permission for ChangePasswordAsync to allow any authenticated user

This ensures that authenticated users can complete the forced password change flow, even when they don't have user management rights required by the class
@coderabbitai

coderabbitai Bot commented May 5, 2026

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@micanipho has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 34 minutes and 45 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8f109256-7aa8-47c5-aec3-0fcaa4cdf0f6

📥 Commits

Reviewing files that changed from the base of the PR and between 9609737 and 4072f96.

📒 Files selected for processing (2)
  • shesha-core/src/Shesha.NHibernate/NHibernate/SheshaNHibernateModule.cs
  • shesha-reactjs/src/providers/auth/authenticator.ts
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and specifically describes the main change: fixing the forced password-change flow that occurs after first login, which is the core focus of all modifications across backend and frontend.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
shesha-reactjs/src/providers/auth/authenticator.ts (1)

231-235: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

loginUserAsync doesn't apply the requireChangePassword guard — forced-password-change can be bypassed by editing the URL after fresh login.

loginUserAsync unconditionally sets #loginInfo and state 'ready', even when userProfile.user?.requireChangePassword is true. checkAuthAsync short-circuits at line 314 (if (this.loginInfo && !this.#isTokenExpired()) return) before reaching the requireChangePassword check. The result:

  1. Login with requireChangePassword=true#loginInfo is set, state = 'ready', caller navigates to change-password URL.
  2. User edits the URL to /home (or any authenticated route).
  3. checkAuthAsync fires → this.loginInfo is truthy + token valid → early return, no redirect → user accesses the app without changing their password.

This bypass only fails after a page refresh (where #loginInfo is cleared), so the protection is inconsistent. The PR's stated goal — "users cannot bypass the change-password screen by manually editing the URL" — is not fully achieved via the initial login path.

checkAuthAsync already handles this correctly (lines 341-345): it doesn't set #loginInfo when requireChangePassword is true. loginUserAsync should be consistent. The token expiration timer doesn't need to be restarted here since #saveUserToken already started it during #loginUserHttp.

🐛 Proposed fix — align loginUserAsync with checkAuthAsync
         const userProfile = await this.#fetchUserInfoHttp();
-        this.#loginInfo = userProfile;
-
-        this.#updateState('ready', null, null);
-
-        const redirectUrl = this.#getRedirectUrl(this.#router.fullPath, userProfile.user);
-
-        return {
-            userProfile: userProfile,
-            url: redirectUrl ?? this.#router.fullPath
-        };
+        if (userProfile.user?.requireChangePassword) {
+            this.#updateState('waiting', 'Password change required', null);
+            return {
+                userProfile: userProfile,
+                url: REQUIRED_PASSWORD_CHANGE_URL
+            };
+        }
+
+        this.#loginInfo = userProfile;
+        this.#updateState('ready', null, null);
+
+        const redirectUrl = this.#getRedirectUrl(this.#router.fullPath, userProfile.user);
+        return {
+            userProfile: userProfile,
+            url: redirectUrl ?? this.#router.fullPath
+        };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@shesha-reactjs/src/providers/auth/authenticator.ts` around lines 231 - 235,
In loginUserAsync: don't unconditionally set this.#loginInfo or call
this.#updateState('ready', ...) when userProfile.user?.requireChangePassword is
true; instead detect requireChangePassword on the userProfile returned from
`#loginUserHttp`, save the token as needed (`#saveUserToken` already started the
timer), then perform the redirect to the change-password flow (use
`#getRedirectUrl` or the same redirect logic you use in checkAuthAsync) and return
early so the guarded state isn't set; ensure the behavior matches
checkAuthAsync's branch that avoids assigning this.#loginInfo when
requireChangePassword is true.
shesha-core/src/Shesha.NHibernate/NHibernate/SheshaNHibernateModule.cs (1)

214-223: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

IsEnabledForAnonymousUsers not restored if InitializeDbPermissions throws.

Configuration.EntityHistory.IsEnabledForAnonymousUsers = prev is only reached on the happy path. If InitializeDbPermissions() (or SeedDatabaseAsync) throws, the setting is left as false. Wrapping both calls in a try/finally is the idiomatic fix.

🛡️ Proposed fix — restore config in all cases
             var prev = Configuration.EntityHistory.IsEnabledForAnonymousUsers;
             Configuration.EntityHistory.IsEnabledForAnonymousUsers = false;
-            AsyncHelper.RunSync(async () => {
-                await SeedDatabaseAsync();
-            });
-            IocManager.Resolve<ShaPermissionManager>().InitializeDbPermissions();
-            Configuration.EntityHistory.IsEnabledForAnonymousUsers = prev;
+            try
+            {
+                AsyncHelper.RunSync(async () => {
+                    await SeedDatabaseAsync();
+                });
+                IocManager.Resolve<ShaPermissionManager>().InitializeDbPermissions();
+            }
+            finally
+            {
+                Configuration.EntityHistory.IsEnabledForAnonymousUsers = prev;
+            }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@shesha-core/src/Shesha.NHibernate/NHibernate/SheshaNHibernateModule.cs`
around lines 214 - 223, The code sets
Configuration.EntityHistory.IsEnabledForAnonymousUsers to false but only
restores it on the happy path; wrap the calls that run seeding and permission
initialization (AsyncHelper.RunSync(() => SeedDatabaseAsync()) and
IocManager.Resolve<ShaPermissionManager>().InitializeDbPermissions()) in a
try/finally so that the original value held in prev is always restored (assign
back to Configuration.EntityHistory.IsEnabledForAnonymousUsers in the finally
block), while keeping the existing SkipDbSeed guard and prev capture.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@shesha-reactjs/src/providers/auth/authenticator.ts`:
- Around line 341-345: When checkAuthAsync detects
userProfile.user.requireChangePassword is true, it currently updates state and
returns before calling `#startTokenExpirationTimer`; move or add a call to
this.#startTokenExpirationTimer(...) immediately before the
this.#updateState(...) / this.#router.push(REQUIRED_PASSWORD_CHANGE_URL) branch
so the token expiration watcher is started even when redirecting to the
change-password flow; ensure you pass the same token/session values used
elsewhere by `#startTokenExpirationTimer` and keep the early return after the
redirect.

---

Outside diff comments:
In `@shesha-core/src/Shesha.NHibernate/NHibernate/SheshaNHibernateModule.cs`:
- Around line 214-223: The code sets
Configuration.EntityHistory.IsEnabledForAnonymousUsers to false but only
restores it on the happy path; wrap the calls that run seeding and permission
initialization (AsyncHelper.RunSync(() => SeedDatabaseAsync()) and
IocManager.Resolve<ShaPermissionManager>().InitializeDbPermissions()) in a
try/finally so that the original value held in prev is always restored (assign
back to Configuration.EntityHistory.IsEnabledForAnonymousUsers in the finally
block), while keeping the existing SkipDbSeed guard and prev capture.

In `@shesha-reactjs/src/providers/auth/authenticator.ts`:
- Around line 231-235: In loginUserAsync: don't unconditionally set
this.#loginInfo or call this.#updateState('ready', ...) when
userProfile.user?.requireChangePassword is true; instead detect
requireChangePassword on the userProfile returned from `#loginUserHttp`, save the
token as needed (`#saveUserToken` already started the timer), then perform the
redirect to the change-password flow (use `#getRedirectUrl` or the same redirect
logic you use in checkAuthAsync) and return early so the guarded state isn't
set; ensure the behavior matches checkAuthAsync's branch that avoids assigning
this.#loginInfo when requireChangePassword is true.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: d423fc4d-d3ff-4b6f-b60a-4ad48485e1ec

📥 Commits

Reviewing files that changed from the base of the PR and between 3606775 and c4f33d0.

📒 Files selected for processing (4)
  • shesha-core/src/Shesha.Application/Users/UserAppService.cs
  • shesha-core/src/Shesha.Framework/Authorization/ShaPermissionManager.cs
  • shesha-core/src/Shesha.NHibernate/NHibernate/SheshaNHibernateModule.cs
  • shesha-reactjs/src/providers/auth/authenticator.ts

Comment thread shesha-reactjs/src/providers/auth/authenticator.ts

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@shesha-core/src/Shesha.NHibernate/NHibernate/SheshaNHibernateModule.cs`:
- Around line 362-367: The catch block currently captures the original
initialization exception as "e" but calls
appStartup.StartupFailedAsync(startupDto.Id, e) which may itself throw and
overwrite the original; change the catch to preserve "e" by wrapping the call to
appStartup.StartupFailedAsync in its own try/catch (or swallow/log any
exceptions from StartupFailedAsync) and always rethrow the original exception
"e" (use throw; to preserve stack) so that failures in StartupFailedAsync do not
replace the root-cause exception—target the catch(Exception e) block and the
call to appStartup.StartupFailedAsync(startupDto.Id, e).
- Line 223: The call to
IocManager.Resolve<ShaPermissionManager>().InitializeDbPermissions() must be
executed inside a distributed lock to avoid race conditions across instances;
wrap this call with the distributed lock facility (use
lockFactory.DoExclusiveAsync() with a sensible lock key) after
SeedDatabaseAsync() completes, or alternatively modify
ShaPermissionManager.InitializeDbPermissions to perform idempotent,
concurrency-safe upserts (or rely on DB unique constraints) so concurrent
executions cannot create duplicates. Ensure the lock is awaited and released
correctly and that the lock key is unique to permission initialization.

In `@shesha-reactjs/src/providers/auth/authenticator.ts`:
- Line 21: When redirecting to the forced-password-change URL
(REQUIRED_PASSWORD_CHANGE_URL), preserve and forward any existing returnUrl so
the user is returned to their original destination after changing password:
update every redirect that navigates to REQUIRED_PASSWORD_CHANGE_URL to read an
existing returnUrl from the current location's query (or from the login query
when coming from the login flow / from the current protected route when
restoring a session) and append it as a returnUrl query parameter to
REQUIRED_PASSWORD_CHANGE_URL before performing the redirect; ensure this change
is applied consistently in all places that reference
REQUIRED_PASSWORD_CHANGE_URL.
- Around line 272-280: The catch block on the fire-and-forget logout call
(this.#httpClient.post to URLS.LOGOFF using currentToken?.accessToken) currently
logs the full error object which can leak the Authorization header; change the
catch to only log sanitized fields (e.g., error?.response?.status,
error?.status, and error?.message or a short string) and avoid including the
entire error or request config; update the anonymous catch handler to extract
those safe fields and log a concise message like "Logout API call failed" with
only status/message details.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 83f9ace4-3dc7-4e40-9edb-3bcfa186ce58

📥 Commits

Reviewing files that changed from the base of the PR and between c4f33d0 and 9609737.

⛔ Files ignored due to path filters (1)
  • shesha-functional-tests/backend/src/Boxfusion.SheshaFunctionalTests.Web.Host/appsettings.json is excluded by !shesha-functional-tests/** and included by none
📒 Files selected for processing (2)
  • shesha-core/src/Shesha.NHibernate/NHibernate/SheshaNHibernateModule.cs
  • shesha-reactjs/src/providers/auth/authenticator.ts

Comment thread shesha-core/src/Shesha.NHibernate/NHibernate/SheshaNHibernateModule.cs Outdated
Comment thread shesha-core/src/Shesha.NHibernate/NHibernate/SheshaNHibernateModule.cs Outdated
Comment thread shesha-reactjs/src/providers/auth/authenticator.ts Outdated
Comment thread shesha-reactjs/src/providers/auth/authenticator.ts
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