standard_id 0.10.0 → 0.11.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7ad192cf3b1bad92d1ec8322e203804401d8c10e54ebfcde4340f7840fa624bd
4
- data.tar.gz: 0eb0cc7c613bd3fdd981818c6d4fd359f809003da34d6e15f5e2fea9fd0eab74
3
+ metadata.gz: ebaef83f7c6b587f981d1eddf7b56638760d59e667b1c23f5cbb75d26e98db34
4
+ data.tar.gz: d1476d41140b17b43c745b92d63b64c1e2651be30c9e6714cf22971a04f99e26
5
5
  SHA512:
6
- metadata.gz: 478082fd5a80a66ded10834c7bd1db32912a222086f5fc8db521a90acceb1459418492581d89ca90d5a7d8fb438b3c74845372fe551d52eb892c297136acc002
7
- data.tar.gz: bca63014a4ca013f152bb5bd9112f3649d0e7848d22b7d1114e9bd2715e2847369610e735b83f20bda3aad3fca45936d0f7452438f2a8144d24ddba7b3cb2c07
6
+ metadata.gz: a163de182358974e8e5168194dc9af3e5f084a0de5c5dde0b663048918dbd22bbf326b0708fb5cc5c0ba0c5d92953baaf7b8ea4fd319225170164e762e663091
7
+ data.tar.gz: '068d45f4edce5e084b3f35e038dc0c4dbb377efa16dcb6dc4c29c1f0a15f0a5c319f110684d6ecb820cbb11a3e5fa4ef61198e4c5385e15513b9fc1f69d059bd'
@@ -4,20 +4,46 @@ module StandardId
4
4
 
5
5
  private
6
6
 
7
+ # Invoke the before_sign_in hook if configured.
8
+ # Called after credential verification, BEFORE session creation.
9
+ #
10
+ # @param account [Object] the authenticated account
11
+ # @param context [Hash] context about the sign-in
12
+ # - :mechanism [String] "password", "passwordless", or "social"
13
+ # - :provider [String, nil] e.g. "google", "apple", or nil
14
+ # - :first_sign_in [Boolean] whether this is the account's first browser session
15
+ # @return [void]
16
+ # @raise [StandardId::AuthenticationDenied] when hook returns { error: "..." }
17
+ def invoke_before_sign_in(account, context)
18
+ hook = StandardId.config.before_sign_in
19
+ return unless hook.respond_to?(:call)
20
+
21
+ context = context.merge(first_sign_in: first_sign_in?(account, session_created: false))
22
+ result = hook.call(account, request, context)
23
+
24
+ if result.is_a?(Hash) && result[:error].present?
25
+ raise StandardId::AuthenticationDenied, result[:error]
26
+ end
27
+ end
28
+
7
29
  # Invoke the after_sign_in hook if configured.
8
30
  #
9
31
  # @param account [Object] the authenticated account
10
32
  # @param context [Hash] context about the sign-in
11
- # - :connection [String] "email", "password", or "social"
33
+ # - :mechanism [String] "password", "passwordless", or "social"
12
34
  # - :provider [String, nil] e.g. "google", "apple", or nil
13
35
  # - :first_sign_in [Boolean] whether this is the account's first browser session
36
+ # - :session [StandardId::Session] the session that was just created
14
37
  # @return [String, nil] redirect path override, or nil for default
15
38
  # @raise [StandardId::AuthenticationDenied] to reject the sign-in
16
39
  def invoke_after_sign_in(account, context)
17
40
  hook = StandardId.config.after_sign_in
18
41
  return nil unless hook.respond_to?(:call)
19
42
 
20
- context = context.merge(first_sign_in: first_sign_in?(account))
43
+ context = context.merge(
44
+ first_sign_in: first_sign_in?(account, session_created: true),
45
+ session: session_manager.current_session
46
+ )
21
47
  hook.call(account, request, context)
22
48
  end
23
49
 
@@ -36,9 +62,12 @@ module StandardId
36
62
  end
37
63
 
38
64
  # Determine if this is the account's first browser session.
