-
Notifications
You must be signed in to change notification settings - Fork 344
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
External Authentication Providers Support for Cody (#6526)
Fixes https://linear.app/sourcegraph/issue/CODY-4642 ## External Authentication Provider Support for Cody This PR introduces support for external authentication providers in Cody, allowing users to integrate with custom authentication proxies and handle complex authentication scenarios. ### Feature Overview This feature requires clients to have reverse proxy and custom sourcegraph instance [configured to use HTTP authentication](https://sourcegraph.com/docs/admin/auth#http-authentication-proxies). The external authentication provider feature allows clients to generate, for a specified endpoint, custom auth headers. Those headers will be attached to every authenticated http request instead of the normal `"Authorization": "token sgp_SOME_TOKEN"` auth header. To generate those custom headers client need to specify command that generates authentication headers for specific endpoints. The command must output a JSON object containing header key-value pairs on stdout. Those endpoints URLs needs to point to [proxies configured by client](https://sourcegraph.com/docs/admin/auth#http-authentication-proxies) which redirects requests to the custom sourcegraph instance. Whole flow looks like this: 1. When Cody attempts to connect to a endpoint which has defined external provider it executes the specified command 2. The command outputs a JSON object containing header key-value pairs on stdout 3. These headers are attached to subsequent authorised requests to the endpoint 4. The proxy server processes these headers and converts them to appropriate `X-Forwarded-User` and/or `X-Forwarded-Email` headers as specified in the [documentation](https://sourcegraph.com/docs/admin/auth#http-authentication-proxies) 5. The Sourcegraph instance authenticates the user based on these forwarded headers ### Configuration Users can configure custom authentication providers in their vscode settings.json using the following structure: ```json "cody.auth.externalProviders": [ { "endpoint": "http://localhost:5555", "executable": { "commandLine": ["echo '{ \"headers\": { \"Authorization\": \"Bearer SomeUser\" } }'"], "shell": "/bin/bash", // Optional: Shell to execute the command with. Default: '/bin/sh' on Unix, process.env.ComSpec on Windows. "environment": { // Optional: Additional environment variables "SOME_ENV": "VALUE" }, "timeout": 5000, // Optional: Timeout in milliseconds "windowsHide": true // Optional: Hide the window on Windows } } ] ``` It can also be configured in IntelliJ using settings editor: ![image](https://github.com/user-attachments/assets/5440b226-534f-471c-a78e-3c6f6d9c76c0) User can define as many external providers as needed. If only one provider is needed and login using this provider should be forced, it [will be possible to accomplish](#6574) using `overrideServerEndpoint`. ### Configuration Options * endpoint: The URL of the proxy server that will handle the authentication * executable: Configuration for the command that generates authentication headers - commandLine: Array of command and arguments to execute - shell: (Optional) Specific shell to use for command execution - environment: (Optional) Additional environment variables for the command - workingDir: (Optional) Working directory for command execution - timeout: (Optional) Command execution timeout - windowsHide: (Optional) Hide window when executing on Windows ### Expected Output Script or executable specified in the configuration have to return valid JSON object which adheres to the schema: ```json { "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "required": ["headers"], "properties": { "headers": { "type": "object", "additionalProperties": { "type": "string" } }, "expiration": { "type": "number" } } } ``` Where: * `headers` *(Required)* [Map with string keys and values] - headers which will be attached to authenticated requests to the http proxy * `expiration` *(Optional)* [a number] - Epoch Unix Timestamp (UTC) of the headers expiration date; after expiration date headers will be re-generated automatically using configured command ### Testing Locally 1. Start the provided reverse proxy: `python agent/scripts/reverse-proxy.py https://your-sourcegraph-instance.com 5555` You can choose different port or start a few different proxies for different endpoints. 2. Add the proxy configuration to your settings: ```json "cody.auth.externalProviders": [ { "endpoint": "http://localhost:5555", "executable": { "commandLine": ["echo '{ \"Authorization\": \"Bearer TestUser\" }'"], "shell": "/bin/bash" } } ] ``` 3. In Cody sign in to `http://localhost:5555` endpoint 4. Verify that you're authenticated as TestUser. ### Security Considerations 1. Ensure that the proxy server properly validates and sanitizes authentication headers 2. The executable should be secured and have appropriate permissions 3. Consider using HTTPS for the proxy endpoint in production environments ### Missing features 1. Fastpath users custom tokens for authentication, we need to check if and how we can support it with custom auth providers. 2. Cli is currently not supported, but should be trivial to add support for it. ## Test plan 1. Setup local testing environment as described in the `Testing Locally` section. 3. Run a full QA. ## Changelog <!-- OPTIONAL; info at https://www.notion.so/sourcegraph/Writing-a-changelog-entry-dd997f411d524caabf0d8d38a24a878c -->
- Loading branch information
Showing
45 changed files
with
585 additions
and
167 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
#!/usr/bin/env python3 | ||
|
||
from aiohttp import web, ClientSession | ||
from urllib.parse import urlparse | ||
import argparse | ||
import asyncio | ||
import re | ||
|
||
async def proxy_handler(request): | ||
async with ClientSession(auto_decompress=False) as session: | ||
print(f'Request to: {request.url}') | ||
|
||
# Modify headers here | ||
headers = dict(request.headers) | ||
|
||
# Reset the Host header to use target server host instead of the proxy host | ||
if 'Host' in headers: | ||
headers['Host'] = urlparse(target_url).netloc.split(':')[0] | ||
|
||
# 'chunked' encoding results in error 400 from Cloudflare, removing it still keeps response chunked anyway | ||
if 'Transfer-Encoding' in headers: | ||
del headers['Transfer-Encoding'] | ||
|
||
# Use value of 'Authorization: Bearer' to fill 'X-Forwarded-User' and remove 'Authorization' header | ||
if 'Authorization' in headers: | ||
match = re.match('Bearer (.*)', headers['Authorization']) | ||
if match: | ||
headers['X-Forwarded-User'] = match.group(1) | ||
del headers['Authorization'] | ||
|
||
# Forward the request to target | ||
async with session.request( | ||
method=request.method, | ||
url=f'{target_url}{request.path_qs}', | ||
headers=headers, | ||
data=await request.read() | ||
) as response: | ||
proxy_response = web.StreamResponse( | ||
status=response.status, | ||
headers=response.headers | ||
) | ||
|
||
await proxy_response.prepare(request) | ||
|
||
# Stream the response back | ||
async for chunk in response.content.iter_chunks(): | ||
await proxy_response.write(chunk[0]) | ||
|
||
await proxy_response.write_eof() | ||
return proxy_response | ||
|
||
app = web.Application() | ||
app.router.add_route('*', '/{path_info:.*}', proxy_handler) | ||
|
||
""" | ||
Reverse Proxy Server for testing External Auth Providers in Cody | ||
This script implements a simple reverse proxy server to facilitate testing of external authentication providers | ||
with Cody. It's role is to simulate simulate HTTP authentication proxy setups. It handles incoming requests by: | ||
- Forwarding them to a target Sourcegraph instance | ||
- Converting Bearer tokens from Authorization headers into X-Forwarded-User headers | ||
- Managing request/response streaming | ||
- Handling header modifications required for Cloudflare compatibility | ||
Target Sourcegraph instance needs to be configured to use HTTP authentication proxies | ||
as described in https://sourcegraph.com/docs/admin/auth#http-authentication-proxies | ||
""" | ||
if __name__ == '__main__': | ||
parser = argparse.ArgumentParser(description='External auth provider test proxy server') | ||
parser.add_argument('target_url', help='Target Sourcegraph instance URL to proxy to') | ||
parser.add_argument('proxy_port', type=int, nargs='?', default=5555, | ||
help='Port for the proxy server (default: %(default)s)') | ||
|
||
args = parser.parse_args() | ||
|
||
target_url = args.target_url.rstrip('/') | ||
port = args.proxy_port | ||
|
||
print(f'Starting proxy server on port {port} targeting {target_url}...') | ||
web.run_app(app, port=port) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
import { describe, expect, test } from 'vitest' | ||
import { type HeaderCredential, type TokenSource, isWindows } from '..' | ||
import { resolveAuth } from './auth-resolver' | ||
import type { ClientSecrets } from './resolver' | ||
|
||
class TempClientSecrets implements ClientSecrets { | ||
constructor(readonly store: Map<string, [string, TokenSource]>) {} | ||
|
||
getToken(endpoint: string): Promise<string | undefined> { | ||
return Promise.resolve(this.store.get(endpoint)?.[0]) | ||
} | ||
getTokenSource(endpoint: string): Promise<TokenSource | undefined> { | ||
return Promise.resolve(this.store.get(endpoint)?.[1]) | ||
} | ||
} | ||
|
||
describe('auth-resolver', () => { | ||
test('resolve with serverEndpoint and credentials overrides', async () => { | ||
const auth = await resolveAuth( | ||
'sourcegraph.com', | ||
{ | ||
authExternalProviders: [], | ||
overrideServerEndpoint: 'my-endpoint.com', | ||
overrideAuthToken: 'my-token', | ||
}, | ||
new TempClientSecrets(new Map([['sourcegraph.com/', ['sgp_212323123', 'paste']]])) | ||
) | ||
|
||
expect(auth.serverEndpoint).toBe('my-endpoint.com/') | ||
expect(auth.credentials).toEqual({ token: 'my-token' }) | ||
}) | ||
|
||
test('resolve with serverEndpoint override', async () => { | ||
const auth = await resolveAuth( | ||
'sourcegraph.com', | ||
{ | ||
authExternalProviders: [], | ||
overrideServerEndpoint: 'my-endpoint.com', | ||
overrideAuthToken: undefined, | ||
}, | ||
new TempClientSecrets(new Map([['my-endpoint.com/', ['sgp_212323123', 'paste']]])) | ||
) | ||
|
||
expect(auth.serverEndpoint).toBe('my-endpoint.com/') | ||
expect(auth.credentials).toEqual({ token: 'sgp_212323123', source: 'paste' }) | ||
}) | ||
|
||
test('resolve with token override', async () => { | ||
const auth = await resolveAuth( | ||
'sourcegraph.com', | ||
{ | ||
authExternalProviders: [], | ||
overrideServerEndpoint: undefined, | ||
overrideAuthToken: 'my-token', | ||
}, | ||
new TempClientSecrets(new Map([['sourcegraph.com/', ['sgp_777777777', 'paste']]])) | ||
) | ||
|
||
expect(auth.serverEndpoint).toBe('sourcegraph.com/') | ||
expect(auth.credentials).toEqual({ token: 'my-token' }) | ||
}) | ||
|
||
test('resolve custom auth provider', async () => { | ||
const credentialsJson = JSON.stringify({ | ||
headers: { Authorization: 'token X' }, | ||
expiration: 1337, | ||
}) | ||
|
||
const auth = await resolveAuth( | ||
'sourcegraph.com', | ||
{ | ||
authExternalProviders: [ | ||
{ | ||
endpoint: 'https://my-server.com', | ||
executable: { | ||
commandLine: [ | ||
isWindows() ? `echo ${credentialsJson}` : `echo '${credentialsJson}'`, | ||
], | ||
shell: isWindows() ? process.env.ComSpec : '/bin/bash', | ||
timeout: 5000, | ||
windowsHide: true, | ||
}, | ||
}, | ||
], | ||
overrideServerEndpoint: 'https://my-server.com', | ||
overrideAuthToken: undefined, | ||
}, | ||
new TempClientSecrets(new Map()) | ||
) | ||
|
||
expect(auth.serverEndpoint).toBe('https://my-server.com/') | ||
|
||
const headerCredential = auth.credentials as HeaderCredential | ||
expect(headerCredential.expiration).toBe(1337) | ||
expect(headerCredential.getHeaders()).toStrictEqual({ | ||
Authorization: 'token X', | ||
}) | ||
|
||
expect(JSON.stringify(headerCredential)).not.toContain('token X') | ||
}) | ||
}) |
Oops, something went wrong.