Skip to content

Commit 6a9e9a1

Browse files
committed
chore: updates Globus registration to better match refactor and uses alternate approach for scope/token discovery.
1 parent 5f8839d commit 6a9e9a1

File tree

6 files changed

+120
-51
lines changed

6 files changed

+120
-51
lines changed

package.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,11 +219,17 @@
219219
"neuroglancer/datasource/dvid:disabled": "./src/util/false.ts",
220220
"default": "./src/datasource/dvid/register_credentials_provider.ts"
221221
},
222+
"#datasource/globus/register_default": {
223+
"neuroglancer/datasource/globus:enabled": "./src/datasource/globus/register_default.ts",
224+
"neuroglancer/datasource:none_by_default": "./src/util/false.ts",
225+
"neuroglancer/datasource/globus:disabled": "./src/util/false.ts",
226+
"default": "./src/datasource/globus/register_default.ts"
227+
},
222228
"#datasource/globus/register_credentials_provider": {
223229
"neuroglancer/python": "./src/util/false.ts",
224230
"neuroglancer/datasource/globus:enabled": "./src/datasource/globus/register_credentials_provider.ts",
225231
"neuroglancer/datasource:none_by_default": "./src/util/false.ts",
226-
"neuroglancer/datasource/globus:disabled": "./src/datasource/globus/register_credentials_provider.ts",
232+
"neuroglancer/datasource/globus:disabled": "./src/util/false.ts",
227233
"default": "./src/datasource/globus/register_credentials_provider.ts"
228234
},
229235
"#datasource/graphene/backend": {

src/datasource/enabled_frontend_modules.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import "#datasource/brainmaps/register_credentials_provider";
66
import "#datasource/deepzoom/register_default";
77
import "#datasource/dvid/register_default";
88
import "#datasource/dvid/register_credentials_provider";
9+
import "#datasource/globus/register_default";
910
import "#datasource/globus/register_credentials_provider";
1011
import "#datasource/graphene/register_default";
1112
import "#datasource/n5/register_default";

src/datasource/globus/credentials_provider.ts

Lines changed: 34 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import {
44
} from "#src/credentials_provider/index.js";
55
import type { OAuth2Credentials } from "#src/credentials_provider/oauth2.js";
66
import { StatusMessage } from "#src/status.js";
7-
import { uncancelableToken } from "#src/util/cancellation.js";
87
import { HttpError } from "#src/util/http_request.js";
98
import {
109
generateCodeChallenge,
@@ -17,17 +16,13 @@ const GLOBUS_AUTH_HOST = "https://auth.globus.org";
1716
const REDIRECT_URI = new URL("./globus_oauth2_redirect.html", import.meta.url)
1817
.href;
1918

20-
function getRequiredScopes(endpoint: string) {
21-
return `https://auth.globus.org/scopes/${endpoint}/https`;
22-
}
23-
2419
function getGlobusAuthorizeURL({
25-
endpoint,
20+
scope,
2621
clientId,
2722
code_challenge,
2823
state,
2924
}: {
30-
endpoint: string;
25+
scope: string[];
3126
clientId: string;
3227
code_challenge: string;
3328
state: string;
@@ -39,7 +34,7 @@ function getGlobusAuthorizeURL({
3934
url.searchParams.set("code_challenge", code_challenge);
4035
url.searchParams.set("code_challenge_method", "S256");
4136
url.searchParams.set("state", state);
42-
url.searchParams.set("scope", getRequiredScopes(endpoint));
37+
url.searchParams.set("scope", scope.join(" "));
4338
return url.toString();
4439
}
4540

@@ -84,7 +79,7 @@ function getStorage() {
8479

8580
async function waitForAuth(
8681
clientId: string,
87-
gcsHttpsHost: string,
82+
globusConnectServerDomain: string,
8883
): Promise<OAuth2Credentials> {
8984
const status = new StatusMessage(/*delay=*/ false, /*modal=*/ true);
9085

@@ -97,44 +92,35 @@ async function waitForAuth(
9792

9893
frag.appendChild(title);
9994

100-
let identifier = getStorage().domainMappings?.[gcsHttpsHost];
101-
10295
const link = document.createElement("button");
10396
link.textContent = "Log in to Globus";
104-
link.disabled = true;
105-
106-
if (!identifier) {
107-
const label = document.createElement("label");
108-
label.textContent = "Globus Collection UUID";
109-
label.style.display = "block";
110-
label.style.margin = ".5em 0";
111-
frag.appendChild(label);
112-
const endpoint = document.createElement("input");
113-
endpoint.style.width = "100%";
114-
endpoint.style.margin = ".5em 0";
115-
endpoint.type = "text";
116-
endpoint.placeholder = "a17d7fac-ce06-4ede-8318-ad8dc98edd69";
117-
endpoint.addEventListener("input", async (e) => {
118-
identifier = (e.target as HTMLInputElement).value;
119-
link.disabled = !identifier;
120-
});
121-
frag.appendChild(endpoint);
122-
} else {
123-
link.disabled = false;
124-
}
12597

12698
link.addEventListener("click", async (event) => {
12799
event.preventDefault();
128-
if (!identifier) {
129-
status.setText("You must provide a Globus Collection UUID.");
130-
return;
131-
}
100+
/**
101+
* We make a request to the Globus Connect Server domain **even though we _know_ we're
102+
* unauthorized** to get the required consents for the resource.
103+
*/
104+
console.log(globusConnectServerDomain);
105+
const authorizationIntrospectionRequest = await fetch(
106+
globusConnectServerDomain,
107+
{
108+
method: "GET",
109+
headers: {
110+
"X-Requested-With": "XMLHttpRequest",
111+
},
112+
},
113+
);
114+
115+
const { authorization_parameters } =
116+
await authorizationIntrospectionRequest.json();
117+
132118
const verifier = generateCodeVerifier();
133119
const state = getRandomHexString();
134120
const challenge = await generateCodeChallenge(verifier);
135121
const url = getGlobusAuthorizeURL({
136122
clientId,
137-
endpoint: identifier,
123+
scope: authorization_parameters.required_scopes,
138124
code_challenge: challenge,
139125
state,
140126
});
@@ -154,7 +140,6 @@ async function waitForAuth(
154140
const token = await waitForPKCEResponseMessage({
155141
source,
156142
state,
157-
cancellationToken: uncancelableToken,
158143
tokenExchangeCallback: async (code) => {
159144
const response = await fetch(
160145
getGlobusTokenURL({ clientId, code, code_verifier: verifier }),
@@ -195,7 +180,7 @@ async function waitForAuth(
195180
};
196181
storage.domainMappings = {
197182
...storage.domainMappings,
198-
[gcsHttpsHost]: rawToken.resource_server,
183+
[globusConnectServerDomain]: rawToken.resource_server,
199184
};
200185

201186
localStorage.setItem("globus", JSON.stringify(storage));
@@ -215,30 +200,35 @@ async function waitForAuth(
215200
export class GlobusCredentialsProvider extends CredentialsProvider<OAuth2Credentials> {
216201
constructor(
217202
public clientId: string,
218-
public gcsHttpsHost: string,
203+
public assetUrl: URL,
219204
) {
220205
super();
221206
}
222207
get = makeCredentialsGetter(async () => {
223-
const resourceServer = getStorage().domainMappings?.[this.gcsHttpsHost];
208+
const globusConnectServerDomain = this.assetUrl.origin;
209+
210+
const resourceServer =
211+
getStorage().domainMappings?.[globusConnectServerDomain];
224212
const token = resourceServer
225213
? getStorage().authorizations?.[resourceServer]
226214
: undefined;
215+
227216
if (!token) {
228-
return await waitForAuth(this.clientId, this.gcsHttpsHost);
217+
return await waitForAuth(this.clientId, globusConnectServerDomain);
229218
}
230-
const response = await fetch(`${this.gcsHttpsHost}`, {
219+
const response = await fetch(this.assetUrl, {
231220
method: "HEAD",
232221
headers: {
233222
"X-Requested-With": "XMLHttpRequest",
234223
Authorization: `${token?.tokenType} ${token?.accessToken}`,
235224
},
236225
});
226+
237227
switch (response.status) {
238228
case 200:
239229
return token;
240230
case 401:
241-
return await waitForAuth(this.clientId, this.gcsHttpsHost);
231+
return await waitForAuth(this.clientId, globusConnectServerDomain);
242232
default:
243233
throw HttpError.fromResponse(response);
244234
}

src/datasource/globus/register_credentials_provider.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { defaultCredentialsManager } from "#src/credentials_provider/default_manager.js";
1+
import { registerDefaultCredentialsProvider } from "#src/credentials_provider/default_manager.js";
22
import { GlobusCredentialsProvider } from "#src/datasource/globus/credentials_provider.js";
33

44
export declare const GLOBUS_CLIENT_ID: string | undefined;
@@ -8,7 +8,7 @@ export function isGlobusEnabled() {
88
}
99

1010
if (typeof GLOBUS_CLIENT_ID !== "undefined") {
11-
defaultCredentialsManager.register(
11+
registerDefaultCredentialsProvider(
1212
"globus",
1313
(serverUrl) => new GlobusCredentialsProvider(GLOBUS_CLIENT_ID, serverUrl),
1414
);
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/**
2+
* @license
3+
* Copyright 2024 Google Inc.
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import type {
18+
CredentialsManager,
19+
CredentialsProvider,
20+
} from "#src/credentials_provider/index.js";
21+
import type { OAuth2Credentials } from "#src/credentials_provider/oauth2.js";
22+
import { fetchOkWithOAuth2CredentialsAdapter } from "#src/credentials_provider/oauth2.js";
23+
import { isGlobusEnabled } from "#src/datasource/globus/register_credentials_provider.js";
24+
import type { BaseKvStoreProvider } from "#src/kvstore/context.js";
25+
import { HttpKvStore } from "#src/kvstore/http/index.js";
26+
import type { SharedKvStoreContextBase } from "#src/kvstore/register.js";
27+
import { frontendBackendIsomorphicKvStoreProviderRegistry } from "#src/kvstore/register.js";
28+
import { getBaseHttpUrlAndPath } from "#src/kvstore/url.js";
29+
30+
function getGlobusCredentialsProvider(
31+
credentialsManager: CredentialsManager,
32+
url: string,
33+
): CredentialsProvider<OAuth2Credentials> {
34+
return credentialsManager.getCredentialsProvider("globus", new URL(url));
35+
}
36+
37+
const SCHEME_PREFIX = "globus+";
38+
39+
function globusProvider(
40+
scheme: string,
41+
context: SharedKvStoreContextBase,
42+
): BaseKvStoreProvider {
43+
return {
44+
scheme: SCHEME_PREFIX + scheme,
45+
description: `Globus Connect Server via ${scheme}`,
46+
getKvStore(url) {
47+
const httpUrl = url.url.substring(SCHEME_PREFIX.length);
48+
const credentialsProvider = getGlobusCredentialsProvider(
49+
context.credentialsManager,
50+
httpUrl,
51+
);
52+
try {
53+
const { baseUrl, path } = getBaseHttpUrlAndPath(httpUrl);
54+
return {
55+
store: new HttpKvStore(
56+
context.chunkManager.memoize,
57+
baseUrl,
58+
SCHEME_PREFIX + baseUrl,
59+
fetchOkWithOAuth2CredentialsAdapter(credentialsProvider),
60+
),
61+
path,
62+
};
63+
} catch (e) {
64+
throw new Error(`Invalid URL ${JSON.stringify(url.url)}`, {
65+
cause: e,
66+
});
67+
}
68+
},
69+
};
70+
}
71+
72+
if (isGlobusEnabled()) {
73+
frontendBackendIsomorphicKvStoreProviderRegistry.registerBaseKvStoreProvider(
74+
(context) => globusProvider("https", context),
75+
);
76+
}

src/util/pkce.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import type { OAuth2Credentials } from "#src/credentials_provider/oauth2.js";
2-
import { type CancellationToken, CANCELED } from "#src/util/cancellation.js";
32
import { RefCounted } from "#src/util/disposable.js";
43
import {
54
verifyObject,
@@ -63,12 +62,10 @@ export async function generateCodeChallenge(verifier: string) {
6362
export async function waitForPKCEResponseMessage({
6463
source,
6564
state,
66-
cancellationToken,
6765
tokenExchangeCallback,
6866
}: {
6967
source: Window;
7068
state: string;
71-
cancellationToken: CancellationToken;
7269
/**
7370
* Callback to exchange the received code for OAuth2 credentials.
7471
* This will be called when a valid message (`code` and origin match) is received from the `source`.
@@ -78,7 +75,6 @@ export async function waitForPKCEResponseMessage({
7875
const context = new RefCounted();
7976
try {
8077
return await new Promise((resolve, reject) => {
81-
context.registerDisposer(cancellationToken.add(() => reject(CANCELED)));
8278
context.registerEventListener(
8379
window,
8480
"message",

0 commit comments

Comments
 (0)