diff --git a/src/TableauConnector.js b/src/TableauConnector.js index e90d661..d91a9ae 100644 --- a/src/TableauConnector.js +++ b/src/TableauConnector.js @@ -80,7 +80,7 @@ class TableauConnector { if (this.code && tableau.phase !== tableau.phaseEnum.gatherDataPhase) { utils.log('SUCCESS: Authenticate (oauth)') const code = this.code - return auth.getToken(code).then(accessToken => { + return auth.exchangeCodeForTokens(code).then(({accessToken, refreshToken}) => { // Restore canonical WDC URL, which Tableau saves with data source const parsedQueryString = queryString.parse(location.search) let canonicalQueryString = `/?state=${parsedQueryString.state}` @@ -90,23 +90,23 @@ class TableauConnector { window.location = canonicalQueryString // For correctness only. Should never be reached. - return accessToken + return refreshToken }) } else { - const apiKey = auth.getApiKey(true) - utils.log(`SUCCESS: Authenticate (cached: ${apiKey ? 'hit' : 'miss'})`) - return Promise.resolve(apiKey) + const refreshToken = auth.getRefreshToken(true) + utils.log(`SUCCESS: Authenticate (cached: ${refreshToken ? 'hit' : 'miss'})`) + return refreshToken } } - validateAccessIfNeeded (accessToken) { + validateAccessIfNeeded (refreshToken) { utils.log('START: Validate access') if (tableau.phase === tableau.phaseEnum.gatherDataPhase) { return api.getUser() .then((user) => { utils.log('SUCCESS: Validate access') analytics.identify(user.id) - return accessToken + return refreshToken }) .catch((error) => { Sentry.captureException(error) @@ -115,7 +115,7 @@ class TableauConnector { }) } else { utils.log('SUCCESS: Validate access (not needed)') - return Promise.resolve(accessToken) + return refreshToken } } @@ -124,9 +124,9 @@ class TableauConnector { tableau.authType = tableau.authTypeEnum.custom this.authenticate() - .then(accessToken => this.validateAccessIfNeeded(accessToken)) - .then(accessToken => { - const hasAuth = !!accessToken + .then(refreshToken => this.validateAccessIfNeeded(refreshToken)) + .then(refreshToken => { + const hasAuth = !!refreshToken utils.log(`HAS AUTH: ${hasAuth}`) if (!hasAuth) { @@ -141,7 +141,7 @@ class TableauConnector { if (tableau.phase === tableau.phaseEnum.interactivePhase || tableau.phase === tableau.phaseEnum.authPhase) { if (hasAuth) { - auth.storeApiKey(accessToken) + auth.storeRefreshToken(refreshToken) if (tableau.phase === tableau.phaseEnum.authPhase) { // Auto-submit here if we are in the auth phase tableau.submit() diff --git a/src/api.js b/src/api.js index 68b636f..5e445db 100644 --- a/src/api.js +++ b/src/api.js @@ -19,7 +19,7 @@ import axios from 'axios' import * as queryString from 'query-string' -import { getApiKey, storeApiKey } from './auth' +import { getAccessToken, storeRefreshToken } from './auth' const basePath = 'https://api.data.world/v0' const basePathQuery = 'https://query.data.world' @@ -31,24 +31,25 @@ axios.interceptors.response.use( }, (error) => { if (error.response && error.response.status === 401) { - storeApiKey('') + storeRefreshToken('') } return Promise.reject(error) }) -const runQuery = (dataset, query, queryType = 'sql') => { +const runQuery = async (dataset, query, queryType = 'sql') => { + const accessToken = await getAccessToken(true) return axios.post( `${basePathQuery}/${queryType}/${dataset}`, queryString.stringify({query}), { headers: { - 'authorization': `Bearer ${getApiKey(true)}`, + 'authorization': `Bearer ${accessToken}`, 'content-type': 'application/x-www-form-urlencoded' } }) } -const getToken = (code, code_verifier) => { +const exchangeCodeForTokens = (code, code_verifier) => { return axios.post('https://data.world/oauth/access_token', { code, client_id: process.env.REACT_APP_OAUTH_CLIENT_ID, @@ -58,18 +59,29 @@ const getToken = (code, code_verifier) => { }) } -const getUser = () => { +const getUser = async () => { + const accessToken = await getAccessToken(true) return axios.get( `${basePath}/user`, { headers: { - 'authorization': `Bearer ${getApiKey(true)}` + 'authorization': `Bearer ${accessToken}` } }) } +const getRefreshedTokens = (refreshToken) => { + return axios.post('https://data.world/oauth/access_token', { + client_id: process.env.REACT_APP_OAUTH_CLIENT_ID, + client_secret: process.env.REACT_APP_OAUTH_CLIENT_SECRET, + grant_type: 'refresh_token', + refresh_token: refreshToken + }) +} + export { runQuery, - getToken, - getUser + exchangeCodeForTokens, + getUser, + getRefreshedTokens } diff --git a/src/auth.js b/src/auth.js index 08490a4..98e28cf 100644 --- a/src/auth.js +++ b/src/auth.js @@ -19,9 +19,9 @@ import * as api from './api' import crypto from 'crypto' import uuidv1 from 'uuid/v1' -import { parseJSON } from './util.js' +import { parseJSON, log } from './util.js' -const apiTokenKey = 'DW-API-KEY' +const refreshTokenKey = 'DW-REFRESH-TOKEN-KEY' const codeVerifierKey = 'DW-CODE-VERIFIER' const generateCodeVerifier = () => { @@ -44,25 +44,42 @@ const generateCodeVerifier = () => { return codeVerifier } -const getApiKey = (useTableauPassword = false) => { +const storeRefreshToken = (refreshToken) => { if (window.localStorage) { - let apiKey = window.localStorage.getItem(apiTokenKey) - if (window.tableau && useTableauPassword) { - apiKey = window.tableau.password || apiKey + window.localStorage.setItem(refreshTokenKey, refreshToken) + + if (window.tableau) { + window.tableau.password = refreshToken } - return apiKey + return refreshToken } return null } -const storeApiKey = (key) => { +const getRefreshToken = (useTableauPassword = false) => { if (window.localStorage) { - window.localStorage.setItem(apiTokenKey, key) + let refreshToken = window.localStorage.getItem(refreshTokenKey) + if (window.tableau && useTableauPassword) { + refreshToken = window.tableau.password || refreshToken + } + return refreshToken + } + return null +} - if (window.tableau) { - window.tableau.password = key +const getAccessToken = async (useTableauPassword = false) => { + const refreshToken = getRefreshToken(useTableauPassword) + if (refreshToken) { + // exchange refresh token for access token + try { + const response = await api.getRefreshedTokens(refreshToken) + // store new refresh token + storeRefreshToken(response.data.refresh_token) + return response.data.access_token + } catch (error) { + log(`ERROR : Failed to refresh tokens - ${error.message}`) + return null } - return key } return null } @@ -125,14 +142,17 @@ const redirectToAuth = (state) => { window.location = getAuthUrl(codeVerifier, state) } -const getToken = (code) => { - return api.getToken(code, useCodeVerifier()).then(response => { - let token = '' - if (response.data.access_token) { - token = response.data.access_token +const exchangeCodeForTokens = (code) => { + return api.exchangeCodeForTokens(code, useCodeVerifier()).then(response => { + let refreshToken = '' + let accessToken = '' + if (response.data) { + refreshToken = response.data.refresh_token + accessToken = response.data.access_token } - return storeApiKey(token) + storeRefreshToken(refreshToken) + return Promise.resolve({accessToken, refreshToken}) }) } -export { redirectToAuth, getToken, getApiKey, storeApiKey, getStateObject } +export { redirectToAuth, exchangeCodeForTokens, getAccessToken, storeRefreshToken, getRefreshToken, getStateObject }