Skip to content

Commit 2d2991b

Browse files
authored
fix(auth): detect stale credentials at startup via empty /account response (#331)
* fix(auth): detect stale credentials at startup via empty /account response (#330) getCurrentUser hit /api/core/public/account, which is public and answers a 200 with an empty body for an unauthenticated request rather than a 401. response.json() then threw a SyntaxError, which startup validation (checkExistingAuthentication) treated as a transient failure and kept the invalid token. Read the body as text and surface an empty 200 as ApiError(401) so the existing 401 branch clears the stale credentials. The 401 is only raised after a successful HTTP response, so genuine transient/network errors still throw earlier in makeRequest and credentials are kept. * test(auth): cover whitespace-only /account body as unauthenticated (#330)
1 parent 00fc90a commit 2d2991b

3 files changed

Lines changed: 49 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ All notable changes to the Artemis VS Code extension will be documented in this
99
- **WebSocket status bar:** Removed the `artemis.showWebSocketStatusBar` setting. The connection indicator now appears automatically only when there is a problem; enable `artemis.developerMode` to keep it always visible with full diagnostics on hover. When the connection drops, students now see a plain-language explanation (no "WS" jargon) instead of a technical label.
1010
- **Server URL change:** Removed the manual "Clear Credentials" prompts that appeared when the Artemis server URL changed. Changing the server while logged in now logs you out automatically and returns you to the login view (a session is not valid across servers); the logout command remains for clearing credentials on demand.
1111

12+
### Fixed
13+
14+
- **Stale credentials at startup:** Credentials that are no longer valid on the configured Artemis server are now reliably detected during startup validation and cleared, instead of lingering until a later request fails.
15+
1216
## [0.4.8] - 2026-06-24
1317

1418
### Changed

extension/src/extension/api/artemisApi.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,15 @@ export class ArtemisApiService {
155155
// Get current user information
156156
async getCurrentUser(): Promise<ArtemisUser> {
157157
const response = await this.makeRequest('/api/core/public/account');
158-
return parseArtemisUser(await response.json());
158+
const body = (await response.text()).trim();
159+
if (!body) {
160+
// /api/core/public/account is public: an unauthenticated request gets a
161+
// 200 with an empty body rather than a 401. Surface it as a 401 so callers
162+
// (notably startup credential validation) clear the stale token instead of
163+
// treating the empty response as a transient failure and keeping it.
164+
throw new ApiError('Not authenticated', 401);
165+
}
166+
return parseArtemisUser(JSON.parse(body));
159167
}
160168

161169
// Get archived courses (inactive courses from previous semesters)

extension/test/unit/api/artemisApi.test.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ suite('Artemis API Service Test Suite', () => {
5555
return {
5656
ok: true,
5757
status: 200,
58-
json: async () => mockUser,
58+
text: async () => JSON.stringify(mockUser),
5959
} as any;
6060
};
6161

@@ -65,6 +65,41 @@ suite('Artemis API Service Test Suite', () => {
6565
assert.strictEqual(user.login, 'test');
6666
});
6767

68+
test('treats a 200 account response with an empty body as unauthenticated (401)', async () => {
69+
// /api/core/public/account is public: an unauthenticated request gets a 200
70+
// with an empty body rather than a 401. getCurrentUser must surface this as a
71+
// 401 so startup credential validation clears the stale token.
72+
global.fetch = async () => ({
73+
ok: true,
74+
status: 200,
75+
text: async () => '',
76+
} as any);
77+
78+
try {
79+
await apiService.getCurrentUser();
80+
assert.fail('Should have thrown error');
81+
} catch (error: unknown) {
82+
assert.ok(error instanceof ApiError);
83+
assert.strictEqual((error as ApiError).status, 401);
84+
}
85+
});
86+
87+
test('treats a 200 account response with a whitespace-only body as unauthenticated (401)', async () => {
88+
global.fetch = async () => ({
89+
ok: true,
90+
status: 200,
91+
text: async () => ' \n',
92+
} as any);
93+
94+
try {
95+
await apiService.getCurrentUser();
96+
assert.fail('Should have thrown error');
97+
} catch (error: unknown) {
98+
assert.ok(error instanceof ApiError);
99+
assert.strictEqual((error as ApiError).status, 401);
100+
}
101+
});
102+
68103
test('should handle 401 error', async () => {
69104
global.fetch = async () => ({
70105
ok: false,

0 commit comments

Comments
 (0)