EnableOAuth2Sso Access token expires after 1 hour of activity and UserRedirectRequiredException thrown - spring

I have implemented SSO using spring-security-oauth2-autoconfigure 2.1.8.RELEASE and it works fine on first launch. I am able to login and call my backend api.
After one hour of activity, the REST calls to my backend api fails due to UserRedirectRequiredException because the access token has expired and OAuth2RestTemplate is unable to refresh it. I get this stack:
org.springframework.security.oauth2.client.resource.UserRedirectRequiredException: A redirect is required to get the users approval
at org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeAccessTokenProvider.getRedirectForAuthorization(AuthorizationCodeAccessTokenProvider.java:359)
at org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeAccessTokenProvider.obtainAccessToken(AuthorizationCodeAccessTokenProvider.java:205)
at org.springframework.security.oauth2.client.token.AccessTokenProviderChain.obtainNewAccessTokenInternal(AccessTokenProviderChain.java:148)
at org.springframework.security.oauth2.client.token.AccessTokenProviderChain.obtainAccessToken(AccessTokenProviderChain.java:121)
at org.springframework.security.oauth2.client.OAuth2RestTemplate.acquireAccessToken(OAuth2RestTemplate.java:221)
at org.springframework.security.oauth2.client.OAuth2RestTemplate.getAccessToken(OAuth2RestTemplate.java:173)
My App Configuration code is:
#Configuration
#EnableOAuth2Sso
#Order(value = 0)
public class AppConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
private Environment env;
#Autowired
OAuth2ProtectedResourceDetails oAuth2ProtectedResourceDetails;
#Autowired
OAuth2ClientContext oauth2ClientContext;
#Autowired
private OAuth2ClientContextFilter oauth2ClientContextFilter;
#Value("${security.oauth2.resource.user-info-uri}")
private String userInfoUri;
#Value("${security.oauth2.client.access-token-uri}")
private String accessTokenUri;
#Value("${security.oauth2.client.user-authorization-uri}")
private String userAuthorizationUri;
#Value("${security.oauth2.client.client-id}")
private String clientID;
#Value("${security.oauth2.client.client-secret}")
private String clientSecret;
#Value("${security.oauth2.client.pre-established-redirect-uri}")
private String preEstRedirectUri;
#Value("#{'${security.oauth2.client.scope}'.split(' ')}")
private List<String> scope;
#Override
public void configure(HttpSecurity http) throws Exception {
String logoutUrl = env.getProperty("endSessionEndpoint") + "?post_logout_redirect_uri=" +
URLEncoder.encode(env.getProperty("homePage"), "UTF-8");
http.requiresChannel().anyRequest().requiresSecure();
http.antMatcher("/**")
.authorizeRequests(a -> a
.antMatchers("/", "/static/**", "/webjars/**", "/login**", "/error**", "/js/**", "/css/**", "/img/**").permitAll()
.anyRequest().authenticated()
)
.csrf(c -> c
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
)
.logout(l -> l
.deleteCookies()
.invalidateHttpSession(true)
.logoutSuccessUrl(logoutUrl)
);
http.addFilterAfter(oauth2ClientContextFilter, SecurityContextPersistenceFilter.class);
http.addFilterBefore(swqSSOFilter(), BasicAuthenticationFilter.class);
http.exceptionHandling().authenticationEntryPoint(unauthorizedEntryPoint());
}
private Filter swqSSOFilter() {
private Filter swqSSOFilter() {
OAuth2ClientAuthenticationProcessingFilter azureSsoFilter = new OAuth2ClientAuthenticationProcessingFilter(preEstRedirectUri);
OAuth2RestTemplate oauth2RestTemplate = new OAuth2RestTemplate(oAuth2ProtectedResourceDetails, oauth2ClientContext);
azureSsoFilter.setRestTemplate(oauth2RestTemplate);
UserInfoTokenServices tokenServices = new UserInfoTokenServices(userInfoUri, oAuth2ProtectedResourceDetails.getClientId());
tokenServices.setRestTemplate(oauth2RestTemplate);
azureSsoFilter.setTokenServices(tokenServices);
return azureSsoFilter;
}
#Bean
public AuthenticationEntryPoint unauthorizedEntryPoint() {
return (request, response, authException) -> response.sendRedirect(preEstRedirectUri);
}
#Bean
public OAuth2RestTemplate oauth2RestTemplate(OAuth2ClientContext oauth2ClientContext, OAuth2ProtectedResourceDetails details) {
return new OAuth2RestTemplate(details, oauth2ClientContext);
}
#Bean
public FilterRegistrationBean<OAuth2ClientContextFilter> oauth2ClientFilterRegistration(
OAuth2ClientContextFilter filter) {
FilterRegistrationBean<OAuth2ClientContextFilter> registration = new FilterRegistrationBean<OAuth2ClientContextFilter>();
registration.setFilter(filter);
registration.setOrder(-100);
return registration;
}
}
I followed a lot of stackoverflow posts, but none of the solutions worked for me.
Can someone advise what I am doing wrong?

