omniauth_openid_federation 1.3.2 → 2.0.0

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.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +32 -1
  3. data/README.md +119 -13
  4. data/app/controllers/omniauth_openid_federation/federation_controller.rb +2 -2
  5. data/config/polyrun_coverage.yml +15 -0
  6. data/config/polyrun_spec_quality.yml +29 -0
  7. data/examples/README_INTEGRATION_TESTING.md +3 -0
  8. data/examples/integration_test_flow.rb +10 -8
  9. data/examples/jobs/federation_cache_refresh_job.rb.example +7 -7
  10. data/examples/jobs/federation_files_generation_job.rb.example +3 -2
  11. data/examples/mock_op_server.rb +4 -5
  12. data/examples/mock_rp_server.rb +3 -3
  13. data/examples/standalone_multiple_endpoints_example.rb +494 -0
  14. data/lib/omniauth_openid_federation/access_token.rb +91 -450
  15. data/lib/omniauth_openid_federation/cache.rb +16 -0
  16. data/lib/omniauth_openid_federation/cache_adapter.rb +6 -3
  17. data/lib/omniauth_openid_federation/constants.rb +3 -0
  18. data/lib/omniauth_openid_federation/federation/entity_statement.rb +0 -4
  19. data/lib/omniauth_openid_federation/federation/entity_statement_validator.rb +2 -4
  20. data/lib/omniauth_openid_federation/federation/signed_jwks.rb +0 -1
  21. data/lib/omniauth_openid_federation/federation/trust_chain_resolver.rb +51 -6
  22. data/lib/omniauth_openid_federation/federation_endpoint.rb +1 -1
  23. data/lib/omniauth_openid_federation/http_client.rb +145 -32
  24. data/lib/omniauth_openid_federation/http_errors.rb +14 -0
  25. data/lib/omniauth_openid_federation/id_token.rb +19 -0
  26. data/lib/omniauth_openid_federation/jwe.rb +26 -0
  27. data/lib/omniauth_openid_federation/jwks/decode.rb +0 -13
  28. data/lib/omniauth_openid_federation/jwks/fetch.rb +16 -39
  29. data/lib/omniauth_openid_federation/jws.rb +2 -11
  30. data/lib/omniauth_openid_federation/jwt_response_decoder.rb +290 -0
  31. data/lib/omniauth_openid_federation/oidc_client.rb +101 -0
  32. data/lib/omniauth_openid_federation/rack.rb +2 -0
  33. data/lib/omniauth_openid_federation/rack_endpoint.rb +2 -2
  34. data/lib/omniauth_openid_federation/rate_limiter.rb +10 -12
  35. data/lib/omniauth_openid_federation/secure_compare.rb +15 -0
  36. data/lib/omniauth_openid_federation/strategy/authorization_request.rb +220 -0
  37. data/lib/omniauth_openid_federation/strategy/callback_handling.rb +135 -0
  38. data/lib/omniauth_openid_federation/strategy/client_construction.rb +101 -0
  39. data/lib/omniauth_openid_federation/strategy/client_entity_statement.rb +211 -0
  40. data/lib/omniauth_openid_federation/strategy/endpoint_resolution.rb +433 -0
  41. data/lib/omniauth_openid_federation/strategy/failure_handling.rb +75 -0
  42. data/lib/omniauth_openid_federation/strategy/id_token_decoding.rb +268 -0
  43. data/lib/omniauth_openid_federation/strategy/jwks_resolution.rb +268 -0
  44. data/lib/omniauth_openid_federation/strategy/provider_entity_statement.rb +134 -0
  45. data/lib/omniauth_openid_federation/strategy/userinfo_decoding.rb +61 -0
  46. data/lib/omniauth_openid_federation/strategy.rb +27 -1910
  47. data/lib/omniauth_openid_federation/tasks/authentication_flow_tester.rb +237 -0
  48. data/lib/omniauth_openid_federation/tasks/callback_processor.rb +235 -0
  49. data/lib/omniauth_openid_federation/tasks/client_keys_preparer.rb +96 -0
  50. data/lib/omniauth_openid_federation/tasks/local_endpoint_tester.rb +189 -0
  51. data/lib/omniauth_openid_federation/tasks/path_resolver.rb +20 -0
  52. data/lib/omniauth_openid_federation/tasks_helper.rb +28 -808
  53. data/lib/omniauth_openid_federation/time_helpers.rb +7 -0
  54. data/lib/omniauth_openid_federation/user_info.rb +15 -0
  55. data/lib/omniauth_openid_federation/utils.rb +10 -2
  56. data/lib/omniauth_openid_federation/validators.rb +43 -8
  57. data/lib/omniauth_openid_federation/version.rb +1 -1
  58. data/lib/omniauth_openid_federation.rb +9 -3
  59. data/lib/tasks/omniauth_openid_federation.rake +1 -2
  60. data/sig/omniauth_openid_federation.rbs +2 -1
  61. data/sig/strategy.rbs +6 -6
  62. metadata +181 -71
