How to convert LDAP AuthenticationManagerBuilder to AuthenticationManager - spring

I'm using the following LDAP configuration that makes use of GlobalAuthenticationConfigurerAdapter:
public static class AuthenticationConfiguration extends GlobalAuthenticationConfigurerAdapter {
#Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
for (String ldapUrl : ldapUrls) { //I have multiple LDAP servers that must be included
auth.ldapAuthentication()
.userSearchFilter("...")
.contextSource()
.url(ldapUrl + ldapBase)
.managerDn(ldapUsername)
.managerPassword(ldapPassword);
}
}
}
}
As per the following docs, there is a new way of providing LDAP authentication beans:
https://spring.io/blog/2022/02/21/spring-security-without-the-websecurityconfigureradapter#ldap-authentication > LDAP Authentication
#Bean
AuthenticationManager ldapAuthenticationManager(
BaseLdapPathContextSource contextSource) {
LdapBindAuthenticationManagerFactory factory =
new LdapBindAuthenticationManagerFactory(contextSource);
factory.setUserDnPatterns("uid={0},ou=people");
factory.setUserDetailsContextMapper(new PersonContextMapper());
return factory.createAuthenticationManager();
}
Question: how can I now transform my old configuration (that includes multiple LDAP urls) into the new beans?
I mean, where can I get the BaseLdapPathContextSource from, and feed it with my ldap login credentials and base urls?

As AuthenticationManager only has a single instance per SecurityFilterChain i would think you have to create a set of AuthenticationProvider instead. and add those to you SecurityFilterChain.
Below code is a mock as i'm not sure if LdapContextSource needs more properties to be set and if you want base UserDetailsServiceLdapAuthoritiesPopulator or something custom.
#Bean
public SecurityFilterChain filterChain(HttpSecurity http, List<LdapAuthenticator> ldapAuthenticators) throws Exception
{
for(LdapAuthenticator authenticator : ldapAuthenticators)
{
LdapAuthoritiesPopulator ldapAuthoritiesPopulator = new UserDetailsServiceLdapAuthoritiesPopulator(userDetails);
http.authenticationProvider(new LdapAuthenticationProvider(authenticator, ldapAuthoritiesPopulator));
}
}
#Bean
public List<BindAuthenticator> ldapAuthenticator()
{
List<BindAuthenticator> authenticators = new ArrayList<>();
for (String ldapUrl : ldapUrls)
{
LdapContextSource contextSource = new LdapContextSource();
contextSource.setUrl(ldapUrl);
BindAuthenticator authenticator = new BindAuthenticator(contextSource);
authenticator.setUserSearch(new FilterBasedLdapUserSearch("ou=people", "(uid={0})", ldapContextSource));
authenticators.add(authenticator);
}
return authenticators;
}

Related

Spring Boot + Keycloak via SAML - Invalid reqeuests