39
- # A count of 1 means the session just created is the only one.
40
- def first_sign_in?(account)
41
- account.sessions.where(type: "StandardId::BrowserSession").active.count <= 1
65
+ # When called before session creation (before_sign_in), count == 0 means first.
66
+ # When called after session creation (after_sign_in), count <= 1 means first
67
+ # (the just-created session is the only one).
68
+ def first_sign_in?(account, session_created: true)
69
+ active_count = account.sessions.where(type: "StandardId::BrowserSession").active.count
70
+ session_created ? active_count <= 1 : active_count == 0
42
71
  end
43
72
 
44
73
  # Handle AuthenticationDenied by revoking the session and redirecting to login.
@@ -48,7 +77,7 @@ module StandardId
48
77
  # @param account [Object, nil] the account to clean up if newly created
49
78
  # @param newly_created [Boolean] whether the account was created during this request
50
79
  def handle_authentication_denied(error, account: nil, newly_created: false)
51
- session_manager.revoke_current_session!
80
+ session_manager.revoke_current_session! if session_manager.current_session.present?
52
81
  destroy_newly_created_account(account) if newly_created
53
82
  message = error.message
54
83
  # When raised without arguments, StandardError#message returns the class name
@@ -59,7 +59,7 @@ module StandardId
59
59
  session.delete(:return_to_after_authenticating) || "/"
60
60
  end
61
61
 
62
- def sign_in_account(login_params)
62
+ def sign_in_account(login_params, &before_session)
63
63
  login = login_params[:email] || login_params[:login] # support both :email and :login keys
64
64
  password = login_params[:password]
65
65
  remember_me = ActiveModel::Type::Boolean.new.cast(login_params[:remember_me])
@@ -94,6 +94,10 @@ module StandardId
94
94
  credential_id: password_credential.id
95
95
  )
96
96
 
97
+ # Allow callers to run before_sign_in hooks after credential verification
98
+ # but before session creation. The block may raise AuthenticationDenied.
99
+ before_session&.call(password_credential.account)
100
+
97
101
  session_manager.sign_in_account(password_credential.account)
98
102
  session_manager.set_remember_cookie(password_credential) if remember_me
99
103
 
@@ -37,6 +37,8 @@ module StandardId
37
37
  account = find_or_create_account_from_social(social_info)
38
38
  end
39
39
  newly_created = account.previously_new_record?
40
+
41
+ invoke_before_sign_in(account, { mechanism: "social", provider: provider.provider_name })
40
42
  session_manager.sign_in_account(account)
41
43
 
42
44
  provider_name = provider.provider_name
@@ -50,7 +52,7 @@ module StandardId
50
52
  original_request_params: state_data
51
53
  )
52
54
 
53
- context = { connection: "social", provider: provider_name }
55
+ context = { mechanism: "social", provider: provider_name }
54
56
  redirect_override = invoke_after_sign_in(account, context)
55
57
 
56
58
  destination = redirect_override || state_data["redirect_uri"]
@@ -54,8 +54,12 @@ module StandardId
54
54
  def handle_password_login
55
55
  return head(:not_found) unless StandardId.config.web.password_login
56
56
 
57
- if sign_in_account(login_params)
58
- context = { connection: "password", provider: nil }
57
+ result = sign_in_account(login_params) { |account|
58
+ invoke_before_sign_in(account, { mechanism: "password", provider: nil })
59
+ }
60
+
61
+ if result
62
+ context = { mechanism: "password", provider: nil }
59
63
  redirect_override = invoke_after_sign_in(current_account, context)
60
64
  destination = redirect_override || params[:redirect_uri] || after_authentication_url
61
65
  redirect_to destination, status: :see_other, notice: "Successfully signed in"
@@ -50,6 +50,8 @@ module StandardId
50
50
  account = result.account
51
51
  newly_created = account.previously_new_record?
52
52
 
53
+ invoke_before_sign_in(account, { mechanism: "passwordless", provider: nil })
54
+
53
55
  session_manager.sign_in_account(account)
54
56
  emit_authentication_succeeded(account)
55
57
 
@@ -58,7 +60,7 @@ module StandardId
58
60
  invoke_after_account_created(account, { mechanism: "passwordless", provider: nil })
59
61
  end
60
62
 