@@ -0,0 +1,494 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Standalone Multiple Auth Endpoints Example with Sinatra
5
+ #
6
+ # This example demonstrates how to use omniauth_openid_federation
7
+ # in a standalone Sinatra application with multiple authentication
8
+ # endpoints that differentiate entrance points (portal, admin, mobile).
9
+ #
10
+ # Features:
11
+ # - Multiple auth endpoints with different parameters
12
+ # - Entrance point tracking
13
+ # - Different redirects based on entrance point after callback
14
+ # - No Rails/Devise dependency - pure Sinatra
15
+ #
16
+ # Usage:
17
+ # ruby examples/standalone_multiple_endpoints_example.rb
18
+ #
19
+ # Access:
20
+ # http://localhost:9294/ - Home page
21
+ # http://localhost:9294/auth/openid_federation_portal - Portal login
22
+ # http://localhost:9294/auth/openid_federation_admin - Admin login
23
+ # http://localhost:9294/auth/openid_federation_mobile - Mobile login
24
+
25
+ require "bundler/setup"
26
+ require "sinatra"
27
+ require "omniauth"
28
+ require "omniauth_openid_federation"
29
+ require "json"
30
+ require "securerandom"
31
+
32
+ # Add the gem to the load path
33
+ $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
34
+
35
+ # Configuration
36
+ OP_ENTITY_STATEMENT_URL = ENV["OP_ENTITY_STATEMENT_URL"] || "https://provider.example.com/.well-known/openid-federation"
37
+ OP_ENTITY_STATEMENT_FINGERPRINT = ENV["OP_ENTITY_STATEMENT_FINGERPRINT"] # Optional: for verification
38
+ CLIENT_ID = ENV["OPENID_CLIENT_ID"] || "your-client-id"
39
+ REDIRECT_URI_BASE = ENV["OPENID_REDIRECT_URI_BASE"] || "http://localhost:9294"
40
+
41
+ # Load private key (auto-provisioned from environment or auto-generated)
42
+ def load_private_key
43
+ if ENV["OPENID_CLIENT_PRIVATE_KEY_BASE64"]
44
+ require "base64"
45
+ require "openssl"
46
+ OpenSSL::PKey::RSA.new(Base64.decode64(ENV["OPENID_CLIENT_PRIVATE_KEY_BASE64"]))
47
+ else
48
+ # Auto-generate keys if not provided (for development/testing)
49
+ # In production, always provide OPENID_CLIENT_PRIVATE_KEY_BASE64
50
+ require "openssl"
51
+ @auto_generated_key ||= OpenSSL::PKey::RSA.new(2048)
52
+ end
53
+ end
54
+
55
+ # Fetch and refresh OP entity statement (called on each request)
56
+ def fetch_op_entity_statement
57
+ return nil unless OP_ENTITY_STATEMENT_URL
58
+
59
+ begin
60
+ OmniauthOpenidFederation::Federation::EntityStatement.fetch!(
61
+ OP_ENTITY_STATEMENT_URL,
62
+ fingerprint: OP_ENTITY_STATEMENT_FINGERPRINT,
63
+ timeout: 10
64
+ )
65
+ rescue => e
66
+ puts "Warning: Failed to fetch OP entity statement: #{e.message}"
67
+ nil
68
+ end
69
+ end
70
+
71
+ # Configure session
72
+ set :session_secret, ENV.fetch("SESSION_SECRET") { SecureRandom.hex(32) }
73
+ enable :sessions
74
+
75
+ # Fetch/refresh OP entity statement on each request
76
+ before do
77
+ # Fetch OP entity statement to ensure we have latest metadata and keys
78
+ # This happens automatically in the strategy, but we can also do it here
79
+ # to warm up the cache and verify connectivity
80
+ fetch_op_entity_statement if request.path.start_with?("/auth/")
81
+ end
82
+
83
+ # Configure OmniAuth
84
+ OmniAuth.config.allowed_request_methods = [:get, :post]
85
+ OmniAuth.config.silence_get_warning = true
86
+
87
+ # Configure CSRF protection for request phase
88
+ OmniAuth.config.request_validation_phase = lambda do |env|
89
+ request = Rack::Request.new(env)
90
+ return true if request.path.end_with?("/callback")
91
+
92
+ session = env["rack.session"] || {}
93
+ token = request.params["authenticity_token"] || request.get_header("X-CSRF-Token")
94
+ expected_token = session[:_csrf_token] || session["_csrf_token"]
95
+
96
+ if token && expected_token
97
+ Rack::Utils.secure_compare(token.to_s, expected_token.to_s)
98
+ else
99
+ false
100
+ end
101
+ end
102
+
103
+ # Configure multiple OpenID Federation strategies
104
+ use OmniAuth::Builder do
105
+ # Strategy 1: Portal entrance point
106
+ provider(:openid_federation_portal,
107
+ strategy_class: OmniAuth::Strategies::OpenIDFederation,
108
+ name: :openid_federation_portal,
109
+ scope: [:openid, :profile, :email],
110
+ response_type: "code",
111
+ discovery: true,
112
+ client_auth_method: :jwt_bearer,
113
+ client_signing_alg: :RS256,
114
+ entity_statement_url: OP_ENTITY_STATEMENT_URL,
115
+ entity_statement_fingerprint: OP_ENTITY_STATEMENT_FINGERPRINT,
116
+ always_encrypt_request_object: false,
117
+ request_object_params: ["entrance_point", "portal_id", "acr_values"],
118
+ prepare_request_object_params: proc do |params|
119
+ # Always set entrance_point for portal
120
+ params["entrance_point"] = "portal"
121
+
122
+ # Combine config acr_values with form acr_values
123
+ form_acr_values = params["acr_values"]&.to_s&.strip
124
+ config_acr_values = (ENV["OPENID_ACR_VALUES_PORTAL"] || "").to_s.strip
125
+
126
+ if !config_acr_values.empty? && !form_acr_values.to_s.empty?
127
+ params["acr_values"] = "#{config_acr_values} #{form_acr_values}".strip
128
+ elsif !config_acr_values.empty?
129
+ params["acr_values"] = config_acr_values
130
+ end
131
+
132
+ # Add portal-specific parameter if provided
133
+ portal_id = ENV["OPENID_PORTAL_ID"]
134
+ params["portal_id"] = portal_id if portal_id && !portal_id.empty?
135
+
136
+ params
137
+ end,
138
+ client_options: {
139
+ identifier: CLIENT_ID,
140
+ redirect_uri: "#{REDIRECT_URI_BASE}/auth/openid_federation_portal/callback",
141
+ private_key: load_private_key
142
+ })
143
+
144
+ # Strategy 2: Admin entrance point
145
+ provider(:openid_federation_admin,
146
+ strategy_class: OmniAuth::Strategies::OpenIDFederation,
147
+ name: :openid_federation_admin,
148
+ scope: [:openid, :profile, :email],
149
+ response_type: "code",
150
+ discovery: true,
151
+ client_auth_method: :jwt_bearer,
152
+ client_signing_alg: :RS256,
153
+ entity_statement_url: OP_ENTITY_STATEMENT_URL,
154
+ entity_statement_fingerprint: OP_ENTITY_STATEMENT_FINGERPRINT,
155
+ always_encrypt_request_object: false,
156
+ request_object_params: ["entrance_point", "acr_values", "prompt"],
157
+ prepare_request_object_params: proc do |params|
158
+ # Always set entrance_point for admin
159
+ params["entrance_point"] = "admin"
160
+
161
+ # Admin requires higher ACR level
162
+ admin_acr = ENV["OPENID_ACR_VALUES_ADMIN"] || "urn:mace:incommon:iap:silver"
163
+ params["acr_values"] = admin_acr
164
+
165
+ # Force re-authentication for admin
166
+ params["prompt"] = "login consent"
167
+
168
+ params
169
+ end,
170
+ client_options: {
171
+ identifier: CLIENT_ID,
172
+ redirect_uri: "#{REDIRECT_URI_BASE}/auth/openid_federation_admin/callback",
173
+ private_key: load_private_key
174
+ })
175
+
176
+ # Strategy 3: Mobile app entrance point
177
+ provider(:openid_federation_mobile,
178
+ strategy_class: OmniAuth::Strategies::OpenIDFederation,
179
+ name: :openid_federation_mobile,
180
+ scope: [:openid, :profile, :email],
181
+ response_type: "code",
182
+ discovery: true,
183
+ client_auth_method: :jwt_bearer,
184
+ client_signing_alg: :RS256,
185
+ entity_statement_url: OP_ENTITY_STATEMENT_URL,
186
+ entity_statement_fingerprint: OP_ENTITY_STATEMENT_FINGERPRINT,
187
+ always_encrypt_request_object: false,
188
+ request_object_params: ["entrance_point", "device_id", "app_version"],
189
+ prepare_request_object_params: proc do |params|
190
+ # Always set entrance_point for mobile
191
+ params["entrance_point"] = "mobile"
192
+
193
+ # Add device-specific parameters if provided
194
+ params["device_id"] = params["device_id"] if params["device_id"] && !params["device_id"].to_s.empty?
195
+ params["app_version"] = params["app_version"] if params["app_version"] && !params["app_version"].to_s.empty?
196
+
197
+ params
198
+ end,
199
+ client_options: {
200
+ identifier: CLIENT_ID,
201
+ redirect_uri: "#{REDIRECT_URI_BASE}/auth/openid_federation_mobile/callback",
202
+ private_key: load_private_key
203
+ })
204
+ end
205
+
206
+ # Helper to generate CSRF token
207
+ def csrf_token
208
+ session[:_csrf_token] ||= SecureRandom.hex(32)
209
+ end
210
+
211
+ # Helper to get entrance point redirect URL
212
+ def redirect_for_entrance_point(entrance_point, user_info = {})
213
+ case entrance_point
214
+ when "portal"
215
+ "/portal/dashboard"
216
+ when "admin"
217
+ "/admin/dashboard"
218
+ when "mobile"
219
+ "/mobile/home"
220
+ else
221
+ "/dashboard"
222
+ end
223
+ end
224
+
225
+ # Home page
226
+ get "/" do
227
+ erb :index
228
+ end
229
+
230
+ # Portal dashboard (example)
231
+ get "/portal/dashboard" do
232
+ content_type :json
233
+ {
234
+ status: "success",
235
+ entrance_point: "portal",
236
+ message: "Welcome to Portal Dashboard",
237
+ user: params[:user]
238
+ }.to_json
239
+ end
240
+
241
+ # Admin dashboard (example)
242
+ get "/admin/dashboard" do
243
+ content_type :json
244
+ {
245
+ status: "success",
246
+ entrance_point: "admin",
247
+ message: "Welcome to Admin Dashboard",
248
+ user: params[:user]
249
+ }.to_json
250
+ end
251
+
252
+ # Mobile home (example)
253
+ get "/mobile/home" do
254
+ content_type :json
255
+ {
256
+ status: "success",
257
+ entrance_point: "mobile",
258
+ message: "Welcome to Mobile App",
259
+ user: params[:user]
260
+ }.to_json
261
+ end
262
+
263
+ # Default dashboard (fallback)
264
+ get "/dashboard" do
265
+ content_type :json
266
+ {
267
+ status: "success",
268
+ message: "Welcome",
269
+ user: params[:user]
270
+ }.to_json
271
+ end
272
+
273
+ # Callback handlers for each entrance point
274
+ get "/auth/:provider/callback" do
275
+ auth = request.env["omniauth.auth"]
276
+
277
+ if auth
278
+ # Extract entrance point from strategy name or raw_info
279
+ provider_name = params[:provider]
280
+ entrance_point = case provider_name
281
+ when "openid_federation_portal"
282
+ "portal"
283
+ when "openid_federation_admin"
284
+ "admin"
285
+ when "openid_federation_mobile"
286
+ "mobile"
287
+ else
288
+ # Try to get from raw_info if provider returns it
289
+ raw_info = auth.extra[:raw_info] || {}
290
+ raw_info["entrance_point"] || "unknown"
291
+ end
292
+
293
+ # Extract user information
294
+ user_info = {
295
+ provider: auth.provider,
296
+ uid: auth.uid,
297
+ email: auth.info.email,
298
+ name: auth.info.name,
299
+ first_name: auth.info.first_name,
300
+ last_name: auth.info.last_name,
301
+ raw_info: auth.extra[:raw_info]
302
+ }
303
+
304
+ # Store user info in session
305
+ session[:user] = user_info
306
+ session[:entrance_point] = entrance_point
307
+
308
+ # Extract additional parameters from raw_info if provider returns them
309
+ raw_info = auth.extra[:raw_info] || {}
310
+ session[:portal_id] = raw_info["portal_id"] if raw_info["portal_id"]
311
+ session[:device_id] = raw_info["device_id"] if raw_info["device_id"]
312
+
313
+ # Redirect based on entrance point
314
+ redirect redirect_for_entrance_point(entrance_point, user_info)
315
+ else
316
+ redirect "/auth/failure"
317
+ end
318
+ end
319
+
320
+ # Failure handler
321
+ get "/auth/failure" do
322
+ error_type = request.env["omniauth.error.type"]
323
+ error_message = request.env["omniauth.error"]&.message || "Authentication failed"
324
+
325
+ content_type :json
326
+ {
327
+ status: "error",
328
+ error_type: error_type,
329
+ message: error_message
330
+ }.to_json
331
+ end
332
+
333
+ # User info endpoint (example)
334
+ get "/user/info" do
335
+ if session[:user]
336
+ content_type :json
337
+ {
338
+ status: "success",
339
+ user: session[:user],
340
+ entrance_point: session[:entrance_point],
341
+ portal_id: session[:portal_id],
342
+ device_id: session[:device_id]
343
+ }.to_json
344
+ else
345
+ status 401
346
+ content_type :json
347
+ { status: "error", message: "Not authenticated" }.to_json
348
+ end
349
+ end
350
+
351
+ # Run the server
352
+ if __FILE__ == $0
353
+ require "sinatra"
354
+
355
+ puts "Starting standalone multiple endpoints example server..."
356
+ puts "Server: http://localhost:9294"
357
+ puts ""
358
+ puts "Endpoints:"
359
+ puts " GET / - Home page with login links"
360
+ puts " POST /auth/openid_federation_portal - Portal login"
361
+ puts " POST /auth/openid_federation_admin - Admin login"
362
+ puts " POST /auth/openid_federation_mobile - Mobile login"
363
+ puts " GET /auth/:provider/callback - OAuth callback (redirects based on entrance point)"
364
+ puts " GET /auth/failure - Authentication failure handler"
365
+ puts " GET /user/info - Current user info (JSON)"
366
+ puts ""
367
+ puts "Environment variables:"
368
+ puts " OP_ENTITY_STATEMENT_URL - Provider entity statement URL (required)"
369
+ puts " OP_ENTITY_STATEMENT_FINGERPRINT - Expected fingerprint for verification (optional)"
370
+ puts " OPENID_CLIENT_ID - Your client ID (required)"
371
+ puts " OPENID_REDIRECT_URI_BASE - Base URL for redirect URIs (default: http://localhost:9294)"
372
+ puts " OPENID_CLIENT_PRIVATE_KEY_BASE64 - Base64-encoded private key (auto-generated if not set)"
373
+ puts " OPENID_ACR_VALUES_PORTAL - ACR values for portal (optional)"
374
+ puts " OPENID_ACR_VALUES_ADMIN - ACR values for admin (optional)"
375
+ puts " OPENID_PORTAL_ID - Portal ID (optional)"
376
+ puts " SESSION_SECRET - Session secret (auto-generated if not set)"
377
+ puts ""
378
+ puts "Note: OP entity statement and keys are automatically fetched/refreshed on each request."
379
+ puts ""
380
+
381
+ set :port, ENV.fetch("PORT", 9294).to_i
382
+ set :bind, ENV.fetch("HOST", "localhost")
383
+ end
384
+
385
+ # View templates
386
+ __END__
387
+
388
+ @@index
389
+ <!DOCTYPE html>
390
+ <html>
391
+ <head>
392
+ <title>Multiple Auth Endpoints Example</title>
393
+ <style>
394
+ body {
395
+ font-family: Arial, sans-serif;
396
+ max-width: 800px;
397
+ margin: 50px auto;
398
+ padding: 20px;
399
+ }
400
+ h1 {
401
+ color: #333;
402
+ }
403
+ .login-section {
404
+ margin: 30px 0;
405
+ padding: 20px;
406
+ border: 1px solid #ddd;
407
+ border-radius: 5px;
408
+ }
409
+ .login-section h2 {
410
+ margin-top: 0;
411
+ color: #555;
412
+ }
413
+ .login-button {
414
+ display: inline-block;
415
+ padding: 10px 20px;
416
+ background-color: #007bff;
417
+ color: white;
418
+ text-decoration: none;
419
+ border-radius: 4px;
420
+ margin: 5px 0;
421
+ }
422
+ .login-button:hover {
423
+ background-color: #0056b3;
424
+ }
425
+ .info {
426
+ background-color: #f0f0f0;
427
+ padding: 15px;
428
+ border-radius: 4px;
429
+ margin: 20px 0;
430
+ }
431
+ </style>
432
+ </head>
433
+ <body>
434
+ <h1>OpenID Federation - Multiple Auth Endpoints Example</h1>
435
+
436
+ <div class="info">
437
+ <p>This example demonstrates multiple authentication endpoints with different entrance points.</p>
438
+ <p>Each endpoint sends different parameters and redirects to different destinations after authentication.</p>
439
+ </div>
440
+
441
+ <div class="login-section">
442
+ <h2>Portal Login</h2>
443
+ <p>Login via portal entrance point. Will redirect to <code>/portal/dashboard</code> after authentication.</p>
444
+ <form method="post" action="/auth/openid_federation_portal">
445
+ <input type="hidden" name="authenticity_token" value="<%= csrf_token %>">
446
+ <input type="hidden" name="portal_id" value="main_portal">
447
+ <input type="hidden" name="acr_values" value="urn:mace:incommon:iap:bronze">
448
+ <button type="submit" class="login-button">Login via Portal</button>
449
+ </form>
450
+ </div>
451
+
452
+ <div class="login-section">
453
+ <h2>Admin Login</h2>
454
+ <p>Login via admin entrance point. Will redirect to <code>/admin/dashboard</code> after authentication.</p>
455
+ <p><strong>Note:</strong> Admin login requires higher ACR level and forces re-authentication.</p>
456
+ <form method="post" action="/auth/openid_federation_admin">
457
+ <input type="hidden" name="authenticity_token" value="<%= csrf_token %>">
458
+ <button type="submit" class="login-button">Login as Admin</button>
459
+ </form>
460
+ </div>
461
+
462
+ <div class="login-section">
463
+ <h2>Mobile Login</h2>
464
+ <p>Login via mobile entrance point. Will redirect to <code>/mobile/home</code> after authentication.</p>
465
+ <form method="post" action="/auth/openid_federation_mobile">
466
+ <input type="hidden" name="authenticity_token" value="<%= csrf_token %>">
467
+ <input type="hidden" name="device_id" value="device_123">
468
+ <input type="hidden" name="app_version" value="1.0.0">
469
+ <button type="submit" class="login-button">Login via Mobile</button>
470
+ </form>
471
+ </div>
472
+
473
+ <div class="info">
474
+ <h3>How it works:</h3>
475
+ <ul>
476
+ <li>Each endpoint has a different <code>name</code> (portal, admin, mobile)</li>
477
+ <li>Each endpoint sends different custom parameters in the signed request object</li>
478
+ <li>The <code>prepare_request_object_params</code> proc customizes parameters per endpoint</li>
479
+ <li>After callback, users are redirected based on their entrance point</li>
480
+ <li>User info and entrance point are stored in session</li>
481
+ </ul>
482
+ </div>
483
+
484
+ <% if session[:user] %>
485
+ <div class="info">
486
+ <h3>Current Session:</h3>
487
+ <p><strong>Entrance Point:</strong> <%= session[:entrance_point] %></p>
488
+ <p><strong>User:</strong> <%= session[:user][:email] || session[:user][:uid] %></p>
489
+ <p><a href="/user/info">View full user info (JSON)</a></p>
490
+ </div>
491
+ <% end %>
492
+ </body>
493
+ </html>
494
+