0

I'm developing a web app where user can authenticate with OAuth2. In the web app there are a list of endpoints that are accessible only by sending requests (GET-POST-PUT-DELETE) including a valid token. Now I want to implement my web app by allowing users to authenticate with username and password. There is a form where user can Signup to the web app (they need to fill the form with the required data). So, once an user clicked on Signup, a POST request is sent to the URL localhost:8080/api/v1/accounts/register and their data are stored into the db. My problem is that the POST request works only if a valid token is included into the request. If I don't specify a valid token, I get an "error 401 unauthorized". If I send the request by including the token, I got 201. What I tried to do was to make the endpoint (/api/v1/accounts/register) accessible to everyone. But I still get the error 401.

Those are the classes involved into the authorization:

SecurityConfig.java:

package it.app.demoaimeetingroombe.configuration;

import it.app.demoaimeetingroombe.csrf.CsrfTokenCustom;
import it.app.demoaimeetingroombe.filter.AccountCheckFilterChain;
import it.app.demoaimeetingroombe.repository.AccountRepository;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.oauth2.server.resource.web.authentication.BearerTokenAuthenticationFilter;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.csrf.CsrfTokenRepository;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.web.cors.CorsConfiguration;

import java.util.Collections;
import java.util.List;

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http, AccountRepository accountRepository) throws Exception {
        List<AntPathRequestMatcher> excludeFromCheck = List.of(
                new AntPathRequestMatcher("/api/v1/accounts/authenticate"),
                new AntPathRequestMatcher("/api/v1/rooms/*/bookings/daily/*"),
                new AntPathRequestMatcher("/swagger-ui/**"),
                new AntPathRequestMatcher("/swagger-ui.html"),
                new AntPathRequestMatcher("/api/v1/accounts/register"),
                new AntPathRequestMatcher("/v3/api-docs/**"),
                new AntPathRequestMatcher("/api/v1/webauthn/**")
        );

        return http
                .csrf().csrfTokenRepository(customCsrfTokenRepository())
                .ignoringRequestMatchers(new AntPathRequestMatcher("/api/v1/webauthn/**"))
                .and()
                .authorizeHttpRequests(requests -> requests
                        .requestMatchers(
                                new AntPathRequestMatcher("/api/v1/rooms/*/bookings/daily/*"),
                                new AntPathRequestMatcher("/swagger-ui/**"),
                                new AntPathRequestMatcher("/swagger-ui.html"),
                                new AntPathRequestMatcher("/api/v1/accounts/register"),
                                new AntPathRequestMatcher("/v3/api-docs/**"),
                                new AntPathRequestMatcher("/api/v1/webauthn/**")).permitAll()
                        .anyRequest().authenticated()
                )
                .addFilterAfter(new AccountCheckFilterChain(accountRepository, excludeFromCheck), BearerTokenAuthenticationFilter.class)
                .oauth2ResourceServer()
                .jwt()
                .and()
                .and().cors().configurationSource(httpServletRequest -> {
                    CorsConfiguration corsConfiguration = new CorsConfiguration();
                    corsConfiguration.setAllowedOriginPatterns(Collections.singletonList("http://localhost:3000"));
                    corsConfiguration.setAllowedMethods(Collections.singletonList("*"));
                    corsConfiguration.setAllowCredentials(true);
                    corsConfiguration.setAllowedHeaders(Collections.singletonList("*"));
                    corsConfiguration.setExposedHeaders(Collections.singletonList("Authorization"));
                    corsConfiguration.setMaxAge(3600L);
                    return corsConfiguration;
                })
                .and().build();
    }

    private CsrfTokenRepository customCsrfTokenRepository() {
        return new CsrfTokenCustom();
    }
}

AccountCheckFilterChain.java:

package it.app.demoaimeetingroombe.filter;


import it.app.demoaimeetingroombe.repository.AccountRepository;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import java.io.IOException;
import java.util.List;

public class AccountCheckFilterChain implements Filter {
    private final AccountRepository accountRepository;
    private final List<AntPathRequestMatcher> excludeFromCheck;

    public AccountCheckFilterChain(AccountRepository accountRepository, List<AntPathRequestMatcher> excludeFromCheck) {
        this.accountRepository = accountRepository;
        this.excludeFromCheck = excludeFromCheck;
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
            throws IOException, ServletException {
        HttpServletRequest httpRequest = ((HttpServletRequest) servletRequest);
        HttpServletResponse httpResponse = ((HttpServletResponse) servletResponse);
        boolean shouldExclude = excludeFromCheck.stream().anyMatch(exclude -> exclude.matches(httpRequest));
        if (!shouldExclude) {
            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
            if (authentication == null) {
                setUnauthorized(httpResponse);
                return;
            }
            String accountId = authentication.getName();
            if (StringUtils.isBlank(accountId) || !accountRepository.existsByIdAndIsActiveTrue(accountId)) {
                setUnauthorized(httpResponse);
                return;
            }
        }
        filterChain.doFilter(servletRequest, servletResponse);
    }

    private static void setUnauthorized(HttpServletResponse httpResponse) {
        httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        httpResponse.setContentType("application/json");
        httpResponse.setContentLength(0);
    }
}

CsrfTokenCustom.java:

package it.app.demoaimeetingroombe.csrf;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.security.web.csrf.CsrfTokenRepository;
import org.springframework.security.web.csrf.DefaultCsrfToken;

public class CsrfTokenCustom implements CsrfTokenRepository {
    private final CookieCsrfTokenRepository delegate = CookieCsrfTokenRepository.withHttpOnlyFalse();

    @Override
    public CsrfToken generateToken(HttpServletRequest request) {
        return delegate.generateToken(request);
    }

    @Override
    public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) {
        delegate.saveToken(token, request, response);
    }

    @Override
    public CsrfToken loadToken(HttpServletRequest request) {
        CsrfToken token = delegate.loadToken(request);
        if (isWebAuthnEndpoint(request)) {
            String newToken = "ALLOW_ALL";
            return new DefaultCsrfToken(token.getHeaderName(), token.getParameterName(), newToken);
        }
        return token;
    }

    private boolean isWebAuthnEndpoint(HttpServletRequest request) {
        String requestURI = request.getRequestURI();
        return requestURI.startsWith("/api/v1/webauthn/");
    }
}
Count
  • 9
  • 2

1 Answers1

1

You have used Spring Security to manage authentication and permission in your web project based on the code you gave. You need help because you want to make the /api/v1/accounts/register endpoint publicly available, but it requires a valid token to access.

You must alter your SecurityConfig class to make the /api/v1/accounts/register endpoint available without authentication. This endpoint is now included in the excludeFromCheck list, which implies that authentication checks should not be performed on it. It is still viewed as an authenticated request though.

To resolve this, try out updating the SecurityConfig class in this way,

//... 
public SecurityFilterChain securityFilterChain(HttpSecurity http, AccountRepository accountRepository) throws Exception {
    List<AntPathRequestMatcher> excludeFromCheck = List.of(
        //...

        new AntPathRequestMatcher("/api/v1/accounts/register")
    );

    return http
        //...
        .authorizeHttpRequests(requests -> requests
            .requestMatchers(excludeFromCheck.toArray(new AntPathRequestMatcher[0])).permitAll()
            .anyRequest().authenticated()
        )
        //...
} 
  • Thanks for the answer. I modified the code as you suggested, I sent the POST request without include the token and I still get the error 401. – Count Jul 11 '23 at 07:53