standard_id 0.1.6 → 0.1.7

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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +368 -22
  3. data/app/controllers/concerns/standard_id/set_current_request_details.rb +19 -0
  4. data/app/controllers/concerns/standard_id/social_authentication.rb +86 -37
  5. data/app/controllers/concerns/standard_id/web_authentication.rb +29 -1
  6. data/app/controllers/standard_id/api/base_controller.rb +1 -0
  7. data/app/controllers/standard_id/api/oauth/callback/providers_controller.rb +7 -18
  8. data/app/controllers/standard_id/web/auth/callback/providers_controller.rb +33 -37
  9. data/app/controllers/standard_id/web/base_controller.rb +1 -0
  10. data/app/controllers/standard_id/web/login_controller.rb +6 -19
  11. data/app/controllers/standard_id/web/signup_controller.rb +3 -6
  12. data/app/forms/standard_id/web/signup_form.rb +32 -1
  13. data/app/models/standard_id/browser_session.rb +8 -0
  14. data/app/models/standard_id/client_secret_credential.rb +11 -0
  15. data/app/models/standard_id/device_session.rb +4 -0
  16. data/app/models/standard_id/identifier.rb +28 -0
  17. data/app/models/standard_id/service_session.rb +1 -1
  18. data/app/models/standard_id/session.rb +16 -2
  19. data/app/views/standard_id/web/auth/callback/providers/{apple_mobile.html.erb → mobile_callback.html.erb} +1 -1
  20. data/config/routes/api.rb +1 -2
  21. data/config/routes/web.rb +4 -3
  22. data/lib/generators/standard_id/install/templates/standard_id.rb +11 -8
  23. data/lib/standard_config/config.rb +3 -12
  24. data/lib/standard_config/config_provider.rb +6 -6
  25. data/lib/standard_config/schema.rb +2 -2
  26. data/lib/standard_id/account_locking.rb +86 -0
  27. data/lib/standard_id/account_status.rb +45 -0
  28. data/lib/standard_id/api/authentication_guard.rb +40 -1
  29. data/lib/standard_id/api/token_manager.rb +1 -1
  30. data/lib/standard_id/config/schema.rb +11 -9
  31. data/lib/standard_id/current_attributes.rb +9 -0
  32. data/lib/standard_id/engine.rb +9 -0
  33. data/lib/standard_id/errors.rb +12 -0
  34. data/lib/standard_id/events/definitions.rb +157 -0
  35. data/lib/standard_id/events/event.rb +123 -0
  36. data/lib/standard_id/events/subscribers/account_locking_subscriber.rb +17 -0
  37. data/lib/standard_id/events/subscribers/account_status_subscriber.rb +17 -0
  38. data/lib/standard_id/events/subscribers/base.rb +165 -0
  39. data/lib/standard_id/events/subscribers/logging_subscriber.rb +122 -0
  40. data/lib/standard_id/events.rb +137 -0
  41. data/lib/standard_id/oauth/authorization_code_flow.rb +10 -0
  42. data/lib/standard_id/oauth/client_credentials_flow.rb +31 -0
  43. data/lib/standard_id/oauth/password_flow.rb +36 -4
  44. data/lib/standard_id/oauth/passwordless_otp_flow.rb +38 -2
  45. data/lib/standard_id/oauth/subflows/social_login_grant.rb +11 -22
  46. data/lib/standard_id/oauth/token_grant_flow.rb +22 -1
  47. data/lib/standard_id/passwordless/base_strategy.rb +32 -0
  48. data/lib/standard_id/provider_registry.rb +73 -0
  49. data/lib/standard_id/{social_providers → providers}/apple.rb +46 -7
  50. data/lib/standard_id/providers/base.rb +242 -0
  51. data/lib/standard_id/{social_providers → providers}/google.rb +26 -7
  52. data/lib/standard_id/version.rb +1 -1
  53. data/lib/standard_id/web/authentication_guard.rb +29 -0
  54. data/lib/standard_id/web/session_manager.rb +39 -1
  55. data/lib/standard_id/web/token_manager.rb +2 -2
  56. data/lib/standard_id.rb +13 -2
  57. metadata +18 -6
  58. data/lib/standard_id/social_providers/response_builder.rb +0 -18
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7a8069c98c5ede27fc1f485806e5a9b150d4f43294d177f59fe89008e11bd289
4
- data.tar.gz: e88dae408bce4db81c42c780985d9e4a833b4864636a2d10b5ff979407dc9562
3
+ metadata.gz: 2878f305d6dfe83c5a1c0851cde68602cbd17899b1a676981afd8166f56995e0
4
+ data.tar.gz: 4c1802cc0bb54045165eb42289d75dda85db9a8838d61a6c1b4e7a7ff50e7828
5
5
  SHA512:
