shield 0.1.0 → 1.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
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