Kerberos to Front via REST Controller

1k views Asked by At

Hi everyone,

I'm trying to implement Kerberos for an application in Spring Boot + Vue.js currently using LDAP Authentication. I managed to set up everything correctly to get this sample working (full tutorial here) on a remote server with a connection using Windows AD. Now I want to implement this in a REST API context so an application with a Vue.js frontend can access it and skip the login page if the Kerberos authentication succeeds.

I tried to implement the following code on my Spring Boot application. My first try was to make it display the name of the user on the web page like on the given example to validate the good functioning of the application. There's no error but I get the following message: User null. Here's the code that I tried to implement:

WebController.java

 @GetMapping(value = "/kerberos")
     @ResponseBody
     public String  sayHello(HttpServletRequest req) {
 
         if (req != null) {
             LOGGER.info("User " + req.getRemoteUser());
             return "Hello " + req.getRemoteUser();
         } else {
             LOGGER.info("REQ IS NULL");
             return "PRINCIPAL IS NULL";
         }
     }

WebSecurityConfiguration.java

@Configuration
@EnableWebSecurity
@PropertySource("classpath:application.properties")
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {

  @Autowired
  public Environment env;
    
  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http
        .exceptionHandling()
          .authenticationEntryPoint(spnegoEntryPoint())
          .and()
        .authorizeRequests()
            .antMatchers("/**").permitAll()
          .anyRequest().authenticated()
          .and()
        .formLogin()
            .loginPage("/login").permitAll()
            .and()
        .logout()
          .permitAll()
          .and()
        .addFilterBefore(
            spnegoAuthenticationProcessingFilter(),
            BasicAuthenticationFilter.class);
  }

  @Override
  protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth
            .authenticationProvider(activeDirectoryLdapAuthenticationProvider())
            .authenticationProvider(kerberosServiceAuthenticationProvider());
  }

  @Bean
  public ActiveDirectoryLdapAuthenticationProvider activeDirectoryLdapAuthenticationProvider() {
    return new >ActiveDirectoryLdapAuthenticationProvider(env.getRequiredProperty("custom.ad.domain"), env.getRequiredProperty("spring.ldap.urls"));
  }

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

  @Bean
  public SpnegoAuthenticationProcessingFilter spnegoAuthenticationProcessingFilter() {
    SpnegoAuthenticationProcessingFilter filter = new SpnegoAuthenticationProcessingFilter();
    try {
      AuthenticationManager authenticationManager = authenticationManagerBean();
      filter.setAuthenticationManager(authenticationManager);
    } catch (Exception e) {
      e.printStackTrace();
    }
    return filter;
  }

  @Bean
  public SunJaasKerberosTicketValidator sunJaasKerberosTicketValidator() {
    SunJaasKerberosTicketValidator ticketValidator = new SunJaasKerberosTicketValidator();
    ticketValidator.setServicePrincipal(env.getRequiredProperty("spring.krb.principal"));
    ticketValidator.setKeyTabLocation(new FileSystemResource(env.getRequiredProperty("spring.krb.keytab")));
    ticketValidator.setDebug(true);
    return ticketValidator;
  }

  @Bean
  public KerberosLdapContextSource kerberosLdapContextSource() throws Exception {
    KerberosLdapContextSource contextSource = new KerberosLdapContextSource(env.getRequiredProperty("spring.ldap.urls"));
    contextSource.setLoginConfig(loginConfig());
    return contextSource;
  }

  public SunJaasKrb5LoginConfig loginConfig() throws Exception {
    SunJaasKrb5LoginConfig loginConfig = new SunJaasKrb5LoginConfig();
    loginConfig.setKeyTabLocation(new FileSystemResource(env.getRequiredProperty("spring.krb.keytab")));
    loginConfig.setServicePrincipal(env.getRequiredProperty("spring.krb.principal"));
    loginConfig.setDebug(true);
    loginConfig.setIsInitiator(true);
    loginConfig.afterPropertiesSet();
    return loginConfig;
  }

  @Bean
  public LdapUserDetailsService ldapUserDetailsService() throws Exception {
    FilterBasedLdapUserSearch userSearch =
            new FilterBasedLdapUserSearch(env.getRequiredProperty("custom.ldap.base"), env.getRequiredProperty("custom.ldap.filter"), kerberosLdapContextSource());
    LdapUserDetailsService service =
            new LdapUserDetailsService(userSearch, new ActiveDirectoryLdapAuthoritiesPopulator());
    service.setUserDetailsMapper(new LdapUserDetailsMapper());
    return service;
  }

  @Bean
  public KerberosServiceAuthenticationProvider kerberosServiceAuthenticationProvider() throws Exception {
    KerberosServiceAuthenticationProvider provider = new KerberosServiceAuthenticationProvider();
    provider.setTicketValidator(sunJaasKerberosTicketValidator());
    provider.setUserDetailsService(ldapUserDetailsService());
    return provider;
  }



  @Bean
  @Override
  public AuthenticationManager authenticationManagerBean() throws Exception {
    return super.authenticationManagerBean();
  }
}

