Skip to content

Commit

Permalink
Implement authentication/authorization verifier
Browse files Browse the repository at this point in the history
  • Loading branch information
0x7466 committed Mar 4, 2021
1 parent 2a25ced commit 18686aa
Show file tree
Hide file tree
Showing 12 changed files with 191 additions and 35 deletions.
42 changes: 15 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,29 @@ $ gem install active_entry

## Usage
With Active Entry authentication and authorization is done in your Rails controllers. To enable authentication and authorization in one of your controllers, just add a before action for `authenticate!` and `authorize!` and the user has to authenticate and authorize on every call.
You probably want to control authentication and authorization for every controller action you have in your app. To enable this, just add the before action to the `ApplicationController`.

### 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 controller and the call for auth is not forgotten in development, add the `#verify_authentication!` and `#verify_authorization` as after action callbacks to your `ApplicationController`.

```ruby
class ApplicationController < ActionController::Base
before_action :verify_authentication!, :verify_authorization!
# ...
end
```
This ensures, that you call `authenticate!` and/or `authorize!` in all your controllers and raises an `ActiveEntry::AuthenticationNotPerformedError` / `ActiveEntry::AuthorizationNotPerformedError` if not.

### Perform authentication and authorization
in order to do the actual authentication and authorization, you have to add `authenticate!` and `authorize!` as before action callback in your controllers.

```ruby
class DashboardController < ApplicationController
before_action :authenticate!, :authorize!
# ...
end
```

If you try to open a page, you will get an `ActiveEntry::AuthenticationNotPerformedError` or `ActiveEntry::AuthorizationNotPerformedError`. This means that you have to instruct Active Entry when a user is authenticated/authorized and when not.
If you try to open a page, you will get an `ActiveEntry::AuthenticationDecisionMakerMissingError` or `ActiveEntry::AuthorizationDecisionMakerMissingError`. This means that you have to instruct Active Entry when a user is authenticated/authorized and when not.
You can do this by defining the methods `authenticated?` and `authorized?` in your controller.

```ruby
Expand Down Expand Up @@ -194,31 +207,6 @@ class ApplicationController < ActionController::Base
end
```

## Known Issues
The authentication/authorization is done in a before action. These Rails controller before callbacks are done in defined order. If you set an instance variable which is needed in the `authenticated?` or `authorized?` method, you have to call the before action after the other method again.

For example if you set `@user` in your controller in the `set_user` before action and you want to use the variable in `authorized?` action, you have to add the `authenticate!` or `authorize!` method after the `set_user` again, otherwise `@user` won't be available in `authenticate!` or `authorized?` yet.

```ruby
class UsersController < ApplicationController
before_action :set_user
before_action :authenticate!, :authorize!

def show
end

private

def authenticated?
return true if user_signed_in?
end

def authorized?
return true if current_user == @user
end
end
```

## Contributing
Create pull requests on Github and help us to improve this Gem. There are some guidelines to follow:

Expand Down
22 changes: 20 additions & 2 deletions lib/active_entry.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@
require "active_entry/railtie" if defined? Rails::Railtie

module ActiveEntry
# Verifies that #authenticate! has been called in the controller.
def verify_authentication!
raise ActiveEntry::AuthenticationNotPerformedError unless @_authentication_done == true
end

# Authenticates the user
def authenticate!
general_decision_maker_method_name = :authenticated?
Expand All @@ -20,7 +25,7 @@ def authenticate!
#
# This ensures that you actually do authentication in your controller.
if !scoped_decision_maker_defined && !general_decision_maker_defined
raise ActiveEntry::AuthenticationNotPerformedError
raise ActiveEntry::AuthenticationDecisionMakerMissingError
end

error = {}
Expand All @@ -37,6 +42,15 @@ def authenticate!
# Use the .rescue_from method from ActionController::Base
# to catch the exception and show the user a proper error message.
raise ActiveEntry::NotAuthenticatedError.new(error) unless is_authenticated == true

