authcodegrant samples->oauth2webclient samples

The authcodegrant samples were initially meant to be very simple
demonstration of authorization code flow. However, it has become
obvious since then that the real intent of the demo is how to use
the WebClient with OAuth (there is no other reason to do authorization
code flow unless you use the token to make a request).

The samples have been migrated to oauth2webclient and oauth2webclient-webflux
respectively. They have been improved:

* The sample demonstrates usage with annotations, webclient directly, form login
  oauth2Login, and public APIs
* The samples externalize the endpoint that is requested in the sample
  making it easier to try other endpoints
* The UI no longer relies on a data structure for the result of the
  endpoint also making it easier to try other endpoints

Issue: gh-4921
This commit is contained in:
Rob Winch 2018-09-06 15:10:31 -05:00
parent 438d2911fb
commit 2495025845
30 changed files with 582 additions and 421 deletions

View File

@ -1,57 +0,0 @@
/*
* Copyright 2002-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.HttpHeaders;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizationCodeGrantFilter;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.reactive.server.WebTestClient;
/**
* Integration tests for the OAuth 2.0 client filters {@link OAuth2AuthorizationRequestRedirectFilter}
* and {@link OAuth2AuthorizationCodeGrantFilter}. These filters work together to realize
* the OAuth 2.0 Authorization Code Grant flow.
*
* @author Joe Grandja
* @since 5.1
*/
@SpringBootTest
@ActiveProfiles("test")
@AutoConfigureWebTestClient
@RunWith(SpringRunner.class)
public class OAuth2AuthorizationCodeGrantApplicationTests {
@Autowired
private WebTestClient rest;
@Test
@WithMockUser
public void requestWhenClientNotAuthorizedThenRedirectForAuthorization() throws Exception {
this.rest.get()
.uri("http://localhost/repos")
.exchange()
.expectStatus().is3xxRedirection()
.expectHeader().valueMatches(HttpHeaders.LOCATION, "https://github.com/login/oauth/authorize\\?response_type=code&client_id=client-id&scope=public_repo&state=.{15,}&redirect_uri=http%3A%2F%2Flocalhost%2Fauthorize%2Foauth2%2Fcode%2Fgithub");
}
}

View File

@ -1,23 +0,0 @@
server:
port: 8080
logging:
level:
root: INFO
org.springframework.web: INFO
org.springframework.security: INFO
# org.springframework.boot.autoconfigure: DEBUG
spring:
thymeleaf:
cache: false
security:
oauth2:
client:
registration:
github:
client-id: your-app-client-id
client-secret: your-app-client-secret
scope: public_repo
redirect-uri-template: "{baseUrl}/authorize/oauth2/code/github"
client-name: GitHub Repositories

View File

@ -1,26 +0,0 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
<title>Spring Security - OAuth 2.0 Authorization Code Grant</title>
<meta charset="utf-8" />
</head>
<body>
<div style="float: right" th:fragment="logout">
<div style="float:left">
<span style="font-weight:bold">User: </span><span th:text="${username}"></span>
</div>
<div style="float:none">&nbsp;</div>
<div style="float:right">
<a href="/logout">Log Out</a>
</div>
</div>
<h1>GitHub Repositories</h1>
<div>
<ul>
<li th:each="repo : ${repos}">
<span style="font-weight:bold" th:text="${repo.name}"></span>: <span th:text="${repo.url}"></span>
</li>
</ul>
</div>
</body>
</html>

View File

