How to handle and custom error 401 from Spring boot ldap? - java

I'm working in an application that uses Spring secutiry LDAP to authenticate its users, the authentication is working fine, my problem is that if someone try to log in with unauthorized credentials, the application sends immediatly a error 401, and I don't want that, I want to custom some friendly message to show this user, but even in debug I can't find where the application executes de authentication, it even seems that my backend isn't called, how can I custom this exception ?
Here's my configuration on my Security configuration class:
#Configuration
#Order(2147483640)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
http.httpBasic().and()
.cors().and()
.csrf().disable()
.authorizeRequests()
.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
.antMatchers(HttpMethod.GET, "/**").permitAll()
.antMatchers(HttpMethod.POST, "/**").permitAll()
.antMatchers(HttpMethod.PUT, "/**").permitAll()
.antMatchers(HttpMethod.DELETE, "/**").permitAll()
.antMatchers("/**").permitAll();
}
#Configuration
protected static class AuthenticationConfiguration extends GlobalAuthenticationConfigurerAdapter {
#Autowired
private UsrPessoaService usrPessoaService;
#Override
public void init(AuthenticationManagerBuilder auth) throws Exception {
auth.ldapAuthentication().userDetailsContextMapper(usrPessoaService)
.userDnPatterns("secret")
.groupSearchBase("secret")
.contextSource()
.root("secret")
.url("secret").port(000);
System.out.println(auth.toString());
}
}
}
Thanks in advance for any help !

Have you considered a WhiteLabel Page?
For example:
https://www.baeldung.com/spring-boot-custom-error-page
#RequestMapping("/error")
public String handleError(HttpServletRequest request) {
Object status = request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
if (status != null) {
Integer statusCode = Integer.valueOf(status.toString());
if(statusCode == HttpStatus.NOT_FOUND.value()) {
return "error-404";
}
else if(statusCode == HttpStatus.INTERNAL_SERVER_ERROR.value()) {
return "error-500";
}
}
return "error";
}
And an example "error.html" template:
<!DOCTYPE html>
<html>
<body>
<h1>Page not found</h1>
<h2>Sorry, we couldn't find the page you were looking for</h2>
Go Home
</body>
</html>
ADDENDUM:
Since the OP has an Angular front end, these links might also be applicable:
Tutorial: Spring Security and Angular.
In this tutorial we show some nice features of Spring Security, Spring
Boot and Angular working together to provide a pleasant and secure
user experience.
It should be accessible to beginners with Spring and Angular, but
there also is plenty of detail that will be of use to experts in
either.
See also:
https://github.com/spring-projects/spring-security/blob/master/web/src/main/java/org/springframework/security/web/AuthenticationEntryPoint.java

Related

How to fix overriding of default AccessDeniedHandler (AccessDeniedHandlerImpl) in Spring Security (Java) config?

