Skip to content

Frontend/auth updates#1727

Open
jsandoval81 wants to merge 16 commits into
csg-org:mainfrom
InspiringApps:frontend/auth-updates
Open

Frontend/auth updates#1727
jsandoval81 wants to merge 16 commits into
csg-org:mainfrom
InspiringApps:frontend/auth-updates

Conversation

@jsandoval81

@jsandoval81 jsandoval81 commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

Requirements List

  • Deploy backend updates if testing pre-merge

Description List

  • Moved auth-related helper functions into their own utility module
  • Separated the AuthCallback pages into separate pages per userType/compact
  • Updated the login state parameter to be a random-value CSRF protection
  • Added PKCE to the login flows
  • Updated the Cognito app clients to accept the new separate redirect_uri landing pages

Testing List

  • yarn test:unit:all should run without errors or warnings
  • yarn serve should run without errors or warnings
  • yarn build should run without errors or warnings
  • Code review
  • Testing
    • Deploy the backend updates to your sandbox / test environment
    • Login to each compact, ensuring that correct logins are successful with no regressions
    • Ensure user sessions remain valid in the browser, with no regressions to page refreshes or auto logout

Closes #1641

Summary by CodeRabbit

  • New Features

    • Added dedicated sign-in callback routes for staff and licensee applications.
    • OAuth sign-in now uses role- and application-specific redirect paths.
    • Added CSRF state validation and PKCE protection to hosted sign-in and password-reset flows.
    • Authentication errors now provide a consistent recovery link back to the dashboard.
  • Bug Fixes

    • Improved callback URL handling for hosted and local environments.
  • Tests

    • Added coverage for callback routing, OAuth security parameters, and authentication error states.

@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

OAuth authentication is reorganized around a shared utility module with CSRF state and PKCE support. Cognito clients and frontend routes now use application-specific callback paths, with dedicated callback pages, shared token-exchange handling, updated tests, and refreshed Postman collections.

Changes

OAuth callback flow

Layer / File(s) Summary
Backend callback contracts
backend/common-cdk/..., backend/*/stacks/..., backend/*/tests/..., backend/*/docs/postman/...
Cognito UI clients accept configurable callback paths, and application-specific redirect URLs are applied across stacks, tests, and Postman collections.
Authentication utility foundation
webroot/src/app.config.ts, webroot/src/utils/auth.ts, webroot/tests/helpers/setup.ts, webroot/tsconfig.json, webroot/vue.config.js
Authentication constants move into @utils/auth, which adds CSRF state, PKCE, Cognito configuration, hosted-login URI generation, and auto-logout exports.
Authentication import migration
webroot/src/components/..., webroot/src/models/..., webroot/src/network/..., webroot/src/router/index.ts, webroot/src/store/...
Frontend modules and tests import authentication values from the new utility module while retaining application configuration imports for non-auth constants.
PKCE-protected login initiation
webroot/src/pages/PublicDashboard/..., webroot/src/pages/MfaReset*/*
Login and licensee MFA reset flows generate CSRF and PKCE values before building hosted-login URLs.
Callback handling and routing
webroot/src/pages/AuthCallback/..., webroot/src/router/routes.ts, webroot/src/styles.common/...
A shared callback mixin validates state, exchanges authorization codes with PKCE, and supports dedicated callback pages for staff and licensee contexts.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Browser
  participant PublicDashboard
  participant Cognito
  participant AuthCallback
  PublicDashboard->>Browser: Generate CSRF state and PKCE challenge
  Browser->>Cognito: Request hosted login with state and code challenge
  Cognito-->>AuthCallback: Redirect with authorization code and state
  AuthCallback->>Cognito: Exchange code with PKCE verifier
  Cognito-->>AuthCallback: Return tokens
Loading

Possibly related PRs

Suggested reviewers: rmolinares, isabeleliassen, jlkravitz

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title is accurate but too generic to identify the main change. Use a concise, specific title like “Add PKCE and CSRF-protected auth callbacks”.
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The description matches the template well and covers requirements, changes, testing, and issue closure.
Linked Issues check ✅ Passed The changes implement random CSRF state, PKCE, separate callback pages, updated allowlists, and related tests.
Out of Scope Changes check ✅ Passed I don't see material changes outside the auth/callback and supporting config/test scope.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 Stylelint (17.14.0)
webroot/src/styles.common/_mixins.less

