From 49f0308c4f89d3e83bb2102ebf1cf62e1fd2e896 Mon Sep 17 00:00:00 2001 From: Peter Hormanns Date: Sat, 12 Mar 2022 18:27:58 +0100 Subject: [PATCH] new cas auth module --- .gitignore | 16 +++ README.md | 8 ++ pom.xml | 69 ++++++++++ ...ngAuthEventExecutionPlanConfiguration.java | 46 +++++++ .../HostsharingAuthenticationHandler.java | 129 ++++++++++++++++++ .../cas/auth/PasswordValidationException.java | 19 +++ .../hostsharing/cas/auth/TicketService.java | 85 ++++++++++++ src/main/resources/META-INF/spring.factories | 1 + 8 files changed, 373 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 pom.xml create mode 100644 src/main/java/net/hostsharing/cas/auth/HostsharingAuthEventExecutionPlanConfiguration.java create mode 100644 src/main/java/net/hostsharing/cas/auth/HostsharingAuthenticationHandler.java create mode 100644 src/main/java/net/hostsharing/cas/auth/PasswordValidationException.java create mode 100644 src/main/java/net/hostsharing/cas/auth/TicketService.java create mode 100644 src/main/resources/META-INF/spring.factories diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5b7e1d1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +/bin +/build +/target +/.classpath +/.project +/.settings/ +/cas-overlay/target/ +/cas-overlay/build/ +/cas-overlay/.classpath +/cas-overlay/.project +/cas-overlay/.settings/ +/hostsharing-auth/target/ +/hostsharing-auth/build/ +/hostsharing-auth/.classpath +/hostsharing-auth/.project +/hostsharing-auth/.settings/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..7943bd6 --- /dev/null +++ b/README.md @@ -0,0 +1,8 @@ +CAS Module für Hostsharing +========================== + +Dieses Modul authentifiziert Username und Passwort gegen die HSAdmin API. + +Dieses Modul wird gebaut und in das lokale Maven-Repository des aktuellen Users instaliert mit dem Befehl + + mvn clean install diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..24bc2ba --- /dev/null +++ b/pom.xml @@ -0,0 +1,69 @@ + + 4.0.0 + net.hostsharing.cas + casauthhsadmin + 1.0.4 + CAS Auth HSAdmin + + 11 + UTF-8 + 6.4.4.2 + + + + org.apereo.cas + cas-server-core + ${cas.version} + provided + + + org.apereo.cas + cas-server-core-authentication-api + ${cas.version} + provided + + + org.apereo.cas + cas-server-core-util-api + ${cas.version} + provided + + + org.apereo.cas + cas-server-core-configuration-api + ${cas.version} + provided + + + org.apache.xmlrpc + xmlrpc-client + 3.1.3 + + + xml-apis + xml-apis + + + + + junit + junit + 4.13 + test + + + + + + maven-compiler-plugin + 3.8.1 + + + UTF-8 + + + + casauthhsadmin + + diff --git a/src/main/java/net/hostsharing/cas/auth/HostsharingAuthEventExecutionPlanConfiguration.java b/src/main/java/net/hostsharing/cas/auth/HostsharingAuthEventExecutionPlanConfiguration.java new file mode 100644 index 0000000..c13aa88 --- /dev/null +++ b/src/main/java/net/hostsharing/cas/auth/HostsharingAuthEventExecutionPlanConfiguration.java @@ -0,0 +1,46 @@ +package net.hostsharing.cas.auth; + +import org.apereo.cas.authentication.AuthenticationEventExecutionPlan; +import org.apereo.cas.authentication.AuthenticationEventExecutionPlanConfigurer; +import org.apereo.cas.authentication.AuthenticationHandler; +import org.apereo.cas.authentication.principal.PrincipalFactory; +import org.apereo.cas.authentication.principal.PrincipalFactoryUtils; +import org.apereo.cas.authentication.principal.PrincipalResolver; +import org.apereo.cas.services.ServicesManager; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.cloud.context.config.annotation.RefreshScope; +import org.springframework.context.annotation.Bean; + +public class HostsharingAuthEventExecutionPlanConfiguration implements AuthenticationEventExecutionPlanConfigurer { + + @Autowired + @Qualifier("servicesManager") + private ObjectProvider servicesManager; + + @Autowired + @Qualifier("defaultPrincipalResolver") + private ObjectProvider defaultPrincipalResolver; + + @ConditionalOnMissingBean(name = "hostsharingAuthenticationPrincipalFactory") + @Bean + @RefreshScope + public PrincipalFactory hostsharingAuthenticationPrincipalFactory() { + return PrincipalFactoryUtils.newPrincipalFactory(); + } + + + @Bean + public AuthenticationHandler myAuthenticationHandler() { + final String name = "Hostsharing Authentication"; + return new HostsharingAuthenticationHandler(name, servicesManager.getObject(), hostsharingAuthenticationPrincipalFactory()); + } + + @Override + public void configureAuthenticationExecutionPlan(AuthenticationEventExecutionPlan plan) { + plan.registerAuthenticationHandler(myAuthenticationHandler()); + } + +} diff --git a/src/main/java/net/hostsharing/cas/auth/HostsharingAuthenticationHandler.java b/src/main/java/net/hostsharing/cas/auth/HostsharingAuthenticationHandler.java new file mode 100644 index 0000000..6cb4d2f --- /dev/null +++ b/src/main/java/net/hostsharing/cas/auth/HostsharingAuthenticationHandler.java @@ -0,0 +1,129 @@ +package net.hostsharing.cas.auth; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Serializable; +import java.net.URL; +import java.security.GeneralSecurityException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +import org.apache.xmlrpc.XmlRpcException; +import org.apache.xmlrpc.client.XmlRpcClient; +import org.apache.xmlrpc.client.XmlRpcClientConfigImpl; +import org.apereo.cas.authentication.AuthenticationHandlerExecutionResult; +import org.apereo.cas.authentication.CoreAuthenticationUtils; +import org.apereo.cas.authentication.PreventedException; +import org.apereo.cas.authentication.credential.UsernamePasswordCredential; +import org.apereo.cas.authentication.handler.support.AbstractUsernamePasswordAuthenticationHandler; +import org.apereo.cas.authentication.principal.Principal; +import org.apereo.cas.authentication.principal.PrincipalFactory; +import org.apereo.cas.services.ServicesManager; +import org.w3c.dom.Document; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +public class HostsharingAuthenticationHandler extends AbstractUsernamePasswordAuthenticationHandler { + + public HostsharingAuthenticationHandler(String name, ServicesManager servicesManager, PrincipalFactory principalFactory) { + super(name, servicesManager, principalFactory, Integer.MAX_VALUE); + } + + @Override + protected AuthenticationHandlerExecutionResult authenticateUsernamePasswordInternal(UsernamePasswordCredential credential, String originalPassword) + throws GeneralSecurityException, PreventedException { + + final String username = credential.getUsername(); + final String password = credential.getPassword(); + + try { + + final Map> attributes = validateCredentials(username, password); + final Principal principal = this.principalFactory.createPrincipal(username, attributes); + return createHandlerResult(credential, principal); + + } catch (PasswordValidationException | IOException | XmlRpcException | ParserConfigurationException | SAXException e) { + throw new GeneralSecurityException(e); + } + } + + private static Map> validateCredentials(final String login, final String password) + throws PasswordValidationException, XmlRpcException, GeneralSecurityException, IOException, ParserConfigurationException, SAXException { + + if (!login.contains("@")) { + throw new GeneralSecurityException("expect email address"); + } + final String emailDomain = login.split("@")[1]; + final URL url = new URL("http://" + emailDomain + "/.well-known/autoconfig/mail/config-v1.1.xml?emailaddress=" + login); + final InputStream autoconfigStream = url.openConnection().getInputStream(); + final DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); + final DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder(); + final Document document = documentBuilder.parse(autoconfigStream); + final NodeList inServersNodes = document.getElementsByTagName("username"); + if (inServersNodes.getLength() != 2) { + throw new GeneralSecurityException("expect email address"); + } + + final String username = inServersNodes.item(0).getTextContent(); + + + final TicketService ticketService = new TicketService(username, password); + final String grantingTicket = ticketService.getGrantingTicket(); + final String ticket = ticketService.getServiceTicket(grantingTicket); + + final XmlRpcClientConfigImpl config = new XmlRpcClientConfigImpl(); + config.setServerURL(new URL("https://config.hostsharing.net:443/hsar/xmlrpc/hsadmin")); + config.setEnabledForExtensions(true); + final XmlRpcClient client = new XmlRpcClient(); + client.setConfig(config); + + final List xmlRpcParamsList = new ArrayList(); + xmlRpcParamsList.add(username); + xmlRpcParamsList.add(ticket); + final HashMap whereParamsMap = new HashMap(); + whereParamsMap.put("name", username); + xmlRpcParamsList.add(whereParamsMap); + final Object[] rpcResult = (Object[]) client.execute("user.search", xmlRpcParamsList); + if (rpcResult.length != 1) { + throw new GeneralSecurityException("unknown username"); + } + @SuppressWarnings("unchecked") + final Map userData = (Map) rpcResult[0]; + final String comment = (String) userData.get("comment"); + int firstCommaIndex = comment.indexOf(','); + String displayName = comment; + String[] groups = new String[0]; + if (firstCommaIndex > 0) { + displayName = comment.substring(0, firstCommaIndex).trim(); + final String[] splitStrings = comment.substring(firstCommaIndex + 1).split(","); + groups = new String[splitStrings.length]; + for (int idx=0; idx attribsMap = new HashMap(); + attribsMap.put("groups", groups); + attribsMap.put("displayName", displayName); + attribsMap.put("mail", login); + final Map> attributes = CoreAuthenticationUtils.convertAttributeValuesToMultiValuedObjects(attribsMap); + return attributes; + } + + public static void main(String[] args) { + try { + Map> map = validateCredentials(args[0], args[1]); + for (String key : map.keySet()) { + System.out.println(key + ": " + map.get(key)); + } + } catch (IOException | PasswordValidationException | XmlRpcException | GeneralSecurityException | ParserConfigurationException | SAXException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } +} diff --git a/src/main/java/net/hostsharing/cas/auth/PasswordValidationException.java b/src/main/java/net/hostsharing/cas/auth/PasswordValidationException.java new file mode 100644 index 0000000..fbae120 --- /dev/null +++ b/src/main/java/net/hostsharing/cas/auth/PasswordValidationException.java @@ -0,0 +1,19 @@ +package net.hostsharing.cas.auth; + +public class PasswordValidationException extends Exception { + + private static final long serialVersionUID = 1L; + + public PasswordValidationException(final String message) { + super(message); + } + + public PasswordValidationException(final Throwable excption) { + super(excption); + } + + public PasswordValidationException(final String message, final Throwable exception) { + super(message, exception); + } + +} \ No newline at end of file diff --git a/src/main/java/net/hostsharing/cas/auth/TicketService.java b/src/main/java/net/hostsharing/cas/auth/TicketService.java new file mode 100644 index 0000000..5dec238 --- /dev/null +++ b/src/main/java/net/hostsharing/cas/auth/TicketService.java @@ -0,0 +1,85 @@ +package net.hostsharing.cas.auth; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.net.URL; +import java.net.URLEncoder; + +import javax.net.ssl.HttpsURLConnection; + +/** + * Helper for service tickets. + * Hostsharing uses the CAS authentication service to authenticate + * users of hostsharing services. This class is used to create a + * "ticket granting ticket" for a session and service ticket for + * individual service calls. + */ +public class TicketService { + final String user; + final String password; + + public TicketService(final String user, final String password) { + this.user = user; + this.password = password; + } + + public String getGrantingTicket() throws PasswordValidationException { + String ticket = null; + try { + String userParam = "username=" + URLEncoder.encode(user, "UTF-8"); + String passwordParam = "password=" + URLEncoder.encode(password, "UTF-8"); + String encodedData = userParam + "&" + passwordParam; + URL url = new URL("https://login.hostsharing.net/cas/v1/tickets"); + + final HttpsURLConnection connection = (HttpsURLConnection) url.openConnection(); + connection.setRequestMethod("POST"); + connection.setRequestProperty("Content-type", "application/x-www-form-urlencoded; charset=UTF-8"); + connection.setDoInput(true); + connection.setDoOutput(true); + connection.setUseCaches(false); + connection.setAllowUserInteraction(false); + final BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(connection.getOutputStream())); + writer.write(encodedData); + writer.close(); + connection.connect(); + ticket = connection.getHeaderField("Location"); + } catch (Exception e) { + throw new PasswordValidationException(e); + } + + return ticket; + } + + public String getServiceTicket(String grantingTicket) throws PasswordValidationException { + String ticket = null; + try { + String serviceParam = "service=" + URLEncoder.encode("https://config.hostsharing.net:443/hsar/backend", "UTF-8"); + URL url = new URL(grantingTicket); + + final HttpsURLConnection connection = (HttpsURLConnection) url.openConnection(); + connection.setRequestMethod("POST"); + connection.setRequestProperty("Content-type", "application/x-www-form-urlencoded; charset=UTF-8"); + connection.setDoInput(true); + connection.setDoOutput(true); + connection.setUseCaches(false); + connection.setAllowUserInteraction(false); + final BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(connection.getOutputStream())); + writer.write(serviceParam); + writer.close(); + connection.connect(); + final BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream())); + ticket = reader.readLine(); + String readLine = reader.readLine(); + do { + readLine = reader.readLine(); + } while (readLine != null); + } catch (Exception e) { + throw new PasswordValidationException(e); + } + + return ticket; + } + +} diff --git a/src/main/resources/META-INF/spring.factories b/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..aea3009 --- /dev/null +++ b/src/main/resources/META-INF/spring.factories @@ -0,0 +1 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=net.hostsharing.cas.auth.HostsharingAuthEventExecutionPlanConfiguration