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

Viewed 1721

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.
2 Answers

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.

Re your comnent: "I'm attempting to build an Authorization Server":

Coding your own Authorization Server (AS) or having to build its code yourself is highly inadvisable, since it is easy to get bogged down in plumbing or to make security mistakes.

By all means use Spring OAuth Security in your apps though. It is hard enough to get these working as desired, without taking on extra work.

SUGGESTED APPROACH

  • Choose a free AS and run it as a Docker Container, then connect to its endpoints from your apps.

  • If you need to customize logins, use a plugin model, write a small amount of code, then deploy a JAR file or two to the Docker container.

This will get you up and running very quickly. Also, since Spring Security is standards based, you are free to change your mind about providers, and defer decisions on the final one.

EXAMPLE IMPLEMENTATION

Curity, along with other good choices like Keycloak or Ory Hydra are Java based and support plugins:

Related