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);
}
}
Related
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!
My intention is to use JWT in order to generate auth token. Everything works as expected when I am sending email+password as json from postman, but when I am sending from Angular using HttpClient, I get the following exception:
Method threw 'com.fasterxml.jackson.databind.exc.MismatchedInputException' exception. No content to map due to end-of-input
I also tried to replace line
creds = mapper.readValue(req.getInputStream(), UserBE.class);
with:
String c = IOUtils.toString( req.getInputStream());
This way I noticed that result is empty. (PLEASE SEE SCREENSHOTS)
JwtAuthenticationFilter class:
FAILING LINE:
creds = mapper.readValue(req.getInputStream(), UserBE.class);
private AuthenticationManager authenticationManager;
public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
setFilterProcessesUrl("/user/login");
}
#Override
public Authentication attemptAuthentication(HttpServletRequest req,
HttpServletResponse res) throws AuthenticationException {
ObjectMapper mapper = new ObjectMapper();
UserBE creds = null;
try {
creds = mapper.readValue(req.getInputStream(), UserBE.class); **THIS IS THE LINE WHERE CODE FAILS**
} catch (IOException e) {
e.printStackTrace();
}
return authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
creds.getEmail(),
creds.getPassword(),
new ArrayList<>())
);
}
#Override
protected void successfulAuthentication(HttpServletRequest req,
HttpServletResponse res,
FilterChain chain,
Authentication auth) throws IOException {
String token = JWT.create()
.withSubject(((User) auth.getPrincipal()).getUsername())
.withExpiresAt(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
.sign(Algorithm.HMAC512(SECRET.getBytes()));
String body = ((User) auth.getPrincipal()).getUsername() + " " + token;
res.getWriter().write(body);
res.getWriter().flush();
}
}
WebSecurityConfig class:
private final BCryptPasswordEncoder bCryptPasswordEncoder;
private final UserService userService;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable().authorizeRequests()
.antMatchers("/user/create").permitAll()
.antMatchers("/user/confirm/**").permitAll()
.anyRequest().authenticated()
.and()
.addFilter(new JwtAuthenticationFilter(authenticationManager()))
.addFilter(new JwtAuthorizationFilter(authenticationManager()))
// this disables session creation on Spring Security
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
#Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService).passwordEncoder(bCryptPasswordEncoder);
}
#Bean
CorsConfigurationSource corsConfigurationSource() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration().applyPermitDefaultValues();
config.setAllowedOrigins(Collections.singletonList("*"));
config.setAllowCredentials(true);
config.setAllowedOrigins(Collections.singletonList("*"));
config.setAllowedMethods(Collections.singletonList("*"));
config.setAllowedHeaders(Collections.singletonList("*"));
source.registerCorsConfiguration("/**", config);
return source;
}
}
ANGULAR HttpClient REQUEST:
login(email: string, password: string): Observable<any> {
let options = {headers: new HttpHeaders().set('Content-Type', 'application/json')};
console.log({email, password});
return this.http.post<any>(this.baseUrl + "/user/login", {email, password}, options);
}
AGAIN, this is working as expected when sending from POSTMAN, but not from Angular. Any thoughts?
SCREENSHOTS:
Exception MESSAGE + EXCEPTION LINE (BLUE HIGHLIGHTED LINE)
Tried a different way. Just to to convert req.getInputStream to String and see what is receiving from Angular Request. Looks Like result is empty. Please also see next screenshot which is the Request from Postman (WHICH WORKES AS EXPECTED)
Showing and proving that request from POSTMAN has a result
Please note that I made researches and tried a lot of things I've found. Nothing helped.
Found the problem (FINALLY!): I had to add cors on HttpSecurity, and change CORS configuration. So I changed followings:
Added .cors() at the beginning of the expression. See code below:
http.cors().and().csrf().disable().authorizeRequests()
.antMatchers("/user/create").permitAll()
.antMatchers("/user/confirm/**").permitAll()
.anyRequest().authenticated()
.and()
.addFilter(new JwtAuthenticationFilter(authenticationManager()))
.addFilter(new JwtAuthorizationFilter(authenticationManager()))
// this disables session creation on Spring Security
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
Changed corsConfiguration Bean as following:
CorsConfiguration config = new CorsConfiguration().applyPermitDefaultValues();
config.setAllowCredentials(true);
config.setAllowedOriginPatterns(List.of("*"));
source.registerCorsConfiguration("/**", config);
return source;
I have a problem with CORS, but only at some versions of Firefox and Safari: Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at ... (Reason: CORS request did not succeed). At Chrome it's fine for all testing machines. Here's my configuration:
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
#EnableAutoConfiguration
public class ApplicationConfig extends WebSecurityConfigurerAdapter {
private static final RequestMatcher PUBLIC_URLS = new OrRequestMatcher(
new AntPathRequestMatcher("/some_public_urls")
);
private static final RequestMatcher PROTECTED_URLS = new NegatedRequestMatcher(PUBLIC_URLS);
TokenAuthenticationProvider provider;
public ApplicationConfig(final TokenAuthenticationProvider provider) {
super();
this.provider = requireNonNull(provider);
}
#Autowired
private Environment env;
#Override
protected void configure(final AuthenticationManagerBuilder auth) {
auth.authenticationProvider(provider);
}
#Override
public void configure(final WebSecurity web) {
web.ignoring().requestMatchers(PUBLIC_URLS);
}
#Override
protected void configure(final HttpSecurity http) throws Exception {
http
.headers()
.and()
.sessionManagement()
.sessionCreationPolicy(STATELESS)
.and()
.exceptionHandling()
.defaultAuthenticationEntryPointFor(forbiddenEntryPoint(), PROTECTED_URLS)
.and()
.authenticationProvider(provider)
.addFilterBefore(restAuthenticationFilter(), AnonymousAuthenticationFilter.class)
.authorizeRequests()
.requestMatchers(PROTECTED_URLS)
.authenticated()
.and()
.cors()
.and()
.csrf().disable()
.formLogin().disable()
.httpBasic().disable()
.logout().disable();
}
#Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("*"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(Arrays.asList("*"));
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
#Bean
TokenAuthenticationFilter restAuthenticationFilter() throws Exception {
final TokenAuthenticationFilter filter = new TokenAuthenticationFilter(PROTECTED_URLS);
filter.setAuthenticationManager(authenticationManager());
filter.setAuthenticationSuccessHandler(successHandler());
return filter;
}
#Bean
SimpleUrlAuthenticationSuccessHandler successHandler() {
final SimpleUrlAuthenticationSuccessHandler successHandler = new SimpleUrlAuthenticationSuccessHandler();
successHandler.setRedirectStrategy((httpServletRequest, httpServletResponse, s) -> {
// No redirect is required
});
return successHandler;
}
/**
* Disable Spring boot automatic filter registration.
*/
#Bean
FilterRegistrationBean disableAutoRegistration(final TokenAuthenticationFilter filter) {
final FilterRegistrationBean registration = new FilterRegistrationBean(filter);
registration.setEnabled(false);
return registration;
}
#Bean
AuthenticationEntryPoint forbiddenEntryPoint() {
return new HttpStatusEntryPoint(FORBIDDEN);
}
}
Each RestController is annotated with:
#RestController
#CrossOrigin
I could guess that you are performing testing on some old browsers and it doesn't work.
Here is the landscape of CORS support in browsers. Please check it out.
As of mid-2014, approximately 83% of the browsers out there have full
support for CORS, and another 6% have partial support.
If it's the case, you could try some other techniques like
JSON-P or using Proxy Server to make cross-origin requests in older browser.
Ok,
It turned out, that it was certificate issue. We have a certificate bundle (wildcard certificate) and it was placed in wrong order. Some browsers could handle this, some versions of Firefox were blocking it.
I'm trying to configure spring security and controller CORS requests, but the preflight request is not working. I have a controller, test, and spring security config which are related to the problem. What I'm doing wrong?)
Java 8, Spring Boot 2.1.4
Controller
#RestController
#RequestMapping("/login")
#CrossOrigin
public class LoginController {
#RequestMapping(path = "/admin", consumes = "application/json", produces = "application/json")
public ResponseEntity<Admin> loginAdmin(#RequestBody Admin admin) {
String username = admin.getUsername();
String password = admin.getPassword();
authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
ResponseEntity<Admin> responseEntity;
Admin foundAdmin = adminRepository.findByUsername(username);
if (foundAdmin != null) {
String token = jwtTokenProvider.createToken(username, adminRepository.findByUsername(username).getRoles());
AdminAuthenticatedResponse contractorAuthenticatedResponse = new AdminAuthenticatedResponse(foundAdmin, token);
responseEntity = new ResponseEntity<>(contractorAuthenticatedResponse, HttpStatus.ACCEPTED);
} else {
responseEntity = new ResponseEntity<>(admin, HttpStatus.NOT_FOUND);
}
return responseEntity;
}
}
Security config
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.httpBasic().disable()
.csrf().disable()
.cors().and().formLogin().and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/login/**").permitAll()
.antMatchers("/register/**").permitAll()
.antMatchers("/h2-console/**").permitAll()
.antMatchers(HttpMethod.GET, "/restapi/customers/**").permitAll()
.antMatchers(HttpMethod.DELETE, "/restapi/customers/**").hasRole("CUSTOMER")
.antMatchers(HttpMethod.PUT, "/restapi/customers/**").hasRole("CUSTOMER")
.antMatchers(HttpMethod.PATCH, "/restapi/customers/**").hasRole("CUSTOMER")
.antMatchers(HttpMethod.GET, "/restapi/contractors/**").permitAll()
.antMatchers(HttpMethod.DELETE, "/restapi/contractors/**").hasRole("CONTRACTOR")
.antMatchers(HttpMethod.PUT, "/restapi/contractors/**").hasRole("CONTRACTOR")
.antMatchers(HttpMethod.PATCH, "/restapi/contractors/**").hasRole("CONTRACTOR")
.antMatchers(HttpMethod.PATCH, "/restapi/workRequests/**").hasAnyRole("CONTRACTOR", "CONTRACTOR", "ADMIN")
.antMatchers(HttpMethod.PUT, "/restapi/workRequests/**").hasAnyRole("CONTRACTOR", "CONTRACTOR", "ADMIN")
.antMatchers(HttpMethod.POST, "/restapi/workRequests/**").hasAnyRole("CONTRACTOR", "CONTRACTOR", "ADMIN")
.antMatchers(HttpMethod.DELETE, "/restapi/workRequests/**").hasAnyRole("CONTRACTOR", "CONTRACTOR", "ADMIN")
.antMatchers(HttpMethod.GET, "/restapi/workRequests/**").hasAnyRole("CONTRACTOR", "CONTRACTOR", "ADMIN")
.anyRequest().authenticated()
.and()
.apply(new JwtConfigurer(jwtTokenProvider))
// Allow pages to be loaded in frames from the same origin; needed for H2-Console
.and()
.headers()
.frameOptions()
.sameOrigin();
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(userRepositoryUserDetailsService)
.passwordEncoder(encoder());
}
#Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList(ALL));
configuration.setAllowedMethods(Arrays.asList(ALL));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring()
.antMatchers("/**", "/resources/**", "/index.html", "/login/admin", "/template/**", "/",
"/error/**", "/h2-console", "*/h2-console/*");
}
Test
#Test
public void testLogin() {
Admin admin = new Admin("network", "network");
// adminRepository.save(admin);
String s = adminRepository.findByUsername("network").getUsername();
RequestEntity<Admin> requestEntity =
RequestEntity
.post(uri("/login/admin"))
.contentType(MediaType.APPLICATION_JSON)
.body(admin);
ResponseEntity<Admin> responseEntity = this.restTemplate.exchange(requestEntity, Admin.class);
assertNotNull(responseEntity);
System.out.println(responseEntity.getStatusCode());
}
I'm expecting successful request, but I'm getting INTERNAL SERVER ERROR.
403 Forbidden or No Permission to Access.
A 403 Forbidden error means that you do not have permission to view the requested file or resource
Please attempt the test runs with #WithMockUser
#Test
#WithMockUser
public void corsWithAnnotation() throws Exception {
ResponseEntity<Admin> entity = this.restTemplate.exchange(
...
}
Reference : Preview Spring Security Test: Web Security
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();
}