In this section we modify the logout app we built earlier, switching to Github authentication, and also giving some feedback to users that cannot authenticate. At the same time we take the opportunity to 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, e.g. with Google you might want to only authenticate users from a specific domain.
The logout sample use Facebook as an OAuth2 provider. We can easily switch to Github by changing the local configuration:
security:
oauth2:
client:
clientId: bd1c0a783ccdd1c9b9e4
clientSecret: 1a9030fbca47a5b2c28e92f19050bb77824b5ad1
accessTokenUri: https://github.com/login/oauth/access_token
userAuthorizationUri: https://github.com/login/oauth/authorize
clientAuthenticationScheme: form
resource:
userInfoUri: https://api.github.com/user
On the client we need to be able to provide some feedback for a user that could not authenticate. To facilitate this we add a div with an informative message:
<div class="container text-danger error" style="display:none"> There was an error (bad credentials). </div>
This text will only be shown when the "error" element is shown, so we need some code to do that:
$.ajax({ url : "/user", success : function(data) { $(".unauthenticated").hide(); $("#user").html(data.userAuthentication.details.name); $(".authenticated").show(); }, error : function(data) { $("#user").html(''); $(".unauthenticated").show(); $(".authenticated").hide(); if (location.href.indexOf("error=true")>=0) { $(".error").show(); } } });
The authentication function checks the browser location when it loads and if it finds a URL with "error=true" in it, the flag is set.
To support the flag setting in the client we need to be able to
capture an authentication error and redirect to the home page
with that flag set in query parameters. Hence we need an
endpoint, in a regular @Controller
like this:
@RequestMapping("/unauthenticated")
public String unauthenticated() {
return "redirect:/?error=true";
}
In the sample app we put this in the main application class, which is
now a @Controller
(not a @RestController
) so it can handle the
redirect. The last thing we need is a mapping from an unauthenticated
response (HTTP 401, a.k.a. UNAUTHORIZED) to the "/unauthenticated"
endpoint we just added:
@Configuration
public class ServletCustomizer {
@Bean
public EmbeddedServletContainerCustomizer customizer() {
return container -> {
container.addErrorPages(new ErrorPage(HttpStatus.UNAUTHORIZED, "/unauthenticated"));
};
}
}
(In the sample, this is added as a nested class inside the main application, just for conciseness.)
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 we will extend the authentication rule to
reject users that are not in the right organization. It is easy to use
the Github API to find out more about the user, so we 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 we declare a @Bean
of type
AuthoritiesExtractor
it will be used to construct the authorities
(typically "roles") of an authenticated user. We can use that hook to
assert the the user is in the correct orignization, and throw an
exception if not:
@Bean
public AuthoritiesExtractor authoritiesExtractor(OAuth2RestOperations template) {
return map -> {
String url = (String) map.get("organizations_url");
@SuppressWarnings("unchecked")
List<Map<String, Object>> orgs = template.getForObject(url, List.class);
if (orgs.stream()
.anyMatch(org -> "spring-projects".equals(org.get("login")))) {
return AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER");
}
throw new BadCredentialsException("Not in Spring Projects origanization");
};
}
Note that we have autowired a OAuth2RestOperations
into this method,
so we can use that to access the Github API on behalf of the
authenticated user. We do that, and loop 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, we throw BadCredentialsException
and
this is picked up by Spring Security and turned in to a 401 response.
The OAuth2RestOperations
has to be created as a bean as well (as of
Spring Boot 1.4), but that’s trivial because its ingredients are all
autowirable by virtue of having used @EnableOAuth2Sso
:
@Bean
public OAuth2RestTemplate oauth2RestTemplate(OAuth2ProtectedResourceDetails resource, OAuth2ClientContext context) {
return new OAuth2RestTemplate(resource, context);
}
Tip
|
Obviously the code above can be generalized to other
authentication rules, some applicable to Github and some to other
OAuth2 providers. All you need is the OAuth2RestOperations and some
knowledge of the provider’s API.
|