shield 0.1.0 → 1.0.0.rc1

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 ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2009 Michel Martens, Damian Janowski and Cyril David
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,230 @@
1
+ # Shield
2
+
3
+ Shield
4
+
5
+ _n. A solid piece of metal code used to protect your application._
6
+
7
+ ## Why another authentication library?
8
+
9
+ 1. Because most of the other libraries are too huge.
10
+ 2. Extending other libraries is a pain.
11
+ 3. Writing code is fun :-)
12
+
13
+ ## What shield is
14
+
15
+ 1. Simple (~ 110 lines of Ruby code)
16
+ 2. Doesn't get in the way
17
+ 3. Treats you like a grown up
18
+
19
+ ## What shield is not
20
+
21
+ - is _not_ a ready-made end-to-end authentication solution.
22
+ - is _not_ biased towards any kind of ORM.
23
+
24
+ ## Understanding Shield in 15 minutes
25
+
26
+ ### Shield::Model
27
+
28
+ `Shield::Model` is a very basic protocol for doing authentication
29
+ against your model. It doesn't assume a lot, apart from the following:
30
+
31
+ 1. You will implement `User.fetch` which receives the login string.
32
+ 2. You have an attribute `crypted_password` which is able to store
33
+ up to __192__ characters.
34
+
35
+ And that's it.
36
+
37
+ In order to implement the model protocol, you start by
38
+ including `Shield::Model`.
39
+
40
+ ```ruby
41
+ class User < Struct.new(:email, :crypted_password)
42
+ include Shield::Model
43
+
44
+ def self.fetch(email)
45
+ user = new(email)
46
+ user.password = "pass1234"
47
+
48
+ return user
49
+ end
50
+ end
51
+ ```
52
+
53
+ By including `Shield::Model`, you get all the general methods needed
54
+ in order to do authentication.
55
+
56
+ 1. You get `User.authenticate` which receives the login string and
57
+ password as the two parameters.
58
+ 2. You get `User#password=` which automatically converts the clear text
59
+ password into a hashed form and assigns it into `#crypted_password`.
60
+
61
+ ```ruby
62
+ u = User.new("foo@bar.com")
63
+
64
+ # A password accessor has been added which manages `crypted_password`.
65
+ u.password = "pass1234"
66
+
67
+ Shield::Password.check("pass1234", u.crypted_password)
68
+ # => true
69
+
70
+ # Since we've hard coded all passwords to pass1234
71
+ # we're able to authenticate properly.
72
+ nil == User.authenticate("foo@bar.com", "pass1234")
73
+ # => false
74
+
75
+ # If we try a different password on the other hand,
76
+ # we get `nil`.
77
+ nil == User.authenicate("foo@bar.com", "wrong")
78
+ # => true
79
+ ```
80
+
81
+ Shield includes tests for [ohm][ohm] and [sequel][sequel] and makes sure
82
+ that each release works with the latest respective versions.
83
+
84
+ Take a look at [test/ohm.rb][ohm-test] and [test/sequel.rb][sequel-test]
85
+ to learn more.
86
+
87
+ ### Logging in with an email and username?
88
+
89
+ If your requirements dictate that you need to be able to support logging
90
+ in using either username or email, then you can simply extend `User.fetch`
91
+ a bit by doing:
92
+
93
+ ```ruby
94
+ # in Sequel
95
+ class User < Sequel::Model
96
+ def self.fetch(identifier)
97
+ filter(email: identifier).first || filter(username: identifier).first
98
+ end
99
+ end
100
+
101
+ # in Ohm
102
+ class User < Ohm::Model
103
+ attribute :email
104
+ attribute :username
105
+
106
+ unique :email
107
+ unique :username
108
+
109
+ def self.fetch(identifier)
110
+ with(:email, identifier) || with(:username, identifier)
111
+ end
112
+ end
113
+ ```
114
+
115
+ If you want to allow case-insensitive logins for some reason, you can
116
+ simply normalize the values to their lowercase form.
117
+
118
+ [ohm]: http://ohm.keyvalue.org
119
+ [sequel]: http://sequel.rubyforge.org
120
+
121
+ [ohm-test]: https://github.com/cyx/shield/blob/master/test/ohm.rb
122
+ [sequel-test]: https://github.com/cyx/shield/blob/master/test/sequel.rb
123
+
124
+ ### Shield::Helpers
125
+
126
+ As the name suggests, `Shield::Helpers` is out there to aid you a bit,
127
+ but this time it aids you in the context of your Rack application.
128
+
129
+ `Shield::Helpers` assumes only the following:
130
+
131
+ 1. You have included in your application a Session handler,
132
+ (e.g. Rack::Session::Cookie)
133
+ 2. You have an `env` method which returns the environment hash as
134
+ was passed in Rack.
135
+
136
+ **Note:** As of this writing, Sinatra, Cuba & Rails adhere to having an `env`
137
+ method in the handler / controller context. Shield also ships with tests for
138
+ both Cuba and Sinatra.
139
+
140
+ ```ruby
141
+ require "sinatra"
142
+
143
+ # Satisifies assumption number 1 above.
144
+ use Rack::Session::Cookie
145
+
146
+ # Mixes `Shield::Helpers` into your routes context.
147
+ helpers Shield::Helpers
148
+
149
+ get "/private" do
150
+ error(401) unless authenticated(User)
151
+
152
+ "Private"
153
+ end
154
+
155
+ get "/login" do
156
+ erb :login
157
+ end
158
+
159
+ post "/login" do
160
+ if login(User, params[:login], params[:password], params[:remember_me])
161
+ redirect(params[:return] || "/")
162
+ else
163
+ redirect "/login"
164
+ end
165
+ end
166
+
167
+ get "/logout" do
168
+ logout(User)
169
+ redirect "/"
170
+ end
171
+
172
+ __END__
173
+
174
+ @@ login
175
+ <h1>Login</h1>
176
+
177
+ <form action='/login' method='post'>
178
+ <input type='text' name='login' placeholder='Email'>
179
+ <input type='password' name='password' placeholder='Password'>
180
+ <input type='submit' name='proceed' value='Login'>
181
+ ```
182
+
183
+ **Note for the reader**: The redirect to `params[:return]` in the example
184
+ is vulnerable to URL hijacking. You can whitelist redirectable urls, or
185
+ simply make sure the URL matches the pattern `/\A[\/a-z0-9\-]+\z/i`.
186
+
187
+ ### Shield::Middleware
188
+
189
+ If you have a keen eye you might have noticed that instead of redirecting
190
+ away to the login URL in the example above, we instead chose to do a
191
+ `401 Unauthorized`. In strict HTTP Status code terms, this is the proper
192
+ approach. The redirection is simply the user experience pattern that has
193
+ emerged in web applications.
194
+
195
+ But don't despair! If you want to do redirects simply add
196
+ `Shield::Middleware` to your middleware stack like so:
197
+
198
+ ```ruby
199
+ # taken from example above
200
+ use Shield::Middleware, "/login"
201
+ use Rack::Session::Cookie
202
+
203
+ # rest of code follows here
204
+ # ...
205
+ ```
206
+
207
+ Now when your application responds with a `401`, `Shield::Middleware`
208
+ will be responsible for doing the redirect to `/login`.
209
+
210
+ If you try and do a `curl --head http://localhost:4567/private` with
211
+ `Shield::Middleware`, you'll get a response similar to the following:
212
+
213
+ ```
214
+ HTTP/1.1 302 Found
215
+ Location: http://localhost:4567/login?return=%2Fprivate
216
+ Content-Type: text/html
217
+ ```
218
+
219
+ Notice that it specifies `/private` as the return URL.
220
+
221
+ ## Jump starting your way.
222
+
223
+ For people interested in using Cuba, Ohm, Shield and Bootstrap we've
224
+ created a starting point that includes **Login**, **Signup** and
225
+ **Forgot Password** functionality.
226
+
227
+ Head on over to the [cuba-app][cuba-app] repository if you want
228
+ to know more.
229
+
230
+ [cuba-app]: http://github.com/citrusbyte/cuba-app
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ desc "Run all tests using cutest."
2
+ task :test do
3
+ system("cutest -r ./test/helper ./test/*.rb")
4
+ end
5
+
6
+ task :default => :test
data/lib/shield.rb CHANGED
@@ -1,5 +1,130 @@
1
+ require "pbkdf2"
2
+ require "uri"
3
+
1
4
  module Shield
