1

I am using okta to do authentication. Our company's okta disabled the 'default' authorization server. So right now I cannot use 'okta-spring-security-starter' to simple do this to verify token passed from url headers:

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
public class OktaOAuth2WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/health").permitAll()
                .anyRequest().authenticated()
                .and()
                .oauth2ResourceServer().jwt();

        http.cors();

        Okta.configureResourceServer401ResponseBody(http);

    }
}

So I need to hit okta introspect endpoint (https://developer.okta.com/docs/reference/api/oidc/#introspect) to verify. So I am wondering can I integrate this procedure within the config of WebSecurityConfigurerAdapter. maybe something like this???:

import com.okta.spring.boot.oauth.Okta;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
public class OktaOAuth2WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/health").permitAll()
                .anyRequest().authenticated()
                .and()
                /*add something there*/

        http.cors();


    }
}

I saw something like override AuthenticationProvider(Custom Authentication provider with Spring Security and Java Config), and use httpbasic auth. Can I do similiar thing if I use .oauth2ResourceServer().jwt().

My idea is override the authentication provider and in the provider, hit the okta introspect endpoint, will this work???

Hongli Bu
  • 461
  • 12
  • 37

2 Answers2

2

I don't use Okta thus I don't know how exactly it works. But I have 2 assumptions:

  • Every request contains an accessToken in the Authorization header
  • You make a POST request to ${baseUrl}/v1/introspect and it will answer you with true or false to indicate that accessToken is valid or not

With these 2 assumptions in mind, if I have to manually implement custom security logic authentication, I would do following steps:

  • Register and implement a CustomAuthenticationProvider
  • Add a filter to extract access token from request

Registering custom authentication provider:

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

@Bean
CustomAuthenticationProvider customAuthenticationProvider(){
    return new CustomAuthenticationProvider();
}

CustomAuthenticationProvider:

public class CustomAuthenticationProvider implements AuthenticationProvider {

private static final Logger logger = LoggerFactory.getLogger(CustomAuthenticationProvider.class);

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    logger.debug("Authenticating authenticationToken");
    OktaTokenAuthenticationToken auth = (OktaTokenAuthenticationToken) authentication;
    String accessToken = auth.getToken();

    // You should make a POST request to ${oktaBaseUrl}/v1/introspect
    // to determine if the access token is good or bad

    // I just put a dummy if here

    if ("ThanhLoyal".equals(accessToken)){
        List<GrantedAuthority> authorities = Collections.singletonList(new SimpleGrantedAuthority("USER"));
        logger.debug("Good access token");
        return new UsernamePasswordAuthenticationToken(auth.getPrincipal(), "[ProtectedPassword]", authorities);
    }
    logger.debug("Bad access token");
    return null;
}

@Override
public boolean supports(Class<?> clazz) {
    return clazz == OktaTokenAuthenticationToken.class;
}

}

To register the filter to extract accessToken from request:

// Still in OktaOAuth2WebSecurityConfig.java
@Override
protected void configure(HttpSecurity http) throws Exception {
    http
            .addFilterAfter(accessTokenExtractorFilter(), UsernamePasswordAuthenticationFilter.class)
            .authorizeRequests().anyRequest().authenticated();
            // And other configurations

}

@Bean
AccessTokenExtractorFilter accessTokenExtractorFilter(){
    return new AccessTokenExtractorFilter();
}

And the filter it self:

public class AccessTokenExtractorFilter extends OncePerRequestFilter {

private static final Logger logger = LoggerFactory.getLogger(AccessTokenExtractorFilter.class);

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
    logger.debug("Filtering request");
    Authentication authentication = getAuthentication(request);
    if (authentication == null){
        logger.debug("Continuing filtering process without an authentication");
        filterChain.doFilter(request, response);
    } else {
        logger.debug("Now set authentication on the request");
        SecurityContextHolder.getContext().setAuthentication(authentication);
        filterChain.doFilter(request, response);
    }
}

private Authentication getAuthentication(HttpServletRequest request) {
    String accessToken = request.getHeader("Authorization");
    if (accessToken != null){
        logger.debug("An access token found in request header");
        List<GrantedAuthority> authorities = Collections.singletonList(new SimpleGrantedAuthority("USER"));
        return new OktaTokenAuthenticationToken(accessToken, authorities);
    }

    logger.debug("No access token found in request header");
    return null;
}

}

I have uploaded a simple project here for your easy reference: https://github.com/MrLoyal/spring-security-custom-authentication

