Let's see how to use Devise authentication on AngularJS.
I assume you have an AngularJS application running on a domain (https://myfabulousapp.com
) and the Rails API running on another domain (https://api.myfabulousapp.com
).
Let's create a simple project "test" from the scratch, with this structure:
test
+--- api (Rails, running on localhost:3000)
+--- app (AngularJS, running on localhost:3001)
+--- index.html
+--- app.js
+--- bower_components/
+--- views
+--- home.html
First, we create the project root folder, test
:
# create aplication folder
mkdir test
cd test
Now, we create the Rails app with Devise on test/api
:
# create backend (api, Rails)
rails new api --api
cd api
# install Devise
gem 'devise' >> Gemfile
bundle install
rails generate devise:install
rails generate devise user
rake db:migrate
# start backend server
rails s
Open another terminal window:
# create frontend folder (app, Angular)
cd test
mkdir app
cd app
# install Angular and the router library
bower install angular --save
bower install angular-ui-router --save
# install a minimalist html webserver
# gem install adsf
# start frontend server
adsf -p 3001
Our app/index.html
:
<html ng-app="myApp">
<header>
<script src="/bower_components/angular/angular.js"></script>
<script src="/bower_components/ui-router/release/angular-ui-router.js"></script>
<script src="/app.js"></script>
</header>
<body>
<ul>
<li><a href="/">Home</a></li>
</ul>
<ui-view></ui-view>
</body>
</html>
Our very basic app/app.js
:
var app = angular.module('myApp', [
'ui.router'
]);
app.config([
'$stateProvider',
'$urlRouterProvider',
'$locationProvider',
function (
$stateProvider,
$urlRouterProvider,
$locationProvider
) {
/** Uses "/my-url" instead of "/!#my-url". */
if (window.history && window.history.pushState) {
$locationProvider.html5Mode(true);
}
/** Router. */
$stateProvider
.state('home', {
url: '/',
templateUrl: '/views/home.html',
.otherwise({
redirectTo: '/'
});
}]);
The main page on the browser (open http://localhost:3001
):
Our Awesome Home Page
Add to api/Gemfile
:
gem 'rack-cors', :require => 'rack/cors'
Install it:
bundle install
On api/application.rb
:
# CORS
config.middleware.insert_before 0, Rack::Cors do
allow do
origins '*' # on production, use the line below instead
# origins 'localhost:3001', 'myfabulousapp.com'
resource '*', :headers => :any, :methods => [:get, :post, :delete, :put, :head]
end
end
WARNING
DO NOT add ":options".
If you do that, you won't be able to destroy sessions, because AngularJS replaces "delete" with "options" when communicating with another domain (and Devise doesn't recognize the "option" verb as a "delete")
Then, restart the server.
On api/application.rb
:
# Session
config.middleware.use ActionDispatch::Cookies
config.middleware.use ActionDispatch::Session::CookieStore
config.session_store :cookie_store, :key => '_namespace_key'
config.action_dispatch.cookies_serializer = :json
cd test/app
bower install --save angular-devise
<!-- app/views/home.html -->
<html ng-app="myApp">
<header>
<script src="/bower_components/angular/angular.js"></script>
<script src="/bower_components/ui-router/release/angular-ui-router.js"></script>
<script src="/bower_components/angular-devise/lib/devise.js"></script>
<script src="/app.js"></script>
</header>
<body>
<ul>
<li><a href="/">Home</a></li>
</ul>
<ui-view></ui-view>
</body>
</html>
// app/app.js
angular.module('myApp', ['Devise'])
$http
to always provide credentials// app/app.js
$httpProvider.defaults.withCredentials = true;
api_url
to your actual API location)// app/app.js
/** The API base URL. */
var api_url = 'http://localhost:3000/';
/** Sign in. */
AuthProvider.loginPath(api_url + 'users/sign_in.json');
AuthProvider.loginMethod('POST');
AuthProvider.resourceName('user');
/** Sign up. */
AuthProvider.registerPath(api_url + 'users.json');
AuthProvider.registerMethod('POST');
AuthProvider.resourceName('user');
/** Sign out. */
AuthProvider.logoutPath(api_url + 'users/sign_out.json');
AuthProvider.logoutMethod('DELETE');
Or a more extensive example:
// app/app.js
var app = angular.module('myApp', [
'ui.router',
'Devise'
]);
app.config([
'$stateProvider',
'$urlRouterProvider',
'$locationProvider',
'$httpProvider',
'AuthProvider',
function (
$stateProvider,
$urlRouterProvider,
$locationProvider,
$httpProvider,
AuthProvider
) {
/** Always provide credentials. */
$httpProvider.defaults.withCredentials = true;
/** The API base URL. */
var api_url = 'http://localhost:3000/';
/** Sign in. */
AuthProvider.loginPath(api_url + 'users/sign_in.json');
AuthProvider.loginMethod('POST');
AuthProvider.resourceName('user');
/** Sign up. */
AuthProvider.registerPath(api_url + 'users.json');
AuthProvider.registerMethod('POST');
AuthProvider.resourceName('user');
/** Sign out. */
AuthProvider.logoutPath(api_url + 'users/sign_out.json');
AuthProvider.logoutMethod('DELETE');
/** Uses "/my-url" instead of "/!#my-url". */
if (window.history && window.history.pushState) {
$locationProvider.html5Mode(true);
}
/** Router. */
$stateProvider
.state('home', {
url: '/',
templateUrl: '/views/home.html',
.otherwise({
redirectTo: '/'
});
}]);
Comment (or delete) the protect_from_forgery
line. Also, add a :json
to the respond_to
, so it will respond to JSON calls.
# api/application_controller.rb
respond_to :html, :json
# protect_from_forgery
WARNING
After testing your app and know it is working, you can look for a better solution to deal with CSRF, like use this angular_rails_csrf (opens new window) gem (which hasn't worked for me, but you may have more luck - see my tries here (opens new window)).
// app/app.js
angular.module('myModule', ['Devise'])
.run(['Auth', function (Auth) {
Auth.currentUser().then(function(user) {
console.log(user);
console.log(Auth._currentUser);
});
}]);