6
- metadata.gz: 19f08dcd3da104952f2313669bc7bceb18ef0ce866ff234a0beb966e64d83294cefc76886c2f5e74080b935271444afeae0057deadcbac7bcfda40f6dd0441b4
7
- data.tar.gz: 2531fba6a76ea0dd4800cebe0444a86cdea8ab516c64888b40b7d60aa681a957af2f3f4fb9622aebc6cf6f25b44a3caf0b3d03f16dcc673ce2f0a30c79599211
6
+ metadata.gz: 8073e2e1f0208261525be8218960ef6481e8a5558f281a56baf2316b4750f9557b37365831b9aa530706e2d52d6c088e3cc3e9c23c4a4ef722d641f2041ea357
7
+ data.tar.gz: a803c19a19f5fbcc3acae0511f629c6d4b1520d67a54cfa16d062fcc60333083d28cd5149a6658bfb16e37ae3a2ff1d7cdf06d657d16130010a08c67b8c851ff
data/README.md CHANGED
@@ -123,9 +123,11 @@ StandardId.configure do |config|
123
123
  # config.use_inertia = true
124
124
  # config.inertia_component_namespace = "auth"
125
125
 
126
- # Passwordless delivery callbacks
127
- # config.passwordless_email_sender = ->(email, code) { UserMailer.send_code(email, code).deliver_now }
128
- # config.passwordless_sms_sender = ->(phone, code) { SmsService.send_code(phone, code) }
126
+ # Session lifetimes
127
+ # config.session.browser_session_lifetime = 86400 # 24 hours (web sessions)
128
+ # config.session.browser_session_remember_me_lifetime = 2_592_000 # 30 days (remember me cookies)
129
+ # config.session.device_session_lifetime = 2_592_000 # 30 days (API device sessions)
130
+ # config.session.service_session_lifetime = 7_776_000 # 90 days (service-to-service sessions)
129
131
 
130
132
  # Subset configuration
131
133
  # config.password.minimum_length = 12
@@ -186,21 +188,24 @@ StandardId.configure do |config|
186
188
  name: social_info[:name] || social_info[:given_name]
187
189
  }
188
190
  }
189
-
190
- # Optional: run a callback whenever a social login completes
191
- config.social.social_callback = ->(social_info:, provider:, tokens:, account:) {
192
- AuditLog.social_login(
193
- provider: provider,
194
- email: social_info[:email],
195
- tokens: tokens,
196
- account_id: account.id,
197
- )
198
- }
199
191
  end
