In this section, you’ll modify the two-providers app you built earlier to give some feedback to users that cannot authenticate. At the same time you’ll extend the authentication logic to include a rule that only allows users if they belong to a specific GitHub organization. The "organization" is a GitHub domain-specific concept, but similar rules could be devised for other providers. For example, with Google you might want to only authenticate users from a specific domain.
The two-providers sample uses GitHub as an OAuth 2.0 provider:
spring:
security:
oauth2:
client:
registration:
github:
client-id: bd1c0a783ccdd1c9b9e4
client-secret: 1a9030fbca47a5b2c28e92f19050bb77824b5ad1
# ...
On the client, you might like to provide some feedback for a user that could not authenticate. To facilitate this, you can add a div to which you’ll eventually add an informative message.
<div class="container text-danger error"></div>
Then, add a call to the /error
endpoint, populating the <div>
with the result:
$.get("/error", function(data) { if (data) { $(".error").html(data); } else { $(".error").html(''); } });
The error function checks with the backend if there is any error to display
To support the retrieval of an error message, you’ll need to capture it when authentication fails.
To achieve this, you can configure an AuthenticationFailureHandler
, like so:
protected void configure(HttpSecurity http) throws Exception {
// @formatter:off
http
// ... existing configuration
.oauth2Login(o -> o
.failureHandler((request, response, exception) -> {
request.getSession().setAttribute("error.message", exception.getMessage());
handler.onAuthenticationFailure(request, response, exception);
})
);
}
The above will save an error message to the session whenever authentication fails.
Then, you can add a simple /error
controller, like this one:
@GetMapping("/error")
public String error(HttpServletRequest request) {
String message = (String) request.getSession().getAttribute("error.message");
request.getSession().removeAttribute("error.message");
return message;
}
Note
|
This will replace the default /error page in the app, which is fine for our case, but may not be sophisticated enough for your needs.
|
A 401 response will already be coming from Spring Security if the user cannot or does not want to login with GitHub, so the app is already working if you fail to authenticate (e.g. by rejecting the token grant).
To spice things up a bit, you can extend the authentication rule to reject users that are not in the right organization.
You can use the GitHub API to find out more about the user, so you’ll just need to plug that into the right part of the authentication process.
Fortunately, for such a simple use case, Spring Boot has provided an easy extension point:
If you declare a @Bean
of type OAuth2UserService
, it will be used to identify the user principal.
You can use that hook to assert the the user is in the correct organization, and throw an exception if not:
@Bean
public OAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService(WebClient rest) {
DefaultOAuth2UserService delegate = new DefaultOAuth2UserService();
return request -> {
OAuth2User user = delegate.loadUser(request);
if (!"github".equals(request.getClientRegistration().getRegistrationId())) {
return user;
}
OAuth2AuthorizedClient client = new OAuth2AuthorizedClient
(request.getClientRegistration(), user.getName(), request.getAccessToken());
String url = user.getAttribute("organizations_url");
List<Map<String, Object>> orgs = rest
.get().uri(url)
.attributes(oauth2AuthorizedClient(client))
.retrieve()
.bodyToMono(List.class)
.block();
if (orgs.stream().anyMatch(org -> "spring-projects".equals(org.get("login")))) {
return user;
}
throw new OAuth2AuthenticationException(new OAuth2Error("invalid_token", "Not in Spring Team", ""));
};
}
Note that this code is dependent on a WebClient
instance for accessing the GitHub API on behalf of the authenticated user.
Having done that, it loops over the organizations, looking for one that matches "spring-projects" (this is the organization that is used to store Spring open source projects).
You can substitute your own value there if you want to be able to authenticate successfully and you are not in the Spring Engineering team.
If there is no match, it throws an OAuth2AuthenticationException
, and this is picked up by Spring Security and turned in to a 401 response.
The WebClient
has to be created as a bean as well, but that’s trivial because its ingredients are all autowirable by virtue of having used spring-boot-starter-oauth2-client
:
@Bean
public WebClient rest(ClientRegistrationRepository clients, OAuth2AuthorizedClientRepository authz) {
ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2 =
new ServletOAuth2AuthorizedClientExchangeFilterFunction(clients, authz);
return WebClient.builder()
.filter(oauth2).build();
}
Tip
|
Obviously the code above can be generalized to other authentication rules, some applicable to GitHub and some to other OAuth 2.0 providers.
All you need is the WebClient and some knowledge of the provider’s API.
|