2
- autoload :Password, "shield/password"
3
- autoload :Helpers, "shield/helpers"
4
- autoload :Model, "shield/model"
5
- end
5
+ class Middleware
6
+ attr :url
7
+
8
+ def initialize(app, url = "/login")
9
+ @app = app
10
+ @url = url
11
+ end
12
+
13
+ def call(env)
14
+ tuple = @app.call(env)
15
+
16
+ if tuple[0] == 401
17
+ [302, headers(env["PATH_INFO"]), []]
18
+ else
19
+ tuple
20
+ end
21
+ end
22
+
23
+ private
24
+ def headers(path)
25
+ { "Location" => "%s?return=%s" % [url, encode(path)],
26
+ "Content-Type" => "text/html",
27
+ "Content-Length" => "0"
28
+ }
29
+ end
30
+
31
+ def encode(str)
32
+ URI.encode_www_form_component(str)
33
+ end
34
+ end
35
+
36
+ module Helpers
37
+ def persist_session!
38
+ if session[:remember_for]
39
+ env["rack.session.options"][:expire_after] = session[:remember_for]
40
+ end
41
+ end
42
+
43
+ def authenticated(model)
44
+ @_shield ||= {}
45
+ @_shield[model] ||= session[model.to_s] && model[session[model.to_s]]
46
+ end
47
+
48
+ def authenticate(user)
49
+ session[user.class.to_s] = user.id
50
+ end
51
+
52
+ def login(model, username, password, remember = false, expire = 1209600)
53
+ return unless user = model.authenticate(username, password)
54
+
55
+ session[:remember_for] = expire if remember
56
+ authenticate(user)
57
+ end
58
+
59
+ def logout(model)
60
+ session.delete(model.to_s)
61
+ session.delete(:remember_for)
62
+
63
+ @_shield.delete(model) if defined?(@_shield)
64
+ end
65
+ end
66
+
67
+ module Model
68
+ def self.included(model)
69
+ model.extend(ClassMethods)
70
+ end
71
+
72
+ class FetchMissing < StandardError; end
73
+
74
+ module ClassMethods
75
+ def authenticate(username, password)
76
+ user = fetch(username)
77
+
78
+ if user and is_valid_password?(user, password)
79
+ return user
80
+ end
81
+ end
82
+
83
+ def fetch(login)
84
+ raise FetchMissing, "#{self}.fetch not implemented"
85
+ end
86
+
87
+ def is_valid_password?(user, password)
88
+ Shield::Password.check(password, user.crypted_password)
89
+ end
90
+ end
91
+
92
+ def password=(password)
93
+ self.crypted_password = Shield::Password.encrypt(password.to_s)
94
+ end
95
+ end
96
+
97
+ module Password
98
+ def self.iterations
99
+ @iterations ||= 5000
100
+ end
101
+
102
+ def self.iterations=(iterations)
103
+ @iterations = iterations
104
+ end
105
+
106
+ def self.encrypt(password, salt = generate_salt)
107
+ digest(password, salt) + salt
108
+ end
109
+
110
+ def self.check(password, encrypted)
111
+ sha512, salt = encrypted.to_s[0..127], encrypted.to_s[128..-1]
112
+
113
+ digest(password, salt) == sha512
114
+ end
115
+
116
+ protected
117
+ def self.digest(password, salt)
118
+ PBKDF2.new do |p|
119
+ p.password = password
120
+ p.salt = salt
121
+ p.iterations = iterations
122
+ p.hash_function = :sha512
123
+ end.hex_string
124
+ end
125
+
126
+ def self.generate_salt
127
+ Digest::SHA512.hexdigest(Time.now.to_f.to_s)[0, 64]
128
+ end
129
+ end
130
+ end
data/test/cuba.rb CHANGED
@@ -2,6 +2,7 @@ require File.expand_path("helper", File.dirname(__FILE__))
2
2
  require File.expand_path("user", File.dirname(__FILE__))
