Imagine the following (hypothetical) data structure
endpoint | username | password
users admin 123
info george awd
data magnus e4
this means that every endpoint requires different credentials and no one username/password combo can log in to every endpoint. I am looking for a way to make this scalable in our Spring MVC project when adding more endpoints. We could use roles and hardcore this into the config class but the endpoints and login combinations vary for every customer installation
Given the following SecurityConfiguration with LookupAuthenticationService being the class that looks up the username/password data in the database
#EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private static final String[] ENDPOINT_LIST = {
"/rest/**"
};
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers(ENDPOINT_LIST)
.authenticated()
.and()
.httpBasic();
}
#Autowired
protected void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider());
}
#Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
authenticationProvider.setUserDetailsService(userDetailsService());
authenticationProvider.setPasswordEncoder(passwordEncoder());
return authenticationProvider;
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Override
protected UserDetailsService userDetailsService() {
return new LookupAuthenticationService(passwordEncoder());
}
}
The ideal situation would be if LookupAuthenticationService has access to the request so we know which endpoint to fetch but I guess this is only possible when working with individual Filters
The possibilities I've found so far are:
Add a WebSecurityConfigurerAdapter and multiple UserDetailsServer specific per endpoint -> lots of code
Add a HandlerInterceptor per endpoint -> lots of code
AuthenticationManagerResolver returning a different AuthenticationManager based on pathInfo?
Any input how to best resolve this issue would be appreciated
You can have a table where you map endpoints to rules, like so:
pattern
authority
/users/**
ROLE_ADMIN
/info/**
ROLE_USER
/another/**
ROLE_ANOTHER
And instead of assigning a user to an endpoint, you assign a role to the users. With this in place, you can create an AuthorizationManager which is going to protect your endpoints based on the request path.
#Component
public class AccessRuleAuthorizationManager implements AuthorizationManager<RequestAuthorizationContext> {
private final AccessRuleRepository rules;
private RequestMatcherDelegatingAuthorizationManager delegate;
public AccessRuleAuthorizationManager(AccessRuleRepository rules) {
this.rules = rules;
}
#Override
public AuthorizationDecision check(Supplier<Authentication> authentication, RequestAuthorizationContext object) {
return this.delegate.check(authentication, object.getRequest());
}
#EventListener
void applyRules(ApplicationReadyEvent event) {
Builder builder = builder();
for (AccessRule rule : this.rules.findAll()) {
builder.add(
new AntPathRequestMatcher(rule.getPattern()),
AuthorityAuthorizationManager.hasAuthority(rule.getAuthority())
);
}
this.delegate = builder.build();
}
}
And, in your SecurityConfiguration you simply do this:
#Autowired
private AccessRuleAuthorizationManager access;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authz) ->
authz.anyRequest().access(this.access)
)
.httpBasic(Customizer.withDefaults());
}
I recommend you to take a look at this repository and watch the presentation from the repository's description. The last steps of the presentation was adding the custom AuthorizationManager, and there's a great explanation about it.
Related
What I have currently
I'm currently implementing an OIDC Resource Provider for my company. They use their intern OIDC servers, which I managed to work with by following this example: https://github.com/jgrandja/oauth2login-demo/tree/linkedin
I'm now able to retrieve user information from the Authorization Server, like that:
#RestController
#RequestMapping("/some/route")
public class SomeController {
#GetMapping("/some/route")
public ResponseEntity<?> getSomething(#RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient authorizedClient) {
String userInfoEndpointUri = authorizedClient.getClientRegistration()
.getProviderDetails().getUserInfoEndpoint().getUri();
Map userAttributes = this.webClient
.get()
.uri(userInfoEndpointUri)
.attributes(oauth2AuthorizedClient(authorizedClient))
.retrieve()
.bodyToMono(Map.class)
.block();
String firstName = (String) userAttributes.get("first_name");
String lastName = (String) userAttributes.get("last_name");
...
}
}
What I'd like
I am now searching for a solution to map the userAttributes to an Object prior to
the controller method call, so that I get e.g.:
#GetMapping("/some/route")
public ResponseEntity<?> getSomething(MyCostumUserBean user) {
String firstName = user.getFirstName();
String lastName = user.getLastName();
...
}
I read something about the ChannelInterceptor and HandlerInterceptor and also the PrincipalExtractor and AuthoritiesExtractor.
The problem is, that I am just learning the Spring framework and these possibilities are overwhelming me.
It would be a plus if that method would allow some validation and would immediately respond with Error codes if the validation fails.
After that is achieved, I would like to add additional information to MyCostumUserBean from another server, which I send the identity of the current session's user to and receive e.g. Role/Permissions of that user.
I tried to put it in a picture:
Question
What is the proper / by the Spring Framework intended way to deal with that? How do I achieve that?
Extra: Is it secure to rely on OAuth2AuthorizedClient.getPrincipalName()? Or can that be faked by an user, by faking the Cookie/Token?
I think you are asking the way to configure the success handler, or a filter which can check the user attributes.
If this is what you are asking, There are many ways to do it.
For examples:
Use User scope check:(need to assign the scope to the user in advance.)
#ResponseBody
#GetMapping("/some/route")
public String getSomeThing(#RegisteredOAuth2AuthorizedClient("custom") OAuth2AuthorizedClient authorizedClient) {
Set<String> scopes = authorizedClient.getAccessToken()
.getScopes();
if (scopes.contains("users:read")) {
} else if (scopes.contains("users:read")) {
return " page 1";
} else {
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Forbidden.");
}
}
You can put some logic in the successHandler:
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/**")
.permitAll().and()
.formLogin()
.successHandler(successHandler());
}
#Bean
public CustomSuccessHandler successHandler() {
return new CustomSuccessHandler();
}
If you want to apply a filter for your Security Chians:
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
...
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
...
.and()
.addFilterBefore(getBeforeAuthenticationFilter(), CustomBeforeAuthenticationFilter.class)
.formLogin()
.loginPage()
.permitAll()
...
}
public UsernamePasswordAuthenticationFilter getBeforeAuthenticationFilter() throws Exception {
CustomBeforeAuthenticationFilter filter = new CustomBeforeAuthenticationFilter();
....
return filter;
}
}
You can also achieve the same purpose by using a Customizing Filter Chains, by give the different order and the relative login in it.
#Configuration
#Order(SecurityProperties.BASIC_AUTH_ORDER - 10)
public class ApplicationConfigurerAdapterForUserGroup1 extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
...;
}
}
#Configuration
#Order(SecurityProperties.BASIC_AUTH_ORDER - 10)
public class ApplicationConfigurerAdapterForUserGroup2 extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
...;
}
}
I'm trying to implement an authentication and authorization service for an ongoing spring-boot project. I have implemented a JPA based authentication provider and it is working fine. How do I add LDAP authentication provider to the same project and switch between the authentication methods depending on the user authentication type?
Below is my code
#Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {
#Autowired
private UsernamePasswordAuthProvider authProvider;
#Autowired
private LdapAuth ldapAuth;
#Autowired
private LdapAuthenticationpopulator ldapAuthenticationpopulator;
private String ldapUrls = "ldap://localhost:3890";
private String ldapSecurityPrincipal = "cn=admin,dc=mycompany,dc=com";
private String ldapPrincipalPassword = "admin";
private String userDnPattern = "uid={0}";
#Autowired
private UsernamePasswordAuthFilter usernamePasswordAuthFilter;
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Order(1)
protected void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.ldapAuthentication()
.contextSource()
.url(ldapUrls)
.managerDn(ldapSecurityPrincipal)
.managerPassword(ldapPrincipalPassword)
.and()
.userDnPatterns(userDnPattern)
.ldapAuthoritiesPopulator(ldapAuthenticationpopulator);
}
#Override
#Order(2)
protected void configure(AuthenticationManagerBuilder auth) {
auth.authenticationProvider(authProvider);
}
#Override
protected void configure(HttpSecurity http) {
http.addFilterAt(usernamePasswordAuthFilter,
BasicAuthenticationFilter.class);
}
#Override
#Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
Though my LDAP credentials are correct it is not reaching that method.
How do I get the authentication method from DB ex(LDAP, JPA, SSO) for my application and execute the corresponding auth provider method?
I have gone through multiple documents for MultipleAuthenticationProviders but I couldn't find much clarity
Please let me know if there is any possible solution for this.
Thanks in advance
I found one solution to this. I have put up LDAP enabled or JPA auth enabled properties in DB and loading them at run time depending upon the boolean value I'm calling the particular auth method.
I am quite new to the world of Java Spring Boot and I am a bit confused on one of the projects I'm working on. This project requires the use of two authentication methods, and I need to use x509 for some requests only.
Basically, the application right now uses authentication using tokens, but let's say I am using basic authentication to simplify the process.
So I set up a basic test app, that uses basic authentication. I have a simple controller that has two methods.
#GetMapping("/test")
private String test() {
return "we use basic auth only";
}
#GetMapping("/certiTest")
private String testCert() {
return "we use certificate auth on this method";
}
I want the first method to require basic auth only, and the second to require the user to have the right certificate as well to access the method.
The problem is that I'm having trouble using one or the other without having them at the same time. I have tried a few things I read online, right now my WebSecurityConfig class looks like this
#Configuration
#EnableWebSecurity
public class WebSecurityConfig{
public static class BasicAuthConfig extends WebSecurityConfigurerAdapter{
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.csrf().disable()
.authorizeRequests().anyRequest().authenticated()
.and().httpBasic();
}
#Autowired
public void configureGlobal(AuthenticationManagerBuilder authentication)
throws Exception
{
authentication.inMemoryAuthentication()
.withUser("user")
.password(passwordEncoder().encode("user"))
.authorities("ROLE_USER");
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
#Configuration
#Order(2)
public static class X509SecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/user/certiTest").authorizeRequests()
.anyRequest().authenticated().and()
.x509()
.subjectPrincipalRegex("CN=(.*?)(?:,|$)")
.userDetailsService(userDetailsService());
http.headers().httpStrictTransportSecurity()
.maxAgeInSeconds(0)
.includeSubDomains(true);
}
#Bean
public UserDetailsService userDetailsService() {
return (UserDetailsService) username -> {
if (username.equals("server")) {
return new User(username, "",
AuthorityUtils
.commaSeparatedStringToAuthorityList("ROLE_USER"));
} else {
throw new UsernameNotFoundException(String.format("User %s not found", username));
}
};
}
}
}
The thing is right now both are setup, so I require both the certificate and the login for it to work. I believe I have the right configuration in the application.yml, I have tried to play with client-auth: want or need, without success.
I'm pretty sure there is a way simpler way to handle this. If someone has a solution, I would be glad.
Maintain separate class for cofiguration
I have an application that authenticates the login against the Microsoft Active Directory, but now I need that if the user is not in the AD of the organization, try to authenticate against an OpenLDAP directory, is it possible with spring-boot in a single application?
How can I indicate in the configuration class that there are two providers to authenticate? Or, do I have to use a handler or similar to perform double authentication?
My code is similar to the following with its own filters and some changes, but the scheme is similar.
Code source: https://medium.com/#dmarko484/spring-boot-active-directory-authentication-5ea04969f220
#Configuration
#EnableWebSecurity
public class WebSecurityConfigAD extends WebSecurityConfigurerAdapter {
#Value("${ad.domain}")
private String AD_DOMAIN;
#Value("${ad.url}")
private String AD_URL;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated().and().httpBasic();
}
#Override
protected void configure(AuthenticationManagerBuilder authManagerBuilder) throws Exception {
authManagerBuilder.authenticationProvider(activeDirectoryLdapAuthenticationProvider()).userDetailsService(userDetailsService());
}
#Bean
public AuthenticationManager authenticationManager() {
return new ProviderManager(Arrays.asList(activeDirectoryLdapAuthenticationProvider()));
}
#Bean
public AuthenticationProvider activeDirectoryLdapAuthenticationProvider() {
ActiveDirectoryLdapAuthenticationProvider provider = new ActiveDirectoryLdapAuthenticationProvider(AD_DOMAIN, AD_URL);
provider.setConvertSubErrorCodesToExceptions(true);
provider.setUseAuthenticationRequestCredentials(true);
return provider;
}
}
I am also doing the same in my application:
Authenticate against the LDAP
Authenticate against the database.
First of all LDAP server is already set up.
So in application.properties file we have all the information required to connect to the LDAP server and a Boolean field to check whether we want to authenticate against LDAP server or not.
project.ldap.server.protocol=
project.ldap.server.host.name=
project.ldap.server.ip=
project.ldap.server.port=
project.ldap.service.url=
project.ldap.authentication=(false/true)
A service which will perform the authentication against the LDAP server
#Service
public class LDAPAuthenticationConnectorService {
#Value("${project.ldap.server.protocol}")
private String LDAP_SERVER_PROTOCOL;
#Value("${project.ldap.server.ip}")
private String LDAP_SERVER_IP;
#Value("${project.ldap.server.port}")
private int LDAP_SERVER_PORT;
#Value("${project.ldap.service.url}")
private String LDAP_SERVICE_URL;
/**
*
* #param loginDto
* #throws ADAuthenticationException
*/
public String authenticate(LoginDto loginDto) throws ADAuthenticationException{//logic }
Now in FacadeImplementation you can do as following:
public class LoginFacadeImpl implements LoginFacadeInt {
#Value("${project.ldap.authentication}")
private Boolean isProjectLDAPAuthenticationEnabled;
#Override
public UserDto authenticateUser(String userName, String password) {
try {
authenticateViaLDAP(userName, password);
} catch (Exception e1) {
//do authetication against database
UserEntity userEntityForAuthentication =
UserManagementDaoInt.authenticateUser(userName,
AuthenticationUtils.convertPasswordToSha256(password));
}
Hope this helps:)
Let me know:)
I found the solution.
Spring Security supports a wide range of authentication mechanisms. AuthenticationManagerBuilder object allows using multiple built-in authentication provider like In-Memory authentication, LDAP authentication, JDBC based authentication. In addition to its own set of authentication models, Spring Security allows to write your custom authentication mechanism to authenticate, for example, against a secure RESTful or SOAP remote API authentication service.
Link: https://www.baeldung.com/spring-security-multiple-auth-providers
The following example shows how to put two authentication providers, one in a row from another. One: in memory, and other: customAuthentcationProvider.
auth.authenticationProvider(customAuthProvider);
auth.inMemoryAuthentication()
Example:
#EnableWebSecurity
public class MultipleAuthProvidersSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
CustomAuthenticationProvider customAuthProvider;
#Override
public void configure(AuthenticationManagerBuilder auth)
throws Exception {
auth.authenticationProvider(customAuthProvider);
auth.inMemoryAuthentication()
.withUser("memuser")
.password(encoder().encode("pass"))
.roles("USER");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.httpBasic()
.and()
.authorizeRequests()
.antMatchers("/api/**")
.authenticated();
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
I'm trying to use authentication by google. I am using springboot2, so most of the configuration is automatic. The authentication itself works good, but afterwards I would like to populate Principal with my own data (roles, username, and stuff).
I've created MyUserService that exteds DefaultOauth2UserService, and I am trying to use it as follows:
#Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
MyUserService myUserService;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.oauth2Login()
.userInfoEndpoint()
.userService(myUserService);
}
}
I've checked with debuger, that application never actually uses loadUser methods. And here is implementation of MyUserService:
#Component
public class MyUserService extends DefaultOAuth2UserService {
#Autowired
UserRepository userRepository;
public MyUserService(){
LoggerFactory.getLogger(MyUserService.class).info("initializing user service");
}
#Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User oAuth2User = super.loadUser(userRequest);
Map<String, Object> attributes = oAuth2User.getAttributes();
String emailFromGoogle = (String) attributes.get("email");
User user = userRepository.findByEmail(emailFromGoogle);
attributes.put("given_name", user.getFirstName());
attributes.put("family_name", user.getLastName());
Set<GrantedAuthority> authoritySet = new HashSet<>(oAuth2User.getAuthorities());
return new DefaultOAuth2User(authoritySet, attributes, "sub");
}
}
Actually the solution was just to add another property for google authentication:
spring.security.oauth2.client.registration.google.scope=profile email
Not sure, what is the default scope, and why entrance to the service is dependent on scope, but without this line the code never reached my custom service.
I think you're missing the #EnableOAuth2Client annotation at the top of your SecurityConfig class.
Regardless, I made an examplewith a Custom user service for oauth2 here https://github.com/TwinProduction/spring-security-oauth2-client-example/ if it helps