Spring oauth2 dont redirect to original url

14,622

Solution 1

It's a workaround and I don't like it. If somebody finds a better solution i would be grateful.

For the purpose I will keep the implementation simple and dirty. First I have implemented UpdateSavedRequestFilter to save request in the requestCache:

public class UpdateSavedRequestFilter extends OncePerRequestFilter {
    private RequestCache requestCache = new HttpSessionRequestCache();

    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        String queryString = request.getQueryString();
        if(!StringUtils.contains(queryString, "code") && authentication == null) {
            requestCache.saveRequest(request, response);
        }
        filterChain.doFilter(request, response);
    }
}

It didn't work as wished, I was redirected to "/resource/test" but the auth process was triggered again. So i have implemented my own Oauth2Filter. It doesn't do much, i have mostly copy code from http://www.baeldung.com/spring-security-openid-connect .My only bit to it was an extenstion of the doFilter method with a call of requiresAuthentication which check if the user is already authenticated

public class OAuth2Filter extends AbstractAuthenticationProcessingFilter {

    public OAuth2RestOperations restTemplate;

    private UserInfoTokenServices tokenServices;

    public OAuth2Filter(String defaultFilterProcessesUrl) {
        super(defaultFilterProcessesUrl);
        setAuthenticationManager(new NoopAuthenticationManager());
        SavedRequestAwareAuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
        setAuthenticationSuccessHandler(successHandler);
    }

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        if (requiresAuthentication()) {
            super.doFilter(req, res, chain);
        } else {
            chain.doFilter(req, res);
        }
    }

    @Override
    public Authentication attemptAuthentication(
            HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException{
        OAuth2AccessToken accessToken;
        try {
            accessToken = restTemplate.getAccessToken();
        } catch (OAuth2Exception e) {
            throw new BadCredentialsException("Could not obtain access token", e);
        }
        try {
            OAuth2Authentication result = tokenServices.loadAuthentication(accessToken.getValue());
            if (authenticationDetailsSource!=null) {
                request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, accessToken.getValue());
                request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE, accessToken.getTokenType());
                result.setDetails(authenticationDetailsSource.buildDetails(request));
            }
            publish(new AuthenticationSuccessEvent(result));
            return result;
        } catch (InvalidTokenException e) {
            throw new BadCredentialsException("Could not obtain user details from token", e);
        }
    }

    private void publish(ApplicationEvent event) {
        if (eventPublisher!=null) {
            eventPublisher.publishEvent(event);
        }
    }

    private static class NoopAuthenticationManager implements AuthenticationManager {

        @Override
        public Authentication authenticate(Authentication authentication) throws AuthenticationException {
            throw new UnsupportedOperationException("No authentication should be done with this AuthenticationManager");
        }

    }

    private boolean requiresAuthentication() {
        Authentication currentUser = SecurityContextHolder.getContext()
                .getAuthentication();

        if (currentUser == null) {
            return true;
        }
        OAuth2AccessToken accessToken = restTemplate.getAccessToken();
        if (accessToken == null) {
            return true;
        }
        return accessToken.isExpired();
    }

    public void setRestTemplate(OAuth2RestOperations restTemplate) {
        this.restTemplate = restTemplate;
    }

    public void setTokenServices(UserInfoTokenServices tokenServices) {
        this.tokenServices = tokenServices;
    }
}

This is the rest of my conig classes:

@Configuration
@EnableWebSecurity
public class SecureConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private OAuth2RestTemplate restTemplate;

    @Value("${openId.userinfo}")
    private String userInfoUri;

    @Value("${openId.clientId}")
    private String clientId;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .addFilterAfter(new OAuth2ClientContextFilter(), AbstractPreAuthenticatedProcessingFilter.class)
                .addFilterAfter(openIdConnectFilter(), OAuth2ClientContextFilter.class)
                .addFilterBefore(new UpdateSavedRequestFilter(), OAuth2Filter.class);
    }

    @Bean
    public OAuth2Filter openIdConnectFilter() {
        OAuth2Filter filter = new OAuth2Filter("/resource/**");
        filter.setRestTemplate(restTemplate);
        UserInfoTokenServices tokenServices = new UserInfoTokenServices(userInfoUri, clientId);
        tokenServices.setRestTemplate(restTemplate);
        filter.setTokenServices(tokenServices);
        return filter;
    }
}

