Spring Boot Azure Multiple HttpSecurity - java

Is it possible to mix two authentication modes?
Internal user: Azure ad
External user: form authentication
So far I have this:
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration {
#Configuration
#Order(1)
public static class MfaAuthentication extends AadWebSecurityConfigurerAdapter {
private final UserService userService;
#Autowired
public MfaAuthentication(UserService userService) {
this.userService = userService;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http
.antMatcher("/internal/**")
.authorizeHttpRequests()
.anyRequest().authenticated()
.and()
.oauth2Login()
.userInfoEndpoint(userInfoEndpointConfig -> {
userInfoEndpointConfig.oidcUserService(this.oidcUserService());
});
}
private OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
final OidcUserService delegate = new OidcUserService();
return (userRequest) -> {
// Delegate to the default implementation for loading a user
OidcUser oidcUser = delegate.loadUser(userRequest);
OAuth2AccessToken accessToken = userRequest.getAccessToken();
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
// TODO
// 1) Fetch the authority information from the protected resource using accessToken
// 2) Map the authority information to one or more GrantedAuthority's and add it to mappedAuthorities
// 3) Create a copy of oidcUser but use the mappedAuthorities instead
List<String> dummy = userService.fetchUserRoles("dummy");
dummy.forEach(user -> mappedAuthorities.add((GrantedAuthority) () -> user));
oidcUser = new DefaultOidcUser(mappedAuthorities, oidcUser.getIdToken(), oidcUser.getUserInfo());
return oidcUser;
};
}
}
#Configuration
public static class ExternalAuthentication extends WebSecurityConfigurerAdapter {
private final ThdAuthenticationProvider thdAuthenticationProvider;
#Autowired
public ExternalAuthentication(ThdAuthenticationProvider thdAuthenticationProvider) {
this.thdAuthenticationProvider = thdAuthenticationProvider;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.antMatcher("/external/**")
.authorizeRequests()
.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
.anyRequest().fullyAuthenticated()
.and()
.formLogin()
.loginPage("/external/login").permitAll()
.defaultSuccessUrl("/external/index", true)
.failureUrl("/external/denied")
.and()
.logout()
.invalidateHttpSession(true)
.and()
.authenticationProvider(thdAuthenticationProvider);
}
}
}
We have mixed accounts (external users/internal users) so we need to check which kind of account wants to have access in the first place.
My idea is to provide a dedicated login form for internal/external user where the routing is done like /internal/** goes to our Azure login and /external/** goes to a custom authentication provider.
When I travel to http://localhost:8080/internal it gets redirected to http://localhost:8080/oauth2/authorization/azure saying there is no mapping. I want to be redirected to our Azure login.
Is this makeable?
EDIT
application.properties
# Enable related features.
spring.cloud.azure.active-directory.enabled=true
# Specifies your Active Directory ID:
spring.cloud.azure.active-directory.profile.tenant-id=some-id
# Specifies your App Registration's Application ID:
spring.cloud.azure.active-directory.credential.client-id=some-client-id
# Specifies your App Registration's secret key:
spring.cloud.azure.active-directory.credential.client-secret=some-secret
Error Message:
Whitelabel Error Page
This application has no explicit mapping for /error, so you are seeing this as a fallback.
Fri May 06 12:41:41 CEST 2022
There was an unexpected error (type=Not Found, status=404).
EDIT 2
Thanks to the comments i figured out the right configuration - at least for the routing.
I have this configuration at the moment:
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration {
#Configuration
public static class MfaAuthentication extends AadWebSecurityConfigurerAdapter {
private final UserService userService;
#Autowired
public MfaAuthentication(UserService userService) {
this.userService = userService;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http
.authorizeRequests()
.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
.antMatchers("/index").permitAll()
.antMatchers("/public/**").permitAll()
.antMatchers("/internal/**").hasAnyAuthority("Administrator")
.anyRequest()
.authenticated()
.and()
.oauth2Login()
.userInfoEndpoint(userInfoEndpointConfig -> {
userInfoEndpointConfig.oidcUserService(this.oidcUserService());
})
.defaultSuccessUrl("/internal/index", true);
}
private OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
final OidcUserService delegate = new OidcUserService();
return (userRequest) -> {
// Delegate to the default implementation for loading a user
OidcUser oidcUser = delegate.loadUser(userRequest);
OAuth2AccessToken accessToken = userRequest.getAccessToken();
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
// TODO
// 1) Fetch the authority information from the protected resource using accessToken
// 2) Map the authority information to one or more GrantedAuthority's and add it to mappedAuthorities
// 3) Create a copy of oidcUser but use the mappedAuthorities instead
List<String> dummy = userService.fetchUserRoles("dummy");
dummy.forEach(user -> mappedAuthorities.add((GrantedAuthority) () -> user));
oidcUser = new DefaultOidcUser(mappedAuthorities, oidcUser.getIdToken(), oidcUser.getUserInfo());
return oidcUser;
};
}
}
#Configuration
#Order(1)
public static class ExternalAuthentication extends WebSecurityConfigurerAdapter {
private final ThdAuthenticationProvider thdAuthenticationProvider;
#Autowired
public ExternalAuthentication(ThdAuthenticationProvider thdAuthenticationProvider) {
this.thdAuthenticationProvider = thdAuthenticationProvider;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.antMatcher("/external/**")
.authorizeRequests()
.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
.antMatchers("/login").permitAll()
.anyRequest()
.fullyAuthenticated()
.and()
.formLogin()
.loginPage("/external/login").permitAll()
.loginProcessingUrl("/external/login").permitAll()
.defaultSuccessUrl("/external/index", true)
.failureUrl("/external/denied")
.and()
.logout()
.invalidateHttpSession(true)
.and()
.authenticationProvider(thdAuthenticationProvider);
}
}
}
Problem now:
When i travel to /external/index i get redirected to my custom login page. When i want to login (routed via POST to /login) i get redirected to a page where i can choose from oauth2 login which itself is targeted to http://localhost:8080/oauth2/authorization/azure
Here is an excerpt from my (thymeleaf) form:
<form action="#" th:action="#{/login}" method="post" class="form-signin"
accept-charset="utf-8">
</form>
I know that /login is the fixed route for spring security and form based authentication. So is this intended to work with azure in a mixed environment?
Does this setup collide with each other in any way?
Thank you!

Thanks to the inputs from the commentators and some heavy googling i ended up with this working version:
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration {
#Configuration
public static class MfaAuthentication extends AadWebSecurityConfigurerAdapter {
private final UserService userService;
#Autowired
public MfaAuthentication(UserService userService) {
this.userService = userService;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http
.csrf()
.and()
.authorizeRequests(authorize -> authorize.antMatchers("/").permitAll()
.antMatchers("/index").permitAll()
.antMatchers("/public/**").permitAll()
.antMatchers("/internal/**").hasAnyAuthority("Administrator")
.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
.anyRequest().authenticated())
.oauth2Login()
.userInfoEndpoint(userInfoEndpointConfig -> userInfoEndpointConfig.oidcUserService(this.oidcUserService()))
.defaultSuccessUrl("/internal/index", true);
}
private OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
final OidcUserService delegate = new OidcUserService();
return (userRequest) -> {
// Delegate to the default implementation for loading a user
OidcUser oidcUser = delegate.loadUser(userRequest);
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
List<String> dummy = userService.fetchUserRoles("dummy");
dummy.forEach(user -> mappedAuthorities.add((GrantedAuthority) () -> user));
oidcUser = new DefaultOidcUser(mappedAuthorities, oidcUser.getIdToken(), oidcUser.getUserInfo());
return oidcUser;
};
}
}
#Configuration
#Order(1)
public static class ExternalAuthentication extends WebSecurityConfigurerAdapter {
private final ThdAuthenticationProvider thdAuthenticationProvider;
#Autowired
public ExternalAuthentication(ThdAuthenticationProvider thdAuthenticationProvider) {
this.thdAuthenticationProvider = thdAuthenticationProvider;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.antMatcher("/external/**")
.authorizeRequests(authorize -> authorize.antMatchers("/").permitAll()
.antMatchers("/index").permitAll()
.antMatchers("/public/**").permitAll()
.antMatchers("/external/**").hasAnyAuthority("External")
.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
.anyRequest().authenticated())
.formLogin()
.loginPage("/external/login").permitAll()
.loginProcessingUrl("/external/login").permitAll()
.defaultSuccessUrl("/external/index", true)
.failureUrl("/external/denied")
.and()
.logout()
.invalidateHttpSession(true)
.and()
.authenticationProvider(thdAuthenticationProvider);
}
}
}
Here is my custom external login form - at least an excerpt of it:
<form accept-charset="utf-8" action="#" class="form-signin" method="post"
th:action="#{/external/login}">
</form>
All /internal/** routings go to our Azure AD login.
Please note that there is a custom oidc user service to load additional roles for the given user.
All /external/** routings go to our custom AuthenticationProvider
I donĀ“t know if we will implement this in production ready code.
Personally i have a bad feeling about this mix up of various authentication scenarios.
I think it is better to seperate both (when having external/internal user) into individual apps with individual SecurityConfiguration
Any help/comments/tips on mixing external/internal users is very welcome!

Related

How to resolve error spring boot oauth2 google in heroku?

I am using Google OAuth to login into my springboot apps. It works perfectly in my local computer. But it shows this error when I deploy it to heroku.
Method springSecurityFilterChain in org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration required a bean of type 'org.springframework.security.oauth2.client.registration.ClientRegistrationRepository' that could not be found.
Action: Consider defining a bean of type 'org.springframework.security.oauth2.client.registration.ClientRegistrationRepository' in your configuration.
Here is my WebSecurityConfiguration.
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private UserDao userDao;
#Autowired
private PenggunaService penggunaService;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/", "/login", "/oauth/**", "/js/**","/css/**","/image/**").permitAll()
.antMatchers("/admin").hasAnyAuthority("admin")
.anyRequest().authenticated()
.and()
.oauth2Login()
.userInfoEndpoint()
.userAuthoritiesMapper(authoritiesMapper())
.and()
.successHandler(new AuthenticationSuccessHandler() {
#Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication auth) throws IOException, ServletException {
DefaultOidcUser oauthUser = (DefaultOidcUser) auth.getPrincipal();
String email = oauthUser.getAttribute("email");
penggunaService.processOAuthPostLogin(email, auth);
response.sendRedirect("/");
}
})
.and()
.logout().logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
.logoutSuccessUrl("/").permitAll();
}
private GrantedAuthoritiesMapper authoritiesMapper(){
return (authorities) -> {
String emailAttrName = "email";
String email = authorities.stream()
.filter(OAuth2UserAuthority.class::isInstance)
.map(OAuth2UserAuthority.class::cast)
.filter(userAuthority -> userAuthority.getAttributes().containsKey(emailAttrName))
.map(userAuthority -> userAuthority.getAttributes().get(emailAttrName).toString())
.findFirst()
.orElse(null);
if (email == null) {
return authorities; // data email tidak ada di userInfo dari Google
}
Pengguna user = userDao.findByEmailPengguna(email);
if(user == null) {
return authorities; // email user ini belum terdaftar di database
}
List<Permission> userAuthorities = user.getRole().getPermissions();
if (userAuthorities.isEmpty()) {
return authorities; // Return the 'unmapped' authorities
}
return Stream.concat(
authorities.stream(),
userAuthorities.stream()
.map(Permission::getIdPermission)
.map(SimpleGrantedAuthority::new)
).collect(Collectors.toCollection(ArrayList::new));
};
}
#Bean
public SpringSecurityDialect springSecurityDialect() {
return new SpringSecurityDialect();
}
}
Thank you in advance.

Spring Security: Unable to have both authenticated and non authenticated Endpoints

I'm facing what I think is an ordering issue of my extended WebSecurityConfigurationAdapter which I would be very grateful if someone could take a look / offer some advice.
To give some context. I'm working on a API which has a fair amount of endpoints. Up until this point all endpoints where secured behind a JWT / Authentication object like so:
#GetMapping("/me")
public User getLoggedInUsersProfile(#ApiIgnore Authentication authentication,
#RequestParam(value = "profileView", required = false) String profileView) {
logger.info("Request received from User with Id {} to retrieve their profile", authentication.getName());
return userService.getUserProfileFromDB(authentication.getName());
}
However now I need to have a single endpoint which can be accessed by anyone without a JWT.
I tried adding a antMatcher and permit all for said endpoint BUT it giving me a 401. Now I did manage to get it somewhat working however then the endpoint in the code snippet above would throw a 500 if a JWT was omitted from the request (due to it authentication being null). I don't really want to add a null check for each of the endpoints as there are a LOT.
Here is my security config:
#Configuration
#EnableWebSecurity(debug = false)
#EnableGlobalMethodSecurity(
securedEnabled = true,
jsr250Enabled = true,
prePostEnabled = true
)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Value("${auth0.audience}")
private String audience;
#Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}")
private String issuer;
#Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("http://localhost:3010"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST"));
configuration.setAllowCredentials(true);
configuration.addAllowedHeader("Authorization");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.headers().referrerPolicy(ReferrerPolicyHeaderWriter.ReferrerPolicy.SAME_ORIGIN)
.and()
.xssProtection()
.and()
.contentSecurityPolicy("script-src 'self'").and()
.and()
.csrf()
.disable()
.formLogin()
.disable()
.httpBasic()
.disable()
.exceptionHandling()
.authenticationEntryPoint(new RestAuthenticationEntryPoint())
.and()
.anonymous().and()
.authorizeRequests()
.antMatchers("/api/v1/admin/**").permitAll().and()
.authorizeRequests().anyRequest()
.authenticated()
.and()
.oauth2ResourceServer().jwt();
}
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/health", "/health/**");
}
#Bean
JwtDecoder jwtDecoder() {
/*
By default, Spring Security does not validate the "aud" claim of the token, to ensure that this token is
indeed intended for our app. Adding our own validator is easy to do:
*/
NimbusJwtDecoder jwtDecoder = (NimbusJwtDecoder)
JwtDecoders.fromOidcIssuerLocation(issuer);
OAuth2TokenValidator<Jwt> audienceValidator = new AudienceValidator(audience);
OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(issuer);
OAuth2TokenValidator<Jwt> withAudience = new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator);
jwtDecoder.setJwtValidator(withAudience);
return jwtDecoder;
}
JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter converter = new JwtGrantedAuthoritiesConverter();
converter.setAuthoritiesClaimName("permissions");
converter.setAuthorityPrefix("");
JwtAuthenticationConverter jwtConverter = new JwtAuthenticationConverter();
jwtConverter.setJwtGrantedAuthoritiesConverter(converter);
return jwtConverter;
}
}
Can anyone please give me some advice / spot whats wrong.
Many thanks in advance
** EDIT **
I currently handle when a user does not supply a JWT with this class:
#Component
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
#Value("${error.unauthorized}")
private String unauthorizedErrorCode;
#Value("${api.version}")
private String currentApiVersion;
private static final Logger logger = LoggerFactory.getLogger(RestAuthenticationEntryPoint.class);
private final Logger LOGGER = LoggerFactory.getLogger(RestAuthenticationEntryPoint.class);
// This is invoked when user tries to access a secured REST resource without supplying any credentials
#Override
public void commence(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
AuthenticationException e) throws IOException, ServletException {
logger.error("Responding with unauthorized error. Message - {}", e.getMessage());
final AppError error = new AppError(
currentApiVersion,
unauthorizedErrorCode,
"Access Denied",
httpServletRequest.getRequestURI(),
"Invalid or Missing Token",
e.getMessage(),
"https://xxxxxx.ai/sendreport?"
);
httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
httpServletResponse.setContentType("application/json");
MDC.put("api.version", httpServletRequest.getContextPath());
MDC.put("Server.IP", httpServletRequest.getServerName());
MDC.put("API.Controller", httpServletRequest.getServletPath());
MDC.put("Response.code", String.valueOf(httpServletResponse.getStatus()));
MDC.put("Request.Method.Type", httpServletRequest.getMethod());
LOGGER.info("statusCode {}, path: {}, method: {}, query {}, context {}, serverName {}, RequestURI {}, RemoteHost {}, Cookies {}",
httpServletResponse.getStatus(), httpServletRequest.getRequestURI(), httpServletRequest.getMethod(),
httpServletRequest.getQueryString(), httpServletRequest.getContextPath(),
httpServletRequest.getServerName(), httpServletRequest.getRequestURI(), httpServletRequest.getRemoteAddr(),
httpServletRequest.getCookies());
MDC.clear();
ObjectMapper mapper = new ObjectMapper();
mapper.writeValue(httpServletResponse.getOutputStream(), error);
}
}

