Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
2b6ae2f
update gitignore
0314R Aug 13, 2025
d50fabc
.gitignore .idea folder entirely
0314R Mar 3, 2026
cde6fe1
Frontend: No longer prefill "metadata validUntil" field at all, inste…
0314R May 22, 2026
bf7d814
Fix signature validation by adding fix id setup and improve logging i…
0314R May 22, 2026
a68af99
Proper placement of git.commit.id.abbrev in jar name.
0314R May 22, 2026
8f00418
Added to our frontend: e2e tests and Keycloak's default ArtifactBindi…
0314R May 22, 2026
18db73d
mostly frontend: (re)added "assertionConsumingServiceIndex"; removed …
0314R May 22, 2026
7f05748
removed folder "theme-resources" because that extends the v1 admin th…
0314R May 22, 2026
4754692
config-frontend: minor bugfixes for "enabled" and "hide on login" not…
0314R May 22, 2026
b918c92
More test classes: SAMLUtil.java, SAMLEndpointTest (testHandleSamlRes…
0314R May 22, 2026
540f898
Minor frontend config fix
0314R May 22, 2026
73a65ef
Reverted removal of Test; minor additional fixes.
0314R May 27, 2026
facc1ed
Renamed wrongly capitalized serverUrl; allow for possible relativePath
0314R May 28, 2026
20d3c27
Merge pull request #13 from First8/26.0.x-bugfixes-features-tests-log…
0314R May 28, 2026
73a8129
Add TracingProvider to keep SAMLEndpointTest working in 26.1
0314R May 28, 2026
decad79
Merge branch '26.1.x' into feature/propagate-26.0-fixes-to-26.1
0314R May 28, 2026
8a87a49
Merge pull request #15 from First8/feature/propagate-26.0-fixes-to-26.1
0314R May 28, 2026
8c13246
Merge branch '26.2.x' into feature/propagate-26.0-fixes-to-26.2
0314R May 28, 2026
d90adc5
Merge pull request #16 from First8/feature/propagate-26.0-fixes-to-26.2
0314R May 28, 2026
482869c
tiny pom version fix
0314R May 28, 2026
5103b6d
set pom versioning right before PR
0314R May 28, 2026
4467db2
Merge branch '26.3.x' into feature/propagate-26.0-fixes-to-26.3
0314R May 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,8 @@ fabric.properties

!.idea/codeStyles
!.idea/runConfigurations
/.idea/codeStyles/codeStyleConfig.xml
/.idea/codeStyles/Project.xml

### Maven ###
target/
Expand All @@ -167,6 +169,13 @@ buildNumber.properties
.classpath

# End of https://www.toptal.com/developers/gitignore/api/maven,intellij+all,eclipse

# E2E test credentials
src/test/e2e/frontend/.env
src/test/e2e/frontend/node_modules/

# Claude Code
.claude/
/patch-tool/.work/

.logs
12 changes: 10 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@

<groupId>nl.first8.keycloak.broker</groupId>
<artifactId>idp-saml2-extended</artifactId>
<name>KeyCloak: SAML v2.0 - Extended</name>
<name>Keycloak: SAML v2.0 - Extended</name>
<description>
SAML v2.0 extensions that adds more configuration options to connect to SAML Service Providers.
</description>
<version>1.1-SNAPSHOT-26</version>
<version>26.3</version>

<properties>
<maven.compiler.source>17</maven.compiler.source>
Expand All @@ -29,6 +29,7 @@
</properties>

<build>
<finalName>${project.artifactId}-${project.version}-${git.commit.id.abbrev}</finalName>
<sourceDirectory>${project.basedir}/src/main/java</sourceDirectory>
<directory>${project.basedir}/target</directory>
<resources>
Expand All @@ -46,6 +47,7 @@
<goals>
<goal>revision</goal>
</goals>
<phase>validate</phase>
</execution>
</executions>

Expand Down Expand Up @@ -156,6 +158,9 @@
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
<configuration>
<argLine>-Djava.util.logging.manager=org.jboss.logmanager.LogManager</argLine>
</configuration>
</plugin>

<plugin>
Expand All @@ -168,6 +173,9 @@
<goals>
<goal>spotbugs</goal>
</goals>
<configuration>
<skip>${skipSpotBugs}</skip>
</configuration>
</execution>
</executions>
<configuration>
Expand Down
73 changes: 40 additions & 33 deletions src/main/java/nl/first8/keycloak/broker/saml/SAMLEndpoint.java
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ protected Response handleSamlRequest(String samlRequest, String relayState) {
RequestAbstractType requestAbstractType = (RequestAbstractType) holder.getSamlObject();
// validate destination
if (isDestinationRequired() &&
requestAbstractType.getDestination() == null && containsUnencryptedSignature(holder)) {
requestAbstractType.getDestination() == null && containsUnencryptedSignature(holder)) {
event.event(EventType.IDENTITY_PROVIDER_RESPONSE);
event.detail(Details.REASON, Errors.MISSING_REQUIRED_DESTINATION);
event.error(Errors.INVALID_REQUEST);
Expand Down Expand Up @@ -311,10 +311,10 @@ protected Response logoutRequest(LogoutRequestType request, String relayState) {
if (request.getSessionIndex() == null || request.getSessionIndex().isEmpty()) {
AtomicReference<LogoutRequestType> ref = new AtomicReference<>(request);
session.sessions().getUserSessionByBrokerUserIdStream(realm, brokerUserId)
.filter(userSession -> userSession.getState() != UserSessionModel.State.LOGGING_OUT &&
userSession.getState() != UserSessionModel.State.LOGGED_OUT)
.toList() // collect to avoid concurrent modification as backchannelLogout removes the user sessions.
.forEach(processLogout(ref));
.filter(userSession -> userSession.getState() != UserSessionModel.State.LOGGING_OUT &&
userSession.getState() != UserSessionModel.State.LOGGED_OUT)
.collect(Collectors.toList()) // collect to avoid concurrent modification as backchannelLogout removes the user sessions.
.forEach(processLogout(ref));
request = ref.get();

} else {
Expand Down Expand Up @@ -345,14 +345,14 @@ protected Response logoutRequest(LogoutRequestType request, String relayState) {
builder.destination(config.getSingleLogoutServiceUrl());
builder.issuer(issuerURL);
org.keycloak.protocol.saml.JaxrsSAML2BindingBuilder binding = new org.keycloak.protocol.saml.JaxrsSAML2BindingBuilder(session)
.relayState(relayState);
.relayState(relayState);
boolean postBinding = config.isPostBindingLogout();
if (config.isWantAuthnRequestsSigned()) {
KeyManager.ActiveRsaKey keys = session.keys().getActiveRsaKey(realm);
String keyName = config.getXmlSigKeyInfoKeyNameTransformer().getKeyName(keys.getKid(), keys.getCertificate());
binding.signWith(keyName, keys.getPrivateKey(), keys.getPublicKey(), keys.getCertificate())
.signatureAlgorithm(provider.getSignatureAlgorithm())
.signDocument();
.signatureAlgorithm(provider.getSignatureAlgorithm())
.signDocument();
if (!postBinding && config.isAddExtensionsElementWithKeyInfo()) { // Only include extension if REDIRECT binding and signing whole SAML protocol message
builder.addExtension(new KeycloakKeySamlExtensionGenerator(keyName));
}
Expand Down Expand Up @@ -392,7 +392,6 @@ private String getEntityId(UriInfo uriInfo, RealmModel realm) {
}

protected Response handleLoginResponse(String samlResponse, SAMLDocumentHolder holder, ResponseType responseType, String relayState, String clientId) {

try {
AuthenticationSessionModel authSession;
if (StringUtil.isNotBlank(clientId)) {
Expand Down Expand Up @@ -448,10 +447,11 @@ protected Response handleLoginResponse(String samlResponse, SAMLDocumentHolder h
assertionElement = DocumentUtil.getElement(holder.getSamlDocument(), new QName(Objects.requireNonNull(JBossSAMLConstants.ASSERTION.get())));
}

logger.trace("Validating the response Issuer");
// Validate the response Issuer
final String responseIssuer = responseType.getIssuer() != null ? responseType.getIssuer().getValue() : null;
final boolean responseIssuerValidationSuccess = config.getIdpEntityId() == null ||
(responseIssuer != null && responseIssuer.equals(config.getIdpEntityId()));
(responseIssuer != null && responseIssuer.equals(config.getIdpEntityId()));
if (!responseIssuerValidationSuccess) {
logger.errorf("Response Issuer validation failed: expected %s, actual %s", config.getIdpEntityId(), responseIssuer);
event.event(EventType.IDENTITY_PROVIDER_RESPONSE);
Expand All @@ -474,6 +474,7 @@ protected Response handleLoginResponse(String samlResponse, SAMLDocumentHolder h
final boolean signatureNotValid = signed && config.isValidateSignature() && !AssertionUtil.isSignatureValid(assertionElement, getIDPKeyLocator());
final boolean hasNoSignatureWhenRequired = !signed && config.isValidateSignature() && !containsUnencryptedSignature(holder);


if (assertionSignatureNotExistsWhenRequired || signatureNotValid || hasNoSignatureWhenRequired) {
logger.error("validation failed");
event.event(EventType.IDENTITY_PROVIDER_RESPONSE);
Expand All @@ -493,10 +494,11 @@ protected Response handleLoginResponse(String samlResponse, SAMLDocumentHolder h

AssertionType assertion = responseType.getAssertions().get(0).getAssertion();

logger.trace("Validating the assertion issuer");
// Validate the assertion Issuer
final String assertionIssuer = assertion.getIssuer() != null ? assertion.getIssuer().getValue() : null;
final boolean assertionIssuerValidationSuccess = config.getIdpEntityId() == null ||
(assertionIssuer != null && assertionIssuer.equals(config.getIdpEntityId()));
(assertionIssuer != null && assertionIssuer.equals(config.getIdpEntityId()));
if (!assertionIssuerValidationSuccess) {
logger.errorf("Assertion Issuer validation failed: expected %s, actual %s", config.getIdpEntityId(), assertionIssuer);
event.event(EventType.IDENTITY_PROVIDER_RESPONSE);
Expand All @@ -514,7 +516,7 @@ protected Response handleLoginResponse(String samlResponse, SAMLDocumentHolder h
return ErrorPage.error(session, authSession, Response.Status.BAD_REQUEST, Messages.INVALID_REQUESTER);
}

BrokeredIdentityContext identity = new BrokeredIdentityContext(principal,config);
BrokeredIdentityContext identity = new BrokeredIdentityContext(principal, config);
identity.getContextData().put(SAML_LOGIN_RESPONSE, responseType);
identity.getContextData().put(SAML_ASSERTION, assertion);
identity.setAuthenticationSession(authSession);
Expand All @@ -530,8 +532,9 @@ protected Response handleLoginResponse(String samlResponse, SAMLDocumentHolder h
identity.setToken(samlResponse);
}


ConditionsValidator.Builder cvb = new ConditionsValidator.Builder(assertion.getID(), assertion.getConditions(), destinationValidator)
.clockSkewInMillis(1000 * config.getAllowedClockSkew());
.clockSkewInMillis(1000 * config.getAllowedClockSkew());
try {
String issuerURL = getEntityId(session.getContext().getUri(), realm);
cvb.addAllowedAudience(URI.create(issuerURL));
Expand All @@ -558,6 +561,7 @@ protected Response handleLoginResponse(String samlResponse, SAMLDocumentHolder h
break;
}
}

if (assertion.getAttributeStatements() != null) {
String email = getX500Attribute(assertion, X500SAMLProfileConstants.EMAIL);
if (email != null) {
Expand All @@ -569,12 +573,15 @@ protected Response handleLoginResponse(String samlResponse, SAMLDocumentHolder h
String brokerUserId = config.getAlias() + "." + principal;
identity.setBrokerUserId(brokerUserId);
identity.setIdp(provider);


if (authn != null && authn.getSessionIndex() != null) {
String brokerSessionId = config.getAlias() + "." + authn.getSessionIndex();
logger.debugf("Set broker SessionID to \"%s\".", brokerSessionId);
identity.setBrokerSessionId(brokerSessionId);
}

logger.trace("handleLoginResponse finished succesfully.");
return callback.authenticated(identity);
} catch (WebApplicationException e) {
return e.getResponse();
Expand All @@ -596,8 +603,8 @@ private AuthenticationSessionModel samlIdpInitiatedSSO(final String clientUrlNam
event.event(EventType.LOGIN);
CacheControlUtil.noBackButtonCacheControlHeader(session);
Optional<ClientModel> oClient = SAMLEndpoint.this.session.clients()
.searchClientsByAttributes(realm, Collections.singletonMap(SamlProtocol.SAML_IDP_INITIATED_SSO_URL_NAME, clientUrlName), 0, 1)
.findFirst();
.searchClientsByAttributes(realm, Collections.singletonMap(SamlProtocol.SAML_IDP_INITIATED_SSO_URL_NAME, clientUrlName), 0, 1)
.findFirst();

if (oClient.isEmpty()) {
event.error(Errors.CLIENT_NOT_FOUND);
Expand All @@ -620,10 +627,10 @@ private AuthenticationSessionModel samlIdpInitiatedSSO(final String clientUrlNam

private boolean isSuccessfulSamlResponse(ResponseType responseType) {
return responseType != null
&& responseType.getStatus() != null
&& responseType.getStatus().getStatusCode() != null
&& responseType.getStatus().getStatusCode().getValue() != null
&& Objects.equals(responseType.getStatus().getStatusCode().getValue().toString(), JBossSAMLURIConstants.STATUS_SUCCESS.get());
&& responseType.getStatus() != null
&& responseType.getStatus().getStatusCode() != null
&& responseType.getStatus().getStatusCode().getValue() != null
&& Objects.equals(responseType.getStatus().getStatusCode().getValue().toString(), JBossSAMLURIConstants.STATUS_SUCCESS.get());
}


Expand All @@ -650,7 +657,7 @@ public Response handleSamlResponse(String samlResponse, String relayState, Strin

// validate destination
if (isDestinationRequired()
&& statusResponse.getDestination() == null && containsUnencryptedSignature(holder)) {
&& statusResponse.getDestination() == null && containsUnencryptedSignature(holder)) {
logger.warnf("Destination %s required, destination (%s) is NULL or Holder contains unencrypted signature (%s)", (isDestinationRequired()) ? "is" : "is not", statusResponse.getDestination(), containsUnencryptedSignature(holder));
event.event(EventType.IDENTITY_PROVIDER_RESPONSE);
event.detail(Details.REASON, Errors.MISSING_REQUIRED_DESTINATION);
Expand All @@ -664,7 +671,9 @@ public Response handleSamlResponse(String samlResponse, String relayState, Strin
event.error(Errors.INVALID_SAML_RESPONSE);
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);
}
logger.trace("Right before handleSamlResponse's if (config.isValidateSignature())");
if (config.isValidateSignature()) {
logger.trace("TRUE: if (config.isValidateSignature())");
try {
if (isArtifactResponse) {
logger.debugf("Verifying signature for %s", GeneralConstants.SAML_ARTIFACT_RESPONSE_KEY);
Expand All @@ -688,15 +697,13 @@ public Response handleSamlResponse(String samlResponse, String relayState, Strin
logger.debug("SAML Response was NOT of type ResponseType so calling logout.");
return handleLogoutResponse(holder, statusResponse, relayState);
}


}

private ResponseType convertToResponseType(StatusResponseType statusResponse) {
ResponseType responseType;
if (statusResponse instanceof org.keycloak.dom.saml.v2.protocol.ResponseType) {
org.keycloak.dom.saml.v2.protocol.ResponseType kcResponseType =
(org.keycloak.dom.saml.v2.protocol.ResponseType) ((ArtifactResponseType) statusResponse).getAny();
(org.keycloak.dom.saml.v2.protocol.ResponseType) ((ArtifactResponseType) statusResponse).getAny();
responseType = new ResponseType(kcResponseType.getID(), kcResponseType.getIssueInstant());
responseType.setExtensions(kcResponseType.getExtensions());
responseType.setInResponseTo(kcResponseType.getInResponseTo());
Expand Down Expand Up @@ -787,6 +794,7 @@ protected void verifySignature(String key, SAMLDocumentHolder documentHolder) th
protected SAMLDocumentHolder extractRequestDocument(String samlRequest) {
return SAMLRequestParser.parseRequestPostBinding(samlRequest);
}

@Override
protected SAMLDocumentHolder extractResponseDocument(String response) {
byte[] samlBytes = response.getBytes();
Expand Down Expand Up @@ -893,15 +901,15 @@ private String getPrincipal(AssertionType assertion) {

private String getFirstMatchingAttribute(AssertionType assertion, Predicate<AttributeType> predicate) {
return assertion.getAttributeStatements().stream()
.map(AttributeStatementType::getAttributes)
.flatMap(Collection::stream)
.map(AttributeStatementType.ASTChoiceType::getAttribute)
.filter(predicate)
.map(AttributeType::getAttributeValue)
.flatMap(Collection::stream)
.findFirst()
.map(Object::toString)
.orElse(null);
.map(AttributeStatementType::getAttributes)
.flatMap(Collection::stream)
.map(AttributeStatementType.ASTChoiceType::getAttribute)
.filter(predicate)
.map(AttributeType::getAttributeValue)
.flatMap(Collection::stream)
.findFirst()
.map(Object::toString)
.orElse(null);
}

private String expectedPrincipalType() {
Expand Down Expand Up @@ -979,7 +987,6 @@ private boolean validateInResponseToAttribute(ResponseType responseType, String
}
}
}

return true;
}
}
Loading
Loading