Add support for device authorization response
Closes gh-12852
This commit is contained in:
parent
ac1d269e73
commit
8c17b978c8
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* 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.
|
||||
|
@ -32,6 +32,7 @@ import org.springframework.util.Assert;
|
|||
* extensibility mechanism for defining additional grant types.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @author Steve Riesenberg
|
||||
* @since 5.0
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-1.3">Section
|
||||
* 1.3 Authorization Grant</a>
|
||||
|
@ -62,6 +63,12 @@ public final class AuthorizationGrantType implements Serializable {
|
|||
public static final AuthorizationGrantType JWT_BEARER = new AuthorizationGrantType(
|
||||
"urn:ietf:params:oauth:grant-type:jwt-bearer");
|
||||
|
||||
/**
|
||||
* @since 6.1
|
||||
*/
|
||||
public static final AuthorizationGrantType DEVICE_CODE = new AuthorizationGrantType(
|
||||
"urn:ietf:params:oauth:grant-type:device_code");
|
||||
|
||||
private final String value;
|
||||
|
||||
/**
|
||||
|
|
|
@ -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 @@
|
|||
/*
|
||||
* 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 @@
|
|||
/*
|
||||
* 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* 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.
|
||||
|
@ -22,6 +22,7 @@ package org.springframework.security.oauth2.core.endpoint;
|
|||
* endpoint.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @author Steve Riesenberg
|
||||
* @since 5.0
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-11.2">11.2
|
||||
* OAuth Parameters Registry</a>
|
||||
|
@ -150,6 +151,38 @@ public final class OAuth2ParameterNames {
|
|||
*/
|
||||
public static final String TOKEN_TYPE_HINT = "token_type_hint";
|
||||
|
||||
/**
|
||||
* {@code device_code} - used in Device Authorization Request and Device Authorization
|
||||
* Response.
|
||||
* @since 6.1
|
||||
*/
|
||||
public static final String DEVICE_CODE = "device_code";
|
||||
|
||||
/**
|
||||
* {@code user_code} - used in Device Authorization Request and Device Authorization
|
||||
* Response.
|
||||
* @since 6.1
|
||||
*/
|
||||
public static final String USER_CODE = "user_code";
|
||||
|
||||
/**
|
||||
* {@code verification_uri} - Used in Device Authorization Response.
|
||||
* @since 6.1
|
||||
*/
|
||||
public static final String VERIFICATION_URI = "verification_uri";
|
||||
|
||||
/**
|
||||
* {@code verification_uri_complete} - Used in Device Authorization Response.
|
||||
* @since 6.1
|
||||
*/
|
||||
public static final String VERIFICATION_URI_COMPLETE = "verification_uri_complete";
|
||||
|
||||
/**
|
||||
* {@code interval} - Used in Device Authorization Response.
|
||||
* @since 6.1
|
||||
*/
|
||||
public static final String INTERVAL = "interval";
|
||||
|
||||
private OAuth2ParameterNames() {
|
||||
}
|
||||
|
||||
|
|
|
@ -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 @@
|
|||
/*
|
||||
* 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