@ -1,183 +0,0 @@
/*
* Copyright 2002-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.samples;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.mock.web.MockHttpSession;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.oauth2.client.InMemoryOAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.AuthenticatedPrincipalOAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository;
import org.springframework.security.oauth2.client.web.HttpSessionOAuth2AuthorizationRequestRepository;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizationCodeGrantFilter;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import sample.config.WebClientConfig;
import java.util.HashMap;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* Integration tests for the OAuth 2.0 client filters {@link OAuth2AuthorizationRequestRedirectFilter}
* and {@link OAuth2AuthorizationCodeGrantFilter}. These filters work together to realize
* the OAuth 2.0 Authorization Code Grant flow.
*
* @author Joe Grandja
* @since 5.1
*/
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class OAuth2AuthorizationCodeGrantApplicationTests {
@Autowired
private ClientRegistrationRepository clientRegistrationRepository;
@Autowired
private OAuth2AuthorizedClientRepository authorizedClientRepository;
@Autowired
private MockMvc mockMvc;
@Test
public void requestWhenClientNotAuthorizedThenRedirectForAuthorization() throws Exception {
MvcResult mvcResult = this.mockMvc.perform(get("/repos").with(user("user")))
.andExpect(status().is3xxRedirection())
.andReturn();
assertThat(mvcResult.getResponse().getRedirectedUrl()).matches("https://github.com/login/oauth/authorize\\?response_type=code&client_id=your-app-client-id&scope=public_repo&state=.{15,}&redirect_uri=http%3A%2F%2Flocalhost%2Fgithub-repos");
}
@Test
@DirtiesContext
public void requestWhenClientGrantedAuthorizationThenAuthorizedClientSaved() throws Exception {
// Setup the Authorization Request in the session
ClientRegistration registration = this.clientRegistrationRepository.findByRegistrationId("github");
Map<String, Object> additionalParameters = new HashMap<>();
additionalParameters.put(OAuth2ParameterNames.REGISTRATION_ID, registration.getRegistrationId());
OAuth2AuthorizationRequest authorizationRequest = OAuth2AuthorizationRequest.authorizationCode()
.authorizationUri(registration.getProviderDetails().getAuthorizationUri())
.clientId(registration.getClientId())
.redirectUri("http://localhost/github-repos")
.scopes(registration.getScopes())
.state("state")
.additionalParameters(additionalParameters)
.build();
AuthorizationRequestRepository<OAuth2AuthorizationRequest> authorizationRequestRepository =
new HttpSessionOAuth2AuthorizationRequestRepository();
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
authorizationRequestRepository.saveAuthorizationRequest(authorizationRequest, request, response);
MockHttpSession session = (MockHttpSession) request.getSession();
String principalName = "user";
TestingAuthenticationToken authentication = new TestingAuthenticationToken(principalName, "password");
// Authorization Response
this.mockMvc.perform(get("/github-repos")
.param(OAuth2ParameterNames.CODE, "code")
.param(OAuth2ParameterNames.STATE, "state")
.with(authentication(authentication))
.session(session))
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("http://localhost/github-repos"));
OAuth2AuthorizedClient authorizedClient = this.authorizedClientRepository.loadAuthorizedClient(
registration.getRegistrationId(), authentication, request);
assertThat(authorizedClient).isNotNull();
}
@EnableWebSecurity
static class OAuth2ClientConfig extends WebSecurityConfigurerAdapter {
// @formatter:off
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.oauth2Client()
.authorizationCodeGrant()
.accessTokenResponseClient(this.accessTokenResponseClient());
}
// @formatter:on
private OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient() {
OAuth2AccessTokenResponse accessTokenResponse = OAuth2AccessTokenResponse.withToken("access-token-1234")
.tokenType(OAuth2AccessToken.TokenType.BEARER)
.expiresIn(60 * 1000)
.build();
OAuth2AccessTokenResponseClient tokenResponseClient = mock(OAuth2AccessTokenResponseClient.class);
when(tokenResponseClient.getTokenResponse(any())).thenReturn(accessTokenResponse);
return tokenResponseClient;
}
}
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(basePackages = "sample.web")
@Import(WebClientConfig.class)
public static class SpringBootApplicationTestConfig {
@Bean
public OAuth2AuthorizedClientService authorizedClientService(ClientRegistrationRepository clientRegistrationRepository) {
return new InMemoryOAuth2AuthorizedClientService(clientRegistrationRepository);
}
@Bean
public OAuth2AuthorizedClientRepository authorizedClientRepository(OAuth2AuthorizedClientService authorizedClientService) {
return new AuthenticatedPrincipalOAuth2AuthorizedClientRepository(authorizedClientService);
}
}
}