Is there a way to implement the behaviour with Kerberos and a REST API? Or the method I tried to implement is only working with Java Servlets?

2

There are 2 answers

1
pan-leszeczek On

by having in your security configuration those lines:

...
.authorizeRequests()
  .antMatchers("/**").permitAll()
...

you opened any of your endpoints to anyone. Including your test endpoint "/kerberos". You got username "null" because Kerberos didn't authenticate anyone. Try being more specific in your opened endpoints, then you can investigate in debug mode why your request was not validated against LDAP The finish line is that once your request sender will be validated via Kreberos you'll get your username as you expected.

0
Rospote On

After months on research we manage to make it work. The application tries to connect through Kerberos at initialization when the user tries to access the website. If it doesn't work, a LDAP login fallback page is called. Here's the configuration file we used for Kerberos + LDAP backup authentication.

UserController.java


    @RestController
    @RequestMapping("users")
    public class UserController {

    private final AuthenticationManager authenticationManager;
    private final JwtUtils jwtUtils;

    public UserController(AuthenticationManager authenticationManager, JwtUtils jwtUtils) {
        this.authenticationManager = authenticationManager;
        this.jwtUtils = jwtUtils;
    }

    @PreAuthorize("isAuthenticated()")
    @GetMapping
    public Principal getCurrentUser(Principal principal) {
        return principal;
    }

    @PostMapping("login")
    public JwtResponse login(@RequestBody Credentials credentials) {
        Authentication authentication = authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(credentials.getUsername(), credentials.getPassword()));

        SecurityContextHolder.getContext().setAuthentication(authentication);
        String jwt = jwtUtils.generateJwtToken(authentication);

        Set<String> roles = authentication.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority)
                .collect(Collectors.toSet());

        return new JwtResponse(jwt, credentials.getUsername(), roles);
    }
}

