Эх сурвалжийг харах

Add customization to support public clients for device grant

Issue gh-1157
Joe Grandja 2 жил өмнө
parent
commit
be50f66b4c

+ 2 - 2
samples/device-client/src/main/java/sample/web/DeviceController.java

@@ -105,8 +105,8 @@ public class DeviceController {
 		Map<String, Object> responseParameters =
 				this.webClient.post()
 						.uri(clientRegistration.getProviderDetails().getAuthorizationUri())
-						.headers(headers -> headers.setBasicAuth(clientRegistration.getClientId(),
-								clientRegistration.getClientSecret()))
+//						.headers(headers -> headers.setBasicAuth(clientRegistration.getClientId(),
+//								clientRegistration.getClientSecret()))
 						.contentType(MediaType.APPLICATION_FORM_URLENCODED)
 						.body(BodyInserters.fromFormData(requestParameters))
 						.retrieve()

+ 1 - 1
samples/device-client/src/main/java/sample/web/authentication/OAuth2DeviceAccessTokenResponseClient.java

@@ -58,7 +58,7 @@ public final class OAuth2DeviceAccessTokenResponseClient implements OAuth2Access
 		ClientRegistration clientRegistration = deviceGrantRequest.getClientRegistration();
 
 		HttpHeaders headers = new HttpHeaders();
-		headers.setBasicAuth(clientRegistration.getClientId(), clientRegistration.getClientSecret());
+//		headers.setBasicAuth(clientRegistration.getClientId(), clientRegistration.getClientSecret());
 
 		MultiValueMap<String, Object> requestParameters = new LinkedMultiValueMap<>();
 		requestParameters.add(OAuth2ParameterNames.GRANT_TYPE, deviceGrantRequest.getGrantType().getValue());

+ 2 - 2
samples/device-client/src/main/resources/application.yml

@@ -15,8 +15,8 @@ spring:
         registration:
           messaging-client-device-grant:
             provider: spring
-            client-id: messaging-client
-            client-secret: secret
+            client-id: device-messaging-client
+            client-authentication-method: none
             authorization-grant-type: urn:ietf:params:oauth:grant-type:device_code
             scope: message.read,message.write
             client-name: messaging-client-device-grant

+ 93 - 0
samples/device-grant-authorizationserver/src/main/java/sample/authentication/DeviceClientAuthenticationProvider.java

@@ -0,0 +1,93 @@
+/*
+ * 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.authentication;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.core.OAuth2Error;
+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.util.Assert;
+
+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());
+	private final RegisteredClientRepository registeredClientRepository;
+
+	public DeviceClientAuthenticationProvider(RegisteredClientRepository registeredClientRepository) {
+		Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null");
+		this.registeredClientRepository = registeredClientRepository;
+	}
+
+	@Override
+	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
+		DeviceClientAuthenticationToken deviceClientAuthentication =
+				(DeviceClientAuthenticationToken) authentication;
+
+		if (!ClientAuthenticationMethod.NONE.equals(deviceClientAuthentication.getClientAuthenticationMethod())) {
+			return null;
+		}
+
+		String clientId = deviceClientAuthentication.getPrincipal().toString();
+		RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(clientId);
+		if (registeredClient == null) {
+			throwInvalidClient(OAuth2ParameterNames.CLIENT_ID);
+		}
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Retrieved registered client");
+		}
+
+		if (!registeredClient.getClientAuthenticationMethods().contains(
+				deviceClientAuthentication.getClientAuthenticationMethod())) {
+			throwInvalidClient("authentication_method");
+		}
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Validated device client authentication parameters");
+		}
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Authenticated device client");
+		}
+
+		return new DeviceClientAuthenticationToken(registeredClient,
+				deviceClientAuthentication.getClientAuthenticationMethod(), null);
+	}
+
+	@Override
+	public boolean supports(Class<?> authentication) {
+		return DeviceClientAuthenticationToken.class.isAssignableFrom(authentication);
+	}
+
+	private static void throwInvalidClient(String parameterName) {
+		OAuth2Error error = new OAuth2Error(
+				OAuth2ErrorCodes.INVALID_CLIENT,
+				"Device client authentication failed: " + parameterName,
+				ERROR_URI
+		);
+		throw new OAuth2AuthenticationException(error);
+	}
+
+}

+ 39 - 0
samples/device-grant-authorizationserver/src/main/java/sample/authentication/DeviceClientAuthenticationToken.java

@@ -0,0 +1,39 @@
+/*
+ * 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.authentication;
+
+import java.util.Map;
+
+import org.springframework.lang.Nullable;
+import org.springframework.security.core.Transient;
+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;
+
+@Transient
+public class DeviceClientAuthenticationToken extends OAuth2ClientAuthenticationToken {
+
+	public DeviceClientAuthenticationToken(String clientId, ClientAuthenticationMethod clientAuthenticationMethod,
+			@Nullable Object credentials, @Nullable Map<String, Object> additionalParameters) {
+		super(clientId, clientAuthenticationMethod, credentials, additionalParameters);
+	}
+
+	public DeviceClientAuthenticationToken(RegisteredClient registeredClient, ClientAuthenticationMethod clientAuthenticationMethod,
+			@Nullable Object credentials) {
+		super(registeredClient, clientAuthenticationMethod, credentials);
+	}
+
+}

+ 24 - 3
samples/device-grant-authorizationserver/src/main/java/sample/config/SecurityConfig.java

@@ -26,6 +26,8 @@ import com.nimbusds.jose.jwk.RSAKey;
 import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
 import com.nimbusds.jose.jwk.source.JWKSource;
 import com.nimbusds.jose.proc.SecurityContext;
+import sample.authentication.DeviceClientAuthenticationProvider;
+import sample.web.authentication.DeviceClientAuthenticationConverter;
 
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
@@ -68,12 +70,23 @@ public class SecurityConfig {
 
 	@Bean
 	@Order(1)
-	public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
+	public SecurityFilterChain authorizationServerSecurityFilterChain(
+			HttpSecurity http, RegisteredClientRepository registeredClientRepository,
+			AuthorizationServerSettings authorizationServerSettings) throws Exception {
 		OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
 		http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
 			.deviceAuthorizationEndpoint((deviceAuthorizationEndpoint) -> deviceAuthorizationEndpoint
 				.verificationUri("/activate")
 			)
+			.clientAuthentication((clientAuthentication) ->
+				clientAuthentication
+					.authenticationConverter(
+						new DeviceClientAuthenticationConverter(
+							authorizationServerSettings.getDeviceAuthorizationEndpoint()))
+					.authenticationProvider(
+						new DeviceClientAuthenticationProvider(
+							registeredClientRepository))
+			)
 			.oidc(Customizer.withDefaults());	// Enable OpenID Connect 1.0
 
 		// @formatter:off
@@ -124,7 +137,6 @@ public class SecurityConfig {
 				.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
 				.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
 				.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
-				.authorizationGrantType(AuthorizationGrantType.DEVICE_CODE)
 				.redirectUri("http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc")
 				.redirectUri("http://127.0.0.1:8080/authorized")
 				.scope(OidcScopes.OPENID)
@@ -134,7 +146,16 @@ public class SecurityConfig {
 				.clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
 				.build();
 
-		return new InMemoryRegisteredClientRepository(registeredClient);
+		RegisteredClient deviceClient = RegisteredClient.withId(UUID.randomUUID().toString())
+				.clientId("device-messaging-client")
+				.clientAuthenticationMethod(ClientAuthenticationMethod.NONE)
+				.authorizationGrantType(AuthorizationGrantType.DEVICE_CODE)
+				.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
+				.scope("message.read")
+				.scope("message.write")
+				.build();
+
+		return new InMemoryRegisteredClientRepository(registeredClient, deviceClient);
 	}
 
 	@Bean

+ 71 - 0
samples/device-grant-authorizationserver/src/main/java/sample/web/authentication/DeviceClientAuthenticationConverter.java

@@ -0,0 +1,71 @@
+/*
+ * 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 jakarta.servlet.http.HttpServletRequest;
+
+import sample.authentication.DeviceClientAuthenticationToken;
+
+import org.springframework.http.HttpMethod;
+import org.springframework.lang.Nullable;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
+import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
+import org.springframework.security.web.authentication.AuthenticationConverter;
+import org.springframework.security.web.util.matcher.AndRequestMatcher;
+import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
+import org.springframework.security.web.util.matcher.RequestMatcher;
+import org.springframework.util.StringUtils;
+
+public final class DeviceClientAuthenticationConverter implements AuthenticationConverter {
+	private final RequestMatcher deviceAuthorizationRequestMatcher;
+	private final RequestMatcher deviceAccessTokenRequestMatcher;
+
+	public DeviceClientAuthenticationConverter(String deviceAuthorizationEndpointUri) {
+		RequestMatcher clientIdParameterMatcher = request ->
+				request.getParameter(OAuth2ParameterNames.CLIENT_ID) != null;
+		this.deviceAuthorizationRequestMatcher = new AndRequestMatcher(
+				new AntPathRequestMatcher(
+						deviceAuthorizationEndpointUri, HttpMethod.POST.name()),
+				clientIdParameterMatcher);
+		this.deviceAccessTokenRequestMatcher = request ->
+				AuthorizationGrantType.DEVICE_CODE.getValue().equals(request.getParameter(OAuth2ParameterNames.GRANT_TYPE)) &&
+						request.getParameter(OAuth2ParameterNames.DEVICE_CODE) != null &&
+						request.getParameter(OAuth2ParameterNames.CLIENT_ID) != null;
+	}
+
+	@Nullable
+	@Override
+	public Authentication convert(HttpServletRequest request) {
+		if (!this.deviceAuthorizationRequestMatcher.matches(request) &&
+				!this.deviceAccessTokenRequestMatcher.matches(request)) {
+			return null;
+		}
+
+		// client_id (REQUIRED)
+		String clientId = request.getParameter(OAuth2ParameterNames.CLIENT_ID);
+		if (!StringUtils.hasText(clientId) ||
+				request.getParameterValues(OAuth2ParameterNames.CLIENT_ID).length != 1) {
+			throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);
+		}
+
+		return new DeviceClientAuthenticationToken(clientId, ClientAuthenticationMethod.NONE, null, null);
+	}
+
+}