Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add OAuthModel InternetAccount token refresh #3175

Merged
merged 4 commits into from
Sep 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions plugins/authentication/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@
"build:es5": "tsc --build tsconfig.build.es5.json",
"clean": "rimraf dist esm *.tsbuildinfo"
},
"dependencies": {
"jwt-decode": "^3.1.2"
},
"peerDependencies": {
"@jbrowse/core": "^2.0.0",
"@mui/material": "^5.0.0",
Expand Down
44 changes: 41 additions & 3 deletions plugins/authentication/src/OAuthModel/model.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { ConfigurationReference, getConf } from '@jbrowse/core/configuration'
import { InternetAccount } from '@jbrowse/core/pluggableElementTypes/models'
import { isElectron } from '@jbrowse/core/util'
import { isElectron, UriLocation } from '@jbrowse/core/util'
import { Instance, types } from 'mobx-state-tree'
import jwtDecode, { JwtPayload } from 'jwt-decode'

// locals
import { OAuthInternetAccountConfigModel } from './configSchema'
Expand Down Expand Up @@ -152,9 +153,17 @@ const stateModelFactory = (configSchema: OAuthInternetAccountConfigModel) => {

if (!response.ok) {
self.removeToken()
let errorMessage
const contentType = response.headers.get('Content-Type')
Copy link
Collaborator

@cmdcolin cmdcolin Sep 9, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there is a chance this could trigger CORS warnings, but it might be ok. one alternative could be using response.json() despite content-type or something i've done elsewhere where i don't know if the response will be json let text = await response.text(); try { let obj= JSON.parse(text); /* do json stuff here */ } catch(e) { /* just text */; }

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

random link saying Content-Type might be an explicit Access-Control-Allow-Headers CORS header https://stackoverflow.com/questions/5027705/error-content-type-is-not-allowed-by-access-control-allow-headers

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

went ahead with adding this, and then merged to main :) if interested can test out main branch!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, @cmdcolin that example you linked is about setting Content-Type in a request, not reading it off a response.
But I think I know what you mean. AFAIK this could be a problem in the "no-cors" request, but since this is oAuth and we need to send an Authorization header it shouldn't be a problem.

Sorry for a late reply

let errorMessage, errorJson
try {
errorMessage = await response.text()
if (contentType && contentType.indexOf('application/json') !== -1) {
errorJson = await response.json()
if (errorJson.error && errorJson.error === 'invalid_grant') {
this.removeRefreshToken()
}
}
errorMessage =
errorJson?.error_description ?? (await response.text())
} catch (error) {
errorMessage = ''
}
Expand All @@ -166,11 +175,15 @@ const stateModelFactory = (configSchema: OAuthInternetAccountConfigModel) => {
}

const accessToken = await response.json()
if (accessToken.refresh_token) {
this.storeRefreshToken(accessToken.refresh_token)
}
return accessToken.access_token
},
}))
.actions(self => {
let listener: (event: MessageEvent) => void
let refreshTokenPromise: Promise<string> | undefined = undefined
return {
// used to listen to child window for auth code/token
addMessageChannel(
Expand Down Expand Up @@ -305,6 +318,31 @@ const stateModelFactory = (configSchema: OAuthInternetAccountConfigModel) => {
this.addMessageChannel(resolve, reject)
this.useEndpointForAuthorization(resolve, reject)
},
async validateToken(
token: string,
location: UriLocation,
): Promise<string> {
const decoded = jwtDecode<JwtPayload>(token)
if (decoded.exp && decoded.exp < new Date().getTime() / 1000) {
const refreshToken =
self.hasRefreshToken && self.retrieveRefreshToken()
if (refreshToken) {
try {
if (!refreshTokenPromise) {
refreshTokenPromise =
self.exchangeRefreshForAccessToken(refreshToken)
}
const newToken = await refreshTokenPromise
return this.validateToken(newToken, location)
} catch (err) {
throw new Error(`Token could not be refreshed. ${err}`)
}
}
} else {
refreshTokenPromise = undefined
}
return token
},
}
})
}
Expand Down