3
3
 
4
4
  Cuba.use Rack::Session::Cookie
5
+ Cuba.use Shield::Middleware
5
6
  Cuba.plugin Shield::Helpers
6
7
 
7
8
  Cuba.define do
@@ -10,9 +11,11 @@ Cuba.define do
10
11
  end
11
12
 
12
13
  on get, "private" do
13
- ensure_authenticated(User)
14
-
15
- res.write "Private"
14
+ if authenticated(User)
15
+ res.write "Private"
16
+ else
17
+ res.status = 401
18
+ end
16
19
  end
17
20
 
18
21
  on get, "login" do
@@ -21,7 +24,7 @@ Cuba.define do
21
24
 
22
25
  on post, "login", param("login"), param("password") do |u, p|
23
26
  if login(User, u, p, req[:remember_me])
24
- res.redirect(session[:return_to] || "/")
27
+ res.redirect(req[:return] || "/")
25
28
  else
26
29
  res.redirect "/login"
27
30
  end
@@ -50,10 +53,10 @@ scope do
50
53
  test "successful logging in" do
51
54
  get "/private"
52
55
 
53
- assert_redirected_to "/login"
54
- assert "/private" == session[:return_to]
56
+ assert_equal "/login?return=%2Fprivate", redirection_url
57
+
58
+ post "/login", login: "quentin", password: "password", return: "/private"
55
59
 
