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

Multiple token suggestion #30

Closed
cjcrawford opened this issue Sep 21, 2016 · 31 comments
Closed

Multiple token suggestion #30

cjcrawford opened this issue Sep 21, 2016 · 31 comments

Comments

@cjcrawford
Copy link

Rob, this is fantastic. Thank you for pioneering this plugin. I've used it on a few APIs and I'm impressed.

Recently I found myself having to authenticate to a rails API using devise as their JWT provider, which seems common in the rails world. Well, the API requires 3 tokens to help facilitate simultaneous multi-user auth.

How difficult would it be to handle the config to accept an array of intercepted header tokens? Perhaps you could combine the token, tokenHeader, and authType into this format:

example config:

token: [{
    name: 'Authorization',
    authType: 'bearer',
    foundIn: 'header', // default
},{
    name: 'uid',
    type: 'basic',
    foundIn: 'response'
},{
    name: 'client',
    type: 'basic'
}],

j-toker (a jquery based auth) handles their config like this

tokenFormat: {
    "access-token": "{{ access-token }}",
    "token-type":   "Bearer",
    client:         "{{ client }}",
    expiry:         "{{ expiry }}",
    uid:            "{{ uid }}"
  },

I understand I can overwrite the existing methods via config (thank you!) , but just thought I would offer some suggestions to bring added flexibility out-of-the-box.

Thanks for all your work!

@websanova
Copy link
Owner

Ya, that's a good idea, I can add that in. Bit swamped for now with some projects so will look into it next week.

@websanova
Copy link
Owner

Hey, thx again for the suggestion. So I've update the scheme as per your suggestion.

In the response it will go through the array of token auth schemes and attempt one until it finds a token there and use that.

For the request it will just default to using the name and authType from the first scheme in the array. Thing is it will always do it as a header. Not sure it really makes sense otherwise so I will leave it like that for now.

One thing I'm a bit unclear on in your situation. Is it attempting to validate all those schemes per request, or just to find the first one that works?

@cjcrawford
Copy link
Author

cjcrawford commented Sep 30, 2016

This diagram shows a more technical response.

@cjcrawford
Copy link
Author

In short, I need to capture the API's 3 response headers, update my localStorage with those values, then use them in my next request.

I login to the API just like you would in Laravel via a single email/password post request. The response back on a successful authentication includes three tokens in the response headers: uid, client, access-token.

Every time I make a request to the API, I need to use those values in the request header. The access-token changes on each response. Based on the existing rails session and how long I sit idle vs how long my token is configured to expire, the client and uid tokens may change also. So because i am lazy I just grab, store, inject like this:

// auth.js
export default {
  tokens: [{
    name: 'access-token',
    type: 'bearer',
    header: true,
  }, {
    name: 'uid',
    header: true,
  }, {
    name: 'client',
    header: true,
  }],

  interceptResponse(httpHeaderResponse){
    this.tokens.forEach(token => {
      if (token.header && httpHeaderResponse.has(token.name)){
        window.localStorage.setItem(token.name,httpHeaderResponse.get(token.name))
      }
    })
  },
  // ... more auth stuff
}

...
// main.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import VueResource from 'vue-resource'
import auth from './auth'

Vue.use(VueRouter)
Vue.use(VueResource)

Vue.http.interceptors.push((request, next) => {

  // modify request
  auth.tokens.forEach(token => {
    if (window.localStorage.getItem(token.name)) {
      let tokenValue = localStorage.getItem(token.name)
      if (token.type) {
        tokenValue = token.type + ' ' + tokenValue
      }
      request.headers.set(token.name, tokenValue)
    }
  })

  // continue to next interceptor
  next(response => { auth.interceptResponse(response.headers) })
})

@websanova
Copy link
Owner

Ok, so actually in this case I don't think I would implement this directly into the plugin. Unless there is some actual spec for it (is there?).

So best approach would be to just add a custom auth scheme which is already supported. Here is a sample from the current bearer one:

bearerAuth: {
    request: function (req, token) {
        var data = {};

        data[this.options.token[0].name] = 'Bearer ' + token;

        this.options._setHeaders.call(this, req, data);
    },
    response: function (res, token) {
        token = token.split('Bearer ');
        __token.set.call(this, null, token[token.length > 1 ? 1 : 0]);
    }
},

