standard_id 0.3.1 → 0.4.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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/concerns/standard_id/api_authentication.rb +6 -0
  3. data/app/controllers/concerns/standard_id/controller_policy.rb +99 -0
  4. data/app/controllers/concerns/standard_id/sentry_context.rb +36 -0
  5. data/app/controllers/concerns/standard_id/set_current_request_details.rb +1 -1
  6. data/app/controllers/concerns/standard_id/web_authentication.rb +5 -0
  7. data/app/controllers/standard_id/api/authorization_controller.rb +2 -0
  8. data/app/controllers/standard_id/api/base_controller.rb +1 -0
  9. data/app/controllers/standard_id/api/oauth/callback/providers_controller.rb +2 -0
  10. data/app/controllers/standard_id/api/oauth/tokens_controller.rb +2 -0
  11. data/app/controllers/standard_id/api/oidc/logout_controller.rb +2 -0
  12. data/app/controllers/standard_id/api/passwordless_controller.rb +2 -0
  13. data/app/controllers/standard_id/api/userinfo_controller.rb +2 -0
  14. data/app/controllers/standard_id/api/well_known/jwks_controller.rb +6 -0
  15. data/app/controllers/standard_id/web/account_controller.rb +2 -0
  16. data/app/controllers/standard_id/web/auth/callback/providers_controller.rb +2 -0
  17. data/app/controllers/standard_id/web/base_controller.rb +1 -0
  18. data/app/controllers/standard_id/web/login_controller.rb +2 -0
  19. data/app/controllers/standard_id/web/login_verify_controller.rb +12 -84
  20. data/app/controllers/standard_id/web/logout_controller.rb +7 -0
  21. data/app/controllers/standard_id/web/reset_password/confirm_controller.rb +2 -0
  22. data/app/controllers/standard_id/web/reset_password/start_controller.rb +2 -0
  23. data/app/controllers/standard_id/web/sessions_controller.rb +2 -0
  24. data/app/controllers/standard_id/web/signup_controller.rb +2 -0
  25. data/app/controllers/standard_id/web/verify_email/base_controller.rb +2 -0
  26. data/app/controllers/standard_id/web/verify_email/start_controller.rb +1 -1
  27. data/app/controllers/standard_id/web/verify_phone/base_controller.rb +2 -0
  28. data/app/controllers/standard_id/web/verify_phone/start_controller.rb +1 -1
  29. data/lib/standard_id/api/session_manager.rb +7 -3
  30. data/lib/standard_id/api/token_manager.rb +2 -2
  31. data/lib/standard_id/authorization_bypass.rb +121 -0
  32. data/lib/standard_id/config/schema.rb +2 -0
  33. data/lib/standard_id/jwt_service.rb +41 -15
  34. data/lib/standard_id/oauth/password_flow.rb +5 -1
  35. data/lib/standard_id/oauth/passwordless_otp_flow.rb +10 -61
  36. data/lib/standard_id/passwordless/base_strategy.rb +1 -1
  37. data/lib/standard_id/passwordless/verification_service.rb +227 -0
  38. data/lib/standard_id/testing/authentication_helpers.rb +75 -0
  39. data/lib/standard_id/testing/factories/credentials.rb +24 -0
  40. data/lib/standard_id/testing/factories/identifiers.rb +37 -0
  41. data/lib/standard_id/testing/factories/oauth.rb +89 -0
  42. data/lib/standard_id/testing/factories/sessions.rb +112 -0
  43. data/lib/standard_id/testing/factory_bot.rb +7 -0
  44. data/lib/standard_id/testing/request_helpers.rb +60 -0
  45. data/lib/standard_id/testing.rb +26 -0
  46. data/lib/standard_id/utils/ip_normalizer.rb +16 -0
  47. data/lib/standard_id/version.rb +1 -1
  48. data/lib/standard_id/web/session_manager.rb +14 -1
  49. data/lib/standard_id/web/token_manager.rb +1 -1
  50. data/lib/standard_id.rb +7 -0
  51. metadata +42 -1