61
- context = { connection: @otp_data[:connection], provider: nil }
63
+ context = { mechanism: "passwordless", provider: nil }
62
64
  redirect_override = invoke_after_sign_in(account, context)
63
65
 
64
66
  session.delete(:standard_id_otp_payload)
@@ -39,10 +39,11 @@ module StandardId
39
39
  form = StandardId::Web::SignupForm.new(signup_params)
40
40
 
41
41
  if form.submit
42
+ invoke_before_sign_in(form.account, { mechanism: "password", provider: nil })
42
43
  session_manager.sign_in_account(form.account)
43
44
  invoke_after_account_created(form.account, { mechanism: "signup", provider: nil })
44
45
 
45
- context = { connection: "password", provider: nil }
46
+ context = { mechanism: "password", provider: nil }
46
47
  redirect_override = invoke_after_sign_in(form.account, context)
47
48
  destination = redirect_override || params[:redirect_uri] || after_authentication_url
48
49
  redirect_to destination, notice: "Account created successfully"
@@ -17,11 +17,11 @@
17
17
  "note": "Auth engine intentionally redirects to params[:redirect_uri] when user is not authenticated"
18
18
  },
19
19
  {
20
- "fingerprint": "68be03a57d3ef2cfb68582fc78ac2eb6b96aaa0a9897a9a975c24b889fdbb2aa",
20
+ "fingerprint": "1fe5fcac2c90d0480ef08f002ad04041eec2b95caabbc4e8b5d6cccc23c9283f",
21
21
  "note": "after_sign_in hook redirect is controlled by the host app configuration, not user input"
22
22
  },
23
23
  {
24
- "fingerprint": "277cf277d1c94f46d0abaeba9c51312d1d17e6f62c2e8d457dda47a6aad422aa",
24
+ "fingerprint": "413b5740add2c365198a540734a9e8c12b1c0483f521e2db145a80f3d582a4cc",
25
25
  "note": "after_sign_in hook redirect is controlled by the host app configuration, not user input"
26
26
  }
27
27
  ],
@@ -24,17 +24,26 @@ StandardConfig.schema.draw do
24
24
 
25
25
  # Post-authentication lifecycle hooks (synchronous, WebEngine only)
26
26
  #
27
+ # after_account_created: Called after a new account is created via any mechanism.
28
+ # Receives: (account, request, context)
29
+ # Context: { mechanism: "passwordless"/"social"/"signup", provider: nil/"google"/"apple" }
30
+ field :after_account_created, type: :any, default: nil
31
+
32
+ # before_sign_in: Called after credential verification, BEFORE session creation.
33
+ # Receives: (account, request, context)
34
+ # Context: { mechanism: "password"/"passwordless"/"social", provider: nil/"google"/"apple",
35
+ # first_sign_in: bool }
36
+ # Return: nil or truthy to proceed with sign-in.
37
+ # Return { error: "message" } Hash to reject sign-in (error message is passed to the error flow).
38
+ field :before_sign_in, type: :any, default: nil
39
+
27
40
  # after_sign_in: Called after successful sign-in, before redirect.
28
41
  # Receives: (account, request, context)
29
- # Context: { first_sign_in: bool, connection: "email"/"password"/"social", provider: nil/"google"/"apple" }
42
+ # Context: { first_sign_in: bool, mechanism: "password"/"passwordless"/"social",
43
+ # provider: nil/"google"/"apple", session: StandardId::Session }
30
44
  # Return: nil (default redirect) or a path string (override redirect)
31
45
  # Raise StandardId::AuthenticationDenied.new("message") to reject sign-in.
32
46
  field :after_sign_in, type: :any, default: nil
33
-
34
- # after_account_created: Called after a new account is created via any mechanism.
35
- # Receives: (account, request, context)
36
- # Context: { mechanism: "passwordless"/"social"/"signup", provider: nil/"google"/"apple" }
37
- field :after_account_created, type: :any, default: nil
38
47
  end
39
48
 
40
49
  scope :events do
@@ -1,3 +1,3 @@
1
1
  module StandardId
2
- VERSION = "0.10.0"
2
+ VERSION = "0.11.0"
3
3
  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.10.0
4
+ version: 0.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jaryl Sim