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 +4 -4
- data/app/controllers/concerns/standard_id/lifecycle_hooks.rb +35 -6
- data/app/controllers/concerns/standard_id/web_authentication.rb +5 -1
- data/app/controllers/standard_id/web/auth/callback/providers_controller.rb +3 -1
- data/app/controllers/standard_id/web/login_controller.rb +6 -2
- data/app/controllers/standard_id/web/login_verify_controller.rb +3 -1
- data/app/controllers/standard_id/web/signup_controller.rb +2 -1
- data/config/brakeman.ignore +2 -2
- data/lib/standard_id/config/schema.rb +15 -6
- data/lib/standard_id/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ebaef83f7c6b587f981d1eddf7b56638760d59e667b1c23f5cbb75d26e98db34
|
|
4
|
+
data.tar.gz: d1476d41140b17b43c745b92d63b64c1e2651be30c9e6714cf22971a04f99e26
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
# - :
|
|
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(
|
|
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
|
-
#
|
|
40
|
-
|
|
41
|
-
|
|
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 = {
|
|
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
|
-
|
|
58
|
-
|
|
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 = {
|
|
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 = {
|
|
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"
|
data/config/brakeman.ignore
CHANGED
|
@@ -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": "
|
|
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": "
|
|
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,
|
|
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
|
data/lib/standard_id/version.rb
CHANGED