standard_id 0.1.5 → 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.
- checksums.yaml +4 -4
- data/README.md +529 -20
- data/app/controllers/concerns/standard_id/inertia_rendering.rb +49 -0
- data/app/controllers/concerns/standard_id/inertia_support.rb +31 -0
- data/app/controllers/concerns/standard_id/set_current_request_details.rb +19 -0
- data/app/controllers/concerns/standard_id/social_authentication.rb +86 -37
- data/app/controllers/concerns/standard_id/web_authentication.rb +50 -1
- data/app/controllers/standard_id/api/base_controller.rb +1 -0
- data/app/controllers/standard_id/api/oauth/callback/providers_controller.rb +7 -18
- data/app/controllers/standard_id/web/auth/callback/providers_controller.rb +33 -37
- data/app/controllers/standard_id/web/base_controller.rb +1 -0
- data/app/controllers/standard_id/web/login_controller.rb +12 -21
- data/app/controllers/standard_id/web/signup_controller.rb +11 -8
- data/app/forms/standard_id/web/signup_form.rb +32 -1
- data/app/models/standard_id/browser_session.rb +8 -0
- data/app/models/standard_id/client_secret_credential.rb +11 -0
- data/app/models/standard_id/device_session.rb +4 -0
- data/app/models/standard_id/identifier.rb +28 -0
- data/app/models/standard_id/service_session.rb +1 -1
- data/app/models/standard_id/session.rb +16 -2
- data/app/views/standard_id/web/auth/callback/providers/{apple_mobile.html.erb → mobile_callback.html.erb} +1 -1
- data/config/routes/api.rb +1 -2
- data/config/routes/web.rb +4 -3
- data/lib/generators/standard_id/install/templates/standard_id.rb +19 -8
- data/lib/standard_config/config.rb +13 -12
- data/lib/standard_config/config_provider.rb +6 -6
- data/lib/standard_config/schema.rb +2 -2
- data/lib/standard_id/account_locking.rb +86 -0
- data/lib/standard_id/account_status.rb +45 -0
- data/lib/standard_id/api/authentication_guard.rb +40 -1
- data/lib/standard_id/api/token_manager.rb +1 -1
- data/lib/standard_id/config/schema.rb +13 -9
- data/lib/standard_id/current_attributes.rb +9 -0
- data/lib/standard_id/engine.rb +9 -0
- data/lib/standard_id/errors.rb +12 -0
- data/lib/standard_id/events/definitions.rb +157 -0
- data/lib/standard_id/events/event.rb +123 -0
- data/lib/standard_id/events/subscribers/account_locking_subscriber.rb +17 -0
- data/lib/standard_id/events/subscribers/account_status_subscriber.rb +17 -0
- data/lib/standard_id/events/subscribers/base.rb +165 -0
- data/lib/standard_id/events/subscribers/logging_subscriber.rb +122 -0
- data/lib/standard_id/events.rb +137 -0
- data/lib/standard_id/oauth/authorization_code_flow.rb +10 -0
- data/lib/standard_id/oauth/client_credentials_flow.rb +31 -0
- data/lib/standard_id/oauth/password_flow.rb +36 -4
- data/lib/standard_id/oauth/passwordless_otp_flow.rb +38 -2
- data/lib/standard_id/oauth/subflows/social_login_grant.rb +11 -22
- data/lib/standard_id/oauth/token_grant_flow.rb +22 -1
- data/lib/standard_id/passwordless/base_strategy.rb +32 -0
- data/lib/standard_id/provider_registry.rb +73 -0
- data/lib/standard_id/{social_providers → providers}/apple.rb +46 -7
- data/lib/standard_id/providers/base.rb +242 -0
- data/lib/standard_id/{social_providers → providers}/google.rb +26 -7
- data/lib/standard_id/version.rb +1 -1
- data/lib/standard_id/web/authentication_guard.rb +29 -0
- data/lib/standard_id/web/session_manager.rb +39 -1
- data/lib/standard_id/web/token_manager.rb +2 -2
- data/lib/standard_id.rb +13 -2
- metadata +20 -6
- data/lib/standard_id/social_providers/response_builder.rb +0 -18
|
@@ -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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
29
|
-
|
|
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
|
|
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
|
|
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
|
|
53
|
-
|
|
54
|
-
|
|
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
|
|
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
|
-
|
|
99
|
-
|
|
104
|
+
emit_social_auth_completed(provider, social_info, provider_tokens, account)
|
|
105
|
+
end
|
|
100
106
|
|
|
101
|
-
|
|
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
|
-
|
|
105
|
-
|
|
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
|
-
|
|
109
|
-
|
|
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
|
|
@@ -3,6 +3,7 @@ module StandardId
|
|
|
3
3
|
extend ActiveSupport::Concern
|
|
4
4
|
|
|
5
5
|
included do
|
|
6
|
+
include StandardId::InertiaSupport
|
|
6
7
|
helper_method :current_account, :authenticated?
|
|
7
8
|
end
|
|
8
9
|
|
|
@@ -18,6 +19,26 @@ module StandardId
|
|
|
18
19
|
authentication_guard.require_session!(session_manager, session: session, request: request)
|
|
19
20
|
end
|
|
20
21
|
|
|
22
|
+
# Require authentication with redirect to login page instead of raising an error.
|
|
23
|
+
# Use this for pages that should redirect unauthenticated users to login.
|
|
24
|
+
def authenticate_account!
|
|
25
|
+
return if authenticated?
|
|
26
|
+
|
|
27
|
+
store_location_for_redirect
|
|
28
|
+
redirect_to_login
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Store the current URL to redirect back after authentication
|
|
32
|
+
def store_location_for_redirect
|
|
33
|
+
session[:return_to_after_authenticating] = request.url if request.get?
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Redirect to login page, handling both Inertia and standard requests
|
|
37
|
+
def redirect_to_login
|
|
38
|
+
login_path = StandardId.config.login_url.presence || "/login"
|
|
39
|
+
redirect_with_inertia login_path
|
|
40
|
+
end
|
|
41
|
+
|
|
21
42
|
def after_authentication_url
|
|
22
43
|
# TODO: add configurable value
|
|
23
44
|
session.delete(:return_to_after_authenticating) || "/"
|
|
@@ -28,11 +49,39 @@ module StandardId
|
|
|
28
49
|
password = login_params[:password]
|
|
29
50
|
remember_me = ActiveModel::Type::Boolean.new.cast(login_params[:remember_me])
|
|
30
51
|
|
|
52
|
+
StandardId::Events.publish(
|
|
53
|
+
StandardId::Events::AUTHENTICATION_ATTEMPT_STARTED,
|
|
54
|
+
account_lookup: login,
|
|
55
|
+
auth_method: "password"
|
|
56
|
+
)
|
|
57
|
+
|
|
31
58
|
StandardId::PasswordCredential.find_by(login:).tap do |password_credential|
|
|
32
|
-
|
|
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
|
+
)
|
|
33
75
|
|
|
34
76
|
session_manager.sign_in_account(password_credential.account)
|
|
35
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
|
+
)
|
|
36
85
|
end
|
|
37
86
|
end
|
|
38
87
|
|
|
@@ -6,37 +6,24 @@ module StandardId
|
|
|
6
6
|
|
|
7
7
|
skip_before_action :validate_content_type!
|
|
8
8
|
|
|
9
|
-
def
|
|
10
|
-
expect_and_permit!([], [:id_token, :code])
|
|
11
|
-
handle_social_callback("google")
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
def apple
|
|
15
|
-
expect_and_permit!([], [:id_token, :code, :state, :flow])
|
|
16
|
-
handle_social_callback("apple")
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
private
|
|
20
|
-
|
|
21
|
-
def handle_social_callback(connection)
|
|
9
|
+
def callback
|
|
22
10
|
original_params = decode_state_params
|
|
23
|
-
|
|
24
|
-
provider_response = get_user_info_from_provider(connection, flow: flow)
|
|
11
|
+
provider_response = get_user_info_from_provider(flow: resolve_flow_for(provider.provider_name))
|
|
25
12
|
social_info = provider_response[:user_info]
|
|
26
13
|
provider_tokens = provider_response[:tokens]
|
|
27
|
-
account = find_or_create_account_from_social(social_info
|
|
14
|
+
account = find_or_create_account_from_social(social_info)
|
|
28
15
|
|
|
29
16
|
flow = StandardId::Oauth::SocialFlow.new(
|
|
30
17
|
params,
|
|
31
18
|
request,
|
|
32
19
|
account: account,
|
|
33
|
-
connection:
|
|
20
|
+
connection: provider.provider_name,
|
|
34
21
|
original_params: original_params
|
|
35
22
|
)
|
|
36
23
|
|
|
37
24
|
token_response = flow.execute
|
|
38
25
|
run_social_callback(
|
|
39
|
-
provider:
|
|
26
|
+
provider: provider.provider_name,
|
|
40
27
|
social_info: social_info,
|
|
41
28
|
provider_tokens: provider_tokens,
|
|
42
29
|
account: account,
|
|
@@ -44,6 +31,8 @@ module StandardId
|
|
|
44
31
|
render json: token_response, status: :ok
|
|
45
32
|
end
|
|
46
33
|
|
|
34
|
+
private
|
|
35
|
+
|
|
47
36
|
def decode_state_params
|
|
48
37
|
encoded_state = params[:state]
|
|
49
38
|
|
|
@@ -8,35 +8,10 @@ module StandardId
|
|
|
8
8
|
|
|
9
9
|
# Social callbacks must be accessible without an existing browser session
|
|
10
10
|
# because they create/sign-in the session upon successful callback.
|
|
11
|
-
skip_before_action :require_browser_session!, only: [:
|
|
12
|
-
skip_before_action :verify_authenticity_token, only: [:
|
|
11
|
+
skip_before_action :require_browser_session!, only: [:callback, :mobile_callback]
|
|
12
|
+
skip_before_action :verify_authenticity_token, only: [:callback, :mobile_callback], if: :skip_csrf_verification?
|
|
13
13
|
|
|
14
|
-
def
|
|
15
|
-
handle_social_callback("google")
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def apple
|
|
19
|
-
handle_social_callback("apple")
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def apple_mobile
|
|
23
|
-
state_data = decode_state_params
|
|
24
|
-
destination = state_data["redirect_uri"]
|
|
25
|
-
|
|
26
|
-
unless allow_other_host_redirect?(destination)
|
|
27
|
-
raise StandardId::InvalidRequestError, "Redirect URI is not allowed"
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
relay_params = mobile_relay_params
|
|
31
|
-
@mobile_redirect_url = build_mobile_redirect(destination, relay_params)
|
|
32
|
-
render :apple_mobile, layout: false
|
|
33
|
-
rescue StandardId::InvalidRequestError => e
|
|
34
|
-
render plain: e.message, status: :unprocessable_entity
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
private
|
|
38
|
-
|
|
39
|
-
def handle_social_callback(connection)
|
|
14
|
+
def callback
|
|
40
15
|
if params[:error].present?
|
|
41
16
|
handle_callback_error
|
|
42
17
|
return
|
|
@@ -46,22 +21,22 @@ module StandardId
|
|
|
46
21
|
|
|
47
22
|
begin
|
|
48
23
|
state_data = decode_state_params
|
|
49
|
-
redirect_uri =
|
|
50
|
-
provider_response = get_user_info_from_provider(
|
|
24
|
+
redirect_uri = callback_url_for
|
|
25
|
+
provider_response = get_user_info_from_provider(redirect_uri: redirect_uri)
|
|
51
26
|
social_info = provider_response[:user_info]
|
|
52
27
|
provider_tokens = provider_response[:tokens]
|
|
53
|
-
account = find_or_create_account_from_social(social_info
|
|
28
|
+
account = find_or_create_account_from_social(social_info)
|
|
54
29
|
session_manager.sign_in_account(account)
|
|
55
30
|
|
|
56
31
|
run_social_callback(
|
|
57
|
-
provider:
|
|
32
|
+
provider: provider.provider_name,
|
|
58
33
|
social_info: social_info,
|
|
59
34
|
provider_tokens: provider_tokens,
|
|
60
35
|
account: account,
|
|
61
36
|
)
|
|
62
37
|
|
|
63
38
|
destination = state_data["redirect_uri"]
|
|
64
|
-
redirect_options = { notice: "Successfully signed in with #{
|
|
39
|
+
redirect_options = { notice: "Successfully signed in with #{provider.provider_name.humanize}" }
|
|
65
40
|
redirect_options[:allow_other_host] = true if allow_other_host_redirect?(destination)
|
|
66
41
|
redirect_to destination, redirect_options
|
|
67
42
|
rescue StandardId::OAuthError => e
|
|
@@ -69,12 +44,33 @@ module StandardId
|
|
|
69
44
|
end
|
|
70
45
|
end
|
|
71
46
|
|
|
72
|
-
def
|
|
73
|
-
|
|
47
|
+
def mobile_callback
|
|
48
|
+
unless provider.supports_mobile_callback?
|
|
49
|
+
raise StandardId::InvalidRequestError, "Provider #{provider.provider_name} does not support mobile callback"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
state_data = decode_state_params
|
|
53
|
+
destination = state_data["redirect_uri"]
|
|
54
|
+
|
|
55
|
+
unless allow_other_host_redirect?(destination)
|
|
56
|
+
raise StandardId::InvalidRequestError, "Redirect URI is not allowed"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
relay_params = mobile_relay_params
|
|
60
|
+
@mobile_redirect_url = build_mobile_redirect(destination, relay_params)
|
|
61
|
+
render :mobile_callback, layout: false
|
|
62
|
+
rescue StandardId::InvalidRequestError => e
|
|
63
|
+
render plain: e.message, status: :unprocessable_entity
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def callback_url_for
|
|
69
|
+
"#{request.base_url}#{provider.callback_path}"
|
|
74
70
|
end
|
|
75
71
|
|
|
76
|
-
def
|
|
77
|
-
|
|
72
|
+
def skip_csrf_verification?
|
|
73
|
+
provider.skip_csrf?
|
|
78
74
|
end
|
|
79
75
|
|
|
80
76
|
def decode_state_params
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
module StandardId
|
|
2
2
|
module Web
|
|
3
3
|
class LoginController < BaseController
|
|
4
|
+
include StandardId::InertiaRendering
|
|
5
|
+
|
|
4
6
|
layout "public"
|
|
5
7
|
|
|
6
8
|
skip_before_action :require_browser_session!, only: [:show, :create]
|
|
@@ -11,6 +13,8 @@ module StandardId
|
|
|
11
13
|
def show
|
|
12
14
|
@redirect_uri = params[:redirect_uri] || after_authentication_url
|
|
13
15
|
@connection = params[:connection]
|
|
16
|
+
|
|
17
|
+
render_with_inertia props: auth_page_props
|
|
14
18
|
end
|
|
15
19
|
|
|
16
20
|
def create
|
|
@@ -18,7 +22,7 @@ module StandardId
|
|
|
18
22
|
redirect_to params[:redirect_uri] || after_authentication_url, status: :see_other, notice: "Successfully signed in"
|
|
19
23
|
else
|
|
20
24
|
flash.now[:alert] = "Invalid email or password"
|
|
21
|
-
|
|
25
|
+
render_with_inertia action: :show, props: auth_page_props, status: :unprocessable_content
|
|
22
26
|
end
|
|
23
27
|
end
|
|
24
28
|
|
|
@@ -29,32 +33,19 @@ module StandardId
|
|
|
29
33
|
end
|
|
30
34
|
|
|
31
35
|
def redirect_if_social_login
|
|
32
|
-
|
|
36
|
+
redirect_with_inertia social_login_url, allow_other_host: true if params[:connection].present?
|
|
33
37
|
end
|
|
34
38
|
|
|
35
39
|
def social_login_url
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
google_authorization_url
|
|
39
|
-
when "apple"
|
|
40
|
-
apple_authorization_url
|
|
41
|
-
else
|
|
42
|
-
raise StandardId::InvalidRequestError, "Unsupported social connection: #{connection}"
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
def google_authorization_url
|
|
47
|
-
StandardId::SocialProviders::Google.authorization_url(
|
|
48
|
-
state: encode_state,
|
|
49
|
-
redirect_uri: auth_callback_google_url
|
|
50
|
-
)
|
|
51
|
-
end
|
|
40
|
+
connection = params[:connection]
|
|
41
|
+
provider = StandardId::ProviderRegistry.get(connection)
|
|
52
42
|
|
|
53
|
-
|
|
54
|
-
StandardId::SocialProviders::Apple.authorization_url(
|
|
43
|
+
provider.authorization_url(
|
|
55
44
|
state: encode_state,
|
|
56
|
-
redirect_uri:
|
|
45
|
+
redirect_uri: "#{request.base_url}#{provider.callback_path}"
|
|
57
46
|
)
|
|
47
|
+
rescue StandardId::ProviderRegistry::ProviderNotFoundError => e
|
|
48
|
+
raise StandardId::InvalidRequestError, e.message
|
|
58
49
|
end
|
|
59
50
|
|
|
60
51
|
def encode_state
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
module StandardId
|
|
2
2
|
module Web
|
|
3
3
|
class SignupController < BaseController
|
|
4
|
+
include StandardId::InertiaRendering
|
|
5
|
+
|
|
4
6
|
layout "public"
|
|
5
7
|
|
|
6
8
|
skip_before_action :require_browser_session!, only: [:show, :create]
|
|
@@ -11,6 +13,8 @@ module StandardId
|
|
|
11
13
|
def show
|
|
12
14
|
@redirect_uri = params[:redirect_uri] || after_authentication_url
|
|
13
15
|
@connection = params[:connection] # For social login detection
|
|
16
|
+
|
|
17
|
+
render_with_inertia props: auth_page_props
|
|
14
18
|
end
|
|
15
19
|
|
|
16
20
|
def create
|
|
@@ -24,7 +28,7 @@ module StandardId
|
|
|
24
28
|
end
|
|
25
29
|
|
|
26
30
|
def redirect_if_social_login
|
|
27
|
-
|
|
31
|
+
redirect_with_inertia social_signup_url, allow_other_host: true if params[:connection].present?
|
|
28
32
|
end
|
|
29
33
|
|
|
30
34
|
def handle_password_signup
|
|
@@ -35,8 +39,10 @@ module StandardId
|
|
|
35
39
|
redirect_to params[:redirect_uri] || after_authentication_url,
|
|
36
40
|
notice: "Account created successfully"
|
|
37
41
|
else
|
|
42
|
+
@redirect_uri = params[:redirect_uri] || after_authentication_url
|
|
43
|
+
@connection = params[:connection]
|
|
38
44
|
flash.now[:alert] = form.errors.full_messages.join(", ")
|
|
39
|
-
|
|
45
|
+
render_with_inertia action: :show, props: auth_page_props(errors: form.errors.to_hash), status: :unprocessable_content
|
|
40
46
|
end
|
|
41
47
|
end
|
|
42
48
|
|
|
@@ -55,12 +61,9 @@ module StandardId
|
|
|
55
61
|
end
|
|
56
62
|
|
|
57
63
|
def callback_url
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
when "apple"
|
|
62
|
-
auth_callback_apple_url
|
|
63
|
-
end
|
|
64
|
+
connection = params[:connection]
|
|
65
|
+
provider = StandardId::ProviderRegistry.get(connection)
|
|
66
|
+
"#{request.base_url}#{provider.callback_path}"
|
|
64
67
|
end
|
|
65
68
|
|
|
66
69
|
def encode_state
|
|
@@ -16,15 +16,21 @@ module StandardId
|
|
|
16
16
|
def submit
|
|
17
17
|
return false unless valid?
|
|
18
18
|
|
|
19
|
+
emit_account_creating
|
|
20
|
+
|
|
19
21
|
ActiveRecord::Base.transaction do
|
|
20
22
|
@account = Account.create!(account_params)
|
|
21
|
-
|
|
23
|
+
|
|
24
|
+
password_credential = StandardId::PasswordCredential.create!(
|
|
22
25
|
password_credential_params.merge(
|
|
23
26
|
credential_attributes: {
|
|
24
27
|
identifier_attributes: email_identifier_params.merge(account: @account)
|
|
25
28
|
}
|
|
26
29
|
)
|
|
27
30
|
)
|
|
31
|
+
|
|
32
|
+
emit_account_created
|
|
33
|
+
emit_credential_created(password_credential)
|
|
28
34
|
end
|
|
29
35
|
|
|
30
36
|
true
|
|
@@ -38,6 +44,31 @@ module StandardId
|
|
|
38
44
|
|
|
39
45
|
private
|
|
40
46
|
|
|
47
|
+
def emit_account_creating
|
|
48
|
+
StandardId::Events.publish(
|
|
49
|
+
StandardId::Events::ACCOUNT_CREATING,
|
|
50
|
+
account_params: account_params,
|
|
51
|
+
auth_method: "password"
|
|
52
|
+
)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def emit_account_created
|
|
56
|
+
StandardId::Events.publish(
|
|
57
|
+
StandardId::Events::ACCOUNT_CREATED,
|
|
58
|
+
account: @account,
|
|
59
|
+
auth_method: "password",
|
|
60
|
+
source: "signup"
|
|
61
|
+
)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def emit_credential_created(password_credential)
|
|
65
|
+
StandardId::Events.publish(
|
|
66
|
+
StandardId::Events::CREDENTIAL_PASSWORD_CREATED,
|
|
67
|
+
credential: password_credential,
|
|
68
|
+
account: @account
|
|
69
|
+
)
|
|
70
|
+
end
|
|
71
|
+
|
|
41
72
|
def account_params
|
|
42
73
|
{ name: (email.to_s.split("@").first.presence || "User"), email: }
|
|
43
74
|
end
|
|
@@ -2,6 +2,14 @@ module StandardId
|
|
|
2
2
|
class BrowserSession < Session
|
|
3
3
|
validates :user_agent, presence: true
|
|
4
4
|
|
|
5
|
+
def self.expiry
|
|
6
|
+
StandardId.config.session.browser_session_lifetime.seconds.from_now
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def self.remember_me_expiry
|
|
10
|
+
StandardId.config.session.browser_session_remember_me_lifetime.seconds.from_now
|
|
11
|
+
end
|
|
12
|
+
|
|
5
13
|
def browser_info
|
|
6
14
|
return {} if user_agent.blank?
|
|
7
15
|
|
|
@@ -18,6 +18,7 @@ module StandardId
|
|
|
18
18
|
|
|
19
19
|
def revoke!
|
|
20
20
|
update!(active: false, revoked_at: Time.current)
|
|
21
|
+
emit_revoked_event
|
|
21
22
|
end
|
|
22
23
|
|
|
23
24
|
def active?
|
|
@@ -57,6 +58,16 @@ module StandardId
|
|
|
57
58
|
self.client_secret ||= SecureRandom.hex(32)
|
|
58
59
|
end
|
|
59
60
|
|
|
61
|
+
def emit_revoked_event
|
|
62
|
+
StandardId::Events.publish(
|
|
63
|
+
StandardId::Events::CREDENTIAL_CLIENT_SECRET_REVOKED,
|
|
64
|
+
credential: self,
|
|
65
|
+
client_application: client_application,
|
|
66
|
+
client_id: client_id,
|
|
67
|
+
revoked_at: revoked_at
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
|
|
60
71
|
# Note: We intentionally do not enforce subset validation for per-secret overrides here.
|
|
61
72
|
# If needed later, we can introduce a configuration flag to enable enforcement.
|
|
62
73
|
end
|