I have a Rails and Ionic project. The back-end uses the devise_token_auth Gem, and the front end ng-token-auth; these are supposed to work "seamlessly".
I've got everything working as far as registration and sign in, which returns a valid response object. Hoever, any further requests after I use $state.go('app.somepage') result in 401 Unauthorized responses.
I get the sense that I'm not actually storing the token anywhere. Can someone please help?
Here are some snippets:
.controller('LoginCtrl',['$scope', '$auth', '$state', function($scope, $auth, $state) {
$scope.loginForm = {}
$scope.handleLoginBtnClick = function() {
console.log($scope.loginForm);
$auth.submitLogin($scope.loginForm)
.then(function(resp) {
$state.go('app.feed');
})
.catch(function(resp) {
console.log(resp.errors);
});
};
State definition:
.state('app', {
url: "/app",
abstract: true,
templateUrl: "templates/menu.html",
controller: 'AppCtrl',
resolve: {
auth: function($auth) {
return $auth.validateUser();
}
}
})
Resources:
factory('Post', ['railsResourceFactory', 'apiUrl', function (railsResourceFactory, apiUrl) {
return railsResourceFactory({
url: apiUrl + '/posts',
name: 'post'
});
}]).
And in PostsCtrl:
$scope.loadFeed = function() {
Post.query().then(function (posts) {
$scope.posts = posts;
}, function (error) {
console.log( 'Did not get posts!'); ### THIS FIRES
}).finally(function() {
// Stop the ion-refresher from spinning
$scope.$broadcast('scroll.refreshComplete');
});
};
Login response object:
{"data":{"id":1,"provider":"email","uid":"1234","phone":null,"name":"Admin","image":null,"username":"admin"}}
Top of ApplicationController:
class ApplicationController < ActionController::Base
include DeviseTokenAuth::Concerns::SetUserByToken
before_filter :add_allow_credentials_headers
before_filter :cors_preflight_check
after_filter :cors_set_access_control_headers
before_action :configure_permitted_parameters, if: :devise_controller?
..yadayada...
def configure_permitted_parameters
devise_parameter_sanitizer.for(:sign_up) << :phone
devise_parameter_sanitizer.for(:sign_up) << :username
devise_parameter_sanitizer.for(:sign_up) << :session
devise_parameter_sanitizer.for(:sign_in) << :phone
devise_parameter_sanitizer.for(:sign_in) << :username
devise_parameter_sanitizer.for(:sign_in) << :session
end
And some default models for User on the rails side.
Rails log:
Started GET "/posts" for 192.168.83.26 at 2015-02-24 23:29:02 -0500
Processing by PostsController#index as JSON
Parameters: {"post"=>{}}
Filter chain halted as :authenticate_user! rendered or redirected
Completed 401 Unauthorized in 1ms (Views: 0.2ms | ActiveRecord: 0.0ms)
If anyone can provide some insight that would be wonderful. I'm happy to post more snippets as needed.
As it turns out, the solution was rather simple. It seems that in most of the examples everyone provides, they neglect to permit access-token
, along with all the other CORS headers.
We used rack-cors for this, at the bottom of config.ru
:
require 'rack/cors'
use Rack::Cors do
# allow all origins in development
allow do
origins '*'
resource '*',
:headers => :any,
:expose => ['access-token', 'expiry', 'token-type', 'uid', 'client'],
:methods => [:get, :post, :delete, :put, :options]
end
end
And then in ApplicationController.rb:
before_filter :add_allow_credentials_headers
skip_before_filter :verify_authenticity_token
before_filter :cors_preflight_check
after_filter :cors_set_access_control_headers
def cors_set_access_control_headers
headers['Access-Control-Allow-Origin'] = '*'
headers['Access-Control-Allow-Methods'] = 'POST, GET, PUT, DELETE, OPTIONS'
headers['Access-Control-Allow-Headers'] = 'Origin, Content-Type, Accept, Authorization, Token'
headers['Access-Control-Max-Age'] = '1728000'
end
def cors_preflight_check
if request.method == 'OPTIONS'
headers['Access-Control-Allow-Origin'] = '*'
headers['Access-Control-Allow-Methods'] = 'POST, GET, PUT, DELETE, OPTIONS'
headers['Access-Control-Allow-Headers'] = 'X-Requested-With, X-Prototype-Version, Token'
headers['Access-Control-Max-Age'] = '1728000'
render :text => '', :content_type => 'text/plain'
end
end
def add_allow_credentials_headers
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS#section_5
#
# Because we want our front-end to send cookies to allow the API to be authenticated
# (using 'withCredentials' in the XMLHttpRequest), we need to add some headers so
# the browser will not reject the response
response.headers['Access-Control-Allow-Origin'] = request.headers['Origin'] || '*'
response.headers['Access-Control-Allow-Credentials'] = 'true'
end
This information may be relevant to you.
jwako/ionic_rails_sample
As for my case, i use cookies to store the token. And whenever we do $auth
methods in our Angular application, some of the methods will try to go to the devise route that you have defined in your Rails router and match/validate the token that is stored in any of the header's requests. (everytime you try to do http request! checkout your request headers using your browser inspector's if they're containing uid
or auth_token
if you're going to validate through the GET
/validate_token
(https://github.com/lynndylanhurley/devise_token_auth#usage-tldr))
Since you didn't mention your route, we can assume /auth
.
And those $http
request provided by the $auth
should be containing token to be authenticated the the Rails's Devise, and catch and store it to the browser's cookies whenever we do $auth.submitLogin()
.
Here's the example on how it works in my previous project.
app.factory('authInterceptor', ['$q', 'ipCookie', '$location', function($q, ipCookie, $location) {
return {
request: function(config) {
config.headers = config.headers || {};
if (ipCookie('access-token')) {
config.headers['Access-Token'] = ipCookie('access-token');
config.headers['Client'] = ipCookie('client');
config.headers['Expiry'] = ipCookie('expiry');
config.headers['Uid'] = ipCookie('uid');
}
return config;
},
responseError: function(response) {
if (response.status === 401) {
$location.path('/login');
ipCookie.remove('access-token');
}
return $q.reject(response);
}
};
}])
And set the token format to be looked like this (or custom as you need it)
$authProvider.configure({
tokenValidationPath: '/auth/validate_token',
signOutUrl: '/auth/sign_out',
confirmationSuccessUrl: window.location.href,
emailSignInPath: '/auth/sign_in',
storage: 'cookies',
tokenFormat: {
"access-token": "{{ token }}",
"token-type": "Bearer",
"client": "{{ clientId }}",
"expiry": "{{ expiry }}",
"uid": "{{ uid }}"
}
});
Don't forget to inject ipCookie
(lookup for angular-cookie
instead of angular-cookies
) to the Interceptor since this is the cookie library that ng-token-auth use for cookies management.
Please comment below with questions if i didn't make myself clear enough. :D
Maybe is too late,
But the problem is because you can't have the auth on cookies (only Android). So, you can try to use the localStorage to save your session info (on iOS & Android)
e.g
.config(function($authProvider) {
$authProvider.configure({
apiUrl: 'http://myprivateapidomain/api',
storage: 'localStorage'
});
})
You can read more in the specific issue of the documentation: https://github.com/lynndylanhurley/ng-token-auth/issues/93
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With