fix(login): reject expired JWT from local storage on startup - #3489
fix(login): reject expired JWT from local storage on startup#3489sinchubhat wants to merge 4 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
Adds startup-time validation for a persisted (localStorage) login session so the app won’t consider the user authenticated when the stored JWT is expired, reducing first-load 401s and redirecting the user to login as needed.
Changes:
- Replaces the constructor’s “any stored user => logged in” logic with a dedicated session restore path that checks JWT expiration.
- Hardens token retrieval from
localStorageagainst malformed JSON. - Adds unit tests covering session restore behavior for expired/malformed storage entries.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| src/app/auth.service.ts | Introduces restoreStoredSession() + JWT exp parsing to avoid restoring expired sessions from localStorage. |
| src/app/auth.service.spec.ts | Adds tests for stored-session restore behavior, including expired/corrupted storage scenarios. |
Suppressed comments (2)
src/app/auth.service.ts:140
- restoreStoredSession currently restores any stored session unless it can prove the token is expired. For malformed/non-JWT/empty tokens (or JWTs with an unreadable payload), this still marks the user logged in and allows protected routes to activate, so initial requests can still be sent with an invalid token and return 401—contradicting the PR description that unreadable tokens are discarded on startup.
const expiresAt = this.getTokenExpiration(token)
if (expiresAt != null && expiresAt <= Date.now()) {
console.warn(`[auth] discarded stored session: expired ${new Date(expiresAt).toISOString()}`)
localStorage.removeItem('loggedInUser')
return
src/app/auth.service.spec.ts:281
- These tests currently assert that non-JWT/empty tokens are treated as a valid restored session. That keeps protected routes open and allows initial API calls to be made with an invalid token (leading to 401s), which conflicts with the PR goal of discarding unreadable tokens on startup.
it('should restore a session whose token is not a JWT', () => {
const restored = createServiceWithStoredSession(JSON.stringify({ token: 'not-a-jwt' }))
expect(restored.isLoggedIn).toBeTrue()
expect(localStorage.getItem('loggedInUser')).not.toBeNull()
})
it('should restore a session whose token is empty', () => {
const restored = createServiceWithStoredSession(JSON.stringify({ token: '' }))
expect(restored.isLoggedIn).toBeTrue()
expect(localStorage.getItem('loggedInUser')).not.toBeNull()
})
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.
Suppressed comments (3)
src/app/auth.service.ts:129
restoreStoredSession()assumesJSON.parse(stored).tokenis a string. If the JSON is valid buttokenis not a string,getTokenExpiration(token)will throw at runtime (token.split is not a function) during app startup. Add a type check and discard the stored session whentokenisn’t a string.
let token: string
try {
token = JSON.parse(stored).token ?? ''
} catch {
console.warn('[auth] discarded stored session: unreadable')
src/app/auth.service.ts:136
- PR description says “unreadable or expired token is discarded and the user is routed to login”, but the current restore logic only discards (1) unreadable localStorage JSON or (2) tokens with a readable
expthat is expired. Tokens that are malformed JWTs / missingexp/ empty are explicitly kept (and covered by the new tests), which can still produce an initial 401 in those cases. Please either update the PR description/linked issue expectations to match this behavior, or change the restore logic (and tests) to also discard unreadable token strings on startup.
// Only a provably expired token is dropped here. When the expiration cannot be read the
// session is kept - the server answers 401 and errorHandlingInterceptor logs the user out.
const expiresAt = this.getTokenExpiration(token)
src/app/auth.service.ts:113
getLoggedUserToken()assumesloggedInUser.tokenis a string. If localStorage is tampered with (e.g.,tokenis an object/number), this method will return a non-string value despite thestringreturn type, which can break callers that build headers or do string operations. Parse into anunknownand type-check before returning.
This issue also appears on line 125 of the same file.
try {
const token: string = JSON.parse(loggedInUser).token
return token ?? ''
} catch {
return ''
}
|
I have tested using both cloud and console. Tested using both curl command and via manual browser test as mentioned in PR description. |
Report a session timeout for any 401 on an authenticated route, not only when the body matches Kong's wording, so the Console gateway no longer logs the user out with no message. Guard the body access that threw on an empty 401, and keep concurrent failures from stacking a dialog each. Leave the login call alone, since it answers 401 for bad credentials and the login page reports that itself. Vet a session restored from local storage with one protected call at startup, so routes that issue no request of their own cannot render behind a token the server has already rejected. Resolves: device-management-toolkit/console#993
4fd6c41 to
6436248
Compare
…ample-web-ui into fix/jwt-expiration
…ample-web-ui into fix/jwt-expiration
…ample-web-ui into fix/jwt-expiration
Validate the exp claim before restoring a persisted session, so protected requests are no longer sent with a stale token on first load. An unreadable or expired token is discarded and the user is routed to login instead of receiving a 401.
Resolves: device-management-toolkit/console#993
Testing with Console
Backend API Tests (curl)
Test 1: Fresh Valid Token
Login and get token
Test with backend
Expected: HTTP 200 {"totalCount":0,"connectedCount":0,"disconnectedCount":0}
Test 2: Expired Token (10 minutes ago)
Create expired token
Test with backend
Expected: HTTP 401 {"error":"invalid access token"}
Test 3: Invalid/No Token
Malformed token
Expected: HTTP 401 {"error":"invalid access token"}
No token
curl -sk "$BASE/api/v1/devices/stats"Expected: HTTP 401 {"error":"request does not contain an access token"}
Testing with Cloud
Kong does the JWT validation and not MPS
Endpoints through Kong: /mps/login/api/v1/authorize and /mps/api/v1/devices/stats
cloud base url should be https://ipaddress:443/ and not http://localhost:3000
git clone -b v2 --recursive https://github.com/device-management-toolkit/deployment.git deployment-v2 cd deployment-v2 cp .env.template .envEdit .env and kong.yaml as mentioned in documentation https://device-management-toolkit.github.io/docs/2.36/GetStarted/Cloud/setup/
Testing via curl cmds
Test 1: Login and get valid token
Test 2: Valid token - should work
Expected: {"totalCount":0,"connectedCount":0,"disconnectedCount":0}
Test 3: Expired token (10 minutes ago) - should fail
Expected: HTTP 401 {"message":"No mandatory 'iss' in claims"}
Test 4: Malformed token - should fail
Expected: HTTP 401 {"message":"Bad token; invalid JSON"}
Test 5: No token - should fail
curl -sk "$BASE/mps/api/v1/devices/stats"Expected: HTTP 401 {"message": "Unauthorized"}
Manual Browser Tests
Test 1: Valid Token (Should Stay Logged In)
http://localhost:4200Expected: Stays logged in, dashboard loads
Test 2: Expired Token (Should Redirect to Login)
What this does: Creates token expired 10 minutes ago
Expected:
/loginimmediatelyHelper:
Check Current Token Status
To see your token expiration, press F12 → Console → Run:
Debugging:
Monitor Network
/api/v1/devices/statsPR Checklist
What are you changing?
Anything the reviewer should know when reviewing this PR?
If the there are associated PRs in other repositories, please link them here (i.e. device-management-toolkit/repo#365 )