Skip to content

Latest commit

 

History

History
151 lines (121 loc) · 5.9 KB

README.adoc

File metadata and controls

151 lines (121 loc) · 5.9 KB

Adding an Error Page for Unauthenticated Users

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.

Switching to GitHub

The two-providers sample uses GitHub as an OAuth 2.0 provider:

application.yml
spring:
  security:
    oauth2:
      client:
        registration:
          github:
            client-id: bd1c0a783ccdd1c9b9e4
            client-secret: 1a9030fbca47a5b2c28e92f19050bb77824b5ad1
          # ...

Detecting an Authentication Failure in the Client

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.

index.html
<div class="container text-danger error"></div>

Then, add a call to the /error endpoint, populating the <div> with the result:

index.html
$.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

Adding an Error Message

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:

SocialApplication.java
@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.

Generating a 401 in the Server

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:

SocialApplication.java
@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.