WebSecurityConfiguration.java

    @Configuration
    @EnableWebSecurity
    @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
    public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Value("${cors.allowed-origins}")
    private String[] allowedOrigins;

    @Value("${spring.ldap.urls}")
    private String ldapUrls;

    @Value("${spring.ldap.base}")
    private String ldapBase;

    @Value("${spring.ldap.filter}")
    private String ldapFilter;

    @Value("${spring.ldap.ad.domain}")
    private String adDomain;

    @Value("${kerberos.principal}")
    private String kerberosPrincipal;

    @Value("${kerberos.keytab}")
    private String kerberosKeytab;

    @Value("${ldap.mock:false}")
    private boolean mockLdap;

    @Autowired
    private JwtAuthFilter jwtAuthFilter;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                // no session for stateless use
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                // deactivation CSRF
                .and().csrf().disable()
                // activation CORS (cf. corsConfigurationSource())
                .cors()
                // by default, no need of authentication for endpoints : dealt with case by case in controllers
                .and().authorizeRequests().anyRequest().permitAll();

        if (!mockLdap) { //we use mock LDAP for local testing
            // activate spnego in case of 401, to add the right header in the response (WWW-Authenticate : Negotiate)
            http.exceptionHandling().authenticationEntryPoint(spnegoEntryPoint()).and()
                    .addFilterBefore(spnegoAuthenticationProcessingFilter(authenticationManagerBean()), BasicAuthenticationFilter.class);
        }

        http.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
    }

    @Bean
    CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(Arrays.asList(allowedOrigins));
        configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
        configuration.setAllowedHeaders(Arrays.asList("authorization", "content-type", "x-auth-token"));
        configuration.setAllowCredentials(true);
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(activeDirectoryLdapAuthenticationProvider());
        if (!mockLdap) {
            auth.authenticationProvider(kerberosServiceAuthenticationProvider());
        }
    }

    @Bean
    public ActiveDirectoryLdapAuthenticationProvider activeDirectoryLdapAuthenticationProvider() {
        ActiveDirectoryLdapAuthenticationProvider provider = new ActiveDirectoryLdapAuthenticationProvider(adDomain, ldapUrls, ldapBase);
        // the provider injects two params ({0} the principal and {1} the username) in the filter
        provider.setSearchFilter(ldapFilter);
        return provider;
    }

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

    @Bean
    @ConditionalOnProperty(name = "ldap.mock", havingValue = "false", matchIfMissing = true)
    public SpnegoAuthenticationProcessingFilter spnegoAuthenticationProcessingFilter(AuthenticationManager authenticationManager) {
        SpnegoAuthenticationProcessingFilter filter = new SpnegoAuthenticationProcessingFilter();
        filter.setAuthenticationManager(authenticationManager);
        return filter;
    }

    @Bean
    @ConditionalOnProperty(name = "ldap.mock", havingValue = "false", matchIfMissing = true)
    public SunJaasKerberosTicketValidator sunJaasKerberosTicketValidator() {
        SunJaasKerberosTicketValidator ticketValidator = new SunJaasKerberosTicketValidator();
        ticketValidator.setServicePrincipal(kerberosPrincipal);
        ticketValidator.setKeyTabLocation(new FileSystemResource(kerberosKeytab));
        ticketValidator.setDebug(true);
        return ticketValidator;
    }

    @Bean
    @ConditionalOnProperty(name = "ldap.mock", havingValue = "false", matchIfMissing = true)
    public KerberosLdapContextSource kerberosLdapContextSource() throws Exception {
        KerberosLdapContextSource contextSource = new KerberosLdapContextSource(ldapUrls);
        contextSource.setLoginConfig(loginConfig());
        return contextSource;
    }

    @ConditionalOnProperty(name = "ldap.mock", havingValue = "false", matchIfMissing = true)
    public SunJaasKrb5LoginConfig loginConfig() throws Exception {
        SunJaasKrb5LoginConfig loginConfig = new SunJaasKrb5LoginConfig();
        loginConfig.setKeyTabLocation(new FileSystemResource(kerberosKeytab));
        loginConfig.setServicePrincipal(kerberosPrincipal);
        loginConfig.setDebug(true);
        loginConfig.setIsInitiator(true);
        loginConfig.afterPropertiesSet();
        return loginConfig;
    }

    @Bean
    public LdapUserDetailsService ldapUserDetailsService() throws Exception {
        FilterBasedLdapUserSearch userSearch = new FilterBasedLdapUserSearch(ldapBase,
                                                                             // in the FilterBasedLdapUserSearch, only one parameter is passed to the filter instead of 2, like in 
                                                                             // in the ActiveDirectoryLdapAuthenticationProvider. We replace {1} by {0} to avoid any problem
                                                                             ldapFilter.replace("{1}", "{0}"),
                                                                             mockLdap ? new DefaultSpringSecurityContextSource(ldapUrls) :
                                                                             kerberosLdapContextSource()
        );
        LdapUserDetailsService service = new UserDetailService(userSearch, new ActiveDirectoryLdapAuthoritiesPopulator());
        service.setUserDetailsMapper(new LdapUserDetailsMapper());
        return service;
    }

    @Bean
    @ConditionalOnProperty(name = "ldap.mock", havingValue = "false", matchIfMissing = true)
    public KerberosServiceAuthenticationProvider kerberosServiceAuthenticationProvider() throws Exception {
        KerberosServiceAuthenticationProvider provider = new KerberosServiceAuthenticationProvider();
        provider.setTicketValidator(sunJaasKerberosTicketValidator());
        provider.setUserDetailsService(ldapUserDetailsService());
        return provider;
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}