56
- post "/login", :login => "quentin", :password => "password"
57
60
  assert_redirected_to "/private"
58
61
 
59
62
  assert 1001 == session["User"]
@@ -72,7 +75,6 @@ scope do
72
75
  get "/logout"
73
76
 
74
77
  assert nil == session["User"]
75
- assert nil == session[:return_to]
76
78
  end
77
79
 
78
80
  test "remember functionality" do
data/test/helper.rb CHANGED
@@ -16,6 +16,10 @@ class Cutest::Scope
16
16
  assert_equal path, URI(last_response.headers["Location"]).path
17
17
  end
18
18
 
19
+ def redirection_url
20
+ last_response.headers["Location"]
21
+ end
22
+
19
23
  def session
20
24
  last_request.env["rack.session"]
21
25
  end
@@ -0,0 +1,29 @@
1
+ require File.expand_path("helper", File.dirname(__FILE__))
2
+ require File.expand_path("user", File.dirname(__FILE__))
3
+
4
+ Cuba.use Rack::Session::Cookie
5
+ Cuba.use Shield::Middleware
6
+
7
+ Cuba.plugin Shield::Helpers
8
+
9
+ Cuba.define do
10
+ on "secured" do
11
+ if not authenticated(User)
12
+ halt [401, { "Content-Type" => "text/html" }, []]
13
+ end
14
+
15
+ res.write "You're in"
16
+ end
17
+
18
+ on "foo" do
19
+ puts env.inspect
20
+ end
21
+ end
22
+
23
+ test do
24
+ env = { "PATH_INFO" => "/secured", "SCRIPT_NAME" => "" }
25
+ status, headers, body = Cuba.call(env)
26
+
27
+ assert_equal 302, status
28
+ assert_equal "/login?return=%2Fsecured", headers["Location"]
29
+ end
data/test/model.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  require File.expand_path("helper", File.dirname(__FILE__))
2
2
 
3
3
  class User < Struct.new(:crypted_password)
4
- extend Shield::Model
4
+ include Shield::Model
5
5
  end
6
6
 
7
7
  test "fetch" do
@@ -9,12 +9,11 @@ test "fetch" do
9
9
 
10
10
  begin
11
11
  User.fetch("quentin")
12
- rescue Exception => e
13
- ex = e
12
+ rescue Exception => ex
14
13
  end
15
14
 
16
15
  assert ex.kind_of?(Shield::Model::FetchMissing)
17
- assert Shield::Model::FetchMissing.new.message == ex.message
16
+ assert "User.fetch not implemented" == ex.message
18
17
  end
19
18
 
20
19
  test "is_valid_password?" do
@@ -43,3 +42,10 @@ test "authenticate" do
43
42
  assert nil == User.authenticate("unknown", "pass")
44
43
  assert nil == User.authenticate("quentin", "wrongpass")
45
44
  end
45
+
46
+ test "#password=" do
47
+ u = User.new
48
+ u.password = "pass1234"
49
+
50
+ assert Shield::Password.check("pass1234", u.crypted_password)
51
+ end
data/test/ohm.rb ADDED
@@ -0,0 +1,30 @@
1
+ require_relative "helper"
2
+ require "ohm"
3
+
4
+ class User < Ohm::Model
5
+ include Shield::Model
6
+
7
+ attribute :email
8
+ attribute :crypted_password
9
+ index :email
10
+
11
+ def self.fetch(email)
12
+ find(email: email).first
13
+ end
14
+ end
15
+
16
+ prepare do
17
+ Ohm.flush
18
+ end
19
+
20
+ setup do
21
+ User.create(email: "foo@bar.com", password: "pass1234")
22
+ end
23
+
24
+ test "fetch" do |user|
25
+ assert_equal user, User.fetch("foo@bar.com")
26
+ end
27
+
28
+ test "authenticate" do |user|
29
+ assert_equal user, User.authenticate("foo@bar.com", "pass1234")
30
+ end
data/test/password.rb CHANGED
@@ -1,6 +1,5 @@
1
1
  require File.expand_path("helper", File.dirname(__FILE__))
