Authentication issue with Devise, devise_token_aut

2019-09-17 15:53发布

问题:

Im a little stuck and i haven't been able to find a solution. I have a rails site that uses devise for the front end and devise_token_auth for the api. The front end uses a combination of server sided rendered pages and api calls to to display data to the user. The login form will eventally work (ushally 2-3 submits) if i use a pure angular login:

%div{'ng-controller'=>'logInCtrl'}
  %h2 Log In
  %div{:layout=>'column'}
    %div{:flex=>20}
    %div{:flex=>60, :layout=>'column'}
      = form_for(resource, as: resource_name, url: session_path(resource_name)) do |f|
        %div{:layout=>'column'}
          %md-input-container
            =f.label :login
            %input{'ng-model'=>'loginForm.login', :autofocus => 'true'}
          %md-input-container
            = f.label :password
            %input{:type=>'password', 'ng-model'=>'loginForm.password', :autocomplete => 'off'}

          %md-input-container
            %md-button.md-raised.md-primary{'ng-click'=>'submitMe()'}
              -#{:type=>'submit'}
              %md-icon.mdi.mdi-account-key
              Log In

:coffee
  myApp.controller 'logInCtrl', ($scope, $resource, $http, $mdDialog, $auth ) ->
    $scope.submitMe = () ->
      $auth.submitLogin($scope.loginForm).then( (resp)->
        location.replace('/users/sign_in')
      )

If i use a standard post method the correct information is rendered by the server but there is not token set for ng-token-auth. I can generate and send the token headers manually on session#create using:

  # POST /resource/sign_in
  def create
    super do |user|
      newAuth = user.create_new_auth_token
      response.headers.merge!(newAuth)
    end
  end

The issue that i have with this approach is that ng-token-auth never picks up the token from the headers since it didn't make the request. I've looked for a way to manual set the token header with no luck.

-- As a side not i will eventually be moving to a oauth solution so whatever workaround i use will need to port to that. -- I should mention that the server side rendering take care of design elements as well as turning function on and off. I also use an element of current_user to set a sub-set of table names based on a users location.

回答1:

After some research and a little time I came up with a solution that works, although a little hacky.

When Devise creates a new session a redirect is called to the route specified in your configuration files, so setting any variables is lost. the second issue is that ng-token-auth will only set and use a token that is set during the sign-in function, so simply sending a token via the pages headers is not detected (this is a limitation of the browser not the code).

After trying different iterations of using ng-token-auth and a standard devise authentication for my user I came to the conclusion that it would better to first authorize the user with devise and then somehow set a token with ng-token-auth; so I started to research what ng-token-auth actually did when it received a token via login. It turns out that it sets two cookies:

currentConfigName | default | domain | / | exp date

auth_headers | url encoded token data | domain | / | exp date

Now the issue was how to pass a newly generated token to the front end; this turned our to be simpler than I thought. Since the only data that is persisted between calls in Rails is session data I decided it would make sense to add a flag to the session data which signaled my ApplicationController to generate a new key.

First I extended Devise::SessionsController#create

def create
  super do |user|
    session[:token] = true
  end
end

This sets a session variable named token to True. Than in ApplicationController I added:

 before_filter :set_token

  def set_token
    if session[:token] && user_signed_in?
      newAuth = current_user.create_new_auth_token
      response.headers.merge!(newAuth)
      @auth_token = newAuth
      session.delete(:token)
    else
      @auth_token = false
    end
  end

This before filter looks for session[:token] and if set calls devise-token-auth's create_new_auth_token function to 'sign-in' the current user. This header information is both written to the outgoing pages headers as well as assigned to the variable @auth_token. Finally in views/laoyouts/applicationhtml.haml this code block is added to right after the %body tag

- if @auth_token
  :coffee
    myApp.controller 'cookieController', ($scope, $resource, $http, $mdDialog, $auth, ipCookie ) ->
      ipCookie('auth_headers', "{\"access-token\": \"#{@auth_token['access-token']}\",\"token-type\": \"Bearer\",\"client\": \"#{@auth_token['client']}\",\"expiry\": \"#{@auth_token['expiry']}\",\"uid\": \"#{@auth_token['uid']}\"}", {path: "/",expires: 9999,expirationUnit: 'days',secure: false})
      ipCookie('currentConfigName', 'default', {path: "/",expires: 9999,expirationUnit: 'days',secure: false})
  %div{'ng-controller'=>'cookieController'}

This adds an empty div tag and angular controller that uses the data written to @auth_token and the ipCookie function (required by ng-token-auth) to write the necessary cookies.

a few notes:

  • I added the code block to the main layout page and ApplicationController because when a user is signed into my site they may be redirected to one of two pages depending on their credentials. This ensures that no matter what page they are sent to the code token is generated and the code block is inserted.
  • I know that there is probably a better way to handle the generation of the cookies instead of creating an empty div and assigning a controller to it. I am open to more elegant solutions.