Spring Security 5 introduced new authentication flow. The flow guides an user through several endpoints and pages, one of those pages is a login page.
By default the login page shows a list of authentication methods. For example Facebook OAuth 2.0 or username-password login, and allows an user to choose one. It does not make too much sense to show the login page if only one method is configured. In that case the page should be skipped and an user should be send directly to an authentication endpoint.
Authentication flow
-
An user tries to access protected page. The "not authenticated" exception is thrown.
-
The exception is processed by an exception filter, configured by
ExceptionHandlingConfigurer
. -
The exception filter calls
AuthenticationEntryPoint.commence()
which redirects the user to a login page. -
By default the login page is generated by
LoginPageGeneratingWebFilter
. The generated page shows list of available authentication methods. By choosing one, the user is sent to an authentication endpoint. -
The authentication request is generated in
OAuth2AuthorizationRequestRedirectFilter.sendRedirectForAuthorization()
and the user is redirected to an authentication server, for example Google. -
After successful authentication by the authentication server, the user is redirected back to the application. His request is processed by
OAuth2LoginAuthenticationFilter.attemptAuthentication()
which fires a background request to the authentication server to retrieve user's details and to verify the user has been properly authenticated. -
Finally the user is redirected to the protected page, he tried to access in step 1.
To skip the login page and to go from point 3. directly to 5. we need to provide our custom authentication entry point to the ExceptionHandlingConfigurer
.
In our custom implementation of WebSecurityConfigurerAdapter
we can easily hard-code the redirection to an authentication endpoint.
@EnableWebSecurity
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
public void init(WebSecurity web) throws Exception {
ExceptionHandlingConfigurer exceptionHandlingConfigurer = web.getConfigurer(ExceptionHandlingConfigurer.class);
exceptionHandlingConfigurer.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/authentication-entry-point-url"));
super.init(web);
}
}
By adding a custom authentication entry point, the LoginPageGeneratingWebFilter
is not created in DefaultLoginPageConfigurer.configure()
so it is a good idea to configure the login page URL to some page capable of processing authentication errors by WebSecurity.oauth2Login().loginPage("my-login-page-url")
.
There is a possibility to implement more robust solution that will get the URL of configured authentication method from its configurer.
@EnableWebSecurity
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
public void init(WebSecurity web) throws Exception {
String authenticationEntryPointUrl = getSingleAuthenticationUrl(web);
if (authenticationEntryPointUrl != null) {
// If no URL is found configuration will fall back to its defaults.
ExceptionHandlingConfigurer exceptionHandlingConfigurer = web.getConfigurer(ExceptionHandlingConfigurer.class);
exceptionHandlingConfigurer.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint(authenticationEntryPointUrl));
}
super.init(web);
}
private String getSingleAuthenticationUrl(WebSecurity builder) {
// Get the list of registration repositories
ClientRegistrationRepository clientRegistrationRepository = builder.getSharedObject(ClientRegistrationRepository.class);
ResolvableType type = ResolvableType.forInstance(clientRegistrationRepository).as(Iterable.class);
if (type == ResolvableType.NONE || !ClientRegistration.class.isAssignableFrom(type.resolveGenerics()[0])) {
return null;
}
// Go through registrations, return nothing if there are more or less registrations than 1
Iterable<ClientRegistration> clientRegistrationIterable = (Iterable<ClientRegistration>) clientRegistrationRepository;
List<ClientRegistration> clientRegistrations = new ArrayList<>();
clientRegistrationIterable.forEach(clientRegistrations::add);
if (clientRegistrations.size() != 1) {
return null;
}
// Return authorization URL. Instead of default base URI the configured URI should be retrieved from
// OAuth2LoginConfigurer.AuthorizationEndpointConfig.authorizationRequestBaseUri
// Unfortunately to do that the reflection must be used.
String authorizationRequestBaseUri = OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI;
return authorizationRequestBaseUri + "/" + clientRegistrations.get(0).getRegistrationId();
}
}