better_auth-sso 0.6.2 → 0.7.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: 2e57052c749ed1cc476cde2b203dc6f1965345a5f032666b801973d9215fe7e7
4
- data.tar.gz: b8b1214d8de33f3b1a24a80d97d36048cb80fbeb67a26c7d3ff87eda0721bc89
3
+ metadata.gz: d4f762319ea61412d5e0eb1ac83d359c9b533f761ba2f655d166544ad2d21b83
4
+ data.tar.gz: aa5768114552b9cdb59dd3ae6ac5b394172d0ae859193f4d6ac3495e8ef00f6a
5
5
  SHA512:
6
- metadata.gz: 954ad27f4c4f388f1a5b937e584cd7cacdcb5a0cda037896c1b885e2bf0c5b12cf4875fa5bb69d118d46c4f596ad21f728a2949026a54e9971dba129f5560153
7
- data.tar.gz: a03a6cba0f4a05fa925152a8574a0aee37c4adf66137796d9cf71c87312f16aaab46a50330a0ee8ddc9d574c8f61d1a4acab6091eb36c4c74da63eb0374550ce
6
+ metadata.gz: 147d540a80e7584227916a79c3fa2e34508de75c39de90332c2a1fd4c37913d00b31285830a18b95e27bc0d8679953565506e4f6f506692c5fb83f24273e4dff
7
+ data.tar.gz: 31807bffa4c57fa065295b2bc2ff27f5f346990597c314e63b86d531d44cf13a4466b6717efae71626ea51a44a8708e4f71fef2d6ee97ea306f77ecdbdd7d7b0
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ ## 0.7.0 - 2026-05-05
6
+
7
+ - Fixed SAML config validation for `singleSignOnService` and added validation for `singleLogoutService`.
8
+ - Hardened OIDC callbacks by binding signed state `providerId` to the callback route and verifying `nonce` on JWKS-backed ID tokens.
9
+ - Changed SSO domain verification to require exact TXT record matches and corrected the insufficient access error code to `INSUFFICIENT_ACCESS`.
10
+ - Declared `jwt` as a direct runtime dependency for the SSO gem.
11
+ - Added regression coverage for SAML SP metadata XML responses.
12
+
3
13
  ## 0.2.0 - 2026-04-29
4
14
 
5
15
  - Improved SSO upstream parity for OIDC and SAML provider flows, organization handling, callback behavior, metadata parsing, account linking, and response/error shapes.
data/README.md CHANGED
@@ -2,7 +2,10 @@
2
2
 
3
3
  External SSO plugin package for `better_auth`.
4
4
 
5
- SSO is the app-facing feature. It supports OIDC SSO and SAML SSO. SAML is not the same thing as SSO; SAML is one protocol used by SSO.
5
+ SSO is the app-facing feature. It supports OIDC SSO, SAML SSO, provider management,
6
+ domain verification, SAML replay protection, runtime OIDC discovery, organization
7
+ assignment, and SAML Single Logout. SAML is not the same thing as SSO; SAML is
8
+ one protocol used by SSO.
6
9
 
7
10
  ```ruby
8
11
  require "better_auth"
@@ -15,7 +18,11 @@ BetterAuth.auth(
15
18
  )
16
19
  ```
17
20
 
18
- SAML XML validation is included in this package and backed by `ruby-saml`:
21
+ SAML XML validation is included in this package and backed by `ruby-saml`.
22
+ Production XML SAML deployments should configure `BetterAuth::SSO::SAML.sso_options`
23
+ or compatible SAML hooks so AuthnRequest generation and SAMLResponse parsing use
24
+ the real XML/SAML boundary instead of the lightweight JSON/base64 fallback used by
25
+ local tests:
19
26
 