Moving answer from comments,
To be specific in order to increase your token lifetime you need to implement refresh token in your code.Please refer ms docs.

Related

Method security ignored when using Jersey with Spring Security

I'm having trouble getting Spring-Security to work with Jersey within my Spring Boot project. It seems to be something to do with the way the two are integrated but I can't figure out why...
In Jersey endpoints the Spring Security enforced authentication requirement (configured in the WebSecurityConfigurerAdapter) works fine however some method security annotations are ignored. JSR-250 annotations (such as #DenyAll, #RolesAllowed, etc) seem to work if one registers the RolesAllowedDynamicFeature feature. However, #PreAuthorize, #PostAuthorize, #PreFilter, #PostFilter don't work.
Also, when logging in via the /login endpoint the XSRF-TOKEN cookie is not sent. Normally Spring security handles this and if I rewrite the endpoint with Spring MVC, I get both the XSRF-TOKEN and my own ACCESS-TOKEN (generated and sent by the endpoint itself).
(I found a workaround to this - see end of post)
Here's some code (I left out the obvious stuff like JwtUtils, general bean config, etc). Full source (not that there's much - this is a pared-down version of the real project) can be found at https://github.com/ChambreNoire/jersey-issue.
Thanks!
(I'm using Spring Boot 2.3.10.REALEASE and upgrading isn't currently an option)
application.yaml
spring:
jersey:
application-path: /resources
servlet:
load-on-startup: 1
type: filter
JerseyConfig
#Configuration
public class JerseyConfig {
#Bean
public ResourceConfig resourceConfig(ObjectMapper objectMapper) {
return new ResourceConfig()
.property(ServletProperties.FILTER_FORWARD_ON_404, true)
.register((ContextResolver<ObjectMapper>) aClass -> objectMapper)
.register(JacksonFeature.class)
.register(HttpMethodOverrideFilter.class)
.register(AuthResource.class)
.register(UserResource.class);
}
}
SecurityConfigurationAdapter
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class SecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
private static final String[] PUBLIC_PATTERNS;
private final JwtTokenAuthFilter jwtTokenAuthFilter;
private final JwtTokenAuthEntryPoint unauthorizedHandler;
#Autowired
public SecurityConfigurationAdapter(final JwtTokenAuthFilter jwtTokenAuthFilter, final JwtTokenAuthEntryPoint unauthorizedHandler) {
this.jwtTokenAuthFilter = jwtTokenAuthFilter;
this.unauthorizedHandler = unauthorizedHandler;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.cors()
.configurationSource(corsConfig()).and()
.csrf()
.ignoringAntMatchers("/login", "/login2")
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()).and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.exceptionHandling()
.authenticationEntryPoint(unauthorizedHandler).and()
.authorizeRequests()
.antMatchers("/login", "/login2").permitAll()
.anyRequest().authenticated().and()
.addFilterBefore(jwtTokenAuthFilter, UsernamePasswordAuthenticationFilter.class)
.headers()
.xssProtection().and()
.contentSecurityPolicy("script-src 'self';require-trusted-types-for 'script';object-src 'none';");
}
private CorsConfigurationSource corsConfig() {
return request -> {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(singletonList("http://localhost:9000"));
config.setAllowedMethods(singletonList("*"));
config.setAllowCredentials(true);
config.setAllowedHeaders(singletonList("*"));
config.setExposedHeaders(singletonList("Authorization"));
config.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return config;
};
}
}
JwtTokenAuthFilter
#Priority(Priorities.AUTHENTICATION)
public class JwtTokenAuthFilter extends OncePerRequestFilter {
private final UserDetailsService userDetailsService;
private final JwtTokenUtils jwtTokenUtils;
public JwtTokenAuthFilter(final UserDetailsService userDetailsService, final JwtTokenUtils jwtTokenUtils) {
this.userDetailsService = userDetailsService;
this.jwtTokenUtils = jwtTokenUtils;
}
#Override
protected void doFilterInternal(final HttpServletRequest req, final HttpServletResponse resp, final FilterChain chain)
throws ServletException, IOException {
if (req.getCookies() == null) {
chain.doFilter(req, resp);
return;
}
String token = Arrays.stream(req.getCookies())
.filter(c -> "ACCESS-TOKEN".equals(c.getName()))
.findFirst()
.map(Cookie::getValue)
.orElse(null);
if (isEmpty(token) || !jwtTokenUtils.validateJwtToken(token)) {
chain.doFilter(req, resp);
return;
}
String username = jwtTokenUtils.getUserNameFromJwtToken(token);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(req));
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(req, resp);
}
}
AuthResource
#Singleton
#Path("")
#Produces(MediaType.APPLICATION_JSON)
#Consumes(MediaType.APPLICATION_JSON)
public class AuthResource {
private final AuthenticationManager authenticationManager;
private final JwtTokenUtils jwtTokenUtils;
#Inject
public AuthResource(AuthenticationManager authenticationManager, JwtTokenUtils jwtTokenUtils) {
this.authenticationManager = authenticationManager;
this.jwtTokenUtils = jwtTokenUtils;
}
#POST
#Path("/login")
public Response login(#RequestBody LoginRequest request) {
try {
org.springframework.security.core.Authentication authentication = authenticationManager
.authenticate(
new UsernamePasswordAuthenticationToken(
request.getEmail(), request.getPassword()
)
);
UserDetailsImpl user = (UserDetailsImpl) authentication.getPrincipal();
return Response.ok()
.cookie(new NewCookie(
"ACCESS-TOKEN",
jwtTokenUtils.generateJwtToken(user),
"/", null, null,
NewCookie.DEFAULT_MAX_AGE,
true, true
))
.build();
} catch (BadCredentialsException ex) {
return Response.status(Response.Status.UNAUTHORIZED).build();
}
}
}
If anyone could enlighten me on why this is the case that would be most appreciated, or indeed any general comments/improvements you may have !
Cheers
UPDATE
It looks like adding the JWT cookie to the javax.ws.rs.core.Response is causing the issue although I'm not certain how. If I add it directly to the HttpServletResponse, the XSRF-TOKEN cookie is present in the response.
Here's the updated Jersey authentication endpoint :
#POST
#Path("/login")
public Response authenticateUser(#Context HttpServletResponse response, #RequestBody LoginRequest request) {
try {
Authentication authentication = authenticationManager
.authenticate(
new UsernamePasswordAuthenticationToken(
request.getEmail(), request.getPassword()
)
);
UserDetailsImpl user = (UserDetailsImpl) authentication.getPrincipal();
Cookie cookie = new Cookie("ACCESS-TOKEN", jwtTokenUtils.generateJwtToken(user));
cookie.setPath("/");
cookie.setSecure(true);
cookie.setHttpOnly(true);
cookie.setMaxAge(-1);
response.addCookie(cookie);
return Response.ok().build();
} catch (BadCredentialsException ex) {
return Response.status(Response.Status.UNAUTHORIZED).build();
}
}