@Configuration
@EnableOAuth2Client
public class OpenIdConnectConfig {
    @Value("${openId.userinfo}")
    private String userInfoUri;

    @Value("${openId.clientId}")
    private String clientId;

    @Value("${openId.clientSecret}")
    private String clientSecret;

    @Value("${openId.accessTokenUri}")
    private String accessTokenUri;

    @Value("${openId.userAuthorizationUri}")
    private String userAuthorizationUri;


    @Bean
    public OAuth2ProtectedResourceDetails protectedResourceDetails() {
        AuthorizationCodeResourceDetails details = new AuthorizationCodeResourceDetails();
        details.setClientId(clientId);
        details.setClientSecret(clientSecret);
        details.setAccessTokenUri(accessTokenUri);
        details.setUserAuthorizationUri(userAuthorizationUri);
        details.setScope(Arrays.asList("read"));
        details.setUseCurrentUri(true);
        return details;
    }

    @Bean
    @Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)
    public OAuth2RestTemplate restTemplate(OAuth2ClientContext clientContext) {
        OAuth2RestTemplate oAuth2RestTemplate = new OAuth2RestTemplate(protectedResourceDetails(), clientContext);
        return oAuth2RestTemplate;
    }
}

Solution 2

This is what AbstractAuthenticationProcessingFilter says which OAuth2ClientAuthenticationProcessingFilter extends.

enter image description here

Try this:

        private OAuth2ClientAuthenticationProcessingFilter ssoFilter() {
                OAuth2ClientAuthenticationProcessingFilter openIDFilter = new OAuth2ClientAuthenticationProcessingFilter("/resource/**");
                openIDFilter.setRestTemplate(restTemplate());
                UserInfoTokenServices tokenServices = new UserInfoTokenServices(userInfoUri, clientId);
                tokenServices.setRestTemplate(restTemplate());
                openIDFilter.setTokenServices(tokenServices);

openIDFilter.setAuthenticationSuccessHandler(new SavedRequestAwareAuthenticationSuccessHandler());


return openIDFilter;

 }

Update:

This is my debug log for your reference.

    - Checking match of request : '/dist/i_do not_exist.html'; against '/favicon.ico'
- Checking match of request : '/dist/i_do not_exist.html'; against '/images/**'
- Checking match of request : '/dist/i_do not_exist.html'; against '/css/**'
- Checking match of request : '/dist/i_do not_exist.html'; against '/maxsession.jsp'
- /dist/i_do not_exist.html at position 1 of 12 in additional filter chain; firing Filter: 'SecurityContextPersistenceFilter'
- No HttpSession currently exists
- No SecurityContext was available from the HttpSession: null. A new one will be created.
- /dist/i_do not_exist.html at position 2 of 12 in additional filter chain; firing Filter: 'ConcurrentSessionFilter'
- /dist/i_do not_exist.html at position 3 of 12 in additional filter chain; firing Filter: 'WebAsyncManagerIntegrationFilter'
- /dist/i_do not_exist.html at position 4 of 12 in additional filter chain; firing Filter: 'LogoutFilter'
- /dist/i_do not_exist.html at position 5 of 12 in additional filter chain; firing Filter: 'BasicAuthenticationFilter'
- /dist/i_do not_exist.html at position 6 of 12 in additional filter chain; firing Filter: 'RequestCacheAwareFilter'
- /dist/i_do not_exist.html at position 7 of 12 in additional filter chain; firing Filter: 'SecurityContextHolderAwareRequestFilter'
- /dist/i_do not_exist.html at position 8 of 12 in additional filter chain; firing Filter: 'SessionManagementFilter'
- /dist/i_do not_exist.html at position 9 of 12 in additional filter chain; firing Filter: 'ExceptionTranslationFilter'
- /dist/i_do not_exist.html at position 10 of 12 in additional filter chain; firing Filter: 'OAuth2ClientContextFilter'
- /dist/i_do not_exist.html at position 11 of 12 in additional filter chain; firing Filter: 'OpenIdConnectFilter'
- /dist/i_do not_exist.html at position 12 of 12 in additional filter chain; firing Filter: 'FilterSecurityInterceptor'
- Secure object: FilterInvocation: URL: /dist/i_do not_exist.html; Attributes: [isFullyAuthenticated()]
11:43:23.719 [http-nio-8080-exec-3] DEBUG o.a.camel.spring.SpringCamelContext - onApplicationEvent: org.springframework.security.access.event.AuthenticationCredentialsNotFoundEvent[source=FilterInvocation: URL: /dist/i_do not_exist.html]
- Authentication exception occurred; redirecting to authentication entry point
org.springframework.security.authentication.AuthenticationCredentialsNotFoundException: An Authentication object was not found in the SecurityContext
    at org.springframework.security.access.intercept.AbstractSecurityInterceptor.credentialsNotFound(AbstractSecurityInterceptor.java:339)
    at org.springframework.security.access.intercept.AbstractSecurityInterceptor.beforeInvocation(AbstractSecurityInterceptor.java:198)
    at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.invoke(FilterSecurityInterceptor.java:115)
    at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.doFilter(FilterSecurityInterceptor.java:84)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
    at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:199)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
    at org.springframework.security.oauth2.client.filter.OAuth2ClientContextFilter.doFilter(OAuth2ClientContextFilter.java:60)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
    at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:113)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
    at org.springframework.security.web.session.SessionManagementFilter.doFilter(SessionManagementFilter.java:103)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
    at org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter.doFilter(SecurityContextHolderAwareRequestFilter.java:154)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
    at org.springframework.security.web.savedrequest.RequestCacheAwareFilter.doFilter(RequestCacheAwareFilter.java:45)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
    at org.springframework.security.web.authentication.www.BasicAuthenticationFilter.doFilter(BasicAuthenticationFilter.java:150)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
    at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:110)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
    at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:50)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
    at org.springframework.security.web.session.ConcurrentSessionFilter.doFilter(ConcurrentSessionFilter.java:125)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
    at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:87)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:192)
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:160)
    at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:346)
    at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:262)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:240)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207)
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:212)
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:94)
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:616)
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:141)
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:79)
    at org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:620)
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:88)
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:502)
    at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1132)
    at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:684)
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1539)
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.run(NioEndpoint.java:1495)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
    at java.lang.Thread.run(Thread.java:745)
