Skip to content

Commit 423742c

Browse files
committed
Rely on the secrets API + Refactorings
1 parent 125dd85 commit 423742c

File tree

13 files changed

+561
-627
lines changed

13 files changed

+561
-627
lines changed

src/api/coderApi.ts

Lines changed: 2 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ import {
33
type AxiosInstance,
44
type AxiosHeaders,
55
type AxiosResponseTransformer,
6-
isAxiosError,
7-
type AxiosError,
86
} from "axios";
97
import { Api } from "coder/site/src/api/api";
108
import {
@@ -33,8 +31,6 @@ import {
3331
HttpClientLogLevel,
3432
} from "../logging/types";
3533
import { sizeOf } from "../logging/utils";
36-
import { parseOAuthError, requiresReAuthentication } from "../oauth/errors";
37-
import { type OAuthSessionManager } from "../oauth/sessionManager";
3834
import { HttpStatusCode } from "../websocket/codes";
3935
import {
4036
type UnidirectionalStream,
@@ -76,15 +72,14 @@ export class CoderApi extends Api {
7672
baseUrl: string,
7773
token: string | undefined,
7874
output: Logger,
79-
oauthSessionManager?: OAuthSessionManager,
8075
): CoderApi {
8176
const client = new CoderApi(output);
8277
client.setHost(baseUrl);
8378
if (token) {
8479
client.setSessionToken(token);
8580
}
8681

87-
setupInterceptors(client, output, oauthSessionManager);
82+
setupInterceptors(client, output);
8883
return client;
8984
}
9085

@@ -395,11 +390,7 @@ export class CoderApi extends Api {
395390
/**
396391
* Set up logging and request interceptors for the CoderApi instance.
397392
*/
398-
function setupInterceptors(
399-
client: CoderApi,
400-
output: Logger,
401-
oauthSessionManager?: OAuthSessionManager,
402-
): void {
393+
function setupInterceptors(client: CoderApi, output: Logger): void {
403394
addLoggingInterceptors(client.getAxiosInstance(), output);
404395

405396
client.getAxiosInstance().interceptors.request.use(async (config) => {
@@ -437,11 +428,6 @@ function setupInterceptors(
437428
}
438429
},
439430
);
440-
441-
// OAuth token refresh interceptors
442-
if (oauthSessionManager) {
443-
addOAuthInterceptors(client, output, oauthSessionManager);
444-
}
445431
}
446432

447433
function addLoggingInterceptors(client: AxiosInstance, logger: Logger) {
@@ -487,116 +473,6 @@ function addLoggingInterceptors(client: AxiosInstance, logger: Logger) {
487473
);
488474
}
489475

490-
/**
491-
* Add OAuth token refresh interceptors.
492-
* Success interceptor: proactively refreshes token when approaching expiry.
493-
* Error interceptor: reactively refreshes token on 401 responses.
494-
*/
495-
function addOAuthInterceptors(
496-
client: CoderApi,
497-
logger: Logger,
498-
oauthSessionManager: OAuthSessionManager,
499-
) {
500-
client.getAxiosInstance().interceptors.response.use(
501-
// Success response interceptor: proactive token refresh
502-
(response) => {
503-
if (oauthSessionManager.shouldRefreshToken()) {
504-
logger.debug(
505-
"Token approaching expiry, triggering proactive refresh in background",
506-
);
507-
508-
// Fire-and-forget: don't await, don't block response
509-
oauthSessionManager.refreshToken().catch((error) => {
510-
logger.warn("Background token refresh failed:", error);
511-
});
512-
}
513-
514-
return response;
515-
},
516-
// Error response interceptor: reactive token refresh on 401
517-
async (error: unknown) => {
518-
if (!isAxiosError(error)) {
519-
throw error;
520-
}
521-
522-
if (error.config) {
523-
const config = error.config as {
524-
_oauthRetryAttempted?: boolean;
525-
};
526-
if (config._oauthRetryAttempted) {
527-
throw error;
528-
}
529-
}
530-
531-
const status = error.response?.status;
532-
533-
// These could indicate permanent auth failures that won't be fixed by token refresh
534-
if (status === 400 || status === 403) {
535-
handlePossibleOAuthError(error, logger, oauthSessionManager);
536-
throw error;
537-
} else if (status === 401) {
538-
return handle401Error(error, client, logger, oauthSessionManager);
539-
}
540-
541-
throw error;
542-
},
543-
);
544-
}
545-
546-
function handlePossibleOAuthError(
547-
error: unknown,
548-
logger: Logger,
549-
oauthSessionManager: OAuthSessionManager,
550-
): void {
551-
const oauthError = parseOAuthError(error);
552-
if (oauthError && requiresReAuthentication(oauthError)) {
553-
logger.error(
554-
`OAuth error requires re-authentication: ${oauthError.errorCode}`,
555-
);
556-
557-
oauthSessionManager.showReAuthenticationModal(oauthError).catch((err) => {
558-
logger.error("Failed to show re-auth modal:", err);
559-
});
560-
}
561-
}
562-
563-
async function handle401Error(
564-
error: AxiosError,
565-
client: CoderApi,
566-
logger: Logger,
567-
oauthSessionManager: OAuthSessionManager,
568-
): Promise<void> {
569-
if (!oauthSessionManager.isLoggedInWithOAuth()) {
570-
throw error;
571-
}
572-
573-
logger.info("Received 401 response, attempting token refresh");
574-
575-
try {
576-
const newTokens = await oauthSessionManager.refreshToken();
577-
client.setSessionToken(newTokens.access_token);
578-
579-
logger.info("Token refresh successful, retrying request");
580-
581-
// Retry the original request with the new token
582-
if (error.config) {
583-
const config = error.config as RequestConfigWithMeta & {
584-
_oauthRetryAttempted?: boolean;
585-
};
586-
config._oauthRetryAttempted = true;
587-
config.headers[coderSessionTokenHeader] = newTokens.access_token;
588-
return client.getAxiosInstance().request(config);
589-
}
590-
591-
throw error;
592-
} catch (refreshError) {
593-
logger.error("Token refresh failed:", refreshError);
594-
595-
handlePossibleOAuthError(refreshError, logger, oauthSessionManager);
596-
throw error;
597-
}
598-
}
599-
600476
function wrapRequestTransform(
601477
transformer: AxiosResponseTransformer | AxiosResponseTransformer[],
602478
config: RequestConfigWithMeta,

src/api/oauthInterceptors.ts

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import { type AxiosError, isAxiosError } from "axios";
2+
3+
import { type Logger } from "../logging/logger";
4+
import { type RequestConfigWithMeta } from "../logging/types";
5+
import { parseOAuthError, requiresReAuthentication } from "../oauth/errors";
6+
import { type OAuthSessionManager } from "../oauth/sessionManager";
7+
8+
import { type CoderApi } from "./coderApi";
9+
10+
const coderSessionTokenHeader = "Coder-Session-Token";
11+
12+
/**
13+
* Attach OAuth token refresh interceptors to a CoderApi instance.
14+
* This should be called after creating the CoderApi when OAuth authentication is being used.
15+
*
16+
* Success interceptor: proactively refreshes token when approaching expiry.
17+
* Error interceptor: reactively refreshes token on 401 responses.
18+
*/
19+
export function attachOAuthInterceptors(
20+
client: CoderApi,
21+
logger: Logger,
22+
oauthSessionManager: OAuthSessionManager,
23+
): void {
24+
client.getAxiosInstance().interceptors.response.use(
25+
// Success response interceptor: proactive token refresh
26+
(response) => {
27+
if (oauthSessionManager.shouldRefreshToken()) {
28+
logger.debug(
29+
"Token approaching expiry, triggering proactive refresh in background",
30+
);
31+
32+
// Fire-and-forget: don't await, don't block response
33+
oauthSessionManager.refreshToken().catch((error) => {
34+
logger.warn("Background token refresh failed:", error);
35+
});
36+
}
37+
38+
return response;
39+
},
40+
// Error response interceptor: reactive token refresh on 401
41+
async (error: unknown) => {
42+
if (!isAxiosError(error)) {
43+
throw error;
44+
}
45+
46+
if (error.config) {
47+
const config = error.config as {
48+
_oauthRetryAttempted?: boolean;
49+
};
50+
if (config._oauthRetryAttempted) {
51+
throw error;
52+
}
53+
}
54+
55+
const status = error.response?.status;
56+
57+
// These could indicate permanent auth failures that won't be fixed by token refresh
58+
if (status === 400 || status === 403) {
59+
handlePossibleOAuthError(error, logger, oauthSessionManager);
60+
throw error;
61+
} else if (status === 401) {
62+
return handle401Error(error, client, logger, oauthSessionManager);
63+
}
64+
65+
throw error;
66+
},
67+
);
68+
}
69+
70+
function handlePossibleOAuthError(
71+
error: unknown,
72+
logger: Logger,
73+
oauthSessionManager: OAuthSessionManager,
74+
): void {
75+
const oauthError = parseOAuthError(error);
76+
if (oauthError && requiresReAuthentication(oauthError)) {
77+
logger.error(
78+
`OAuth error requires re-authentication: ${oauthError.errorCode}`,
79+
);
80+
81+
oauthSessionManager.showReAuthenticationModal(oauthError).catch((err) => {
82+
logger.error("Failed to show re-auth modal:", err);
83+
});
84+
}
85+
}
86+
87+
async function handle401Error(
88+
error: AxiosError,
89+
client: CoderApi,
90+
logger: Logger,
91+
oauthSessionManager: OAuthSessionManager,
92+
): Promise<void> {
93+
if (!oauthSessionManager.isLoggedInWithOAuth()) {
94+
throw error;
95+
}
96+
97+
logger.info("Received 401 response, attempting token refresh");
98+
99+
try {
100+
const newTokens = await oauthSessionManager.refreshToken();
101+
client.setSessionToken(newTokens.access_token);
102+
103+
logger.info("Token refresh successful, retrying request");
104+
105+
// Retry the original request with the new token
106+
if (error.config) {
107+
const config = error.config as RequestConfigWithMeta & {
108+
_oauthRetryAttempted?: boolean;
109+
};
110+
config._oauthRetryAttempted = true;
111+
config.headers[coderSessionTokenHeader] = newTokens.access_token;
112+
return client.getAxiosInstance().request(config);
113+
}
114+
115+
throw error;
116+
} catch (refreshError) {
117+
logger.error("Token refresh failed:", refreshError);
118+
119+
handlePossibleOAuthError(refreshError, logger, oauthSessionManager);
120+
throw error;
121+
}
122+
}

src/commands.ts

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,6 @@ export class Commands {
6868
*/
6969
public async login(args?: {
7070
url?: string;
71-
token?: string;
7271
label?: string;
7372
autoLogin?: boolean;
7473
}): Promise<void> {
@@ -89,8 +88,7 @@ export class Commands {
8988
this.logger.info("Using deployment label", label);
9089

9190
const result = await this.loginCoordinator.promptForLogin({
92-
url,
93-
label,
91+
deployment: { url, label },
9492
autoLogin: args?.autoLogin,
9593
oauthSessionManager: this.oauthSessionManager,
9694
});
@@ -105,14 +103,11 @@ export class Commands {
105103

106104
// Store for later sessions
107105
await this.mementoManager.setUrl(url);
108-
await this.secretsManager.setSessionToken(label, {
106+
await this.secretsManager.setSessionAuth(label, {
109107
url,
110-
sessionToken: result.token,
108+
token: result.token,
111109
});
112110

113-
// Store on disk for CLI
114-
await this.cliManager.configure(label, url, result.token);
115-
116111
// Update contexts
117112
this.contextManager.set("coder.authenticated", true);
118113
if (result.user.roles.some((role) => role.name === "owner")) {
@@ -195,7 +190,7 @@ export class Commands {
195190

196191
// Clear from memory.
197192
await this.mementoManager.setUrl(undefined);
198-
await this.secretsManager.setSessionToken(label, undefined);
193+
await this.secretsManager.clearAllAuthData(label);
199194

200195
this.contextManager.set("coder.authenticated", false);
201196
vscode.window

0 commit comments

Comments
 (0)