2
2
 
3
- # Shield::Password::Simple
4
3
  scope do
5
4
  test "encrypt" do
6
5
  encrypted = Shield::Password.encrypt("password")
@@ -11,33 +10,4 @@ scope do
11
10
  encrypted = Shield::Password.encrypt("password", "A" * 64)
12
11
  assert Shield::Password.check("password", encrypted)
13
12
  end
14
-
15
- test "nil password doesn't raise" do
16
- ex = nil
17
-
18
- begin
19
- encrypted = Shield::Password.encrypt(nil)
20
- rescue Exception => e
21
- ex = e
22
- end
23
-
24
- assert nil == ex
25
- end
26
13
  end
27
-
28
- # Shield::Password::PBKDF2
29
- scope do
30
- setup do
31
- Shield::Password.strategy = Shield::Password::PBKDF2
32
- end
33
-
34
- test "encrypt" do
35
- encrypted = Shield::Password.encrypt("password")
36
- assert Shield::Password.check("password", encrypted)
37
- end
38
-
39
- test "with custom 64 character salt" do
40
- encrypted = Shield::Password.encrypt("password", "A" * 64)
41
- assert Shield::Password.check("password", encrypted)
42
- end
43
- end
data/test/sequel.rb ADDED
@@ -0,0 +1,36 @@
1
+ require_relative "helper"
2
+ require "sequel"
3
+
4
+ DB = Sequel.sqlite
5
+
6
+ DB.run(%(
7
+ CREATE TABLE users (
8
+ id INTEGER PRIMARY KEY,
9
+ email VARCHAR(255) UNIQUE,
10
+ crypted_password VARCHAR(255)
11
+ )
12
+ ))
13
+
14
+ class User < Sequel::Model
15
+ include Shield::Model
16
+
17
+ def self.fetch(email)
18
+ filter(email: email).first
19
+ end
20
+ end
21
+
22
+ prepare do
23
+ User.truncate
24
+ end
25
+
26
+ setup do
27
+ User.create(email: "foo@bar.com", password: "pass1234")
28
+ end
29
+
30
+ test "fetch" do |user|
31
+ assert_equal user, User.fetch("foo@bar.com")
32
+ end
33
+
34
+ test "authenticate" do |user|
35
+ assert_equal user, User.authenticate("foo@bar.com", "pass1234")
36
+ end
data/test/shield.rb CHANGED
@@ -44,19 +44,6 @@ setup do
44
44
  Context.new("/events/1")
45
45
  end
46
46
 
47
- test "ensure_authenticated when logged out" do |context|
48
- context.ensure_authenticated(User)
49
- assert "/events/1" == context.session[:return_to]
50
- assert "/login" == context.redirect
51
- end
52
-
53
- test "ensure_authenticated when logged in" do |context|
54
- context.session["User"] = 1
55
- assert true == context.ensure_authenticated(User)
56
- assert nil == context.redirect
57
- assert nil == context.session[:return_to]
58
- end
59
-
60
47
  class Admin < Struct.new(:id)
61
48
  def self.[](id)
62
49
  new(id) unless id.to_s.empty?
@@ -70,11 +57,11 @@ test "authenticated" do |context|
70
57
  assert nil == context.authenticated(Admin)
71
58
  end
72
59
 
73
- test "caches authenticated in @_authenticated" do |context|
60
+ test "caches authenticated in @_shield" do |context|
74
61
  context.session["User"] = 1
75
62
  context.authenticated(User)
76
63
 
77
- assert User.new(1) == context.instance_variable_get(:@_authenticated)[User]
64
+ assert User.new(1) == context.instance_variable_get(:@_shield)[User]
78
65
  end
79
66
 
80
67
  test "login success" do |context|
@@ -83,13 +70,12 @@ test "login success" do |context|
83
70
  end
84
71
 
85
72
  test "login failure" do |context|
86
- assert false == context.login(User, "wrong", "creds")
73
+ assert ! context.login(User, "wrong", "creds")
87
74
  assert nil == context.session["User"]
88
75
  end
89
76
 
90
77
  test "logout" do |context|
91
78
  context.session["User"] = 1001
