Add sample for OAuth2 RestClient interceptor

Closes gh-294
This commit is contained in:
Steve Riesenberg
2024-07-30 11:35:22 -05:00
parent 277055548f
commit cc0e6f0d33
38 changed files with 1891 additions and 1 deletions

View File

@@ -0,0 +1,65 @@
/*
* Copyright 2024 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
*
* https://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 example;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import okhttp3.mockwebserver.Dispatcher;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.RecordedRequest;
import org.springframework.core.io.ClassPathResource;
import org.springframework.lang.NonNull;
import org.springframework.util.StringUtils;
import org.springframework.web.util.UriComponentsBuilder;
/**
* Dispatcher for {@link okhttp3.mockwebserver.MockWebServer} that returns resources on
* the classpath.
*
* @author Steve Riesenberg
*/
final class ClassPathDispatcher extends Dispatcher {
@NonNull
@Override
public MockResponse dispatch(RecordedRequest recordedRequest) {
if (recordedRequest.getPath() != null && recordedRequest.getRequestUrl() != null) {
try {
String requestUrl = recordedRequest.getRequestUrl().toString();
String baseUrl = UriComponentsBuilder.fromUriString(requestUrl).replacePath("").toUriString();
String responseBody = readResource(recordedRequest.getPath()).replace("{baseUrl}", baseUrl);
return new MockResponse().setResponseCode(200)
.setHeader("Content-Type", "application/json")
.setBody(responseBody);
}
catch (IOException ignored) {
}
}
return new MockResponse().setResponseCode(404);
}
private static String readResource(String path) throws IOException {
if (path.startsWith("/")) {
path = StringUtils.trimLeadingCharacter(path, '/');
}
return new ClassPathResource("responses/" + path + ".json").getContentAsString(StandardCharsets.UTF_8);
}
}

View File