200
192
  ```
201
193
 
202
194
  `social_info` is an indifferent-access hash containing at least `email`, `name`, and `provider_id`.
203
195
 
196
+ To handle social login completion (e.g., for analytics or audit logging), subscribe to the `SOCIAL_AUTH_COMPLETED` event:
197
+
198
+ ```ruby
199
+ # config/initializers/standard_id_events.rb
200
+ StandardId::Events.subscribe(StandardId::Events::SOCIAL_AUTH_COMPLETED) do |event|
201
+ Analytics.track_social_login(
202
+ provider: event[:provider],
203
+ account_id: event[:account].id,
204
+ tokens: event[:tokens]
205
+ )
206
+ end
207
+ ```
208
+
204
209
  ### Inertia.js Integration
205
210
 
206
211
  StandardId supports [Inertia.js](https://inertiajs.com/) for modern React, Vue, or Svelte frontends. When enabled, web controllers render Inertia components instead of ERB views.
@@ -355,22 +360,363 @@ end
355
360
 
356
361
  This will redirect unauthenticated users to the login page using `inertia_location` for Inertia requests, ensuring proper SPA navigation.
357
362
 
358
- ### Passwordless Authentication
363
+ ### Passwordless Code Delivery
364
+
365
+ Subscribe to the `PASSWORDLESS_CODE_GENERATED` event to deliver OTP codes:
366
+
367
+ ```ruby
368
+ # config/initializers/standard_id_events.rb
369
+ StandardId::Events.subscribe(StandardId::Events::PASSWORDLESS_CODE_GENERATED) do |event|
370
+ case event[:channel]
371
+ when "email"
372
+ UserMailer.send_code(event[:identifier], event[:code_challenge].code).deliver_now
373
+ when "sms"
374
+ SmsService.send_code(event[:identifier], event[:code_challenge].code)
375
+ end
376
+ end
377
+ ```
378
+
379
+ Event payload includes:
380
+ - `channel` - `"email"` or `"sms"`
381
+ - `identifier` - The email address or phone number
382
+ - `code_challenge` - The code challenge object with `.code` method
383
+ - `expires_at` - When the code expires
384
+
385
+ > **Note**: If you're using the deprecated `passwordless_email_sender` or `passwordless_sms_sender` callbacks, see the [Migration Guide](docs/MIGRATION_GUIDE.md) for upgrade instructions.
386
+
387
+ ## Event System
388
+
389
+ StandardId emits events throughout the authentication lifecycle using `ActiveSupport::Notifications`. This enables decoupled handling of cross-cutting concerns like logging, analytics, audit trails, and webhooks.
390
+
391
+ ### Enabling Event Logging
392
+
393
+ Enable the built-in structured logging subscriber:
359
394
 
360
395
  ```ruby
361
396
  StandardId.configure do |config|
