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 +19 -0
- data/README.md +230 -0
- data/Rakefile +6 -0
- data/lib/shield.rb +129 -4
- data/test/cuba.rb +10 -8
- data/test/helper.rb +4 -0
- data/test/middleware.rb +29 -0
- data/test/model.rb +10 -4
- data/test/ohm.rb +30 -0
- data/test/password.rb +0 -30
- data/test/sequel.rb +36 -0
- data/test/shield.rb +4 -19
- data/test/sinatra.rb +11 -6
- data/test/user.rb +1 -1
- metadata +53 -19
- data/lib/shield/helpers.rb +0 -67
- data/lib/shield/model.rb +0 -35
- data/lib/shield/password.rb +0 -28
- data/lib/shield/password/pbkdf2.rb +0 -23
- data/lib/shield/password/simple.rb +0 -22
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
data/lib/shield.rb
CHANGED
@@ -1,5 +1,130 @@
|
|
1
|
+
require "pbkdf2"
|
2
|
+
require "uri"
|
3
|
+
|
1
4
|
module Shield
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
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
|
-
|
14
|
-
|
15
|
-
|
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(
|
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
|
-
|
54
|
-
|
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
data/test/middleware.rb
ADDED
@@ -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
|
-
|
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 =>
|
13
|
-
ex = e
|
12
|
+
rescue Exception => ex
|
14
13
|
end
|
15
14
|
|
16
15
|
assert ex.kind_of?(Shield::Model::FetchMissing)
|
17
|
-
assert
|
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 @
|
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(:@
|
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
|
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
|
-
|
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(
|
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
|
-
|
54
|
-
|
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
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.
|
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-
|
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: &
|
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: *
|
37
|
+
version_requirements: *70325933977700
|
27
38
|
- !ruby/object:Gem::Dependency
|
28
39
|
name: cuba
|
29
|
-
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: *
|
48
|
+
version_requirements: *70325933976980
|
38
49
|
- !ruby/object:Gem::Dependency
|
39
50
|
name: sinatra
|
40
|
-
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: *
|
59
|
+
version_requirements: *70325933976340
|
49
60
|
- !ruby/object:Gem::Dependency
|
50
61
|
name: rack-test
|
51
|
-
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: *
|
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
|
-
-
|
72
|
-
-
|
73
|
-
-
|
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:
|
135
|
+
version: 1.3.1
|
102
136
|
requirements: []
|
103
|
-
rubyforge_project:
|
137
|
+
rubyforge_project:
|
104
138
|
rubygems_version: 1.8.11
|
105
139
|
signing_key:
|
106
140
|
specification_version: 3
|
data/lib/shield/helpers.rb
DELETED
@@ -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
|
data/lib/shield/password.rb
DELETED
@@ -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
|