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 +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
|