pasaporte 0.0.1 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,2 +1,2 @@
1
- === HEAD
2
- * Initial rollout
1
+ === 0.0.3 - 14.02.2009
2
+ * Most glaring bugs fixed
@@ -3,10 +3,9 @@ Manifest.txt
3
3
  README.txt
4
4
  Rakefile
5
5
  TODO.txt
6
+ bin/pasaporte-emit-app.rb
6
7
  bin/pasaporte-fcgi.rb
7
8
  lib/pasaporte.rb
8
- lib/pasaporte/.DS_Store
9
- lib/pasaporte/assets/.DS_Store
10
9
  lib/pasaporte/assets/bgbar.png
11
10
  lib/pasaporte/assets/lock.png
12
11
  lib/pasaporte/assets/mainbg_green.gif
@@ -20,22 +19,32 @@ lib/pasaporte/auth/remote_web_workplace.rb
20
19
  lib/pasaporte/auth/yaml_digest_table.rb
21
20
  lib/pasaporte/auth/yaml_table.rb
22
21
  lib/pasaporte/faster_openid.rb
22
+ lib/pasaporte/hacks.rb
23
23
  lib/pasaporte/iso_countries.yml
24
24
  lib/pasaporte/julik_state.rb
25
+ lib/pasaporte/lighttpd/cacert.pem
26
+ lib/pasaporte/lighttpd/cert_localhost_combined.pem
27
+ lib/pasaporte/lighttpd/sample-lighttpd-config.conf
25
28
  lib/pasaporte/markaby_ext.rb
29
+ lib/pasaporte/models.rb
26
30
  lib/pasaporte/pasaporte_store.rb
27
31
  lib/pasaporte/timezones.yml
32
+ lib/pasaporte/token_box.rb
28
33
  test/fixtures/pasaporte_approvals.yml
29
34
  test/fixtures/pasaporte_profiles.yml
30
35
  test/fixtures/pasaporte_throttles.yml
31
36
  test/helper.rb
32
37
  test/mosquito.rb
33
- test/test_throttle.rb
34
- test/testable_openid_fetcher.rb
35
38
  test/test_approval.rb
36
39
  test/test_auth_backends.rb
40
+ test/test_edit_profile.rb
37
41
  test/test_openid.rb
38
42
  test/test_pasaporte.rb
39
43
  test/test_profile.rb
44
+ test/test_public_signon.rb
40
45
  test/test_settings.rb
41
- test/test_throttle.rb
46
+ test/test_signout.rb
47
+ test/test_throttle.rb
48
+ test/test_token_box.rb
49
+ test/test_with_partial_ssl.rb
50
+ test/testable_openid_fetcher.rb
data/README.txt CHANGED
@@ -4,7 +4,7 @@ This is Pasaporte, a small identity server with a colored bar on top. It's in th
4
4
  of Crowd (but smaller). Will act as a mediator between OpenID and arbitary services where
5
5
  users are distinguished by their nickname (login), their password and a domain name.
6
6
 
7
- ==The idea
7
+ == The idea
8
8
 
9
9
  Pasaporte brings OpenID to the traditional simplicity of
10
10
 
@@ -15,29 +15,66 @@ that, when called, will return true or false. Yes, it's that simple. All the neg
15
15
  smorgasbord, profile editing, encryptodecryption and other electrabombastic niceties are
16
16
  going to be taken care of.
17
17
 
18
- Should the password become stale or should the authentication backend say that it no
18
+ Here is an example of a simple auth procedure:
19
+
20
+ # Stick your super auth HERE. Should be a proc accepting login, pass and domain
21
+ Pasaporte::AUTH = lambda do | login, pass, domain |
22
+ allowd = {"joe" => "secret"}
23
+ return (allowd[login] && (allowd[login] == pass))
24
+ end
25
+
26
+ Obviously you can let your auth procedure look up in ACLs and things like that if you
27
+ really need it to.
28
+
29
+ If the password becomes stale or should the authentication backend say that it no
19
30
  longer has the user in question the authorization tokens are immediately revoked, and any
20
31
  authorization requests will be denied.
21
32
 
22
- ==Configuration
33
+ == Using SSL
34
+
35
+ It is recommended that you run pasaporte in full SSL mode. However,
36
+ some OpenID consumers disallow OpenID providers with self-signed (i.e. free)
37
+ SSL certificates. Pasaporte mitigates this by offering the "partial SSL" mode. When turned on,
38
+ only the signon page (where the password is entered) and subsequent pages with which the user
39
+ interacts will be protected with SSL encryption, while the public OpenID endpoint will NOT be
40
+ SSL-enabled. Same is true for the server-server step of OpenID handshake.
41
+
42
+ This will allow even stricter providers to use Pasaporte servers, but without passing the user login
43
+ over the wire in clear text.
44
+
45
+ When partial SSL is turned on, the profile page (OpenID identity) will forcibly be made
46
+ unencrypted (will redirect to non-secure port).
47
+
48
+ Partial SSL is disabled by default - to enable set PARTIAL_SSL to true.
49
+
50
+ == Current issues
51
+
52
+ As of now, we are not aware of sites that cannot consume OpenID from Pasaporte.
53
+ There is currently no provision for verifying that the URL you are logging in from is the
54
+ one Pasaporte manages.
55
+
56
+ == Configuration
23
57
 
24
58
  The adventurous among us can override the defaults (Pasaporte constants) by placing a
25
59
  hash-formatted YAML file called "config.yml" in the pasaporte dir. And don't ask me what
26
60
  a "hash-formatted YAML file" is, because if you do you are not adventurous.
27
61
 
28
- ==A word of warning
62
+ Here the rundown of the config parameters:
29
63
 
30
- Considering the clear-text passwords issue, we strongly recommend running Pasaporte under
31
- SSL and under SSL only. But of course this might be prohibitive especially if you cannot
32
- be self-signed or don't have an extra IP at hand. When you run Pasaporte under HTTPS all
33
- URLs are going to be rewritten automatically to redirect and link to the HTTPS site.
64
+ MAX_FAILED_LOGIN_ATTEMPTS - after how many login attempts the user will be trottled
65
+ THROTTLE_FOR - Trottle length in seconds
66
+ ALLOW_DELEGATION - if set to true, the user will be able to redirect his OpenID
67
+ SESSION_LIFETIME - in seconds - how long does a session remain valid
68
+ PARTIAL_SSL - see above
69
+ HTTP_PORT - if partial SSL is used, the port on which the standard version runs
70
+ SSL_PORT - if partial SSL is used, the port on which the secure version runs
34
71
 