Log user information on failed attempt

I want to be able to access my user details when login fail so that I can count the number of times of failed attempt a user has. How can i access the userdetails and save it to my database on failure? I read somewhere that AuthenticationFailureHandler might a be a solution to this but from what I see, it works only with formLogin?
I have a signin endpoint
#CrossOrigin
#RequestMapping(value = "/signin", method = RequestMethod.POST)
#ApiOperation(value = "Sign in endpoint", notes = "You have to provide a valid login request")
public ResponseEntity<?> authenticateUser(#ApiParam(value = "The login request", required = true)
#Valid #RequestBody LoginRequest loginRequest) {
Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword()));
UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal();
// Set authentication so that when /authenticate, we can retrieve the authenticated user
. . .
This is my authentrypointjwt when authenticate fails.
#Component
public class AuthEntryPointJwt implements AuthenticationEntryPoint {
private static final Logger logger = LoggerFactory.getLogger(AuthEntryPointJwt.class);
#Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
logger.info("> AuthEntryPointJwt");
logger.error("Unauthorized error: {}", authException.getMessage());
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Error: Unauthorized");
}
}
This is my websecurity
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private static final Logger LOGGER = LoggerFactory.getLogger(WebSecurityConfig.class);
#Autowired
UserDetailsServiceImpl userDetailsService;
#Autowired
private AuthEntryPointJwt unauthorizedHandler;
#Bean
public AuthTokenFilter authenticationJwtTokenFilter() {
LOGGER.info("> AuthenticationManagerBuilder authenticationJwtTokenFilter");
return new AuthTokenFilter();
}
#Override
public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
LOGGER.info("> AuthenticationManagerBuilder authenticationManagerBuilder");
authenticationManagerBuilder.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
LOGGER.info("> AuthenticationManagerBuilder authenticationManagerBean");
return super.authenticationManagerBean();
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable()
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests()
.antMatchers("/test/**").authenticated()
.antMatchers("/signup").hasAuthority("SUPER_ADMIN");
http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);
}
#Bean
public CorsConfigurationSource corsConfigurationSource() {
final CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("*"));
configuration.setAllowedMethods(Arrays.asList("HEAD",
"GET", "POST", "PUT", "DELETE", "PATCH"));
configuration.setAllowCredentials(true);
configuration.setAllowedHeaders(Arrays.asList("*"));
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
Yes. AuthenticationFailureHandler will only be called if you customise the filter for authentication by extending AbstractAuthenticationProcessingFilter like what formLogin does.
But it seems that you are now implementing a customised authentication way using spring-mvc-rest , and you can do the following which equivalents to what AbstractAuthenticationProcessingFilter does to invoke AuthenticationFailureHandler :
#RestController
public void AuthenticateController {
#Autowired AuthenticationFailureHandler failureHandler;
#RequestMapping(value = "/signin", method = RequestMethod.POST)
public ResponseEntity<?> authenticateUser(LoginRequest loginRequest, HttpServletRequest request, HttpServletResponse response) {
try {
Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword()));
}catch (AuthenticationException failed) {
failureHandler.onAuthenticationFailure(request, response, failed);
}
}
}
P.S. As you are customising authentication using spring-mvc-rest rather than following the spring security infrastructure to implement it based on the Servlet Filter , I assume that you also need to configure spring security to totally ignore AuthenticateController and no other spring security feature will be applied to it . I normally will follow the spring security infrastructure which customize the authentication process based on Servlet Filter as it is more compatible with the spring security ecosystem.

