-
Notifications
You must be signed in to change notification settings - Fork 372
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Refactor external auth providers to re-generate headers on demand #6687
Conversation
@@ -23,8 +23,7 @@ export interface AuthCredentials { | |||
|
|||
export interface HeaderCredential { | |||
// We use function instead of property to prevent accidential top level serialization - we never want to store this data | |||
getHeaders(): Record<string, string> | |||
expiration: number | undefined |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Exposing it in the API only complicates the code, we don't need it there.
@@ -33,28 +37,43 @@ export class OpenTelemetryService { | |||
|
|||
constructor() { | |||
this.configSubscription = combineLatest( | |||
externalAuthRefresh, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A little hack, as CodyTraceExport
does not expose API which would allow us to add custom headers per request, so we need to create new instance of it every time headers changes.
Changes are simple though, diff is big due to indentation change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, this is not watertight. We could leak the credentials from site X to trace provider for site Y if the external auth refresh and resolved config timings are out of step.
Also because the external auth refresh pings for changes but doesn't carry the value (which seems good, it will catch up immediately) if there's some rapid changes to auth, maybe things can get out of step more easily.
Could you drop a TODO here and point to https://linear.app/sourcegraph/project/cody-auth-state-rewritecleanup-7ac1409cc579/overview or a task within it, that this state should be joined?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point, I will add a TODO!
On a good side I do not think we would leak anything either way because I added safeguard to the function sending headers:
export function addAuthHeaders(auth: AuthCredentials, headers: Headers, url: URL): void {
// We want to be sure we sent authorization headers only to the valid endpoint
if (auth.credentials && url.host === new URL(auth.serverEndpoint).host) {
So in worst case request won't pass because of missing credentials, but it will not leak them.
But I agree it's a problem regardless.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh nice, I missed that guard. 👍
c9d62f0
to
1027505
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some feedback inline.
return credentials | ||
} | ||
|
||
async function createTokenCredentails( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
async function createTokenCredentails( | |
async function createTokenCredentials( |
|
||
return undefined | ||
function createHeaderCredentails( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
function createHeaderCredentails( | |
function createHeaderCredentials( |
|
||
export class ExternalProviderAuthError extends Error {} | ||
|
||
export function isExternalProviderAuthError(error: unknown): boolean { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use a type predicate so the result of this check flows into TypeScript type inference, see https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates
export function isExternalProviderAuthError(error: unknown): boolean { | |
export function isExternalProviderAuthError(error: unknown): error is ExternalAuthProvider { |
@@ -33,28 +37,43 @@ export class OpenTelemetryService { | |||
|
|||
constructor() { | |||
this.configSubscription = combineLatest( | |||
externalAuthRefresh, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, this is not watertight. We could leak the credentials from site X to trace provider for site Y if the external auth refresh and resolved config timings are out of step.
Also because the external auth refresh pings for changes but doesn't carry the value (which seems good, it will catch up immediately) if there's some rapid changes to auth, maybe things can get out of step more easily.
Could you drop a TODO here and point to https://linear.app/sourcegraph/project/cody-auth-state-rewritecleanup-7ac1409cc579/overview or a task within it, that this state should be joined?
async getHeaders() { | ||
try { | ||
if (!_headersCache || hasExpired((await _headersCache)?.expiration)) { | ||
_headersCache = getExternalProviderHeaders(externalProvider) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This isn't right. You can have two race in here and both call getExternalProviderHeaders
one after the other.
Typical pattern would be to check the Promise you awaited is the one you're resetting, something like:
while (true) {
const observed = _headersCache;
if (!observed || hasExpired(await observed)?.expiration) {
if (observed !== _headersCache) {
continue; // retry
}
observed = _headersCache = getExternalProviderHeaders(externalProvider);
}
return (await observed).headers;
}
throw new Error(`Output of the external auth command is invalid: ${result}`) | ||
} | ||
|
||
if (credentials.expiration && hasExpired(credentials.expiration)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hasExpired
already handles undefined
as "not expired" so you can simplify this to skip credentials.expiration &&
1027505
to
f089485
Compare
f089485
to
9831df6
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Splendid!
@@ -33,28 +37,43 @@ export class OpenTelemetryService { | |||
|
|||
constructor() { | |||
this.configSubscription = combineLatest( | |||
externalAuthRefresh, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh nice, I missed that guard. 👍
Changes
That PR eliminates annoying chat reload upon an token refresh if external auth provider is used.
It does so by splitting external auth provider evaluation into two steps which happens at different stages: config resolution and http request building.
At config resolution we only evaluate config itself and prepare a function which will be later used to generate auth headers later. That function (
getHeaders
) is then used to obtain auth headers every time we are doing an authorised http request. Normally headers are cached, but if they expiregetHeaders
should internally refresh the cache and return an updated headers. If updated headers are still expired error will be shown to the user.In opposition to the previous solution errors in executing external provider command, or token expiration, does not lead
to the config invalidation - config always remains the same unless changed by the user. The only thing which changes is result of
getHeaders
functions.Errors from
getHeaders
are processed and handled in the same way as e.g.Network Error
orInvalid Access Token
, so we do not need custom code to handle them.Test plan
Setup
Set
cody.auth.externalProviders
config to use provider which support credentials expiration.You can use configuration shown bellow.
Please also make sure you have proxy running and your sourcegraph server have http auth proxy enabled, as described in #6526.
Scenario 1:
simple-external-auth-provider.py
should be successfully signed-in assomeuser
simple-external-auth-provider.py
and changecurrent_epoch = int(time.time()) + 30
tocurrent_epoch = int(time.time()) - 30
simple-external-auth-provider.py
Scenario 2:
simple-external-auth-provider.py
should be successfully signed-in assomeuser
simple-external-auth-provider.py
and changecurrent_epoch = int(time.time()) + 30
tocurrent_epoch = int(time.time()) - 30
They should be successful, but when 30 sec will pas since last action before the
simple-external-auth-provider.py
edit, you should get an credential expiration errorsimple-external-auth-provider.py
Scenario 3:
simple-external-auth-provider.py
and changecurrent_epoch = int(time.time()) + 30
tocurrent_epoch = int(time.time()) - 30
simple-external-auth-provider.py