35
- ==Profiles
72
+ == Profiles
36
73
 
37
74
  Pasaporte allows the user to have a simple passport page, where some info can be placed
38
- for people who follow the OpenID profile URL. Sharing the information is entirelly optional.
75
+ for people who follow the OpenID profile URL. Sharing the information is entirelly optional.
39
76
 
40
- ==The all-id
77
+ == The all-id
41
78
 
42
79
  The login that you use is ultimately the nickname that comes in the URL. For that reason
43
80
  no other login name can be entered in the form. However the user still can verify that
@@ -52,20 +89,20 @@ It's important to understand that a Profile record is a helper for metadata and
52
89
  something authoritative - it's the auth routine that takes the actual decision about the
53
90
  user's state.
54
91
 
55
- ==Persistence
92
+ == Persistence
56
93
 
57
94
  We store some data that the user might find useful to store and maybe display on his user
58
- page. No sites that the user authorizes are stored. No sessions of the exchange are kept
95
+ page. No sessions of the exchange are kept
59
96
  except of the standard OpenID shared secrets (there are not linked to user records in any
60
97
  way).
61
98
 
62
- ==SREG data sharing
99
+ == SREG data sharing
63
100
 
64
101
  There is currently no provision for fetching SREG data (like email, date of birth and such)
65
102
  from the autorizing routine. We might consider this in the future, for now the user has to
66
103
  fill it in himself.
67
104
 
68
- ==Sharding
105
+ == Sharding
69
106
 
70
107
  The users in Pasaporte are segregated by the domain name of Pasaporte server. That is, if
71
108
  you have two domains pointed at +one+ Pasaporte, you will not have name clashes between
data/Rakefile CHANGED
@@ -4,18 +4,11 @@ require 'hoe'
4
4
  require File.dirname(__FILE__) + '/lib/pasaporte'
5
5
  $KCODE = 'u'
6
6
 
7
- class KolkHoe < Hoe
8
- def define_tasks
9
- extra_deps.reject! {|e| e[0] == 'hoe' }
10
- super
11
- end
12
- end
13
-
14
7
  # Disable spurious warnings when running tests, ActiveMagic cannot stand -w
15
8
  Hoe::RUBY_FLAGS.replace ENV['RUBY_FLAGS'] || "-I#{%w(lib test).join(File::PATH_SEPARATOR)}" +
16
9
  (Hoe::RUBY_DEBUG ? " #{RUBY_DEBUG}" : '')
17
10
 
18
- KolkHoe.new('Pasaporte', Pasaporte::VERSION) do |p|
11
+ psp = Hoe.new('Pasaporte', Pasaporte::VERSION) do |p|
19
12
  p.name = "pasaporte"
20
13
  p.author = "Julik Tarkhanov"
21
14
  p.description = "An OpenID server with a colored bar on top"
@@ -25,9 +18,11 @@ KolkHoe.new('Pasaporte', Pasaporte::VERSION) do |p|
25
18
  p.rdoc_pattern = /README.txt|CHANGELOG.txt|lib/
26
19
  p.test_globs = 'test/test_*.rb'
27
20
  p.need_zip = true
28
- p.extra_deps = ['activerecord', 'camping', ['ruby-openid', '>=2.1.0'], 'flexmock']
21
+ p.extra_deps = ['activerecord', ['camping' '>=1.5.180'], ['ruby-openid', '>=2.1.0'], 'flexmock']
29
22
  end
30
23
 
24
+ psp.spec.rdoc_options << "--charset" << "utf-8"
25
+
31
26
  desc "Generate the proper list of country codes from the ISO list"
32
27
  task :build_country_list do
33
28
  ISO_LIST = 'http://www.iso.org/iso/iso3166_en_code_lists.txt'
@@ -0,0 +1,5 @@
1
+ #! /usr/bin/env ruby
2
+ require 'rubygems'
3
+ require 'pasaporte'
4
+ require 'fileutils'
5
+ FileUtils.cp_r(Pasaporte::PATH, Dir.getcwd + '/')
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env ruby
1
+ #! /usr/bin/env ruby
2
2
  require 'rubygems'
3
3
  require 'camping'
4
4
  require 'camping/fastcgi'
@@ -12,6 +12,10 @@ Camping::Models::Base.establish_connection(
12
12
  Pasaporte.create
13
13
  Pasaporte::LOGGER = Logger.new(ENV['HOME'] + "/pasaporte.log")
14
14
 
15
+ ENV.keys.grep(/^PASAPORTE_/).each do | envar |
16
+ Pasaporte.const_set(envar.gsub(/^PASAPORTE_/, ''), ENV[envar])
17
+ end
18
+
15
19
  serv = Camping::FastCGI.new
16
20
  serv.mount('/', Pasaporte)
17
21
  serv.start
@@ -1,5 +1,5 @@
1
1
  require 'rubygems'
2
- gem 'ruby-openid', '>=2.1.0'
2
+ gem 'ruby-openid', '>=2.1.0' # Not less!
3
3
 
4
4
  $: << File.dirname(__FILE__)
5
5
 
@@ -8,16 +8,24 @@ $: << File.dirname(__FILE__)
8
8
  camping/session
9
9
  openid
10
10
  openid/extensions/sreg
11
- pasaporte/faster_openid
11
+ ).each {|r| require r }
12
+
13
+ Camping.goes :Pasaporte
14
+
15
+ %w(
12
16
  pasaporte/julik_state
13
17
  pasaporte/markaby_ext
18
+ pasaporte/hacks
19
+ pasaporte/token_box
14
20
  ).each {|r| require r }
15
21
 
16
- Camping.goes :Pasaporte
17
22
 
23
+ Markaby::Builder.set(:indent, 2)
18
24
  Markaby::Builder.set(:output_xml_instruction, false)
