Skip to content
This repository has been archived by the owner on Jan 30, 2025. It is now read-only.

Commit

Permalink
Auth: Always Authorize on localhost
Browse files Browse the repository at this point in the history
  • Loading branch information
iohub committed Dec 6, 2023
1 parent 0e375a1 commit 9d11ca6
Show file tree
Hide file tree
Showing 4 changed files with 293 additions and 17 deletions.
5 changes: 1 addition & 4 deletions vscode/src/chat/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,10 +220,7 @@ export interface LocalEnv {
}

export function isLoggedIn(authStatus: AuthStatus): boolean {
if (!authStatus.siteHasCodyEnabled) {
return false
}
return authStatus.authenticated && (authStatus.requiresVerifiedEmail ? authStatus.hasVerifiedEmail : true)
return true
}

// The OS and Arch support for Cody app
Expand Down
3 changes: 2 additions & 1 deletion vscode/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ import { logDebug } from './log'
import { FixupController } from './non-stop/FixupController'
import { showSetupNotification } from './notifications/setup-notification'
import { SearchViewProvider } from './search/SearchViewProvider'
import { AuthProvider } from './services/AuthProvider'
// import { AuthProvider } from './services/AuthProvider'
import { AlwaysAuthProvider as AuthProvider } from './services/AlwaysAuthProvider'
import { showFeedbackSupportQuickPick } from './services/FeedbackOptions'
import { GuardrailsProvider } from './services/GuardrailsProvider'
import { Comment, InlineController } from './services/InlineController'
Expand Down
278 changes: 278 additions & 0 deletions vscode/src/services/AlwaysAuthProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
import * as vscode from 'vscode'

import { ConfigurationWithAccessToken } from '@sourcegraph/cody-shared/src/configuration'
import { DOTCOM_URL, isLocalApp, LOCAL_APP_URL } from '@sourcegraph/cody-shared/src/sourcegraph-api/environments'

import { CodyChatPanelViewType } from '../chat/chat-view/ChatManager'
import {
AuthStatus
} from '../chat/protocol'
import { newAuthStatus } from '../chat/utils'
import { getFullConfig } from '../configuration'
import { logDebug } from '../log'

import { AuthMenu, showAccessTokenInputBox, showInstanceURLInputBox } from './AuthMenus'
import { localStorage } from './LocalStorageProvider'
import { secretStorage } from './SecretStorageProvider'
import { AuthProvider } from './AuthProvider'

type Listener = (authStatus: AuthStatus) => void
type Unsubscribe = () => {}

