|
@@ -0,0 +1,232 @@
|
|
|
|
+/*
|
|
|
|
+ * Copyright 2002-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 org.springframework.security.oauth2.core.http.converter;
|
|
|
|
+
|
|
|
|
+import java.time.Instant;
|
|
|
|
+import java.time.temporal.ChronoUnit;
|
|
|
|
+import java.util.Arrays;
|
|
|
|
+import java.util.HashMap;
|
|
|
|
+import java.util.HashSet;
|
|
|
|
+import java.util.LinkedHashMap;
|
|
|
|
+import java.util.Map;
|
|
|
|
+import java.util.Set;
|
|
|
|
+
|
|
|
|
+import org.springframework.core.ParameterizedTypeReference;
|
|
|
|
+import org.springframework.core.convert.converter.Converter;
|
|
|
|
+import org.springframework.http.HttpInputMessage;
|
|
|
|
+import org.springframework.http.HttpOutputMessage;
|
|
|
|
+import org.springframework.http.MediaType;
|
|
|
|
+import org.springframework.http.converter.AbstractHttpMessageConverter;
|
|
|
|
+import org.springframework.http.converter.GenericHttpMessageConverter;
|
|
|
|
+import org.springframework.http.converter.HttpMessageConverter;
|
|
|
|
+import org.springframework.http.converter.HttpMessageNotReadableException;
|
|
|
|
+import org.springframework.http.converter.HttpMessageNotWritableException;
|
|
|
|
+import org.springframework.security.oauth2.core.endpoint.OAuth2DeviceAuthorizationResponse;
|
|
|
|
+import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
|
|
|
+import org.springframework.util.Assert;
|
|
|
|
+import org.springframework.util.CollectionUtils;
|
|
|
|
+import org.springframework.util.StringUtils;
|
|
|
|
+
|
|
|
|
+/**
|
|
|
|
+ * A {@link HttpMessageConverter} for an {@link OAuth2DeviceAuthorizationResponse OAuth
|
|
|
|
+ * 2.0 Device Authorization Response}.
|
|
|
|
+ *
|
|
|
|
+ * @author Steve Riesenberg
|
|
|
|
+ * @since 6.1
|
|
|
|
+ * @see AbstractHttpMessageConverter
|
|
|
|
+ * @see OAuth2DeviceAuthorizationResponse
|
|
|
|
+ */
|
|
|
|
+public class OAuth2DeviceAuthorizationResponseHttpMessageConverter
|
|
|
|
+ extends AbstractHttpMessageConverter<OAuth2DeviceAuthorizationResponse> {
|
|
|
|
+
|
|
|
|
+ private static final ParameterizedTypeReference<Map<String, Object>> STRING_OBJECT_MAP = new ParameterizedTypeReference<>() {
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ private final GenericHttpMessageConverter<Object> jsonMessageConvereter = HttpMessageConverters
|
|
|
|
+ .getJsonMessageConverter();
|
|
|
|
+
|
|
|
|
+ private Converter<Map<String, Object>, OAuth2DeviceAuthorizationResponse> deviceAuthorizationResponseConverter = new DefaultMapOAuth2DeviceAuthorizationResponseConverter();
|
|
|
|
+
|
|
|
|
+ private Converter<OAuth2DeviceAuthorizationResponse, Map<String, Object>> deviceAuthorizationResponseParametersConverter = new DefaultOAuth2DeviceAuthorizationResponseMapConverter();
|
|
|
|
+
|
|
|
|
+ @Override
|
|
|
|
+ protected boolean supports(Class<?> clazz) {
|
|
|
|
+ return OAuth2DeviceAuthorizationResponse.class.isAssignableFrom(clazz);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ @Override
|
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
|
+ protected OAuth2DeviceAuthorizationResponse readInternal(Class<? extends OAuth2DeviceAuthorizationResponse> clazz,
|
|
|
|
+ HttpInputMessage inputMessage) throws HttpMessageNotReadableException {
|
|
|
|
+
|
|
|
|
+ try {
|
|
|
|
+ Map<String, Object> deviceAuthorizationResponseParameters = (Map<String, Object>) this.jsonMessageConvereter
|
|
|
|
+ .read(STRING_OBJECT_MAP.getType(), null, inputMessage);
|
|
|
|
+ return this.deviceAuthorizationResponseConverter.convert(deviceAuthorizationResponseParameters);
|
|
|
|
+ }
|
|
|
|
+ catch (Exception ex) {
|
|
|
|
+ throw new HttpMessageNotReadableException(
|
|
|
|
+ "An error occurred reading the OAuth 2.0 Device Authorization Response: " + ex.getMessage(), ex,
|
|
|
|
+ inputMessage);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ @Override
|
|
|
|
+ protected void writeInternal(OAuth2DeviceAuthorizationResponse deviceAuthorizationResponse,
|
|
|
|
+ HttpOutputMessage outputMessage) throws HttpMessageNotWritableException {
|
|
|
|
+
|
|
|
|
+ try {
|
|
|
|
+ Map<String, Object> deviceauthorizationResponseParameters = this.deviceAuthorizationResponseParametersConverter
|
|
|
|
+ .convert(deviceAuthorizationResponse);
|
|
|
|
+ this.jsonMessageConvereter.write(deviceauthorizationResponseParameters, STRING_OBJECT_MAP.getType(),
|
|
|
|
+ MediaType.APPLICATION_JSON, outputMessage);
|
|
|
|
+ }
|
|
|
|
+ catch (Exception ex) {
|
|
|
|
+ throw new HttpMessageNotWritableException(
|
|
|
|
+ "An error occurred writing the OAuth 2.0 Device Authorization Response: " + ex.getMessage(), ex);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * Sets the {@link Converter} used for converting the OAuth 2.0 Device Authorization
|
|
|
|
+ * Response parameters to an {@link OAuth2DeviceAuthorizationResponse}.
|
|
|
|
+ * @param deviceAuthorizationResponseConverter the {@link Converter} used for
|
|
|
|
+ * converting to an {@link OAuth2DeviceAuthorizationResponse}
|
|
|
|
+ */
|
|
|
|
+ public void setDeviceAuthorizationResponseConverter(
|
|
|
|
+ Converter<Map<String, Object>, OAuth2DeviceAuthorizationResponse> deviceAuthorizationResponseConverter) {
|
|
|
|
+ Assert.notNull(deviceAuthorizationResponseConverter, "deviceAuthorizationResponseConverter cannot be null");
|
|
|
|
+ this.deviceAuthorizationResponseConverter = deviceAuthorizationResponseConverter;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * Sets the {@link Converter} used for converting the
|
|
|
|
+ * {@link OAuth2DeviceAuthorizationResponse} to a {@code Map} representation of the
|
|
|
|
+ * OAuth 2.0 Device Authorization Response parameters.
|
|
|
|
+ * @param deviceAuthorizationResponseParametersConverter the {@link Converter} used
|
|
|
|
+ * for converting to a {@code Map} representation of the Device Authorization Response
|
|
|
|
+ * parameters
|
|
|
|
+ */
|
|
|
|
+ public void setDeviceAuthorizationResponseParametersConverter(
|
|
|
|
+ Converter<OAuth2DeviceAuthorizationResponse, Map<String, Object>> deviceAuthorizationResponseParametersConverter) {
|
|
|
|
+ Assert.notNull(deviceAuthorizationResponseParametersConverter,
|
|
|
|
+ "deviceAuthorizationResponseParametersConverter cannot be null");
|
|
|
|
+ this.deviceAuthorizationResponseParametersConverter = deviceAuthorizationResponseParametersConverter;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ private static final class DefaultMapOAuth2DeviceAuthorizationResponseConverter
|
|
|
|
+ implements Converter<Map<String, Object>, OAuth2DeviceAuthorizationResponse> {
|
|
|
|
+
|
|
|
|
+ private static final Set<String> DEVICE_AUTHORIZATION_RESPONSE_PARAMETER_NAMES = new HashSet<>(
|
|
|
|
+ Arrays.asList(OAuth2ParameterNames.DEVICE_CODE, OAuth2ParameterNames.USER_CODE,
|
|
|
|
+ OAuth2ParameterNames.VERIFICATION_URI, OAuth2ParameterNames.VERIFICATION_URI_COMPLETE,
|
|
|
|
+ OAuth2ParameterNames.EXPIRES_IN, OAuth2ParameterNames.INTERVAL));
|
|
|
|
+
|
|
|
|
+ @Override
|
|
|
|
+ public OAuth2DeviceAuthorizationResponse convert(Map<String, Object> parameters) {
|
|
|
|
+ String deviceCode = getParameterValue(parameters, OAuth2ParameterNames.DEVICE_CODE);
|
|
|
|
+ String userCode = getParameterValue(parameters, OAuth2ParameterNames.USER_CODE);
|
|
|
|
+ String verificationUri = getParameterValue(parameters, OAuth2ParameterNames.VERIFICATION_URI);
|
|
|
|
+ String verificationUriComplete = getParameterValue(parameters,
|
|
|
|
+ OAuth2ParameterNames.VERIFICATION_URI_COMPLETE);
|
|
|
|
+ long expiresIn = getParameterValue(parameters, OAuth2ParameterNames.EXPIRES_IN, 0L);
|
|
|
|
+ long interval = getParameterValue(parameters, OAuth2ParameterNames.INTERVAL, 0L);
|
|
|
|
+ Map<String, Object> additionalParameters = new LinkedHashMap<>();
|
|
|
|
+ parameters.forEach((key, value) -> {
|
|
|
|
+ if (!DEVICE_AUTHORIZATION_RESPONSE_PARAMETER_NAMES.contains(key)) {
|
|
|
|
+ additionalParameters.put(key, value);
|
|
|
|
+ }
|
|
|
|
+ });
|
|
|
|
+ // @formatter:off
|
|
|
|
+ return OAuth2DeviceAuthorizationResponse.with(deviceCode, userCode)
|
|
|
|
+ .verificationUri(verificationUri)
|
|
|
|
+ .verificationUriComplete(verificationUriComplete)
|
|
|
|
+ .expiresIn(expiresIn)
|
|
|
|
+ .interval(interval)
|
|
|
|
+ .additionalParameters(additionalParameters)
|
|
|
|
+ .build();
|
|
|
|
+ // @formatter:on
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ private static String getParameterValue(Map<String, Object> parameters, String parameterName) {
|
|
|
|
+ Object obj = parameters.get(parameterName);
|
|
|
|
+ return (obj != null) ? obj.toString() : null;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ private static long getParameterValue(Map<String, Object> tokenResponseParameters, String parameterName,
|
|
|
|
+ long defaultValue) {
|
|
|
|
+ long parameterValue = defaultValue;
|
|
|
|
+
|
|
|
|
+ Object obj = tokenResponseParameters.get(parameterName);
|
|
|
|
+ if (obj != null) {
|
|
|
|
+ // Final classes Long and Integer do not need to be coerced
|
|
|
|
+ if (obj.getClass() == Long.class) {
|
|
|
|
+ parameterValue = (Long) obj;
|
|
|
|
+ }
|
|
|
|
+ else if (obj.getClass() == Integer.class) {
|
|
|
|
+ parameterValue = (Integer) obj;
|
|
|
|
+ }
|
|
|
|
+ else {
|
|
|
|
+ // Attempt to coerce to a long (typically from a String)
|
|
|
|
+ try {
|
|
|
|
+ parameterValue = Long.parseLong(obj.toString());
|
|
|
|
+ }
|
|
|
|
+ catch (NumberFormatException ignored) {
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return parameterValue;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ private static final class DefaultOAuth2DeviceAuthorizationResponseMapConverter
|
|
|
|
+ implements Converter<OAuth2DeviceAuthorizationResponse, Map<String, Object>> {
|
|
|
|
+
|
|
|
|
+ @Override
|
|
|
|
+ public Map<String, Object> convert(OAuth2DeviceAuthorizationResponse deviceAuthorizationResponse) {
|
|
|
|
+ Map<String, Object> parameters = new HashMap<>();
|
|
|
|
+ parameters.put(OAuth2ParameterNames.DEVICE_CODE,
|
|
|
|
+ deviceAuthorizationResponse.getDeviceCode().getTokenValue());
|
|
|
|
+ parameters.put(OAuth2ParameterNames.USER_CODE, deviceAuthorizationResponse.getUserCode().getTokenValue());
|
|
|
|
+ parameters.put(OAuth2ParameterNames.VERIFICATION_URI, deviceAuthorizationResponse.getVerificationUri());
|
|
|
|
+ if (StringUtils.hasText(deviceAuthorizationResponse.getVerificationUriComplete())) {
|
|
|
|
+ parameters.put(OAuth2ParameterNames.VERIFICATION_URI_COMPLETE,
|
|
|
|
+ deviceAuthorizationResponse.getVerificationUriComplete());
|
|
|
|
+ }
|
|
|
|
+ parameters.put(OAuth2ParameterNames.EXPIRES_IN, getExpiresIn(deviceAuthorizationResponse));
|
|
|
|
+ if (deviceAuthorizationResponse.getInterval() > 0) {
|
|
|
|
+ parameters.put(OAuth2ParameterNames.INTERVAL, deviceAuthorizationResponse.getInterval());
|
|
|
|
+ }
|
|
|
|
+ if (!CollectionUtils.isEmpty(deviceAuthorizationResponse.getAdditionalParameters())) {
|
|
|
|
+ parameters.putAll(deviceAuthorizationResponse.getAdditionalParameters());
|
|
|
|
+ }
|
|
|
|
+ return parameters;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ private static long getExpiresIn(OAuth2DeviceAuthorizationResponse deviceAuthorizationResponse) {
|
|
|
|
+ if (deviceAuthorizationResponse.getDeviceCode().getExpiresAt() != null) {
|
|
|
|
+ return ChronoUnit.SECONDS.between(Instant.now(),
|
|
|
|
+ deviceAuthorizationResponse.getDeviceCode().getExpiresAt());
|
|
|
|
+ }
|
|
|
|
+ return -1;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+}
|