Active Entry - Simple and flexible authentication and authorization
Active Entry is a secure way to check for authentication and authorization before an action is performed. It's currently only compatible with Rails. But in later versions will ActiveEntry be Framework independent.
Active Entry works like many other Authorization Systems like Pundit or Action Policy with Policies. However in Active Entry it's all about the method calling the auth mechanism. For every method that needs authentication or authorization, a decision maker method counterpart has to be created in the policy of the class.
Example
Let's say we have an Users controller in our application:
# app/controllers/users_controller.rb
class UsersController < ApplicationController
include ActiveEntry::ControllerConcern # Glue for the controller and Active Entry
def index
pass! # The auth happens here
load_users
end
end
We have to create the UsersPolicy in order for Active Entry to know who is authenticated and authorized and who not.
# app/policies/users_policy.rb
module UsersPolicy
class Authentication < ActiveEntry::Base::Authentication
def index?
Current.user_signed_in? # Only signed in users are considered to be authenticated.
end
end
class Authorization < ActiveEntry::Base::Authorization
def index?
Current.user.admin? # Only admins are authorized to perform this action
end
end
end
Now every time somebody calls the users#index
endpoint, he or she has to be signed in and an admin. Otherwise ActiveEntry::NotAuthenticatedError
or ActiveEntry::NotAuthorizedError
are raised.
You can catch them easily in your controller by using Rails' rescue_from
.
class ApplicationController < ActionController::Base
rescue_from ActiveEntry::NotAuthenticatedError, with: :not_authenticated
rescue_from ActiveEntry::NotAuthorizedError, with: :not_authorized
def not_authenticated
flash[:danger] = "Not authenticated. Please sign in."
redirect_to sign_in_path
end
def
flash[:danger] = "Not authorized."
redirect_to root_path
end
end
Installation
Add this line to your application's Gemfile:
gem 'active_entry'
Or install it without bundler:
$ gem install active_entry
Run Bundle:
$ bundle
And then install Active Entry:
$ rails g active_entry:install
This will generate app/policies/application_policy.rb
.
Usage
Active Entry works with Policies. You can generate policies the following way:
Let's consider the example from above. We have an UsersController and we want a policy for that:
$ rails g policy Users
This generates a policy called UsersPolicy
and is located in app/policies/users_policy.rb
.
The above generator call would generate something like this, but with a few comments to help you get started:
module UsersPolicy
class Authentication < ActiveEntry::Base::Authentication
end
class Authorization < ActiveEntry::Base::Authorization
end
end
Verify authentication and authorization
You probably want to control authentication and authorization for every controller action you have in your app. As a safeguard to ensure, that auth is performed in every request and the auth call is not forgotten in development, add the verify_authentication!
and verify_authorization!
to your ApplicationController
:
class ApplicationController < ActionController::Base
verify_authentication!
# ...
end
This ensures, that you perform auth in all your controllers and raises errors if not.
Perform authentication and authorization
in order to do the actual authentication and authorization, you have to use authenticate!
and authorize!
or pass!
as in your actions.
class UsersController < ApplicationController
def authentication_only_action
authenticate!
end
def
end
def
pass!
end
end
If you try to open a page, Active Entry will raise ActiveEntry::DecisionMakerMethodNotDefinedError
. This means we have to define the decision makers in our policy.
module UsersPolicy
class Authentication < ApplicationPolicy::Authentication
def authentication_only_action?
success # == true | Everybody is allowed
end
def
success
end
end
class Authorization < ApplicationPolicy::Authorization
def
success
end
def
success
end
end
end
Every decision maker ends with an ?
. The name has to be the same as the name of the controller action. So index
is going to be index?
.
In order for Active Entry to not raise an auth error, the decision makers have to return true
. In our above example we used success
, which simply returns true
.
Note: It has to be an explicit true
and not just a truthy value. A string or object return value would raise an auth error.
Rescuing from errors
Catch the errors in your controllers to redirect the user or show them a message.
class ApplicationController < ActionController::Base
# ...
rescue_from ActiveEntry::NotAuthenticatedError, with: :not_authenticated
rescue_from ActiveEntry::NotAuthorizedError, with: :not_authorized
private
def not_authenticated
flash[:danger] = "You are not authenticated!"
redirect_to login_path
end
def
flash[:danger] = "You are not authorized to call this action!"
redirect_to root_path
end
end
In this example above, the user will be redirected with a flash message. But you can do whatever you want. For example logging.
Authenticate/authorize outside the action
You can authenticate and authorize outside the action:
class UsersController < ApplicationController
authenticate_now!
# pass_now! # Does both, authentication and authorization
end
Access control on class level will ensure that every action performs it.
Note: Don't use the class methods if the controller is inherited in other controllers. Best, don't use them at all and use the methods in the actions conciously.
Variables
You can pass variables to the decision maker.
class UsersController < ApplicationController
def show
@user = User.find params[:id]
pass! user: @user
end
end
You can now access the user object as instance variable in your decision maker.
module Users
class Authentication < ApplicationPolicy::Authentication
def show?
@user # == <User:Instance>
end
end
class Authorization < ApplicationPolicy::Authorization
def show?
@user # == <User:Instance>
end
end
end
Custom error data
If you write something into @error
in our decision maker, you can access it in your rescue methods in the controller:
module UsersPolicy
class Authentication < ApplicationPolicy::Authentication
def show?
@error = { code: 100 }
end
end
class Authorization < ApplicationPolicy::Authorization
def show?
@error = { code: 100 }
end
end
end
class ApplicationController < ActionController::Base
# ...
rescue_from ActiveEntry::NotAuthenticatedError, with: :not_authenticated
rescue_from ActiveEntry::NotAuthorizedError, with: :not_authorized
private
def not_authenticated exception
flash[:danger] = "You are not authenticated! Code: #{exception.error[:code]}"
redirect_to root_path
end
def exception
flash[:danger] = "You are not authorized to call this action! Code: #{exception.error[:code]}"
redirect_to root_path
end
end
But you can pass in whatever you want into your error hash.
Testing
You can easily test your policies in RSpec. Let's start with the generator:
$ rails g rspec:policy Users
This will generate a spec for the UsersPolicy
located in spec/policies/users_policy_spec.rb
require "rails_helper"
RSpec.describe UsersPolicy, type: :policy do
pending "add some examples to (or delete) #{__FILE__}"
end
Now you can easily test every decision maker with the be_authenticated_for
and be_authorized_for
matchers.
require "rails_helper"
RSpec.describe UsersPolicy, type: :policy do
describe UsersPolicy::Authentication do
subject { UsersPolicy::Authentication }
context "anonymous" do
it { is_expected.to_not be_authenticated_for :index }
it { is_expected.to be_authenticated_for :new }
it { is_expected.to be_authenticated_for :create }
it { is_expected.to_not be_authenticated_for :edit }
it { is_expected.to_not be_authenticated_for :update }
it { is_expected.to_not be_authenticated_for :destroy }
it { is_expected.to_not be_authenticated_for :restore }
end
context "signed in" do
before { Current.user = build :user }
it { is_expected.to be_authenticated_for :index }
it { is_expected.to be_authenticated_for :new }
it { is_expected.to be_authenticated_for :create }
it { is_expected.to be_authenticated_for :edit }
it { is_expected.to be_authenticated_for :update }
it { is_expected.to be_authenticated_for :destroy }
it { is_expected.to be_authenticated_for :restore }
end
end
describe UsersPolicy::Authorization do
subject { UsersPolicy::Authorization }
let(:user) { build :user }
context "anonymous" do
it { is_expected.to :index }
it { is_expected.to :new }
it { is_expected.to :create }
it { is_expected.to :show, user: user }
it { is_expected.to_not :edit, user: user }
it { is_expected.to_not :update, user: user }
it { is_expected.to_not :destroy, user: user }
it { is_expected.to_not :restore, user: user }
end
context "if @user is Current.user" do
before { Current.user = user }
it { is_expected.to :show, user: user }
it { is_expected.to :edit, user: user }
it { is_expected.to :update, user: user }
it { is_expected.to :destroy, user: user }
it { is_expected.to :restore, user: user }
end
context "if @user is not Current.user" do
before { Current.user = build :user }
it { is_expected.to :show, user: user }
it { is_expected.to_not :edit, user: user }
it { is_expected.to_not :update, user: user }
it { is_expected.to_not :destroy, user: user }
it { is_expected.to_not :restore, user: user }
end
end
end
Differences to Action Policy
Action Policy is an awesome gem which works pretty similar to Active Entry. But there are some differences:
Action Policy expects a performing subject and a target object
class PostPolicy < ApplicationPolicy
def update?
# `user` is a performing subject,
# `record` is a target object (post we want to update)
user.admin? || (user.id == record.user_id)
end
end
In Active Entry you can pass in anything you want into the decision maker, which is accessible as instance variables. See Variables.
One strategy is not better than the other. It's just our preference.
Policies in Action Policy are for Resources/Models
If you have a Post
model, you have a PostPolicy
in Action Policy. In Active Entry you create policies for controllers. So if you have a PostsController
, you have a PostsPolicy
.
We like to build access control logic around controller endpoints.
Action Policy performs only authorization
Active Entry does technically also not provide authentication mechanisms. It's just that you place your authentication logic in an authentication decision maker.
We like both authentication and authorization logic in the same place but seperated hence UsersPolicy::Authentication
and UsersPolicy::Authorization
.
Contributing
Create pull requests on Github and help us to improve this Gem. There are some guidelines to follow:
- Follow the conventions
- Test all your implementations
- Document methods that aren't self-explaining (we are using YARD)
License
The gem is available as open source under the terms of the MIT License.