19
25
 
20
26
  module Pasaporte
27
+ VERSION = '0.0.3'
28
+
21
29
  module Auth; end # Acts as a container for auth classes
22
30
 
23
31
  MAX_FAILED_LOGIN_ATTEMPTS = 3
@@ -25,8 +33,12 @@ module Pasaporte
25
33
  DEFAULT_COUNTRY = 'nl'
26
34
  DEFAULT_TZ = 'Europe/Amsterdam'
27
35
  ALLOW_DELEGATION = true
28
- VERSION = '0.0.1'
29
- SESSION_LIFETIME = 10.hours
36
+
37
+
38
+ SESSION_LIFETIME = 12.hours
39
+ PARTIAL_SSL = false
40
+ HTTP_PORT = 80
41
+ SSL_PORT = 443
30
42
 
31
43
  LOGGER = Logger.new(STDERR) #:nodoc:
32
44
  PATH = File.expand_path(__FILE__) #:nodoc:
@@ -82,18 +94,13 @@ module Pasaporte
82
94
  end
83
95
  end
84
96
 
85
- module LoggerHijack
86
- #def service(*the_rest)
87
- # _hijacked, OpenID::Util.logger = OpenID::Util.logger, LOGGER
88
- # returning(super(*the_rest)) { OpenID::Util.logger = _hijacked }
89
- #end
90
- end
91
-
92
97
  # Or a semblance thereof
93
98
  module Secure
94
99
  class PleaseLogin < RuntimeError; end
95
100
  class Throttled < RuntimeError; end
96
101
  class FullStop < RuntimeError; end
102
+ class RedirectToSSL < RuntimeError; end
103
+ class RedirectToPlain < RuntimeError; end
97
104
 
98
105
  module CheckMethods
99
106
  def _redir_to_login_page!(persistables)
@@ -105,7 +112,7 @@ module Pasaporte
105
112
  end
106
113
 
107
114
  # Allow the user to log in. Suspend any variables passed in the session.
108
- def require_login(persistables = {})
115
+ def require_login!(persistables = {})
109
116
  deny_throttled!
110
117
  raise "No nickname" unless @nickname
111
118
  _redir_to_login_page!(persistables) unless is_logged_in?
@@ -117,33 +124,66 @@ module Pasaporte
117
124
  def deny_throttled!
118
125
  raise Throttled if Models::Throttle.throttled?(env)
119
126
  end
120
-
127
+
128
+ # When partial SSL is enabled we use this method to redirect
129
+ def require_ssl!
130
+ raise RedirectToSSL if PARTIAL_SSL && !@env.HTTPS
131
+ end
132
+
133
+ # When partial SSL is enabled we use this method to force a plain page
134
+ def require_plain!
135
+ raise RedirectToPlain if PARTIAL_SSL && @env.HTTPS
136
+ end
137
+
138
+ def validate_token!
139
+ #from = URI.parse(@env.HTTP_REFERER).path
140
+ #LOGGER.debug "Validating form token"
141
+ #token_box.validate!(from, @input.tok)
142
+ end
143
+
121
144
  def profile_by_nickname(n)
122
145
  ::Pasaporte::Models::Profile.find_or_create_by_nickname_and_domain_name(n, my_domain)
123
146
  end
124
147
  end
125
148
 
126
- def expire_old_sessions!
127
- day_zero = (Time.now - SESSION_LIFETIME).to_s(:db)
128
- # JulikState.expire_old(SESSION_LIFETIME)
129
- # Camping::Models::Session.delete_all("created_at < '%s'" % day_zero)
149
+ def token_box
150
+ @state.token_box ||= TokenBox.new
130
151
  end
131
152
 
132
153
  def service(*a)
133
154
  begin
134
- expire_old_sessions!
135
155
  @ctr = self.class.to_s.split('::').pop
136
156
  super(*a)
137
157
  rescue FullStop
138
158
  return self
139
159
  rescue PleaseLogin
140
- LOGGER.info "#{env['REMOTE_ADDR']} - Redirecting to signon #{@cookies.inspect}"
160
+ LOGGER.info "#{env['REMOTE_ADDR']} - Redirecting to signon"
141
161
  redirect R(Pasaporte::Controllers::Signon, @nickname)
142
162
  return self
143
163
  rescue Throttled
144
164
  LOGGER.info "#{env['REMOTE_ADDR']} - Throttled user tried again"
145
165
  redirect R(Pasaporte::Controllers::ThrottledPage)
146
166
  return self
167
+ rescue TokenBox::Invalid => i
168
+ LOGGER.warn "Form token has been compromised on #{@env.REQUEST_URI} - #{i}"
169
+ LOGGER.warn @state.token_box.inspect
170
+ redirect R(Pasaporte::Controllers::FormExpired)
171
+ rescue RedirectToSSL
172
+ LOGGER.info "Forcing redirect to SSL page"
173
+ the_uri = URI.parse(@env.REQUEST_URI)
174
+ the_uri.host = @env.SERVER_NAME
175
+ the_uri.scheme = 'https'
176
+ the_uri.port = SSL_PORT unless SSL_PORT.to_i == 443
177
+ redirect the_uri.to_s
178
+ return self
179
+ rescue RedirectToPlain
180
+ LOGGER.info "Forcing redirect to plain (non-SSL) page"
181
+ the_uri = URI.parse(@env.REQUEST_URI)
182
+ the_uri.host = @env.SERVER_NAME
183
+ the_uri.scheme = 'http'
184
+ the_uri.port = HTTP_PORT unless HTTP_PORT.to_i == 80
185
+ redirect the_uri.to_s
186
+ return self
147
187
  end
148
188
  self
149
189
  end
@@ -153,272 +193,19 @@ module Pasaporte
153
193
  module CookiePreservingRedirect
154
194
  def redirect(*args)
155
195
  @headers['Set-Cookie'] = @cookies.map { |k,v| "#{k}=#{C.escape(v)}; path=#{self/"/"}" if v != @k[k] } - [nil]
196
+ force_session_save!
156
197
  super(*args)
157
198
  end
158
199
  end
159
200
 
