javaspringactive-directorysingle-sign-onkerberos

Kerberos SSO Issue with AD change of upns in Spring Boot Application


So I have a spring boot application that authenticates its users via kerberos and then makes an ldap-search for roles and authorities and such. It is working fine. The problem is, that there will be a change in the AD. So lets say a user is named Jane Doe and her upn right now is JDOE@ABC.COM and it will be changed to jane.doe@anotherbestcompany.com. Now there are users like me who have already been changed for test-purposes and SSO is not working for me and I can't figure out why. Lets say im this Jane Doe so it says in the log

2024-04-17 16:10:16,448 WARN  [https-jsse-nio-10.100.147.15-8443-exec-10][o.s.s.k.w.a.SpnegoAuthenticationProcessingFilter] Negotiate Header was invalid: Negotiate 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 {span_id=8b7b8299baf54a54, trace_flags=01, trace_id=c316521436a8f809253b89209d5147ff}
org.springframework.security.core.userdetails.UsernameNotFoundException: User JDOE@ABC.COM not found in directory.

which makes sense because I'm Jane.Doe@anotherbestcompany.com now. But where does the old upn come from? I don't know where to look.

Oh, and here is the setup in the springboot application

@Configuration
@EnableWebMvc
@ImportResource({"classpath*:spring/**/*applicationContext.xml"})
@ComponentScan(basePackages = {"com.abc"})
public class WebConfiguration implements WebMvcConfigurer {
  private ApplicationContext applicationContext;

  @Autowired
  public void setApplicationContext(ApplicationContext applicationContext) {
    this.applicationContext = applicationContext;
  }

  @Override
  public void configureMessageConverters(final List<HttpMessageConverter<?>> converters) {

//    DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
  //  dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));

    ObjectMapper dateFormatMapper = new ObjectMapper();
    dateFormatMapper.registerModule(new JavaTimeModule());
    dateFormatMapper/*.findAndRegisterModules()*/.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
//    dateFormatMapper.setDateFormat(dateFormat);
    converters.add(new MappingJackson2HttpMessageConverter(dateFormatMapper));


    converters.add(new ByteArrayHttpMessageConverter());
    converters.add(new StringHttpMessageConverter());
    converters.add(new ResourceHttpMessageConverter());
    converters.add(new FormHttpMessageConverter());
    /*
    SourceHttpMessageConverter – converts javax.xml.transform.Source
    FormHttpMessageConverter – converts form data to/from a MultiValueMap<String, String>.
        Jaxb2RootElementHttpMessageConverter – converts Java objects to/from XML (added only if JAXB2 is present on the classpath)
    MappingJackson2HttpMessageConverter – converts JSON (added only if Jackson 2 is present on the classpath)
    MappingJacksonHttpMessageConverter – converts JSON (added only if Jackson is present on the classpath)
    AtomFeedHttpMessageConverter – converts Atom feeds (added only if Rome is present on the classpath)
    RssChannelHttpMessageConverter – converts RSS feeds (added only if Rome is present on the classpath)
*/
  }

  @EventListener(ContextRefreshedEvent.class)
  public void onContextRefreshed() {
    final DbMigrateService migrateService = applicationContext.getBean(DbMigrateService.class);
    migrateService.migrateIfNecessary();
    final ReportingDbMigrateService reportingMigrateService =
        applicationContext.getBean(ReportingDbMigrateService.class);
    reportingMigrateService.migrateIfNecessary();
  }
}




package com.abc.def.service.config;


import com.abc.def.service.ProductionSelector;
import com.abc.def.service.model.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.FileSystemResource;
import org.springframework.jndi.JndiTemplate;
import org.springframework.ldap.core.ContextSource;
import org.springframework.ldap.core.support.BaseLdapPathContextSource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.kerberos.authentication.KerberosServiceAuthenticationProvider;
import org.springframework.security.kerberos.authentication.sun.SunJaasKerberosTicketValidator;
import org.springframework.security.kerberos.client.config.SunJaasKrb5LoginConfig;
import org.springframework.security.kerberos.client.ldap.KerberosLdapContextSource;
import org.springframework.security.kerberos.web.authentication.SpnegoAuthenticationProcessingFilter;
import org.springframework.security.kerberos.web.authentication.SpnegoEntryPoint;
import org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider;
import org.springframework.security.ldap.search.FilterBasedLdapUserSearch;
import org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator;
import org.springframework.security.ldap.userdetails.LdapAuthoritiesPopulator;
import org.springframework.security.ldap.userdetails.LdapUserDetailsMapper;
import org.springframework.security.ldap.userdetails.LdapUserDetailsService;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.web.cors.CorsUtils;