I have a small web application based on Spring MVC and Spring Security. I have difficulties setting my own AccessDeniedHandler that should redirect unauthorized users to my custom error page.
I use http.exceptionHandling().accessDeniedHandler(accessDeniedHandler) in my config class that extends WebSecurityConfigurerAdapter. The default AccessDeniedHandler keeps being invoked despite the setting (I debugged ExceptionTranslationFilter). As a result the container-defined error page is displayed instead of my custom one.
Do you have an idea what I am missing here? What could be the issue? Thank you kindly for your help.
An excerpt from my WebSecurityConfigurerAdapter super class:
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/static/**", "/login/*", "/login").permitAll()
.antMatchers("/site/admin*").hasRole("ADMIN")
.anyRequest().authenticated()
.and().formLogin()
.loginPage("/login")
.usernameParameter("user-name")
.passwordParameter("password")
.defaultSuccessUrl("/site/welcome", true)
.loginProcessingUrl("/process-login")
.failureUrl("/login?login_error=1")
.and().logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/login")
.and().sessionManagement()
.invalidSessionUrl("/login")
.and().csrf()
.and().exceptionHandling().accessDeniedHandler(accessDeniedHandler);
}
My custom AccessDeniedHandler implementation:
#Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
private static Logger LOG = Logger.getLogger(CustomAccessDeniedHandler.class);
#Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException, ServletException {
final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null) {
LOG.warn(String.format("User [%s] attempted to access the protected URL [%s]!", authentication.getName(), request.getRequestURI()));
}
response.sendRedirect(request.getContextPath() + "/site/403");
}
}
I forgot to assign the autowired constructor parameter to a field! I am sorry for posting such a trivial problem here, but after I spent half a day looking for a solution, I was blind and I missed it...
public SpringSecurityConfiguration(
AccessDeniedHandler accessDeniedHandler, ...) {
this.accessDeniedHandler = accessDeniedHandler; // This line was missing.
...
}

Postman 403 Forbidden message

I made some api with REST Spring. GET request works fine in Postman but when I try to do POST request I receive this error :
{
"timestamp": "2018-09-25T06:39:27.226+0000",
"status": 403,
"error": "Forbidden",
"message": "Forbidden",
"path": "/cidashboard/projects"
}
This is my controller :
#RestController
#RequestMapping(ProjectController.PROJECT_URL)
public class ProjectController {
public static final String PROJECT_URL = "/cidashboard/projects";
private final ProjectService projectService;
public ProjectController(ProjectService projectService) {
this.projectService = projectService;
}
#GetMapping
List<Project> getAllProjects(){
return projectService.findAllProjects();
}
#GetMapping("/{id}")
Project getProjectById(#PathVariable int id) {
return projectService.findProjectById(id);
}
#PostMapping
void addProject(#RequestBody Project newProject) {
projectService.saveProject(newProject);
}
}
Security configuration
initial I wanted to work with ldap, but in my application properties i left only the conection at database....................................................................................................................................................
#EnableGlobalMethodSecurity
#Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/css/**").permitAll();
// .anyRequest().fullyAuthenticated();
// .and()
// .formLogin().loginPage("/login").permitAll()
// .failureUrl("/login-error");
}
#Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.ldapAuthentication()
.userDnPatterns("uid={0},ou=people")
.groupSearchBase("ou=groups")
.contextSource(contextSource())
.passwordCompare()
//.passwordEncoder(new LdapShaPasswordEncoder())
.passwordAttribute("userPassword");
}
#Override
public void configure(WebSecurity web) throws Exception {
web
.ignoring()
.antMatchers("/resources/static/**"); // #3
}
#Bean
public DefaultSpringSecurityContextSource contextSource() {
return new DefaultSpringSecurityContextSource(Arrays.asList("ldap://localhost:8389/"), "dc=springframework,dc=org");
}
}
Enable spring security with #EnableWebSecurity usage.By default enables csrf support, you have to disable it to prevent Forbidden errors.
#Override
protected void configure(HttpSecurity http) throws Exception {
http //other configure params.
.csrf().disable();
}
PS: 415 unsupported type --> add to your mapping like this annotation for which type of data is sending from Postman.
#PostMapping(consumes = "application/json")
void addProject(#RequestBody Project newProject) {
projectService.saveProject(newProject);
}
In case you want to solve this issue without compromising security, you can send the xsrf-token with your request in postman.
Create a new environment in Postman (e.g. "local").
Create a new variable in this environment (e.g. "xsrf-token")
Go back to your request and make sure the right environment is selected on the top right corner ("local" in this case)
In your POST request, add a header with key "X-XSRF-TOKEN" and value "{{csrf-token}}"
In the "tests" tab, add following code:
var xsrfCookie = pm.cookies.get('XSRF-TOKEN')
pm.environment.set("xsrf-token", xsrfCookie)
The first time you make this request, you will still get a 403, but you'll also receive a cookie with the xsrf-token. The script will copy this token in the environment variable and the next requests you'll make use the appropriate token.
Check the "User-Agent" included in Headers section, If not add the "User-Agent" field
I I was also getting the same error. I found the solution using a different application, not postman {Insomnia REST Client}.
When I went back to postman after wondering, I realized that it is related to permissions in spring security. So after setting the permissions it will work.

LDAP authentication in spring boot app

I know almost nothing about LDAP and even less about spring security but I am trying to configure a spring boot app to authenticate against an ldap instance and am stuck.
I was given the ldap server name at adldap.company.com and base dn of dc=ad,dc=company,dc=com
I have some python code that does a simple bind and works.
LDAP_USERNAME = 'username#ad.company.com'
LDAP_PASSWORD = 'password'
base_dn = 'dc=ad,dc=company,dc=com' # not used for bind I guess, only search
try:
ldap_client = ldap.initialize('ldap://adldap.company.com')
ldap_client.set_option(ldap.OPT_REFERRALS,0)
ldap_client.simple_bind_s(LDAP_USERNAME, LDAP_PASSWORD)
except ldap.INVALID_CREDENTIALS as e:
ldap_client.unbind()
return 'Wrong username and password: %s' % e
except ldap.SERVER_DOWN:
return 'AD server not available'
If I run this code, it seems to successfully bind as "username#ad.company.com" with password "password".
I also have a WebSecurityConfig class that I think should be handling auth:
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.antMatcher("/secure")
.authorizeRequests()
.anyRequest().fullyAuthenticated()
.and()
.httpBasic();
}
#Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.ldapAuthentication()
.userDnPatterns("uid={0}")
.contextSource()
.url("ldap://adldap.company.com");
//.url("ldap://adldap.company.com/dc=ad,dc=company,dc=com");
}
}
When I go to /secure in the app, I get a basic auth pop up but then anything I try entering gets me a 401 Unauthorized. I have tried "username#ad.company.com", without the domain, putting that stuff in the userDnPatterns like {0}#adldap.company.com and a bunch of other things. I have tried using different URLs with the base dn in it or not. Nothing seems to work. What am I missing?
Also, is this the right way to auth users? I've read about both bind authentication and something about binding and searching but the server doesn't allow anonyous binds so I guess I would need some kind of "app user" that could bind and do the searches, right? Is that "better"?
Active Directory has its own non-standard syntax for user authentication, different from the usual LDAP DN binding.
Spring Security provides a specialized AuthenticationProvider for Active Directory.
Try this :
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.antMatcher("/secure")
.authorizeRequests()
.anyRequest().fullyAuthenticated()
.and()
.httpBasic();
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(activeDirectoryLdapAuthenticationProvider());
}
#Bean
public AuthenticationManager authenticationManager() {
return new ProviderManager(Arrays.asList(activeDirectoryLdapAuthenticationProvider()));
}
#Bean
public AuthenticationProvider activeDirectoryLdapAuthenticationProvider() {
ActiveDirectoryLdapAuthenticationProvider provider = new ActiveDirectoryLdapAuthenticationProvider("adldap.company.com", "ldap://adldap.company.com");
provider.setConvertSubErrorCodesToExceptions(true);
provider.setUseAuthenticationRequestCredentials(true);
return provider;
}
}
Long story short, the problem is that Microsoft Active Directory LDAP is not "Vanilla" LDAP and thus you need to connect to it differently.
The working solution is here: https://medium.com/#dmarko484/spring-boot-active-directory-authentication-5ea04969f220

Why do I get a 404 after I login to springs default login page?

I am getting a 404 after I loggin in a very simple Spring Boot Application. It happen's since I added the password encoder stuff into my configureAuth method. Can someone help me?
Here ist my security configuration code:
#Configuration
#EnableGlobalAuthentication
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
private DataSource dataSource;
#Autowired
public void configureAuth(final AuthenticationManagerBuilder auth) throws Exception {
auth.jdbcAuthentication().passwordEncoder(passwordEncoder()).dataSource(dataSource).withDefaultSchema()
.withUser("admin").password(passwordEncoder().encode("admin123")).roles("USER", "ADMIN");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated().and().formLogin().and().httpBasic().and().csrf().disable()
.headers().frameOptions().disable();
}
#Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
There is no exception or other error. A simple whitelabel error page with 404 is showing up.
EDIT: The login form is coming up, but I think there is something wrong with the authentication.
Thank you,
Christian
You have to configure requests to the login form I believe. Reference.
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login");
}
From what it looks like, it's important to specify the .loginPage. I'm using the following config for my project.
http.
.authorizeRequests().antMathcers("/login-page", "/login", "/successful-login", "/error-login").anonymous().and()
.formLogin()
.loginPage("/login-page")
.defaultSuccessUrl("/successful-login")
.loginProcessingUrl("/login")
.failureUrl("/error-login")
.permitAll()
The .loginProcessingUrl is I believe the URL to handle the login POST request.
I'm also using the #EnableWebSecurity annotation on my SecurityConfig class,
My case...
It worked properly
antMatchers("/admin/**")
It was failed after I changed like this
antMatchers("/admin/xxx", "/admin/yyyy", "/admin/zzz")
Solution is to add the loginProc URL like this
antMatchers("/admin/xxx", "/admin/yyyy", "/admin/zzz", "/admin/loginProc")

Spring Security logout does not work - does not clear security context and authenticated user still exists

I know, there are many articles about this topic, but I have a problem and I can't find any solution.
I have a classic spring security java config:
#Configuration
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private AuctionAuthenticationProvider auctionAuthenticationProvider;
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(auctionAuthenticationProvider);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.httpBasic();
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry authorizeRequest = http.authorizeRequests();
configureAdminPanelAccess(authorizeRequest);
configureFrontApplicationAccess(authorizeRequest);
configureCommonAccess(authorizeRequest);
http.csrf()
.csrfTokenRepository(csrfTokenRepository()).and()
.addFilterAfter(csrfHeaderFilter(), CsrfFilter.class);
http.logout()
.clearAuthentication(true)
.invalidateHttpSession(true);
}
...
}
Also, I have two controller methods, where I login/logout from my web application by AJAX.
When I would like to logout, I first call this method, which I expect to clear user sessions and clear everything from the security context.
#Override
#RequestMapping(value = "/logout", method = GET, produces = APPLICATION_JSON_UTF8_VALUE)
public ResponseEntity<Boolean> logout(final HttpServletRequest request, final HttpServletResponse response) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null){
new SecurityContextLogoutHandler().logout(request, response, auth);
}
return new ResponseEntity<>(Boolean.TRUE, HttpStatus.OK);
}
After this I reload my client web application and each time, when it is reloaded, I check whether the user is authenticated by calling the following controller method:
#Override
#RequestMapping(value = "/user", method = GET, produces = APPLICATION_JSON_UTF8_VALUE)
public ResponseEntity<UserDetails> user() {
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {
return new ResponseEntity<>((UserDetails) principal, HttpStatus.OK);
}
return null;
}
And here I aways receive the last authenticated user. It seems that in the previous logout method, Spring logout doesn't work.
Keep in mind that I tried to logout with the following code, without any success:
#Override
#RequestMapping(value = "/logout", method = GET, produces = APPLICATION_JSON_UTF8_VALUE)
public ResponseEntity<Boolean> logout(final HttpServletRequest request) {
try {
request.logout();
return new ResponseEntity<>(Boolean.TRUE, HttpStatus.OK);
} catch (ServletException ex) {
if (LOG.isDebugEnabled()) {
LOG.debug("There is a problem with the logout of the user", ex);
}
}
Are you have any idea what I miss in my config and the logout process?
From your question, I see you are trying to create your own logout and you also trying to use the default Spring logout. I advise that you should choose one method and not mix them both. There are two I recommend to logout from Spring:
First: Default spring security logout
.logout().logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
.logoutSuccessUrl("/logout.done").deleteCookies("JSESSIONID")
.invalidateHttpSession(true)
From the example above, you should only need to call the /logout URL whenever you want to logout the user. No need to create any #Controller to handle that logout instead Spring will help to log the user out. You also can add other thing you want to invalidate here.
Second: Programmatically logout
#RequestMapping(value = {"/logout"}, method = RequestMethod.POST)
public String logoutDo(HttpServletRequest request,HttpServletResponse response){
HttpSession session= request.getSession(false);
SecurityContextHolder.clearContext();
session= request.getSession(false);
if(session != null) {
session.invalidate();
}
for(Cookie cookie : request.getCookies()) {
cookie.setMaxAge(0);
}
return "logout";
}
If you are using this logout approach, you don't need to include the first method in ht eSpring security config. By using this method, you can add an extra action to perform before and after logout done. BTW, to use this logout, just call the /logout url and the user will be logged out manually. This method will invalidate the session, clear Spring security context and cookies.
In addition for the second method, if you are using RequestMethod.POST, you need to include the CSRF key on the POST request. The alternative way is to create a form with a hidden input CSRF key. This is some example of auto generated logout link with jQuery :
$("#Logout").click(function(){
$form=$("<form>").attr({"action":"${pageContext.request.contextPath}"+"/logout","method":"post"})
.append($("<input>").attr({"type":"hidden","name":"${_csrf.parameterName}","value":"${_csrf.token}"}))
$("#Logout").append($form);
$form.submit();
});
You just need to create a hyperlink <a id="Logout">Logout</a> to use it.
If you are using RequestMethod.GET,just include a CSRF key as a parameter in you link like this:
Logout
Thats all, hope it helps.
Just a heads up, there is Clear Site Data HTTP header as shown below
Clear-Site-Data: "cache", "cookies", "storage", "executionContexts"
I also helped add support for Clear-Site-Data header into Spring-Security 5.2 project. For more details around the implementation, see the PR.
Here is a sample of how it is going to work
#EnableWebSecurity
static class HttpLogoutConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.logout()
.addLogoutHandler(new HeaderWriterLogoutHandler(
new ClearSiteDataHeaderWriter(SOURCE)));
}
}
Where SOURCE is a vararg of one or more of the following
"*" Clear everything
One or more of "cache", "cookies", "storage", "executionContexts"
For more details see the sample test in the LogoutConfigurerClearSiteDataTests.java.
This will help, i think clearAuthentication(true) is enough:
#Configuration
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
....
#Override
protected void configure(HttpSecurity http) throws Exception
{
http
.httpBasic()
.and()
.logout().clearAuthentication(true)
.logoutSuccessUrl("/")
.deleteCookies("JSESSIONID")
.invalidateHttpSession(true)
.and()
I solved my problem similarly by adding the following parameter to the application.properties file
spring.cache.type=NONE
Just change logout URL from "/logout" to "war or snapshot name/logout"

Categories