How it works:

  • The AccessTokenExtractorFilter is placed right after the UsernamePasswordAuthenticationFilter, which is a default filter by Spring Security
  • A request arrives, the above filter extracts accessToken from it and place it in the SecurityContext
  • Later, the AuthenticationManager calls the AuthenticationProvider(s) to authenticate request. This case, the CustomAuthenticationProvider is invoked

BTW, your question should contain spring-security tag.

Update 1: About AuthenticationEntryPoint

An AuthenticationEntryPoint declares what to do when an unauthenticated request arrives ( in our case, what to do when the request does not contain a valid "Authorization" header).

In my REST API, I simply response 401 HTTP status code to client.

// CustomAuthenticationEntryPoint
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
    response.reset();
    response.setStatus(401);
    // A utility method to add CORS headers to the response
    SecUtil.writeCorsHeaders(request, response);
}

Spring's LoginUrlAuthenticationEntryPoint redirects user to login page if one is configured.

So if you want to redirect unauthenticated requests to Okta's login page, you may use a AuthenticationEntryPoint.

ThanhLoyal
  • 393
  • 3
  • 11
  • 1
    I will update my answer, the additional answer is too long for a comment. – ThanhLoyal Jun 11 '20 at 08:38
  • 1
    ```new UsernamePasswordAuthenticationToken(auth.getPrincipal(), "[ProtectedPassword]", authorities);``` in my understanding the authorities is like "Role" right? so we can use it like```antMatchers("/health").hasAuthorities('Admin')``` – Hongli Bu Jun 11 '20 at 09:01
  • 1
    can we put the /introspect http call(the verification procedure) into addFilterAfter? – Hongli Bu Jun 11 '20 at 09:04
  • 1
    Yeah, it is like "Role", except a few cases, e.g ```@PreAuthorize("hasRole('ADMIN')") ``` and ```@PreAuthorize("hasAuthority('ADMIN')") ``` (Please figure it out your self). But if you pass an empty ```List<> authorities``` into the ```UsernamePasswordAuthenticationToken``` constructor, Spring Security will treat this token as UNAUTHORIZED, so I just passed a dummy "USER" authority into the constructor to pass the check. – ThanhLoyal Jun 11 '20 at 09:08
2

Spring Security 5.2 ships with support for introspection endpoints. Please take a look at the Opaque Token sample in the GitHub repo.

To answer briefly here, though, you can do:

http
    .authorizeRequests(authz -> authz
        .anyRequest().authenticated()
    )
    .oauth2ResourceServer(oauth2 -> oauth2
        .opaqueToken(opaque -> opaque
            .introspectionUri("the-endpoint")
            .introspectionClientCredentials("client-id", "client-password")
        )
    );

If you are using Spring Boot, then it's a bit simpler. You can provide those properties in your application.yml:

spring:
  security:
    oauth2:
      resourceserver:
        opaquetoken:
          introspection-uri: ...
          client-id: ...
          client-secret: ...

And then your DSL can just specify opaqueToken:

http
    .authorizeRequests(authz -> authz
        .anyRequest().authenticated()
    )
    .oauth2ResourceServer(oauth2 -> oauth2
        .opaqueToken(opaque -> {})
    );
jzheaux
  • 7,042
  • 3
  • 22
  • 36
  • is it the client-secret required? what if we use Authorization Code flow with PKCE? – Hongli Bu Jun 11 '20 at 17:07
  • 1
    I think that's up to Okta. The RFC requires that introspection endpoints require a client id and secret. – jzheaux Jun 11 '20 at 17:11
  • 1
    Additionally, the flows (Authorization Code, Client Credentials, etc.) are typically done by OAuth 2.0 Clients, not by OAuth 2.0 Resource Servers. The two concepts (Authorization Code and Introspection) don't typically go together. – jzheaux Jun 11 '20 at 17:17
  • yeah, Introspection and Flows are different things, sorry for confusion. So if my okta app are using PKCE, I don't need to provide client-secret right? ```oauth2ResourceServer``` lib(or functions) won't throw errors right? – Hongli Bu Jun 11 '20 at 17:25
  • I dive into the source code of oauth2ResourceServer, it seems client_secret is required. – Hongli Bu Jun 11 '20 at 17:30
  • 1
    PKCE is a mechanism used by OAuth 2.0 Clients that coordinates with Authorization Code to exchange a code for a token. Introspection is a mechanism used by OAuth 2.0 Resource Servers to verify a token's validity and extract its claims. They don't have anything to do with one another. According to Okta's docs, a client_id and secret are required, which aligns with the RFC. – jzheaux Jun 11 '20 at 17:57
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/215771/discussion-between-hongli-bu-and-jzheaux). – Hongli Bu Jun 11 '20 at 19:02