diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AuthorizationGrantType.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AuthorizationGrantType.java
index 29501ae5141..49e75113568 100644
--- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AuthorizationGrantType.java
+++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AuthorizationGrantType.java
@@ -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.
@@ -62,6 +62,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;
/**
diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2DeviceCode.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2DeviceCode.java
new file mode 100644
index 00000000000..95ffa0f847a
--- /dev/null
+++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2DeviceCode.java
@@ -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 Section
+ * 3.2 Device Authorization Response
+ */
+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);
+ }
+
+}
diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2UserCode.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2UserCode.java
new file mode 100644
index 00000000000..b0ee1776221
--- /dev/null
+++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2UserCode.java
@@ -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 Section
+ * 3.2 Device Authorization Response
+ */
+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);
+ }
+
+}
diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2DeviceAuthorizationResponse.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2DeviceAuthorizationResponse.java
new file mode 100644
index 00000000000..c4cedd59afe
--- /dev/null
+++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2DeviceAuthorizationResponse.java
@@ -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 Section
+ * 3.2 Device Authorization Response
+ */
+public final class OAuth2DeviceAuthorizationResponse {
+
+ private OAuth2DeviceCode deviceCode;
+
+ private OAuth2UserCode userCode;
+
+ private String verificationUri;
+
+ private String verificationUriComplete;
+
+ private long interval;
+
+ private Map 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 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 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 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;
+ }
+
+ }
+
+}
diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2ParameterNames.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2ParameterNames.java
index ac1bb11e65a..9d0c653d5ef 100644
--- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2ParameterNames.java
+++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2ParameterNames.java
@@ -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 @@
* endpoint.
*
* @author Joe Grandja
+ * @author Steve Riesenberg
* @since 5.0
* @see 11.2
* OAuth Parameters Registry
@@ -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() {
}
diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/http/converter/OAuth2DeviceAuthorizationResponseHttpMessageConverter.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/http/converter/OAuth2DeviceAuthorizationResponseHttpMessageConverter.java
new file mode 100644
index 00000000000..c1e67805a81
--- /dev/null
+++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/http/converter/OAuth2DeviceAuthorizationResponseHttpMessageConverter.java
@@ -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 {
+
+ private static final ParameterizedTypeReference