import javax.naming.NamingException;

@Configuration
@EnableWebSecurity
@Slf4j
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

  @Value("DC=abc,DC=com")
  private String rootDN;

  @Value("/apps/HTTP.keytab")
  private String keytabLocation;

  @Value("DC=abc,DC=com")
  private String searchBase;

  @Value("(| (userPrincipalName={0}) (sAMAccountName={0}))")
  private String searchFilter;

  @Value("OU=GroupsAdministrative,OU=GE,DC=abc,DC=com")
  private String groupSearchBase;

  @Value("cn")
  private String groupRoleAttribute;

  @Value("(member={0})")
  private String groupSearchFilter;

  @Override
  protected void configure(HttpSecurity http) throws Exception {

    CookieCsrfTokenRepository cookieCsrfTokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse();

    // this is needed because of two applications.
    cookieCsrfTokenRepository.setCookiePath("/");

    boolean production = ProductionSelector.isProduction();

    http
        .csrf().csrfTokenRepository((cookieCsrfTokenRepository))
        .and()
        .exceptionHandling()
        .authenticationEntryPoint(spnegoEntryPoint())
        .and()
        .authorizeRequests()
        .requestMatchers(CorsUtils::isPreFlightRequest).permitAll()
        .antMatchers("/ownbank/legal").hasAuthority(User.getOwnBankLegalPermission(production))
        .antMatchers("/ownbank/organisational").hasAuthority(User.getOwnBankOrganisationalPermission(production))
        .antMatchers("/ownbank/finance").hasAuthority(User.getOwnBankFinancePermission(production))
        .antMatchers("/calendar/*").hasAuthority(User.getWritePermission(production))
        .antMatchers("/compatibl/*").hasAnyAuthority(User.getCompatiblReadPermission(production),
            User.getCompatiblWritePermission(production))
        .antMatchers("/healthcheck/*").hasAnyAuthority(User.getHealthCheckReadPermission(production), User.getHealthCheckWritePermission(production))
        .antMatchers("/s2s/dump-config").hasAnyAuthority(User.getS2SWritePermission(production))
        .antMatchers("/s2s/run", "/s2s/upload/**").hasAnyAuthority(User.getS2SWritePermission(production))
        .antMatchers("/s2s/sap-dumps", "/s2s/summit-dumps", "/s2s/processing-events", "/s2s/processing-events-summary",
            "/s2s/download-processing-events", "/s2s/audit-records", "/s2s/download-sap-dump",
            "/s2s/download-summit-dump", "/s2s/download/**", "/s2s/replay", "/s2s/processing-steps",
            "/s2s/summit-dump-counts", "/s2s/start-diff", "/s2s/diffs", "/s2s/diffs/**", "/s2s/diffGroup",
            "/s2s/tradeDiff"
        ).hasAnyAuthority(User.getS2SReadPermission(production), User.getS2SWritePermission(production))
        .anyRequest().authenticated()
        .and()
        .addFilterBefore(spnegoAuthenticationProcessingFilter(authenticationManagerBean()),
            BasicAuthenticationFilter.class);
  }

  @Override
  protected void configure(AuthenticationManagerBuilder authManagerBuilder) throws Exception {
    UserDetailsService userDetailsService = ldapUserDetailsService();
    authManagerBuilder
        .authenticationProvider(activeDirectoryLdapAuthenticationProvider())
        .authenticationProvider(kerberosServiceAuthenticationProvider(userDetailsService))
        .userDetailsService(userDetailsService);
  }

  @Bean
  @Override
  public UserDetailsService userDetailsServiceBean() throws Exception {
    return super.userDetailsServiceBean();
  }

  @Bean
  public SpnegoEntryPoint spnegoEntryPoint() {
    return new SpnegoEntryPoint();
  }

  @Bean
  public AuthenticationProvider activeDirectoryLdapAuthenticationProvider() {
    return new ActiveDirectoryLdapAuthenticationProvider("", getAdServerUrl(),rootDN);
  }


  @Bean
  public SpnegoAuthenticationProcessingFilter spnegoAuthenticationProcessingFilter(
      AuthenticationManager authenticationManager) {
    SpnegoAuthenticationProcessingFilter filter = new SpnegoAuthenticationProcessingFilter();
    filter.setAuthenticationManager(authenticationManager);
    filter.setSkipIfAlreadyAuthenticated(true);
    return filter;
  }


  @Bean
  public AuthenticationProvider kerberosServiceAuthenticationProvider(UserDetailsService userDetailsService) {
    KerberosServiceAuthenticationProvider provider = new org.springframework.security.kerberos.authentication.KerberosServiceAuthenticationProvider();
    provider.setTicketValidator(getTicketValidator());
    provider.setUserDetailsService(userDetailsService);

    return provider;
  }

  @Bean
  public SunJaasKerberosTicketValidator getTicketValidator() {
    SunJaasKerberosTicketValidator ticketValidator = new SunJaasKerberosTicketValidator();

    log.info("service principal path for Kerberos => {}", getAdServicePrincipal());

    ticketValidator.setServicePrincipal(getAdServicePrincipal());
    ticketValidator.setKeyTabLocation(new FileSystemResource(keytabLocation));
//    ticketValidator.setDebug(true);
    return ticketValidator;
  }

  @Bean
  public UserDetailsService ldapUserDetailsService() {

    BaseLdapPathContextSource contextSource = ldapContextSource();
    FilterBasedLdapUserSearch userSearch = new FilterBasedLdapUserSearch(searchBase, searchFilter, contextSource);
    userSearch.setSearchSubtree(true);
    LdapUserDetailsService ldapUserDetailsService = new LdapUserDetailsService(userSearch,
        ldapAuthoritiesPopulator(contextSource));
    ldapUserDetailsService.setUserDetailsMapper(new LdapUserDetailsMapper());

    return ldapUserDetailsService;
  }

  @Bean
  public LdapAuthoritiesPopulator ldapAuthoritiesPopulator(ContextSource contextSource) {

    DefaultLdapAuthoritiesPopulator populator = new DefaultLdapAuthoritiesPopulator(contextSource, groupSearchBase);
    populator.setSearchSubtree(true);
    populator.setGroupRoleAttribute(groupRoleAttribute);
    populator.setGroupSearchFilter(groupSearchFilter);
    populator.setRolePrefix("");
    populator.setConvertToUpperCase(false);
    return populator;
  }

  @Bean
  public KerberosLdapContextSource ldapContextSource() {
    KerberosLdapContextSource source = new KerberosLdapContextSource(getAdServerUrl());
    source.setLoginConfig(getSunJaasKrb5LoginConfig());
    return source;
  }

  @Bean
  public SunJaasKrb5LoginConfig getSunJaasKrb5LoginConfig() {
    SunJaasKrb5LoginConfig config = new SunJaasKrb5LoginConfig();
    config.setServicePrincipal(getAdServicePrincipal());
    config.setKeyTabLocation(new FileSystemResource(keytabLocation));
    config.setUseTicketCache(false);
    config.setIsInitiator(true);

    //config.setDebug(true);
    return config;
  }

  private String getAdServerUrl() {

    JndiTemplate jndi = new JndiTemplate();
    try {
      return (String) jndi.lookup("java:/comp/env/ad.server");
    } catch (NamingException e) {
      throw new RuntimeException(e);
    }
  }

  private String getAdServicePrincipal() {
    JndiTemplate jndi = new JndiTemplate();
    try {
      return (String) jndi.lookup("java:/comp/env/ad.service.principal");
    } catch (NamingException e) {
      throw new RuntimeException(e);
    }
  }
}

Solution

  • That's not the UPN – that's the Kerberos principal name. It's the primary user (and service) identifier in Kerberos, and it's only similar to the UPN but otherwise there's no relationship between the two.

    (Tickets issued by AD do include the UPN as well, but only as an additional field within the MS PAC, and most software doesn't know how to extract anything from the PAC.)

    In Active Directory, user Kerberos principals are formed from account@REALM, where the first half is always the user's sAMAccountName and the second half is the Kerberos realm name (each AD domain has exactly one Kerberos realm and it is always upper-case, unlike UPN suffixes which are typically lowercase). You can see yours from klist.

    So if there are no domain trusts, then it is safe to trim the @REALM and search for a sAM­Account­Name matching the first half of the user principal. According to this, Spring can do it using (sAMAccountName={1}).