Adding an API key authentication along with form based login for same endpoints

I have a spring boot application secured using a simple form based login. There is a necessity to add an API key support. I have created datasource to store the key and link it with user account. I am not sure that I am adding the filter part for API key authentication correctly.
#Configuration
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth)
throws Exception {
auth.inMemoryAuthentication().withUser("test")
.password(passwordEncoder.encode("test")).roles("USER","ADMIN");
}
#Override
public void configure(HttpSecurity httpSecurity) throws Exception {
TokenAuthenticationFilter tokenAuthenticationFilter = new TokenAuthenticationFilter(authHeaderName);
tokenAuthenticationFilter.setAuthenticationManager(authentication -> {
String principal = (String) authentication.getPrincipal();
Optional<AuthToken> authTokenOptional = authTokenRepo.findByAuthKey(principal);
if (!authTokenOptional.isPresent())
{
throw new BadCredentialsException("The API key was not found "
+ "or not the expected value.");
}
authentication.setAuthenticated(true);
return authentication;
});
httpSecurity.authorizeRequests()
.antMatchers("/login", "/userlogin/**")
.permitAll()
.antMatchers("/**")
.hasAnyRole("ADMIN", "USER")
.and()
.formLogin()
.successForwardUrl("/userlogin/success")
.failureForwardUrl("/userlogin/failure")
.permitAll()
.and()
.logout()
.logoutSuccessUrl("/")
.invalidateHttpSession(true)
.permitAll()
.and()
.addFilterBefore(tokenAuthenticationFilter, AnonymousAuthenticationFilter.class)
.csrf()
.disable();
}
}
public class TokenAuthenticationFilter extends AbstractPreAuthenticatedProcessingFilter {
private final String authHeaderName;
public TokenAuthenticationFilter(String authHeaderName){
this.authHeaderName = authHeaderName;
}
#Override
protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
return request.getHeader(authHeaderName);
}
#Override
protected Object getPreAuthenticatedCredentials(HttpServletRequest request) {
return "Not available";
}
}
I am not sure if I'm adding the Filter in the correct order. I know I can create MultiHttpSecurityConfig, however unlike in MultiHttpSecurityConfig, would like to use same endpoints. I would like Spring Security to first try the API key, if not present use the form login.