160
- # The order here is important. Camping::Session has to come LAST (innermost)
201
+ # The order here is important. Camping::Session has to come LAST (outermost)
161
202
  # otherwise you risk losing the session if one of the services upstream
162
203
  # redirects.
163
- [CampingFlash, CookiePreservingRedirect, Secure, JulikState, LoggerHijack].map{|m| include m }
164
-
165
-
166
- module Models
167
- MAX = :limit # Thank you rails core, it was MAX before
168
- class CreatePasaporte < V 1.0
169
- def self.up
170
- create_table :pasaporte_profiles, :force => true do |t|
171
- # http://openid.net/specs/openid-simple-registration-extension-1_0.html
172
- t.column :nickname, :string, MAX => 20
173
- t.column :email, :string, MAX => 70
174
- t.column :fullname, :string, MAX => 50
175
- t.column :dob, :date, :null => true
176
- t.column :gender, :string, MAX => 1
177
- t.column :postcode, :string, MAX => 10
178
- t.column :country, :string, MAX => 2
179
- t.column :language, :string, MAX => 5
180
- t.column :timezone, :string, MAX => 50
181
-
182
- # And our extensions
183
- # is the profile shared (visible to others)
184
- t.column :shared, :boolean, :default => false
185
-
186
- # his bio
187
- t.column :info, :text
188
-
189
- # when he last used Pasaporte
190
- t.column :last_login, :datetime
191
-
192
- # the encryption part that we generate for every user, the other is the pass
193
- # the total encryption key for private data will be stored in the session only when
194
- # the user is logged in
195
- t.column :secret_salt, :integer
196
-
197
- # Good servers delegate
198
- t.column :openid_server, :string
199
- t.column :openid_delegate, :string
200
-
201
- # We shard by domain
202
- t.column :domain_name, :string, :null => false, :default => 'localhost'
203
-
204
- # Keep a close watch on those who
205
- t.column :throttle_count, :integer, :default => 0
206
- t.column :suspicious, :boolean, :default => false
207
- end
208
-
209
- add_index(:pasaporte_profiles, [:nickname, :domain_name], :unique)
210
-
211
- create_table :pasaporte_settings do |t|
212
- t.column :setting, :string
213
- t.column :value, :binary
214
- end
215
-
216
- create_table :pasaporte_associations do |t|
217
- # server_url is blob, because URLs could be longer
218
- # than db can handle as a string
219
- t.column :server_url, :binary
220
- t.column :handle, :string
221
- t.column :secret, :binary
222
- t.column :issued, :integer
223
- t.column :lifetime, :integer
224
- t.column :assoc_type, :string
225
- end
226
-
227
- create_table :pasaporte_nonces do |t|
228
- t.column :nonce, :string
229
- t.column :created, :integer
230
- end
204
+ [CampingFlash, Secure, JulikState, CookiePreservingRedirect].map{|m| include m }
231
205
 
232
- create_table :pasaporte_throttles do |t|
233
- t.column :created_at, :datetime
234
- t.column :client_fingerprint, :string, MAX => 40
235
- end
236
- end
237
206
 
