-1

I have this demo springboot app and my goal is to understand more about oauth2, I was able to make it work using github auth I configured like this

spring:
  security:
    oauth2:
      client:
        registration:
          github:
            clientId: ***
            clientSecret: ***

This is my gradle dependencies

implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
implementation 'org.springframework.boot:spring-boot-starter-web'

this is my security filter chain

@Slf4j
@Configuration
@EnableWebSecurity
public class OAuth2LoginSecurityConfig {

    @Autowired
    private CustomOAuth2AuthenticationFailureHandler failureHandler;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        CookieCsrfTokenRepository cRepo = CookieCsrfTokenRepository.withHttpOnlyFalse();
        http
            .addFilterBefore(new PrintCsrfTokenFilter(cRepo), CsrfFilter.class)
            .csrf(csrf ->  csrf.csrfTokenRepository(cRepo))
            .authorizeHttpRequests(authorize -> authorize
                .requestMatchers("/", "/error", "/webjars/**","/index.html").permitAll()
                .anyRequest().authenticated()
            )
            .logout((logout) -> logout.logoutSuccessUrl("/").permitAll())
            .oauth2Login(t -> t.failureHandler((request, response, exception) -> {
                log.error(exception.getMessage());
                request.getSession().setAttribute("error.message", exception.getMessage());
                failureHandler.onAuthenticationFailure(request, response, exception);
            }));
        return http.build();
    }
}

This is my debug filter class

@Slf4j
public final class PrintCsrfTokenFilter extends OncePerRequestFilter {

    private CsrfTokenRepository tokenRepository;

    public PrintCsrfTokenFilter(CsrfTokenRepository csrfTokenRepository) {
        Assert.notNull(csrfTokenRepository, "csrfTokenRepository cannot be null");
        this.tokenRepository = csrfTokenRepository;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        DeferredCsrfToken deferredCsrfToken = this.tokenRepository.loadDeferredToken(request, response);
        CsrfToken csrfToken = deferredCsrfToken.get();
        String actualToken = this.resolveCsrfTokenValue(request, csrfToken);
        log.info("csrfToken: {} , actualToken: {}", csrfToken.getToken(), actualToken);

        String xcsrfToken = request.getHeader("X-XSRF-TOKEN");
        log.info("xcsrfToken Token: " + xcsrfToken);

        filterChain.doFilter(request, response);
    }

    private String resolveCsrfTokenValue(HttpServletRequest request, CsrfToken csrfToken) {
        Assert.notNull(request, "request cannot be null");
        Assert.notNull(csrfToken, "csrfToken cannot be null");
        String actualToken = request.getHeader(csrfToken.getHeaderName());
        log.info("header {}: {} ", csrfToken.getHeaderName(), actualToken);
        if (actualToken == null) {
            actualToken = request.getParameter(csrfToken.getParameterName());
            log.info("param {}: {} ", csrfToken.getParameterName(), actualToken);
        }
        return actualToken;
    }
}

heres my front end side

<body>
<script type="text/javascript">
// Declare logout function in the global scope
window.logout = function() {
    $.post("/logout", function() {
        $("#user").html('');
        $(".unauthenticated").show();
        $(".authenticated").hide();
    })
    return true;
}

$.ajaxSetup({
  beforeSend : function(xhr, settings) {
    if (settings.type == 'POST' || settings.type == 'PUT'
        || settings.type == 'DELETE') {
      if (!(/^http:.*/.test(settings.url) || /^https:.*/
        .test(settings.url))) {
        // Only send the token to relative URLs i.e. locally.
        xhr.setRequestHeader("X-XSRF-TOKEN",
          Cookies.get('XSRF-TOKEN'));
      }
    }
  }
});

$(document).ready(function() {
    $.get("/user", function(data, status, xhr) {
        if (xhr.status == 200 && data.name != null && data.name != "") {
            $("#user").html(data.name);
            $(".unauthenticated").hide();
            $(".authenticated").show();
        } else {
            $(".authenticated").hide();
            $(".unauthenticated").show();
        }
    }).fail(function() {
        $(".authenticated").hide();
        $(".unauthenticated").show();
    });
});

</script>



<div class="container">
    <h1>Demo</h1>
    <div class="container unauthenticated">
        With GitHub: <a href="/oauth2/authorization/github">click here</a>
    </div>
    <div class="container authenticated">
        Logged in as: <span id="user"></span>
        <div>
            <button onClick="logout()" class="btn btn-primary">Logout</button>
        </div>
    </div>
</div>
</body>

I can see that the actualToken and the csrfToken.getToken() is the same when I try to debug with breakpoint but I still get this failures when I try to logout

2023-07-31T18:17:31.904+08:00  INFO 10277 --- [nio-8080-exec-9] c.mark.oauth2.Demo.PrintCsrfTokenFilter  : csrfToken: afc6a080-363c-4d9f-87e2-187314baf11a , actualToken: afc6a080-363c-4d9f-87e2-187314baf11a
2023-07-31T18:17:31.905+08:00 DEBUG 10277 --- [nio-8080-exec-9] o.s.security.web.csrf.CsrfFilter         : Invalid CSRF token found for http://localhost:8080/logout

This is how it looks like from the browser, [browser logout case(https://i.stack.imgur.com/HEygr.png) request headers

I wanted to know if I am missing how I should implement OAuth2. Any Answers will be appreciated, I will keep this post updated for everyones learning and reference.

I tried to logout from the browser but its not working because even though the csrf token is the same it keeps on saying Invalid CSRF token found, I am expecting it to work as I have understood it but clearly somethings missing.

mark ortiz
  • 659
  • 1
  • 6
  • 13

1 Answers1

0

The documentation for CSRF in Spring security is now a bit spread and it is not so trivial to find the right section for SPAs. Just apply what is explained there and you should be good.

And, by the way, if you use my starter, activating CSRF protection with http-only=false is just a matter of setting a property:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <!-- For a reactive application, use spring-boot-starter-webflux instead -->
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
    <groupId>com.c4-soft.springaddons</groupId>
    <artifactId>spring-addons-starter-oidc</artifactId>
    <version>7.0.7</version>
</dependency>
cognito-issuer: https://cognito-idp.us-west-2.amazonaws.com/us-west-2_RzhmgLwjl
cognito-client-id: change-me
cognito-secret: change-me

spring:
  security:
    oauth2:
      client:
        provider:
          cognito:
            issuer-uri: ${cognito-issuer}
        registration:
          cognito-authorization-code:
            authorization-grant-type: authorization_code
            client-id: ${cognito-client-id}
            client-secret: ${cognito-secret}
            provider: cognito
            scope: openid,profile,email,offline_access
com:
  c4-soft:
    springaddons:
      oidc:
        ops:
        - iss: ${cognito-issuer}
          username-claim: username
          authorities:
          - path: cognito:groups
        client:
          csrf: cookie-accessible-from-js
          security-matchers:
          - /**
          permit-all:
          - /login/**
          - /oauth2/**
          - /
          # Auth0 and Cognito do not follow strictly the OpenID RP-Initiated Logout spec and need specific configuration
          oauth2-logout:
            cognito-authorization-code:
              uri: https://spring-addons.auth.us-west-2.amazoncognito.com/logout
              client-id-request-param: client_id
              post-logout-uri-request-param: logout_uri
ch4mp
  • 6,622
  • 6
  • 29
  • 49