@@ -0,0 +1,37 @@
1
+ FactoryBot.define do
2
+ factory :standard_id_email_identifier, class: "StandardId::EmailIdentifier" do
3
+ sequence(:value) { |n| "user#{n}@example.com" }
4
+
5
+ trait :verified do
6
+ verified_at { Time.current }
7
+ end
8
+
9
+ trait :unverified do
10
+ verified_at { nil }
11
+ end
12
+ end
13
+
14
+ factory :standard_id_phone_number_identifier, class: "StandardId::PhoneNumberIdentifier" do
15
+ sequence(:value) { |n| "+1555#{(n % 10_000_000).to_s.rjust(7, '0')}" }
16
+
17
+ trait :verified do
18
+ verified_at { Time.current }
19
+ end
20
+
21
+ trait :unverified do
22
+ verified_at { nil }
23
+ end
24
+ end
25
+
26
+ factory :standard_id_username_identifier, class: "StandardId::UsernameIdentifier" do
27
+ sequence(:value) { |n| "user_#{n}" }
28
+
29
+ trait :verified do
30
+ verified_at { Time.current }
31
+ end
32
+
33
+ trait :unverified do
34
+ verified_at { nil }
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,89 @@
1
+ FactoryBot.define do
2
+ # ClientApplication requires a polymorphic `owner` association.
3
+ # The host app must define an `:account` factory for this association to resolve.
4
+ factory :standard_id_client_application, class: "StandardId::ClientApplication" do
5
+ association :owner, factory: :account
6
+ sequence(:name) { |n| "Test App #{n}" }
7
+ redirect_uris { "https://example.com/callback" }
8
+ scopes { "openid profile email" }
9
+ grant_types { "authorization_code refresh_token" }
10
+ response_types { "code" }
11
+ client_type { "confidential" }
12
+ require_pkce { true }
13
+ code_challenge_methods { "S256" }
14
+ access_token_lifetime { 3600 }
15
+ refresh_token_lifetime { 2_592_000 }
16
+ authorization_code_lifetime { 600 }
17
+ active { true }
18
+
19
+ trait :public_client do
20
+ client_type { "public" }
21
+ end
22
+
23
+ trait :inactive do
24
+ active { false }
25
+ deactivated_at { Time.current }
26
+ end
27
+
28
+ # Replaces the default grant_types value. To combine with other grant types,
29
+ # set grant_types explicitly: grant_types { "authorization_code client_credentials" }
30
+ trait :with_client_credentials do
31
+ grant_types { "client_credentials" }
32
+ end
33
+ end
34
+
35
+ factory :standard_id_code_challenge, class: "StandardId::CodeChallenge" do
36
+ realm { "authentication" }
37
+ channel { "email" }
38
+ sequence(:target) { |n| "user#{n}@example.com" }
39
+ code { SecureRandom.random_number(10**6).to_s.rjust(6, "0") }
40
+ expires_at { 10.minutes.from_now }
41
+
42
+ trait :expired do
43
+ expires_at { 5.minutes.ago }
44
+ end
45
+
46
+ trait :used do
47
+ used_at { Time.current }
48
+ end
49
+
50
+ trait :for_verification do
51
+ realm { "verification" }
52
+ end
53
+
54
+ trait :for_sms do
55
+ channel { "sms" }
56
+ sequence(:target) { |n| "+1555#{(n % 10_000_000).to_s.rjust(7, '0')}" }
57
+ end
58
+ end
59
+
60
+ # AuthorizationCode has `belongs_to :account, optional: true`.
61
+ # client_id is a plain string (no FK to ClientApplication) — intentionally
62
+ # unlinked so tests can create authorization codes without a full OAuth setup.
63
+ factory :standard_id_authorization_code, class: "StandardId::AuthorizationCode" do
64
+ transient do
65
+ plaintext_code { SecureRandom.hex(20) }
66
+ end
67
+
68
+ association :account, factory: :account
69
+ code_hash { StandardId::AuthorizationCode.hash_for(plaintext_code) }
70
+ client_id { SecureRandom.hex(16) }
71
+ redirect_uri { "https://example.com/callback" }
72
+ scope { "openid profile" }
73
+ issued_at { Time.current }
74
+ expires_at { 10.minutes.from_now }
75
+
76
+ trait :expired do
77
+ expires_at { 5.minutes.ago }
78
+ end
79
+
80
+ trait :consumed do
81
+ consumed_at { Time.current }
82
+ end
83
+
84
+ trait :with_pkce do
85
+ code_challenge { SecureRandom.hex(32) }
86
+ code_challenge_method { "S256" }
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,112 @@
1
+ FactoryBot.define do
2
+ # BrowserSession requires an `account` association (belongs_to :account).
3
+ # The host app must define an `:account` factory for this association to resolve.
4
+ factory :standard_id_browser_session, class: "StandardId::BrowserSession" do
5
+ association :account, factory: :account
6
+ user_agent { "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 Chrome/120.0" }
7
+ sequence(:ip_address) { |n| "192.168.1.#{(n % 254) + 1}" }
8
+ expires_at { StandardId::BrowserSession.expiry }
9
+
10
+ trait :active do
11
+ revoked_at { nil }
12
+ end
13
+
14
+ trait :expired do
15
+ expires_at { 2.days.ago }
16
+ revoked_at { nil }
17
+ end
18
+
19
+ trait :revoked do
20
+ revoked_at { 1.day.ago }
21
+ end
22
+
23
+ trait :chrome do
24
+ user_agent { "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 Chrome/120.0" }
25
+ end
26
+
27
+ trait :firefox do
28
+ user_agent { "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Firefox/121.0" }
29
+ end
30
+
31
+ trait :safari do
32
+ user_agent { "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 Safari/17.2" }
33
+ end
34
+
35
+ trait :edge do
36
+ user_agent { "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Edg/120.0" }
37
+ end
38
+ end
39
+
40
+ # DeviceSession requires an `account` association (belongs_to :account).
41
+ # The host app must define an `:account` factory for this association to resolve.
42
+ factory :standard_id_device_session, class: "StandardId::DeviceSession" do
43
+ association :account, factory: :account
44
+ device_agent { "App/1.0 (iPhone; iOS 17.2)" }
45
+ sequence(:device_id) { |n| "device_#{n}" }
46
+ sequence(:ip_address) { |n| "10.0.0.#{(n % 254) + 1}" }
47
+ expires_at { StandardId::DeviceSession.expiry }
48
+ last_refreshed_at { 30.minutes.ago }
49
+
50
+ trait :active do
51
+ revoked_at { nil }
52
+ last_refreshed_at { 30.minutes.ago }
53
+ end
54
+
55
+ trait :expired do
56
+ expires_at { 15.days.ago }
57
+ revoked_at { nil }
58
+ end
59
+
60
+ trait :revoked do
61
+ expires_at { 30.days.from_now }
62
+ revoked_at { 5.days.ago }
63
+ end
64
+
65
+ trait :stale do
66
+ expires_at { 30.days.from_now }
67
+ revoked_at { nil }
68
+ last_refreshed_at { 2.hours.ago }
69
+ end
70
+
71
+ trait :iphone do
72
+ device_agent { "App/1.0 (iPhone; iOS 17.2)" }
73
+ end
74
+
75
+ trait :android do
76
+ device_agent { "App/1.0 (Android; Samsung Galaxy S24)" }
77
+ end
78
+
79
+ trait :ipad do
80
+ device_agent { "App/1.0 (iPad; iPadOS 17.2)" }
81
+ end
82
+ end
83
+
84
+ # NOTE: ServiceSession inherits `belongs_to :account` from Session and adds
85
+ # `belongs_to :owner` (polymorphic). Both are required.
86
+ # By default, account and owner are distinct :account instances. Override one
87
+ # or both if your test needs them to be the same object.
88
+ # The host app must define an `:account` factory for these associations to resolve.
89
+ factory :standard_id_service_session, class: "StandardId::ServiceSession" do
90
+ association :account, factory: :account
91
+ association :owner, factory: :account
92
+ service_name { "test-service" }
93
+ service_version { "1.0.0" }
94
+ ip_address { "10.0.0.1" }
95
+ user_agent { "ServiceClient/1.0" }
96
+ expires_at { StandardId::ServiceSession.default_expiry }
97
+
98
+ trait :active do
99
+ revoked_at { nil }
100
+ end
101
+
102
+ trait :expired do
103
+ expires_at { 30.days.ago }
104
+ revoked_at { nil }
105
+ end
106
+
107
+ trait :revoked do
108
+ expires_at { 90.days.from_now }
109
+ revoked_at { 1.day.ago }
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,7 @@
1
+ require "factory_bot"
2
+
3
+ # Factories are loaded alphabetically via glob. FactoryBot resolves associations
4
+ # lazily, so load order does not affect correctness. If adding a factory file
5
+ # with an explicit dependency on another, use require_relative instead.
6
+ factory_paths = Dir[File.join(__dir__, "factories", "*.rb")]
7
+ factory_paths.each { |f| require f }
@@ -0,0 +1,60 @@
1
+ module StandardId
2
+ module Testing
3
+ # Integration test helpers for signing in accounts and making authenticated requests.
4
+ #
5
+ # Usage in rails_helper.rb:
6
+ #
7
+ # require "standard_id/testing"
8
+ #
9
+ # RSpec.configure do |config|
10
+ # config.include StandardId::Testing::RequestHelpers, type: :request
11
+ # end
12
+ #
13
+ module RequestHelpers
14
+ # Create a browser session record for integration tests.
15
+ #
16
+ # For a simpler approach, use stub_web_authentication from AuthenticationHelpers instead.
17
+ #
18
+ # @param account [Object] the account to sign in
19
+ # @param user_agent [String] the user agent string (default: "RSpec")
20
+ # @return [StandardId::BrowserSession] the created session
21
+ #
22
+ def create_browser_session(account, user_agent: "RSpec")
23
+ StandardId::BrowserSession.create!(
24
+ account: account,
25
+ ip_address: "127.0.0.1",
26
+ user_agent: user_agent,
27
+ expires_at: StandardId::BrowserSession.expiry
28
+ )
29
+ end
30
+
31
+ # Build a JWT token for API/service authentication.
32
+ #
33
+ # @param account [Object, nil] account (uses account.id as sub claim)
34
+ # @param sub [String, nil] explicit subject claim (overrides account.id)
35
+ # @param client_id [String] OAuth client ID
36
+ # @param scope [String] space-separated scopes
37
+ # @param grant_type [String] OAuth grant type
38
+ # @param extra [Hash] additional JWT claims
39
+ # @return [String] encoded JWT token
40
+ #
41
+ def build_jwt(account: nil, sub: nil, client_id: "test-client",
42
+ scope: "openid", grant_type: "authorization_code", extra: {})
43
+ sub ||= account&.id
44
+ raise ArgumentError, "account or sub must be provided" if sub.nil?
45
+
46
+ claims = { sub: sub, client_id: client_id, scope: scope, grant_type: grant_type }.merge(extra)
47
+ StandardId::JwtService.encode(claims)
48
+ end
49
+
50
+ # Returns an Authorization header hash for Bearer token authentication.
51
+ #
52
+ # @param token [String] the JWT token
53
+ # @return [Hash] header hash
54
+ #
55
+ def bearer_auth_header(token)
56
+ { "Authorization" => "Bearer #{token}" }
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,26 @@
1
+ require "standard_id/testing/authentication_helpers"
2
+ require "standard_id/testing/request_helpers"
3
+
4
+ module StandardId
5
+ module Testing
6
+ # Load StandardId's FactoryBot factory definitions.
7
+ #
8
+ # Requires the `factory_bot` (or `factory_bot_rails`) gem in the host app's
9
+ # Gemfile under the :test group.
10
+ #
11
+ # Recommended usage in rails_helper.rb:
12
+ #
13
+ # require "standard_id/testing"
14
+ # StandardId::Testing.setup_factory_bot!
15
+ #
16
+ def self.setup_factory_bot!
17
+ require "standard_id/testing/factory_bot"
18
+ rescue LoadError => e
19
+ raise unless e.message.include?("factory_bot")
20
+
21
+ raise LoadError,
22
+ "StandardId::Testing.setup_factory_bot! requires the `factory_bot` gem. " \
23
+ "Add `gem 'factory_bot_rails'` (or `gem 'factory_bot'`) to your Gemfile's :test group."
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,16 @@
1
+ module StandardId
2
+ module Utils
3
+ class IpNormalizer
4
+ IPV6_LOCALHOST = "::1"
5
+ IPV4_LOCALHOST = "127.0.0.1"
6
+
7
+ class << self
8
+ def normalize(ip)
9
+ return ip unless ip == IPV6_LOCALHOST
10
+
11
+ IPV4_LOCALHOST
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -1,3 +1,3 @@
1
1
  module StandardId
