| // Copyright 2011 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 adaptorlib; |
| |
| import static org.junit.Assert.*; |
| |
| import com.google.enterprise.secmgr.http.HttpClientInterface; |
| import com.google.enterprise.secmgr.modules.SamlClient; |
| |
| import com.sun.net.httpserver.HttpExchange; |
| |
| import org.junit.*; |
| |
| import org.opensaml.common.xml.SAMLConstants; |
| |
| import java.io.IOException; |
| import java.nio.charset.Charset; |
| import java.util.Collections; |
| |
| /** |
| * Test cases for {@link SamlAssertionConsumerHandler}. |
| */ |
| public class SamlAssertionConsumerHandlerTest { |
| 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"); |
| private SamlAssertionConsumerHandler handler |
| = new SamlAssertionConsumerHandler("localhost", charset, sessionManager); |
| private MockHttpExchange ex |
| = new MockHttpExchange("https", "GET", "/?SAMLart=1234someid5678", |
| new MockHttpContext(null, "/")); |
| private MockHttpExchange initialEx |
| = new MockHttpExchange("https", "GET", "/doc/someid", |
| new MockHttpContext(null, "/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 testNormal() 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(ex, initialEx, samlClient); |
| |
| handler.handle(ex); |
| assertEquals(303, ex.getResponseCode()); |
| assertTrue(isAuthned(sessionManager.getSession(ex))); |
| } |
| |
| @Test |
| public void testAuthnFailure() 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(ex, initialEx, samlClient); |
| |
| handler.handle(ex); |
| assertEquals(403, ex.getResponseCode()); |
| assertTrue(!isAuthned(sessionManager.getSession(ex))); |
| } |
| |
| @Test |
| public void testNoAuthnResponse() 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(ex, initialEx, samlClient); |
| |
| handler.handle(ex); |
| assertEquals(403, ex.getResponseCode()); |
| assertTrue(!isAuthned(sessionManager.getSession(ex))); |
| } |
| |
| @Test |
| public void testWrongResponse() 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(ex, initialEx, samlClient); |
| |
| handler.handle(ex); |
| assertEquals(403, ex.getResponseCode()); |
| assertTrue(!isAuthned(sessionManager.getSession(ex))); |
| } |
| |
| @Test |
| public void testWrongIssuer() 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(ex, initialEx, samlClient); |
| |
| handler.handle(ex); |
| assertEquals(403, ex.getResponseCode()); |
| assertTrue(!isAuthned(sessionManager.getSession(ex))); |
| } |
| |
| @Test |
| public void testAlreadyAuthned() throws Exception { |
| MockHttpClient httpClient = new MockHttpClient() { |
| @Override |
| protected void handleExchange(ClientExchange ex) { |
| fail("No request should have been issued."); |
| } |
| }; |
| SamlClient samlClient = createSamlClient(httpClient); |
| issueRequest(ex, initialEx, samlClient); |
| |
| // Authenticate the session. |
| Session session = sessionManager.getSession(ex, false); |
| AuthnState authn = (AuthnState) session |
| .getAttribute(AuthnState.SESSION_ATTR_NAME); |
| authn.authenticated("test", Collections.<String>emptySet(), Long.MAX_VALUE); |
| |
| handler.handle(ex); |
| assertEquals(409, ex.getResponseCode()); |
| assertTrue(isAuthned(sessionManager.getSession(ex))); |
| } |
| |
| @Test |
| public void testNoSession() throws Exception { |
| handler.handle(ex); |
| assertEquals(409, ex.getResponseCode()); |
| assertTrue(!isAuthned(sessionManager.getSession(ex))); |
| } |
| |
| @Test |
| public void testUnrequestedAuthnResponse() throws Exception { |
| AuthnState authnState = new AuthnState(); |
| sessionManager.getSession(ex).setAttribute(AuthnState.SESSION_ATTR_NAME, |
| authnState); |
| |
| handler.handle(ex); |
| assertEquals(500, ex.getResponseCode()); |
| assertTrue(!isAuthned(sessionManager.getSession(ex))); |
| } |
| |
| @Test |
| public void testPost() throws Exception { |
| MockHttpExchange ex |
| = new MockHttpExchange("http", "POST", "/?SAMLart=1234someid5678", |
| new MockHttpContext(null, "/")); |
| handler.handle(ex); |
| assertEquals(405, ex.getResponseCode()); |
| assertTrue(!isAuthned(sessionManager.getSession(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(AuthnState.SESSION_ATTR_NAME, |
| authnState); |
| // Used to generate a request id. |
| samlClient.sendAuthnRequest(new HttpExchangeOutTransportAdapter(initialEx)); |
| } |
| |
| private boolean isAuthned(Session session) { |
| AuthnState authnState = (AuthnState) session |
| .getAttribute(AuthnState.SESSION_ATTR_NAME); |
| if (authnState == null) { |
| return false; |
| } |
| return authnState.isAuthenticated(); |
| } |
| |
| private String massageMessage(String message) { |
| return message.replaceAll("ID=\"[^\"]+\"", "ID=\"someid\"") |
| .replaceAll("IssueInstant=\"[^\"]+\"", "IssueInstant=\"sometime\""); |
| } |
| |
| private abstract static class SamlHttpClient extends MockHttpClient { |
| protected SamlClient samlClient; |
| |
| public void setSamlClient(SamlClient samlClient) { |
| this.samlClient = samlClient; |
| } |
| } |
| } |