# Tell #verify_authentication! that authentication
# has been performed.
@_authentication_done = true
end

# Verifies that #authorize! has been called in the controller.
def verify_authorization!
raise ActiveEntry::AuthorizationNotPerformedError unless @_authorization_done == true
end

# Authorizes the user.
Expand All @@ -55,7 +69,7 @@ def authorize!
#
# This ensures that you actually do authorization in your controller.
if !scoped_decision_maker_defined && !general_decision_maker_defined
raise ActiveEntry::AuthorizationNotPerformedError
raise ActiveEntry::AuthorizationDecisionMakerMissingError
end

error = {}
Expand All @@ -72,5 +86,9 @@ def authorize!
# Use the .rescue_from method from ActionController::Base
# to catch the exception and show the user a proper error message.
raise ActiveEntry::NotAuthorizedError.new(error) unless is_authorized == true

# Tell #verify_authorization! that authorization
# has been performed.
@_authorization_done = true
end
end
20 changes: 18 additions & 2 deletions lib/active_entry/errors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,19 @@ class AuthorizationError < StandardError
# Error for controllers in which authorization isn't handled.
#
# @raise [AuthorizationNotPerformedError]
# if the #authorized? method isn't defined
# if authorize! is not called
# in the controller class.
class AuthorizationNotPerformedError < AuthorizationError
end

# Error for controllers in which authorization decision maker is missing.
#
# @raise [AuthorizationDecisionMakerMissingError]
# if the #authorized? method isn't defined
# in the controller class.
class AuthorizationDecisionMakerMissingError < AuthorizationError
end

# Error if user unauthorized.
#
# @raise [NotAuthorizedError]
Expand Down Expand Up @@ -43,11 +51,19 @@ class AuthenticationError < StandardError
# Error for controllers in which authentication isn't handled.
#
# @raise [AuthenticationNotPerformedError]
# if the #authenticated? method isn't defined
# if authenticate! is not called
# in the controller class.
class AuthenticationNotPerformedError < AuthenticationError
end

# Error for controllers in which authentication decision maker is missing.
#
# @raise [AuthenticationDecisionMakerMissingError]
# if the #authenticated? method isn't defined
# in the controller class.
class AuthenticationDecisionMakerMissingError < AuthenticationError
end

# Error if user not authenticated
#
# @raise [NotAuthenticatedError]
Expand Down
4 changes: 2 additions & 2 deletions spec/authentication_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
before { dummy_class.define_method(:index) {} }
before { dummy_class.define_method(:action_name) { "index" } }

it 'raises `AuthenticationNotPerformedError` if #authenticated? is not defined' do
expect{ dummy_class.new.authenticate! }.to raise_error ActiveEntry::AuthenticationNotPerformedError
it 'raises `AuthenticationDecisionMakerMissingError` if #authenticated? is not defined' do
expect{ dummy_class.new.authenticate! }.to raise_error ActiveEntry::AuthenticationDecisionMakerMissingError
end

it 'raises `NotAuthenticatedError` if #authenticated? is false' do
Expand Down
4 changes: 2 additions & 2 deletions spec/authorization_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
before { dummy_class.define_method(:index) {} }
before { dummy_class.define_method(:action_name) { "index" } }

it 'raises `AuthorizationNotPerformedError` if #authorized? is not defined' do
expect{ dummy_class.new.authorize! }.to raise_error ActiveEntry::AuthorizationNotPerformedError
it 'raises `AuthorizationDecisionMakerMissingError` if #authorized? is not defined' do
expect{ dummy_class.new.authorize! }.to raise_error ActiveEntry::AuthorizationDecisionMakerMissingError
end

it 'raises `NotAuthorizedError` if #authorized? is false' do
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
require 'rails_helper'

describe AuthenticationVerificationTestController, type: :controller do
describe "#authentication_not_performed" do
it "does raise error" do
expect{ get :authentication_not_performed }.to raise_error ActiveEntry::AuthenticationNotPerformedError
end
end