- Publishing event: org.springframework.security.web.session.HttpSessionCreatedEvent[source=org.apache.catalina.session.StandardSessionFacade@cd8de57]
11:43:25.895 [http-nio-8080-exec-3] DEBUG o.a.camel.spring.SpringCamelContext - onApplicationEvent: org.springframework.security.web.session.HttpSessionCreatedEvent[source=org.apache.catalina.session.StandardSessionFacade@cd8de57]
- DefaultSavedRequest added to Session: DefaultSavedRequest[http://ecuio197m0221:8080/Pagos/dist/i_do%20not_exist.html]
- Calling Authentication entry point.
- Redirecting to 'http://<server>:<port>/<AppContext>/idp-login;jsessionid=CB7BAACDAEDC3A0E37AD5F75C0E38C26'
Share:
14,622
alex.b
Author by

alex.b

Updated on June 11, 2022

Comments

  • alex.b
    alex.b almost 2 years

    I tried to configure the authorization code flow as a client. As far the flow is working. I get a redirect to the login page. The oauth2 server gives me an auth code and I can exchange the code for an access token.

    But I can't get the last step right: get back to the original resource. This is my SecurityConfig:

    @Configuration
    @EnableWebSecurity
    @EnableOAuth2Client
    public class SecureConfig extends WebSecurityConfigurerAdapter {
    
        @Autowired
        OAuth2ClientContext oauth2ClientContext;
    
        @Value("${openId.userinfo}")
        private String userInfoUri;
    
        @Value("${openId.clientId}")
        private String clientId;
    
        @Value("${openId.clientSecret}")
        private String clientSecret;
    
        @Value("${openId.accessTokenUri}")
        private String accessTokenUri;
    
        @Value("${openId.userAuthorizationUri}")
        private String userAuthorizationUri;
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
                    http.csrf().disable()
                    .addFilterAfter(ssoFilter(), BasicAuthenticationFilter.class);
        }
    
        private OAuth2ClientAuthenticationProcessingFilter ssoFilter() {
            OAuth2ClientAuthenticationProcessingFilter openIDFilter = new OAuth2ClientAuthenticationProcessingFilter("/resource/**");
            openIDFilter.setRestTemplate(restTemplate());
            UserInfoTokenServices tokenServices = new UserInfoTokenServices(userInfoUri, clientId);
            tokenServices.setRestTemplate(restTemplate());
            openIDFilter.setTokenServices(tokenServices);
            return openIDFilter;
        }
    
        @Bean
        @Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)
        public OAuth2RestTemplate restTemplate() {
            return new OAuth2RestTemplate(protectedResourceDetails(), oauth2ClientContext);
        }
    
        @Bean
        public FilterRegistrationBean oauth2ClientFilterRegistration(
                OAuth2ClientContextFilter filter) {
            FilterRegistrationBean registration = new FilterRegistrationBean();
            registration.setFilter(filter);
            registration.setOrder(-100);
            return registration;
        }
    
        @Bean
        public OAuth2ProtectedResourceDetails protectedResourceDetails() {
            AuthorizationCodeResourceDetails details = new AuthorizationCodeResourceDetails();
            details.setClientId(clientId);
            details.setClientSecret(clientSecret);
            details.setAccessTokenUri(accessTokenUri);
            details.setUserAuthorizationUri(userAuthorizationUri);
            details.setScope(Arrays.asList("read"));
            details.setUseCurrentUri(true);
            return details;
        }
    }
    

    And this is my controller:

    @Controller
    @RequestMapping("/resource")
    public class TestController {
    
        @RequestMapping(value = "/test",  method = {RequestMethod.GET, RequestMethod.POST})
        @ResponseStatus(code = HttpStatus.OK)
        public void test(){
            System.out.println("hello world");
        }
    }
    

    In the last step spring redirect me to my base url: enter image description here

    I found this forum post

    It suggests saving the request in the RequestCache. But this post is about 6 years old, maybe spring offers a more elegant solution in the meantime?

    EDIT: This are my dependencies:

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>1.5.2.RELEASE</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.security.oauth</groupId>
            <artifactId>spring-security-oauth2</artifactId>
        </dependency>
    </dependencies>
    
  • alex.b
    alex.b about 6 years
    Thanks for you answer. The 'SavedRequestAwareAuthenticationSuccessHandler' is the default successHandler. But the 'ExceptionTranslationFilter' don't get called, so the RequestCache is empty and the successHandler redirect me to root. This this forum post suggest to add the request manually to the requestCache. I thought maybe spring supports my desired behaviour in the meantime, but apparently not. I will try to post my solution this weekend
  • Agam
    Agam about 6 years
    Well it's great that you have a solution, but for the sake of somenone in future who could find this question related, I would say that this solution is working perfectly fine for me. If I start the flow with any URL like "http://<server>:<port>/applicationcontext/any_thing" with Authorization code flow, I end up in browser with same URL. Secondly, If you are using authorization code flow, spring definately throws UserRedirectException, which is handled by 'ExceptionTranslationFilter' and your current request is saved in RequestCache. I am wondering why this is not happening in your case.
  • Agam
    Agam about 6 years
    It's 'UserRedirectRequiredException' not 'UserRedirectException'.
  • alex.b
    alex.b about 6 years
    Thanks for you continues support. Yes spring throws the UserRedirectRequiredException. I didn't express myself clearly. The ExceptionTranslationFilter is getting called but after the OAuth2ClientAuthenticationProcessingFilter. Maybe this is my problem? I have inspected again my configuration its exactly what i have posted. Do you have the same spring version?
  • Agam
    Agam about 6 years
    Just Update the answer with my debug log. May it can trigger something.
  • alex.b
    alex.b about 6 years
    I tried to set my ssoFilter before ExceptionTranslationFilter. First problem was the auth code flow didint work at all. I needed to configure .anonymous().disable(). After this the same behavior appear as previously, i was getting redirected to root. I started to debug the ExceptionTranslationFilter and noticed the request is not saved in the requestCache. Apparently the request will be only saved if the exception is of instance AuthenticationException or AccessDeniedException. But the exception UserRedirectRequiredException (which is getting throwed) is a RuntimeException.
  • Kalle Richter
    Kalle Richter almost 5 years
    Don't provide information as image which can be text.
  • coretechie
    coretechie about 2 years
    This looks like a crisp solution but I am not able to add this dependency in my pom.xml. Can someone help understand why? <dependency> <groupId>org.springframework.security.oauth</groupId> <artifactId>spring-security-oauth2</artifactId> <version>2.5.1.RELEASE</version> <!-- <version>2.0.12.RELEASE</version>--> </dependency>