diff --git a/services/src/main/java/org/keycloak/protocol/oidc/grants/device/endpoints/DeviceEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/grants/device/endpoints/DeviceEndpoint.java
index 0f9074fb6010..900f16662328 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/grants/device/endpoints/DeviceEndpoint.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/grants/device/endpoints/DeviceEndpoint.java
@@ -80,6 +80,8 @@ public class DeviceEndpoint extends AuthorizationEndpointBase implements RealmRe
protected static final Logger logger = Logger.getLogger(DeviceEndpoint.class);
+ public static final String SHORT_VERIFICATION_URI = "shortVerificationUri";
+
private final HttpRequest request;
private Cors cors;
@@ -168,7 +170,7 @@ public Response handleDeviceRequest() {
singleUseStore.put(userCode.serializeKey(), lifespanSeconds, userCode.serializeValue());
try {
- String deviceUrl = DeviceGrantType.oauth2DeviceVerificationUrl(session.getContext().getUri()).build(realm.getName())
+ String deviceUrl = realm.getAttribute(SHORT_VERIFICATION_URI) != null ? realm.getAttribute(SHORT_VERIFICATION_URI) : DeviceGrantType.oauth2DeviceVerificationUrl(session.getContext().getUri()).build(realm.getName())
.toString();
OAuth2DeviceAuthorizationResponse response = new OAuth2DeviceAuthorizationResponse();
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuth2DeviceAuthorizationGrantTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuth2DeviceAuthorizationGrantTest.java
index 15bce68cc204..0b1c6e959907 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuth2DeviceAuthorizationGrantTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuth2DeviceAuthorizationGrantTest.java
@@ -33,6 +33,7 @@
import org.keycloak.models.OAuth2DeviceConfig;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
+import org.keycloak.protocol.oidc.grants.device.endpoints.DeviceEndpoint;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.UserInfo;
import org.keycloak.representations.idm.ClientRepresentation;
@@ -71,10 +72,11 @@ public class OAuth2DeviceAuthorizationGrantTest extends AbstractKeycloakTest {
private static String userId;
- public static final String REALM_NAME = "test";
- public static final String DEVICE_APP = "test-device";
- public static final String DEVICE_APP_PUBLIC = "test-device-public";
- public static final String DEVICE_APP_PUBLIC_CUSTOM_CONSENT = "test-device-public-custom-consent";
+ private static final String REALM_NAME = "test";
+ private static final String DEVICE_APP = "test-device";
+ private static final String DEVICE_APP_PUBLIC = "test-device-public";
+ private static final String DEVICE_APP_PUBLIC_CUSTOM_CONSENT = "test-device-public-custom-consent";
+ private static final String SHORT_DEVICE_FLOW_URL = "https://keycloak.org/device";
@Rule
public AssertEvents events = new AssertEvents(this);
@@ -217,6 +219,32 @@ public void testPublicClient() throws Exception {
assertNotNull(token);
}
+
+ @Test
+ public void testCustomVerificationUri() throws Exception {
+ // Device Authorization Request from device
+ try {
+ RealmResource testRealm = adminClient.realm(REALM_NAME);
+ RealmRepresentation realmRep = testRealm.toRepresentation();
+ realmRep.getAttributes().put(DeviceEndpoint.SHORT_VERIFICATION_URI, SHORT_DEVICE_FLOW_URL);
+ testRealm.update(realmRep);
+ oauth.realm(REALM_NAME);
+ oauth.clientId(DEVICE_APP_PUBLIC);
+ OAuthClient.DeviceAuthorizationResponse response = oauth.doDeviceAuthorizationRequest(DEVICE_APP_PUBLIC, null);
+
+ Assert.assertEquals(200, response.getStatusCode());
+ assertNotNull(response.getDeviceCode());
+ assertNotNull(response.getUserCode());
+ Assert.assertEquals(SHORT_DEVICE_FLOW_URL,response.getVerificationUri());
+ Assert.assertEquals(SHORT_DEVICE_FLOW_URL + "?user_code=" + response.getUserCode(),response.getVerificationUriComplete());
+ } finally {
+ RealmResource testRealm = adminClient.realm(REALM_NAME);
+ RealmRepresentation realmRep = testRealm.toRepresentation();
+ realmRep.getAttributes().remove("shortVerificationUri");
+ testRealm.update(realmRep);
+ }
+ }
+
@Test
public void testPublicClientOptionalScope() throws Exception {
// Device Authorization Request from device - check giving optional scope phone
diff --git a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
index 848bae55a25a..cd6945b2f427 100644
--- a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
+++ b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
@@ -474,6 +474,8 @@ policy-uri=Policy URL
policy-uri.tooltip=URL that the Relying Party Client provides to the End-User to read about the how the profile data will be used
tos-uri=Terms of service URL
tos-uri.tooltip=URL that the Relying Party Client provides to the End-User to read about the Relying Party's terms of service
+short-verification-uri= Short verification_uri in Device Authorization flow
+short-verification-uri.tooltip= If set, this value will be return as verification_uri in Device Authorization flow. This uri need to redirect to {server-root}/realms/{realm}/device
# client import
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/realm-tokens.html b/themes/src/main/resources/theme/base/admin/resources/partials/realm-tokens.html
index 65232d2de568..c5243a0058db 100755
--- a/themes/src/main/resources/theme/base/admin/resources/partials/realm-tokens.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/realm-tokens.html
@@ -387,6 +387,14 @@