ducky/web

Paddy 2015-06-30 Parent:ef386518fa14 Child:791aec3eb17b

11:3bdc03963abe Browse Files

Implement OAuth token refreshing helper. Create an ampersand-sync/XHR request wrapper that will detect when a request fails because an OAuth2 token has expired, and will try to refresh the token then retry the request.

package.json src/helpers/oauth-refresh.js

     1.1 --- a/package.json	Tue Jun 30 01:18:52 2015 -0400
     1.2 +++ b/package.json	Tue Jun 30 01:21:42 2015 -0400
     1.3 @@ -8,8 +8,10 @@
     1.4      "ampersand-collection": "^1.4.5",
     1.5      "ampersand-model": "^5.0.3",
     1.6      "ampersand-react-mixin": "^0.1.2",
     1.7 +    "ampersand-rest-collection": "^4.0.0",
     1.8      "ampersand-router": "^3.0.2",
     1.9      "ampersand-sync": "^3.0.7",
    1.10 +    "andlog": "^1.0.0",
    1.11      "babel": "^5.1.13",
    1.12      "babel-loader": "^5.0.0",
    1.13      "css-loader": "^0.12.0",
    1.14 @@ -33,7 +35,8 @@
    1.15      "sass-loader": "0.4.2",
    1.16      "style-loader": "^0.12.0",
    1.17      "url-loader": "^0.5.5",
    1.18 -    "webpack": "1.8.9"
    1.19 +    "webpack": "1.8.9",
    1.20 +    "xhr": "^2.0.2"
    1.21    },
    1.22    "devDependencies": {
    1.23      "react-hot-loader": "^1.2.5",
     2.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     2.2 +++ b/src/helpers/oauth-refresh.js	Tue Jun 30 01:21:42 2015 -0400
     2.3 @@ -0,0 +1,112 @@
     2.4 +import app from 'ampersand-app'
     2.5 +import xhr from 'xhr'
     2.6 +import qs from 'qs'
     2.7 +import Sync from 'ampersand-sync'
     2.8 +import config from '../config'
     2.9 +
    2.10 +const getRefresh = (opts, callback) => {
    2.11 +  const refreshOpts = {
    2.12 +    url: config.urlBase + '/token',
    2.13 +    method: 'POST',
    2.14 +    headers: {
    2.15 +      'Content-Type': 'application/x-www-form-urlencoded',
    2.16 +      'Authorization': 'Basic ' + btoa(config.clientID + ':' + config.clientSecret),
    2.17 +    },
    2.18 +    data: qs.stringify({
    2.19 +      'grant_type': 'refresh_token',
    2.20 +      'refresh_token': app.me.refresh_token
    2.21 +    }),
    2.22 +  }
    2.23 +  xhr(refreshOpts, function(err, resp, body) {
    2.24 +    if (resp.statusCode != 200) {
    2.25 +      callback(err, resp, body)
    2.26 +      return
    2.27 +    }
    2.28 +    if (body && resp.headers['content-type'] == 'application/json') {
    2.29 +      try {
    2.30 +        body = JSON.parse(body)
    2.31 +      } catch (err) {
    2.32 +        app.trigger('token:refreshError', body)
    2.33 +        callback(err, resp, body)
    2.34 +      }
    2.35 +    }
    2.36 +    if (body.access_token) {
    2.37 +      app.me.set(body)
    2.38 +    } else {
    2.39 +      app.trigger('token:refreshError', body)
    2.40 +    }
    2.41 +    callback(err, resp, body)
    2.42 +  })
    2.43 +}
    2.44 +
    2.45 +const shouldRefreshXHR = (err, resp, body) => {
    2.46 +  if(body && resp.headers['Content-Type'] == 'application/json') {
    2.47 +    try {
    2.48 +      body = JSON.parse(body)
    2.49 +    } catch (err) {
    2.50 +      return false
    2.51 +    }
    2.52 +  }
    2.53 +  if (resp.statusCode == 401 && body.errors && body.errors[0]
    2.54 +  && body.errors[0].header == 'authorization' && body.errors[0].error == 'access_denied') {
    2.55 +    return true
    2.56 +  }
    2.57 +  return false
    2.58 +}
    2.59 +
    2.60 +const doRefreshXHR = (opts, callback) => {
    2.61 +  opts.noRefresh = true
    2.62 +  getRefresh(opts, function(err, resp, body) {
    2.63 +    if (body.access_token) {
    2.64 +      opts.headers['Authorization'] = 'Bearer '+body.access_token
    2.65 +      xhr(opts, callback)
    2.66 +    }
    2.67 +  })
    2.68 +}
    2.69 +
    2.70 +const refreshXHR = (opts, callback) => {
    2.71 +  return xhr(opts, function(err, resp, body) {
    2.72 +    if (opts.noRefresh || !shouldRefreshXHR(err, resp, body)) {
    2.73 +      callback(err, resp, body)
    2.74 +      return
    2.75 +    }
    2.76 +    doRefreshXHR(opts, callback)
    2.77 +  })
    2.78 +  xhr(opts, callback)
    2.79 +}
    2.80 +
    2.81 +const shouldRefreshSync = (resp) => {
    2.82 +  if (resp && resp.errors && resp.errors[0]
    2.83 +  && resp.errors[0].header == 'authorization' && resp.errors[0].error == 'access_denied') {
    2.84 +    return true
    2.85 +  }
    2.86 +  return false
    2.87 +}
    2.88 +
    2.89 +const doRefreshSync = (action, moc, opts) => {
    2.90 +  opts.noRefresh = true
    2.91 +  getRefresh(opts, function(err, resp, body) {
    2.92 +    if (body.access_token) {
    2.93 +      Sync(action, moc, opts)
    2.94 +    }
    2.95 +  })
    2.96 +}
    2.97 +
    2.98 +const refreshSync = (action, moc, opts) => {
    2.99 +  const oldError = opts.error
   2.100 +  opts.error = function(resp) {
   2.101 +    if(opts.noRefresh || !shouldRefreshSync) {
   2.102 +      oldError(resp)
   2.103 +      return
   2.104 +    }
   2.105 +    doRefreshSync(action, moc, opts)
   2.106 +  }
   2.107 +  Sync(action, moc, opts)
   2.108 +}
   2.109 +
   2.110 +const refresh = {
   2.111 +  Sync: refreshSync,
   2.112 +  XHR: refreshXHR,
   2.113 +}
   2.114 +
   2.115 +export default refresh