View File

@ -1,23 +0,0 @@
server:
port: 8080
logging:
level:
root: INFO
org.springframework.web: INFO
org.springframework.security: INFO
# org.springframework.boot.autoconfigure: DEBUG
spring:
thymeleaf:
cache: false
security:
oauth2:
client:
registration:
github:
client-id: your-app-client-id
client-secret: your-app-client-secret
scope: public_repo
redirect-uri-template: "{baseUrl}/github-repos"
client-name: GitHub Repositories

View File

@ -1,28 +0,0 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">
<head>
<title>Spring Security - OAuth 2.0 Authorization Code Grant</title>
<meta charset="utf-8" />
</head>
<body>
<div style="float: right" th:fragment="logout" sec:authorize="isAuthenticated()">
<div style="float:left">
<span style="font-weight:bold">User: </span><span sec:authentication="name"></span>
</div>
<div style="float:none">&nbsp;</div>
<div style="float:right">
<form action="#" th:action="@{/logout}" method="post">
<input type="submit" value="Logout" />
</form>
</div>
</div>
<h1>GitHub Repositories</h1>
<div>
<ul>
<li th:each="repo : ${repos}">
<span style="font-weight:bold" th:text="${repo.name}"></span>: <span th:text="${repo.url}"></span>
</li>
</ul>
</div>
</body>
</html>

View File