ConfigurationError: Could not find "stylelint-config-rational-order". Do you need to install the package or use the "configBasedir" option?
at getModulePath (file:///usr/local/lib/node_modules/stylelint/lib/utils/getModulePath.mjs:38:9)
at loadExtendedConfig (file:///usr/local/lib/node_modules/stylelint/lib/augmentConfig.mjs:285:21)
at extendConfig (file:///usr/local/lib/node_modules/stylelint/lib/augmentConfig.mjs:252:25)
at async augmentConfigBasic (file:///usr/local/lib/node_modules/stylelint/lib/augmentConfig.mjs:85:20)
at async augmentConfigFull (file:///usr/local/lib/node_modules/stylelint/lib/augmentConfig.mjs:138:24)
at async getConfigForFile (file:///usr/local/lib/node_modules/stylelint/lib/getConfigForFile.mjs:102:26)
at async resolveOptionValue (file:///usr/local/lib/node_modules/stylelint/lib/utils/resolveOptionValue.mjs:27:24)
at async standalone (file:///usr/local/lib/node_modules/stylelint/lib/standalone.mjs:127:22)

webroot/src/styles.common/mixins/auth-error.less

ConfigurationError: Could not find "stylelint-config-rational-order". Do you need to install the package or use the "configBasedir" option?
at getModulePath (file:///usr/local/lib/node_modules/stylelint/lib/utils/getModulePath.mjs:38:9)
at loadExtendedConfig (file:///usr/local/lib/node_modules/stylelint/lib/augmentConfig.mjs:285:21)
at extendConfig (file:///usr/local/lib/node_modules/stylelint/lib/augmentConfig.mjs:252:25)
at async augmentConfigBasic (file:///usr/local/lib/node_modules/stylelint/lib/augmentConfig.mjs:85:20)
at async augmentConfigFull (file:///usr/local/lib/node_modules/stylelint/lib/augmentConfig.mjs:138:24)
at async getConfigForFile (file:///usr/local/lib/node_modules/stylelint/lib/getConfigForFile.mjs:102:26)
at async resolveOptionValue (file:///usr/local/lib/node_modules/stylelint/lib/utils/resolveOptionValue.mjs:27:24)
at async standalone (file:///usr/local/lib/node_modules/stylelint/lib/standalone.mjs:127:22)

webroot/src/pages/AuthCallback/StaffSocialWork/StaffSocialWork.less

ConfigurationError: Could not find "stylelint-config-rational-order". Do you need to install the package or use the "configBasedir" option?
at getModulePath (file:///usr/local/lib/node_modules/stylelint/lib/utils/getModulePath.mjs:38:9)
at loadExtendedConfig (file:///usr/local/lib/node_modules/stylelint/lib/augmentConfig.mjs:285:21)
at extendConfig (file:///usr/local/lib/node_modules/stylelint/lib/augmentConfig.mjs:252:25)
at async augmentConfigBasic (file:///usr/local/lib/node_modules/stylelint/lib/augmentConfig.mjs:85:20)
at async augmentConfigFull (file:///usr/local/lib/node_modules/stylelint/lib/augmentConfig.mjs:138:24)
at async getConfigForFile (file:///usr/local/lib/node_modules/stylelint/lib/getConfigForFile.mjs:102:26)
at async resolveOptionValue (file:///usr/local/lib/node_modules/stylelint/lib/utils/resolveOptionValue.mjs:27:24)
at async standalone (file:///usr/local/lib/node_modules/stylelint/lib/standalone.mjs:127:22)

  • 3 others

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.

@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

🧹 Nitpick comments (6)
webroot/src/pages/AuthCallback/StaffCosmo/StaffCosmo.vue (1)

8-20: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider extracting shared error markup.

This template is duplicated verbatim across StaffCosmo.vue, StaffJcc.vue, and (per the stack outline) StaffSocialWork.vue/LicenseeJcc.vue. Each new page only needs to render this same error card, so extracting a shared AuthCallbackError component (or a slot in a common wrapper) would reduce duplication and centralize future error-copy changes.

🤖 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 `@webroot/src/pages/AuthCallback/StaffCosmo/StaffCosmo.vue` around lines 8 -
20, Extract the duplicated error card markup from StaffCosmo.vue, StaffJcc.vue,
StaffSocialWork.vue, and LicenseeJcc.vue into a shared AuthCallbackError
component or common wrapper. Preserve the existing isError conditional,
translations, styling classes, and DashboardPublic router link, then replace
each page’s inline markup with the shared component.
backend/social-work-app/docs/internal/postman/postman-collection.json (1)

164-167: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Exercise the PKCE and CSRF flow in these requests.

The authorize requests omit state, code_challenge, and code_challenge_method; the token request omits code_verifier. Update the collection’s pre-request setup and exchange request so Postman validates the security contract introduced here.

Also applies to: 240-247, 312-319

🤖 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 `@backend/social-work-app/docs/internal/postman/postman-collection.json` around
lines 164 - 167, Update the Postman authorization-flow requests and shared
pre-request setup to generate and send a CSRF state and PKCE code challenge with
code_challenge_method, then persist the matching code_verifier for the flow. Add
the stored state, challenge parameters, and verifier to the corresponding token
exchange requests, including the sections identified by the repeated
authorize/token request blocks.
webroot/src/pages/AuthCallback/_mixins/handler.mixin.ts (3)

138-138: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove commented-out toNative export.

The commented-out // export default toNative(MixinAuthCallbackHandler); is dead code. The direct class export on line 140 is correct for use with mixins() in subclasses like StaffCosmo.ts.

🧹 Proposed cleanup
-
-// export default toNative(MixinAuthCallbackHandler);
-
 export default MixinAuthCallbackHandler;
🤖 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 `@webroot/src/pages/AuthCallback/_mixins/handler.mixin.ts` at line 138, Remove
the commented-out toNative export near MixinAuthCallbackHandler, keeping the
active direct class export unchanged for mixins() consumers.

84-84: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add type annotations for cognitoAuthDomain and cognitoClientId parameters.

Both getTokens and fetchCognitoTokens leave cognitoAuthDomain and cognitoClientId untyped, defaulting to any. Adding string annotations would improve type safety and consistency with the typed authType parameter.

🔧 Proposed fix
-    async getTokens(appMode: AppModes, authType: AuthTypes, cognitoAuthDomain, cognitoClientId): Promise<void> {
+    async getTokens(appMode: AppModes, authType: AuthTypes, cognitoAuthDomain: string, cognitoClientId: string): Promise<void> {
-    async fetchCognitoTokens(authType: AuthTypes, cognitoAuthDomain, cognitoClientId): Promise<void> {
+    async fetchCognitoTokens(authType: AuthTypes, cognitoAuthDomain: string, cognitoClientId: string): Promise<void> {

Also applies to: 99-99

🤖 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 `@webroot/src/pages/AuthCallback/_mixins/handler.mixin.ts` at line 84, Add
explicit string type annotations to the cognitoAuthDomain and cognitoClientId
parameters in both getTokens and fetchCognitoTokens, preserving the existing
appMode and authType types.

99-117: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Add a timeout and error logging to the Cognito token exchange.

The axios.post() call has no explicit timeout — if Cognito is slow or unresponsive, the user is stuck on a blank callback page until the browser's default timeout fires. Additionally, the .catch() on line 88 silently swallows the error with no logging, making production debugging difficult.

⏱️ Proposed fix: add timeout and error logging
         params.append('code_verifier', consumePkceCodeVerifier() || '');

-        const { data } = await axios.post(`${cognitoAuthDomain}/oauth2/token`, params);
+        const { data } = await axios.post(`${cognitoAuthDomain}/oauth2/token`, params, {
+            timeout: 15000,
+        });

And in getTokens():

-        await this.fetchCognitoTokens(authType, cognitoAuthDomain, cognitoClientId).catch(() => {
+        await this.fetchCognitoTokens(authType, cognitoAuthDomain, cognitoClientId).catch((err) => {
+            console.error('Auth callback token exchange failed:', err);
             this.isError = true;
         });
🤖 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 `@webroot/src/pages/AuthCallback/_mixins/handler.mixin.ts` around lines 99 -
117, Update fetchCognitoTokens to pass an explicit timeout option to the
axios.post token request, using the project’s standard request timeout if
available. In getTokens, replace the silent catch with error logging that
includes the caught error and relevant Cognito token-exchange context before
preserving the existing failure handling.
webroot/src/pages/AuthCallback/StaffSocialWork/StaffSocialWork.spec.ts (1)

12-19: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Smoke test only covers mounting; consider testing auth callback behavior.

The test verifies the component mounts but doesn't exercise the CSRF state verification, token exchange, or error display paths that are the core purpose of this callback page. The mixin's created() will set isError = true when verifyCsrfState() fails (no stored state in test), which is untested.

💡 Suggested additional test cases
it('should display error when CSRF state verification fails', async () => {
    const wrapper = await mountShallow(StaffSocialWork);
    await wrapper.vm.$nextTick();
    expect(wrapper.findComponent(StaffSocialWork).exists()).to.equal(true);
    // Assert isError is true and error UI is rendered
});

it('should verify CSRF state and exchange tokens on valid callback', async () => {
    // Set up sessionStorage with matching state, route query with code+state
    // Assert token exchange was called and redirect occurred
});
🤖 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 `@webroot/src/pages/AuthCallback/StaffSocialWork/StaffSocialWork.spec.ts`
around lines 12 - 19, Expand the StaffSocialWork tests beyond mounting by
covering the auth callback behavior in the component’s created lifecycle and
mixin. Add a test for failed verifyCsrfState() that waits for $nextTick and
asserts isError plus the rendered error UI, and a valid callback test that
configures matching sessionStorage state and route code/state, then verifies
token exchange and redirect behavior using the relevant mocked methods.
🤖 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 `@webroot/src/pages/AuthCallback/_mixins/mixins.spec.ts`:
- Around line 43-50: Update the “should successfully get tokens” test to stub
the axios.post token exchange with a successful response and spy on the router
navigation method used by getTokens, then assert it is called with the expected
route instead of checking history.state.replaced. Ensure no real request to
http://localhost/oauth2/token can occur.

In `@webroot/src/pages/MfaResetConfirmLicensee/MfaResetConfirmLicensee.ts`:
- Around line 51-61: Handle failures from createPkceChallenge() within
MfaResetConfirmLicensee.created() using try/catch, preserve the existing CSRF
initialization, and surface a clear user-facing error or disable login when PKCE
generation fails so navigation cannot occur with an empty code_challenge.
- Line 92: Update the getHostedLoginUri call in MfaResetConfirmLicensee to pass
AppModes.JCC instead of this.appMode, ensuring the licensee callback path always
targets the supported JCC route.

In `@webroot/src/utils/auth.ts`:
- Around line 133-218: Resolve the LICENSEE callback mismatch in
getHostedLoginUri: either add the missing LicenseeCosmo and LicenseeSocialWork
callback pages and router entries for /auth/callback/licensee/cosmo and
/auth/callback/licensee/socialwork, or restrict callback-path generation so
LICENSEE only produces supported routes such as /auth/callback/licensee/jcc.

---

Nitpick comments:
In `@backend/social-work-app/docs/internal/postman/postman-collection.json`:
- Around line 164-167: Update the Postman authorization-flow requests and shared
pre-request setup to generate and send a CSRF state and PKCE code challenge with
code_challenge_method, then persist the matching code_verifier for the flow. Add
the stored state, challenge parameters, and verifier to the corresponding token
exchange requests, including the sections identified by the repeated
authorize/token request blocks.

In `@webroot/src/pages/AuthCallback/_mixins/handler.mixin.ts`:
- Line 138: Remove the commented-out toNative export near
MixinAuthCallbackHandler, keeping the active direct class export unchanged for
mixins() consumers.
- Line 84: Add explicit string type annotations to the cognitoAuthDomain and
cognitoClientId parameters in both getTokens and fetchCognitoTokens, preserving
the existing appMode and authType types.
- Around line 99-117: Update fetchCognitoTokens to pass an explicit timeout
option to the axios.post token request, using the project’s standard request
timeout if available. In getTokens, replace the silent catch with error logging
that includes the caught error and relevant Cognito token-exchange context
before preserving the existing failure handling.

In `@webroot/src/pages/AuthCallback/StaffCosmo/StaffCosmo.vue`:
- Around line 8-20: Extract the duplicated error card markup from
StaffCosmo.vue, StaffJcc.vue, StaffSocialWork.vue, and LicenseeJcc.vue into a
shared AuthCallbackError component or common wrapper. Preserve the existing
isError conditional, translations, styling classes, and DashboardPublic router
link, then replace each page’s inline markup with the shared component.

In `@webroot/src/pages/AuthCallback/StaffSocialWork/StaffSocialWork.spec.ts`:
- Around line 12-19: Expand the StaffSocialWork tests beyond mounting by
covering the auth callback behavior in the component’s created lifecycle and
mixin. Add a test for failed verifyCsrfState() that waits for $nextTick and
asserts isError plus the rendered error UI, and a valid callback test that
configures matching sessionStorage state and route code/state, then verifies
token exchange and redirect behavior using the relevant mocked methods.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 85950346-9adc-4ed8-8df5-ef52b99643b1

📥 Commits

Reviewing files that changed from the base of the PR and between a8102ac and 3a1a9d2.

📒 Files selected for processing (80)
  • backend/common-cdk/common_constructs/user_pool.py
  • backend/common-cdk/tests/test_user_pool.py
  • backend/compact-connect/docs/internal/postman/postman-collection.json
  • backend/compact-connect/docs/postman/postman-collection.json
  • backend/compact-connect/stacks/persistent_stack/staff_users.py
  • backend/compact-connect/stacks/provider_users/provider_users.py
  • backend/compact-connect/tests/app/base.py
  • backend/cosmetology-app/docs/internal/postman/postman-collection.json
  • backend/cosmetology-app/docs/postman/postman-collection.json
  • backend/cosmetology-app/docs/search-internal/postman/postman-collection.json
  • backend/cosmetology-app/stacks/persistent_stack/staff_users.py
  • backend/cosmetology-app/tests/app/base.py
  • backend/social-work-app/docs/internal/postman/postman-collection.json
  • backend/social-work-app/docs/postman/postman-collection.json
  • backend/social-work-app/docs/search-internal/postman/postman-collection.json
  • backend/social-work-app/stacks/persistent_stack/staff_users.py
  • backend/social-work-app/tests/app/base.py
  • webroot/src/app.config.ts
  • webroot/src/components/App/App.spec.ts
  • webroot/src/components/App/App.ts
  • webroot/src/components/AutoLogout/AutoLogout.ts
  • webroot/src/components/ChangePassword/ChangePassword.ts
  • webroot/src/components/MilitaryAffiliationInfoBlock/MilitaryAffiliationInfoBlock.ts
  • webroot/src/components/Page/PageMainNav/PageMainNav.ts
  • webroot/src/components/StateSettingsList/StateSettingsList.ts
  • webroot/src/components/UserAccount/UserAccount.ts
  • webroot/src/models/LicenseeUser/LicenseeUser.model.spec.ts
  • webroot/src/models/LicenseeUser/LicenseeUser.model.ts
  • webroot/src/models/StaffUser/StaffUser.model.spec.ts
  • webroot/src/models/StaffUser/StaffUser.model.ts
  • webroot/src/models/User/User.model.ts
  • webroot/src/network/licenseApi/data.api.ts
  • webroot/src/network/licenseApi/interceptors.ts
  • webroot/src/network/searchApi/interceptors.ts
  • webroot/src/network/stateApi/interceptors.ts
  • webroot/src/network/userApi/interceptors.ts
  • webroot/src/pages/AuthCallback/AuthCallback.spec.ts
  • webroot/src/pages/AuthCallback/AuthCallback.ts
  • webroot/src/pages/AuthCallback/LicenseeJcc/LicenseeJcc.less
  • webroot/src/pages/AuthCallback/LicenseeJcc/LicenseeJcc.spec.ts
  • webroot/src/pages/AuthCallback/LicenseeJcc/LicenseeJcc.ts
  • webroot/src/pages/AuthCallback/LicenseeJcc/LicenseeJcc.vue
  • webroot/src/pages/AuthCallback/StaffCosmo/StaffCosmo.less
  • webroot/src/pages/AuthCallback/StaffCosmo/StaffCosmo.spec.ts
  • webroot/src/pages/AuthCallback/StaffCosmo/StaffCosmo.ts
  • webroot/src/pages/AuthCallback/StaffCosmo/StaffCosmo.vue
  • webroot/src/pages/AuthCallback/StaffJcc/StaffJcc.less
  • webroot/src/pages/AuthCallback/StaffJcc/StaffJcc.spec.ts
  • webroot/src/pages/AuthCallback/StaffJcc/StaffJcc.ts
  • webroot/src/pages/AuthCallback/StaffJcc/StaffJcc.vue
  • webroot/src/pages/AuthCallback/StaffSocialWork/StaffSocialWork.less
  • webroot/src/pages/AuthCallback/StaffSocialWork/StaffSocialWork.spec.ts
  • webroot/src/pages/AuthCallback/StaffSocialWork/StaffSocialWork.ts
  • webroot/src/pages/AuthCallback/StaffSocialWork/StaffSocialWork.vue
  • webroot/src/pages/AuthCallback/_mixins/handler.mixin.ts
  • webroot/src/pages/AuthCallback/_mixins/mixins.spec.ts
  • webroot/src/pages/CompactSettings/CompactSettings.ts
  • webroot/src/pages/Home/Home.ts
  • webroot/src/pages/Logout/Logout.ts
  • webroot/src/pages/MfaResetConfirmLicensee/MfaResetConfirmLicensee.ts
  • webroot/src/pages/MfaResetStartLicensee/MfaResetStartLicensee.ts
  • webroot/src/pages/PrivilegeDetail/PrivilegeDetail.ts
  • webroot/src/pages/PublicDashboard/PublicDashboard.spec.ts
  • webroot/src/pages/PublicDashboard/PublicDashboard.ts
  • webroot/src/pages/StateSettings/StateSettings.ts
  • webroot/src/router/index.ts
  • webroot/src/router/routes.ts
  • webroot/src/store/global/global.mutations.ts
  • webroot/src/store/global/global.spec.ts
  • webroot/src/store/global/global.state.ts
  • webroot/src/store/user/user.actions.ts
  • webroot/src/store/user/user.mutations.ts
  • webroot/src/store/user/user.spec.ts
  • webroot/src/store/user/user.state.ts
  • webroot/src/styles.common/_mixins.less
  • webroot/src/styles.common/mixins/auth-error.less
  • webroot/src/utils/auth.ts
  • webroot/tests/helpers/setup.ts
  • webroot/tsconfig.json
  • webroot/vue.config.js
💤 Files with no reviewable changes (2)
  • webroot/src/pages/AuthCallback/AuthCallback.spec.ts
  • webroot/src/pages/AuthCallback/AuthCallback.ts

Comment on lines +43 to +50
it('should successfully get tokens', async () => {
const wrapper = await mountShallow(AuthCallbackHandlerMixin);
const component = wrapper.vm;

await component.getTokens(AppModes.JCC, AuthTypes.STAFF, 'http://localhost', 'abc');

expect(component.$router.options.history.state.replaced).to.equal(true);
});

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if axios is mocked in the test setup files
rg -n --type=ts 'axios|sinon.*stub|mock.*axios' webroot/tests/helpers/ webroot/src/pages/AuthCallback/_mixins/mixins.spec.ts -g '!**/node_modules/**'

Repository: csg-org/CompactConnect

Length of output: 251


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## File list\n'
git ls-files 'webroot/src/pages/AuthCallback/_mixins/*' 'webroot/tests/helpers/*' | sed 's#^`#-` #'

printf '\n## Spec outline\n'
ast-grep outline webroot/src/pages/AuthCallback/_mixins/mixins.spec.ts --view expanded || true

printf '\n## Mixin outline\n'
ast-grep outline webroot/src/pages/AuthCallback/_mixins/mixins.ts --view expanded || true

printf '\n## Search for relevant symbols\n'
rg -n "getTokens|redirectUser|created\\(|axios\\.post|oauth2/token|pkce|code_verifier|isError|replaced" webroot/src/pages/AuthCallback/_mixins webroot/tests/helpers -g '!**/node_modules/**'

Repository: csg-org/CompactConnect

Length of output: 2213


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## handler.mixin.ts (selected lines)\n'
sed -n '1,220p' webroot/src/pages/AuthCallback/_mixins/handler.mixin.ts | cat -n

printf '\n## mixins.spec.ts (selected lines)\n'
sed -n '1,220p' webroot/src/pages/AuthCallback/_mixins/mixins.spec.ts | cat -n

printf '\n## test helper files\n'
sed -n '1,220p' webroot/tests/helpers/setup.ts | cat -n
printf '\n--- index.ts ---\n'
sed -n '1,220p' webroot/tests/helpers/index.ts | cat -n

printf '\n## broader axios/search hits\n'
rg -n "axios\\.post|stub\\(.*axios|mock.*axios|sinon\\.stub\\(.*post|createSandbox|mountShallow|router\\.options\\.history\\.state\\.replaced" webroot/src webroot/tests -g '!**/node_modules/**'

Repository: csg-org/CompactConnect

Length of output: 50378


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## router/history-related setup\n'
rg -n "createWebHistory|history.state.replaced|router\\.replace|router\\.push|isReady\\(|replaceState|replaced" webroot/tests/helpers webroot/src/pages/AuthCallback webroot/src/router webroot/src -g '!**/node_modules/**' | head -n 200

printf '\n## any axios mocking/interception in tests\n'
rg -n "axios|nock|msw|fetchMock|MockAdapter|axios-mock|stub.*post|mock.*post" webroot/tests webroot/src -g '!**/node_modules/**' | head -n 200

Repository: csg-org/CompactConnect

Length of output: 50379


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## explicit axios mocks in tests\n'
rg -n "vi\\.mock\\(['\"]axios['\"]|jest\\.mock\\(['\"]axios['\"]|mock.*axios|axios-mock-adapter|MockAdapter|nock|msw|sinon\\.stub\\(axios|stub\\(.*post" webroot/tests webroot/src/pages/AuthCallback -g '!**/node_modules/**' | head -n 200

printf '\n## auth callback related specs\n'
rg -n "redirectUser|created\\(|verifyCsrfState|consumePkceCodeVerifier|getTokens\\(" webroot/src/pages/AuthCallback -g '!**/node_modules/**' | head -n 200

Repository: csg-org/CompactConnect

Length of output: 194


Mock the token exchange in this test
getTokens() calls axios.post() and the success path navigates with router.push(), so asserting history.state.replaced doesn’t prove the right behavior. Stub the token response and assert the route change directly; otherwise this spec can hit http://localhost/oauth2/token for real.

🤖 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 `@webroot/src/pages/AuthCallback/_mixins/mixins.spec.ts` around lines 43 - 50,
Update the “should successfully get tokens” test to stub the axios.post token
exchange with a successful response and spy on the router navigation method used
by getTokens, then assert it is called with the expected route instead of
checking history.state.replaced. Ensure no real request to
http://localhost/oauth2/token can occur.

Comment on lines +51 to +61
csrfState = '';
pkceChallenge = '';

//
// Lifecycle
//
async created(): Promise<void> {
this.csrfState = createAuthCsrfState();
this.pkceChallenge = await createPkceChallenge();
}

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.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Unhandled rejection in async created() if createPkceChallenge() fails.

If the Web Crypto API throws (e.g., crypto.subtle.digest unavailable), the rejection is unhandled and this.pkceChallenge remains ''. The user could then click "go to login" and get a URI with an empty code_challenge, causing a confusing token-exchange failure rather than a clear error message.

🛡️ Suggested fix: add try/catch in created()
 async created(): Promise<void> {
     this.csrfState = createAuthCsrfState();
-    this.pkceChallenge = await createPkceChallenge();
+    try {
+        this.pkceChallenge = await createPkceChallenge();
+    } catch {
+        this.serverMessage = this.$t('serverErrors.networkError');
+        this.isLoading = false;
+    }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
csrfState = '';
pkceChallenge = '';
//
// Lifecycle
//
async created(): Promise<void> {
this.csrfState = createAuthCsrfState();
this.pkceChallenge = await createPkceChallenge();
}
csrfState = '';
pkceChallenge = '';
//
// Lifecycle
//
async created(): Promise<void> {
this.csrfState = createAuthCsrfState();
try {
this.pkceChallenge = await createPkceChallenge();
} catch {
this.serverMessage = this.$t('serverErrors.networkError');
this.isLoading = false;
}
}
🤖 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 `@webroot/src/pages/MfaResetConfirmLicensee/MfaResetConfirmLicensee.ts` around
lines 51 - 61, Handle failures from createPkceChallenge() within
MfaResetConfirmLicensee.created() using try/catch, preserve the existing CSRF
initialization, and surface a clear user-facing error or disable login when PKCE
generation fails so navigation cannot occur with an empty code_challenge.


get hostedLoginUriLicensee(): string {
return getHostedLoginUri(this.appMode, AuthTypes.LICENSEE, '/login');
return getHostedLoginUri(this.appMode, AuthTypes.LICENSEE, '/login', this.csrfState, this.pkceChallenge);

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check routes.ts for licensee callback route definitions
rg -n 'auth/callback/licensee' webroot/src/router/routes.ts

Repository: csg-org/CompactConnect

Length of output: 208


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the URI builder and the related dashboard call site
rg -n "function getHostedLoginUri|getHostedLoginUri\(" webroot/src
rg -n "PublicDashboard" webroot/src

# Inspect the relevant files and nearby context
sed -n '1,220p' webroot/src/utils/*.ts 2>/dev/null || true
sed -n '1,220p' webroot/src/pages/PublicDashboard/PublicDashboard.ts 2>/dev/null || true

# Search for any non-JCC licensee callback routes/pages
rg -n "auth/callback/licensee|LicenseeJcc|licensee/(cosmo|socialwork|jcc)" webroot/src

Repository: csg-org/CompactConnect

Length of output: 16937


this.appMode should not drive the licensee callback path. getHostedLoginUri() builds /auth/callback/licensee/{mode}, but only /auth/callback/licensee/jcc exists here, and PublicDashboard.ts already pins licensee login to AppModes.JCC. Use AppModes.JCC here too.

🤖 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 `@webroot/src/pages/MfaResetConfirmLicensee/MfaResetConfirmLicensee.ts` at line
92, Update the getHostedLoginUri call in MfaResetConfirmLicensee to pass
AppModes.JCC instead of this.appMode, ensuring the licensee callback path always
targets the supported JCC route.

Comment thread webroot/src/utils/auth.ts
Comment on lines +133 to +218
export const getCognitoConfig = (appMode: AppModes, authType: AuthTypes): CognitoConfig => {
const config: CognitoConfig = {
scopes: '',
clientId: '',
authDomain: '',
};

switch (authType) {
case AuthTypes.STAFF:
config.scopes = staffLoginScopes;

if (appMode === AppModes.JCC) {
config.clientId = envConfig.cognitoClientIdStaff;
config.authDomain = envConfig.cognitoAuthDomainStaff;
} else if (appMode === AppModes.COSMETOLOGY) {
config.clientId = envConfig.cognitoClientIdStaffCosmo;
config.authDomain = envConfig.cognitoAuthDomainStaffCosmo;
} else if (appMode === AppModes.SOCIAL_WORK) {
config.clientId = envConfig.cognitoClientIdStaffSw;
config.authDomain = envConfig.cognitoAuthDomainStaffSw;
}

break;
case AuthTypes.LICENSEE:
config.scopes = licenseeLoginScopes;
config.clientId = envConfig.cognitoClientIdLicensee;
config.authDomain = envConfig.cognitoAuthDomainLicensee;
break;
default:
break;
}

return config;
};

export const getHostedLoginUri = (appMode: AppModes, authType: AuthTypes, hostedIdpPath = '/login', state = '', codeChallenge = ''): string => {
const { domain } = envConfig;
const {
scopes,
clientId,
authDomain
} = getCognitoConfig(appMode, authType);
const getCallbackPath = () => {
let userScopePath = ``;
let compactScopePath = ``;

switch (authType) {
case AuthTypes.STAFF:
userScopePath += `/staff`;
break;
case AuthTypes.LICENSEE:
userScopePath += `/licensee`;
break;
default:
break;
}

switch (appMode) {
case AppModes.JCC:
compactScopePath += `/jcc`;
break;
case AppModes.COSMETOLOGY:
compactScopePath += `/cosmo`;
break;
case AppModes.SOCIAL_WORK:
compactScopePath += `/socialwork`;
break;
default:
break;
}

return `/auth/callback${userScopePath}${compactScopePath}`;
};
const loginUriQuery = [
`?client_id=${clientId}`,
`&response_type=code`,
`&scope=${encodeURIComponent(scopes || '')}`,
`&state=${encodeURIComponent(state)}`,
`&code_challenge=${encodeURIComponent(codeChallenge)}`,
`&code_challenge_method=S256`,
`&redirect_uri=${encodeURIComponent(`${domain}${getCallbackPath()}`)}`,
].join('');
const loginUri = `${authDomain}${hostedIdpPath}${loginUriQuery}`;

return loginUri;
};

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check for all AuthCallback page components and verify route coverage.

# 1. List all callback page component directories
echo "=== AuthCallback page components ==="
fd -t d . webroot/src/pages/AuthCallback --max-depth 1 --exec echo {}

# 2. Check routes file for callback path definitions
echo ""
echo "=== Callback routes in routes.ts ==="
rg -n 'auth/callback' webroot/src/router/routes.ts -C2

# 3. Check for any LicenseeCosmo or LicenseeSocialWork components
echo ""
echo "=== Searching for LicenseeCosmo / LicenseeSocialWork ==="
fd -t f -i 'licensee' webroot/src/pages/AuthCallback

Repository: csg-org/CompactConnect

Length of output: 1669


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== AuthCallback routes ==="
sed -n '55,110p' webroot/src/router/routes.ts

echo
echo "=== AuthCallback page tree ==="
find webroot/src/pages/AuthCallback -maxdepth 2 -type f | sort

echo
echo "=== Search for licensee callback paths ==="
rg -n '/auth/callback/licensee/(cosmo|socialwork|jcc)|LicenseeCosmo|LicenseeSocialWork' webroot/src -C 2

Repository: csg-org/CompactConnect

Length of output: 4025


Missing licensee callback routes for non-JCC modes

getHostedLoginUri can generate /auth/callback/licensee/cosmo and /auth/callback/licensee/socialwork, but the router only defines /auth/callback/licensee/jcc and there are no LicenseeCosmo or LicenseeSocialWork callback pages. Add those routes/pages or stop generating those LICENSEE paths.

🤖 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 `@webroot/src/utils/auth.ts` around lines 133 - 218, Resolve the LICENSEE
callback mismatch in getHostedLoginUri: either add the missing LicenseeCosmo and
LicenseeSocialWork callback pages and router entries for
/auth/callback/licensee/cosmo and /auth/callback/licensee/socialwork, or
restrict callback-path generation so LICENSEE only produces supported routes
such as /auth/callback/licensee/jcc.

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.

[Security/Medium] Cognito Hosted UI OAuth flow lacks PKCE and state CSRF validation

2 participants