fix(hasura-auth-js): transition to the signedOut state when the token is invalid or expired (#2835)

### **User description**
fixes https://github.com/nhost/nhost/issues/2817


___

### **PR Type**
Bug fix, Tests, Enhancement


___

### **Description**
- Added error handling logic to transition to the `signedOut` state when
the token is invalid or expired.
- Updated the authentication machine to handle 401 errors by signing out
the user.
- Enhanced test cases to verify the new behavior of signing out on
unauthorized errors.
- Updated Hasura page teardown logic to ensure the first matching
element is clicked.
- Added `micromatch` to the audit-ci allowlist for dependency
management.



___



### **Changes walkthrough** 📝
<table><thead><tr><th></th><th align="left">Relevant
files</th></tr></thead><tbody><tr><td><strong>Bug
fix</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>machine.ts</strong><dd><code>Add error handling for
unauthorized token refresh</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

packages/hasura-auth-js/src/machines/authentication/machine.ts

<li>Added error handling logic to transition to <code>signedOut</code>
state on <br>unauthorized error.<br> <li> Introduced a new condition
<code>isUnauthorizedError</code> to check for 401 <br>status.<br> <li>
Reordered imports for better organization.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2835/files#diff-a8fdfee087ad5a72ea0a64667e2a0c7f25baa84eaaf73ebfee3f5a5a1b7584d1">+10/-3</a>&nbsp;
&nbsp; </td>

</tr>                    
</table></td></tr><tr><td><strong>Tests</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>refreshToken.test.ts</strong><dd><code>Update token
refresh test for unauthorized error handling</code></dd></summary>
<hr>

packages/hasura-auth-js/tests/refreshToken.test.ts

<li>Updated test to expect sign out on unauthorized error during token
<br>refresh.<br> <li> Adjusted test logic to match new authentication
state transitions.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2835/files#diff-271b5a8899ade50e4876f5a50f06da16954125f50d16f28219598cff4e39344b">+3/-7</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>                    
</table></td></tr><tr><td><strong>Enhancement</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>global-teardown.ts</strong><dd><code>Update Hasura
locator to click first matching element</code>&nbsp; &nbsp; &nbsp;
&nbsp; </dd></summary>
<hr>

dashboard/global-teardown.ts

<li>Updated locator to click the first matching element for Hasura page
<br>teardown.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2835/files#diff-1ee3d64258c498cdfa30665ec61605ab817622c7dae2a09bd4b6b23606c13e9f">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>machine.typegen.ts</strong><dd><code>Update type
definitions for unauthorized error handling</code>&nbsp; &nbsp;
</dd></summary>
<hr>

packages/hasura-auth-js/src/machines/authentication/machine.typegen.ts

- Added `isUnauthorizedError` to type definitions.



</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2835/files#diff-b0050ab06a8f00d3ae5decd65565adb1bdae3b4b6d19d4f67b9013ffb14e18ee">+2/-0</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>                    
</table></td></tr><tr><td><strong>Documentation</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>silent-lies-smoke.md</strong><dd><code>Document bug fix
for invalid token handling</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></summary>
<hr>

.changeset/silent-lies-smoke.md

<li>Documented the bug fix for transitioning to <code>signedOut</code>
state on invalid <br>token.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2835/files#diff-f8d41906481f17db7208e2c154075e8679f222536c7958000e6f50f1f019aa01">+5/-0</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>                    
</table></td></tr><tr><td><strong>Configuration
changes</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>audit-ci.jsonc</strong><dd><code>Update audit-ci
allowlist with micromatch</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; </dd></summary>
<hr>

audit-ci.jsonc

- Added `micromatch` to the audit-ci allowlist.



</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2835/files#diff-4ede69da2a1704e53e08b8d647a315c202f037cc9277f16c94176d9622d261c6">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>                    
</table></td></tr></tr></tbody></table>

___

> 💡 **PR-Agent usage**:
>Comment `/help` on the PR to get a list of all available PR-Agent tools
and their descriptions
This commit is contained in:
Hassan Ben Jobrane
2024-08-23 09:29:33 +01:00
committed by GitHub
parent 40c0d7b914
commit caa8bd75ec
6 changed files with 22 additions and 12 deletions

View File

@@ -0,0 +1,5 @@
---
'@nhost/hasura-auth-js': patch
---
fix: add error handling logic to transition to the signedOut state when the token is invalid or expired

View File

@@ -2,5 +2,5 @@
// $schema provides code completion hints to IDEs.
"$schema": "https://github.com/IBM/audit-ci/raw/main/docs/schema.json",
"moderate": true,
"allowlist": ["vue-template-compiler"]
"allowlist": ["vue-template-compiler", "micromatch"]
}