238
- def self.down
239
- drop_table :pasaporte_profiles
240
- drop_table :pasaporte_settings
241
- drop_table :pasaporte_associations
242
- drop_table :pasaporte_nonces
243
- drop_table :pasaporte_throttles
244
- end
245
- end
246
- class AddAprovals < V(1.1)
247
- def self.up
248
- create_table :pasaporte_approvals do | t |
249
- t.column :profile_id, :integer, :null => false
250
- t.column :trust_root, :string, :null => false
251
- end
252
- add_index(:pasaporte_approvals, [:profile_id, :trust_root], :unique)
253
- end
254
-
255
- def self.down
256
- drop_table :pasaporte_approvals
257
- end
258
- end
259
-
260
- class MigrateOpenidTables < V(1.2)
261
- def self.up
262
- drop_table :pasaporte_settings
263
- drop_table :pasaporte_nonces
264
- create_table :pasaporte_nonces, :force => true do |t|
265
- t.column :server_url, :string, :null => false
266
- t.column :timestamp, :integer, :null => false
267
- t.column :salt, :string, :null => false
268
- end
269
- end
270
-
271
- def self.down
272
- drop_table :pasaporte_nonces
273
- create_table :pasaporte_nonces, :force => true do |t|
274
- t.column "nonce", :string
275
- t.column "created", :integer
276
- end
277
-
278
- create_table :pasaporte_settings, :force => true do |t|
279
- t.column "setting", :string
280
- t.column "value", :binary
281
- end
282
- end
283
- end
284
-
285
- class ShardOpenidTables < V(1.3)
286
- def self.up
287
- add_column :pasaporte_associations, :pasaporte_domain, :string, :null => false, :default => 'localhost'
288
- add_column :pasaporte_nonces, :pasaporte_domain, :string, :null => false, :default => 'localhost'
289
- end
290
-
291
- def self.down
292
- remove_column :pasaporte_nonces, :pasaporte_domain
293
- remove_column :pasaporte_associations, :pasaporte_domain
294
- end
295
- end
296
-
297
- # Minimal info we store about people. It's the container for the sreg data
298
- # in the first place.
299
- class Profile < Base
300
- before_create { |p| p.secret_salt = rand(Time.now) }
301
- before_save :validate_delegate_uris
302
- validates_presence_of :nickname
303
- validates_presence_of :domain_name
304
- validates_uniqueness_of :nickname, :scope => :domain_name
305
- attr_protected :domain_name, :nickname
306
- has_many :approvals, :dependent => :delete_all
307
-
308
- any_url_present = lambda do |r|
309
- !r.openid_server.blank? || !r.openid_server.blank?
310
- end
311
- %w(openid_server openid_delegate).map do | c |
312
- validates_presence_of c, :if => any_url_present
313
- end
314
-
315
- # Convert the profile to sreg according to the spec (YYYY-MM-DD for dob and such)
316
- def to_sreg_fields(fields_to_extract = nil)
317
- fields_to_extract ||= %w( nickname email fullname dob gender postcode country language timezone )
318
- fields_to_extract.inject({}) do | out, field |
319
- v = self[field]
320
- v.blank? ? out : (out[field.to_s] = v.to_s; out)
321
- end
322
- end
323
-
324
- # We have to override that because we want our protected attributes
325
- def self.find_or_create_by_nickname_and_domain_name(nick, domain)
326
- returning(super(nick, domain)) do | me |
327
- ((me.nickname, me.domain_name = nick, domain) && me.save) if me.new_record?
328
- end
329
- end
330
- class << self
331
- alias_method :find_or_create_by_domain_name_and_nickname,
332
- :find_or_create_by_nickname_and_domain_name
333
- end
334
-
335
- def generate_sess_key
336
- self.secret_salt ||= rand(Time.now)
337
- s = [nickname, secret_salt, Time.now.year, Time.now.month].join('|')
338
- OpenSSL::Digest::SHA1.new(s).hexdigest.to_s
339
- end
340
-
341
- # Check if this profile wants us to delegate his openid to a different identity provider.
342
- # If both delegate and server are filled in properly this will return true
343
- def delegates_openid?
344
- ALLOW_DELEGATION && (!openid_server.blank? && !openid_delegate.blank?)
345
- end
346
-
347
- def delegates_openid=(nv)
348
- (self.openid_server, self.openid_delegate = nil, nil) if [false, '0', 0, 'no'].include?(nv)
349
- end
350
- alias_method :delegates_openid, :delegates_openid? # for checkboxes
351
-
352
- def to_s; nickname; end
353
-
354
- private
355
- def validate_delegate_uris
356
- if ([self.openid_server, self.openid_delegate].select{|i| i.blank?}).length == 1
357
- errors.add(:delegate_server, "If you use delegation you have to specify both addresses")
358
- false
359
- end
360
-
361
- %w(openid_server openid_delegate).map do | attr |
362
- return if self[attr].blank?
363
- begin
364
- self[attr] = OpenID::URINorm.urinorm(self[attr])
365
- rescue Exception => e
366
- errors.add(attr, e.message)
367
- end
368
- end
369
- end
370
- end
371
-
372
- # A token that the user has approved a site (a site's trust root) as legal
373
- # recipient of his information
374
- class Approval < Base
375
- belongs_to :profile
376
- validates_presence_of :profile_id, :trust_root
377
- validates_uniqueness_of :trust_root, :scope => :profile_id
378
- def to_s; trust_root; end
379
- end
380
-
381
- # Openid setting
382
- class Setting < Base; end
383
-
384
- # Openid nonces
385
- class Nonce < Base; end
386
-
387
- # Openid assocs
388
- class Association < Base
389
- def from_record
390
- OpenID::Association.new(handle, secret, issued, lifetime, assoc_type)
391
- end
392
-
393
- def expired?
394
- Time.now.to_i > (issued + lifetime)
395
- end
396
- end
397
-
398
- # Set throttles
399
- class Throttle < Base
400
-
401
- # Set a throttle with the environment of the request
402
- def self.set!(e)
403
- create(:client_fingerprint => env_hash(e))
404
- end
405
-
406
- # Check if an environment is throttled
407
- def self.throttled?(e)
408
- prune!
409
- count(:conditions => ["client_fingerprint = ? AND created_at > ?", env_hash(e), cutoff]) > 0
410
- end
411
-
412
- private
413
- def self.prune!; delete_all "created_at < '#{cutoff.to_s(:db)}'"; end
414
- def self.cutoff; Time.now - THROTTLE_FOR; end
415
- def self.env_hash(e)
416
- OpenSSL::Digest::SHA1.new([e['REMOTE_ADDR'], e['HTTP_USER_AGENT']].map(&:to_s).join('|')).to_s
417
- end
418
- end
419
- end
420
-
421
- require File.dirname(__FILE__) + '/pasaporte/pasaporte_store'
207
+ require 'pasaporte/models'
208
+ require 'pasaporte/pasaporte_store'
422
209
 
423
210
  module Controllers
424
211
 
@@ -437,6 +224,12 @@ module Pasaporte
437
224
  post_with_nick(*extras)
438
225
  end
439
226
 
227
+ def head(*extras)
228
+ raise "Nickname is required for this action" unless (@nickname = extras.shift)
229
+ raise "#{self.class} does not respond to head_with_nick" unless respond_to?(:head_with_nick)
230
+ head_with_nick(*extras)
231
+ end
232
+
440
233
  # So that we can define put_with_nick and Camping sees it as #put being available
441
234
  def respond_to?(m, *whatever)
442
235
  super(m.to_sym) || super("#{m}_with_nick".to_sym)
@@ -457,29 +250,34 @@ module Pasaporte
457
250
  include OpenID::Server
458
251
 
459
252
  class Err < RuntimeError; end #:nodoc
460
- class NeedsApproval < RuntimeError; end #:nodoc
253
+ class NeedsApproval < Err; end #:nodoc
461
254
  class Denied < Err; end #:nodoc
462
- class NoOpenidRequest < RuntimeError; end #:nodoc
255
+ class NoOpenidRequest < Err; end #:nodoc
256
+ class SwitchUser < Secure::PleaseLogin; end #:nodoc
463
257
 
464
258
  def get_with_nick
259
+ require_plain!
465
260
  begin
466
261
  @oid_request = openid_request_from_input_or_session
467
262
 
468
- LOGGER.info "pasaporte: user #{@nickname} must not be throttled"
263
+ LOGGER.info "OpenID: user #{@nickname} must not be throttled"
469
264
  deny_throttled!
470
265
 
471
- LOGGER.info "pasaporte: nick must match the identity URL"
266
+ LOGGER.info "OpenID: nick must match the identity URL"
472
267
  check_nickname_matches_identity_url
268
+
269
+ LOGGER.info "OpenID: identity must reside on our server"
270
+ check_identity_lives_here
473
271
 
474
- LOGGER.info "pasaporte: user must be logged in"
272
+ LOGGER.info "OpenID: user must be logged in"
475
273
  check_logged_in
476
274
 
477
275
  @profile = profile_by_nickname(@nickname)
478
276
 
479
- LOGGER.info "pasaporte: trust root is on the approvals list"
277
+ LOGGER.info "OpenID: trust root is on the approvals list"
480
278
  check_if_previously_approved
