@@ -16,12 +16,13 @@ import type { Logger } from "../logging/logger";
1616
1717type LoginResult =
1818 | { success : false }
19- | { success : true ; user ? : User ; token : string } ;
19+ | { success : true ; user : User ; token : string } ;
2020
2121interface LoginOptions {
2222 safeHostname : string ;
2323 url : string | undefined ;
2424 autoLogin ?: boolean ;
25+ token ?: string ;
2526}
2627
2728/**
@@ -49,6 +50,7 @@ export class LoginCoordinator {
4950 const result = await this . attemptLogin (
5051 { safeHostname, url } ,
5152 options . autoLogin ?? false ,
53+ options . token ,
5254 ) ;
5355
5456 await this . persistSessionAuth ( result , safeHostname , url ) ;
@@ -95,6 +97,7 @@ export class LoginCoordinator {
9597 const result = await this . attemptLogin (
9698 { url : newUrl , safeHostname } ,
9799 false ,
100+ options . token ,
98101 ) ;
99102
100103 await this . persistSessionAuth ( result , safeHostname , newUrl ) ;
@@ -168,10 +171,17 @@ export class LoginCoordinator {
168171 const promise = new Promise < LoginResult > ( ( resolve ) => {
169172 disposable = this . secretsManager . onDidChangeSessionAuth (
170173 safeHostname ,
171- ( auth ) => {
174+ async ( auth ) => {
172175 if ( auth ?. token ) {
173176 disposable ?. dispose ( ) ;
174- resolve ( { success : true , token : auth . token } ) ;
177+ const client = CoderApi . create ( auth . url , auth . token , this . logger ) ;
178+ try {
179+ const user = await client . getAuthenticatedUser ( ) ;
180+ resolve ( { success : true , token : auth . token , user } ) ;
181+ } catch {
182+ // Token from other window was invalid, ignore and keep waiting
183+ // (or user can click Login/Cancel in the dialog)
184+ }
175185 }
176186 } ,
177187 ) ;
@@ -191,54 +201,93 @@ export class LoginCoordinator {
191201 private async attemptLogin (
192202 deployment : Deployment ,
193203 isAutoLogin : boolean ,
204+ providedToken ?: string ,
194205 ) : Promise < LoginResult > {
195- const needsToken = needToken ( vscode . workspace . getConfiguration ( ) ) ;
196206 const client = CoderApi . create ( deployment . url , "" , this . logger ) ;
197207
198- let storedToken : string | undefined ;
199- if ( needsToken ) {
200- const auth = await this . secretsManager . getSessionAuth (
201- deployment . safeHostname ,
208+ // mTLS authentication (no token needed)
209+ if ( ! needToken ( vscode . workspace . getConfiguration ( ) ) ) {
210+ return this . tryMtlsAuth ( client , isAutoLogin ) ;
211+ }
212+
213+ // Try provided token first
214+ if ( providedToken ) {
215+ const result = await this . tryTokenAuth (
216+ client ,
217+ providedToken ,
218+ isAutoLogin ,
202219 ) ;
203- storedToken = auth ?. token ;
204- if ( storedToken ) {
205- client . setSessionToken ( storedToken ) ;
220+ if ( result !== "retry" ) {
221+ return result ;
206222 }
207223 }
208224
209- // Attempt authentication with current credentials (token or mTLS)
210- try {
211- if ( ! needsToken || storedToken ) {
212- const user = await client . getAuthenticatedUser ( ) ;
213- // Return the token that was used (empty string for mTLS since
214- // the `vscodessh` command currently always requires a token file)
215- return { success : true , token : storedToken ?? "" , user } ;
225+ // Try stored token (skip if same as provided)
226+ const auth = await this . secretsManager . getSessionAuth (
227+ deployment . safeHostname ,
228+ ) ;
229+ if ( auth ?. token && auth . token !== providedToken ) {
230+ const result = await this . tryTokenAuth ( client , auth . token , isAutoLogin ) ;
231+ if ( result !== "retry" ) {
232+ return result ;
216233 }
234+ }
235+
236+ // Prompt user for token
237+ return this . loginWithToken ( client ) ;
238+ }
239+
240+ private async tryMtlsAuth (
241+ client : CoderApi ,
242+ isAutoLogin : boolean ,
243+ ) : Promise < LoginResult > {
244+ try {
245+ const user = await client . getAuthenticatedUser ( ) ;
246+ return { success : true , token : "" , user } ;
217247 } catch ( err ) {
218- const is401 = isAxiosError ( err ) && err . response ?. status === 401 ;
219- if ( needsToken && is401 ) {
220- // For token auth with 401: silently continue to prompt for new credentials
221- } else {
222- // For mTLS or non-401 errors: show error and abort
223- const message = getErrorMessage ( err , "no response from the server" ) ;
224- if ( isAutoLogin ) {
225- this . logger . warn ( "Failed to log in to Coder server:" , message ) ;
226- } else {
227- this . vscodeProposed . window . showErrorMessage (
228- "Failed to log in to Coder server" ,
229- {
230- detail : message ,
231- modal : true ,
232- useCustom : true ,
233- } ,
234- ) ;
235- }
236- return { success : false } ;
248+ this . showAuthError ( err , isAutoLogin ) ;
249+ return { success : false } ;
250+ }
251+ }
252+
253+ /**
254+ * Returns 'retry' on 401 to signal trying next token.
255+ */
256+ private async tryTokenAuth (
257+ client : CoderApi ,
258+ token : string ,
259+ isAutoLogin : boolean ,
260+ ) : Promise < LoginResult | "retry" > {
261+ client . setSessionToken ( token ) ;
262+ try {
263+ const user = await client . getAuthenticatedUser ( ) ;
264+ return { success : true , token, user } ;
265+ } catch ( err ) {
266+ if ( isAxiosError ( err ) && err . response ?. status === 401 ) {
267+ return "retry" ;
237268 }
269+ this . showAuthError ( err , isAutoLogin ) ;
270+ return { success : false } ;
238271 }
272+ }
239273
240- const result = await this . loginWithToken ( client ) ;
241- return result ;
274+ /**
275+ * Shows auth error via dialog or logs it for autoLogin.
276+ */
277+ private showAuthError ( err : unknown , isAutoLogin : boolean ) : void {
278+ const message = getErrorMessage ( err , "no response from the server" ) ;
279+ if ( isAutoLogin ) {
280+ this . logger . warn ( "Failed to log in to Coder server:" , message ) ;
281+ } else {
282+ this . vscodeProposed . window . showErrorMessage (
283+ "Failed to log in to Coder server" ,
284+ {
285+ detail : message ,
286+ modal : true ,
287+ useCustom : true ,
288+ } ,
289+ ) ;
290+ }
242291 }
243292
244293 /**
0 commit comments