Can i use two different tables for login in my spring boot application by spring security?

In my current project I have two separate entities.
User :- TO authenticate users
Customer :- To authenticate customers
I'm confuse How will we manage login process in same project for two separate entities by spring security?
As of now its working with one User entity , now i have to integrate one more login for customers with the help of Customer table.
is it possible ?
Note :- Due to some another requirement i can't use the same table for both users and customer.
I am sharing some code for more clearance.
Below code is the implementation of user detail service.
private final Logger log = LoggerFactory.getLogger(DomainUserDetailsService.class);
private final UserLoginRepository userRepository;
public DomainUserDetailsService(UserLoginRepository userRepository) {
this.userRepository = userRepository;
}
#Override
#Transactional
public UserDetails loadUserByUsername(final String login) {
log.debug("Authenticating {}", login);
if (new EmailValidator().isValid(login, null)) {
Optional<User> userByEmailFromDatabase = userRepository.findOneWithAuthoritiesByLogin(login);
return userByEmailFromDatabase.map(user -> createSpringSecurityUser(login, user))
.orElseThrow(() -> new UsernameNotFoundException("User with email " + login + " was not found in the database"));
}
String lowercaseLogin = login.toLowerCase(Locale.ENGLISH);
Optional<User> userByLoginFromDatabase = userRepository.findOneWithAuthoritiesByLogin(lowercaseLogin);
return userByLoginFromDatabase.map(user -> createSpringSecurityUser(lowercaseLogin, user))
.orElseThrow(() -> new UsernameNotFoundException("User " + lowercaseLogin + " was not found in the database"));
}
private org.springframework.security.core.userdetails.User createSpringSecurityUser(String lowercaseLogin, User user) {
List<GrantedAuthority> grantedAuthorities = user.getAuthorities().stream()
.map(authority -> new SimpleGrantedAuthority(authority.getName()))
.collect(Collectors.toList());
return new org.springframework.security.core.userdetails.User(user.getLogin(),
user.getPassword(),
grantedAuthorities);
}
}
Below is the implantation of security config class.
private final AuthenticationManagerBuilder authenticationManagerBuilder;
private final UserDetailsService userDetailsService;
private final TokenProvider tokenProvider;
private final CorsFilter corsFilter;
private final SecurityProblemSupport problemSupport;
public SecurityConfiguration(AuthenticationManagerBuilder authenticationManagerBuilder, UserDetailsService userDetailsService,TokenProvider tokenProvider,CorsFilter corsFilter, SecurityProblemSupport problemSupport) {
this.authenticationManagerBuilder = authenticationManagerBuilder;
this.userDetailsService = userDetailsService;
this.tokenProvider = tokenProvider;
this.corsFilter = corsFilter;
this.problemSupport = problemSupport;
}
#PostConstruct
public void init() {
try {
authenticationManagerBuilder
.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
} catch (Exception e) {
throw new BeanInitializationException("Security configuration failed", e);
}
}
#Override
#Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring()
.antMatchers(HttpMethod.OPTIONS, "/**")
.antMatchers("/app/**/*.{js,html}")
.antMatchers("/i18n/**")
.antMatchers("/content/**")
.antMatchers("/swagger-ui/index.html");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class)
.exceptionHandling()
.authenticationEntryPoint(problemSupport)
.accessDeniedHandler(problemSupport)
.and()
.csrf()
.disable()
.headers()
.frameOptions()
.disable()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/api/register").permitAll()
.antMatchers("/api/activate").permitAll()
.antMatchers("/api/userLogin").permitAll()
.antMatchers("/api/account/reset-password/init").permitAll()
.antMatchers("/api/account/reset-password/finish").permitAll()
.antMatchers("/api/**").authenticated()
.antMatchers("/management/health").permitAll()
.antMatchers("/management/info").permitAll()
.antMatchers("/management/**").hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/v2/api-docs/**").permitAll()
.antMatchers("/swagger-resources/configuration/ui").permitAll()
.antMatchers("/swagger-ui/index.html").hasAuthority(AuthoritiesConstants.ADMIN)
.and()
.apply(securityConfigurerAdapter());
}
private JWTConfigurer securityConfigurerAdapter() {
return new JWTConfigurer(tokenProvider);
}
}
Login api
#PostMapping("/userLogin")
#Timed
public Response<JWTToken> authorize(
#Valid #RequestBody UserLoginReq userLoginReq) {
Map<String, Object> responseData = null;
try {
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
userLoginReq.getUsername(), userLoginReq.getPassword());
Authentication authentication = this.authenticationManager
.authenticate(authenticationToken);
SecurityContextHolder.getContext()
.setAuthentication(authentication);
}
Yes you can pass user type combined in userName, separated by a character like :
Example:
String userName=inputUserName +":APP_USER";
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(userName, password);
in UserDetailsService.loadUserByUsername(userName)
first split get the userName part and also get userType part.
Now on userType base you can decide the user should be authenticate from which table.
String userNamePart = null;
if (userName.contains(":")) {
int colonIndex = userName.lastIndexOf(":");
userNamePart = userName.substring(0, colonIndex);
}
userNamePart = userNamePart == null ? userName : userNamePart;
String userTypePart = null;
if (userName.contains(":")) {
int colonIndex = userName.lastIndexOf(":");
userTypePart = userName.substring(colonIndex + 1, userName.length());
}
At first customer is also user, isn't it? So maybe simpler solution would be to create customer also as user (use some flag/db field usertype ={USER|CUSTOMER|...}). If you still need to manage two entities, your approach is right, but In your DetailService just implement the another method which will read customer entity and then create spring's User.
I faced the same problem too!
It gave a error message like following:
AuthenticationManager This predefined class will help you to achieve this.
Consider marking one of the beans as #Primary, updating the consumer to accept multiple beans, or using #Qualifier to identify the bean that should be consumed
To over come this issue we should use Qualifier!
Please go through the following link , it will guide you to use qualifier
This is my first answer give it a like!
https://developpaper.com/can-spring-security-dock-multiple-user-tables-at-the-same-time/
#Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Bean
#Primary
UserDetailsService us1() {
return new InMemoryUserDetailsManager(User.builder().username("javaboy").password("{noop}123").roles("admin").build());
}
#Bean
UserDetailsService us2() {
return new InMemoryUserDetailsManager(User.builder().username("sang").password("{noop}123").roles("user").build());
}
#Override
#Bean
protected AuthenticationManager authenticationManager() throws Exception {
DaoAuthenticationProvider dao1 = new DaoAuthenticationProvider();
dao1.setUserDetailsService(us1());
DaoAuthenticationProvider dao2 = new DaoAuthenticationProvider();
dao2.setUserDetailsService(us2());
ProviderManager manager = new ProviderManager(dao1, dao2);
return manager;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/hello").hasRole("user")
.antMatchers("/admin").hasRole("admin")
.and()
.formLogin()
.loginProcessingUrl("/doLogin")
.permitAll()
.and()
.csrf().disable();
}
}

Spring boot Security, Oauth2 replace access token with long-lived token from facebook

I have been following Dave Syer astounding tutorial to implement OAuth2 in microservices which provide RESTful APIs for mobile devices (Android and iOS). I have configured gateway security with the following code:
#SpringBootApplication
#EnableDiscoveryClient
#EnableZuulProxy
#EnableCircuitBreaker
#EnableFeignClients
#EnableOAuth2Client
public class GatewayApplication extends WebSecurityConfigurerAdapter {
private OAuth2ClientContext oauth2ClientContext;
private SimpleUrlAuthenticationSuccessHandler simpleUrlAuthenticationSuccessHandler;
private ScoreAuthorizationFilter scoreAuthorizationFilter;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.antMatcher("/**")
.authorizeRequests()
.antMatchers("/", "/test", "/login**", "/webjars/**", "/error**")
.permitAll()
.anyRequest()
.authenticated()
.and().logout().logoutSuccessUrl("/").permitAll()
.and().addFilterBefore(ssoFilter(), BasicAuthenticationFilter.class)
.addFilterBefore(scoreAuthorizationFilter, BasicAuthenticationFilter.class)
;
}
private Filter ssoFilter() {
OAuth2ClientAuthenticationProcessingFilter facebookFilter = new OAuth2ClientAuthenticationProcessingFilter("/login/facebook");
OAuth2RestTemplate facebookTemplate = new OAuth2RestTemplate(facebook(), oauth2ClientContext);
facebookFilter.setRestTemplate(facebookTemplate);
UserInfoTokenServices tokenServices = new UserInfoTokenServices(facebookResource().getUserInfoUri(), facebook().getClientId());
tokenServices.setRestTemplate(facebookTemplate);
facebookFilter.setTokenServices(tokenServices);
facebookFilter.setAuthenticationSuccessHandler(simpleUrlAuthenticationSuccessHandler);
return facebookFilter;
}
#Bean
#ConfigurationProperties("facebook.client")
public AuthorizationCodeResourceDetails facebook() {
return new AuthorizationCodeResourceDetails();
}
#Bean
#ConfigurationProperties("facebook.resource")
public ResourceServerProperties facebookResource() {
return new ResourceServerProperties();
}
#Bean
public FilterRegistrationBean<OAuth2ClientContextFilter> oauth2ClientFilterRegistration(OAuth2ClientContextFilter filter) {
FilterRegistrationBean<OAuth2ClientContextFilter> registration = new FilterRegistrationBean<OAuth2ClientContextFilter>();
registration.setFilter(filter);
registration.setOrder(-100);
return registration;
}
#Bean
public RequestInterceptor getFeignClientInterceptor() {
return new FeignClientInterceptor();
}
}
It turns out that the user's session expires after a while. As I dug a little deeper, I found out that Facebook doesn't provide refresh tokens. Instead, we can exchange a short-lived token for a long-lived token (Facebook long-lived token). How can I override the standard OAuth2 flow implemented in Spring Security to send another request to Facebook for getting the long-lived token and then replacing the old access token?
You can achieve what you want by extending the OAuth2ClientAuthenticationProcessingFilter class like this:
public class CustomAuthenticationProcessingFilter extends OAuth2ClientAuthenticationProcessingFilter {
private ResourceServerTokenServices tokenServices;
private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new OAuth2AuthenticationDetailsSource();
private ApplicationEventPublisher eventPublisher;
private AuthorizationCodeResourceDetails facebook;
private String longLivedTokenUri;
public CustomAuthenticationProcessingFilter(String defaultFilterProcessesUrl) {
super(defaultFilterProcessesUrl);
setAuthenticationDetailsSource(authenticationDetailsSource);
}
#Override
public void setTokenServices(ResourceServerTokenServices tokenServices) {
this.tokenServices = tokenServices;
super.setTokenServices(tokenServices);
}
#Override
public void setApplicationEventPublisher(ApplicationEventPublisher eventPublisher) {
this.eventPublisher = eventPublisher;
super.setApplicationEventPublisher(eventPublisher);
}
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
OAuth2AccessToken accessToken;
try {
accessToken = restTemplate.getAccessToken();
} catch (OAuth2Exception e) {
BadCredentialsException bad = new BadCredentialsException("Could not obtain access token", e);
publish(new OAuth2AuthenticationFailureEvent(bad));
throw bad;
}
String longLivedToken = getFromFacebook(); //Get long lived token from facebook here
try {
OAuth2Authentication result = tokenServices.loadAuthentication(longLivedToken);
if (authenticationDetailsSource != null) {
request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, longLivedToken);
request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE, accessToken.getTokenType());
result.setDetails(authenticationDetailsSource.buildDetails(request));
}
publish(new AuthenticationSuccessEvent(result));
return result;
} catch (InvalidTokenException e) {
BadCredentialsException bad = new BadCredentialsException("Could not obtain user details from token", e);
publish(new OAuth2AuthenticationFailureEvent(bad));
throw bad;
}
}
private void publish(ApplicationEvent event) {
if (eventPublisher != null) {
eventPublisher.publishEvent(event);
}
}
}
I hope this helps.

Spring Oauth 2 Facebook Authentication Redirects User To My Home Page

I am trying to redirect a user who have been authenticated to another page other than the home page. I am using spring boot 1.5.6 and Oauth 2. User is authenticated but was redirected to the home page. I don't understand why this is happening. Please, someone should help me. Some answers to related problem on stackoverflow and the internet didn't help me.
Here is my SecurityConfig file
#Configuration
#EnableGlobalAuthentication
#EnableOAuth2Client
#EnableGlobalMethodSecurity(prePostEnabled = true)
#Order(2)
public class SecurityConfig extends WebSecurityConfigurerAdapter{
protected final Log logger = LogFactory.getLog(getClass());
#Autowired
private OAuth2ClientContext oauth2ClientContext;
#Autowired
private UserDetailsService userDetailsService;
#Autowired
private GeneralConfig generalConfig;
#Override
public void configure(WebSecurity web) throws Exception {
super.configure(web);
}
#Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/user*")
.access("hasRole('CUSTOMER')")
.and()
.formLogin()
.loginPage("/loginUser")
.loginProcessingUrl("/user_login")
.failureUrl("/loginUser?error=loginError")
.defaultSuccessUrl("/customer/dashboard")
.and()
.logout()
.logoutUrl("/user_logout")
.logoutSuccessUrl("/loginUser").permitAll()
.deleteCookies("JSESSIONID")
.and()
.exceptionHandling()
.accessDeniedPage("/403")
.and()
.csrf().disable()
.addFilterBefore(ssoFilter(), BasicAuthenticationFilter.class);
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).
passwordEncoder(bCryptPasswordEncoder());
}
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws
Exception {
auth.userDetailsService(userDetailsService);
}
#Bean
public FilterRegistrationBeanoauth2ClientFilterRegistration
(OAuth2ClientContextFilter filter) {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(filter);
registration.setOrder(-100);
return registration;
}
private Filter ssoFilter(ClientResources client, String path) {
OAuth2ClientAuthenticationProcessingFilter filter = new
OAuth2ClientAuthenticationProcessingFilter(path);
OAuth2RestTemplate template = new
OAuth2RestTemplate(client.getClient(), oauth2ClientContext);
filter.setRestTemplate(template);
UserInfoTokenServices tokenServices = new
UserInfoTokenServices(client.getResource().getUserInfoUri(),
client.getClient().getClientId());
tokenServices.setRestTemplate(template);
filter.setTokenServices(tokenServices);
return filter;
}
private Filter ssoFilter() {
CompositeFilter filter = new CompositeFilter();
List<Filter> filters = new ArrayList<>();
filters.add(ssoFilter(facebook(), "/signin/facebook"));
filters.add(ssoFilter(google(), "/signin/google"));
filter.setFilters(filters);
return filter;
}
#Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
#Bean
#ConfigurationProperties("google")
public ClientResources google() {
return new ClientResources();
}
#Bean
#ConfigurationProperties("facebook")
public ClientResources facebook() {
return new ClientResources();
}
}
From the SecurityConfig I expect the user upon successful authentication to be redirected to customer/dashboard so that I can do further processing. I know the user is authenticated because I can access their data. It's not just redirecting to the right page
But instead it keep redirecting the user to the home page. What am I doing wrong? I also have another Security Config File for admin. I can provide it if required.
To change the default strategy, you have to set an AuthenticationSuccessHandler, see AbstractAuthenticationProcessingFilter#setAuthenticationSuccessHandler:
Sets the strategy used to handle a successful authentication. By default a SavedRequestAwareAuthenticationSuccessHandler is used.
Your modified code:
private Filter ssoFilter(ClientResources client, String path) {
OAuth2ClientAuthenticationProcessingFilter filter = new OAuth2ClientAuthenticationProcessingFilter(path);
OAuth2RestTemplate template = new OAuth2RestTemplate(client.getClient(), oauth2ClientContext);
filter.setRestTemplate(template);
UserInfoTokenServices tokenServices = new UserInfoTokenServices(client.getResource().getUserInfoUri(),client.getClient().getClientId());
tokenServices.setRestTemplate(template);
filter.setTokenServices(tokenServices);
filter.setAuthenticationSuccessHandler(new SimpleUrlAuthenticationSuccessHandler("/customer/dashboard")‌​;
return filter;
}

Resources