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.
@@ -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 service(*a)
31
-
32
- fresh = Camping::H[{}]
33
- @js_rec = State.find_by_sid_and_app(@cookies.jsid, _appn) || State.new(:app => _appn, :blob => fresh,
34
- :sid => (@cookies.jsid ||= _sid))
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)) do
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
@@ -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'