Skip to content

Commit 354d4d0

Browse files
committed
make authentication optional
1 parent 04544be commit 354d4d0

File tree

4 files changed

+271
-270
lines changed

4 files changed

+271
-270
lines changed

src/caStatusBarProvider.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,12 @@ class CAStatusBarProvider implements Disposable {
5151
* Shows authentication required status in the status bar.
5252
*/
5353
public showAuthRequired(): void {
54-
this.statusBarItem.text = `$(shield) RHDA: Sign In Required`;
54+
this.statusBarItem.text = `$(account) RHDA: Not Signed In`;
5555
this.statusBarItem.command = {
5656
title: 'Authenticate with RHDA',
5757
command: 'rhda.authenticate',
5858
};
59-
this.statusBarItem.tooltip = 'RHDA features are disabled. Click to authenticate and enable dependency analysis.';
59+
this.statusBarItem.tooltip = 'Click to sign in for enhanced RHDA features (optional)';
6060
this.statusBarItem.show();
6161
}
6262

src/extension.ts

Lines changed: 18 additions & 266 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { StatusMessages, PromptText } from './constants';
1111
import { caStatusBarProvider } from './caStatusBarProvider';
1212
import { CANotification, CANotificationData } from './caNotification';
1313
import { DepOutputChannel } from './depOutputChannel';
14-
import { record, startUp, TelemetryActions } from './redhatTelemetry';
14+
import { initTelemetry, record, TelemetryActions } from './redhatTelemetry';
1515
import { applySettingNameMappings, buildLogErrorMessage, buildNotificationErrorMessage } from './utils';
1616
import { clearCodeActionsMap, getDiagnosticsCodeActions } from './codeActionHandler';
1717
import { AnalysisMatcher } from './fileHandler';
@@ -21,265 +21,16 @@ import { LLMAnalysisReportPanel } from './llmAnalysisReportPanel';
2121
// eslint-disable-next-line @typescript-eslint/no-require-imports
2222
import CliTable3 = require('cli-table3');
2323
import { Language, Parser, Query } from 'web-tree-sitter';
24-
import * as client from 'openid-client';
24+
import { getValidAccessToken, performOIDCAuthorizationFlow } from './oidcAuthentication';
2525

2626
export let outputChannelDep: DepOutputChannel;
2727

2828
export const notifications = new EventEmitter();
2929

30-
// OIDC flow state storage
31-
interface OIDCFlowState {
32-
codeVerifier: string;
33-
state?: string;
34-
nonce: string;
35-
}
36-
37-
/**
38-
* Performs OIDC Authorization Code Flow with VSCode URL handler
39-
*/
40-
async function performOIDCAuthorizationFlow(context: vscode.ExtensionContext): Promise<void> {
41-
try {
42-
const clientId = 'vscode-extension';
4330

44-
let config: client.Configuration | undefined;
45-
const realmUrl = 'http://localhost:8090/realms/trustify';
4631

47-
try {
48-
config = await client.discovery(
49-
new URL(realmUrl),
50-
clientId,
51-
undefined,
52-
undefined,
53-
{
54-
execute: [client.allowInsecureRequests],
55-
}
56-
);
57-
outputChannelDep.info(`✓ Discovery successful for: ${realmUrl}`);
58-
} catch (error) {
59-
outputChannelDep.info(`✗ Discovery failed for ${realmUrl}: ${error}`);
60-
}
61-
62-
if (!config) {
63-
throw new Error(`Could not discover OIDC configuration from ${realmUrl}`);
64-
}
65-
66-
outputChannelDep.info(`Using issuer: ${realmUrl}`);
67-
outputChannelDep.info(`Authorization endpoint: ${config.serverMetadata().authorization_endpoint}`);
68-
69-
const redirectUri = `vscode://redhat.fabric8-analytics/auth-callback`;
70-
71-
// Generate PKCE parameters
72-
const codeVerifier = client.randomPKCECodeVerifier();
73-
const codeChallenge = await client.calculatePKCECodeChallenge(codeVerifier);
74-
const nonce = client.randomNonce();
75-
76-
let state: string | undefined;
77-
const parameters: Record<string, string> = {
78-
// eslint-disable-next-line @typescript-eslint/naming-convention
79-
redirect_uri: redirectUri,
80-
scope: 'openid profile email',
81-
// eslint-disable-next-line @typescript-eslint/naming-convention
82-
code_challenge: codeChallenge,
83-
// eslint-disable-next-line @typescript-eslint/naming-convention
84-
code_challenge_method: 'S256',
85-
nonce: nonce,
86-
};
87-
88-
// Use state if PKCE is not supported
89-
if (!config.serverMetadata().supportsPKCE()) {
90-
state = client.randomState();
91-
parameters.state = state;
92-
}
93-
94-
// Store flow state for callback verification
95-
const flowState: OIDCFlowState = {
96-
codeVerifier,
97-
state,
98-
nonce,
99-
};
100-
await context.globalState.update('oidc-flow-state', flowState);
101-
102-
// Register URL handler for the callback
103-
const disposable = vscode.window.registerUriHandler({
104-
handleUri: async (uri: vscode.Uri) => {
105-
try {
106-
await handleOIDCCallback(context, config, uri, flowState);
107-
} catch (error) {
108-
outputChannelDep.error(`OIDC callback error: ${error}`);
109-
vscode.window.showErrorMessage(`RHDA authentication failed: ${(error as Error).message}`);
110-
} finally {
111-
disposable.dispose();
112-
await context.globalState.update('oidc-flow-state', undefined);
113-
}
114-
},
115-
});
116-
117-
// Build authorization URL and redirect user
118-
outputChannelDep.info(`Building authorization URL with parameters: ${JSON.stringify(parameters)}`);
119-
const authUrl = client.buildAuthorizationUrl(config, parameters);
120-
outputChannelDep.info(`Complete authorization URL: ${authUrl.href}`);
121-
122-
// Open authorization URL in browser
123-
await vscode.env.openExternal(vscode.Uri.parse(authUrl.href));
124-
125-
vscode.window.showInformationMessage('Please complete RHDA authentication in your browser.');
126-
} catch (error) {
127-
outputChannelDep.error(`OIDC flow initialization error: ${error}`);
128-
vscode.window.showErrorMessage(`RHDA authentication initialization failed: ${(error as Error).message}`);
129-
}
130-
}
131-
132-
/**
133-
* Handles the OIDC callback from VSCode URL handler
134-
*/
135-
async function handleOIDCCallback(context: vscode.ExtensionContext, config: client.Configuration, callbackUri: vscode.Uri, flowState: OIDCFlowState): Promise<void> {
136-
try {
137-
const url = new URL(callbackUri.toString());
138-
// Handle double-encoded query parameters from VSCode
139-
const searchParams = new URLSearchParams(url.searchParams.keys().next().value!);
140-
141-
// Reconstruct a properly formatted callback URL with parsed parameters
142-
// VSCode's URL handler double-encodes parameters, so we need to fix this
143-
const redirectUri = `vscode://redhat.fabric8-analytics/auth-callback`;
144-
145-
// Create a simple URL with just the base and our clean parameters
146-
const cleanCallbackUrl = new URL(`${redirectUri}?${searchParams.toString()}`);
147-
outputChannelDep.info(`Clean callback URL: ${cleanCallbackUrl.toString()}`);
148-
149-
// First try the standard approach
150-
const tokens = await client.authorizationCodeGrant(
151-
config,
152-
cleanCallbackUrl,
153-
{
154-
pkceCodeVerifier: flowState.codeVerifier,
155-
expectedState: flowState.state,
156-
expectedNonce: flowState.nonce,
157-
}
158-
);
159-
160-
outputChannelDep.info('Successfully obtained tokens from OIDC authorization server');
161-
162-
// Store tokens securely
163-
if (tokens.access_token) {
164-
await context.secrets.store('oidc-access-token', tokens.access_token);
165-
}
166-
if (tokens.refresh_token) {
167-
await context.secrets.store('oidc-refresh-token', tokens.refresh_token);
168-
}
169-
if (tokens.id_token) {
170-
await context.secrets.store('oidc-id-token', tokens.id_token);
171-
}
172-
173-
// Store token expiration time
174-
if (tokens.expires_in) {
175-
const expirationTime = Date.now() + tokens.expires_in * 1000;
176-
await context.globalState.update('oidc-token-expiration', expirationTime);
177-
}
178-
179-
vscode.window.showInformationMessage('RHDA authentication successful!');
180-
181-
// Record successful authentication telemetry
182-
record(context, TelemetryActions.vulnerabilityReportDone, {
183-
action: 'oidc-authentication-success',
184-
});
185-
186-
// Enable all extension features now that authentication is complete
187-
outputChannelDep.info('🎯 Authentication complete - enabling extension features');
188-
caStatusBarProvider.showAuthenticated();
189-
await enableExtensionFeatures(context);
190-
} catch (error) {
191-
outputChannelDep.error(`Token exchange failed: ${error}`);
192-
throw new Error(`Failed to complete authentication: ${(error as Error).message}`);
193-
}
194-
}
195-
196-
/**
197-
* Retrieves a valid access token, refreshing if necessary
198-
*/
199-
export async function getValidAccessToken(context: vscode.ExtensionContext): Promise<string | null> {
200-
try {
201-
const accessToken = await context.secrets.get('oidc-access-token');
202-
const expirationTime = context.globalState.get<number>('oidc-token-expiration');
203-
204-
if (!accessToken) {
205-
// No token means user hasn't authenticated yet
206-
return null;
207-
}
208-
209-
// Check if token is expired (with 5 minute buffer)
210-
if (expirationTime && Date.now() > expirationTime - 300000) {
211-
const refreshToken = await context.secrets.get('oidc-refresh-token');
212-
if (refreshToken) {
213-
return await refreshAccessToken(context, refreshToken);
214-
}
215-
return null;
216-
}
217-
218-
return accessToken;
219-
} catch (error) {
220-
outputChannelDep.error(`Failed to get access token: ${error}`);
221-
return null;
222-
}
223-
}
224-
225-
/**
226-
* Refreshes the access token using the refresh token
227-
*/
228-
async function refreshAccessToken(context: vscode.ExtensionContext, refreshToken: string): Promise<string | null> {
229-
try {
230-
const config = await client.discovery(
231-
new URL('http://localhost:8090/realms/trustify'),
232-
'trustify-realm',
233-
undefined,
234-
undefined,
235-
{
236-
execute: [client.allowInsecureRequests],
237-
}
238-
);
239-
240-
const tokens = await client.refreshTokenGrant(config, refreshToken);
241-
242-
// Update stored tokens
243-
if (tokens.access_token) {
244-
await context.secrets.store('oidc-access-token', tokens.access_token);
245-
}
246-
if (tokens.refresh_token) {
247-
await context.secrets.store('oidc-refresh-token', tokens.refresh_token);
248-
}
249-
250-
// Update token expiration time
251-
if (tokens.expires_in) {
252-
const expirationTime = Date.now() + tokens.expires_in * 1000;
253-
await context.globalState.update('oidc-token-expiration', expirationTime);
254-
}
255-
256-
outputChannelDep.info('Successfully refreshed access token');
257-
return tokens.access_token || null;
258-
} catch (error) {
259-
outputChannelDep.error(`Token refresh failed: ${error}`);
260-
// Clear invalid tokens
261-
await context.secrets.delete('oidc-access-token');
262-
await context.secrets.delete('oidc-refresh-token');
263-
await context.secrets.delete('oidc-id-token');
264-
await context.globalState.update('oidc-token-expiration', undefined);
265-
266-
// Update status bar to show session expired
267-
caStatusBarProvider.showSessionExpired();
268-
return null;
269-
}
270-
}
271-
272-
/**
273-
* Activates the extension upon launch.
274-
* @param context - The extension context.
275-
*/
276-
/**
277-
* Enables all extension features after successful authentication
278-
*/
27932
async function enableExtensionFeatures(context: vscode.ExtensionContext): Promise<void> {
280-
outputChannelDep.info('Authentication successful - enabling all RHDA features');
281-
282-
startUp(context);
33+
outputChannelDep.info('Initializing RHDA analysis features');
28334

28435
context.subscriptions.push(vscode.languages.registerCodeActionsProvider('*', new class implements vscode.CodeActionProvider {
28536
provideCodeActions(document: vscode.TextDocument, range: vscode.Range | vscode.Selection, ctx: vscode.CodeActionContext): vscode.ProviderResult<vscode.CodeAction[]> {
@@ -592,9 +343,6 @@ async function enableExtensionFeatures(context: vscode.ExtensionContext): Promis
592343
vscode.workspace.onDidChangeConfiguration(() => {
593344
globalConfig.loadData();
594345
});
595-
596-
outputChannelDep.info('All RHDA extension features are now active and ready to use');
597-
vscode.window.showInformationMessage('RHDA: All features enabled. Ready for dependency analysis!');
598346
}
599347

600348
/**
@@ -606,6 +354,8 @@ export async function activate(context: vscode.ExtensionContext) {
606354

607355
globalConfig.linkToSecretStorage(context);
608356

357+
initTelemetry(context);
358+
609359
// Register authentication command (always available)
610360
const authenticateCommand = vscode.commands.registerCommand('rhda.authenticate', async () => {
611361
const existingToken = await getValidAccessToken(context);
@@ -626,29 +376,31 @@ export async function activate(context: vscode.ExtensionContext) {
626376
});
627377
context.subscriptions.push(authenticateCommand);
628378

629-
// Check if user is already authenticated
379+
// Check if user is already authenticated (optional)
630380
const existingToken = await getValidAccessToken(context);
631381
if (existingToken) {
632-
outputChannelDep.info('User already authenticated, enabling all features');
382+
outputChannelDep.info('User authenticated');
633383
caStatusBarProvider.showAuthenticated();
634-
await enableExtensionFeatures(context);
635384
} else {
636-
outputChannelDep.info('User not authenticated - extension features disabled until authentication');
385+
outputChannelDep.info('User not authenticated (authentication is optional)');
637386
caStatusBarProvider.showAuthRequired();
638-
// Must use showErrorMessage, otherwise the notification will automatically disappear after ~15s
639-
// See more: https://github.com/microsoft/azuredatastudio/issues/22567
640-
vscode.window.showErrorMessage('RHDA: Authentication required. Extension features are disabled until you complete the login process.', 'Authenticate Now').then(selection => {
641-
if (selection === 'Authenticate Now') {
642-
// Start OIDC flow when user clicks the button
387+
388+
// Show optional authentication prompt
389+
vscode.window.showInformationMessage(
390+
'RHDA: You can optionally authenticate for enhanced features.',
391+
'Authenticate',
392+
'Skip'
393+
).then(selection => {
394+
if (selection === 'Authenticate') {
643395
performOIDCAuthorizationFlow(context).catch(error => {
644396
outputChannelDep.error(`Authentication failed: ${buildLogErrorMessage(error)}`);
645397
vscode.window.showErrorMessage(`RHDA: Authentication failed. ${buildNotificationErrorMessage(error as Error)}`);
646398
});
647399
}
648400
});
649-
650-
outputChannelDep.info('Extension started in restricted mode. Authentication required for full functionality.');
651401
}
402+
403+
await enableExtensionFeatures(context);
652404
}
653405

654406
/**

0 commit comments

Comments
 (0)