481
279
 
482
- LOGGER.info "pasaporte: OpenID verified, redirecting"
280
+ LOGGER.info "OpenID: OpenID verified, redirecting"
483
281
 
484
282
  succesful_resp = @oid_request.answer(true)
485
283
  add_sreg(@oid_request, succesful_resp)
@@ -487,28 +285,36 @@ module Pasaporte
487
285
  rescue NoOpenidRequest
488
286
  return 'This is an OpenID server endpoint.'
489
287
  rescue ProtocolError => e
490
- LOGGER.error "pasaporte: Cannot decode the OpenID request - #{e.message}"
288
+ LOGGER.error "OpenID: Cannot decode the OpenID request - #{e.message}"
491
289
  return "Something went wrong processing your request"
290
+ rescue SwitchUser => e
291
+ # Force a session save, remove the current user from the session and throw
292
+ # to the login page for the user to switch to
293
+ @state.nickname = nil
294
+ force_session_save!
295
+ LOGGER.warn "OpenID: suspend - need to switch user first"
296
+ @oid_request.immediate ? ask_user_to_approve : (raise e)
492
297
  rescue PleaseLogin => e
493
298
  # There is a subtlety here. If the user had NO session before entering
494
299
  # this, he will get a new SID upon arriving at the signon page and thus
495
300
  # will loose his openid request
496
301
  force_session_save!
497
- LOGGER.warn "pasaporte: suspend - the user needs to login first, saving session"
302
+ LOGGER.warn "OpenID: suspend - the user needs to login first, saving session"
498
303
  @oid_request.immediate ? ask_user_to_approve : (raise e)
499
304
  rescue NeedsApproval
500
- LOGGER.warn "pasaporte: suspend - the URL needs approval first"
305
+ LOGGER.warn "OpenID: suspend - the URL needs approval first"
501
306
  ask_user_to_approve
502
307
  rescue Denied => d
503
- LOGGER.warn "pasaporte: deny OpenID to #{@nickname} - #{d.message}"
308
+ LOGGER.warn "OpenID: deny OpenID to #{@nickname} - #{d.message}"
504
309
  send_openid_response(@oid_request.answer(false))
505
310
  rescue Secure::Throttled => e
506
- LOGGER.warn "pasaporte: deny OpenID to #{@nickname} - user is throttled"
311
+ LOGGER.warn "OpenID: deny OpenID to #{@nickname} - user is throttled"
507
312
  send_openid_response(@oid_request.answer(false))
508
313
  end
509
314
  end
510
315
 
511
316
  def post_with_nick
317
+ require_plain!
512
318
  req = openid_server.decode_request(input)
513
319
  raise ProtocolError, "The decoded request was nil" if req.nil?
514
320
  # Check for dumb mode HIER!
@@ -523,7 +329,7 @@ module Pasaporte
523
329
  if input.keys.grep(/openid/).any?
524
330
  @state.delete(:pending_openid)
525
331
  r = openid_server.decode_request(input)
526
- LOGGER.info "Starting a new OpenID session with #{r.trust_root}"
332
+ LOGGER.info "Starting a session #{r.trust_root} -> #{r.identity}"
527
333
  @state.pending_openid = r
528
334
  elsif @state.pending_openid
529
335
  LOGGER.info "Resuming an OpenID session with #{@state.pending_openid.trust_root}"
@@ -534,19 +340,28 @@ module Pasaporte
534
340
  end
535
341
 
536
342
  def check_nickname_matches_identity_url
