I'm building an application which will allow only a specific set of users in my org. to login. Only those users that belong to a particular AD Group can login. Eg: GDL - MyTeam is a GDL, only who's members I want to allow to get in.
I checked out Atlassian's tutorial, and confluent's tutorial as well as Megha's answer here.
What is different in my case, compared to other stack overflow questions is that I'm using ActiveDirectoryLdapAuthenticationProvider as can be seen in my code snippet below. That is the one to be dictating the terms.
However, My application would still allow any user in the org to get in to my application. I'm really not able to understand what criteria is it using to allow anyone.
I'm a totally newbie to ldiff syntax and filtering ldap using Java. Combine with springboot, I really don't know if I should use group search base or user search base. I just want people of my GDL to be able to get in. Rest should receive an authentication failure.
Here is my code file for reference:
#Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
configureLdap(auth);
configureActiveDirectory(auth);
}
private void configureLdap(AuthenticationManagerBuilder auth) throws Exception {
auth
.ldapAuthentication()
.contextSource(contextSource())
.userSearchFilter("(&(objectClass=user)(sAMAccountName=*)(memberOf=cn=GDL-MyTeam,ou=users,dc=myCompany,dc=com)))")
.passwordCompare()
.passwordEncoder(passwordEncoder())
.passwordAttribute("userPassword");
}
private void configureActiveDirectory(AuthenticationManagerBuilder auth) {
ActiveDirectoryLdapAuthenticationProvider adProvider = activeDirectoryLdapAuthenticationProvider();
if (adProvider != null) {
auth.authenticationProvider(adProvider);
auth.eraseCredentials(false);
}
}
#Bean(BeanIds.AUTHENTICATION_MANAGER)
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Bean
public LdapContextSource contextSource() {
LdapContextSource contextSource = new LdapContextSource();
contextSource.setUrl(ldapUrls); //mycompany.com:389
contextSource.setBase(ldapBaseDn); //dc=myCompany,dc=com
contextSource.setUserDn(env.getProperty(ldapSecurityPrincipal));
contextSource.setPassword(env.getProperty(ldapPrincipalPassword));
contextSource.setReferral("follow");
contextSource.afterPropertiesSet();
return contextSource;
}
#Bean
protected ActiveDirectoryLdapAuthenticationProvider activeDirectoryLdapAuthenticationProvider() {
ActiveDirectoryLdapAuthenticationProvider provider = new ActiveDirectoryLdapAuthenticationProvider("myCompany.com", ldapUrls,
ldapBaseDn);
provider.setConvertSubErrorCodesToExceptions(true);
provider.setUseAuthenticationRequestCredentials(true);
provider.setUserDetailsContextMapper(new CustomUserDetailsContextMapper());
return provider;
}
#Bean
public LdapTemplate ldapTemplate() {
LdapTemplate template = new LdapTemplate();
template.setContextSource(contextSource());
template.setIgnoreNameNotFoundException(true);
template.setIgnorePartialResultException(true);
return template;
}
I believe this filter is the place where I have specified the correct matching criteria, but for some reason it's allowing everyone and not just My team's specific GDL.
.userSearchFilter("(&(objectClass=user)(sAMAccountName=*)(memberOf=cn=GDL-MyTeam,ou=users,dc=myCompany,dc=com)))")
Can anyone please provide guidance as to where I am going wrong.
Thanks a ton!
EDIT:
I figured out that ActiveDirectoryLdapAuthenticationProvider is dictating the terms. I believe this is the place where I need to put in the search filter. If I put in the exact same filter as the other answers
In order to perform this operation a successful bind must be completed on the connection., data 0, v3839]; remaining name '/'
But I really don't understand what to put in here. Suggestions please?
I wonder if this might be the issue:
.userSearchFilter("(&(objectClass=user)(sAMAccountName=*)(memberOf=cn=GDL-MyTeam,ou=users,dc=myCompany,dc=com)))")
You use sAMAccountName=* (with the *). Looks like wild card to me, meaning anyone?. What if you replace that with {1} like in
.userSearchFilter("(&(objectClass=user)(sAMAccountName={1})(memberOf=cn=GDL-MyTeam,ou=users,dc=myCompany,dc=com)))")
Related
I'm writing a simple REST API using Spring Boot and I want to enable basic authentication. Therefore I have used the WebSecurityConfigurerAdapter as shown below. For simplicity, I just want to check only the password (pwd123) and allow any user to log in. Please refer to the code below.
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(new AuthenticationProvider() {
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
if (authentication == null || authentication.getCredentials() == null) {
throw new BadCredentialsException("Bad credentials");
}
if (authentication.getCredentials().equals("pwd123")) {
return new UsernamePasswordAuthenticationToken(authentication.getName(),
authentication.getCredentials().toString(),
Collections.emptyList());
}
return null;
}
#Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
});
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated()
.and().httpBasic();
}
}
Assume user_A has accessed the REST API with a valid password, i.e pwd123, and then do the send API call with a wrong password. However the user is allowed to access the API which is the problem.
When I do the debugging I realized that authenticationIsRequired function in BasicAuthenticationFilter class which is in Spring Security, returns false in such scenario. Please refer that code.
private boolean authenticationIsRequired(String username) {
// Only reauthenticate if username doesn't match SecurityContextHolder and user
// isn't authenticated (see SEC-53)
Authentication existingAuth = SecurityContextHolder.getContext().getAuthentication();
if (existingAuth == null || !existingAuth.isAuthenticated()) {
return true;
}
// Limit username comparison to providers which use usernames (ie
// UsernamePasswordAuthenticationToken) (see SEC-348)
if (existingAuth instanceof UsernamePasswordAuthenticationToken && !existingAuth.getName().equals(username)) {
return true;
}
// Handle unusual condition where an AnonymousAuthenticationToken is already
// present. This shouldn't happen very often, as BasicProcessingFitler is meant to
// be earlier in the filter chain than AnonymousAuthenticationFilter.
// Nevertheless, presence of both an AnonymousAuthenticationToken together with a
// BASIC authentication request header should indicate reauthentication using the
// BASIC protocol is desirable. This behaviour is also consistent with that
// provided by form and digest, both of which force re-authentication if the
// respective header is detected (and in doing so replace/ any existing
// AnonymousAuthenticationToken). See SEC-610.
return (existingAuth instanceof AnonymousAuthenticationToken);
}
Please let me know what is missing in my implementation
As mentioned in the comments, instead of providing a custom AuthenticationProvider you can try providing a custom UserDetailsService. Here's the complete configuration:
#EnableWebSecurity
public class SecurityConfiguration {
#Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeRequests((authorizeRequests) -> authorizeRequests
.anyRequest().authenticated()
)
.httpBasic(Customizer.withDefaults());
return http.build();
}
#Bean
public UserDetailsService userDetailsService() {
return (username) -> new User(username, "{noop}pwd123", AuthorityUtils.createAuthorityList("ROLE_USER"));
}
}
When you evolve to looking up the user via a third-party service, you can add the code to do this in the custom UserDetailsService (a lambda function or an actual class that implements the interface) and continue returning a org.springframework.security.core.userdetails.User.
Note: I don't actually recommend plain-text passwords in production. You would replace {noop}pwd123 with something like {bcrypt}<bcrypt encoded password here>.
As suggested in the comments and answers, even if you use the InMemoryUserDetailsManager the problem does not get resolved, which means, once the user is authenticated with the correct user name and password, his password is not validated in the subsequent REST API calls,i.e. can use any password. This is because of the functionality in BasicAuthenticationFilter class where it skips users who are having a valid JSESSION cookie.
To fix the issue, we should configure http to create state-less sessions via
http .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
in configure function of the WebSecurityConfigurerAdapter
Please refer Why BasicAuthenticationFilter in spring security matches only username and not the password
This is my first Question ever here on SO, it was helpfull and saved me lots of time, but now I can't find any solution to my problem.
As I'm rather new to spring and espacially to spring-security, I'm stuck with something that might be easy if i had more knowledge.
I have an existing Application that uses a local user database. It uses a custom UserDetails implementation that works if used with user:password authentification through a login form.
Here is the current setup:
public class SecurityContext extends WebSecurityConfigurerAdapter {
....
#Autowired
public void configureGlobal(final AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider()).userDetailsService(userDetailsService());
}
#Bean
public AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider result = new DaoAuthenticationProvider();
result.setUserDetailsService(userDetailsService());
result.setPasswordEncoder(passwordEncoder());
return result;
}
#Override
#Bean
public GatesUserDetailsService userDetailsService() {
GatesUserDetailsService result = new GatesUserDetailsService();
result.setClientService(clientService);
result.setAccountService(accountService);
result.setCardService(cardService);
result.setPersonService(personService);
result.setAccountPropertyService(accountPropertyService);
result.setLoginAttemptService(loginAttemptService);
return result;
}
Now I want to use SSO from an external IDP that speaks OpenIdConnect.
Going through the documentation I was able to get this up and running in a "default" manner. That is, at the and of my process a get a user that is an Instance of OidcUser. I need that user to be either extended or incorporate the existing userDetails.
The documentation (Spring Boot and OAuth2) recommends to
Implement and expose OAuth2UserService to call the Authorization
Server as well as your database. Your implementation can delegate to
the default implementation, which will do the heavy lifting of calling
the Authorization Server. Your implementation should return something
that extends your custom User object and implements OAuth2User.
I was able to introduce my own Oauth2UserService that gets called right at the and of the authentification by setting:
#Override
protected void configure(final HttpSecurity http) throws Exception {
http.exceptionHandling()
.accessDeniedHandler(accessDeniedHandler())
.and()
.oauth2Login()
.failureHandler(authenticationFailureHandler())
.successHandler(authenticationSuccessHandler())
.userInfoEndpoint()
.userService(this.oauth2UserService())
.oidcUserService(this.oidcUserService());}
private OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
final OidcUserService delegate = new OidcUserService();
return (userRequest) -> {
OidcUser oidcUser = delegate.loadUser(userRequest);
//..DO some additional Stuff check against external Server
//Here I could load my custom userDetails
GatesUserDetails userDetails = (GatesUserDetails) userDetailsService.loadUserByUsername("131:" + username);
....
But I have now Idea how to make my customUser a vaild return to my function.
I tried to implement the OidcUser Interface in my userDetails, but still it does not work.
Any hint (even to a more understandable doc) would be highly appreciated.
EDIT
To clarify things, I implemented the oidcUser Interface as stated in the docs along with the necessary implementations (getAttribute, getAttributes, getAuthorities) but still I could not use this as the return type would still be our GatesUserDetails, no way (for me) to cast it to oidcUser
Have the same problem with spring-security-oauth2-client-5.6.2, after hours google and debugger it solved.
First, make sure your UserInfo entrypoint is correct in case you own the
Auth server.
Plus requested scopes contains any of profiles not
only openid.
Logic found here: OidcUserService::shouldRetrieveUserInfo
private boolean shouldRetrieveUserInfo(OidcUserRequest userRequest) {
// Auto-disabled if UserInfo Endpoint URI is not provided
ProviderDetails providerDetails = userRequest.getClientRegistration().getProviderDetails();
if (StringUtils.isEmpty(providerDetails.getUserInfoEndpoint().getUri())) {
return false;
}
// The Claims requested by the profile, email, address, and phone scope values
// are returned from the UserInfo Endpoint (as described in Section 5.3.2),
// when a response_type value is used that results in an Access Token being
// issued.
// However, when no Access Token is issued, which is the case for the
// response_type=id_token,
// the resulting Claims are returned in the ID Token.
// The Authorization Code Grant Flow, which is response_type=code, results in an
// Access Token being issued.
if (AuthorizationGrantType.AUTHORIZATION_CODE
.equals(userRequest.getClientRegistration().getAuthorizationGrantType())) {
// Return true if there is at least one match between the authorized scope(s)
// and accessible scope(s)
return this.accessibleScopes.isEmpty()
|| CollectionUtils.containsAny(userRequest.getAccessToken().getScopes(), this.accessibleScopes);
}
return false;
}
Hope this could help someone.
As far as Spring security is concerned, it is completely new to me. I found many sources online describing how to set up basic security and was able to get HTTPS REST calls to work with the following configuration on the server side:
#Configuration
#EnableWebSecurity
#EnableConfigurationProperties(SecurityAuthProperties.class)
public class ServerSecurityConfiguration extends WebSecurityConfigurerAdapter {
private final SecurityAuthProperties properties;
#Autowired
public ServerSecurityConfiguration(SecurityAuthProperties properties) {
this.properties = properties;
}
#Override
public void configure(HttpSecurity http) throws Exception {
properties.getEndpoints().forEach((key, value) -> {
try {
for (HttpMethod method : value.getMethods()) {
http.authorizeRequests().antMatchers(method, value.getPath()).permitAll().and()
.httpBasic().and().csrf().disable();
}
} catch (Exception e) {
throw new SecurityConfigurationException(
"Problem encountered while setting up endpoint restrictions", e);
}
});
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
Upon closer inspection, though, it looks as though some portion (not sure how much) is actually being disabled. Could this be why it allows access from a client?
When I modified the configuration to what follows below, I always get the response "Forbidden".
#Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/rst/**").permitAll();
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
It seems to me that this code would allow access to anything in the path /rst and under, yet the opposite seems to be true. What am I missing?
Note: Another thing I should mention is that there is currently no "user" authentication. The "client" is not web based, but is a separate Spring Boot service that has its own client-side security configuration.
Update:
Here is one of the controllers:
#RestController
#RequestMapping("/rst/missionPlanning")
public class MissionPlannerController {
#Autowired
private MissionPlanner service;
#Autowired
private ThreadPoolTaskExecutor executor;
#PostMapping(value = "/planMission", produces = MediaType.APPLICATION_JSON_VALUE)
public DeferredResult<ResponseEntity<GeneralResponse>> planMission() {
DeferredResult<ResponseEntity<GeneralResponse>> result = new DeferredResult<>(60000L);
executor.execute(new Runner(result));
return result;
}
private class Runner implements ITask {
private DeferredResult<ResponseEntity<GeneralResponse>> result;
public Runner(DeferredResult<ResponseEntity<GeneralResponse>> result) {
this.result = result;
}
#Override
public void executeTask() {
// Invoke service and set result.
result.setResult(ResponseEntity.ok(service.planMission()));
}
}
}
Update:
Interesting. I found an example from another SO post (Security configuration with Spring-boot) that seems to work. The only thing that's different is the disabling of CSRF.
I see that stands for Cross-Site Request Forgery, but I don't really understand what that is, whether I should have it enabled, and if I do, how do I get it to work then?
#Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable().authorizeRequests().antMatchers("/rst/**").permitAll();
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
There could be something wrong with how you've set up your controller. Does your controller that contains that path have #RequestMapping("/rst")?
It'd be helpful if you updated your post with what your controller looks like.
Edit:
It seems your issue was the type of request being made if you had to disble CSRF.
CSRF requires a token to be specified on all request methods that can cause a change (i.e. POST, PUT, DELETE, PATCH, but not GET).
The reason for this is that when you control the web page, it adds a layer of security where only you are allowed to make these API calls. Without the CSRF token specified in the request, a malicious user will not be able to make that request to your service since the CSRF token is impossible to guess.
You can read more about it here:
https://docs.spring.io/spring-security/site/docs/3.2.0.CI-SNAPSHOT/reference/html/csrf.html#csrf-include-csrf-token
And here: https://www.baeldung.com/spring-security-csrf
I need to use a custom LdapAuthenticationProvider, with only one minor change, in order to execute the authentication, a certain precondition needs to be met.
What I want basically:
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
if (!precondition) {
throw new DisabledException("");
}
return super.authenticate(authentication);
}
My WebSecurityConfigurerAdapter:
#Autowired
protected void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(userDetailsService)
.passwordEncoder(encoder);
if (ldapSecurityConfig.isLdapEnabled()) {
auth
.ldapAuthentication()
.contextSource(ldapContextSource)
.userSearchFilter(ldapSecurityConfig.getUserSearchFilter())
.ldapAuthoritiesPopulator(ldapAuthoritiesPopulator)
.userDetailsContextMapper(userDetailsContextMapper);
}
}
The problem is, that the line
auth.ldapAuthentication()
creates an LdapAuthenticationProviderConfigurer object, and its build method instantiates an LdapAuthenticationProvider object:
LdapAuthenticationProvider ldapAuthenticationProvider = new LdapAuthenticationProvider(ldapAuthenticator, authoritiesPopulator);
It looks like I don't have control over which LdapAuthenticationProvider will be used at the end.
As a workaround, I could check the precondition in my UserDetailsContextMapper object and throw an exception if it is not met, but it is not optimal, since in this case the LDAP server will be queried even if it's not needed.
My question is, how can I force that my custom provider will be used, or is there any other "simple" way to achieve the behaviour I want?
I've configured the Spring Boot Security as per:
https://spring.io/guides/gs/securing-web/
I am able to login using my credentials perfectly. However, I need to add a checking that the AD user must also belong to a specific AD group (ie. AD-this-is-a-specific-group). On login, if the user does not belong to the specific AD group, then it should return a login error.
I've been searching for hours now and cannot seem to find a clear way to do this in the WebSecurityConfigurerAdapter , am I using the auth.groupSearchFilter correctly?
Here is my code:
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
Environment env;
public LdapContextSource contextSource () {
LdapContextSource contextSource= new LdapContextSource();
contextSource.setUrl(env.getRequiredProperty("ldap.url"));
contextSource.setBase(env.getRequiredProperty("ldap.baseDn"));
contextSource.setUserDn(env.getRequiredProperty("ldap.bindDn"));
contextSource.setPassword(env.getRequiredProperty("ldap.batchPassword"));
contextSource.afterPropertiesSet();
return contextSource;
}
#Override
protected void configure(AuthenticationManagerBuilder auth)
throws Exception {
auth.ldapAuthentication()
.userSearchFilter("(cn={0})")
.groupSearchBase("OU=Account Groups,OU=ITS Security")
.groupSearchFilter("(cn=AD-this-is-a-specific-group)")
.contextSource(contextSource());
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().fullyAuthenticated()
.and()
.formLogin();
}
I am sorry for beeing 5 years late for the party but I had the exact same problem with my very simple LDAP authentication implemented in Spring Boot.
I only wanted this:
- Is it the correct username?
- Is it the correct password?
- If yes, is the usr in group MYGROUP?
So my configure method now looks really small. I added the populator in a separate bean, just realize that I needed to add it in "auth.ldapAuthentication" so it would be called.
#Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.ldapAuthentication()
.userSearchFilter("uid={0}")
.ldapAuthoritiesPopulator(ldapAuthoritiesPopulator())
.groupSearchFilter("(member={0})")
.contextSource(contextSource());
}
#Bean
public LdapAuthoritiesPopulator ldapAuthoritiesPopulator() {
DefaultLdapAuthoritiesPopulator populi = new DefaultLdapAuthoritiesPopulator(contextSource(), "") {
#Override
public Set<GrantedAuthority> getGroupMembershipRoles(String userDn, String username) {
Set<GrantedAuthority> groupMembershipRoles = super.getGroupMembershipRoles(userDn, username);
boolean isMemberOfSpecificAdGroup = false;
for (GrantedAuthority grantedAuthority : groupMembershipRoles) {
if ("ROLE_MYGROUP".equals(grantedAuthority.toString())) {
isMemberOfSpecificAdGroup = true;
break;
}
}
if (!isMemberOfSpecificAdGroup) {
throw new BadCredentialsException("User must be a member of " + "ROLE_MYGROUP");
}
return groupMembershipRoles;
}
};
return populi;
}
#Bean
public DefaultSpringSecurityContextSource contextSource() {
return new DefaultSpringSecurityContextSource("ldap://blabla-some-url:389/dc=something,dc=something,dc=ch");
}
And by the way: The url did not work like mentioned in the Spring Boot guide it only worked like this, like everything in one line:
return new DefaultSpringSecurityContextSource("ldap://blabla-some-url:389/dc=something,dc=something,dc=ch");
And by the way for everyone following that guide: If you connect to an already existing LDAP server you don't need all those "spring.ldap.embedded" application properties.
So thank you alot for your help!
Not sure if this is the best way to do this (in terms of Spring Security's lifecycle), but basically I provided my own DefaultLdapAuthoritiesPopulator, where I only override the getGroupMembershipRoles.
First thing though, I have wrong auth.groupSearchFilter above, it should be:
.groupSearchFilter("(member={0})")
Second, I've created an anonymous class with overridden method (that calls the super and checks for a the membership in the list of roles):
auth
.ldapAuthentication()
.ldapAuthoritiesPopulator(new DefaultLdapAuthoritiesPopulator(contextSource, "OU=Account Groups,OU=ITS Security") {
#Override
public Set<GrantedAuthority> getGroupMembershipRoles(String userDn, String username) {
Set<GrantedAuthority> groupMembershipRoles = super.getGroupMembershipRoles(userDn, username);
boolean isMemberOfSpecificAdGroup = false;
for (GrantedAuthority grantedAuthority : groupMembershipRoles) {
if ("ROLE_AD-this-is-a-specific-group".equals(grantedAuthority.toString())) {
isMemberOfSpecificAdGroup = true;
break;
}
}
if (!isMemberOfSpecificAdGroup ) {
throw new BadCredentialsException("User must be a member of " + "AD-this-is-a-specific-group");
}
return groupMembershipRoles;
}
})
.userSearchFilter("(cn={0})")
.groupSearchBase("OU=Account Groups,OU=ITS Security")
.groupSearchFilter("(member={0})")
.contextSource(contextSource);
I'll put this here since I think it's the easier way without overriding any method.
In user search filter (i'll use yours) add the following if it corresponds to your LDAP structure
Original:
.userSearchFilter("(cn={0})")
Modified to search roles:
.userSearchFilter("(&(cn={0})(memberOf=CN=MYGROUP,OU=GROUP,DC=com,DC=company)")
This searches both the user and the membership
In my case I had to do this because I have 3 possible roles:
(&(cn={0})(|(group1)(group2)(group3)))
As you can see it searches user AND 1 OR more roles
Credit to this question's answer: Spring Security Ldap, log in only users in specified group