I've been trying to set up a Keycloak locally with docker to be able to login to our application with SAML 2.0.
Versions used:
Keyloak 19.0.3
Spring Boot 2.7.3
When I call an REST endpoint of the application, I am correctly redirected to Keycloak (redirect to http://localhost:8085/realms/PocRealm/protocol/saml). But I don't get a login form but the message: "We are sorry ... Invalid Request" .
I then see the following entry in the Docker console logs.
2022-10-07 12:22:41,972 WARN [org.keycloak.events] (executor-thread-104) type=LOGIN_ERROR, realmId=e026f301-c74b-4247-bb0a-58cdb651ae00, clientId=null, userId=null, ipAddress=172.17.0.1, error=client_not_found, reason=Cannot_match_source_hash
These are my configurations:
application.properties
# Spring Server Settings
server.port=8081
#Keycloak Settings
keycloak.auth-server-url=http://localhost:8085
keycloak.realm=PocRealm
keycloak.resource=pocApp
keycloak.principal-attribute=preferred_username
#SAML Settings
spring.security.saml2.relyingparty.registration.keycloak.signing.credentials[0].private-key-location=classpath:credentials/myKey.key
spring.security.saml2.relyingparty.registration.keycloak.signing.credentials[0].certificate-location=classpath:credentials/myCert.crt
spring.security.saml2.relyingparty.registration.keycloak.assertingparty.metadata-uri=http://localhost:8085/realms/PocRealm/protocol/saml/descriptor
KeycloakConfig.java
#Configuration
public class KeycloakConfig {
#Bean
public KeycloakSpringBootConfigResolver keycloakConfigResolver() {
return new KeycloakSpringBootConfigResolver();
}
}
SecurityConfig.java
#KeycloakConfiguration
public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter
{
#Autowired
public void configureGlobal(AuthenticationManagerBuilder authenticationManagerBuilder)
{
SimpleAuthorityMapper simpleAuthorityMapper = new SimpleAuthorityMapper();
simpleAuthorityMapper.setPrefix("ROLE_");
KeycloakAuthenticationProvider keycloakAuthenticationProvider =
keycloakAuthenticationProvider();
keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(simpleAuthorityMapper);
authenticationManagerBuilder.authenticationProvider(keycloakAuthenticationProvider);
}
#Bean
#Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy ()
{
return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
}
#Bean
#Override
#ConditionalOnMissingBean(HttpSessionManager.class)
protected HttpSessionManager httpSessionManager()
{
return new HttpSessionManager();
}
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception
{
OpenSaml4AuthenticationProvider authenticationProvider = new OpenSaml4AuthenticationProvider();
authenticationProvider.setResponseAuthenticationConverter(groupsConverter());
// #formatter:off
httpSecurity
.authorizeHttpRequests(authorize -> authorize
.mvcMatchers("/favicon.ico").permitAll()
.anyRequest().authenticated()
)
.saml2Login(saml2 -> saml2
.authenticationManager(new ProviderManager(authenticationProvider))
)
.saml2Logout(withDefaults());
// #formatter:on
}
private Converter<OpenSaml4AuthenticationProvider.ResponseToken, Saml2Authentication> groupsConverter() {
Converter<ResponseToken, Saml2Authentication> delegate =
OpenSaml4AuthenticationProvider.createDefaultResponseAuthenticationConverter();
return (responseToken) -> {
Saml2Authentication authentication = delegate.convert(responseToken);
Saml2AuthenticatedPrincipal principal = (Saml2AuthenticatedPrincipal) authentication.getPrincipal();
List<String> groups = principal.getAttribute("groups");
Set<GrantedAuthority> authorities = new HashSet<>();
if (groups != null) {
groups.stream().map(SimpleGrantedAuthority::new).forEach(authorities::add);
} else {
authorities.addAll(authentication.getAuthorities());
}
return new Saml2Authentication(principal, authentication.getSaml2Response(), authorities);
};
}
}
I don't see the problem and even after hours of research I can't get anywhere at this point and maybe someone here can help me? Maybe there is a better approach in general? (OpenID-Connect instead of SAML is unfortunately not an option)
Thank you

Use sAMAccountName instead of userPrincipalName in LDAP auth in Spring Boot

So I am using the code below to connect to our LDAP server in my spring boot app, I can authorized successfully using the userPrincipalName -> (LastName.FirstName#enterprise.com), but I want to use the assigned sAMAccountName instead (ID00001).
I messed around with the setSearchFilter by doing: provider.setSearchFilter("(sAMAccountName ={0})")
but it's not working. (I'm getting bad credentials)
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.antMatcher("/myapplication")
.authorizeRequests()
.anyRequest().fullyAuthenticated()
.and()
.httpBasic();
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(activeDirectoryLdapAuthenticationProvider());
}
#Bean
public AuthenticationManager authenticationManager() {
return new ProviderManager(Arrays.asList(activeDirectoryLdapAuthenticationProvider()));
}
#Bean
public AuthenticationProvider activeDirectoryLdapAuthenticationProvider() {
ActiveDirectoryLdapAuthenticationProvider provider = new ActiveDirectoryLdapAuthenticationProvider("", "ldap://test.enterprise.com","dc=ORG1, dc=ORG2");
provider.setConvertSubErrorCodesToExceptions(true);
provider.setUseAuthenticationRequestCredentials(true);
provider.setSearchFilter("(sAMAccountName={0})"); // THIS DOES NOT WORK
provider.setSearchFilter("(userPrincipalName={0})"); // THIS WORKS
return provider;
}
}
EDIT: OK turns out I have been using the wrong field, there is another field: sAMAccountName that has the same value that I should be using, updated the title and question contents.
EDIT:
Ok I tried:
provider.setSearchFilter("(&(objectClass=user)(cn={0}))");
provider.setSearchFilter("(&(objectClass=user)(sAMAccountName={0}))");
provider.setSearchFilter("(&(objectClass=user)(cn={1}))");
provider.setSearchFilter("(&(objectClass=user)(sAMAccountName={1}))");
and it's still the same error,
bad credentials, AcceptSecurityContextError, 52e v2580
I managed to solve my issue but instead of using spring ldap dependencies, I created a custom authentication provider that implements AuthenticationProvider and and used the following code to connect to ldap and validate the credentials:
autowire it to WebSecurityConfig class.
#Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
#Autowired
private UserDetails userDetails;
#Override
public Authentication authenticate(Authentication auth) throws
AuthenticationException {
String user = authentication.getName();
String pass = authentication.getCredentials.ToString();
try {
if (isRegistrered(user,pass) {
return new UsernamePasswordAuthenticationToken(user,pass, new
ArrayList<>());
} else {
return null;
}
} catch (Exception e) {
e.printStackTrace();
}
}
#Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
private isRegisterd(String user, String pass) {
boolean result = false;
try {
// Set up the environment for creating the initial context
Hashtable<String, String> env = new Hashtable<String, String>();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL, "ldap://ldap_server:389");
//
env.put(Context.SECURITY_AUTHENTICATION, "simple");
env.put(Context.SECURITY_PRINCIPAL, "DOMAIN\\USER");
env.put(Context.SECURITY_CREDENTIALS, "PASSWORD");
// Create the initial context
DirContext ctx = new InitialDirContext(env);
if(ctx != null) {
ctx.close();
}
return result;
} catch (Exception e) {
return result;
}
}
private void retrieveUserDetails(DirContext ctx, String username) throws NamingException {
String userSearchBase = "dc=TEST,dc=SAMPLE";
String userSearchFilter = String.format("sAMAccountName=%s", username);
//any attributes that you want
String[] attributes = { "sAMAccountName", "department" };
SearchControls controls = new SearchControls();
controls.setSearchScope(SearchControls.SUBTREE_SCOPE);
controls.setReturningAttributes(attributes);
SearchResult result = null;
NamingEnumeration<?> users = ctx.search(userSearchBase, userSearchFilter, controls);
while(users.hasmore()) {
result = (SearchResult) users.next();
Attributes attr = result.getAttribtutes();
String sAMAccountName = attr.get("sAMAccountName").get(0).toString();
String department = attr.get("department").get(0).toString();
//assign to autowired object to be accessed anywhere
this.userDetails.setAccountName(sAMAccountName);
this.userDetails.setDepartment(department);
}
}
}
There's another approach by using LdapAuthenticationProvider. I use LdapAuthenticationProvider with a single parameter constructor.
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
#Profile("integration")
public class SecurityConfigurationT extends WebSecurityConfigurerAdapter {
private final AuthenticationManagerBuilder authenticationManagerBuilder;
private final TokenProvider tokenProvider;
public SecurityConfigurationT(
AuthenticationManagerBuilder authenticationManagerBuilder,
TokenProvider tokenProvider) {
this.authenticationManagerBuilder = authenticationManagerBuilder;
this.tokenProvider = tokenProvider;
}
#PostConstruct
public void initIntegration() {
try {
authenticationManagerBuilder
.authenticationProvider(ldapAuthenticationProvider());
} catch (Exception e) {
throw new BeanInitializationException("Security configuration failed", e);
}
}
#Override
#Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Bean
public LdapAuthenticationProvider ldapAuthenticationProvider() throws Exception {
return new LdapAuthenticationProvider(ldapAuthenticator());
}
#Bean
public LdapContextSource ldapContextSource() throws Exception {
PasswordPolicyAwareContextSource contextSource = new PasswordPolicyAwareContextSource("ldaps://int.root.company.ag:636");
contextSource.setUserDn("CN=system_user,OU=companygroup svc accs,DC=int,DC=root,DC=company,DC=ag");
contextSource.setPassword("XXXXXX");
return contextSource;
}
// Use this for other filter such as "sAMAccountName".
#Bean
public LdapAuthenticator ldapAuthenticator() {
BindAuthenticator authenticator = new BindAuthenticator(ldapContextSource());
authenticator.setUserSearch(new FilterBasedLdapUserSearch("OU=company,OU=companygroup users,DC=int,DC=root,DC=company,DC=ag", "(sAMAccountName={0})", ldapContextSource()));
return authenticator;
}
}
Reference:
https://www.stevenschwenke.de/LDAPWithSpringSecurity

How to extract custom Principal in OAuth2 Resource Server?

I'm using Keycloak as my OAuth2 Authorization Server and I configured an OAuth2 Resource Server for Multitenancy following this official example on GitHub.
The current Tenant is resolved considering the Issuer field of the JWT token.
Hence the token is verified against the JWKS exposed at the corresponding OpenID Connect well known endpoint.
This is my Security Configuration:
#EnableWebSecurity
#RequiredArgsConstructor
#EnableAutoConfiguration(exclude = UserDetailsServiceAutoConfiguration.class)
public class OrganizationSecurityConfiguration extends WebSecurityConfigurerAdapter {
private final TenantService tenantService;
private List<Tenant> tenants;
#PostConstruct
public void init() {
this.tenants = this.tenantService.findAllWithRelationships();
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests().anyRequest().authenticated()
.and()
.oauth2ResourceServer()
.authenticationManagerResolver(new MultiTenantAuthenticationManagerResolver(this.tenants));
}
}
and this is my custom AuthenticationManagerResolver:
public class MultiTenantAuthenticationManagerResolver implements AuthenticationManagerResolver<HttpServletRequest> {
private final AuthenticationManagerResolver<HttpServletRequest> resolver;
private List<Tenant> tenants;
public MultiTenantAuthenticationManagerResolver(List<Tenant> tenants) {
this.tenants = tenants;
List<String> trustedIssuers = this.tenants.stream()
.map(Tenant::getIssuers)
.flatMap(urls -> urls.stream().map(URL::toString))
.collect(Collectors.toList());
this.resolver = new JwtIssuerAuthenticationManagerResolver(trustedIssuers);
}
#Override
public AuthenticationManager resolve(HttpServletRequest context) {
return this.resolver.resolve(context);
}
}
Now, because of the design of org.springframework.security.oauth2.server.resource.authentication.JwtIssuerAuthenticationManagerResolver.TrustedIssuerJwtAuthenticationManagerResolver
which is private, the only way I can think in order to extract a custom principal is to reimplement everything that follows:
TrustedIssuerJwtAuthenticationManagerResolver
the returned AuthenticationManager
the AuthenticationConverter
the CustomAuthenticationToken which extends JwtAuthenticationToken
the CustomPrincipal
To me it seems a lot of Reinventing the wheel, where my only need would be to have a custom Principal.
The examples that I found don't seem to suit my case since they refer to OAuth2Client or are not tought for Multitenancy.
https://www.baeldung.com/spring-security-oauth-principal-authorities-extractor
How to extend OAuth2 principal
Do I really need to reimplement all such classes/interfaes or is there a smarter approach?
This is how I did it, without reimplementing a huge amount of classes. This is without using a JwtAuthenticationToken however.
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
...
#Override
protected void configure(HttpSecurity http) throws Exception {
http
...
.oauth2ResourceServer(oauth2 -> oauth2.authenticationManagerResolver(authenticationManagerResolver()));
}
#Bean
JwtIssuerAuthenticationManagerResolver authenticationManagerResolver() {
List<String> issuers = ... // get this from list of tennants or config, whatever
Predicate<String> trustedIssuer = issuers::contains;
Map<String, AuthenticationManager> authenticationManagers = new ConcurrentHashMap<>();
AuthenticationManagerResolver<String> resolver = (String issuer) -> {
if (trustedIssuer.test(issuer)) {
return authenticationManagers.computeIfAbsent(issuer, k -> {
var jwtDecoder = JwtDecoders.fromIssuerLocation(issuer);
var provider = new JwtAuthenticationProvider(jwtDecoder);
provider.setJwtAuthenticationConverter(jwtAuthenticationService::loadUserByJwt);
return provider::authenticate;
});
}
return null;
};
return new JwtIssuerAuthenticationManagerResolver(resolver);
}
}
#Service
public class JwtAuthenticationService {
public AbstractAuthenticationToken loadUserByJwt(Jwt jwt) {
UserDetails userDetails = ... // or your choice of principal
List<GrantedAuthority> authorities = ... // extract from jwt or db
...
return new UsernamePasswordAuthenticationToken(userDetails, null, authorities);
}
}

Dynamic RBAC Configuration in Spring Security

I'm using Spring Security 5.1.2 in Restful Spring MVC project. I have used Custom Filter, Authentication Provider etc. In my Project which works fine, My Security config file is as follows:
#Configuration
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private JwtAuthenticationProcessingFilter jwtAuthenticationProcessingFilter;
#Autowired
private JwtAuthenticationProvider jwtAuthenticationProvider;
#Autowired
private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
#Autowired
private JwtAccessDeniedHandler jwtAccessDeniedHandler;
#Autowired
private RolePermissionService rolePermissionService;
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Override
protected void configure(AuthenticationManagerBuilder auth) {
auth.authenticationProvider(this.jwtAuthenticationProvider);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers(AuthenticationPatternType.SKIP_PATTERN.getPattern()).permitAll();
try {
List<PermissionRule> permissionRules = this.rolePermissionService.permissionRules();
for (PermissionRule p : permissionRules)
http.authorizeRequests().antMatchers(p.getPermission()).hasAnyRole(p.getRoles().toArray(new String[0]));
} catch (SystemException ignored) {
}
http.authorizeRequests().antMatchers(AuthenticationPatternType.AUTH_PATTERN.getPattern()).authenticated();
this.jwtAuthenticationProcessingFilter.init(authenticationManagerBean());
http.addFilterBefore(this.jwtAuthenticationProcessingFilter, UsernamePasswordAuthenticationFilter.class);
http.exceptionHandling()
.authenticationEntryPoint(this.jwtAuthenticationEntryPoint)
.accessDeniedHandler(this.jwtAccessDeniedHandler);
http.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.csrf().disable();
}
}
And my RolePermissionService Class is as follows:
#Service
public class RolePermissionService {
#Autowired
private PermissionDao permissionDao;
public List<PermissionRule> permissionRules() {
List<PermissionEntity> permissionEntities = this.permissionDao.list();
return permissionEntities.stream().map(PermissionRule::new)
.collect(Collectors.toList());
}
}
public class PermissionRule {
private String permission;
private List<String> roles;
public PermissionRule(PermissionEntity permissionEntity) {
this.permission = permissionEntity.getUrl();
this.roles = permissionEntity.getRoles().stream().map(RoleEntity::getName)
.collect(Collectors.toList());
}
// getters and setters
}
In this project, I have role(rid, name), permission(pid, url) and rolePermission(pid, rid) tables which holds my RBAC data. As you can see I'm reading this data from database (RolePermissionService Class) and load this data to HttpSecurity object in above config file.
Now, assume there is a rest Api which I want to edit rolePermission table or assume there is a rest Api which I want to add a role and its permissions in run time.
Q: How can I update security configuration which I can edit RBAC dynamically at run time?
Please refer https://github.com/liubo-tech/spring-security-rbac
#PreAuthorize("hasAuthority(T(com.example.springsecurityrbac.config.PermissionContact).USER_VIEW)")
Used to annotate method for securing. Uses database table mapping to assign permissions.
Please refer repo for further information

method level security using user roles in spring security oauth2

How can i have method level security in resource server using spring security oauth2? I am aware of client scopes and roles, I am talking about method level security using user roles something like -
#PreAuthorize("hasRole('ROLE_ADMIN')")
#RequestMapping("/accessibleByAdminOnly")
public String accessibleByAdminOnly() {
return "Welcome Admin!";
}
My resource server config looks like this -
#SpringBootApplication
#Configuration
#EnableResourceServer
#EnableGlobalMethodSecurity(prePostEnabled=true)
public class DemoAPIServiceApp extends GlobalMethodSecurityConfiguration {
#Autowired
private ApplicationContext applicationContext;
#Override
protected MethodSecurityExpressionHandler createExpressionHandler() {
DefaultMethodSecurityExpressionHandler methodSecurityExpressionHandler = new DefaultMethodSecurityExpressionHandler();
methodSecurityExpressionHandler.setRoleHierarchy(roleHierarchy());
methodSecurityExpressionHandler.setApplicationContext(applicationContext);
return methodSecurityExpressionHandler;
}
#Bean
public RoleHierarchy roleHierarchy() {
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
roleHierarchy.setHierarchy("ROLE_ADMIN > ROLE_USER");
return roleHierarchy;
}
#Bean
public RoleVoter roleVoter() {
return new RoleHierarchyVoter(roleHierarchy());
}
public static void main(String[] args) {
SpringApplication.run(DemoAPIServiceApp.class, args);
}
}
I am using a standard spring oauth2 auth server configuration with custom user details. I have one client and one scope.
Please help. The method level security is not working with this configuration.

Resources