@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.
- package/LICENSE +201 -0
- package/dist/DropboxOAuthModel/configSchema.d.ts +5 -0
- package/dist/DropboxOAuthModel/configSchema.js +69 -0
- package/dist/DropboxOAuthModel/configSchema.js.map +1 -0
- package/dist/DropboxOAuthModel/index.d.ts +2 -0
- package/dist/DropboxOAuthModel/index.js +11 -0
- package/dist/DropboxOAuthModel/index.js.map +1 -0
- package/dist/DropboxOAuthModel/model.d.ts +98 -0
- package/dist/DropboxOAuthModel/model.js +103 -0
- package/dist/DropboxOAuthModel/model.js.map +1 -0
- package/dist/ExternalTokenModel/ExternalTokenEntryForm.d.ts +5 -0
- package/dist/ExternalTokenModel/ExternalTokenEntryForm.js +59 -0
- package/dist/ExternalTokenModel/ExternalTokenEntryForm.js.map +1 -0
- package/dist/ExternalTokenModel/configSchema.d.ts +5 -0
- package/dist/ExternalTokenModel/configSchema.js +26 -0
- package/dist/ExternalTokenModel/configSchema.js.map +1 -0
- package/dist/ExternalTokenModel/index.d.ts +2 -0
- package/dist/ExternalTokenModel/index.js +11 -0
- package/dist/ExternalTokenModel/index.js.map +1 -0
- package/dist/ExternalTokenModel/model.d.ts +67 -0
- package/dist/ExternalTokenModel/model.js +59 -0
- package/dist/ExternalTokenModel/model.js.map +1 -0
- package/dist/GoogleDriveOAuthModel/configSchema.d.ts +5 -0
- package/dist/GoogleDriveOAuthModel/configSchema.js +53 -0
- package/dist/GoogleDriveOAuthModel/configSchema.js.map +1 -0
- package/dist/GoogleDriveOAuthModel/index.d.ts +2 -0
- package/dist/GoogleDriveOAuthModel/index.js +11 -0
- package/dist/GoogleDriveOAuthModel/index.js.map +1 -0
- package/dist/GoogleDriveOAuthModel/model.d.ts +111 -0
- package/dist/GoogleDriveOAuthModel/model.js +115 -0
- package/dist/GoogleDriveOAuthModel/model.js.map +1 -0
- package/dist/HTTPBasicModel/HTTPBasicLoginForm.d.ts +5 -0
- package/dist/HTTPBasicModel/HTTPBasicLoginForm.js +55 -0
- package/dist/HTTPBasicModel/HTTPBasicLoginForm.js.map +1 -0
- package/dist/HTTPBasicModel/configSchema.d.ts +5 -0
- package/dist/HTTPBasicModel/configSchema.js +34 -0
- package/dist/HTTPBasicModel/configSchema.js.map +1 -0
- package/dist/HTTPBasicModel/index.d.ts +2 -0
- package/dist/HTTPBasicModel/index.js +11 -0
- package/dist/HTTPBasicModel/index.js.map +1 -0
- package/dist/HTTPBasicModel/model.d.ts +67 -0
- package/dist/HTTPBasicModel/model.js +59 -0
- package/dist/HTTPBasicModel/model.js.map +1 -0
- package/dist/OAuthModel/configSchema.d.ts +5 -0
- package/dist/OAuthModel/configSchema.js +90 -0
- package/dist/OAuthModel/configSchema.js.map +1 -0
- package/dist/OAuthModel/index.d.ts +2 -0
- package/dist/OAuthModel/index.js +11 -0
- package/dist/OAuthModel/index.js.map +1 -0
- package/dist/OAuthModel/model.d.ts +91 -0
- package/dist/OAuthModel/model.js +317 -0
- package/dist/OAuthModel/model.js.map +1 -0
- package/dist/index.d.ts +399 -0
- package/dist/index.js +80 -0
- package/dist/index.js.map +1 -0
- package/esm/DropboxOAuthModel/configSchema.d.ts +5 -0
- package/esm/DropboxOAuthModel/configSchema.js +64 -0
- package/esm/DropboxOAuthModel/configSchema.js.map +1 -0
- package/esm/DropboxOAuthModel/index.d.ts +2 -0
- package/esm/DropboxOAuthModel/index.js +3 -0
- package/esm/DropboxOAuthModel/index.js.map +1 -0
- package/esm/DropboxOAuthModel/model.d.ts +98 -0
- package/esm/DropboxOAuthModel/model.js +96 -0
- package/esm/DropboxOAuthModel/model.js.map +1 -0
- package/esm/ExternalTokenModel/ExternalTokenEntryForm.d.ts +5 -0
- package/esm/ExternalTokenModel/ExternalTokenEntryForm.js +29 -0
- package/esm/ExternalTokenModel/ExternalTokenEntryForm.js.map +1 -0
- package/esm/ExternalTokenModel/configSchema.d.ts +5 -0
- package/esm/ExternalTokenModel/configSchema.js +24 -0
- package/esm/ExternalTokenModel/configSchema.js.map +1 -0
- package/esm/ExternalTokenModel/index.d.ts +2 -0
- package/esm/ExternalTokenModel/index.js +3 -0
- package/esm/ExternalTokenModel/index.js.map +1 -0
- package/esm/ExternalTokenModel/model.d.ts +67 -0
- package/esm/ExternalTokenModel/model.js +57 -0
- package/esm/ExternalTokenModel/model.js.map +1 -0
- package/esm/GoogleDriveOAuthModel/configSchema.d.ts +5 -0
- package/esm/GoogleDriveOAuthModel/configSchema.js +48 -0
- package/esm/GoogleDriveOAuthModel/configSchema.js.map +1 -0
- package/esm/GoogleDriveOAuthModel/index.d.ts +2 -0
- package/esm/GoogleDriveOAuthModel/index.js +3 -0
- package/esm/GoogleDriveOAuthModel/index.js.map +1 -0
- package/esm/GoogleDriveOAuthModel/model.d.ts +111 -0
- package/esm/GoogleDriveOAuthModel/model.js +108 -0
- package/esm/GoogleDriveOAuthModel/model.js.map +1 -0
- package/esm/HTTPBasicModel/HTTPBasicLoginForm.d.ts +5 -0
- package/esm/HTTPBasicModel/HTTPBasicLoginForm.js +28 -0
- package/esm/HTTPBasicModel/HTTPBasicLoginForm.js.map +1 -0
- package/esm/HTTPBasicModel/configSchema.d.ts +5 -0
- package/esm/HTTPBasicModel/configSchema.js +32 -0
- package/esm/HTTPBasicModel/configSchema.js.map +1 -0
- package/esm/HTTPBasicModel/index.d.ts +2 -0
- package/esm/HTTPBasicModel/index.js +3 -0
- package/esm/HTTPBasicModel/index.js.map +1 -0
- package/esm/HTTPBasicModel/model.d.ts +67 -0
- package/esm/HTTPBasicModel/model.js +57 -0
- package/esm/HTTPBasicModel/model.js.map +1 -0
- package/esm/OAuthModel/configSchema.d.ts +5 -0
- package/esm/OAuthModel/configSchema.js +88 -0
- package/esm/OAuthModel/configSchema.js.map +1 -0
- package/esm/OAuthModel/index.d.ts +2 -0
- package/esm/OAuthModel/index.js +3 -0
- package/esm/OAuthModel/index.js.map +1 -0
- package/esm/OAuthModel/model.d.ts +91 -0
- package/esm/OAuthModel/model.js +289 -0
- package/esm/OAuthModel/model.js.map +1 -0
- package/esm/index.d.ts +399 -0
- package/esm/index.js +64 -0
- package/esm/index.js.map +1 -0
- package/package.json +63 -0
- package/src/DropboxOAuthModel/configSchema.ts +77 -0
- package/src/DropboxOAuthModel/index.ts +2 -0
- package/src/DropboxOAuthModel/model.tsx +141 -0
- package/src/ExternalTokenModel/ExternalTokenEntryForm.tsx +61 -0
- package/src/ExternalTokenModel/configSchema.ts +36 -0
- package/src/ExternalTokenModel/index.ts +2 -0
- package/src/ExternalTokenModel/model.tsx +70 -0
- package/src/GoogleDriveOAuthModel/configSchema.ts +61 -0
- package/src/GoogleDriveOAuthModel/index.ts +2 -0
- package/src/GoogleDriveOAuthModel/model.tsx +174 -0
- package/src/HTTPBasicModel/HTTPBasicLoginForm.tsx +71 -0
- package/src/HTTPBasicModel/configSchema.ts +43 -0
- package/src/HTTPBasicModel/index.ts +2 -0
- package/src/HTTPBasicModel/model.tsx +70 -0
- package/src/OAuthModel/configSchema.ts +98 -0
- package/src/OAuthModel/index.ts +2 -0
- package/src/OAuthModel/model.tsx +357 -0
- package/src/__snapshots__/index.test.js.snap +8 -0
- package/src/index.test.js +96 -0
- 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,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,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
|
+
})
|