Difference between OIDC and OAuth2 in spring oauth client - java

My goal is to authenticate an user through an authentication service (like google or github).
I tried to use both, and I don't understand why with github my authentication is handled with my OAuth2UserService, while with google, this is my OidcUserService which is called.
I expect that both call OidcUserService because it's only authentification that I need.
So, why there is such a difference ?
Can you enlighten me about that ?
Do I miss something ?
Some code to illustrate
#Service
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
#Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User user = super.loadUser(userRequest);
log.info("OAuth2User loading");
return user;
}
}
#Service
public class CustomOidcUserService extends OidcUserService {
#Override
public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException {
OidcUser user = super.loadUser(userRequest);
log.info("OidcUser loading");
return user;
}
}
// MyAppSecurityConfig.java
#Configuration
public class MyAppSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.oauth2Login()
;
}
}
# application.properties
spring.security.oauth2.client.registration.github.client-id=xxxx
spring.security.oauth2.client.registration.github.client-secret=xxx
spring.security.oauth2.client.registration.google.client-id=xxxx
spring.security.oauth2.client.registration.google.client-secret=xxxx

The behaviour you are observing caused by predefined oauth2 configurations in spring-boot:
For common OAuth2 and OpenID providers, including Google, Github,
Facebook, and Okta, we provide a set of provider defaults (google,
github, facebook, and okta, respectively).
If you do not need to customize these providers, you can set the
provider attribute to the one for which you need to infer defaults.
Also, if the key for the client registration matches a default
supported provider, Spring Boot infers that as well.
i.e. spring boot has preconfigured openid-connect for google services and generic oauth2 for github.

Related

How to allow endpoint to be called if request has specific header token using spring security

I'm using spring boot to build a web application, sometimes I ddont want to spend to much time on permissioning and rule management I just want to be able to create a random string token and allow other services to use my service if they know this token...
all know all the danger and problems related to this approach but for prototyping integration this is what i need...
the question is how can i configure spring security to validate a specific token for a specific endpoint [note that i want different tokens for different endpoints, if the thing was only 1 single token for the whole application se solution would be a simple filter]
EXAMPLE:
protected void configure(HttpSecurity http) throws Exception {
http.cors().and()
.csrf().disable()
.authorizeRequests()
.antMatchers(AUTH_WHITELIST).permitAll()
.antMatchers(HttpMethod.POST, "/boards/").access("SOMETHING LIKE: httpRequest->headers->get('tokenHeader') == 'mytoken'")
.anyRequest().authenticated()
//......
}
I would like to achieve something like this....
The only way i'm able to do what i want now is by doing:
#Value("${controller.plan.authorization}")
private String AUTHORIZATION_TOKEN;
#GetMapping
public List<Plan> getPlans(#RequestHeader(HttpHeaders.AUTHORIZATION) authToken: String) {
if (AUTHORIZATION_TOKEN.equals(authToken))
return planService.findAll()
else
throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "invalid authorization_token")
}
which is quite out of spring pattern and really annoying in my opinion
is there any way to achieve that using spring security?
So you have some common code which needs to be executed in the same way in multiple controllers and endpoints.
This makes interceptor a suitable solution.
Try the following
#Configuration
public class WebSecurityConfig implements WebMvcConfigurer
{
#Autowired
private MyInterceptor myInterceptor;
#Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(myInterceptor).addPathPatterns("*/boards/*");
}
}
Then the functionality you have described should be something close to the following
#Component
public class MyInterceptor implements HandlerInterceptor {
#Value("${controller.plan.authorization}")
private String AUTHORIZATION_TOKEN;
#Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
final String requestTokenHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
if (AUTHORIZATION_TOKEN.equals(requestTokenHeader)) {
//continue execution in spring flow to reach the relative controller
return true;
} else {
//break flow and return response.
response.getWriter().write("invalid authorization_token");
response.setStatus(HttpStatus.UNAUTHORIZED.value());
return false;
}
}
}
Now your controller can break free of that boilerplate code.

Spring boot WebSecurityConfigurerAdapter basic authentication user password validation issue

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

How to extend Spring oidcUser to use customUserDetails

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.

Spring security not allowing access with simple matcher and permitAll

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

Spring Security LDAP and Remember Me

I'm building an app with Spring Boot that has integration with LDAP. I was able to connect successfully to LDAP server and authenticate user. Now I have a requirement to add remember-me functionality. I tried to look through different posts (this) but was not able to find an answer to my problem. Official Spring Security document states that
If you are using an authentication provider which doesn't use a
UserDetailsService (for example, the LDAP provider) then it won't work
unless you also have a UserDetailsService bean in your application
context
Here the my working code with some initial thoughts to add remember-me functionality:
WebSecurityConfig
import com.ui.security.CustomUserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.event.LoggerListener;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider;
import org.springframework.security.ldap.userdetails.UserDetailsContextMapper;
import org.springframework.security.web.authentication.RememberMeServices;
import org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices;
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
String DOMAIN = "ldap-server.com";
String URL = "ldap://ds.ldap-server.com:389";
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/ui/**").authenticated()
.antMatchers("/", "/home", "/UIDL/**", "/ui/**").permitAll()
.anyRequest().authenticated()
;
http
.formLogin()
.loginPage("/login").failureUrl("/login?error=true").permitAll()
.and().logout().permitAll()
;
// Not sure how to implement this
http.rememberMe().rememberMeServices(rememberMeServices()).key("password");
}
#Override
protected void configure(AuthenticationManagerBuilder authManagerBuilder) throws Exception {
authManagerBuilder
.authenticationProvider(activeDirectoryLdapAuthenticationProvider())
.userDetailsService(userDetailsService())
;
}
#Bean
public ActiveDirectoryLdapAuthenticationProvider activeDirectoryLdapAuthenticationProvider() {
ActiveDirectoryLdapAuthenticationProvider provider = new ActiveDirectoryLdapAuthenticationProvider(DOMAIN, URL);
provider.setConvertSubErrorCodesToExceptions(true);
provider.setUseAuthenticationRequestCredentials(true);
provider.setUserDetailsContextMapper(userDetailsContextMapper());
return provider;
}
#Bean
public UserDetailsContextMapper userDetailsContextMapper() {
UserDetailsContextMapper contextMapper = new CustomUserDetailsServiceImpl();
return contextMapper;
}
/**
* Impl of remember me service
* #return
*/
#Bean
public RememberMeServices rememberMeServices() {
// TokenBasedRememberMeServices rememberMeServices = new TokenBasedRememberMeServices("password", userService);
// rememberMeServices.setCookieName("cookieName");
// rememberMeServices.setParameter("rememberMe");
return rememberMeServices;
}
#Bean
public LoggerListener loggerListener() {
return new LoggerListener();
}
}
CustomUserDetailsServiceImpl
public class CustomUserDetailsServiceImpl implements UserDetailsContextMapper {
#Autowired
SecurityHelper securityHelper;
Log ___log = LogFactory.getLog(this.getClass());
#Override
public LoggedInUserDetails mapUserFromContext(DirContextOperations ctx, String username, Collection<? extends GrantedAuthority> grantedAuthorities) {
LoggedInUserDetails userDetails = null;
try {
userDetails = securityHelper.authenticateUser(ctx, username, grantedAuthorities);
} catch (NamingException e) {
e.printStackTrace();
}
return userDetails;
}
#Override
public void mapUserToContext(UserDetails user, DirContextAdapter ctx) {
}
}
I know that I need to implement UserService somehow, but not sure how that can be achieved.
There are two issues to configuration of the RememberMe features with LDAP:
selection of the correct RememberMe implementation (Tokens vs. PersistentTokens)
its configuration using Spring's Java Configuration
I'll take these step by step.
The Token-based remember me feature (TokenBasedRememberMeServices) works in the following way during authentication:
user gets authenticated (agaisnt AD) and we currently know user's ID and password
we construct value username + expirationTime + password + staticKey and create an MD5 hash of it
we create a cookie which contains username + expiration + the calculated hash
When user wants to come back to the service and be authenticated using the remember me functionality we:
check whether the cookie exists and isn't expired
populate the user ID from the cookie and call the provided UserDetailsService which is expected to return information related to the user's ID, including the password
we then calculate the hash from the returned data and verify that the hash in the cookie matches with the value we calculated
if it matches we return the user's Authentication object
The hash checking process is required in order to make sure that nobody can create a "fake" remember me cookie, which would let them impersonate another user. The problem is that this process relies on possibility of loading password from our repository - but this is impossible with Active Directory - we cannot load plaintext password based on username.
This makes the Token-based implementation unsuitable for usage with AD (unless we start creating some local user store which contains the password or some other secret user-based credential and I'm not suggesting this approach as I don't know other details of your application, although it might be a good way to go).
The other remember me implementation is based on persistent tokens (PersistentTokenBasedRememberMeServices) and it works like this (in a bit simplified way):
when user authenticates we generate a random token
we store the token in storage together with information about user's ID associated with it
we create a cookie which includes the token ID
When user wants to authenticate we:
check whether we have the cookie with token ID available
verify whether the token ID exists in database
load user's data based on information in the database
As you can see, the password is no longer required, although we now need a token storage (typically database, we can use in-memory for testing) which is used instead of the password verification.
And that gets us to the configuration part. The basic configuration for persistent-token-based remember me looks like this:
#Override
protected void configure(HttpSecurity http) throws Exception {
....
String internalSecretKey = "internalSecretKey";
http.rememberMe().rememberMeServices(rememberMeServices(internalSecretKey)).key(internalSecretKey);
}
#Bean
public RememberMeServices rememberMeServices(String internalSecretKey) {
BasicRememberMeUserDetailsService rememberMeUserDetailsService = new BasicRememberMeUserDetailsService();
InMemoryTokenRepositoryImpl rememberMeTokenRepository = new InMemoryTokenRepositoryImpl();
PersistentTokenBasedRememberMeServices services = new PersistentTokenBasedRememberMeServices(staticKey, rememberMeUserDetailsService, rememberMeTokenRepository);
services.setAlwaysRemember(true);
return services;
}
This implementation will use in-memory token storage which should be replaced with JdbcTokenRepositoryImpl for production. The provided UserDetailsService is responsible for loading of additional data for the user identified by the user ID loaded from the remember me cookie. The simpliest implementation can look like this:
public class BasicRememberMeUserDetailsService implements UserDetailsService {
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return new User(username, "", Collections.<GrantedAuthority>emptyList());
}
}
You could also supply another UserDetailsService implementation which loads additional attributes or group memberships from your AD or internal database, depending on your needs. It could look like this:
#Bean
public RememberMeServices rememberMeServices(String internalSecretKey) {
LdapContextSource ldapContext = getLdapContext();
String searchBase = "OU=Users,DC=test,DC=company,DC=com";
String searchFilter = "(&(objectClass=user)(sAMAccountName={0}))";
FilterBasedLdapUserSearch search = new FilterBasedLdapUserSearch(searchBase, searchFilter, ldapContext);
search.setSearchSubtree(true);
LdapUserDetailsService rememberMeUserDetailsService = new LdapUserDetailsService(search);
rememberMeUserDetailsService.setUserDetailsMapper(new CustomUserDetailsServiceImpl());
InMemoryTokenRepositoryImpl rememberMeTokenRepository = new InMemoryTokenRepositoryImpl();
PersistentTokenBasedRememberMeServices services = new PersistentTokenBasedRememberMeServices(internalSecretKey, rememberMeUserDetailsService, rememberMeTokenRepository);
services.setAlwaysRemember(true);
return services;
}
#Bean
public LdapContextSource getLdapContext() {
LdapContextSource source = new LdapContextSource();
source.setUserDn("user#"+DOMAIN);
source.setPassword("password");
source.setUrl(URL);
return source;
}
This will get you remember me functionality which works with LDAP and provides the loaded data inside RememberMeAuthenticationToken which will be available in the SecurityContextHolder.getContext().getAuthentication(). It will also be able to re-use your existing logic for parsing of LDAP data into an User object (CustomUserDetailsServiceImpl).
As a separate subject, there's also one problem with the code posted in the question, you should replace the:
authManagerBuilder
.authenticationProvider(activeDirectoryLdapAuthenticationProvider())
.userDetailsService(userDetailsService())
;
with:
authManagerBuilder
.authenticationProvider(activeDirectoryLdapAuthenticationProvider())
;
The call to userDetailsService should only be made in order to add DAO-based authentication (e.g. against database) and should be called with a real implementation of the user details service. Your current configuration can lead to infinite loops.
It sounds like you are missing an instance of UserService that your RememberMeService needs a reference to. Since you are using LDAP, you'd need an LDAP version of UserService. I'm only familiar with JDBC/JPA implementations, but looks like org.springframework.security.ldap.userdetails.LdapUserDetailsManager is what you are looking for. Then your config would look something like this:
#Bean
public UserDetailsService getUserDetailsService() {
return new LdapUserDetailsManager(); // TODO give it whatever constructor params it needs
}
#Bean
public RememberMeServices rememberMeServices() {
TokenBasedRememberMeServices rememberMeServices = new TokenBasedRememberMeServices("password", getUserDetailsService());
rememberMeServices.setCookieName("cookieName");
rememberMeServices.setParameter("rememberMe");
return rememberMeServices;
}

Categories