diff --git a/AZURE_AD_SETUP.md b/AZURE_AD_SETUP.md new file mode 100644 index 0000000..825c980 --- /dev/null +++ b/AZURE_AD_SETUP.md @@ -0,0 +1,100 @@ +# Azure AD App Registration Setup for ImmyBot VS Code Extension + +This document explains how to configure Azure AD authentication for the ImmyBot VS Code extension to resolve the `AADSTS900971` error. + +## The Problem + +The `AADSTS900971: No reply address provided` error occurs when the Azure AD app registration used by the extension is missing the required redirect URIs for VS Code authentication. + +## Solution Options + +### Option 1: Configure Your Own Azure AD App Registration (Recommended) + +1. **Create a new Azure AD App Registration:** + - Go to the [Azure Portal](https://portal.azure.com) + - Navigate to **Azure Active Directory** > **App registrations** + - Click **New registration** + +2. **Configure the app registration:** + - **Name**: ImmyBot VS Code Extension (or your preferred name) + - **Supported account types**: Choose based on your needs: + - **Accounts in this organizational directory only** (single tenant) + - **Accounts in any organizational directory** (multi-tenant) + - **Redirect URI**: Leave blank for now, we'll add these next + +3. **Add required redirect URIs:** + - After creating the app, go to **Authentication** + - Click **Add a platform** > **Web** + - Add these redirect URIs: + ``` + https://vscode.dev/redirect + vscode://vscode.github-authentication/did-authenticate + ``` + - Click **Configure** + +4. **Configure API permissions:** + - Go to **API permissions** + - Click **Add a permission** > **Microsoft Graph** > **Delegated permissions** + - Add these permissions: + - `openid` + - `profile` + - `offline_access` + - `Files.ReadWrite` (if needed for file operations) + +5. **Copy the Client ID:** + - Go to the **Overview** tab + - Copy the **Application (client) ID** + +6. **Configure the VS Code extension:** + - Open VS Code settings (Ctrl/Cmd + ,) + - Search for "immybot" + - Set **ImmyBot: Azure Client Id** to your copied Client ID + - Set **ImmyBot: Azure Tenant** to your tenant ID or "common" for multi-tenant + +### Option 2: Request Configuration of the Default App Registration + +If you prefer to use the default app registration, contact the extension maintainers to request that the following redirect URIs be added to the existing app registration (`f72a44d4-d2d4-450e-a2db-76b307cd045f`): + +- `https://vscode.dev/redirect` +- `vscode://vscode.github-authentication/did-authenticate` + +## Configuration Settings + +The extension provides these configuration options in VS Code settings: + +| Setting | Description | Default | +|---------|-------------|---------| +| `immybot.azureClientId` | Azure AD Application Client ID | `""` (uses default) | +| `immybot.azureTenant` | Azure AD Tenant ID or domain | `"common"` | + +## Troubleshooting + +### Common Errors + +**AADSTS900971: No reply address provided** +- Cause: Missing redirect URIs in Azure AD app registration +- Solution: Add the required redirect URIs listed above + +**AADSTS50020: User account from identity provider does not exist in tenant** +- Cause: Single-tenant app registration with wrong tenant configuration +- Solution: Either switch to multi-tenant or set the correct tenant ID in settings + +**AADSTS700016: Application not found in the directory** +- Cause: Invalid Client ID +- Solution: Verify the Client ID is correct in your settings + +### Getting Help + +If you continue to experience authentication issues: + +1. Check the **Microsoft Authentication** output channel in VS Code for detailed error information +2. Verify your Azure AD app registration configuration +3. Ensure your VS Code extension settings are correct +4. Contact your Azure AD administrator for assistance with app registration permissions + +## Security Considerations + +- Only grant the minimum required permissions to your Azure AD app registration +- Use single-tenant app registrations when possible for better security +- Regularly review and audit app registration permissions +- Consider using Conditional Access policies to control access to the app registration \ No newline at end of file diff --git a/package.json b/package.json index 6c9841d..7638daa 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,23 @@ ], "browser": "./dist/web/extension.js", "contributes": { + "configuration": { + "title": "ImmyBot", + "properties": { + "immybot.azureClientId": { + "type": "string", + "default": "", + "description": "Azure AD Application Client ID for authentication. If not provided, uses the default client ID which may require additional configuration.", + "markdownDescription": "Azure AD Application Client ID for authentication. If not provided, uses the default client ID which may require additional configuration.\n\n**Required Redirect URIs for your Azure AD app registration:**\n- `https://vscode.dev/redirect`\n- `vscode://vscode.github-authentication/did-authenticate`" + }, + "immybot.azureTenant": { + "type": "string", + "default": "common", + "description": "Azure AD Tenant ID or domain. Use 'common' for multi-tenant applications.", + "markdownDescription": "Azure AD Tenant ID or domain. Use `common` for multi-tenant applications, or specify your specific tenant ID/domain for single-tenant applications." + } + } + }, "languages": [ { "id": "metascript", diff --git a/src/web/extension.ts b/src/web/extension.ts index c9f6223..60bd2c6 100644 --- a/src/web/extension.ts +++ b/src/web/extension.ts @@ -38,15 +38,36 @@ enum ScriptCategory { Unknown } -const CLIENT_ID = 'f72a44d4-d2d4-450e-a2db-76b307cd045f'; -const SCOPES = [ - `VSCODE_CLIENT_ID:${CLIENT_ID}`, - `VSCODE_TENANT:common`, - 'profile', - 'openid', - 'offline_access', - 'Files.ReadWrite', -]; +// Default client ID - users should configure their own for production use +const DEFAULT_CLIENT_ID = 'f72a44d4-d2d4-450e-a2db-76b307cd045f'; + +// Get client ID from configuration or use default +function getClientId(): string { + const config = vscode.workspace.getConfiguration('immybot'); + const configuredClientId = config.get('azureClientId'); + return configuredClientId && configuredClientId.trim() !== '' ? configuredClientId : DEFAULT_CLIENT_ID; +} + +// Get tenant from configuration or use default +function getTenant(): string { + const config = vscode.workspace.getConfiguration('immybot'); + return config.get('azureTenant') || 'common'; +} + +// Build scopes dynamically based on configuration +function buildScopes(): string[] { + const clientId = getClientId(); + const tenant = getTenant(); + + return [ + `VSCODE_CLIENT_ID:${clientId}`, + `VSCODE_TENANT:${tenant}`, + 'profile', + 'openid', + 'offline_access', + 'Files.ReadWrite', + ]; +} let initialized = false; let session: vscode.AuthenticationSession; let authOutputChannel: vscode.OutputChannel; @@ -763,19 +784,21 @@ export async function activate(context: vscode.ExtensionContext) { // Reset the authentication context to show the sign-in view await vscode.commands.executeCommand('setContext', 'immybot:authenticated', false); - // We can't use signOut directly since it's not available in the API - // Instead, try several approaches to ensure the session is truly cleared - try { - // 1. Clear session preference - await vscode.authentication.getSession('microsoft', SCOPES, { - clearSessionPreference: true - }); - + // We can't use signOut directly since it's not available in the API + // Instead, try several approaches to ensure the session is truly cleared + try { + const scopes = buildScopes(); + + // 1. Clear session preference + await vscode.authentication.getSession('microsoft', scopes, { + clearSessionPreference: true + }); + // 2. For VS Code versions that support it, try to get all sessions and remove them // This may not work in all VS Code versions, so we catch any errors - try { - // @ts-ignore - getSessions might exist in newer VS Code versions - const allSessions = await vscode.authentication.getSessions?.('microsoft', SCOPES); + try { + // @ts-ignore - getSessions might exist in newer VS Code versions + const allSessions = await vscode.authentication.getSessions?.('microsoft', scopes); if (allSessions && allSessions.length > 0) { // Log how many sessions we're clearing authOutputChannel.appendLine(`Found ${allSessions.length} active Microsoft sessions to clear`); @@ -882,73 +905,177 @@ export async function activate(context: vscode.ExtensionContext) { } } -// Helper function to handle sign-in attempts -async function attemptSignIn(promptForAuth: boolean): Promise { - try { - // Check if we need to force a new session (set during sign-out) - // Use globalState instead of workspaceState for better persistence - const forceNewSession = extensionContext.globalState.get('immybot.forceNewSession', false); - - // If we're explicitly signing in and a new session is forced, use forceNewSession - if (promptForAuth && forceNewSession) { - // Clear the flag so we don't force new sessions forever - await extensionContext.globalState.update('immybot.forceNewSession', false); - - // Force a completely new authentication - session = await vscode.authentication.getSession('microsoft', SCOPES, { - forceNewSession: true - }); - - if (session) { - await processSuccessfulSignIn(); - return true; - } - return false; - } - - // Check for existing session first - let existingSession: vscode.AuthenticationSession | undefined; - - try { - // Try to get an existing session without creating a new one - // Use createIfNone=false to ensure we don't auto-create a session - existingSession = await vscode.authentication.getSession('microsoft', SCOPES, { - createIfNone: false, - silent: !promptForAuth // Only show UI if explicitly requested - }); - } catch (e) { - // Ignore errors when silently checking for a session - if (promptForAuth) { - throw e; // Re-throw if we were explicitly trying to authenticate - } - } - - if (existingSession) { - // We have an existing valid session - session = existingSession; - await processSuccessfulSignIn(); - return true; - } else if (promptForAuth) { - // No existing session but user clicked sign-in button, so prompt for auth - session = await vscode.authentication.getSession('microsoft', SCOPES, { - createIfNone: true - }); - - if (session) { - await processSuccessfulSignIn(); - return true; - } - } else { - // No session and not prompting for auth - just show the sign-in view - await vscode.commands.executeCommand('setContext', 'immybot:authenticated', false); - } - - return false; - } catch (error) { - vscode.window.showErrorMessage(`Authentication error: ${error instanceof Error ? error.message : String(error)}`); - console.error("Authentication error:", error); - return false; - } +// Helper function to handle sign-in attempts +async function attemptSignIn(promptForAuth: boolean): Promise { + try { + const scopes = buildScopes(); + const clientId = getClientId(); + const tenant = getTenant(); + + // Log authentication attempt details + authOutputChannel.appendLine(`Authentication attempt:`); + authOutputChannel.appendLine(` Client ID: ${clientId}`); + authOutputChannel.appendLine(` Tenant: ${tenant}`); + authOutputChannel.appendLine(` Scopes: ${scopes.join(', ')}`); + authOutputChannel.appendLine(` Prompt for auth: ${promptForAuth}`); + + // Check if we need to force a new session (set during sign-out) + // Use globalState instead of workspaceState for better persistence + const forceNewSession = extensionContext.globalState.get('immybot.forceNewSession', false); + + // If we're explicitly signing in and a new session is forced, use forceNewSession + if (promptForAuth && forceNewSession) { + // Clear the flag so we don't force new sessions forever + await extensionContext.globalState.update('immybot.forceNewSession', false); + + // Force a completely new authentication + session = await vscode.authentication.getSession('microsoft', scopes, { + forceNewSession: true + }); + + if (session) { + await processSuccessfulSignIn(); + return true; + } + return false; + } + + // Check for existing session first + let existingSession: vscode.AuthenticationSession | undefined; + + try { + // Try to get an existing session without creating a new one + // Use createIfNone=false to ensure we don't auto-create a session + existingSession = await vscode.authentication.getSession('microsoft', scopes, { + createIfNone: false, + silent: !promptForAuth // Only show UI if explicitly requested + }); + } catch (e) { + // Log authentication errors with more detail + const errorMessage = e instanceof Error ? e.message : String(e); + authOutputChannel.appendLine(`Error checking for existing session: ${errorMessage}`); + + // Check for specific Azure AD errors + if (errorMessage.includes('AADSTS900971')) { + const azureErrorMsg = `Azure AD Error AADSTS900971: No reply address provided. + +This error indicates that your Azure AD app registration is missing required redirect URIs for VS Code authentication. + +To fix this issue: +1. Go to your Azure AD app registration (Client ID: ${clientId}) +2. Navigate to "Authentication" section +3. Add these redirect URIs: + - https://vscode.dev/redirect + - vscode://vscode.github-authentication/did-authenticate +4. Save the configuration + +Alternatively, you can: +- Configure a custom Client ID in VS Code settings (immybot.azureClientId) +- Create a new Azure AD app registration with the proper redirect URIs configured`; + + vscode.window.showErrorMessage('Azure AD Authentication Configuration Error', 'Show Details', 'Open Settings').then(async (choice) => { + if (choice === 'Show Details') { + authOutputChannel.appendLine(`\n${azureErrorMsg}`); + authOutputChannel.show(); + } else if (choice === 'Open Settings') { + vscode.commands.executeCommand('workbench.action.openSettings', 'immybot.azureClientId'); + } + }); + + authOutputChannel.appendLine(azureErrorMsg); + return false; + } else if (errorMessage.includes('AADSTS')) { + // Handle other Azure AD errors + const azureErrorMsg = `Azure AD Authentication Error: ${errorMessage} + +This may be due to: +- App registration configuration issues +- Tenant access restrictions +- Invalid client ID or scopes + +Current configuration: +- Client ID: ${clientId} +- Tenant: ${tenant} +- Scopes: ${scopes.join(', ')} + +You can configure custom authentication settings in VS Code settings (immybot.azureClientId, immybot.azureTenant).`; + + vscode.window.showErrorMessage('Azure AD Authentication Error', 'Show Details', 'Open Settings').then(async (choice) => { + if (choice === 'Show Details') { + authOutputChannel.appendLine(`\n${azureErrorMsg}`); + authOutputChannel.show(); + } else if (choice === 'Open Settings') { + vscode.commands.executeCommand('workbench.action.openSettings', 'immybot'); + } + }); + + authOutputChannel.appendLine(azureErrorMsg); + return false; + } + + // Ignore errors when silently checking for a session + if (promptForAuth) { + throw e; // Re-throw if we were explicitly trying to authenticate + } + } + + if (existingSession) { + // We have an existing valid session + session = existingSession; + await processSuccessfulSignIn(); + return true; + } else if (promptForAuth) { + // No existing session but user clicked sign-in button, so prompt for auth + session = await vscode.authentication.getSession('microsoft', scopes, { + createIfNone: true + }); + + if (session) { + await processSuccessfulSignIn(); + return true; + } + } else { + // No session and not prompting for auth - just show the sign-in view + await vscode.commands.executeCommand('setContext', 'immybot:authenticated', false); + } + + return false; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + + // Enhanced error logging and user guidance + authOutputChannel.appendLine(`Authentication failed: ${errorMessage}`); + + // Check for specific error patterns and provide targeted guidance + if (errorMessage.includes('AADSTS900971')) { + const clientId = getClientId(); + const guidance = `Azure AD Error AADSTS900971: No reply address provided. + +Your Azure AD app registration (${clientId}) needs to be configured with these redirect URIs: +- https://vscode.dev/redirect +- vscode://vscode.github-authentication/did-authenticate + +Please update your Azure AD app registration or configure a different Client ID in settings.`; + + vscode.window.showErrorMessage('Authentication Configuration Error', 'Show Solution', 'Open Settings').then(async (choice) => { + if (choice === 'Show Solution') { + authOutputChannel.appendLine(`\n${guidance}`); + authOutputChannel.show(); + } else if (choice === 'Open Settings') { + vscode.commands.executeCommand('workbench.action.openSettings', 'immybot.azureClientId'); + } + }); + } else { + vscode.window.showErrorMessage(`Authentication error: ${errorMessage}`, 'Show Details').then((choice) => { + if (choice === 'Show Details') { + authOutputChannel.show(); + } + }); + } + + console.error("Authentication error:", error); + return false; + } } // Process a successful sign-in - extract user info and set up views