For a test environment, I have an .ldif file to grant the ben user, the ADMIN role, as this role is required by my Spring security configuation: .hasRole("ADMIN").anyRequest().
Here is the .ldif file content:
dn: ou=groups,dc=springframework,dc=org
objectclass: top
objectclass: organizationalUnit
ou: groups
dn: ou=people,dc=springframework,dc=org
objectclass: top
objectclass: organizationalUnit
ou: people
dn: uid=ben,ou=people,dc=springframework,dc=org
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
cn: Ben LeHeros
sn: Ben
uid: ben
userPassword: {SHA}nFCebWjxfaLbHHG1Qk5UU4trbvQ=
dn: uid=toto,ou=people,dc=springframework,dc=org
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
cn: Toto LeHeros
sn: Toto
uid: toto
userPassword: totopass
dn: cn=adMIN,ou=groups,dc=springframework,dc=org
objectclass: top
objectclass: groupOfNames
cn: ADMin
uniqueMember: uid=ben,ou=people,dc=springframework,dc=org
dn: cn=user,ou=groups,dc=baeldung,dc=com
objectclass: top
objectclass: groupOfNames
cn: user
member: uid=toto,ou=people,dc=springframework,dc=org
The test environment Spring Security configuration is the following:
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth)
throws Exception {
auth
.ldapAuthentication()
.userDnPatterns("uid={0},ou=people")
.groupSearchBase("ou=groups")
.contextSource().ldif("classpath:test-server.ldif");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().httpBasic()
.authenticationEntryPoint(restAuthenticationEntryPoint).and()
.authorizeRequests().antMatchers("/**")
.hasRole("ADMIN").anyRequest()
.authenticated();
}
There are a few observations:
1- The test is successful and the user is authenticated and accredited as having the admin role.
2- If instead of configuring the role accreditation as .hasRole("ADMIN").anyRequest() I do it as .hasRole("admin").anyRequest() then the user role given in the test is not accepted and the test fails with a 403 (not a 401) upon authentication.
3- The case does not seem to matter in the .ldif file as the admin group can be written as adMIN and ADMin and the test is still successful.
4- Replacing the admin group by a user group for the user, makes the test fails with a 403, that is, the user requires an admin role to be logged in.
dn: cn=user,ou=groups,dc=springframework,dc=org
objectclass: top
objectclass: groupOfNames
cn: user
uniqueMember: uid=ben,ou=people,dc=springframework,dc=org
How come the case does not matter in the .ldif file and matters in the configure method ?
Per Directory Server schema (that are the defined attributes and objectclasses) the attributes to build the Distinguished Name (DN) of the group entry are defined as case-ignore (DN "spec" https://www.rfc-editor.org/rfc/rfc4514).
Personally I would consider it as a bug that the hasRole(..) matches in a case-sensitive way. However I think this is the case because the DefaultLdapAuthoritiesPopulator defaults to convertToUpperCase to true
Related
I am trying to create Sprint Boot, Spring Security 6, LDAP Server (external not embedded) based authentication application. When I spin up the app and provide the username (uid) and password on the login form I get a "Bad Credentials" message displayed on the UI. There are no exceptions reported in the application log. I do not understand what is causing "Bad Credentials" message to be displayed. Any pointers are much appreciated.
This is what my config file looks like
#Configuration
#EnableWebSecurity
public class WebSecurityConfig {
#Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity.authorizeHttpRequests().anyRequest().fullyAuthenticated()
.and()
.formLogin();
httpSecurity.authenticationProvider(ldapAuthenticationProvider());
return httpSecurity.build();
}
#Bean
LdapAuthenticationProvider ldapAuthenticationProvider() {
return new LdapAuthenticationProvider(authenticator());
}
#Bean
BindAuthenticator authenticator() {
FilterBasedLdapUserSearch search = new FilterBasedLdapUserSearch("ou=people", "(uid={0})", contextSource());
BindAuthenticator authenticator = new BindAuthenticator(contextSource());
authenticator.setUserSearch(search);
return authenticator;
}
#Bean
public DefaultSpringSecurityContextSource contextSource() {
DefaultSpringSecurityContextSource dsCtx = new DefaultSpringSecurityContextSource("ldap://localhost:389/dc=example,dc=com");
dsCtx.setUserDn("cn=admin,dc=example,dc=com");
dsCtx.setPassword("password");
return dsCtx;
}
}
When I try to find user using ldapsearch command I do get the user info
MacBook-Pro:springsecuritywithldapdemo$ ldapsearch -LLL -x -H ldap:// -t -b "dc=example,dc=com" "uid=jsmith1"
dn: uid=jsmith1,ou=people,dc=example,dc=com
objectClass: inetOrgPerson
description: John Smith from Accounting. John is the project manager of the b
uilding project, so contact him with any questions.
cn: John Smith
sn: Smith
uid: jsmith1
userPassword:: anNtaXRoMTIz
I have gone through many of the search results returned by google on different searches, most of them have used an older version of Spring Security or have used JDBC authentication with Spring Security 6.
I have referred to the youtube tutorials to see if I am doing anything wrong but doesn't look like.
first of all I wanted to thank you because based on your solution I was able to solve the same problem in my application. Compared to your solution the only things I have changed are:
FilterBasedLdapUserSearch search = new FilterBasedLdapUserSearch("cn=users,cn=accounts,dc=example,dc=com",
"(uid={0})",
contextSource());
and so in your case you should try:
FilterBasedLdapUserSearch search = new FilterBasedLdapUserSearch("ou=people,dc=example,dc=com", "(uid={0})", contextSource());
and then I also had to make the following changes:
DefaultSpringSecurityContextSource dsCtx = new DefaultSpringSecurityContextSource(
"ldap://localhost:389");
dsCtx.setUserDn("uid=admin,cn=users,cn=accounts,dc=example,dc=com");
which in your case I think is almost identical since I think you also have a user with uid=admin. I hope it will solve your problem, as it did for me
I have a trouble with configuring LDAP authentication with Spring.
Using LDAP Apache Directory Studio I have following working connection to LDAP Server:
Bind DN or USER: cn=HIDDEN_USERNAME,OU=HIDDEN_OU1,OU=HIDDEN2,OU=Admin,DC=MY_COMPANYNAME,DC=COM
Authorization ID: SASL PLAIN only
Bind Password: ******
Using this connection, I can find my account under root:
Root DSE/DC=MY_COMPANYNAME,DC=COM/OU=User Accounts/OU=Enabled Users/OU=Consultants/CN=MySurname My Name
Right click on my account gives following values:
DN: CN=MySurname MyName,OU=Consultants,OU=Enabled Users,OU=User Accounts,DC=MY_COMPANYNAME,DC=COM
URL: ldap://IP_ADRESS:389/CN=MySurname%20MyName,OU=Consultants,OU=Enabled%20Users,OU=User%20Accounts,DC=MY_COMPANYNAME,DC=COM
I am going to configure WebSecurityConfigurerAdapter in order to get authentication via ldap server in the following way:
#Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.ldapAuthentication()
.userDnPatterns("CN={0},OU=Consultants,OU=Enabled Users,OU=User Accounts,DC=MY_COMPANYNAME,DC=COM")
.contextSource()
.url("ldap://IP_ADRESS:389/")
.managerDn("HIDDEN_USERNAME")
.managerPassword("*****")
.and()
.passwordCompare()
.passwordEncoder(new LdapShaPasswordEncoder())
.passwordAttribute("userPassword");
}
I tried to set userDnPattern in many ways without result. What I am doing wrong?
Using the DN pattern you specify, your logon attempt would need to be made with user ID "MySurname MyName" (and the space may be an issue). The user provided logon ID string is inserted into the DN pattern you include above, and you'll be binding with
CN=MySurname MyName,OU=Consultants,OU=Enabled Users,OU=User Accounts,DC=MY_COMPANYNAME,DC=COM
Which matches what your fully qualified DN appears to be. If you want to be able to log on with your ID and not the surname/name string that makes up your CN, or if accounts which need to authenticate exist in multiple OU locations, userSearch may be preferable to DN patterns.
If you are authenticating against an Active Directory domain, you may be able to use {0}#domain.gTLD or DOMAIN{0} as the user pattern -- when a logon ID is supplied, these patterns form the userPrincipalName and sAMAccountName respectively.
In response to your comment above: Active Directory hides the password field and it cannot be read even by domain administrators.
I concur with the other user that for AD you need to use a user search filter and if you want to do it against the username you should use samaccountname={0}
I am trying to integrate LDAP authentication for my Spring MVC app. The users are successfully able to log in, if I set the contextSource to a dummy user DN and respective password.
What I want to do is to be able to bind the ldap connection without using the dummy user.
Here's the code of what works -
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/css/**").permitAll()
.antMatchers("/","/login**").permitAll()
.antMatchers("/home_vm","/details/**").authenticated()
.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.defaultSuccessUrl("/home_vm", true)
.and()
.logout()
.permitAll();
http.headers().httpStrictTransportSecurity();
}
#Configuration
#Profile({"default", "opt_ad_auth"})
protected static class ActiveDirectoryAuthenticationConfiguration extends
GlobalAuthenticationConfigurerAdapter {
#Value("${app.ldap.url}")
private String ldapURL;
#Override
public void init(AuthenticationManagerBuilder auth) throws Exception {
DefaultSpringSecurityContextSource contextSource = new DefaultSpringSecurityContextSource(ldapURL);
contextSource.setUserDn("cn=Dummy User,cn=Users,dc=somecompany,dc=com");
contextSource.setPassword("mypassword");
contextSource.setReferral("follow");
contextSource.afterPropertiesSet();
auth.ldapAuthentication()
.userSearchFilter("(sAMAccountName={0})")
.contextSource(contextSource)
;
}
}
}
Now I have tried to remove the hardcoded userDn and password (updated init())-
public void init(AuthenticationManagerBuilder auth) throws Exception {
auth.ldapAuthentication()
.userSearchFilter("(sAMAccountName={0})")
.contextSource()
.url(ldapURL)
;
}
}
The app starts fine, but I get exception - "a successful bind must be completed on the connection".
Stacktrace -
org.springframework.security.authentication.InternalAuthenticationServiceException: Uncategorized exception occured during LDAP processing; nested exception is javax.naming.NamingException: [LDAP: error code 1 - 000004DC: LdapErr: DSID-0C0906E8, comment: In order to perform this operation a successful bind must be completed on the connection., data 0, v1db1
[UPDATE] I have modified the init method to the following to more closely follow the spring tutorial(https://spring.io/guides/gs/authenticating-ldap/) -
public void init(AuthenticationManagerBuilder auth) throws Exception {
auth.ldapAuthentication()
.userDnPatterns("sAMAccountName={0}")
.contextSource().url(ldapURL)
;
}
I don't get the before-mentioned bind exception, but still not able to authenticate. Bad credentials.
You need to do this in two steps:
Bind to LDAP as an administrative user that has enough privileges to search the tree and find the user via whatever unique information you have about him.
Rebind to LDAP as the found user's DN using the supplied password.
Disconnect.
If all that succeeds, the username existed and the password was correct. If there was any failure you should treat it as a loging failure. Specifically, you should not tell the user whether it was the username not being found or the password being incorrect that was the reason for the failure: this is an information leak to an attacer.
You can authenticate with LDAP in 2 forms:
You can make a bind with the complete DN of the user (full path in the LDAP tree) and the password.
Or you can bind to LDAP as a superuser that can search on all the LDAP tree. The LDAP authenticator finds the user DN and compare the user password with the password in the user entry.
In your first code you use the second system. You need a user that can bind on the LDAP and find the user entry.
If you want user the second system, you must supply the complete DN of the user entry not the uid only.
I have custom authentication filter which creates PreAuthenticatedAuthenticationToken and stores it in security context. The filter works fine, it creates the token with the appropriate granted authorities "ROLE_user" and "ROLE_adminuser". Here's my config:
#Configuration
#EnableWebMvcSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
X509AuthenticationFilter filter = new X509AuthenticationFilter()
filter.setAuthenticationManager(authenticationManager())
http.addFilterAfter(filter, SecurityContextPersistenceFilter.class)
http.authorizeRequests().anyRequest().permitAll()
}
#Bean
public FilterRegistrationBean filterRegistrationBean() {
FilterRegistrationBean registrationBean = new FilterRegistrationBean()
X509AuthenticationFilter filter = new X509AuthenticationFilter()
filter.setAuthenticationManager(authenticationManager())
registrationBean.setFilter(filter)
registrationBean.setEnabled(false)
return registrationBean
}
I'm inserting the Filter before the SecurityContextPersistenceFilter as mentioned in: Spring security and custom AuthenticationFilter with Spring boot.
This seems to work fine, however, when I attempt to add a PreAuthorize annotation to my controller method, like so:
#Controller
#RequestMapping("/security")
class SecurityController {
#RequestMapping("/authenticate")
#PreAuthorize("hasRole('ROLE_user')")
#ResponseBody
ResponseEntity<String> authenticate(HttpServletRequest request, Principal principal) {
println "authenticate: " + SecurityContextHolder.getContext().getAuthentication()
return new ResponseEntity<String>(getPreAuthenticatedPrincipal(request), HttpStatus.OK)
}
I get a 404 error. If I comment out the PreAuthorize annotation the method call works and you can see that I print out the authentication information and the authenticated user has ROLE_user and ROLE_adminuser for its granted authorities. I'm not sure what I'm doing wrong.
Here's the print out of the authentication object in the principal when "PreAuthorize" is commented out:
authenticate:
org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken#817a0240:
Principal:
org.springframework.security.ldap.userdetails.LdapUserDetailsImpl#c279eab8:
Dn: uid=1,ou=people,dc=cdpe,dc=mil; Username: xxxx#gmail.com;
Password: [PROTECTED]; Enabled: true; AccountNonExpired: true;
CredentialsNonExpired: true; AccountNonLocked: true; Granted
Authorities: ROLE_adminuser, ROLE_user; Credentials: [PROTECTED];
Authenticated: true; Details:
org.springframework.security.web.authentication.WebAuthenticationDetails#957e:
RemoteIpAddress: 127.0.0.1; SessionId: null; Granted Authorities:
ROLE_adminuser, ROLE_user authorities: [ROLE_adminuser, ROLE_user]
Update: I've made a little progress. I added the proxyTargetClass to the following annotation:
#EnableGlobalMethodSecurity(prePostEnabled = true, proxyTargetClass = true)
Now I'm getting 403 Forbidden returns when I call my method. I have no idea what that is doing.
Spring Security has always been tedious to configure, and the only foolproof ways are:
either being an expert on it and be prepared to look in the sources and then you can do hard things by hand
or use as much as possible of what is provided by framework using examples from the documentation whenever possible
For the configuration of an X509AuthenticationFilter, HttpSecurity javadoc gives the method x509 with following example (adapted to your config - see javadoc for original one) :
#Configuration
#EnableWebMvcSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests().anyRequest().permitAll()
// Example x509() configuration
.x509();
}
}
with following indication: method returns the X509Configurer for further customizations.
Unless you have a good reason to do differently (and if it is the case please say it) I strongly advise you to stick to that method.
But it is really a bad idea to use pre-post annotation on a controller, for what could be done directly in HttpSecurity configuration. It forced you to use proxyTargetClass = true.
Pre and post annotation are normally applied to methods of service layer what do not require proxyTargetClass=true since services are normally wired to controller through interfaces allowing JDK proxying.
Thanks for your help. For this example I'm being forced to re-use an already working Spring security custom filter. I'm just trying to get it to work in the spring-boot context.
Adding the proxyTargetClass = true to the above code seems to have fixed the problem.
Using spring securiity, I'm faced with a requirement for a complex business decision about whether to allow anonymous access or insist on user login. Logic is too complex for "intercept-url" expressions ( intercept-url pattern='/mypattern/**'...)
E.g. assume a RESTFul books service:
https://myserver/rest/book?title=war_and_peace
https://myserver/rest/book?title=the_firm
Say "war and peace" allows anonymous access because its old and its copyrights have expired.
While "the firm" is copyrighted & requires login (so as to charge the user).
This copyright info is available in a database.
Could anyone please offer any hints as to how to achieve this in spring secuirity? thanks in advance
There are of course multiple ways that code can look like, but to get the idea:
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth instanceof AnonymousAuthenticationToken && "the_firm".equals(title)) {
throw new AccessDeniedException("Requires login!");
}
Update: A more declarative way is to use annotations:
#PostAuthorize("hasPermission(returnObject, 'READ')")
public Book findBook(String title) {
// ...
}
This requires that you create a permission evaluator bean and add config:
<global-method-security pre-post-annotations="enabled">
<expression-handler ref="expressionHandler" />
</global-method-security>
#Bean
public DefaultMethodSecurityExpressionHandler expressionHandler() {
DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler();
handler.setPermissionEvaluator(permissionEvaluator);
return handler;
}