@jbrowse/plugin-authentication 2.2.0

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 (130) hide show
  1. package/LICENSE +201 -0
  2. package/dist/DropboxOAuthModel/configSchema.d.ts +5 -0
  3. package/dist/DropboxOAuthModel/configSchema.js +69 -0
  4. package/dist/DropboxOAuthModel/configSchema.js.map +1 -0
  5. package/dist/DropboxOAuthModel/index.d.ts +2 -0
  6. package/dist/DropboxOAuthModel/index.js +11 -0
  7. package/dist/DropboxOAuthModel/index.js.map +1 -0
  8. package/dist/DropboxOAuthModel/model.d.ts +98 -0
  9. package/dist/DropboxOAuthModel/model.js +103 -0
  10. package/dist/DropboxOAuthModel/model.js.map +1 -0
  11. package/dist/ExternalTokenModel/ExternalTokenEntryForm.d.ts +5 -0
  12. package/dist/ExternalTokenModel/ExternalTokenEntryForm.js +59 -0
  13. package/dist/ExternalTokenModel/ExternalTokenEntryForm.js.map +1 -0
  14. package/dist/ExternalTokenModel/configSchema.d.ts +5 -0
  15. package/dist/ExternalTokenModel/configSchema.js +26 -0
  16. package/dist/ExternalTokenModel/configSchema.js.map +1 -0
  17. package/dist/ExternalTokenModel/index.d.ts +2 -0
  18. package/dist/ExternalTokenModel/index.js +11 -0
  19. package/dist/ExternalTokenModel/index.js.map +1 -0
  20. package/dist/ExternalTokenModel/model.d.ts +67 -0
  21. package/dist/ExternalTokenModel/model.js +59 -0
  22. package/dist/ExternalTokenModel/model.js.map +1 -0
  23. package/dist/GoogleDriveOAuthModel/configSchema.d.ts +5 -0
  24. package/dist/GoogleDriveOAuthModel/configSchema.js +53 -0
  25. package/dist/GoogleDriveOAuthModel/configSchema.js.map +1 -0
  26. package/dist/GoogleDriveOAuthModel/index.d.ts +2 -0
  27. package/dist/GoogleDriveOAuthModel/index.js +11 -0
  28. package/dist/GoogleDriveOAuthModel/index.js.map +1 -0
  29. package/dist/GoogleDriveOAuthModel/model.d.ts +111 -0
  30. package/dist/GoogleDriveOAuthModel/model.js +115 -0
  31. package/dist/GoogleDriveOAuthModel/model.js.map +1 -0
  32. package/dist/HTTPBasicModel/HTTPBasicLoginForm.d.ts +5 -0
  33. package/dist/HTTPBasicModel/HTTPBasicLoginForm.js +55 -0
  34. package/dist/HTTPBasicModel/HTTPBasicLoginForm.js.map +1 -0
  35. package/dist/HTTPBasicModel/configSchema.d.ts +5 -0
  36. package/dist/HTTPBasicModel/configSchema.js +34 -0
  37. package/dist/HTTPBasicModel/configSchema.js.map +1 -0
  38. package/dist/HTTPBasicModel/index.d.ts +2 -0
  39. package/dist/HTTPBasicModel/index.js +11 -0
  40. package/dist/HTTPBasicModel/index.js.map +1 -0
  41. package/dist/HTTPBasicModel/model.d.ts +67 -0
  42. package/dist/HTTPBasicModel/model.js +59 -0
  43. package/dist/HTTPBasicModel/model.js.map +1 -0
  44. package/dist/OAuthModel/configSchema.d.ts +5 -0
  45. package/dist/OAuthModel/configSchema.js +90 -0
  46. package/dist/OAuthModel/configSchema.js.map +1 -0
  47. package/dist/OAuthModel/index.d.ts +2 -0
  48. package/dist/OAuthModel/index.js +11 -0
  49. package/dist/OAuthModel/index.js.map +1 -0
  50. package/dist/OAuthModel/model.d.ts +91 -0
  51. package/dist/OAuthModel/model.js +317 -0
  52. package/dist/OAuthModel/model.js.map +1 -0
  53. package/dist/index.d.ts +399 -0
  54. package/dist/index.js +80 -0
  55. package/dist/index.js.map +1 -0
  56. package/esm/DropboxOAuthModel/configSchema.d.ts +5 -0
  57. package/esm/DropboxOAuthModel/configSchema.js +64 -0
  58. package/esm/DropboxOAuthModel/configSchema.js.map +1 -0
  59. package/esm/DropboxOAuthModel/index.d.ts +2 -0
  60. package/esm/DropboxOAuthModel/index.js +3 -0
  61. package/esm/DropboxOAuthModel/index.js.map +1 -0
  62. package/esm/DropboxOAuthModel/model.d.ts +98 -0
  63. package/esm/DropboxOAuthModel/model.js +96 -0
  64. package/esm/DropboxOAuthModel/model.js.map +1 -0
  65. package/esm/ExternalTokenModel/ExternalTokenEntryForm.d.ts +5 -0
  66. package/esm/ExternalTokenModel/ExternalTokenEntryForm.js +29 -0
  67. package/esm/ExternalTokenModel/ExternalTokenEntryForm.js.map +1 -0
  68. package/esm/ExternalTokenModel/configSchema.d.ts +5 -0
  69. package/esm/ExternalTokenModel/configSchema.js +24 -0
  70. package/esm/ExternalTokenModel/configSchema.js.map +1 -0
  71. package/esm/ExternalTokenModel/index.d.ts +2 -0
  72. package/esm/ExternalTokenModel/index.js +3 -0
  73. package/esm/ExternalTokenModel/index.js.map +1 -0
  74. package/esm/ExternalTokenModel/model.d.ts +67 -0
  75. package/esm/ExternalTokenModel/model.js +57 -0
  76. package/esm/ExternalTokenModel/model.js.map +1 -0
  77. package/esm/GoogleDriveOAuthModel/configSchema.d.ts +5 -0
  78. package/esm/GoogleDriveOAuthModel/configSchema.js +48 -0
  79. package/esm/GoogleDriveOAuthModel/configSchema.js.map +1 -0
  80. package/esm/GoogleDriveOAuthModel/index.d.ts +2 -0
  81. package/esm/GoogleDriveOAuthModel/index.js +3 -0
  82. package/esm/GoogleDriveOAuthModel/index.js.map +1 -0
  83. package/esm/GoogleDriveOAuthModel/model.d.ts +111 -0
  84. package/esm/GoogleDriveOAuthModel/model.js +108 -0
  85. package/esm/GoogleDriveOAuthModel/model.js.map +1 -0
  86. package/esm/HTTPBasicModel/HTTPBasicLoginForm.d.ts +5 -0
  87. package/esm/HTTPBasicModel/HTTPBasicLoginForm.js +28 -0
  88. package/esm/HTTPBasicModel/HTTPBasicLoginForm.js.map +1 -0
  89. package/esm/HTTPBasicModel/configSchema.d.ts +5 -0
  90. package/esm/HTTPBasicModel/configSchema.js +32 -0
  91. package/esm/HTTPBasicModel/configSchema.js.map +1 -0
  92. package/esm/HTTPBasicModel/index.d.ts +2 -0
  93. package/esm/HTTPBasicModel/index.js +3 -0
  94. package/esm/HTTPBasicModel/index.js.map +1 -0
  95. package/esm/HTTPBasicModel/model.d.ts +67 -0
  96. package/esm/HTTPBasicModel/model.js +57 -0
  97. package/esm/HTTPBasicModel/model.js.map +1 -0
  98. package/esm/OAuthModel/configSchema.d.ts +5 -0
  99. package/esm/OAuthModel/configSchema.js +88 -0
  100. package/esm/OAuthModel/configSchema.js.map +1 -0
  101. package/esm/OAuthModel/index.d.ts +2 -0
  102. package/esm/OAuthModel/index.js +3 -0
  103. package/esm/OAuthModel/index.js.map +1 -0
  104. package/esm/OAuthModel/model.d.ts +91 -0
  105. package/esm/OAuthModel/model.js +289 -0
  106. package/esm/OAuthModel/model.js.map +1 -0
  107. package/esm/index.d.ts +399 -0
  108. package/esm/index.js +64 -0
  109. package/esm/index.js.map +1 -0
  110. package/package.json +63 -0
  111. package/src/DropboxOAuthModel/configSchema.ts +77 -0
  112. package/src/DropboxOAuthModel/index.ts +2 -0
  113. package/src/DropboxOAuthModel/model.tsx +141 -0
  114. package/src/ExternalTokenModel/ExternalTokenEntryForm.tsx +61 -0
  115. package/src/ExternalTokenModel/configSchema.ts +36 -0
  116. package/src/ExternalTokenModel/index.ts +2 -0
  117. package/src/ExternalTokenModel/model.tsx +70 -0
  118. package/src/GoogleDriveOAuthModel/configSchema.ts +61 -0
  119. package/src/GoogleDriveOAuthModel/index.ts +2 -0
  120. package/src/GoogleDriveOAuthModel/model.tsx +174 -0
  121. package/src/HTTPBasicModel/HTTPBasicLoginForm.tsx +71 -0
  122. package/src/HTTPBasicModel/configSchema.ts +43 -0
  123. package/src/HTTPBasicModel/index.ts +2 -0
  124. package/src/HTTPBasicModel/model.tsx +70 -0
  125. package/src/OAuthModel/configSchema.ts +98 -0
  126. package/src/OAuthModel/index.ts +2 -0
  127. package/src/OAuthModel/model.tsx +357 -0
  128. package/src/__snapshots__/index.test.js.snap +8 -0
  129. package/src/index.test.js +96 -0
  130. package/src/index.ts +97 -0
