pasaporte 0.0.1 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +2 -2
- data/Manifest.txt +14 -5
- data/README.txt +52 -15
- data/Rakefile +4 -9
- data/bin/pasaporte-emit-app.rb +5 -0
- data/bin/pasaporte-fcgi.rb +5 -1
- data/lib/pasaporte.rb +200 -332
- data/lib/pasaporte/hacks.rb +10 -0
- data/lib/pasaporte/julik_state.rb +14 -10
- data/lib/pasaporte/lighttpd/cacert.pem +21 -0
- data/lib/pasaporte/lighttpd/cert_localhost_combined.pem +32 -0
- data/lib/pasaporte/lighttpd/sample-lighttpd-config.conf +27 -0
- data/lib/pasaporte/models.rb +254 -0
- data/lib/pasaporte/token_box.rb +43 -0
- data/test/helper.rb +7 -1
- data/test/test_edit_profile.rb +53 -0
- data/test/test_openid.rb +20 -13
- data/test/test_pasaporte.rb +0 -103
- data/test/test_profile.rb +3 -2
- data/test/test_public_signon.rb +26 -0
- data/test/test_settings.rb +1 -2
- data/test/test_signout.rb +24 -0
- data/test/test_token_box.rb +54 -0
- data/test/test_with_partial_ssl.rb +99 -0
- metadata +27 -9
- data/lib/pasaporte/.DS_Store +0 -0
- data/lib/pasaporte/assets/.DS_Store +0 -0
@@ -0,0 +1,10 @@
|
|
1
|
+
# Hacks that are needed to make everything work swell
|
2
|
+
# Start an XML tag. We override to get "<stupidbrowserfriendly />" fwd slash
|
3
|
+
class Builder::XmlMarkup
|
4
|
+
def _start_tag(sym, attrs, end_too=false)
|
5
|
+
@target << "<#{sym}"
|
6
|
+
_insert_attributes(attrs)
|
7
|
+
@target << " /" if end_too #HIER!!
|
8
|
+
@target << ">"
|
9
|
+
end
|
10
|
+
end
|
@@ -22,21 +22,25 @@ module JulikState
|
|
22
22
|
def _appn; self.class.to_s.split(/::/).shift; end
|
23
23
|
|
24
24
|
def force_session_save!
|
25
|
-
@cookies.jsid ||= _sid
|
26
25
|
res = @js_rec.update_attributes :blob => @state, :sid => @cookies.jsid
|
27
26
|
raise "Cannot save session" unless res
|
28
27
|
end
|
29
28
|
|
30
|
-
def
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
29
|
+
def reset_session!
|
30
|
+
@cookies.jsid = _sid
|
31
|
+
@js_rec.destroy unless @js_rec.new_record?
|
32
|
+
initialize_session!
|
33
|
+
end
|
34
|
+
|
35
|
+
def initialize_session!(with = Camping::H[{}])
|
36
|
+
@js_rec = State.find_by_sid_and_app(@cookies.jsid, _appn) || State.new(
|
37
|
+
:app => _appn, :blob => with, :sid => (@cookies.jsid = _sid))
|
36
38
|
@state = @js_rec.blob.dup
|
39
|
+
end
|
40
|
+
|
41
|
+
def service(*a)
|
42
|
+
initialize_session!
|
37
43
|
@msg = @state.delete(:msg)
|
38
|
-
returning(super(*a))
|
39
|
-
force_session_save! if (@state != @js_rec.blob)
|
40
|
-
end
|
44
|
+
returning(super(*a)) { force_session_save! }
|
41
45
|
end
|
42
46
|
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
-----BEGIN CERTIFICATE-----
|
2
|
+
MIIDZDCCAkygAwIBAgIBADANBgkqhkiG9w0BAQUFADAQMQ4wDAYDVQQDDAVQVGVz
|
3
|
+
dDAeFw0wODExMDYwMTIyMzlaFw0xMzExMDUwMTIyMzlaMBAxDjAMBgNVBAMMBVBU
|
4
|
+
ZXN0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvsRwETWeR2VEhRev
|
5
|
+
kd7UQNFYKAd0b219/Lbt0QWFeRPki6pxRDBWlq5ZvUTfJV6LlV3NDHRL4qSC4NDp
|
6
|
+
ks3EuyE15Y0pGXMcQQ7/2GeOAHIP94Civ3swa5kfWhvzQZlw+wJWDuEEc83RXgJS
|
7
|
+
yNm3wjO0FhoxPgYuSPSxsKCftq7ZgQjqDw1aCk+hpAwZblohyj+neRdfbdIBcYvK
|
8
|
+
IzxwaR1dgcSvR1YD4shc7Hh3240VrqcVNA1XV/hQFVH3bkI4f4cfMz6Xq7wD3NAp
|
9
|
+
Y45tA/n+k5oHy5rMTYPgW8eTrCJ8clvVYCtyD9gfs9KmuVLRyRnVQeOslN3fwB3X
|
10
|
+
UB2IIQIDAQABo4HIMIHFMA8GA1UdEwEB/wQFMAMBAf8wMQYJYIZIAYb4QgENBCQW
|
11
|
+
IlJ1YnkvT3BlblNTTCBHZW5lcmF0ZWQgQ2VydGlmaWNhdGUwHQYDVR0OBBYEFGqv
|
12
|
+
1S7TNlJ/5Eps7UtIPC9k4+moMA4GA1UdDwEB/wQEAwIBBjAWBgNVHSUBAf8EDDAK
|
13
|
+
BggrBgEFBQcDATA4BgNVHSMEMTAvgBRqr9Uu0zZSf+RKbO1LSDwvZOPpqKEUpBIw
|
14
|
+
EDEOMAwGA1UEAwwFUFRlc3SCAQAwDQYJKoZIhvcNAQEFBQADggEBAIqa1uDqrRSk
|
15
|
+
jPLN6XBta6KjrSJ3gd/BSgVj4VTCRPP3AwLzTavSSUVS+U6Be4Xd9PC096gf0irH
|
16
|
+
iqViMJrytBW1alyTrjyhMVLsUPbQOrHOAbJvSthDsCx+pM5g9VibUnFC5V13fkxf
|
17
|
+
Byu5H/SJA2qlrYOHgrb1/6mOqn9upP27q1l+gpCFZKQutvFlwPkdIO5z28tM5bnZ
|
18
|
+
ZZTll4+19BquA+qAfr2hb5p9ksANTd5Qh1sDs+aOiPseLz0C7IZMycBewnFWc3B0
|
19
|
+
icStB2AaSeLm5bAGBQ2VjrrOD4kPSyX3xY+Rs3bi2oGY1hhzyT/NljVDCeyNTeDN
|
20
|
+
tdMa50hTagM=
|
21
|
+
-----END CERTIFICATE-----
|
@@ -0,0 +1,32 @@
|
|
1
|
+
-----BEGIN CERTIFICATE-----
|
2
|
+
MIICxzCCAa+gAwIBAgIBATANBgkqhkiG9w0BAQUFADAQMQ4wDAYDVQQDDAVQVGVz
|
3
|
+
dDAeFw0wODExMDYwMTIyNDBaFw0wOTExMDYwMTIyNDBaMCExEjAQBgNVBAMMCWxv
|
4
|
+
Y2FsaG9zdDELMAkGA1UECwwCQ0EwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGB
|
5
|
+
AMtCjxQbJwktQFLJ+NOzUvsX1yEWPU7QdD0dmdDTflxyymUpbXyFKIdR5XPvVT7w
|
6
|
+
CxjHQXCTNle16DYrQbjKNVlK6voTC6NkZEUfPg/nTxwxb1ZUbdfvCEiEG2Tz75SQ
|
7
|
+
E9VPpUiB4Yk+NuEurs5gn2K/m9kUF5Pazb26x4sIY/itAgMBAAGjgZ4wgZswDAYD
|
8
|
+
VR0TAQH/BAIwADAxBglghkgBhvhCAQ0EJBYiUnVieS9PcGVuU1NMIEdlbmVyYXRl
|
9
|
+
ZCBDZXJ0aWZpY2F0ZTAdBgNVHQ4EFgQUyF7HBbbl960mFxCkaFO4c+szNOkwCwYD
|
10
|
+
VR0PBAQDAgOoMBMGA1UdJQQMMAoGCCsGAQUFBwMBMBcGA1UdEQEB/wQNMAuCCWxv
|
11
|
+
Y2FsaG9zdDANBgkqhkiG9w0BAQUFAAOCAQEAiqBI77ifmbNnaOCY5hZH6V9XkrvH
|
12
|
+
tzEeDrgLLCGXCWgIKWkjeNZQDCfRbyu7rPxUav87+WQWSndcSnFsWVOcxYDmPq2N
|
13
|
+
aXHfzIU51sEY57Ie9/pzZgsxs6Hhrky81NyzepG9B76ZzJOrYToJxywQmh3NThD9
|
14
|
+
tfywNXGnoiO55DZJfg7w4cOH4VnxkpqLMT8AxOpSlXCD4p57AutXSGLUOfzurU/Q
|
15
|
+
npqoQcp9m/eYUKIDzS9TyA6scQ8akKCuMuMuX4HQOJl17y6d5+CrNhfvq1nYkj0M
|
16
|
+
LWBzK5Uqf/O/R/bbMKMlgduuxSB2jfBtgl5C+hhQfDKtqf5F5S4csebPwA==
|
17
|
+
-----END CERTIFICATE-----
|
18
|
+
-----BEGIN RSA PRIVATE KEY-----
|
19
|
+
MIICXQIBAAKBgQDLQo8UGycJLUBSyfjTs1L7F9chFj1O0HQ9HZnQ035ccsplKW18
|
20
|
+
hSiHUeVz71U+8AsYx0FwkzZXteg2K0G4yjVZSur6EwujZGRFHz4P508cMW9WVG3X
|
21
|
+
7whIhBtk8++UkBPVT6VIgeGJPjbhLq7OYJ9iv5vZFBeT2s29useLCGP4rQIDAQAB
|
22
|
+
AoGAP2sk+UD/jP1xdGNQH71zxqRJmyk1N8ISgn8Z3u4eHvox7B5g6tkhLBeBYArs
|
23
|
+
rhZ3X+PLpzRHYFaBfWVBvEZbHlH5vIFEB7hVtomr5yhv8oVi+IY6ynAp6qFh47JY
|
24
|
+
QcqNm+PKDVOAIPyUafMjFCAJljngIup3f0r/Tuj/9jhaVIECQQD9xkj52bG2GelP
|
25
|
+
Jtqa6mmI7LaI5xX3Bi3M3AswFlj3ICI5ZSTiLI5/5PjzBAbu9F1TwQLqH6g3lkrK
|
26
|
+
KREGlmnTAkEAzQre1RT8ixFIlyvUfi6tLtn8oARYd42WXibZT7e6j4t0xaZ8MV3J
|
27
|
+
gfVH413EmcJhHw4oKNYlS+XMLO1o4FgDfwJBAKnPHZO5/HUan4hsOkkA4/9QTdAL
|
28
|
+
uSHjS5BSCVZzDbLHGL+JE4YYRH4F7CNIpY8Nisl5VIbvCfOwKHlfw1nCGisCQCJF
|
29
|
+
cN1YtqVf7CwoTUoR7yxnjwwH7el9puZxw9zJLsuTWZ83poZx0J6CKtPb9mJk1Orl
|
30
|
+
6Nx6fp1i+W+A9wiYbW0CQQC92IXy4rgz5gaoXCGCUZ3uQVyysvuUcb0LBPVezNhB
|
31
|
+
aOsMqthVtaddDMm6rshkFN+8GXrottZWJgStRxFLv9mk
|
32
|
+
-----END RSA PRIVATE KEY-----
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# This is a sample config for running Pasaporte in Partial SSL mode.
|
2
|
+
# It runs on non-privileged ports.
|
3
|
+
server.bind = "0.0.0.0"
|
4
|
+
server.port = 9050
|
5
|
+
server.pid-file = CWD + "/lighttpd.pid"
|
6
|
+
server.modules = ( "mod_fastcgi", "mod_redirect" )
|
7
|
+
index-file.names = ("index.html")
|
8
|
+
server.document-root = CWD
|
9
|
+
|
10
|
+
fastcgi.server = ( "" => (
|
11
|
+
"localhost" => (
|
12
|
+
"socket" => "/tmp/camping-pasaporte.socket",
|
13
|
+
"bin-path" => CWD +"/../../../bin/pasaporte-fcgi.rb",
|
14
|
+
"bin-environment" => ("FORCE_ROOT" => "1",
|
15
|
+
"PASAPORTE_PARTIAL_SSL" => "1",
|
16
|
+
"PASAPORTE_SSL_PORT" => "9051",
|
17
|
+
"PASAPORTE_HTTP_PORT" => "9050"
|
18
|
+
),
|
19
|
+
"allow-x-send-file" => "enable",
|
20
|
+
"check-local" => "disable",
|
21
|
+
"max-procs" => 1 ) ) )
|
22
|
+
|
23
|
+
$SERVER["socket"] == ":9051" {
|
24
|
+
ssl.engine = "enable"
|
25
|
+
ssl.ca-file = CWD + "/cacert.pem"
|
26
|
+
ssl.pemfile = CWD + "/cert_localhost_combined.pem"
|
27
|
+
}
|
@@ -0,0 +1,254 @@
|
|
1
|
+
module Pasaporte::Models
|
2
|
+
MAX = :limit # Thank you rails core, it was MAX before
|
3
|
+
class CreatePasaporte < V 1.0
|
4
|
+
def self.up
|
5
|
+
create_table :pasaporte_profiles, :force => true do |t|
|
6
|
+
# http://openid.net/specs/openid-simple-registration-extension-1_0.html
|
7
|
+
t.column :nickname, :string, MAX => 20
|
8
|
+
t.column :email, :string, MAX => 70
|
9
|
+
t.column :fullname, :string, MAX => 50
|
10
|
+
t.column :dob, :date, :null => true
|
11
|
+
t.column :gender, :string, MAX => 1
|
12
|
+
t.column :postcode, :string, MAX => 10
|
13
|
+
t.column :country, :string, MAX => 2
|
14
|
+
t.column :language, :string, MAX => 5
|
15
|
+
t.column :timezone, :string, MAX => 50
|
16
|
+
|
17
|
+
# And our extensions
|
18
|
+
# is the profile shared (visible to others)
|
19
|
+
t.column :shared, :boolean, :default => false
|
20
|
+
|
21
|
+
# his bio
|
22
|
+
t.column :info, :text
|
23
|
+
|
24
|
+
# when he last used Pasaporte
|
25
|
+
t.column :last_login, :datetime
|
26
|
+
|
27
|
+
# the encryption part that we generate for every user, the other is the pass
|
28
|
+
# the total encryption key for private data will be stored in the session only when
|
29
|
+
# the user is logged in
|
30
|
+
t.column :secret_salt, :integer
|
31
|
+
|
32
|
+
# Good servers delegate
|
33
|
+
t.column :openid_server, :string
|
34
|
+
t.column :openid_delegate, :string
|
35
|
+
|
36
|
+
# We shard by domain
|
37
|
+
t.column :domain_name, :string, :null => false, :default => 'localhost'
|
38
|
+
|
39
|
+
# Keep a close watch on those who
|
40
|
+
t.column :throttle_count, :integer, :default => 0
|
41
|
+
t.column :suspicious, :boolean, :default => false
|
42
|
+
end
|
43
|
+
|
44
|
+
add_index(:pasaporte_profiles, [:nickname, :domain_name], :unique)
|
45
|
+
|
46
|
+
create_table :pasaporte_settings do |t|
|
47
|
+
t.column :setting, :string
|
48
|
+
t.column :value, :binary
|
49
|
+
end
|
50
|
+
|
51
|
+
create_table :pasaporte_associations do |t|
|
52
|
+
# server_url is blob, because URLs could be longer
|
53
|
+
# than db can handle as a string
|
54
|
+
t.column :server_url, :binary
|
55
|
+
t.column :handle, :string
|
56
|
+
t.column :secret, :binary
|
57
|
+
t.column :issued, :integer
|
58
|
+
t.column :lifetime, :integer
|
59
|
+
t.column :assoc_type, :string
|
60
|
+
end
|
61
|
+
|
62
|
+
create_table :pasaporte_nonces do |t|
|
63
|
+
t.column :nonce, :string
|
64
|
+
t.column :created, :integer
|
65
|
+
end
|
66
|
+
|
67
|
+
create_table :pasaporte_throttles do |t|
|
68
|
+
t.column :created_at, :datetime
|
69
|
+
t.column :client_fingerprint, :string, MAX => 40
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def self.down
|
74
|
+
drop_table :pasaporte_profiles
|
75
|
+
drop_table :pasaporte_settings
|
76
|
+
drop_table :pasaporte_associations
|
77
|
+
drop_table :pasaporte_nonces
|
78
|
+
drop_table :pasaporte_throttles
|
79
|
+
end
|
80
|
+
end
|
81
|
+
class AddAprovals < V(1.1)
|
82
|
+
def self.up
|
83
|
+
create_table :pasaporte_approvals do | t |
|
84
|
+
t.column :profile_id, :integer, :null => false
|
85
|
+
t.column :trust_root, :string, :null => false
|
86
|
+
end
|
87
|
+
add_index(:pasaporte_approvals, [:profile_id, :trust_root], :unique)
|
88
|
+
end
|
89
|
+
|
90
|
+
def self.down
|
91
|
+
drop_table :pasaporte_approvals
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
class MigrateOpenidTables < V(1.2)
|
96
|
+
def self.up
|
97
|
+
drop_table :pasaporte_settings
|
98
|
+
drop_table :pasaporte_nonces
|
99
|
+
create_table :pasaporte_nonces, :force => true do |t|
|
100
|
+
t.column :server_url, :string, :null => false
|
101
|
+
t.column :timestamp, :integer, :null => false
|
102
|
+
t.column :salt, :string, :null => false
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def self.down
|
107
|
+
drop_table :pasaporte_nonces
|
108
|
+
create_table :pasaporte_nonces, :force => true do |t|
|
109
|
+
t.column "nonce", :string
|
110
|
+
t.column "created", :integer
|
111
|
+
end
|
112
|
+
|
113
|
+
create_table :pasaporte_settings, :force => true do |t|
|
114
|
+
t.column "setting", :string
|
115
|
+
t.column "value", :binary
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
class ShardOpenidTables < V(1.3)
|
121
|
+
def self.up
|
122
|
+
add_column :pasaporte_associations, :pasaporte_domain, :string, :null => false, :default => 'localhost'
|
123
|
+
add_column :pasaporte_nonces, :pasaporte_domain, :string, :null => false, :default => 'localhost'
|
124
|
+
end
|
125
|
+
|
126
|
+
def self.down
|
127
|
+
remove_column :pasaporte_nonces, :pasaporte_domain
|
128
|
+
remove_column :pasaporte_associations, :pasaporte_domain
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
# Minimal info we store about people. It's the container for the sreg data
|
133
|
+
# in the first place.
|
134
|
+
class Profile < Base
|
135
|
+
before_create { |p| p.secret_salt = rand(Time.now) }
|
136
|
+
before_save :validate_delegate_uris
|
137
|
+
validates_presence_of :nickname
|
138
|
+
validates_presence_of :domain_name
|
139
|
+
validates_uniqueness_of :nickname, :scope => :domain_name
|
140
|
+
attr_protected :domain_name, :nickname
|
141
|
+
has_many :approvals, :dependent => :delete_all
|
142
|
+
|
143
|
+
any_url_present = lambda do |r|
|
144
|
+
!r.openid_server.blank? || !r.openid_server.blank?
|
145
|
+
end
|
146
|
+
%w(openid_server openid_delegate).map do | c |
|
147
|
+
validates_presence_of c, :if => any_url_present
|
148
|
+
end
|
149
|
+
|
150
|
+
# Convert the profile to sreg according to the spec (YYYY-MM-DD for dob and such)
|
151
|
+
def to_sreg_fields(fields_to_extract = nil)
|
152
|
+
fields_to_extract ||= %w( nickname email fullname dob gender postcode country language timezone )
|
153
|
+
fields_to_extract.inject({}) do | out, field |
|
154
|
+
v = self[field]
|
155
|
+
v.blank? ? out : (out[field.to_s] = v.to_s; out)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
# We have to override that because we want our protected attributes
|
160
|
+
def self.find_or_create_by_nickname_and_domain_name(nick, domain)
|
161
|
+
returning(super(nick, domain)) do | me |
|
162
|
+
((me.nickname, me.domain_name = nick, domain) && me.save) if me.new_record?
|
163
|
+
end
|
164
|
+
end
|
165
|
+
class << self
|
166
|
+
alias_method :find_or_create_by_domain_name_and_nickname,
|
167
|
+
:find_or_create_by_nickname_and_domain_name
|
168
|
+
end
|
169
|
+
|
170
|
+
def generate_sess_key
|
171
|
+
self.secret_salt ||= rand(Time.now)
|
172
|
+
s = [nickname, secret_salt, Time.now.year, Time.now.month].join('|')
|
173
|
+
OpenSSL::Digest::SHA1.new(s).hexdigest.to_s
|
174
|
+
end
|
175
|
+
|
176
|
+
# Check if this profile wants us to delegate his openid to a different identity provider.
|
177
|
+
# If both delegate and server are filled in properly this will return true
|
178
|
+
def delegates_openid?
|
179
|
+
Pasaporte::ALLOW_DELEGATION && (!openid_server.blank? && !openid_delegate.blank?)
|
180
|
+
end
|
181
|
+
|
182
|
+
def delegates_openid=(nv)
|
183
|
+
(self.openid_server, self.openid_delegate = nil, nil) if [false, '0', 0, 'no'].include?(nv)
|
184
|
+
end
|
185
|
+
alias_method :delegates_openid, :delegates_openid? # for checkboxes
|
186
|
+
|
187
|
+
def to_s; nickname; end
|
188
|
+
|
189
|
+
private
|
190
|
+
def validate_delegate_uris
|
191
|
+
if ([self.openid_server, self.openid_delegate].select{|i| i.blank?}).length == 1
|
192
|
+
errors.add(:delegate_server, "If you use delegation you have to specify both addresses")
|
193
|
+
false
|
194
|
+
end
|
195
|
+
|
196
|
+
%w(openid_server openid_delegate).map do | attr |
|
197
|
+
return if self[attr].blank?
|
198
|
+
begin
|
199
|
+
self[attr] = OpenID::URINorm.urinorm(self[attr])
|
200
|
+
rescue Exception => e
|
201
|
+
errors.add(attr, e.message)
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
# A token that the user has approved a site (a site's trust root) as legal
|
208
|
+
# recipient of his information
|
209
|
+
class Approval < Base
|
210
|
+
belongs_to :profile
|
211
|
+
validates_presence_of :profile_id, :trust_root
|
212
|
+
validates_uniqueness_of :trust_root, :scope => :profile_id
|
213
|
+
def to_s; trust_root; end
|
214
|
+
end
|
215
|
+
|
216
|
+
# Openid setting
|
217
|
+
class Setting < Base; end
|
218
|
+
|
219
|
+
# Openid nonces
|
220
|
+
class Nonce < Base; end
|
221
|
+
|
222
|
+
# Openid assocs
|
223
|
+
class Association < Base
|
224
|
+
def from_record
|
225
|
+
OpenID::Association.new(handle, secret, issued, lifetime, assoc_type)
|
226
|
+
end
|
227
|
+
|
228
|
+
def expired?
|
229
|
+
Time.now.to_i > (issued + lifetime)
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
# Set throttles
|
234
|
+
class Throttle < Base
|
235
|
+
|
236
|
+
# Set a throttle with the environment of the request
|
237
|
+
def self.set!(e)
|
238
|
+
create(:client_fingerprint => env_hash(e))
|
239
|
+
end
|
240
|
+
|
241
|
+
# Check if an environment is throttled
|
242
|
+
def self.throttled?(e)
|
243
|
+
prune!
|
244
|
+
count(:conditions => ["client_fingerprint = ? AND created_at > ?", env_hash(e), cutoff]) > 0
|
245
|
+
end
|
246
|
+
|
247
|
+
private
|
248
|
+
def self.prune!; delete_all "created_at < '#{cutoff.to_s(:db)}'"; end
|
249
|
+
def self.cutoff; Time.now - Pasaporte::THROTTLE_FOR; end
|
250
|
+
def self.env_hash(e)
|
251
|
+
OpenSSL::Digest::SHA1.new([e['REMOTE_ADDR'], e['HTTP_USER_AGENT']].map(&:to_s).join('|')).to_s
|
252
|
+
end
|
253
|
+
end
|
254
|
+
end # Pasaporte::Models
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# A simple but effective CSRF protector
|
2
|
+
class TokenBox
|
3
|
+
class Invalid < RuntimeError; end
|
4
|
+
MAX_TOKENS, TOKEN_SIZE = 2, 64
|
5
|
+
CHARS = [*'A'..'Z'] + [*'0'..'9'] + [*'a'..'z']
|
6
|
+
WINDOW = 10.minutes # Gone in 60 seconds
|
7
|
+
|
8
|
+
class Token
|
9
|
+
attr_reader :token
|
10
|
+
alias_method :to_s, :token
|
11
|
+
|
12
|
+
def initialize(lifetime)
|
13
|
+
@will_expire = Time.now.utc + lifetime
|
14
|
+
@token = (0...TOKEN_SIZE).inject("") { |ret,_| ret << CHARS[rand(CHARS.length)] }
|
15
|
+
end
|
16
|
+
|
17
|
+
def expired?
|
18
|
+
@will_expire < Time.now.utc
|
19
|
+
end
|
20
|
+
|
21
|
+
def inspect
|
22
|
+
"#{@token}:#{'exp' if expired?}"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Procure a CSRF token for a specific request URI
|
27
|
+
def procure!(request, lifetime = nil)
|
28
|
+
returning(Token.new(lifetime || WINDOW)) do | t |
|
29
|
+
@heap ||= {}
|
30
|
+
@heap[request] ||= []
|
31
|
+
@heap[request].shift if @heap[request].length >= MAX_TOKENS
|
32
|
+
@heap[request] << t
|
33
|
+
end.to_s
|
34
|
+
end
|
35
|
+
|
36
|
+
# Validate the token for a specific request URI
|
37
|
+
def validate!(request, token)
|
38
|
+
raise Invalid.new("no heap part") unless (@heap && @heap[request])
|
39
|
+
@heap[request].reject!{|t| t.expired? }
|
40
|
+
raise Invalid.new("no token found in heap") unless @heap[request].find{|e| e.to_s == token}
|
41
|
+
@heap[request].reject!{|e| e.to_s == token }
|
42
|
+
end
|
43
|
+
end
|
data/test/helper.rb
CHANGED
@@ -8,10 +8,13 @@ require File.dirname(__FILE__) + '/mosquito'
|
|
8
8
|
require 'flexmock'
|
9
9
|
require 'flexmock/test_unit'
|
10
10
|
|
11
|
+
Markaby::Builder.set(:indent, nil)
|
12
|
+
|
11
13
|
# for assert_select and friends
|
12
14
|
require 'action_controller'
|
13
15
|
require 'action_controller/assertions'
|
14
16
|
|
17
|
+
|
15
18
|
class Pasaporte::Controllers::ServerError
|
16
19
|
def get(*all)
|
17
20
|
raise all.pop
|
@@ -63,4 +66,7 @@ class Pasaporte::WebTest < Camping::WebTest
|
|
63
66
|
@html_document = HTML::Document.new(@response.body.to_s) unless @response.headers["Location"]
|
64
67
|
end
|
65
68
|
end
|
66
|
-
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Requires Pasaporte:WebTest to be defined already
|
72
|
+
require File.dirname(__FILE__) + '/testable_openid_fetcher'
|