@@ -0,0 +1,181 @@
/*
* Copyright 2024 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
*
* https://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 example;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.RecordedRequest;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
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.http.HttpHeaders;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.util.StringUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.endsWith;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.oauth2Client;
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.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* Integration tests for {@link OAuth2RestClientApplication}.
*
* @author Steve Riesenberg
*/
@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("test")
public class OAuth2RestClientApplicationITests {
private static final String TOKEN_VALUE = "123abc";
private static MockWebServer mockWebServer;
@Autowired
private MockMvc mockMvc;
private OAuth2AccessToken accessToken;
@BeforeAll
static void initialize() throws Exception {
mockWebServer = new MockWebServer();
mockWebServer.setDispatcher(new ClassPathDispatcher());
mockWebServer.start();
}
@BeforeEach
void setUp() {
this.accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, TOKEN_VALUE, null, null,
Set.of("message:read", "message:write"));
}
@AfterAll
static void destroy() throws Exception {
mockWebServer.shutdown();
}
@DynamicPropertySource
static void mockwebserver(DynamicPropertyRegistry registry) {
String issuer = StringUtils.trimTrailingCharacter(mockWebServer.url("/").toString(), '/');
registry.add("mockwebserver.url", () -> issuer);
}
@Test
void messagesWhenAnonymousThenRedirectsToLogin() throws Exception {
// @formatter:off
this.mockMvc.perform(get("/messages"))
.andExpect(status().is3xxRedirection())
.andExpect(header().string(HttpHeaders.LOCATION, endsWith("/oauth2/authorization/login-client")));
// @formatter:on
}
@Test
void messagesWhenAuthenticatedAndUnauthorizedThenRedirectsToAuthorizationEndpoint() throws Exception {
// @formatter:off
this.mockMvc.perform(get("/messages").with(user("user")))
.andExpect(status().is3xxRedirection())
.andExpect(header().string(HttpHeaders.LOCATION, containsString("/oauth2/authorize")));
// @formatter:on
}
@Test
void publicMessagesWhenAnonymousAndUnauthorizedThenRedirectsToAuthorizationEndpoint() throws Exception {
// @formatter:off
this.mockMvc.perform(get("/public/messages"))
.andExpect(status().is3xxRedirection())
.andExpect(header().string(HttpHeaders.LOCATION, containsString("/oauth2/authorize")));
// @formatter:on
}
@Test
void publicMessagesWhenAnonymousAndAuthorizedThenRequestContainsBearerToken() throws Exception {
String expectedResponse = """
<h1>Messages</h1>
<ol>
<li>Hello</li>
<li>Goodbye</li>
</ol>
""".replaceAll("\t", " ");
// @formatter:off
this.mockMvc.perform(get("/public/messages")
.with(oauth2Client("messaging-client").accessToken(this.accessToken)))
.andExpect(status().isOk())
.andExpect(content().string(containsString(expectedResponse)));
// @formatter:on
RecordedRequest recordedRequest = takeRequest("/messages");
assertThat(recordedRequest).isNotNull();
assertThat(recordedRequest.getHeaders().get(HttpHeaders.AUTHORIZATION))
.isEqualTo("Bearer %s".formatted(TOKEN_VALUE));
}
@Test
void messagesWhenAuthenticatedAndAuthorizedThenRequestContainsBearerToken() throws Exception {
String expectedResponse = """
<h1>Messages</h1>
<ol>
<li>Hello</li>
<li>Goodbye</li>
</ol>
""".replaceAll("\t", " ");
// @formatter:off
this.mockMvc.perform(get("/messages")
.with(user("user"))
.with(oauth2Client("messaging-client").accessToken(this.accessToken)))
.andExpect(status().isOk())
.andExpect(content().string(containsString(expectedResponse)));
// @formatter:on
RecordedRequest recordedRequest = takeRequest("/messages");
assertThat(recordedRequest).isNotNull();
assertThat(recordedRequest.getHeaders().get(HttpHeaders.AUTHORIZATION))
.isEqualTo("Bearer %s".formatted(TOKEN_VALUE));
}
/**
* Take a request, ignoring startup requests (e.g.
* "/.well-known/openid-configuration").
* @param path the request path
* @return the {@link RecordedRequest} from the server
*/
private RecordedRequest takeRequest(String path) throws InterruptedException {
RecordedRequest recordedRequest;
do {
recordedRequest = mockWebServer.takeRequest(1, TimeUnit.SECONDS);
}
while (recordedRequest != null && !path.equals(recordedRequest.getPath()));
return recordedRequest;
}
}

View File

