|
| 1 | +[[_custom_error]] |
| 2 | += Adding an Error Page for Unauthenticated Users |
| 3 | + |
| 4 | +In this section we modify the <<_social_login_logout,logout>> app we |
| 5 | +built earlier, switching to Github authentication, and also giving |
| 6 | +some feedback to users that cannot authenticate. At the same time we |
| 7 | +take the opportunity to extend the authentication logic to include a |
| 8 | +rule that only allows users if they belong to a specific Github |
| 9 | +organization. The "organization" is a Github domain-specific concept, |
| 10 | +but similar rules could be devised for other providers, e.g. with |
| 11 | +Google you might want to only authenticate users from a specific |
| 12 | +domain. |
| 13 | + |
| 14 | +== Switching to Github |
| 15 | + |
| 16 | +The <<_social_login_logout,logout>> sample use Facebook as an OAuth2 |
| 17 | +provider. We can easily switch to Github by changing the local |
| 18 | +configuration: |
| 19 | + |
| 20 | +.application.yml |
| 21 | +[source,yaml] |
| 22 | +---- |
| 23 | +security: |
| 24 | + oauth2: |
| 25 | + client: |
| 26 | + clientId: bd1c0a783ccdd1c9b9e4 |
| 27 | + clientSecret: 1a9030fbca47a5b2c28e92f19050bb77824b5ad1 |
| 28 | + accessTokenUri: https://github.com/login/oauth/access_token |
| 29 | + userAuthorizationUri: https://github.com/login/oauth/authorize |
| 30 | + clientAuthenticationScheme: form |
| 31 | + resource: |
| 32 | + userInfoUri: https://api.github.com/user |
| 33 | +---- |
| 34 | + |
| 35 | +== Detecting an Authentication Failure in the Client |
| 36 | + |
| 37 | +On the client we need to be able to provide some feedback for a user |
| 38 | +that could not authenticate. To facilitate this we add a div with an |
| 39 | +informative message: |
| 40 | + |
| 41 | +.index.html |
| 42 | +---- |
| 43 | +<div class="container text-danger" ng-show="home.error"> |
| 44 | +There was an error (bad credentials). |
| 45 | +</div> |
| 46 | +---- |
| 47 | + |
| 48 | +This text will only be shown when the "error" flag is set in the controller, |
| 49 | +so we need some code to do that: |
| 50 | + |
| 51 | +.index.html |
| 52 | +---- |
| 53 | +angular |
| 54 | + .module("app", []) |
| 55 | + .controller("home", function($http, $location) { |
| 56 | + var self = this; |
| 57 | + self.logout = function() { |
| 58 | + if ($location.absUrl().indexOf("error=true") >= 0) { |
| 59 | + self.authenticated = false; |
| 60 | + self.error = true; |
| 61 | + } |
| 62 | + ... |
| 63 | + }; |
| 64 | + }); |
| 65 | +---- |
| 66 | + |
| 67 | +The "home" controller checks the browser location when it loads |
| 68 | +and if it finds a URL with "error=true" in it, the flag is set. |
| 69 | + |
| 70 | +== Adding an Error Page |
| 71 | + |
| 72 | +To support the flag setting in the client we need to be able to |
| 73 | +capture an authentication error and redirect to the home page |
| 74 | +with that flag set in query parameters. Hence we need an |
| 75 | +endpoint, in a regular `@Controller` like this: |
| 76 | + |
| 77 | +.SocialApplication.java |
| 78 | +[source,java] |
| 79 | +---- |
| 80 | +@RequestMapping("/unauthenticated") |
| 81 | +public String unauthenticated() { |
| 82 | + return "redirect:/?error=true"; |
| 83 | +} |
| 84 | +---- |
| 85 | + |
| 86 | +In the sample app we put this in the main application class, which is |
| 87 | +now a `@Controller` (not a `@RestController`) so it can handle the |
| 88 | +redirect. The last thing we need is a mapping from an unauthenticated |
| 89 | +response (HTTP 401, a.k.a. UNAUTHORIZED) to the "/unauthenticated" |
| 90 | +endpoint we just added: |
| 91 | + |
| 92 | +.ServletCustomizer.java |
| 93 | +[source,java] |
| 94 | +---- |
| 95 | +@Configuration |
| 96 | +public class ServletCustomizer { |
| 97 | + @Bean |
| 98 | + public EmbeddedServletContainerCustomizer customizer() { |
| 99 | + return container -> { |
| 100 | + container.addErrorPages( |
| 101 | + new ErrorPage(HttpStatus.UNAUTHORIZED, "/unauthenticated")); |
| 102 | + }; |
| 103 | + } |
| 104 | +} |
| 105 | +---- |
| 106 | + |
| 107 | +(In the sample, this is added as a nested class inside the main |
| 108 | +application, just for conciseness.) |
| 109 | + |
| 110 | +== Generating a 401 in the Server |
| 111 | + |
| 112 | +A 401 response will already be coming from Spring Security if the user |
| 113 | +cannot or does not want to login with Github. What we set out to do |
| 114 | +was extend that rule to reject users that are not in the right |
| 115 | +origanization. It is easy to use the Github API to find out more about |
| 116 | +the user, including his organizations, so we just need to plug that |
| 117 | +into the right part of the authentication process. Fortunately, for |
| 118 | +such a simple use case, Spring Boot has provided an easy extension |
| 119 | +point: if we declare a `@Bean` of type `AuthoritiesExtractor` it will |
| 120 | +be used to construct the authorities (typically "roles") of an |
| 121 | +authenticated user. We can use that hook to assert the the user is in |
| 122 | +the correct orignization, and throw an exception if not: |
| 123 | + |
| 124 | +.SocialApplication.java |
| 125 | +[source,java] |
| 126 | +---- |
| 127 | +@Bean |
| 128 | +public AuthoritiesExtractor authoritiesExtractor(OAuth2RestOperations template) { |
| 129 | + return map -> { |
| 130 | + String url = (String) map.get("organizations_url"); |
| 131 | + @SuppressWarnings("unchecked") |
| 132 | + List<Map<String, Object>> orgs = template.getForObject(url, List.class); |
| 133 | + for (Map<String, Object> org : orgs) { |
| 134 | + if ("spring-projects".equals(org.get("login"))) { |
| 135 | + return AuthorityUtils |
| 136 | + .commaSeparatedStringToAuthorityList("ROLE_USER"); |
| 137 | + } |
| 138 | + } |
| 139 | + throw new BadCredentialsException("Not in Spring Projects origanization"); |
| 140 | + }; |
| 141 | +} |
| 142 | +---- |
| 143 | + |
| 144 | +Note that we have autowired a `OAuth2RestOperations` into this method, |
| 145 | +so we can use that to access the Github API on behalf of the |
| 146 | +authenticated user. We do that, and loop over the organizations, |
| 147 | +looking for one that matches "spring-projects" (this is the |
| 148 | +organization that is used to store Spring open source projects). You |
| 149 | +can substitute your own value there if you want to be able to |
| 150 | +authenticate successfully and you are not in the Spring Engineering |
| 151 | +team. If there is no match, we throw `BadCredentialsException` and |
| 152 | +this is picked up by Spring Security and turned in to a 401 response. |
| 153 | + |
| 154 | +TIP: Obviously the code above can be generalized to other |
| 155 | +authentication rules, some applicable to Github and some to other |
| 156 | +OAuth2 providers. All you need is the `OAuth2RestOperations` and some |
| 157 | +knowledge of the provider's API. |
0 commit comments