There is the request and response and you can pretty much do everything there right through the plugin. It might also be a good idea to put the keys together into one string like uid:client:token. Could also base64 encode an object like jwt based authentication but that would require a base64 encoding lib (extra code which may not be necessary).

@cjcrawford
Copy link
Author

Ok so with the new ability to provide an array of tokens in the config, I just define the authType as my own custom scheme like authDevise: ... Set all the tokens to that type, extend your plugin, loop all defined tokens and get/store/set each request.

By the way, the code I have in my last post is working with your plugin. I just configure the one main token with your plugin and then add my own 2 interceptors. So far it's been working fine.

@websanova
Copy link
Owner

Ya, well if it works, that's great then :P If there is some official spec for this authentication scheme let me know. Otherwise I will close this out.

@neontuna
Copy link

neontuna commented Nov 2, 2016

I've run into a similar situation. A rails API using "devise_token_auth" gem will send back data similar to what @cjcrawford was dealing with and I've actually used his code to get vue-auth working with the API.

However I'd like to use the custom scheme mentioned here. I was wondering if you could point me in the direction of how I'd actually configure vue-auth with a custom scheme similar to bearerAuth.

In terms of standards, the author of devise_token_auth talks about that here https://github.com/lynndylanhurley/devise_token_auth#token-header-format

@websanova
Copy link
Owner

So i see:

"The authentication headers consists of the following params:"

access-token, client, expiry, uid

These are all to be stored locally and are updated on every request?

@websanova websanova reopened this Nov 3, 2016
@neontuna
Copy link

neontuna commented Nov 3, 2016

That's correct. After some more testing last night I'm guessing this presents a problem for vue-auth as its expecting to get/set one header per request.

@websanova
Copy link
Owner

Yea, I will have to rethink this a bit.

On Nov 3, 2016 18:13, "Justin Mullis" notifications@github.com wrote:

That's correct. After some more testing last night I'm guessing this
presents a problem for vue-auth as its expecting to get/set one header per
request.


You are receiving this because you modified the open/close state.
Reply to this email directly, view it on GitHub
#30 (comment),
or mute the thread
https://github.com/notifications/unsubscribe-auth/ABkcy_WMaHboA_7sjmWvCE9um0M1LYCJks5q6cG3gaJpZM4KCS6Q
.

@websanova
Copy link
Owner

websanova commented Nov 4, 2016

Ok, I've made an update here and simplified the auth so that it's much easier to add auth drivers. Make sure to get the latest v1.4.0-beta. I've added an Authentication section to the docs but basically you need to setup the request and response for any custom driver. An example of the multiple tokens is in the docs. It should look something like this:

authType: 'custom2',

custom2Auth: {
    request: function (req, token) {
        token = token.split(';');

        req.headers.set('header1', token[0]);
        req.headers.set('header2', token[1]);
        req.headers.set('header3', token[2]);
        req.headers.set('header4', token[3]);
    },
    response: function (res) {
        var headers = this.options._getHeaders.call(this, res);

        if (headers.header1 && headers.header2 && headers.header3 && headers.header4) {
            return headers.header1 + ';' + headers.header2 + ';' + headers.header3 + ';' + headers.header4;
        }
    }
}

Basically you get the req object so you can parse the token and send back whatever you like to the server. Likewise for a response and needs to build the token and send back the "token" that will be stored by the plugin.

Let me know if that works or makes sense at all.

@websanova
Copy link
Owner

Note you should also be able to use the internal _setHeaders function.

this.options._setHeaders.call(this, req {
    header1: token[0],
    header2: token[1],
    header3: token[2],
    header4: token[3]
});

@cjcrawford
Copy link
Author

cjcrawford commented Nov 4, 2016

So can I do something like this in the config options?

edit: fixed sample config code to something that might actually compile... :

// vue-auth config example for devise on rails API
deviseTokens: ['Token-Type', 'Access-Token', 'Client', 'Uid', 'Expiry'],
authType: 'devise',
deviseAuth: {
    request(req, token){
        let tokens = token.split(';');
        this.options.deviseTokens.forEach((tokenName, index) => {
            if (tokens[index]){
                req.headers.set(tokenName, tokens[index]);
            }
        })
    },
    response(res) {
        let headers = this.options._getHeaders.call(this, res);
        let return_val = [];
            this.options.deviseTokens.forEach((tokenName, index) => {
            if (headers[tokenName]){
                return_val.push(headers[tokenName]);
            }
        })
        return return_val.join(';');
    }
}

