Add Custom AuthenticationProvider to Spring Boot + oauth +oidc
Asked Answered
L

1

8

I've developed a basic oauth/oidc example using SpringBoot 2.1.7 with Okta providing authentication services. Here is my Gradle dependency setup for reference:

plugins {
id 'org.springframework.boot' version '2.1.7.RELEASE'
id 'java'
}
apply plugin: 'io.spring.dependency-management'

sourceCompatibility = '1.8'

configurations {
  developmentOnly
  runtimeClasspath {
    extendsFrom developmentOnly
 }
}

repositories {
    mavenCentral()
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'com.okta.spring:okta-spring-boot-starter:1.2.1'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'com.h2database:h2'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
}

All elements on the Okta side are configured properly and the example works as expected. Pretty much a "hello world" type demo here. I would like to add a custom authentication provider that executes after all other AuthenticationProvider's provided by Spring. I have stepped through the code using my debugger and noticed Spring auto configures several AuthenticationProviders. They are:

  1. AnonymousAuthenticationProvider
  2. OAuth2LoginAuthenticationProvider
  3. OidcAuthorizationCodeAuthenticationProvider
  4. OAuth2AuthorizationCodeAuthenticationProvider
  5. JwtAuthenticationProvider

I would like to run my authentication provider in the 6th position. I tried the following WebSecurityConfig that did not work even though the configure(AuthenticationManagerBuilder authBuilder) method fires:

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter
{

    @Autowired
    private CustomAuthenticationProvider customAuthenticationProvider;

    @Override
    protected void configure(HttpSecurity http) throws Exception
    {
    http.authorizeRequests()
        .anyRequest().authenticated()
        .and()
        .oauth2Login()
        .successHandler(customOauthLoginSuccessHandler())
        .failureHandler(customOauthLoginFailureHandler())
        .and()
        .oauth2Client();
    http.csrf().disable();
    }

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

    @Bean
    public CustomOauthLoginSuccessHandler customOauthLoginSuccessHandler()
    {
        CustomOauthLoginSuccessHandler handler = new CustomOauthLoginSuccessHandler();
        return handler;
    }

    @Bean
    public CustomOauthLoginFailureHandler customOauthLoginFailureHandler()
    {
        CustomOauthLoginFailureHandler handler = new CustomOauthLoginFailureHandler();
        handler.setUseForward(true);
        handler.setDefaultFailureUrl("/oautherror");
        return handler;
    }

}

My Authentication provider never executes. Here is my custom AP:

@Component
public class CustomAuthenticationProvider implements AuthenticationProvider
{
    private Logger logger = LoggerFactory.getLogger(CustomAuthenticationProvider.class);
    private CustomOidcUserService customOidcUserService;

    public CustomAuthenticationProvider(CustomOidcUserService customOidcUserService)
    {
        this.customOidcUserService = customOidcUserService;
    }

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException
    {
        logger.info("CustomAuthenticationProvider executing...");
        Object user = authentication.getPrincipal();
        if (user instanceof DefaultOidcUser)
        {
            logger.info("principal is instanceof DefaultOidcUser");
            DefaultOidcUser authToken = (DefaultOidcUser) user;
            this.customOidcUserService.loadUserByUsername(authToken.getClaims().get("preferred_username").toString());
            // add additional info to the Authentication object
        }

        return authentication;
    }

    @Override
    public boolean supports(Class<?> authentication)
    {
        return OAuth2LoginAuthenticationToken.class.isAssignableFrom(authentication);
    }
}

My main goal is to add additional information about the authenticated user that comes from our legacy Oracle database. It is NOT an option to add this additional information to the Okta side (in the cloud).

Note that I was able to easily add success and failure handlers into the authentication path. I feel like the inherent nature of SpringBoot's auto-config may be getting in my way. Just need to know how to get around this.

Additional info, yml file:

okta:
  oauth2:
    issuer: https://myhost.okta.com/oauth2/default
    client-id: 123456AAABBBCCC
    client-secret: AAAAAAAAABBBBBBBBBBB
Luminiferous answered 23/8, 2019 at 22:1 Comment(0)
L
2

I'm answering my own post because I have determined this is the wrong approach. After running several tests I was able to add a custom AuthenticationProvider (AP), however there is no guarantee that my provider will ever run if a previous provider succeeds and returns a result. This is the default behavior of the ProviderManager class as it spins through each AP. A better approach is to define a CustomUserDetailsService that would extend OidcUserService or DefaultOAuth2UserService. Here is my updated code that answers my question:

// @Component is removed
public class CustomAuthenticationProvider implements AuthenticationProvider
{
    private Logger logger = LoggerFactory.getLogger(CustomAuthenticationProvider.class);
    private CustomOidcUserService customOidcUserService;

    public CustomAuthenticationProvider(CustomOidcUserService customOidcUserService)
    {
        this.customOidcUserService = customOidcUserService;
    }

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException
{
    logger.info("CustomAuthenticationProvider executing...");
    Object user = authentication.getPrincipal();
    if (user instanceof DefaultOidcUser)
    {
        logger.info("principal is instanceof DefaultOidcUser");
        DefaultOidcUser authToken = (DefaultOidcUser) user;
        this.customOidcUserService.loadUserByUsername(authToken.getClaims().get("preferred_username").toString());
        // add additional info to the Authentication object
    }

    return authentication;
}

@Override
public boolean supports(Class<?> authentication)
{
    return OAuth2LoginAuthenticationToken.class.isAssignableFrom(authentication);
}
}

Here are my changes to the WebSecurityConfig:

    @Override
protected void configure(HttpSecurity http) throws Exception
{
    http.authorizeRequests()
        .anyRequest().authenticated()
        .and()
        .oauth2Login()
        .successHandler(customOauthLoginSuccessHandler())
        .failureHandler(customOauthLoginFailureHandler())
        .and()
        .oauth2Client()
        .and().authenticationProvider(new CustomAuthenticationProvider());

    http.csrf().disable();
}

This code does add an additional custom AP to the correct internally stored list of APs. In this case, my custom AP is added to the list maintained by WebSecurityConfigurerAdapter$DefaultPasswordEncoderAuthenticationManagerBuilder. Again, I am not using this approach so this post is closed.

Luminiferous answered 28/8, 2019 at 14:50 Comment(1)
are you refreshing the access token silently? If yes, how?Broth

© 2022 - 2024 — McMap. All rights reserved.