standard_id 0.2.0 → 0.2.1
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/social_authentication.rb +8 -6
- data/app/controllers/concerns/standard_id/web/social_login_params.rb +87 -0
- data/app/controllers/concerns/standard_id/web_authentication.rb +10 -0
- data/app/controllers/standard_id/api/oauth/callback/providers_controller.rb +5 -19
- data/app/controllers/standard_id/web/auth/callback/providers_controller.rb +15 -11
- data/app/controllers/standard_id/web/login_controller.rb +42 -13
- data/lib/standard_id/oauth/social_flow.rb +3 -8
- data/lib/standard_id/providers/base.rb +16 -0
- data/lib/standard_id/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: acbf22ea3a73945fedbcc5d26da84954f4b4e04de00cf2bac51eb59374231a09
|
|
4
|
+
data.tar.gz: 063d9c263aa7ca6910602a1a570676ee96348a88b1265a2c2ac5d8c11dacf076
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8a3e58978c5525de51c16ad46e563567a93790a7ad5a99aeea1d43d3068c01df9120ccc5d0a5694a5e3531fe916906bcc811f2a2dcbce25ec42228ca7ddfa4e5
|
|
7
|
+
data.tar.gz: 2f3d4beee53b0fa961ed8648eb433a65e751f63c8f20fa10153e10c16bfce50440a5714bbd9b8a8cc4fefe98ff806804c6f49e6e3a79362b10e9780a55d49fce
|
|
@@ -16,12 +16,13 @@ module StandardId
|
|
|
16
16
|
raise StandardId::InvalidRequestError, e.message
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
def get_user_info_from_provider(redirect_uri: nil, flow: :web)
|
|
19
|
+
def get_user_info_from_provider(redirect_uri: nil, nonce: nil, flow: :web)
|
|
20
20
|
provider_params = {
|
|
21
21
|
code: params[:code],
|
|
22
22
|
id_token: params[:id_token],
|
|
23
23
|
access_token: params[:access_token],
|
|
24
|
-
redirect_uri
|
|
24
|
+
redirect_uri:,
|
|
25
|
+
nonce:
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
resolved_params = provider.resolve_params(provider_params, context: { flow: flow })
|
|
@@ -100,8 +101,8 @@ module StandardId
|
|
|
100
101
|
end
|
|
101
102
|
end
|
|
102
103
|
|
|
103
|
-
def run_social_callback(provider:, social_info:, provider_tokens:, account:)
|
|
104
|
-
emit_social_auth_completed(provider, social_info, provider_tokens, account)
|
|
104
|
+
def run_social_callback(provider:, social_info:, provider_tokens:, account:, original_request_params: {})
|
|
105
|
+
emit_social_auth_completed(provider, social_info, provider_tokens, account, original_request_params)
|
|
105
106
|
end
|
|
106
107
|
|
|
107
108
|
def emit_social_user_info_fetched(provider, social_info, email)
|
|
@@ -131,13 +132,14 @@ module StandardId
|
|
|
131
132
|
)
|
|
132
133
|
end
|
|
133
134
|
|
|
134
|
-
def emit_social_auth_completed(provider, social_info, provider_tokens, account)
|
|
135
|
+
def emit_social_auth_completed(provider, social_info, provider_tokens, account, original_request_params)
|
|
135
136
|
StandardId::Events.publish(
|
|
136
137
|
StandardId::Events::SOCIAL_AUTH_COMPLETED,
|
|
137
138
|
account: account,
|
|
138
139
|
provider: provider,
|
|
139
140
|
social_info: social_info,
|
|
140
|
-
tokens: provider_tokens
|
|
141
|
+
tokens: provider_tokens,
|
|
142
|
+
original_request_params: original_request_params
|
|
141
143
|
)
|
|
142
144
|
end
|
|
143
145
|
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
module StandardId
|
|
2
|
+
module Web
|
|
3
|
+
module SocialLoginParams
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
OAUTH_PENDING_REQUESTS_COOKIE = "oauth_pending_requests".freeze
|
|
7
|
+
REQUEST_EXPIRY = 10.minutes
|
|
8
|
+
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def store_oauth_request(state:, nonce: nil, params:)
|
|
12
|
+
pending_requests = load_pending_requests || {}
|
|
13
|
+
|
|
14
|
+
cleanup_expired_requests!(pending_requests)
|
|
15
|
+
|
|
16
|
+
pending_requests[state] = {
|
|
17
|
+
"params" => params,
|
|
18
|
+
"nonce" => nonce,
|
|
19
|
+
"expires_at" => REQUEST_EXPIRY.from_now.to_i
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
save_pending_requests(pending_requests)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def consume_oauth_request(state)
|
|
26
|
+
return nil if state.blank?
|
|
27
|
+
|
|
28
|
+
pending_requests = load_pending_requests
|
|
29
|
+
return nil if pending_requests.nil?
|
|
30
|
+
|
|
31
|
+
cleanup_expired_requests!(pending_requests)
|
|
32
|
+
|
|
33
|
+
request_data = pending_requests[state]
|
|
34
|
+
return nil if request_data.nil?
|
|
35
|
+
|
|
36
|
+
# Remove this specific request from pending requests
|
|
37
|
+
pending_requests.delete(state)
|
|
38
|
+
|
|
39
|
+
# Update the cookie with remaining requests
|
|
40
|
+
if pending_requests.empty?
|
|
41
|
+
cookies.delete(OAUTH_PENDING_REQUESTS_COOKIE)
|
|
42
|
+
else
|
|
43
|
+
save_pending_requests(pending_requests)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
request_data.slice("params", "nonce")
|
|
47
|
+
rescue JSON::ParserError => e
|
|
48
|
+
StandardId.logger.error({
|
|
49
|
+
subject: "standard_id.consume_oauth_request.error",
|
|
50
|
+
error: e.message
|
|
51
|
+
})
|
|
52
|
+
nil
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def load_pending_requests
|
|
56
|
+
cookie_value = cookies.encrypted[OAUTH_PENDING_REQUESTS_COOKIE]
|
|
57
|
+
return nil if cookie_value.nil?
|
|
58
|
+
|
|
59
|
+
JSON.parse(cookie_value)
|
|
60
|
+
rescue JSON::ParserError
|
|
61
|
+
nil
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def save_pending_requests(pending_requests)
|
|
65
|
+
cookie_options = {
|
|
66
|
+
value: pending_requests.to_json,
|
|
67
|
+
expires: REQUEST_EXPIRY.from_now,
|
|
68
|
+
httponly: true
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if request.ssl?
|
|
72
|
+
cookie_options[:secure] = true
|
|
73
|
+
cookie_options[:same_site] = :none
|
|
74
|
+
else
|
|
75
|
+
cookie_options[:same_site] = :lax
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
cookies.encrypted[OAUTH_PENDING_REQUESTS_COOKIE] = cookie_options
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def cleanup_expired_requests!(pending_requests)
|
|
82
|
+
current_time = Time.now.to_i
|
|
83
|
+
pending_requests.delete_if { |_state, data| data["expires_at"] && data["expires_at"] < current_time }
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -36,6 +36,16 @@ module StandardId
|
|
|
36
36
|
# Redirect to login page, handling both Inertia and standard requests
|
|
37
37
|
def redirect_to_login
|
|
38
38
|
login_path = StandardId.config.login_url.presence || "/login"
|
|
39
|
+
|
|
40
|
+
# Add redirect_uri parameter to preserve the original destination
|
|
41
|
+
if request.get?
|
|
42
|
+
uri = URI.parse(login_path)
|
|
43
|
+
params = Rack::Utils.parse_nested_query(uri.query)
|
|
44
|
+
params["redirect_uri"] = request.fullpath
|
|
45
|
+
uri.query = params.to_query.presence
|
|
46
|
+
login_path = uri.to_s
|
|
47
|
+
end
|
|
48
|
+
|
|
39
49
|
redirect_with_inertia login_path
|
|
40
50
|
end
|
|
41
51
|
|
|
@@ -7,7 +7,6 @@ module StandardId
|
|
|
7
7
|
skip_before_action :validate_content_type!
|
|
8
8
|
|
|
9
9
|
def callback
|
|
10
|
-
original_params = decode_state_params
|
|
11
10
|
provider_response = get_user_info_from_provider(flow: resolve_flow_for(provider.provider_name))
|
|
12
11
|
social_info = provider_response[:user_info]
|
|
13
12
|
provider_tokens = provider_response[:tokens]
|
|
@@ -16,35 +15,22 @@ module StandardId
|
|
|
16
15
|
flow = StandardId::Oauth::SocialFlow.new(
|
|
17
16
|
params,
|
|
18
17
|
request,
|
|
19
|
-
account
|
|
20
|
-
connection: provider.provider_name
|
|
21
|
-
original_params: original_params
|
|
18
|
+
account:,
|
|
19
|
+
connection: provider.provider_name
|
|
22
20
|
)
|
|
23
21
|
|
|
24
22
|
token_response = flow.execute
|
|
25
23
|
run_social_callback(
|
|
26
24
|
provider: provider.provider_name,
|
|
27
|
-
social_info
|
|
28
|
-
provider_tokens
|
|
29
|
-
account:
|
|
25
|
+
social_info:,
|
|
26
|
+
provider_tokens:,
|
|
27
|
+
account:
|
|
30
28
|
)
|
|
31
29
|
render json: token_response, status: :ok
|
|
32
30
|
end
|
|
33
31
|
|
|
34
32
|
private
|
|
35
33
|
|
|
36
|
-
def decode_state_params
|
|
37
|
-
encoded_state = params[:state]
|
|
38
|
-
|
|
39
|
-
return {} if encoded_state.blank?
|
|
40
|
-
|
|
41
|
-
begin
|
|
42
|
-
JSON.parse(Base64.urlsafe_decode64(encoded_state))
|
|
43
|
-
rescue JSON::ParserError, ArgumentError
|
|
44
|
-
raise StandardId::InvalidRequestError, "Invalid state parameter"
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
|
|
48
34
|
def resolve_flow_for(connection)
|
|
49
35
|
return :mobile unless connection == "apple"
|
|
50
36
|
|
|
@@ -5,6 +5,7 @@ module StandardId
|
|
|
5
5
|
class ProvidersController < StandardId::Web::BaseController
|
|
6
6
|
include StandardId::WebAuthentication
|
|
7
7
|
include StandardId::SocialAuthentication
|
|
8
|
+
include StandardId::Web::SocialLoginParams
|
|
8
9
|
|
|
9
10
|
# Social callbacks must be accessible without an existing browser session
|
|
10
11
|
# because they create/sign-in the session upon successful callback.
|
|
@@ -20,9 +21,9 @@ module StandardId
|
|
|
20
21
|
state_data = nil
|
|
21
22
|
|
|
22
23
|
begin
|
|
23
|
-
state_data
|
|
24
|
+
extract_state_and_nonce => { state_data:, nonce: }
|
|
24
25
|
redirect_uri = callback_url_for
|
|
25
|
-
provider_response = get_user_info_from_provider(redirect_uri:
|
|
26
|
+
provider_response = get_user_info_from_provider(redirect_uri:, nonce:)
|
|
26
27
|
social_info = provider_response[:user_info]
|
|
27
28
|
provider_tokens = provider_response[:tokens]
|
|
28
29
|
account = find_or_create_account_from_social(social_info)
|
|
@@ -33,6 +34,7 @@ module StandardId
|
|
|
33
34
|
social_info: social_info,
|
|
34
35
|
provider_tokens: provider_tokens,
|
|
35
36
|
account: account,
|
|
37
|
+
original_request_params: state_data
|
|
36
38
|
)
|
|
37
39
|
|
|
38
40
|
destination = state_data["redirect_uri"]
|
|
@@ -49,7 +51,7 @@ module StandardId
|
|
|
49
51
|
raise StandardId::InvalidRequestError, "Provider #{provider.provider_name} does not support mobile callback"
|
|
50
52
|
end
|
|
51
53
|
|
|
52
|
-
state_data
|
|
54
|
+
extract_state_and_nonce => { state_data: }
|
|
53
55
|
destination = state_data["redirect_uri"]
|
|
54
56
|
|
|
55
57
|
unless allow_other_host_redirect?(destination)
|
|
@@ -73,15 +75,17 @@ module StandardId
|
|
|
73
75
|
provider.skip_csrf?
|
|
74
76
|
end
|
|
75
77
|
|
|
76
|
-
def
|
|
77
|
-
|
|
78
|
-
raise StandardId::InvalidRequestError, "Missing state parameter" if
|
|
78
|
+
def extract_state_and_nonce
|
|
79
|
+
state_token = params[:state]
|
|
80
|
+
raise StandardId::InvalidRequestError, "Missing state parameter" if state_token.blank?
|
|
79
81
|
|
|
80
|
-
|
|
81
|
-
state
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
82
|
+
oauth_state = consume_oauth_request(state_token)
|
|
83
|
+
raise StandardId::InvalidRequestError, "Invalid or expired state parameter" if oauth_state.nil?
|
|
84
|
+
|
|
85
|
+
{
|
|
86
|
+
state_data: oauth_state["params"],
|
|
87
|
+
nonce: oauth_state["nonce"]
|
|
88
|
+
}
|
|
85
89
|
end
|
|
86
90
|
|
|
87
91
|
def handle_callback_error
|
|
@@ -2,6 +2,8 @@ module StandardId
|
|
|
2
2
|
module Web
|
|
3
3
|
class LoginController < BaseController
|
|
4
4
|
include StandardId::InertiaRendering
|
|
5
|
+
include StandardId::Web::SocialLoginParams
|
|
6
|
+
|
|
5
7
|
|
|
6
8
|
layout "public"
|
|
7
9
|
|
|
@@ -33,26 +35,53 @@ module StandardId
|
|
|
33
35
|
end
|
|
34
36
|
|
|
35
37
|
def redirect_if_social_login
|
|
36
|
-
|
|
37
|
-
|
|
38
|
+
return unless params[:connection].present?
|
|
39
|
+
|
|
40
|
+
provider = StandardId::ProviderRegistry.get(params[:connection].to_s)
|
|
41
|
+
|
|
42
|
+
state = generate_oauth_token
|
|
43
|
+
nonce = provider_supports_nonce?(provider) ? generate_oauth_token : nil
|
|
38
44
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
45
|
+
store_oauth_request(
|
|
46
|
+
state:,
|
|
47
|
+
nonce:,
|
|
48
|
+
params: extract_social_login_params
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
callback_url = "#{request.base_url}#{provider.callback_path}"
|
|
52
|
+
extra_params = extract_oauth_params(provider)
|
|
42
53
|
|
|
43
|
-
provider
|
|
44
|
-
|
|
45
|
-
|
|
54
|
+
# Add nonce to OAuth params if provider supports it
|
|
55
|
+
extra_params[:nonce] = nonce if nonce.present?
|
|
56
|
+
|
|
57
|
+
url = provider.authorization_url(
|
|
58
|
+
state:,
|
|
59
|
+
redirect_uri: callback_url,
|
|
60
|
+
**extra_params.compact
|
|
46
61
|
)
|
|
62
|
+
|
|
63
|
+
redirect_with_inertia url, allow_other_host: true
|
|
47
64
|
rescue StandardId::ProviderRegistry::ProviderNotFoundError => e
|
|
48
65
|
raise StandardId::InvalidRequestError, e.message
|
|
49
66
|
end
|
|
50
67
|
|
|
51
|
-
def
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
68
|
+
def extract_social_login_params
|
|
69
|
+
request.parameters.except("controller", "action", "format", "authenticity_token", "commit", "login").to_h.deep_dup
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def extract_oauth_params(provider)
|
|
73
|
+
supported_params = provider.try(:supported_authorization_params)
|
|
74
|
+
return {} if supported_params.blank?
|
|
75
|
+
|
|
76
|
+
params.permit(*supported_params).to_h.compact.symbolize_keys
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def generate_oauth_token
|
|
80
|
+
SecureRandom.urlsafe_base64(32)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def provider_supports_nonce?(provider)
|
|
84
|
+
provider.supported_authorization_params.include?(:nonce)
|
|
56
85
|
end
|
|
57
86
|
|
|
58
87
|
def login_params
|
|
@@ -3,11 +3,10 @@ module StandardId
|
|
|
3
3
|
class SocialFlow < TokenGrantFlow
|
|
4
4
|
attr_reader :account, :connection, :original_params
|
|
5
5
|
|
|
6
|
-
def initialize(params, request, account:, connection
|
|
6
|
+
def initialize(params, request, account:, connection:)
|
|
7
7
|
super(params, request)
|
|
8
8
|
@account = account
|
|
9
9
|
@connection = connection
|
|
10
|
-
@original_params = original_params
|
|
11
10
|
end
|
|
12
11
|
|
|
13
12
|
def authenticate!
|
|
@@ -21,21 +20,17 @@ module StandardId
|
|
|
21
20
|
end
|
|
22
21
|
|
|
23
22
|
def client_id
|
|
24
|
-
|
|
23
|
+
nil
|
|
25
24
|
end
|
|
26
25
|
|
|
27
26
|
def token_scope
|
|
28
|
-
|
|
27
|
+
nil
|
|
29
28
|
end
|
|
30
29
|
|
|
31
30
|
def grant_type
|
|
32
31
|
"social"
|
|
33
32
|
end
|
|
34
33
|
|
|
35
|
-
def audience
|
|
36
|
-
@original_params["audience"]
|
|
37
|
-
end
|
|
38
|
-
|
|
39
34
|
def supports_refresh_token?
|
|
40
35
|
true
|
|
41
36
|
end
|
|
@@ -209,6 +209,22 @@ module StandardId
|
|
|
209
209
|
false
|
|
210
210
|
end
|
|
211
211
|
|
|
212
|
+
# Returns list of supported authorization parameters for this provider.
|
|
213
|
+
#
|
|
214
|
+
# Include :nonce in this list for OIDC providers to enable nonce validation.
|
|
215
|
+
# Nonce provides replay attack protection for ID tokens.
|
|
216
|
+
#
|
|
217
|
+
# @return [Array<Symbol>] List of supported parameters
|
|
218
|
+
#
|
|
219
|
+
# @example
|
|
220
|
+
# def supported_authorization_params
|
|
221
|
+
# [:scope, :prompt, :nonce]
|
|
222
|
+
# end
|
|
223
|
+
#
|
|
224
|
+
def supported_authorization_params
|
|
225
|
+
[]
|
|
226
|
+
end
|
|
227
|
+
|
|
212
228
|
# Optional setup hook called when provider is registered.
|
|
213
229
|
#
|
|
214
230
|
# Override this method to perform initialization tasks like:
|
data/lib/standard_id/version.rb
CHANGED
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.2.
|
|
4
|
+
version: 0.2.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jaryl Sim
|
|
@@ -69,6 +69,7 @@ files:
|
|
|
69
69
|
- app/controllers/concerns/standard_id/inertia_support.rb
|
|
70
70
|
- app/controllers/concerns/standard_id/set_current_request_details.rb
|
|
71
71
|
- app/controllers/concerns/standard_id/social_authentication.rb
|
|
72
|
+
- app/controllers/concerns/standard_id/web/social_login_params.rb
|
|
72
73
|
- app/controllers/concerns/standard_id/web_authentication.rb
|
|
73
74
|
- app/controllers/standard_id/api/authorization_controller.rb
|
|
74
75
|
- app/controllers/standard_id/api/base_controller.rb
|