From 0b53e8431572bd674bdbfc1c4e0512a8052d217f Mon Sep 17 00:00:00 2001 From: Kehinde Shittu Date: Fri, 31 Aug 2018 15:03:10 +0100 Subject: [PATCH 1/4] Reimplemt refresh token flow --- src/TableauConnector.js | 23 ++++++++--------- src/api.js | 30 +++++++++++++++------- src/auth.js | 55 +++++++++++++++++++++++++++-------------- 3 files changed, 70 insertions(+), 38 deletions(-) diff --git a/src/TableauConnector.js b/src/TableauConnector.js index e90d661..c7bef69 100644 --- a/src/TableauConnector.js +++ b/src/TableauConnector.js @@ -75,12 +75,12 @@ class TableauConnector { tableau.registerConnector(this.connector) } - authenticate () { + async authenticate () { utils.log('START: Authenticate') 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,24 @@ class TableauConnector { window.location = canonicalQueryString // For correctness only. Should never be reached. - return accessToken + return Promise.resolve({accessToken, refreshToken}) }) } else { - const apiKey = auth.getApiKey(true) + const apiKey = await auth.getAccessToken(true) + const refreshToken = auth.getRefreshToken(true) utils.log(`SUCCESS: Authenticate (cached: ${apiKey ? 'hit' : 'miss'})`) - return Promise.resolve(apiKey) + return Promise.resolve({accessToken: apiKey, refreshToken}) } } - validateAccessIfNeeded (accessToken) { + validateAccessIfNeeded (accessToken, 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 Promise.resolve({accessToken, refreshToken}) }) .catch((error) => { Sentry.captureException(error) @@ -115,7 +116,7 @@ class TableauConnector { }) } else { utils.log('SUCCESS: Validate access (not needed)') - return Promise.resolve(accessToken) + return Promise.resolve({accessToken, refreshToken}) } } @@ -124,8 +125,8 @@ class TableauConnector { tableau.authType = tableau.authTypeEnum.custom this.authenticate() - .then(accessToken => this.validateAccessIfNeeded(accessToken)) - .then(accessToken => { + .then(({accessToken, refreshToken}) => this.validateAccessIfNeeded(accessToken, refreshToken)) + .then(({accessToken, refreshToken}) => { const hasAuth = !!accessToken utils.log(`HAS AUTH: ${hasAuth}`) @@ -141,7 +142,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..8917dc0 100644 --- a/src/auth.js +++ b/src/auth.js @@ -21,7 +21,7 @@ import crypto from 'crypto' import uuidv1 from 'uuid/v1' import { parseJSON } from './util.js' -const apiTokenKey = 'DW-API-KEY' +const refreshTokenKey = 'DW-REFRESH-TOKEN-KEY' const codeVerifierKey = 'DW-CODE-VERIFIER' const generateCodeVerifier = () => { @@ -44,25 +44,41 @@ 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 getAccessToken = async (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 + } + // exchange refresh token for access token + const response = await api.getRefreshedTokens(refreshToken) + // store new refresh token + storeRefreshToken(response.data.refresh_token) + return response.data.access_token + } else { + return null + } +} - if (window.tableau) { - window.tableau.password = key +const getRefreshToken = (useTableauPassword = false) => { + if (window.localStorage) { + let refreshToken = window.localStorage.getItem(refreshTokenKey) + if (window.tableau && useTableauPassword) { + refreshToken = window.tableau.password || refreshToken } - return key + return refreshToken } return null } @@ -125,14 +141,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 } From 1d1d12645043adc17d6eab221df8d28f2b071984 Mon Sep 17 00:00:00 2001 From: Kehinde Shittu Date: Fri, 31 Aug 2018 16:41:24 +0100 Subject: [PATCH 2/4] Adds update. --- src/TableauConnector.js | 6 +++--- src/auth.js | 18 +++++++++++------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/TableauConnector.js b/src/TableauConnector.js index c7bef69..4bb03dc 100644 --- a/src/TableauConnector.js +++ b/src/TableauConnector.js @@ -93,10 +93,10 @@ class TableauConnector { return Promise.resolve({accessToken, refreshToken}) }) } else { - const apiKey = await auth.getAccessToken(true) + const accessToken = await auth.getAccessToken(true) const refreshToken = auth.getRefreshToken(true) - utils.log(`SUCCESS: Authenticate (cached: ${apiKey ? 'hit' : 'miss'})`) - return Promise.resolve({accessToken: apiKey, refreshToken}) + utils.log(`SUCCESS: Authenticate (cached: ${refreshToken ? 'hit' : 'miss'})`) + return Promise.resolve({accessToken, refreshToken}) } } diff --git a/src/auth.js b/src/auth.js index 8917dc0..856d559 100644 --- a/src/auth.js +++ b/src/auth.js @@ -62,14 +62,18 @@ const getAccessToken = async (useTableauPassword = false) => { if (window.tableau && useTableauPassword) { refreshToken = window.tableau.password || refreshToken } - // exchange refresh token for access token - const response = await api.getRefreshedTokens(refreshToken) - // store new refresh token - storeRefreshToken(response.data.refresh_token) - return response.data.access_token - } else { - return null + 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) { + return null + } + } } + return null } const getRefreshToken = (useTableauPassword = false) => { From 00baea8c7de3be30ef6431f15e6197fcaf4c0aca Mon Sep 17 00:00:00 2001 From: Kehinde Shittu Date: Wed, 5 Sep 2018 13:23:54 +0100 Subject: [PATCH 3/4] Adds update and resolved conflicts --- src/TableauConnector.js | 17 ++++++++--------- src/auth.js | 35 ++++++++++++++++------------------- 2 files changed, 24 insertions(+), 28 deletions(-) diff --git a/src/TableauConnector.js b/src/TableauConnector.js index 4bb03dc..4243f28 100644 --- a/src/TableauConnector.js +++ b/src/TableauConnector.js @@ -90,24 +90,23 @@ class TableauConnector { window.location = canonicalQueryString // For correctness only. Should never be reached. - return Promise.resolve({accessToken, refreshToken}) + return refreshToken }) } else { - const accessToken = await auth.getAccessToken(true) const refreshToken = auth.getRefreshToken(true) utils.log(`SUCCESS: Authenticate (cached: ${refreshToken ? 'hit' : 'miss'})`) - return Promise.resolve({accessToken, refreshToken}) + return refreshToken } } - validateAccessIfNeeded (accessToken, refreshToken) { + 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 Promise.resolve({accessToken, refreshToken}) + return refreshToken }) .catch((error) => { Sentry.captureException(error) @@ -116,7 +115,7 @@ class TableauConnector { }) } else { utils.log('SUCCESS: Validate access (not needed)') - return Promise.resolve({accessToken, refreshToken}) + return refreshToken } } @@ -125,9 +124,9 @@ class TableauConnector { tableau.authType = tableau.authTypeEnum.custom this.authenticate() - .then(({accessToken, refreshToken}) => this.validateAccessIfNeeded(accessToken, refreshToken)) - .then(({accessToken, refreshToken}) => { - const hasAuth = !!accessToken + .then(refreshToken => this.validateAccessIfNeeded(refreshToken)) + .then(refreshToken => { + const hasAuth = !!refreshToken utils.log(`HAS AUTH: ${hasAuth}`) if (!hasAuth) { diff --git a/src/auth.js b/src/auth.js index 856d559..98e28cf 100644 --- a/src/auth.js +++ b/src/auth.js @@ -19,7 +19,7 @@ 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 refreshTokenKey = 'DW-REFRESH-TOKEN-KEY' const codeVerifierKey = 'DW-CODE-VERIFIER' @@ -56,33 +56,30 @@ const storeRefreshToken = (refreshToken) => { return null } -const getAccessToken = async (useTableauPassword = false) => { +const getRefreshToken = (useTableauPassword = false) => { if (window.localStorage) { let refreshToken = window.localStorage.getItem(refreshTokenKey) if (window.tableau && useTableauPassword) { refreshToken = window.tableau.password || refreshToken } - 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) { - return null - } - } + return refreshToken } return null } -const getRefreshToken = (useTableauPassword = false) => { - if (window.localStorage) { - let refreshToken = window.localStorage.getItem(refreshTokenKey) - if (window.tableau && useTableauPassword) { - refreshToken = window.tableau.password || refreshToken +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 refreshToken } return null } @@ -158,4 +155,4 @@ const exchangeCodeForTokens = (code) => { }) } -export { redirectToAuth, exchangeCodeForTokens, getAccessToken, storeRefreshToken, getRefreshToken } +export { redirectToAuth, exchangeCodeForTokens, getAccessToken, storeRefreshToken, getRefreshToken, getStateObject } From 8d1c03510111e9e58d62ca02ca6235409d105c58 Mon Sep 17 00:00:00 2001 From: Kehinde Shittu Date: Wed, 20 Feb 2019 16:21:28 +0100 Subject: [PATCH 4/4] Adds update --- src/TableauConnector.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/TableauConnector.js b/src/TableauConnector.js index 4243f28..d91a9ae 100644 --- a/src/TableauConnector.js +++ b/src/TableauConnector.js @@ -75,7 +75,7 @@ class TableauConnector { tableau.registerConnector(this.connector) } - async authenticate () { + authenticate () { utils.log('START: Authenticate') if (this.code && tableau.phase !== tableau.phaseEnum.gatherDataPhase) { utils.log('SUCCESS: Authenticate (oauth)')