92
- context.session[:return_to] = "/foo"
93
79
 
94
80
  # Now let's make it memoize the User
95
81
  context.authenticated(User)
@@ -97,7 +83,6 @@ test "logout" do |context|
97
83
  context.logout(User)
98
84
 
99
85
  assert nil == context.session["User"]
100
- assert nil == context.session[:return_to]
101
86
  assert nil == context.authenticated(User)
102
87
  end
103
88
 
@@ -105,4 +90,4 @@ test "authenticate" do |context|
105
90
  context.authenticate(User[1001])
106
91
 
107
92
  assert User[1] == context.authenticated(User)
108
- end
93
+ end
data/test/sinatra.rb CHANGED
@@ -2,6 +2,7 @@ require File.expand_path("helper", File.dirname(__FILE__))
2
2
  require File.expand_path("user", File.dirname(__FILE__))
3
3
 
4
4
  class SinatraApp < Sinatra::Base
5
+ use Shield::Middleware
5
6
  enable :sessions
6
7
  helpers Shield::Helpers
7
8
 
@@ -10,7 +11,7 @@ class SinatraApp < Sinatra::Base
10
11
  end
11
12
 
12
13
  get "/private" do
13
- ensure_authenticated(User)
14
+ error(401) unless authenticated(User)
14
15
 
15
16
  "Private"
16
17
  end
@@ -21,7 +22,7 @@ class SinatraApp < Sinatra::Base
21
22
 
22
23
  post "/login" do
23
24
  if login(User, params[:login], params[:password], params[:remember_me])
24
- redirect(session[:return_to] || "/")
25
+ redirect(params[:return] || "/")
25
26
  else
26
27
  redirect "/login"
27
28
  end
@@ -50,10 +51,11 @@ scope do
50
51
  test "successful logging in" do
51
52
  get "/private"
52
53
 
53
- assert_redirected_to "/login"
54
- assert_equal "/private", session[:return_to]
54
+ assert_equal "/login?return=%2Fprivate", redirection_url
55
+
56
+ post "/login", :login => "quentin", :password => "password",
57
+ :return => "/private"
55
58
 
56
- post "/login", :login => "quentin", :password => "password"
57
59
  assert_redirected_to "/private"
58
60
 
59
61
  assert 1001 == session["User"]
@@ -72,7 +74,6 @@ scope do
72
74
  get "/logout"
73
75
 
74
76
  assert nil == session["User"]
75
- assert nil == session[:return_to]
76
77
  end
77
78
 
78
79
  test "remember functionality" do
@@ -85,3 +86,7 @@ scope do
85
86
  assert_equal nil, session[:remember_for]
86
87
  end
87
88
  end
89
+
90
+ if $0 == __FILE__
91
+ SinatraApp.run!
92
+ end
data/test/user.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  class User
2
- extend Shield::Model
2
+ include Shield::Model
3
3
 
4
4
  def self.[](id)
5
5
  User.new(1001) unless id.to_s.empty?
metadata CHANGED
@@ -1,8 +1,8 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: shield
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
5
- prerelease:
4
+ version: 1.0.0.rc1
5
+ prerelease: 6
6
6
  platform: ruby
7
7
  authors:
8
8
  - Michel Martens
@@ -11,11 +11,22 @@ authors:
11
11
  autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2012-03-28 00:00:00.000000000 Z
14
+ date: 2012-03-30 00:00:00.000000000 Z
15
15
  dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: pbkdf2
18
+ requirement: &70325933980460 !ruby/object:Gem::Requirement
19
+ none: false
20
+ requirements:
21
+ - - ! '>='
22
+ - !ruby/object:Gem::Version
23
+ version: '0'
24
+ type: :runtime
25
+ prerelease: false
26
+ version_requirements: *70325933980460
16
27
  - !ruby/object:Gem::Dependency
17
28
  name: cutest
18
- requirement: &70247759932460 !ruby/object:Gem::Requirement
29
+ requirement: &70325933977700 !ruby/object:Gem::Requirement
19
30
  none: false
20
31
  requirements:
21
32
  - - ! '>='
@@ -23,10 +34,10 @@ dependencies:
23
34
  version: '0'
24
35
  type: :development
25
36
  prerelease: false
26
- version_requirements: *70247759932460
37
+ version_requirements: *70325933977700
27
38
  - !ruby/object:Gem::Dependency
28
39
  name: cuba
