pasaporte 0.0.1 → 0.0.3
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/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'
|