| // Copyright 2013 Google Inc. All Rights Reserved. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package com.google.enterprise.adaptor; |
| |
| import static org.junit.Assert.*; |
| |
| import com.google.enterprise.adaptor.SamlServiceProvider.AuthnState; |
| import com.google.enterprise.secmgr.http.HttpClientInterface; |
| import com.google.enterprise.secmgr.modules.SamlClient; |
| import com.google.enterprise.secmgr.saml.OpenSamlUtil; |
| |
| import com.sun.net.httpserver.HttpExchange; |
| |
| import org.junit.*; |
| import org.junit.rules.ExpectedException; |
| |
| import org.opensaml.common.binding.SAMLMessageContext; |
| import org.opensaml.common.xml.SAMLConstants; |
| import org.opensaml.saml2.binding.decoding.HTTPRedirectDeflateDecoder; |
| import org.opensaml.saml2.binding.encoding.HTTPSOAP11Encoder; |
| import org.opensaml.saml2.core.AuthnRequest; |
| import org.opensaml.saml2.core.NameID; |
| |
| import java.io.IOException; |
| import java.net.URI; |
| import java.nio.charset.Charset; |
| import java.util.*; |
| |
| /** |
| * Test cases for {@link SamlServiceProvider}. |
| */ |
| public class SamlServiceProviderTest { |
| @Rule |
| public ExpectedException thrown = ExpectedException.none(); |
| |
| private Charset charset = Charset.forName("UTF-8"); |
| private SessionManager<HttpExchange> sessionManager |
| = new SessionManager<HttpExchange>(new MockTimeProvider(), |
| new SessionManager.HttpExchangeClientStore(), 1000, 1000); |
| private SamlMetadata metadata = new SamlMetadata("localhost", 80, |
| "thegsa", "http://google.com/enterprise/gsa/security-manager", |
| "http://google.com/enterprise/gsa/adaptor"); |
| private HttpClientAdapter httpClient = new HttpClientAdapter(); |
| private SamlServiceProvider serviceProvider = new SamlServiceProvider( |
| sessionManager, metadata, null, httpClient); |
| private MockHttpExchange ex = new MockHttpExchange("GET", "/", |
| new MockHttpContext("/")); |
| private MockHttpExchange exArtifact |
| = new MockHttpExchange("GET", "/?SAMLart=1234someid5678", |
| new MockHttpContext("/")); |
| private MockHttpExchange initialEx |
| = new MockHttpExchange("GET", "/doc/someid", |
| new MockHttpContext("/doc/")); |
| |
| private static final String GOLDEN_ARTIFACT_RESOLVE_REQUEST |
| = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" |
| + "<soap11:Envelope " |
| + "xmlns:soap11=\"http://schemas.xmlsoap.org/soap/envelope/\">" |
| + "<soap11:Body>" |
| + "<saml2p:ArtifactResolve " |
| + "ID=\"someid\" " |
| + "IssueInstant=\"sometime\" " |
| + "Version=\"2.0\" " |
| + "xmlns:saml2p=\"urn:oasis:names:tc:SAML:2.0:protocol\">" |
| + "<saml2:Issuer " |
| + "xmlns:saml2=\"urn:oasis:names:tc:SAML:2.0:assertion\">" |
| + "http://google.com/enterprise/gsa/adaptor" |
| + "</saml2:Issuer>" |
| + "<saml2p:Artifact>" |
| + "1234someid5678" |
| + "</saml2p:Artifact>" |
| + "</saml2p:ArtifactResolve>" |
| + "</soap11:Body>" |
| + "</soap11:Envelope>"; |
| |
| @BeforeClass |
| public static void initSaml() { |
| GsaCommunicationHandler.bootstrapOpenSaml(); |
| } |
| |
| @Test |
| public void testHandleAuthenticationNewSession() throws Exception { |
| String goldenResponse |
| = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" |
| + "<soap11:Envelope " |
| + "xmlns:soap11=\"http://schemas.xmlsoap.org/soap/envelope/\">" |
| + "<soap11:Body>" |
| + "<saml2p:AuthnRequest " |
| + "Destination=\"https://thegsa/security-manager/samlauthn\" " |
| + "ID=\"someid\" " |
| + "IsPassive=\"false\" " |
| + "IssueInstant=\"sometime\" " |
| + "ProviderName=\"GSA Adaptor\" " |
| + "Version=\"2.0\" " |
| + "xmlns:saml2p=\"urn:oasis:names:tc:SAML:2.0:protocol\">" |
| + "<saml2:Issuer " |
| + "xmlns:saml2=\"urn:oasis:names:tc:SAML:2.0:assertion\">" |
| + "http://google.com/enterprise/gsa/adaptor" |
| + "</saml2:Issuer>" |
| + "</saml2p:AuthnRequest>" |
| + "</soap11:Body>" |
| + "</soap11:Envelope>"; |
| |
| serviceProvider.handleAuthentication(ex); |
| |
| // |
| // We have run the method, now we need to look over the results... |
| // |
| assertEquals(307, ex.getResponseCode()); |
| URI uri = new URI(ex.getResponseHeaders().getFirst("Location")); |
| assertEquals("https", uri.getScheme()); |
| assertEquals("thegsa", uri.getHost()); |
| assertEquals("/security-manager/samlauthn", uri.getPath()); |
| assertTrue(!isAuthned(ex)); |
| |
| // Act like we are the receiving end of the communication. |
| MockHttpExchange remoteEx = new MockHttpExchange("GET", uri.toString(), |
| new MockHttpContext("/security-manager/samlauthn")); |
| |
| SAMLMessageContext<AuthnRequest, AuthnRequest, NameID> context |
| = OpenSamlUtil.makeSamlMessageContext(); |
| OpenSamlUtil.initializeLocalEntity(context, metadata.getPeerEntity(), |
| metadata.getPeerEntity().getIDPSSODescriptor(SAMLConstants.SAML20P_NS)); |
| context.setInboundMessageTransport(new EnhancedInTransport(remoteEx)); |
| context.setOutboundMessageTransport( |
| new HttpExchangeOutTransportAdapter(remoteEx)); |
| |
| HTTPRedirectDeflateDecoder decoder = new RedirectDecoder(); |
| decoder.decode(context); |
| context.setOutboundSAMLMessage(context.getInboundSAMLMessage()); |
| |
| HTTPSOAP11Encoder encoder = new HTTPSOAP11Encoder(); |
| encoder.encode(context); |
| |
| String response = new String(remoteEx.getResponseBytes(), "UTF-8"); |
| response = massageMessage(response); |
| assertEquals(goldenResponse, response); |
| } |
| |
| @Test |
| public void testHandleAuthenticationAlreadyAuthned() throws Exception { |
| // Create a new authenticated session for this HttpExchange. |
| Session session = sessionManager.getSession(ex, true); |
| AuthnState authn = new AuthnState(); |
| session.setAttribute(SamlServiceProvider.SESSION_STATE_ATTR_NAME, authn); |
| AuthnIdentity identity = new AuthnIdentityImpl |
| .Builder(new UserPrincipal("test")).build(); |
| authn.authenticated(identity, Long.MAX_VALUE); |
| serviceProvider.handleAuthentication(ex); |
| // Still should cause them to go through authn |
| assertEquals(307, ex.getResponseCode()); |
| assertTrue(!isAuthned(ex)); |
| } |
| |
| @Test |
| public void testHandleAuthenticationHead() throws Exception { |
| MockHttpExchange ex = new MockHttpExchange("HEAD", "/", |
| new MockHttpContext("/")); |
| serviceProvider.handleAuthentication(ex); |
| assertEquals(307, ex.getResponseCode()); |
| assertTrue(!isAuthned(ex)); |
| } |
| |
| @Test |
| public void testHandleAuthenticationBadMethod() throws Exception { |
| MockHttpExchange ex = new MockHttpExchange("POST", "/", |
| new MockHttpContext("/")); |
| serviceProvider.handleAuthentication(ex); |
| assertEquals(405, ex.getResponseCode()); |
| } |
| |
| @Test |
| public void testAssertionConsumerNormal() throws Exception { |
| SamlHttpClient httpClient = new SamlHttpClient() { |
| @Override |
| protected void handleExchange(ClientExchange ex) { |
| String body = new String(ex.getRequestBody(), charset); |
| body = massageMessage(body); |
| assertEquals(GOLDEN_ARTIFACT_RESOLVE_REQUEST, body); |
| |
| // Generate valid response. |
| String issuer = metadata.getPeerEntity().getEntityID(); |
| String recipient = metadata.getLocalEntity() |
| .getSPSSODescriptor(SAMLConstants.SAML20P_NS) |
| .getAssertionConsumerServices().get(0).getLocation(); |
| String audience = metadata.getLocalEntity().getEntityID(); |
| String response |
| = "<SOAP-ENV:Envelope " |
| + "xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\">" |
| + "<SOAP-ENV:Body>" |
| + "<samlp:ArtifactResponse " |
| + "xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" " |
| + "xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" " |
| + "ID=\"someid1\" Version=\"2.0\" " |
| + "InResponseTo=\"" + samlClient.getRequestId() + "\" " |
| + "IssueInstant=\"2010-01-01T01:01:01Z\">" |
| + "<Issuer>" + issuer + "</Issuer>" |
| + "<samlp:Status>" |
| + "<samlp:StatusCode " |
| + "Value=\"urn:oasis:names:tc:SAML:2.0:status:Success\"/>" |
| + "</samlp:Status>" |
| + "<samlp:Response " |
| + "ID=\"someid2\" " |
| + "Version=\"2.0\" " |
| + "IssueInstant=\"2010-01-01T01:01:01Z\">" |
| + "<samlp:Status>" |
| + "<samlp:StatusCode " |
| + "Value=\"urn:oasis:names:tc:SAML:2.0:status:Success\"/>" |
| + "</samlp:Status>" |
| + "<Assertion " |
| + "Version=\"2.0\" " |
| + "ID=\"someid3\" " |
| + "IssueInstant=\"2010-01-01T01:01:01Z\">" |
| + "<Issuer>" + issuer + "</Issuer>" |
| + "<Subject>" |
| + "<NameID>CN=Polly Hedra</NameID>" |
| + "<SubjectConfirmation " |
| + "Method=\"urn:oasis:names:tc:SAML:2.0:cm:bearer\">" |
| + "<SubjectConfirmationData " |
| + "InResponseTo=\"" + samlClient.getRequestId() + "\" " |
| + "Recipient=\"" + recipient + "\" " |
| + "NotOnOrAfter=\"2030-01-01T01:01:01Z\"/>" |
| + "</SubjectConfirmation>" |
| + "</Subject>" |
| + "<Conditions " |
| + "NotBefore=\"2010-01-01T01:01:01Z\">" |
| + "<AudienceRestriction>" |
| + "<Audience>" + audience + "</Audience>" |
| + "</AudienceRestriction>" |
| + "</Conditions>" |
| + "<AuthnStatement " |
| + "AuthnInstant=\"2010-01-01T01:01:01Z\"/>" |
| + "</Assertion>" |
| + "</samlp:Response>" |
| + "</samlp:ArtifactResponse>" |
| + "</SOAP-ENV:Body>" |
| + "</SOAP-ENV:Envelope>"; |
| ex.setStatusCode(200); |
| ex.setResponseStream(response.getBytes(charset)); |
| } |
| }; |
| SamlClient samlClient = createSamlClient(httpClient); |
| httpClient.setSamlClient(samlClient); |
| issueRequest(exArtifact, initialEx, samlClient); |
| |
| serviceProvider.getAssertionConsumer().handle(exArtifact); |
| assertEquals(303, exArtifact.getResponseCode()); |
| assertTrue(isAuthned(exArtifact)); |
| AuthnIdentity identity = serviceProvider.getUserIdentity(exArtifact); |
| assertEquals("CN=Polly Hedra", identity.getUser().getName()); |
| assertNull(identity.getGroups()); |
| assertNull(identity.getPassword()); |
| } |
| |
| @Test |
| public void testAssertionConsumerNormalWithExtension() throws Exception { |
| SamlHttpClient httpClient = new SamlHttpClient() { |
| @Override |
| protected void handleExchange(ClientExchange ex) { |
| String body = new String(ex.getRequestBody(), charset); |
| body = massageMessage(body); |
| assertEquals(GOLDEN_ARTIFACT_RESOLVE_REQUEST, body); |
| |
| // Generate valid response. |
| String issuer = metadata.getPeerEntity().getEntityID(); |
| String recipient = metadata.getLocalEntity() |
| .getSPSSODescriptor(SAMLConstants.SAML20P_NS) |
| .getAssertionConsumerServices().get(0).getLocation(); |
| String audience = metadata.getLocalEntity().getEntityID(); |
| String response |
| = "<SOAP-ENV:Envelope " |
| + "xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\">" |
| + "<SOAP-ENV:Body>" |
| + "<samlp:ArtifactResponse " |
| + "xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" " |
| + "xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" " |
| + "ID=\"someid1\" Version=\"2.0\" " |
| + "InResponseTo=\"" + samlClient.getRequestId() + "\" " |
| + "IssueInstant=\"2010-01-01T01:01:01Z\">" |
| + "<Issuer>" + issuer + "</Issuer>" |
| + "<samlp:Status>" |
| + "<samlp:StatusCode " |
| + "Value=\"urn:oasis:names:tc:SAML:2.0:status:Success\"/>" |
| + "</samlp:Status>" |
| + "<samlp:Response " |
| + "ID=\"someid2\" " |
| + "Version=\"2.0\" " |
| + "IssueInstant=\"2010-01-01T01:01:01Z\">" |
| + "<samlp:Status>" |
| + "<samlp:StatusCode " |
| + "Value=\"urn:oasis:names:tc:SAML:2.0:status:Success\"/>" |
| + "</samlp:Status>" |
| + "<Assertion " |
| + "Version=\"2.0\" " |
| + "ID=\"someid3\" " |
| + "IssueInstant=\"2010-01-01T01:01:01Z\">" |
| + "<Issuer>" + issuer + "</Issuer>" |
| + "<Subject>" |
| + "<NameID>CN=Polly Hedra</NameID>" |
| + "<SubjectConfirmation " |
| + "Method=\"urn:oasis:names:tc:SAML:2.0:cm:bearer\">" |
| + "<SubjectConfirmationData " |
| + "InResponseTo=\"" + samlClient.getRequestId() + "\" " |
| + "Recipient=\"" + recipient + "\" " |
| + "NotOnOrAfter=\"2030-01-01T01:01:01Z\"/>" |
| + "</SubjectConfirmation>" |
| + "</Subject>" |
| + "<Conditions " |
| + "NotBefore=\"2010-01-01T01:01:01Z\">" |
| + "<AudienceRestriction>" |
| + "<Audience>" + audience + "</Audience>" |
| + "</AudienceRestriction>" |
| + "</Conditions>" |
| + "<AuthnStatement " |
| + "AuthnInstant=\"2010-01-01T01:01:01Z\">" |
| + "<AuthnContext>" |
| + "<AuthnContextClassRef>" |
| + "urn:oasis:names:tc:SAML:2.0:ac:classes:InternetProtocolPassword" |
| + "</AuthnContextClassRef>" |
| + "</AuthnContext>" |
| + "</AuthnStatement>" |
| + "<AttributeStatement>" |
| + "<Attribute Name=\"SecurityManagerState\">" |
| + "<AttributeValue>" |
| + "{" |
| + "\"version\": 1," |
| + "\"timeStamp\": 1330042321589," |
| + "\"sessionState\": {" |
| + "\"instructions\": [" |
| + "{" |
| + "\"operation\": \"ADD_CREDENTIAL\"," |
| + "\"authority\": " |
| + "\"http://google.com/enterprise/gsa/security-manager/Default\"," |
| + "\"operand\": {" |
| + "\"name\": \"CN=Polly Hedra\"," |
| + "\"typeName\": \"AuthnPrincipal\"" |
| + "}" |
| + "}," |
| + "{" |
| + "\"operation\": \"ADD_CREDENTIAL\"," |
| + "\"authority\": " |
| + "\"http://google.com/enterprise/gsa/security-manager/Default\"," |
| + "\"operand\": {" |
| + "\"password\": \"p0ck3t\"," |
| + "\"typeName\": \"CredPassword\"" |
| + "}" |
| + "}," |
| + "{" |
| + "\"operation\": \"ADD_VERIFICATION\"," |
| + "\"authority\": " |
| + "\"http://google.com/enterprise/gsa/security-manager/adaptor\"," |
| + "\"operand\": {" |
| + "\"status\": \"VERIFIED\"," |
| + "\"expirationTime\": 1330043521581," |
| + "\"credentials\": [" |
| + "{" |
| + "\"name\": \"CN=Polly Hedra\"," |
| + "\"typeName\": \"AuthnPrincipal\"" |
| + "}," |
| + "{" |
| + "\"password\": \"p0ck3t\"," |
| + "\"typeName\": \"CredPassword\"" |
| + "}" |
| + "]" |
| + "}" |
| + "}" |
| + "]" |
| + "}," |
| + "\"pviCredentials\": {" |
| + "\"username\": \"CN=Polly Hedra\"," |
| + "\"password\": \"p0ck3t\"," |
| + "\"groups\": [\"group1\", \"pollysGroup\"]" |
| + "}," |
| + "\"basicCredentials\": {" |
| + "\"username\": \"CN=Polly Hedra\"," |
| + "\"password\": \"p0ck3t\"," |
| + "\"groups\": [\"group1\", \"pollysGroup\"]" |
| + "}," |
| + "\"verifiedCredentials\": [" |
| + "{" |
| + "\"username\": \"CN=Polly Hedra\"," |
| + "\"password\": \"p0ck3t\"," |
| + "\"groups\": [\"group1\", \"pollysGroup\"]" |
| + "}" |
| + "]," |
| + "\"connectorCredentials\": []," |
| + "\"cookies\": []" |
| + "}" |
| + "</AttributeValue>" |
| + "</Attribute>" |
| + "</AttributeStatement>" |
| + "</Assertion>" |
| + "</samlp:Response>" |
| + "</samlp:ArtifactResponse>" |
| + "</SOAP-ENV:Body>" |
| + "</SOAP-ENV:Envelope>"; |
| ex.setStatusCode(200); |
| ex.setResponseStream(response.getBytes(charset)); |
| } |
| }; |
| SamlClient samlClient = createSamlClient(httpClient); |
| httpClient.setSamlClient(samlClient); |
| issueRequest(exArtifact, initialEx, samlClient); |
| |
| serviceProvider.getAssertionConsumer().handle(exArtifact); |
| assertEquals(303, exArtifact.getResponseCode()); |
| assertTrue(isAuthned(exArtifact)); |
| AuthnIdentity identity = serviceProvider.getUserIdentity(exArtifact); |
| assertEquals("CN=Polly Hedra", identity.getUser().getName()); |
| // Make sure that the information from the extensions was parsed out and |
| // made available for later use. |
| Set<String> groups = new HashSet<String>(); |
| groups.add("group1"); |
| groups.add("pollysGroup"); |
| assertEquals(GroupPrincipal.makeSet(groups), identity.getGroups()); |
| assertEquals("p0ck3t", identity.getPassword()); |
| } |
| |
| @Test |
| public void testAssertionConsumerAuthnFailure() throws Exception { |
| SamlHttpClient httpClient = new SamlHttpClient() { |
| @Override |
| protected void handleExchange(ClientExchange ex) { |
| String body = new String(ex.getRequestBody(), charset); |
| body = massageMessage(body); |
| assertEquals(GOLDEN_ARTIFACT_RESOLVE_REQUEST, body); |
| |
| // Generate valid response for authn failed. |
| String issuer = metadata.getPeerEntity().getEntityID(); |
| String response |
| = "<SOAP-ENV:Envelope " |
| + "xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\">" |
| + "<SOAP-ENV:Body>" |
| + "<samlp:ArtifactResponse " |
| + "xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" " |
| + "xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" " |
| + "ID=\"someid1\" Version=\"2.0\" " |
| + "InResponseTo=\"" + samlClient.getRequestId() + "\" " |
| + "IssueInstant=\"2010-01-01T01:01:01Z\">" |
| + "<Issuer>" + issuer + "</Issuer>" |
| + "<samlp:Status>" |
| + "<samlp:StatusCode " |
| + "Value=\"urn:oasis:names:tc:SAML:2.0:status:Success\"/>" |
| + "</samlp:Status>" |
| + "<samlp:Response " |
| + "ID=\"someid2\" " |
| + "Version=\"2.0\" " |
| + "IssueInstant=\"2010-01-01T01:01:01Z\">" |
| + "<samlp:Status>" |
| + "<samlp:StatusCode " |
| + "Value=\"urn:oasis:names:tc:SAML:2.0:status:AuthnFailed\"/>" |
| + "</samlp:Status>" |
| + "</samlp:Response>" |
| + "</samlp:ArtifactResponse>" |
| + "</SOAP-ENV:Body>" |
| + "</SOAP-ENV:Envelope>"; |
| ex.setStatusCode(200); |
| ex.setResponseStream(response.getBytes(charset)); |
| } |
| }; |
| SamlClient samlClient = createSamlClient(httpClient); |
| httpClient.setSamlClient(samlClient); |
| issueRequest(exArtifact, initialEx, samlClient); |
| |
| serviceProvider.getAssertionConsumer().handle(exArtifact); |
| assertEquals(403, exArtifact.getResponseCode()); |
| assertTrue(!isAuthned(exArtifact)); |
| } |
| |
| @Test |
| public void testAssertionConsumerNoAuthnResponse() throws Exception { |
| SamlHttpClient httpClient = new SamlHttpClient() { |
| @Override |
| protected void handleExchange(ClientExchange ex) { |
| String body = new String(ex.getRequestBody(), charset); |
| body = massageMessage(body); |
| assertEquals(GOLDEN_ARTIFACT_RESOLVE_REQUEST, body); |
| |
| // Generate valid response for artifact requesting failure. |
| String issuer = metadata.getPeerEntity().getEntityID(); |
| String response |
| = "<SOAP-ENV:Envelope " |
| + "xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\">" |
| + "<SOAP-ENV:Body>" |
| + "<samlp:ArtifactResponse " |
| + "xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" " |
| + "xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" " |
| + "ID=\"someid1\" Version=\"2.0\" " |
| + "InResponseTo=\"" + samlClient.getRequestId() + "\" " |
| + "IssueInstant=\"2010-01-01T01:01:01Z\">" |
| + "<Issuer>" + issuer + "</Issuer>" |
| + "<samlp:Status>" |
| + "<samlp:StatusCode " |
| + "Value=\"urn:oasis:names:tc:SAML:2.0:status:Requester\"/>" |
| + "</samlp:Status>" |
| + "</samlp:ArtifactResponse>" |
| + "</SOAP-ENV:Body>" |
| + "</SOAP-ENV:Envelope>"; |
| ex.setStatusCode(200); |
| ex.setResponseStream(response.getBytes(charset)); |
| } |
| }; |
| SamlClient samlClient = createSamlClient(httpClient); |
| httpClient.setSamlClient(samlClient); |
| issueRequest(exArtifact, initialEx, samlClient); |
| |
| serviceProvider.getAssertionConsumer().handle(exArtifact); |
| assertEquals(403, exArtifact.getResponseCode()); |
| assertTrue(!isAuthned(exArtifact)); |
| } |
| |
| @Test |
| public void testAssertionConsumerWrongResponse() throws Exception { |
| SamlHttpClient httpClient = new SamlHttpClient() { |
| @Override |
| protected void handleExchange(ClientExchange ex) { |
| String body = new String(ex.getRequestBody(), charset); |
| body = massageMessage(body); |
| assertEquals(GOLDEN_ARTIFACT_RESOLVE_REQUEST, body); |
| |
| // Generate valid response for successful authn, but for the wrong |
| // request. |
| String issuer = metadata.getPeerEntity().getEntityID(); |
| String recipient = metadata.getLocalEntity() |
| .getSPSSODescriptor(SAMLConstants.SAML20P_NS) |
| .getAssertionConsumerServices().get(0).getLocation(); |
| String audience = metadata.getLocalEntity().getEntityID(); |
| String response |
| = "<SOAP-ENV:Envelope " |
| + "xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\">" |
| + "<SOAP-ENV:Body>" |
| + "<samlp:ArtifactResponse " |
| + "xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" " |
| + "xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" " |
| + "ID=\"someid1\" Version=\"2.0\" " |
| + "InResponseTo=\"" + samlClient.getRequestId() + "\" " |
| + "IssueInstant=\"2010-01-01T01:01:01Z\">" |
| + "<Issuer>" + issuer + "</Issuer>" |
| + "<samlp:Status>" |
| + "<samlp:StatusCode " |
| + "Value=\"urn:oasis:names:tc:SAML:2.0:status:Success\"/>" |
| + "</samlp:Status>" |
| + "<samlp:Response " |
| + "ID=\"someid2\" " |
| + "Version=\"2.0\" " |
| + "IssueInstant=\"2010-01-01T01:01:01Z\">" |
| + "<samlp:Status>" |
| + "<samlp:StatusCode " |
| + "Value=\"urn:oasis:names:tc:SAML:2.0:status:Success\"/>" |
| + "</samlp:Status>" |
| + "<Assertion " |
| + "Version=\"2.0\" " |
| + "ID=\"someid3\" " |
| + "IssueInstant=\"2010-01-01T01:01:01Z\">" |
| + "<Issuer>" + issuer + "</Issuer>" |
| + "<Subject>" |
| + "<NameID>CN=Polly Hedra</NameID>" |
| + "<SubjectConfirmation " |
| + "Method=\"urn:oasis:names:tc:SAML:2.0:cm:bearer\">" |
| + "<SubjectConfirmationData " |
| + "InResponseTo=\"notthis" + samlClient.getRequestId() + "\" " |
| + "Recipient=\"" + recipient + "\" " |
| + "NotOnOrAfter=\"2030-01-01T01:01:01Z\"/>" |
| + "</SubjectConfirmation>" |
| + "</Subject>" |
| + "<Conditions " |
| + "NotBefore=\"2010-01-01T01:01:01Z\">" |
| + "<AudienceRestriction>" |
| + "<Audience>" + audience + "</Audience>" |
| + "</AudienceRestriction>" |
| + "</Conditions>" |
| + "<AuthnStatement " |
| + "AuthnInstant=\"2010-01-01T01:01:01Z\"/>" |
| + "</Assertion>" |
| + "</samlp:Response>" |
| + "</samlp:ArtifactResponse>" |
| + "</SOAP-ENV:Body>" |
| + "</SOAP-ENV:Envelope>"; |
| ex.setStatusCode(200); |
| ex.setResponseStream(response.getBytes(charset)); |
| } |
| }; |
| SamlClient samlClient = createSamlClient(httpClient); |
| httpClient.setSamlClient(samlClient); |
| issueRequest(exArtifact, initialEx, samlClient); |
| |
| serviceProvider.getAssertionConsumer().handle(exArtifact); |
| assertEquals(403, exArtifact.getResponseCode()); |
| assertTrue(!isAuthned(exArtifact)); |
| } |
| |
| @Test |
| public void testAssertionConsumerWrongIssuer() throws Exception { |
| SamlHttpClient httpClient = new SamlHttpClient() { |
| @Override |
| protected void handleExchange(ClientExchange ex) { |
| String body = new String(ex.getRequestBody(), charset); |
| body = massageMessage(body); |
| assertEquals(GOLDEN_ARTIFACT_RESOLVE_REQUEST, body); |
| |
| // Generate valid response, but from wrong issuer. |
| String issuer = metadata.getPeerEntity().getEntityID(); |
| String recipient = metadata.getLocalEntity() |
| .getSPSSODescriptor(SAMLConstants.SAML20P_NS) |
| .getAssertionConsumerServices().get(0).getLocation(); |
| String audience = metadata.getLocalEntity().getEntityID(); |
| String response |
| = "<SOAP-ENV:Envelope " |
| + "xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\">" |
| + "<SOAP-ENV:Body>" |
| + "<samlp:ArtifactResponse " |
| + "xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" " |
| + "xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" " |
| + "ID=\"someid1\" Version=\"2.0\" " |
| + "InResponseTo=\"" + samlClient.getRequestId() + "\" " |
| + "IssueInstant=\"2010-01-01T01:01:01Z\">" |
| + "<Issuer>" + issuer + "</Issuer>" |
| + "<samlp:Status>" |
| + "<samlp:StatusCode " |
| + "Value=\"urn:oasis:names:tc:SAML:2.0:status:Success\"/>" |
| + "</samlp:Status>" |
| + "<samlp:Response " |
| + "ID=\"someid2\" " |
| + "Version=\"2.0\" " |
| + "IssueInstant=\"2010-01-01T01:01:01Z\">" |
| // Add an issuer that is not the expected one. |
| + "<Issuer>notthexpected" + issuer + "</Issuer>" |
| + "<samlp:Status>" |
| + "<samlp:StatusCode " |
| + "Value=\"urn:oasis:names:tc:SAML:2.0:status:Success\"/>" |
| + "</samlp:Status>" |
| + "<Assertion " |
| + "Version=\"2.0\" " |
| + "ID=\"someid3\" " |
| + "IssueInstant=\"2010-01-01T01:01:01Z\">" |
| + "<Issuer>" + issuer + "</Issuer>" |
| + "<Subject>" |
| + "<NameID>CN=Polly Hedra</NameID>" |
| + "<SubjectConfirmation " |
| + "Method=\"urn:oasis:names:tc:SAML:2.0:cm:bearer\">" |
| + "<SubjectConfirmationData " |
| + "InResponseTo=\"" + samlClient.getRequestId() + "\" " |
| + "Recipient=\"" + recipient + "\" " |
| + "NotOnOrAfter=\"2030-01-01T01:01:01Z\"/>" |
| + "</SubjectConfirmation>" |
| + "</Subject>" |
| + "<Conditions " |
| + "NotBefore=\"2010-01-01T01:01:01Z\">" |
| + "<AudienceRestriction>" |
| + "<Audience>" + audience + "</Audience>" |
| + "</AudienceRestriction>" |
| + "</Conditions>" |
| + "<AuthnStatement " |
| + "AuthnInstant=\"2010-01-01T01:01:01Z\"/>" |
| + "</Assertion>" |
| + "</samlp:Response>" |
| + "</samlp:ArtifactResponse>" |
| + "</SOAP-ENV:Body>" |
| + "</SOAP-ENV:Envelope>"; |
| ex.setStatusCode(200); |
| ex.setResponseStream(response.getBytes(charset)); |
| } |
| }; |
| SamlClient samlClient = createSamlClient(httpClient); |
| httpClient.setSamlClient(samlClient); |
| issueRequest(exArtifact, initialEx, samlClient); |
| |
| serviceProvider.getAssertionConsumer().handle(exArtifact); |
| assertEquals(403, exArtifact.getResponseCode()); |
| assertTrue(!isAuthned(exArtifact)); |
| } |
| |
| @Test |
| public void testAssertionConsumerAlreadyAuthned() throws Exception { |
| MockHttpClient httpClient = new MockHttpClient() { |
| @Override |
| protected void handleExchange(ClientExchange ex) { |
| fail("No request should have been issued."); |
| } |
| }; |
| SamlClient samlClient = createSamlClient(httpClient); |
| issueRequest(exArtifact, initialEx, samlClient); |
| |
| // Authenticate the session. |
| Session session = sessionManager.getSession(exArtifact, false); |
| AuthnState authn = (AuthnState) session |
| .getAttribute(SamlServiceProvider.SESSION_STATE_ATTR_NAME); |
| AuthnIdentity identity = new AuthnIdentityImpl |
| .Builder(new UserPrincipal("test")).build(); |
| authn.authenticated(identity, Long.MAX_VALUE); |
| |
| serviceProvider.getAssertionConsumer().handle(exArtifact); |
| assertEquals(409, exArtifact.getResponseCode()); |
| assertTrue(isAuthned(exArtifact)); |
| } |
| |
| @Test |
| public void testAssertionConsumerNoSession() throws Exception { |
| serviceProvider.getAssertionConsumer().handle(exArtifact); |
| assertEquals(409, exArtifact.getResponseCode()); |
| assertTrue(!isAuthned(exArtifact)); |
| } |
| |
| @Test |
| public void testAssertionConsumerUnrequestedAuthnResponse() throws Exception { |
| AuthnState authnState = new AuthnState(); |
| sessionManager.getSession(exArtifact).setAttribute( |
| SamlServiceProvider.SESSION_STATE_ATTR_NAME, authnState); |
| |
| serviceProvider.getAssertionConsumer().handle(exArtifact); |
| assertEquals(500, exArtifact.getResponseCode()); |
| assertTrue(!isAuthned(exArtifact)); |
| } |
| |
| @Test |
| public void testAssertionConsumerPost() throws Exception { |
| MockHttpExchange ex |
| = new MockHttpExchange("POST", "/?SAMLart=1234someid5678", |
| new MockHttpContext("/")); |
| serviceProvider.getAssertionConsumer().handle(ex); |
| assertEquals(405, ex.getResponseCode()); |
| assertTrue(!isAuthned(ex)); |
| } |
| |
| private SamlClient createSamlClient(HttpClientInterface httpClient) { |
| return new SamlClient(metadata.getLocalEntity(), metadata.getPeerEntity(), |
| "Testing", null, httpClient); |
| } |
| |
| private void issueRequest(HttpExchange ex, HttpExchange initialEx, |
| SamlClient samlClient) throws IOException { |
| AuthnState authnState = new AuthnState(); |
| authnState.startAttempt(samlClient, ex.getRequestURI()); |
| sessionManager.getSession(ex).setAttribute( |
| SamlServiceProvider.SESSION_STATE_ATTR_NAME, authnState); |
| // Used to generate a request id. |
| samlClient.sendAuthnRequest(new HttpExchangeOutTransportAdapter(initialEx)); |
| } |
| |
| private boolean isAuthned(HttpExchange ex) { |
| return serviceProvider.getUserIdentity(ex) != null; |
| } |
| |
| private String massageMessage(String message) { |
| return message.replaceAll("ID=\"[^\"]+\"", "ID=\"someid\"") |
| .replaceAll("IssueInstant=\"[^\"]+\"", "IssueInstant=\"sometime\""); |
| } |
| |
| private static class EnhancedInTransport |
| extends HttpExchangeInTransportAdapter { |
| private Map<String, List<String>> parameters; |
| |
| public EnhancedInTransport(HttpExchange ex) { |
| super(ex); |
| parameters = parseParameters(); |
| } |
| |
| private Map<String, List<String>> parseParameters() { |
| Map<String, List<String>> params = new HashMap<String, List<String>>(); |
| String query = ex.getRequestURI().getQuery(); |
| if (query == null) { |
| return params; |
| } |
| // This is not fully correct, but good enough for the test case. |
| for (String param : query.split("&")) { |
| String[] split = query.split("=", 2); |
| if (!params.containsKey(split[0])) { |
| params.put(split[0], new LinkedList<String>()); |
| } |
| params.get(split[0]).add(split.length == 2 ? split[1] : ""); |
| } |
| return params; |
| } |
| |
| @Override |
| public String getParameterValue(String name) { |
| List<String> values = getParameterValues(name); |
| return values.size() == 0 ? null : values.get(0); |
| } |
| |
| @Override |
| public List<String> getParameterValues(String name) { |
| if (parameters.containsKey(name)) { |
| return parameters.get(name); |
| } else { |
| return Collections.emptyList(); |
| } |
| } |
| |
| public HttpExchange getExchange() { |
| return ex; |
| } |
| } |
| |
| private static class RedirectDecoder extends HTTPRedirectDeflateDecoder { |
| @Override |
| protected String getActualReceiverEndpointURI( |
| SAMLMessageContext messageContext) { |
| EnhancedInTransport inTransport = (EnhancedInTransport) messageContext |
| .getInboundMessageTransport(); |
| String requestUri = inTransport.getExchange().getRequestURI().toString(); |
| // Remove query from URI |
| return requestUri.split("\\?", 2)[0]; |
| } |
| } |
| |
| private abstract static class SamlHttpClient extends MockHttpClient { |
| protected SamlClient samlClient; |
| |
| public void setSamlClient(SamlClient samlClient) { |
| this.samlClient = samlClient; |
| } |
| } |
| } |