better_auth-sso 0.6.2 → 0.8.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/CHANGELOG.md +10 -0
- data/README.md +36 -2
- data/lib/better_auth/plugins/sso.rb +10 -1766
- data/lib/better_auth/sso/linking/org_assignment.rb +0 -3
- data/lib/better_auth/sso/plugin/core.rb +139 -0
- data/lib/better_auth/sso/plugin/endpoints.rb +151 -0
- data/lib/better_auth/sso/plugin/oidc_discovery.rb +75 -0
- data/lib/better_auth/sso/plugin/oidc_runtime.rb +420 -0
- data/lib/better_auth/sso/plugin/provider_utils.rb +216 -0
- data/lib/better_auth/sso/plugin/providers.rb +131 -0
- data/lib/better_auth/sso/plugin/saml_metadata_and_logout.rb +352 -0
- data/lib/better_auth/sso/plugin/saml_response.rb +150 -0
- data/lib/better_auth/sso/plugin/saml_validation_and_state.rb +183 -0
- data/lib/better_auth/sso/plugin/sign_in_and_oidc_callbacks.rb +125 -0
- data/lib/better_auth/sso/routes/schemas.rb +14 -8
- data/lib/better_auth/sso/routes/sso.rb +1 -1
- data/lib/better_auth/sso/saml_state.rb +1 -1
- data/lib/better_auth/sso/version.rb +1 -1
- metadata +27 -2
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterAuth
|
|
4
|
+
module Plugins
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def sso_parse_saml_response(value, config = {}, provider = nil, ctx = nil)
|
|
8
|
+
parser = config.dig(:saml, :parse_response)
|
|
9
|
+
if parser.respond_to?(:call)
|
|
10
|
+
sso_validate_single_saml_assertion!(value) if sso_base64_xml?(value)
|
|
11
|
+
parsed = parser.call(raw_response: value.to_s, provider: provider, context: ctx)
|
|
12
|
+
return normalize_hash(parsed)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
JSON.parse(Base64.decode64(value.to_s), symbolize_names: true)
|
|
16
|
+
rescue APIError
|
|
17
|
+
raise APIError.new("BAD_REQUEST", message: "Invalid SAML response")
|
|
18
|
+
rescue
|
|
19
|
+
raise APIError.new("BAD_REQUEST", message: "Invalid SAML response")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def sso_validate_single_saml_assertion!(saml_response)
|
|
23
|
+
xml = Base64.decode64(saml_response.to_s)
|
|
24
|
+
raise APIError.new("BAD_REQUEST", message: "Invalid base64-encoded SAML response") unless xml.include?("<")
|
|
25
|
+
|
|
26
|
+
assertions = xml.scan(/<(?:\w+:)?Assertion(?:\s|>|\/)/).length
|
|
27
|
+
encrypted_assertions = xml.scan(/<(?:\w+:)?EncryptedAssertion(?:\s|>|\/)/).length
|
|
28
|
+
total = assertions + encrypted_assertions
|
|
29
|
+
raise APIError.new("BAD_REQUEST", message: "SAML response contains no assertions") if total.zero?
|
|
30
|
+
if total > 1
|
|
31
|
+
raise APIError.new("BAD_REQUEST", message: "SAML response contains #{total} assertions, expected exactly 1")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
true
|
|
35
|
+
rescue APIError
|
|
36
|
+
raise
|
|
37
|
+
rescue
|
|
38
|
+
raise APIError.new("BAD_REQUEST", message: "Invalid base64-encoded SAML response")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def sso_validate_saml_timestamp!(conditions, config = {}, now: Time.now.utc)
|
|
42
|
+
conditions = normalize_hash(conditions || {})
|
|
43
|
+
not_before = conditions[:not_before] || conditions[:notBefore]
|
|
44
|
+
not_on_or_after = conditions[:not_on_or_after] || conditions[:notOnOrAfter]
|
|
45
|
+
if not_before.to_s.empty? && not_on_or_after.to_s.empty?
|
|
46
|
+
raise APIError.new("BAD_REQUEST", message: "SAML assertion missing required timestamp conditions") if config.dig(:saml, :require_timestamps)
|
|
47
|
+
|
|
48
|
+
return true
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
clock_skew_seconds = ((config.dig(:saml, :clock_skew) || SSO_DEFAULT_CLOCK_SKEW_MS).to_f / 1000.0)
|
|
52
|
+
parsed_not_before = sso_parse_saml_timestamp(not_before, "SAML assertion has invalid NotBefore timestamp") unless not_before.to_s.empty?
|
|
53
|
+
parsed_not_on_or_after = sso_parse_saml_timestamp(not_on_or_after, "SAML assertion has invalid NotOnOrAfter timestamp") unless not_on_or_after.to_s.empty?
|
|
54
|
+
|
|
55
|
+
raise APIError.new("BAD_REQUEST", message: "SAML assertion is not yet valid") if parsed_not_before && now < (parsed_not_before - clock_skew_seconds)
|
|
56
|
+
raise APIError.new("BAD_REQUEST", message: "SAML assertion has expired") if parsed_not_on_or_after && now > (parsed_not_on_or_after + clock_skew_seconds)
|
|
57
|
+
|
|
58
|
+
true
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def sso_parse_saml_timestamp(value, error_message)
|
|
62
|
+
Time.parse(value.to_s).utc
|
|
63
|
+
rescue
|
|
64
|
+
raise APIError.new("BAD_REQUEST", message: error_message)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def sso_saml_timestamp_conditions(assertion)
|
|
68
|
+
assertion = normalize_hash(assertion || {})
|
|
69
|
+
conditions = normalize_hash(assertion[:conditions] || {})
|
|
70
|
+
conditions[:not_before] ||= assertion[:not_before] || assertion[:notBefore]
|
|
71
|
+
conditions[:not_on_or_after] ||= assertion[:not_on_or_after] || assertion[:notOnOrAfter]
|
|
72
|
+
conditions
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def sso_base64_xml?(value)
|
|
76
|
+
Base64.decode64(value.to_s).lstrip.start_with?("<")
|
|
77
|
+
rescue
|
|
78
|
+
false
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def sso_validate_saml_algorithms!(xml, options = {})
|
|
82
|
+
on_deprecated = (options[:on_deprecated] || "warn").to_s
|
|
83
|
+
signature_algorithms = xml.to_s.scan(/SignatureMethod[^>]+Algorithm=["']([^"']+)["']/).flatten.map { |algorithm| sso_normalize_saml_signature_algorithm(algorithm) }
|
|
84
|
+
digest_algorithms = xml.to_s.scan(/DigestMethod[^>]+Algorithm=["']([^"']+)["']/).flatten.map { |algorithm| sso_normalize_saml_digest_algorithm(algorithm) }
|
|
85
|
+
key_encryption_algorithms = xml.to_s.scan(/<[^\/>]*EncryptedKey\b[\s\S]*?EncryptionMethod[^>]+Algorithm=["']([^"']+)["']/).flatten
|
|
86
|
+
data_encryption_algorithms = xml.to_s.scan(/<[^\/>]*EncryptedData\b[\s\S]*?EncryptionMethod[^>]+Algorithm=["']([^"']+)["']/).flatten
|
|
87
|
+
|
|
88
|
+
sso_validate_saml_algorithm_group!(
|
|
89
|
+
signature_algorithms,
|
|
90
|
+
allowed: options[:allowed_signature_algorithms]&.map { |algorithm| sso_normalize_saml_signature_algorithm(algorithm) },
|
|
91
|
+
secure: SSO_SAML_SECURE_SIGNATURE_ALGORITHMS,
|
|
92
|
+
deprecated: ["http://www.w3.org/2000/09/xmldsig#rsa-sha1"],
|
|
93
|
+
on_deprecated: on_deprecated,
|
|
94
|
+
label: "signature"
|
|
95
|
+
)
|
|
96
|
+
sso_validate_saml_algorithm_group!(
|
|
97
|
+
digest_algorithms,
|
|
98
|
+
allowed: options[:allowed_digest_algorithms]&.map { |algorithm| sso_normalize_saml_digest_algorithm(algorithm) },
|
|
99
|
+
secure: SSO_SAML_SECURE_DIGEST_ALGORITHMS,
|
|
100
|
+
deprecated: ["http://www.w3.org/2000/09/xmldsig#sha1"],
|
|
101
|
+
on_deprecated: on_deprecated,
|
|
102
|
+
label: "digest"
|
|
103
|
+
)
|
|
104
|
+
sso_validate_saml_algorithm_group!(
|
|
105
|
+
key_encryption_algorithms,
|
|
106
|
+
allowed: options[:allowed_key_encryption_algorithms],
|
|
107
|
+
secure: SSO_SAML_SECURE_KEY_ENCRYPTION_ALGORITHMS,
|
|
108
|
+
deprecated: ["http://www.w3.org/2001/04/xmlenc#rsa-1_5"],
|
|
109
|
+
on_deprecated: on_deprecated,
|
|
110
|
+
label: "key encryption"
|
|
111
|
+
)
|
|
112
|
+
sso_validate_saml_algorithm_group!(
|
|
113
|
+
data_encryption_algorithms,
|
|
114
|
+
allowed: options[:allowed_data_encryption_algorithms],
|
|
115
|
+
secure: SSO_SAML_SECURE_DATA_ENCRYPTION_ALGORITHMS,
|
|
116
|
+
deprecated: ["http://www.w3.org/2001/04/xmlenc#tripledes-cbc"],
|
|
117
|
+
on_deprecated: on_deprecated,
|
|
118
|
+
label: "data encryption"
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
true
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def sso_normalize_saml_signature_algorithm(algorithm)
|
|
125
|
+
SSO_SAML_SIGNATURE_ALGORITHMS.fetch(algorithm.to_s.downcase, algorithm.to_s)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def sso_normalize_saml_digest_algorithm(algorithm)
|
|
129
|
+
SSO_SAML_DIGEST_ALGORITHMS.fetch(algorithm.to_s.downcase, algorithm.to_s)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def sso_validate_saml_algorithm_group!(algorithms, allowed:, secure:, deprecated:, on_deprecated:, label:)
|
|
133
|
+
algorithms.each do |algorithm|
|
|
134
|
+
if allowed
|
|
135
|
+
next if allowed.include?(algorithm)
|
|
136
|
+
|
|
137
|
+
raise APIError.new("BAD_REQUEST", message: "SAML #{label} algorithm not in allow-list: #{algorithm}")
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
if deprecated.include?(algorithm)
|
|
141
|
+
raise APIError.new("BAD_REQUEST", message: "SAML response uses deprecated #{label} algorithm: #{algorithm}") if on_deprecated == "reject"
|
|
142
|
+
next
|
|
143
|
+
end
|
|
144
|
+
next if secure.include?(algorithm)
|
|
145
|
+
|
|
146
|
+
raise APIError.new("BAD_REQUEST", message: "SAML #{label} algorithm not recognized: #{algorithm}")
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def sso_generate_saml_relay_state(ctx, state_data)
|
|
151
|
+
ttl_ms = 10 * 60 * 1000
|
|
152
|
+
relay_state = BetterAuth::Crypto.random_string(32)
|
|
153
|
+
now_ms = (Time.now.to_f * 1000).to_i
|
|
154
|
+
stored = state_data.each_with_object({}) { |(key, value), result| result[key.to_s] = value }.merge(
|
|
155
|
+
"codeVerifier" => BetterAuth::Crypto.random_string(128),
|
|
156
|
+
"expiresAt" => now_ms + ttl_ms
|
|
157
|
+
)
|
|
158
|
+
ctx.context.internal_adapter.create_verification_value(
|
|
159
|
+
identifier: "#{SSO_SAML_RELAY_STATE_KEY_PREFIX}#{relay_state}",
|
|
160
|
+
value: JSON.generate(stored),
|
|
161
|
+
expiresAt: Time.at((now_ms + ttl_ms) / 1000.0)
|
|
162
|
+
)
|
|
163
|
+
ctx.set_signed_cookie("relay_state", relay_state, ctx.context.secret, path: "/", max_age: ttl_ms / 1000, http_only: true, same_site: "lax")
|
|
164
|
+
relay_state
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def sso_parse_saml_relay_state(ctx, relay_state)
|
|
168
|
+
state = sso_verify_state(relay_state, ctx.context.secret)
|
|
169
|
+
return state if state
|
|
170
|
+
|
|
171
|
+
verification = ctx.context.internal_adapter.find_verification_value("#{SSO_SAML_RELAY_STATE_KEY_PREFIX}#{relay_state}")
|
|
172
|
+
return nil unless verification
|
|
173
|
+
return nil unless sso_future_time?(verification.fetch("expiresAt"))
|
|
174
|
+
|
|
175
|
+
parsed = JSON.parse(verification.fetch("value"))
|
|
176
|
+
return nil if parsed["expiresAt"].to_i <= (Time.now.to_f * 1000).to_i
|
|
177
|
+
|
|
178
|
+
parsed
|
|
179
|
+
rescue
|
|
180
|
+
nil
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterAuth
|
|
4
|
+
module Plugins
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def sso_sign_in_endpoint(config = {})
|
|
8
|
+
Endpoint.new(path: "/sign-in/sso", method: "POST") do |ctx|
|
|
9
|
+
body = normalize_hash(ctx.body)
|
|
10
|
+
provider = sso_select_provider(ctx, body, config)
|
|
11
|
+
provider_type = body[:provider_type].to_s
|
|
12
|
+
if provider_type == "oidc" && !provider["oidcConfig"]
|
|
13
|
+
raise APIError.new("BAD_REQUEST", message: "OIDC provider is not configured")
|
|
14
|
+
end
|
|
15
|
+
if provider_type == "saml" && !provider["samlConfig"]
|
|
16
|
+
raise APIError.new("BAD_REQUEST", message: "SAML provider is not configured")
|
|
17
|
+
end
|
|
18
|
+
if config.dig(:domain_verification, :enabled) && !(provider.key?("domainVerified") && provider["domainVerified"])
|
|
19
|
+
raise APIError.new("UNAUTHORIZED", message: "Provider domain has not been verified")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
state_data = {
|
|
23
|
+
providerId: provider.fetch("providerId"),
|
|
24
|
+
callbackURL: body[:callback_url] || "/",
|
|
25
|
+
errorURL: body[:error_callback_url],
|
|
26
|
+
newUserURL: body[:new_user_callback_url],
|
|
27
|
+
requestSignUp: body[:request_sign_up]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if provider["oidcConfig"] && provider_type != "saml"
|
|
31
|
+
provider = sso_ensure_runtime_oidc_provider(ctx, provider, config)
|
|
32
|
+
state = BetterAuth::Crypto.sign_jwt(
|
|
33
|
+
state_data.merge({nonce: BetterAuth::Crypto.random_string(32)}).merge(sso_oidc_pkce_state(provider)),
|
|
34
|
+
ctx.context.secret,
|
|
35
|
+
expires_in: 600
|
|
36
|
+
)
|
|
37
|
+
url = sso_oidc_authorization_url(provider, ctx, state, config, body)
|
|
38
|
+
elsif provider["samlConfig"]
|
|
39
|
+
relay_state = sso_generate_saml_relay_state(ctx, state_data)
|
|
40
|
+
url = sso_saml_authorization_url(provider, relay_state, ctx, config)
|
|
41
|
+
sso_store_saml_authn_request(ctx, provider, url, config)
|
|
42
|
+
else
|
|
43
|
+
raise APIError.new("BAD_REQUEST", message: "OIDC provider is not configured")
|
|
44
|
+
end
|
|
45
|
+
ctx.json({url: url, redirect: true})
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def sso_oidc_callback_endpoint(config = {})
|
|
50
|
+
Endpoint.new(path: "/sso/callback/:providerId", method: "GET") do |ctx|
|
|
51
|
+
sso_handle_oidc_callback(ctx, config, sso_fetch(ctx.params, :provider_id))
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def sso_oidc_shared_callback_endpoint(config = {})
|
|
56
|
+
Endpoint.new(path: "/sso/callback", method: "GET") do |ctx|
|
|
57
|
+
state = sso_verify_state(ctx.query[:state] || ctx.query["state"], ctx.context.secret)
|
|
58
|
+
next ctx.redirect("#{ctx.context.base_url}/error?error=invalid_state") unless state
|
|
59
|
+
|
|
60
|
+
sso_handle_oidc_callback(ctx, config, state["providerId"], state: state)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def sso_handle_oidc_callback(ctx, config, provider_id, state: nil)
|
|
65
|
+
state ||= sso_verify_state(ctx.query[:state] || ctx.query["state"], ctx.context.secret)
|
|
66
|
+
return ctx.redirect("#{ctx.context.base_url}/error?error=invalid_state") unless state
|
|
67
|
+
|
|
68
|
+
callback_url = state["callbackURL"] || "/"
|
|
69
|
+
error_url = state["errorURL"] || callback_url
|
|
70
|
+
if ctx.query[:error] || ctx.query["error"]
|
|
71
|
+
error = ctx.query[:error] || ctx.query["error"]
|
|
72
|
+
description = ctx.query[:error_description] || ctx.query["error_description"]
|
|
73
|
+
return sso_redirect(ctx, sso_append_error(error_url, error, description))
|
|
74
|
+
end
|
|
75
|
+
state_provider_id = state["providerId"] || state[:providerId]
|
|
76
|
+
if state_provider_id.to_s != provider_id.to_s
|
|
77
|
+
return sso_redirect(ctx, sso_append_error(error_url, "invalid_state", "provider mismatch"))
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
provider = sso_callback_provider(ctx, config, provider_id)
|
|
81
|
+
return sso_redirect(ctx, sso_append_error(error_url, "invalid_provider", "provider not found")) unless provider
|
|
82
|
+
if config.dig(:domain_verification, :enabled) && !(provider.key?("domainVerified") && provider["domainVerified"])
|
|
83
|
+
raise APIError.new("UNAUTHORIZED", message: "Provider domain has not been verified")
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
provider = sso_ensure_runtime_oidc_provider(ctx, provider, config)
|
|
87
|
+
oidc_config = normalize_hash(provider["oidcConfig"] || {})
|
|
88
|
+
oidc_config[:issuer] ||= provider["issuer"]
|
|
89
|
+
return sso_redirect(ctx, sso_append_error(error_url, "invalid_provider", "provider not found")) if oidc_config.empty?
|
|
90
|
+
|
|
91
|
+
tokens = sso_oidc_tokens(ctx, provider, oidc_config, state, config)
|
|
92
|
+
unless tokens
|
|
93
|
+
return sso_redirect(ctx, sso_append_error(error_url, "invalid_provider", "token_response_not_found"))
|
|
94
|
+
end
|
|
95
|
+
if oidc_config[:user_info_endpoint].to_s.empty? && tokens[:id_token] && oidc_config[:jwks_endpoint].to_s.empty?
|
|
96
|
+
begin
|
|
97
|
+
provider = sso_ensure_runtime_oidc_provider(ctx, provider, config, require_jwks: true)
|
|
98
|
+
oidc_config = normalize_hash(provider["oidcConfig"] || {})
|
|
99
|
+
oidc_config[:issuer] ||= provider["issuer"]
|
|
100
|
+
rescue APIError
|
|
101
|
+
# Fall through to the upstream callback error when JWKS is still unavailable.
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
user_info = sso_oidc_user_info(ctx, oidc_config, tokens, config, expected_nonce: state["nonce"] || state[:nonce])
|
|
105
|
+
if user_info[:_sso_error]
|
|
106
|
+
return sso_redirect(ctx, sso_append_error(error_url, "invalid_provider", user_info[:_sso_error]))
|
|
107
|
+
end
|
|
108
|
+
if user_info[:email].to_s.empty? || user_info[:id].to_s.empty?
|
|
109
|
+
return sso_redirect(ctx, sso_append_error(error_url, "invalid_provider", "missing_user_info"))
|
|
110
|
+
end
|
|
111
|
+
if config[:disable_implicit_sign_up] && !state["requestSignUp"] && !ctx.context.internal_adapter.find_user_by_email(user_info[:email].to_s.downcase)
|
|
112
|
+
return sso_redirect(ctx, sso_append_error(error_url, "signup disabled"))
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
result = sso_find_or_create_user_result(ctx, provider, user_info, config)
|
|
116
|
+
if config[:provision_user].respond_to?(:call) && (result.fetch(:created) || config[:provision_user_on_every_login])
|
|
117
|
+
config[:provision_user].call(user: result.fetch(:user), userInfo: user_info, token: tokens, provider: provider)
|
|
118
|
+
end
|
|
119
|
+
session = ctx.context.internal_adapter.create_session(result.fetch(:user).fetch("id"))
|
|
120
|
+
Cookies.set_session_cookie(ctx, {session: session, user: result.fetch(:user)})
|
|
121
|
+
redirect_to = (result.fetch(:created) && state["newUserURL"].to_s != "") ? state["newUserURL"] : callback_url
|
|
122
|
+
sso_redirect(ctx, redirect_to || "/")
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
@@ -44,17 +44,18 @@ module BetterAuth
|
|
|
44
44
|
|
|
45
45
|
def plugin_schema(config = {})
|
|
46
46
|
normalized = BetterAuth::Plugins.normalize_hash(config || {})
|
|
47
|
+
field_mappings = BetterAuth::Plugins.normalize_hash(normalized[:fields] || {})
|
|
47
48
|
fields = {
|
|
48
|
-
issuer: {type: "string", required: true},
|
|
49
|
-
oidcConfig: {type: "string", required: false},
|
|
50
|
-
samlConfig: {type: "string", required: false},
|
|
51
|
-
userId: {type: "string", required: true},
|
|
52
|
-
providerId: {type: "string", required: true, unique: true},
|
|
53
|
-
domain: {type: "string", required: true},
|
|
54
|
-
organizationId: {type: "string", required: false}
|
|
49
|
+
issuer: field("issuer", {type: "string", required: true}, field_mappings),
|
|
50
|
+
oidcConfig: field("oidcConfig", {type: "string", required: false}, field_mappings),
|
|
51
|
+
samlConfig: field("samlConfig", {type: "string", required: false}, field_mappings),
|
|
52
|
+
userId: field("userId", {type: "string", required: true, references: {model: "user", field: "id"}}, field_mappings),
|
|
53
|
+
providerId: field("providerId", {type: "string", required: true, unique: true}, field_mappings),
|
|
54
|
+
domain: field("domain", {type: "string", required: true}, field_mappings),
|
|
55
|
+
organizationId: field("organizationId", {type: "string", required: false}, field_mappings)
|
|
55
56
|
}
|
|
56
57
|
if normalized.dig(:domain_verification, :enabled)
|
|
57
|
-
fields[:domainVerified] = {type: "boolean", required: false, default_value: false}
|
|
58
|
+
fields[:domainVerified] = field("domainVerified", {type: "boolean", required: false, default_value: false}, field_mappings)
|
|
58
59
|
end
|
|
59
60
|
{
|
|
60
61
|
ssoProvider: {
|
|
@@ -71,6 +72,11 @@ module BetterAuth
|
|
|
71
72
|
def saml_config_key?(key)
|
|
72
73
|
SAML_CONFIG_KEYS.include?(BetterAuth::Plugins.normalize_key(key))
|
|
73
74
|
end
|
|
75
|
+
|
|
76
|
+
def field(logical_name, attributes, mappings)
|
|
77
|
+
mapping = mappings[BetterAuth::Plugins.normalize_key(logical_name)]
|
|
78
|
+
mapping ? attributes.merge(field_name: mapping) : attributes
|
|
79
|
+
end
|
|
74
80
|
end
|
|
75
81
|
end
|
|
76
82
|
end
|
|
@@ -20,7 +20,7 @@ module BetterAuth
|
|
|
20
20
|
initiate_slo: BetterAuth::Plugins.sso_initiate_slo_endpoint(normalized),
|
|
21
21
|
list_sso_providers: BetterAuth::Plugins.sso_list_providers_endpoint,
|
|
22
22
|
get_sso_provider: BetterAuth::Plugins.sso_get_provider_endpoint,
|
|
23
|
-
update_sso_provider: BetterAuth::Plugins.sso_update_provider_endpoint,
|
|
23
|
+
update_sso_provider: BetterAuth::Plugins.sso_update_provider_endpoint(normalized),
|
|
24
24
|
delete_sso_provider: BetterAuth::Plugins.sso_delete_provider_endpoint
|
|
25
25
|
}
|
|
26
26
|
if normalized.dig(:domain_verification, :enabled)
|
|
@@ -7,7 +7,7 @@ module BetterAuth
|
|
|
7
7
|
|
|
8
8
|
def generate_relay_state(ctx, link = nil, additional_data = {})
|
|
9
9
|
callback_url = BetterAuth::Plugins.sso_fetch(ctx.body, :callback_url)
|
|
10
|
-
raise APIError.new("BAD_REQUEST", message: "callbackURL is required") if callback_url.to_s.empty?
|
|
10
|
+
raise BetterAuth::APIError.new("BAD_REQUEST", message: "callbackURL is required") if callback_url.to_s.empty?
|
|
11
11
|
|
|
12
12
|
extra = (additional_data == false) ? {} : (additional_data || {})
|
|
13
13
|
BetterAuth::Plugins.sso_generate_saml_relay_state(
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: better_auth-sso
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sebastian Sala
|
|
@@ -43,6 +43,20 @@ dependencies:
|
|
|
43
43
|
- - "<"
|
|
44
44
|
- !ruby/object:Gem::Version
|
|
45
45
|
version: '1.0'
|
|
46
|
+
- !ruby/object:Gem::Dependency
|
|
47
|
+
name: jwt
|
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
|
49
|
+
requirements:
|
|
50
|
+
- - "~>"
|
|
51
|
+
- !ruby/object:Gem::Version
|
|
52
|
+
version: '2.8'
|
|
53
|
+
type: :runtime
|
|
54
|
+
prerelease: false
|
|
55
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
56
|
+
requirements:
|
|
57
|
+
- - "~>"
|
|
58
|
+
- !ruby/object:Gem::Version
|
|
59
|
+
version: '2.8'
|
|
46
60
|
- !ruby/object:Gem::Dependency
|
|
47
61
|
name: logger
|
|
48
62
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -140,7 +154,8 @@ dependencies:
|
|
|
140
154
|
- !ruby/object:Gem::Version
|
|
141
155
|
version: '1.0'
|
|
142
156
|
description: Adds SSO provider management, OIDC SSO, and SAML SSO integration for
|
|
143
|
-
Better Auth Ruby.
|
|
157
|
+
Better Auth Ruby. Better Auth Ruby is an independent modern authentication framework
|
|
158
|
+
for Ruby inspired by Better Auth.
|
|
144
159
|
email:
|
|
145
160
|
- sebastian.sala.tech@gmail.com
|
|
146
161
|
executables: []
|
|
@@ -161,6 +176,16 @@ files:
|
|
|
161
176
|
- lib/better_auth/sso/oidc/discovery.rb
|
|
162
177
|
- lib/better_auth/sso/oidc/errors.rb
|
|
163
178
|
- lib/better_auth/sso/oidc/types.rb
|
|
179
|
+
- lib/better_auth/sso/plugin/core.rb
|
|
180
|
+
- lib/better_auth/sso/plugin/endpoints.rb
|
|
181
|
+
- lib/better_auth/sso/plugin/oidc_discovery.rb
|
|
182
|
+
- lib/better_auth/sso/plugin/oidc_runtime.rb
|
|
183
|
+
- lib/better_auth/sso/plugin/provider_utils.rb
|
|
184
|
+
- lib/better_auth/sso/plugin/providers.rb
|
|
185
|
+
- lib/better_auth/sso/plugin/saml_metadata_and_logout.rb
|
|
186
|
+
- lib/better_auth/sso/plugin/saml_response.rb
|
|
187
|
+
- lib/better_auth/sso/plugin/saml_validation_and_state.rb
|
|
188
|
+
- lib/better_auth/sso/plugin/sign_in_and_oidc_callbacks.rb
|
|
164
189
|
- lib/better_auth/sso/routes/domain_verification.rb
|
|
165
190
|
- lib/better_auth/sso/routes/helpers.rb
|
|
166
191
|
- lib/better_auth/sso/routes/providers.rb
|