describe "#authentication_performed" do
it "does not raise error" do
expect{ get :authentication_performed }.to_not raise_error
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
require 'rails_helper'

describe AuthorizationVerificationTestController, type: :controller do
describe "#authorization_not_performed" do
it "does raise error" do
expect{ get :authorization_not_performed }.to raise_error ActiveEntry::AuthorizationNotPerformedError
end
end

describe "#authorization_performed" do
it "does not raise error" do
expect{ get :authorization_performed }.to_not raise_error
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
class AuthenticationVerificationTestController < ApplicationController
after_action :verify_authentication!
before_action :authenticate!, only: :authentication_performed

def authentication_not_performed
head :no_content
end

def authentication_performed_authenticated?
true
end
def authentication_performed
head :no_content
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
class AuthorizationVerificationTestController < ApplicationController
after_action :verify_authorization!
before_action :authorize!, only: :authorization_performed

def authorization_not_performed
head :no_content
end

def authorization_performed_authorized?
true
end
def authorization_performed
head :no_content
end
end
6 changes: 6 additions & 0 deletions spec/dummy/config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,10 @@
get "scoped_other" => "scoped_decision_maker_test#other"
get "scoped_non_authenticated" => "scoped_decision_maker_test#non_authenticated"
get "scoped_non_authorized" => "scoped_decision_maker_test#non_authorized"

get "authentication_not_performed" => "authentication_verification_test#authentication_not_performed"
get "authentication_performed" => "authentication_verification_test#authentication_performed"

get "authorization_not_performed" => "authorization_verification_test#authorization_not_performed"
get "authorization_performed" => "authorization_verification_test#authorization_performed"
end
34 changes: 34 additions & 0 deletions spec/verify_authentication_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
require "rails_helper"

describe "Authentication verification" do
let(:dummy_class) { Class.new { include ActiveEntry } }
let(:dummy_obj) { dummy_class.new }

before { dummy_class.define_method(:action_name) { "index" } }

it 'raises `AuthenticationNotPerformedError` if #authenticate! is not called' do
expect{ dummy_obj.verify_authentication! }.to raise_error ActiveEntry::AuthenticationNotPerformedError
end

it 'does not raise error if @_authentication_done is true' do
dummy_obj.instance_variable_set :@_authentication_done, true
expect{ dummy_obj.verify_authentication! }.to_not raise_error
end

it 'does not raise error if #authenticate! is called' do
dummy_class.define_method(:authenticated?) { true }

expect do
dummy_obj.authenticate!
dummy_obj.verify_authentication!
end.to_not raise_error
end

describe '#authenticate!' do
it "sets @_authentication_done" do
dummy_class.define_method(:authenticated?) { true }
dummy_obj.authenticate!
expect(dummy_obj.instance_variable_get(:@_authentication_done)).to be true
end
end
end
34 changes: 34 additions & 0 deletions spec/verify_authorization_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
require "rails_helper"

describe "Authorization verification" do
let(:dummy_class) { Class.new { include ActiveEntry } }
let(:dummy_obj) { dummy_class.new }

before { dummy_class.define_method(:action_name) { "index" } }

it 'raises `AuthorizationNotPerformedError` if #authorize! is not called' do
expect{ dummy_obj.verify_authorization! }.to raise_error ActiveEntry::AuthorizationNotPerformedError
end

it 'does not raise error if @_authorization_done is true' do
dummy_obj.instance_variable_set :@_authorization_done, true
expect{ dummy_obj.verify_authorization! }.to_not raise_error
end

it 'does not raise error if #authenticate! is called' do
dummy_class.define_method(:authorized?) { true }

expect do
dummy_obj.authorize!
dummy_obj.verify_authorization!
end.to_not raise_error
end

describe '#authorize!' do
it "sets @_authorization_done" do
dummy_class.define_method(:authorized?) { true }
dummy_obj.authorize!
expect(dummy_obj.instance_variable_get(:@_authorization_done)).to be true
end
end
end

0 comments on commit 18686aa

Please sign in to comment.