29
- requirement: &70247759931940 !ruby/object:Gem::Requirement
40
+ requirement: &70325933976980 !ruby/object:Gem::Requirement
30
41
  none: false
31
42
  requirements:
32
43
  - - ! '>='
@@ -34,10 +45,10 @@ dependencies:
34
45
  version: '0'
35
46
  type: :development
36
47
  prerelease: false
37
- version_requirements: *70247759931940
48
+ version_requirements: *70325933976980
38
49
  - !ruby/object:Gem::Dependency
39
50
  name: sinatra
40
- requirement: &70247759931460 !ruby/object:Gem::Requirement
51
+ requirement: &70325933976340 !ruby/object:Gem::Requirement
41
52
  none: false
42
53
  requirements:
43
54
  - - ! '>='
@@ -45,10 +56,10 @@ dependencies:
45
56
  version: '0'
46
57
  type: :development
47
58
  prerelease: false
48
- version_requirements: *70247759931460
59
+ version_requirements: *70325933976340
49
60
  - !ruby/object:Gem::Dependency
50
61
  name: rack-test
51
- requirement: &70247759930920 !ruby/object:Gem::Requirement
62
+ requirement: &70325933975460 !ruby/object:Gem::Requirement
52
63
  none: false
53
64
  requirements:
54
65
  - - ! '>='
@@ -56,7 +67,29 @@ dependencies:
56
67
  version: '0'
57
68
  type: :development
58
69
  prerelease: false
59
- version_requirements: *70247759930920
70
+ version_requirements: *70325933975460
71
+ - !ruby/object:Gem::Dependency
72
+ name: sequel
73
+ requirement: &70325933974800 !ruby/object:Gem::Requirement
74
+ none: false
75
+ requirements:
76
+ - - ! '>='
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ type: :development
80
+ prerelease: false
81
+ version_requirements: *70325933974800
82
+ - !ruby/object:Gem::Dependency
83
+ name: ohm
84
+ requirement: &70325933973720 !ruby/object:Gem::Requirement
85
+ none: false
86
+ requirements:
87
+ - - ~>
88
+ - !ruby/object:Gem::Version
89
+ version: '0.1'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: *70325933973720
60
93
  description: ! "\n Provides all the protocol you need in order to do authentication
61
94
  on\n your rack application. The implementation specifics can be found in\n http://github.com/cyx/shield-contrib\n
62
95
  \ "
@@ -68,16 +101,17 @@ executables: []
68
101
  extensions: []
69
102
  extra_rdoc_files: []
70
103
  files:
71
- - lib/shield/helpers.rb
72
- - lib/shield/model.rb
73
- - lib/shield/password/pbkdf2.rb
74
- - lib/shield/password/simple.rb
75
- - lib/shield/password.rb
104
+ - README.md
105
+ - LICENSE
106
+ - Rakefile
76
107
  - lib/shield.rb
77
108
  - test/cuba.rb
78
109
  - test/helper.rb
110
+ - test/middleware.rb
79
111
  - test/model.rb
112
+ - test/ohm.rb
80
113
  - test/password.rb
114
+ - test/sequel.rb
81
115
  - test/shield.rb
82
116
  - test/sinatra.rb
83
117
  - test/user.rb
@@ -96,11 +130,11 @@ required_ruby_version: !ruby/object:Gem::Requirement
96
130
  required_rubygems_version: !ruby/object:Gem::Requirement
97
131
  none: false
98
132
  requirements:
99
- - - ! '>='
133
+ - - ! '>'
100
134
  - !ruby/object:Gem::Version
101
- version: '0'
135
+ version: 1.3.1
102
136
  requirements: []
103
- rubyforge_project: shield
137
+ rubyforge_project:
104
138
  rubygems_version: 1.8.11
105
139
  signing_key:
106
140
  specification_version: 3