@ -1,8 +1,8 @@
= OAuth 2.0 Authorization Code Grant Sample
= OAuth 2.0 WebClient (WebFlux) Sample
== GitHub Repositories
This guide provides instructions on setting up the sample application, which leverages the OAuth 2.0 Authorization Code Grant, and displays a list of public GitHub repositories that are accessible to the authenticated user.
This guide provides instructions on setting up the sample application, which leverages WebClient OAuth2 integration to display a list of public GitHub repositories that are accessible to the authenticated user.
This includes repositories owned by the authenticated user, repositories where the authenticated user is a collaborator, and repositories that the authenticated user has access to through an organization membership.
@ -17,7 +17,7 @@ The following sections provide detailed steps for setting up the sample and cove
To use GitHub's OAuth 2.0 authorization system, you must https://github.com/settings/applications/new[Register a new OAuth application].
When registering the OAuth application, ensure the *Authorization callback URL* is set to `http://localhost:8080/github-repos`.
When registering the OAuth application, ensure the *Authorization callback URL* is set to `http://localhost:8080/login/oauth2/code/client-id`.
The Authorization callback URL (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 OAuth application on the _Authorize application_ page.
@ -36,12 +36,11 @@ spring:
oauth2:
client:
registration: <1>
github: <2>
client-id: github-client-id
client-secret: github-client-secret
scope: public_repo
redirect-uri-template: "{baseUrl}/github-repos"
client-name: GitHub Repositories
client-id: <2>
client-id: replace-with-client-id
client-secret: replace-with-client-secret
provider: github
scopes: read:user,public_repo
----
+
.OAuth Client properties
@ -57,7 +56,7 @@ spring:
Launch the Spring Boot 2.0 sample and go to `http://localhost:8080`.
You are then redirected to the default _auto-generated_ form login page.
Log in using *'user'* (username) and *'password'* (password) and then you'll be redirected to GitHub for authentication.
Log in using *'user'* (username) and *'password'* (password) or click the link to authenticate with GitHub and then you'll be redirected to GitHub for authentication.
After authenticating with your GitHub credentials, the next page presented to you is "Authorize application".
This page will ask you to *Authorize* the application you created in the previous step.

View File

@ -3,6 +3,7 @@ apply plugin: 'io.spring.convention.spring-sample-boot'
dependencies {
compile project(':spring-security-config')
compile project(':spring-security-oauth2-client')
compile project(':spring-security-oauth2-jose')
compile 'org.springframework.boot:spring-boot-starter-thymeleaf'
compile 'org.springframework.boot:spring-boot-starter-webflux'
compile 'org.thymeleaf.extras:thymeleaf-extras-springsecurity4'

View File

@ -22,9 +22,9 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
* @author Joe Grandja
*/
@SpringBootApplication
public class OAuth2AuthorizationCodeGrantApplication {
public class OAuth2WebClientWebFluxApplication {
public static void main(String[] args) {
SpringApplication.run(OAuth2AuthorizationCodeGrantApplication.class, args);
SpringApplication.run(OAuth2WebClientWebFluxApplication.class, args);
}
}

View File

@ -36,8 +36,11 @@ public class SecurityConfig {
SecurityWebFilterChain configure(ServerHttpSecurity http) throws Exception {
http
.authorizeExchange()
.pathMatchers("/", "/public/**").permitAll()
.anyExchange().authenticated()
.and()
.oauth2Login()
.and()
.formLogin()
.and()
.oauth2Client();

View File

@ -33,8 +33,11 @@ public class WebClientConfig {
@Bean
WebClient webClient(ReactiveClientRegistrationRepository clientRegistrationRepository,
ServerOAuth2AuthorizedClientRepository authorizedClientRepository) {
ServerOAuth2AuthorizedClientExchangeFilterFunction oauth =
new ServerOAuth2AuthorizedClientExchangeFilterFunction(clientRegistrationRepository, authorizedClientRepository);
oauth.setDefaultOAuth2AuthorizedClient(true);
return WebClient.builder()
.filter(new ServerOAuth2AuthorizedClientExchangeFilterFunction(clientRegistrationRepository, authorizedClientRepository))
.filter(oauth)
.build();
}
}

View File

@ -0,0 +1,31 @@
/*
* Copyright 2002-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample.web;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
/**
* @author Rob Winch
*/
@Controller
public class IndexController {
@GetMapping("/")
String index() {
return "index";
}
}

View File

@ -15,48 +15,52 @@
*/
package sample.web;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import java.security.Principal;
import java.util.List;
import static org.springframework.security.oauth2.client.web.reactive.function.client.ServerOAuth2AuthorizedClientExchangeFilterFunction.oauth2AuthorizedClient;
import static org.springframework.security.oauth2.client.web.reactive.function.client.ServerOAuth2AuthorizedClientExchangeFilterFunction.clientRegistrationId;
/**
* @author Joe Grandja
* @author Rob Winch
*/
@Controller
public class GitHubReposController {
@RequestMapping(path = {"/webclient", "/public/webclient"})
public class OAuth2WebClientController {
private final WebClient webClient;
public GitHubReposController(WebClient webClient) {
private final String uri;
public OAuth2WebClientController(WebClient webClient, @Value("${resource-uri}") String uri) {
this.webClient = webClient;
this.uri = uri;
}
@GetMapping("/")
public String index() {
return "redirect:/repos";
@GetMapping("/explicit")
String explicit(Model model) {
Mono<String> body = this.webClient
.get()
.uri(this.uri)
.attributes(clientRegistrationId("client-id"))
.retrieve()
.bodyToMono(String.class);
model.addAttribute("body", body);
return "response";
}
@GetMapping("/repos")
public String gitHubRepos(Model model, @RegisteredOAuth2AuthorizedClient("github") OAuth2AuthorizedClient authorizedClient, Principal principal) {
String endpointUri = "https://api.github.com/user/repos";
Mono<List> repos = this.webClient
.get()
.uri(endpointUri)
.attributes(oauth2AuthorizedClient(authorizedClient))
.retrieve()
.bodyToMono(List.class);
model.addAttribute("repos", repos);
model.addAttribute("username", principal.getName());
return "github-repos";
@GetMapping("/implicit")
String implicit(Model model) {
Mono<String> body = this.webClient
.get()
.uri(this.uri)
.retrieve()
.bodyToMono(String.class);
model.addAttribute("body", body);
return "response";
}
}

View File

@ -0,0 +1,70 @@
/*
* Copyright 2002-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample.web;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import static org.springframework.security.oauth2.client.web.reactive.function.client.ServerOAuth2AuthorizedClientExchangeFilterFunction.oauth2AuthorizedClient;
/**
* @author Joe Grandja
* @author Rob Winch
*/
@Controller
@RequestMapping(path = {"/annotation", "/public/annotation"})
public class RegisteredOAuth2AuthorizedClientController {
private final WebClient webClient;
private final String uri;
public RegisteredOAuth2AuthorizedClientController(WebClient webClient, @Value("${resource-uri}") String uri) {
this.webClient = webClient;
this.uri = uri;
}
@GetMapping("/explicit")
String explicit(Model model, @RegisteredOAuth2AuthorizedClient("client-id") OAuth2AuthorizedClient authorizedClient) {
Mono<String> body = this.webClient
.get()
.uri(this.uri)
.attributes(oauth2AuthorizedClient(authorizedClient))
.retrieve()
.bodyToMono(String.class);
model.addAttribute("body", body);
return "response";
}
@GetMapping("/implicit")
String implicit(Model model, @RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient authorizedClient) {
Mono<String> body = this.webClient
.get()
.uri(this.uri)
.attributes(oauth2AuthorizedClient(authorizedClient))
.retrieve()
.bodyToMono(String.class);
model.addAttribute("body", body);
return "response";
}
}

View File

@ -12,9 +12,10 @@ spring:
oauth2:
client:
registration:
github:
client-id: client-id
client-secret: client-secret
scope: public_repo
redirect-uri-template: "{baseUrl}/authorize/oauth2/code/github"
client-name: GitHub Repositories
client-id:
client-id: replace-with-client-id
client-secret: replace-with-client-secret
provider: github
scopes: read:user,public_repo
resource-uri: https://api.github.com/user/repos

View File

@ -0,0 +1,51 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">
<head>
<title>OAuth2 WebClient Showcase</title>
<meta charset="utf-8" />
</head>
<body>
<a th:href="@{/logout}">Log Out</a>
<h1>Examples</h1>
<h2>@RegisteredOAuth2AuthorizedClient</h2>
<p>
Examples on RegisteredOAuth2AuthorizedClientController
<h3>Authenticated</h3>
<ul>
<li><a th:href="@{/annotation/explicit}">Explicit</a> - Explicitly provide a Client Registration Id</li>
<li>
<a th:href="@{/annotation/implicit}">Implicit</a> - Use the currently logged in user's OAuth Token. This will
only work if the user authenticates with oauth2Login and the token provided is the correct token provided at
log in is authorized.</li>
</ul>
<h3>Public</h3>
<ul>
<li><a th:href="@{/public/annotation/explicit}">Explicit</a> - Explicitly provide a Client Registration Id</li>
<li>
<a th:href="@{/public/annotation/implicit}">Implicit</a> - This will fail if the user is not authenticated.
Since it is mapped to permitAll, it is going to fail unless the user already took an action to log in and then
authenticates with oauth2Login()</li>
</ul>
<h2>ServerOAuth2AuthorizedClientExchangeFilterFunction</h2>
<p>
Examples on OAuth2WebClientController that demonstrate how to use ServerOAuth2AuthorizedClientExchangeFilterFunction
<h3>Authenticated</h3>
<ul>
<li><a th:href="@{/webclient/explicit}">Explicit</a> - Explicitly provide a Client Registration Id</li>
<li>
<a th:href="@{/webclient/implicit}">Implicit</a> - Use the currently logged in user's OAuth Token. This will
only work if the user authenticates with oauth2Login and the token provided is the correct token provided at
log in is authorized.</li>
</ul>
<h3>Public</h3>
<ul>
<li><a th:href="@{/public/webclient/explicit}">Explicit</a> - Explicitly provide a Client Registration Id</li>
<li>
<a th:href="@{/public/webclient/implicit}">Implicit</a> - This will fail if the user is not authenticated.
Since it is mapped to permitAll, it is going to fail unless the user already took an action to log in and then
authenticates with oauth2Login()</li>
</ul>
</body>
</html>

View File

@ -0,0 +1,31 @@
<!--
~ Copyright 2002-2018 the original author or authors.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
<title>OAuth2 WebClient Showcase</title>
<meta charset="utf-8" />
</head>
<body>
<a th:href="@{/}">Back</a>
<h1>Response</h1>
<pre><code id="json" class="json" th:text="${body}"></code></pre>
<script>
json.innerHTML = JSON.stringify(JSON.parse(json.innerHTML), null, 4);
</script>
</body>
</html>

View File

@ -0,0 +1,43 @@
/*
* Copyright 2002-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.reactive.server.WebTestClient;
/**
* @author Rob Winch
*/
@SpringBootTest
@AutoConfigureWebTestClient
@RunWith(SpringRunner.class)
public class OAuth2WebClientWebFluxApplicationTests {
@Autowired
private WebTestClient client;
@Test
public void annotationExplicitWhenNotAuthenticatedThenLoginRequested() {
this.client.get().uri("/annotation/explicit")
.exchange()
.expectStatus().is3xxRedirection();
}
}

View File

@ -1,8 +1,8 @@
= OAuth 2.0 Authorization Code Grant Sample
= OAuth 2.0 WebClient (Servlet) Sample
== GitHub Repositories
This guide provides instructions on setting up the sample application, which leverages the OAuth 2.0 Authorization Code Grant, and displays a list of public GitHub repositories that are accessible to the authenticated user.
This guide provides instructions on setting up the sample application, which leverages WebClient OAuth2 integration to display a list of public GitHub repositories that are accessible to the authenticated user.
This includes repositories owned by the authenticated user, repositories where the authenticated user is a collaborator, and repositories that the authenticated user has access to through an organization membership.
@ -17,7 +17,7 @@ The following sections provide detailed steps for setting up the sample and cove
To use GitHub's OAuth 2.0 authorization system, you must https://github.com/settings/applications/new[Register a new OAuth application].
When registering the OAuth application, ensure the *Authorization callback URL* is set to `http://localhost:8080/github-repos`.
When registering the OAuth application, ensure the *Authorization callback URL* is set to `http://localhost:8080/login/oauth2/code/client-id`.
The Authorization callback URL (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 OAuth application on the _Authorize application_ page.
@ -36,12 +36,11 @@ spring:
oauth2:
client:
registration: <1>
github: <2>
client-id: github-client-id
client-secret: github-client-secret
scope: public_repo
redirect-uri-template: "{baseUrl}/github-repos"
client-name: GitHub Repositories
client-id: <2>
client-id: replace-with-client-id
client-secret: replace-with-client-secret
provider: github
scopes: read:user,public_repo
----
+
.OAuth Client properties
@ -57,7 +56,7 @@ spring:
Launch the Spring Boot 2.0 sample and go to `http://localhost:8080`.
You are then redirected to the default _auto-generated_ form login page.
Log in using *'user'* (username) and *'password'* (password) and then you'll be redirected to GitHub for authentication.
Log in using *'user'* (username) and *'password'* (password) or click the link to authenticate with GitHub and then you'll be redirected to GitHub for authentication.
After authenticating with your GitHub credentials, the next page presented to you is "Authorize application".
This page will ask you to *Authorize* the application you created in the previous step.

View File

@ -5,6 +5,7 @@ ext['thymeleaf.version'] = '3.0.9.RELEASE'
dependencies {
compile project(':spring-security-config')
compile project(':spring-security-oauth2-client')
compile project(':spring-security-oauth2-jose')
compile 'org.springframework:spring-webflux'
compile 'org.springframework.boot:spring-boot-starter-thymeleaf'
compile 'org.springframework.boot:spring-boot-starter-web'

View File

@ -19,12 +19,12 @@ import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @author Rob Winch
* @author Joe Grandja
*/
@SpringBootApplication
public class OAuth2AuthorizationCodeGrantApplication {
public class OAuth2WebClientApplication {
public static void main(String[] args) {
SpringApplication.run(OAuth2AuthorizationCodeGrantApplication.class, args);
SpringApplication.run(OAuth2WebClientApplication.class, args);
}
}

View File

@ -37,10 +37,13 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.mvcMatchers("/", "/public/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.oauth2Login()
.and()
.oauth2Client();
}

View File

@ -18,6 +18,8 @@ package sample.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction;
import org.springframework.web.reactive.function.client.WebClient;
@ -29,9 +31,11 @@ import org.springframework.web.reactive.function.client.WebClient;
public class WebClientConfig {
@Bean
WebClient webClient() {
WebClient webClient(ClientRegistrationRepository clientRegistrationRepository, OAuth2AuthorizedClientRepository authorizedClientRepository) {
ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2 = new ServletOAuth2AuthorizedClientExchangeFilterFunction(clientRegistrationRepository, authorizedClientRepository);
oauth2.setDefaultOAuth2AuthorizedClient(true);
return WebClient.builder()
.filter(new ServletOAuth2AuthorizedClientExchangeFilterFunction())
.apply(oauth2.oauth2Configuration())
.build();
}
}

View File

@ -0,0 +1,31 @@
/*
* Copyright 2002-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample.web;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
/**
* @author Rob Winch
*/
@Controller
public class IndexController {
@GetMapping("/")
String index() {
return "index";
}
}

View File

@ -15,46 +15,53 @@
*/
package sample.web;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.reactive.function.client.WebClient;
import java.util.List;
import static org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction.oauth2AuthorizedClient;
import static org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction.clientRegistrationId;
/**
* @author Joe Grandja
* @author Rob Winch
*/
@Controller
public class GitHubReposController {
@RequestMapping(path = {"/webclient", "/public/webclient"})
public class OAuth2WebClientController {
private final WebClient webClient;
public GitHubReposController(WebClient webClient) {
private final String uri;
public OAuth2WebClientController(WebClient webClient, @Value("${resource-uri}") String uri) {
this.webClient = webClient;
this.uri = uri;
}
@GetMapping("/")
public String index() {
return "redirect:/repos";
@GetMapping("/explicit")
String explicit(Model model) {
String body = this.webClient
.get()
.uri(this.uri)
.attributes(clientRegistrationId("client-id"))
.retrieve()
.bodyToMono(String.class)
.block();
model.addAttribute("body", body);
return "response";
}
@GetMapping("/repos")
public String gitHubRepos(Model model, @RegisteredOAuth2AuthorizedClient("github") OAuth2AuthorizedClient authorizedClient) {
String endpointUri = "https://api.github.com/user/repos";
List repos = this.webClient
.get()
.uri(endpointUri)
.attributes(oauth2AuthorizedClient(authorizedClient))
.retrieve()
.bodyToMono(List.class)
.block();
model.addAttribute("repos", repos);
return "github-repos";
@GetMapping("/implicit")
String implicit(Model model) {
String body = this.webClient
.get()
.uri(this.uri)
.retrieve()
.bodyToMono(String.class)
.block();
model.addAttribute("body", body);
return "response";
}
}

View File

@ -0,0 +1,70 @@
/*
* Copyright 2002-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample.web;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.reactive.function.client.WebClient;
import static org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction.oauth2AuthorizedClient;
/**
* @author Joe Grandja
* @author Rob Winch
*/
@Controller
@RequestMapping(path = {"/annotation", "/public/annotation"})
public class RegisteredOAuth2AuthorizedClientController {
private final WebClient webClient;
private final String uri;
public RegisteredOAuth2AuthorizedClientController(WebClient webClient, @Value("${resource-uri}") String uri) {
this.webClient = webClient;
this.uri = uri;
}
@GetMapping("/explicit")
String explicit(Model model, @RegisteredOAuth2AuthorizedClient("client-id") OAuth2AuthorizedClient authorizedClient) {
String body = this.webClient
.get()
.uri(this.uri)
.attributes(oauth2AuthorizedClient(authorizedClient))
.retrieve()
.bodyToMono(String.class)
.block();
model.addAttribute("body", body);
return "response";
}
@GetMapping("/implicit")
String implicit(Model model, @RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient authorizedClient) {
String body = this.webClient
.get()
.uri(this.uri)
.attributes(oauth2AuthorizedClient(authorizedClient))
.retrieve()
.bodyToMono(String.class)
.block();
model.addAttribute("body", body);
return "response";
}
}

View File

@ -0,0 +1,21 @@
logging:
level:
root: INFO
org.springframework.web: INFO
org.springframework.security: INFO
# org.springframework.boot.autoconfigure: DEBUG
spring:
thymeleaf:
cache: false
security:
oauth2:
client:
registration:
client-id:
client-id: replace-with-client-id
client-secret: replace-with-client-secret
provider: github
scopes: read:user,public_repo
resource-uri: https://api.github.com/user/repos

View File

@ -0,0 +1,51 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">
<head>
<title>OAuth2 WebClient Showcase</title>
<meta charset="utf-8" />
</head>
<body>
<a th:href="@{/logout}">Log Out</a>
<h1>Examples</h1>
<h2>@RegisteredOAuth2AuthorizedClient</h2>
<p>
Examples on RegisteredOAuth2AuthorizedClientController
<h3>Authenticated</h3>
<ul>
<li><a th:href="@{/annotation/explicit}">Explicit</a> - Explicitly provide a Client Registration Id</li>
<li>
<a th:href="@{/annotation/implicit}">Implicit</a> - Use the currently logged in user's OAuth Token. This will
only work if the user authenticates with oauth2Login and the token provided is the correct token provided at
log in is authorized.</li>
</ul>
<h3>Public</h3>
<ul>
<li><a th:href="@{/public/annotation/explicit}">Explicit</a> - Explicitly provide a Client Registration Id</li>
<li>
<a th:href="@{/public/annotation/implicit}">Implicit</a> - This will fail if the user is not authenticated.
Since it is mapped to permitAll, it is going to fail unless the user already took an action to log in and then
authenticates with oauth2Login()</li>
</ul>
<h2>ServletOAuth2AuthorizedClientExchangeFilterFunction</h2>
<p>
Examples on OAuth2WebClientController that demonstrate how to use ServletOAuth2AuthorizedClientExchangeFilterFunction
<h3>Authenticated</h3>
<ul>
<li><a th:href="@{/webclient/explicit}">Explicit</a> - Explicitly provide a Client Registration Id</li>
<li>
<a th:href="@{/webclient/implicit}">Implicit</a> - Use the currently logged in user's OAuth Token. This will
only work if the user authenticates with oauth2Login and the token provided is the correct token provided at
log in is authorized.</li>
</ul>
<h3>Public</h3>
<ul>
<li><a th:href="@{/public/webclient/explicit}">Explicit</a> - Explicitly provide a Client Registration Id</li>
<li>
<a th:href="@{/public/webclient/implicit}">Implicit</a> - This will fail if the user is not authenticated.
Since it is mapped to permitAll, it is going to fail unless the user already took an action to log in and then
authenticates with oauth2Login()</li>
</ul>
</body>
</html>

View File

@ -0,0 +1,31 @@
<!--
~ Copyright 2002-2018 the original author or authors.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
<title>OAuth2 WebClient Showcase</title>
<meta charset="utf-8" />
</head>
<body>
<a th:href="@{/}">Back</a>
<h1>Response</h1>
<pre><code id="json" class="json" th:text="${body}"></code></pre>
<script>
json.innerHTML = JSON.stringify(JSON.parse(json.innerHTML), null, 4);
</script>
</body>
</html>

View File

@ -0,0 +1,46 @@
/*
* Copyright 2002-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* @author Rob Winch
*/
@SpringBootTest
@AutoConfigureMockMvc
@RunWith(SpringRunner.class)
public class OAuth2WebClientApplicationTests {
@Autowired
private MockMvc mockMvc;
@Test
public void annotationExplicitWhenNotAuthenticatedThenLoginRequested() throws Exception {
this.mockMvc.perform(get("/annotation/explicit"))
.andExpect(status().is3xxRedirection());
}
}