Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -167,3 +167,10 @@ 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/
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.0</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 @@ -271,7 +271,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 @@ -313,10 +313,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)
.collect(Collectors.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 @@ -347,14 +347,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 @@ -394,7 +394,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 @@ -447,10 +446,11 @@ protected Response handleLoginResponse(String samlResponse, SAMLDocumentHolder h
assertionElement = DocumentUtil.getElement(holder.getSamlDocument(), new QName(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 @@ -473,6 +473,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 @@ -492,10 +493,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 @@ -513,7 +515,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 @@ -529,8 +531,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 @@ -557,6 +560,7 @@ protected Response handleLoginResponse(String samlResponse, SAMLDocumentHolder h
break;
}
}

if (assertion.getAttributeStatements() != null) {
String email = getX500Attribute(assertion, X500SAMLProfileConstants.EMAIL);
if (email != null) {
Expand All @@ -568,12 +572,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 @@ -595,8 +602,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.isPresent()) {
event.error(Errors.CLIENT_NOT_FOUND);
Expand All @@ -619,10 +626,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 @@ -649,7 +656,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 @@ -663,7 +670,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 @@ -687,15 +696,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