362
- # Email delivery
363
- config.passwordless_email_sender = ->(email, code) {
364
- UserMailer.send_code(email, code).deliver_now
365
- }
397
+ config.events.enable_logging = true
398
+ end
399
+ ```
366
400
 
367
- # SMS delivery
368
- config.passwordless_sms_sender = ->(phone, code) {
369
- SmsService.send_code(phone, code)
370
- }
401
+ This outputs JSON-structured logs for all authentication events:
402
+
403
+ ```json
404
+ {
405
+ "subject": "standard_id.authentication.attempt.succeeded",
406
+ "severity": "info",
407
+ "duration": 50.25,
408
+ "account_id": 123,
409
+ "auth_method": "password",
410
+ "ip_address": "192.168.1.1"
411
+ }
412
+ ```
413
+
414
+ ### Available Events
415
+
416
+ | Category | Events |
417
+ |----------|--------|
418
+ | **Authentication** | `authentication.attempt.started`, `authentication.attempt.succeeded`, `authentication.attempt.failed`, `authentication.password.validated`, `authentication.password.failed`, `authentication.otp.validated`, `authentication.otp.failed` |
419
+ | **Session** | `session.creating`, `session.created`, `session.validating`, `session.validated`, `session.expired`, `session.revoked`, `session.refreshed` |
420
+ | **Account** | `account.creating`, `account.created`, `account.verified`, `account.status_changed`, `account.activated`, `account.deactivated`, `account.locked`, `account.unlocked` |
421
+ | **Identifier** | `identifier.created`, `identifier.verification.started`, `identifier.verification.succeeded`, `identifier.verification.failed`, `identifier.linked` |
422
+ | **OAuth** | `oauth.authorization.requested`, `oauth.authorization.granted`, `oauth.authorization.denied`, `oauth.token.issuing`, `oauth.token.issued`, `oauth.token.refreshed`, `oauth.code.consumed` |
423
+ | **Passwordless** | `passwordless.code.requested`, `passwordless.code.generated`, `passwordless.code.sent`, `passwordless.code.verified`, `passwordless.code.failed`, `passwordless.account.created` |
424
+ | **Social** | `social.auth.started`, `social.auth.callback_received`, `social.user_info.fetched`, `social.account.created`, `social.account.linked`, `social.auth.completed` |
425
+ | **Credential** | `credential.password.created`, `credential.password.reset_initiated`, `credential.password.reset_completed`, `credential.password.changed`, `credential.client_secret.created`, `credential.client_secret.rotated` |
426
+
427
+ ### Subscribing to Events
428
+
429
+ #### Block-based (simple)
430
+
431
+ ```ruby
432
+ # config/initializers/standard_id_events.rb
433
+ StandardId::Events.subscribe(StandardId::Events::AUTHENTICATION_SUCCEEDED) do |event|
434
+ Analytics.track_login(
435
+ account_id: event[:account].id,
436
+ method: event[:auth_method],
437
+ ip: event[:ip_address]
438
+ )
439
+ end
440
+
441
+ # Subscribe to multiple events at once
442
+ StandardId::Events.subscribe(
443
+ StandardId::Events::SESSION_CREATING,
444
+ StandardId::Events::SESSION_VALIDATING,
445
+ StandardId::Events::OAUTH_TOKEN_ISSUING
446
+ ) do |event|
447
+ # Handle all three events with the same block
448
+ check_rate_limit(event[:account], event[:ip_address])
449
+ end
450
+
451
+ # Subscribe to events with pattern matching
452
+ StandardId::Events.subscribe(/social/) do |event|
453
+ Rails.logger.info("Social event: #{event.name}")
454
+ end
455
+ ```
456
+
457
+ #### Class-based (complex logic)
458
+
459
+ ```ruby
460
+ # app/subscribers/audit_subscriber.rb
461
+ class AuditSubscriber < StandardId::Events::Subscribers::Base
462
+ subscribe_to StandardId::Events::AUTHENTICATION_SUCCEEDED
463
+ subscribe_to StandardId::Events::AUTHENTICATION_FAILED
464
+ subscribe_to StandardId::Events::SESSION_REVOKED
465
+
466
+ def call(event)
467
+ AuditLog.create!(
468
+ event_type: event.short_name,
469
+ account_id: event[:account]&.id,
470
+ ip_address: event[:ip_address],
471
+ metadata: event.payload
472
+ )
473
+ end
474
+ end
475
+
476
+ # config/initializers/standard_id_events.rb
477
+ AuditSubscriber.attach
478
+ ```
479
+
480
+ ## Account Status (Activation/Deactivation)
481
+
482
+ StandardId provides an optional `AccountStatus` concern for managing account activation and deactivation. This uses Rails enum with the event system to enforce status checks and handle side effects without modifying core authentication logic.
483
+
484
+ ### Setup
485
+
486
+ 1. Add a migration for the status column. For PostgreSQL (recommended), use a native enum type:
487
+
488
+ ```ruby
489
+ # PostgreSQL with native enum (recommended)
490
+ class AddStatusToUsers < ActiveRecord::Migration[8.0]
491
+ def up
492
+ create_enum :account_status, %w[active inactive]
493
+
494
+ add_column :users, :status, :enum, enum_type: :account_status, default: "active", null: false
495
+ add_column :users, :activated_at, :datetime
496
+ add_column :users, :deactivated_at, :datetime
497
+ end
498
+
499
+ def down
500
+ remove_column :users, :status
501
+ remove_column :users, :activated_at
502
+ remove_column :users, :deactivated_at
503
+
504
+ drop_enum :account_status
505
+ end
506
+ end
507
+ ```
508
+
509
+ For other databases (MySQL, SQLite), use a string column:
510
+
511
+ ```ruby
512
+ # String column (MySQL, SQLite)
513
+ class AddStatusToUsers < ActiveRecord::Migration[8.0]
514
+ def change
515
+ add_column :users, :status, :string, default: "active", null: false
516
+ add_column :users, :activated_at, :datetime
517
+ add_column :users, :deactivated_at, :datetime
518
+ add_index :users, :status
519
+ end
520
+ end
521
+ ```
522
+
523
+ 2. Include the concern in your account model:
524
+
525
+ ```ruby
526
+ class User < ApplicationRecord
527
+ include StandardId::AccountStatus
528
+ # ...
371
529
  end
