Search code examples
springspring-securityoauth-2.0spring-security-oauth2

Spring Authorization Server: How to use login form hosted on a separate application?


I am using Spring Security along with Spring Authorization Server and experimenting with creating an auth server.

I have a basic flow allowing me to login with the pre-built login page (from a baledung guide - this is the code I'm working off ). I'm assuming this login page form comes from formLogin() like so:

        http.authorizeRequests(authorizeRequests ->
                authorizeRequests.anyRequest().authenticated()
        )
        //.formLogin(withDefaults());
        return http.build();

login page

I would like to not use this pre-built form as I have a need to host and run the login form front-end application completely separately. ie on a different server, domain and codebase.

Another way to ask this question could be:

  • How do I disable the built in form in authorization-server so I can use it with a completely separate form?
  • Are there any recommended ways of learning about how customise my SecurityFilterChain along these lines? Is this the correct place to look? I find the baledung article (and articles like that) helpful as a starting point, but seldom works for more practical use case. I'm confident Spring Security and the oauth2 libraries will allow me to do what I want, but not entirely clear.

Solution

  • After discussing this with you, I've gathered that what you're trying to do is essentially pre-authenticate the user that was authenticated through another (separately hosted) login page, actually a separate system. The idea is that the other system would redirect back with a signed JWT in a query parameter.

    This really becomes more of a federated login problem at that point, which is what SAML 2.0 and OAuth 2.0 are aimed at solving. However, if you have to stick with things like a signed JWT (similar to a SAML assertion), we could model a fairly simple pre-authenticated authorization_code flow using the Spring Authorization Server.

    Note: I haven't explored options for JWT Profile for OAuth 2.0 Client Authentication and Authorization Grants but it could be a viable alternative. See this issue (#59).

    Additional note: There are numerous security considerations involved with the approach outlined below. What follows is a sketch of the approach. Additional considerations include CSRF protection, using Form Post Response Mode (similar to SAML 2.0) to protect the access token instead of a query parameter, aggressively expiring the access token (2 minutes or less), and others. In other words, using a federated login approach like SAML 2.0 or OAuth 2.0 will always be RECOMMENDED over this approach when possible.

    You could to start with the existing Spring Authorization Server sample and evolve it from there.

    Here's a variation that redirects to an external authentication provider and includes a pre-authentication mechanism on the redirect back:

        @Bean
        @Order(1)
        public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
            OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
            // @formatter:off
            http
                .exceptionHandling(exceptionHandling -> exceptionHandling
                    .authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("https://some-other-sso.example/login"))
                );
            // @formatter:on
            return http.build();
        }
    
        @Bean
        @Order(2)
        public SecurityFilterChain standardSecurityFilterChain(HttpSecurity http) throws Exception {
            // @formatter:off
            http
                .authorizeRequests(authorize -> authorize
                    .anyRequest().authenticated()
                )
                .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
            // @formatter:on
    
            return http.build();
        }
    
        @Bean
        public JwtDecoder jwtDecoder(PublicKey publicKey) {
            return NimbusJwtDecoder.withPublicKey((RSAPublicKey) publicKey).build();
        }
    
        @Bean
        public BearerTokenResolver bearerTokenResolver() {
            DefaultBearerTokenResolver bearerTokenResolver = new DefaultBearerTokenResolver();
            bearerTokenResolver.setAllowUriQueryParameter(true);
            return bearerTokenResolver;
        }
    

    The first filter chain operates on authorization server endpoints, such as /oauth2/authorize, /oauth2/token, etc. Note that the /oauth2/authorize endpoint requires an authenticated user to function, meaning that if the endpoint is invoked, the user has to be authenticated, or else the authentication entry point is invoked, which redirects to the external provider. Also note that there must be a trusted relationship between the two parties, since we're not using OAuth for the external SSO.

    When a redirect from the oauth client comes to the /oauth2/authorize?... endpoint, the request is cached by Spring Security so it can be replayed later (see controller below).

    The second filter chain authenticates a user with a signed JWT. It also includes a customized BearerTokenResolver which reads the JWT from a query parameter in the URL (?access_token=...).

    The PublicKey injected into the JwtDecoder would be from the external SSO provider, so you can plug that in however it makes sense to in your setup.

    We can create a stub authentication endpoint that converts the signed JWT into an authenticated session on the authorization server, like this:

    @Controller
    public class SsoController {
        private AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
    
        @GetMapping("/login")
        public void login(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
                throws ServletException, IOException {
            this.successHandler.onAuthenticationSuccess(request, response, authentication);
        }
    }
    

    The .oauth2ResourceServer() DSL causes the user to be authenticated when the /login endpoint is invoked. It requires an access_token parameter (used by the BearerTokenResolver) to pre-authenticate the user by validating the signed JWT as an assertion that the user has been externally authenticated. At this point, a session is created that will authenticate all future requests by this browser.

    The controller is then invoked, and simply redirects back to the real authorization endpoint using the SavedRequestAwareAuthenticationSuccessHandler, which will happily initiate the authorization_code flow.