export class AlwaysAuthProvider extends AuthProvider {

// Sign into the last endpoint the user was signed into
// if none, try signing in with App URL
public async init(): Promise<void> {
await this.appDetector.init()
const lastEndpoint = localStorage?.getEndpoint() || this.config.serverEndpoint
const token = (await secretStorage.get(lastEndpoint || '')) || this.config.accessToken
logDebug('AuthProvider:init:lastEndpoint', lastEndpoint)
await this.auth(lastEndpoint, token || null)
}

public addChangeListener(listener: Listener): Unsubscribe {
this.listeners.add(listener)
return () => this.listeners.delete(listener)
}

// Display quickpick to select endpoint to sign in to
public async signinMenu(type?: 'enterprise' | 'dotcom' | 'token' | 'app', uri?: string): Promise<void> {
const mode = this.authStatus.isLoggedIn ? 'switch' : 'signin'
logDebug('AuthProvider:signinMenu', mode)
const item = await AuthMenu(mode, this.endpointHistory)
if (!item) {
return
}
const menuID = type || item?.id
switch (menuID) {
case 'enterprise': {
const instanceUrl = await showInstanceURLInputBox(item.uri)
if (!instanceUrl) {
return
}
this.authStatus.endpoint = instanceUrl
this.redirectToEndpointLogin(instanceUrl)
break
}
case 'dotcom':
this.redirectToEndpointLogin(DOTCOM_URL.href)
break
case 'token': {
const instanceUrl = await showInstanceURLInputBox(uri || item.uri)
if (!instanceUrl) {
return
}
await this.signinMenuForInstanceUrl(instanceUrl)
break
}
case 'app': {
if (uri) {
await this.appAuth(uri)
}
break
}
default: {
// Auto log user if token for the selected instance was found in secret
const selectedEndpoint = item.uri
const tokenKey = isLocalApp(selectedEndpoint) ? 'SOURCEGRAPH_CODY_APP' : selectedEndpoint
const token = await secretStorage.get(tokenKey)
let authStatus = await this.auth(selectedEndpoint, token || null)
if (!authStatus?.isLoggedIn) {
const newToken = await showAccessTokenInputBox(item.uri)
authStatus = await this.auth(selectedEndpoint, newToken || null)
}
await showAuthResultMessage(selectedEndpoint, authStatus?.authStatus)
logDebug('AuthProvider:signinMenu', mode, selectedEndpoint)
}
}
}

public async appAuth(uri?: string): Promise<void> {
logDebug('AuthProvider:appAuth:init', '')
const token = await secretStorage.get('SOURCEGRAPH_CODY_APP')
if (token) {
const authStatus = await this.auth(LOCAL_APP_URL.href, token)
if (authStatus?.isLoggedIn) {
return
}
}
if (!uri) {
return
}
await vscode.env.openExternal(vscode.Uri.parse(uri))
}

public async signoutMenu(): Promise<void> {
const { endpoint } = this.authStatus

if (endpoint) {
await this.signout(endpoint)
logDebug('AuthProvider:signoutMenu', endpoint)
}
}

// Log user out of the selected endpoint (remove token from secret)
protected async signout(endpoint: string): Promise<void> {
// Restart appDetector if endpoint is App
if (isLocalApp(endpoint)) {
await this.appDetector.init()
}
await secretStorage.deleteToken(endpoint)
await localStorage.deleteEndpoint()
await this.auth(endpoint, null)
this.authStatus.endpoint = ''
await vscode.commands.executeCommand('setContext', CodyChatPanelViewType, false)
await vscode.commands.executeCommand('setContext', 'cody.activated', false)
}

// Create Auth Status
protected async _makeAuthStatus(
config: Pick<ConfigurationWithAccessToken, 'serverEndpoint' | 'accessToken' | 'customHeaders'>
): Promise<AuthStatus> {
const endpoint = config.serverEndpoint
return newAuthStatus(
endpoint,
false,
true,
true,
true,
/* userCanUpgrade: */ false,
"1.0",
undefined
)
}

public getAuthStatus(): AuthStatus {
return this.authStatus
}

// It processes the authentication steps and stores the login info before sharing the auth status with chatview
public async auth(
uri: string,
token: string | null,
customHeaders?: {} | null
): Promise<{ authStatus: AuthStatus; isLoggedIn: boolean } | null> {
const endpoint = formatURL(uri) || ''
const config = {
serverEndpoint: endpoint,
accessToken: token,
customHeaders: customHeaders || this.config.customHeaders,
}
const authStatus = await this._makeAuthStatus(config)
const isLoggedIn = true
authStatus.isLoggedIn = isLoggedIn
await this.storeAuthInfo(endpoint, token)
await this.syncAuthStatus(authStatus)
await vscode.commands.executeCommand('setContext', 'cody.activated', isLoggedIn)
return { authStatus, isLoggedIn }
}

// Set auth status in case of reload
public async reloadAuthStatus(): Promise<void> {
this.config = await getFullConfig()
await this.auth(this.config.serverEndpoint, this.config.accessToken, this.config.customHeaders)
}

public async announceNewAuthStatus(): Promise<void> {
if (this.authStatus.endpoint === 'init' || !this.webview) {
return
}
const authStatus = this.getAuthStatus()
for (const listener of this.listeners) {
listener(authStatus)
}
await vscode.commands.executeCommand('cody.auth.sync')
}

// Register URI Handler (vscode://sourcegraph.cody-ai) for:
// - Deep linking into VS Code with Cody focused (e.g. from the App setup)
// - Resolving token sending back from sourcegraph.com and App
public async tokenCallbackHandler(uri: vscode.Uri, customHeaders: {}): Promise<void> {
const params = new URLSearchParams(uri.query)
const isApp = params.get('type') === 'app'
const token = params.get('code')
const endpoint = isApp ? LOCAL_APP_URL.href : this.authStatus.endpoint
if (!token || !endpoint) {
return
}
const authState = await this.auth(endpoint, token, customHeaders)
if (authState?.isLoggedIn) {
const successMessage = isApp ? 'Connected to Cody App' : `Signed in to ${endpoint}`
await vscode.window.showInformationMessage(successMessage)
} else {
await showAuthFailureMessage(endpoint)
}
}

/** Open callback URL in browser to get token from instance. */
public redirectToEndpointLogin(uri: string): void {
const endpoint = formatURL(uri)
if (!endpoint) {
return
}

if (vscode.env.uiKind === vscode.UIKind.Web) {
// VS Code Web needs a different kind of callback using asExternalUri and changes to our
// UserSettingsCreateAccessTokenCallbackPage.tsx page in the Sourcegraph web app. So,
// just require manual token entry for now.
const newTokenNoCallbackUrl = new URL('/user/settings/tokens/new', endpoint)
void vscode.env.openExternal(vscode.Uri.parse(newTokenNoCallbackUrl.href))
void this.signinMenuForInstanceUrl(endpoint)
return
}

const newTokenCallbackUrl = new URL('/user/settings/tokens/new/callback', endpoint)
newTokenCallbackUrl.searchParams.append(
'requestFrom',
this.appScheme === 'vscode-insiders' ? 'CODY_INSIDERS' : 'CODY'
)
this.authStatus.endpoint = endpoint
void vscode.env.openExternal(vscode.Uri.parse(newTokenCallbackUrl.href))
}
}

