/* * (C) Copyright 2006-2016 Nuxeo SA (http://nuxeo.com/) and others. * * 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. * * Contributors: * Stephane Lacoin (aka matic) */ package org.nuxeo.ecm.core.opencmis.impl.client.sso; import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertThat; import java.io.IOException; import java.io.InputStream; import java.io.PrintWriter; import java.net.HttpURLConnection; import java.net.URL; import java.util.HashMap; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import org.apache.chemistry.opencmis.client.api.Folder; import org.apache.chemistry.opencmis.client.api.Property; import org.apache.chemistry.opencmis.client.api.Session; import org.apache.commons.httpclient.HttpStatus; import org.apache.commons.io.Charsets; import org.apache.commons.io.IOUtils; import org.junit.Test; import org.mortbay.jetty.Connector; import org.mortbay.jetty.Server; import org.mortbay.jetty.bio.SocketConnector; import org.mortbay.jetty.servlet.Context; import org.mortbay.jetty.servlet.ServletHolder; import org.mortbay.resource.Resource; import org.nuxeo.ecm.core.opencmis.impl.client.protocol.http.HttpURLInstaller; /** * The following application figure out a typical interaction between a portal, a CAS server and a Nuxeo repository. The * use case is fully documented in the documentation center http://doc.nuxeo.com/display/NXDOC/CAS2+Authentication. * * @author Stephane Lacoin (aka matic) */ public class CasPortal { protected static final String CMIS_LOCATION = "http://127.0.0.1:8080/nuxeo/atom/cmis"; protected static final String CAS_LOCATION = "http://127.0.0.1:8080/cas"; protected static final String HOME_LOCATION = "http://127.0.0.1:9090/home"; protected static final String TICKET_LOCATION = "http://127.0.0.1:9090/ticket"; protected static final String TICKET_ACCEPT_LOCATION = TICKET_LOCATION.concat("/accept"); protected static final String TICKET_LOGON_LOCATION = TICKET_LOCATION.concat("/logon"); protected String casLoginLocation() { return String.format("%s/login?service=%s/validate", CAS_LOCATION, TICKET_LOCATION); } protected String casServiceValidateLocation(String ticket) { return String.format("%s/serviceValidate?ticket=%s&service=%s/validate&pgtUrl=%s/accept", CAS_LOCATION, ticket, TICKET_LOCATION, TICKET_LOCATION); } protected String casProxyLocation(String ticket, String targetServiceLocation) { return String.format("%s/proxy?pgt=%s&targetService=%s", CAS_LOCATION, ticket, targetServiceLocation); } protected HttpURLConnection connect(String location) { try { return (HttpURLConnection) new URL(location).openConnection(); } catch (Exception e) { throw new Error("Bad location", e); } } protected Pattern proxyGrantingTicketPattern = Pattern.compile( ".*<cas:proxyGrantingTicket>(.*)</cas:proxyGrantingTicket>.*", Pattern.CASE_INSENSITIVE | Pattern.DOTALL); protected String extractText(Pattern pattern, String content) { Matcher matcher = pattern.matcher(content); if (!matcher.matches()) { throw new Error(String.format("Cannot extract '%s' from '%s'", pattern.pattern(), content)); } return matcher.group(1); } protected String extractProxyGrantingTicket(String content) { return extractText(proxyGrantingTicketPattern, content); } @Test public void testExtractProxyGrantingTicket() { String ticket = extractProxyGrantingTicket("<cas:proxyGrantingTicket>test</cas:proxyGrantingTicket>"); assertThat(ticket, is("test")); } protected String validateServiceTicket(String serviceTicket) throws IOException { HttpURLConnection connection = connect(casServiceValidateLocation(serviceTicket)); if (connection.getResponseCode() != HttpServletResponse.SC_OK) { throw new Error("Cannot validate ticket"); } try (InputStream in = connection.getInputStream()) { String content = IOUtils.toString(in, Charsets.UTF_8); String iou = extractProxyGrantingTicket(content); return proxyGrantingTickets.remove(iou); } } protected Pattern proxyTicketPattern = Pattern.compile(".*<cas:proxyTicket>(.*)</cas:proxyTicket>.*", Pattern.DOTALL); protected String extractProxyTicket(String content) { return extractText(proxyTicketPattern, content); } @Test public void testExtractProxyTicket() { String ticket = extractProxyTicket("...\n<cas:proxyTicket>test</cas:proxyTicket>\n..."); assertThat(ticket, is("test")); } protected String requestProxyTicket(String proxyGrantingTicket, String targetServiceLocation) throws IOException { HttpURLConnection proxyConnection = connect(casProxyLocation(proxyGrantingTicket, targetServiceLocation)); if (proxyConnection.getResponseCode() != HttpServletResponse.SC_OK) { throw new Error("Cannot get service ticket for proxy"); } try (InputStream in = proxyConnection.getInputStream()) { String proxyContent = IOUtils.toString(in, Charsets.UTF_8); return extractProxyTicket(proxyContent); } } public class ValidateServiceTicketServlet extends HttpServlet { public static final long serialVersionUID = 1L; @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String serviceTicket = req.getParameter("ticket"); String proxyGrantingTicket = validateServiceTicket(serviceTicket); String serviceTargetTicket = requestProxyTicket(proxyGrantingTicket, CMIS_LOCATION); CasClient cmis = new CasClient(CMIS_LOCATION); cmis.newGreeter().proxyLogon(serviceTargetTicket, TICKET_ACCEPT_LOCATION, CMIS_LOCATION); cmis.saveClientContext(); try { req.getSession().setAttribute("CMIS", cmis); } catch (Exception e) { throw new ServletException("cannot connect to cmis server", e); } // redirect to portal resp.setStatus(HttpServletResponse.SC_MOVED_TEMPORARILY); resp.setHeader("Location", HOME_LOCATION); } } protected Map<String, String> proxyGrantingTickets = new HashMap<>(); public class AcceptProxyGrantingTicketServlet extends HttpServlet { private static final long serialVersionUID = 1L; @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String id = req.getParameter("pgtId"); String iou = req.getParameter("pgtIou"); proxyGrantingTickets.put(iou, id); resp.setStatus(HttpServletResponse.SC_OK); } } public class HomeServlet extends HttpServlet { private static final long serialVersionUID = 1L; @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { HttpSession http = req.getSession(false); if (http == null) { // initiate CAS logon resp.setStatus(HttpServletResponse.SC_MOVED_TEMPORARILY); resp.setHeader("Location", casLoginLocation()); return; } // restore thread context CasClient cmis = (CasClient) http.getAttribute("CMIS"); cmis.restoreClientContext(); // fetch remote documents Session repository; try { repository = cmis.connect(); } catch (Exception e) { throw new ServletException("cannot connect to repo", e); } Folder folder = repository.getRootFolder(); PrintWriter writer = resp.getWriter(); for (Property<?> prop : folder.getProperties()) { writer.append(String.format("%s=%s\n", prop.getDisplayName(), prop.getValue())); } resp.setStatus(HttpStatus.SC_OK); } } protected final Server server = new Server(); protected void start() { HttpURLInstaller.install(); Server server = new Server(); Connector connector = new SocketConnector(); connector.setHost("127.0.0.1"); connector.setPort(9090); connector.setMaxIdleTime(60 * 1000); // 60 seconds server.addConnector(connector); Context context = new Context(server, "/", Context.SESSIONS); context.setBaseResource(Resource.newClassPathResource("/jetty-test")); ServletHolder holder; holder = new ServletHolder(new HomeServlet()); context.addServlet(holder, "/home"); holder = new ServletHolder(new ValidateServiceTicketServlet()); context.addServlet(holder, "/ticket/validate"); holder = new ServletHolder(new AcceptProxyGrantingTicketServlet()); context.addServlet(holder, "/ticket/accept"); try { server.start(); } catch (Exception e) { throw new Error("Cannot start jetty server", e); } } public static void main(String args[]) { CasPortal app = new CasPortal(); app.start(); } }