View File

@@ -43,7 +43,7 @@ async function globalTeardown() {
await adminSecretInput.press('Enter');
// note: getByRole doesn't work here
await hasuraPage.locator('a', { hasText: /data/i }).click();
await hasuraPage.locator('a', { hasText: /data/i }).nth(0).click();
await hasuraPage.locator('[data-test="sql-link"]').click();
// Set the value of the Ace code editor using JavaScript evaluation in the browser context

View File

@@ -5,7 +5,7 @@ import type {
PublicKeyCredentialRequestOptionsJSON,
RegistrationCredentialJSON
} from '@simplewebauthn/typescript-types'
import { InterpreterFrom, assign, createMachine, send } from 'xstate'
import { assign, createMachine, InterpreterFrom, send } from 'xstate'
import {
NHOST_JWT_EXPIRES_AT_KEY,
NHOST_REFRESH_TOKEN_ID_KEY,
@@ -341,7 +341,13 @@ export const createAuthMachine = ({
actions: ['saveSession', 'resetTimer', 'reportTokenChanged'],
target: 'pending'
},
onError: [{ actions: 'saveRefreshAttempt', target: 'pending' }]
onError: [
{
cond: 'isUnauthorizedError',
target: '#nhost.authentication.signedOut'
},
{ actions: 'saveRefreshAttempt', target: 'pending' }
]
}
}
}
@@ -755,7 +761,8 @@ export const createAuthMachine = ({
// * Event guards
hasSession: (_, e) => !!e.data?.session,
hasMfaTicket: (_, e) => !!e.data?.mfa
hasMfaTicket: (_, e) => !!e.data?.mfa,
isUnauthorizedError: (_, { data: { error } }: any) => error.status === 401
},
services: {

View File

@@ -213,6 +213,7 @@ export interface Typegen0 {
| 'error.platform.authenticateWithPAT'
| 'error.platform.authenticateWithToken'
| 'error.platform.importRefreshToken'
| 'error.platform.refreshToken'
| 'error.platform.signInMfaTotp'
reportTokenChanged:
| 'SESSION_UPDATE'
@@ -305,6 +306,7 @@ export interface Typegen0 {
isAutoRefreshDisabled: ''
isRefreshTokenPAT: ''
isSignedIn: '' | 'error.platform.authenticateWithToken'
isUnauthorizedError: 'error.platform.refreshToken'
noToken: ''
refreshTimerShouldRefresh: ''
shouldRetryImportToken: 'error.platform.importRefreshToken'

View File

@@ -87,21 +87,17 @@ describe(`Time based token refresh`, () => {
server.resetHandlers()
})
test(`token refresh should fail if the signed-in user's refresh token was invalid`, async () => {
test(`token refresh should fail and sign out the user when the server returns an unauthorized error`, async () => {
server.use(authTokenUnauthorizedHandler)
// Fast forwarding to initial expiration date
vi.setSystemTime(initialExpiration)
await waitFor(authServiceWithInitialSession, (state) =>
const state = await waitFor(authServiceWithInitialSession, (state) =>
state.matches({ authentication: { signedIn: { refreshTimer: { running: 'refreshing' } } } })
)
const state = await waitFor(authServiceWithInitialSession, (state) =>
state.matches({ authentication: { signedIn: { refreshTimer: { running: 'pending' } } } })
)
expect(state.context.refreshTimer.attempts).toBeGreaterThan(0)
expect(state.matches({ authentication: 'signedOut' }))
})
test(`access token should always be refreshed when reaching the expiration margin`, async () => {