diff --git a/samples/device-client/src/main/java/sample/web/DeviceController.java b/samples/device-client/src/main/java/sample/web/DeviceController.java index a88b7006..a23a55aa 100644 --- a/samples/device-client/src/main/java/sample/web/DeviceController.java +++ b/samples/device-client/src/main/java/sample/web/DeviceController.java @@ -36,6 +36,7 @@ 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.OAuth2DeviceCode; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; import org.springframework.security.web.context.HttpSessionSecurityContextRepository; @@ -105,8 +106,22 @@ public class DeviceController { Map responseParameters = this.webClient.post() .uri(clientRegistration.getProviderDetails().getAuthorizationUri()) -// .headers(headers -> headers.setBasicAuth(clientRegistration.getClientId(), -// clientRegistration.getClientSecret())) + .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() @@ -142,19 +157,21 @@ public class DeviceController { @RegisteredOAuth2AuthorizedClient("messaging-client-device-grant") 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 client is authorized. + /* + * 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(); } diff --git a/samples/device-client/src/main/java/sample/web/authentication/OAuth2DeviceAccessTokenResponseClient.java b/samples/device-client/src/main/java/sample/web/authentication/OAuth2DeviceAccessTokenResponseClient.java index 92915f48..1b69a3c8 100644 --- a/samples/device-client/src/main/java/sample/web/authentication/OAuth2DeviceAccessTokenResponseClient.java +++ b/samples/device-client/src/main/java/sample/web/authentication/OAuth2DeviceAccessTokenResponseClient.java @@ -23,6 +23,7 @@ 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; @@ -58,7 +59,20 @@ public final class OAuth2DeviceAccessTokenResponseClient implements OAuth2Access ClientRegistration clientRegistration = deviceGrantRequest.getClientRegistration(); HttpHeaders headers = new HttpHeaders(); -// headers.setBasicAuth(clientRegistration.getClientId(), clientRegistration.getClientSecret()); + /* + * 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 requestParameters = new LinkedMultiValueMap<>(); requestParameters.add(OAuth2ParameterNames.GRANT_TYPE, deviceGrantRequest.getGrantType().getValue()); diff --git a/samples/device-grant-authorizationserver/src/main/java/sample/authentication/DeviceClientAuthenticationProvider.java b/samples/device-grant-authorizationserver/src/main/java/sample/authentication/DeviceClientAuthenticationProvider.java index dc32f4d1..2ba2668e 100644 --- a/samples/device-grant-authorizationserver/src/main/java/sample/authentication/DeviceClientAuthenticationProvider.java +++ b/samples/device-grant-authorizationserver/src/main/java/sample/authentication/DeviceClientAuthenticationProvider.java @@ -17,6 +17,7 @@ package sample.authentication; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import sample.web.authentication.DeviceClientAuthenticationConverter; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.core.Authentication; @@ -28,8 +29,17 @@ import org.springframework.security.oauth2.core.OAuth2ErrorCodes; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; +import org.springframework.security.oauth2.server.authorization.web.OAuth2ClientAuthenticationFilter; import org.springframework.util.Assert; +/** + * @author Joe Grandja + * @author Steve Riesenberg + * @since 1.1 + * @see DeviceClientAuthenticationToken + * @see DeviceClientAuthenticationConverter + * @see OAuth2ClientAuthenticationFilter + */ public final class DeviceClientAuthenticationProvider implements AuthenticationProvider { private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-3.2.1"; private final Log logger = LogFactory.getLog(getClass()); diff --git a/samples/device-grant-authorizationserver/src/main/java/sample/authentication/DeviceClientAuthenticationToken.java b/samples/device-grant-authorizationserver/src/main/java/sample/authentication/DeviceClientAuthenticationToken.java index 8b73963b..4e9a3d2f 100644 --- a/samples/device-grant-authorizationserver/src/main/java/sample/authentication/DeviceClientAuthenticationToken.java +++ b/samples/device-grant-authorizationserver/src/main/java/sample/authentication/DeviceClientAuthenticationToken.java @@ -23,6 +23,11 @@ import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken; import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; +/** + * @author Joe Grandja + * @author Steve Riesenberg + * @since 1.1 + */ @Transient public class DeviceClientAuthenticationToken extends OAuth2ClientAuthenticationToken { diff --git a/samples/device-grant-authorizationserver/src/main/java/sample/config/SecurityConfig.java b/samples/device-grant-authorizationserver/src/main/java/sample/config/SecurityConfig.java index 9fb0cab7..23a2e0c3 100644 --- a/samples/device-grant-authorizationserver/src/main/java/sample/config/SecurityConfig.java +++ b/samples/device-grant-authorizationserver/src/main/java/sample/config/SecurityConfig.java @@ -74,20 +74,40 @@ public class SecurityConfig { HttpSecurity http, RegisteredClientRepository registeredClientRepository, AuthorizationServerSettings authorizationServerSettings) throws Exception { OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http); + + /* + * This sample demonstrates the use of a public client that does not + * store credentials or authenticate with the authorization server. + * + * The following components show how to customize the authorization + * server to allow for device clients to perform requests to the + * OAuth 2.0 Device Authorization Endpoint and Token Endpoint without + * a clientId/clientSecret. + * + * CAUTION: These endpoints will not require any authentication, and can + * be accessed by any client that has a valid clientId. + * + * It is therefore RECOMMENDED to carefully monitor the use of these + * endpoints and employ any additional protections as needed, which is + * outside the scope of this sample. + */ + DeviceClientAuthenticationConverter deviceClientAuthenticationConverter = + new DeviceClientAuthenticationConverter( + authorizationServerSettings.getDeviceAuthorizationEndpoint()); + DeviceClientAuthenticationProvider deviceClientAuthenticationProvider = + new DeviceClientAuthenticationProvider(registeredClientRepository); + + // @formatter:off http.getConfigurer(OAuth2AuthorizationServerConfigurer.class) .deviceAuthorizationEndpoint((deviceAuthorizationEndpoint) -> deviceAuthorizationEndpoint .verificationUri("/activate") ) - .clientAuthentication((clientAuthentication) -> - clientAuthentication - .authenticationConverter( - new DeviceClientAuthenticationConverter( - authorizationServerSettings.getDeviceAuthorizationEndpoint())) - .authenticationProvider( - new DeviceClientAuthenticationProvider( - registeredClientRepository)) + .clientAuthentication((clientAuthentication) -> clientAuthentication + .authenticationConverter(deviceClientAuthenticationConverter) + .authenticationProvider(deviceClientAuthenticationProvider) ) .oidc(Customizer.withDefaults()); // Enable OpenID Connect 1.0 + // @formatter:on // @formatter:off http diff --git a/samples/device-grant-authorizationserver/src/main/java/sample/web/authentication/DeviceClientAuthenticationConverter.java b/samples/device-grant-authorizationserver/src/main/java/sample/web/authentication/DeviceClientAuthenticationConverter.java index 5d46f889..14dd3e5f 100644 --- a/samples/device-grant-authorizationserver/src/main/java/sample/web/authentication/DeviceClientAuthenticationConverter.java +++ b/samples/device-grant-authorizationserver/src/main/java/sample/web/authentication/DeviceClientAuthenticationConverter.java @@ -16,7 +16,6 @@ package sample.web.authentication; import jakarta.servlet.http.HttpServletRequest; - import sample.authentication.DeviceClientAuthenticationToken; import org.springframework.http.HttpMethod; @@ -33,6 +32,11 @@ import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.util.StringUtils; +/** + * @author Joe Grandja + * @author Steve Riesenberg + * @since 1.1 + */ public final class DeviceClientAuthenticationConverter implements AuthenticationConverter { private final RequestMatcher deviceAuthorizationRequestMatcher; private final RequestMatcher deviceAccessTokenRequestMatcher;