372
530
  ```
373
531
 
532
+ The concern works with both PostgreSQL enum and string columns - Rails enum handles both transparently.
533
+
534
+ ### Usage
535
+
536
+ ```ruby
537
+ # Deactivate an account
538
+ user.deactivate!
539
+ # => Emits ACCOUNT_DEACTIVATED event
540
+ # => All active sessions are automatically revoked
541
+
542
+ # Reactivate an account
543
+ user.activate!
544
+ # => Emits ACCOUNT_ACTIVATED event
545
+ # => User can log in again
546
+
547
+ # Check status
548
+ user.active? # => true/false
549
+ user.inactive? # => true/false
550
+
551
+ # Query scopes
552
+ User.active # => Users with status 'active'
553
+ User.inactive # => Users with status 'inactive'
554
+ ```
555
+
556
+ ### Handling AccountDeactivatedError
557
+
558
+ When an inactive account attempts to authenticate, `StandardId::AccountDeactivatedError` is raised. You need to handle this error in your application controller:
559
+
560
+ ```ruby
561
+ # app/controllers/application_controller.rb
562
+ class ApplicationController < ActionController::Base
563
+ include StandardId::WebAuthentication
564
+
565
+ rescue_from StandardId::AccountDeactivatedError, with: :handle_account_deactivated
566
+
567
+ private
568
+
569
+ def handle_account_deactivated
570
+ # For web requests, redirect with a message
571
+ redirect_to login_path, alert: "Your account has been deactivated. Please contact support."
572
+ end
573
+ end
574
+ ```
575
+
576
+ For API controllers:
577
+
578
+ ```ruby
579
+ # app/controllers/api/base_controller.rb
580
+ class Api::BaseController < ActionController::API
581
+ include StandardId::ApiAuthentication
582
+
583
+ rescue_from StandardId::AccountDeactivatedError, with: :handle_account_deactivated
584
+
585
+ private
586
+
587
+ def handle_account_deactivated
588
+ render json: {
589
+ error: "account_deactivated",
590
+ message: "Your account has been deactivated"
591
+ }, status: :forbidden
592
+ end
593
+ end
594
+ ```
595
+
596
+ ## Account Locking (Administrative Security)
597
+
598
+ StandardId provides an optional `AccountLocking` concern for administrative account locking. This is distinct from account deactivation - locking is for security enforcement by administrators, while deactivation is for lifecycle management.
599
+
600
+ ### Key Differences from Account Deactivation
601
+
602
+ | Feature | Account Status | Account Locking |
603
+ |---------|---------------|-----------------|
604
+ | **Purpose** | Lifecycle management | Security enforcement |
605
+ | **Who Controls** | System/User | Admin/Staff only |
606
+ | **User Reversible** | Yes (future) | No |
607
+ | **Use Cases** | Inactivity, user choice | Policy violation, security incident, fraud |
608
+
609
+ An account can be in any combination:
610
+ - Active + Unlocked ✅ (normal operation)
611
+ - Active + Locked ⚠️ (admin locked for security)
612
+ - Inactive + Unlocked ⚠️ (deactivated but not locked)
613
+ - Inactive + Locked 🚫 (both restrictions apply)
614
+
615
+ ### Setup
616
+
617
+ 1. Add a migration for the locking columns:
618
+
619
+ ```ruby
620
+ class AddLockingToUsers < ActiveRecord::Migration[8.0]
621
+ def change
622
+ add_column :users, :locked, :boolean, default: false, null: false
623
+ add_column :users, :locked_at, :datetime
624
+ add_column :users, :lock_reason, :string
625
+ add_column :users, :locked_by_id, :integer
626
+ add_column :users, :locked_by_type, :string
627
+ add_column :users, :unlocked_at, :datetime
628
+ add_column :users, :unlocked_by_id, :integer
629
+ add_column :users, :unlocked_by_type, :string
630
+
631
+ add_index :users, :locked
632
+ add_index :users, [:locked_by_type, :locked_by_id]
633
+ end
634
+ end
635
+ ```
636
+
637
+ 2. Include the concern in your account model:
638
+
639
+ ```ruby
640
+ class User < ApplicationRecord
641
+ include StandardId::AccountLocking # For admin locking
642
+ include StandardId::AccountStatus # Optional: for activation/deactivation
643
+ # ...
644
+ end
645
+ ```
646
+
647
+ ### Usage
648
+
649
+ ```ruby
650
+ # Lock an account (revokes all active sessions immediately)
651
+ user.lock!(reason: "Suspicious activity detected", locked_by: current_admin)
652
+ # => Emits ACCOUNT_LOCKED event
653
+ # => All active sessions (browser, device, service) are revoked
654
+
655
+ # Unlock an account (user must log in again)
656
+ user.unlock!(unlocked_by: current_admin)
657
+ # => Emits ACCOUNT_UNLOCKED event
658
+ # => User can log in again
659
+
660
+ # Check lock status
661
+ user.locked? # => true/false
662
+ user.unlocked? # => true/false
663
+
664
+ # Query scopes
665
+ User.locked # => Users with locked = true
666
+ User.unlocked # => Users with locked = false
667
+
668
+ # Combine with AccountStatus scopes
669
+ User.unlocked.active # => Users who can log in
670
+ ```
671
+
672
+ ### Handling AccountLockedError
673
+
674
+ When a locked account attempts to authenticate, `StandardId::AccountLockedError` is raised. The error includes metadata about the lock:
675
+
676
+ ```ruby
677
+ # app/controllers/application_controller.rb
678
+ class ApplicationController < ActionController::Base
679
+ include StandardId::WebAuthentication
680
+
681
+ rescue_from StandardId::AccountLockedError, with: :handle_account_locked
682
+
683
+ private
684
+
685
+ def handle_account_locked(error)
686
+ # error.account - The locked account
687
+ # error.lock_reason - Why the account was locked
688
+ # error.locked_at - When the account was locked
689
+ redirect_to login_path, alert: "Your account has been locked. Please contact support."
690
+ end
691
+ end
692
+ ```
693
+
694
+ For API controllers:
695
+
696
+ ```ruby
697
+ # app/controllers/api/base_controller.rb
698
+ class Api::BaseController < ActionController::API
699
+ include StandardId::ApiAuthentication
700
+
701
+ rescue_from StandardId::AccountLockedError, with: :handle_account_locked
702
+
703
+ private
704
+
705
+ def handle_account_locked(error)
706
+ render json: {
707
+ error: "account_locked",
708
+ message: "Your account has been locked. Please contact support.",
709
+ locked_at: error.locked_at&.iso8601
710
+ # Note: Consider not exposing lock_reason to end users for security
711
+ }, status: :forbidden
712
+ end
713
+ end
714
+ ```
715
+
716
+ ### Event Subscriptions
717
+
718
+ Both `AccountStatus` and `AccountLocking` subscribe to the same events (`OAUTH_TOKEN_ISSUING`, `SESSION_CREATING`, `SESSION_VALIDATING`). The lock check runs alongside the status check - authentication fails if either condition prevents access.
719
+
374
720
  ## Usage Examples
375
721
 
376
722
  ### Web Authentication
@@ -0,0 +1,19 @@
1
+ module StandardId
2
+ module SetCurrentRequestDetails
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ before_action :set_current_request_details
7
+ end
8
+
9
+ private
10
+
11
+ def set_current_request_details
12
+ return unless defined?(::Current)
13
+
14
+ ::Current.request_id = request.request_id if ::Current.respond_to?(:request_id=)
15
+ ::Current.ip_address = request.remote_ip if ::Current.respond_to?(:ip_address=)
16
+ ::Current.user_agent = request.user_agent if ::Current.respond_to?(:user_agent=)
17
+ end
18
+ end
19
+ end
@@ -2,64 +2,70 @@ module StandardId
2
2
  module SocialAuthentication
3
3
  extend ActiveSupport::Concern
4
4
 
5
+ included do
6
+ prepend_before_action :prepare_provider
7
+ end
8
+
5
9
  private
6
10
 
7
- def get_user_info_from_provider(connection, redirect_uri: nil, flow: :web)
8
- case connection
9
- when "google"
10
- StandardId::SocialProviders::Google.get_user_info(
11
- code: params[:code],
12
- id_token: params[:id_token],
13
- access_token: params[:access_token],
14
- redirect_uri: redirect_uri
15
- )
16
- when "apple"
17
- StandardId::SocialProviders::Apple.get_user_info(
18
- code: params[:code],
19
- id_token: params[:id_token],
20
- redirect_uri: redirect_uri,
21
- client_id: apple_client_id_for_flow(flow)
22
- )
23
- else
24
- raise StandardId::InvalidRequestError, "Unsupported provider: #{connection}"
25
- end
11
+ attr_reader :provider
12
+
13
+ def prepare_provider
14
+ @provider = StandardId::ProviderRegistry.get(params[:provider])
15
+ rescue StandardId::ProviderRegistry::ProviderNotFoundError => e
16
+ raise StandardId::InvalidRequestError, e.message
26
17
  end
27
18
 
28
- def apple_client_id_for_flow(flow)
29
- flow == :web ? StandardId.config.apple_client_id : StandardId.config.apple_mobile_client_id
19
+ def get_user_info_from_provider(redirect_uri: nil, flow: :web)
20
+ provider_params = {
21
+ code: params[:code],
22
+ id_token: params[:id_token],
23
+ access_token: params[:access_token],
24
+ redirect_uri: redirect_uri
25
+ }
26
+
27
+ resolved_params = provider.resolve_params(provider_params, context: { flow: flow })
28
+ provider.get_user_info(**resolved_params.compact)
30
29
  end
31
30
 
32
- def find_or_create_account_from_social(raw_social_info, provider)
31
+ def find_or_create_account_from_social(raw_social_info)
33
32
  social_info = raw_social_info.to_h.with_indifferent_access
34
33
  email = social_info[:email]
35
- raise StandardId::InvalidRequestError, "No email provided by #{provider}" if email.blank?
34
+ raise StandardId::InvalidRequestError, "No email provided by #{provider.provider_name}" if email.blank?
35
+
36
+ emit_social_user_info_fetched(provider, social_info, email)
36
37
 
37
38
  identifier = StandardId::EmailIdentifier.find_by(value: email)
38
39
 
39
40
  if identifier.present?
41
+ emit_social_account_linked(identifier.account, provider, identifier)
40
42
  identifier.account
41
43
  else
42
- account = build_account_from_social(social_info, provider)
44
+ account = build_account_from_social(social_info)
43
45
  identifier = StandardId::EmailIdentifier.create!(
44
46
  account: account,
45
47
  value: email
46
48
  )
47
49
  identifier.verify! if identifier.respond_to?(:verify!)
50
+ emit_social_account_created(account, provider, social_info)
48
51
  account
49
52
  end
50
53
  end
51
54
 
52
- def build_account_from_social(social_info, provider)
53
- attrs = resolve_account_attributes(social_info, provider)
54
- StandardId.account_class.create!(attrs)
55
+ def build_account_from_social(social_info)
56
+ emit_account_creating_from_social(social_info)
57
+ attrs = resolve_account_attributes(social_info)
58
+ account = StandardId.account_class.create!(attrs)
59
+ emit_account_created_from_social(account)
60
+ account
55
61
  end
56
62
 
57
- def resolve_account_attributes(social_info, provider)
63
+ def resolve_account_attributes(social_info)
58
64
  resolver = StandardId.config.social_account_attributes
59
65
  attrs = if resolver.respond_to?(:call)
60
66
  payload = {
61
67
  social_info: social_info,
62
- provider: provider
68
+ provider: provider.provider_name
63
69
  }
64
70
 
65
71
  filtered_payload = StandardId::Utils::CallableParameterFilter.filter(resolver, payload)
@@ -95,18 +101,61 @@ module StandardId
95
101
  end
96
102
 
97
103
  def run_social_callback(provider:, social_info:, provider_tokens:, account:)
98
- callback = StandardId.config.social_callback
99
- return if callback.blank?
104
+ emit_social_auth_completed(provider, social_info, provider_tokens, account)
105
+ end
100
106
 
101
- payload = {
107
+ def emit_social_user_info_fetched(provider, social_info, email)
108
+ StandardId::Events.publish(
109
+ StandardId::Events::SOCIAL_USER_INFO_FETCHED,
102
110
  provider: provider,
103
111
  social_info: social_info,
104
- tokens: provider_tokens.presence,
105
- account: account
106
- }
112
+ email: email
113
+ )
114
+ end
115
+
116
+ def emit_social_account_created(account, provider, social_info)
117
+ StandardId::Events.publish(
118
+ StandardId::Events::SOCIAL_ACCOUNT_CREATED,
119
+ account: account,
120
+ provider: provider,
121
+ social_info: social_info
122
+ )
123
+ end
124
+
125
+ def emit_social_account_linked(account, provider, identifier)
126
+ StandardId::Events.publish(
127
+ StandardId::Events::SOCIAL_ACCOUNT_LINKED,
128
+ account: account,
129
+ provider: provider,
130
+ identifier: identifier
131
+ )
132
+ end
133
+
134
+ def emit_social_auth_completed(provider, social_info, provider_tokens, account)
135
+ StandardId::Events.publish(
136
+ StandardId::Events::SOCIAL_AUTH_COMPLETED,
137
+ account: account,
138
+ provider: provider,
139
+ social_info: social_info,
140
+ tokens: provider_tokens
141
+ )
142
+ end
143
+
144
+ def emit_account_creating_from_social(social_info)
145
+ StandardId::Events.publish(
146
+ StandardId::Events::ACCOUNT_CREATING,
147
+ account_params: resolve_account_attributes(social_info),
148
+ auth_method: "social:#{provider.provider_name}"
149
+ )
150
+ end
107
151
 
108
- filtered_payload = StandardId::Utils::CallableParameterFilter.filter(callback, payload)
109
- callback.call(**filtered_payload.symbolize_keys)
152
+ def emit_account_created_from_social(account)
153
+ StandardId::Events.publish(
154
+ StandardId::Events::ACCOUNT_CREATED,
155
+ account: account,
156
+ auth_method: "social:#{provider.provider_name}",
157
+ source: "social"
158
+ )
110
159
  end
111
160
  end
112
161
  end
@@ -49,11 +49,39 @@ module StandardId
49
49
  password = login_params[:password]
50
50
  remember_me = ActiveModel::Type::Boolean.new.cast(login_params[:remember_me])
51
51
 
52
+ StandardId::Events.publish(
53
+ StandardId::Events::AUTHENTICATION_ATTEMPT_STARTED,
54
+ account_lookup: login,
55
+ auth_method: "password"
56
+ )
57
+
52
58
  StandardId::PasswordCredential.find_by(login:).tap do |password_credential|
53
- return nil unless password_credential&.authenticate(password)
59
+ unless password_credential&.authenticate(password)
60
+ StandardId::Events.publish(
61
+ StandardId::Events::AUTHENTICATION_FAILED,
62
+ account_lookup: login,
63
+ auth_method: "password",
64
+ error_code: "invalid_credentials",
65
+ error_message: "Invalid login or password"
66
+ )
67
+ return nil
68
+ end
69
+
70
+ StandardId::Events.publish(
71
+ StandardId::Events::PASSWORD_VALIDATED,
72
+ account: password_credential.account,
73
+ credential_id: password_credential.id
74
+ )
54
75
 
55
76
  session_manager.sign_in_account(password_credential.account)
56
77
  session_manager.set_remember_cookie(password_credential) if remember_me
78
+
79
+ StandardId::Events.publish(
80
+ StandardId::Events::AUTHENTICATION_SUCCEEDED,
81
+ account: password_credential.account,
82
+ auth_method: "password",
83
+ session_type: "browser"
84
+ )
57
85
  end
58
86
  end
59
87
 
@@ -2,6 +2,7 @@ module StandardId
2
2
  module Api
3
3
  class BaseController < ActionController::API
4
4
  include StandardId::ApiAuthentication
5
+ include StandardId::SetCurrentRequestDetails
5
6
 
6
7
  before_action :validate_content_type!
7
8