/* * JBoss, Home of Professional Open Source. * Copyright 2012, Red Hat, Inc., and individual contributors * as indicated by the @author tags. See the copyright.txt file in the * distribution for a full listing of individual contributors. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.picketlink.test.identity.federation.bindings.authenticators; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertNotNull; import static junit.framework.Assert.assertTrue; import static junit.framework.Assert.fail; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.StringWriter; import java.util.ArrayList; import java.util.List; import java.util.Set; import java.util.logging.Logger; import javax.xml.transform.OutputKeys; import javax.xml.transform.Transformer; import javax.xml.transform.stream.StreamResult; import org.junit.Before; import org.junit.Test; import org.picketlink.identity.federation.api.saml.v2.request.SAML2Request; import org.picketlink.identity.federation.api.saml.v2.response.SAML2Response; import org.picketlink.identity.federation.bindings.tomcat.idp.IDPWebBrowserSSOValve; import org.picketlink.identity.federation.core.config.IDPType; import org.picketlink.identity.federation.core.exceptions.ConfigurationException; import org.picketlink.identity.federation.core.exceptions.ParsingException; import org.picketlink.identity.federation.core.exceptions.ProcessingException; import org.picketlink.identity.federation.core.saml.v2.common.IDGenerator; import org.picketlink.identity.federation.core.saml.v2.constants.JBossSAMLURIConstants; import org.picketlink.identity.federation.core.saml.v2.util.DocumentUtil; import org.picketlink.identity.federation.core.util.TransformerUtil; import org.picketlink.identity.federation.saml.v2.assertion.AssertionType; import org.picketlink.identity.federation.saml.v2.assertion.AttributeStatementType; import org.picketlink.identity.federation.saml.v2.assertion.AttributeStatementType.ASTChoiceType; import org.picketlink.identity.federation.saml.v2.assertion.AudienceRestrictionType; import org.picketlink.identity.federation.saml.v2.assertion.ConditionAbstractType; import org.picketlink.identity.federation.saml.v2.assertion.ConditionsType; import org.picketlink.identity.federation.saml.v2.assertion.StatementAbstractType; import org.picketlink.identity.federation.saml.v2.protocol.AuthnRequestType; import org.picketlink.identity.federation.saml.v2.protocol.ResponseType; import org.picketlink.identity.federation.web.constants.GeneralConstants; import org.picketlink.identity.federation.web.core.IdentityParticipantStack; import org.picketlink.identity.federation.web.util.PostBindingUtil; import org.picketlink.identity.federation.web.util.RedirectBindingSignatureUtil; import org.picketlink.identity.federation.web.util.RedirectBindingUtil; import org.picketlink.test.identity.federation.bindings.authenticators.idp.TestIdentityParticipantStack; import org.picketlink.test.identity.federation.bindings.mock.MockCatalinaRequest; import org.picketlink.test.identity.federation.bindings.mock.MockCatalinaResponse; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NodeList; /** * <p> * Test class for the IDP authenticator {@link IDPWebBrowserSSOValve}. * </p> * <p> * This tests simulates a scenario with the following characteristics: <br/> * <ul> * <li>Identity Provider is deployed in a host with this address: <code>IDENTITY_PROVIDER_HOST_ADDRESS</code>. The URL is * <code>IDENTITY_PROVIDER_URL</code></li> * <li>Service Provider is deployed in a host with this address: <code>SERVICE_PROVIDER_HOST_ADDRESS</code>. The URL is * <code>SERVICE_PROVIDER_URL</code></li> * </ul> * </p> * * </p> * * @author <a href="mailto:psilva@redhat.com">Pedro Silva</a> * */ public class IDPWebBrowserSSOTestCase { private static final String CERTIFICATE_ALIAS = "servercert"; private static final Logger logger = Logger.getLogger(IDPWebBrowserSSOTestCase.class.getName()); private static final String IDENTITY_PROVIDER_HOST_ADDRESS = "192.168.1.1"; private static final String SERVICE_PROVIDER_HOST_ADDRESS = "192.168.1.4"; private static final String IDENTITY_PROVIDER_URL = "http://" + IDENTITY_PROVIDER_HOST_ADDRESS + ":8080/idp-sig/"; private static final String SERVICE_PROVIDER_URL = "http://" + SERVICE_PROVIDER_HOST_ADDRESS + ":8080/fake-sp"; private IDPWebBrowserSSOValve identityProvider; @Before public void onSetup() { TestIdentityParticipantStack.reset(); } /** * <p> * Tests the configuration of a custom {@link IdentityParticipantStack}. * </p> */ @Test public void testRoleGeneratorConfiguration() { logger.info("testRoleGeneratorConfiguration"); MockCatalinaRequest request = AuthenticatorTestUtils.createRequest(SERVICE_PROVIDER_HOST_ADDRESS, true); MockCatalinaResponse response = new MockCatalinaResponse(); sendAuthenticationRequest(request, response, SERVICE_PROVIDER_URL, true); ResponseType responseType = getResponseTypeAndCheckSignature(response, null); assertNotNull(responseType); assertEquals(1, responseType.getAssertions().size()); AssertionType assertion = responseType.getAssertions().get(0).getAssertion(); assertEquals(assertion.getIssuer().getValue(), IDENTITY_PROVIDER_URL); List<String> expectedRoles = new ArrayList<String>(); expectedRoles.add("test-role1"); expectedRoles.add("test-role2"); expectedRoles.add("test-role3"); Set<StatementAbstractType> statements = assertion.getStatements(); for (StatementAbstractType statementType : statements) { if (statementType instanceof AttributeStatementType) { AttributeStatementType attributeType = (AttributeStatementType) statementType; List<ASTChoiceType> attributes = attributeType.getAttributes(); for (ASTChoiceType astChoiceType : attributes) { if (astChoiceType.getAttribute().getName().equals("Role")) { expectedRoles.remove(astChoiceType.getAttribute().getAttributeValue().get(0)); } } } } assertTrue(expectedRoles.isEmpty()); // The response should redirect back to the caller SP assertTrue("Expected a redirect to the SP.", response.redirectString.contains(SERVICE_PROVIDER_URL)); } /** * <p> * Tests the configuration of a custom {@link IdentityParticipantStack}. * </p> */ @Test public void testIdentityParticipantStackConfiguration() { logger.info("testIdentityParticipantStackConfiguration"); MockCatalinaRequest request = AuthenticatorTestUtils.createRequest(SERVICE_PROVIDER_HOST_ADDRESS, true); MockCatalinaResponse response = new MockCatalinaResponse(); sendAuthenticationRequest(request, response, SERVICE_PROVIDER_URL, true); IdentityParticipantStack testIdentityParticipantStack = TestIdentityParticipantStack.getDelegate(); assertEquals("Unexpected total created sessions.", 1, testIdentityParticipantStack.totalSessions()); ResponseType responseType = getResponseTypeAndCheckSignature(response, null); assertNotNull(responseType); assertEquals(1, responseType.getAssertions().size()); assertEquals(responseType.getAssertions().get(0).getAssertion().getIssuer().getValue(), IDENTITY_PROVIDER_URL); // The response should redirect back to the caller SP assertTrue("Expected a redirect to the SP.", response.redirectString.contains(SERVICE_PROVIDER_URL)); String currentSessionID = request.getSession().getId(); // asserts if there is a participant for the current session ID assertEquals(1, testIdentityParticipantStack.getParticipants(currentSessionID)); // asserts if the last participant in the stack is the last caller SP assertEquals(SERVICE_PROVIDER_URL, testIdentityParticipantStack.peek(currentSessionID)); } /** * <p> * Tests the StrictPostBinding configuration. * </p> * * @throws ProcessingException * @throws ParsingException * @throws ConfigurationException */ @Test public void testStrictPostBindingConfiguration() throws ConfigurationException, ParsingException, ProcessingException { logger.info("testStrictPostBindingConfiguration"); ((IDPType) getAuthenticator().getConfiguration().getIdpOrSP()).setStrictPostBinding(true); MockCatalinaRequest request = AuthenticatorTestUtils.createRequest(SERVICE_PROVIDER_HOST_ADDRESS, true); MockCatalinaResponse response = new MockCatalinaResponse(); ByteArrayOutputStream bos = new ByteArrayOutputStream(); response.setOutputStream(bos); sendAuthenticationRequest(request, response, SERVICE_PROVIDER_URL, true); ResponseType responseType = getResponseTypeAndCheckSignature(response, bos); assertNotNull(responseType); assertEquals(1, responseType.getAssertions().size()); AssertionType assertion = responseType.getAssertions().get(0).getAssertion(); assertEquals(assertion.getIssuer().getValue(), IDENTITY_PROVIDER_URL); ConditionsType conditions = assertion.getConditions(); assertNotNull(conditions); List<ConditionAbstractType> conditionList = conditions.getConditions(); assertEquals(1, conditionList.size()); AudienceRestrictionType audience = (AudienceRestrictionType) conditionList.get(0); assertEquals(SERVICE_PROVIDER_URL, audience.getAudience().get(0).toString()); } /** * <p> * Tests if the IDP respond with an ResponseType with a <code>JBossSAMLURIConstants.STATUS_AUTHNFAILED.get()</code> status * code. This test sends a request without any signature information. Because the IDP is configured with signatures an error * response is expected. * </p> */ @Test public void testInvalidRequestWithoutSignature() { logger.info("testInvalidRequestWithoutSignature"); MockCatalinaRequest request = AuthenticatorTestUtils.createRequest(SERVICE_PROVIDER_HOST_ADDRESS, true); MockCatalinaResponse response = new MockCatalinaResponse(); sendAuthenticationRequest(request, response, SERVICE_PROVIDER_URL, false); ResponseType responseType = getResponseTypeAndCheckSignature(response, null); assertNotNull(responseType); assertEquals(JBossSAMLURIConstants.STATUS_AUTHNFAILED.get(), responseType.getStatus().getStatusCode().getValue() .toString()); // The response should redirect back to the caller SP assertTrue("Expected a redirect to the SP.", response.redirectString.contains(SERVICE_PROVIDER_URL)); } /** * <p> * Tests if the IDP respond with an ResponseType with a <code>JBossSAMLURIConstants.STATUS_AUTHNFAILED.get()</code> status * code. This test sends a {@link AuthnRequestType} with a invalid issuer. The issuer is not in the IDP ValidatingAlias * list. * </p> */ @Test public void testRequestFromInvalidValidatingAlias() { logger.info("testRequestFromInvalidValidatingAlias"); String notTrustedDomain = "123.123.123.123"; String notTrustedDomainForIssuer = "145.145.145.145"; String notTrustedServiceProviderURL = SERVICE_PROVIDER_URL.replace(SERVICE_PROVIDER_HOST_ADDRESS, notTrustedDomain); String notTrustedIssuerURL = SERVICE_PROVIDER_URL.replace(SERVICE_PROVIDER_HOST_ADDRESS, notTrustedDomainForIssuer); MockCatalinaRequest request = AuthenticatorTestUtils.createRequest(notTrustedDomain, true); MockCatalinaResponse response = new MockCatalinaResponse(); // We will use different URL for assertionConsumerServiceURL and for issuerURL to ensure that error response // will be redirected to assertionConsumerServiceURL sendAuthenticationRequest(request, response, notTrustedIssuerURL, notTrustedServiceProviderURL, true); ResponseType responseType = getResponseTypeAndCheckSignature(response, null); assertNotNull(responseType); assertEquals(JBossSAMLURIConstants.STATUS_AUTHNFAILED.get(), responseType.getStatus().getStatusCode().getValue() .toString()); // The response should redirect back to the caller SP assertTrue("Expected a redirect to the SP.", response.redirectString.contains(notTrustedServiceProviderURL)); } /** * <p> * Tests if the IDP respond with an ResponseType with a <code>JBossSAMLURIConstants.STATUS_REQUEST_DENIED.get()</code> * status code. This test sends a {@link AuthnRequestType} with a invalid issuer. The issuer is not in the IDP trusted * domain list. * </p> */ @Test public void testRequestFromUntrustedDOmain() { logger.info("testRequestFromUntrustedDOmain"); String notTrustedDomain = "192.168.1.5"; String notTrustedServiceProviderURL = SERVICE_PROVIDER_URL.replace(SERVICE_PROVIDER_HOST_ADDRESS, notTrustedDomain); MockCatalinaRequest request = AuthenticatorTestUtils.createRequest(notTrustedDomain, true); MockCatalinaResponse response = new MockCatalinaResponse(); sendAuthenticationRequest(request, response, notTrustedServiceProviderURL, true); ResponseType responseType = getResponseTypeAndCheckSignature(response, null); assertNotNull(responseType); assertEquals(JBossSAMLURIConstants.STATUS_REQUEST_DENIED.get(), responseType.getStatus().getStatusCode().getValue() .toString()); // The response should redirect back to the caller SP assertTrue("Expected a redirect to the SP.", response.redirectString.contains(notTrustedServiceProviderURL)); } /** * <p> * Tests if the IDP respond with a valid {@link AssertionType} given a valid {@link AuthnRequestType}. * </p> * * @throws Exception */ @Test public void testSimpleAuthenticationRequest() throws Exception { logger.info("testSimpleAuthenticationRequest"); MockCatalinaRequest request = AuthenticatorTestUtils.createRequest(SERVICE_PROVIDER_HOST_ADDRESS, true); MockCatalinaResponse response = new MockCatalinaResponse(); sendAuthenticationRequest(request, response, SERVICE_PROVIDER_URL, true); ResponseType responseType = getResponseTypeAndCheckSignature(response, null); assertNotNull(responseType); assertEquals(1, responseType.getAssertions().size()); assertEquals(responseType.getAssertions().get(0).getAssertion().getIssuer().getValue(), IDENTITY_PROVIDER_URL); // The response should redirect back to the caller SP assertTrue("Expected a redirect to the SP.", response.redirectString.contains(SERVICE_PROVIDER_URL)); } /** * <p> * Tests if the IDP respond with a valid {@link AssertionType} given a valid {@link AuthnRequestType}. This test disables * signature support on the IDP and try to get an assertion without signatures. * </p> * * @throws Exception */ @Test public void testSimpleAuthenticationRequestWithoutSignature() throws Exception { logger.info("testSimpleAuthenticationRequest"); getAuthenticator().getConfiguration().getIdpOrSP().setSupportsSignature(false); MockCatalinaRequest request = AuthenticatorTestUtils.createRequest(SERVICE_PROVIDER_HOST_ADDRESS, true); MockCatalinaResponse response = new MockCatalinaResponse(); sendAuthenticationRequest(request, response, SERVICE_PROVIDER_URL, false); ResponseType responseType = getResponseType(response, null); assertNotNull(responseType); assertEquals(1, responseType.getAssertions().size()); assertEquals(responseType.getAssertions().get(0).getAssertion().getIssuer().getValue(), IDENTITY_PROVIDER_URL); // The response should redirect back to the caller SP assertTrue("Expected a redirect to the SP.", response.redirectString.contains(SERVICE_PROVIDER_URL)); } /** * <p> * Tests if the the assertion issued by the IDP has the expected time conditions. This test asserts if the * PicketLinkSTS.TokenTimeout attribute is being considered when creating the assertion conditions. * </p> * * @throws Exception */ @Test public void testAssertionTokenTimeoutAndClockSkew() throws Exception { logger.info("testAssertionTokenTimeoutAndClockSkew"); MockCatalinaRequest request = AuthenticatorTestUtils.createRequest(SERVICE_PROVIDER_HOST_ADDRESS, true); MockCatalinaResponse response = new MockCatalinaResponse(); sendAuthenticationRequest(request, response, SERVICE_PROVIDER_URL, true); ResponseType responseType = getResponseTypeAndCheckSignature(response, null); assertNotNull(responseType); assertEquals(1, responseType.getAssertions().size()); AssertionType issuedAssertion = responseType.getAssertions().get(0).getAssertion(); assertEquals(issuedAssertion.getIssuer().getValue(), IDENTITY_PROVIDER_URL); // The response should redirect back to the caller SP assertTrue("Expected a redirect to the SP.", response.redirectString.contains(SERVICE_PROVIDER_URL)); ConditionsType conditions = issuedAssertion.getConditions(); assertEquals("The assertion timeout is invalid.", 3000, conditions.getNotOnOrAfter().toGregorianCalendar() .getTimeInMillis() - conditions.getNotBefore().toGregorianCalendar().getTimeInMillis()); } /** * <p> * Extracts the {@link ResponseType} from the http response. This methos allows to extract the {@link ResponseType} from a * {@link StringWriter} or direct from the response.redirectString. If using HTTP Redirect Binding you should pass null. to * the writer param. * </p> * * @param response * @param bos if not null, try to get the {@link ResponseType} from the this writer. Otherwise try to get from * the response querystring. * @return */ private ResponseType getResponseType(MockCatalinaResponse response, ByteArrayOutputStream bos) { ResponseType responseType = null; try { SAML2Response samlResponse = new SAML2Response(); if (bos == null) { MockCatalinaRequest requestTmp = new MockCatalinaRequest(); AuthenticatorTestUtils.populateParametersWithQueryString(response.redirectString, requestTmp); responseType = (ResponseType) samlResponse.getSAML2ObjectFromStream(RedirectBindingUtil .base64DeflateDecode(requestTmp.getParameter(GeneralConstants.SAML_RESPONSE_KEY))); } else { Document postBindingForm = DocumentUtil.getDocument(bos.toString()); logger.info("POST Binding response from the IDP:"); logger.info(prettyPrintDocument(postBindingForm).toString()); NodeList nodes = postBindingForm.getElementsByTagName("INPUT"); Element inputElement = (Element) nodes.item(0); String idpResponse = inputElement.getAttributeNode("VALUE").getValue(); responseType = (ResponseType) samlResponse.getSAML2ObjectFromStream(PostBindingUtil .base64DecodeAsStream(idpResponse)); } Document convert = samlResponse.convert(responseType); logger.info("ResponseType returned from the IDP:"); System.out.println(prettyPrintDocument(convert)); } catch (Exception e) { e.printStackTrace(); fail("Error getting the ResponseType."); } return responseType; } private ResponseType getResponseTypeAndCheckSignature(MockCatalinaResponse response, ByteArrayOutputStream bos) { ResponseType responseType = getResponseType(response, bos); try { if (bos == null) { assertTrue(RedirectBindingSignatureUtil.validateSignature(response.redirectString, getAuthenticator() .getKeyManager().getPublicKey(CERTIFICATE_ALIAS), RedirectBindingSignatureUtil .getSignatureValueFromSignedURL(response.redirectString))); } else { assertTrue("No Signature element found.", responseType.getSignature() != null); } } catch (Exception e) { e.printStackTrace(); fail("Error checking response signature."); } return responseType; } // We use same URL for assertionConsumerServiceURL and for issuer in this case private void sendAuthenticationRequest(MockCatalinaRequest request, MockCatalinaResponse response, String issuer, boolean signToken) { sendAuthenticationRequest(request, response, issuer, issuer, signToken); } /** * <p> * Sends an authentication request ({@link AuthnRequestType}) to the IDP. * </p> * * @param request * @param response * @param issuer * @param assertionConsumerURL * @param signToken */ private void sendAuthenticationRequest(MockCatalinaRequest request, MockCatalinaResponse response, String issuer, String assertionConsumerURL, boolean signToken) { try { SAML2Request samlRequest = new SAML2Request(); AuthnRequestType authnRequestType = samlRequest.createAuthnRequestType(IDGenerator.create("ID_"), assertionConsumerURL, getAuthenticator().getConfiguration().getIdpOrSP().getIdentityURL(), issuer); Document authnRequestDocument = samlRequest.convert(authnRequestType); logger.info("AuthRequestType:" + prettyPrintDocument(authnRequestDocument).toString()); if (signToken) { request.setQueryString(RedirectBindingSignatureUtil.getSAMLRequestURLWithSignature(authnRequestType, null, getAuthenticator().getKeyManager().getSigningKey())); AuthenticatorTestUtils.populateParametersWithQueryString(request.getQueryString(), request); } else { String deflateBase64URLEncode = RedirectBindingUtil.deflateBase64Encode(DocumentUtil.asString( authnRequestDocument).getBytes("UTF-8")); request.setQueryString("SAMLRequest=" + deflateBase64URLEncode); request.setParameter("SAMLRequest", deflateBase64URLEncode); } getAuthenticator().invoke(request, response); } catch (Exception e) { e.printStackTrace(); fail("Error sending AuthnRequestType."); } } /** * <p> * Creates and returns a instance of {@link IDPWebBrowserSSOValve}. * </p> * * @return */ private IDPWebBrowserSSOValve getAuthenticator() { if (this.identityProvider == null) { this.identityProvider = AuthenticatorTestUtils.createIdentityProvider("saml2/redirect/idp-sig"); } return this.identityProvider; } private StringWriter prettyPrintDocument(Document authnRequestDocument) { StringWriter writer = new StringWriter(); try { Transformer transformer = TransformerUtil.getTransformer(); transformer.setOutputProperty(OutputKeys.INDENT, "yes"); transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2"); transformer.transform(DocumentUtil.getXMLSource(authnRequestDocument), new StreamResult(writer)); } catch (Exception e) { e.printStackTrace(); fail("Error printing the document."); } finally { try { writer.close(); } catch (IOException e) { e.printStackTrace(); } } return writer; } }