@@ -0,0 +1,70 @@
1
+ import { ConfigurationReference, getConf } from '@jbrowse/core/configuration'
2
+ import { InternetAccount } from '@jbrowse/core/pluggableElementTypes/models'
3
+ import { UriLocation } from '@jbrowse/core/util/types'
4
+ import { HTTPBasicInternetAccountConfigModel } from './configSchema'
5
+ import { Instance, types, getRoot } from 'mobx-state-tree'
6
+
7
+ import { HTTPBasicLoginForm } from './HTTPBasicLoginForm'
8
+
9
+ const stateModelFactory = (
10
+ configSchema: HTTPBasicInternetAccountConfigModel,
11
+ ) => {
12
+ return InternetAccount.named('HTTPBasicInternetAccount')
13
+ .props({
14
+ type: types.literal('HTTPBasicInternetAccount'),
15
+ configuration: ConfigurationReference(configSchema),
16
+ })
17
+ .views(self => ({
18
+ get validateWithHEAD(): boolean {
19
+ return getConf(self, 'validateWithHEAD')
20
+ },
21
+ }))
22
+ .actions(self => ({
23
+ getTokenFromUser(
24
+ resolve: (token: string) => void,
25
+ reject: (error: Error) => void,
26
+ ) {
27
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
28
+ const { session } = getRoot<any>(self)
29
+ session.queueDialog((doneCallback: () => void) => [
30
+ HTTPBasicLoginForm,
31
+ {
32
+ internetAccountId: self.internetAccountId,
33
+ handleClose: (token: string) => {
34
+ if (token) {
35
+ resolve(token)
36
+ } else {
37
+ reject(new Error('user cancelled entry'))
38
+ }
39
+ doneCallback()
40
+ },
41
+ },
42
+ ])
43
+ },
44
+ async validateToken(token: string, location: UriLocation) {
45
+ if (!self.validateWithHEAD) {
46
+ return token
47
+ }
48
+ const newInit = self.addAuthHeaderToInit({ method: 'HEAD' }, token)
49
+ const response = await fetch(location.uri, newInit)
50
+ if (!response.ok) {
51
+ let errorMessage
52
+ try {
53
+ errorMessage = await response.text()
54
+ } catch (error) {
55
+ errorMessage = ''
56
+ }
57
+ throw new Error(
58
+ `Error validating token — ${response.status} (${
59
+ response.statusText
60
+ })${errorMessage ? ` (${errorMessage})` : ''}`,
61
+ )
62
+ }
63
+ return token
64
+ },
65
+ }))
66
+ }
67
+
68
+ export default stateModelFactory
69
+ export type HTTPBasicStateModel = ReturnType<typeof stateModelFactory>
70
+ export type HTTPBasicModel = Instance<HTTPBasicStateModel>
@@ -0,0 +1,98 @@
1
+ import { ConfigurationSchema } from '@jbrowse/core/configuration'
2
+ import { Instance } from 'mobx-state-tree'
3
+ import { BaseInternetAccountConfig } from '@jbrowse/core/pluggableElementTypes/models'
4
+
5
+ /**
6
+ * #config OAuthInternetAccount
7
+ */
8
+ function x() {} // eslint-disable-line @typescript-eslint/no-unused-vars
9
+
10
+ const OAuthConfigSchema = ConfigurationSchema(
11
+ 'OAuthInternetAccount',
12
+ {
13
+ /**
14
+ * #slot
15
+ */
16
+ tokenType: {
17
+ description: 'a custom name for a token to include in the header',
18
+ type: 'string',
19
+ defaultValue: 'Bearer',
20
+ },
21
+ /**
22
+ * #slot
23
+ */
24
+ authEndpoint: {
25
+ description: 'the authorization code endpoint of the internet account',
26
+ type: 'string',
27
+ defaultValue: '',
28
+ },
29
+ /**
30
+ * #slot
31
+ */
32
+ tokenEndpoint: {
33
+ description: 'the token endpoint of the internet account',
34
+ type: 'string',
35
+ defaultValue: '',
36
+ },
37
+ /**
38
+ * #slot
39
+ */
40
+ needsPKCE: {
41
+ description: 'boolean to indicate if the endpoint needs a PKCE code',
42
+ type: 'boolean',
43
+ defaultValue: false,
44
+ },
45
+ /**
46
+ * #slot
47
+ */
48
+ clientId: {
49
+ description: 'id for the OAuth application',
50
+ type: 'string',
51
+ defaultValue: '',
52
+ },
53
+ /**
54
+ * #slot
55
+ */
56
+ scopes: {
57
+ description: 'optional scopes for the authorization call',
58
+ type: 'string',
59
+ defaultValue: '',
60
+ },
61
+ /**
62
+ * #slot
63
+ */
64
+ state: {
65
+ description: 'optional state for the authorization call',
66
+ type: 'string',
67
+ defaultValue: '',
68
+ },
69
+ /**
70
+ * #slot
71
+ */
72
+ responseType: {
73
+ description: 'the type of response from the authorization endpoint',
74
+ type: 'string',
75
+ defaultValue: 'code',
76
+ },
77
+ /**
78
+ * #slot
79
+ */
80
+ hasRefreshToken: {
81
+ description: 'true if the endpoint can supply a refresh token',
82
+ type: 'boolean',
83
+ defaultValue: false,
84
+ },
85
+ },
86
+ {
87
+ /**
88
+ * #baseConfiguration
89
+ */
90
+ baseConfiguration: BaseInternetAccountConfig,
91
+ explicitlyTyped: true,
92
+ },
93
+ )
94
+
95
+ export type OAuthInternetAccountConfigModel = typeof OAuthConfigSchema
96
+ export type OAuthInternetAccountConfig =
97
+ Instance<OAuthInternetAccountConfigModel>
98
+ export default OAuthConfigSchema
@@ -0,0 +1,2 @@
1
+ export { default as configSchema } from './configSchema'
2
+ export { default as modelFactory } from './model'
@@ -0,0 +1,357 @@
1
+ import { ConfigurationReference, getConf } from '@jbrowse/core/configuration'
2
+ import { InternetAccount } from '@jbrowse/core/pluggableElementTypes/models'
3
+ import { isElectron, UriLocation } from '@jbrowse/core/util'
4
+ import { Instance, types } from 'mobx-state-tree'
5
+ import jwtDecode, { JwtPayload } from 'jwt-decode'
6
+
7
+ // locals
8
+ import { OAuthInternetAccountConfigModel } from './configSchema'
9
+
10
+ interface OAuthData {
11
+ client_id: string
12
+ redirect_uri: string
13
+ response_type: 'token' | 'code'
14
+ scope?: string
15
+ code_challenge?: string
16
+ code_challenge_method?: string
17
+ token_access_type?: string
18
+ state?: string
19
+ }
20
+
21
+ function fixup(buf: string) {
22
+ return buf.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '')
23
+ }
24
+
25
+ function getGlobalObject(): Window {
26
+ // Based on window-or-global
27
+ // https://github.com/purposeindustries/window-or-global/blob/322abc71de0010c9e5d9d0729df40959e1ef8775/lib/index.js
28
+ return (
29
+ // eslint-disable-next-line no-restricted-globals
30
+ (typeof self === 'object' && self.self === self && self) ||
31
+ (typeof global === 'object' && global.global === global && global) ||
32
+ // @ts-ignore
33
+ this
34
+ )
35
+ }
36
+
37
+ const stateModelFactory = (configSchema: OAuthInternetAccountConfigModel) => {
38
+ return InternetAccount.named('OAuthInternetAccount')
39
+ .props({
40
+ type: types.literal('OAuthInternetAccount'),
41
+ configuration: ConfigurationReference(configSchema),
42
+ })
43
+ .views(() => {
44
+ let codeVerifier: string | undefined = undefined
45
+ return {
46
+ get codeVerifierPKCE() {
47
+ if (codeVerifier) {
48
+ return codeVerifier
49
+ }
50
+ const global = getGlobalObject()
51
+ const array = new Uint8Array(32)
52
+ global.crypto.getRandomValues(array)
53
+ codeVerifier = fixup(Buffer.from(array).toString('base64'))
54
+ return codeVerifier
55
+ },
56
+ }
57
+ })
58
+ .views(self => ({
59
+ get authEndpoint(): string {
60
+ return getConf(self, 'authEndpoint')
61
+ },
62
+ get tokenEndpoint(): string {
63
+ return getConf(self, 'tokenEndpoint')
64
+ },
65
+ get needsPKCE(): boolean {
66
+ return getConf(self, 'needsPKCE')
67
+ },
68
+ get clientId(): string {
69
+ return getConf(self, 'clientId')
70
+ },
71
+ get scopes(): string {
72
+ return getConf(self, 'scopes')
73
+ },
74
+ /**
75
+ * OAuth state parameter: https://www.rfc-editor.org/rfc/rfc6749#section-4.1.1
76
+ * Can override or extend if dynamic state is needed.
77
+ */
78
+ state(): string | undefined {
79
+ return getConf(self, 'state') || undefined
80
+ },
81
+ get responseType(): 'token' | 'code' {
82
+ return getConf(self, 'responseType')
83
+ },
84
+ get hasRefreshToken(): boolean {
85
+ return getConf(self, 'hasRefreshToken')
86
+ },
87
+ get refreshTokenKey() {
88
+ return `${self.internetAccountId}-refreshToken`
89
+ },
90
+ }))
91
+ .actions(self => ({
92
+ storeRefreshToken(refreshToken: string) {
93
+ localStorage.setItem(self.refreshTokenKey, refreshToken)
94
+ },
95
+ removeRefreshToken() {
96
+ localStorage.removeItem(self.refreshTokenKey)
97
+ },
98
+ retrieveRefreshToken() {
99
+ return localStorage.getItem(self.refreshTokenKey)
100
+ },
101
+ async exchangeAuthorizationForAccessToken(
102
+ token: string,
103
+ redirectUri: string,
104
+ ): Promise<string> {
105
+ const data = {
106
+ code: token,
107
+ grant_type: 'authorization_code',
108
+ client_id: self.clientId,
109
+ code_verifier: self.codeVerifierPKCE,
110
+ redirect_uri: redirectUri,
111
+ }
112
+
113
+ const params = new URLSearchParams(Object.entries(data))
114
+
115
+ const response = await fetch(self.tokenEndpoint, {
116
+ method: 'POST',
117
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
118
+ body: params.toString(),
119
+ })
120
+
121
+ if (!response.ok) {
122
+ let errorMessage
123
+ try {
124
+ errorMessage = await response.text()
125
+ } catch (error) {
126
+ errorMessage = ''
127
+ }
128
+ throw new Error(
129
+ `Failed to obtain token from endpoint: ${response.status} (${
130
+ response.statusText
131
+ })${errorMessage ? ` (${errorMessage})` : ''}`,
132
+ )
133
+ }
134
+
135
+ const accessToken = await response.json()
136
+ if (accessToken.refresh_token) {
137
+ this.storeRefreshToken(accessToken.refresh_token)
138
+ }
139
+ return accessToken.access_token
140
+ },
141
+ async exchangeRefreshForAccessToken(
142
+ refreshToken: string,
143
+ ): Promise<string> {
144
+ const data = {
145
+ grant_type: 'refresh_token',
146
+ refresh_token: refreshToken,
147
+ client_id: self.clientId,
148
+ }
149
+
150
+ const params = new URLSearchParams(Object.entries(data))
151
+
152
+ const response = await fetch(self.tokenEndpoint, {
153
+ method: 'POST',
154
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
155
+ body: params.toString(),
156
+ })
157
+
158
+ if (!response.ok) {
159
+ self.removeToken()
160
+ let text = await response.text()
161
+ try {
162
+ const obj = JSON.parse(text)
163
+ if (obj.error === 'invalid_grant') {
164
+ this.removeRefreshToken()
165
+ }
166
+ text = obj?.error_description ?? text
167
+ } catch (e) {
168
+ /* just use original text as error */
169
+ }
170
+
171
+ throw new Error(
172
+ `Network response failure — ${response.status} (${
173
+ response.statusText
174
+ }) ${text ? ` (${text})` : ''}`,
175
+ )
176
+ }
177
+
178
+ const accessToken = await response.json()
179
+ if (accessToken.refresh_token) {
180
+ this.storeRefreshToken(accessToken.refresh_token)
181
+ }
182
+ return accessToken.access_token
183
+ },
184
+ }))
185
+ .actions(self => {
186
+ let listener: (event: MessageEvent) => void
187
+ let refreshTokenPromise: Promise<string> | undefined = undefined
188
+ return {
189
+ // used to listen to child window for auth code/token
190
+ addMessageChannel(
191
+ resolve: (token: string) => void,
192
+ reject: (error: Error) => void,
193
+ ) {
194
+ listener = event => {
195
+ this.finishOAuthWindow(event, resolve, reject)
196
+ }
197
+ window.addEventListener('message', listener)
198
+ },
199
+ deleteMessageChannel() {
200
+ window.removeEventListener('message', listener)
201
+ },
202
+ async finishOAuthWindow(
203
+ event: MessageEvent,
204
+ resolve: (token: string) => void,
205
+ reject: (error: Error) => void,
206
+ ) {
207
+ if (
208
+ event.data.name !== `JBrowseAuthWindow-${self.internetAccountId}`
209
+ ) {
210
+ return this.deleteMessageChannel()
211
+ }
212
+ const redirectUriWithInfo = event.data.redirectUri
213
+ const fixedQueryString = redirectUriWithInfo.replace('#', '?')
214
+ const redirectUrl = new URL(fixedQueryString)
215
+ const queryStringSearch = redirectUrl.search
216
+ const urlParams = new URLSearchParams(queryStringSearch)
217
+ if (urlParams.has('access_token')) {
218
+ const token = urlParams.get('access_token')
219
+ if (!token) {
220
+ return reject(new Error('Error with token endpoint'))
221
+ }
222
+ self.storeToken(token)
223
+ return resolve(token)
224
+ }
225
+ if (urlParams.has('code')) {
226
+ const code = urlParams.get('code')
227
+ if (!code) {
228
+ return reject(new Error('Error with authorization endpoint'))
229
+ }
230
+ try {
231
+ const token = await self.exchangeAuthorizationForAccessToken(
232
+ code,
233
+ redirectUrl.origin + redirectUrl.pathname,
234
+ )
235
+ self.storeToken(token)
236
+ return resolve(token)
237
+ } catch (error) {
238
+ if (error instanceof Error) {
239
+ return reject(error)
240
+ } else {
241
+ return reject(new Error(String(error)))
242
+ }
243
+ }
244
+ }
245
+ if (redirectUriWithInfo.includes('access_denied')) {
246
+ return reject(new Error('OAuth flow was cancelled'))
247
+ }
248
+ if (redirectUriWithInfo.includes('error')) {
249
+ return reject(new Error('Oauth flow error: ' + queryStringSearch))
250
+ }
251
+ this.deleteMessageChannel()
252
+ },
253
+ // opens external OAuth flow, popup for web and new browser window for desktop
254
+ async useEndpointForAuthorization(
255
+ resolve: (token: string) => void,
256
+ reject: (error: Error) => void,
257
+ ) {
258
+ const redirectUri = isElectron
259
+ ? 'http://localhost/auth'
260
+ : window.location.origin + window.location.pathname
261
+ const data: OAuthData = {
262
+ client_id: self.clientId,
263
+ redirect_uri: redirectUri,
264
+ response_type: self.responseType || 'code',
265
+ }
266
+
267
+ if (self.state()) {
268
+ data.state = self.state()
269
+ }
270
+
271
+ if (self.scopes) {
272
+ data.scope = self.scopes
273
+ }
274
+
275
+ if (self.needsPKCE) {
276
+ const { codeVerifierPKCE } = self
277
+
278
+ const sha256 = await import('crypto-js/sha256').then(f => f.default)
279
+ const Base64 = await import('crypto-js/enc-base64')
280
+ const codeChallenge = fixup(
281
+ Base64.stringify(sha256(codeVerifierPKCE)),
282
+ )
283
+ data.code_challenge = codeChallenge
284
+ data.code_challenge_method = 'S256'
285
+ }
286
+
287
+ if (self.hasRefreshToken) {
288
+ data.token_access_type = 'offline'
289
+ }
290
+
291
+ const params = new URLSearchParams(Object.entries(data))
292
+
293
+ const url = new URL(self.authEndpoint)
294
+ url.search = params.toString()
295
+
296
+ const eventName = `JBrowseAuthWindow-${self.internetAccountId}`
297
+ if (isElectron) {
298
+ const { ipcRenderer } = window.require('electron')
299
+ const redirectUri = await ipcRenderer.invoke('openAuthWindow', {
300
+ internetAccountId: self.internetAccountId,
301
+ data,
302
+ url: url.toString(),
303
+ })
304
+
305
+ const eventFromDesktop = new MessageEvent('message', {
306
+ data: { name: eventName, redirectUri: redirectUri },
307
+ })
308
+ this.finishOAuthWindow(eventFromDesktop, resolve, reject)
309
+ } else {
310
+ const options = `width=500,height=600,left=0,top=0`
311
+ window.open(url, eventName, options)
312
+ }
313
+ },
314
+ async getTokenFromUser(
315
+ resolve: (token: string) => void,
316
+ reject: (error: Error) => void,
317
+ ): Promise<void> {
318
+ const refreshToken =
319
+ self.hasRefreshToken && self.retrieveRefreshToken()
320
+ if (refreshToken) {
321
+ resolve(await self.exchangeRefreshForAccessToken(refreshToken))
322
+ }
323
+ this.addMessageChannel(resolve, reject)
324
+ this.useEndpointForAuthorization(resolve, reject)
325
+ },
326
+ async validateToken(
327
+ token: string,
328
+ location: UriLocation,
329
+ ): Promise<string> {
330
+ const decoded = jwtDecode<JwtPayload>(token)
331
+ if (decoded.exp && decoded.exp < new Date().getTime() / 1000) {
332
+ const refreshToken =
333
+ self.hasRefreshToken && self.retrieveRefreshToken()
334
+ if (refreshToken) {
335
+ try {
336
+ if (!refreshTokenPromise) {
337
+ refreshTokenPromise =
338
+ self.exchangeRefreshForAccessToken(refreshToken)
339
+ }
340
+ const newToken = await refreshTokenPromise
341
+ return this.validateToken(newToken, location)
342
+ } catch (err) {
343
+ throw new Error(`Token could not be refreshed. ${err}`)
344
+ }
345
+ }
346
+ } else {
347
+ refreshTokenPromise = undefined
348
+ }
349
+ return token
350
+ },
351
+ }
352
+ })
353
+ }
354
+
355
+ export default stateModelFactory
356
+ export type OAuthStateModel = ReturnType<typeof stateModelFactory>
357
+ export type OAuthModel = Instance<OAuthStateModel>
@@ -0,0 +1,8 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`initialized correctly 1`] = `
4
+ Object {
5
+ "internetAccountId": "HTTPBasicTest",
6
+ "type": "HTTPBasicInternetAccount",
7
+ }
8
+ `;
@@ -0,0 +1,96 @@
1
+ import Plugin from '@jbrowse/core/Plugin'
2
+ import PluginManager from '@jbrowse/core/PluginManager'
3
+ import InternetAccountType from '@jbrowse/core/pluggableElementTypes/InternetAccountType'
4
+ import {
5
+ configSchema as OAuthConfigSchema,
6
+ modelFactory as OAuthInternetAccountModelFactory,
7
+ } from './OAuthModel'
8
+ import {
9
+ configSchema as ExternalTokenConfigSchema,
10
+ modelFactory as ExternalTokenInternetAccountModelFactory,
11
+ } from './ExternalTokenModel'
12
+ import {
13
+ configSchema as HTTPBasicConfigSchema,
14
+ modelFactory as HTTPBasicInternetAccountModelFactory,
15
+ } from './HTTPBasicModel'
16
+ import {
17
+ configSchema as DropboxOAuthConfigSchema,
18
+ modelFactory as DropboxOAuthInternetAccountModelFactory,
19
+ } from './DropboxOAuthModel'
20
+ import {
21
+ configSchema as GoogleDriveOAuthConfigSchema,
22
+ modelFactory as GoogleDriveOAuthInternetAccountModelFactory,
23
+ } from './GoogleDriveOAuthModel'
24
+ import { getSnapshot } from 'mobx-state-tree'
25
+
26
+ // mock warnings to avoid unnecessary outputs
27
+ beforeEach(() => {
28
+ jest.spyOn(console, 'warn').mockImplementation(() => {})
29
+ })
30
+
31
+ afterEach(() => {
32
+ console.warn.mockRestore()
33
+ })
34
+
35
+ class AuthenticationPlugin extends Plugin {
36
+ install(pluginManager) {
37
+ pluginManager.addInternetAccountType(() => {
38
+ return new InternetAccountType({
39
+ name: 'OAuthInternetAccount',
40
+ configSchema: OAuthConfigSchema,
41
+ stateModel: OAuthInternetAccountModelFactory(OAuthConfigSchema),
42
+ })
43
+ })
44
+ pluginManager.addInternetAccountType(() => {
45
+ return new InternetAccountType({
46
+ name: 'ExternalTokenInternetAccount',
47
+ configSchema: ExternalTokenConfigSchema,
48
+ stateModel: ExternalTokenInternetAccountModelFactory(
49
+ ExternalTokenConfigSchema,
50
+ ),
51
+ })
52
+ })
53
+ pluginManager.addInternetAccountType(() => {
54
+ return new InternetAccountType({
55
+ name: 'HTTPBasicInternetAccount',
56
+ configSchema: HTTPBasicConfigSchema,
57
+ stateModel: HTTPBasicInternetAccountModelFactory(HTTPBasicConfigSchema),
58
+ })
59
+ })
60
+ pluginManager.addInternetAccountType(() => {
61
+ return new InternetAccountType({
62
+ name: 'DropboxOAuthInternetAccount',
63
+ configSchema: DropboxOAuthConfigSchema,
64
+ stateModel: DropboxOAuthInternetAccountModelFactory(
65
+ DropboxOAuthConfigSchema,
66
+ ),
67
+ })
68
+ })
69
+ pluginManager.addInternetAccountType(() => {
70
+ return new InternetAccountType({
71
+ name: 'GoogleDriveOAuthInternetAccount',
72
+ configSchema: GoogleDriveOAuthConfigSchema,
73
+ stateModel: GoogleDriveOAuthInternetAccountModelFactory(
74
+ GoogleDriveOAuthConfigSchema,
75
+ ),
76
+ })
77
+ })
78
+ }
79
+ }
80
+
81
+ test('initialized correctly', () => {
82
+ const pm = new PluginManager([
83
+ new AuthenticationPlugin(),
84
+ ]).createPluggableElements()
85
+
86
+ expect(Object.values(pm.internetAccountTypes.registeredTypes).length).toEqual(
87
+ 5,
88
+ )
89
+
90
+ const HTTPBasic = pm.getInternetAccountType('HTTPBasicInternetAccount')
91
+ const config = HTTPBasic.configSchema.create({
92
+ type: 'HTTPBasicInternetAccount',
93
+ internetAccountId: 'HTTPBasicTest',
94
+ })
95
+ expect(getSnapshot(config)).toMatchSnapshot()
96
+ })