Device verification flow always requires consent

Force consent for device verification flow when there are no client scopes to approve by adding a default client scope to approve

Closes #26100

Signed-off-by: graziang <g.graziano94@gmail.com>
This commit is contained in:
graziang 2024-03-04 09:47:03 +01:00 committed by Marek Posolda
parent 662ab9811b
commit 4fa940a31e
2 changed files with 47 additions and 4 deletions

View file

@ -1058,7 +1058,7 @@ public class AuthenticationManager {
UserConsentModel grantedConsent = getEffectiveGrantedConsent(session, authSession);
// See if any clientScopes need to be approved on consent screen
List<AuthorizationDetails> clientScopesToApprove = getClientScopesToApproveOnConsentScreen(grantedConsent, session);
List<AuthorizationDetails> clientScopesToApprove = getClientScopesToApproveOnConsentScreen(grantedConsent, session, authSession);
if (!clientScopesToApprove.isEmpty()) {
return CommonClientSessionModel.Action.OAUTH_GRANT.name();
}
@ -1122,7 +1122,7 @@ public class AuthenticationManager {
UserConsentModel grantedConsent = getEffectiveGrantedConsent(session, authSession);
List<AuthorizationDetails> clientScopesToApprove = getClientScopesToApproveOnConsentScreen(grantedConsent, session);
List<AuthorizationDetails> clientScopesToApprove = getClientScopesToApproveOnConsentScreen(grantedConsent, session, authSession);
// Skip grant screen if everything was already approved by this user
if (clientScopesToApprove.size() > 0) {
@ -1149,7 +1149,7 @@ public class AuthenticationManager {
}
private static List<AuthorizationDetails> getClientScopesToApproveOnConsentScreen(UserConsentModel grantedConsent, KeycloakSession session) {
private static List<AuthorizationDetails> getClientScopesToApproveOnConsentScreen(UserConsentModel grantedConsent, KeycloakSession session, AuthenticationSessionModel authSession) {
// Client Scopes to be displayed on consent screen
List<AuthorizationDetails> clientScopesToDisplay = new LinkedList<>();
@ -1165,6 +1165,10 @@ public class AuthenticationManager {
clientScopesToDisplay.add(authDetails);
}
}
//force consent when running a verification flow of OAuth 2.0 Device Authorization Grant
if(clientScopesToDisplay.isEmpty() && isOAuth2DeviceVerificationFlow(authSession)) {
clientScopesToDisplay.add(new AuthorizationDetails(authSession.getClient()));
}
return clientScopesToDisplay;
}

View file

@ -30,6 +30,7 @@ import org.junit.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.OAuthErrorException;
import org.keycloak.admin.client.resource.ClientResource;
import org.keycloak.admin.client.resource.ClientsResource;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.common.Profile;
import org.keycloak.events.Errors;
@ -42,6 +43,7 @@ import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentatio
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.UserInfo;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.ClientScopeRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.AbstractKeycloakTest;
@ -83,6 +85,7 @@ public class OAuth2DeviceAuthorizationGrantTest extends AbstractKeycloakTest {
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 DEVICE_APP_WITHOUT_SCOPES = "test-device-without-scopes";
private static final String SHORT_DEVICE_FLOW_URL = "https://keycloak.org/device";
@Rule
@ -105,7 +108,7 @@ public class OAuth2DeviceAuthorizationGrantTest extends AbstractKeycloakTest {
ClientRepresentation app = ClientBuilder.create()
.id(KeycloakModelUtils.generateId())
.clientId("test-device")
.clientId(DEVICE_APP)
.secret("secret")
.attribute(OAuth2DeviceConfig.OAUTH2_DEVICE_AUTHORIZATION_GRANT_ENABLED, "true")
.build();
@ -125,6 +128,13 @@ public class OAuth2DeviceAuthorizationGrantTest extends AbstractKeycloakTest {
.build();
realm.client(appPublicCustomConsent);
ClientRepresentation appWithoutScopes = ClientBuilder.create().publicClient()
.id(KeycloakModelUtils.generateId())
.clientId(DEVICE_APP_WITHOUT_SCOPES)
.attribute(OAuth2DeviceConfig.OAUTH2_DEVICE_AUTHORIZATION_GRANT_ENABLED, "true")
.build();
realm.client(appWithoutScopes);
userId = KeycloakModelUtils.generateId();
UserRepresentation user = UserBuilder.create()
.id(userId)
@ -478,6 +488,35 @@ public class OAuth2DeviceAuthorizationGrantTest extends AbstractKeycloakTest {
assertNotNull(token);
}
@Test
public void testPublicClientConsentWithoutScopes() throws Exception {
ClientsResource clients = realmsResouce().realm(REALM_NAME).clients();
ClientRepresentation clientRep = clients.findByClientId(DEVICE_APP_WITHOUT_SCOPES).get(0);
ClientResource client = clients.get(clientRep.getId());
List<ClientScopeRepresentation> defaultClientScopes = client.getDefaultClientScopes();
defaultClientScopes.forEach(scope -> client.removeDefaultClientScope(scope.getId()));
List<ClientScopeRepresentation> optionalClientScopes = client.getOptionalClientScopes();
optionalClientScopes.forEach(scope -> client.removeOptionalClientScope(scope.getId()));
// Device Authorization Request from device
oauth.realm(REALM_NAME);
oauth.clientId(DEVICE_APP_WITHOUT_SCOPES);
OAuthClient.DeviceAuthorizationResponse response = oauth.doDeviceAuthorizationRequest(DEVICE_APP_WITHOUT_SCOPES, null);
Assert.assertEquals(200, response.getStatusCode());
assertNotNull(response.getVerificationUriComplete());
openVerificationPage(response.getVerificationUriComplete());
loginPage.assertCurrent();
// Do Login
oauth.fillLoginForm("device-login", "password");
// Consent
grantPage.assertCurrent();
}
@Test
public void testNoRefreshToken() throws Exception {
ClientResource client = ApiUtil.findClientByClientId(adminClient.realm(REALM_NAME), DEVICE_APP);