@@ -0,0 +1,150 @@
/*
* Copyright 2024 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
*
* https://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 example;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextHolderStrategy;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.client.web.client.OAuth2ClientHttpRequestInterceptor.ClientRegistrationIdResolver;
import org.springframework.security.oauth2.client.web.client.RequestAttributeClientRegistrationIdResolver;
/**
* Configuration for demonstrating custom strategies for resolving a
* {@code clientRegistrationId} via the {@link ClientRegistrationIdResolver}. This sample
* uses the following profiles to demonstrate multiple configurations:
*
* <ol>
* <li>{@code default} - Demonstrates the default setup with
* {@link RequestAttributeClientRegistrationIdResolver}. Uses {@code login-client} as the
* {@code clientRegistrationId} to log in and {@code messaging-client} for
* authorization.</li>
* <li>{@code current-user} - Demonstrates a custom {@link ClientRegistrationIdResolver}
* that simply resolves the {@code clientRegistrationId} from the current user. Uses
* {@code login-client-with-messaging} to log in.</li>
* <li>{@code composite} - Demonstrates a composite {@link ClientRegistrationIdResolver}
* that tries multiple ways of resolving a {@code clientRegistrationId}. Uses
* {@code login-client-with-messaging} to log in.</li>
* <li>{@code authentication-required} - Demonstrates a custom
* {@link ClientRegistrationIdResolver} that requires authentication using OAuth 2.0 or
* Open ID Connect 1.0. Uses {@code login-client-with-messaging} to log in.</li>
* </ol>
*
* @author Steve Riesenberg
*/
@Configuration
public class ClientRegistrationIdResolverConfiguration {
/**
* This demonstrates a custom {@link ClientRegistrationIdResolver} that simply
* resolves the {@code clientRegistrationId} from the current user.
* @return a custom {@link ClientRegistrationIdResolver}
*/
private static ClientRegistrationIdResolver currentUserClientRegistrationIdResolver() {
SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder.getContextHolderStrategy();
return (request) -> {
Authentication authentication = securityContextHolderStrategy.getContext().getAuthentication();
return (authentication instanceof OAuth2AuthenticationToken principal)
? principal.getAuthorizedClientRegistrationId() : null;
};
}
/**
* This demonstrates a composite {@link ClientRegistrationIdResolver} that tries to
* resolve the {@code clientRegistrationId} in multiple ways.
* <p>
* <ol>
* <li>resolve the {@code clientRegistrationId} from attributes</li>
* <li>use the {@code clientRegistrationId} from OAuth 2.0 or OpenID Connect 1.0
* Login</li>
* <li>use the default {@code clientRegistrationId}</li>
* </ol>
* @param defaultClientRegistrationId the default {@code clientRegistrationId}
* @return a custom {@link ClientRegistrationIdResolver}
*/
private static ClientRegistrationIdResolver compositeClientRegistrationIdResolver(
String defaultClientRegistrationId) {
ClientRegistrationIdResolver requestAttributes = new RequestAttributeClientRegistrationIdResolver();
ClientRegistrationIdResolver currentUser = currentUserClientRegistrationIdResolver();
return (request) -> {
String clientRegistrationId = requestAttributes.resolve(request);
if (clientRegistrationId == null) {
clientRegistrationId = currentUser.resolve(request);
}
if (clientRegistrationId == null) {
clientRegistrationId = defaultClientRegistrationId;
}
return clientRegistrationId;
};
}
/**
* This demonstrates a custom {@link ClientRegistrationIdResolver} that requires
* authentication using OAuth 2.0 or Open ID Connect 1.0. If the user is not logged
* in, they are sent to the login page prior to obtaining an access token.
* @return a custom {@link ClientRegistrationIdResolver}
*/
private static ClientRegistrationIdResolver authenticationRequiredClientRegistrationIdResolver() {
ClientRegistrationIdResolver currentUser = currentUserClientRegistrationIdResolver();
return (request) -> {
String clientRegistrationId = currentUser.resolve(request);
if (clientRegistrationId == null) {
throw new AccessDeniedException(
"Authentication with OAuth 2.0 or OpenID Connect 1.0 Login is required");
}
return clientRegistrationId;
};
}
@Configuration
@Profile("current-user")
public static class CurrentUserConfiguration {
@Bean
public ClientRegistrationIdResolver clientRegistrationIdResolver() {
return currentUserClientRegistrationIdResolver();
}
}
@Configuration
@Profile("composite")
public static class CompositeConfiguration {
@Bean
public ClientRegistrationIdResolver clientRegistrationIdResolver() {
return compositeClientRegistrationIdResolver("messaging-client");
}
}
@Configuration
@Profile("authentication-required")
public static class AuthenticationRequiredConfiguration {
@Bean
public ClientRegistrationIdResolver clientRegistrationIdResolver() {
return authenticationRequiredClientRegistrationIdResolver();
}
}
}

View File

