Authentication and Authorization tooling for Ember with Rails.
Based on code found in:
- ember-rails-devise-token-authentication
- shelving-emberjs-was-authorization-in
- writing-a-helper-to-check-permissions-in-ember
And a few other places (see below)
This gem includes both coffeescript assets and Rails controller code to get you started ;)
The Rails controller code depends on the cancan and rails-api gems.
This gem is a work in progress... please help improve it!
Also help improve the README and usage instructions.
Add this line to your application's Gemfile:
gem 'ember-beercan'
And then execute:
$ bundle
Or install it yourself as:
$ gem install ember-beercan
Add an ApiUrl
hash with some Rails authorization routes info stored:
# routes/api_url.js.coffee.erb
<% url_helpers = MyApp::Application.routes.url_helpers %>
App.Routes.ApiUrl = {
tokens_path: '<%= url_helpers.tokens_path %>',
authorization_path: '<%= url_helpers.authorization_path %>'
};
Usually the authorization_path
will point to AuthorizationsController#show
and the tokens_path
to TokensController
#create
and #destroy
methods.
Simply add the following to your script manifest.
#= require beercan
Or alternatively include individual "modules"
#= require beercan/authorization
Or with more control: #= require_tree beercan/authorization
or
#= require beercan/authorization/authorizer
Now add permissions:
App.Permissions.register("createPost", App.Permission.extend({
can: function() {
return this.get("currentUser.isAdmin");
}.property("currentUser.isAdmin")
}));
App.Permissions.register("editPost", App.Permission.extend({
can: function(){
return this.get("currentUser.isAdmin") || this.get("content.author.id") == this.get("currentUser.id");
}.property("currentUser.isAdmin", "content")
}));
See writing-a-helper-to-check-permissions-in-ember for more instructions.
Alternatively use the App.Authorization.Authorizer#authorize
method, which should call the AuthorizationsController
on the server, and respond with the result of a can? check there, so that the server is responsible for authorization and has the rules there...
This approach is sketched out here: shelving-emberjs-was-authorization-in
There are also some nice ideas presented in this gist about how to integrate with cancan permissions.
You must have a currentUser
on the controller for permissions to work.
For this a App.CurrentUserController
is included. On this controller you can set the currentUserPath
property to the path of your server API that returns the current user.
It might be useful to return a Guest
user, in case no user is logged in.
A CurrentUserController
with currentUser
functionality, is made available in all controllers using the Ember.Application.initializer
, named 'currentUser'.
The mixin module BeerCan::FindMe
can be included in a UsersController
on the server, to add the me
method ('users/me' path). This method tries to respond with a User (Serialized as JSON), by calling User.find_by token_hash
where token is assumed to be passed as a :authenticity_token
(or similar) parameter from the client (using Ajax).
You can customize the lookup-field used for the token on the user, by overriding the token_hash
method like this (default field is token:
)
class UsersController
include BeerCan::FindMe
protected
def token_hash
{auth_token: authenticity_token}
end
end
Simple sign in/out
- signedIn (is the user signed in?)
- signIn
- signOut
Autorizer.create(action: 'update', object: post).authorize
App.Authorization.Permissions.register(name, type)
App.UserSessionNewController.signIn(email, password)
This can be used for simple (old school) email/password login. The server should generate an auth token on successful login, which is then communicate between client and server until it expires, which then requires a new login.
App.Routes.SigninMatcher.map(match)
can be used to add signin routes:
match('/signIn').to('userSessionNew')
match('/users/:user_id').to('user')
- AuthorizationsController (subclass to have your control authorization enabled)
- TokensController (for managing auth tokens)
- GuardedController
The TokensController
uses a Tokener
. Currently tokeners are provided for Sorcery and Devise:
SorceryTokener
DeviseTokener
You can implement this simple interface for whichever Auth token solution you are using.
class SorceryTokener
attr_reader :user, :controller
def initialize user, controller = nil
@user = user
@controller = controller
end
def token
user.token
end
def reset_token
controller.destroy_access_token
end
def authenticate_token
# should fire after_login hook, which performs token authentication
controller.login_from_access_token
end
end
Note: This version of the sorcery gem, provides an AccessToken
module you can use as an alternative to Devise. See access_token
To select a tokener, you can use the tokener_for
macro, which expects a tokener class of the form <name>Tokener
class TokensController < APIController
tokener_for :sorcery
end
Use doorkeeper to setup your API with OAuth protection :)
BeerCan comes with a GuardedController
you can subclass which include basic DoorKeeper functionality to return the current_user based on the doorkeeper token.
Simply subclass GuardedBaseController
for controllers you want guarded by doorkeeper!
Then use the doorkeeper_for
macro to customize for which actions etc. it should apply
Default is for any xhr (AJAX) request received:
doorkeeper_for :all, :if => lambda { request.xhr? }
First install doorkeeper:
$ rails generate doorkeeper:install
Config example (mongoid):
# initializers/doorkeeper.rb
Doorkeeper.configure do
orm :mongoid2 # or :mongoid3, :mongo_mapper
# et resource owner, typically a User
resource_owner_authenticator do
User.find(session[:current_user_id]) || redirect_to(login_url)
end
end
For mongoid, make sure to create indexes!
$ rake db:mongoid:create_indexes
See wiki for more option and info
Currently, the BeerCan coffeescript auth modules are not setup to use the GuardedController
.Please help out implementing this integration ;)
See railscast: securing an api for a walk-through of what is required...
Would be nice to have built-in support for: ember-auth-rails and pundit
Note: to set global app state, use this tip: emberjs/ember.js#1780
# fired no matter what route is hit on application launch.
App.ApplicationRoute = Ember.Route.extend({
setupController: function(controller, model){
controller.loadCompanyName();
this._super(controller, model);
}
});
This approach might also be useful...
https://gist.github.com/ivanvanderbyl/4560416
class ApplicationController < ActionController::Base
helper_method :current_permission_json
serialization_scope :current_user
protected
# resource must return the current resource user tries to access
def current_permission
PermissionSerializer.new resource, :root => false
end
def current_permission_json
current_permission.to_json
end
end
class PermissionSerializer < ActiveModel::Serializer
attributes :can_update, :can_delete, :can_manage
def attributes
hash = super
hash.merge!(can_manage: can_manage, can_update: can_update, can_delete: can_delete)
hash.merge(special_permissions)
end
private
def special_permissions
return {} unless scope.role?(:author) && object.kind_of? Article
{can_delete: object.try(:user) == user }
end
def ability
@ability ||= Ability.new(scope)
end
def can? *args
ability.can? *args
end
def can_manage
can? :manage, all
end
def can_update
can? :update, object
end
def can_delete
can? :delete, object
end
end
And here a gem to achieve this effect: active_model_serializers-cancan
hasOne
and hasMany
serializer macros now support an additional property, authorize
. Associations with this property set to true will be authorized and filtered via CanCan. For example:
class PostSerializer < ActiveModel::Serializer
attributes :title, :content
has_one :author, authorize: true
has_many :comments, authorize: true
end
Serializers now also have access to the same helpers as controllers, namely current_ability
, can?
, and cannot?
i.e you can use similar logic to the example for PermissionSerializer
.
http://guides.rubyonrails.org/asset_pipeline.html
For faster asset precompiles, you can partially load your application by setting config.assets.initialize_on_precompile
to false
in config/application.rb
, though in that case templates cannot see application objects or methods. Heroku requires this to be false.
config.assets.initialize_on_precompile = false
- Fork it
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Add some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create new Pull Request