export function isNetworkError(error: string): boolean {
return (
error.includes('ENOTFOUND') ||
error.includes('ECONNREFUSED') ||
error.includes('ECONNRESET') ||
error.includes('EHOSTUNREACH')
)
}

function formatURL(uri: string): string | null {
if (!uri) {
return null
}
// Check if the URI is in the correct URL format
// Add missing https:// if needed
if (!uri.startsWith('http')) {
uri = `https://${uri}`
}
try {
const endpointUri = new URL(uri)
return endpointUri.href
} catch {
console.error('Invalid URL')
}
return null
}

async function showAuthResultMessage(endpoint: string, authStatus: AuthStatus | undefined): Promise<void> {
if (authStatus?.isLoggedIn) {
const authority = vscode.Uri.parse(endpoint).authority
const isApp = endpoint === LOCAL_APP_URL.href
const successMessage = isApp ? 'Connected to Cody App' : `Signed in to ${authority}`
await vscode.window.showInformationMessage(successMessage)
} else {
await showAuthFailureMessage(endpoint)
}
}

async function showAuthFailureMessage(endpoint: string): Promise<void> {
const authority = vscode.Uri.parse(endpoint).authority
await vscode.window.showErrorMessage(
`Authentication failed. Please ensure Cody is enabled for ${authority} and verify your email address if required.`
)
}
24 changes: 12 additions & 12 deletions vscode/src/services/AuthProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,18 @@ type Listener = (authStatus: AuthStatus) => void
type Unsubscribe = () => {}

export class AuthProvider {
private endpointHistory: string[] = []
protected endpointHistory: string[] = []

private appScheme = vscode.env.uriScheme
private client: SourcegraphGraphQLAPIClient | null = null
protected appScheme = vscode.env.uriScheme
protected client: SourcegraphGraphQLAPIClient | null = null
public appDetector: LocalAppDetector

private authStatus: AuthStatus = defaultAuthStatus
protected authStatus: AuthStatus = defaultAuthStatus
public webview?: SidebarChatWebview
private listeners: Set<Listener> = new Set()
protected listeners: Set<Listener> = new Set()

constructor(
private config: Pick<ConfigurationWithAccessToken, 'serverEndpoint' | 'accessToken' | 'customHeaders'>
protected config: Pick<ConfigurationWithAccessToken, 'serverEndpoint' | 'accessToken' | 'customHeaders'>
) {
this.authStatus.endpoint = 'init'
this.loadEndpointHistory()
Expand Down Expand Up @@ -115,7 +115,7 @@ export class AuthProvider {
}
}

private async signinMenuForInstanceUrl(instanceUrl: string): Promise<void> {
protected async signinMenuForInstanceUrl(instanceUrl: string): Promise<void> {
const accessToken = await showAccessTokenInputBox(instanceUrl)
if (!accessToken) {
return
Expand Down Expand Up @@ -153,7 +153,7 @@ export class AuthProvider {
}

// Log user out of the selected endpoint (remove token from secret)
private async signout(endpoint: string): Promise<void> {
protected async signout(endpoint: string): Promise<void> {
// Restart appDetector if endpoint is App
if (isLocalApp(endpoint)) {
await this.appDetector.init()
Expand All @@ -167,7 +167,7 @@ export class AuthProvider {
}

// Create Auth Status
private async makeAuthStatus(
protected async makeAuthStatus(
config: Pick<ConfigurationWithAccessToken, 'serverEndpoint' | 'accessToken' | 'customHeaders'>
): Promise<AuthStatus> {
const endpoint = config.serverEndpoint
Expand Down Expand Up @@ -279,7 +279,7 @@ export class AuthProvider {
}

// Set auth status and share it with chatview
private async syncAuthStatus(authStatus: AuthStatus): Promise<void> {
protected async syncAuthStatus(authStatus: AuthStatus): Promise<void> {
if (this.authStatus === authStatus) {
return
}
Expand Down Expand Up @@ -364,12 +364,12 @@ export class AuthProvider {
}

// Refresh current endpoint history with the one from local storage
private loadEndpointHistory(): void {
protected loadEndpointHistory(): void {
this.endpointHistory = localStorage.getEndpointHistory() || []
}

// Store endpoint in local storage, token in secret storage, and update endpoint history
private async storeAuthInfo(endpoint: string | null | undefined, token: string | null | undefined): Promise<void> {
protected async storeAuthInfo(endpoint: string | null | undefined, token: string | null | undefined): Promise<void> {
if (!endpoint) {
return
}
Expand Down

0 comments on commit 9d11ca6

Please sign in to comment.