I lost my desire for living trying to deal with problems like these when playing basic auth with Spring and Axios.
After trying almost everything, even configuring like yours, I found a solution for my use case that can be yours too.
Axios request
I use Axios like this to make requests, notice that there is an auth param:
async removeAnexo(uuidAnexo, successCallback, errorCallback) {
const credentials = JSON.parse(Cookies.get("credentials"));
const axiosRequest = axios.create({
baseURL: serverUrl,
auth: {
username: credentials.username,
password: credentials.password
},
headers: {
"Content-Type": "application/json"
}
});
await axiosRequest
.post("processos/anexo/" + uuidAnexo + "/remove")
.then(res => {
if (res.status === 200) {
successCallback();
return res;
}
})
.catch(error => {
errorCallback(error);
});
}
Spring Configuration
- You need to have an Authentication Entry Point to expose a few necessary headers when preflight fires an OPTIONS request to the endpoint before making the actual request you asked for.
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint;
import org.springframework.stereotype.Component;
@Component
public class AuthEntryPoint extends BasicAuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
org.springframework.security.core.AuthenticationException authException)
throws IOException, ServletException {
if (HttpMethod.OPTIONS.matches( request.getMethod() )) {
response.setStatus( HttpServletResponse.SC_OK );
response.setHeader( HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, request.getHeader( HttpHeaders.ORIGIN ) );
response.setHeader( HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, request.getHeader( HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS ) );
response.setHeader( HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, request.getHeader( HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD ) );
response.setHeader( HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, "*" );
response.setHeader( HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true" );
} else {
response.sendError( HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage() );
}
}
@Override
public void afterPropertiesSet() throws Exception {
this.setRealmName( "simpleRealm" );
super.afterPropertiesSet();
}
}
- Then you need to provide a custom filter, a cors one, also exposing
a few headers, necessary for the SessionManagementFilter.
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
@Component
public class CorsFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse) servletResponse;
HttpServletRequest request = (HttpServletRequest) servletRequest;
response.setHeader( "Access-Control-Allow-Origin", request.getHeader( "Origin" ) );
response.setHeader( "Access-Control-Allow-Methods", "GET,POST,DELETE,PUT,OPTIONS" );
response.setHeader( "Access-Control-Allow-Headers", "*" );
response.setHeader( "Access-Control-Allow-Credentials", "true" );
response.setHeader( "Access-Control-Expose-Headers", "Content-Disposition,X-Suggested-Filename,X-Auth-Token,Authorization" );
response.setHeader( "Access-Control-Max-Age", "180" );
filterChain.doFilter( servletRequest, servletResponse );
}
@Override
public void destroy() {
}
}
- Now, you have to provide an AuthenticationProvider, in my case I
configured a CustomAuthenticationProvider based in BasicAuth.
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.core.env.Profiles;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import com.pontosistemas.clif.entities.system.Login;
import com.pontosistemas.clif.repositories.system.LoginRepository;
@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
@Autowired
private LoginRepository loginRepository;
@Autowired
private Environment env;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public Authentication authenticate(Authentication auth) throws AuthenticationException {
boolean notProductionProfile = env.acceptsProfiles( Profiles.of( "default", "homolog" ) );
String username = auth.getName();
String password = auth.getCredentials().toString();
if (notProductionProfile && "admin".equals( username ) && "admin".equals( password )) {
return new UsernamePasswordAuthenticationToken( username, password, Arrays.asList( new SimpleGrantedAuthority( "ADMIN" ), new SimpleGrantedAuthority( "protected" ) ) );
} else {
boolean usernameActiveExists = loginRepository.existsLoginByUsernameAndActiveIsTrue( username );
if (usernameActiveExists) {
Login login = loginRepository.findByUsernameAndActiveIsTrue( username );
String hashPassword = login.getPassword();
boolean matches = passwordEncoder.matches( password, hashPassword );
if (matches)
return new UsernamePasswordAuthenticationToken( username, hashPassword, Arrays.asList( new SimpleGrantedAuthority( login.getRole().toString() ), new SimpleGrantedAuthority( "protected" ) ) );
else
throw new BadCredentialsException( "Senha inválida!" );
}
}
throw new UsernameNotFoundException( "Nome de usuário não encontrado" );
}
@Override
public boolean supports(Class<?> auth) {
return auth.equals( UsernamePasswordAuthenticationToken.class );
}
}
- Finally, you gonna need a glue to put all these parts together, this
glue is a WebSecurityConfigurerAdapter, you can look carefully for more details, try to comment each configuration line to see what happens. In my case I configured what happens when login with success, or logout, or login failure. For each case there is a handler to implement.
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.session.SessionManagementFilter;
import com.fasterxml.jackson.databind.ObjectMapper;
@Configuration
@EnableWebSecurity
public class BasicSecurityAdapter extends WebSecurityConfigurerAdapter {
@Autowired
private AuthEntryPoint authEntryPoint;
@Autowired
private CustomAuthenticationProvider customAuthProvider;
@Autowired
private CorsFilter corsFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.addFilterBefore( corsFilter, SessionManagementFilter.class );
http.headers().frameOptions().disable();
http.httpBasic().authenticationEntryPoint( authEntryPoint );
http.authenticationProvider( customAuthProvider );
http
.authorizeRequests()
.antMatchers( "/public/**" ).permitAll()
.antMatchers( HttpMethod.OPTIONS, "/**" ).permitAll()
.antMatchers( "/**" ).hasAuthority( "protected" )
.anyRequest().authenticated()
.and()
.sessionManagement()
.sessionCreationPolicy( SessionCreationPolicy.STATELESS );
http.formLogin().loginProcessingUrl( "/login" );
http.formLogin().successHandler( new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
response.addHeader( "Access-Control-Expose-Headers", "role" );
response.setHeader( "Access-Control-Allow-Origin", request.getHeader( "Origin" ) );
response.setHeader( "Access-Control-Allow-Methods", "GET,POST,DELETE,PUT,OPTIONS" );
response.setHeader( "Access-Control-Allow-Headers", "*" );
response.setHeader( "Access-Control-Allow-Credentials", "true" );
response.setHeader( "Access-Control-Expose-Headers", "Content-Disposition,X-Suggested-Filename,X-Auth-Token,Authorization" );
response.setHeader( "Access-Control-Max-Age", "180" );
String role = authentication.getAuthorities().stream().findFirst().get().toString();
response.addHeader( "role", role );
ObjectMapper objectMapper = new ObjectMapper();
Map<String, Object> data = new HashMap<String, Object>();
data.put( "role", role );
response.getWriter().print( objectMapper.writeValueAsString( data ) );
}
} );
http.formLogin().failureHandler( new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
response.addHeader( "Access-Control-Expose-Headers", "role" );
response.setHeader( "Access-Control-Allow-Origin", request.getHeader( "Origin" ) );
response.setHeader( "Access-Control-Allow-Methods", "GET,POST,DELETE,PUT,OPTIONS" );
response.setHeader( "Access-Control-Allow-Headers", "*" );
response.setHeader( "Content-type", "application/json; charset=UTF-8" );
response.setCharacterEncoding( "UTF-8" );
response.setStatus( HttpServletResponse.SC_UNAUTHORIZED );
ObjectMapper objectMapper = new ObjectMapper();
Map<String, String> data = new HashMap<String, String>();
data.put( "message", exception.getMessage() );
response.getWriter().print( objectMapper.writeValueAsString( data ) );
}
} );
http.logout().logoutSuccessHandler( (request, response, authentication) -> {
response.setHeader( "Access-Control-Allow-Origin", request.getHeader( "Origin" ) );
response.setHeader( "Access-Control-Allow-Methods", "GET,POST,DELETE,PUT,OPTIONS" );
response.setHeader( "Access-Control-Allow-Headers", "*" );
response.setStatus( HttpServletResponse.SC_OK );
} );
}
}
My use case
In my scenario I store the user login and password on a cookie when user log in and I carry those credentials when calling the API.
I believe that you can change from basic auth to another option trying to change the CustomAuthenticationProvider.
Maybe a little late for you, but I hope this helps somebody.