@@ -0,0 +1,35 @@
/*
* Copyright 2024 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
*
* https://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 example;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
/**
* Index controller.
*
* @author Steve Riesenberg
*/
@Controller
public class IndexController {
@GetMapping("/")
public String index() {
return "index";
}
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright 2024 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
*
* https://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 example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* OAuth2 Rest Client application.
*
* @author Steve Riesenberg
*/
@SpringBootApplication
public class OAuth2RestClientApplication {
public static void main(String[] args) {
SpringApplication.run(OAuth2RestClientApplication.class, args);
}
}

View File

@@ -0,0 +1,73 @@
/*
* Copyright 2024 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
*
* https://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 example;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.client.OAuth2AuthorizationFailureHandler;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.client.web.client.OAuth2ClientHttpRequestInterceptor;
import org.springframework.security.oauth2.client.web.client.RequestAttributeClientRegistrationIdResolver;
import org.springframework.web.client.RestClient;
/**
* Configuration for providing a {@link RestClient} bean.
*
* @author Steve Riesenberg
*/
@Configuration
public class RestClientConfiguration {
private final String baseUrl;
public RestClientConfiguration(@Value("${messages.base-url}") String baseUrl) {
this.baseUrl = baseUrl;
}
@Bean
public RestClient restClient(OAuth2AuthorizedClientManager authorizedClientManager,
OAuth2AuthorizedClientRepository authorizedClientRepository,
OAuth2ClientHttpRequestInterceptor.ClientRegistrationIdResolver clientRegistrationIdResolver,
RestClient.Builder builder) {
OAuth2ClientHttpRequestInterceptor requestInterceptor = new OAuth2ClientHttpRequestInterceptor(
authorizedClientManager, clientRegistrationIdResolver);
OAuth2AuthorizationFailureHandler authorizationFailureHandler = OAuth2ClientHttpRequestInterceptor
.authorizationFailureHandler(authorizedClientRepository);
requestInterceptor.setAuthorizationFailureHandler(authorizationFailureHandler);
return builder.baseUrl(this.baseUrl).requestInterceptor(requestInterceptor).build();
}
/**
* This sample uses profiles to demonstrate additional strategies for resolving the
* {@code clientRegistrationId}. See {@link ClientRegistrationIdResolverConfiguration}
* for alternate implementations.
* @return the default {@link ClientRegistrationIdResolverConfiguration}
* @see ClientRegistrationIdResolverConfiguration
*/
@Bean
@ConditionalOnMissingBean
public OAuth2ClientHttpRequestInterceptor.ClientRegistrationIdResolver clientRegistrationIdResolver() {
return new RequestAttributeClientRegistrationIdResolver();
}
}

View File

@@ -0,0 +1,62 @@
/*
* Copyright 2024 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
*
* https://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 example;
import org.springframework.security.oauth2.client.web.client.RequestAttributeClientRegistrationIdResolver;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.client.RestClient;
/**
* Rest client controller.
*
* @author Steve Riesenberg
*/
@Controller
public class RestClientController {
private static final String CLIENT_REGISTRATION_ID = "messaging-client";
private final RestClient restClient;
public RestClientController(RestClient restClient) {
this.restClient = restClient;
}
@GetMapping({ "/messages", "/public/messages" })
public String getMessages(Model model) {
// @formatter:off
Message[] messages = this.restClient.get()
.uri("/messages")
/*
* This demonstrates the default way of resolving the clientRegistrationId
* through attributes. See ClientRegistrationIdResolverConfiguration for
* trying another.
*/
.attributes(RequestAttributeClientRegistrationIdResolver.clientRegistrationId(CLIENT_REGISTRATION_ID))
.retrieve()
.body(Message[].class);
// @formatter:on
model.addAttribute("messages", messages);
return "messages";
}
public record Message(String message) {
}
}

View File

@@ -0,0 +1,65 @@
/*
* Copyright 2024 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
*
* https://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 example;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
/**
* Security configuration for {@link OAuth2RestClientApplication}.
*
* @author Steve Riesenberg
*/
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
private final String loginPage;
public SecurityConfiguration(@Value("${app.login-page}") String loginPage) {
this.loginPage = loginPage;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// @formatter:off
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/", "/public/**", "/error").permitAll()
.anyRequest().authenticated()
)
.oauth2Login((oauth2Login) -> oauth2Login
/*
* Customize the login page used in the redirect when the AuthenticationEntryPoint
* is triggered. This sample switches the URL based on the profile.
*
* See application.yml.
*/
.loginPage(this.loginPage)
)
.oauth2Client(Customizer.withDefaults());
// @formatter:on
return http.build();
}
}

View File

@@ -0,0 +1,10 @@
spring:
security:
oauth2:
client:
provider:
spring:
issuer-uri: ${mockwebserver.url}
messages:
base-url: ${mockwebserver.url}

View File

