7 changed files with 829 additions and 2 deletions
@ -0,0 +1,43 @@
@@ -0,0 +1,43 @@
|
||||
/* |
||||
* 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; |
||||
|
||||
import java.time.Instant; |
||||
|
||||
/** |
||||
* An implementation of an {@link AbstractOAuth2Token} representing a device code as part |
||||
* of the OAuth 2.0 Device Authorization Grant. |
||||
* |
||||
* @author Steve Riesenberg |
||||
* @since 6.1 |
||||
* @see OAuth2UserCode |
||||
* @see <a target="_blank" href= "https://tools.ietf.org/html/rfc8628#section-3.2">Section |
||||
* 3.2 Device Authorization Response</a> |
||||
*/ |
||||
public final class OAuth2DeviceCode extends AbstractOAuth2Token { |
||||
|
||||
/** |
||||
* Constructs an {@code OAuth2DeviceCode} using the provided parameters. |
||||
* @param tokenValue the token value |
||||
* @param issuedAt the time at which the token was issued |
||||
* @param expiresAt the time at which the token expires |
||||
*/ |
||||
public OAuth2DeviceCode(String tokenValue, Instant issuedAt, Instant expiresAt) { |
||||
super(tokenValue, issuedAt, expiresAt); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,43 @@
@@ -0,0 +1,43 @@
|
||||
/* |
||||
* 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; |
||||
|
||||
import java.time.Instant; |
||||
|
||||
/** |
||||
* An implementation of an {@link AbstractOAuth2Token} representing a user code as part of |
||||
* the OAuth 2.0 Device Authorization Grant. |
||||
* |
||||
* @author Steve Riesenberg |
||||
* @since 6.1 |
||||
* @see OAuth2DeviceCode |
||||
* @see <a target="_blank" href= "https://tools.ietf.org/html/rfc8628#section-3.2">Section |
||||
* 3.2 Device Authorization Response</a> |
||||
*/ |
||||
public final class OAuth2UserCode extends AbstractOAuth2Token { |
||||
|
||||
/** |
||||
* Constructs an {@code OAuth2UserCode} using the provided parameters. |
||||
* @param tokenValue the token value |
||||
* @param issuedAt the time at which the token was issued |
||||
* @param expiresAt the time at which the token expires |
||||
*/ |
||||
public OAuth2UserCode(String tokenValue, Instant issuedAt, Instant expiresAt) { |
||||
super(tokenValue, issuedAt, expiresAt); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,263 @@
@@ -0,0 +1,263 @@
|
||||
/* |
||||
* 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.endpoint; |
||||
|
||||
import java.time.Instant; |
||||
import java.time.temporal.ChronoUnit; |
||||
import java.util.Collections; |
||||
import java.util.Map; |
||||
|
||||
import org.springframework.security.oauth2.core.OAuth2DeviceCode; |
||||
import org.springframework.security.oauth2.core.OAuth2UserCode; |
||||
import org.springframework.util.Assert; |
||||
import org.springframework.util.CollectionUtils; |
||||
|
||||
/** |
||||
* A representation of an OAuth 2.0 Device Authorization Response. |
||||
* |
||||
* @author Steve Riesenberg |
||||
* @since 6.1 |
||||
* @see OAuth2DeviceCode |
||||
* @see OAuth2UserCode |
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc8628#section-3.2">Section |
||||
* 3.2 Device Authorization Response</a> |
||||
*/ |
||||
public final class OAuth2DeviceAuthorizationResponse { |
||||
|
||||
private OAuth2DeviceCode deviceCode; |
||||
|
||||
private OAuth2UserCode userCode; |
||||
|
||||
private String verificationUri; |
||||
|
||||
private String verificationUriComplete; |
||||
|
||||
private long interval; |
||||
|
||||
private Map<String, Object> additionalParameters; |
||||
|
||||
private OAuth2DeviceAuthorizationResponse() { |
||||
} |
||||
|
||||
/** |
||||
* Returns the {@link OAuth2DeviceCode Device Code}. |
||||
* @return the {@link OAuth2DeviceCode} |
||||
*/ |
||||
public OAuth2DeviceCode getDeviceCode() { |
||||
return this.deviceCode; |
||||
} |
||||
|
||||
/** |
||||
* Returns the {@link OAuth2UserCode User Code}. |
||||
* @return the {@link OAuth2UserCode} |
||||
*/ |
||||
public OAuth2UserCode getUserCode() { |
||||
return this.userCode; |
||||
} |
||||
|
||||
/** |
||||
* Returns the end-user verification URI. |
||||
* @return the end-user verification URI |
||||
*/ |
||||
public String getVerificationUri() { |
||||
return this.verificationUri; |
||||
} |
||||
|
||||
/** |
||||
* Returns the end-user verification URI that includes the user code. |
||||
* @return the end-user verification URI that includes the user code |
||||
*/ |
||||
public String getVerificationUriComplete() { |
||||
return this.verificationUriComplete; |
||||
} |
||||
|
||||
/** |
||||
* Returns the minimum amount of time (in seconds) that the client should wait between |
||||
* polling requests to the token endpoint. |
||||
* @return the minimum amount of time between polling requests |
||||
*/ |
||||
public long getInterval() { |
||||
return this.interval; |
||||
} |
||||
|
||||
/** |
||||
* Returns the additional parameters returned in the response. |
||||
* @return a {@code Map} of the additional parameters returned in the response, may be |
||||
* empty. |
||||
*/ |
||||
public Map<String, Object> getAdditionalParameters() { |
||||
return this.additionalParameters; |
||||
} |
||||
|
||||
/** |
||||
* Returns a new {@link Builder}, initialized with the provided device code and user |
||||
* code values. |
||||
* @param deviceCode the value of the device code |
||||
* @param userCode the value of the user code |
||||
* @return the {@link Builder} |
||||
*/ |
||||
public static Builder with(String deviceCode, String userCode) { |
||||
Assert.hasText(deviceCode, "deviceCode cannot be empty"); |
||||
Assert.hasText(userCode, "userCode cannot be empty"); |
||||
return new Builder(deviceCode, userCode); |
||||
} |
||||
|
||||
/** |
||||
* Returns a new {@link Builder}, initialized with the provided device code and user |
||||
* code. |
||||
* @param deviceCode the {@link OAuth2DeviceCode} |
||||
* @param userCode the {@link OAuth2UserCode} |
||||
* @return the {@link Builder} |
||||
*/ |
||||
public static Builder with(OAuth2DeviceCode deviceCode, OAuth2UserCode userCode) { |
||||
Assert.notNull(deviceCode, "deviceCode cannot be null"); |
||||
Assert.notNull(userCode, "userCode cannot be null"); |
||||
return new Builder(deviceCode, userCode); |
||||
} |
||||
|
||||
/** |
||||
* Returns a new {@link Builder}, initialized with the provided response. |
||||
* @param deviceAuthorizationResponse the response to initialize the builder with |
||||
* @return the {@link Builder} |
||||
*/ |
||||
public static Builder withResponse(OAuth2DeviceAuthorizationResponse deviceAuthorizationResponse) { |
||||
Assert.notNull(deviceAuthorizationResponse, "deviceAuthorizationResponse cannot be null"); |
||||
return new Builder(deviceAuthorizationResponse); |
||||
} |
||||
|
||||
/** |
||||
* A builder for {@link OAuth2DeviceAuthorizationResponse}. |
||||
*/ |
||||
public static final class Builder { |
||||
|
||||
private final String deviceCode; |
||||
|
||||
private final String userCode; |
||||
|
||||
private String verificationUri; |
||||
|
||||
private String verificationUriComplete; |
||||
|
||||
private long expiresIn; |
||||
|
||||
private long interval; |
||||
|
||||
private Map<String, Object> additionalParameters; |
||||
|
||||
private Builder(OAuth2DeviceAuthorizationResponse response) { |
||||
OAuth2DeviceCode deviceCode = response.getDeviceCode(); |
||||
OAuth2UserCode userCode = response.getUserCode(); |
||||
this.deviceCode = deviceCode.getTokenValue(); |
||||
this.userCode = userCode.getTokenValue(); |
||||
this.verificationUri = response.getVerificationUri(); |
||||
this.verificationUriComplete = response.getVerificationUriComplete(); |
||||
this.expiresIn = ChronoUnit.SECONDS.between(deviceCode.getIssuedAt(), deviceCode.getExpiresAt()); |
||||
this.interval = response.getInterval(); |
||||
} |
||||
|
||||
private Builder(OAuth2DeviceCode deviceCode, OAuth2UserCode userCode) { |
||||
this.deviceCode = deviceCode.getTokenValue(); |
||||
this.userCode = userCode.getTokenValue(); |
||||
this.expiresIn = ChronoUnit.SECONDS.between(deviceCode.getIssuedAt(), deviceCode.getExpiresAt()); |
||||
} |
||||
|
||||
private Builder(String deviceCode, String userCode) { |
||||
this.deviceCode = deviceCode; |
||||
this.userCode = userCode; |
||||
} |
||||
|
||||
/** |
||||
* Sets the end-user verification URI. |
||||
* @param verificationUri the end-user verification URI |
||||
* @return the {@link Builder} |
||||
*/ |
||||
public Builder verificationUri(String verificationUri) { |
||||
this.verificationUri = verificationUri; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Sets the end-user verification URI that includes the user code. |
||||
* @param verificationUriComplete the end-user verification URI that includes the |
||||
* user code |
||||
* @return the {@link Builder} |
||||
*/ |
||||
public Builder verificationUriComplete(String verificationUriComplete) { |
||||
this.verificationUriComplete = verificationUriComplete; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Sets the lifetime (in seconds) of the device code and user code. |
||||
* @param expiresIn the lifetime (in seconds) of the device code and user code |
||||
* @return the {@link Builder} |
||||
*/ |
||||
public Builder expiresIn(long expiresIn) { |
||||
this.expiresIn = expiresIn; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Sets the minimum amount of time (in seconds) that the client should wait |
||||
* between polling requests to the token endpoint. |
||||
* @param interval the minimum amount of time between polling requests |
||||
* @return the {@link Builder} |
||||
*/ |
||||
public Builder interval(long interval) { |
||||
this.interval = interval; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Sets the additional parameters returned in the response. |
||||
* @param additionalParameters the additional parameters returned in the response |
||||
* @return the {@link Builder} |
||||
*/ |
||||
public Builder additionalParameters(Map<String, Object> additionalParameters) { |
||||
this.additionalParameters = additionalParameters; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Builds a new {@link OAuth2DeviceAuthorizationResponse}. |
||||
* @return a {@link OAuth2DeviceAuthorizationResponse} |
||||
*/ |
||||
public OAuth2DeviceAuthorizationResponse build() { |
||||
Assert.hasText(this.verificationUri, "verificationUri cannot be empty"); |
||||
Assert.isTrue(this.expiresIn > 0, "expiresIn must be greater than zero"); |
||||
|
||||
Instant issuedAt = Instant.now(); |
||||
Instant expiresAt = issuedAt.plusSeconds(this.expiresIn); |
||||
OAuth2DeviceCode deviceCode = new OAuth2DeviceCode(this.deviceCode, issuedAt, expiresAt); |
||||
OAuth2UserCode userCode = new OAuth2UserCode(this.userCode, issuedAt, expiresAt); |
||||
|
||||
OAuth2DeviceAuthorizationResponse deviceAuthorizationResponse = new OAuth2DeviceAuthorizationResponse(); |
||||
deviceAuthorizationResponse.deviceCode = deviceCode; |
||||
deviceAuthorizationResponse.userCode = userCode; |
||||
deviceAuthorizationResponse.verificationUri = this.verificationUri; |
||||
deviceAuthorizationResponse.verificationUriComplete = this.verificationUriComplete; |
||||
deviceAuthorizationResponse.interval = this.interval; |
||||
deviceAuthorizationResponse.additionalParameters = Collections |
||||
.unmodifiableMap(CollectionUtils.isEmpty(this.additionalParameters) ? Collections.emptyMap() |
||||
: this.additionalParameters); |
||||
|
||||
return deviceAuthorizationResponse; |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,232 @@
@@ -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; |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,206 @@
@@ -0,0 +1,206 @@
|
||||
/* |
||||
* 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.util.HashMap; |
||||
import java.util.Map; |
||||
|
||||
import org.junit.jupiter.api.BeforeEach; |
||||
import org.junit.jupiter.api.Test; |
||||
|
||||
import org.springframework.core.convert.converter.Converter; |
||||
import org.springframework.http.HttpStatus; |
||||
import org.springframework.http.converter.HttpMessageNotReadableException; |
||||
import org.springframework.http.converter.HttpMessageNotWritableException; |
||||
import org.springframework.mock.http.MockHttpOutputMessage; |
||||
import org.springframework.mock.http.client.MockClientHttpResponse; |
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2DeviceAuthorizationResponse; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType; |
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; |
||||
import static org.assertj.core.api.Assertions.entry; |
||||
import static org.mockito.ArgumentMatchers.any; |
||||
import static org.mockito.BDDMockito.given; |
||||
import static org.mockito.Mockito.mock; |
||||
|
||||
/** |
||||
* Tests for {@link OAuth2DeviceAuthorizationResponseHttpMessageConverter}. |
||||
* |
||||
* @author Steve Riesenberg |
||||
*/ |
||||
public class OAuth2DeviceAuthorizationResponseHttpMessageConverterTest { |
||||
|
||||
private OAuth2DeviceAuthorizationResponseHttpMessageConverter messageConverter; |
||||
|
||||
@BeforeEach |
||||
public void setup() { |
||||
this.messageConverter = new OAuth2DeviceAuthorizationResponseHttpMessageConverter(); |
||||
} |
||||
|
||||
@Test |
||||
public void supportsWhenOAuth2DeviceAuthorizationResponseThenTrue() { |
||||
assertThat(this.messageConverter.supports(OAuth2DeviceAuthorizationResponse.class)).isTrue(); |
||||
} |
||||
|
||||
@Test |
||||
public void setDeviceAuthorizationResponseConverterWhenConverterIsNullThenThrowIllegalArgumentException() { |
||||
assertThatIllegalArgumentException() |
||||
.isThrownBy(() -> this.messageConverter.setDeviceAuthorizationResponseConverter(null)); |
||||
} |
||||
|
||||
@Test |
||||
public void setDeviceAuthorizationResponseParametersConverterWhenConverterIsNullThenThrowIllegalArgumentException() { |
||||
assertThatIllegalArgumentException() |
||||
.isThrownBy(() -> this.messageConverter.setDeviceAuthorizationResponseParametersConverter(null)); |
||||
} |
||||
|
||||
@Test |
||||
public void readInternalWhenSuccessfulResponseWithAllParametersThenReadOAuth2DeviceAuthorizationResponse() { |
||||
// @formatter:off
|
||||
String authorizationResponse = """ |
||||
{ |
||||
"device_code": "GmRhm_DnyEy", |
||||
"user_code": "WDJB-MJHT", |
||||
"verification_uri": "https://example.com/device", |
||||
"verification_uri_complete": "https://example.com/device?user_code=WDJB-MJHT", |
||||
"expires_in": 1800, |
||||
"interval": 5, |
||||
"custom_parameter_1": "custom-value-1", |
||||
"custom_parameter_2": "custom-value-2" |
||||
} |
||||
"""; |
||||
// @formatter:on
|
||||
MockClientHttpResponse response = new MockClientHttpResponse(authorizationResponse.getBytes(), HttpStatus.OK); |
||||
OAuth2DeviceAuthorizationResponse deviceAuthorizationResponse = this.messageConverter |
||||
.readInternal(OAuth2DeviceAuthorizationResponse.class, response); |
||||
assertThat(deviceAuthorizationResponse.getDeviceCode().getTokenValue()) |
||||
.isEqualTo("GmRhm_DnyEy"); |
||||
assertThat(deviceAuthorizationResponse.getDeviceCode().getIssuedAt()).isNotNull(); |
||||
assertThat(deviceAuthorizationResponse.getDeviceCode().getExpiresAt()) |
||||
.isBeforeOrEqualTo(Instant.now().plusSeconds(1800)); |
||||
assertThat(deviceAuthorizationResponse.getUserCode().getTokenValue()).isEqualTo("WDJB-MJHT"); |
||||
assertThat(deviceAuthorizationResponse.getUserCode().getIssuedAt()) |
||||
.isEqualTo(deviceAuthorizationResponse.getDeviceCode().getIssuedAt()); |
||||
assertThat(deviceAuthorizationResponse.getUserCode().getExpiresAt()) |
||||
.isEqualTo(deviceAuthorizationResponse.getDeviceCode().getExpiresAt()); |
||||
assertThat(deviceAuthorizationResponse.getVerificationUri()).isEqualTo("https://example.com/device"); |
||||
assertThat(deviceAuthorizationResponse.getVerificationUriComplete()) |
||||
.isEqualTo("https://example.com/device?user_code=WDJB-MJHT"); |
||||
assertThat(deviceAuthorizationResponse.getInterval()).isEqualTo(5); |
||||
assertThat(deviceAuthorizationResponse.getAdditionalParameters()).containsExactly( |
||||
entry("custom_parameter_1", "custom-value-1"), entry("custom_parameter_2", "custom-value-2")); |
||||
} |
||||
|
||||
@Test |
||||
public void readInternalWhenSuccessfulResponseWithNullValuesThenReadOAuth2DeviceAuthorizationResponse() { |
||||
// @formatter:off
|
||||
String authorizationResponse = """ |
||||
{ |
||||
"device_code": "GmRhm_DnyEy", |
||||
"user_code": "WDJB-MJHT", |
||||
"verification_uri": "https://example.com/device", |
||||
"verification_uri_complete": null, |
||||
"expires_in": 1800, |
||||
"interval": null |
||||
} |
||||
"""; |
||||
// @formatter:on
|
||||
MockClientHttpResponse response = new MockClientHttpResponse(authorizationResponse.getBytes(), HttpStatus.OK); |
||||
OAuth2DeviceAuthorizationResponse deviceAuthorizationResponse = this.messageConverter |
||||
.readInternal(OAuth2DeviceAuthorizationResponse.class, response); |
||||
assertThat(deviceAuthorizationResponse.getDeviceCode().getTokenValue()) |
||||
.isEqualTo("GmRhm_DnyEy"); |
||||
assertThat(deviceAuthorizationResponse.getDeviceCode().getIssuedAt()).isNotNull(); |
||||
assertThat(deviceAuthorizationResponse.getDeviceCode().getExpiresAt()) |
||||
.isBeforeOrEqualTo(Instant.now().plusSeconds(1800)); |
||||
assertThat(deviceAuthorizationResponse.getUserCode().getTokenValue()).isEqualTo("WDJB-MJHT"); |
||||
assertThat(deviceAuthorizationResponse.getUserCode().getIssuedAt()) |
||||
.isEqualTo(deviceAuthorizationResponse.getDeviceCode().getIssuedAt()); |
||||
assertThat(deviceAuthorizationResponse.getUserCode().getExpiresAt()) |
||||
.isEqualTo(deviceAuthorizationResponse.getDeviceCode().getExpiresAt()); |
||||
assertThat(deviceAuthorizationResponse.getVerificationUri()).isEqualTo("https://example.com/device"); |
||||
assertThat(deviceAuthorizationResponse.getVerificationUriComplete()).isNull(); |
||||
assertThat(deviceAuthorizationResponse.getInterval()).isEqualTo(0); |
||||
} |
||||
|
||||
@Test |
||||
@SuppressWarnings("unchecked") |
||||
public void readInternalWhenConversionFailsThenThrowHttpMessageNotReadableException() { |
||||
Converter<Map<String, Object>, OAuth2DeviceAuthorizationResponse> deviceAuthorizationResponseConverter = mock( |
||||
Converter.class); |
||||
given(deviceAuthorizationResponseConverter.convert(any())).willThrow(RuntimeException.class); |
||||
this.messageConverter.setDeviceAuthorizationResponseConverter(deviceAuthorizationResponseConverter); |
||||
String authorizationResponse = "{}"; |
||||
MockClientHttpResponse response = new MockClientHttpResponse(authorizationResponse.getBytes(), HttpStatus.OK); |
||||
assertThatExceptionOfType(HttpMessageNotReadableException.class) |
||||
.isThrownBy(() -> this.messageConverter.readInternal(OAuth2DeviceAuthorizationResponse.class, response)) |
||||
.withMessageContaining("An error occurred reading the OAuth 2.0 Device Authorization Response"); |
||||
} |
||||
|
||||
@Test |
||||
public void writeInternalWhenOAuth2DeviceAuthorizationResponseThenWriteResponse() { |
||||
Map<String, Object> additionalParameters = new HashMap<>(); |
||||
additionalParameters.put("custom_parameter_1", "custom-value-1"); |
||||
additionalParameters.put("custom_parameter_2", "custom-value-2"); |
||||
// @formatter:off
|
||||
OAuth2DeviceAuthorizationResponse deviceAuthorizationResponse = |
||||
OAuth2DeviceAuthorizationResponse.with("GmRhm_DnyEy", "WDJB-MJHT") |
||||
.verificationUri("https://example.com/device") |
||||
.verificationUriComplete("https://example.com/device?user_code=WDJB-MJHT") |
||||
.expiresIn(1800) |
||||
.interval(5) |
||||
.additionalParameters(additionalParameters) |
||||
.build(); |
||||
// @formatter:on
|
||||
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); |
||||
this.messageConverter.writeInternal(deviceAuthorizationResponse, outputMessage); |
||||
String authorizationResponse = outputMessage.getBodyAsString(); |
||||
assertThat(authorizationResponse).contains("\"device_code\":\"GmRhm_DnyEy\""); |
||||
assertThat(authorizationResponse).contains("\"user_code\":\"WDJB-MJHT\""); |
||||
assertThat(authorizationResponse).contains("\"verification_uri\":\"https://example.com/device\""); |
||||
assertThat(authorizationResponse) |
||||
.contains("\"verification_uri_complete\":\"https://example.com/device?user_code=WDJB-MJHT\""); |
||||
assertThat(authorizationResponse).contains("\"expires_in\":"); |
||||
assertThat(authorizationResponse).contains("\"interval\":5"); |
||||
assertThat(authorizationResponse).contains("\"custom_parameter_1\":\"custom-value-1\""); |
||||
assertThat(authorizationResponse).contains("\"custom_parameter_2\":\"custom-value-2\""); |
||||
} |
||||
|
||||
@Test |
||||
@SuppressWarnings("unchecked") |
||||
public void writeInternalWhenConversionFailsThenThrowHttpMessageNotWritableException() { |
||||
Converter<OAuth2DeviceAuthorizationResponse, Map<String, Object>> deviceAuthorizationResponseParametersConverter = mock( |
||||
Converter.class); |
||||
given(deviceAuthorizationResponseParametersConverter.convert(any())).willThrow(RuntimeException.class); |
||||
this.messageConverter |
||||
.setDeviceAuthorizationResponseParametersConverter(deviceAuthorizationResponseParametersConverter); |
||||
// @formatter:off
|
||||
OAuth2DeviceAuthorizationResponse deviceAuthorizationResponse = |
||||
OAuth2DeviceAuthorizationResponse.with("GmRhm_DnyEy", "WDJB-MJHT") |
||||
.verificationUri("https://example.com/device") |
||||
.expiresIn(1800) |
||||
.build(); |
||||
// @formatter:on
|
||||
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); |
||||
assertThatExceptionOfType(HttpMessageNotWritableException.class) |
||||
.isThrownBy(() -> this.messageConverter.writeInternal(deviceAuthorizationResponse, outputMessage)) |
||||
.withMessageContaining("An error occurred writing the OAuth 2.0 Device Authorization Response"); |
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue