@jbrowse/plugin-authentication 4.2.1 → 5.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/esm/DropboxOAuthModel/DropboxIcon.d.ts +1 -1
  2. package/esm/DropboxOAuthModel/configSchema.d.ts +110 -82
  3. package/esm/DropboxOAuthModel/configSchema.js +32 -2
  4. package/esm/DropboxOAuthModel/index.js +2 -2
  5. package/esm/DropboxOAuthModel/model.d.ts +235 -201
  6. package/esm/DropboxOAuthModel/model.js +46 -21
  7. package/esm/DropboxOAuthModel/util.d.ts +1 -1
  8. package/esm/DropboxOAuthModel/util.js +9 -12
  9. package/esm/ExternalTokenModel/ExternalTokenEntryForm.d.ts +1 -1
  10. package/esm/ExternalTokenModel/ExternalTokenEntryForm.js +11 -15
  11. package/esm/ExternalTokenModel/configSchema.d.ts +43 -24
  12. package/esm/ExternalTokenModel/configSchema.js +22 -1
  13. package/esm/ExternalTokenModel/index.js +2 -2
  14. package/esm/ExternalTokenModel/model.d.ts +47 -71
  15. package/esm/ExternalTokenModel/model.js +11 -54
  16. package/esm/GoogleDriveOAuthModel/GoogleDriveFilehandle.d.ts +2 -9
  17. package/esm/GoogleDriveOAuthModel/GoogleDriveFilehandle.js +27 -6
  18. package/esm/GoogleDriveOAuthModel/GoogleDriveIcon.d.ts +1 -1
  19. package/esm/GoogleDriveOAuthModel/configSchema.d.ts +109 -82
  20. package/esm/GoogleDriveOAuthModel/configSchema.js +32 -3
  21. package/esm/GoogleDriveOAuthModel/index.js +2 -2
  22. package/esm/GoogleDriveOAuthModel/model.d.ts +230 -202
  23. package/esm/GoogleDriveOAuthModel/model.js +45 -22
  24. package/esm/GoogleDriveOAuthModel/util.d.ts +1 -1
  25. package/esm/GoogleDriveOAuthModel/util.js +2 -11
  26. package/esm/HTTPBasicModel/HTTPBasicLoginForm.d.ts +1 -1
  27. package/esm/HTTPBasicModel/HTTPBasicLoginForm.js +17 -22
  28. package/esm/HTTPBasicModel/configSchema.d.ts +51 -28
  29. package/esm/HTTPBasicModel/configSchema.js +26 -1
  30. package/esm/HTTPBasicModel/index.js +2 -2
  31. package/esm/HTTPBasicModel/model.d.ts +55 -76
  32. package/esm/HTTPBasicModel/model.js +21 -51
  33. package/esm/OAuthModel/OAuthLoginPrompt.d.ts +4 -0
  34. package/esm/OAuthModel/OAuthLoginPrompt.js +12 -0
  35. package/esm/OAuthModel/configSchema.d.ts +79 -52
  36. package/esm/OAuthModel/configSchema.js +30 -1
  37. package/esm/OAuthModel/index.js +2 -2
  38. package/esm/OAuthModel/model.d.ts +218 -114
  39. package/esm/OAuthModel/model.js +361 -217
  40. package/esm/OAuthModel/util.d.ts +46 -7
  41. package/esm/OAuthModel/util.js +159 -16
  42. package/esm/index.d.ts +295 -981
  43. package/esm/index.js +29 -45
  44. package/esm/lazyLoginForms.d.ts +18 -0
  45. package/esm/lazyLoginForms.js +25 -0
  46. package/esm/tokenEntryModelFactory.d.ts +125 -0
  47. package/esm/tokenEntryModelFactory.js +85 -0
  48. package/esm/util.d.ts +10 -0
  49. package/esm/util.js +24 -2
  50. package/package.json +10 -13
