merb-auth-core 0.9.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/LICENSE +20 -0
- data/README.textile +338 -0
- data/Rakefile +65 -0
- data/TODO +0 -0
- data/lib/merb-auth-core/authenticated_helper.rb +42 -0
- data/lib/merb-auth-core/authentication.rb +130 -0
- data/lib/merb-auth-core/bootloader.rb +10 -0
- data/lib/merb-auth-core/customizations.rb +24 -0
- data/lib/merb-auth-core/errors.rb +66 -0
- data/lib/merb-auth-core/merbtasks.rb +6 -0
- data/lib/merb-auth-core/responses.rb +36 -0
- data/lib/merb-auth-core/router_helper.rb +25 -0
- data/lib/merb-auth-core/session_mixin.rb +57 -0
- data/lib/merb-auth-core/strategy.rb +206 -0
- data/lib/merb-auth-core.rb +26 -0
- data/spec/helpers/authentication_helper_spec.rb +111 -0
- data/spec/merb-auth-core/activation_fixture.rb +2 -0
- data/spec/merb-auth-core/authentication_spec.rb +318 -0
- data/spec/merb-auth-core/customizations_spec.rb +22 -0
- data/spec/merb-auth-core/errors_spec.rb +51 -0
- data/spec/merb-auth-core/merb-auth-core_spec.rb +15 -0
- data/spec/merb-auth-core/router_helper_spec.rb +114 -0
- data/spec/merb-auth-core/strategy_spec.rb +274 -0
- data/spec/spec_helper.rb +93 -0
- metadata +100 -0
@@ -0,0 +1,66 @@
|
|
1
|
+
module Merb
|
2
|
+
class Authentication
|
3
|
+
|
4
|
+
def errors
|
5
|
+
@errors ||= Errors.new
|
6
|
+
end
|
7
|
+
|
8
|
+
# Lifted from DataMapper's dm-validations plugin :)
|
9
|
+
# @author Guy van den Berg
|
10
|
+
# @since DM 0.9
|
11
|
+
class Errors
|
12
|
+
|
13
|
+
include Enumerable
|
14
|
+
|
15
|
+
# Clear existing authentication errors.
|
16
|
+
def clear!
|
17
|
+
errors.clear
|
18
|
+
end
|
19
|
+
|
20
|
+
# Add a authentication error. Use the field_name :general if the errors does
|
21
|
+
# not apply to a specific field of the Resource.
|
22
|
+
#
|
23
|
+
# @param <Symbol> field_name the name of the field that caused the error
|
24
|
+
# @param <String> message the message to add
|
25
|
+
def add(field_name, message)
|
26
|
+
(errors[field_name] ||= []) << message
|
27
|
+
end
|
28
|
+
|
29
|
+
# Collect all errors into a single list.
|
30
|
+
def full_messages
|
31
|
+
errors.inject([]) do |list,pair|
|
32
|
+
list += pair.last
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Return authentication errors for a particular field_name.
|
37
|
+
#
|
38
|
+
# @param <Symbol> field_name the name of the field you want an error for
|
39
|
+
def on(field_name)
|
40
|
+
errors_for_field = errors[field_name]
|
41
|
+
errors_for_field.blank? ? nil : errors_for_field
|
42
|
+
end
|
43
|
+
|
44
|
+
def each
|
45
|
+
errors.map.each do |k,v|
|
46
|
+
next if v.blank?
|
47
|
+
yield(v)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def empty?
|
52
|
+
entries.empty?
|
53
|
+
end
|
54
|
+
|
55
|
+
def method_missing(meth, *args, &block)
|
56
|
+
errors.send(meth, *args, &block)
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
def errors
|
61
|
+
@errors ||= {}
|
62
|
+
end
|
63
|
+
|
64
|
+
end # class Errors
|
65
|
+
end # Authentication
|
66
|
+
end # Merb
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Merb
|
2
|
+
# These are not intended to be used directly
|
3
|
+
class Authentication
|
4
|
+
attr_accessor :body
|
5
|
+
|
6
|
+
def redirected?
|
7
|
+
!!headers["Location"]
|
8
|
+
end
|
9
|
+
|
10
|
+
def headers
|
11
|
+
@headers ||= {}
|
12
|
+
end
|
13
|
+
|
14
|
+
def status
|
15
|
+
@status ||= 200
|
16
|
+
end
|
17
|
+
|
18
|
+
def status=(sts)
|
19
|
+
@status = sts
|
20
|
+
end
|
21
|
+
|
22
|
+
def halted?
|
23
|
+
!!@halt
|
24
|
+
end
|
25
|
+
|
26
|
+
def headers=(headers)
|
27
|
+
raise ArgumentError, "Need to supply a hash to headers. Got #{headers.class}" unless headers.kind_of?(Hash)
|
28
|
+
@headers = headers
|
29
|
+
end
|
30
|
+
|
31
|
+
def halt!
|
32
|
+
@halt = true
|
33
|
+
end
|
34
|
+
|
35
|
+
end # Merb::Authentication
|
36
|
+
end # Merb
|
@@ -0,0 +1,25 @@
|
|
1
|
+
Merb::Router.extensions do
|
2
|
+
|
3
|
+
def authenticate(*strategies, &block)
|
4
|
+
p = Proc.new do |request, params|
|
5
|
+
if request.session.authenticated?
|
6
|
+
params
|
7
|
+
else
|
8
|
+
if strategies.blank?
|
9
|
+
result = request.session.authenticate!(request, params)
|
10
|
+
else
|
11
|
+
result = request.session.authenticate!(request, params, *strategies)
|
12
|
+
end
|
13
|
+
|
14
|
+
if request.session.authentication.halted?
|
15
|
+
auth = request.session.authentication
|
16
|
+
[auth.status, auth.headers, auth.body]
|
17
|
+
else
|
18
|
+
params
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
defer(p, &block)
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Merb
|
2
|
+
module Session
|
3
|
+
|
4
|
+
def self.included(base)
|
5
|
+
base.send(:include, InstanceMethods)
|
6
|
+
base.send(:extend, ClassMethods)
|
7
|
+
end
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
end # ClassMethods
|
11
|
+
|
12
|
+
module InstanceMethods
|
13
|
+
|
14
|
+
# Access to the authentication object directly. Particularly useful
|
15
|
+
# for accessing the errors.
|
16
|
+
#
|
17
|
+
# === Example
|
18
|
+
#
|
19
|
+
# <%= error_messages_for session.authentication %>
|
20
|
+
#
|
21
|
+
def authentication
|
22
|
+
@authentication ||= Merb::Authentication.new(self)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Check to see if the current session is authenticated
|
26
|
+
# @return true if authenticated. false otherwise
|
27
|
+
def authenticated?
|
28
|
+
authentication.authenticated?
|
29
|
+
end
|
30
|
+
|
31
|
+
# Authenticates the session via the authentication object.
|
32
|
+
#
|
33
|
+
# See Merb::Authentication#authenticate for usage
|
34
|
+
def authenticate!(request, params, *rest)
|
35
|
+
authentication.authenticate!(request, params, *rest)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Provides access to the currently authenticated user.
|
39
|
+
def user
|
40
|
+
authentication.user
|
41
|
+
end
|
42
|
+
|
43
|
+
# set the currently authenticated user manually
|
44
|
+
# Merb::Authentication#store_user should know how to store the object into the session
|
45
|
+
def user=(the_user)
|
46
|
+
authentication.user = the_user
|
47
|
+
end
|
48
|
+
|
49
|
+
# Remove the user from the session and clear all data.
|
50
|
+
def abandon!
|
51
|
+
authentication.abandon!
|
52
|
+
end
|
53
|
+
|
54
|
+
end # InstanceMethods
|
55
|
+
|
56
|
+
end # Session
|
57
|
+
end # Merb
|
@@ -0,0 +1,206 @@
|
|
1
|
+
module Merb
|
2
|
+
class Authentication
|
3
|
+
cattr_reader :strategies, :default_strategy_order, :registered_strategies
|
4
|
+
@@strategies, @@default_strategy_order, @@registered_strategies = [], [], {}
|
5
|
+
|
6
|
+
# Use this to set the default order of strategies
|
7
|
+
# if you need to in your application. You don't need to use all avaiable strategies
|
8
|
+
# in this array, but you may not include a strategy that has not yet been defined.
|
9
|
+
#
|
10
|
+
# @params [Merb::Authentiation::Strategy,Merb::Authentication::Strategy]
|
11
|
+
#
|
12
|
+
# @public
|
13
|
+
def self.default_strategy_order=(*order)
|
14
|
+
order = order.flatten
|
15
|
+
bad = order.select{|s| !s.ancestors.include?(Strategy)}
|
16
|
+
raise ArgumentError, "#{bad.join(",")} do not inherit from Merb::Authentication::Strategy" unless bad.empty?
|
17
|
+
@@default_strategy_order = order
|
18
|
+
end
|
19
|
+
|
20
|
+
# Allows for the registration of strategies.
|
21
|
+
# @params <Symbol, String>
|
22
|
+
# +label+ The label is the label to identify this strategy
|
23
|
+
# +path+ The path to the file containing the strategy. This must be an absolute path!
|
24
|
+
#
|
25
|
+
# Registering a strategy does not add it to the list of strategies to use
|
26
|
+
# it simply makes it available through the Merb::Authentication.activate method
|
27
|
+
#
|
28
|
+
# This is for plugin writers to make a strategy availalbe but this should not
|
29
|
+
# stop you from declaring your own strategies
|
30
|
+
#
|
31
|
+
# @plugin
|
32
|
+
def self.register(label, path)
|
33
|
+
self.registered_strategies[label] = path
|
34
|
+
end
|
35
|
+
|
36
|
+
# Activates a registered strategy by it's label.
|
37
|
+
# Intended for use with plugin authors. There is little
|
38
|
+
# need to register your own strategies. Just declare them
|
39
|
+
# and they will be active.
|
40
|
+
def self.activate!(label)
|
41
|
+
path = self.registered_strategies[label]
|
42
|
+
raise "The #{label} Strategy is not registered" unless path
|
43
|
+
require path
|
44
|
+
end
|
45
|
+
|
46
|
+
# The Merb::Authentication::Strategy is where all the action happens in the merb-auth framework.
|
47
|
+
# Inherit from this class to setup your own strategy. The strategy will automatically
|
48
|
+
# be placed in the default_strategy_order array, and will be included in the strategy runs.
|
49
|
+
#
|
50
|
+
# The strategy you implment should have a YourStrategy#run! method defined that returns
|
51
|
+
# 1. A user object if authenticated
|
52
|
+
# 2. nil if no authenticated user was found.
|
53
|
+
#
|
54
|
+
# === Example
|
55
|
+
#
|
56
|
+
# class MyStrategy < Merb::Authentication::Strategy
|
57
|
+
# def run!
|
58
|
+
# u = User.get(params[:login])
|
59
|
+
# u if u.authentic?(params[:password])
|
60
|
+
# end
|
61
|
+
# end
|
62
|
+
#
|
63
|
+
#
|
64
|
+
class Strategy
|
65
|
+
attr_accessor :request
|
66
|
+
attr_writer :body
|
67
|
+
|
68
|
+
class << self
|
69
|
+
def inherited(klass)
|
70
|
+
Merb::Authentication.strategies << klass
|
71
|
+
Merb::Authentication.default_strategy_order << klass
|
72
|
+
end
|
73
|
+
|
74
|
+
# Use this to declare the strategy should run before another strategy
|
75
|
+
def before(strategy)
|
76
|
+
order = Merb::Authentication.default_strategy_order
|
77
|
+
order.delete(self)
|
78
|
+
index = order.index(strategy)
|
79
|
+
order.insert(index,self)
|
80
|
+
end
|
81
|
+
|
82
|
+
# Use this to declare the strategy should run after another strategy
|
83
|
+
def after(strategy)
|
84
|
+
order = Merb::Authentication.default_strategy_order
|
85
|
+
order.delete(self)
|
86
|
+
index = order.index(strategy)
|
87
|
+
index == order.size ? order << self : order.insert(index + 1, self)
|
88
|
+
end
|
89
|
+
|
90
|
+
# Mark a strategy as abstract. This means that a strategy will not
|
91
|
+
# ever be run as part of the authentication. Instead this
|
92
|
+
# will be available to inherit from as a way to share code.
|
93
|
+
#
|
94
|
+
# You could for example setup a strategy to check for a particular kind of login
|
95
|
+
# and then have a subclass for each class type of user in your system.
|
96
|
+
# i.e. Customer / Staff, Student / Staff etc
|
97
|
+
def abstract!
|
98
|
+
@abstract = true
|
99
|
+
end
|
100
|
+
|
101
|
+
# Asks is this strategy abstract. i.e. can it be run as part of the authentication
|
102
|
+
def abstract?
|
103
|
+
!!@abstract
|
104
|
+
end
|
105
|
+
|
106
|
+
end # End class << self
|
107
|
+
|
108
|
+
def initialize(request, params)
|
109
|
+
@request = request
|
110
|
+
@params = params
|
111
|
+
end
|
112
|
+
|
113
|
+
# An alias to the request.params hash
|
114
|
+
# Only rely on this hash to find any router params you are looking for.
|
115
|
+
# If looking for paramteres use request.params
|
116
|
+
def params
|
117
|
+
@params
|
118
|
+
end
|
119
|
+
|
120
|
+
# An alials to the request.cookies hash
|
121
|
+
def cookies
|
122
|
+
request.cookies
|
123
|
+
end
|
124
|
+
|
125
|
+
# An alias to the request.session hash
|
126
|
+
def session
|
127
|
+
request.session
|
128
|
+
end
|
129
|
+
|
130
|
+
# Redirects causes the strategy to signal a redirect
|
131
|
+
# to the provided url.
|
132
|
+
#
|
133
|
+
# ====Parameters
|
134
|
+
# url<String>:: The url to redirect to
|
135
|
+
# options<Hash>:: An options hash with the following keys:
|
136
|
+
# +:permanent+ Set this to true to make the redirect permanent
|
137
|
+
# +:status+ Set this to an integer for the status to return
|
138
|
+
def redirect!(url, opts = {})
|
139
|
+
self.headers["Location"] = url
|
140
|
+
self.status = opts[:permanent] ? 301 : 302
|
141
|
+
self.status = opts[:status] if opts[:status]
|
142
|
+
self.body = opts[:message] || "<div>You are being redirected to <a href='#{url}'>#{url}</a></div>"
|
143
|
+
halt!
|
144
|
+
return true
|
145
|
+
end
|
146
|
+
|
147
|
+
# Returns ture if the strategy redirected
|
148
|
+
def redirected?
|
149
|
+
!!headers["Location"]
|
150
|
+
end
|
151
|
+
|
152
|
+
# Provides a place to put the status of the response
|
153
|
+
attr_accessor :status
|
154
|
+
|
155
|
+
# Provides a place to put headers
|
156
|
+
def headers
|
157
|
+
@headers ||={}
|
158
|
+
end
|
159
|
+
|
160
|
+
# Mark this strategy as complete for this request. Will cause that no other
|
161
|
+
# strategies will be executed.
|
162
|
+
def halt!
|
163
|
+
@halt = true
|
164
|
+
end
|
165
|
+
|
166
|
+
# Checks to see if this strategy has been halted
|
167
|
+
def halted?
|
168
|
+
!!@halt
|
169
|
+
end
|
170
|
+
|
171
|
+
|
172
|
+
# Allows you to provide a body of content to return when halting
|
173
|
+
def body
|
174
|
+
@body || ""
|
175
|
+
end
|
176
|
+
|
177
|
+
# This is the method that is called as the test for authentication and is where
|
178
|
+
# you put your code.
|
179
|
+
#
|
180
|
+
# You must overwrite this method in your strategy
|
181
|
+
#
|
182
|
+
# @api overwritable
|
183
|
+
def run!
|
184
|
+
raise NotImplemented
|
185
|
+
end
|
186
|
+
|
187
|
+
# Overwrite this method to scope a strategy to a particular user type
|
188
|
+
# you can use this with inheritance for example to try the same strategy
|
189
|
+
# on different user types
|
190
|
+
#
|
191
|
+
# By default, Merb::Authentication.user_class is used. This method allows for
|
192
|
+
# particular strategies to deal with a different type of user class.
|
193
|
+
#
|
194
|
+
# For example. If Merb::Authentication.user_class is Customer
|
195
|
+
# and you have a PasswordStrategy, you can subclass the PasswordStrategy
|
196
|
+
# and change this method to return Staff. Giving you a PasswordStrategy strategy
|
197
|
+
# for first Customer(s) and then Staff.
|
198
|
+
#
|
199
|
+
# @api overwritable
|
200
|
+
def user_class
|
201
|
+
Merb::Authentication.user_class
|
202
|
+
end
|
203
|
+
|
204
|
+
end # Strategy
|
205
|
+
end # Merb::Authentication
|
206
|
+
end # Merb
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# make sure we're running inside Merb
|
2
|
+
|
3
|
+
require 'extlib'
|
4
|
+
|
5
|
+
require 'merb-auth-core/authentication'
|
6
|
+
require 'merb-auth-core/strategy'
|
7
|
+
require 'merb-auth-core/session_mixin'
|
8
|
+
require 'merb-auth-core/errors'
|
9
|
+
require 'merb-auth-core/responses'
|
10
|
+
require 'merb-auth-core/authenticated_helper'
|
11
|
+
require 'merb-auth-core/customizations'
|
12
|
+
require 'merb-auth-core/bootloader'
|
13
|
+
require 'merb-auth-core/router_helper'
|
14
|
+
|
15
|
+
Merb::BootLoader.before_app_loads do
|
16
|
+
# require code that must be loaded before the application
|
17
|
+
Merb::Controller.send(:include, Merb::AuthenticatedHelper)
|
18
|
+
end
|
19
|
+
|
20
|
+
Merb::BootLoader.after_app_loads do
|
21
|
+
# code that can be required after the application loads
|
22
|
+
end
|
23
|
+
|
24
|
+
Merb::Plugins.add_rakefiles "merb-auth-core/merbtasks"
|
25
|
+
|
26
|
+
Merb.push_path(:lib_authentication, Merb.root / "merb" / "merb-auth")
|
@@ -0,0 +1,111 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), "..", 'spec_helper.rb')
|
2
|
+
|
3
|
+
describe "Merb::AuthenticationHelper" do
|
4
|
+
|
5
|
+
class ControllerMock < Merb::Controller
|
6
|
+
before :ensure_authenticated
|
7
|
+
end
|
8
|
+
|
9
|
+
before(:each) do
|
10
|
+
clear_strategies!
|
11
|
+
@controller = ControllerMock.new(fake_request)
|
12
|
+
@request = @controller.request
|
13
|
+
@session = @controller.session
|
14
|
+
@session.user = "WINNA"
|
15
|
+
Viking.captures.clear
|
16
|
+
|
17
|
+
class Kone < Merb::Authentication::Strategy
|
18
|
+
def run!
|
19
|
+
puts params.inspect
|
20
|
+
Viking.capture(self.class)
|
21
|
+
params[self.class.name]
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class Ktwo < Kone; end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should not raise and Unauthenticated error" do
|
30
|
+
lambda do
|
31
|
+
@controller.send(:ensure_authenticated)
|
32
|
+
end.should_not raise_error(Merb::Controller::Unauthenticated)
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should raise an Unauthenticated error" do
|
36
|
+
@controller = ControllerMock.new(Merb::Request.new({}))
|
37
|
+
lambda do
|
38
|
+
@controller.send(:ensure_authenticated)
|
39
|
+
end.should raise_error(Merb::Controller::Unauthenticated)
|
40
|
+
end
|
41
|
+
|
42
|
+
it "should run the authentication when testing if it is authenticated" do
|
43
|
+
@controller = ControllerMock.new(fake_request)
|
44
|
+
@controller.session.should_receive(:user).and_return(nil, "WINNA")
|
45
|
+
@controller.session.authentication.should_receive(:authenticate!).and_return("WINNA")
|
46
|
+
@controller.send(:ensure_authenticated)
|
47
|
+
end
|
48
|
+
|
49
|
+
it "should accept and execute the provided strategies" do
|
50
|
+
# This allows using a before filter with specific arguments
|
51
|
+
# before :ensure_authenticated, :with => [Authenticaiton::OAuth, Merb::Authentication::BasicAuth]
|
52
|
+
controller = ControllerMock.new(fake_request)
|
53
|
+
controller.request.params["Ktwo"] = true
|
54
|
+
controller.send(:ensure_authenticated, Kone, Ktwo)
|
55
|
+
Viking.captures.should == %w( Kone Ktwo )
|
56
|
+
end
|
57
|
+
|
58
|
+
describe "redirection" do
|
59
|
+
|
60
|
+
before(:all) do
|
61
|
+
class FooController < Merb::Controller
|
62
|
+
before :ensure_authenticated
|
63
|
+
|
64
|
+
def index; "FooController#index" end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
before(:each) do
|
69
|
+
class MyStrategy < Merb::Authentication::Strategy
|
70
|
+
def run!
|
71
|
+
if params[:url]
|
72
|
+
opts = {}
|
73
|
+
opts[:permanent] = true if params[:permanent]
|
74
|
+
opts[:status] = params[:status].to_i if params[:status]
|
75
|
+
!opts.empty? ? redirect!(params[:url], opts) : redirect!(params[:url])
|
76
|
+
elsif params[:fail]
|
77
|
+
nil
|
78
|
+
else
|
79
|
+
"WINNA"
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end # MyStrategy
|
83
|
+
|
84
|
+
Merb::Router.reset!
|
85
|
+
Merb::Router.prepare{ match("/").to(:controller => "foo_controller")}
|
86
|
+
end
|
87
|
+
|
88
|
+
it "should redirect the controller to a Location if the strategy redirects" do
|
89
|
+
controller = get "/", :url => "/some/url"
|
90
|
+
controller.headers["Location"].should == "/some/url"
|
91
|
+
end
|
92
|
+
|
93
|
+
it "should use a 302 redirection by default" do
|
94
|
+
c = get "/", :url => "/some/url"
|
95
|
+
c.status.should == 302
|
96
|
+
end
|
97
|
+
|
98
|
+
it "should use a 301 when marked as permanent" do
|
99
|
+
c = get "/", :url => "/some/url", :permanent => "true"
|
100
|
+
c.status.should == 301
|
101
|
+
end
|
102
|
+
|
103
|
+
it "should use a custom status" do
|
104
|
+
c = get "/", :url => "/some/url", :status => 401
|
105
|
+
c.status.should == 401
|
106
|
+
end
|
107
|
+
|
108
|
+
|
109
|
+
end
|
110
|
+
|
111
|
+
end
|