Angular 6 Basic Auth returns 401 from client

So I've looked around for the answer to my problem for quite a while now and tried many suggestions but I can't seem to find an answer.
The problem is, when I use Postman to check if basic auth works I get a 200 code back and it's all good, but as soon as I try to authenticate using my Login Component I get the code 401 back and says "Full authentication is required to access this resource".
I'm fairly new to Angular and completely new to using Basic Auth so I have no idea why does it work with Postman and why doesn't it work from the app.
Any help is appreciated
Below are the relevant codes
log-in.component.ts:
onLogin(form: NgForm) {
/* ... */
let headers = new Headers();
let userCredentials = user.userName + ":" + user.password;
headers.append("Origin", "http://localhost:8080");
headers.append("Authorization", "Basic " + btoa(userCredentials));
return this.http.post('http://localhost:8080/api/users/login', headers).subscribe(
(response) => {
/* ... */
},
(error) => {
console.log(error);
}
);
}
Endpoint on the server side:
#PostMapping(LOG_IN)
public ResponseEntity<User> login() {
return ResponseEntity.ok().build();
}
WebSecurityConfig:
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors()
.and()
.csrf().disable()
.authorizeRequests()
.antMatchers("/h2/**").permitAll()
.anyRequest().authenticated()
.and()
.httpBasic()
.authenticationEntryPoint(getBasicAuthEntryPoint())
.and()
.headers()
.frameOptions().disable()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
#Autowired
protected void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.withUser("admin").password("1234").roles("ADMIN");
}
#Autowired
private UserDetailsService userDetailsService;
#Autowired
protected void configureAuthentication(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
}
#Bean
public CustomBasicAuthenticationEntryPoint getBasicAuthEntryPoint(){
return new CustomBasicAuthenticationEntryPoint();
}
#Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
CustomBasicAuthenticationEntryPoint:
public class CustomBasicAuthenticationEntryPoint extends BasicAuthenticationEntryPoint {
#Override
public void commence(final HttpServletRequest request,
final HttpServletResponse response,
final AuthenticationException authException) throws IOException, ServletException {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.addHeader("WWW-Authenticate", "Basic realm=" + getRealmName() + "");
PrintWriter writer = response.getWriter();
writer.println("HTTP Status 401 : " + authException.getMessage());
}
#Override
public void afterPropertiesSet() throws Exception {
setRealmName("MY REALM");
super.afterPropertiesSet();
}
}
MyUserDetailsService:
#Service
public class MyUserDetailsService implements UserDetailsService {
#Autowired
private UserRepository userRepository;
#Autowired
private AuthenticatedUser authenticatedUser;
#Override
#Transactional(readOnly = true)
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Optional<User> oUser = userRepository.findByUserName(username);
if (!oUser.isPresent()) {
throw new UsernameNotFoundException(username);
}
User user = oUser.get();
authenticatedUser.setUser(user);
Set<GrantedAuthority> grantedAuthorities = new HashSet<>();
grantedAuthorities.add(new SimpleGrantedAuthority(user.getRole().toString()));
return new org.springframework.security.core.userdetails.User(user.getUserName(), user.getPassword(), grantedAuthorities);
}
}
You need to pass the headers as 3rd parameter for the post method. The 2nd one is the body
return this.http.post('http://localhost:8080/api/users/login', {}, {headers}).subscribe(
(response) => {
If you are using angular 6, you should really be using the new HttpClient class, the old Http class being deprecated
This is because the browser send OPTION method to the server before send your request, , try to update your security configuration by allowing OPTION method. like this
protected void configure(HttpSecurity http) throws Exception
{
http
.csrf().disable()
.authorizeRequests()
.antMatchers(HttpMethod.OPTIONS,"/path/to/allow").permitAll()//allow CORS option calls
.antMatchers("/resources/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.httpBasic();
}

Spring security logout not working because of CORS

Technologies: Spring Security, Spring Boot on backend and ReactJs and axios on front-end.
What I want to have: When hitting a logout button on my front-end I want to log out the user. In order to do so I make a call to backend using delete. Then I want my backend to log out.
My issue: When I call Spring Security logout endpoint from my front-end I receive the following message:
Failed to load http://localhost:8080/logout: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:8888' is therefore not allowed access. The response had HTTP status code 403. I do understand why I have this error - backend is on localhost:8080 and front-end on localhost:8888. But what I don't understand is why my configuration doesn't work for logout, while it works perfectly fine for all other situations (e.g. calling spring security login endpoint or some of my custom endpoints).
How I make the call from the front-end
const endpoint = 'logout';
return axios.delete(
'http://localhost:8080/' + `${endpoint}`,
{withCredentials: true}
)
.then(response => {
let resp = {
httpCode: response.status,
data: response.data
};
return {response: resp};
})
.catch(error => {
let err = {
httpStatusCode: error.response.status,
message: `Error calling endpoint ${endpoint}`
};
return {error: err};
});
Enabling CORS from front-end
#Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurerAdapter() {
#Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**").allowedOrigins("http://localhost:8888");
}
};
}
SecurityConfig.java - here you might notice some parts are commented - they are other solutions I tried but they didn't work.
#EnableGlobalMethodSecurity(prePostEnabled = true)
#EnableWebSecurity
#EnableJpaRepositories(basePackageClasses = UsersRepository.class)
#Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private CustomUserDetailsService userDetailsService;
#Autowired
private RESTAuthenticationEntryPoint restAuthenticationEntryPoint;
#Autowired
private RESTAuthenticationSuccessHandler restAuthenticationSuccessHandler;
#Autowired
private RESTAuthenticationFailureHandler restAuthenticationFailureHandler;
#Bean
public CustomLogoutHandler customLogoutHandler(){
return new CustomLogoutHandler();
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(getPasswordEncoder());
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors()
.and()
.csrf().disable()
.authorizeRequests()
.antMatchers("**/anna/**").authenticated()
.anyRequest().permitAll()
.and()
.logout()
.addLogoutHandler(customLogoutHandler())
.logoutRequestMatcher(new AntPathRequestMatcher("/logout"));
http.exceptionHandling().authenticationEntryPoint(restAuthenticationEntryPoint);
http.formLogin().successHandler(restAuthenticationSuccessHandler);
http.formLogin().failureHandler(restAuthenticationFailureHandler);
// http
// .logout()
// .logoutUrl("/logout")
// .addLogoutHandler(new CustomLogoutHandler())
// .invalidateHttpSession(true);
// http
// .cors()
// .and()
// .csrf().disable()
// .logout()
// .logoutRequestMatcher(new AntPathRequestMatcher("/logout"));
}
private PasswordEncoder getPasswordEncoder() {
return new PasswordEncoder() {
#Override
public String encode(CharSequence charSequence) {
return charSequence.toString();
}
#Override
public boolean matches(CharSequence charSequence, String s) {
return true;
}
};
}
}
CustomLogoutHandler.java I read somewhere about the solution with setting the header here. I guess it's bad practice and would prefer not to do it, but basically I'd be happy to see the log working. Currently it's not logging anything so my guess it that it's not called on logout.
//#Slf4j
public class CustomLogoutHandler implements LogoutHandler{
#Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication){
response.setHeader("Access-Control-Allow-Origin", "*");
System.out.println("TodosLogoutHandler logging you out of the back-end app.");
}
}
I checked the source code of CorsRegistration,
public CorsConfiguration applyPermitDefaultValues() {
[...]
if (this.allowedMethods == null) {
this.setAllowedMethods(Arrays.asList(
HttpMethod.GET.name(), HttpMethod.HEAD.name(), HttpMethod.POST.name()));
}
[...]
return this;
}
As you can see, the Delete method is NOT ALLOWED by default, So you need add the delete method.
#Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurerAdapter() {
#Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:8888")
.addAllowedMethod(HttpMethod.DELETE);
}
};
}

Categories

Resources