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.