@@ -0,0 +1,64 @@
server:
port: 8080
logging:
level:
org.springframework.security: trace
spring:
security:
oauth2:
client:
registration:
login-client:
client-name: "Login Client"
client-id: "login-client"
client-secret: "openid-connect"
provider: "spring"
scope:
- "openid"
- "profile"
redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
client-authentication-method: "client_secret_basic"
authorization-grant-type: "authorization_code"
login-client-with-messaging:
client-name: "Login Client with Messaging"
client-id: "login-client-with-messaging"
client-secret: "with-messages"
provider: "spring"
scope:
- "openid"
- "profile"
- "message:read"
- "message:write"
redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
client-authentication-method: "client_secret_basic"
authorization-grant-type: "authorization_code"
messaging-client:
client-name: "Messaging Client"
client-id: "messaging-client"
client-secret: "secret"
provider: "spring"
scope:
- "message:read"
- "message:write"
redirect-uri: "{baseUrl}/authorized"
client-authentication-method: "client_secret_basic"
authorization-grant-type: "authorization_code"
provider:
spring:
issuer-uri: "http://localhost:9000"
messages:
base-url: "http://localhost:8090"
app:
login-page: /oauth2/authorization/login-client
---
spring:
config:
activate:
on-profile: current-user|composite|authentication-required
app:
login-page: /oauth2/authorization/login-client-with-messaging

View File

@@ -0,0 +1,33 @@
<!--
~ Copyright 2024 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
~
~ https://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">
<head>
<title>OAuth2 RestClient Showcase</title>
<meta charset="utf-8" />
</head>
<body>
<h1>Examples</h1>
<ul>
<li><a href="/messages">Authenticated</a></li>
<li><a href="/public/messages">Public</a></li>
</ul>
</body>
</html>

View File

@@ -0,0 +1,33 @@
<!--
~ Copyright 2024 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
~
~ https://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="https://www.thymeleaf.org">
<head>
<title>OAuth2 WebClient Showcase</title>
<meta charset="utf-8" />
</head>
<body>
<a th:href="@{/}">Back</a>
<h1>Messages</h1>
<ol>
<li th:each="message : ${messages}" th:text="${message.message}"></li>
</ol>
</body>
</html>

View File

@@ -0,0 +1,58 @@
{
"issuer": "{baseUrl}",
"authorization_endpoint": "{baseUrl}/oauth2/authorize",
"device_authorization_endpoint": "{baseUrl}/oauth2/device_authorization",
"token_endpoint": "{baseUrl}/oauth2/token",
"token_endpoint_auth_methods_supported": [
"client_secret_basic",
"client_secret_post",
"client_secret_jwt",
"private_key_jwt",
"tls_client_auth",
"self_signed_tls_client_auth"
],
"jwks_uri": "{baseUrl}/oauth2/jwks",
"userinfo_endpoint": "{baseUrl}/userinfo",
"end_session_endpoint": "{baseUrl}/connect/logout",
"response_types_supported": [
"code"
],
"grant_types_supported": [
"authorization_code",
"client_credentials",
"refresh_token",
"urn:ietf:params:oauth:grant-type:device_code",
"urn:ietf:params:oauth:grant-type:token-exchange"
],
"revocation_endpoint": "{baseUrl}/oauth2/revoke",
"revocation_endpoint_auth_methods_supported": [
"client_secret_basic",
"client_secret_post",
"client_secret_jwt",
"private_key_jwt",
"tls_client_auth",
"self_signed_tls_client_auth"
],
"introspection_endpoint": "{baseUrl}/oauth2/introspect",
"introspection_endpoint_auth_methods_supported": [
"client_secret_basic",
"client_secret_post",
"client_secret_jwt",
"private_key_jwt",
"tls_client_auth",
"self_signed_tls_client_auth"
],
"code_challenge_methods_supported": [
"S256"
],
"tls_client_certificate_bound_access_tokens": true,
"subject_types_supported": [
"public"
],
"id_token_signing_alg_values_supported": [
"RS256"
],
"scopes_supported": [
"openid"
]
}

View File

@@ -0,0 +1,8 @@
[
{
"message": "Hello"
},
{
"message": "Goodbye"
}
]