Skip to content

Upgrade SAML IdP-initiated example to Spring Boot 4.1 with a local Keycloak - #42

Closed
Wictorgirardi wants to merge 8 commits into
p2-inc:mainfrom
Wictorgirardi:feat/saml2-idp-initiated
Closed

Wictorgirardi wants to merge 8 commits into
p2-inc:mainfrom
Wictorgirardi:feat/saml2-idp-initiated

Conversation

@Wictorgirardi

Copy link
Copy Markdown
Contributor

Depends on #28 for the shared Gradle workflow. The branch includes that commit, and each later commit is one step.

Summary

  • Toolchain:
    • Spring Boot 3.4.3 → 4.1.1 (Spring Security 7.1), Gradle 8.12.1 → 9.7.1, and Java 17 → a Java 21 toolchain resolved by the foojay plugin.
    • The dependencies use Boot 4's modular starters, with spring-boot-starter-security-saml2 in place of the bare spring-security-saml2-service-provider.
    • The OpenSAML 5.1.3 constraint is removed, so the managed OpenSAML 5.2.3 is no longer downgraded. The Shibboleth repository moves to its current URL.
  • No committed keys:
    • The SP private key and certificate in src/main/resources/credentials/ are deleted, along with keycloak/saml-client.json, which embedded a client private key.
    • scripts/generate-sp-credentials.sh creates the key pair in a gitignored credentials/ folder. Both old keys stay in the git history, so the README says never to trust them.
  • Local Keycloak:
    • docker-compose.yml runs quay.io/phasetwo/phasetwo-keycloak:26.6 on port 8080, without the /auth path, like the tutorial.
    • It imports keycloak/test-realm-export.json, which replaces a 2,200-line export that referenced a real Okta tenant. The new realm has the SAML client okta-client, the user test / test, and a disabled okta-broker identity provider with placeholder values.
    • The file keeps the -export suffix because Keycloak imports a file named <name>-realm.json as the realm <name> and refused test-realm.json.
  • Service provider:
    • The registration is renamed okta-app → keycloak, and the invalid idp-entity-id property is removed.
    • The single logout URL is set, and a SecurityConfiguration adds saml2Logout and saml2Metadata, so the SP publishes its metadata at /saml2/metadata.
    • The page shows the NameID, the session index and every attribute. It reads them through Spring Security 7's Saml2AssertionAuthentication, which replaces the deprecated Saml2AuthenticatedPrincipal.
  • Tests: five offline tests cover the metadata endpoints, the redirect to Keycloak, the signed authentication request and the page after a SAML login. They read the IdP metadata from the classpath. The old contextLoads test needed a running Keycloak, so it couldn't pass in CI.
  • CI: saml2-idp-initiated.yml runs the shared Gradle workflow.
  • README: rewritten.
    • It explains the flow, with a sequence diagram, and the endpoints.
    • It adds a quick test without Okta through Keycloak's IdP-initiated SSO URL, then the Okta flow.
    • It covers signing keys, using your own Keycloak, and the security notes, including the replay window of unsolicited responses.

Test plan

  • ./gradlew build passes without credentials or Keycloak (5 tests), with the Java 21 toolchain.
  • Against the local Keycloak:
    • docker compose up -d --wait imports the realm, and the admin API shows the expected client, mappers, identity provider and user.
  • The IdP-initiated flow logs in.
    • The response is unsolicited (no InResponseTo) and signed, with Destination = ACS and Audience = SP entity ID.
    • The page shows NameID test, email, firstName and lastName.
  • SP-initiated login works (the response carries InResponseTo).
  • SAML single logout works.
    • The signed LogoutRequest gets a signed success LogoutResponse.
    • Opening the SP afterwards shows Keycloak's login form, so the Keycloak session really ended.
  • All flows also pass with "Client signature required" on and the SP's certificate in Keycloak.
  • A tampered response is rejected with invalid_signature.
  • No private keys or Okta tenant values are tracked.

Docs drift

blog/2025-02-25-saml-idp-initiated-flow.mdx:

  • L96–103: the post recommends a Phase Two Starter cluster, but all its URLs are for a local Keycloak without /auth. Point it to the example's docker compose up -d --wait instead. Hosted Phase Two URLs include /auth.
  • L145–151: creating an identity provider with the alias okta-broker now clashes with the disabled placeholder in the realm. Readers should edit that one instead (entity ID, SSO URL, certificate, then enable it) or delete it first.
  • L153–155: before starting the SP, readers run ./scripts/generate-sp-credentials.sh and start Keycloak. The SP serves its metadata at /saml2/metadata.
  • L157–169: "Import Client" pointed to saml-client.json, which is gone, since the client now comes with the realm.
    • For another Keycloak, import the SP metadata from http://localhost:8081/saml2/metadata, which also turns "Client signature required" on.
    • Then set the IdP-initiated SSO URL name to okta-client.
  • L171–179: add the quick test without Okta: http://localhost:8080/realms/test-realm/protocol/saml/clients/okta-client, user test / test.
  • Optional, in "What Just Happened?":
    • The replay window of an unsolicited response is about a minute plus Spring's five minutes of clock skew, and the client's "Assertion Lifespan" shortens it.
    • Restart the SP after recreating Keycloak, since it has new signing keys.
  • Still valid: the Okta SSO URL and Audience URI, okta-broker, okta-client, /login/saml2/sso, /saml2/metadata, the example link and the client screenshots.
  • Editorial:
    • The in-page links #service-provider-initated-flow and #identity-provider-initated-flow (L26–27) are misspelled.
    • Typos: "differen" (L43), "Login in your Okta tenant" (L132), "turn of" (L232, L238), "a ACS", "a OIDC".

@Wictorgirardi

Copy link
Copy Markdown
Contributor Author

Combined into #46 with the other Spring Boot example. The changes are the same; only the commit SHAs differ.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant