I have a trouble with configuring LDAP authentication with Spring.
Using LDAP Apache Directory Studio I have following working connection to LDAP Server:
Bind DN or USER: cn=HIDDEN_USERNAME,OU=HIDDEN_OU1,OU=HIDDEN2,OU=Admin,DC=MY_COMPANYNAME,DC=COM
Authorization ID: SASL PLAIN only
Bind Password: ******
Using this connection, I can find my account under root:
Root DSE/DC=MY_COMPANYNAME,DC=COM/OU=User Accounts/OU=Enabled Users/OU=Consultants/CN=MySurname My Name
Right click on my account gives following values:
DN: CN=MySurname MyName,OU=Consultants,OU=Enabled Users,OU=User Accounts,DC=MY_COMPANYNAME,DC=COM
URL: ldap://IP_ADRESS:389/CN=MySurname%20MyName,OU=Consultants,OU=Enabled%20Users,OU=User%20Accounts,DC=MY_COMPANYNAME,DC=COM
I am going to configure WebSecurityConfigurerAdapter in order to get authentication via ldap server in the following way:
#Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.ldapAuthentication()
.userDnPatterns("CN={0},OU=Consultants,OU=Enabled Users,OU=User Accounts,DC=MY_COMPANYNAME,DC=COM")
.contextSource()
.url("ldap://IP_ADRESS:389/")
.managerDn("HIDDEN_USERNAME")
.managerPassword("*****")
.and()
.passwordCompare()
.passwordEncoder(new LdapShaPasswordEncoder())
.passwordAttribute("userPassword");
}
I tried to set userDnPattern in many ways without result. What I am doing wrong?
Using the DN pattern you specify, your logon attempt would need to be made with user ID "MySurname MyName" (and the space may be an issue). The user provided logon ID string is inserted into the DN pattern you include above, and you'll be binding with
CN=MySurname MyName,OU=Consultants,OU=Enabled Users,OU=User Accounts,DC=MY_COMPANYNAME,DC=COM
Which matches what your fully qualified DN appears to be. If you want to be able to log on with your ID and not the surname/name string that makes up your CN, or if accounts which need to authenticate exist in multiple OU locations, userSearch may be preferable to DN patterns.
If you are authenticating against an Active Directory domain, you may be able to use {0}#domain.gTLD or DOMAIN{0} as the user pattern -- when a logon ID is supplied, these patterns form the userPrincipalName and sAMAccountName respectively.
In response to your comment above: Active Directory hides the password field and it cannot be read even by domain administrators.
I concur with the other user that for AD you need to use a user search filter and if you want to do it against the username you should use samaccountname={0}
Related
I am trying to create Sprint Boot, Spring Security 6, LDAP Server (external not embedded) based authentication application. When I spin up the app and provide the username (uid) and password on the login form I get a "Bad Credentials" message displayed on the UI. There are no exceptions reported in the application log. I do not understand what is causing "Bad Credentials" message to be displayed. Any pointers are much appreciated.
This is what my config file looks like
#Configuration
#EnableWebSecurity
public class WebSecurityConfig {
#Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity.authorizeHttpRequests().anyRequest().fullyAuthenticated()
.and()
.formLogin();
httpSecurity.authenticationProvider(ldapAuthenticationProvider());
return httpSecurity.build();
}
#Bean
LdapAuthenticationProvider ldapAuthenticationProvider() {
return new LdapAuthenticationProvider(authenticator());
}
#Bean
BindAuthenticator authenticator() {
FilterBasedLdapUserSearch search = new FilterBasedLdapUserSearch("ou=people", "(uid={0})", contextSource());
BindAuthenticator authenticator = new BindAuthenticator(contextSource());
authenticator.setUserSearch(search);
return authenticator;
}
#Bean
public DefaultSpringSecurityContextSource contextSource() {
DefaultSpringSecurityContextSource dsCtx = new DefaultSpringSecurityContextSource("ldap://localhost:389/dc=example,dc=com");
dsCtx.setUserDn("cn=admin,dc=example,dc=com");
dsCtx.setPassword("password");
return dsCtx;
}
}
When I try to find user using ldapsearch command I do get the user info
MacBook-Pro:springsecuritywithldapdemo$ ldapsearch -LLL -x -H ldap:// -t -b "dc=example,dc=com" "uid=jsmith1"
dn: uid=jsmith1,ou=people,dc=example,dc=com
objectClass: inetOrgPerson
description: John Smith from Accounting. John is the project manager of the b
uilding project, so contact him with any questions.
cn: John Smith
sn: Smith
uid: jsmith1
userPassword:: anNtaXRoMTIz
I have gone through many of the search results returned by google on different searches, most of them have used an older version of Spring Security or have used JDBC authentication with Spring Security 6.
I have referred to the youtube tutorials to see if I am doing anything wrong but doesn't look like.
first of all I wanted to thank you because based on your solution I was able to solve the same problem in my application. Compared to your solution the only things I have changed are:
FilterBasedLdapUserSearch search = new FilterBasedLdapUserSearch("cn=users,cn=accounts,dc=example,dc=com",
"(uid={0})",
contextSource());
and so in your case you should try:
FilterBasedLdapUserSearch search = new FilterBasedLdapUserSearch("ou=people,dc=example,dc=com", "(uid={0})", contextSource());
and then I also had to make the following changes:
DefaultSpringSecurityContextSource dsCtx = new DefaultSpringSecurityContextSource(
"ldap://localhost:389");
dsCtx.setUserDn("uid=admin,cn=users,cn=accounts,dc=example,dc=com");
which in your case I think is almost identical since I think you also have a user with uid=admin. I hope it will solve your problem, as it did for me
I'm having no luck in setting up a simple Spring gateway + oauth2 client with Keycloak standalone. The keycloack part of it works fine. Wireshark shows the token correctly generated.
The gateway security config is as follows. I'm still not sure whether there is a need to permitAll() the login callback url. Some guides suggest that it should be the case, others dont. I suspect the oauth provider manages that part behind the scenes. Nonetheless, with or without permitAll for the "/login/*" path, the result remains the same.
#Configuration
#EnableWebFluxSecurity
public class SecurityConfig {
#Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http.authorizeExchange(e -> e.anyExchange().authenticated());
http.oauth2Login(Customizer.withDefaults());
http.csrf().disable();
return http.build();
}
}
After login the redirect to https://localhost:9000/login seems incorrect, it should retry the original url, say https://localhost:9000/test-service/v1/listall/
EDIT
In order to rule out any misconfigurations, even tried a simplest possible gateway and api resource (un-authenticated) and setup simplest possible relam in keyclock. The results haven't changed :( There are dozens of articles out there doing the exact same thing.
Any pointers, ideas?
Many Thanks
Even I have completed all configurations including "creating a client", "creating a scope" and creating a "user" with this scope. I have encountered this issue again.
The only solution worked for me is adding scope :openid to application.yaml.
You can refer application.yaml Security OAuth config from here:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://${keycloak.base.url}/auth/realms/${realm}
client:
registration:
gateway:
provider: keycloak
client-id: ${client id}
client-secret:${client secret}
scope: openid
provider:
keycloak:
user-name-attribute: preferred_username
issuer-uri: https://${keycloak.base.url}/auth/realms/${realm}
According to offical doc(found at https://www.keycloak.org/docs/latest/securing_apps/#_spring_security_adapter), you should use
#KeycloakConfiguration
public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter
I figured it out, it was an incorrect user-name-attribute. The correct value is
user-name-attribute: preferred_username
For some reason, I had it set to preferred_name. It would save a lot of debug-time if only spring oauth writes the actual error instead of a generic invalid_grant.
First of all, for anyone troubleshooting Spring Security, I recommend enabling debug logging by setting the logging level in your application.properties or application.yml file.
application.properties format:
logging.level.org.springframework.security=DEBUG
application.yml format:
logging:
level:
org:
springframework:
security: DEBUG
I was having a similar issue when using OAuth2 along with Spring Session. Even after authenticating successfully with Keycloak, I would get this login error whenever my Spring Session had expired.
I do not condone messing around with authentication flows, but I was able to resolve this issue by setting my own authentication entry point on the ServerHttpSecurity:
http.exceptionHandling().authenticationEntryPoint(new RedirectServerAuthenticationEntryPoint("/oauth2/authorization/keycloak"));
I then had to handle any requests to the "/login" page in my Controller. For me, I just redirect to my default landing page for a root request:
#Controller
#RequestMapping("/")
public class RootController {
#GetMapping({"", "login"})
public Mono<String> index() {
return Mono.just("redirect:/myLandingPage");
}
}
Somehow adding the "scope=openid" in my applications.properties works fine for me
spring.security.oauth2.client.registration.spring-cloud-client.scope=openid
I'm creating an application integrating with Shopify's API, which uses OAuth2 for authentication and authorization. Using the tutorial for Spring Security OAuth2, and the tutorial for Shopify, I've been able to get integration working with a single shop. The YAML configuration looks like this:
shopify:
shop: myshop
scopes: read_customers,read_orders
security:
oauth2:
client:
clientId: myclientid
clientSecret: mysecret
tokenName: access_token
authenticationScheme: query
clientAuthenticationScheme: form
accessTokenUri: https://${shopify.shop}.myshopify.com/admin/oauth/access_token
userAuthorizationUri: https://${shopify.shop}.myshopify.com/admin/oauth/authorize?scope=${shopify.scopes}&grant_options[]=
pre-established-redirect-uri: https://myapp/login
registered-redirect-uri: https://myapp/login
use-current-uri: false
resource:
userInfoUri: https://${shopify.shop}.myshopify.com/admin/shop.json
However, this static configuration won't work for an app published in Shopify's App Store because the redirect, access token, user info, and user authorization URIs depend on the shop name. There are examples of using more than one provider, but they still have to be static.
To allow these URI's to be dynamic, I've come up with a few possible options:
Use a parameter in the /login path to identify the shop, then create a filter that adds the shop name to a ThreadLocal that runs before everything else, then dynamically create the AuthorizationCodeResourceDetails that is needed by the OAuth2 filter via a Spring proxied factory bean.
Use a sort of "metafilter" that dynamically recreates the OAuth2ClientAuthenticationProcessingFilter per request along with all of the resources that it needs.
Override OAuth2ClientAuthenticationProcessingFilter so that it can handle recreating the RestTemplate it needs to obtain the access token.
All of these options seem pretty difficult. What's a good way to handle dynamically-generated URI's for access tokens and user information in Spring Security OAuth2?
Also, since I'm new to OAuth2 in general, do I need to enable a Resource Server in my Spring configuration to protect my app with the access token?
a bit late but I did return a dynamic url for an oauth resource by overriding the getter for the Oauth2ProtectedResource
#Bean(name = "googleOauthResource")
public BaseOAuth2ProtectedResourceDetails getGoogleOauthResource() {
final AuthorizationCodeResourceDetails details = new AuthorizationCodeResourceDetails() {
#Override
public String getPreEstablishedRedirectUri() {
final RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
if (requestAttributes instanceof ServletRequestAttributes) {
final HttpServletRequest request = ((ServletRequestAttributes)requestAttributes).getRequest();
return request.getRequestURL() + "?" + request.getQueryString() + "&addStuff";
}
return super.getPreEstablishedRedirectUri();
}
};
details.setId("google-oauth-client");
details.setClientId("xxxxxxxxxxx");
details.setClientSecret("xxxxxxxx");
details.setAccessTokenUri("https://www.googleapis.com/oauth2/v4/token");
details.setUserAuthorizationUri("https://accounts.google.com/o/oauth2/v2/auth");
details.setTokenName("authorization_code");
details.setScope(Arrays.asList("https://mail.google.com/,https://www.googleapis.com/auth/gmail.modify"));
details.setPreEstablishedRedirectUri("http://localhost:8080/xxx-api-web/v2/gmail"); //TODO
details.setUseCurrentUri(false);
details.setAuthenticationScheme(AuthenticationScheme.query);
details.setClientAuthenticationScheme(AuthenticationScheme.form);
details.setGrantType("authorization_code");
return details;
}
I'm having the same problem you are and I'm leaning toward your first theory of using ThreadLocal storage. Here is how I'll probably go about my solution:
Set values from ServletRequest in LocalThread storage by overriding methods in OAuth2ClientAuthenticationProcessingFilter:
attemptAuthentication
successfulAuthentication
unsuccessfulAuthentication
requiresAuthentication
Then translate the URI's within the OAuth2RestTemplate by overriding the followign methods:
createRequest
doExecute
appendQueryParameter
I'll probably have to make my own #Bean for the RestTemplate that has an injected #Service that will lookup the dynamic Shopify domain.
I'll post my solution if it works.
I'm really trying to understand how Spring Security works, but I'm a bit lost at the moment. Here's the simple scenario:
User visits the website home page but doesn't log in
SecurityContextPersistenceFilter logs that no SecurityContext was available and a new one will be created
AnonymousAuthenticationFilter populates SecurityContextHolder with an anonymous token
A session is created with ID = C2A35ED5A41E29865FF53162B0024D52
User lets the page sit idle until the session times out
User clicks on the About page (or home page again)
SecurityContextPersistenceFilter again logs that no SecurityContext was available and a new one will be created
AnonymousAuthenticationFilter again populates SecurityContextHolder with an anonymous token
SessionManagementFilter logs that requested session ID C2A35ED5A41E29865FF53162B0024D52 is invalid
SessionManagementFilter logs that it is starting a new session and redirecting to /invalidsession
These pages are configured to .authorizeRequests().antMatchers("/","/home","/about").permitAll(). I have the invalid session option turned on to handle authenticated users: .sessionManagement().invalidSessionUrl("/errors/invalidSession"). If I comment out that option, then everything described above is exactly the same EXCEPT for step #10 - SessionManagementFilter sees that the requested session ID is invalid (#9) but does NOT start a new session and perform the redirect (#10).
WHY? What can I do to keep the invalid session option but correctly handle anonymous users, i.e., not be redirected? Or is that just not possible and I'll have to handle authenticated users separately? I'd be very grateful if anyone can help me understand what's happening here and point me in a direction to solve this. Let me know if you need to see my full http configuration.
EDIT
I ran a series of tests with anonymous and registered (authenticated) users. If .sessionManagement().invalidSessionUrl("/errors/invalidSession") is enabled then both types of users will eventually arrive at the error page. Authenticated users with RememberMe unchecked are the same as anon users. If RememberMe is checked, then the error page appears once RememberMe times out.
If I disable the invalid session option, no users ever get the error page (which makes sense). Both types of users can browse public pages as long as they want and authenticated users will be asked to log in after the session or RememberMe expires.
If you're interested the code involved here is in SessionManagementFilter
if (invalidSessionStrategy != null) {
invalidSessionStrategy
.onInvalidSessionDetected(request, response);
return;
}
If .sessionManagement().invalidSessionUrl is enabled the default method SimpleRedirectInvalidSessionStrategy is called, which executes this piece of code:
if (createNewSession) {
request.getSession();
}
redirectStrategy.sendRedirect(request, response, destinationUrl);
The createNewSession boolean can be set through setCreateNewSession(boolean createNewSession), which is described as:
Determines whether a new session should be created before redirecting (to avoid possible looping issues where the same session ID is sent with the redirected request). Alternatively, ensure that the configured URL does not pass through the SessionManagementFilter.
So, it looks to me like .sessionManagement().invalidSessionUrl works best for sites where all pages are authenticated. The options I'm looking at are a custom filter placed before the SessionManagementFilter that checks the page access and turns 'createNewSession' on/off as needed or turning off the invalid session option and handling it elsewhere for authenticated pages (?). I also stumbled across <%# page session=“false” %> in this SO question - Why set a JSP page session = “false” directive? - which I'm going to look into further. Being so new to Spring Security I don't have a good sense of the best practice for handling this situation correctly. Any help would be appreciated.
OK, so I've spent the last couple of weeks digging around in Spring Security trying to understand how it all fits together. I'm still learning, but for this particular situation I found two approaches that work.
The obvious one is to just bypass security for public pages like this:
#Override
public void configure(WebSecurity web) throws Exception
{
web
.ignoring()
.antMatchers("/", "/home", "/about", "/login**", "/thankyou", "/user/signup**", "/resources/**")
;
}
I still don't know enough about web security in general to know if this is an acceptable approach or not, but it allows anonymous users to browse the site w/o ever getting an invalid session error.
The harder solution (for a Java and Spring noob like me) is based upon these SO questions:
Spring security invalid session redirect
How to set a custom invalid session strategy in Spring Security
The default SimpleRedirectInvalidSessionStrategy class is final which meant I had to create basically a copy of that class (not sure how good an idea that is). You can't use a session attribute because the session has been destroyed by the time it gets to this strategy so I created a helper class for a session cookie called authUser (I can post the class if anyone wants to see it). The cookie is created or updated in the LoginSuccessHandler or RememberMeSuccessHandler and it indicates if the user is anonymous or authenticated:
authCookie.setCookie(request, response, "anonymousUser");
or
authCookie.setCookie(request, response, authentication.getName());
I'm currently using the actual login only for testing purposes - it will ultimately be just a simple yes/no indicator of some sort. CustomLogoutSuccessHandler resets it to anonymousUser
The invalid session method looks like this:
#Override
public void onInvalidSessionDetected(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
String url = destinationUrl;
//reset context default value
redirectStrategy.setContextRelative(false);
if (authCookie.isCurrentCookieAnonymous()) {
//pass the URL originally requested by the anonymous user
url = request.getRequestURI();
//the URL needs to have the context removed
redirectStrategy.setContextRelative(true);
}
//always revert to anonymous user
authCookie.setCookie(request, response, "anonymousUser");
logger.debug("Starting new session (if required) and redirecting to '" + url + "'");
if (createNewSession)
request.getSession();
redirectStrategy.sendRedirect(request, response, url);
}
Again, I can post the full class if requested.
The SecurityConfig class includes the following:
#Bean
public SessionManagementBeanPostProcessor sessionManagementBeanPostProcessor() {
return new SessionManagementBeanPostProcessor();
}
protected static class SessionManagementBeanPostProcessor implements BeanPostProcessor {
#Override
public Object postProcessBeforeInitialization(Object bean, String beanName) {
if (bean instanceof SessionManagementFilter) {
SessionManagementFilter filter = (SessionManagementFilter) bean;
filter.setInvalidSessionStrategy(new RedirectInvalidSession("/errors/invalidSession"));
}
return bean;
}
#Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
return bean;
}
}
My testing so far has been successful for both anonymous and authenticated users, but this approach has not been production tested.
We have developed an application using Spring Security and Spring SAML that works in our development environment where we use SSOCircle as our IDP. When we move into our customer's environment using their IDP, we are able to authenticate and navigate through the application without a problem as long as we do not pause. If the user pauses on a page for more than minute before submitting it, the application will redirect to the original landing page and the submitted data is lost.
The logs show:
o.s.s.w.a.ExceptionTranslationFilter
- Authentication exception occurred; redirecting to authentication entry point
org.springframework.security.authentication.ProviderNotFoundException: No AuthenticationProvider found for org.springframework.security.providers.ExpiringUsernameAuthenticationToken
Prior to this you the logs show at about every minute something similar to:
SecurityContextHolder not populated with anonymous token, as it already contained: 'org.springframework.security.providers.ExpiringUsernameAuthenticationToken#e6313ceb: Principal: REDACTED
We have been told by the customer that their IDP has a timeout of 60 seconds with a +-30sec skew time.
We asked them to temporarily adjust the IDP timeout to 30 minutes and our problem went away. When we go to production we must have the original setting of 60 seconds.
Our application is using the SAMLAuthenticationProvider:
#Override
protected void configure(AuthenticationManagerBuilder auth)
throws Exception {
auth.authenticationProvider(samlAuthenticationProvider());
}
#Bean
public SAMLAuthenticationProvider samlAuthenticationProvider() {
SAMLAuthenticationProvider samlAuthenticationProvider = new SAMLAuthenticationProvider();
samlAuthenticationProvider.setForcePrincipalAsString(false);
samlAuthenticationProvider.setUserDetails(samlUserDetailsService);
return samlAuthenticationProvider;
}
How do we configure ExpiringUsernameAuthenticationToken to use this? And why does the original authentication work if it is not set?
And why is the application trying to re-authenticate when the IDP's session expires?
WebSSOProfileConsumerImpl and SingleLogoutProfileImpl both provide ways to set the responseSkew. Should this be set equal to, less than, or greater than the IDP's skew time?
Spring SAML by default observes the SessionNotOnOrAfter field provided by IDP in its SAML Response. This field tells that that once time hits the provided value user must be re-authenticated.
Spring SAML tries to re-authenticate user by sending the current Authentication object to AuthenticationManager, which tries to find an AuhenticationProvier which supports Authentiction object of such type (ExpiringUsernameAuthenticationToken in case of Spring SAML). In your case there is no such provider - that's why you see the ProviderNotFoundException exception. After this error Spring Security probably invokes the default EntryPoint, which redirects your user to the login page.
In order to ignore the SessionNotOnOrAfter value simply extend class SAMLAuthenticationProvider, override method getExpirationDate and make it return null. Then use your new class in the securityContext.xml.
But the correct solution is for your IDP to return SessionNotOnOrAfter value with a sensible session length - I wonder why they insist on using 60 seconds there.