20
27
  ```ruby
21
28
  require "better_auth/sso"
@@ -43,3 +50,30 @@ SAML SLO follows upstream route shapes when `saml.enableSingleLogout` is enabled
43
50
  Ruby keeps the lightweight JSON/base64 fallback used by the local SAML test adapter, and real XML deployments should configure `BetterAuth::SSO::SAML.sso_options` or compatible SAML hooks.
44
51
 
45
52
  SCIM is a separate provisioning feature and lives in `better_auth-scim`.
53
+
54
+ ## Organization Assignment
55
+
56
+ When the organization plugin is installed, SSO can add users to an organization
57
+ linked to an SSO provider. SSO login flows assign from the matched provider.
58
+ Generic OAuth callbacks under `/callback/:provider` also assign by verified SSO
59
+ email domain when domain verification is enabled, matching upstream behavior for
60
+ users who sign in through non-SSO OAuth but share an enterprise domain.
61
+
62
+ ## Schema Compatibility
63
+
64
+ The Ruby package intentionally keeps the historical default SSO provider model
65
+ name `ssoProviders` for backward compatibility. Upstream Better Auth defaults to
66
+ `ssoProvider`; configure `model_name:` if your application needs a different
67
+ storage model name.
68
+
69
+ Field mapping options are supported through `fields:` for the SSO provider
70
+ schema, including `issuer`, `oidcConfig`, `samlConfig`, `userId`, `providerId`,
71
+ `organizationId`, `domain`, and `domainVerified`.
72
+
73
+ ## Scope and Non-Goals
74
+
75
+ This package does not currently imply support for advanced enterprise features
76
+ such as `private_key_jwt`, mTLS client authentication, every SAML XML edge case,
77
+ or large internal SSO refactors. Those items are tracked in the
78
+ [upstream and product alignment backlog](../../.docs/backlog/upstream-product-alignment.md)
79
+ until they have explicit product scope and upstream parity decisions.
@@ -91,11 +91,60 @@ module BetterAuth
91
91
  init: ->(_ctx) { {options: {advanced: {disable_origin_check: ["/sso/saml2/callback", "/sso/saml2/sp/acs", "/sso/saml2/sp/slo"]}}} },
92
92
  schema: BetterAuth::SSO::Routes::Schemas.plugin_schema(config),
93
93
  endpoints: endpoints,
94
+ hooks: sso_hooks(config),
94
95
  error_codes: SSO_ERROR_CODES,
95
96
  options: config
96
97
  )
97
98
  end
98
99
 
100
+ def sso_hooks(config = {})
101
+ {
102
+ before: [
103
+ {
104
+ matcher: ->(ctx) { ctx.path == "/sign-out" },
105
+ handler: ->(ctx) { sso_before_sign_out(ctx, config) }
106
+ }
107
+ ],
108
+ after: [
109
+ {
110
+ matcher: ->(ctx) { ctx.path.to_s.match?(%r{\A/callback/[^/]+\z}) },
111
+ handler: ->(ctx) { sso_after_generic_callback(ctx, config) }
112
+ }
113
+ ]
114
+ }
115
+ end
116
+
117
+ def sso_before_sign_out(ctx, config = {})
118
+ return unless config.dig(:saml, :enable_single_logout)
119
+
120
+ token_cookie = ctx.context.auth_cookies[:session_token]
121
+ session_token = ctx.get_signed_cookie(token_cookie.name, ctx.context.secret)
122
+ return if session_token.to_s.empty?
123
+
124
+ lookup_key = "#{SSO_SAML_SESSION_BY_ID_KEY_PREFIX}#{session_token}"
125
+ session_lookup = ctx.context.internal_adapter.find_verification_value(lookup_key)
126
+ saml_session_key = session_lookup&.fetch("value", nil)
127
+ ctx.context.internal_adapter.delete_verification_by_identifier(saml_session_key) if saml_session_key
128
+ ctx.context.internal_adapter.delete_verification_by_identifier(lookup_key)
129
+ nil
130
+ rescue
131
+ nil
132
+ end
133
+
134
+ def sso_after_generic_callback(ctx, config = {})
135
+ new_session = ctx.context.new_session if ctx.context.respond_to?(:new_session)
136
+ return unless new_session && new_session[:user]
137
+ return unless defined?(BetterAuth::SSO::Linking::OrgAssignment)
138
+
139
+ BetterAuth::SSO::Linking::OrgAssignment.assign_organization_by_domain(
140
+ ctx,
141
+ user: new_session.fetch(:user),
142
+ provisioning_options: config[:organization_provisioning],
143
+ domain_verification: config[:domain_verification]
144
+ )
145
+ nil
146
+ end
147
+
99
148
  def sso_schema(config = {})
100
149
  BetterAuth::SSO::Routes::Schemas.plugin_schema(config)
101
150
  end
@@ -245,7 +294,7 @@ module BetterAuth
245
294
  end
246
295
  end
247
296
 
248
- def sso_update_provider_endpoint
297
+ def sso_update_provider_endpoint(config = {})
249
298
  Endpoint.new(path: "/sso/update-provider", method: "POST") do |ctx|
250
299
  session = Routes.current_session(ctx)
251
300
  body = normalize_hash(ctx.body)
@@ -264,13 +313,17 @@ module BetterAuth
264
313
  current = sso_provider_config_hash(provider["oidcConfig"])
265
314
  raise APIError.new("BAD_REQUEST", message: "Cannot update OIDC config for a provider that doesn't have OIDC configured") if current.empty?
266
315
 
267
- update[:oidcConfig] = current.merge(normalize_hash(body[:oidc_config]))
316
+ resolved_issuer = update[:issuer] || current[:issuer] || provider["issuer"]
317
+ update[:oidcConfig] = current.merge(normalize_hash(body[:oidc_config])).merge(issuer: resolved_issuer).compact
268
318
  end
269
319
  if body.key?(:saml_config)
270
320
  current = sso_provider_config_hash(provider["samlConfig"])
271
321
  raise APIError.new("BAD_REQUEST", message: "Cannot update SAML config for a provider that doesn't have SAML configured") if current.empty?
272
322
 
273
- update[:samlConfig] = current.merge(normalize_hash(body[:saml_config]))
323
+ resolved_issuer = update[:issuer] || current[:issuer] || provider["issuer"]
324
+ merged_saml_config = current.merge(normalize_hash(body[:saml_config])).merge(issuer: resolved_issuer).compact
325
+ sso_validate_saml_config!(merged_saml_config, config)
326
+ update[:samlConfig] = merged_saml_config
274
327
  end
275
328
  updated = ctx.context.adapter.update(model: "ssoProvider", where: [{field: "id", value: provider.fetch("id")}], update: update)
276
329
  ctx.json(sso_sanitize_provider(updated, ctx.context))
@@ -313,7 +366,11 @@ module BetterAuth
313
366
 
314
367
  if provider["oidcConfig"] && provider_type != "saml"
315
368
  provider = sso_ensure_runtime_oidc_provider(ctx, provider, config)
316
- state = BetterAuth::Crypto.sign_jwt(state_data.merge(sso_oidc_pkce_state(provider)), ctx.context.secret, expires_in: 600)
369
+ state = BetterAuth::Crypto.sign_jwt(
370
+ state_data.merge({nonce: BetterAuth::Crypto.random_string(32)}).merge(sso_oidc_pkce_state(provider)),
371
+ ctx.context.secret,
372
+ expires_in: 600
373
+ )
317
374
  url = sso_oidc_authorization_url(provider, ctx, state, config, body)
318
375
  elsif provider["samlConfig"]
319
376
  relay_state = sso_generate_saml_relay_state(ctx, state_data)
@@ -352,6 +409,10 @@ module BetterAuth
352
409
  description = ctx.query[:error_description] || ctx.query["error_description"]
353
410
  return sso_redirect(ctx, sso_append_error(error_url, error, description))
354
411
  end
412
+ state_provider_id = state["providerId"] || state[:providerId]
413
+ if state_provider_id.to_s != provider_id.to_s
414
+ return sso_redirect(ctx, sso_append_error(error_url, "invalid_state", "provider mismatch"))
415
+ end
355
416
 
356
417
  provider = sso_callback_provider(ctx, config, provider_id)
357
418
  return sso_redirect(ctx, sso_append_error(error_url, "invalid_provider", "provider not found")) unless provider
@@ -377,7 +438,7 @@ module BetterAuth
377
438
  # Fall through to the upstream callback error when JWKS is still unavailable.
378
439
  end
379
440
  end
380
- user_info = sso_oidc_user_info(ctx, oidc_config, tokens, config)
441
+ user_info = sso_oidc_user_info(ctx, oidc_config, tokens, config, expected_nonce: state["nonce"] || state[:nonce])
381
442
  if user_info[:_sso_error]
382
443
  return sso_redirect(ctx, sso_append_error(error_url, "invalid_provider", user_info[:_sso_error]))
383
444
  end
@@ -430,13 +491,18 @@ module BetterAuth
430
491
  provider = sso_find_provider!(ctx, sso_fetch(ctx.params, :provider_id))
431
492
  relay_state = sso_fetch(ctx.body, :relay_state) || sso_fetch(ctx.query, :relay_state)
432
493
  if sso_fetch(ctx.body, :saml_response) || sso_fetch(ctx.query, :saml_response)
433
- sso_process_saml_logout_response(ctx, sso_fetch(ctx.body, :saml_response) || sso_fetch(ctx.query, :saml_response))
494
+ raw_response = sso_fetch(ctx.body, :saml_response) || sso_fetch(ctx.query, :saml_response)
495
+ sso_validate_saml_slo_signature!(ctx, raw_response, "Invalid LogoutResponse") if config.dig(:saml, :want_logout_response_signed)
496
+ sso_process_saml_logout_response(ctx, raw_response)
434
497
  Cookies.delete_session_cookie(ctx)
435
498
  next sso_redirect(ctx, sso_safe_slo_redirect_url(ctx, relay_state, provider.fetch("providerId")))
436
499
  end
437
500
 
438
- sso_process_saml_logout_request(ctx, provider, sso_fetch(ctx.body, :saml_request) || sso_fetch(ctx.query, :saml_request))
439
- response = Base64.strict_encode64("<samlp:LogoutResponse xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" ID=\"_#{BetterAuth::Crypto.random_string(32)}\" Version=\"2.0\" IssueInstant=\"#{Time.now.utc.iso8601}\" Destination=\"#{sso_saml_logout_destination(provider)}\"><samlp:Status><samlp:StatusCode Value=\"urn:oasis:names:tc:SAML:2.0:status:Success\"/></samlp:Status></samlp:LogoutResponse>")
501
+ raw_request = sso_fetch(ctx.body, :saml_request) || sso_fetch(ctx.query, :saml_request)
502
+ sso_validate_saml_slo_signature!(ctx, raw_request, "Invalid LogoutRequest") if config.dig(:saml, :want_logout_request_signed)
503
+ logout_request_data = sso_process_saml_logout_request(ctx, provider, raw_request)
504
+ in_response_to = logout_request_data[:id].to_s.empty? ? "" : " InResponseTo=\"#{CGI.escapeHTML(logout_request_data[:id].to_s)}\""
505
+ response = Base64.strict_encode64("<samlp:LogoutResponse xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" ID=\"_#{BetterAuth::Crypto.random_string(32)}\"#{in_response_to} Version=\"2.0\" IssueInstant=\"#{Time.now.utc.iso8601}\" Destination=\"#{sso_saml_logout_destination(provider)}\"><samlp:Status><samlp:StatusCode Value=\"urn:oasis:names:tc:SAML:2.0:status:Success\"/></samlp:Status></samlp:LogoutResponse>")
440
506
  if sso_fetch(ctx.body, :saml_request)
441
507
  next sso_saml_post_form(sso_saml_logout_destination(provider), "SAMLResponse", response, relay_state)
442
508
  end
@@ -526,7 +592,7 @@ module BetterAuth
526
592
 
527
593
  records = sso_resolve_txt_records("#{identifier}.#{hostname}", config)
528
594
  expected = "#{identifier}=#{active.fetch("value")}"
529
- unless records.flatten.any? { |record| record.to_s.include?(expected) }
595
+ unless sso_txt_record_exact_match?(records, expected)
530
596
  raise APIError.new("BAD_GATEWAY", message: "Unable to verify domain ownership. Try again later", code: "DOMAIN_VERIFICATION_FAILED")
531
597
  end
532
598
 
@@ -677,9 +743,27 @@ module BetterAuth
677
743
  end
678
744
  sso_validate_url!(saml_config[:entry_point], "SAML entryPoint must be a valid URL") unless saml_config[:entry_point].to_s.empty?
679
745
  unless saml_config[:single_sign_on_service].to_s.empty?
680
- sso_validate_url!(saml_config[:single_sign_on_service], "SAML singleLogoutService must be a valid URL")
746
+ sso_validate_url!(saml_config[:single_sign_on_service], "SAML singleSignOnService must be a valid URL")
747
+ end
748
+ unless saml_config[:single_logout_service].to_s.empty?
749
+ sso_validate_url!(saml_config[:single_logout_service], "SAML singleLogoutService must be a valid URL")
681
750
  end
682
751
 
752
+ config_algorithm_xml = +""
753
+ unless saml_config[:signature_algorithm].to_s.empty?
754
+ config_algorithm_xml << "<ds:SignatureMethod Algorithm=\"#{saml_config[:signature_algorithm]}\"/>"
755
+ end
756
+ unless saml_config[:digest_algorithm].to_s.empty?
757
+ config_algorithm_xml << "<ds:DigestMethod Algorithm=\"#{saml_config[:digest_algorithm]}\"/>"
758
+ end
759
+ sso_validate_saml_algorithms!(
760
+ config_algorithm_xml,
761
+ on_deprecated: plugin_config.dig(:saml, :algorithms, :on_deprecated) || saml_config[:on_deprecated_algorithm] || "warn",
762
+ allowed_signature_algorithms: plugin_config.dig(:saml, :algorithms, :allowed_signature_algorithms) || saml_config[:allowed_signature_algorithms],
763
+ allowed_digest_algorithms: plugin_config.dig(:saml, :algorithms, :allowed_digest_algorithms) || saml_config[:allowed_digest_algorithms],
764
+ allowed_key_encryption_algorithms: plugin_config.dig(:saml, :algorithms, :allowed_key_encryption_algorithms) || saml_config[:allowed_key_encryption_algorithms],
765
+ allowed_data_encryption_algorithms: plugin_config.dig(:saml, :algorithms, :allowed_data_encryption_algorithms) || saml_config[:allowed_data_encryption_algorithms]
766
+ )
683
767
  sso_validate_saml_algorithms!(
684
768
  metadata.to_s,
685
769
  on_deprecated: plugin_config.dig(:saml, :algorithms, :on_deprecated) || saml_config[:on_deprecated_algorithm] || "warn",
@@ -842,11 +926,11 @@ module BetterAuth
842
926
 
843
927
  def sso_process_saml_logout_request(ctx, provider, raw_request)
844
928
  data = sso_parse_saml_logout_request(raw_request)
845
- return if data[:name_id].to_s.empty?
929
+ return data if data[:name_id].to_s.empty?
846
930
 
847
931
  session_identifier = "#{SSO_SAML_SESSION_KEY_PREFIX}#{provider.fetch("providerId")}:#{data[:name_id]}"
848
932
  verification = ctx.context.internal_adapter.find_verification_value(session_identifier)
849
- return unless verification
933
+ return data unless verification
850
934
 
851
935
  record = JSON.parse(verification.fetch("value"))
852
936
  session_token = record["sessionToken"]
@@ -854,8 +938,9 @@ module BetterAuth
854
938
  ctx.context.internal_adapter.delete_session(session_token) if session_token && session_index_matches
855
939
  ctx.context.internal_adapter.delete_verification_by_identifier(session_identifier)
856
940
  ctx.context.internal_adapter.delete_verification_by_identifier("#{SSO_SAML_SESSION_BY_ID_KEY_PREFIX}#{session_token}") if session_token
941
+ data
857
942
  rescue
858
- nil
943
+ {}
859
944
  end
860
945
 
861
946
  def sso_store_saml_logout_request(ctx, provider, request_id, config)
@@ -883,6 +968,7 @@ module BetterAuth
883
968
  def sso_parse_saml_logout_request(raw_request)
884
969
  xml = Base64.decode64(raw_request.to_s.gsub(/\s+/, ""))
885
970
  {
971
+ id: xml[/\bID=['"]([^'"]+)['"]/, 1],
886
972
  name_id: xml[%r{<(?:\w+:)?NameID[^>]*>([^<]+)</(?:\w+:)?NameID>}, 1],
887
973
  session_index: xml[%r{<(?:\w+:)?SessionIndex[^>]*>([^<]+)</(?:\w+:)?SessionIndex>}, 1]
888
974
  }
@@ -890,6 +976,19 @@ module BetterAuth
890
976
  {}
891
977
  end
892
978
 
979
+ def sso_validate_saml_slo_signature!(ctx, raw_message, error_message)
980
+ return true if !sso_fetch(ctx.body, :signature).to_s.empty? || !sso_fetch(ctx.query, :signature).to_s.empty?
981
+
982
+ xml = Base64.decode64(raw_message.to_s.gsub(/\s+/, ""))
983
+ return true if xml.include?("<Signature") || xml.include?(":Signature")
984
+
985
+ raise APIError.new("BAD_REQUEST", message: error_message)
986
+ rescue APIError
987
+ raise
988
+ rescue
989
+ raise APIError.new("BAD_REQUEST", message: error_message)
990
+ end
991
+
893
992
  def sso_parse_saml_logout_response(raw_response)
894
993
  xml = Base64.decode64(raw_response.to_s.gsub(/\s+/, ""))
895
994
  {
@@ -1187,6 +1286,8 @@ module BetterAuth
1187
1286
  scope: scopes.join(" "),
1188
1287
  state: state
1189
1288
  }.compact
1289
+ nonce = sso_decode_state(state, ctx.context.secret)&.fetch("nonce", nil)
1290
+ query[:nonce] = nonce if nonce && !nonce.to_s.empty?
1190
1291
  login_hint = body[:login_hint] || body[:email]
1191
1292
  query[:login_hint] = login_hint if login_hint
1192
1293
  code_verifier = sso_decode_state(state, ctx.context.secret)&.fetch("codeVerifier", nil)
@@ -1382,7 +1483,7 @@ module BetterAuth
1382
1483
  normalize_hash(JSON.parse(response.body))
1383
1484
  end
1384
1485
 
1385
- def sso_oidc_user_info(ctx, oidc_config, tokens, plugin_config)
1486
+ def sso_oidc_user_info(ctx, oidc_config, tokens, plugin_config, expected_nonce: nil)
1386
1487
  user_callback = oidc_config[:get_user_info]
1387
1488
  raw = if user_callback.respond_to?(:call)
1388
1489
  user_callback.call(tokens)
@@ -1396,7 +1497,8 @@ module BetterAuth
1396
1497
  jwks_endpoint: oidc_config[:jwks_endpoint],
1397
1498
  audience: oidc_config[:client_id],
1398
1499
  issuer: oidc_config[:issuer],
1399
- fetch: plugin_config[:oidc_jwks_fetch]
1500
+ fetch: plugin_config[:oidc_jwks_fetch],
1501
+ expected_nonce: expected_nonce
1400
1502
  ) || {_sso_error: "token_not_verified"}
1401
1503
  else
1402
1504
  {}
@@ -1429,7 +1531,7 @@ module BetterAuth
1429
1531
  {}
1430
1532
  end
1431
1533
 
1432
- def sso_validate_oidc_id_token(token, jwks_endpoint:, audience:, issuer:, fetch: nil)
1534
+ def sso_validate_oidc_id_token(token, jwks_endpoint:, audience:, issuer:, fetch: nil, expected_nonce: nil)
1433
1535
  jwks = sso_fetch_oidc_jwks(jwks_endpoint, fetch: fetch)
1434
1536
  payload, = ::JWT.decode(
1435
1537
  token.to_s,
@@ -1442,6 +1544,11 @@ module BetterAuth
1442
1544
  iss: issuer,
1443
1545
  verify_iss: true
1444
1546
  )
1547
+ if expected_nonce && !expected_nonce.to_s.empty?
1548
+ token_nonce = payload["nonce"] || payload[:nonce]
1549
+ return nil if token_nonce.to_s.empty?
1550
+ return nil unless BetterAuth::Crypto.constant_time_compare(token_nonce.to_s, expected_nonce.to_s)
1551
+ end
1445
1552
  payload
1446
1553
  rescue
1447
1554
  nil
@@ -1628,7 +1735,11 @@ module BetterAuth
1628
1735
  end
1629
1736
  return if provider["userId"] == user_id && is_org_member
1630
1737
 
1631
- raise APIError.new("FORBIDDEN", message: "User must be owner of or belong to the SSO provider organization", code: "INSUFICCIENT_ACCESS")
1738
+ raise APIError.new("FORBIDDEN", message: "User must be owner of or belong to the SSO provider organization", code: "INSUFFICIENT_ACCESS")
1739
+ end
1740
+
1741
+ def sso_txt_record_exact_match?(records, expected)
1742
+ Array(records).flatten.any? { |record| record.to_s.strip == expected.to_s }
1632
1743
  end
1633
1744
 
1634
1745
  def sso_domain_verification_identifier(config, provider_id)
@@ -66,9 +66,6 @@ module BetterAuth
66
66
 
67
67
  def organization_plugin?(ctx)
68
68
  context = ctx.context
69
- return context.hasPlugin("organization") if context.respond_to?(:hasPlugin)
70
- return context.has_plugin?("organization") if context.respond_to?(:has_plugin?)
71
-
72
69
  plugins = context.options.respond_to?(:plugins) ? context.options.plugins : []
73
70
  plugins.any? { |plugin| plugin.respond_to?(:id) && plugin.id == "organization" }
74
71
  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(
@@ -2,7 +2,7 @@
2
2
 
3
3
  module BetterAuth
4
4
  module SSO
5
- VERSION = "0.6.2"
5
+ VERSION = "0.7.0"
6
6
  PACKAGE_VERSION = VERSION
7
7
  end
8
8
  end
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.6.2
4
+ version: 0.7.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