Im Zuge dieses Blogbeitrages soll knapp ein Lösungsansatz zu Single-Sign-On Solutions, kurz SSO genannt, unter Windows vorgestellt werden, der von uns im Rahmen einer Webapplikation für einen Kunden entwickelt wurde. Dabei bestand die Anforderung darin, dass die Benutzer bei Aufruf der Webanwendung ohne extra benötigte Anmeldemaske automatisch per SSO authentifiziert werden. Hierzu gibt es bereits eine Reihe fertiger Bibliotheken wie z.B. “Waffle”, welche auf dem sogenannten Security Support Provider Interface (SSPI) von Microsoft aufbaut.

Der folgende Text soll unsere Vorgehensweise näher darstellen.

Einbindung von Waffle

Die Einbindung erfolgte hierbei über Maven, einem Java-basierten Build-Management-Tool der Apache Software Foundation.

<properties>
      <waffle.spring.security4.version>1.8.2</waffle.spring.security4.version>
 </properties>
 <!-- ... snip -->
 <dependencies>
      <dependency>
             <groupId>com.github.waffle</groupId>
             <artifactId>waffle-spring-security4</artifactId>
             <version>${waffle.spring.security4.version}</version>
      </dependency>
 </dependencies>
 <!-- snap ... -->

Konfiguration von Waffle

Wie Waffle über das Spring Framework konfiguriert werden kann, ist auf der Waffle Website für unterschiedliche Fälle sehr gut erklärt. Der folgende Codeblock zeigt die Konfiguration aus dem Projekt:

<?xml version="1.0" encoding="UTF-8"?>

 <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xmlns:sec="http://www.springframework.org/schema/security"
     xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
             http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd">

     <!-- windows authentication provider -->
     <bean id="waffleWindowsAuthProvider" class="waffle.windows.auth.impl.WindowsAuthProviderImpl" />

     <!-- collection of security filters -->
     <bean id="negotiateSecurityFilterProvider" class="waffle.servlet.spi.NegotiateSecurityFilterProvider">
         <constructor-arg ref="waffleWindowsAuthProvider" />
     </bean>

     <bean id="basicSecurityFilterProvider" class="waffle.servlet.spi.BasicSecurityFilterProvider">
         <constructor-arg ref="waffleWindowsAuthProvider" />
     </bean>

     <bean id="waffleSecurityFilterProviderCollection" class="waffle.servlet.spi.SecurityFilterProviderCollection">
         <constructor-arg>
             <list>
                 <ref bean="negotiateSecurityFilterProvider" />
                 <ref bean="basicSecurityFilterProvider" />
             </list>
         </constructor-arg>
     </bean>
      <bean id="csrfSecurityRequestMatcher" class="com.customer.web.security.CsrfSecurityRequestMatcher">
             <property name="pathExclusions">
                    <set>
                           <value>/processor/**</value>
                    </set>
             </property>
      </bean>
     <!-- spring filter entry point -->
     <sec:http entry-point-ref="negotiateSecurityFilterEntryPoint" use-expressions="true">

         <sec:intercept-url pattern="/**" access="isFullyAuthenticated()" />

         <sec:csrf disabled="false" request-matcher-ref="csrfSecurityRequestMatcher"/>
         <sec:custom-filter ref="waffleNegotiateSecurityFilter" position="BASIC_AUTH_FILTER" />

     </sec:http>

     <bean id="negotiateSecurityFilterEntryPoint" class="waffle.spring.NegotiateSecurityFilterEntryPoint">
         <property name="Provider" ref="waffleSecurityFilterProviderCollection" />
     </bean>

     <!-- spring authentication provider -->
     <sec:authentication-manager alias="authenticationProvider" />

     <!-- spring security filter -->
     <bean id="waffleNegotiateSecurityFilter" class="waffle.spring.NegotiateSecurityFilter">
         <property name="Provider" ref="waffleSecurityFilterProviderCollection" />
         <property name="AllowGuestLogin" value="false" />
         <property name="PrincipalFormat" value="fqn" />
         <property name="RoleFormat" value="both" />
     </bean>

 </beans>

Die folgenden Absätze behandeln zwei Besonderheiten, die bei der Konfiguration zu beachten waren:

<http auto-config='true'/>

Wie in der Dokumentation des Spring Security Projektes auch empfohlen, sollte der <http> Teil nicht im “auto-config” Modus laufen. Ist dies dennoch der Fall, funktioniert Spring Waffle in Verbindung mit SSO nicht und das <http> Tag muss manuell konfiguriert werden.

<csrf/>

Mit der 3.2.8.x Version des Spring Security Frameworks wurde das <csrf/> Tag eingeführt, das einen Schutz vor Cross Site Request Forgery (CSRF) Angriffen darstellt. Dafür wird pro Request ein CSRF Token vom Client zum Server übermittelt. Sofern die Tag-Bibliothek <spring:form>...</spring:form> im Einsatz ist, wird der CSRF Token automatisch in das Web-Formular eingefügt. Werden allerdings auch XH-Requests abgesetzt, muss der Token über den Header des Ajax Requests mitgegeben werden.

Der folgende Absatz erklärt die Umsetzung.

CSRF - Cross Site Request Forgery

Natürlich könnte CSRF auch deaktiviert werden, doch die Entwickler des Spring Security Frameworks empfehlen diesen Schritt nicht. In unserem Fall war es, durch die Verwendung des Java Server Page-Template-Frameworks “Apache Tiles” leicht, den benötigten Token auf allen Seiten zu generieren und die per jQuery abgesetzten XH-Requests global zu konfigurieren. In der JSP Datei, die als Template dient, sieht das wie folgt aus:

<!-- ...snip -->
 <meta name="_csrf" content="${_csrf.token}"/>
 <meta name="_csrf_header" content="${_csrf.headerName}"/>
 <!-- snap ...
 ... snip -->
 <script type="text/javascript">
      $(function(){
             $.ajaxPrefilter(function (options, originalOptions, jqXHR) {
                    jqXHR.setRequestHeader(${_csrf.headerName}, ${_csrf.token});
             });
      });
 </script>
 <!-- snap ... -->

Ausschluss bestimmter Seiten

Wie oben beschrieben, muss für die korrekte Funktionsweise von CSRF ein bekannter Token mitgegeben werden. In unserem Fall sollten bestimmte API Aufrufe davon ausgeschlossen werden. Dies kann über das Attribut request-matcher-ref des <csfr> Tags erreicht werden. Dieses Attribut erwartet eine Klasse der Schnittstelle org.springframework.security.web.util.matcher.RequestMatcher und wurde wie folgt implementiert:

public class CsrfSecurityRequestMatcher implements RequestMatcher {

      private Pattern allowedMethods = Pattern.compile("^(GET|HEAD|TRACE|OPTIONS)$");
      private Set<AntPathRequestMatcher> antPathRequestMatchers = new HashSet<AntPathRequestMatcher>();
      private Set<String> pathExclusions;

     @Override
     public boolean matches(HttpServletRequest request) {

        boolean match = !allowedMethods.matcher(request.getMethod()).matches();

        if(match){
             for(AntPathRequestMatcher antPathRequestMatcher:antPathRequestMatchers){
                    match = !antPathRequestMatcher.matches(request);
                    if(!match){
                           break;
                    }
             }
        }

         return match;
     }

      public void setPathExclusions(Set<String> pathExclusions) {
             this.pathExclusions = pathExclusions;

             updateAntPathRequestMatchers();

      }

      private void updateAntPathRequestMatchers() {

             if(this.antPathRequestMatchers == null){
                    this.antPathRequestMatchers = new HashSet<AntPathRequestMatcher>();
             }

             this.antPathRequestMatchers.clear();

             if(this.pathExclusions != null){
                    for(String pathExclusion:this.pathExclusions){
                           this.antPathRequestMatchers.add(new AntPathRequestMatcher(pathExclusion));
                    }
             }
      }

 }

Die Implementierung kann auf diese Weise als Spring-Bean konfiguriert werden:

<bean id="csrfSecurityRequestMatcher" class="com.customer.web.security.CsrfSecurityRequestMatcher">
      <property name="pathExclusions">
             <set>
                    <value>/processor/**</value>
                    <!-- more paths to exclude here -->
             </set>
      </property>
 </bean>

Die Eigenschaft pathExclusion ist im Stande beliebig viele Pfade aufzunehmen, die von CSRF ausgeschlossen werden sollen. Hier erfolgt nochmal die Einbindung per Sub-Tag <csrf> des <http> Tags aus der oben gelisteten Gesamtkonfiguration:

<!-- ... snip -->
<!-- spring filter entry point -->
<sec:http entry-point-ref="negotiateSecurityFilterEntryPoint" use-expressions="true">

    <sec:intercept-url pattern="/**" access="isFullyAuthenticated() AND hasRole('ROLE_USER')" />

    <sec:csrf disabled="false" request-matcher-ref="csrfSecurityRequestMatcher"/>
    <sec:custom-filter ref="waffleNegotiateSecurityFilter" position="BASIC_AUTH_FILTER" />

</sec:http
<!-- snap ... -->

Verwendung und Einsatz

Nach erfolgreicher Konfiguration wird der Benutzer bei Aufruf einer beliebigen Seite der Webanwendung mithilfe seines Windows-Accounts ohne weitere Login-Maske authentifiziert. Durch die Integration in Spring baut Waffle automatisch ein vollständiges Authentifizierungs-Objekt im Spring Security Context auf.

// vollständig und automatisch befüllt
 SecurityContextHolder.getContext().getAuthentication();
 // zugewiesene Gruppen aus dem AD hierüber erreichbar
 SecurityContextHolder.getContext().getAuthentication().getAuthorities();

Neben dem programmatischen Zugriff funktionieren auch alle anderen Annehmlichkeiten des Spring Security Frameworks wie Taglibs innerhalb von JSP Seiten oder Annotationen.

Auf ein Standardverhalten der Waffle Bibliothek sei zum Schluss noch hingewiesen: Waffle wandelt den Namen der Rolle aus dem Active Directory in Großbuchstaben um und setzt diesen mit dem Prefix ROLE_ zusammen. Angenommen die Rolle lautet im AD “User” so muss die Abfrage im Spring Security Kontext z.B. per hasRole("ROLE_USER") erfolgen.

Dieser Artikel sollte Ihnen einen kurzen Überblick bezüglich der Thematik Single-Sign-On Solutions unter Windows verschaffen. Sollten Sie dazu Fragen haben oder einen spezifischen Lösungsansatz benötigen, kontaktieren Sie uns bitte einfach via sales@scandio.de