@@ -1,67 +0,0 @@
1
- module Shield
2
- module Helpers
3
- class NoSessionError < StandardError; end
4
-
5
- def session
6
- env["rack.session"] || raise(NoSessionError)
7
- end
8
-
9
- def redirect(path, status = 302)
10
- if defined?(super)
11
- # If the application context has defined a proper redirect
12
- # we can simply use that definition.
13
- super
14
- else
15
- # We implement the Cuba redirect here, being Cuba users we
16
- # are biased towards it of course.
17
- halt [status, { "Location" => path, "Content-Type" => "text/html" }, []]
18
- end
19
- end
20
-
21
- def ensure_authenticated(model, login_url = "/login")
22
- if authenticated(model)
23
- return true
24
- else
25
- # If you've ever used request.path, it just so happens
26
- # to be SCRIPT_NAME + PATH_INFO.
27
- session[:return_to] = env["SCRIPT_NAME"] + env["PATH_INFO"]
28
- redirect login_url
29
- return false
30
- end
31
- end
32
-
33
- def authenticated(model)
34
- @_authenticated ||= {}
35
- @_authenticated[model] ||= session[model.to_s] && model[session[model.to_s]]
36
- end
37
-
38
- def persist_session!
39
- if session[:remember_for]
40
- env["rack.session.options"][:expire_after] = session[:remember_for]
41
- end
42
- end
43
-
44
- def login(model, username, password, remember = false, expire = 1209600)
45
- instance = model.authenticate(username, password)
46
-
47
- if instance
48
- session[:remember_for] = expire if remember
49
- session[model.to_s] = instance.id
50
- else
51
- return false
52
- end
53
- end
54
-
55
- def logout(model)
56
- session.delete(model.to_s)
57
- session.delete(:return_to)
58
- session.delete(:remember_for)
59
-
60
- @_authenticated.delete(model) if defined?(@_authenticated)
61
- end
62
-
63
- def authenticate(user)
64
- session[user.class.to_s] = user.id
65
- end
66
- end
67
- end
data/lib/shield/model.rb DELETED
@@ -1,35 +0,0 @@
1
- module Shield
2
- module Model
3
- def authenticate(username, password)
4
- user = fetch(username)
5
-
6
- if user and is_valid_password?(user, password)
7
- return user
8
- end
9
- end
10
-
11
- def fetch(login)
12
- raise FetchMissing
13
- end
14
-
15
- def is_valid_password?(user, password)
16
- Shield::Password.check(password, user.crypted_password)
17
- end
18
-
19
- class FetchMissing < Class.new(StandardError)
20
- def message
21
- %{
22
- !! You need to implement `fetch`.
23
- Below is a quick example implementation (in Ohm):
24
-
25
- def fetch(email)
26
- find(:email => email).first
27
- end
28
-
29
- For more example implementations, check out
30
- http://github.com/cyx/shield-contrib
31
- }.gsub(/^ {10}/, "")
32
- end
33
- end
34
- end
35
- end
@@ -1,28 +0,0 @@
1
- require "digest/sha2"
2
-
3
- module Shield
4
- module Password
5
- autoload :Simple, "shield/password/simple"
6
- autoload :PBKDF2, "shield/password/pbkdf2"
7
-
8
- def self.strategy=(s)
9
- @strategy = s
10
- end
11
-
12
- def self.strategy
13
- @strategy ||= Shield::Password::Simple
14
- end
15
-
16
- def self.encrypt(password, salt = generate_salt)
17
- strategy.encrypt(password, salt)
18
- end
19
-
20
- def self.check(password, encrypted)
21
- strategy.check(password, encrypted)
22
- end
23
-
24
- def self.generate_salt
25
- Digest::SHA512.hexdigest(Time.now.to_f.to_s)[0, 64]
26
- end
27
- end
28
- end
@@ -1,23 +0,0 @@
1
- require "pbkdf2"
2
-
3
- module Shield
4
- module Password
5
- module PBKDF2
6
- extend Shield::Password::Simple
7
-
8
- def self.digest(password, salt)
9
- ::PBKDF2.new do |p|
10
- p.password = password
11
- p.salt = salt
12
- p.iterations = iterations
13
- p.hash_function = :sha512
14
- end.hex_string
15
- end
16
-
17
- class << self
18
- attr_accessor :iterations
19
- end
20
- @iterations = 5000
21
- end
22
- end
23
- end
@@ -1,22 +0,0 @@
1
- module Shield
2
- module Password
3
- module Simple
4
- extend self
5
-
6
- def encrypt(password, salt)
7
- digest(password, salt) + salt
8
- end
9
-
10
- def check(password, encrypted)
11
- sha512, salt = encrypted.to_s[0..127], encrypted.to_s[128..-1]
12
-
13
- digest(password, salt) == sha512
14
- end
15
-
16
- private
17
- def digest(password, salt)
18
- Digest::SHA512.hexdigest("#{ password }#{ salt }")
19
- end
20
- end
21
- end
22
- end