2
- VERSION = "0.3.1"
2
+ VERSION = "0.4.0"
3
3
  end
@@ -15,7 +15,7 @@ module StandardId
15
15
  end
16
16
 
17
17
  def current_account
18
- Current.account ||= current_session&.account&.tap { |a| a.strict_loading!(false) }
18
+ Current.account ||= load_current_account
19
19
  end
20
20
 
21
21
  def sign_in_account(account)
@@ -50,6 +50,19 @@ module StandardId
50
50
 
51
51
  private
52
52
 
53
+ def load_current_account
54
+ if StandardId.config.account_scope
55
+ account_id = current_session&.account_id
56
+ return unless account_id
57
+
58
+ scope = StandardId.account_class
59
+ scope = StandardId.config.account_scope.call(scope)
60
+ scope.find_by(id: account_id)&.tap { |a| a.strict_loading!(false) }
61
+ else
62
+ current_session&.account&.tap { |a| a.strict_loading!(false) }
63
+ end
64
+ end
65
+
53
66
  def load_current_session
54
67
  Current.session ||= load_session_from_session_token
55
68
  Current.session ||= load_session_from_remember_token
@@ -10,7 +10,7 @@ module StandardId
10
10
  def create_browser_session(account)
11
11
  StandardId::BrowserSession.create!(
12
12
  account: account,
13
- ip_address: request.remote_ip,
13
+ ip_address: StandardId::Utils::IpNormalizer.normalize(request.remote_ip),
14
14
  user_agent: request.user_agent,
15
15
  expires_at: StandardId::BrowserSession.expiry
16
16
  )