@@ -1,7 +1,46 @@
1
- export declare function generateChallenge(val: string): Promise<string>;
2
- export declare function processError(text: string, invalidErrorCb: () => void): any;
3
- export declare function processTokenResponse(data: {
4
- refresh_token?: string;
5
- access_token: string;
6
- }, storeRefreshTokenCb: (str: string) => void): string;
7
- export { toBase64Url } from '@jbrowse/core/util';
1
+ export interface OAuthWindowParams {
2
+ internetAccountId: string;
3
+ expectedState: string | undefined;
4
+ exchangeAuthorizationCode: (code: string, redirectUri: string) => Promise<string>;
5
+ storeToken: (token: string) => void;
6
+ }
7
+ export declare function parseOAuthError(text: string): {
8
+ isInvalidGrant: boolean;
9
+ statusText: string;
10
+ };
11
+ /**
12
+ * Completes the flow from the URL the provider redirected back to. Returns the
13
+ * token, or undefined if the redirect carries neither a token, a code, nor an
14
+ * error. Throws on any OAuth error.
15
+ */
16
+ export declare function finishOAuthRedirect(redirectUri: string, params: OAuthWindowParams): Promise<string | undefined>;
17
+ /**
18
+ * The redirect URI a message event carries, if it is this account's popup
19
+ * reporting back, and undefined otherwise. Synchronous, so a caller can tell a
20
+ * relevant message from an unrelated one before finishing it — finishing can
21
+ * take a round trip of its own.
22
+ */
23
+ export declare function getOAuthRedirectUri(event: MessageEvent, internetAccountId: string): string | undefined;
24
+ /**
25
+ * Parses an OAuth redirect message event. Returns the token if the event
26
+ * completes the flow, undefined if the event isn't this account's redirect.
27
+ * Throws on any OAuth error.
28
+ */
29
+ export declare function finishOAuthWindow(event: MessageEvent, params: OAuthWindowParams): Promise<string | undefined>;
30
+ /**
31
+ * Returns a promise that resolves to the token when the OAuth popup posts its
32
+ * redirect back. The listener is self-contained and cleaned up when the promise
33
+ * settles.
34
+ *
35
+ * Given a `watch`, closing the popup without completing the flow rejects rather
36
+ * than leaving the promise pending — and pending is not a benign state here,
37
+ * because `getToken` caches this promise and hands it to every later request:
38
+ * one dismissed login window left the account dead for the rest of the session,
39
+ * showing nothing. Same failure the blocked-popup check in `getTokenViaAuthFlow`
40
+ * covers, reached the other way.
41
+ */
42
+ export declare function waitForOAuthMessage(finish: (event: MessageEvent) => Promise<string | undefined>, watch?: {
43
+ popup: Pick<Window, 'closed'>;
44
+ /** whether an event is this popup's redirect, tested synchronously */
45
+ isOwnMessage: (event: MessageEvent) => boolean;
46
+ }): Promise<string>;
@@ -1,23 +1,166 @@
1
- import { sha256Base64Url } from '@jbrowse/core/util';
2
- export async function generateChallenge(val) {
3
- return sha256Base64Url(val);
4
- }
5
- export function processError(text, invalidErrorCb) {
1
+ export function parseOAuthError(text) {
6
2
  try {
7
- const obj = JSON.parse(text);
8
- if (obj.error === 'invalid_grant') {
9
- invalidErrorCb();
3
+ const { error, error_description } = JSON.parse(text);
4
+ return {
5
+ isInvalidGrant: error === 'invalid_grant',
6
+ statusText: error_description ?? text,
7
+ };
8
+ }
9
+ catch {
10
+ return { isInvalidGrant: false, statusText: text };
11
+ }
12
+ }
13
+ /**
14
+ * Completes the flow from the URL the provider redirected back to. Returns the
15
+ * token, or undefined if the redirect carries neither a token, a code, nor an
16
+ * error. Throws on any OAuth error.
17
+ */
18
+ export async function finishOAuthRedirect(redirectUri, params) {
19
+ const redirectUrl = new URL(redirectUri);
20
+ // Both halves: the code flow answers in the query string, the implicit flow
21
+ // in the fragment. Read as a union rather than by rewriting the '#' into a
22
+ // '?', which produced a second '?' — and so one unsplittable parameter — for
23
+ // any redirect that carried both.
24
+ const urlParams = new URLSearchParams(redirectUrl.search);
25
+ for (const [key, value] of new URLSearchParams(redirectUrl.hash.replace('#', ''))) {
26
+ if (!urlParams.has(key)) {
27
+ urlParams.set(key, value);
10
28
  }
11
- return obj?.error_description ?? text;
12
29
  }
13
- catch (e) {
30
+ if (params.expectedState && urlParams.get('state') !== params.expectedState) {
31
+ throw new Error('OAuth state mismatch — possible CSRF attack');
32
+ }
33
+ const accessToken = urlParams.get('access_token');
34
+ if (accessToken) {
35
+ params.storeToken(accessToken);
36
+ return accessToken;
37
+ }
38
+ const code = urlParams.get('code');
39
+ if (code) {
40
+ const token = await params.exchangeAuthorizationCode(code, redirectUrl.origin + redirectUrl.pathname);
41
+ params.storeToken(token);
42
+ return token;
14
43
  }
15
- return text;
44
+ const error = urlParams.get('error');
45
+ if (error === 'access_denied') {
46
+ throw new Error('OAuth flow was cancelled');
47
+ }
48
+ if (error) {
49
+ throw new Error(`OAuth flow error: ${error}`);
50
+ }
51
+ return undefined;
16
52
  }
17
- export function processTokenResponse(data, storeRefreshTokenCb) {
18
- if (data.refresh_token) {
19
- storeRefreshTokenCb(data.refresh_token);
53
+ /**
54
+ * The redirect URI a message event carries, if it is this account's popup
55
+ * reporting back, and undefined otherwise. Synchronous, so a caller can tell a
56
+ * relevant message from an unrelated one before finishing it — finishing can
57
+ * take a round trip of its own.
58
+ */
59
+ export function getOAuthRedirectUri(event, internetAccountId) {
60
+ // The popup redirects back to our own origin and posts from there
61
+ // (`initAuthWindow`), so anything else is another page talking to us — and
62
+ // its `redirectUri` would inject that page's access token as ours.
63
+ if (event.origin !== window.location.origin) {
64
+ return undefined;
65
+ }
66
+ // The window receives messages from anything holding a handle to it, of any
67
+ // shape. Reading through an unchecked `event.data` throws a TypeError, and
68
+ // the listener in waitForOAuthMessage turns a throw into a *rejected* auth
69
+ // flow — so an unrelated `postMessage(null)` used to fail the login.
70
+ const data = event.data;
71
+ if (typeof data !== 'object' ||
72
+ data === null ||
73
+ !('name' in data) ||
74
+ !('redirectUri' in data)) {
75
+ return undefined;
20
76
  }
21
- return data.access_token;
77
+ const { name, redirectUri } = data;
78
+ return name === `JBrowseAuthWindow-${internetAccountId}` &&
79
+ typeof redirectUri === 'string'
80
+ ? redirectUri
81
+ : undefined;
82
+ }
83
+ /**
84
+ * Parses an OAuth redirect message event. Returns the token if the event
85
+ * completes the flow, undefined if the event isn't this account's redirect.
86
+ * Throws on any OAuth error.
87
+ */
88
+ export async function finishOAuthWindow(event, params) {
89
+ const redirectUri = getOAuthRedirectUri(event, params.internetAccountId);
90
+ return redirectUri === undefined
91
+ ? undefined
92
+ : finishOAuthRedirect(redirectUri, params);
93
+ }
94
+ // How often the popup is checked for having been closed, and how many
95
+ // consecutive closed readings it takes to call the flow cancelled.
96
+ const POLL_INTERVAL_MS = 500;
97
+ const CLOSED_POLLS_TO_CANCEL = 2;
98
+ /**
99
+ * Returns a promise that resolves to the token when the OAuth popup posts its
100
+ * redirect back. The listener is self-contained and cleaned up when the promise
101
+ * settles.
102
+ *
103
+ * Given a `watch`, closing the popup without completing the flow rejects rather
104
+ * than leaving the promise pending — and pending is not a benign state here,
105
+ * because `getToken` caches this promise and hands it to every later request:
106
+ * one dismissed login window left the account dead for the rest of the session,
107
+ * showing nothing. Same failure the blocked-popup check in `getTokenViaAuthFlow`
108
+ * covers, reached the other way.
109
+ */
110
+ export function waitForOAuthMessage(finish, watch) {
111
+ return new Promise((resolve, reject) => {
112
+ let timer;
113
+ let finishing = false;
114
+ let closedPolls = 0;
115
+ const settle = (act) => {
116
+ window.removeEventListener('message', listener);
117
+ clearInterval(timer);
118
+ act();
119
+ };
120
+ const listener = async (event) => {
121
+ const own = watch?.isOwnMessage(event) ?? false;
122
+ finishing ||= own;
123
+ try {
124
+ const token = await finish(event);
125
+ if (token !== undefined) {
126
+ settle(() => {
127
+ resolve(token);
128
+ });
129
+ return;
130
+ }
131
+ }
132
+ catch (e) {
133
+ settle(() => {
134
+ reject(e instanceof Error ? e : new Error(String(e)));
135
+ });
136
+ return;
137
+ }
138
+ // a redirect of ours that completed nothing — a provider that came back
139
+ // with neither token, code nor error. Hand the popup back to the
140
+ // watchdog rather than latching it off for good.
141
+ if (own) {
142
+ finishing = false;
143
+ }
144
+ };
145
+ window.addEventListener('message', listener);
146
+ if (watch) {
147
+ timer = setInterval(() => {
148
+ // A closed popup only means cancellation while no redirect of ours has
149
+ // arrived: the popup posts and *then* closes itself, and the code flow
150
+ // then spends a token exchange finishing. Hence both the `finishing`
151
+ // latch and a poll of grace for the gap between the post and this
152
+ // listener running.
153
+ if (finishing || !watch.popup.closed) {
154
+ closedPolls = 0;
155
+ return;
156
+ }
157
+ closedPolls += 1;
158
+ if (closedPolls >= CLOSED_POLLS_TO_CANCEL) {
159
+ settle(() => {
160
+ reject(new Error('OAuth flow was cancelled'));
161
+ });
162
+ }
163
+ }, POLL_INTERVAL_MS);
164
+ }
165
+ });
22
166
  }
23
- export { toBase64Url } from '@jbrowse/core/util';