Skip to content
This repository was archived by the owner on Jan 22, 2019. It is now read-only.

Commit 6acd186

Browse files
authored
feat: use access tokens for authentication (#245)
This PR changes request logic to use access tokens. This means that admins won't have to add their code hosts to the `corsOrigin` anymore. This fixes `corsOrigin` errors because we are able to make requests to the API from the options menu. Any request from the options menu will create an access token. I landed on creating a token on each request rather than explicitly creating a token when entering and saving a new URL because multiple requests go out at different times and it was difficult to make this happen before other requests go out. The easiest way was to get to the root of each request. Without this architecture change/decision, some requests would fail with a 401 which could cause problems. You would have to click save and then refresh the page for everything to work properly.
1 parent 6c1fc71 commit 6acd186

File tree

9 files changed

+316
-133
lines changed

9 files changed

+316
-133
lines changed

src/browser/types.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,21 @@ export const featureFlagDefaults: FeatureFlags = {
2020
newInject: false,
2121
}
2222

23+
/** A map where the key is the server URL and the value is the token. */
24+
export interface AccessTokens {
25+
[url: string]: string
26+
}
27+
2328
// TODO(chris) Switch to Partial<StorageItems> to eliminate bugs caused by
2429
// missing items.
2530
export interface StorageItems {
2631
sourcegraphURL: string
32+
/**
33+
* The current users access tokens the different sourcegraphUrls they have
34+
* had configured.
35+
*/
36+
accessTokens: AccessTokens
37+
2738
gitHubEnterpriseURL: string
2839
phabricatorURL: string
2940
inlineSymbolSearchEnabled: boolean
@@ -66,6 +77,8 @@ interface ClientConfigurationDetails {
6677

6778
export const defaultStorageItems: StorageItems = {
6879
sourcegraphURL: 'https://sourcegraph.com',
80+
accessTokens: {},
81+
6982
serverUrls: ['https://sourcegraph.com'],
7083
gitHubEnterpriseURL: '',
7184
phabricatorURL: '',

src/extension/scripts/background.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,10 @@ permissions.onRemoved(permissions => {
162162
})
163163

164164
storage.addSyncMigration((items, set, remove) => {
165+
if (!items.accessTokens) {
166+
set({ accessTokens: {} })
167+
}
168+
165169
if (items.phabricatorURL) {
166170
remove('phabricatorURL')
167171

src/shared/auth/access_token.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { omit } from 'lodash'
2+
import { Observable } from 'rxjs'
3+
import { switchMap } from 'rxjs/operators'
4+
import storage from '../../browser/storage'
5+
6+
export const getAccessToken = (url: string): Observable<string | undefined> =>
7+
new Observable(observer => {
8+
storage.getSync(items => {
9+
observer.next(items.accessTokens[url])
10+
observer.complete()
11+
})
12+
})
13+
14+
export const setAccessToken = (url: string) => (tokens: Observable<string>): Observable<string> =>
15+
tokens.pipe(
16+
switchMap(
17+
token =>
18+
new Observable<string>(observer => {
19+
storage.getSync(({ accessTokens }) =>
20+
storage.setSync({ accessTokens: { ...accessTokens, [url]: token } }, () => {
21+
observer.next(token)
22+
observer.complete()
23+
})
24+
)
25+
})
26+
)
27+
)
28+
29+
export const removeAccessToken = (url: string): Observable<void> =>
30+
new Observable(observer => {
31+
storage.getSync(({ accessTokens }) =>
32+
storage.setSync({ accessTokens: omit(accessTokens, url) }, () => {
33+
observer.next()
34+
observer.complete()
35+
})
36+
)
37+
})

src/shared/backend/auth.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { map } from 'rxjs/operators'
2+
import { GQL } from '../../types/gqlschema'
3+
import { getPlatformName } from '../util/context'
4+
import { memoizeObservable } from '../util/memoize'
5+
import { getContext } from './context'
6+
import { createAggregateError } from './errors'
7+
import { mutateGraphQLNoRetry } from './graphql'
8+
9+
/**
10+
* Create an access token for the current user on the currently configured
11+
* sourcegraph instance.
12+
*/
13+
export const createAccessToken = memoizeObservable((userID: GQL.ID) =>
14+
mutateGraphQLNoRetry(
15+
getContext({ repoKey: '' }),
16+
`
17+
mutation CreateAccessToken($userID: ID!, $scopes: [String!]!, $note: String!) {
18+
createAccessToken(user: $userID, scopes: $scopes, note: $note) {
19+
id
20+
token
21+
}
22+
}
23+
`,
24+
{ userID, scopes: ['user:all'], note: `sourcegraph-${getPlatformName()}` },
25+
false
26+
).pipe(
27+
map(({ data, errors }) => {
28+
if (!data || !data.createAccessToken || (errors && errors.length > 0)) {
29+
throw createAggregateError(errors)
30+
}
31+
return data.createAccessToken.token
32+
})
33+
)
34+
)

src/shared/backend/graphql.tsx

Lines changed: 87 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ import { QueryResult } from '@sourcegraph/extensions-client-common/lib/graphql'
22
import { IQuery } from '@sourcegraph/extensions-client-common/lib/schema/graphqlschema'
33
import { Observable, throwError } from 'rxjs'
44
import { ajax } from 'rxjs/ajax'
5-
import { catchError, map } from 'rxjs/operators'
5+
import { catchError, map, switchMap } from 'rxjs/operators'
66
import { GQL } from '../../types/gqlschema'
7+
import { removeAccessToken } from '../auth/access_token'
78
import { DEFAULT_SOURCEGRAPH_URL, isPrivateRepository, repoUrlCache, sourcegraphUrl } from '../util/context'
89
import { RequestContext } from './context'
910
import { AuthRequiredError, createAuthRequiredError, NoSourcegraphURLError } from './errors'
@@ -17,19 +18,33 @@ export interface MutationResult {
1718
errors?: GQL.IGraphQLResponseError[]
1819
}
1920

21+
interface RequestGraphQLOptions {
22+
/** Whether we should use the retry logic to fall back to other URLs. */
23+
retry?: boolean
24+
/**
25+
* Whether or not to use an access token for the request. All requests
26+
* except requests used while creating an access token should use an access
27+
* token. i.e. `createAccessToken` and the `fetchCurrentUser` used to get the
28+
* user ID for `createAccessToken`.
29+
*/
30+
useAccessToken?: boolean
31+
}
32+
2033
/**
2134
* Does a GraphQL request to the Sourcegraph GraphQL API running under `/.api/graphql`
2235
*
2336
* @param request The GraphQL request (query or mutation)
2437
* @param variables A key/value object with variable values
38+
* @param url the url the request is going to
39+
* @param options configuration options for the request
2540
* @return Observable That emits the result or errors if the HTTP request failed
2641
*/
2742
function requestGraphQL(
2843
ctx: RequestContext,
2944
request: string,
3045
variables: any = {},
3146
url: string = sourcegraphUrl,
32-
retry = true,
47+
{ retry, useAccessToken }: RequestGraphQLOptions = { retry: true, useAccessToken: true },
3348
authError?: AuthRequiredError
3449
): Observable<GQL.IGraphQLResponseRoot> {
3550
// Check if it's a private repo - if so don't make a request to Sourcegraph.com.
@@ -39,44 +54,67 @@ function requestGraphQL(
3954
const nameMatch = request.match(/^\s*(?:query|mutation)\s+(\w+)/)
4055
const queryName = nameMatch ? '?' + nameMatch[1] : ''
4156

42-
return ajax({
43-
method: 'POST',
44-
url: `${url}/.api/graphql` + queryName,
45-
headers: getHeaders(),
46-
crossDomain: true,
47-
withCredentials: true,
48-
body: JSON.stringify({ query: request, variables }),
49-
async: true,
50-
}).pipe(
51-
map(({ response }) => {
52-
if (shouldResponseTriggerRetryOrError(response)) {
53-
delete repoUrlCache[ctx.repoKey]
54-
throw response
55-
}
56-
if (ctx.isRepoSpecific && response.data.repository) {
57-
repoUrlCache[ctx.repoKey] = url
58-
}
59-
return response
60-
}),
61-
catchError(err => {
62-
if (err.status === 401) {
63-
// Ensure all urls are tried and update authError to be the last seen 401.
64-
// This ensures that the correct URL is used for sign in and also that all possible
65-
// urls were checked.
66-
authError = createAuthRequiredError(url)
67-
}
57+
return getHeaders(url, useAccessToken).pipe(
58+
switchMap(headers =>
59+
ajax({
60+
method: 'POST',
61+
url: `${url}/.api/graphql` + queryName,
62+
headers,
63+
crossDomain: true,
64+
withCredentials: true,
65+
body: JSON.stringify({ query: request, variables }),
66+
async: true,
67+
}).pipe(
68+
map(({ response }) => {
69+
if (shouldResponseTriggerRetryOrError(response)) {
70+
delete repoUrlCache[ctx.repoKey]
71+
throw response
72+
}
73+
if (ctx.isRepoSpecific && response.data.repository) {
74+
repoUrlCache[ctx.repoKey] = url
75+
}
76+
return response
77+
}),
78+
catchError(err => {
79+
if (err.status === 401) {
80+
// Ensure all urls are tried and update authError to be the last seen 401.
81+
// This ensures that the correct URL is used for sign in and also that all possible
82+
// urls were checked.
83+
authError = createAuthRequiredError(url)
84+
85+
if (headers && headers.authorization) {
86+
// If we got a 401 with a token, get rid of the and
87+
// try again. The token may be invalid and we just
88+
// need to recreate one.
89+
return removeAccessToken(url).pipe(
90+
switchMap(() =>
91+
requestGraphQL(ctx, request, variables, url, { retry, useAccessToken }, authError)
92+
)
93+
)
94+
}
95+
}
96+
97+
if (!retry || url === DEFAULT_SOURCEGRAPH_URL) {
98+
// If there was an auth error and we tried all of the possible URLs throw the auth error.
99+
if (authError) {
100+
throw authError
101+
}
102+
delete repoUrlCache[ctx.repoKey]
103+
// We just tried the last url
104+
throw err
105+
}
68106

69-
if (!retry || url === DEFAULT_SOURCEGRAPH_URL) {
70-
// If there was an auth error and we tried all of the possible URLs throw the auth error.
71-
if (authError) {
72-
throw authError
73-
}
74-
delete repoUrlCache[ctx.repoKey]
75-
// We just tried the last url
76-
throw err
77-
}
78-
return requestGraphQL(ctx, request, variables, DEFAULT_SOURCEGRAPH_URL, retry, authError)
79-
})
107+
return requestGraphQL(
108+
ctx,
109+
request,
110+
variables,
111+
DEFAULT_SOURCEGRAPH_URL,
112+
{ retry, useAccessToken: true },
113+
authError
114+
)
115+
})
116+
)
117+
)
80118
)
81119
}
82120

@@ -140,9 +178,12 @@ export function queryGraphQLNoRetry(
140178
ctx: RequestContext,
141179
query: string,
142180
variables: any = {},
143-
url: string = sourcegraphUrl
181+
url: string = sourcegraphUrl,
182+
useAccessToken?: boolean
144183
): Observable<QueryResult<IQuery>> {
145-
return requestGraphQL(ctx, query, variables, url, false) as Observable<QueryResult<IQuery>>
184+
return requestGraphQL(ctx, query, variables, url, { retry: false, useAccessToken }) as Observable<
185+
QueryResult<IQuery>
186+
>
146187
}
147188

148189
/**
@@ -167,7 +208,10 @@ export function mutateGraphQL(ctx: RequestContext, mutation: string, variables:
167208
export function mutateGraphQLNoRetry(
168209
ctx: RequestContext,
169210
mutation: string,
170-
variables: any = {}
211+
variables: any = {},
212+
useAccessToken?: boolean
171213
): Observable<MutationResult> {
172-
return requestGraphQL(ctx, mutation, variables, sourcegraphUrl, false) as Observable<MutationResult>
214+
return requestGraphQL(ctx, mutation, variables, sourcegraphUrl, { retry: false, useAccessToken }) as Observable<
215+
MutationResult
216+
>
173217
}

0 commit comments

Comments
 (0)