@websanova
Copy link
Owner

Ya, in theory :p. I may just add that as a standard auth method. Is it
working?

On Nov 4, 2016 16:10, "Conan Crawford" notifications@github.com wrote:

So can I do something like this in the config options?

deviseTokens = ['Authorization bearer', 'access-token', 'client', 'uid'],
authType: 'devise',
deviseAuth: {
request(req, token){
let tokens = token.split(';');
this.options.deviseTokens.forEach((index, tokenName) => {
if (tokens[index]){
req.headers.set(tokenName, tokens[index]);
}
})
},
response(res) {
let headers = this.options._getHeaders.call(this, res);
let return_val = [];
this.options.deviseTokens.forEach((index, tokenName) => {
if (headers[tokenName]){
return_val.push(headers[tokenName]);
}
})
return return_val.join(';');
}
}


You are receiving this because you modified the open/close state.
Reply to this email directly, view it on GitHub
#30 (comment),
or mute the thread
https://github.com/notifications/unsubscribe-auth/ABkcy3OTMxksimxM2sJVisfp9UAulGklks5q6vaJgaJpZM4KCS6Q
.

@cjcrawford
Copy link
Author

I'll have some time this weekend to test it against the rails API. v1.4.0-beta right?

@neontuna
Copy link

neontuna commented Nov 4, 2016

I'll also check this weekend. Really appreciate the changes by the way!

@neontuna
Copy link

neontuna commented Nov 6, 2016

This is working! @cjcrawford flip the index and tokenName arguments on forEach. Also on my install the token names were case sensitive. deviseTokens: ['Token-Type', 'Access-Token', 'Client', 'Uid', 'Expiry'],

Thank you guys!

Just for reference I'm using
Rails 4.2.6
devise_token_auth 0.1.39

@cjcrawford
Copy link
Author

I'll fix the code to line up with your fixes and we should have a good prototype for a devise auth. I'll be testing against my rails server later today, too.

@websanova
Copy link
Owner

@cjcrawford yes v1.4.0-beta.

@websanova
Copy link
Owner

Awesome @cjcrawford and @NonAdmin.

@cjcrawford would you like to submit a PR or should I go ahead and put this in myself?

@cjcrawford
Copy link
Author

I can confirm that the fixed code above is working for me on my rails API. I would rather you implement it your own way with your own patterns. Here's my vue-auth config with standard devise endpoints.

import Vue from 'vue'
import VueRouter from 'vue-router'

const VueAuth = require('@websanova/vue-auth') // "@websanova/vue-auth": "^1.4.0-beta"
const apiEndPoint = 'https://API.YOURDOMAIN.COM/api/v1'

Vue.use(VueRouter)

Vue.router = new VueRouter({
 //... config
})

Vue.use(VueAuth, {
  router: Vue.router,
  deviseTokens: ['Token-Type', 'Access-Token', 'Client', 'Uid', 'Expiry'],
  authType: 'devise',
  deviseAuth: {
    request(req, token){
      let tokens = token.split(';')
      this.options.deviseTokens.forEach((tokenName, index) => {
        if (tokens[index]){
          req.headers.set(tokenName, tokens[index])
        }
      })
    },
    response(res) {
      let headers = this.options._getHeaders.call(this, res)
      let return_val = []
      this.options.deviseTokens.forEach((tokenName) => {
        if (headers[tokenName]){
          return_val.push(headers[tokenName])
        }
      })
      return return_val.join(';')
    }
  },
  registerData: {url: apiEndPoint + '/auth', method: 'POST', redirect: '/account'},
  loginData: {url: apiEndPoint + '/auth/sign_in', method: 'POST', redirect: '/account'},
  logoutData: {url: apiEndPoint + '/auth/sign_out', method: 'DELETE', redirect: '/account/login', makeRequest: true},
  refreshData: {url: apiEndPoint + '/auth/validate_token', method: 'GET', atInit: true},
  fetchData: {url: apiEndPoint + '/auth/validate_token', method: 'GET'},
})

@websanova
Copy link
Owner

Ok, I've added this as a standard auth method. v1.5.0-beta. should be able to just set authType: 'devise' now. I changed the code around a bit so please let me know if there are any issues.

