My spring boot (2.2.4) application is using siteminder as authentication provider. Upon successful authentication siteminder sends user details (Roles) in Http header. Inside the application I need to use spring security for role based authorization. Here is reference documentation I am following -
https://docs.spring.io/spring-security/site/docs/5.0.x/reference/html/preauth.html
My spring-security.xml will look this.
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:security="http://www.springframework.org/schema/security"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/security
https://www.springframework.org/schema/security/spring-security.xsd">
<security:http>
<!-- Additional http configuration omitted -->
<security:csrf disabled="true"/>
<security:intercept-url pattern="/**" access="hasRole('ROLE_ABC')"/>
<security:custom-filter position="PRE_AUTH_FILTER" ref="siteminderFilter" />
</security:http>
<bean id="siteminderFilter" class="org.springframework.security.web.authentication.preauth.RequestHeaderAuthenticationFilter">
<property name="principalRequestHeader" value="SM_USER"/>
<property name="authenticationManager" ref="authenticationManager" />
</bean>
<bean id="preauthAuthProvider" class="org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider">
<property name="preAuthenticatedUserDetailsService">
<bean id="userDetailsServiceWrapper"
class="org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper">
<property name="userDetailsService" ref="userDetailsService"/>
</bean>
</property>
</bean>
<bean id="userDetailsService" class="com.mycode.service.SCPMUserDetailsService"/>
<security:authentication-manager alias="authenticationManager">
<security:authentication-provider ref="preauthAuthProvider" />
</security:authentication-manager>
</beans>
How do I create a Java config file which will incorporate above security for Spring boot application.
I started something like this and need to inject all the pieces in above xml file.
#EnableWebSecurity
#Order(SecurityProperties.BASIC_AUTH_ORDER - 10)
public class SecurityConfigurer extends WebSecurityConfigurerAdapter {
#Autowired
private MyUserDetailsService myUserDetailsService;
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myUserDetailsService);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()// Disable for GraphIQL to work locally
.antMatcher("/**")
.authorizeRequests()
.antMatchers("/healthcheck/**").permitAll()
.antMatchers("/scpm/**").permitAll()
.antMatchers("/approvers/**").hasRole("APPROVER")
.antMatchers("/departments/**").hasRole("ADMIN")
.anyRequest().authenticated()
.and().httpBasic();
}
}
Spring Security reference document does not provide Java Code for SiteMinder integration. It just provides xml configuration for non-spring boot applications. Here is the Java code which can be used in any Spring Boot Application.
#EnableWebSecurity
#Configuration
#Order(SecurityProperties.BASIC_AUTH_ORDER - 10)
public class SecurityConfigurer extends WebSecurityConfigurerAdapter {
#Autowired
private MyUserDetailsService userDetailsService;
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
auth.authenticationProvider(preAuthenticatedAuthenticationProvider());
}
#Bean
public RequestHeaderAuthenticationFilter siteminderFilter() throws Exception {
RequestHeaderAuthenticationFilter filter = new RequestHeaderAuthenticationFilter();
filter.setPrincipalRequestHeader("SM_USER");
filter.setAuthenticationManager(this.authenticationManager());
return filter;
}
#Bean
public PreAuthenticatedAuthenticationProvider preAuthenticatedAuthenticationProvider() {
PreAuthenticatedAuthenticationProvider preauthAuthProvider = new PreAuthenticatedAuthenticationProvider();
preauthAuthProvider.setPreAuthenticatedUserDetailsService(userDetailsByNameServiceWrapper());
return preauthAuthProvider;
}
#Bean
public UserDetailsByNameServiceWrapper userDetailsByNameServiceWrapper() {
UserDetailsByNameServiceWrapper userDetailsServiceWrapper = new UserDetailsByNameServiceWrapper();
userDetailsServiceWrapper.setUserDetailsService(userDetailsService);
return userDetailsServiceWrapper;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.addFilter(siteminderFilter())
.csrf().disable()// Disable for GraphIQL to work locally
.antMatcher("/**")
.authorizeRequests()
.antMatchers("/healthcheck/**").permitAll()
.antMatchers("/approvers/**").hasRole("APPROVER")
.antMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
.and().httpBasic();
}
}
Here is custom UserDetailsService.
#Service
public class MyUserDetailsService implements UserDetailsService {
private Logger logger = LoggerFactory.getLogger(SCPMUserDetailsService.class);
#Autowired
private HttpServletRequest request;
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException{
HttpServletRequest request =
((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
.getRequest();
String csgroups = "";
Collection<GrantedAuthority> auth = new ArrayList<>();
try {
username = request.getHeader("SM_USER");
} catch (Exception ex){
logger.trace("SM_USER not found in Request Http Headers");
ex.printStackTrace();
}
try {
csgroups = request.getHeader("ROLES");;
} catch (Exception ex){
logger.trace("CSGROUPS not found in Request Http Headers");
ex.printStackTrace();
}
//Roles
if(csgroups != null) {
StringTokenizer st = new StringTokenizer(csgroups, "^");
while (st.hasMoreElements()) {
GrantedAuthority authority = new SimpleGrantedAuthority("ROLE_" + st.nextToken());
}
}
return new User(username, "", true, true, true, true,auth);
}
#Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
}
Related
I have a below setup and all the process are running in different machines and there is no common database between them.
1.Resource server(my protected resources)
2.authorization server(WS02--> for Issuing OAuth tokens)
3.client Application(trying to access the resource server)
ClientApp contacts the authorization server and gets the access token and uses this token to contact resource server. How can I verify the access token #resource server?
My authorization server provides REST API where I can provide the access token as input and to check the token validity.
I am new to spring security so not if there is any inbuilt API/callback to verify the token before giving the resource content. Any pointers on how to verify the token #resource server will be helpful.
When the client App try to access the resource server, resource server consults with the Auth server and validates the access token.
Configurations at resource server :
<bean id="tokenServices" class="org.springframework.security.oauth2.provider.token.RemoteTokenServices">
<property name="checkTokenEndpointUrl" value="http://localhost:8181/OUTPOST/oauth/check_token"/>
<property name="clientId" value="atlas"/>
<property name="clientSecret" value="atlas"/>
</bean>
Use RemoteTokenService instead of DefaultTokenService at resource server.
oauth/check_token is the endpoint for connecting resource-server and auth-server.
Configuration at auth server
#EnableAuthorizationServer
#Configuration
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
private static String REALM = "OUTPOST_API";
private ClientDetailsService clientService;
#Autowired
public AuthorizationServerConfig(AuthenticationManager authenticationManager,
RedisConnectionFactory redisConnectionFactory, ClientDetailsService clientService) {
this.authenticationManager = authenticationManager;
this.redisTokenStore = new RedisTokenStore(redisConnectionFactory);
this.clientService = clientService;
}
private AuthenticationManager authenticationManager;
private TokenStore redisTokenStore;
#Autowired
private UserApprovalHandler userApprovalHandler;
#Autowired
private RedisConnectionFactory redisConnectionFactory;
#Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.tokenKeyAccess("isAuthenticated()").checkTokenAccess("isAuthenticated()")
.allowFormAuthenticationForClients().realm(REALM + "/client");
}
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(clientService);
}
#Bean
#Autowired
public TokenStore tokenStore(RedisConnectionFactory redisConnectionFactory) {
this.redisTokenStore = new RedisTokenStore(redisConnectionFactory);
return this.redisTokenStore;
}
#Bean
public WebResponseExceptionTranslator loggingExceptionTranslator() {
return new DefaultWebResponseExceptionTranslator() {
#Override
public ResponseEntity<OAuth2Exception> translate(Exception e) throws Exception {
// This is the line that prints the stack trace to the log. You can customise
// this to format the trace etc if you like
e.printStackTrace();
// Carry on handling the exception
ResponseEntity<OAuth2Exception> responseEntity = super.translate(e);
HttpHeaders headers = new HttpHeaders();
headers.setAll(responseEntity.getHeaders().toSingleValueMap());
OAuth2Exception excBody = responseEntity.getBody();
return new ResponseEntity<>(excBody, headers, responseEntity.getStatusCode());
}
};
}
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(redisTokenStore).userApprovalHandler(userApprovalHandler)
.authenticationManager(authenticationManager).exceptionTranslator(loggingExceptionTranslator());
}
public void setRedisConnectionFactory(RedisConnectionFactory redisConnectionFactory) {
this.redisConnectionFactory = redisConnectionFactory;
}
#Bean
public TokenStoreUserApprovalHandler userApprovalHandler() {
TokenStoreUserApprovalHandler handler = new TokenStoreUserApprovalHandler();
handler.setTokenStore(redisTokenStore);
handler.setRequestFactory(new DefaultOAuth2RequestFactory(clientService));
handler.setClientDetailsService(clientService);
return handler;
}
#Bean
#Autowired
public ApprovalStore approvalStore() throws Exception {
TokenApprovalStore store = new TokenApprovalStore();
store.setTokenStore(redisTokenStore);
return store;
}
#Bean
#Primary
#Autowired
public DefaultTokenServices tokenServices() {
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setSupportRefreshToken(true);
tokenServices.setTokenStore(redisTokenStore);
return tokenServices;
}
}
#Component
class MyOAuth2AuthenticationEntryPoint extends OAuth2AuthenticationEntryPoint {
}
#Configuration
#EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private MyOAuth2AuthenticationEntryPoint authenticationEntryPoint;
private AccessDecisionManager accessDecisionManager;
private MongoDBAuthenticationProvider authenticationProvider;
#Autowired
public SecurityConfiguration( MyOAuth2AuthenticationEntryPoint authenticationEntryPoint,AccessDecisionManager accessDecisionManager, MongoDBAuthenticationProvider authenticationProvider) {
this.authenticationEntryPoint = authenticationEntryPoint;
this.accessDecisionManager = accessDecisionManager;
this.authenticationProvider = authenticationProvider;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
/*.addFilterAfter(
new CustomFilter(), BasicAuthenticationFilter.class)*/
.csrf().disable()
.anonymous().disable()
.authorizeRequests()
.antMatchers("/oauth/token").fullyAuthenticated()
.and()
.httpBasic()
.authenticationEntryPoint(authenticationEntryPoint)
.and().exceptionHandling().accessDeniedHandler(new OAuth2AccessDeniedHandler());
http
/*.addFilterAfter(
new CustomFilter(), BasicAuthenticationFilter.class)*/
.csrf().disable()
.anonymous().disable()
.authorizeRequests()
.antMatchers("/oauth/check_token").fullyAuthenticated()
.and()
.httpBasic()
.authenticationEntryPoint(authenticationEntryPoint)
.and().exceptionHandling().accessDeniedHandler(new OAuth2AccessDeniedHandler())
.and()
.authorizeRequests()
.anyRequest()
.authenticated()
.accessDecisionManager(accessDecisionManager);
}
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider)
/*.inMemoryAuthentication()
.withUser("javabycode").password("123456").roles("USER")
.and()
.withUser("admin").password("admin123").roles("ADMIN");*/;
}
}
Following is my sample Spring Security configuration.
I want all /api to return HTTP 401 code instead of redirecting 302 to login page.
Also I want to keep the redirect feature for old web pages.
<security:http auto-config='true' use-expressions="true" >
<security:intercept-url pattern="/api*" access="hasRole('USER')" />
<security:intercept-url pattern="/oldweb*" access="hasRole('USER')" />
<security:form-login login-page="/login.jsp" authentication-failure-url="/login.jsp?login_error=1" default-target-url="/home"/>
</security:http>
I came to more simpler solution.
In Spring Boot and Java config you just have to register additional entry point in addition to default one. And because all your rest-services resides in "/api" name space, you could use AntPathRequestMatcher("/api/**") to match necessary requests.
So, final solution is:
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.exceptionHandling()
//Actually Spring already configures default AuthenticationEntryPoint - LoginUrlAuthenticationEntryPoint
//This one is REST-specific addition to default one, that is based on PathRequest
.defaultAuthenticationEntryPointFor(getRestAuthenticationEntryPoint(), new AntPathRequestMatcher("/api/**"));
}
private AuthenticationEntryPoint getRestAuthenticationEntryPoint() {
return new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED);
}
}
You need to have a custom authentication entry point.
public class CustomEntryPoint extends LoginUrlAuthenticationEntryPoint {
private static final String XML_HTTP_REQUEST = "XMLHttpRequest";
private static final String X_REQUESTED_WITH = "X-Requested-With";
public CustomEntryPoint(String loginFormUrl) {
super(loginFormUrl);
}
#Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception)
throws IOException, ServletException {
if (XML_HTTP_REQUEST.equals(request.getHeader(X_REQUESTED_WITH))) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
} else {
super.commence(request, response, exception);
}
}
}
Finally change your config to this:
<security:http auto-config='true' use-expressions="true" entry-point-ref="customEntryPoint">
<security:intercept-url pattern="/api*" access="hasRole('USER')" />
<security:intercept-url pattern="/oldweb*" access="hasRole('USER')" />
<security:form-login login-page="/login.jsp" authentication-failure-url="/login.jsp?login_error=1" default-target-url="/home"/>
<beans:bean id="customEntryPoint" class="CustomEntryPoint">
<beans:constructor-arg value="/login"/>
</beans:bean>
</security:http>
When text/html is not acceptable, return 401.
#EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(final HttpSecurity http) throws Exception {
// ...
final RequestMatcher matcher = new NegatedRequestMatcher(
new MediaTypeRequestMatcher(MediaType.TEXT_HTML));
http.exceptionHandling()
.defaultAuthenticationEntryPointFor(
new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED), matcher);
}
}
I am currently moving from an xml-based configuration of spring security to a java based one. I need to setup a custom WebAuthenticationDetails via Java. Is there a way to do that? In XML, I would just set that authenticationDetailsSource of the UsernamePasswordAuthenticationFilter. Relevant sample below
<http entry-point-ref="loginUrlAuthenticationEntryPoint">
<custom-filter ref="rememberMeFilter" position="REMEMBER_ME_FILTER"/>
<custom-filter ref="loginFilter" position="FORM_LOGIN_FILTER"/>
<intercept-url pattern="/access/**" access="ROLE_USER" />
<csrf/>
<access-denied-handler error-page="/login" />
<logout logout-success-url="/login?logout" />
</http>
<beans:bean id="myWebAuthDetails"
class="com.auth.CustomWebAuthenticationDetailsSource">
</beans:bean>
<beans:bean id="loginFilter" class="org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter">
<beans:property name="authenticationManager" ref="authenticationManager" />
<beans:property name="rememberMeServices" ref="rememberMeServices" />
<beans:property name="usernameParameter" value="username" />
<beans:property name="passwordParameter" value="password" />
<beans:property name="sessionAuthenticationStrategy" ref="sas" />
<beans:property name="allowSessionCreation" value="true" />
<beans:property name="authenticationDetailsSource" ref="myWebAuthDetails" />
<beans:property name="authenticationFailureHandler" ref="failureHandler" />
<beans:property name="authenticationSuccessHandler" ref="successHandler" />
<beans:property name="filterProcessesUrl" value="/processlogin" />
</beans:bean>
Please find the configuration below. I have mapped your xml configuration to Java config.
#Configuration
#EnableWebMvcSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/", "/home").permitAll()
.anyRequest().authenticated().and().formLogin()
.authenticationDetailsSource(authenticationDetailsSource())
.successHandler(authenticationSuccessHandler())
.failureHandler(failureHandler()).loginPage("/login")
.usernameParameter("usernameCustom")
.passwordParameter("passwordCustom").permitAll().and().logout()
.permitAll().and().rememberMe()
.rememberMeServices(rememberMeServices());
}
private AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> authenticationDetailsSource() {
return new AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails>() {
#Override
public WebAuthenticationDetails buildDetails(
HttpServletRequest request) {
return new WebAuthenticationDetails(request);
}
};
}
#Bean
RememberMeServices rememberMeServices() {
RememberMeServices rememberMeServices = new RememberMeServices() {
#Override
public void loginSuccess(HttpServletRequest arg0,
HttpServletResponse arg1, Authentication arg2) {
}
#Override
public void loginFail(HttpServletRequest arg0,
HttpServletResponse arg1) {
}
#Override
public Authentication autoLogin(HttpServletRequest arg0,
HttpServletResponse arg1) {
return null;
}
};
return rememberMeServices;
}
#Bean
AuthenticationFailureHandler failureHandler() {
return new AuthenticationFailureHandler() {
#Override
public void onAuthenticationFailure(HttpServletRequest req,
HttpServletResponse res, AuthenticationException arg2)
throws IOException, ServletException {
req.setAttribute("error", "forward");
req.getRequestDispatcher("/homedefault").forward(req, res);
}
};
}
#Bean
AuthenticationSuccessHandler authenticationSuccessHandler() {
return new AuthenticationSuccessHandler() {
#Override
public void onAuthenticationSuccess(HttpServletRequest req,
HttpServletResponse res, Authentication arg2)
throws IOException, ServletException {
res.sendRedirect("homedefault");
}
};
}
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth)
throws Exception {
auth.inMemoryAuthentication().withUser("user").password("password")
.roles("USER");
}
}
I would have one question regarding the configuration of spring-security-oauth2 2.0.7 please.
I am doing the Authentication using LDAP via a GlobalAuthenticationConfigurerAdapter:
#SpringBootApplication
#Controller
#SessionAttributes("authorizationRequest")
public class AuthorizationServer extends WebMvcConfigurerAdapter {
public static void main(String[] args) {
SpringApplication.run(AuthorizationServer.class, args);
}
#Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/login").setViewName("login");
registry.addViewController("/oauth/confirm_access").setViewName("authorize");
}
#Configuration
public static class JwtConfiguration {
#Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
KeyPair keyPair = new KeyStoreKeyFactory(
new ClassPathResource("keystore.jks"), "foobar".toCharArray())
.getKeyPair("test");
converter.setKeyPair(keyPair);
return converter;
}
#Bean
public JwtTokenStore jwtTokenStore(){
return new JwtTokenStore(jwtAccessTokenConverter());
}
}
#Configuration
#EnableAuthorizationServer
public static class OAuth2Config extends AuthorizationServerConfigurerAdapter implements EnvironmentAware {
private static final String ENV_OAUTH = "authentication.oauth.";
private static final String PROP_CLIENTID = "clientid";
private static final String PROP_SECRET = "secret";
private static final String PROP_TOKEN_VALIDITY_SECONDS = "tokenValidityInSeconds";
private RelaxedPropertyResolver propertyResolver;
#Inject
private AuthenticationManager authenticationManager;
#Inject
private JwtAccessTokenConverter jwtAccessTokenConverter;
#Inject
private JwtTokenStore jwtTokenStore;
#Inject
private UserDetailsService userDetailsService;
#Override
public void setEnvironment(Environment environment) {
this.propertyResolver = new RelaxedPropertyResolver(environment, ENV_OAUTH);
}
#Bean
#Primary
public DefaultTokenServices tokenServices() {
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setSupportRefreshToken(true);
tokenServices.setTokenStore(jwtTokenStore);
tokenServices.setAuthenticationManager(authenticationManager);
return tokenServices;
}
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager).tokenStore(jwtTokenStore).accessTokenConverter(
jwtAccessTokenConverter).userDetailsService(userDetailsService);
}
#Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer)
throws Exception {
oauthServer.tokenKeyAccess("permitAll()").checkTokenAccess(
"isAuthenticated()");
}
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient(propertyResolver.getProperty(PROP_CLIENTID))
.scopes("read", "write")
.authorities(AuthoritiesConstants.ADMIN, AuthoritiesConstants.USER)
.authorizedGrantTypes("authorization_code", "refresh_token", "password")
.secret(propertyResolver.getProperty(PROP_SECRET))
.accessTokenValiditySeconds(propertyResolver.getProperty(PROP_TOKEN_VALIDITY_SECONDS, Integer.class, 1800));
}
}
#Configuration
#Order(-10)
protected static class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.formLogin().loginPage("/login").permitAll()
.and()
.requestMatchers().antMatchers("/login", "/oauth/authorize", "/oauth/confirm_access")
.and()
.authorizeRequests().anyRequest().authenticated();
}
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Bean
#Override
public UserDetailsService userDetailsServiceBean() throws Exception {
return super.userDetailsServiceBean();
}
}
#Configuration
protected static class AuthenticationConfiguration extends
GlobalAuthenticationConfigurerAdapter {
#Override
public void init(AuthenticationManagerBuilder auth) throws Exception {
auth
.ldapAuthentication()
.userDnPatterns("uid={0},ou=people")
.groupSearchBase("ou=groups")
.contextSource().ldif("classpath:test-server.ldif");
}
}
}
While the refresh token works fine with the release 2.0.6 of spring-security-oauth2, it does not work anymore with the version 2.0.7.
As read here, one should set the AuthenticationManager to be used when trying to get a new access token during the refresh.
As far as I understand, this has something to do with the following change of spring-security-oauth2.
I unfortunately did not manage to set it up properly.
org.springframework.security.oauth2.provider.token.DefaultTokenServices#setAuthenticationManager
is called and gets an AuthenticationManager injected. I an not sure I understand how the LdapUserDetailsService is then going to be injected. The only thing I see is that the PreAuthenticatedAuthenticationProvider is going to be called while trying to re-authenticate the user during the token refresh call.
Can anyone advise me on how to do it please?
ps: The exception I am getting is the following:
p.PreAuthenticatedAuthenticationProvider : PreAuthenticated authentication request: org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken#5775: Principal: org.springframework.security.authentication.UsernamePasswordAuthenticationToken#441d5545: Principal: bob; Credentials: [PROTECTED]; Authenticated: true; Details: null; Granted Authorities: ROLE_USER; Credentials: [PROTECTED]; Authenticated: true; Details: null; Granted Authorities: ROLE_USER
o.s.s.o.provider.endpoint.TokenEndpoint : Handling error: IllegalStateException, UserDetailsService is required.
I had a similar issue when I was implementing a a OAuth2 server with JWT tokens with a custom AuthenticationProvider instead of a UserDetailsService implementation to solve login authentications.
But lately I found that the error Spring raises is correct if you want the refresh_token working correctly. For an AuthenticationProvider implementation is impossible to refresh a token with a refresh_token, because in that kind of implementation you have to resolve if the password is correct, but the refresh token doesn't have that information. However, UserDetailsService is agnostic of the password.
The version 2.0.6 of spring-security-oauth2 works because never checks the user grants, just checks if the refresh token is valid (signed with the private key), but, if the user was deleted from the system after a first login, with a refresh token the deleted user will have infinite time access to your system, that is a big security issue.
Take a look to the issue I reported with this: https://github.com/spring-projects/spring-security-oauth/issues/813
What you need for the OAuth piece is to create an LdapUserDetailsService with the same query as you authenticator and inject it into the AuthorizationServerEndpointsConfigurer. I don't think there's any support for creating a UserDetailService in #Configuration style (might be worth opening a ticket for that in JIRA), but it looks like you can do it in XML.
As advised by Dave Syer, I created a custom LdapUserDetailsService.
The working solution can be found under the following tag.
Application Context
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<context:annotation-config/>
<context:property-placeholder location="application.yml"/>
<bean id="contextSource" class="org.springframework.security.ldap.DefaultSpringSecurityContextSource">
<constructor-arg value="${authentication.ldap.url}" />
</bean>
<bean id="userSearch" class="org.springframework.security.ldap.search.FilterBasedLdapUserSearch">
<constructor-arg index="0" value="${authentication.ldap.userSearchBase}" />
<constructor-arg index="1" value="uid={0}" />
<constructor-arg index="2" ref="contextSource"/>
</bean>
<bean id="ldapAuthoritiesPopulator" class="org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator">
<constructor-arg index="0" ref="contextSource"/>
<constructor-arg index="1" value="${authentication.ldap.groupSearchBase}"/>
<property name="groupSearchFilter" value="${authentication.ldap.groupSearchFilter}"/>
</bean>
<bean id="myUserDetailsService"
class="org.springframework.security.ldap.userdetails.LdapUserDetailsService">
<constructor-arg index="0" ref="userSearch"/>
<constructor-arg index="1" ref="ldapAuthoritiesPopulator"/>
</bean>
</beans>
Properties
authentication:
ldap:
url: ldap://127.0.0.1:33389/dc=springframework,dc=org
userSearchBase:
userDnPatterns: uid={0},ou=people
groupSearchBase: ou=groups
groupSearchFilter: (uniqueMember={0})
I am not very familiar with spring, so added everything related to spring.
I have a spring 4 struts 1.x application.
Spring context is loaded using struts plugin which points to spring-context.xml
<plug-in className="org.springframework.web.struts.ContextLoaderPlugIn">
<set-property property="contextConfigLocation"
value="/WEB-INF/spring-context.xml"/>
</plug-in>
In spring-context.xml I have configured Jpa's EntityManager like so:
<context:component-scan base-package="com.epam.testsystem" />
<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalEntityManagerFactoryBean">
<property name="persistenceUnitName" value="primary"/>
</bean>
<!-- Transactions -->
<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="entityManagerFactory" />
</bean>
I also have Spring Security integrated using
public class SecurityWebApplicationInitializer
extends AbstractSecurityWebApplicationInitializer {
public SecurityWebApplicationInitializer() {
super(SecurityConfig.class);
}
}
and
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(securedEnabled = true)
#ImportResource("/WEB-INF/spring-context.xml")
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private UserDetailsService userDetailsService;
#Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
#Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/welcome.do").hasRole("ADMIN")
.and()
.formLogin()
.loginPage("/login.do")
.loginProcessingUrl("/login")
.failureUrl("/login?error=1")
.permitAll()
.and()
.csrf();
}
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
AND IT WORKS WITHOUT ERRORS.
Now I decided to move the configuration of EntityManager to java Config. So I deleted bean definitions from spring-context.xml added those beans to SecurityConfig
#Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
em.setPersistenceUnitName("primary");
return em;
}
#Bean
public PlatformTransactionManager transactionManager(EntityManagerFactory emf){
JpaTransactionManager transactionManager = new JpaTransactionManager();
transactionManager.setEntityManagerFactory(emf);
return transactionManager;
}
But now I get this Exception:
Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'entityManagerFactory': Requested bean is currently in creation: Is there an unresolvable circular reference?"
I think that happened because I use #PersistenceContext annotation in UserDaoImpl, before I imported it into Java Config.
So what is the most clean way to handle this?
Thanks
One of the proposed solution is
#Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
em.setPersistenceUnitName("primary");
return em;
}
#Bean
public PlatformTransactionManager transactionManager(){
JpaTransactionManager transactionManager = new JpaTransactionManager();
// SOlution below
transactionManager.setEntityManagerFactory(entityManagerFactory());
return transactionManager;
}
Here we pass the dependent bean via entityManagerFactory()
Kindly let me know if this fulfills the solution
I made it work by creating a separate PersistenceConfig
#Configuration
#EnableTransactionManagement
#EnableJpaRepositories(basePackages="com.epam.testsystem.repository")
public class PersistenceConfig {
#Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
em.setJpaVendorAdapter(vendorAdapter);
em.setPersistenceUnitName("primary");
return em;
}
#Bean
public PlatformTransactionManager transactionManager(EntityManagerFactory emf){
JpaTransactionManager transactionManager = new JpaTransactionManager();
transactionManager.setEntityManagerFactory(emf);
return transactionManager;
}
}
and import Java Config to xml config (spring-context.xml):
<bean class="com.epam.testsystem.config.PersistenceConfig" />