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.
Weitere Beiträge
von Thomas Bosch
Dein Job bei codecentric?
Jobs
Agile Developer und Consultant (w/d/m)
Alle Standorte
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.
Blog-Autor*in
Thomas Bosch
IT Consultant
Du hast noch Fragen zu diesem Thema? Dann sprich mich einfach an.
Du hast noch Fragen zu diesem Thema? Dann sprich mich einfach an.