I'm integrating Active Directory authentication in an existing Spring web app.
At the moment user details are stored in a DB table and I implemented a custom UserDetailsService to authenticate users.
Following several tutorials I implemented Active Directory support via ActiveDirectoryLdapAuthenticationProvider in my application, and it basically works, but I need something more specific.
Instead of mapping AD groups to application roles, I need to map AD users to existing users in my web app. In other words I want to enable AD authentication only for users in my Users table.
A user can access via AD credentials ONLY IF is already registered in application DB.
Authorization info for each user are stored in DB. This way, each user can be authenticated both via DB user+password or via AD.
Is it possible to achieve this with Spring Security? and how?
NOTE I'm using Spring v 3.2.9 and Spring Security v 3.2.3
As a workaround I implementend a custom AuthenticationProvider and a custom UserDetailsContextMapper.
Becouse ActiveDirectoryLdapAuthenticationProvider is a final class I implemented the ADCustomAuthenticationProvider via composition, this way:
public class ADCustomAuthenticationProvider implements AuthenticationProvider {
#Autowired
private ActiveDirectoryLdapAuthenticationProvider adAuthProvider;
#Autowired
private UserDao uDao;
#Override
#Transactional(propagation = Propagation.REQUIRES_NEW)
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
String principal = authentication.getPrincipal().toString();
String username = principal.split("#")[0];
User utente = uDao.findByUsername(username);
if (utente == null) {
throw new ADUnregisteredUserAuthenticationException("user ["
+ principal + "] is not registered");
}
return adAuthProvider.authenticate(authentication);
}
#Override
public boolean supports(Class<?> authentication) {
return adAuthProvider.supports(authentication);
}
}
In the mapper I extended LdapUserDetailsMapper implementing only mapUserFromContext method.
public class ADCustomUserDetailsContextMapper extends LdapUserDetailsMapper {
#Autowired
private UserDetailsService userDetailsService; // ... the service used for DB authentication
#Override
public UserDetails mapUserFromContext(DirContextOperations ctx,
String username, Collection<? extends GrantedAuthority> authorities) {
return userDetailsService.loadUserByUsername(username);
}
}
(I'll probably need to implement mapUserToContext method beacuse I'm using a custom UserDetails implementation that not extends LdapUserDetails, so the reverse convertion process could throw an exception...)
NOTE This way I'm repeating the same query (to Users table) two times... I'd like to find a way to make a single query and share the result among AuthenticationProvider and UserDetailsContextMapper.e result among AuthenticationProvider and UserDetailsContextMapper.
If the user is not the user database, it should be assigned "no access" in the authorization process. So it doesn't really matter if he's authenticated or not, the application won't allow him to access because he's not authorized to the application.
Related
I use Spring Boot Data REST to map Hibernate repositories of entities to REST routes. I also use Spring Security's #PreAuthorize annotation to secure certain methods of those repositories. This works fine during production, but when I want to programmatically initialize the repositories with data (e.g. adding a first user to get started and some other sample data), it complains that "An Authentication object was not found in the SecurityContext". I realize that this is because there is no authenticated user in the security context at the time I configure the database (with an autowired component saving objects to repositories). However, I'd like to temporarily circumvent this authorization during configuration, so that I can initialize the database.
Is there any way to achieve this? And if not, how can I manually log in in the initialization method? I've seen some attempts to the second approach, but they all require access to an AuthenticationManager, which I don't know how to obtain.
You can use the following codes to create a fake user who has enough permission and set it to the SecurityContext just before running that protected method:
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
//Setup the permission that the user should have in order to run that method.
//It is the customized value based on your security configuration
grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_SUPER_ADMIN"));
//Ensure this user is enabled , active and has above permission
User user = new User("admin", "password", true, true, true, true, grantedAuthorities);
Authentication auth = new UsernamePasswordAuthenticationToken(user, user.getPassword(), user.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(auth);
After executing that protected method , reset the SecurityContext (which is equivalent to logout) :
SecurityContextHolder.clearContext();
An easy solution: Ignore security and focus on an empty repository
#Component
class InitializeDataIfNoDataPresent {
private final UserRepo repository;
public InitializeDataIfNoDataPresent(UserRepo repo) {
this.repository = repo;
}
#EventListener(ApplicationReadyEvent.class)
public void init() {
if(0 == repository.count()) {
setupData();
}
}
#PreAuthorize("hasRole('ADMIN')")
public void setupData() {
// your existing setup
}
}
When the application starts and your repository is empty, setupData() will be called even if there is no user present.
ref: stackoverflow-initialization-secured
Is it possible to use OAuth2 for certain endpoints in my rest application and use basic authentication too for some other endpoints.
It should all work on spring security version 2.0.1.RELEASE. I hope someone can help me further.
Yes, it's possible to use a basic authentication as well as an OAuth2 authentication intertwined, but I doubt you'll be able to set it up easily as HttpSecurity's authenticated() method doesn't allow you to pick which of your authentication method (oauth2Login/formLogin) will work.
However, there's a way to easily bypass that:
You could add a custom authority, let's call it ROLE_BASICAUTH, when an user connects using basic auth, and ROLE_OAUTH2 when an user connects using OAuth2. That way, you can use
.antMatchers("/endpoint-that-requires-basic-auth").hasRole("BASICAUTH")
.antMatchers("/endpoint-that-requires-oauth2").hasRole("OAUTH2")
.anyRequest().authenticated()
When they reach an endpoint that you want basic authentication (and not OAuth2), you check their current authorities, and if it's not BASICAUTH, then you invalidate their session you display a login form without OAuth2 (to force them to use the basic authentication).
The downside to doing that is that you'd need to implement both a custom UserDetailsService as well as a custom OAuth2UserService...
But that's actually not that hard:
#Service
public class UserService extends DefaultOAuth2UserService implements UserDetailsService {
// ...
#Override
public OAuth2User loadUser(OAuth2UserRequest oAuth2UserRequest) throws OAuth2AuthenticationException {
OAuth2User user = super.loadUser(oAuth2UserRequest);
Map<String, Object> attributes = user.getAttributes();
Set<GrantedAuthority> authoritySet = new HashSet<>(user.getAuthorities());
String userNameAttributeName = oAuth2UserRequest.getClientRegistration().getProviderDetails()
.getUserInfoEndpoint().getUserNameAttributeName();
authoritySet.add(new SimpleGrantedAuthority("ROLE_OAUTH2"));
return new DefaultOAuth2User(authoritySet, attributes, userNameAttributeName);
}
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserDetails user = getUserFromDatabase(username); // you'll need to provide that method (where are the username/password stored?)
if (user == null) { // UserDetailsService doesn't allow loadUserByUsername to return null, so throw exception
throw new UsernameNotFoundException("Couldn't find user with username '"+username+"'");
}
// add ROLE_BASICAUTH (you might need a custom UserDetails implementation here, because by defaut, UserDetails.getAuthorities() is immutable (I think, I might be a liar)
return user;
}
}
Note that this is a rough implementation, so you'll have to work it out a bit on your end as well.
You can also use this repository I made https://github.com/TwinProduction/spring-security-oauth2-client-example/tree/master/custom-userservice-sample as a guideline for the custom OAuth2UserService
Good luck.
How can I easily create spring user sessions based on information found in a mySQL database connected with JPA ?
As a simple example lets say I have 2 tables:
users
INT id
VARCHAR(30) username
VARCHAR(20) password
bookmark
INT id
VARCHAR(20) name
TEXT desctription
int user (FK)
Users will be able to view their bookmarks by requesting the following URL:
http://localhost:8080/bookmarks
In this manner I need separate user sessions (authentication not my main priority in this case) to be able to show bookmarks specific to the user.
Another way to go is by accessing bookmark information through:
http://localhost:8080/{userId}/bookmarks
In this case how can I prevent users from accessing another users' bookmark information ? (Such as preventing user id 1 from accessing user id 2s' bookmarks by using the url http://localhost:8080/2/bookmarks
If you would use Spring Security to secure your app, you can create custom UserDetailsService to read user data for each request from DB. Something like this:
#Component
public class CustomUserDetailsService implements UserDetailsService {
#Autowired
protected UserRepository userRepository;
#Override
public UserDetails loadUserByUsername(String email)
throws UsernameNotFoundException {
User user = userRepository.findByEmail(email);
if (user == null) {
throw new UsernameNotFoundException(String.format("User with email=%s was not found", email));
}
return user;
}
}
Of course assuming that you have JPA entity called User implementing UserDetails interface.
With this mechanism you can inject User instance into MVC controllers:
#GetMapping("/bookmarks")
public List<Bookmark> readBookmarks(Principal principal) {
User user = (User) principal;
// read bookmarks code
}
You can also read in anywhere in the app via:
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
User user = (User) principal;
REACTION ON COMMENT:
It is broad topic with a lot of decisions to make. e.g. you may consider using JWT, OAUTH2 or token based based authentication. If you are starting with Spring Security, I recommend to look at their Guides section. Especially Spting MVC guide is relevant for you.
I would like to ask for your help regarding my problem in Spring Security.
I have a requirement wherein I have to validate a login credential based on the option selected by the user. Option 1 would be validating the logged in user via third party service. Option 2 would be the normal validation using database authentication level. How can I implement this?
General Strategy
Provide a custom implementation of org.springframework.security.authentication.AuthenticationProvider that delegates authentication to the appropriate backend (third-party service, another AuthenticationProvider, etc.).
Pass the user-selected option to the custom AuthenticationProvider, enabling it to choose the correct authentication backend.
Configure the custom AuthenticationProvider as the default authentication provider.
Step 1: Implement AuthenticationProvider
AuthenticationProvider is an interface with a single method. Therefore, a custom implementation may look like:
class DelegatingAuthenticationProvider implements AuthenticationProvider {
#Autowired
private ThirdPartyAuthenticationService service;
#Autowired
#Qualifier("anotherAuthenticationProvider")
private AuthenticationProvider provider;
#Override
public Authentication authenticate(final Authentication authentication) throws AuthenticationException {
// Get the user selection.
String selection = (String) authentication.getDetails();
// Take action depending on the selection.
Authentication result;
if("ThirdParty".equals(selection)) {
// Authenticate using "service" and generate a new
// Authentication "result" appropriately.
}
else {
// Authenticate using "provider" and generate a new
// Authentication "result" appropriately.
}
return result;
}
}
Step 2: Pass the user selection to the AuthenticationProvider
The AuthenticationProvider implementation above picks up the user selection from the details property of the Authentication object. Presumably, the user selection would have to be picked up from the HttpServletRequest and added to the Authentication object before the AuthenticationProvider is invoked. This means, another component that has access to both the Authentication and HttpServletRequest objects needs to be invoked before the AuthenticationProvider is called.
The Authentication object is created by an implementation of AbstractAuthenticationProcessingFilter. This class has a method named attemptAuthentication that accepts an HttpServletRequest object and returns an Authentication object. So it seems this would be a good candidate for implementing what is needed. For username-password based authentication, the implementation class is UsernamePasswordAuthenticationFilter. This class returns a new instance of UsernamePasswordAuthenticationToken, which is an implementation of Authentication. So, a class extending UsernamePasswordAuthenticationFilter should be sufficient.
class ExtendedUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
...
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(username, password);
authentication.setDetails(obtainUserSelection(request));
...
return authentication;
}
}
obtainUserSelection is a private method that gets the user selection out of the request.
Step 3: Configuration
Configure the AuthenticationProvider and filter implementations in the Spring Security configuration. The exact steps will vary depending on whether XML or Java configuration is used.
I am trying to handle a situation when after an successful authentication with openId provider I discover that there is no account in my db associated with user openId identifier.
Can you tell me how should I handle the situation. Now, I am displaying register form and ask a user for creating an account. However, I have a problem with user authentication status, he is now being seen as authenticated by spring SecurityContext class.
How do I deauthenticate user in my controller action before redirecting to ''register new user page''? Is this approach a good one or should I do it in some other way?
Ok, so separating authentication from authorization as was mentioned in Samuel's post was really helpful. However there are still many gotchas and I found deauthentication still a must because there is no easy way in spring to add to user new roles. So the easiest way is to force user to login again and let spring handle role assignment during login.
In order to deauthenticate user in spring security you have to invoke:
SecurityContextHolder.clearContext();
as an alternative you can throw an exception in your UserDetailsService implementation (see below). It has the downside that you would deauthenticate user and lose user context data so it would be impossible to match new user accout with openid account during process of creating new local account. And you have to match those account after user login with traditional username and password. My solution was to deauthenticate user just after creating new account.
In order to grant user roles(privileges) you have to override UserDetailsService, in case someone find this useful here is my implementation:
public final class MyUserDetailsService implements UserDetailsService {
private final UsersDao usersDao;
#Autowired
public UserDetailsServiceImpl(final UsersDao usersDao) {
this.usersDao = usersDao;
}
#Override
public UserDetails loadUserByUsername(final String username) {
UserEntity user = usersDao.getUserByOpenIdIdentifier(username);
if (user == null) {
// there is no such user in our db, we could here throw
// an Exception instead then the user would also be deuthenticated
return new User(username, "", new ArrayList<GrantedAuthority>());
}
//here we are granting to users roles based on values from db
final Collection<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
authorities.add(new SimpleGrantedAuthority(user.getUserType().toString()));
final UserDetails result = new User(username, "", authorities);
return result;
}
}
I think that you might be mixing two concepts: authentication and authorization. Authentication is knowing who the user is, authorization is the right to use access a resource of a feature.
In spring security, this two concepts are implemented by the authentication-manager and the access-decision-manager.
The fact that a user does not exist in your database is not a reason to deny him is identity: no deauthentication! But beeing authenticated can be a criterion in the access decision management. Example: the AuthenticatedVoter.
You should not touch at the authentication, but customize the access-decision-manager to apply the following rules:
A user who exists in your database has access to everything except account creation feature
A user who doesn't exist in your database has access only to the account creation feature.
This is all about access management, not authentication.
Read more at http://static.springsource.org/spring-security/site/docs/3.0.x/reference/ns-config.html#ns-access-manager
PS: The documentation is not exhaustive in spring security, but the source code is very readable. My advice is to check it out and look at the implementation of the elements you need to customize.