data/lib/standard_id.rb CHANGED
@@ -39,7 +39,10 @@ require "standard_id/oauth/passwordless_otp_flow"
39
39
  require "standard_id/passwordless/base_strategy"
40
40
  require "standard_id/passwordless/email_strategy"
41
41
  require "standard_id/passwordless/sms_strategy"
42
+ require "standard_id/passwordless/verification_service"
43
+ require "standard_id/authorization_bypass"
42
44
  require "standard_id/utils/callable_parameter_filter"
45
+ require "standard_id/utils/ip_normalizer"
43
46
 
44
47
  require "concurrent/delay"
45
48
 
@@ -74,5 +77,9 @@ module StandardId
74
77
  def account_class
75
78
  config.account_class_name.constantize
76
79
  end
80
+
81
+ def skip_host_authorization(framework: nil, callback: nil)
82
+ AuthorizationBypass.apply(framework: framework, callback: callback)
83
+ end
77
84
  end
78
85
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: standard_id
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jaryl Sim
@@ -65,6 +65,34 @@ dependencies:
65
65
  - - ">="
66
66
  - !ruby/object:Gem::Version
67
67
  version: '0'
68
+ - !ruby/object:Gem::Dependency
69
+ name: concurrent-ruby
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '1.3'
75
+ type: :runtime
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '1.3'
82
+ - !ruby/object:Gem::Dependency
83
+ name: factory_bot
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '6.5'
89
+ type: :development
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '6.5'
68
96
  description: StandardId is an authentication engine that provides a complete, secure-by-default
