├── .github └── dco.yml ├── .gitignore ├── .mvn └── wrapper │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── .travis.yml ├── CONTRIBUTING.adoc ├── Gemfile ├── Guardfile ├── LICENSE.code.txt ├── LICENSE.writing.txt ├── README.adoc ├── click ├── README.adoc ├── pom.xml └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── example │ │ │ └── SocialApplication.java │ └── resources │ │ ├── application.yml │ │ └── static │ │ └── index.html │ └── test │ └── java │ └── com │ └── example │ └── SocialApplicationTests.java ├── custom-error ├── README.adoc ├── pom.xml └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── example │ │ │ └── SocialApplication.java │ └── resources │ │ ├── application.yml │ │ └── static │ │ └── index.html │ └── test │ └── java │ └── com │ └── example │ └── SocialApplicationTests.java ├── logout ├── README.adoc ├── pom.xml └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── example │ │ │ └── SocialApplication.java │ └── resources │ │ ├── application.yml │ │ └── static │ │ └── index.html │ └── test │ └── java │ └── com │ └── example │ └── SocialApplicationTests.java ├── mvnw ├── mvnw.cmd ├── overview.adoc ├── pom.xml ├── simple ├── README.adoc ├── pom.xml └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── example │ │ │ └── SocialApplication.java │ └── resources │ │ ├── application.yml │ │ └── static │ │ └── index.html │ └── test │ └── java │ └── com │ └── example │ └── SocialApplicationTests.java └── two-providers ├── README.adoc ├── pom.xml └── src ├── main ├── java │ └── com │ │ └── example │ │ └── SocialApplication.java └── resources │ ├── application.yml │ └── static │ └── index.html └── test └── java └── com └── example └── SocialApplicationTests.java /.github/dco.yml: -------------------------------------------------------------------------------- 1 | require: 2 | members: false 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.iml 3 | .factorypath 4 | classes 5 | target 6 | bin/ 7 | .classpath 8 | .project 9 | .settings/ 10 | Gemfile.lock 11 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spring-guides/tut-spring-boot-oauth2/c827fc8739f9e2658fced818bf8c6a40af7b9256/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.3.3/apache-maven-3.3.3-bin.zip -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | cache: 2 | directories: 3 | - $HOME/.m2 4 | jdk: 5 | - openjdk8 6 | language: java 7 | script: ./mvnw --fail-at-end --update-snapshots clean install 8 | notifications: 9 | slack: 10 | secure: oba1C58doySmrOhh1vFxccgp9JCQi1cNBKhC0YD5I9uk44eXAsOs8GjIs3wqfReKkSf4e9N8jRpbQWoIayQV/qRvkRJnpxuFv5q3Rtr4+S50Ie2IfrghRpphV7uyL3ktswyPHLTLVNxEW1cext9ZDOMb3S+Z4lwCz0gBjf9cZWGzBIsj6Wb0cr31Jo/xKuvBTwxRdTJGh20CvHXpXYeB/dNbY5BmlDP1Ys8dtP0/NrWTJ5/MG2BubaQ/xuUA+aw573XaYA+3t6vDVEfedNcDO8oNcQgp9zpp3p5Z4pipqKGyrqQAp/xnRt73TGRl6XQ9t/ZGdOSz6vSHaaVhtBKkOpoW8DxQ9nq9GWOK7R9r1ok919blzSfZvjJ5jUVM931XaUhIfWbv1f/UGgFRJSJ/G81s27trd0GCYXPXyq1tFVQBp/CxrBwuOfPtE7rTwFaFM4eo6tgwkbHFW4ZXJWk5MXWhkgRM7Frqq5mMophz6YT2suuEYgYVM7f0l2lXAC/O7PkobMzbeDRiMwREx5KBKGd6GFpmKfUua8b75nOcn9UB4aDtdCb70LwBCVotvQT0rKk1zxLp28iNilk1HQubfOG9S5gjjuqsXPfHgsmzA2CnQbj1yjansGXA8uq0NBOBUo2lvopsXGBw0tJNj53DY5sxd3jXaNIYQjsGT2knHp0= 11 | -------------------------------------------------------------------------------- /CONTRIBUTING.adoc: -------------------------------------------------------------------------------- 1 | All commits must include a __Signed-off-by__ trailer at the end of each commit message to indicate that the contributor agrees to the Developer Certificate of Origin. 2 | For additional details, please refer to the blog post https://spring.io/blog/2025/01/06/hello-dco-goodbye-cla-simplifying-contributions-to-spring[Hello DCO, Goodbye CLA: Simplifying Contributions to Spring]. 3 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "guard" 4 | gem "guard-shell" 5 | gem "asciidoctor" 6 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | require 'asciidoctor' 2 | require 'erb' 3 | 4 | options = {:mkdirs => true, :safe => :unsafe, :attributes => ['linkcss', 'allow-uri-read']} 5 | 6 | guard 'shell' do 7 | watch('.*.adoc') {|m| 8 | Asciidoctor.render_file('README.adoc', options.merge(:to_dir => 'target/generated-docs')) 9 | } 10 | end 11 | -------------------------------------------------------------------------------- /LICENSE.code.txt: -------------------------------------------------------------------------------- 1 | All code in this repository is: 2 | ======================================================================= 3 | Copyright (c) 2013 GoPivotal, Inc. All Rights Reserved 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | -------------------------------------------------------------------------------- /LICENSE.writing.txt: -------------------------------------------------------------------------------- 1 | Except where otherwise noted, this work is licensed under https://creativecommons.org/licenses/by-nd/3.0/ 2 | -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | --- 2 | tags: [security,javascript,rest,oauth] 3 | projects: [spring-security,spring-security-oauth,spring-boot] 4 | --- 5 | :toc: left 6 | :icons: font 7 | :source-highlighter: prettify 8 | :image-width: 500 9 | :doctype: book 10 | :star: {asterisk} 11 | :all: {asterisk}{asterisk} 12 | 13 | = Social Login with Spring Boot and OAuth 2.0 14 | 15 | This guide shows you how to build a sample app doing various things with "social login" using https://tools.ietf.org/html/rfc6749[OAuth 2.0] and https://projects.spring.io/spring-boot/[Spring Boot]. 16 | 17 | It starts with a simple, single-provider single-sign on, and works up to a client with a choice of authentication providers: 18 | https://github.com/settings/developers[GitHub] or https://developers.google.com/identity/protocols/OpenIDConnect[Google]. 19 | 20 | The samples are all single-page apps using Spring Boot and Spring Security on the back end. 21 | They also all use plain https://jquery.org/[jQuery] on the front end. 22 | But, the changes needed to convert to a different JavaScript framework or to use server-side rendering would be minimal. 23 | 24 | All samples are implemented using the native OAuth 2.0 support in https://docs.spring.io/spring-boot/docs/current-SNAPSHOT/reference/htmlsingle/#boot-features-security-oauth2[Spring Boot]. 25 | 26 | include::overview.adoc[] 27 | 28 | include::simple/README.adoc[leveloffset=+1] 29 | include::click/README.adoc[leveloffset=+1] 30 | include::logout/README.adoc[leveloffset=+1] 31 | include::two-providers/README.adoc[leveloffset=+1] 32 | include::custom-error/README.adoc[leveloffset=+1] 33 | 34 | == Conclusion 35 | 36 | We have seen how to use Spring Boot and Spring Security to build apps in a number of styles with very little effort. 37 | The main theme running through all of the samples is authentication using an external OAuth 2.0 provider. 38 | 39 | All of the sample apps can be easily extended and re-configured for more specific use cases, usually with nothing more than a configuration file change. 40 | Remember if you use versions of the samples in your own servers to register with GitHub (or similar) and get client credentials for your own host addresses. 41 | And remember not to put those credentials in source control! 42 | 43 | include::https://raw.githubusercontent.com/spring-guides/getting-started-macros/master/footer.adoc[] 44 | -------------------------------------------------------------------------------- /click/README.adoc: -------------------------------------------------------------------------------- 1 | [[_social_login_click]] 2 | = Add a Welcome Page 3 | 4 | In this section, you'll modify the <<_social_login_simple,simple>> app you just built by adding an explicit link to login with GitHub. 5 | Instead of being redirected immediately, the new link will be visible on the home page, and the user can choose to login or to stay unauthenticated. 6 | Only when the user has clicked on the link will the secure content be rendered. 7 | 8 | == Conditional Content on the Home Page 9 | 10 | To render content on the condition that the user is authenticated, you have the option of either server-side or client-side rendering. 11 | 12 | Here, you'll change the client side with https://jquery.org/[JQuery], though if you prefer to use something else, it shouldn't be very hard to translate the client code. 13 | 14 | To get started with the dynamic content, you need to mark a couple of HTML elements like so: 15 | 16 | .index.html 17 | [source,html] 18 | ---- 19 |
20 | With GitHub: click here 21 |
22 | 25 | ---- 26 | 27 | By default, the first `
` will show, and the second one won't. 28 | Note also the empty `` with an `id` attribute. 29 | 30 | In a moment, you'll add a server-side endpoint that will return the logged in user details as JSON. 31 | 32 | But, first, add the following JavaScript, which will hit that endpoint. 33 | Based on the endpoint's response, this JavaScript will populate the `` tag with the user's name and toggle the `
` appropriately: 34 | 35 | .index.html 36 | [source,html] 37 | ---- 38 | 45 | ---- 46 | 47 | Note that this JavaScript expects the server-side endpoint to be called `/user`. 48 | 49 | == The `/user` Endpoint 50 | 51 | Now, you'll add the server-side endpoint just mentioned, calling it `/user`. 52 | It will send back the currently logged-in user, which we can do quite easily in our main class: 53 | 54 | .SocialApplication.java 55 | [source,java] 56 | ---- 57 | @SpringBootApplication 58 | @RestController 59 | public class SocialApplication { 60 | 61 | @GetMapping("/user") 62 | public Map user(@AuthenticationPrincipal OAuth2User principal) { 63 | return Collections.singletonMap("name", principal.getAttribute("name")); 64 | } 65 | 66 | public static void main(String[] args) { 67 | SpringApplication.run(SocialApplication.class, args); 68 | } 69 | 70 | } 71 | ---- 72 | 73 | Note the use of `@RestController`, `@GetMapping`, and the `OAuth2User` injected into the handler method. 74 | 75 | WARNING: It's not a great idea to return a whole `OAuth2User` in an endpoint since it might contain information you would rather not reveal to a browser client. 76 | 77 | == Making the Home Page Public 78 | 79 | There's one final change you'll need to make. 80 | 81 | This app will now work fine and authenticate as before, but it's still going to redirect before showing the page. 82 | To make the link visible, we also need to switch off the security on the home page by extending `WebSecurityConfigurerAdapter`: 83 | 84 | .SocialApplication 85 | [source,java] 86 | ---- 87 | @SpringBootApplication 88 | @RestController 89 | public class SocialApplication extends WebSecurityConfigurerAdapter { 90 | 91 | // ... 92 | 93 | @Override 94 | protected void configure(HttpSecurity http) throws Exception { 95 | // @formatter:off 96 | http 97 | .authorizeRequests(a -> a 98 | .antMatchers("/", "/error", "/webjars/**").permitAll() 99 | .anyRequest().authenticated() 100 | ) 101 | .exceptionHandling(e -> e 102 | .authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)) 103 | ) 104 | .oauth2Login(); 105 | // @formatter:on 106 | } 107 | 108 | } 109 | ---- 110 | 111 | Spring Boot attaches special meaning to a `WebSecurityConfigurerAdapter` on the class annotated with `@SpringBootApplication`: 112 | It uses it to configure the security filter chain that carries the OAuth 2.0 authentication processor. 113 | 114 | The above configuration indicates a whitelist of permitted endpoints, with every other endpoint requiring authentication. 115 | 116 | You want to allow: 117 | 118 | * `/` since that's the page you just made dynamic, with some of its content visible to unauthenticated users 119 | * `/error` since that's a Spring Boot endpoint for displaying errors, and 120 | * `/webjars/**` since you'll want your JavaScript to run for all visitors, authenticated or not 121 | 122 | You won't see anything about `/user` in this configuration, though. 123 | Everything, including `/user` remains secure unless indicated because of the `.anyRequest().authenticated()` configuration at the end. 124 | 125 | Finally, since we are interfacing with the backend over Ajax, we'll want to configure endpoints to respond with a 401 instead of the default behavior of redirecting to a login page. 126 | Configuring the `authenticationEntryPoint` achieves this for us. 127 | 128 | With those changes in place, the application is complete, and if you run it and visit the home page you should see a nicely styled HTML link to "login with GitHub". 129 | The link takes you not directly to GitHub, but to the local path that processes the authentication (and sends a redirect to GitHub). 130 | Once you have authenticated, you get redirected back to the local app, where it now displays your name (assuming you have set up your permissions in GitHub to allow access to that data). 131 | -------------------------------------------------------------------------------- /click/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.example 7 | social-click 8 | 0.0.1-SNAPSHOT 9 | jar 10 | 11 | social-click 12 | Demo project for Spring Boot 13 | 14 | 15 | org.springframework.boot 16 | spring-boot-starter-parent 17 | 2.2.2.RELEASE 18 | 19 | 20 | 21 | 22 | UTF-8 23 | 1.8 24 | 25 | 26 | 27 | 28 | org.springframework.boot 29 | spring-boot-starter-oauth2-client 30 | 31 | 32 | org.springframework.boot 33 | spring-boot-starter-web 34 | 35 | 36 | org.webjars 37 | jquery 38 | 2.1.1 39 | 40 | 41 | org.webjars 42 | bootstrap 43 | 3.2.0 44 | 45 | 46 | org.webjars 47 | webjars-locator-core 48 | 49 | 50 | 51 | org.springframework.boot 52 | spring-boot-starter-test 53 | test 54 | 55 | 56 | 57 | 58 | 59 | 60 | org.springframework.boot 61 | spring-boot-maven-plugin 62 | 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /click/src/main/java/com/example/SocialApplication.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2015 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example; 17 | 18 | import java.util.Collections; 19 | import java.util.Map; 20 | 21 | import org.springframework.boot.SpringApplication; 22 | import org.springframework.boot.autoconfigure.SpringBootApplication; 23 | import org.springframework.http.HttpStatus; 24 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 25 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 26 | import org.springframework.security.core.annotation.AuthenticationPrincipal; 27 | import org.springframework.security.oauth2.core.user.OAuth2User; 28 | import org.springframework.security.web.authentication.HttpStatusEntryPoint; 29 | import org.springframework.web.bind.annotation.GetMapping; 30 | import org.springframework.web.bind.annotation.RestController; 31 | 32 | @SpringBootApplication 33 | @RestController 34 | public class SocialApplication extends WebSecurityConfigurerAdapter { 35 | 36 | @GetMapping("/user") 37 | public Map user(@AuthenticationPrincipal OAuth2User principal) { 38 | return Collections.singletonMap("name", principal.getAttribute("name")); 39 | } 40 | 41 | @Override 42 | protected void configure(HttpSecurity http) throws Exception { 43 | // @formatter:off 44 | http 45 | .authorizeRequests(a -> a 46 | .antMatchers("/", "/error", "/webjars/**").permitAll() 47 | .anyRequest().authenticated() 48 | ) 49 | .exceptionHandling(e -> e 50 | .authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)) 51 | ) 52 | .oauth2Login(); 53 | // @formatter:on 54 | } 55 | 56 | public static void main(String[] args) { 57 | SpringApplication.run(SocialApplication.class, args); 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /click/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | security: 3 | oauth2: 4 | client: 5 | registration: 6 | github: 7 | client-id: your-github-client-id 8 | client-secret: your-github-client-secret 9 | 10 | #logging: 11 | # level: 12 | # org.springframework.security: DEBUG 13 | -------------------------------------------------------------------------------- /click/src/main/resources/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Demo 7 | 8 | 9 | 10 | 12 | 13 | 15 | 16 | 17 |

Login

18 |
19 | With GitHub: click here 20 |
21 | 24 | 31 | 32 | -------------------------------------------------------------------------------- /click/src/test/java/com/example/SocialApplicationTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2015 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example; 17 | 18 | import org.junit.Test; 19 | import org.junit.runner.RunWith; 20 | import org.springframework.boot.test.context.SpringBootTest; 21 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 22 | 23 | @RunWith(SpringJUnit4ClassRunner.class) 24 | @SpringBootTest 25 | public class SocialApplicationTests { 26 | 27 | @Test 28 | public void contextLoads() { 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /custom-error/README.adoc: -------------------------------------------------------------------------------- 1 | [[_custom_error]] 2 | = Adding an Error Page for Unauthenticated Users 3 | 4 | In this section, you'll modify the <<_social_login_two_providers,two-providers>> app you built earlier to give some feedback to users that cannot authenticate. 5 | 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. 6 | The "organization" is a GitHub domain-specific concept, but similar rules could be devised for other providers. 7 | For example, with Google you might want to only authenticate users from a specific domain. 8 | 9 | == Switching to GitHub 10 | 11 | The <<_social_login_two_providers,two-providers>> sample uses GitHub as an OAuth 2.0 provider: 12 | 13 | .application.yml 14 | [source,yaml] 15 | ---- 16 | spring: 17 | security: 18 | oauth2: 19 | client: 20 | registration: 21 | github: 22 | client-id: bd1c0a783ccdd1c9b9e4 23 | client-secret: 1a9030fbca47a5b2c28e92f19050bb77824b5ad1 24 | # ... 25 | ---- 26 | 27 | == Detecting an Authentication Failure in the Client 28 | 29 | On the client, you might like to provide some feedback for a user that could not authenticate. 30 | To facilitate this, you can add a div to which you'll eventually add an informative message. 31 | 32 | .index.html 33 | ---- 34 |
35 | ---- 36 | 37 | Then, add a call to the `/error` endpoint, populating the `
` with the result: 38 | 39 | .index.html 40 | ---- 41 | $.get("/error", function(data) { 42 | if (data) { 43 | $(".error").html(data); 44 | } else { 45 | $(".error").html(''); 46 | } 47 | }); 48 | ---- 49 | 50 | The error function checks with the backend if there is any error to display 51 | 52 | == Adding an Error Message 53 | 54 | To support the retrieval of an error message, you'll need to capture it when authentication fails. 55 | To achieve this, you can configure an `AuthenticationFailureHandler`, like so: 56 | 57 | [source,java] 58 | ---- 59 | protected void configure(HttpSecurity http) throws Exception { 60 | // @formatter:off 61 | http 62 | // ... existing configuration 63 | .oauth2Login(o -> o 64 | .failureHandler((request, response, exception) -> { 65 | request.getSession().setAttribute("error.message", exception.getMessage()); 66 | handler.onAuthenticationFailure(request, response, exception); 67 | }) 68 | ); 69 | } 70 | ---- 71 | 72 | The above will save an error message to the session whenever authentication fails. 73 | 74 | Then, you can add a simple `/error` controller, like this one: 75 | 76 | .SocialApplication.java 77 | [source,java] 78 | ---- 79 | @GetMapping("/error") 80 | public String error(HttpServletRequest request) { 81 | String message = (String) request.getSession().getAttribute("error.message"); 82 | request.getSession().removeAttribute("error.message"); 83 | return message; 84 | } 85 | ---- 86 | 87 | 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. 88 | 89 | == Generating a 401 in the Server 90 | 91 | 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). 92 | 93 | To spice things up a bit, you can extend the authentication rule to reject users that are not in the right organization. 94 | 95 | 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. 96 | 97 | Fortunately, for such a simple use case, Spring Boot has provided an easy extension point: 98 | If you declare a `@Bean` of type `OAuth2UserService`, it will be used to identify the user principal. 99 | You can use that hook to assert the the user is in the correct organization, and throw an exception if not: 100 | 101 | .SocialApplication.java 102 | [source,java] 103 | ---- 104 | @Bean 105 | public OAuth2UserService oauth2UserService(WebClient rest) { 106 | DefaultOAuth2UserService delegate = new DefaultOAuth2UserService(); 107 | return request -> { 108 | OAuth2User user = delegate.loadUser(request); 109 | if (!"github".equals(request.getClientRegistration().getRegistrationId())) { 110 | return user; 111 | } 112 | 113 | OAuth2AuthorizedClient client = new OAuth2AuthorizedClient 114 | (request.getClientRegistration(), user.getName(), request.getAccessToken()); 115 | String url = user.getAttribute("organizations_url"); 116 | List> orgs = rest 117 | .get().uri(url) 118 | .attributes(oauth2AuthorizedClient(client)) 119 | .retrieve() 120 | .bodyToMono(List.class) 121 | .block(); 122 | 123 | if (orgs.stream().anyMatch(org -> "spring-projects".equals(org.get("login")))) { 124 | return user; 125 | } 126 | 127 | throw new OAuth2AuthenticationException(new OAuth2Error("invalid_token", "Not in Spring Team", "")); 128 | }; 129 | } 130 | ---- 131 | 132 | Note that this code is dependent on a `WebClient` instance for accessing the GitHub API on behalf of the authenticated user. 133 | 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). 134 | 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. 135 | If there is no match, it throws an `OAuth2AuthenticationException`, and this is picked up by Spring Security and turned in to a 401 response. 136 | 137 | 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`: 138 | 139 | [source,java] 140 | ---- 141 | @Bean 142 | public WebClient rest(ClientRegistrationRepository clients, OAuth2AuthorizedClientRepository authz) { 143 | ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2 = 144 | new ServletOAuth2AuthorizedClientExchangeFilterFunction(clients, authz); 145 | return WebClient.builder() 146 | .filter(oauth2).build(); 147 | } 148 | ---- 149 | 150 | TIP: Obviously the code above can be generalized to other authentication rules, some applicable to GitHub and some to other OAuth 2.0 providers. 151 | All you need is the `WebClient` and some knowledge of the provider's API. 152 | -------------------------------------------------------------------------------- /custom-error/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.example 7 | custom-error 8 | 0.0.1-SNAPSHOT 9 | jar 10 | 11 | custom-error 12 | Demo project for Spring Boot 13 | 14 | 15 | org.springframework.boot 16 | spring-boot-starter-parent 17 | 2.2.2.RELEASE 18 | 19 | 20 | 21 | 22 | UTF-8 23 | 1.8 24 | 25 | 26 | 27 | 28 | org.springframework.boot 29 | spring-boot-starter-oauth2-client 30 | 31 | 32 | org.springframework.boot 33 | spring-boot-starter-web 34 | 35 | 36 | org.springframework 37 | spring-webflux 38 | 39 | 40 | io.projectreactor.netty 41 | reactor-netty 42 | 43 | 44 | org.webjars 45 | js-cookie 46 | 2.1.0 47 | 48 | 49 | org.webjars 50 | jquery 51 | 2.1.1 52 | 53 | 54 | org.webjars 55 | bootstrap 56 | 3.2.0 57 | 58 | 59 | org.webjars 60 | webjars-locator-core 61 | 62 | 63 | 64 | org.springframework.boot 65 | spring-boot-starter-test 66 | test 67 | 68 | 69 | 70 | 71 | 72 | 73 | org.springframework.boot 74 | spring-boot-maven-plugin 75 | 76 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /custom-error/src/main/java/com/example/SocialApplication.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2015 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example; 17 | 18 | import java.util.Collections; 19 | import java.util.List; 20 | import java.util.Map; 21 | import javax.servlet.http.HttpServletRequest; 22 | 23 | import org.springframework.boot.SpringApplication; 24 | import org.springframework.boot.autoconfigure.SpringBootApplication; 25 | import org.springframework.context.annotation.Bean; 26 | import org.springframework.http.HttpStatus; 27 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 28 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 29 | import org.springframework.security.core.annotation.AuthenticationPrincipal; 30 | import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; 31 | import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; 32 | import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; 33 | import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; 34 | import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; 35 | import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; 36 | import org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction; 37 | import org.springframework.security.oauth2.core.OAuth2AuthenticationException; 38 | import org.springframework.security.oauth2.core.OAuth2Error; 39 | import org.springframework.security.oauth2.core.user.OAuth2User; 40 | import org.springframework.security.web.authentication.HttpStatusEntryPoint; 41 | import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; 42 | import org.springframework.security.web.csrf.CookieCsrfTokenRepository; 43 | import org.springframework.stereotype.Controller; 44 | import org.springframework.web.bind.annotation.GetMapping; 45 | import org.springframework.web.bind.annotation.ResponseBody; 46 | import org.springframework.web.reactive.function.client.WebClient; 47 | 48 | import static org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction.oauth2AuthorizedClient; 49 | 50 | @SpringBootApplication 51 | @Controller 52 | public class SocialApplication extends WebSecurityConfigurerAdapter { 53 | 54 | @Bean 55 | public WebClient rest(ClientRegistrationRepository clients, OAuth2AuthorizedClientRepository authz) { 56 | ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2 = 57 | new ServletOAuth2AuthorizedClientExchangeFilterFunction(clients, authz); 58 | return WebClient.builder() 59 | .filter(oauth2).build(); 60 | } 61 | 62 | @Bean 63 | public OAuth2UserService oauth2UserService(WebClient rest) { 64 | DefaultOAuth2UserService delegate = new DefaultOAuth2UserService(); 65 | return request -> { 66 | OAuth2User user = delegate.loadUser(request); 67 | if (!"github".equals(request.getClientRegistration().getRegistrationId())) { 68 | return user; 69 | } 70 | 71 | OAuth2AuthorizedClient client = new OAuth2AuthorizedClient 72 | (request.getClientRegistration(), user.getName(), request.getAccessToken()); 73 | String url = user.getAttribute("organizations_url"); 74 | List> orgs = rest 75 | .get().uri(url) 76 | .attributes(oauth2AuthorizedClient(client)) 77 | .retrieve() 78 | .bodyToMono(List.class) 79 | .block(); 80 | 81 | if (orgs.stream().anyMatch(org -> "spring-projects".equals(org.get("login")))) { 82 | return user; 83 | } 84 | 85 | throw new OAuth2AuthenticationException(new OAuth2Error("invalid_token", "Not in Spring Team", "")); 86 | }; 87 | } 88 | 89 | @GetMapping("/user") 90 | @ResponseBody 91 | public Map user(@AuthenticationPrincipal OAuth2User principal) { 92 | return Collections.singletonMap("name", principal.getAttribute("name")); 93 | } 94 | 95 | @GetMapping("/error") 96 | @ResponseBody 97 | public String error(HttpServletRequest request) { 98 | String message = (String) request.getSession().getAttribute("error.message"); 99 | request.getSession().removeAttribute("error.message"); 100 | return message; 101 | } 102 | 103 | @Override 104 | protected void configure(HttpSecurity http) throws Exception { 105 | SimpleUrlAuthenticationFailureHandler handler = new SimpleUrlAuthenticationFailureHandler("/"); 106 | 107 | // @formatter:off 108 | http.antMatcher("/**") 109 | .authorizeRequests(a -> a 110 | .antMatchers("/", "/error", "/webjars/**").permitAll() 111 | .anyRequest().authenticated() 112 | ) 113 | .exceptionHandling(e -> e 114 | .authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)) 115 | ) 116 | .csrf(c -> c 117 | .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) 118 | ) 119 | .logout(l -> l 120 | .logoutSuccessUrl("/").permitAll() 121 | ) 122 | .oauth2Login(o -> o 123 | .failureHandler((request, response, exception) -> { 124 | request.getSession().setAttribute("error.message", exception.getMessage()); 125 | handler.onAuthenticationFailure(request, response, exception); 126 | }) 127 | ); 128 | // @formatter:on 129 | } 130 | 131 | public static void main(String[] args) { 132 | SpringApplication.run(SocialApplication.class, args); 133 | } 134 | 135 | } 136 | -------------------------------------------------------------------------------- /custom-error/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | security: 3 | oauth2: 4 | client: 5 | registration: 6 | github: 7 | client-id: your-github-client-id 8 | client-secret: your-github-client-secret 9 | google: 10 | client-id: your-google-client-id 11 | client-secret: your-google-client-secret 12 | 13 | # logging: 14 | # level: 15 | # org.springframework.security: DEBUG 16 | -------------------------------------------------------------------------------- /custom-error/src/main/resources/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Demo 7 | 8 | 9 | 10 | 12 | 13 | 15 | 16 | 17 |

Login

18 |
19 |
20 | With GitHub: click here 21 |
22 |
23 | With Google: click here 24 |
25 | 31 | 32 | 67 | 68 | -------------------------------------------------------------------------------- /custom-error/src/test/java/com/example/SocialApplicationTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2015 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example; 17 | 18 | import org.junit.Test; 19 | import org.junit.runner.RunWith; 20 | import org.springframework.boot.test.context.SpringBootTest; 21 | import org.springframework.boot.test.mock.mockito.MockBean; 22 | import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; 23 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 24 | 25 | @RunWith(SpringJUnit4ClassRunner.class) 26 | @SpringBootTest 27 | public class SocialApplicationTests { 28 | 29 | @MockBean 30 | ClientRegistrationRepository clientRegistrationRepository; 31 | 32 | @Test 33 | public void contextLoads() { 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /logout/README.adoc: -------------------------------------------------------------------------------- 1 | [[_social_login_logout]] 2 | = Add a Logout Button 3 | 4 | In this section, we modify the <<_social_login_click,click>> app we built by adding a button that allows the user to log out of the app. 5 | This seems like a simple feature, but it requires a bit of care to implement, so it's worth spending some time discussing exactly how to do it. 6 | Most of the changes are to do with the fact that we are transforming the app from a read-only resource to a read-write one (logging out requires a state change), so the same changes would be needed in any realistic application that wasn't just static content. 7 | 8 | == Client Side Changes 9 | 10 | On the client, we just need to provide a logout button and some JavaScript to call back to the server to ask for the authentication to be cancelled. 11 | First, in the "authenticated" section of the UI, we add the button: 12 | 13 | .index.html 14 | ---- 15 |
16 | Logged in as: 17 |
18 | 19 |
20 |
21 | ---- 22 | 23 | and then we provide the `logout()` function that it refers to in the JavaScript: 24 | 25 | .index.html 26 | ---- 27 | var logout = function() { 28 | $.post("/logout", function() { 29 | $("#user").html(''); 30 | $(".unauthenticated").show(); 31 | $(".authenticated").hide(); 32 | }) 33 | return true; 34 | } 35 | ---- 36 | 37 | The `logout()` function does a POST to `/logout` and then clears the dynamic content. 38 | Now we can switch over to the server side to implement that endpoint. 39 | 40 | == Adding a Logout Endpoint 41 | 42 | Spring Security has built in support for a `/logout` endpoint which will do the right thing for us (clear the session and invalidate the cookie). 43 | To configure the endpoint we simply extend the existing `configure()` method in our `WebSecurityConfigurerAdapter`: 44 | 45 | .SocialApplication.java 46 | [source,java] 47 | ---- 48 | @Override 49 | protected void configure(HttpSecurity http) throws Exception { 50 | // @formatter:off 51 | http 52 | // ... existing code here 53 | .logout(l -> l 54 | .logoutSuccessUrl("/").permitAll() 55 | ) 56 | // ... existing code here 57 | // @formatter:on 58 | } 59 | ---- 60 | 61 | The `/logout` endpoint requires us to POST to it, and to protect the user from Cross Site Request Forgery (CSRF, pronounced "sea surf"), it requires a token to be included in the request. 62 | The value of the token is linked to the current session, which is what provides the protection, so we need a way to get that data into our JavaScript app. 63 | 64 | Many JavaScript frameworks have built in support for CSRF (e.g. in Angular they call it XSRF), but it is often implemented in a slightly different way than the out-of-the box behaviour of Spring Security. 65 | For instance, in Angular, the front end would like the server to send it a cookie called "XSRF-TOKEN" and if it sees that, it will send the value back as a header named "X-XSRF-TOKEN". 66 | We can implement the same behaviour with our simple jQuery client, and then the server-side changes will work with other front end implementations with no or very few changes. 67 | To teach Spring Security about this we need to add a filter that creates the cookie. 68 | 69 | In the `WebSecurityConfigurerAdapter` we do the following: 70 | 71 | .SocialApplication.java 72 | [source,java] 73 | ---- 74 | @Override 75 | protected void configure(HttpSecurity http) throws Exception { 76 | // @formatter:off 77 | http 78 | // ... existing code here 79 | .csrf(c -> c 80 | .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) 81 | ) 82 | // ... existing code here 83 | // @formatter:on 84 | } 85 | ---- 86 | 87 | == Adding the CSRF Token in the Client 88 | 89 | Since we are not using a higher level framework in this sample, you'll need to explicitly add the CSRF token, which you just made available as a cookie from the backend. 90 | To make the code a bit simpler, include the `js-cookie` library: 91 | 92 | .pom.xml 93 | [source,xml] 94 | ---- 95 | 96 | org.webjars 97 | js-cookie 98 | 2.1.0 99 | 100 | ---- 101 | 102 | And then, you can reference it in your HTML: 103 | 104 | .index.html 105 | [source,html] 106 | ---- 107 | 108 | ---- 109 | 110 | Finally, you can use `Cookies` convenience methods in XHR: 111 | 112 | .index.html 113 | [source,html] 114 | ---- 115 | $.ajaxSetup({ 116 | beforeSend : function(xhr, settings) { 117 | if (settings.type == 'POST' || settings.type == 'PUT' 118 | || settings.type == 'DELETE') { 119 | if (!(/^http:.*/.test(settings.url) || /^https:.*/ 120 | .test(settings.url))) { 121 | // Only send the token to relative URLs i.e. locally. 122 | xhr.setRequestHeader("X-XSRF-TOKEN", 123 | Cookies.get('XSRF-TOKEN')); 124 | } 125 | } 126 | } 127 | }); 128 | ---- 129 | 130 | == Ready To Roll! 131 | 132 | With those changes in place, we are ready to run the app and try out the new logout button. 133 | Start the app and load the home page in a new browser window. 134 | Click on the "Login" link to take you to GitHub (if you are already logged in there you might not notice the redirect). 135 | Click on the "Logout" button to cancel the current session and return the app to the unauthenticated state. 136 | If you are curious, you should be able to see the new cookies and headers in the requests that the browser exchanges with the local server. 137 | 138 | Remember that now the logout endpoint is working with the browser client, then all other HTTP requests (POST, PUT, DELETE, etc.) will also work just as well. 139 | So this should be a good platform for an application with some more realistic features. -------------------------------------------------------------------------------- /logout/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.example 7 | social-logout 8 | 0.0.1-SNAPSHOT 9 | jar 10 | 11 | social-logout 12 | Demo project for Spring Boot 13 | 14 | 15 | org.springframework.boot 16 | spring-boot-starter-parent 17 | 2.2.2.RELEASE 18 | 19 | 20 | 21 | 22 | UTF-8 23 | 1.8 24 | 25 | 26 | 27 | 28 | org.springframework.boot 29 | spring-boot-starter-oauth2-client 30 | 31 | 32 | org.springframework.boot 33 | spring-boot-starter-web 34 | 35 | 36 | org.webjars 37 | js-cookie 38 | 2.1.0 39 | 40 | 41 | org.webjars 42 | jquery 43 | 2.1.1 44 | 45 | 46 | org.webjars 47 | bootstrap 48 | 3.2.0 49 | 50 | 51 | org.webjars 52 | webjars-locator-core 53 | 54 | 55 | 56 | org.springframework.boot 57 | spring-boot-starter-test 58 | test 59 | 60 | 61 | 62 | 63 | 64 | 65 | org.springframework.boot 66 | spring-boot-maven-plugin 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /logout/src/main/java/com/example/SocialApplication.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2015 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example; 17 | 18 | import java.util.Collections; 19 | import java.util.Map; 20 | 21 | import org.springframework.boot.SpringApplication; 22 | import org.springframework.boot.autoconfigure.SpringBootApplication; 23 | import org.springframework.http.HttpStatus; 24 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 25 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 26 | import org.springframework.security.core.annotation.AuthenticationPrincipal; 27 | import org.springframework.security.oauth2.core.user.OAuth2User; 28 | import org.springframework.security.web.authentication.HttpStatusEntryPoint; 29 | import org.springframework.security.web.csrf.CookieCsrfTokenRepository; 30 | import org.springframework.web.bind.annotation.RequestMapping; 31 | import org.springframework.web.bind.annotation.RestController; 32 | 33 | @SpringBootApplication 34 | @RestController 35 | public class SocialApplication extends WebSecurityConfigurerAdapter { 36 | 37 | @RequestMapping("/user") 38 | public Map user(@AuthenticationPrincipal OAuth2User principal) { 39 | return Collections.singletonMap("name", principal.getAttribute("name")); 40 | } 41 | 42 | @Override 43 | protected void configure(HttpSecurity http) throws Exception { 44 | // @formatter:off 45 | http 46 | .authorizeRequests(a -> a 47 | .antMatchers("/", "/error", "/webjars/**").permitAll() 48 | .anyRequest().authenticated() 49 | ) 50 | .exceptionHandling(e -> e 51 | .authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)) 52 | ) 53 | .csrf(c -> c 54 | .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) 55 | ) 56 | .logout(l -> l 57 | .logoutSuccessUrl("/").permitAll() 58 | ) 59 | .oauth2Login(); 60 | // @formatter:on 61 | } 62 | 63 | public static void main(String[] args) { 64 | SpringApplication.run(SocialApplication.class, args); 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /logout/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | security: 3 | oauth2: 4 | client: 5 | registration: 6 | github: 7 | client-id: your-github-client-id 8 | client-secret: your-github-client-secret 9 | 10 | #logging: 11 | # level: 12 | # org.springframework.security: DEBUG 13 | -------------------------------------------------------------------------------- /logout/src/main/resources/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Demo 7 | 8 | 9 | 10 | 12 | 13 | 15 | 16 | 17 |

Login

18 |
19 | With GitHub: click here 20 |
21 | 27 | 29 | 58 | 59 | -------------------------------------------------------------------------------- /logout/src/test/java/com/example/SocialApplicationTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2015 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example; 17 | 18 | import org.junit.Test; 19 | import org.junit.runner.RunWith; 20 | import org.springframework.boot.test.context.SpringBootTest; 21 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 22 | 23 | @RunWith(SpringJUnit4ClassRunner.class) 24 | @SpringBootTest 25 | public class SocialApplicationTests { 26 | 27 | @Test 28 | public void contextLoads() { 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # https://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven2 Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /etc/mavenrc ] ; then 40 | . /etc/mavenrc 41 | fi 42 | 43 | if [ -f "$HOME/.mavenrc" ] ; then 44 | . "$HOME/.mavenrc" 45 | fi 46 | 47 | fi 48 | 49 | # OS specific support. $var _must_ be set to either true or false. 50 | cygwin=false; 51 | darwin=false; 52 | mingw=false 53 | case "`uname`" in 54 | CYGWIN*) cygwin=true ;; 55 | MINGW*) mingw=true;; 56 | Darwin*) darwin=true 57 | # 58 | # Look for the Apple JDKs first to preserve the existing behaviour, and then look 59 | # for the new JDKs provided by Oracle. 60 | # 61 | if [ -z "$JAVA_HOME" ] && [ -L /System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK ] ; then 62 | # 63 | # Apple JDKs 64 | # 65 | export JAVA_HOME=/System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK/Home 66 | fi 67 | 68 | if [ -z "$JAVA_HOME" ] && [ -L /System/Library/Java/JavaVirtualMachines/CurrentJDK ] ; then 69 | # 70 | # Apple JDKs 71 | # 72 | export JAVA_HOME=/System/Library/Java/JavaVirtualMachines/CurrentJDK/Contents/Home 73 | fi 74 | 75 | if [ -z "$JAVA_HOME" ] && [ -L "/Library/Java/JavaVirtualMachines/CurrentJDK" ] ; then 76 | # 77 | # Oracle JDKs 78 | # 79 | export JAVA_HOME=/Library/Java/JavaVirtualMachines/CurrentJDK/Contents/Home 80 | fi 81 | 82 | if [ -z "$JAVA_HOME" ] && [ -x "/usr/libexec/java_home" ]; then 83 | # 84 | # Apple JDKs 85 | # 86 | export JAVA_HOME=`/usr/libexec/java_home` 87 | fi 88 | ;; 89 | esac 90 | 91 | if [ -z "$JAVA_HOME" ] ; then 92 | if [ -r /etc/gentoo-release ] ; then 93 | JAVA_HOME=`java-config --jre-home` 94 | fi 95 | fi 96 | 97 | if [ -z "$M2_HOME" ] ; then 98 | ## resolve links - $0 may be a link to maven's home 99 | PRG="$0" 100 | 101 | # need this for relative symlinks 102 | while [ -h "$PRG" ] ; do 103 | ls=`ls -ld "$PRG"` 104 | link=`expr "$ls" : '.*-> \(.*\)$'` 105 | if expr "$link" : '/.*' > /dev/null; then 106 | PRG="$link" 107 | else 108 | PRG="`dirname "$PRG"`/$link" 109 | fi 110 | done 111 | 112 | saveddir=`pwd` 113 | 114 | M2_HOME=`dirname "$PRG"`/.. 115 | 116 | # make it fully qualified 117 | M2_HOME=`cd "$M2_HOME" && pwd` 118 | 119 | cd "$saveddir" 120 | # echo Using m2 at $M2_HOME 121 | fi 122 | 123 | # For Cygwin, ensure paths are in UNIX format before anything is touched 124 | if $cygwin ; then 125 | [ -n "$M2_HOME" ] && 126 | M2_HOME=`cygpath --unix "$M2_HOME"` 127 | [ -n "$JAVA_HOME" ] && 128 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 129 | [ -n "$CLASSPATH" ] && 130 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 131 | fi 132 | 133 | # For Migwn, ensure paths are in UNIX format before anything is touched 134 | if $mingw ; then 135 | [ -n "$M2_HOME" ] && 136 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 137 | [ -n "$JAVA_HOME" ] && 138 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 139 | # TODO classpath? 140 | fi 141 | 142 | if [ -z "$JAVA_HOME" ]; then 143 | javaExecutable="`which javac`" 144 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 145 | # readlink(1) is not available as standard on Solaris 10. 146 | readLink=`which readlink` 147 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 148 | if $darwin ; then 149 | javaHome="`dirname \"$javaExecutable\"`" 150 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 151 | else 152 | javaExecutable="`readlink -f \"$javaExecutable\"`" 153 | fi 154 | javaHome="`dirname \"$javaExecutable\"`" 155 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 156 | JAVA_HOME="$javaHome" 157 | export JAVA_HOME 158 | fi 159 | fi 160 | fi 161 | 162 | if [ -z "$JAVACMD" ] ; then 163 | if [ -n "$JAVA_HOME" ] ; then 164 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 165 | # IBM's JDK on AIX uses strange locations for the executables 166 | JAVACMD="$JAVA_HOME/jre/sh/java" 167 | else 168 | JAVACMD="$JAVA_HOME/bin/java" 169 | fi 170 | else 171 | JAVACMD="`which java`" 172 | fi 173 | fi 174 | 175 | if [ ! -x "$JAVACMD" ] ; then 176 | echo "Error: JAVA_HOME is not defined correctly." >&2 177 | echo " We cannot execute $JAVACMD" >&2 178 | exit 1 179 | fi 180 | 181 | if [ -z "$JAVA_HOME" ] ; then 182 | echo "Warning: JAVA_HOME environment variable is not set." 183 | fi 184 | 185 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 186 | 187 | # For Cygwin, switch paths to Windows format before running java 188 | if $cygwin; then 189 | [ -n "$M2_HOME" ] && 190 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 191 | [ -n "$JAVA_HOME" ] && 192 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 193 | [ -n "$CLASSPATH" ] && 194 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 195 | fi 196 | 197 | # traverses directory structure from process work directory to filesystem root 198 | # first directory with .mvn subdirectory is considered project base directory 199 | find_maven_basedir() { 200 | local basedir=$(pwd) 201 | local wdir=$(pwd) 202 | while [ "$wdir" != '/' ] ; do 203 | if [ -d "$wdir"/.mvn ] ; then 204 | basedir=$wdir 205 | break 206 | fi 207 | wdir=$(cd "$wdir/.."; pwd) 208 | done 209 | echo "${basedir}" 210 | } 211 | 212 | # concatenates all lines of a file 213 | concat_lines() { 214 | if [ -f "$1" ]; then 215 | echo "$(tr -s '\n' ' ' < "$1")" 216 | fi 217 | } 218 | 219 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-$(find_maven_basedir)} 220 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 221 | 222 | # Provide a "standardized" way to retrieve the CLI args that will 223 | # work with both Windows and non-Windows executions. 224 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" 225 | export MAVEN_CMD_LINE_ARGS 226 | 227 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 228 | 229 | exec "$JAVACMD" \ 230 | $MAVEN_OPTS \ 231 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 232 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 233 | ${WRAPPER_LAUNCHER} "$@" 234 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM https://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven2 Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' 39 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 40 | 41 | @REM set %HOME% to equivalent of $HOME 42 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 43 | 44 | @REM Execute a user defined script before this one 45 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 46 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 47 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" 48 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" 49 | :skipRcPre 50 | 51 | @setlocal 52 | 53 | set ERROR_CODE=0 54 | 55 | @REM To isolate internal variables from possible post scripts, we use another setlocal 56 | @setlocal 57 | 58 | @REM ==== START VALIDATION ==== 59 | if not "%JAVA_HOME%" == "" goto OkJHome 60 | 61 | echo. 62 | echo Error: JAVA_HOME not found in your environment. >&2 63 | echo Please set the JAVA_HOME variable in your environment to match the >&2 64 | echo location of your Java installation. >&2 65 | echo. 66 | goto error 67 | 68 | :OkJHome 69 | if exist "%JAVA_HOME%\bin\java.exe" goto init 70 | 71 | echo. 72 | echo Error: JAVA_HOME is set to an invalid directory. >&2 73 | echo JAVA_HOME = "%JAVA_HOME%" >&2 74 | echo Please set the JAVA_HOME variable in your environment to match the >&2 75 | echo location of your Java installation. >&2 76 | echo. 77 | goto error 78 | 79 | @REM ==== END VALIDATION ==== 80 | 81 | :init 82 | 83 | set MAVEN_CMD_LINE_ARGS=%* 84 | 85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 86 | @REM Fallback to current working directory if not found. 87 | 88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 90 | 91 | set EXEC_DIR=%CD% 92 | set WDIR=%EXEC_DIR% 93 | :findBaseDir 94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 95 | cd .. 96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 97 | set WDIR=%CD% 98 | goto findBaseDir 99 | 100 | :baseDirFound 101 | set MAVEN_PROJECTBASEDIR=%WDIR% 102 | cd "%EXEC_DIR%" 103 | goto endDetectBaseDir 104 | 105 | :baseDirNotFound 106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 107 | cd "%EXEC_DIR%" 108 | 109 | :endDetectBaseDir 110 | 111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 112 | 113 | @setlocal EnableExtensions EnableDelayedExpansion 114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 116 | 117 | :endReadAdditionalConfig 118 | 119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 120 | 121 | set WRAPPER_JAR="".\.mvn\wrapper\maven-wrapper.jar"" 122 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 123 | 124 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CMD_LINE_ARGS% 125 | if ERRORLEVEL 1 goto error 126 | goto end 127 | 128 | :error 129 | set ERROR_CODE=1 130 | 131 | :end 132 | @endlocal & set ERROR_CODE=%ERROR_CODE% 133 | 134 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 135 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 136 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 137 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 138 | :skipRcPost 139 | 140 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 141 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 142 | 143 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 144 | 145 | exit /B %ERROR_CODE% -------------------------------------------------------------------------------- /overview.adoc: -------------------------------------------------------------------------------- 1 | There are several samples building on each other, adding new features at each step: 2 | 3 | * <<_social_login_simple,**simple**>>: a very basic static app with just a home page and 4 | unconditional login via Spring Boot's OAuth 2.0 configuration properties 5 | (if you visit the home page, you will be automatically redirected to GitHub). 6 | 7 | * <<_social_login_click,**click**>>: adds an explicit link that the user has to click to login. 8 | 9 | * <<_social_login_logout,**logout**>>: adds a logout link as well for authenticated users. 10 | 11 | * <<_social_login_two_providers,**two-providers**>>: adds a second login provider so the user can 12 | choose on the home page which one to use. 13 | 14 | * <<_social_login_custom_error,**custom-error**>>: adds an error message for unauthenticated users, 15 | and a custom authentication based on GitHub's API. 16 | 17 | NOTE: The changes needed to migrate from one app to the next one in the feature ladder can be tracked in 18 | https://github.com/spring-guides/tut-spring-boot-oauth2[the source code]. 19 | Each version of the app is its own directory so that you can compare their differences. 20 | 21 | Each app can be imported into an IDE. You can run the `main` method in `SocialApplication` to start an app. 22 | They all come up with a home page on http://localhost:8080 23 | (and all require that you have at least a GitHub and Google account if you want to log in and see the content). 24 | 25 | You can also run all the apps on the command line using `mvn spring-boot:run` 26 | or by building the jar file and running it with `mvn package` and `java -jar target/*.jar` 27 | (per the 28 | https://docs.spring.io/spring-boot/docs/current-SNAPSHOT/reference/htmlsingle/#getting-started-first-application-run[Spring 29 | Boot docs] and other https://spring.io/guides/gs/spring-boot/[available documentation]). 30 | There is no need to install Maven if you use the https://github.com/takari/maven-wrapper[wrapper] at the top level, 31 | e.g. 32 | 33 | ``` 34 | $ cd simple 35 | $ ../mvnw package 36 | $ java -jar target/*.jar 37 | ``` 38 | 39 | NOTE: The apps all work on `localhost:8080` because they'll use OAuth 2.0 clients registered with GitHub and Google for that address. 40 | To run them on a different host or port, you need to register your apps that way. 41 | There is no danger of leaking your credentials beyond localhost if you use the default values. 42 | But, be careful what you expose on the Internet, and don't put your own app registrations in public source control. 43 | 44 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | org.springframework.guides 6 | tut-spring-boot-oauth 7 | 0.0.1-SNAPSHOT 8 | pom 9 | Spring Security OAuth2 Samples 10 | 11 | 12 | simple 13 | click 14 | logout 15 | two-providers 16 | custom-error 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /simple/README.adoc: -------------------------------------------------------------------------------- 1 | [[_social_login_simple]] 2 | = Single Sign On With GitHub 3 | 4 | In this section, you'll create a minimal application that uses GitHub for authentication. 5 | This will be quite easy by taking advantage of the autoconfiguration features in Spring Boot. 6 | 7 | == Creating a New Project 8 | 9 | First, you need to create a Spring Boot application, which can be done in a number of ways. 10 | The easiest is to go to https://start.spring.io and generate an empty project (choosing the "Web" dependency as a starting point). 11 | Equivalently, do this on the command line: 12 | 13 | [source] 14 | ---- 15 | $ mkdir ui && cd ui 16 | $ curl https://start.spring.io/starter.tgz -d style=web -d name=simple | tar -xzvf - 17 | ---- 18 | 19 | You can then import that project into your favorite IDE (it's a normal Maven Java project by default), or just work with the files and `mvn` on the command line. 20 | 21 | == Add a Home Page 22 | 23 | In your new project, create `index.html` in the `src/main/resources/static` folder. 24 | You should add some stylesheets and JavaScript links so the result looks like this: 25 | 26 | .index.html 27 | [source,html] 28 | ---- 29 | 30 | 31 | 32 | 33 | 34 | Demo 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 |

Demo

44 |
45 | 46 | 47 | ---- 48 | 49 | None of this is necessary to demonstrate the OAuth 2.0 login features, but it'll be nice to have a pleasant UI in the end, so you might as well start with some basic stuff in the home page. 50 | 51 | If you start the app and load the home page, you'll notice that the stylesheets have not been loaded. 52 | So, you need to add those as well by adding jQuery and Twitter Bootstrap: 53 | 54 | .pom.xml 55 | [source,xml] 56 | ---- 57 | 58 | org.webjars 59 | jquery 60 | 3.4.1 61 | 62 | 63 | org.webjars 64 | bootstrap 65 | 4.3.1 66 | 67 | 68 | org.webjars 69 | webjars-locator-core 70 | 71 | ---- 72 | 73 | The final dependency is the webjars "locator" which is provided as a library by the webjars site. 74 | Spring can use the locator to locate static assets in webjars without needing to know the exact versions (hence the versionless `/webjars/{all}` links in the `index.html`). 75 | The webjar locator is activated by default in a Spring Boot app, as long as you don't switch off the MVC autoconfiguration. 76 | 77 | With those changes in place, you should have a nice looking home page for your app. 78 | 79 | == Securing the Application with GitHub and Spring Security 80 | 81 | To make the application secure, you can simply add Spring Security as a dependency. 82 | Since you're wanting to do a "social" login (delegate to GitHub), you should include the Spring Security OAuth 2.0 Client starter: 83 | 84 | .pom.xml 85 | [source,xml] 86 | ---- 87 | 88 | org.springframework.boot 89 | spring-boot-starter-oauth2-client 90 | 91 | ---- 92 | 93 | By adding that, it will secure your app with OAuth 2.0 by default. 94 | 95 | Next, you need to configure your app to use GitHub as the authentication provider. 96 | To achieve this, do the following: 97 | 98 | * <> 99 | * <> 100 | * <> 101 | 102 | [[github-register-application]] 103 | === Add a New GitHub App 104 | 105 | To use GitHub's OAuth 2.0 authentication system for login, you must first https://github.com/settings/developers[Add a new GitHub app]. 106 | 107 | Select "New OAuth App" and then the "Register a new OAuth application" page is presented. 108 | Enter an app name and description. 109 | Then, enter your app's home page, which should be http://localhost:8080, in this case. 110 | Finally, indicate the Authorization callback URL as `http://localhost:8080/login/oauth2/code/github` and click _Register Application_. 111 | 112 | The OAuth redirect URI is the path in the application that the end-user's user-agent is redirected back to after they have authenticated with GitHub and have granted access to the application on the _Authorize application_ page. 113 | 114 | TIP: The default redirect URI template is `{baseUrl}/login/oauth2/code/{registrationId}`. 115 | The *_registrationId_* is a unique identifier for the `ClientRegistration`. 116 | 117 | [[github-application-config]] 118 | === Configure `application.yml` 119 | 120 | Then, to make the link to GitHub, add the following to your `application.yml`: 121 | 122 | .application.yml 123 | [source,yaml] 124 | ---- 125 | spring: 126 | security: 127 | oauth2: 128 | client: 129 | registration: 130 | github: 131 | clientId: github-client-id 132 | clientSecret: github-client-secret 133 | # ... 134 | ---- 135 | 136 | Simply use the OAuth 2.0 credentials you just created with GitHub, replacing `github-client-id` with the client id and `github-client-secret` with the client secret. 137 | 138 | [[github-boot-application]] 139 | ==== Boot Up the Application 140 | 141 | With that change, you can run your app again and visit the home page at http://localhost:8080. 142 | Now, instead of the home page, you should be redirected to login with GitHub. 143 | If you do that, and accept any authorizations you are asked to make, you will be redirected back to the local app, and the home page will be visible. 144 | 145 | If you stay logged in to GitHub, you won't have to re-authenticate with this local app, even if you open it in a fresh browser with no cookies and no cached data. 146 | (That's what Single Sign-On means.) 147 | 148 | TIP: If you are working through this section with the sample application, be sure to clear your browser cache of cookies and HTTP Basic credentials. 149 | The best way to do that for a single server is to open a new private window. 150 | 151 | **** 152 | It's safe to grant access to this sample since only the app running locally can use the tokens and the scope it asks for is limited. 153 | Be aware of what you are approving when you log into apps like this though: 154 | They might ask for permission to do more than you are comfortable with (e.g. they might ask for permission to change your personal data, which is unlikely to be in your interest). 155 | **** 156 | 157 | == What Just Happened? 158 | 159 | The app you just wrote, in OAuth 2.0 terms, is a _Client Application_, and it uses the https://tools.ietf.org/html/rfc6749#section-4[authorization code grant] to obtain an access token from GitHub (the Authorization Server). 160 | 161 | It then uses the access token to ask GitHub for some personal details (only what you permitted it to do), including your login ID and your name. 162 | In this phase, GitHub is acting as a Resource Server, decoding the token that you send and checking if it gives the app permission to access the user's details. 163 | If that process is successful, the app inserts the user details into the Spring Security context so that you are authenticated. 164 | 165 | If you look in the browser tools (F12 on Chrome or Firefox) and follow the network traffic for all the hops, you will see the redirects back and forth with GitHub, and finally you'll land back on the home page with a new `Set-Cookie` header. 166 | This cookie (`JSESSIONID` by default) is a token for your authentication details for Spring (or any servlet-based) applications. 167 | 168 | So we have a secure application, in the sense that to see any content a user has to authenticate with an external provider (GitHub). 169 | 170 | We wouldn't want to use that for an internet banking website. 171 | But for basic identification purposes, and to segregate content between different users of your site, it's an excellent starting point. 172 | That's why this kind of authentication is very popular these days. 173 | 174 | In the next section, we are going to add some basic features to the application. 175 | We'll also make it a bit more obvious to users what is going on when they get that initial redirect to GitHub. 176 | -------------------------------------------------------------------------------- /simple/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.example 7 | social-simple 8 | 0.0.1-SNAPSHOT 9 | jar 10 | 11 | social-simple 12 | Demo project for Spring Boot 13 | 14 | 15 | org.springframework.boot 16 | spring-boot-starter-parent 17 | 2.2.2.RELEASE 18 | 19 | 20 | 21 | 22 | UTF-8 23 | 1.8 24 | 25 | 26 | 27 | 28 | org.springframework.boot 29 | spring-boot-starter-oauth2-client 30 | 31 | 32 | org.springframework.boot 33 | spring-boot-starter-web 34 | 35 | 36 | org.webjars 37 | jquery 38 | 2.1.1 39 | 40 | 41 | org.webjars 42 | bootstrap 43 | 3.2.0 44 | 45 | 46 | org.webjars 47 | webjars-locator-core 48 | 49 | 50 | 51 | org.springframework.boot 52 | spring-boot-starter-test 53 | test 54 | 55 | 56 | 57 | 58 | 59 | 60 | org.springframework.boot 61 | spring-boot-maven-plugin 62 | 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /simple/src/main/java/com/example/SocialApplication.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2015 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example; 17 | 18 | import org.springframework.boot.SpringApplication; 19 | import org.springframework.boot.autoconfigure.SpringBootApplication; 20 | 21 | @SpringBootApplication 22 | public class SocialApplication { 23 | 24 | public static void main(String[] args) { 25 | SpringApplication.run(SocialApplication.class, args); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /simple/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | security: 3 | oauth2: 4 | client: 5 | registration: 6 | github: 7 | client-id: your-github-client-id 8 | client-secret: your-github-client-secret 9 | -------------------------------------------------------------------------------- /simple/src/main/resources/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Demo 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |

Demo

16 |
17 | 18 | -------------------------------------------------------------------------------- /simple/src/test/java/com/example/SocialApplicationTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2015 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example; 17 | 18 | import org.junit.Test; 19 | import org.junit.runner.RunWith; 20 | import org.springframework.boot.test.context.SpringBootTest; 21 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 22 | 23 | @RunWith(SpringJUnit4ClassRunner.class) 24 | @SpringBootTest 25 | public class SocialApplicationTests { 26 | 27 | @Test 28 | public void contextLoads() { 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /two-providers/README.adoc: -------------------------------------------------------------------------------- 1 | [[_social_login_first_party]] 2 | = Login with GitHub 3 | 4 | In this section, you'll modify the <<_social_login_logout,logout>> app you built already, adding a sticker page so that the end-user can choose between multiple sets of credentials. 5 | 6 | Let's add Google as a second option for the end user. 7 | 8 | [[google-initial-setup]] 9 | == Initial setup 10 | 11 | To use Google's OAuth 2.0 authentication system for login, you must set up a project in the Google API Console to obtain OAuth 2.0 credentials. 12 | 13 | NOTE: https://developers.google.com/identity/protocols/OpenIDConnect[Google's OAuth 2.0 implementation] for authentication conforms to the 14 | https://openid.net/connect/[OpenID Connect 1.0] specification and is https://openid.net/certification/[OpenID Certified]. 15 | 16 | Follow the instructions on the https://developers.google.com/identity/protocols/OpenIDConnect[OpenID Connect] page, starting in the section, "Setting up OAuth 2.0". 17 | 18 | After completing the "Obtain OAuth 2.0 credentials" instructions, you should have a new OAuth Client with credentials consisting of a Client ID and a Client Secret. 19 | 20 | [[google-redirect-uri]] 21 | == Setting the redirect URI 22 | 23 | Also, you'll need to supply a redirect URI, as you did for GitHub earlier. 24 | 25 | In the "Set a redirect URI" sub-section, ensure that the *Authorized redirect URIs* field is set to `http://localhost:8080/login/oauth2/code/google`. 26 | 27 | == Adding the Client Registration 28 | 29 | Then, you need to configure the client to point Google. 30 | Because Spring Security is built with multiple clients in mind, you can add our Google credentials alongside the ones you created for GitHub: 31 | 32 | .application.yml 33 | [source,yaml] 34 | ---- 35 | spring: 36 | security: 37 | oauth2: 38 | client: 39 | registration: 40 | github: 41 | clientId: github-client-id 42 | clientSecret: github-client-secret 43 | google: 44 | client-id: google-client-id 45 | client-secret: google-client-secret 46 | ---- 47 | 48 | As you can see, Google is another provider that Spring Security ships out-of-the-box support for. 49 | 50 | == Adding the Login Link 51 | 52 | In the client, the change is trivial - you can just add another link: 53 | 54 | .index.html 55 | [source,html] 56 | ---- 57 |
58 |
59 | With GitHub: click here 60 |
61 |
62 | With Google: click here 63 |
64 |
65 | ---- 66 | 67 | NOTE: The final path in the URL should match the client registration id in `application.yml`. 68 | 69 | TIP: Spring Security ships with a default provider selection page that can be reached by pointing to `/login` instead of `/oauth2/authorization/{registrationId}`. 70 | 71 | == How to Add a Local User Database 72 | 73 | Many applications need to hold data about their users locally, even if authentication is delegated to an external provider. 74 | We don't show the code here, but it is easy to do in two steps. 75 | 76 | 1. Choose a backend for your database, and set up some repositories (using Spring Data, say) for a custom `User` object that suits your needs and can be populated, fully or partially, from external authentication. 77 | 78 | 2. Implement and expose `OAuth2UserService` to call the Authorization Server as well as your database. 79 | Your implementation can delegate to the default implementation, which will do the heavy lifting of calling the Authorization Server. 80 | Your implementation should return something that extends your custom `User` object and implements `OAuth2User`. 81 | 82 | Hint: add a field in the `User` object to link to a unique identifier in the external provider (not the user's name, but something that's unique to the account in the external provider). -------------------------------------------------------------------------------- /two-providers/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.example 7 | social-github 8 | 0.0.1-SNAPSHOT 9 | jar 10 | 11 | social-two-providers 12 | Demo project for Spring Boot 13 | 14 | 15 | org.springframework.boot 16 | spring-boot-starter-parent 17 | 2.2.2.RELEASE 18 | 19 | 20 | 21 | 22 | UTF-8 23 | 1.8 24 | 25 | 26 | 27 | 28 | org.springframework.boot 29 | spring-boot-starter-security 30 | 31 | 32 | org.springframework.boot 33 | spring-boot-starter-web 34 | 35 | 36 | org.springframework.security 37 | spring-security-oauth2-client 38 | 39 | 40 | org.springframework.security 41 | spring-security-oauth2-jose 42 | 43 | 44 | org.webjars 45 | js-cookie 46 | 2.1.0 47 | 48 | 49 | org.webjars 50 | jquery 51 | 2.1.1 52 | 53 | 54 | org.webjars 55 | bootstrap 56 | 3.2.0 57 | 58 | 59 | org.webjars 60 | webjars-locator-core 61 | 62 | 63 | 64 | org.springframework.boot 65 | spring-boot-starter-test 66 | test 67 | 68 | 69 | org.springframework.boot 70 | spring-boot-configuration-processor 71 | true 72 | 73 | 74 | 75 | 76 | 77 | 78 | org.springframework.boot 79 | spring-boot-maven-plugin 80 | 81 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /two-providers/src/main/java/com/example/SocialApplication.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2015 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example; 17 | 18 | import java.util.Collections; 19 | import java.util.Map; 20 | 21 | import org.springframework.boot.SpringApplication; 22 | import org.springframework.boot.autoconfigure.SpringBootApplication; 23 | import org.springframework.http.HttpStatus; 24 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 25 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 26 | import org.springframework.security.core.annotation.AuthenticationPrincipal; 27 | import org.springframework.security.oauth2.core.user.OAuth2User; 28 | import org.springframework.security.web.authentication.HttpStatusEntryPoint; 29 | import org.springframework.security.web.csrf.CookieCsrfTokenRepository; 30 | import org.springframework.web.bind.annotation.RequestMapping; 31 | import org.springframework.web.bind.annotation.RestController; 32 | 33 | @SpringBootApplication 34 | @RestController 35 | public class SocialApplication extends WebSecurityConfigurerAdapter { 36 | 37 | @RequestMapping("/user") 38 | public Map user(@AuthenticationPrincipal OAuth2User principal) { 39 | return Collections.singletonMap("name", principal.getAttribute("name")); 40 | } 41 | 42 | @Override 43 | protected void configure(HttpSecurity http) throws Exception { 44 | // @formatter:off 45 | http 46 | .authorizeRequests(a -> a 47 | .antMatchers("/", "/error", "/webjars/**").permitAll() 48 | .anyRequest().authenticated() 49 | ) 50 | .exceptionHandling(e -> e 51 | .authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)) 52 | ) 53 | .csrf(c -> c 54 | .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) 55 | ) 56 | .logout(l -> l 57 | .logoutSuccessUrl("/").permitAll() 58 | ) 59 | .oauth2Login(); 60 | // @formatter:on 61 | } 62 | 63 | public static void main(String[] args) { 64 | SpringApplication.run(SocialApplication.class, args); 65 | } 66 | 67 | } -------------------------------------------------------------------------------- /two-providers/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | security: 3 | oauth2: 4 | client: 5 | registration: 6 | github: 7 | client-id: your-github-client-id 8 | client-secret: your-github-client-secret 9 | google: 10 | client-id: your-google-client-id 11 | client-secret: your-google-client-secret 12 | 13 | #logging: 14 | # level: 15 | # org.springframework.security: DEBUG 16 | -------------------------------------------------------------------------------- /two-providers/src/main/resources/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Demo 7 | 8 | 9 | 10 | 12 | 13 | 15 | 16 | 17 |

Login

18 |
19 |
20 | With GitHub: click here 21 |
22 |
23 | With Google: click here 24 |
25 |
26 | 32 | 33 | 61 | 62 | -------------------------------------------------------------------------------- /two-providers/src/test/java/com/example/SocialApplicationTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2015 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example; 17 | 18 | import org.junit.Test; 19 | import org.junit.runner.RunWith; 20 | import org.springframework.boot.test.context.SpringBootTest; 21 | import org.springframework.boot.test.mock.mockito.MockBean; 22 | import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; 23 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 24 | 25 | @RunWith(SpringJUnit4ClassRunner.class) 26 | @SpringBootTest 27 | public class SocialApplicationTests { 28 | 29 | @MockBean 30 | ClientRegistrationRepository clientRegistrationRepository; 31 | 32 | @Test 33 | public void contextLoads() { 34 | } 35 | 36 | } 37 | --------------------------------------------------------------------------------