@neontuna
Copy link

neontuna commented Nov 8, 2016

I'll have to check out the new version, but I'm running into issues trying to use "rememberMe" on login. It works initially and I can close and re-open the browser, move around my app (currently just the login page and a single route that pulls an index from the API). However eventually the token info that vue-auth sends to the API gets rejected. I can't seem to figure out why. I've tried in Safari and Chrome, and I even downloaded the "demo" API for the devise_token_auth gem and observed the same behavior. It almost seems like sometimes vue-auth isn't recording the latest token information to local storage, and therefore tries to use an old token which the API rejects.

Also once this starts happening vue-auth stops working at all. Login goes through but then it immediately gets rejected by the API on the next request (usually fetch data). If I clear the cache and start again it will work ok for 5-10 requests before bugging out again.

@neontuna
Copy link

neontuna commented Nov 8, 2016

Ok I can confirm that is still an issue in v1.5.0-beta. I can definitely see what's happening. Refreshing the app and watching various requests go, I check and make sure that the data in localStorage matches what I see on the last response. After a few refreshes I can see that localStorage hasn't been updated with the latest token information, on the next refresh I get 401 errors and bounced back to login.

@neontuna
Copy link

neontuna commented Nov 8, 2016

I think this is more than likely some sort of issue with devise_token_auth. It has a setting where requests made within 5 seconds of each other are considered a "batch" and the token doesn't change. However this seems to have caused some outstanding issues for other users and they're seeing the same random logouts I'm seeing.

@neontuna
Copy link

neontuna commented Nov 8, 2016

Ok, I think I've gotten to the bottom of this. It is due to how devise_token_auth handles "batches". More info here. Sometimes XHR responses come back as 304, instructing the browser to pull cached results for a request. This will eventually screw things up as token information is pulled from cached headers. This is compounded by the fact that devise_token_auth doesn't send back any headers when in batch mode.

One work around I tried from here is to attach some unique string to the request, for instance a timestamp. This prevents caching as every request is unique

Vue.http.interceptors.push((request, next) => {
  request.url += (request.url.indexOf('?') > 0 ? '&' : '?') + `cb=${new Date().getTime()}`
  next()
})

Pretty hacky, but it does work. However, other users of devise_token_auth think the API ought to be sending back headers even when in batch mode, and just set them to blank or something to let the client know not to change its stored token. So I went with a fix that sets "Access-Token" to "".

This will require vue-auth to make sure its not then trying to save a blank value. The current code is going to save something that looks like a bunch of ;;;; so I just added an additional check to make sure Access-Token isn't blank, and if it is the entire response returns false.

response: function (res) {
  var token = [],
    headers = this.options._getHeaders.call(this, res);
  if (headers['Access-Token']) {   
    this.options.deviseAuth.tokens.forEach(function (tokenName) {
      if (headers[tokenName]) {
        token.push(headers[tokenName]);
      }
    });

    return token.join(';');
  } else {
    return false;
  }
 }
}

Kind of annoying. But really just due to devise_token_auth doing some weird stuff. It looks like the caching thing just became an issue in the last couple of months. I imagine the gem will be fixed to not require any sort of work around on the client as it seems to me that normal browser behavior (caching XHR) shouldn't disrupt anything.

@websanova
Copy link
Owner

Ok, I put that in for you. v1.5.1-beta

Another thing, I'm not sure about devise. But this sounds similar to an issue I had with Laravel JWT Auth library. It was basically invalidating tokens immediately instead of letting expired ones actually still be valid for a minute so that many async requests could still go through without sending a 401.

There was a config setting to change this and all worked after that. Maybe something similar in devise?

@neontuna
Copy link

neontuna commented Nov 9, 2016

Yes devise token auth has some settings related to this that would probably fix things but imo would make things less secure. For instance I like the fact that the token is constantly being refreshed but this could be turned off and that would help here. Ultimately I'm not too sad about the workarounds and I imagine devise_token_auth will fix things up on their end before too long. Thank you the addition!

@cjcrawford
Copy link
Author

v1.5.1-beta working great for me. The only devise thing I'm using is:

  authType: 'devise'

Nice work!

@websanova
Copy link
Owner

Note the new driver centric model in 2.x stream now (for anyone still watching the issue). Please check the updated docs and change log.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants