@@ -11,7 +11,7 @@ import { StatusMessages, PromptText } from './constants';
1111import { caStatusBarProvider } from './caStatusBarProvider' ;
1212import { CANotification , CANotificationData } from './caNotification' ;
1313import { DepOutputChannel } from './depOutputChannel' ;
14- import { record , startUp , TelemetryActions } from './redhatTelemetry' ;
14+ import { initTelemetry , record , TelemetryActions } from './redhatTelemetry' ;
1515import { applySettingNameMappings , buildLogErrorMessage , buildNotificationErrorMessage } from './utils' ;
1616import { clearCodeActionsMap , getDiagnosticsCodeActions } from './codeActionHandler' ;
1717import { AnalysisMatcher } from './fileHandler' ;
@@ -21,265 +21,16 @@ import { LLMAnalysisReportPanel } from './llmAnalysisReportPanel';
2121// eslint-disable-next-line @typescript-eslint/no-require-imports
2222import CliTable3 = require( 'cli-table3' ) ;
2323import { Language , Parser , Query } from 'web-tree-sitter' ;
24- import * as client from 'openid-client ' ;
24+ import { getValidAccessToken , performOIDCAuthorizationFlow } from './oidcAuthentication ' ;
2525
2626export let outputChannelDep : DepOutputChannel ;
2727
2828export 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- */
27932async 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