Rename messages-client to demo-client
Issue gh-1189
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2020-2021 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 sample;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
/**
|
||||
* @author Joe Grandja
|
||||
* @since 0.0.1
|
||||
*/
|
||||
@SpringBootApplication
|
||||
public class DemoClientApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(DemoClientApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright 2020-2023 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 sample.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
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.WebSecurityCustomizer;
|
||||
import org.springframework.security.oauth2.client.oidc.web.logout.OidcClientInitiatedLogoutSuccessHandler;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
|
||||
|
||||
import static org.springframework.security.config.Customizer.withDefaults;
|
||||
|
||||
/**
|
||||
* @author Joe Grandja
|
||||
* @author Dmitriy Dubson
|
||||
* @author Steve Riesenberg
|
||||
* @since 0.0.1
|
||||
*/
|
||||
@EnableWebSecurity
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
public class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
public WebSecurityCustomizer webSecurityCustomizer() {
|
||||
return (web) -> web.ignoring().requestMatchers("/webjars/**", "/assets/**");
|
||||
}
|
||||
|
||||
// @formatter:off
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http,
|
||||
ClientRegistrationRepository clientRegistrationRepository) throws Exception {
|
||||
http
|
||||
.authorizeHttpRequests(authorize ->
|
||||
authorize
|
||||
.requestMatchers("/logged-out").permitAll()
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.oauth2Login(oauth2Login ->
|
||||
oauth2Login.loginPage("/oauth2/authorization/messaging-client-oidc"))
|
||||
.oauth2Client(withDefaults())
|
||||
.logout(logout ->
|
||||
logout.logoutSuccessHandler(oidcLogoutSuccessHandler(clientRegistrationRepository)));
|
||||
return http.build();
|
||||
}
|
||||
// @formatter:on
|
||||
|
||||
private LogoutSuccessHandler oidcLogoutSuccessHandler(
|
||||
ClientRegistrationRepository clientRegistrationRepository) {
|
||||
OidcClientInitiatedLogoutSuccessHandler oidcLogoutSuccessHandler =
|
||||
new OidcClientInitiatedLogoutSuccessHandler(clientRegistrationRepository);
|
||||
|
||||
// Set the location that the End-User's User Agent will be redirected to
|
||||
// after the logout has been performed at the Provider
|
||||
oidcLogoutSuccessHandler.setPostLogoutRedirectUri("{baseUrl}/logged-out");
|
||||
|
||||
return oidcLogoutSuccessHandler;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright 2020-2023 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 sample.config;
|
||||
|
||||
import sample.web.authentication.DeviceCodeOAuth2AuthorizedClientProvider;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
|
||||
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProvider;
|
||||
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProviderBuilder;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager;
|
||||
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;
|
||||
|
||||
/**
|
||||
* @author Joe Grandja
|
||||
* @author Steve Riesenberg
|
||||
* @since 0.0.1
|
||||
*/
|
||||
@Configuration
|
||||
public class WebClientConfig {
|
||||
|
||||
@Bean
|
||||
public WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {
|
||||
ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client =
|
||||
new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
|
||||
// @formatter:off
|
||||
return WebClient.builder()
|
||||
.apply(oauth2Client.oauth2Configuration())
|
||||
.build();
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Bean
|
||||
public OAuth2AuthorizedClientManager authorizedClientManager(
|
||||
ClientRegistrationRepository clientRegistrationRepository,
|
||||
OAuth2AuthorizedClientRepository authorizedClientRepository) {
|
||||
|
||||
// @formatter:off
|
||||
OAuth2AuthorizedClientProvider authorizedClientProvider =
|
||||
OAuth2AuthorizedClientProviderBuilder.builder()
|
||||
.authorizationCode()
|
||||
.refreshToken()
|
||||
.clientCredentials()
|
||||
.provider(new DeviceCodeOAuth2AuthorizedClientProvider())
|
||||
.build();
|
||||
// @formatter:on
|
||||
|
||||
DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(
|
||||
clientRegistrationRepository, authorizedClientRepository);
|
||||
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
|
||||
|
||||
// Set a contextAttributesMapper to obtain device_code from the request
|
||||
authorizedClientManager.setContextAttributesMapper(DeviceCodeOAuth2AuthorizedClientProvider
|
||||
.deviceCodeContextAttributesMapper());
|
||||
|
||||
return authorizedClientManager;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* Copyright 2020-2023 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 sample.web;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
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.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
import org.springframework.web.reactive.function.client.WebClientResponseException;
|
||||
|
||||
import static org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction.clientRegistrationId;
|
||||
import static org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction.oauth2AuthorizedClient;
|
||||
|
||||
/**
|
||||
* @author Joe Grandja
|
||||
* @since 0.0.1
|
||||
*/
|
||||
@Controller
|
||||
public class AuthorizationController {
|
||||
private final WebClient webClient;
|
||||
private final String messagesBaseUri;
|
||||
|
||||
public AuthorizationController(WebClient webClient,
|
||||
@Value("${messages.base-uri}") String messagesBaseUri) {
|
||||
this.webClient = webClient;
|
||||
this.messagesBaseUri = messagesBaseUri;
|
||||
}
|
||||
|
||||
@GetMapping(value = "/authorize", params = "grant_type=authorization_code")
|
||||
public String authorizationCodeGrant(Model model,
|
||||
@RegisteredOAuth2AuthorizedClient("messaging-client-authorization-code")
|
||||
OAuth2AuthorizedClient authorizedClient) {
|
||||
|
||||
String[] messages = this.webClient
|
||||
.get()
|
||||
.uri(this.messagesBaseUri)
|
||||
.attributes(oauth2AuthorizedClient(authorizedClient))
|
||||
.retrieve()
|
||||
.bodyToMono(String[].class)
|
||||
.block();
|
||||
model.addAttribute("messages", messages);
|
||||
|
||||
return "index";
|
||||
}
|
||||
|
||||
// '/authorized' is the registered 'redirect_uri' for authorization_code
|
||||
@GetMapping(value = "/authorized", params = OAuth2ParameterNames.ERROR)
|
||||
public String authorizationFailed(Model model, HttpServletRequest request) {
|
||||
String errorCode = request.getParameter(OAuth2ParameterNames.ERROR);
|
||||
if (StringUtils.hasText(errorCode)) {
|
||||
model.addAttribute("error",
|
||||
new OAuth2Error(
|
||||
errorCode,
|
||||
request.getParameter(OAuth2ParameterNames.ERROR_DESCRIPTION),
|
||||
request.getParameter(OAuth2ParameterNames.ERROR_URI))
|
||||
);
|
||||
}
|
||||
|
||||
return "index";
|
||||
}
|
||||
|
||||
@GetMapping(value = "/authorize", params = "grant_type=client_credentials")
|
||||
public String clientCredentialsGrant(Model model) {
|
||||
|
||||
String[] messages = this.webClient
|
||||
.get()
|
||||
.uri(this.messagesBaseUri)
|
||||
.attributes(clientRegistrationId("messaging-client-client-credentials"))
|
||||
.retrieve()
|
||||
.bodyToMono(String[].class)
|
||||
.block();
|
||||
model.addAttribute("messages", messages);
|
||||
|
||||
return "index";
|
||||
}
|
||||
|
||||
@GetMapping(value = "/authorize", params = "grant_type=device_code")
|
||||
public String deviceCodeGrant() {
|
||||
return "device-activate";
|
||||
}
|
||||
|
||||
@ExceptionHandler(WebClientResponseException.class)
|
||||
public String handleError(Model model, WebClientResponseException ex) {
|
||||
model.addAttribute("error", ex.getMessage());
|
||||
return "index";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2020-2023 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 sample.web;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
/**
|
||||
* @author Joe Grandja
|
||||
* @author Dmitriy Dubson
|
||||
* @since 0.0.1
|
||||
*/
|
||||
@Controller
|
||||
public class DefaultController {
|
||||
|
||||
@GetMapping("/")
|
||||
public String root() {
|
||||
return "redirect:/index";
|
||||
}
|
||||
|
||||
@GetMapping("/index")
|
||||
public String index() {
|
||||
return "index";
|
||||
}
|
||||
|
||||
@GetMapping("/logged-out")
|
||||
public String loggedOut() {
|
||||
return "logged-out";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
/*
|
||||
* Copyright 2020-2023 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 sample.web;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
|
||||
import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.reactive.function.BodyInserters;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
|
||||
import static org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction.oauth2AuthorizedClient;
|
||||
|
||||
/**
|
||||
* @author Steve Riesenberg
|
||||
* @since 1.1
|
||||
*/
|
||||
@Controller
|
||||
public class DeviceController {
|
||||
|
||||
private static final Set<String> DEVICE_GRANT_ERRORS = new HashSet<>(Arrays.asList(
|
||||
"authorization_pending",
|
||||
"slow_down",
|
||||
"access_denied",
|
||||
"expired_token"
|
||||
));
|
||||
|
||||
private static final ParameterizedTypeReference<Map<String, Object>> TYPE_REFERENCE =
|
||||
new ParameterizedTypeReference<>() {};
|
||||
|
||||
private final ClientRegistrationRepository clientRegistrationRepository;
|
||||
|
||||
private final WebClient webClient;
|
||||
|
||||
private final String messagesBaseUri;
|
||||
|
||||
public DeviceController(ClientRegistrationRepository clientRegistrationRepository, WebClient webClient,
|
||||
@Value("${messages.base-uri}") String messagesBaseUri) {
|
||||
|
||||
this.clientRegistrationRepository = clientRegistrationRepository;
|
||||
this.webClient = webClient;
|
||||
this.messagesBaseUri = messagesBaseUri;
|
||||
}
|
||||
|
||||
@GetMapping("/device_authorize")
|
||||
public String authorize(Model model) {
|
||||
// @formatter:off
|
||||
ClientRegistration clientRegistration =
|
||||
this.clientRegistrationRepository.findByRegistrationId(
|
||||
"messaging-client-device-code");
|
||||
// @formatter:on
|
||||
|
||||
MultiValueMap<String, String> requestParameters = new LinkedMultiValueMap<>();
|
||||
requestParameters.add(OAuth2ParameterNames.CLIENT_ID, clientRegistration.getClientId());
|
||||
requestParameters.add(OAuth2ParameterNames.SCOPE, StringUtils.collectionToDelimitedString(
|
||||
clientRegistration.getScopes(), " "));
|
||||
|
||||
String deviceAuthorizationUri = (String) clientRegistration.getProviderDetails().getConfigurationMetadata().get("device_authorization_endpoint");
|
||||
|
||||
// @formatter:off
|
||||
Map<String, Object> responseParameters =
|
||||
this.webClient.post()
|
||||
.uri(deviceAuthorizationUri)
|
||||
.headers(headers -> {
|
||||
/*
|
||||
* This sample demonstrates the use of a public client that does not
|
||||
* store credentials or authenticate with the authorization server.
|
||||
*
|
||||
* See DeviceClientAuthenticationProvider in the authorization server
|
||||
* sample for an example customization that allows public clients.
|
||||
*
|
||||
* For a confidential client, change the client-authentication-method to
|
||||
* client_secret_basic and set the client-secret to send the
|
||||
* OAuth 2.0 Device Authorization Request with a clientId/clientSecret.
|
||||
*/
|
||||
if (!clientRegistration.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.NONE)) {
|
||||
headers.setBasicAuth(clientRegistration.getClientId(), clientRegistration.getClientSecret());
|
||||
}
|
||||
})
|
||||
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
.body(BodyInserters.fromFormData(requestParameters))
|
||||
.retrieve()
|
||||
.bodyToMono(TYPE_REFERENCE)
|
||||
.block();
|
||||
// @formatter:on
|
||||
|
||||
Objects.requireNonNull(responseParameters, "Device Authorization Response cannot be null");
|
||||
Instant issuedAt = Instant.now();
|
||||
Integer expiresIn = (Integer) responseParameters.get(OAuth2ParameterNames.EXPIRES_IN);
|
||||
Instant expiresAt = issuedAt.plusSeconds(expiresIn);
|
||||
|
||||
model.addAttribute("deviceCode", responseParameters.get(OAuth2ParameterNames.DEVICE_CODE));
|
||||
model.addAttribute("expiresAt", expiresAt);
|
||||
model.addAttribute("userCode", responseParameters.get(OAuth2ParameterNames.USER_CODE));
|
||||
model.addAttribute("verificationUri", responseParameters.get(OAuth2ParameterNames.VERIFICATION_URI));
|
||||
// Note: You could use a QR-code to display this URL
|
||||
model.addAttribute("verificationUriComplete", responseParameters.get(
|
||||
OAuth2ParameterNames.VERIFICATION_URI_COMPLETE));
|
||||
|
||||
return "device-authorize";
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #handleError(OAuth2AuthorizationException)
|
||||
*/
|
||||
@PostMapping("/device_authorize")
|
||||
public ResponseEntity<Void> poll(@RequestParam(OAuth2ParameterNames.DEVICE_CODE) String deviceCode,
|
||||
@RegisteredOAuth2AuthorizedClient("messaging-client-device-code")
|
||||
OAuth2AuthorizedClient authorizedClient) {
|
||||
|
||||
/*
|
||||
* The client will repeatedly poll until authorization is granted.
|
||||
*
|
||||
* The OAuth2AuthorizedClientManager uses the device_code parameter
|
||||
* to make a token request, which returns authorization_pending until
|
||||
* the user has granted authorization.
|
||||
*
|
||||
* If the user has denied authorization, access_denied is returned and
|
||||
* polling should stop.
|
||||
*
|
||||
* If the device code expires, expired_token is returned and polling
|
||||
* should stop.
|
||||
*
|
||||
* This endpoint simply returns 200 OK when the client is authorized.
|
||||
*/
|
||||
return ResponseEntity.status(HttpStatus.OK).build();
|
||||
}
|
||||
|
||||
@ExceptionHandler(OAuth2AuthorizationException.class)
|
||||
public ResponseEntity<OAuth2Error> handleError(OAuth2AuthorizationException ex) {
|
||||
String errorCode = ex.getError().getErrorCode();
|
||||
if (DEVICE_GRANT_ERRORS.contains(errorCode)) {
|
||||
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(ex.getError());
|
||||
}
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(ex.getError());
|
||||
}
|
||||
|
||||
@GetMapping("/device_authorized")
|
||||
public String authorized(Model model,
|
||||
@RegisteredOAuth2AuthorizedClient("messaging-client-device-code")
|
||||
OAuth2AuthorizedClient authorizedClient) {
|
||||
|
||||
String[] messages = this.webClient.get()
|
||||
.uri(this.messagesBaseUri)
|
||||
.attributes(oauth2AuthorizedClient(authorizedClient))
|
||||
.retrieve()
|
||||
.bodyToMono(String[].class)
|
||||
.block();
|
||||
model.addAttribute("messages", messages);
|
||||
|
||||
return "index";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* Copyright 2020-2023 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 sample.web.authentication;
|
||||
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.springframework.security.oauth2.client.ClientAuthorizationException;
|
||||
import org.springframework.security.oauth2.client.OAuth2AuthorizationContext;
|
||||
import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest;
|
||||
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
|
||||
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProvider;
|
||||
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2Token;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* @author Steve Riesenberg
|
||||
* @since 1.1
|
||||
*/
|
||||
public final class DeviceCodeOAuth2AuthorizedClientProvider implements OAuth2AuthorizedClientProvider {
|
||||
|
||||
private OAuth2AccessTokenResponseClient<OAuth2DeviceGrantRequest> accessTokenResponseClient =
|
||||
new OAuth2DeviceAccessTokenResponseClient();
|
||||
|
||||
private Duration clockSkew = Duration.ofSeconds(60);
|
||||
|
||||
private Clock clock = Clock.systemUTC();
|
||||
|
||||
public void setAccessTokenResponseClient(OAuth2AccessTokenResponseClient<OAuth2DeviceGrantRequest> accessTokenResponseClient) {
|
||||
this.accessTokenResponseClient = accessTokenResponseClient;
|
||||
}
|
||||
|
||||
public void setClockSkew(Duration clockSkew) {
|
||||
this.clockSkew = clockSkew;
|
||||
}
|
||||
|
||||
public void setClock(Clock clock) {
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OAuth2AuthorizedClient authorize(OAuth2AuthorizationContext context) {
|
||||
Assert.notNull(context, "context cannot be null");
|
||||
ClientRegistration clientRegistration = context.getClientRegistration();
|
||||
if (!AuthorizationGrantType.DEVICE_CODE.equals(clientRegistration.getAuthorizationGrantType())) {
|
||||
return null;
|
||||
}
|
||||
OAuth2AuthorizedClient authorizedClient = context.getAuthorizedClient();
|
||||
if (authorizedClient != null && !hasTokenExpired(authorizedClient.getAccessToken())) {
|
||||
// If client is already authorized but access token is NOT expired than no
|
||||
// need for re-authorization
|
||||
return null;
|
||||
}
|
||||
if (authorizedClient != null && authorizedClient.getRefreshToken() != null) {
|
||||
// If client is already authorized but access token is expired and a
|
||||
// refresh token is available, delegate to refresh_token.
|
||||
return null;
|
||||
}
|
||||
// *****************************************************************
|
||||
// Get device_code set via DefaultOAuth2AuthorizedClientManager#setContextAttributesMapper()
|
||||
// *****************************************************************
|
||||
String deviceCode = context.getAttribute(OAuth2ParameterNames.DEVICE_CODE);
|
||||
// Attempt to authorize the client, which will repeatedly fail until the user grants authorization
|
||||
OAuth2DeviceGrantRequest deviceGrantRequest = new OAuth2DeviceGrantRequest(clientRegistration, deviceCode);
|
||||
OAuth2AccessTokenResponse tokenResponse = getTokenResponse(clientRegistration, deviceGrantRequest);
|
||||
return new OAuth2AuthorizedClient(clientRegistration, context.getPrincipal().getName(),
|
||||
tokenResponse.getAccessToken(), tokenResponse.getRefreshToken());
|
||||
}
|
||||
|
||||
private OAuth2AccessTokenResponse getTokenResponse(ClientRegistration clientRegistration,
|
||||
OAuth2DeviceGrantRequest deviceGrantRequest) {
|
||||
try {
|
||||
return this.accessTokenResponseClient.getTokenResponse(deviceGrantRequest);
|
||||
} catch (OAuth2AuthorizationException ex) {
|
||||
throw new ClientAuthorizationException(ex.getError(), clientRegistration.getRegistrationId(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasTokenExpired(OAuth2Token token) {
|
||||
return this.clock.instant().isAfter(token.getExpiresAt().minus(this.clockSkew));
|
||||
}
|
||||
|
||||
public static Function<OAuth2AuthorizeRequest, Map<String, Object>> deviceCodeContextAttributesMapper() {
|
||||
return (authorizeRequest) -> {
|
||||
HttpServletRequest request = authorizeRequest.getAttribute(HttpServletRequest.class.getName());
|
||||
Assert.notNull(request, "request cannot be null");
|
||||
|
||||
// Obtain device code from request
|
||||
String deviceCode = request.getParameter(OAuth2ParameterNames.DEVICE_CODE);
|
||||
return (deviceCode != null) ? Collections.singletonMap(OAuth2ParameterNames.DEVICE_CODE, deviceCode) :
|
||||
Collections.emptyMap();
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* Copyright 2020-2023 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 sample.web.authentication;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.RequestEntity;
|
||||
import org.springframework.http.converter.FormHttpMessageConverter;
|
||||
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
|
||||
import org.springframework.security.oauth2.client.http.OAuth2ErrorResponseErrorHandler;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.client.RestClientException;
|
||||
import org.springframework.web.client.RestOperations;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
/**
|
||||
* @author Steve Riesenberg
|
||||
* @since 1.1
|
||||
*/
|
||||
public final class OAuth2DeviceAccessTokenResponseClient implements OAuth2AccessTokenResponseClient<OAuth2DeviceGrantRequest> {
|
||||
|
||||
private RestOperations restOperations;
|
||||
|
||||
public OAuth2DeviceAccessTokenResponseClient() {
|
||||
RestTemplate restTemplate = new RestTemplate(Arrays.asList(new FormHttpMessageConverter(),
|
||||
new OAuth2AccessTokenResponseHttpMessageConverter()));
|
||||
restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
|
||||
this.restOperations = restTemplate;
|
||||
}
|
||||
|
||||
public void setRestOperations(RestOperations restOperations) {
|
||||
this.restOperations = restOperations;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OAuth2AccessTokenResponse getTokenResponse(OAuth2DeviceGrantRequest deviceGrantRequest) {
|
||||
ClientRegistration clientRegistration = deviceGrantRequest.getClientRegistration();
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
/*
|
||||
* This sample demonstrates the use of a public client that does not
|
||||
* store credentials or authenticate with the authorization server.
|
||||
*
|
||||
* See DeviceClientAuthenticationProvider in the authorization server
|
||||
* sample for an example customization that allows public clients.
|
||||
*
|
||||
* For a confidential client, change the client-authentication-method
|
||||
* to client_secret_basic and set the client-secret to send the
|
||||
* OAuth 2.0 Token Request with a clientId/clientSecret.
|
||||
*/
|
||||
if (!clientRegistration.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.NONE)) {
|
||||
headers.setBasicAuth(clientRegistration.getClientId(), clientRegistration.getClientSecret());
|
||||
}
|
||||
|
||||
MultiValueMap<String, Object> requestParameters = new LinkedMultiValueMap<>();
|
||||
requestParameters.add(OAuth2ParameterNames.GRANT_TYPE, deviceGrantRequest.getGrantType().getValue());
|
||||
requestParameters.add(OAuth2ParameterNames.CLIENT_ID, clientRegistration.getClientId());
|
||||
requestParameters.add(OAuth2ParameterNames.DEVICE_CODE, deviceGrantRequest.getDeviceCode());
|
||||
|
||||
// @formatter:off
|
||||
RequestEntity<MultiValueMap<String, Object>> requestEntity =
|
||||
RequestEntity.post(deviceGrantRequest.getClientRegistration().getProviderDetails().getTokenUri())
|
||||
.headers(headers)
|
||||
.body(requestParameters);
|
||||
// @formatter:on
|
||||
|
||||
try {
|
||||
return this.restOperations.exchange(requestEntity, OAuth2AccessTokenResponse.class).getBody();
|
||||
} catch (RestClientException ex) {
|
||||
OAuth2Error oauth2Error = new OAuth2Error("invalid_token_response",
|
||||
"An error occurred while attempting to retrieve the OAuth 2.0 Access Token Response: "
|
||||
+ ex.getMessage(), null);
|
||||
throw new OAuth2AuthorizationException(oauth2Error, ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2020-2023 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 sample.web.authentication;
|
||||
|
||||
import org.springframework.security.oauth2.client.endpoint.AbstractOAuth2AuthorizationGrantRequest;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* @author Steve Riesenberg
|
||||
* @since 1.1
|
||||
*/
|
||||
public final class OAuth2DeviceGrantRequest extends AbstractOAuth2AuthorizationGrantRequest {
|
||||
|
||||
private final String deviceCode;
|
||||
|
||||
public OAuth2DeviceGrantRequest(ClientRegistration clientRegistration, String deviceCode) {
|
||||
super(AuthorizationGrantType.DEVICE_CODE, clientRegistration);
|
||||
Assert.hasText(deviceCode, "deviceCode cannot be empty");
|
||||
this.deviceCode = deviceCode;
|
||||
}
|
||||
|
||||
public String getDeviceCode() {
|
||||
return this.deviceCode;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user