Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit8ce9114

Browse files
committed
Hook up into the login/logout logic of the extension
1 parent4b58ea4 commit8ce9114

File tree

5 files changed

+252
-109
lines changed

5 files changed

+252
-109
lines changed

‎package.json‎

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -255,16 +255,6 @@
255255
"title":"Search",
256256
"category":"Coder",
257257
"icon":"$(search)"
258-
},
259-
{
260-
"command":"coder.oauth.login",
261-
"title":"OAuth Login",
262-
"category":"Coder"
263-
},
264-
{
265-
"command":"coder.oauth.logout",
266-
"title":"OAuth Logout",
267-
"category":"Coder"
268258
}
269259
],
270260
"menus": {

‎src/commands.ts‎

Lines changed: 180 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { type SecretsManager } from "./core/secretsManager";
1919
import{CertificateError}from"./error";
2020
import{getGlobalFlags}from"./globalFlags";
2121
import{typeLogger}from"./logging/logger";
22+
import{typeCoderOAuthHelper}from"./oauth/oauthHelper";
2223
import{escapeCommandArg,toRemoteAuthority,toSafeHost}from"./util";
2324
import{
2425
AgentTreeItem,
@@ -48,6 +49,7 @@ export class Commands {
4849
publicconstructor(
4950
serviceContainer:ServiceContainer,
5051
privatereadonlyrestClient:Api,
52+
privatereadonlyoauthHelper:CoderOAuthHelper,
5153
){
5254
this.vscodeProposed=serviceContainer.getVsCodeProposed();
5355
this.logger=serviceContainer.getLogger();
@@ -182,59 +184,119 @@ export class Commands {
182184
}
183185

184186
/**
185-
* Log into the provided deployment. If the deployment URL is not specified,
186-
* ask for it first with a menu showing recent URLs along with the default URL
187-
* and CODER_URL, if those are set.
187+
* Check if server supports OAuth by attempting to fetch the well-known endpoint.
188188
*/
189-
publicasynclogin(args?:{
190-
url?:string;
191-
token?:string;
192-
label?:string;
193-
autoLogin?:boolean;
194-
}):Promise<void>{
195-
if(this.contextManager.get("coder.authenticated")){
196-
return;
189+
privateasynccheckOAuthSupport(client:CoderApi):Promise<boolean>{
190+
try{
191+
awaitclient
192+
.getAxiosInstance()
193+
.get("/.well-known/oauth-authorization-server");
194+
this.logger.debug("Server supports OAuth");
195+
returntrue;
196+
}catch(error){
197+
this.logger.debug("Server does not support OAuth:",error);
198+
returnfalse;
197199
}
198-
this.logger.info("Logging in");
200+
}
199201

200-
consturl=awaitthis.maybeAskUrl(args?.url);
201-
if(!url){
202-
return;// The user aborted.
203-
}
202+
/**
203+
* Ask user to choose between OAuth and legacy API token authentication.
204+
*/
205+
privateasyncaskAuthMethod():Promise<"oauth"|"legacy"|undefined>{
206+
constchoice=awaitvscode.window.showQuickPick(
207+
[
208+
{
209+
label:"$(key) OAuth (Recommended)",
210+
detail:"Secure authentication with automatic token refresh",
211+
value:"oauth",
212+
},
213+
{
214+
label:"$(lock) API Token",
215+
detail:"Use a manually created API key",
216+
value:"legacy",
217+
},
218+
],
219+
{
220+
title:"Choose Authentication Method",
221+
placeHolder:"How would you like to authenticate?",
222+
ignoreFocusOut:true,
223+
},
224+
);
204225

205-
// It is possible that we are trying to log into an old-style host, in which
206-
// case we want to write with the provided blank label instead of generating
207-
// a host label.
208-
constlabel=args?.label===undefined ?toSafeHost(url) :args.label;
226+
returnchoice?.valueas"oauth"|"legacy"|undefined;
227+
}
209228

210-
// Try to get a token from the user, if we need one, and their user.
211-
constautoLogin=args?.autoLogin===true;
212-
constres=awaitthis.maybeAskToken(url,args?.token,autoLogin);
213-
if(!res){
214-
return;// The user aborted, or unable to auth.
229+
/**
230+
* Authenticate using OAuth flow.
231+
* Returns the access token and authenticated user, or null if failed/cancelled.
232+
*/
233+
privateasyncloginWithOAuth(
234+
url:string,
235+
):Promise<{user:User;token:string}|null>{
236+
try{
237+
this.logger.info("Starting OAuth authentication");
238+
239+
// Start OAuth authorization flow
240+
const{ code, verifier}=awaitthis.oauthHelper.startAuthorization(url);
241+
242+
// Exchange authorization code for tokens
243+
consttokenResponse=awaitthis.oauthHelper.exchangeToken(
244+
code,
245+
verifier,
246+
);
247+
248+
// Validate token by fetching user
249+
constclient=CoderApi.create(
250+
url,
251+
tokenResponse.access_token,
252+
this.logger,
253+
);
254+
constuser=awaitclient.getAuthenticatedUser();
255+
256+
this.logger.info("OAuth authentication successful");
257+
258+
return{
259+
token:tokenResponse.access_token,
260+
user,
261+
};
262+
}catch(error){
263+
this.logger.error("OAuth authentication failed:",error);
264+
vscode.window.showErrorMessage(
265+
`OAuth authentication failed:${getErrorMessage(error,"Unknown error")}`,
266+
);
267+
returnnull;
215268
}
269+
}
216270

217-
// The URL is good and the token is either good or not required; authorize
218-
// the global client.
271+
/**
272+
* Complete the login process by storing credentials and updating context.
273+
*/
274+
privateasynccompleteLogin(
275+
url:string,
276+
label:string,
277+
token:string,
278+
user:User,
279+
):Promise<void>{
280+
// Authorize the global client
219281
this.restClient.setHost(url);
220-
this.restClient.setSessionToken(res.token);
282+
this.restClient.setSessionToken(token);
221283

222-
// Storethese to be used inlater sessions.
284+
// Storeforlater sessions
223285
awaitthis.mementoManager.setUrl(url);
224-
awaitthis.secretsManager.setSessionToken(res.token);
286+
awaitthis.secretsManager.setSessionToken(token);
225287

226-
// Store on diskto be used by the cli.
227-
awaitthis.cliManager.configure(label,url,res.token);
288+
// Store on diskfor CLI
289+
awaitthis.cliManager.configure(label,url,token);
228290

229-
//These contexts control various menu items and the sidebar.
291+
//Update contexts
230292
this.contextManager.set("coder.authenticated",true);
231-
if(res.user.roles.find((role)=>role.name==="owner")){
293+
if(user.roles.find((role)=>role.name==="owner")){
232294
this.contextManager.set("coder.isOwner",true);
233295
}
234296

235297
vscode.window
236298
.showInformationMessage(
237-
`Welcome to Coder,${res.user.username}!`,
299+
`Welcome to Coder,${user.username}!`,
238300
{
239301
detail:
240302
"You can now use the Coder extension to manage your Coder instance.",
@@ -252,6 +314,73 @@ export class Commands {
252314
vscode.commands.executeCommand("coder.refreshWorkspaces");
253315
}
254316

317+
/**
318+
* Log into the provided deployment. If the deployment URL is not specified,
319+
* ask for it first with a menu showing recent URLs along with the default URL
320+
* and CODER_URL, if those are set.
321+
*/
322+
publicasynclogin(args?:{
323+
url?:string;
324+
token?:string;
325+
label?:string;
326+
autoLogin?:boolean;
327+
}):Promise<void>{
328+
if(this.contextManager.get("coder.authenticated")){
329+
return;
330+
}
331+
this.logger.info("Logging in");
332+
333+
consturl=awaitthis.maybeAskUrl(args?.url);
334+
if(!url){
335+
return;// The user aborted.
336+
}
337+
338+
constlabel=args?.label??toSafeHost(url);
339+
constautoLogin=args?.autoLogin===true;
340+
341+
// Check if we have an existing valid legacy token
342+
constexistingToken=awaitthis.secretsManager.getSessionToken();
343+
constclient=CoderApi.create(url,existingToken,this.logger);
344+
if(existingToken&&!args?.token){
345+
try{
346+
constuser=awaitclient.getAuthenticatedUser();
347+
this.logger.info("Using existing valid session token");
348+
awaitthis.completeLogin(url,label,existingToken,user);
349+
return;
350+
}catch{
351+
this.logger.debug("Existing token invalid, clearing it");
352+
awaitthis.secretsManager.setSessionToken();
353+
}
354+
}
355+
356+
// Check if server supports OAuth
357+
constsupportsOAuth=awaitthis.checkOAuthSupport(client);
358+
359+
if(supportsOAuth&&!autoLogin){
360+
constchoice=awaitthis.askAuthMethod();
361+
if(!choice){
362+
return;
363+
}
364+
365+
if(choice==="oauth"){
366+
constres=awaitthis.loginWithOAuth(url);
367+
if(!res){
368+
return;
369+
}
370+
awaitthis.completeLogin(url,label,res.token,res.user);
371+
return;
372+
}
373+
}
374+
375+
// Use legacy token flow (existing behavior)
376+
constres=awaitthis.maybeAskToken(url,args?.token,autoLogin);
377+
if(!res){
378+
return;
379+
}
380+
381+
awaitthis.completeLogin(url,label,res.token,res.user);
382+
}
383+
255384
/**
256385
* If necessary, ask for a token, and keep asking until the token has been
257386
* validated. Return the token and user that was fetched to validate the
@@ -377,6 +506,22 @@ export class Commands {
377506
// Sanity check; command should not be available if no url.
378507
thrownewError("You are not logged in");
379508
}
509+
510+
// Check if using OAuth
511+
consthasOAuthTokens=awaitthis.secretsManager.getOAuthTokens();
512+
if(hasOAuthTokens){
513+
this.logger.info("Logging out via OAuth");
514+
try{
515+
awaitthis.oauthHelper.logout();
516+
}catch(error){
517+
this.logger.warn(
518+
"OAuth logout failed, continuing with cleanup:",
519+
error,
520+
);
521+
}
522+
}
523+
524+
// Continue with standard logout (clears sessionToken, contexts, etc)
380525
awaitthis.forceLogout();
381526
}
382527

‎src/core/secretsManager.ts‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,20 @@ export class SecretsManager {
8484
});
8585
}
8686

87+
/**
88+
* Listens for session token changes.
89+
*/
90+
publiconDidChangeSessionToken(
91+
listener:(token:string|undefined)=>Promise<void>,
92+
):Disposable{
93+
returnthis.secrets.onDidChange(async(e)=>{
94+
if(e.key===SESSION_TOKEN_KEY){
95+
consttoken=awaitthis.getSessionToken();
96+
awaitlistener(token);
97+
}
98+
});
99+
}
100+
87101
/**
88102
* Store OAuth client registration data.
89103
*/

‎src/extension.ts‎

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,11 +124,34 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
124124
);
125125

126126
constoauthHelper=awaitactivateCoderOAuth(
127-
client,
127+
url||"",
128128
secretsManager,
129129
output,
130130
ctx,
131131
);
132+
ctx.subscriptions.push(oauthHelper);
133+
134+
// Listen for session token changes and sync state across all components
135+
ctx.subscriptions.push(
136+
secretsManager.onDidChangeSessionToken(async(token)=>{
137+
if(!token){
138+
output.debug("Session token cleared");
139+
client.setSessionToken("");
140+
return;
141+
}
142+
143+
output.debug("Session token changed, syncing state");
144+
145+
client.setSessionToken(token);
146+
consturl=mementoManager.getUrl();
147+
if(url){
148+
constcliManager=serviceContainer.getCliManager();
149+
// TODO label might not match?
150+
awaitcliManager.configure(toSafeHost(url),url,token);
151+
output.debug("Updated CLI config with new token");
152+
}
153+
}),
154+
);
132155

133156
// Handle vscode:// URIs.
134157
consturiHandler=vscode.window.registerUriHandler({
@@ -293,7 +316,7 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
293316

294317
// Register globally available commands. Many of these have visibility
295318
// controlled by contexts, see `when` in the package.json.
296-
constcommands=newCommands(serviceContainer,client);
319+
constcommands=newCommands(serviceContainer,client,oauthHelper);
297320
ctx.subscriptions.push(
298321
vscode.commands.registerCommand(
299322
"coder.login",

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp