Beliebte Suchanfragen
//

Kerberos Single-Sign-On auf Tomcat und Websphere mit Active Directory

4.3.2018 | 7 Minuten Lesezeit

Wir hatten bei einem Kunden die Notwendigkeit, eine Webanwendung per Single-Sign-On, im weiteren Verlauf als SSO bezeichnet, abzusichern. Dabei werden die Daten für die Authentifizierung und Autorisierung benutzt, die der Benutzer schon bei der Anmeldung in der Windows-Domäne eingegeben hat. Hierbei kommuniziert der Webbrowser über einen speziellen Mechanismus (Kerberos) mit der Webanwendung und dem Active Directory, welches hier als Authentication-Service und Ticket Granting Service fungiert. Eine einfache Erläuterung des Mechanismus kann man hier erhalten .

Gibt’s da nicht was von Spring?

Nun hatten wir beim Kunden zudem die Anforderung, dass das SSO der Webanwendung sowohl im altbewährten Websphere Application Server als auch in einem Tomcat im Docker-Container funktionieren soll. Insbesondere hierin bestand die Herausforderung. Da es sich bei der Webanwendung im weitesten Sinne um eine Spring-Anwendung handelt, haben wir uns für die Spring-Lösung mit Spring-Security und Spring-Security-Kerberos entschieden. Dieses ließ sich leicht integrieren und durch die Nutzung von Profilen für Websphere und Tomcat auch leicht konfigurieren.

Konfiguration

Zunächst muss ein WebApplicationInitializer im Klassenpfad platziert werden, der mittels Servlet-3.0-Api automatisch geladen wird und die Spring-Security springSecurityFilterChain mit einer Reihe von HTTP-Servlet-Filtern registriert. Der dafür vorgesehene WebApplicationInitializer muss die Spring-Klasse AbstractSecurityWebApplicationInitializer erweitern, kann ansonsten aber leer sein. Dieser WebApplicationInitializer registriert allerdings nur die Referenzen auf die Servlet-Filter-Chain, daher müssen des Weiteren noch weitere Spring-Beans erzeugt werden. Mithilfe von Java-Config kann das leicht erfolgen. Hierzu wird im ApplicationContext der Anwendung eine Konfiguration abgelegt, die die Spring-Klasse WebSecurityConfigurerAdapter erweitert und mit @EnableWebSecurity annotiert wird. In dieser Konfigurationsklasse werden nun auch die Spring-Beans erzeugt, die für den Kerberos-Mechanismus nötig sind. Hierzu zählen insbesondere KerberosTicketValidator, KerberosServiceAuthenticationProvider, SpnegoAuthenticationProcessingFilter und SpnegoEntryPoint.

Ein Beispiel, wie die Konfiguration aussehen könnte:

1@EnableWebSecurity
2public abstract class AbstractSecurityConfiguration extends WebSecurityConfigurerAdapter {
3 
4    @Value("${security.ldap.server-url:ldap://ads.codecentric.de:389}")
5    private String serverUrl;
6 
7    @Value("${security.ldap.domain:CODECENTRIC.DE}")
8    private String domain;
9 
10    @Value("${security.ldap.search-base:dc=codecentric,dc=de}")
11    private String searchBase;
12 
13    @Value("${security.ldap.search-filter:(sAMAccountName={0})}")
14    private String searchFilter;
15 
16    @Value("${security.ldap.connection-name:an_admin_user}")
17    private String connectionName;
18 
19    @Value("${security.ldap.connection-password:secret_password}")
20    private String connectionPassword;
21 
22    @Value("${security.ldap.referral:follow}")
23    private String referral;
24 
25    @Value("${security.debug:false}")
26    private boolean debug;
27 
28    public boolean isDebug() {
29        return debug;
30    }
31 
32    @Override
33    protected void configure(HttpSecurity http) throws Exception {
34        http.exceptionHandling()//
35            .authenticationEntryPoint(spnegoEntryPoint())//
36            .and()//
37            .authorizeRequests().antMatchers("/js/**", "/css/**").permitAll()//
38            .antMatchers("/secured/*").authenticated()//
39            .and()//
40            .formLogin().loginPage("/login").failureUrl("/login?error=true").loginProcessingUrl("/j_security_check").permitAll()//
41            .usernameParameter("j_username").passwordParameter("j_password")//
42            .and()//
43            .logout().permitAll()//
44            .and()//
45            .csrf().disable()//
46            .addFilterBefore(spnegoAuthenticationProcessingFilter(authenticationManagerBean()),
47                BasicAuthenticationFilter.class);
48    }
49 
50    @Override
51    public void configure(AuthenticationManagerBuilder auth) throws Exception {
52        auth //
53            .authenticationProvider(activeDirectoryLdapAuthenticationProvider()) //
54            .authenticationProvider(kerberosServiceAuthenticationProvider());
55    }
56 
57    @Bean
58    public ActiveDirectoryLdapAuthenticationProvider activeDirectoryLdapAuthenticationProvider() {
59        return new ActiveDirectoryLdapAuthenticationProvider(domain, serverUrl);
60    }
61 
62    @Bean
63    public SpnegoEntryPoint spnegoEntryPoint() {
64        return new SSOUrlSpnegoEntryPoint("/login");
65    }
66 
67    @Bean
68    public SpnegoAuthenticationProcessingFilter spnegoAuthenticationProcessingFilter(
69            AuthenticationManager authenticationManager) {
70        SpnegoAuthenticationProcessingFilter filter = new SpnegoAuthenticationProcessingFilter();
71        filter.setAuthenticationManager(authenticationManager);
72        return filter;
73    }
74 
75    @Bean
76    public KerberosServiceAuthenticationProvider kerberosServiceAuthenticationProvider() {
77        KerberosServiceAuthenticationProvider provider = new KerberosServiceAuthenticationProvider();
78        provider.setTicketValidator(kerberosTicketValidator());
79        provider.setUserDetailsService(ldapUserDetailsService());
80        return provider;
81    }
82 
83    @Bean
84    public LdapUserDetailsService ldapUserDetailsService() {
85        FilterBasedLdapUserSearch userSearch = new FilterBasedLdapUserSearch("", searchFilter, contextSource());
86        DefaultLdapAuthoritiesPopulator authoritiesPopulator = new DefaultLdapAuthoritiesPopulator(contextSource(), "");
87        authoritiesPopulator.setSearchSubtree(true);
88        LdapUserDetailsService service = new UsernameStrippingLdapUserDetailsService(userSearch, authoritiesPopulator);
89        service.setUserDetailsMapper(new LdapUserDetailsMapper());
90        return service;
91    }
92 
93    @Bean
94    public DefaultSpringSecurityContextSource contextSource() {
95        DefaultSpringSecurityContextSource contextSource = new DefaultSpringSecurityContextSource(serverUrl);
96        contextSource.setBase(searchBase);
97        contextSource.setUserDn(connectionName);
98        contextSource.setPassword(connectionPassword);
99        contextSource.setReferral(referral);
100        contextSource.afterPropertiesSet();
101        return contextSource;
102    }
103 
104    @Bean
105    public GlobalSunJaasKerberosConfig globalSunJaasKerberosConfig() {
106        GlobalSunJaasKerberosConfig config = new GlobalSunJaasKerberosConfig();
107        config.setKrbConfLocation(getKrbConfLocation());
108        config.setDebug(debug);
109        return config;
110    }
111 
112    protected abstract String getKrbConfLocation();
113 
114    protected abstract String getKeytabLocation();
115 
116    protected abstract String getServicePrincipal();
117 
118    protected abstract KerberosTicketValidator kerberosTicketValidator();
119 
120}

Wie man sehen kann, ist diese Konfiguration abstrakt und muss noch implementiert werden. Dies wurde nun profilabhängig für den Websphere und den Tomcat implementiert, um die unterschiedlichen Systeme abzubilden. Dazu nun also zwei Konfigurationen.

Websphere-Konfiguration

1@Configuration
2@Profile("websphere")
3public class SecurityWebsphereConfiguration extends AbstractSecurityConfiguration {
4 
5    @Value("${security.kerberos.websphere.service-principal:HTTP/websphere.codecentric.de}")
6    private String servicePrincipal;
7 
8    @Value("${security.kerberos.websphere.keytab-location:/etc/kerberos/kerberos.keytab}")
9    private String keytabLocation;
10 
11    @Value("${security.kerberos.websphere.krb-conf-location:/etc/kerberos/krb5.ini}")
12    private String krbConfLocation;
13 
14    @Override
15    public String getServicePrincipal() {
16        return servicePrincipal;
17    }
18 
19    @Override
20    public String getKeytabLocation() {
21        return keytabLocation;
22    }
23 
24    @Override
25    public String getKrbConfLocation() {
26        return krbConfLocation;
27    }
28 
29    @Override
30    @Bean
31    public KerberosTicketValidator kerberosTicketValidator() {
32        IbmJaasKerberosTicketValidator ticketValidator = new IbmJaasKerberosTicketValidator();
33        ticketValidator.setServicePrincipal(getServicePrincipal());
34        ticketValidator.setKeyTabLocation(new FileSystemResource(getKeytabLocation()));
35        ticketValidator.setDebug(isDebug());
36        return ticketValidator;
37    }
38 
39}

Tomcat-Konfiguration

1@Configuration
2@Profile("tomcat")
3public class SecurityDistributedConfiguration extends AbstractSecurityConfiguration {
4 
5    @Value("${security.kerberos.tomcat.service-principal:HTTP/tomcat.codecentric.de}")
6    private String servicePrincipal;
7 
8    @Value("${security.kerberos.tomcat.keytab-location:/usr/local/tomcat/conf/kerberos.keytab}")
9    private String keytabLocation;
10 
11    @Value("${security.kerberos.tomcat.krb-conf-location:/usr/local/tomcat/conf/krb5.ini}")
12    private String krbConfLocation;
13 
14    @Override
15    public String getServicePrincipal() {
16        return servicePrincipal;
17    }
18 
19    @Override
20    public String getKeytabLocation() {
21        return keytabLocation;
22    }
23 
24    @Override
25    public String getKrbConfLocation() {
26        return krbConfLocation;
27    }
28 
29    @Override
30    @Bean
31    public KerberosTicketValidator kerberosTicketValidator() {
32        SunJaasKerberosTicketValidator ticketValidator = new SunJaasKerberosTicketValidator();
33        ticketValidator.setServicePrincipal(getServicePrincipal());
34        ticketValidator.setKeyTabLocation(new FileSystemResource(getKeytabLocation()));
35        ticketValidator.setDebug(isDebug());
36        return ticketValidator;
37    }
38 
39}

Vorbereitung des Key Distribution Center (KDC)

In dem Beispiel wird die Rolle des KDC vom Active Directory ausgefüllt. Wie man sieht, wurden in der Konfiguration sogenannt „Service-Principals“ und weitere Dateien referenziert, auf die ich im folgenden eingehe.

1. Service-Principal

Der Service-Principal, kurz SPN, bezeichnet den Namen eines Dienstes im der Netzwerk-Domäne mit Kerberos-Authentifizierung. Dieser besteht aus der Dienstklasse, einem Hostnamen und ggf. einem Port. In dem Beispiel gibt es für jeden Server, der die Webanwendung bereitstellt, einen SPN der Dienstklasse HTTP.

  • HTTP/tomcat.codecentric.de
  • HTTP/websphere.codecentric.de

Der Hostnamen muss dem entsprechen, wie die Anwendung vom Webbrowser aufgerufen wird. In dem Beispiel könnte es also die URL https://tomcat.provinzial.com:8080/beispiel sein. Diese beiden SPNs müssen im KDC nun registriert und einem Benutzer zugeordnet werden. Hierzu werden in einer Windows-Konsole (der PC muss in der Domäne angemeldet sein) die folgenden Befehle ausgeführt:

1setspn -A HTTP/tomcat.codecentric.de A_KERBEROS_USER

Mit diesem Befehl wird der SPN im erzeugt und dem Benutzer A_KERBEROS_USER zugeordnet.

2. kerberos.keytab

Diese Datei ist der Schlüssel, der zwischen der Webanwendung und dem KDC zur Authentifizierung benutzt wird.

1ktpass /out c:\kerberos.keytab /mapuser A_KERBEROS_USER@CODECENTRIC.DE 
2  /princ HTTP/tomcat.codecentric.de@CODECENTRIC.DE /pass A_SECRET_PASSWD 
3  /kvno 0 /crypto RC4-HMAC-NT

Der Befehlt legt für den Benutzer A_KERBEROS_USER und sein Passwort A_SECRET_PASSWD die Keytab-Datei an, die jedoch nur für den SPN HTTP/tomcat.codecentric.de verwendet werden kann. Für den SPN HTTP/websphere.codecentric.de muss analog dazu eine weitere Keytab-Datei erzeugt werden.

3. krb5.ini

In dieser Datei wird Kerberos selbst konfiguriert. Hier wird zum Beispiel der KDC konfiguriert:

1[libdefaults]
2default_realm = CODECENTRIC.DE
3default_keytab_name = FILE:/usr/local/tomcat/conf/kerberos.keytab
4default_tkt_enctypes = rc4-hmac
5default_tgs_enctypes = rc4-hmac
6dns_lookup_realm = false
7dns_lookup_kdc = false
8forwardable=true
9 
10[realms]
11CODECENTRIC.DE = {
12  kdc = 192.168.0.1
13  admin_server = 192.168.0.1
14}
15 
16[domain_realm]
17codecentric.de = CODECENTRIC.DE
18.codecentric.de = CODECENTRIC.DE

Diese Konfiguration ist stark von der Domänen-Konfiguration im Active Directory abhängig. Dies hier ist nur ein Beispiel.

Aufruf der Webanwendung

Nach dem Erstellen der notwendigen Dateien und dem Deployment der Webanwendungen mit der obigen Spring-Konfiguration wird beim Aufruf des URL https://tomcat.codecentric.de/beispiel der Kerberos-Mechanismus in Gang gesetzt. Zunächst prüft die Anwendung, ob der Aufrufer schon eingeloggt ist, wenn nicht, schreibt der SpnegoEntryPoint einen HTTP-Header (WWW-Authenticate=Negotiate) und gibt Status 401 zurück. Damit weiß der Webbrowser, dass er die Daten des Windows-Benutzers in einem neuen Request das Kerberos-Ticket im HTTP-Header mitsenden muss. Dieses Ticket liegt entweder schon im Ticket-Cache oder es muss noch beim KDC geholt werden.

Webspheres IBM Runtime Java

Die Spring-Bibliothek für Kerberos beinhaltet leider nur einen KerberosTicketValidator, der mit dem Oracle-JRE funktioniert und explizit nicht mit dem IBM-JRE. Speziell die Referenz auf die Implementierung des LoginModule ist bei der IBM-JRE eine andere. Einen weiteren Unterschied bildet die Implementierung der Kommunikation mit dem KDC.

Die IBM-Implementierung ist im Wesentlichen eine Kopie des SunJaasKerberosTicketValidator bis auf  folgende Ausschnitte:

1@Override
2public KerberosTicketValidation run() throws Exception {
3    Principal p = (Principal) Subject.getSubject(AccessController.getContext()).getPrincipals().toArray()[0];
4    GSSManager manager = GSSManager.getInstance();
5    Oid kerberos = new Oid("1.3.6.1.5.5.2");
6    GSSName serverGSSName = manager.createName(p.getName(), null);
7    GSSCredential serverGSSCreds =
8        manager.createCredential(serverGSSName, GSSCredential.INDEFINITE_LIFETIME, kerberos, GSSCredential.ACCEPT_ONLY);
9    GSSContext context = manager.createContext(serverGSSCreds);
10    byte[] responseToken = context.acceptSecContext(kerberosTicket, 0, kerberosTicket.length);
11    GSSName gssName = context.getSrcName();
12    if (gssName == null) {
13        throw new BadCredentialsException("GSSContext name of the context initiator is null");
14    }
15    if (!holdOnToGSSContext) {
16        context.dispose();
17    }
18    return new KerberosTicketValidation(gssName.toString(), servicePrincipal, responseToken, context);
19}
20 
21@Override
22public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
23    HashMap<String, String> options = new HashMap<String, String>();
24    options.put("useKeytab", this.keyTabLocation);
25    options.put("principal", this.servicePrincipalName);
26    if (this.debug) {
27        options.put("debug", "true");
28    }
29    options.put("credsType", "acceptor");
30 
31    return new AppConfigurationEntry[] {new AppConfigurationEntry("com.ibm.security.auth.module.Krb5LoginModule",
32        AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options),};
33}

Die Implementierung der run()-Methode geht auf die Dokumentation von IBM zurück.

Möglichkeit des Aufrufs ohne SSO

Eine letzte Anforderung war die Schaffung der Möglichkeit, einen Login in die Webanwendung zu ermöglichen, ohne dass eine automatische Anmeldung per SSO erfolgt. Dies kann nützlich sein, wenn der Anwender sich nicht mit seinem eigenen Benutzer, sondern mit einem fremden anmelden möchte.

Die Idee hierzu war simpel. Es wurde ein Hostname im DNS eingetragen, der die Subdomäne nosso enthält und auf die gleiche IP zeigt wie der Hostname ohne nosso-Subdomäne. In dem Beispiel wäre die Webanwendung also auch über die URL https://tomcat.nosso.codecentric.de/beispiel erreichbar.

In der Webanwendung wurde dann in dem eigenen SpnegoEntryPoint eine Weiche implementiert. Der Ausschnitt der überschrieben Methode zeigt die Weiche.

1@Override
2public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException ex)
3        throws IOException, ServletException {
4    if (!request.getRequestURL().contains(".nosso.")) {
5        response.addHeader("WWW-Authenticate", "Negotiate");
6        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
7    } else {
8        // Es wird kein HTTP-Header ergänzt, so dass der Request ganz normal an die springSecurityFilterChain 
9        // übergeben wird und somit kein SSO gestartet wird
10    }
11    ...
12}

Somit wird der Aufruf von https://tomcat.nosso.codecentric.de/beispiel keinen Kerberos-Mechanismus auslösen, sondern zu einem Redirect zu https://tomcat.nosso.codecentric.de/beispiel/login führen, wo der Anwender dann die gewünschten Benutzerdaten eingeben kann.

Fazit

Viel Experimentieren war notwendig, um diese Lösung zu erarbeiten. Ich hoffe, mit meiner Ausführung dazu beitragen zu können, dass künftig weniger experimentiert werden muss.

Beitrag teilen

//

Weitere Artikel in diesem Themenbereich

Entdecke spannende weiterführende Themen und lass dich von der codecentric Welt inspirieren.

//

Gemeinsam bessere Projekte umsetzen.

Wir helfen deinem Unternehmen.

Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.

Hilf uns, noch besser zu werden.

Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.