From e7192fcabcd8746d4e5ace4e78ab5c6325bcbf15 Mon Sep 17 00:00:00 2001 From: sispeo <42068883+fperot74@users.noreply.github.com> Date: Mon, 2 Nov 2020 12:02:34 +0100 Subject: [PATCH] [CLOUDTRUST-2708] Add install.sh and fix some Sonar code smells --- README.md | 41 +- .../broker/wsfed/SAML2RequestedTokenTest.java | 4 +- .../broker/wsfed/WSFedEndpointTest.java | 8 +- .../wsfed/WSFedIdentityProviderTest.java | 4 +- .../keycloak/common/wsfed/MockHelper.java | 8 +- .../wsfed/parsers/AbstractParserTest.java | 53 ++ ...rityTokenResponseCollectionParserTest.java | 350 +++++++++++++ .../wsfed/parsers/WSTrustParserTest.java | 43 ++ .../keycloak/integration/WsFedClient.java | 4 +- .../steps/AbstractStepBuilder.java | 2 +- .../CreateWsFedAuthRequestStepBuilder.java | 3 - .../protocol/wsfed/WSFedServiceTest.java | 101 ++-- .../builders/WSFedProtocolParametersTest.java | 120 ++--- .../WsFedSAML11AssertionTypeBuilderTest.java | 26 +- .../samples/complete-computed-key.xml | 28 + .../src/test/resources/samples/complete.xml | 32 ++ .../src/test/resources/samples/dummy.xml | 1 + .../src/test/resources/samples/empty.xml | 0 .../resources/samples/invalid-collection.xml | 5 + .../samples/invalid-request-type.xml | 8 + .../src/test/resources/samples/minimum.xml | 5 + .../test/resources/samples/unknown-tag.xml | 11 + .../resources/samples/without-context.xml | 5 + keycloak-wsfed/assembly.xml | 33 ++ keycloak-wsfed/install.sh | 166 ++++++ keycloak-wsfed/module.xml | 2 +- keycloak-wsfed/pom.xml | 30 +- .../keycloak/broker/wsfed/RequestedToken.java | 22 +- .../broker/wsfed/SAML11RequestedToken.java | 81 +-- .../broker/wsfed/SAML2RequestedToken.java | 43 +- .../broker/wsfed/WSFedDataMarshaller.java | 76 +-- .../keycloak/broker/wsfed/WSFedEndpoint.java | 111 ++-- .../broker/wsfed/WSFedIdentityProvider.java | 32 +- .../wsfed/WSFedIdentityProviderConfig.java | 7 +- .../wsfed/mappers/AttributeToRoleMapper.java | 53 +- .../wsfed/mappers/UserAttributeMapper.java | 59 +-- .../wsfed/builders/WSFedResponseBuilder.java | 19 +- ...SecurityTokenResponseCollectionParser.java | 14 +- ...WSTRequestSecurityTokenResponseParser.java | 492 ++++++++++-------- .../common/wsfed/parsers/WSTrustParser.java | 34 +- .../common/wsfed/utils/WSFedValidator.java | 6 +- .../wsfed/writers/WSTrustResponseWriter.java | 144 ++--- .../protocol/wsfed/WSFedLoginProtocol.java | 5 +- .../keycloak/protocol/wsfed/WSFedService.java | 55 +- .../wsfed/WsFedSAMLAssertionTokenFormat.java | 4 +- .../RequestSecurityTokenResponseBuilder.java | 2 +- .../builders/SAML11AssertionTypeBuilder.java | 10 +- .../builders/SAML2AssertionTypeBuilder.java | 17 +- .../builders/WSFedOIDCAccessTokenBuilder.java | 28 +- .../builders/WSFedProtocolParameters.java | 246 ++++----- .../WSFedSAML2AssertionTypeBuilder.java | 34 +- .../WsFedSAML11AssertionTypeBuilder.java | 3 +- ...WsFedSAMLAssertionTypeAbstractBuilder.java | 8 +- .../WSFedIDPDescriptorClientInstallation.java | 12 +- .../mappers/AbstractWsfedProtocolMapper.java | 14 +- .../wsfed/mappers/OIDCAddressMapper.java | 2 +- .../wsfed/mappers/OIDCFullNameMapper.java | 2 +- .../wsfed/mappers/OIDCUserPropertyMapper.java | 2 +- .../wsfed/mappers/SAMLRoleListMapper.java | 2 +- .../SAMLUserAttributeStatementMapper.java | 2 +- ...LUserFullNameAttributeStatementMapper.java | 4 +- ...LUserPropertyAttributeStatementMapper.java | 2 +- .../protocol/wsfed/sig/SAML11Signature.java | 4 +- .../core/saml/v2/util/AssertionUtil.java | 4 + .../exceptions/CtRuntimeException.java | 13 - 65 files changed, 1801 insertions(+), 960 deletions(-) create mode 100644 keycloak-wsfed-tests/src/test/java/com/quest/keycloak/common/wsfed/parsers/AbstractParserTest.java create mode 100644 keycloak-wsfed-tests/src/test/java/com/quest/keycloak/common/wsfed/parsers/WSTRequestSecurityTokenResponseCollectionParserTest.java create mode 100644 keycloak-wsfed-tests/src/test/java/com/quest/keycloak/common/wsfed/parsers/WSTrustParserTest.java create mode 100644 keycloak-wsfed-tests/src/test/resources/samples/complete-computed-key.xml create mode 100644 keycloak-wsfed-tests/src/test/resources/samples/complete.xml create mode 100644 keycloak-wsfed-tests/src/test/resources/samples/dummy.xml create mode 100644 keycloak-wsfed-tests/src/test/resources/samples/empty.xml create mode 100644 keycloak-wsfed-tests/src/test/resources/samples/invalid-collection.xml create mode 100644 keycloak-wsfed-tests/src/test/resources/samples/invalid-request-type.xml create mode 100644 keycloak-wsfed-tests/src/test/resources/samples/minimum.xml create mode 100644 keycloak-wsfed-tests/src/test/resources/samples/unknown-tag.xml create mode 100644 keycloak-wsfed-tests/src/test/resources/samples/without-context.xml create mode 100644 keycloak-wsfed/assembly.xml create mode 100644 keycloak-wsfed/install.sh delete mode 100644 keycloak-wsfed/src/main/java/io/cloudtrust/keycloak/exceptions/CtRuntimeException.java diff --git a/README.md b/README.md index 4194d88..ffed23f 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,46 @@ for its operations. This module is currently working on 8.0.1 (check tags for compatibility with previous Keycloak versions) -## How to Install +## How to build + +Before building this project with a basic `mvn clean install`, you need to first build cloudtrust-common. + +```Bash +git clone git@github.com:cloudtrust/cloudtrust-parent.git +cd cloudtrust-parent +mvn clean install +``` + +Then build keycloak-wsfed: + +```Bash +git clone git@github.com:cloudtrust/keycloak-wsfed.git +cd keycloak-wsfed +mvn clean install +``` + +If you get an error telling `Could not find artifact org.keycloak.testsuite:integration-arquillian-tests:pom`, you might build Keycloak with: + +```Bash +mvn install -Pconsole-ui-tests -DskipTests +``` + + +## How to install + +After building it, you can automatically install this module using the following command line: + +```Bash +./keycloak-wsfed/install.sh {path-to-keycloak} +``` + +You can uninstall it with: + +```Bash +./keycloak-wsfed/install.sh {path-to-keycloak} -u +``` + +But you can choose to manually install it: ### Copy files diff --git a/keycloak-wsfed-tests/src/test/java/com/quest/keycloak/broker/wsfed/SAML2RequestedTokenTest.java b/keycloak-wsfed-tests/src/test/java/com/quest/keycloak/broker/wsfed/SAML2RequestedTokenTest.java index 5ed00cd..8aec6a4 100644 --- a/keycloak-wsfed-tests/src/test/java/com/quest/keycloak/broker/wsfed/SAML2RequestedTokenTest.java +++ b/keycloak-wsfed-tests/src/test/java/com/quest/keycloak/broker/wsfed/SAML2RequestedTokenTest.java @@ -22,7 +22,7 @@ import com.quest.keycloak.common.wsfed.MockHelper; import com.quest.keycloak.protocol.wsfed.builders.WSFedSAML2AssertionTypeBuilder; -import io.cloudtrust.keycloak.exceptions.CtRuntimeException; +import io.cloudtrust.exception.CloudtrustRuntimeException; import org.junit.Test; import org.keycloak.dom.saml.v2.assertion.AssertionType; @@ -81,7 +81,7 @@ public void testInvalidSignature() throws Exception { generator.initialize(2048); keyPair = generator.generateKeyPair(); } catch (NoSuchAlgorithmException e) { - throw new CtRuntimeException(e); + throw new CloudtrustRuntimeException(e); } EventBuilder event = mock(EventBuilder.class); diff --git a/keycloak-wsfed-tests/src/test/java/com/quest/keycloak/broker/wsfed/WSFedEndpointTest.java b/keycloak-wsfed-tests/src/test/java/com/quest/keycloak/broker/wsfed/WSFedEndpointTest.java index 4e05535..de0f716 100644 --- a/keycloak-wsfed-tests/src/test/java/com/quest/keycloak/broker/wsfed/WSFedEndpointTest.java +++ b/keycloak-wsfed-tests/src/test/java/com/quest/keycloak/broker/wsfed/WSFedEndpointTest.java @@ -21,7 +21,7 @@ import com.quest.keycloak.common.wsfed.WSFedConstants; import com.quest.keycloak.protocol.wsfed.builders.RequestSecurityTokenResponseBuilder; -import io.cloudtrust.keycloak.exceptions.CtRuntimeException; +import io.cloudtrust.exception.CloudtrustRuntimeException; import org.junit.Before; import org.junit.Rule; @@ -328,7 +328,7 @@ public void testHandleLoginResponseException() throws Exception { when(token.getSessionIndex()).thenReturn("123"); when(token.getUsername()).thenReturn("username"); - when(callback.authenticated(any(BrokeredIdentityContext.class))).thenThrow(new CtRuntimeException("Exception")); + when(callback.authenticated(any(BrokeredIdentityContext.class))).thenThrow(new CloudtrustRuntimeException("Exception")); expectedException.expect(IdentityBrokerException.class); expectedException.expectMessage(equalTo("Could not process response from WS-Fed identity provider.")); @@ -397,12 +397,12 @@ public void testHandleWsFedResponseBadSig() throws Exception { generator.initialize(2048); keyPair = generator.generateKeyPair(); } catch (NoSuchAlgorithmException e) { - throw new CtRuntimeException(e); + throw new CloudtrustRuntimeException(e); } try { CertificateUtils.generateV1SelfSignedCertificate(keyPair, "junk"); } catch (Exception e) { - throw new CtRuntimeException(e); + throw new CloudtrustRuntimeException(e); } RequestSecurityTokenResponseBuilder builder = SAML2RequestedTokenTest.generateRequestSecurityTokenResponseBuilder(mockHelper); when(config.isValidateSignature()).thenReturn(true); diff --git a/keycloak-wsfed-tests/src/test/java/com/quest/keycloak/broker/wsfed/WSFedIdentityProviderTest.java b/keycloak-wsfed-tests/src/test/java/com/quest/keycloak/broker/wsfed/WSFedIdentityProviderTest.java index 8ff26c1..2046368 100644 --- a/keycloak-wsfed-tests/src/test/java/com/quest/keycloak/broker/wsfed/WSFedIdentityProviderTest.java +++ b/keycloak-wsfed-tests/src/test/java/com/quest/keycloak/broker/wsfed/WSFedIdentityProviderTest.java @@ -20,7 +20,7 @@ import com.quest.keycloak.common.wsfed.TestHelpers.*; import com.quest.keycloak.common.wsfed.WSFedConstants; -import io.cloudtrust.keycloak.exceptions.CtRuntimeException; +import io.cloudtrust.exception.CloudtrustRuntimeException; import org.apache.http.HttpStatus; import org.junit.Before; @@ -84,7 +84,7 @@ public void testCallback() throws Exception { @Test public void testPerformLoginException() throws Exception { - doThrow(new CtRuntimeException("Message")).when(config).getWsFedRealm(); + doThrow(new CloudtrustRuntimeException("Message")).when(config).getWsFedRealm(); expectedException.expect(IdentityBrokerException.class); expectedException.expectMessage(equalTo("Could not create authentication request.")); diff --git a/keycloak-wsfed-tests/src/test/java/com/quest/keycloak/common/wsfed/MockHelper.java b/keycloak-wsfed-tests/src/test/java/com/quest/keycloak/common/wsfed/MockHelper.java index 96b1913..93ae412 100644 --- a/keycloak-wsfed-tests/src/test/java/com/quest/keycloak/common/wsfed/MockHelper.java +++ b/keycloak-wsfed-tests/src/test/java/com/quest/keycloak/common/wsfed/MockHelper.java @@ -41,7 +41,7 @@ import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; -import io.cloudtrust.keycloak.exceptions.CtRuntimeException; +import io.cloudtrust.exception.CloudtrustRuntimeException; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; @@ -99,7 +99,7 @@ public class MockHelper { @Mock private AuthenticationSessionModel authSession; @Mock - private ClientSessionCode accessCode; + private ClientSessionCode accessCode; @Mock private UserSessionModel userSessionModel; @@ -168,13 +168,13 @@ public static void generateActiveRealmKeys(KeyManager keyManager, KeyManager.Act generator.initialize(2048); keyPair = generator.generateKeyPair(); } catch (NoSuchAlgorithmException e) { - throw new CtRuntimeException(e); + throw new CloudtrustRuntimeException(e); } X509Certificate certificate = null; try { certificate = CertificateUtils.generateV1SelfSignedCertificate(keyPair, realm.getName()); } catch (Exception e) { - throw new CtRuntimeException(e); + throw new CloudtrustRuntimeException(e); } KeyWrapper activeKeyWrapper = new KeyWrapper(); diff --git a/keycloak-wsfed-tests/src/test/java/com/quest/keycloak/common/wsfed/parsers/AbstractParserTest.java b/keycloak-wsfed-tests/src/test/java/com/quest/keycloak/common/wsfed/parsers/AbstractParserTest.java new file mode 100644 index 0000000..330eac6 --- /dev/null +++ b/keycloak-wsfed-tests/src/test/java/com/quest/keycloak/common/wsfed/parsers/AbstractParserTest.java @@ -0,0 +1,53 @@ +package com.quest.keycloak.common.wsfed.parsers; + +import java.io.ByteArrayInputStream; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.function.Function; + +import javax.xml.stream.XMLEventReader; + +import org.picketlink.common.exceptions.ParsingException; +import org.picketlink.common.util.StaxParserUtil; + +public abstract class AbstractParserTest { + @SuppressWarnings({ "unchecked" }) + protected T parseFile(String filename, Class clazz, Function... updaters) throws ParsingException, IOException { + return clazz.cast(new WSTrustParser().parse(getXMLEventReader(filename, updaters))); + } + + protected InputStream getInputStream(String filename) { + InputStream stream = WSTRequestSecurityTokenResponseCollectionParserTest.class.getResourceAsStream(filename); + if (stream==null) { + try { + stream = new FileInputStream("src/test/resources"+filename); + } catch (IOException e) { + // Ignore + } + } + return stream; + } + + @SuppressWarnings("unchecked") + protected XMLEventReader getXMLEventReader(String filename, Function... updaters) throws IOException { + if (updaters==null) { + return StaxParserUtil.getXMLEventReader(getInputStream(filename)); + } + String xml; + try (InputStream input = getInputStream(filename)) { + byte[] content = new byte[input.available()]; + input.read(content); + xml = new String(content, StandardCharsets.UTF_8); + } + if (updaters!=null) { + for(Function updater : updaters) { + xml = updater.apply(xml); + } + } + try (InputStream stream = new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8))) { + return StaxParserUtil.getXMLEventReader(stream); + } + } +} diff --git a/keycloak-wsfed-tests/src/test/java/com/quest/keycloak/common/wsfed/parsers/WSTRequestSecurityTokenResponseCollectionParserTest.java b/keycloak-wsfed-tests/src/test/java/com/quest/keycloak/common/wsfed/parsers/WSTRequestSecurityTokenResponseCollectionParserTest.java new file mode 100644 index 0000000..2f3f8ab --- /dev/null +++ b/keycloak-wsfed-tests/src/test/java/com/quest/keycloak/common/wsfed/parsers/WSTRequestSecurityTokenResponseCollectionParserTest.java @@ -0,0 +1,350 @@ +package com.quest.keycloak.common.wsfed.parsers; + +import java.io.IOException; +import java.util.Map; +import java.util.function.Function; + +import javax.xml.namespace.QName; +import javax.xml.stream.XMLEventReader; +import javax.xml.stream.events.StartElement; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.picketlink.common.constants.WSTrustConstants; +import org.picketlink.common.exceptions.ParsingException; +import org.picketlink.common.parsers.ParserNamespaceSupport; +import org.picketlink.common.util.StaxParserUtil; +import org.picketlink.identity.federation.core.parsers.ParserController; +import org.picketlink.identity.federation.core.wstrust.wrappers.RequestSecurityTokenResponse; +import org.picketlink.identity.federation.core.wstrust.wrappers.RequestSecurityTokenResponseCollection; +import org.picketlink.identity.federation.ws.policy.AppliesTo; +import org.picketlink.identity.federation.ws.trust.BinarySecretType; +import org.picketlink.identity.federation.ws.trust.ComputedKeyType; +import org.picketlink.identity.federation.ws.wss.secext.UsernameTokenType; + +public class WSTRequestSecurityTokenResponseCollectionParserTest extends AbstractParserTest { + public static class AdditionalNamespaceSupport implements ParserNamespaceSupport { + private String tag = "AdditionalTag"; + + @Override + public boolean supports(QName qname) { + return tag.equals(qname.getLocalPart()); + } + + @Override + public Object parse(XMLEventReader xmlEventReader) throws ParsingException { + StartElement startElement = StaxParserUtil.getNextStartElement(xmlEventReader); + StaxParserUtil.validate(startElement, tag); + if (!StaxParserUtil.hasTextAhead(xmlEventReader)) { + return this; + } + + return new AppliesToUnknownTag(StaxParserUtil.getElementText(xmlEventReader)); + } + }; + + public static class AppliesToUnknownTag extends AppliesTo { + private String value; + + public AppliesToUnknownTag(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + } + + private AdditionalNamespaceSupport additionalTagSupport; + + @Before + public void testSetup() { + if (additionalTagSupport==null) { + additionalTagSupport = new AdditionalNamespaceSupport(); + ParserController.add(additionalTagSupport); + } + } + + @Test + public void supportRSTCollectionTest() { + WSTRequestSecurityTokenResponseCollectionParser parser = new WSTRequestSecurityTokenResponseCollectionParser(); + Assert.assertFalse(parser.supports(new QName("dummy", "dummy"))); + Assert.assertFalse(parser.supports(new QName(WSTrustConstants.BASE_NAMESPACE, "dummy"))); + Assert.assertFalse(parser.supports(new QName("dummy", WSTrustConstants.RSTR_COLLECTION))); + Assert.assertTrue(parser.supports(new QName(WSTrustConstants.BASE_NAMESPACE, WSTrustConstants.RSTR_COLLECTION))); + } + + @Test + public void supportRSTTest() { + WSTRequestSecurityTokenResponseParser parser = new WSTRequestSecurityTokenResponseParser(); + Assert.assertFalse(parser.supports(new QName("dummy", "dummy"))); + Assert.assertFalse(parser.supports(new QName(WSTrustConstants.BASE_NAMESPACE, "dummy"))); + Assert.assertFalse(parser.supports(new QName("dummy", WSTrustConstants.RST))); + Assert.assertTrue(parser.supports(new QName(WSTrustConstants.BASE_NAMESPACE, WSTrustConstants.RST))); + } + + private RequestSecurityTokenResponseCollection readRSTRCollection(String filename) throws ParsingException { + try { + @SuppressWarnings("unchecked") + XMLEventReader reader = getXMLEventReader(filename); + return (RequestSecurityTokenResponseCollection)new WSTrustParser().parse(reader); + } catch (IOException ioe) { + throw new ParsingException("invalid input"); + } + } + + private RequestSecurityTokenResponse readFirstRSTR(String filename) throws ParsingException { + return readRSTRCollection(filename).getRequestSecurityTokenResponses().get(0); + } + + @SuppressWarnings("unchecked") + private RequestSecurityTokenResponse readFirstRSTRAlterInput(String filename, Function... updaters) throws ParsingException, IOException { + XMLEventReader reader = getXMLEventReader(filename, updaters); + RequestSecurityTokenResponseCollection res = (RequestSecurityTokenResponseCollection)new WSTrustParser().parse(reader); + return res.getRequestSecurityTokenResponses().get(0); + } + + private String replaceXMLTag(String xml, String tagName, String replaceValue) { + int pos = 0; + String search = "<"+tagName; + while (pos>=0 && pos=0) { + char c = xml.charAt(pos+search.length()); + if ((c>='A' && c<='Z') || (c>='a' && c<='z') || c=='_') { + continue; + } + int end = xml.indexOf('>', pos); + String tag = xml.substring(pos, end).trim(); + if (!tag.endsWith("/")) { + end = xml.indexOf("pos) { + end = xml.indexOf('>', end); + } + } + if (pos attrbs = resp.getRequestedAttachedReference().getSecurityTokenReference().getOtherAttributes(); + Assert.assertTrue(attrbs.values().contains("attached-tktype")); + attrbs = resp.getRequestedUnattachedReference().getSecurityTokenReference().getOtherAttributes(); + Assert.assertTrue(attrbs.values().contains("unattached-tktype")); + Assert.assertEquals("status.code", resp.getStatus().getCode()); + Assert.assertEquals("status.reason", resp.getStatus().getReason()); + Assert.assertTrue(resp.getRenewing().isAllow()); + Assert.assertTrue(resp.getRenewing().isOK()); + } + + @SuppressWarnings("unchecked") + @Test(expected = NullPointerException.class) + public void noLifeTimeCreated() throws ParsingException, IOException { + readFirstRSTRAlterInput("/samples/complete.xml", c -> replaceXMLTag(c, "Created", "")); + } + + @SuppressWarnings("unchecked") + @Test(expected = RuntimeException.class) + public void noLifeTimeExpires() throws ParsingException, IOException { + readFirstRSTRAlterInput("/samples/complete.xml", c -> replaceXMLTag(c, "Expires", "")); + } + + @SuppressWarnings("unchecked") + @Test(expected = ParsingException.class) + public void requestTypeWithoutText() throws ParsingException, IOException { + readFirstRSTRAlterInput("/samples/complete.xml", c -> replaceXMLTag(c, "RequestType", "")); + } + + @SuppressWarnings("unchecked") + @Test(expected = ParsingException.class) + public void tokenTypeWithoutText() throws ParsingException, IOException { + readFirstRSTRAlterInput("/samples/complete.xml", c -> replaceXMLTag(c, "TokenType", "")); + } + + @SuppressWarnings("unchecked") + @Test(expected = ParsingException.class) + public void keyTypeWithoutText() throws ParsingException, IOException { + readFirstRSTRAlterInput("/samples/complete.xml", c -> replaceXMLTag(c, "KeyType", "")); + } + + @SuppressWarnings("unchecked") + @Test(expected = ParsingException.class) + public void keyTypeInvalidURI() throws ParsingException, IOException{ + readFirstRSTRAlterInput("/samples/complete.xml", c -> replaceXMLTag(c, "KeyType", "}{")); + } + + @SuppressWarnings("unchecked") + @Test(expected = ParsingException.class) + public void keySizeEmpty() throws ParsingException, IOException { + readFirstRSTRAlterInput("/samples/complete.xml", c -> replaceXMLTag(c, "KeySize", "")); + } + + @SuppressWarnings("unchecked") + @Test(expected = ParsingException.class) + public void keySizeNotANumber() throws ParsingException, IOException { + readFirstRSTRAlterInput("/samples/complete.xml", c -> replaceXMLTag(c, "KeySize", "not-a-numbe")); + } + + @Test(expected = ParsingException.class) + public void invalidRequestTypeTest() throws ParsingException { + readFirstRSTR("/samples/invalid-request-type.xml"); + } + + @SuppressWarnings("unchecked") + @Test + public void binarySecretWithoutTypeTest() throws Throwable { + RequestSecurityTokenResponse resp = readFirstRSTRAlterInput("/samples/complete.xml", c -> c.replace(" Type=\"TYPE999\"", "")); + BinarySecretType entropyType = (BinarySecretType)resp.getEntropy().getAny().get(0); + Assert.assertNull(entropyType.getType()); + Assert.assertArrayEquals("ABCDEFGH".getBytes(), entropyType.getValue()); + } + + @SuppressWarnings("unchecked") + @Test(expected = ParsingException.class) + public void binarySecretWithoutTextTest() throws Throwable { + readFirstRSTRAlterInput("/samples/complete.xml", c -> replaceXMLTag(c, "BinarySecret", "")); + } + + @SuppressWarnings("unchecked") + @Test(expected = ParsingException.class) + public void noBinarySecretTest() throws Throwable { + try { + readFirstRSTRAlterInput("/samples/complete.xml", c -> replaceXMLTag(c, "BinarySecret", "")); + } catch (RuntimeException re) { + throw re.getCause(); + } + } + + @SuppressWarnings("unchecked") + @Test(expected = RuntimeException.class) + public void invalidUseKeyTypeTest() throws Throwable { + readFirstRSTRAlterInput("/samples/complete.xml", c -> replaceXMLTag(c, "X509Certificate", "")); + } + + @SuppressWarnings("unchecked") + @Test + public void useKeyKeyValueTest() throws Throwable { + RequestSecurityTokenResponse resp = readFirstRSTRAlterInput("/samples/complete.xml", c -> replaceXMLTag(c, "X509Certificate", "")); + Object useKey = resp.getUseKey().getAny().get(0); + Assert.assertTrue(useKey.toString().contains("KeyValue")); + } + + @SuppressWarnings("unchecked") + @Test(expected = ParsingException.class) + public void requestedProofTokenWithEmptyBinarySecretAlgorithmTest() throws Throwable { + readFirstRSTRAlterInput("/samples/complete-computed-key.xml", + c -> replaceXMLTag(c, "RequestedProofToken", "")); + } + + @Test + public void requestedProofTokenWithComputedKeyTest() throws Throwable { + RequestSecurityTokenResponse resp = readFirstRSTR("/samples/complete-computed-key.xml"); + ComputedKeyType keyType = (ComputedKeyType)resp.getRequestedProofToken().getAny().get(0); + Assert.assertEquals("Algorithm", keyType.getAlgorithm()); + } + + @SuppressWarnings("unchecked") + @Test(expected = ParsingException.class) + public void requestedProofTokenWithComputedKeyMissingAlgorithmTest() throws Throwable { + readFirstRSTRAlterInput("/samples/complete-computed-key.xml", + c -> replaceXMLTag(c, "RequestedProofToken", "")); + } + + @SuppressWarnings("unchecked") + @Test(expected = RuntimeException.class) + public void requestedProofTokenWithUnknownContentTest() throws Throwable { + readFirstRSTRAlterInput("/samples/complete-computed-key.xml", + c -> replaceXMLTag(c, "RequestedProofToken", "")); + } + + @Test(expected = RuntimeException.class) + public void unknownTagTest() throws Throwable { + readFirstRSTR("/samples/unknown-tag.xml"); + } + + @SuppressWarnings("unchecked") + @Test + public void unknownTagWithAddedSupportTest() throws Throwable { + RequestSecurityTokenResponse res = readFirstRSTRAlterInput("/samples/unknown-tag.xml", c -> c.replace("UnknownTag", "AdditionalTag")); + AppliesToUnknownTag appliesTo = (AppliesToUnknownTag)res.getAppliesTo(); + Assert.assertEquals("http://uri/unknown-tag", appliesTo.getValue()); + } + + @SuppressWarnings("unchecked") + @Test(expected = ParsingException.class) + public void invalidStatusCodeTest() throws Throwable { + readFirstRSTRAlterInput("/samples/complete.xml", + c -> replaceXMLTag(c, "Code", "")); + } + + @SuppressWarnings("unchecked") + @Test(expected = ParsingException.class) + public void invalidStatusReasonTest() throws Throwable { + readFirstRSTRAlterInput("/samples/complete.xml", + c -> replaceXMLTag(c, "Reason", "")); + } + + @SuppressWarnings("unchecked") + @Test(expected = RuntimeException.class) + public void statusWithUnknownTagTest() throws Throwable { + readFirstRSTRAlterInput("/samples/complete.xml", + c -> replaceXMLTag(c, "Reason", "")); + } + + @Test(expected = RuntimeException.class) + @SuppressWarnings("unchecked") + public void invalidCollectionTest() throws ParsingException, IOException { + parseFile("/samples/invalid-collection.xml", RequestSecurityTokenResponseCollection.class); + } +} diff --git a/keycloak-wsfed-tests/src/test/java/com/quest/keycloak/common/wsfed/parsers/WSTrustParserTest.java b/keycloak-wsfed-tests/src/test/java/com/quest/keycloak/common/wsfed/parsers/WSTrustParserTest.java new file mode 100644 index 0000000..7d58a02 --- /dev/null +++ b/keycloak-wsfed-tests/src/test/java/com/quest/keycloak/common/wsfed/parsers/WSTrustParserTest.java @@ -0,0 +1,43 @@ +package com.quest.keycloak.common.wsfed.parsers; + +import java.io.IOException; + +import javax.xml.namespace.QName; +import javax.xml.stream.XMLEventReader; + +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; +import org.picketlink.common.constants.WSTrustConstants; +import org.picketlink.common.exceptions.ParsingException; +import org.picketlink.identity.federation.core.wstrust.wrappers.RequestSecurityTokenResponse; +import org.picketlink.identity.federation.core.wstrust.wrappers.RequestSecurityTokenResponseCollection; + +public class WSTrustParserTest extends AbstractParserTest { + @Test + public void supportTest() { + WSTrustParser parser = new WSTrustParser(); + Assert.assertFalse(parser.supports(new QName("dummy", "dummy"))); + Assert.assertTrue(parser.supports(new QName(WSTrustConstants.BASE_NAMESPACE, "dummy"))); + } + + @Test + @SuppressWarnings("unchecked") + public void rstrCollectionTest() throws ParsingException, IOException { + Assert.assertNotNull(parseFile("/samples/complete.xml", RequestSecurityTokenResponseCollection.class)); + } + + @Test + @SuppressWarnings("unchecked") + public void rstrTest() throws ParsingException, IOException { + Assert.assertNotNull(parseFile("/samples/complete.xml", RequestSecurityTokenResponse.class, + c -> c.substring(c.indexOf('>')+1, c.indexOf(" T executeAndTransform(ResultExtractor resultTransformer, List> implements WsFedClient.Step { +public abstract class AbstractStepBuilder implements WsFedClient.Step { private final WsFedClientBuilder clientBuilder; public AbstractStepBuilder(WsFedClientBuilder clientBuilder) { diff --git a/keycloak-wsfed-tests/src/test/java/com/quest/keycloak/integration/steps/CreateWsFedAuthRequestStepBuilder.java b/keycloak-wsfed-tests/src/test/java/com/quest/keycloak/integration/steps/CreateWsFedAuthRequestStepBuilder.java index 0ad36f0..278293a 100644 --- a/keycloak-wsfed-tests/src/test/java/com/quest/keycloak/integration/steps/CreateWsFedAuthRequestStepBuilder.java +++ b/keycloak-wsfed-tests/src/test/java/com/quest/keycloak/integration/steps/CreateWsFedAuthRequestStepBuilder.java @@ -19,11 +19,8 @@ public class CreateWsFedAuthRequestStepBuilder extends AbstractStepBuilder { private final String clientId; private final String context; - private WsFedClientBuilder clientBuilder; - public CreateWsFedAuthRequestStepBuilder(WsFedClientBuilder clientBuilder, URI authServerWsFedUrl, String consumerUrl, String clientId, String context) { super(clientBuilder); - this.clientBuilder = clientBuilder; this.authServerWsFedUrl = authServerWsFedUrl; this.consumerUrl = consumerUrl; this.clientId = clientId; diff --git a/keycloak-wsfed-tests/src/test/java/com/quest/keycloak/protocol/wsfed/WSFedServiceTest.java b/keycloak-wsfed-tests/src/test/java/com/quest/keycloak/protocol/wsfed/WSFedServiceTest.java index 62041e3..ce55fcc 100644 --- a/keycloak-wsfed-tests/src/test/java/com/quest/keycloak/protocol/wsfed/WSFedServiceTest.java +++ b/keycloak-wsfed-tests/src/test/java/com/quest/keycloak/protocol/wsfed/WSFedServiceTest.java @@ -21,7 +21,6 @@ import com.quest.keycloak.common.wsfed.WSFedConstants; import com.quest.keycloak.protocol.wsfed.builders.WSFedProtocolParameters; import org.jboss.logging.Logger; -import org.jboss.resteasy.plugins.server.servlet.HttpServletResponseHeaders; import org.jboss.resteasy.specimpl.MultivaluedMapImpl; import org.jboss.resteasy.spi.HttpRequest; import org.jboss.resteasy.spi.HttpResponse; @@ -38,7 +37,13 @@ import org.keycloak.common.util.PemUtils; import org.keycloak.events.Errors; import org.keycloak.events.EventBuilder; -import org.keycloak.models.*; +import org.keycloak.models.AuthenticatedClientSessionModel; +import org.keycloak.models.AuthenticationFlowModel; +import org.keycloak.models.IdentityProviderModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserSessionModel; +import org.keycloak.models.UserSessionProvider; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.saml.common.util.DocumentUtil; import org.keycloak.services.managers.AuthenticationManager; @@ -53,10 +58,11 @@ import org.w3c.dom.Node; import javax.ws.rs.HttpMethod; -import javax.ws.rs.core.*; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; import java.util.Arrays; -import java.util.Collections; import java.util.HashSet; import java.util.UUID; @@ -205,17 +211,17 @@ public void testBasicChecksRealmDisabled() throws Exception { @Test public void testBasicChecksMissingActionLoggingOut() throws Exception { WSFedProtocolParameters parameters = new WSFedProtocolParameters(); - parameters.setWsfed_realm("https://realm"); + parameters.setWsfedRealm("https://realm"); doReturn(UserSessionModel.State.LOGGING_OUT).when(mockHelper.getUserSessionModel()).getState(); assertNull(service.basicChecks(parameters)); - assertEquals(UserSessionModel.State.LOGGING_OUT.toString(), parameters.getWsfed_action()); + assertEquals(UserSessionModel.State.LOGGING_OUT.toString(), parameters.getWsfedAction()); } @Test public void testBasicChecksMissingAction() throws Exception { WSFedProtocolParameters parameters = new WSFedProtocolParameters(); - parameters.setWsfed_realm("https://realm"); + parameters.setWsfedRealm("https://realm"); doReturn(UserSessionModel.State.LOGGED_IN).when(mockHelper.getUserSessionModel()).getState(); assertNotNull(service.basicChecks(parameters)); @@ -225,7 +231,7 @@ public void testBasicChecksMissingAction() throws Exception { @Test public void testBasicChecksMissingRealm() throws Exception { WSFedProtocolParameters parameters = new WSFedProtocolParameters(); - parameters.setWsfed_action(WSFedConstants.WSFED_SIGNIN_ACTION); + parameters.setWsfedAction(WSFedConstants.WSFED_SIGNIN_ACTION); assertNotNull(service.basicChecks(parameters)); assertErrorPage(mockHelper.getLoginFormsProvider(), Messages.INVALID_REQUEST); @@ -234,19 +240,19 @@ public void testBasicChecksMissingRealm() throws Exception { @Test public void testBasicChecksMissingRealmLoggingOut() throws Exception { WSFedProtocolParameters parameters = new WSFedProtocolParameters(); - parameters.setWsfed_action(WSFedConstants.WSFED_SIGNOUT_ACTION); + parameters.setWsfedAction(WSFedConstants.WSFED_SIGNOUT_ACTION); doReturn("https://realm").when(mockHelper.getUserSessionModel()).getNote(eq(WSFedConstants.WSFED_REALM)); assertNull(service.basicChecks(parameters)); - assertEquals("https://realm", parameters.getWsfed_realm()); + assertEquals("https://realm", parameters.getWsfedRealm()); } @Test public void testBasicChecks() throws Exception { WSFedProtocolParameters parameters = new WSFedProtocolParameters(); - parameters.setWsfed_action(WSFedConstants.WSFED_SIGNIN_ACTION); - parameters.setWsfed_realm("https://realm"); + parameters.setWsfedAction(WSFedConstants.WSFED_SIGNIN_ACTION); + parameters.setWsfedRealm("https://realm"); assertNull(service.basicChecks(parameters)); } @@ -255,16 +261,16 @@ public void testBasicChecks() throws Exception { public void testIsSignout() throws Exception { WSFedProtocolParameters params = new WSFedProtocolParameters(); - params.setWsfed_action(WSFedConstants.WSFED_SIGNOUT_ACTION); + params.setWsfedAction(WSFedConstants.WSFED_SIGNOUT_ACTION); assertTrue(service.isSignout(params)); - params.setWsfed_action(WSFedConstants.WSFED_SIGNOUT_CLEANUP_ACTION); + params.setWsfedAction(WSFedConstants.WSFED_SIGNOUT_CLEANUP_ACTION); assertTrue(service.isSignout(params)); - params.setWsfed_action(UserSessionModel.State.LOGGING_OUT.toString()); + params.setWsfedAction(UserSessionModel.State.LOGGING_OUT.toString()); assertTrue(service.isSignout(params)); - params.setWsfed_action(WSFedConstants.WSFED_SIGNIN_ACTION); + params.setWsfedAction(WSFedConstants.WSFED_SIGNIN_ACTION); assertFalse(service.isSignout(params)); } @@ -272,14 +278,14 @@ public void testIsSignout() throws Exception { public void testClientChecksSignout() throws Exception { WSFedProtocolParameters params = new WSFedProtocolParameters(); - params.setWsfed_action(WSFedConstants.WSFED_SIGNOUT_ACTION); + params.setWsfedAction(WSFedConstants.WSFED_SIGNOUT_ACTION); assertNull(service.clientChecks(mockHelper.getClient(), params)); } @Test public void testClientChecksNullClient() throws Exception { WSFedProtocolParameters params = new WSFedProtocolParameters(); - params.setWsfed_action(WSFedConstants.WSFED_SIGNIN_ACTION); + params.setWsfedAction(WSFedConstants.WSFED_SIGNIN_ACTION); assertNotNull(service.clientChecks(null, params)); assertErrorPage(mockHelper.getLoginFormsProvider(), Messages.UNKNOWN_LOGIN_REQUESTER); @@ -288,7 +294,7 @@ public void testClientChecksNullClient() throws Exception { @Test public void testClientChecksDisabledClient() throws Exception { WSFedProtocolParameters params = new WSFedProtocolParameters(); - params.setWsfed_action(WSFedConstants.WSFED_SIGNIN_ACTION); + params.setWsfedAction(WSFedConstants.WSFED_SIGNIN_ACTION); doReturn(false).when(mockHelper.getClient()).isEnabled(); assertNotNull(service.clientChecks(mockHelper.getClient(), params)); @@ -298,7 +304,7 @@ public void testClientChecksDisabledClient() throws Exception { @Test public void testClientChecksBearerOnlyClient() throws Exception { WSFedProtocolParameters params = new WSFedProtocolParameters(); - params.setWsfed_action(WSFedConstants.WSFED_SIGNIN_ACTION); + params.setWsfedAction(WSFedConstants.WSFED_SIGNIN_ACTION); doReturn(true).when(mockHelper.getClient()).isBearerOnly(); assertNotNull(service.clientChecks(mockHelper.getClient(), params)); @@ -308,7 +314,7 @@ public void testClientChecksBearerOnlyClient() throws Exception { @Test public void testClientChecks() throws Exception { WSFedProtocolParameters params = new WSFedProtocolParameters(); - params.setWsfed_action(WSFedConstants.WSFED_SIGNIN_ACTION); + params.setWsfedAction(WSFedConstants.WSFED_SIGNIN_ACTION); assertNull(service.clientChecks(mockHelper.getClient(), params)); verify(mockHelper.getSession().getContext(), times(1)).setClient(eq(mockHelper.getClient())); @@ -452,43 +458,43 @@ public void testHandleLogoutRequestNoClientOrReply() throws Exception { @Test public void testHandleLogoutRequestReplyAddress() throws Exception { WSFedProtocolParameters params = new WSFedProtocolParameters(); - params.setWsfed_reply("https://redirectUri"); - params.setWsfed_context("context"); + params.setWsfedReply("https://redirectUri"); + params.setWsfedContext("context"); - doReturn(new HashSet<>(Arrays.asList(params.getWsfed_reply()))).when(mockHelper.getClient()).getRedirectUris(); + doReturn(new HashSet<>(Arrays.asList(params.getWsfedReply()))).when(mockHelper.getClient()).getRedirectUris(); doReturn(null).when(service).authenticateIdentityCookie(); Response response = service.handleLogoutRequest(params, mockHelper.getClient()); Document doc = responseToDocument(response); - assertFormAction(doc, HttpMethod.GET, params.getWsfed_reply()); - assertInputNode(doc, WSFedConstants.WSFED_CONTEXT, params.getWsfed_context()); + assertFormAction(doc, HttpMethod.GET, params.getWsfedReply()); + assertInputNode(doc, WSFedConstants.WSFED_CONTEXT, params.getWsfedContext()); } //@Test public void testHandleLogoutRequestReplyAddressNoClient() throws Exception { WSFedProtocolParameters params = new WSFedProtocolParameters(); - params.setWsfed_reply("https://redirectUri"); - params.setWsfed_context("context"); + params.setWsfedReply("https://redirectUri"); + params.setWsfedContext("context"); - doReturn(new HashSet<>(Arrays.asList(params.getWsfed_reply()))).when(mockHelper.getClient()).getRedirectUris(); + doReturn(new HashSet<>(Arrays.asList(params.getWsfedReply()))).when(mockHelper.getClient()).getRedirectUris(); doReturn(null).when(service).authenticateIdentityCookie(); doReturn(Arrays.asList(mockHelper.getClient())).when(mockHelper.getRealm()).getClients(); Response response = service.handleLogoutRequest(params, null); Document doc = responseToDocument(response); - assertFormAction(doc, HttpMethod.GET, params.getWsfed_reply()); - assertInputNode(doc, WSFedConstants.WSFED_CONTEXT, params.getWsfed_context()); + assertFormAction(doc, HttpMethod.GET, params.getWsfedReply()); + assertInputNode(doc, WSFedConstants.WSFED_CONTEXT, params.getWsfedContext()); } @Test public void testHandleLogoutRequest() throws Exception { WSFedProtocolParameters params = new WSFedProtocolParameters(); - params.setWsfed_reply("https://redirectUri"); - params.setWsfed_context("context"); + params.setWsfedReply("https://redirectUri"); + params.setWsfedContext("context"); - doReturn(new HashSet<>(Arrays.asList(params.getWsfed_reply()))).when(mockHelper.getClient()).getRedirectUris(); + doReturn(new HashSet<>(Arrays.asList(params.getWsfedReply()))).when(mockHelper.getClient()).getRedirectUris(); //doReturn(Collections.singletonMap(getMockHelper().getClient().getId(),mockHelper.getClientSessionModel())).when(mockHelper.getUserSessionModel().getAuthenticatedClientSessions()); doReturn(mockHelper.getClient()).when(mockHelper.getClientSessionModel()).getClient(); @@ -500,8 +506,8 @@ public void testHandleLogoutRequest() throws Exception { catch(NullPointerException ex) { } - verify(mockHelper.getUserSessionModel(), times(1)).setNote(eq(WSFedLoginProtocol.WSFED_LOGOUT_BINDING_URI), eq(params.getWsfed_reply())); - verify(mockHelper.getUserSessionModel(), times(1)).setNote(eq(WSFedLoginProtocol.WSFED_CONTEXT), eq(params.getWsfed_context())); + verify(mockHelper.getUserSessionModel(), times(1)).setNote(eq(WSFedLoginProtocol.WSFED_LOGOUT_BINDING_URI), eq(params.getWsfedReply())); + verify(mockHelper.getUserSessionModel(), times(1)).setNote(eq(WSFedLoginProtocol.WSFED_CONTEXT), eq(params.getWsfedContext())); verify(mockHelper.getUserSessionModel(), times(1)).setNote(eq(AuthenticationManager.KEYCLOAK_LOGOUT_PROTOCOL), eq(WSFedLoginProtocol.LOGIN_PROTOCOL)); verify(mockHelper.getClientSessionModel(), times(1)).setAction(eq(AuthenticatedClientSessionModel.Action.LOGGED_OUT.name())); @@ -517,8 +523,8 @@ public void testHandleLogoutRequest() throws Exception { @Test public void testHandleLoginRequestInvalidRedirect() throws Exception { WSFedProtocolParameters params = new WSFedProtocolParameters(); - params.setWsfed_reply("https://redirectUri"); - params.setWsfed_context("context"); + params.setWsfedReply("https://redirectUri"); + params.setWsfedContext("context"); //doReturn(new HashSet<>(Arrays.asList(params.getWsfed_reply()))).when(mockHelper.getClient()).getRedirectUris(); assertNotNull(service.handleLoginRequest(params, mockHelper.getClient(), false)); @@ -528,10 +534,10 @@ public void testHandleLoginRequestInvalidRedirect() throws Exception { @Test public void testHandleLoginRequest() throws Exception { WSFedProtocolParameters params = new WSFedProtocolParameters(); - params.setWsfed_reply("https://redirectUri"); - params.setWsfed_context("context"); + params.setWsfedReply("https://redirectUri"); + params.setWsfedContext("context"); - doReturn(new HashSet<>(Arrays.asList(params.getWsfed_reply()))).when(mockHelper.getClient()).getRedirectUris(); + doReturn(new HashSet<>(Arrays.asList(params.getWsfedReply()))).when(mockHelper.getClient()).getRedirectUris(); AuthenticationFlowModel flow = mock(AuthenticationFlowModel.class); doReturn(UUID.randomUUID().toString()).when(flow).getId(); @@ -555,23 +561,24 @@ public void testHandleLoginRequest() throws Exception { verify(mockHelper.getLoginFormsProvider(), times(1)).createErrorPage(Response.Status.BAD_REQUEST); verify(mockHelper.getAuthSessionModel(), times(1)).setProtocol(eq(WSFedLoginProtocol.LOGIN_PROTOCOL)); - verify(mockHelper.getAuthSessionModel(), times(1)).setRedirectUri(eq(params.getWsfed_reply())); + verify(mockHelper.getAuthSessionModel(), times(1)).setRedirectUri(eq(params.getWsfedReply())); verify(mockHelper.getAuthSessionModel(), times(1)).setAction(eq(AuthenticationSessionModel.Action.AUTHENTICATE.name())); - verify(mockHelper.getAuthSessionModel(), times(1)).setClientNote(eq(WSFedConstants.WSFED_CONTEXT), eq(params.getWsfed_context())); + verify(mockHelper.getAuthSessionModel(), times(1)).setClientNote(eq(WSFedConstants.WSFED_CONTEXT), eq(params.getWsfedContext())); String issuer = RealmsResource.realmBaseUrl(mockHelper.getUriInfo()).build(mockHelper.getRealmName()).toString(); verify(mockHelper.getAuthSessionModel(), times(1)).setClientNote(eq(OIDCLoginProtocol.ISSUER), eq(issuer)); } + @Test @Ignore public void testHandleLoginRequestRedirectToIdentityProvider() throws Exception { WSFedProtocolParameters params = new WSFedProtocolParameters(); - params.setWsfed_reply("https://redirectUri"); - params.setWsfed_context("context"); + params.setWsfedReply("https://redirectUri"); + params.setWsfedContext("context"); // The home realm parameter 'whr' is used to log in using an identity provider - params.setWsfed_home_realm("dummyIdentityProvider"); + params.setWsfedHomeRealm("dummyIdentityProvider"); doReturn(identityProvider).when(mockHelper.getRealm()).getIdentityProviderByAlias("dummyIdentityProvider"); - doReturn(new HashSet<>(Arrays.asList(params.getWsfed_reply()))).when(mockHelper.getClient()).getRedirectUris(); + doReturn(new HashSet<>(Arrays.asList(params.getWsfedReply()))).when(mockHelper.getClient()).getRedirectUris(); AuthenticationFlowModel flow = mock(AuthenticationFlowModel.class); doReturn(UUID.randomUUID().toString()).when(flow).getId(); doReturn(flow).when(mockHelper.getRealm()).getBrowserFlow(); diff --git a/keycloak-wsfed-tests/src/test/java/com/quest/keycloak/protocol/wsfed/builders/WSFedProtocolParametersTest.java b/keycloak-wsfed-tests/src/test/java/com/quest/keycloak/protocol/wsfed/builders/WSFedProtocolParametersTest.java index 6d1e728..dc4cc35 100644 --- a/keycloak-wsfed-tests/src/test/java/com/quest/keycloak/protocol/wsfed/builders/WSFedProtocolParametersTest.java +++ b/keycloak-wsfed-tests/src/test/java/com/quest/keycloak/protocol/wsfed/builders/WSFedProtocolParametersTest.java @@ -48,79 +48,79 @@ public void testFromParameters() { requestParams.add(WSFedConstants.WSFED_RESULT_URL, "resurl"); WSFedProtocolParameters params = WSFedProtocolParameters.fromParameters(requestParams); - assertEquals(requestParams.getFirst(WSFedConstants.WSFED_ACTION), params.getWsfed_action()); - assertEquals(requestParams.getFirst(WSFedConstants.WSFED_REPLY), params.getWsfed_reply()); - assertEquals(requestParams.getFirst(WSFedConstants.WSFED_CONTEXT), params.getWsfed_context()); - assertEquals(requestParams.getFirst(WSFedConstants.WSFED_POLICY), params.getWsfed_policy()); - assertEquals(requestParams.getFirst(WSFedConstants.WSFED_CURRENT_TIME), params.getWsfed_current_time()); - assertEquals(requestParams.getFirst(WSFedConstants.WSFED_FEDERATION_ID), params.getWsfed_federation_id()); - assertEquals(requestParams.getFirst(WSFedConstants.WSFED_ENCODING), params.getWsfed_encoding()); - assertEquals(requestParams.getFirst(WSFedConstants.WSFED_REALM), params.getWsfed_realm()); - assertEquals(requestParams.getFirst(WSFedConstants.WSFED_FRESHNESS), params.getWsfed_freshness()); - assertEquals(requestParams.getFirst(WSFedConstants.WSFED_AUTHENTICATION_LEVEL), params.getWsfed_authentication_level()); - assertEquals(requestParams.getFirst(WSFedConstants.WSFED_TOKEN_REQUEST_TYPE), params.getWsfed_token_request_type()); - assertEquals(requestParams.getFirst(WSFedConstants.WSFED_HOME_REALM), params.getWsfed_home_realm()); - assertEquals(requestParams.getFirst(WSFedConstants.WSFED_REQUEST_URL), params.getWsfed_request_url()); - assertEquals(requestParams.getFirst(WSFedConstants.WSFED_RESULT), params.getWsfed_result()); - assertEquals(requestParams.getFirst(WSFedConstants.WSFED_RESULT_URL), params.getWsfed_result_url()); + assertEquals(requestParams.getFirst(WSFedConstants.WSFED_ACTION), params.getWsfedAction()); + assertEquals(requestParams.getFirst(WSFedConstants.WSFED_REPLY), params.getWsfedReply()); + assertEquals(requestParams.getFirst(WSFedConstants.WSFED_CONTEXT), params.getWsfedContext()); + assertEquals(requestParams.getFirst(WSFedConstants.WSFED_POLICY), params.getWsfedPolicy()); + assertEquals(requestParams.getFirst(WSFedConstants.WSFED_CURRENT_TIME), params.getWsfedCurrentTime()); + assertEquals(requestParams.getFirst(WSFedConstants.WSFED_FEDERATION_ID), params.getWsfedFederationId()); + assertEquals(requestParams.getFirst(WSFedConstants.WSFED_ENCODING), params.getWsfedEncoding()); + assertEquals(requestParams.getFirst(WSFedConstants.WSFED_REALM), params.getWsfedRealm()); + assertEquals(requestParams.getFirst(WSFedConstants.WSFED_FRESHNESS), params.getWsfedFreshness()); + assertEquals(requestParams.getFirst(WSFedConstants.WSFED_AUTHENTICATION_LEVEL), params.getWsfedAuthenticationLevel()); + assertEquals(requestParams.getFirst(WSFedConstants.WSFED_TOKEN_REQUEST_TYPE), params.getWsfedTokenRequestType()); + assertEquals(requestParams.getFirst(WSFedConstants.WSFED_HOME_REALM), params.getWsfedHomeRealm()); + assertEquals(requestParams.getFirst(WSFedConstants.WSFED_REQUEST_URL), params.getWsfedRequestUrl()); + assertEquals(requestParams.getFirst(WSFedConstants.WSFED_RESULT), params.getWsfedResult()); + assertEquals(requestParams.getFirst(WSFedConstants.WSFED_RESULT_URL), params.getWsfedResultUrl()); } @Test public void testFromParametersNull() { MultivaluedMap requestParams = new MultivaluedMapImpl<>(); WSFedProtocolParameters params = WSFedProtocolParameters.fromParameters(requestParams); - assertNull(params.getWsfed_action()); - assertNull(params.getWsfed_reply()); - assertNull(params.getWsfed_context()); - assertNull(params.getWsfed_policy()); - assertNull(params.getWsfed_current_time()); - assertNull(params.getWsfed_federation_id()); - assertNull(params.getWsfed_encoding()); - assertNull(params.getWsfed_realm()); - assertNull(params.getWsfed_freshness()); - assertNull(params.getWsfed_authentication_level()); - assertNull(params.getWsfed_token_request_type()); - assertNull(params.getWsfed_home_realm()); - assertNull(params.getWsfed_request_url()); - assertNull(params.getWsfed_result()); - assertNull(params.getWsfed_result_url()); + assertNull(params.getWsfedAction()); + assertNull(params.getWsfedReply()); + assertNull(params.getWsfedContext()); + assertNull(params.getWsfedPolicy()); + assertNull(params.getWsfedCurrentTime()); + assertNull(params.getWsfedFederationId()); + assertNull(params.getWsfedEncoding()); + assertNull(params.getWsfedRealm()); + assertNull(params.getWsfedFreshness()); + assertNull(params.getWsfedAuthenticationLevel()); + assertNull(params.getWsfedTokenRequestType()); + assertNull(params.getWsfedHomeRealm()); + assertNull(params.getWsfedRequestUrl()); + assertNull(params.getWsfedResult()); + assertNull(params.getWsfedResultUrl()); } @Test public void testSetters() { WSFedProtocolParameters params = new WSFedProtocolParameters(); - params.setWsfed_action("action"); - params.setWsfed_reply("reply"); - params.setWsfed_context("context"); - params.setWsfed_policy("policy"); - params.setWsfed_current_time("time"); - params.setWsfed_federation_id("fedid"); - params.setWsfed_encoding("encoding"); - params.setWsfed_realm("realm"); - params.setWsfed_freshness("freshness"); - params.setWsfed_authentication_level("authlevel"); - params.setWsfed_token_request_type("trt"); - params.setWsfed_home_realm("homerealm"); - params.setWsfed_request_url("req"); - params.setWsfed_result("res"); - params.setWsfed_result_url("resurl"); + params.setWsfedAction("action"); + params.setWsfedReply("reply"); + params.setWsfedContext("context"); + params.setWsfedPolicy("policy"); + params.setWsfedCurrentTime("time"); + params.setWsfedFederationId("fedid"); + params.setWsfedEncoding("encoding"); + params.setWsfedRealm("realm"); + params.setWsfedFreshness("freshness"); + params.setWsfedAuthenticationLevel("authlevel"); + params.setWsfedTokenRequestType("trt"); + params.setWsfedHomeRealm("homerealm"); + params.setWsfedRequestUrl("req"); + params.setWsfedResult("res"); + params.setWsfedResultUrl("resurl"); - assertEquals("action", params.getWsfed_action()); - assertEquals("reply", params.getWsfed_reply()); - assertEquals("context", params.getWsfed_context()); - assertEquals("policy", params.getWsfed_policy()); - assertEquals("time", params.getWsfed_current_time()); - assertEquals("fedid", params.getWsfed_federation_id()); - assertEquals("encoding", params.getWsfed_encoding()); - assertEquals("realm", params.getWsfed_realm()); - assertEquals("freshness", params.getWsfed_freshness()); - assertEquals("authlevel", params.getWsfed_authentication_level()); - assertEquals("trt", params.getWsfed_token_request_type()); - assertEquals("homerealm", params.getWsfed_home_realm()); - assertEquals("req", params.getWsfed_request_url()); - assertEquals("res", params.getWsfed_result()); - assertEquals("resurl", params.getWsfed_result_url()); + assertEquals("action", params.getWsfedAction()); + assertEquals("reply", params.getWsfedReply()); + assertEquals("context", params.getWsfedContext()); + assertEquals("policy", params.getWsfedPolicy()); + assertEquals("time", params.getWsfedCurrentTime()); + assertEquals("fedid", params.getWsfedFederationId()); + assertEquals("encoding", params.getWsfedEncoding()); + assertEquals("realm", params.getWsfedRealm()); + assertEquals("freshness", params.getWsfedFreshness()); + assertEquals("authlevel", params.getWsfedAuthenticationLevel()); + assertEquals("trt", params.getWsfedTokenRequestType()); + assertEquals("homerealm", params.getWsfedHomeRealm()); + assertEquals("req", params.getWsfedRequestUrl()); + assertEquals("res", params.getWsfedResult()); + assertEquals("resurl", params.getWsfedResultUrl()); } } \ No newline at end of file diff --git a/keycloak-wsfed-tests/src/test/java/com/quest/keycloak/protocol/wsfed/builders/WsFedSAML11AssertionTypeBuilderTest.java b/keycloak-wsfed-tests/src/test/java/com/quest/keycloak/protocol/wsfed/builders/WsFedSAML11AssertionTypeBuilderTest.java index b2c97c0..8c87886 100644 --- a/keycloak-wsfed-tests/src/test/java/com/quest/keycloak/protocol/wsfed/builders/WsFedSAML11AssertionTypeBuilderTest.java +++ b/keycloak-wsfed-tests/src/test/java/com/quest/keycloak/protocol/wsfed/builders/WsFedSAML11AssertionTypeBuilderTest.java @@ -20,16 +20,21 @@ import com.quest.keycloak.common.wsfed.MockHelper; import com.quest.keycloak.common.wsfed.TestHelpers; -import com.quest.keycloak.protocol.wsfed.mappers.*; -import org.apache.http.client.methods.CloseableHttpResponse; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClients; -import org.apache.http.util.EntityUtils; +import com.quest.keycloak.protocol.wsfed.mappers.SAMLGroupMembershipMapper; +import com.quest.keycloak.protocol.wsfed.mappers.SAMLRoleListMapper; +import com.quest.keycloak.protocol.wsfed.mappers.SAMLScriptBasedMapper; +import com.quest.keycloak.protocol.wsfed.mappers.SAMLUserAttributeStatementMapper; +import com.quest.keycloak.protocol.wsfed.mappers.SAMLUserPropertyAttributeStatementMapper; +import com.quest.keycloak.protocol.wsfed.mappers.WSFedSAMLAttributeStatementMapper; +import com.quest.keycloak.protocol.wsfed.mappers.WSFedSAMLRoleListMapper; import org.junit.Before; import org.junit.Ignore; import org.junit.Test; -import org.keycloak.dom.saml.v1.assertion.*; +import org.keycloak.dom.saml.v1.assertion.SAML11AssertionType; +import org.keycloak.dom.saml.v1.assertion.SAML11AttributeStatementType; +import org.keycloak.dom.saml.v1.assertion.SAML11AttributeType; +import org.keycloak.dom.saml.v1.assertion.SAML11AudienceRestrictionCondition; +import org.keycloak.dom.saml.v1.assertion.SAML11AuthenticationStatementType; import org.keycloak.dom.saml.v2.assertion.AttributeStatementType; import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.ProtocolMapperModel; @@ -39,10 +44,11 @@ import org.keycloak.saml.processing.core.saml.v2.util.XMLTimeUtil; import org.mockito.MockitoAnnotations; -import java.io.IOException; import java.net.URI; -import java.nio.charset.StandardCharsets; -import java.util.*; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.UUID; import java.util.stream.Collectors; import static com.quest.keycloak.protocol.wsfed.builders.SAML11AssertionTypeBuilder.CLOCK_SKEW; diff --git a/keycloak-wsfed-tests/src/test/resources/samples/complete-computed-key.xml b/keycloak-wsfed-tests/src/test/resources/samples/complete-computed-key.xml new file mode 100644 index 0000000..4016c6a --- /dev/null +++ b/keycloak-wsfed-tests/src/test/resources/samples/complete-computed-key.xml @@ -0,0 +1,28 @@ + + + http://uri/request-type + http://uri/token-type + http://uri/key-type + 256 + + 2002-05-30T09:00:00 + 2020-09-30T18:45:00 + + + username + + + + ABCDEFGH + + + Algorithm + + + + + + \ No newline at end of file diff --git a/keycloak-wsfed-tests/src/test/resources/samples/complete.xml b/keycloak-wsfed-tests/src/test/resources/samples/complete.xml new file mode 100644 index 0000000..f8f68bb --- /dev/null +++ b/keycloak-wsfed-tests/src/test/resources/samples/complete.xml @@ -0,0 +1,32 @@ + + + http://uri/request-type + http://uri/token-type + http://uri/key-type + 256 + + 2002-05-30T09:00:00 + 2020-09-30T18:45:00 + + + username + + + + ABCDEFGH + + + STUVWXYZ + + + + + + + + status.codestatus.reason + + + \ No newline at end of file diff --git a/keycloak-wsfed-tests/src/test/resources/samples/dummy.xml b/keycloak-wsfed-tests/src/test/resources/samples/dummy.xml new file mode 100644 index 0000000..799990e --- /dev/null +++ b/keycloak-wsfed-tests/src/test/resources/samples/dummy.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/keycloak-wsfed-tests/src/test/resources/samples/empty.xml b/keycloak-wsfed-tests/src/test/resources/samples/empty.xml new file mode 100644 index 0000000..e69de29 diff --git a/keycloak-wsfed-tests/src/test/resources/samples/invalid-collection.xml b/keycloak-wsfed-tests/src/test/resources/samples/invalid-collection.xml new file mode 100644 index 0000000..a02d01e --- /dev/null +++ b/keycloak-wsfed-tests/src/test/resources/samples/invalid-collection.xml @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/keycloak-wsfed-tests/src/test/resources/samples/invalid-request-type.xml b/keycloak-wsfed-tests/src/test/resources/samples/invalid-request-type.xml new file mode 100644 index 0000000..754637a --- /dev/null +++ b/keycloak-wsfed-tests/src/test/resources/samples/invalid-request-type.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/keycloak-wsfed-tests/src/test/resources/samples/minimum.xml b/keycloak-wsfed-tests/src/test/resources/samples/minimum.xml new file mode 100644 index 0000000..c438e64 --- /dev/null +++ b/keycloak-wsfed-tests/src/test/resources/samples/minimum.xml @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/keycloak-wsfed-tests/src/test/resources/samples/unknown-tag.xml b/keycloak-wsfed-tests/src/test/resources/samples/unknown-tag.xml new file mode 100644 index 0000000..e0407ea --- /dev/null +++ b/keycloak-wsfed-tests/src/test/resources/samples/unknown-tag.xml @@ -0,0 +1,11 @@ + + + http://uri/request-type + http://uri/token-type + http://uri/key-type + http://uri/unknown-tag + + \ No newline at end of file diff --git a/keycloak-wsfed-tests/src/test/resources/samples/without-context.xml b/keycloak-wsfed-tests/src/test/resources/samples/without-context.xml new file mode 100644 index 0000000..b3ed04e --- /dev/null +++ b/keycloak-wsfed-tests/src/test/resources/samples/without-context.xml @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/keycloak-wsfed/assembly.xml b/keycloak-wsfed/assembly.xml new file mode 100644 index 0000000..9a87280 --- /dev/null +++ b/keycloak-wsfed/assembly.xml @@ -0,0 +1,33 @@ + + dist + + tar.gz + + ${project.name} + + + install.sh + 0755 + + + module.xml + + + + + ${project.basedir} + + lib-modules/** + + + + ${project.build.directory} + target + + ${project.build.finalName}.jar + + + + diff --git a/keycloak-wsfed/install.sh b/keycloak-wsfed/install.sh new file mode 100644 index 0000000..a94b9a1 --- /dev/null +++ b/keycloak-wsfed/install.sh @@ -0,0 +1,166 @@ +#!/bin/bash +# install.sh +# +# install keycloak module : +# keycloak-wsfed + +set -eE +MODULE_DIR=$(dirname $0) +TARGET_DIR=${MODULE_DIR}/target +LIBRARY_DIR=${MODULE_DIR}/lib-modules + +usage () +{ + echo "usage: $0 keycloak_path [-c]" +} + + +init() +{ + # deps + [[ $(xmlstarlet --version) ]] || { echo >&2 "Requires xmlstarlet"; exit 1; } + + #optional args + argv__CLUSTER=0; + argv__UNINSTALL=0 + getopt_results=$(getopt -s bash -o cu --long cluster,uninstall -- "$@") + + if test $? != 0 + then + echo "unrecognized option" + exit 1 + fi + eval set -- "$getopt_results" + + while true + do + case "$1" in + -u|--uninstall) + argv__UNINSTALL=1 + echo "--delete set. will remove plugin" + shift + ;; + -c|--cluster) + argv__CLUSTER=1 + echo "--cluster set. Will edit cluster config" + shift + ;; + --) + shift + break + ;; + *) + EXCEPTION=$Main__ParameterException + EXCEPTION_MSG="unparseable option $1" + exit 1 + ;; + esac + done + + # positional args + argv__KEYCLOAK="" + if [[ "$#" -ne 1 ]]; then + usage + exit 1 + fi + argv__KEYCLOAK="$1" + # optional args + CONF_FILE="" + if [[ "$argv__CLUSTER" -eq 1 ]]; then + CONF_FILE=$argv__KEYCLOAK/standalone/configuration/standalone-ha.xml + else + CONF_FILE=$argv__KEYCLOAK/standalone/configuration/standalone.xml + fi + echo $CONF_FILE + MODULE_NAME=$(xmlstarlet sel -N oe="urn:jboss:module:1.3" -t -v '/oe:module/@name' -n $MODULE_DIR/module.xml) + MODULE=${MODULE_NAME##*.} + JAR_PATH=`find $TARGET_DIR/ -type f -name "*.jar" -not -name "*sources.jar" | grep -v "archive-tmp"` + JAR_NAME=`basename $JAR_PATH` + MODULE_PATH=${MODULE_NAME//./\/}/main +} + +init_exceptions() +{ + EXCEPTION=0 + EXCEPTION_MSG="" + #Main__Default_Unkown=1 + Main__ParameterException=2 +} + +cleanup() +{ + #clean dir structure in case of script failure + echo "cleanup..." + xmlstarlet ed -L -N c="urn:jboss:domain:keycloak-server:1.1" -d "/_:server/_:profile/c:subsystem/c:providers/c:provider[text()='module:$MODULE_NAME']" $CONF_FILE + xmlstarlet ed -L -N c="urn:jboss:domain:keycloak-server:1.1" -d "/_:server/_:profile/c:subsystem/c:theme/c:modules/c:module[text()='$MODULE_NAME']" $CONF_FILE + sed -i "$ s/,$MODULE$//" $argv__KEYCLOAK/modules/layers.conf + sed -i "$ s/\([=,]\)$MODULE,/\1/" $argv__KEYCLOAK/modules/layers.conf + rm -rf $argv__KEYCLOAK/modules/system/layers/$MODULE + echo "done" +} +copy_lib() +{ + LIB_PATH=$1 + LIB_NAME=$2 + mkdir -p $argv__KEYCLOAK/modules/system/layers/$MODULE/${LIB_PATH}/main + cp ${LIBRARY_DIR}/${LIB_NAME}/* $argv__KEYCLOAK/modules/system/layers/$MODULE/${LIB_PATH}/main +} + +Main__interruptHandler() +{ + # @description signal handler for SIGINT + echo "$0: SIGINT caught" + exit +} +Main__terminationHandler() +{ + # @description signal handler for SIGTERM + echo "$0: SIGTERM caught" + exit +} +Main__exitHandler() +{ + cleanup + if [[ "$EXCEPTION" -ne 0 ]] ; then + echo "$0: error : ${EXCEPTION_MSG}" + fi + exit +} + +trap Main__interruptHandler INT +trap Main__terminationHandler TERM +trap Main__exitHandler ERR + +Main__main() +{ + # init scipt temporals + init_exceptions + init "$@" + if [[ "$argv__UNINSTALL" -eq 1 ]]; then + cleanup + exit 0 + fi + # install module + mkdir -p $argv__KEYCLOAK/modules/system/layers/$MODULE/$MODULE_PATH/ + cp $JAR_PATH $argv__KEYCLOAK/modules/system/layers/$MODULE/$MODULE_PATH/ + cp $MODULE_DIR/module.xml $argv__KEYCLOAK/modules/system/layers/$MODULE/$MODULE_PATH/ + sed -i "s@JAR_NAME@${JAR_NAME}@g" $argv__KEYCLOAK/modules/system/layers/$MODULE/$MODULE_PATH/module.xml + if ! grep -q "$MODULE" "$argv__KEYCLOAK/modules/layers.conf"; then + sed -i "$ s/$/,$MODULE/" $argv__KEYCLOAK/modules/layers.conf + fi + # FIXME make this reentrant then test + xmlstarlet ed -L -N c="urn:jboss:domain:keycloak-server:1.1" -s /_:server/_:profile/c:subsystem/c:providers -t elem -n provider -v "module:$MODULE_NAME" $CONF_FILE + + MODULES_EXISTS=`xmlstarlet sel -N c="urn:jboss:domain:keycloak-server:1.1" -t -v "count(/_:server/_:profile/c:subsystem/c:theme/c:modules/c:module)" $CONF_FILE` + if [ $MODULES_EXISTS -eq "0" ]; then + xmlstarlet ed -L -N c="urn:jboss:domain:keycloak-server:1.1" -d /_:server/_:profile/c:subsystem/c:theme/c:modules $CONF_FILE + xmlstarlet ed -L -N c="urn:jboss:domain:keycloak-server:1.1" -s /_:server/_:profile/c:subsystem/c:theme -t elem -n modules $CONF_FILE + fi + xmlstarlet ed -L -N c="urn:jboss:domain:keycloak-server:1.1" -s /_:server/_:profile/c:subsystem/c:theme/c:modules -t elem -n module -v "$MODULE_NAME" $CONF_FILE + + #TODO add command to create module in theme if it doesn't already exist. + + exit 0 +} + +Main__main "$@" diff --git a/keycloak-wsfed/module.xml b/keycloak-wsfed/module.xml index b752732..556ab2a 100644 --- a/keycloak-wsfed/module.xml +++ b/keycloak-wsfed/module.xml @@ -3,7 +3,7 @@ - + diff --git a/keycloak-wsfed/pom.xml b/keycloak-wsfed/pom.xml index eefefb2..947a9a2 100644 --- a/keycloak-wsfed/pom.xml +++ b/keycloak-wsfed/pom.xml @@ -36,18 +36,27 @@ org.keycloak keycloak-server-spi + provided org.keycloak keycloak-server-spi-private + provided org.keycloak keycloak-saml-core + provided org.keycloak keycloak-services + provided + + + + org.apache.commons + commons-lang3 @@ -55,13 +64,11 @@ org.picketlink picketlink-federation ${picketlink.version} - provided org.picketlink picketlink-common ${picketlink.version} - provided @@ -71,6 +78,25 @@ org.apache.maven.plugins maven-compiler-plugin + + maven-assembly-plugin + + + default-package + package + + single + + + + + false + + assembly.xml + + posix + + diff --git a/keycloak-wsfed/src/main/java/com/quest/keycloak/broker/wsfed/RequestedToken.java b/keycloak-wsfed/src/main/java/com/quest/keycloak/broker/wsfed/RequestedToken.java index 9550377..a2d0a12 100644 --- a/keycloak-wsfed/src/main/java/com/quest/keycloak/broker/wsfed/RequestedToken.java +++ b/keycloak-wsfed/src/main/java/com/quest/keycloak/broker/wsfed/RequestedToken.java @@ -23,6 +23,8 @@ import org.w3c.dom.Document; import org.w3c.dom.Node; import org.w3c.dom.NodeList; + +import javax.xml.XMLConstants; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import org.xml.sax.InputSource; @@ -56,10 +58,18 @@ public interface RequestedToken { String getLastName(); - default Document createXmlDocument(String response) throws ProcessingException, ParserConfigurationException { + default DocumentBuilder newDocumentBuilder() throws ParserConfigurationException { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setNamespaceAware(true); - DocumentBuilder builder = factory.newDocumentBuilder(); + // Prevent DOS attack + factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); + // Prevent XXE attack + factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + return factory.newDocumentBuilder(); + } + + default Document createXmlDocument(String response) throws ProcessingException, ParserConfigurationException { + DocumentBuilder builder = newDocumentBuilder(); InputSource source = new InputSource(); source.setCharacterStream(new StringReader(response)); @@ -68,19 +78,17 @@ default Document createXmlDocument(String response) throws ProcessingException, JAXPValidationUtil.checkSchemaValidation(document); return document; } catch (SAXException | IOException e) { - throw new ProcessingException("Error while extracting SAML from WSFed response."); + throw new ProcessingException("Error while extracting SAML from WSFed response.", e); } } default Document extractSamlDocument(Document document) throws ProcessingException, XPathExpressionException { try { - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - factory.setNamespaceAware(true); XPath xpath = XPathFactory.newInstance().newXPath(); XPathExpression xPathExpression = xpath.compile("//*[local-name() = 'Assertion']"); NodeList samlNodes = (NodeList) xPathExpression.evaluate(document, XPathConstants.NODESET); - Document samlDoc = factory.newDocumentBuilder().newDocument(); + Document samlDoc = newDocumentBuilder().newDocument(); for (int i = 0; i < samlNodes.getLength(); i++) { Node node = samlNodes.item(i); Node copyNode = samlDoc.importNode(node, true); @@ -88,7 +96,7 @@ default Document extractSamlDocument(Document document) throws ProcessingExcepti } return samlDoc; } catch (XPathExpressionException | ParserConfigurationException e) { - throw new ProcessingException("Error while extracting SAML Assertion from WSFed XML document."); + throw new ProcessingException("Error while extracting SAML Assertion from WSFed XML document.", e); } } diff --git a/keycloak-wsfed/src/main/java/com/quest/keycloak/broker/wsfed/SAML11RequestedToken.java b/keycloak-wsfed/src/main/java/com/quest/keycloak/broker/wsfed/SAML11RequestedToken.java index cf7778d..e517229 100644 --- a/keycloak-wsfed/src/main/java/com/quest/keycloak/broker/wsfed/SAML11RequestedToken.java +++ b/keycloak-wsfed/src/main/java/com/quest/keycloak/broker/wsfed/SAML11RequestedToken.java @@ -26,7 +26,9 @@ import org.keycloak.events.EventType; import org.keycloak.models.KeycloakSession; import org.keycloak.saml.common.constants.JBossSAMLURIConstants; +import org.keycloak.saml.common.exceptions.ConfigurationException; import org.keycloak.saml.common.exceptions.ParsingException; +import org.keycloak.saml.common.exceptions.ProcessingException; import org.keycloak.saml.common.util.DocumentUtil; import org.keycloak.saml.processing.core.parsers.saml.SAMLParser; import org.keycloak.saml.processing.core.saml.v2.util.AssertionUtil; @@ -37,13 +39,20 @@ import org.w3c.dom.Node; import javax.ws.rs.core.Response; +import javax.xml.datatype.DatatypeConfigurationException; import javax.xml.datatype.DatatypeFactory; import javax.xml.datatype.XMLGregorianCalendar; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.xpath.XPathExpressionException; + import java.io.ByteArrayInputStream; import java.io.IOException; import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; import java.security.PublicKey; import java.util.List; +import java.util.Objects; /** * The purpose of this class is to handle a SAML 1.1 token sent back from an external IdP. It has methods both to verify @@ -73,7 +82,6 @@ public SAML11RequestedToken(String wsfedResponse, Object token) throws IOExcepti this.samlAssertion = getAssertionType(token); } - public static boolean isSignatureValid(Element assertionElement, PublicKey publicKey) { try { Document doc = DocumentUtil.createDocument(); @@ -81,7 +89,7 @@ public static boolean isSignatureValid(Element assertionElement, PublicKey publi doc.appendChild(n); return new SAML11Signature().validate(doc, publicKey); - } catch (Exception e) { + } catch (ConfigurationException | ProcessingException e) { logger.error("Cannot validate signature of assertion", e); } return false; @@ -114,7 +122,7 @@ public Response validate(PublicKey key, WSFedIdentityProviderConfig config, Even return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_FEDERATED_IDENTITY_ACTION); } - } catch (Exception e) { + } catch (GeneralSecurityException | DatatypeConfigurationException | XPathExpressionException | ParserConfigurationException e) { logger.error("Unable to validate signature", e); event.event(EventType.IDENTITY_PROVIDER_RESPONSE); event.error(Errors.INVALID_SAML_RESPONSE); @@ -207,32 +215,38 @@ private String getSubjectOrNameIdentifier() { if (samlAssertion.getStatements().isEmpty()) { return null; } - for (SAML11StatementAbstractType st : samlAssertion.getStatements()) { - if (st instanceof SAML11SubjectStatementType) { - SAML11SubjectStatementType subjectStatement = (SAML11SubjectStatementType) st; - SAML11SubjectType subject = subjectStatement.getSubject(); - //first attempts to get subject - if (subject != null && subject.getChoice() != null) { - SAML11SubjectType.SAML11SubjectTypeChoice choice = subject.getChoice(); - if (choice.getNameID() != null) { - String nameId = choice.getNameID().getValue(); - if (nameId != null && !nameId.isEmpty()) { - return nameId; - } - } - } - // The "nameidentifier" is a unique user id. - if (subjectStatement instanceof SAML11AttributeStatementType) { - SAML11AttributeStatementType attributeStatement = (SAML11AttributeStatementType) subjectStatement; - for (SAML11AttributeType attribute : attributeStatement.get()) { - if (!attribute.get().isEmpty() && ("nameidentifier".equalsIgnoreCase(attribute.getAttributeName()) - || JBossSAMLURIConstants.CLAIMS_NAME_IDENTIFIER.get().equalsIgnoreCase(attribute.getAttributeName()))) { - return attribute.get().get(0).toString(); - } - } + return samlAssertion.getStatements().stream() + .filter(st -> st instanceof SAML11SubjectStatementType) + .map(this::getSubjectOrNameFromSubjectStatement) + .filter(Objects::nonNull) + .findFirst() + .orElse(null); + } + + private String getSubjectOrNameFromSubjectStatement(SAML11StatementAbstractType statement) { + SAML11SubjectStatementType st = (SAML11SubjectStatementType) statement; + SAML11SubjectType subject = st.getSubject(); + //first attempts to get subject + if (subject != null && subject.getChoice() != null) { + SAML11SubjectType.SAML11SubjectTypeChoice choice = subject.getChoice(); + if (choice.getNameID() != null) { + String nameId = choice.getNameID().getValue(); + if (nameId != null && !nameId.isEmpty()) { + return nameId; } } } + // The "nameidentifier" is a unique user id. + if (!(st instanceof SAML11AttributeStatementType)) { + return null; + } + SAML11AttributeStatementType attributeStatement = (SAML11AttributeStatementType) st; + for (SAML11AttributeType attribute : attributeStatement.get()) { + if (!attribute.get().isEmpty() && ("nameidentifier".equalsIgnoreCase(attribute.getAttributeName()) + || JBossSAMLURIConstants.CLAIMS_NAME_IDENTIFIER.get().equalsIgnoreCase(attribute.getAttributeName()))) { + return attribute.get().get(0).toString(); + } + } return null; } @@ -278,14 +292,11 @@ private boolean isValidAudienceRestriction(URI... uris) { } private List getAudienceRestrictions() { - SAML11ConditionsType conditions = samlAssertion.getConditions(); - for (SAML11ConditionAbstractType condition : conditions.get()) { - if (condition instanceof SAML11AudienceRestrictionCondition) { - return ((SAML11AudienceRestrictionCondition) condition).get(); - } - } - - return null; + return samlAssertion.getConditions().get().stream() + .filter(c -> c instanceof SAML11AudienceRestrictionCondition) + .map(c -> ((SAML11AudienceRestrictionCondition)c).get()) + .findFirst() + .orElse(null); } /** @@ -300,7 +311,7 @@ private SAML11AssertionType getAssertionType(Object token) throws IOException, P SAML11AssertionType assertionType = null; String assertionXml = DocumentUtil.asString(((Element) token).getOwnerDocument()); - try (ByteArrayInputStream bis = new ByteArrayInputStream(assertionXml.getBytes())) { + try (ByteArrayInputStream bis = new ByteArrayInputStream(assertionXml.getBytes(StandardCharsets.UTF_8))) { SAMLParser parser = SAMLParser.getInstance(); Object assertion = parser.parse(bis); diff --git a/keycloak-wsfed/src/main/java/com/quest/keycloak/broker/wsfed/SAML2RequestedToken.java b/keycloak-wsfed/src/main/java/com/quest/keycloak/broker/wsfed/SAML2RequestedToken.java index b166d84..680f737 100644 --- a/keycloak-wsfed/src/main/java/com/quest/keycloak/broker/wsfed/SAML2RequestedToken.java +++ b/keycloak-wsfed/src/main/java/com/quest/keycloak/broker/wsfed/SAML2RequestedToken.java @@ -23,7 +23,6 @@ import org.keycloak.dom.saml.v2.assertion.AttributeStatementType; import org.keycloak.dom.saml.v2.assertion.AttributeType; import org.keycloak.dom.saml.v2.assertion.AudienceRestrictionType; -import org.keycloak.dom.saml.v2.assertion.ConditionAbstractType; import org.keycloak.dom.saml.v2.assertion.EncryptedAssertionType; import org.keycloak.dom.saml.v2.assertion.NameIDType; import org.keycloak.dom.saml.v2.assertion.SubjectType; @@ -52,12 +51,18 @@ import org.w3c.dom.Node; import javax.ws.rs.core.Response; +import javax.xml.datatype.DatatypeConfigurationException; import javax.xml.datatype.DatatypeFactory; import javax.xml.datatype.XMLGregorianCalendar; import javax.xml.namespace.QName; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.xpath.XPathExpressionException; + import java.io.ByteArrayInputStream; import java.io.IOException; import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; import java.security.PrivateKey; import java.security.PublicKey; import java.util.List; @@ -66,8 +71,9 @@ * Created on 5/15/15. */ public class SAML2RequestedToken implements RequestedToken { - private NameIDType subjectNameID; protected static final Logger logger = Logger.getLogger(SAML2RequestedToken.class); + + private NameIDType subjectNameID; private AssertionType saml2Assertion; private String wsfedResponse; private KeycloakSession session; @@ -106,8 +112,7 @@ public Response validate(PublicKey key, WSFedIdentityProviderConfig config, Even event.error(Errors.INVALID_SAML_RESPONSE); return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_FEDERATED_IDENTITY_ACTION); } - - } catch (Exception e) { + } catch (GeneralSecurityException | DatatypeConfigurationException | XPathExpressionException | ParserConfigurationException e) { logger.error("Unable to validate signature", e); event.event(EventType.IDENTITY_PROVIDER_RESPONSE); event.error(Errors.INVALID_SAML_RESPONSE); @@ -117,7 +122,7 @@ public Response validate(PublicKey key, WSFedIdentityProviderConfig config, Even return null; } - public boolean isValidAudienceRestriction(URI...uris) { + protected boolean isValidAudienceRestriction(URI... uris) { List audienceRestriction = getAudienceRestrictions(); if(audienceRestriction == null) { @@ -134,14 +139,11 @@ public boolean isValidAudienceRestriction(URI...uris) { } public List getAudienceRestrictions() { - List conditions = saml2Assertion.getConditions().getConditions(); - for(ConditionAbstractType condition : conditions) { - if(condition instanceof AudienceRestrictionType) { - return ((AudienceRestrictionType) condition).getAudience(); - } - } - - return null; + return saml2Assertion.getConditions().getConditions().stream() + .filter(c -> c instanceof AudienceRestrictionType) + .map(c -> ((AudienceRestrictionType)c).getAudience()) + .findFirst() + .orElse(null); } protected NameIDType getSubjectNameID() { @@ -185,12 +187,11 @@ public String getEmail() { for (AttributeStatementType attrStatement : saml2Assertion.getAttributeStatements()) { for (AttributeStatementType.ASTChoiceType choice : attrStatement.getAttributes()) { AttributeType attribute = choice.getAttribute(); - if (X500SAMLProfileConstants.EMAIL.getFriendlyName().equals(attribute.getFriendlyName()) - || X500SAMLProfileConstants.EMAIL.get().equals(attribute.getName()) - || "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress".equals(attribute.getName())) { - if (!attribute.getAttributeValue().isEmpty()) { - return attribute.getAttributeValue().get(0).toString(); - } + List attributeValue = attribute.getAttributeValue(); + if (!attributeValue.isEmpty() && (X500SAMLProfileConstants.EMAIL.getFriendlyName().equals(attribute.getFriendlyName()) + || X500SAMLProfileConstants.EMAIL.get().equals(attribute.getName()) + || "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress".equals(attribute.getName()))) { + return attributeValue.get(0).toString(); } } } @@ -209,12 +210,12 @@ public String getSessionIndex() { return null; } - public AssertionType getAssertionType(Object token, RealmModel realm) throws IOException, ParsingException, ProcessingException, ConfigurationException { + protected AssertionType getAssertionType(Object token, RealmModel realm) throws IOException, ParsingException, ProcessingException, ConfigurationException { AssertionType assertionType = null; SAMLParser parser = SAMLParser.getInstance(); String assertionXml = DocumentUtil.asString(((Element) token).getOwnerDocument()); - try (ByteArrayInputStream bis = new ByteArrayInputStream(assertionXml.getBytes())) { + try (ByteArrayInputStream bis = new ByteArrayInputStream(assertionXml.getBytes(StandardCharsets.UTF_8))) { Object assertion = parser.parse(bis); if (assertion instanceof EncryptedAssertionType) { diff --git a/keycloak-wsfed/src/main/java/com/quest/keycloak/broker/wsfed/WSFedDataMarshaller.java b/keycloak-wsfed/src/main/java/com/quest/keycloak/broker/wsfed/WSFedDataMarshaller.java index 41eaad5..35a9ada 100644 --- a/keycloak-wsfed/src/main/java/com/quest/keycloak/broker/wsfed/WSFedDataMarshaller.java +++ b/keycloak-wsfed/src/main/java/com/quest/keycloak/broker/wsfed/WSFedDataMarshaller.java @@ -26,69 +26,85 @@ import org.keycloak.saml.processing.core.saml.v1.writers.SAML11AssertionWriter; import org.keycloak.saml.processing.core.saml.v2.writers.SAMLAssertionWriter; -import io.cloudtrust.keycloak.exceptions.CtRuntimeException; +import io.cloudtrust.exception.CloudtrustRuntimeException; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.function.Function; +import java.util.function.Predicate; public class WSFedDataMarshaller extends DefaultDataMarshaller { - @Override - public String serialize(Object obj) { - if (obj instanceof AssertionType) { + private static final Map, Function> serializers; + private static final Map, Function> deserializers; + + static { + serializers = new HashMap<>(); + serializers.put(o -> o instanceof AssertionType, obj -> { try { ByteArrayOutputStream bos = new ByteArrayOutputStream(); AssertionType assertion = (AssertionType) obj; SAMLAssertionWriter samlWriter = new SAMLAssertionWriter(StaxUtil.getXMLStreamWriter(bos)); samlWriter.write(assertion); - return new String(bos.toByteArray()); + return new String(bos.toByteArray(), StandardCharsets.UTF_8); } catch (ProcessingException pe) { - throw new CtRuntimeException(pe); + throw new CloudtrustRuntimeException(pe); } - } - else if (obj instanceof SAML11AssertionType) { + }); + serializers.put(o -> o instanceof SAML11AssertionType, obj -> { try { ByteArrayOutputStream bos = new ByteArrayOutputStream(); SAML11AssertionType assertion = (SAML11AssertionType) obj; SAML11AssertionWriter samlWriter = new SAML11AssertionWriter(StaxUtil.getXMLStreamWriter(bos)); samlWriter.write(assertion); - return new String(bos.toByteArray()); + return new String(bos.toByteArray(), StandardCharsets.UTF_8); } catch (ProcessingException pe) { - throw new CtRuntimeException(pe); + throw new CloudtrustRuntimeException(pe); } - } else { - return super.serialize(obj); - } - } + }); - @Override - public T deserialize(String serialized, Class clazz) { - if (clazz.equals(AssertionType.class)) { + deserializers = new HashMap<>(); + deserializers.put(AssertionType.class, s -> { try { - byte[] bytes = serialized.getBytes(); + byte[] bytes = s.getBytes(StandardCharsets.UTF_8); InputStream is = new ByteArrayInputStream(bytes); - Object respType = SAMLParser.getInstance().parse(is); - - return clazz.cast(respType); + return SAMLParser.getInstance().parse(is); } catch (ParsingException pe) { - throw new CtRuntimeException(pe); + throw new CloudtrustRuntimeException(pe); } - } - else if (clazz.equals(SAML11AssertionType.class)) { + }); + deserializers.put(SAML11AssertionType.class, s -> { try { - byte[] bytes = serialized.getBytes(); + byte[] bytes = s.getBytes(StandardCharsets.UTF_8); InputStream is = new ByteArrayInputStream(bytes); - Object respType = SAMLParser.getInstance().parse(is); - - return clazz.cast(respType); + return SAMLParser.getInstance().parse(is); } catch (ParsingException pe) { - throw new CtRuntimeException(pe); + throw new CloudtrustRuntimeException(pe); } - } else { + }); + } + + @Override + public String serialize(Object obj) { + return serializers.entrySet().stream() + .filter(e -> e.getKey().test(obj)).map(Entry::getValue).findFirst() + .orElse(super::serialize) + .apply(obj); + } + + @Override + public T deserialize(String serialized, Class clazz) { + Function deserializer = deserializers.get(clazz); + if (deserializer==null) { return super.deserialize(serialized, clazz); } + return clazz.cast(deserializer.apply(serialized)); } } diff --git a/keycloak-wsfed/src/main/java/com/quest/keycloak/broker/wsfed/WSFedEndpoint.java b/keycloak-wsfed/src/main/java/com/quest/keycloak/broker/wsfed/WSFedEndpoint.java index 3afac8c..24f246c 100644 --- a/keycloak-wsfed/src/main/java/com/quest/keycloak/broker/wsfed/WSFedEndpoint.java +++ b/keycloak-wsfed/src/main/java/com/quest/keycloak/broker/wsfed/WSFedEndpoint.java @@ -56,7 +56,9 @@ import javax.xml.datatype.XMLGregorianCalendar; import java.io.ByteArrayInputStream; import java.io.IOException; +import java.io.UnsupportedEncodingException; import java.net.URI; +import java.net.URISyntaxException; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.security.PublicKey; @@ -64,14 +66,18 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.function.Consumer; /** * @author Kevin Horvatin * @version $Revision: 1 $ */ public class WSFedEndpoint { - public static final String WSFED_REQUESTED_TOKEN = "WSFED_REQUESTED_TOKEN"; protected static final Logger logger = Logger.getLogger(WSFedEndpoint.class); + + public static final String WSFED_REQUESTED_TOKEN = "WSFED_REQUESTED_TOKEN"; + private static final String ACTIVE_CODE = "active_code"; // duplicating because ClientSessionCode.ACTIVE_CODE is private + protected RealmModel realm; protected EventBuilder event; protected WSFedIdentityProviderConfig config; @@ -135,8 +141,7 @@ protected Response execute(String wsfedAction, String wsfedResult, String contex if (context != null) { // strip out any additions made for C-BAS, e.g. &username=xxx, etc. // otherwise it will choke while trying to process it as a code - String[] contextParts = context.split("&"); - context = contextParts[0]; + context = context.split("&")[0]; } if (wsfedAction == null && config.handleEmptyActionAsLogout()) { return handleSignoutResponse(context); @@ -147,10 +152,12 @@ protected Response execute(String wsfedAction, String wsfedResult, String contex return response; if (wsfedResult != null) return handleWsFedResponse(wsfedResult, context); - if (wsfedAction.compareTo(WSFedConstants.WSFED_SIGNOUT_ACTION) == 0) + if (WSFedConstants.WSFED_SIGNOUT_ACTION.equals(wsfedAction)) { return handleSignoutRequest(context); - if (wsfedAction.compareTo(WSFedConstants.WSFED_SIGNOUT_CLEANUP_ACTION) == 0) + } + if (WSFedConstants.WSFED_SIGNOUT_CLEANUP_ACTION.equals(wsfedAction)) { return handleSignoutResponse(context); + } return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST); } @@ -226,39 +233,13 @@ private Map getContextParameters(String context) { return map; } - protected Response handleLoginResponse(String wsfedResponse, RequestedToken token, String context) throws IdentityBrokerException { + protected Response handleLoginResponse(String wsfedResponse, RequestedToken token, String context) { try { BrokeredIdentityContext identity = new BrokeredIdentityContext(token.getId()); if (context != null) { - String decodedContext = URLDecoder.decode(context, StandardCharsets.UTF_8.name()); - if (decodedContext.contains("redirectUri=")) { - Map map = getContextParameters(decodedContext); - String redirectUri = URLDecoder.decode(map.get("redirectUri"), StandardCharsets.UTF_8.name()); - if (decodedContext.contains("&code=")) { - //TODO not sure that we indeed have a AuthenticationSessionModel here. It could potentially be a AuthenticatedClientSessionModel. ALSO tabID set to null, but likely broken - ClientSessionCode.ParseResult clientCode = ClientSessionCode.parseResult(map.get("code"), null, this.session, this.session.getContext().getRealm(), this.session.getContext().getClient(), event, AuthenticationSessionModel.class); - if (clientCode != null && clientCode.getCode().isValid(CommonClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) { - String ACTIVE_CODE = "active_code"; // duplicating because ClientSessionCode.ACTIVE_CODE is private - // restore ACTIVE_CODE note because it must have been removed by parse() if code==activeCode - clientCode.getClientSession().setClientNote(ACTIVE_CODE, map.get("code")); - - // set authorization code and redirectUri - identity.setCode(map.get("code")); - identity.getContextData().put(WSFedConstants.WSFED_CONTEXT, redirectUri); - } else { - /* - * browser session expired, redirect to original URL - * which if protected would trigger SSO - */ - return Response.seeOther(new URI(redirectUri)).build(); - } - } else { - // only redirectUri (User added by subscription admin) - return Response.seeOther(new URI(redirectUri)).build(); - } - } else { - // regular login with no create user parameters - identity.setCode(decodedContext); + Response resp = apply(identity, context); + if (resp!=null) { + return resp; } } //This token has to be something that the broker code can deserialize. So using our RequestedToken class doesn't work because it can't find the class @@ -272,19 +253,9 @@ protected Response handleLoginResponse(String wsfedResponse, RequestedToken toke "set for Keycloak to be able to link the external IdP user to a local user"); } - if (token.getEmail() != null) { - identity.setEmail(token.getEmail()); - } - - String givenName; - if ((givenName = token.getFirstName()) != null) { - identity.setFirstName(givenName); - } - - String surName; - if ((surName = token.getLastName()) != null) { - identity.setLastName(surName); - } + whenNotNull(token.getEmail(), identity::setEmail); + whenNotNull(token.getFirstName(), identity::setFirstName); + whenNotNull(token.getLastName(), identity::setLastName); if (config.isStoreToken()) { identity.setToken(wsfedResponse); @@ -299,7 +270,6 @@ protected Response handleLoginResponse(String wsfedResponse, RequestedToken toke } return callback.authenticated(identity); - } catch (IdentityBrokerException e) { throw e; } catch (Exception e) { @@ -307,6 +277,45 @@ protected Response handleLoginResponse(String wsfedResponse, RequestedToken toke } } + private Response apply(BrokeredIdentityContext identity, String context) throws URISyntaxException, UnsupportedEncodingException { + String decodedContext = URLDecoder.decode(context, StandardCharsets.UTF_8.name()); + if (decodedContext.contains("redirectUri=")) { + Map map = getContextParameters(decodedContext); + String redirectUri = URLDecoder.decode(map.get("redirectUri"), StandardCharsets.UTF_8.name()); + if (decodedContext.contains("&code=")) { + //TODO not sure that we indeed have a AuthenticationSessionModel here. It could potentially be a AuthenticatedClientSessionModel. ALSO tabID set to null, but likely broken + ClientSessionCode.ParseResult clientCode = ClientSessionCode.parseResult(map.get("code"), null, this.session, this.session.getContext().getRealm(), this.session.getContext().getClient(), event, AuthenticationSessionModel.class); + if (clientCode != null && clientCode.getCode().isValid(CommonClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) { + // restore ACTIVE_CODE note because it must have been removed by parse() if code==activeCode + clientCode.getClientSession().setClientNote(ACTIVE_CODE, map.get("code")); + + // set authorization code and redirectUri + identity.setCode(map.get("code")); + identity.getContextData().put(WSFedConstants.WSFED_CONTEXT, redirectUri); + } else { + /* + * browser session expired, redirect to original URL + * which if protected would trigger SSO + */ + return Response.seeOther(new URI(redirectUri)).build(); + } + } else { + // only redirectUri (User added by subscription admin) + return Response.seeOther(new URI(redirectUri)).build(); + } + } else { + // regular login with no create user parameters + identity.setCode(decodedContext); + } + return null; + } + + private static void whenNotNull(String value, Consumer action) { + if (value!=null) { + action.accept(value); + } + } + protected Response handleWsFedResponse(String wsfedResponse, String context) { try { RequestSecurityTokenResponse rstr = getWsfedToken(wsfedResponse); @@ -348,7 +357,7 @@ protected Response handleWsFedResponse(String wsfedResponse, String context) { } } - protected boolean hasExpired(RequestSecurityTokenResponse rstr) throws ConfigurationException, DatatypeConfigurationException { + protected boolean hasExpired(RequestSecurityTokenResponse rstr) throws DatatypeConfigurationException { boolean expiry = false; Lifetime lifetime = rstr.getLifetime(); if (lifetime != null) { @@ -363,13 +372,11 @@ protected boolean hasExpired(RequestSecurityTokenResponse rstr) throws Configura } XMLGregorianCalendar notOnOrAfter = lifetime.getExpires(); - if (notOnOrAfter != null) { logger.trace("RequestSecurityTokenResponse: " + rstr.getContext() + " ::Now=" + now.toXMLFormat() + " ::notOnOrAfter=" + notOnOrAfter); } expiry = !XMLTimeUtil.isValid(now, notBefore, notOnOrAfter); - if (expiry) { logger.info("RequestSecurityTokenResponse has expired with context=" + rstr.getContext()); } diff --git a/keycloak-wsfed/src/main/java/com/quest/keycloak/broker/wsfed/WSFedIdentityProvider.java b/keycloak-wsfed/src/main/java/com/quest/keycloak/broker/wsfed/WSFedIdentityProvider.java index b5b6c92..6328301 100644 --- a/keycloak-wsfed/src/main/java/com/quest/keycloak/broker/wsfed/WSFedIdentityProvider.java +++ b/keycloak-wsfed/src/main/java/com/quest/keycloak/broker/wsfed/WSFedIdentityProvider.java @@ -18,6 +18,8 @@ import com.quest.keycloak.common.wsfed.WSFedConstants; import com.quest.keycloak.common.wsfed.builders.WSFedResponseBuilder; + +import org.apache.commons.lang3.StringUtils; import org.jboss.logging.Logger; import org.keycloak.broker.provider.AbstractIdentityProvider; import org.keycloak.broker.provider.AuthenticationRequest; @@ -37,9 +39,11 @@ import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; import java.io.BufferedReader; +import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.util.stream.Collectors; public class WSFedIdentityProvider extends AbstractIdentityProvider { @@ -64,9 +68,11 @@ public Response performLogin(AuthenticationRequest request) { // not sure how valuable this null-check is in real life, but it breaks in the tests without it. if (request.getHttpRequest() != null && request.getHttpRequest().getUri() != null) { MultivaluedMap params = request.getHttpRequest().getUri().getQueryParameters(); - if (params != null && params.containsKey("login_hint")) { + if (params != null) { String loginHint = params.getFirst("login_hint"); - context += "&username=" + URLEncoder.encode(loginHint, "UTF-8"); + if (loginHint!=null) { + context += "&username=" + URLEncoder.encode(loginHint, StandardCharsets.UTF_8.name()); + } } } WSFedResponseBuilder builder = new WSFedResponseBuilder() @@ -100,22 +106,22 @@ public Response keycloakInitiatedBrowserLogout(KeycloakSession session, UserSess } //Generate signout to IDP - WSFedResponseBuilder builder = new WSFedResponseBuilder(); - builder.setMethod(HttpMethod.GET) - .setAction(WSFedConstants.WSFED_SIGNOUT_ACTION) - .setRealm(getConfig().getWsFedRealm()) - .setContext(userSession.getId()) - .setReplyTo(getEndpoint(uriInfo, realm)) - .setDestination(singleLogoutServiceUrl); - - return builder.buildResponse(null); + return new WSFedResponseBuilder() + .setMethod(HttpMethod.GET) + .setAction(WSFedConstants.WSFED_SIGNOUT_ACTION) + .setRealm(getConfig().getWsFedRealm()) + .setContext(userSession.getId()) + .setReplyTo(getEndpoint(uriInfo, realm)) + .setDestination(singleLogoutServiceUrl) + .buildResponse(null); } @Override public void backchannelLogout(KeycloakSession session, UserSessionModel userSession, UriInfo uriInfo, RealmModel realm) { String singleLogoutServiceUrl = getConfig().getSingleLogoutServiceUrl(); - if (singleLogoutServiceUrl == null || singleLogoutServiceUrl.trim().equals("") || !getConfig().isBackchannelSupported()) + if (StringUtils.isBlank(singleLogoutServiceUrl) || !getConfig().isBackchannelSupported()) { return; + } try { int status = SimpleHttp.doGet(singleLogoutServiceUrl, session) @@ -126,7 +132,7 @@ public void backchannelLogout(KeycloakSession session, UserSessionModel userSess if (!success) { logger.warn("Failed ws-fed backchannel broker logout to: " + singleLogoutServiceUrl); } - } catch (Exception e) { + } catch (IOException e) { logger.warn("Failed ws-fed backchannel broker logout to: " + singleLogoutServiceUrl, e); } } diff --git a/keycloak-wsfed/src/main/java/com/quest/keycloak/broker/wsfed/WSFedIdentityProviderConfig.java b/keycloak-wsfed/src/main/java/com/quest/keycloak/broker/wsfed/WSFedIdentityProviderConfig.java index 748e57b..eb14102 100644 --- a/keycloak-wsfed/src/main/java/com/quest/keycloak/broker/wsfed/WSFedIdentityProviderConfig.java +++ b/keycloak-wsfed/src/main/java/com/quest/keycloak/broker/wsfed/WSFedIdentityProviderConfig.java @@ -18,6 +18,7 @@ import org.keycloak.models.IdentityProviderModel; +@SuppressWarnings("serial") public class WSFedIdentityProviderConfig extends IdentityProviderModel { public WSFedIdentityProviderConfig() { @@ -44,7 +45,7 @@ public void setSingleLogoutServiceUrl(String singleLogoutServiceUrl) { } public boolean isValidateSignature() { - return Boolean.valueOf(getConfig().get("validateSignature")); + return Boolean.parseBoolean(getConfig().get("validateSignature")); } public void setValidateSignature(boolean validateSignature) { @@ -68,7 +69,7 @@ public void setWsFedRealm(String wsfedRealm) { } public boolean isBackchannelSupported() { - return Boolean.valueOf(getConfig().get("backchannelSupported")); + return Boolean.parseBoolean(getConfig().get("backchannelSupported")); } public void setBackchannelSupported(boolean backchannel) { @@ -76,7 +77,7 @@ public void setBackchannelSupported(boolean backchannel) { } public boolean handleEmptyActionAsLogout() { - return Boolean.valueOf(getConfig().get("emptyActionHandledAsLogout")); + return Boolean.parseBoolean(getConfig().get("emptyActionHandledAsLogout")); } public void setHandleEmptyActionAsLogout(boolean handleAsLogout) { diff --git a/keycloak-wsfed/src/main/java/com/quest/keycloak/broker/wsfed/mappers/AttributeToRoleMapper.java b/keycloak-wsfed/src/main/java/com/quest/keycloak/broker/wsfed/mappers/AttributeToRoleMapper.java index f588f47..0b03672 100644 --- a/keycloak-wsfed/src/main/java/com/quest/keycloak/broker/wsfed/mappers/AttributeToRoleMapper.java +++ b/keycloak-wsfed/src/main/java/com/quest/keycloak/broker/wsfed/mappers/AttributeToRoleMapper.java @@ -19,6 +19,8 @@ import com.quest.keycloak.broker.wsfed.WSFedEndpoint; import com.quest.keycloak.broker.wsfed.WSFedIdentityProviderFactory; import com.quest.keycloak.common.wsfed.utils.AttributeUtils; + +import org.apache.commons.lang3.StringUtils; import org.jboss.logging.Logger; import org.keycloak.broker.provider.AbstractIdentityProviderMapper; import org.keycloak.broker.provider.BrokeredIdentityContext; @@ -28,47 +30,29 @@ import org.keycloak.models.*; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.provider.ProviderConfigProperty; +import org.keycloak.provider.ProviderConfigurationBuilder; -import java.util.ArrayList; +import java.util.Arrays; import java.util.List; public class AttributeToRoleMapper extends AbstractIdentityProviderMapper { protected static final Logger logger = Logger.getLogger(AttributeToRoleMapper.class); - private static final String[] COMPATIBLE_PROVIDERS = {WSFedIdentityProviderFactory.PROVIDER_ID}; + private static final List COMPATIBLE_PROVIDERS = Arrays.asList(WSFedIdentityProviderFactory.PROVIDER_ID); - private static final List configProperties = new ArrayList<>(); + private static final List configProperties; public static final String ATTRIBUTE_NAME = "attribute.name"; public static final String ATTRIBUTE_FRIENDLY_NAME = "attribute.friendly.name"; public static final String ATTRIBUTE_VALUE = "attribute.value"; static { - ProviderConfigProperty property; - property = new ProviderConfigProperty(); - property.setName(ATTRIBUTE_NAME); - property.setLabel("Attribute Name"); - property.setHelpText("Name of attribute to search for in assertion. You can leave this blank and specify a friendly name instead."); - property.setType(ProviderConfigProperty.STRING_TYPE); - configProperties.add(property); - property = new ProviderConfigProperty(); - property.setName(ATTRIBUTE_FRIENDLY_NAME); - property.setLabel("Friendly Name"); - property.setHelpText("Friendly name of attribute to search for in assertion. You can leave this blank and specify a name instead."); - property.setType(ProviderConfigProperty.STRING_TYPE); - configProperties.add(property); - property = new ProviderConfigProperty(); - property.setName(ATTRIBUTE_VALUE); - property.setLabel("Attribute Value"); - property.setHelpText("Value the attribute must have. If the attribute is a list, then the value must be contained in the list."); - property.setType(ProviderConfigProperty.STRING_TYPE); - configProperties.add(property); - property = new ProviderConfigProperty(); - property.setName(ConfigConstants.ROLE); - property.setLabel("Role"); - property.setHelpText("Role to grant to user. To reference an application role the syntax is appname.approle, i.e. myapp.myrole"); - property.setType(ProviderConfigProperty.STRING_TYPE); - configProperties.add(property); + configProperties = ProviderConfigurationBuilder.create() + .property(ATTRIBUTE_NAME, "Attribute Name", "Name of attribute to search for in assertion. You can leave this blank and specify a friendly name instead.", ProviderConfigProperty.STRING_TYPE, null, null) + .property(ATTRIBUTE_FRIENDLY_NAME, "Friendly Name", "Friendly name of attribute to search for in assertion. You can leave this blank and specify a name instead.", ProviderConfigProperty.STRING_TYPE, null, null) + .property(ATTRIBUTE_VALUE, "Attribute Value", "Value the attribute must have. If the attribute is a list, then the value must be contained in the list.", ProviderConfigProperty.STRING_TYPE, null, null) + .property(ConfigConstants.ROLE, "Role", "Role to grant to user. To reference an application role the syntax is appname.approle, i.e. myapp.myrole", ProviderConfigProperty.STRING_TYPE, null, null) + .build(); } public static final String PROVIDER_ID = "wsfed-role-idp-mapper"; @@ -85,7 +69,7 @@ public String getId() { @Override public String[] getCompatibleProviders() { - return COMPATIBLE_PROVIDERS; + return COMPATIBLE_PROVIDERS.toArray(new String[COMPATIBLE_PROVIDERS.size()]); } @Override @@ -109,10 +93,8 @@ public void importNewUser(KeycloakSession session, RealmModel realm, UserModel u } protected boolean isAttributePresent(IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { - String name = mapperModel.getConfig().get(ATTRIBUTE_NAME); - if (name != null && name.trim().equals("")) name = null; - String friendly = mapperModel.getConfig().get(ATTRIBUTE_FRIENDLY_NAME); - if (friendly != null && friendly.trim().equals("")) friendly = null; + String name = StringUtils.defaultIfBlank(mapperModel.getConfig().get(ATTRIBUTE_NAME), null); + String friendly = StringUtils.defaultIfBlank(mapperModel.getConfig().get(ATTRIBUTE_FRIENDLY_NAME), null); String desiredValue = mapperModel.getConfig().get(ATTRIBUTE_VALUE); try { @@ -120,9 +102,8 @@ protected boolean isAttributePresent(IdentityProviderMapperModel mapperModel, Br if (token instanceof AssertionType) { return isAttributePresent((AssertionType) token, name, friendly, desiredValue); - } - //TODO: else if token type == JWSInput - else { + } else { + //TODO: else if token type == JWSInput logger.warn("WS-Fed attribute role mapper doesn't currently support this token type."); } } catch (Exception ex) { diff --git a/keycloak-wsfed/src/main/java/com/quest/keycloak/broker/wsfed/mappers/UserAttributeMapper.java b/keycloak-wsfed/src/main/java/com/quest/keycloak/broker/wsfed/mappers/UserAttributeMapper.java index 2ad6eca..e949baf 100644 --- a/keycloak-wsfed/src/main/java/com/quest/keycloak/broker/wsfed/mappers/UserAttributeMapper.java +++ b/keycloak-wsfed/src/main/java/com/quest/keycloak/broker/wsfed/mappers/UserAttributeMapper.java @@ -19,6 +19,8 @@ import com.quest.keycloak.broker.wsfed.WSFedEndpoint; import com.quest.keycloak.broker.wsfed.WSFedIdentityProviderFactory; import com.quest.keycloak.common.wsfed.utils.AttributeUtils; + +import org.apache.commons.lang3.StringUtils; import org.jboss.logging.Logger; import org.keycloak.broker.provider.AbstractIdentityProviderMapper; import org.keycloak.broker.provider.BrokeredIdentityContext; @@ -28,41 +30,28 @@ import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.provider.ProviderConfigProperty; +import org.keycloak.provider.ProviderConfigurationBuilder; -import java.util.ArrayList; +import java.util.Arrays; import java.util.List; public class UserAttributeMapper extends AbstractIdentityProviderMapper { - private static final Logger logger = Logger.getLogger(AttributeToRoleMapper.class); + private static final Logger logger = Logger.getLogger(UserAttributeMapper.class); - private static final String[] COMPATIBLE_PROVIDERS = {WSFedIdentityProviderFactory.PROVIDER_ID}; + private static final List COMPATIBLE_PROVIDERS = Arrays.asList(WSFedIdentityProviderFactory.PROVIDER_ID); - private static final List configProperties = new ArrayList<>(); + private static final List configProperties; public static final String ATTRIBUTE_NAME = "attribute.name"; public static final String ATTRIBUTE_FRIENDLY_NAME = "attribute.friendly.name"; public static final String USER_ATTRIBUTE = "user.attribute"; static { - ProviderConfigProperty property; - property = new ProviderConfigProperty(); - property.setName(ATTRIBUTE_NAME); - property.setLabel("Attribute Name"); - property.setHelpText("Name of attribute to search for in assertion. You can leave this blank and specify a friendly name instead."); - property.setType(ProviderConfigProperty.STRING_TYPE); - configProperties.add(property); - property = new ProviderConfigProperty(); - property.setName(ATTRIBUTE_FRIENDLY_NAME); - property.setLabel("Friendly Name"); - property.setHelpText("Friendly name of attribute to search for in assertion. You can leave this blank and specify a name instead."); - property.setType(ProviderConfigProperty.STRING_TYPE); - configProperties.add(property); - property = new ProviderConfigProperty(); - property.setName(USER_ATTRIBUTE); - property.setLabel("User Attribute Name"); - property.setHelpText("User attribute name to store saml attribute."); - property.setType(ProviderConfigProperty.STRING_TYPE); - configProperties.add(property); + configProperties = ProviderConfigurationBuilder.create() + .property(ATTRIBUTE_NAME, "Attribute Name", "Name of attribute to search for in assertion. You can leave this blank and specify a friendly name instead.", ProviderConfigProperty.STRING_TYPE, null, null) + .property(ATTRIBUTE_FRIENDLY_NAME, "Friendly Name", "Friendly name of attribute to search for in assertion. You can leave this blank and specify a name instead.", ProviderConfigProperty.STRING_TYPE, null, null) + .property(USER_ATTRIBUTE, "User Attribute Name", "User attribute name to store saml attribute.", ProviderConfigProperty.STRING_TYPE, null, null) + .build(); } public static final String PROVIDER_ID = "wsfed-user-attribute-idp-mapper"; @@ -79,7 +68,7 @@ public String getId() { @Override public String[] getCompatibleProviders() { - return COMPATIBLE_PROVIDERS; + return COMPATIBLE_PROVIDERS.toArray(new String[COMPATIBLE_PROVIDERS.size()]); } @Override @@ -102,19 +91,16 @@ public void importNewUser(KeycloakSession session, RealmModel realm, UserModel u } protected String getAttribute(IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { - String name = mapperModel.getConfig().get(ATTRIBUTE_NAME); - if (name != null && name.trim().equals("")) name = null; - String friendly = mapperModel.getConfig().get(ATTRIBUTE_FRIENDLY_NAME); - if (friendly != null && friendly.trim().equals("")) friendly = null; + String name = StringUtils.defaultIfBlank(mapperModel.getConfig().get(ATTRIBUTE_NAME), null); + String friendly = StringUtils.defaultIfBlank(mapperModel.getConfig().get(ATTRIBUTE_FRIENDLY_NAME), null); try { Object token = context.getContextData().get(WSFedEndpoint.WSFED_REQUESTED_TOKEN); if (token instanceof AssertionType) { return getAttribute((AssertionType) token, name, friendly); - } - //TODO: else if token type == JWSInput - else { + } else { + //TODO: else if token type == JWSInput logger.warn("WS-Fed user attribute mapper doesn't currently support this token type."); } } catch (Exception ex) { @@ -136,13 +122,14 @@ protected String getAttribute(AssertionType assertion, String name, String frien public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { String attribute = mapperModel.getConfig().get(USER_ATTRIBUTE); Object value = getAttribute(mapperModel, context); - String current = user.getFirstAttribute(attribute); - if (value != null && !value.equals(current)) { - user.setSingleAttribute(attribute, value.toString()); - } else if (value == null) { + if (value == null) { user.removeAttribute(attribute); + } else { + String current = user.getFirstAttribute(attribute); + if (!value.equals(current)) { + user.setSingleAttribute(attribute, value.toString()); + } } - } @Override diff --git a/keycloak-wsfed/src/main/java/com/quest/keycloak/common/wsfed/builders/WSFedResponseBuilder.java b/keycloak-wsfed/src/main/java/com/quest/keycloak/common/wsfed/builders/WSFedResponseBuilder.java index e97f778..7d6971a 100644 --- a/keycloak-wsfed/src/main/java/com/quest/keycloak/common/wsfed/builders/WSFedResponseBuilder.java +++ b/keycloak-wsfed/src/main/java/com/quest/keycloak/common/wsfed/builders/WSFedResponseBuilder.java @@ -18,12 +18,14 @@ import com.quest.keycloak.common.wsfed.WSFedConstants; +import java.util.stream.Stream; + import javax.ws.rs.HttpMethod; import javax.ws.rs.core.CacheControl; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; -import static org.keycloak.saml.common.util.StringUtil.isNotNull; +import org.keycloak.saml.common.util.StringUtil; /** * This class builds the self-executing html form that is actually used as a response for a WS-FED passive requestor. @@ -190,11 +192,9 @@ protected String buildHtml(String destination, String action, String result, Str { WSFedConstants.WSFED_REPLY, replyTo }, { WSFedConstants.WSFED_CONTEXT, context } }; - for(String[] p : params) { - if (isNotNull(p[1])) { - builder.append(""); - } - } + Stream.of(params) + .filter(p -> StringUtil.isNotNull(p[1])) + .forEach(p -> builder.append("")); return builder.append("