69
97
  solution for identity management, reducing boilerplate and eliminating common security
70
98
  pitfalls.
@@ -81,9 +109,11 @@ files:
81
109
  - app/channels/concerns/standard_id/cable_authentication.rb
82
110
  - app/controllers/concerns/standard_id/api_authentication.rb
83
111
  - app/controllers/concerns/standard_id/audience_verification.rb
112
+ - app/controllers/concerns/standard_id/controller_policy.rb
84
113
  - app/controllers/concerns/standard_id/inertia_rendering.rb
85
114
  - app/controllers/concerns/standard_id/inertia_support.rb
86
115
  - app/controllers/concerns/standard_id/passwordless_strategy.rb
116
+ - app/controllers/concerns/standard_id/sentry_context.rb
87
117
  - app/controllers/concerns/standard_id/set_current_request_details.rb
88
118
  - app/controllers/concerns/standard_id/social_authentication.rb
89
119
  - app/controllers/concerns/standard_id/web/social_login_params.rb
@@ -173,6 +203,7 @@ files:
173
203
  - lib/standard_id/api/session_manager.rb
174
204
  - lib/standard_id/api/token_manager.rb
175
205
  - lib/standard_id/api_engine.rb
206
+ - lib/standard_id/authorization_bypass.rb
176
207
  - lib/standard_id/bearer_token_extraction.rb
177
208
  - lib/standard_id/config/schema.rb
178
209
  - lib/standard_id/current_attributes.rb
@@ -205,9 +236,19 @@ files:
205
236
  - lib/standard_id/passwordless/base_strategy.rb
206
237
  - lib/standard_id/passwordless/email_strategy.rb
207
238
  - lib/standard_id/passwordless/sms_strategy.rb
239
+ - lib/standard_id/passwordless/verification_service.rb
208
240
  - lib/standard_id/provider_registry.rb
209
241
  - lib/standard_id/providers/base.rb
242
+ - lib/standard_id/testing.rb
243
+ - lib/standard_id/testing/authentication_helpers.rb
244
+ - lib/standard_id/testing/factories/credentials.rb
245
+ - lib/standard_id/testing/factories/identifiers.rb
246
+ - lib/standard_id/testing/factories/oauth.rb
247
+ - lib/standard_id/testing/factories/sessions.rb
248
+ - lib/standard_id/testing/factory_bot.rb
249
+ - lib/standard_id/testing/request_helpers.rb
210
250
  - lib/standard_id/utils/callable_parameter_filter.rb
251
+ - lib/standard_id/utils/ip_normalizer.rb
211
252
  - lib/standard_id/version.rb
212
253
  - lib/standard_id/web/authentication_guard.rb
213
254
  - lib/standard_id/web/session_manager.rb