fix: make the SAML IdP work and verify request signatures - #82
Merged
Merged
Conversation
|
Warning Review limit reachedNext included review available in 33 minutes. View limit detailsLimit details: You’ve used the included review currently available. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Review configuration: ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Team Run ID: 📒 Files selected for processing (10)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
The IdP could never issue an assertion. The controller replaced saml_idp's
validate_saml_request with a presence check, and saml_idp's default
saml_request is a stub whose issuer is nil, so the request was never decoded
and every authentication stopped at "Unknown or disabled service provider".
Behind that were four more faults, none of them reachable until the first was
fixed:
- encode_saml_response passed keyword arguments to SamlResponse.new, which
takes nine required positional ones. It also called add_attribute, which
does not exist, and handed session_expiry a Time where an Integer belongs.
Use the gem's encode_authn_response instead of rebuilding it.
- attributes_for returned { name => value }, but the assertion builder wants
{ name => { getter: } }. Add saml_attributes_for to convert.
- The name ID format was passed as a URN string. saml_idp builds the URN from
a { version => { key => getter } } shape, so the format was ignored and
emailAddress came out as 2.0 rather than 1.1.
- The view base64 encoded an already encoded response, and read RelayState off
params, which is empty after the login redirect.
- store_saml_request wrote session[:saml_return_to]. AuthController reads
return_to, so signing in abandoned the SAML flow.
AuthnRequest signatures are now verified. want_authn_requests_signed is a new
per provider flag, defaulting to false so existing integrations keep working,
and the service provider finder supplies the certificate fingerprint that
saml_idp needs before it will check a signature.
Covered by 25 integration tests, from nothing. Against the previous code they
produce 11 failures and 7 errors.
Claude-Session: https://claude.ai/code/session_018gopCvu5KgRjeWBM4Zw6pm
jaspermayone
force-pushed
the
jaspermayone/fix-saml-idp
branch
from
September 7, 2026 18:02
6bbdea4 to
50bde9e
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The SAML IdP could never issue an assertion. Fixing the first fault exposed five more behind it, none of which were reachable before. There was no SAML test coverage at all, so all of it is now covered.
The root fault
The controller replaced saml_idp's
validate_saml_requestwith a presence check:saml_idp's default
saml_requestis a stub Struct, so it is always present. The check always passed,decode_requestwas never called, and the stub'sissuerisnil. Every authentication therefore ended atfind_service_providerwith 403 Unknown or disabled service provider.flowchart TD A[AuthnRequest arrives] --> B[validate_saml_request] B --> C{"saml_request.present?"} C -->|"always true, it is a stub"| D[return early] D --> E[decode_request never runs] E --> F["saml_request.issuer is nil"] F --> G[403 Unknown service provider]What was behind it
SamlResponse.newcalled with keyword argumentsArgumentErroron Ruby 3.4response.add_attribute(...)NoMethodErrorsession_expiry: 1.hour.from_nowTimewhere the gem does.zero?andnow + expiryattributes_forreturns{ name => value }{ name => { getter: } }name_id_format:passed as a URN string{ version => { key => getter } }, so the setting was ignored and emailAddress came out as 2.0 instead of 1.1Base64.strict_encode64params[:RelayState]store_saml_requestwrotesession[:saml_return_to]AuthControllerreadsreturn_to. Signing in abandoned the SAML flow and landed on the dashboardThe response building is no longer hand rolled. It now goes through the gem's own
encode_authn_response, which handles the positional wiring.Request signatures
AuthnRequestsignatures were never verified. saml_idp 1.0 can check them, so this wires that up:want_authn_requests_signedcolumn, default false so every existing integration keeps working. Turn it on per provider.sign_authn_request,validate_signature, and the certificate fingerprint. saml_idp refuses to check a signature unless both the certificate and its fingerprint are present, socertificate_fingerprintcomputes the latter.acs_urlstays the stored URL rather than the one the request carries, so a caller cannot redirect an assertion elsewhere.Exposed in the admin form as "Require signed AuthnRequests".
Also fixed
config.x.app_hostwas set in development and production but not test, so the SAML issuer and entity ID came out as a bare/samlunder test.Verification
25 integration tests, covering the feature flag, metadata, request decoding, provider resolution, assertion contents (audience, destination, NameID, NameID format, attributes,
InResponseTo, signature), the sign in round trip, authentication logging, and signature acceptance and rejection.They are real regression tests, not confirmation of current behaviour. Against the previous controller and view they produce 11 failures and 7 errors of 24. I reverted the fix and ran them to confirm this rather than trusting a green first run.
bin/rails test: 392 runs, 1044 assertions, 0 failures, 0 errors, 0 skipsbin/rubocop,bin/brakeman(0 warnings),bin/bundler-audit(no vulnerabilities): cleanNote
Single Logout is repaired to the extent that it now decodes and validates the request, uses
request_idrather than the non-existent.id, and sends the response to the provider's own logout endpoint. It has no test coverage here, because exercising it needs a signed LogoutRequest fixture and the endpoint appears unused. Worth a follow up before anyone relies on it.https://claude.ai/code/session_018gopCvu5KgRjeWBM4Zw6pm