Spring 5, get info from LDAP (AD) - java

I'm trying to make a controller that receives an email and fetches information from that user in an Active Directory returning it as json.
I'm having a hard time finding useful material because everything I find is trying to teach authentication through WebSecurity annotation...
I couldn't care less about the authentication, I only want Spring to get info and nothing else.
Can anybody tell me how to get out from this to what I need?
#Bean
public ActiveDirectoryLdapAuthenticationProvider activeDirectoryLdapAuthenticationProvider() {
ActiveDirectoryLdapAuthenticationProvider provider = new ActiveDirectoryLdapAuthenticationProvider(LDAP_DOMAIN, LDAP_URL, LDAP_ROOT_DN);
provider.setConvertSubErrorCodesToExceptions(true);
provider.setUseAuthenticationRequestCredentials(true);
provider.setSearchFilter(LDAP_FILTER);
return provider;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().fullyAuthenticated().and().formLogin();
}

You can use LdapTemplate if you want to just get information. You can find information spring documentation . Besides, this tutorial has lots of ldap query examples with LdapTemplate

Related

Can I mix both basic authentication and JWT token authentication to protect APIs of a single Spring Boot project?

I am pretty new in Spring Security and I am working on a Spring Boot project that uses Basic Authentication in order to protect some APIs. I am starting from an existing tutorial code (a Udemy course) trying to adapt it to my own use cases.
In this project I have this SecurityConfiguration used to configure the basic authentication.
#Configuration
#EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter
{
private static String REALM = "REAME";
private static final String[] USER_MATCHER = { "/api/utenti/cerca/**"};
private static final String[] ADMIN_MATCHER = { "/api/utenti/inserisci/**", "/api/utenti/elimina/**" };
#Override
protected void configure(HttpSecurity http) throws Exception
{
http.csrf().disable()
.authorizeRequests()
.antMatchers(USER_MATCHER).hasAnyRole("USER")
.antMatchers(ADMIN_MATCHER).hasAnyRole("ADMIN")
.anyRequest().authenticated()
.and()
.httpBasic().realmName(REALM).authenticationEntryPoint(getBasicAuthEntryPoint()).and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
#Bean
public AuthEntryPoint getBasicAuthEntryPoint()
{
return new AuthEntryPoint();
}
/* To allow Pre-flight [OPTIONS] request from browser */
#Override
public void configure(WebSecurity web)
{
web.ignoring().antMatchers(HttpMethod.OPTIONS, "/**");
}
#Bean
public BCryptPasswordEncoder passwordEncoder()
{
return new BCryptPasswordEncoder();
};
#Bean
#Override
public UserDetailsService userDetailsService()
{
UserBuilder users = User.builder();
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(users
.username("ReadUser")
.password(new BCryptPasswordEncoder().encode("BimBumBam_2018"))
.roles("USER").build());
manager.createUser(users
.username("Admin")
.password(new BCryptPasswordEncoder().encode("MagicaBula_2018"))
.roles("USER", "ADMIN").build());
return manager;
}
}
So from what I have understand:
Here it id defined the list of API that can be accessed by a nornmal user and the list of API that can be accessed by and admin user:
private static final String[] USER_MATCHER = { "/api/utenti/cerca/**"};
private static final String[] ADMIN_MATCHER = { "/api/utenti/inserisci/**", "/api/utenti/elimina/**" };
Into the previous configure() method basically it is stating that the API URL matching with the USER_MATCHER are accessible by logged user having role USER while API having URL matching ADMIN_MATCHER are accessible by logged user having role ADMIN. Is this interpretation correct?
Finnally the UserDetailsService bean simply define two users: one belonging to the USER "group" and the other one belonging to both the USER and ADMIN "group".
So, if I well understood, the first one will be aple only to access to the API having enpoint URL /api/utenti/cerca/** while the second one will be able to access also to the APIs having endpoint URLs /api/utenti/inserisci/** and /api/utenti/elimina/**
Is it my reasoning correct?
And now my doubt: into a controller class of this project I defined this method:
#RestController
#RequestMapping("api/users")
#Log
public class UserController {
#Autowired
UserService userService;
//#Autowired
//private BCryptPasswordEncoder passwordEncoder;
//#Autowired
//private ResourceBundleMessageSource errMessage;
#GetMapping(value = "/test", produces = "application/json")
public ResponseEntity<String> getTest() throws NotFoundException {
log.info(String.format("****** getTest() START *******"));
return new ResponseEntity<String>("TEST", HttpStatus.OK);
}
..............................................................................................................
..............................................................................................................
..............................................................................................................
}
As you can see this method handling a GET request toward the localhost:8019/api/users/test endpoint.
This endpoint URL is not in any of the previous two list related the protected endpoint (it is not into the USER_MATCHER list neither into the ADMIN_MATCHER list. So I expected that simply this endpoint was not protected and accessible to everyone. But performing the previous request using PostMan, I obtain this error message:
HTTP Status 401 : Full authentication is required to access this resource
So basically it seems to me that also if this endpoint not belong to any protected endpoint list it is in some way protected anyway (it seems to me that at least the user must be authenticated (infact trying both the previous user I can obtain the expected output, so it should mean that the endpoint is not protected by the user rule but it is protected againts not authenticated access).
Why? Maybe it depende by the previous configure() method settings, in particular this line?
.anyRequest().authenticated()
In case is it possible to disable in some way to implement something like this:
If a called endpoint belong to one of the previous two lists (USER_MATCHER and ADMIN_MATCHER) --> the user must be authenticated and need to have the correct role.
If a called endpoint not belong to one of the previous lists --> everybody can access, also not authenticated user.
This approach make sense or am I loosing something?
I take this occasion to ask you also another information: do you think that it is possible to configure Spring security of this specific project in order to protect some specific endpoints using the basic authentication and some other specific endpoints using the JWT authentication.
Sone further notes to explain why this last question. This project is a microservice that at the moment is used by another microservice (used to generate JWT token) in order to obtain user information. (the other microservice call an API of this project in order to receive user information so it can generate a JWT token that will be used in my application. The comunication between these 2 microservice must use basic authentication).
Since this project contains all the entity classes used to map the tables related to the users on my DB, my idea was to use this project also for generic user management, so it could include functionality like: add a brand new user, changes information of an existing user, obtain the list of all the users, search a specific user, and so on.
These new APIs will be protected by JWT token because each API can be called from a specific user type having different privileges on the system.
So I am asking if in a situation like this I can add without problem 2 different types of authentication (basic authentication for the API that retrieve a user so the other microservice can obtain this info) and JWT authentication for all the other APIs. It make sense or is it better to create a brand new project for a new user management microservice?
So, if I well understood, the first one will be aple only to access to the API having enpoint URL /api/utenti/cerca/** while the second one will be able to access also to the APIs having endpoint URLs /api/utenti/inserisci/** and /api/utenti/elimina/**
Yes.
Why? Maybe it depende by the previous configure() method settings, in particular this line?
Yes, when using .anyRequest().authenticated(), any requests that have not been matched will have to be authenticated.
If a called endpoint not belong to one of the previous lists --> everybody can access, also not authenticated user.
You can achieve this by doing anyRequest().permitAll(). But this is not so secure because you are allowing access to every other endpoints, instead you should stay with anyRequest().authenticated() and allow access to specific endpoints manually, like so:
http
.authorizeRequests()
.antMatchers(USER_MATCHER).hasAnyRole("USER")
.antMatchers(ADMIN_MATCHER).hasAnyRole("ADMIN")
.antMatchers("/api/users/test").permitAll()
.anyRequest().authenticated()
...

Spring Security - Role check fails permanently [duplicate]

This question already has answers here:
Springboot Security hasRole not working
(3 answers)
Closed 1 year ago.
I am trying to learn Spring Security and want to secure an API. I want a login form with database authentication on the one hand, and on the other hand a OAuth2 authentication.
But I am stuck at the first task. The problem is that the application doesn't accept my roles and I have no idea why.
Here's my Config class:
#EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
DataSource dataSource;
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.jdbcAuthentication()
.dataSource(dataSource)
.usersByUsernameQuery("SELECT username,password,enabled FROM users WHERE username=?")
.authoritiesByUsernameQuery("SELECT username, authority FROM authorities WHERE username=?");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/createTrack").hasRole("CREATOR")
.antMatchers("/getTrack").hasAnyRole("CREATOR", "USER")
.antMatchers("/test").hasRole("CREATOR")
.antMatchers("/all").permitAll()
.and().formLogin();
}
#Bean
public PasswordEncoder getPasswordEncoder() {
// TODO: Change this to hashed password (this is for demo purposes)
return NoOpPasswordEncoder.getInstance();
}
}
Everything is made like descriped in the Spring documentation: https://docs.spring.io/spring-security/site/docs/current/reference/html5/#servlet-authentication-jdbc
I worked out a few endpoints of my API to test the logged in user's name and role. Everything works fine - the login seems to work great, also the role is correct.
But when I try to call an endpoint that's only accessible for one or both specific roles, it gets me a 403 error response.
What am I doing wrong? I tried a lot of things and it still does not work.
Thanks!
PS: I am using MyBatis (task from my company) - if that matters in any way?
Thank you guys for your help!
Toerktumlare's answer brought me to the solution. I turned on Debug Logging as you suggested. CORS was not the problem, but reading the debug messages brought me on the right way.
The problem was minor though. I stored the roles in the database as "CREATOR" or "USER". Debug messages showed me that Spring was looking for "ROLE_CREATOR" or "ROLE_USER" and - because my roles weren't saved this way - didn't find them. Hence I got a 403 HTTP response.

Concurrent session management not working. I have followed the documentation, but I think I am missing some small thing

This is my Security config
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/assets/**", "/register/**", "/","/login**")
.permitAll().antMatchers("/profile/**").hasAuthority("ROLE_1").anyRequest().authenticated()
.antMatchers("/actuator/**").hasAuthority("ROLE_2").anyRequest().authenticated()
.and().formLogin().loginPage("/login").permitAll()
.and().sessionManagement().maximumSessions(1).maxSessionsPreventsLogin(true).expiredUrl("/login?expired")
.and().and().logout().deleteCookies("JSESSIONID").invalidateHttpSession(true)
.and().csrf().disable();
// .failureUrl("/fail");
}
This is to add HttpSessionEventPublisher into app context
#Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}
Please let me know what I am missing. I am still able to login from two browsers using same credentials.
I found the solution. It is to override equals and hashCode methods of my User class which implements UserDetails based on below solutions. Comparison of UserDetails objects for authenticated principals is how session concurrency is controlled / measured
http://forum.spring.io/forum/spring-projects/security/99166-maximum-sessions-1-does-not-work
Spring Security maxSession doesn't work

Authentication is required to obtain an access token (anonymous not allowed)

I try to modify existing example - Tonr2 and Sparklr2.
Also I viewed this tutorial based on Spring Boot Spring Boot OAuth2. I try to build application like in Tonr2 example but without first login (on tonr2). I just need one Authentication on Sparklr2 side. I do this:
#Bean
public OAuth2ProtectedResourceDetails sparklr() {
AuthorizationCodeResourceDetails details = new AuthorizationCodeResourceDetails();
details.setId("sparklr/tonr");
details.setClientId("tonr");
details.setTokenName("oauth_token");
details.setClientSecret("secret");
details.setAccessTokenUri(accessTokenUri);
details.setUserAuthorizationUri(userAuthorizationUri);
details.setScope(Arrays.asList("openid"));
details.setGrantType("client_credentials");
details.setAuthenticationScheme(AuthenticationScheme.none);
details.setClientAuthenticationScheme(AuthenticationScheme.none);
return details;
}
But I have Authentication is required to obtain an access token (anonymous not allowed) . I checked this question. Of course, my user is anonymous - I want to login on Sparklr2. Also, I tried different combinations of settings of this bean, but nothing good. How to fix it? How to make it work as I want?
Almost two years late for the post.
The exception is thrown from AccessTokenProviderChain
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth instanceof AnonymousAuthenticationToken) {
if (!resource.isClientOnly()) {
throw new InsufficientAuthenticationException(
"Authentication is required to obtain an access token (anonymous not allowed)");
}
}
You either
Use ClientCredentialsResourceDetails in your OAuth2RestTemplate, or
Authenticate the user before using AuthorizationCodeResourceDetails to access external resources
In fact, in the tonr2 and sparklr2 example (I personally find the name very confusing), to access resources on sparklr2, a user has to first authenticate on tonr2. As seen in oauth2/tonr:
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("marissa").password("wombat").roles("USER").and().withUser("sam")
.password("kangaroo").roles("USER");
}
If your user is anonymous, you might want to check for Single Sign On.
For whoever just want to quickly try out Oauth2 integration, add basic auth to your application:
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated().and().httpBasic();
}
application.properties:
spring.security.user.password=password
spring.security.user.name=user
Don't forget to add spring-boot-starter-security to your project.
e.g. In gradle: compile 'org.springframework.boot:spring-boot-starter-security'
Or you can also disable AnonymousAuthenticationToken from creating by:
#Override
protected void configure(HttpSecurity http) throws Exception {
http.anonymous().disable();
}
Old post...
The exception indeed is thrown form AccessTokenProviderChain but it happens when spring security filters invoking if incorrect order. Make sure that your OpenIdAuthenticationFilter is invoking after OAuth2ClientContextFilter.

Spring Security for Active Directory searches by 'userPrincipalName' -- is there a way to change this?

I'm still learning LDAP / Active Directory so correct me if my terminology is wrong at all :)
In our Java Web Application, I'm trying to secure it with Spring Security LDAP. I managed to get Spring Security working with in-memory authentication but we need to tie it to our AD server.
I'm going to mask our actual domain with com.test
Here is the error I receive when I try to login from my application
13:39:55,701 ERROR ActiveDirectoryLdapAuthenticationProvider:133 -
Failed to locate directory entry for authenticated user: johnsmit
javax.naming.NameNotFoundException: [LDAP: error code 32 - 0000208D:
NameErr: DSID-0310020A, problem 2001 (NO_OBJECT), data 0, best match
of: 'CN=Users,DC=ad,DC=test,DC=com'
I am using class based configuration with Spring
Here is my SecurityConfiguration class
#Configuration
#EnableWebMvcSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Bean
public ActiveDirectoryLdapAuthenticationProvider activeDirectoryLdapAuthenticationProvider() {
provider = new ActiveDirectoryLdapAuthenticationProvider("ad.test.com", "ldap://servername.ad.test.com:389/cn=Users,dc=ad,dc=test,dc=com");
provider.setUseAuthenticationRequestCredentials(true);
return provider;
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(activeDirectoryLdapAuthenticationProvider());
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.
authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin().failureUrl("/login?error")
.loginPage("/login")
.permitAll()
.and()
.logout().logoutRequestMatcher(new AntPathRequestMatcher("/logout")).logoutSuccessUrl("/login")
.permitAll()
.and()
.httpBasic();
}
}
So here is the issue (at least I think)...
In our AD server we have our
cn,name,sAMAccountName and uid as our username that we login with, johnsmit in my example above.
Our userPrincipalName (in our AD server) is our email address so john.smith#test.com.
I was looking at the ActiveDirectoryLdapAuthenticationProvider class and it says it uses the userPrincipalName. Looking in the code here on github it shows that it is using userPrincipalName. I checked the newer versions of Spring Security which is not General Availability yet, but it was the same thing.
There must be someway that I can search AD with the username "johnsmit" instead of "john.smith#test.com"...
If the searchFilter was
String searchFilter = "(&(objectClass=user)(sAMAccountName={0}))"; that would be the ideal situation but I don't know if that is possible to override anywhere and I can't find any documentation?
As I found this eminently helpful in solving my own problem, I'd just like to add that the changes suggested by #JanTheGun appear to have been folded into Spring Security 5.0.8.RELEASE / Spring Boot 2.0.5
The JavaDoc for ActiveDirectoryLdapAuthenticationProvider.setSearchFilter(String) reads:
The LDAP filter string to search for the user being authenticated. Occurrences of {0} are replaced with the username#domain. Occurrences of {1} are replaced with the username only.
Defaults to: (&(objectClass=user)(userPrincipalName={0})))
Therefore, in Spring Security 5.0.8.RELEASE, it's possible to use the suggested search-filter change without having to replicate any of the Spring Security classes!
I guess I will answer my own question with this Jira ticket
https://jira.spring.io/browse/SEC-1915
Looks like it simply hasn't been merged in. The patch in this ticket would be the answer I need though.
Three years later I am also struggling with that problem. As there is the recommendation to change the userPrincipalName to the mail address for Office365 (see Office 365 – Why Your UPN Should Match Your Primary SMTP Address) I thought others might also have the problem - so here is my fix.
There is a discussion on github that is talking about the issue:
https://github.com/spring-projects/spring-security/issues/2448
The answer of user gkibilov solved my problem. The idea is to change the "searchForUser" method in the ActiveDirectoryLdapAuthenticationProvider class in order to not only pass the bindprincipal (=USERNAME#DOMAIN) but also the username.
Afterwards the following searchFilter can be used to look for the sAMAccountName instead of the userPrincipalName:
"(&(objectClass=user)(sAMAccountName={1}))"
Here are the single steps that I did:
Copy the source of the ActiveDirectoryAuthenticationException class
to a new class and rename it (ie "MyActiveDirectoryAuthenticationException"): SourceLink
Copy the source of the ActiveDirectoryLdapAuthenticationProvider
class to a new class and rename it (ie
"MyActiveDirectoryLdapAuthenticationProvider"): SourceLink
Exchange the ActiveDirectoryAuthenticationException class in MyActiveDirectoryLdapAuthenticationProvider with the new exception class (MyActiveDirectoryAuthenticationException)
Change the method in MyActiveDirectoryAuthenticationException class to also pass the username:
private DirContextOperations searchForUser(DirContext context, String username)
throws NamingException {
SearchControls searchControls = new SearchControls();
searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
String bindPrincipal = createBindPrincipal(username);
String searchRoot = rootDn != null ? rootDn
: searchRootFromPrincipal(bindPrincipal);
try {
return SpringSecurityLdapTemplate.searchForSingleEntryInternal(context,
searchControls, searchRoot, searchFilter,
new Object[] { bindPrincipal, username});
}
...
}
Change the searchFilter to look for the sAMAccountName attribute. In your case the bean should look like this:
#Bean
public MyActiveDirectoryLdapAuthenticationProvider activeDirectoryLdapAuthenticationProvider() {
provider = new MyActiveDirectoryLdapAuthenticationProvider("ad.test.com", "ldap://servername.ad.test.com:389/cn=Users,dc=ad,dc=test,dc=com");
provider.setSearchFilter("(&(objectClass=user)(sAMAccountName={1}))");
provider.setUseAuthenticationRequestCredentials(true);
return provider;
}
I have to admit that it is not the nicest solution to copy a class but as the original class is "final" I couldn't find a better way to fix this problem.

Categories

Resources