537
- nick_from_uri = @oid_request.identity.to_s.split(/\//)[-2]
343
+ nick_from_uri = @oid_request.identity.to_s.split(/\//).pop
538
344
  if (nick_from_uri != @nickname)
539
345
  raise Denied, "The identity '#{@oid_request.claimed_id}' does not mach the URL realm"
540
346
  end
541
-
347
+
542
348
  if (@state.nickname && (nick_from_uri != @state.nickname))
543
- raise Denied, "The identity '#{@oid_request.claimed_id}' is not the one of the current user"
349
+ raise SwitchUser, "The identity '#{@oid_request.claimed_id}' is not the one of the current user"
544
350
  end
545
351
  end
546
-
352
+
353
+ # TODO: we need to give the user a chance to say "I want these URLs to be legit too"
354
+ def check_identity_lives_here
355
+ the_id = URI.parse(@oid_request.identity)
356
+ # HTTP_HOST check
357
+ if (the_id.host != @env['HTTP_HOST'])
358
+ raise Denied, "The identity '#{@oid_request.claimed_id}' is not in this server(#{@env['HTTP_HOST']}"
359
+ end
360
+ end
361
+
547
362
  def check_logged_in
548
363
  message = "Before authorizing '%s' you will need to login" % input["openid.trust_root"]
549
- require_login(:pending_openid => @oid_request, :msg => message)
364
+ require_login!(:pending_openid => @oid_request, :msg => message)
550
365
  end
551
366
 
552
367
  def ask_user_to_approve
@@ -576,29 +391,28 @@ module Pasaporte
576
391
 
577
392
  # Return the yadis autodiscovery XML for the user
578
393
  class Yadis < personal(:yadis)
579
- YADIS_TPL = %{
580
- <xrds:XRDS xmlns:xrds="xri://$xrds"
581
- xmlns="xri://$xrd*($v*2.0)"
582
- xmlns:openid="http://openid.net/xmlns/1.0">
394
+ YADIS_TPL = %{<?xml version="1.0" encoding="UTF-8"?>
395
+ <xrds:XRDS xmlns:xrds="xri://$xrds" xmlns="xri://$xrd*($v*2.0)">
583
396
  <XRD>
584
- <Service priority="1">
397
+ <Service>
585
398
  <Type>http://openid.net/signon/1.0</Type>
586
399
  <URI>%s</URI>
587
- <openid:Delegate>%s</openid:Delegate>
588
400
  </Service>
589
401
  </XRD>
590
402
  </xrds:XRDS>
591
403
  }
592
404
 
593
405
  def get_with_nick
594
- @headers["Content-type"] = "application/xrds+xml"
406
+ @headers["Content-Type"] = "application/xrds+xml"
407
+ LOGGER.info "YADIS requested for #{@nickname}"
595
408
  @skip_layout = true
596
- YADIS_TPL % get_endpoints
409
+ # We only use the server for now
410
+ YADIS_TPL % get_endpoints[0]
597
411
  end
598
412
 
599
413
  private
600
414
  def get_endpoints
601
- defaults = [_our_endpoint_uri, _our_endpoint_uri]
415
+ defaults = [_our_endpoint_uri, _our_identity_url]
602
416
  @profile = Profile.find_by_nickname_and_domain_name(@nickname, my_domain)
603
417
  return defaults unless @profile && @profile.delegates_openid?
604
418
  [@profile.openid_server, @profile.openid_delegate]
@@ -607,7 +421,7 @@ module Pasaporte
607
421
 
608
422
  class ApprovalsPage < personal(:approvals)
609
423
  def get_with_nick
610
- require_login
424
+ require_login!
611
425
  @approvals = @profile.approvals
612
426
  if @approvals.empty?
613
427
  @msg = 'You currently do not have any associations with other sites through us'
@@ -620,7 +434,7 @@ module Pasaporte
620
434
 
621
435
  class DeleteApproval < personal('disapprove/(\d+)')
622
436
  def get_with_nick(appr_id)
623
- require_login
437
+ require_login!
624
438
  ap = @profile.approvals.find(appr_id); ap.destroy
625
439
  show_message "The site #{ap} has been removed from your approvals list"
626
440
  redirect R(ApprovalsPage, @nickname)
@@ -633,18 +447,27 @@ module Pasaporte
633
447
  include Secure::CheckMethods
634
448
 
635
449
  def get(nick=nil)
636
- LOGGER.info "Entered signon with #{@cookies.inspect}"
450
+ LOGGER.info "Entered signon, #{@env.HTTPS ? :HTTPS : :HTTP_plain }"
637
451
  deny_throttled!
638
452
  return redirect(DashPage, @state.nickname) if @state.nickname
639
453
  if nick && @state.pending_openid
640
- humane = URI.parse(@state.pending_openid.trust_root).host
454
+ humane = begin
455
+ URI.parse(@state.pending_openid.trust_root).host
456
+ rescue URI::InvalidURIError
457
+ LOGGER.error "Failed to parse #{@state.pending_openid.trust_root}"
458
+ @state.pending_openid.trust_root
459
+ end
641
460
  show_message "Before authorizing with <b>#{humane}</b> you will need to login"
642
461
  end
462
+
463
+ require_ssl!
464
+
643
465
  @nickname = nick;
644
466
  render :signon_form
645
467
  end
646
468
 
647
469
  def post(n=nil)
470
+
648
471
  begin
649
472
  deny_throttled!
650
473
  rescue Pasaporte::Secure::Throttled => th
@@ -654,11 +477,19 @@ module Pasaporte
654
477
  end
655
478
  raise th
656
479
  end
480
+
481
+ require_ssl!
482
+
657
483
  @nickname = @input.login || n || (raise "No nickname to authenticate")
484
+
658
485
  # The throttling logic must be moved into throttles apparently
486
+
659
487
  # Start counting
660
488
  @state.failed_logins ||= 0
661
489
 
490
+ # Validate token
491
+ validate_token!
492
+
662
493
  # If the user reaches the failed login limit we ban him for a while and
663
494
  # tell the OpenID requesting party to go away
664
495
  if Pasaporte::AUTH.call(@nickname, input.pass, my_domain)
@@ -726,8 +557,7 @@ module Pasaporte
726
557
  class Signout < personal(:signout)
727
558
  def get_with_nick
728
559
  (redirect R(Signon, @nickname); return) unless is_logged_in?
729
- # reset the session in our part only
730
- @state = Camping::H.new
560
+ reset_session!
731
561
  @state.msg = "Thanks for using the service and goodbye"
732
562
  redirect R(Signon, @nickname)
733
563
  end
@@ -737,13 +567,16 @@ module Pasaporte
737
567
  # when associating for the first time
738
568
  class Decide < personal(:decide)
739
569
  def get_with_nick
740
- require_login
570
+ require_ssl!
571
+ require_login!
572
+
741
573
  @oid_request = @state.pending_openid
742
574
  render :decide
743
575
  end
744
576
 
745
577
  def post_with_nick
746
- require_login
578
+ require_ssl!
579
+ require_login!
747
580
  if !@state.pending_openid
748
581
  @report = "There is no OpenID request to approve anymore. Looks like it went out already."
749
582
  render :bailout
@@ -760,12 +593,16 @@ module Pasaporte
760
593
  # Allows the user to modify the settings
761
594
  class EditProfile < personal(:edit)
762
595
  def get_with_nick
763
- require_login
596
+ require_login!
597
+ require_ssl!
764
598
  render :profile_form
765
599
  end
766
600
 
767
601
  def post_with_nick
768
- require_login
602
+ require_login!
603
+ require_ssl!
604
+ validate_token!
605
+
769
606
  _collapse_checkbox_input(input)
770
607
 
771
608
  if @profile.update_attributes(input.profile)
@@ -821,13 +658,32 @@ module Pasaporte
821
658
  render :bailout
822
659
  end
823
660
  end
661
+
662
+ class FormExpired < R('/form-expired')
663
+ def get
664
+ @report = "The form has expired unfortunately"
665
+ render :bailout
666
+ end
667
+ end
824
668
 
825
669
  # Just show a public profile page. Before the user logs in for the first time
826
670
  # it works like a dummy page. After the user has been succesfully authenticated he can
827
671
  # show his personal info on this page (this is his identity URL).
828
672
  class ProfilePage < personal
673
+
674
+ def head(nick)
675
+ get(nick)
676
+ return
677
+ end
678
+
829
679
  def get(nick)
830
680
  @nickname = nick
681
+
682
+ # Redirect the OpenID requesting party to the usual HTTP so that
683
+ # the OpenID procedure takes place without SSL, if partial SSL is turned on
684
+ require_plain! if PARTIAL_SSL
685
+
686
+ LOGGER.info "Profile page GET for #{nick}, sending YADIS header"
831
687
  @headers['X-XRDS-Location'] = _our_identity_url + '/yadis'
832
688
  @title = "#{@nickname}'s profile"
833
689
  @profile = Profile.find_by_nickname_and_domain_name(@nickname, my_domain)
@@ -837,7 +693,7 @@ module Pasaporte
837
693
  end
838
694
 
839
695
  class DashPage < personal(:prefs)
840
- def get_with_nick; require_login; render :dash; end
696
+ def get_with_nick; require_login!; render :dash; end
841
697
  end
842
698
  end
843
699
 
@@ -849,7 +705,8 @@ module Pasaporte
849
705
 
850
706
  # Return a RELATIVELY reliable domain key
851
707
  def my_domain
852
- env["SERVER_NAME"].gsub(/^www\./i, '').chars.downcase.to_s
708
+ server = env["SERVER_NAME"].gsub(/^www\./i, '')
709
+ (server.mb_chars rescue server.chars).downcase.to_s
853
710
  end
854
711
 
855
712
  # Camping processes double values (hidden field with 0 and checkbox with 1) as an array.
@@ -859,15 +716,21 @@ module Pasaporte
859
716
  (v == ["0", "1"]) ? (ha[k] = "1") : (v.is_a?(Hash) ? _collapse_checkbox_input(v) : true)
860
717
  end
861
718
  end
862
-
719
+
720
+ def _csrf_token
721
+ #input :name => :tok, :type => :hidden, :value => token_box.procure!(@env.REQUEST_URI)
722
+ #LOGGER.warn "After token procurement #{token_box.inspect}"
723
+ end
724
+
863
725
  def openid_server
864
726
  @store ||= PasaporteStore.new
865
727
 
866
728
  # Associations etc are sharded per domain on which Pasaporte sits
867
729
  @store.pasaporte_domain = @env['SERVER_NAME']
868
730
 
869
- # we also need to provide endopint URL - this is where Pasaporte is mounted
870
- @server ||= OpenID::Server::Server.new(@store, @env['SERVER_NAME'])
731
+ # we also need to provide endopint URL - this is where Pasaporte is mounted.
732
+ # Op-endpoint is the endpoint used by the server
733
+ @server ||= OpenID::Server::Server.new(@store, _our_endpoint_uri)
871
734
  @server
872
735
  end
873
736
 
@@ -960,22 +823,25 @@ module Pasaporte
960
823
  self << "Your password:"
961
824
  input.pass! :name => "pass", :type => "password"
962
825
  end
826
+ _csrf_token
963
827
  input :type => :submit, :value => 'Log me in'
964
828
  end
965
829
  end
966
830
 
967
831
  def layout
968
832
  @headers['Cache-Control'] = 'no-cache; must-revalidate'
833
+ @headers['Content-Type'] ||= 'text/html'
834
+
969
835
  if @skip_layout
970
836
  self << yield; return
971
837
  end
972
838
 
973
- @headers['Content-type'] = 'text/html'
974
839
  xhtml_transitional do
975
840
  head do
976
- meta 'http-equiv' => 'X-XRDS-Location', :content => (_our_identity_url + '/yadis')
977
- link :rel => "openid.server", :href => _openid_server_uri
841
+ self << '<meta http-equiv="X-XRDS-Location" content="%s/yadis" />' % _our_identity_url
842
+ link :rel => "openid.server", :href => _openid_server_uri
978
843
  link :rel => "openid.delegate", :href => _openid_delegate_uri
844
+
979
845
  link :rel => "stylesheet", :href => _s("pasaporte.css")
980
846
  script :type=>'text/javascript', :src => _s("pasaporte.js")
981
847
  title(@title || ('%s : pasaporte' % env['SERVER_NAME']))
@@ -1006,14 +872,14 @@ module Pasaporte
1006
872
  R(Assets, file)
1007
873
  end
1008
874
 
1009
- # Render either our server URL or the URL of the delegate
875
+ # Render either our endpoint URL or the URL of the delegate
1010
876
  def _openid_server_uri
1011
877
  (@profile && @profile.delegates_openid?) ? @profile.openid_server : _our_endpoint_uri
1012
878
  end
1013
879
 
1014
- # Render either our providing URL or the URL of the delegate
880
+ # Render either our identity URL or the URL of the delegate
1015
881
  def _openid_delegate_uri
1016
- (@profile && @profile.delegates_openid?) ? @profile.openid_delegate : _our_endpoint_uri
882
+ (@profile && @profile.delegates_openid?) ? @profile.openid_delegate : _our_identity_url
1017
883
  end
1018
884
 
1019
885
  # Canonicalized URL of our endpoint
@@ -1050,12 +916,13 @@ module Pasaporte
1050
916
  end
1051
917
 
1052
918
  form :method => :post do
919
+ _csrf_token
1053
920
  input :name => :pleasedo, :type => :submit, :value => " Yes, do allow "
1054
921
  self << '&#160;'
1055
922
  input :name => :nope, :type => :submit, :value => " No, they are villains! "
1056
923
  end
1057
924
  end
1058
-
925
+
1059
926
  def _toolbar
1060
927
  # my profile button
1061
928
  # log me out button
@@ -1103,7 +970,7 @@ module Pasaporte
1103
970
  form(:method => :post) do
1104
971
 
1105
972
  h2 "Your profile"
1106
-
973
+ _csrf_token
1107
974
  label.cblabel :for => :share_info do
1108
975
  _cbox :profile, :shared, :id => :share_info
1109
976
  self << '&#160; Share your info on your OpenID page'
@@ -1206,9 +1073,10 @@ module Pasaporte
1206
1073
  def self.create
1207
1074
  JulikState.create_schema
1208
1075
  self::Models.create_schema
1209
- LOGGER.warn "Deleting stale sessions"
1210
- JulikState::State.delete_all
1211
- LOGGER.warn "Deleting set throttles"
1212
- self::Models::Throttle.delete_all
1076
+ self::LOGGER.warn "Deleting sessions, assocs and nonces"
1077
+ [self::Models::Throttle, self::Models::Nonce,
1078
+ self::Models::Association, JulikState::State].each do | m |
1079
+ m.delete_all
1080
+ end
1213
1081
  end
1214
1082
  end