KEYCLOAK-7675 Support for Device Authorization Grant
This commit is contained in:
parent
f58bf0deeb
commit
298ab0bc3e
41 changed files with 1498 additions and 1007 deletions
|
@ -142,6 +142,9 @@ public class OIDCConfigurationRepresentation {
|
|||
@JsonProperty("backchannel_logout_session_supported")
|
||||
private Boolean backchannelLogoutSessionSupported;
|
||||
|
||||
@JsonProperty("device_authorization_endpoint")
|
||||
private String deviceAuthorizationEndpoint;
|
||||
|
||||
protected Map<String, Object> otherClaims = new HashMap<String, Object>();
|
||||
|
||||
public String getIssuer() {
|
||||
|
@ -445,4 +448,12 @@ public class OIDCConfigurationRepresentation {
|
|||
public void setOtherClaims(String name, Object value) {
|
||||
otherClaims.put(name, value);
|
||||
}
|
||||
|
||||
public void setDeviceAuthorizationEndpoint(String deviceAuthorizationEndpoint) {
|
||||
this.deviceAuthorizationEndpoint = deviceAuthorizationEndpoint;
|
||||
}
|
||||
|
||||
public String getDeviceAuthorizationEndpoint() {
|
||||
return deviceAuthorizationEndpoint;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -268,14 +268,6 @@ public class ClientRepresentation {
|
|||
this.serviceAccountsEnabled = serviceAccountsEnabled;
|
||||
}
|
||||
|
||||
public Boolean isOAuth2DeviceAuthorizationGrantEnabled() {
|
||||
return oauth2DeviceAuthorizationGrantEnabled;
|
||||
}
|
||||
|
||||
public void setOAuth2DeviceAuthorizationGrantEnabled(Boolean oauth2DeviceAuthorizationGrantEnabled) {
|
||||
this.oauth2DeviceAuthorizationGrantEnabled = oauth2DeviceAuthorizationGrantEnabled;
|
||||
}
|
||||
|
||||
public Boolean getAuthorizationServicesEnabled() {
|
||||
if (authorizationSettings != null) {
|
||||
return true;
|
||||
|
|
|
@ -29,6 +29,7 @@ import org.keycloak.storage.client.ClientStorageProvider;
|
|||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
|
@ -40,18 +41,20 @@ public class RealmAdapter implements CachedRealmModel {
|
|||
protected RealmCacheSession cacheSession;
|
||||
protected volatile RealmModel updated;
|
||||
protected KeycloakSession session;
|
||||
private final Supplier<RealmModel> modelSupplier;
|
||||
|
||||
public RealmAdapter(KeycloakSession session, CachedRealm cached, RealmCacheSession cacheSession) {
|
||||
this.cached = cached;
|
||||
this.cacheSession = cacheSession;
|
||||
this.session = session;
|
||||
this.modelSupplier = this::getRealm;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RealmModel getDelegateForUpdate() {
|
||||
if (updated == null) {
|
||||
cacheSession.registerRealmInvalidation(cached.getId(), cached.getName());
|
||||
updated = cacheSession.getRealmDelegate().getRealm(cached.getId());
|
||||
updated = modelSupplier.get();
|
||||
if (updated == null) throw new IllegalStateException("Not found in database");
|
||||
}
|
||||
return updated;
|
||||
|
@ -643,27 +646,11 @@ public class RealmAdapter implements CachedRealmModel {
|
|||
return cached.getRequiredCredentials().stream();
|
||||
}
|
||||
|
||||
public int getOAuth2DeviceCodeLifespan() {
|
||||
if (isUpdated()) return updated.getOAuth2DeviceCodeLifespan();
|
||||
return cached.getOAuth2DeviceCodeLifespan();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOAuth2DeviceCodeLifespan(int oauth2DeviceCodeLifespan) {
|
||||
getDelegateForUpdate();
|
||||
updated.setOAuth2DeviceCodeLifespan(oauth2DeviceCodeLifespan);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOAuth2DevicePollingInterval() {
|
||||
if (isUpdated()) return updated.getOAuth2DevicePollingInterval();
|
||||
return cached.getOAuth2DevicePollingInterval();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOAuth2DevicePollingInterval(int oauth2DevicePollingInterval) {
|
||||
getDelegateForUpdate();
|
||||
updated.setOAuth2DevicePollingInterval(oauth2DevicePollingInterval);
|
||||
public OAuth2DeviceConfig getOAuth2DeviceConfig() {
|
||||
if (isUpdated())
|
||||
return updated.getOAuth2DeviceConfig();
|
||||
return cached.getOAuth2DeviceConfig(modelSupplier);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -1721,6 +1708,10 @@ public class RealmAdapter implements CachedRealmModel {
|
|||
return Collections.unmodifiableMap(localizationTexts);
|
||||
}
|
||||
|
||||
private RealmModel getRealm() {
|
||||
return cacheSession.getRealmDelegate().getRealm(cached.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("%s@%08x", getId(), hashCode());
|
||||
|
|
|
@ -28,12 +28,15 @@ import org.keycloak.models.ClientScopeModel;
|
|||
import org.keycloak.models.GroupModel;
|
||||
import org.keycloak.models.IdentityProviderMapperModel;
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
import org.keycloak.models.OAuth2DeviceConfig;
|
||||
import org.keycloak.models.OTPPolicy;
|
||||
import org.keycloak.models.PasswordPolicy;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.RequiredActionProviderModel;
|
||||
import org.keycloak.models.RequiredCredentialModel;
|
||||
import org.keycloak.models.WebAuthnPolicy;
|
||||
import org.keycloak.models.cache.infinispan.DefaultLazyLoader;
|
||||
import org.keycloak.models.cache.infinispan.LazyLoader;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
|
@ -43,6 +46,7 @@ import java.util.List;
|
|||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
|
@ -96,8 +100,7 @@ public class CachedRealm extends AbstractExtendableRevisioned {
|
|||
protected int accessCodeLifespan;
|
||||
protected int accessCodeLifespanUserAction;
|
||||
protected int accessCodeLifespanLogin;
|
||||
protected int oauth2DeviceCodeLifespan;
|
||||
protected int oauth2DevicePollingInterval;
|
||||
protected LazyLoader<RealmModel, OAuth2DeviceConfig> deviceConfig;
|
||||
protected int actionTokenGeneratedByAdminLifespan;
|
||||
protected int actionTokenGeneratedByUserLifespan;
|
||||
protected int notBefore;
|
||||
|
@ -213,8 +216,7 @@ public class CachedRealm extends AbstractExtendableRevisioned {
|
|||
accessTokenLifespan = model.getAccessTokenLifespan();
|
||||
accessTokenLifespanForImplicitFlow = model.getAccessTokenLifespanForImplicitFlow();
|
||||
accessCodeLifespan = model.getAccessCodeLifespan();
|
||||
oauth2DeviceCodeLifespan = model.getOAuth2DeviceCodeLifespan();
|
||||
oauth2DevicePollingInterval = model.getOAuth2DevicePollingInterval();
|
||||
deviceConfig = new DefaultLazyLoader<>(OAuth2DeviceConfig::new, null);
|
||||
accessCodeLifespanUserAction = model.getAccessCodeLifespanUserAction();
|
||||
accessCodeLifespanLogin = model.getAccessCodeLifespanLogin();
|
||||
actionTokenGeneratedByAdminLifespan = model.getActionTokenGeneratedByAdminLifespan();
|
||||
|
@ -491,12 +493,8 @@ public class CachedRealm extends AbstractExtendableRevisioned {
|
|||
return accessCodeLifespanLogin;
|
||||
}
|
||||
|
||||
public int getOAuth2DeviceCodeLifespan() {
|
||||
return oauth2DeviceCodeLifespan;
|
||||
}
|
||||
|
||||
public int getOAuth2DevicePollingInterval() {
|
||||
return oauth2DevicePollingInterval;
|
||||
public OAuth2DeviceConfig getOAuth2DeviceConfig(Supplier<RealmModel> modelSupplier) {
|
||||
return deviceConfig.get(modelSupplier);
|
||||
}
|
||||
|
||||
public int getActionTokenGeneratedByAdminLifespan() {
|
||||
|
|
|
@ -49,7 +49,7 @@ public class InfinispanOAuth2DeviceTokenStoreProvider implements OAuth2DeviceTok
|
|||
public OAuth2DeviceCodeModel getByDeviceCode(RealmModel realm, String deviceCode) {
|
||||
try {
|
||||
BasicCache<String, ActionTokenValueEntity> cache = codeCache.get();
|
||||
ActionTokenValueEntity existing = cache.get(OAuth2DeviceCodeModel.createKey(realm, deviceCode));
|
||||
ActionTokenValueEntity existing = cache.get(OAuth2DeviceCodeModel.createKey(deviceCode));
|
||||
|
||||
if (existing == null) {
|
||||
return null;
|
||||
|
@ -74,7 +74,7 @@ public class InfinispanOAuth2DeviceTokenStoreProvider implements OAuth2DeviceTok
|
|||
|
||||
@Override
|
||||
public void put(OAuth2DeviceCodeModel deviceCode, OAuth2DeviceUserCodeModel userCode, int lifespanSeconds) {
|
||||
ActionTokenValueEntity deviceCodeValue = new ActionTokenValueEntity(deviceCode.serializeValue());
|
||||
ActionTokenValueEntity deviceCodeValue = new ActionTokenValueEntity(deviceCode.toMap());
|
||||
ActionTokenValueEntity userCodeValue = new ActionTokenValueEntity(userCode.serializeValue());
|
||||
|
||||
try {
|
||||
|
@ -142,7 +142,7 @@ public class InfinispanOAuth2DeviceTokenStoreProvider implements OAuth2DeviceTok
|
|||
OAuth2DeviceUserCodeModel data = OAuth2DeviceUserCodeModel.fromCache(realm, userCode, existing.getNotes());
|
||||
String deviceCode = data.getDeviceCode();
|
||||
|
||||
String deviceCodeKey = OAuth2DeviceCodeModel.createKey(realm, deviceCode);
|
||||
String deviceCodeKey = OAuth2DeviceCodeModel.createKey(deviceCode);
|
||||
ActionTokenValueEntity existingDeviceCode = cache.get(deviceCodeKey);
|
||||
|
||||
if (existingDeviceCode == null) {
|
||||
|
@ -164,7 +164,7 @@ public class InfinispanOAuth2DeviceTokenStoreProvider implements OAuth2DeviceTok
|
|||
|
||||
// Update the device code with approved status
|
||||
BasicCache<String, ActionTokenValueEntity> cache = codeCache.get();
|
||||
cache.replace(approved.serializeKey(), new ActionTokenValueEntity(approved.serializeApprovedValue()));
|
||||
cache.replace(approved.serializeKey(), new ActionTokenValueEntity(approved.toMap()));
|
||||
|
||||
return true;
|
||||
} catch (HotRodClientException re) {
|
||||
|
@ -189,7 +189,7 @@ public class InfinispanOAuth2DeviceTokenStoreProvider implements OAuth2DeviceTok
|
|||
OAuth2DeviceCodeModel denied = deviceCode.deny();
|
||||
|
||||
BasicCache<String, ActionTokenValueEntity> cache = codeCache.get();
|
||||
cache.replace(denied.serializeKey(), new ActionTokenValueEntity(denied.serializeDeniedValue()));
|
||||
cache.replace(denied.serializeKey(), new ActionTokenValueEntity(denied.toMap()));
|
||||
|
||||
return true;
|
||||
} catch (HotRodClientException re) {
|
||||
|
@ -207,7 +207,7 @@ public class InfinispanOAuth2DeviceTokenStoreProvider implements OAuth2DeviceTok
|
|||
public boolean removeDeviceCode(RealmModel realm, String deviceCode) {
|
||||
try {
|
||||
BasicCache<String, ActionTokenValueEntity> cache = codeCache.get();
|
||||
String key = OAuth2DeviceCodeModel.createKey(realm, deviceCode);
|
||||
String key = OAuth2DeviceCodeModel.createKey(deviceCode);
|
||||
ActionTokenValueEntity existing = cache.remove(key);
|
||||
return existing == null ? false : true;
|
||||
} catch (HotRodClientException re) {
|
||||
|
|
|
@ -17,18 +17,14 @@
|
|||
|
||||
package org.keycloak.models.sessions.infinispan;
|
||||
|
||||
import org.infinispan.Cache;
|
||||
import org.infinispan.client.hotrod.Flag;
|
||||
import org.infinispan.client.hotrod.RemoteCache;
|
||||
import org.infinispan.commons.api.BasicCache;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
|
||||
import org.keycloak.models.*;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.models.OAuth2DeviceTokenStoreProvider;
|
||||
import org.keycloak.models.OAuth2DeviceTokenStoreProviderFactory;
|
||||
import org.keycloak.models.sessions.infinispan.entities.ActionTokenValueEntity;
|
||||
import org.keycloak.models.sessions.infinispan.util.InfinispanUtil;
|
||||
|
||||
import java.util.UUID;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
|
@ -36,8 +32,6 @@ import java.util.function.Supplier;
|
|||
*/
|
||||
public class InfinispanOAuth2DeviceTokenStoreProviderFactory implements OAuth2DeviceTokenStoreProviderFactory {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(InfinispanOAuth2DeviceTokenStoreProviderFactory.class);
|
||||
|
||||
// Reuse "actionTokens" infinispan cache for now
|
||||
private volatile Supplier<BasicCache<String, ActionTokenValueEntity>> codeCache;
|
||||
|
||||
|
@ -50,25 +44,7 @@ public class InfinispanOAuth2DeviceTokenStoreProviderFactory implements OAuth2De
|
|||
private void lazyInit(KeycloakSession session) {
|
||||
if (codeCache == null) {
|
||||
synchronized (this) {
|
||||
if (codeCache == null) {
|
||||
InfinispanConnectionProvider connections = session.getProvider(InfinispanConnectionProvider.class);
|
||||
Cache cache = connections.getCache(InfinispanConnectionProvider.ACTION_TOKEN_CACHE);
|
||||
|
||||
RemoteCache remoteCache = InfinispanUtil.getRemoteCache(cache);
|
||||
|
||||
if (remoteCache != null) {
|
||||
LOG.debugf("Having remote stores. Using remote cache '%s' for token of OAuth 2.0 Device Authorization Grant", remoteCache.getName());
|
||||
this.codeCache = () -> {
|
||||
// Doing this way as flag is per invocation
|
||||
return remoteCache.withFlags(Flag.FORCE_RETURN_VALUE);
|
||||
};
|
||||
} else {
|
||||
LOG.debugf("Not having remote stores. Using normal cache '%s' for token of OAuth 2.0 Device Authorization Grant", cache.getName());
|
||||
this.codeCache = () -> {
|
||||
return cache;
|
||||
};
|
||||
}
|
||||
}
|
||||
codeCache = InfinispanSingleUseTokenStoreProviderFactory.getActionTokenCache(session);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -588,23 +588,8 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
|
|||
}
|
||||
|
||||
@Override
|
||||
public int getOAuth2DeviceCodeLifespan() {
|
||||
return getAttribute(RealmAttributes.OAUTH2_DEVICE_CODE_LIFESPAN, Constants.DEFAULT_OAUTH2_DEVICE_CODE_LIFESPAN);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOAuth2DeviceCodeLifespan(int seconds) {
|
||||
setAttribute(RealmAttributes.OAUTH2_DEVICE_CODE_LIFESPAN, seconds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOAuth2DevicePollingInterval() {
|
||||
return getAttribute(RealmAttributes.OAUTH2_DEVICE_POLLING_INTERVAL, Constants.DEFAULT_OAUTH2_DEVICE_POLLING_INTERVAL);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOAuth2DevicePollingInterval(int seconds) {
|
||||
setAttribute(RealmAttributes.OAUTH2_DEVICE_POLLING_INTERVAL, seconds);
|
||||
public OAuth2DeviceConfig getOAuth2DeviceConfig() {
|
||||
return new OAuth2DeviceConfig(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -35,15 +35,10 @@ public interface RealmAttributes {
|
|||
|
||||
String OFFLINE_SESSION_MAX_LIFESPAN = "offlineSessionMaxLifespan";
|
||||
|
||||
// OAuth 2.0 Device Authorization Grant
|
||||
String OAUTH2_DEVICE_CODE_LIFESPAN = "oauth2DeviceCodeLifespan";
|
||||
String OAUTH2_DEVICE_POLLING_INTERVAL = "oauth2DevicePollingInterval";
|
||||
|
||||
String CLIENT_SESSION_IDLE_TIMEOUT = "clientSessionIdleTimeout";
|
||||
String CLIENT_SESSION_MAX_LIFESPAN = "clientSessionMaxLifespan";
|
||||
String CLIENT_OFFLINE_SESSION_IDLE_TIMEOUT = "clientOfflineSessionIdleTimeout";
|
||||
String CLIENT_OFFLINE_SESSION_MAX_LIFESPAN = "clientOfflineSessionMaxLifespan";
|
||||
|
||||
String WEBAUTHN_POLICY_RP_ENTITY_NAME = "webAuthnPolicyRpEntityName";
|
||||
String WEBAUTHN_POLICY_SIGNATURE_ALGORITHMS = "webAuthnPolicySignatureAlgorithms";
|
||||
|
||||
|
|
|
@ -119,8 +119,4 @@ public final class Constants {
|
|||
*/
|
||||
public static final String STORAGE_BATCH_SIZE = "org.keycloak.storage.batch_size";
|
||||
|
||||
// 10 minutes
|
||||
public static final int DEFAULT_OAUTH2_DEVICE_CODE_LIFESPAN = 600;
|
||||
// 5 seconds
|
||||
public static final int DEFAULT_OAUTH2_DEVICE_POLLING_INTERVAL = 5;
|
||||
}
|
||||
|
|
|
@ -16,19 +16,19 @@
|
|||
*/
|
||||
package org.keycloak.models;
|
||||
|
||||
import org.keycloak.common.util.Time;
|
||||
|
||||
import javax.ws.rs.core.MultivaluedHashMap;
|
||||
import javax.ws.rs.core.MultivaluedMap;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.keycloak.common.util.Time;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:h2-wada@nri.co.jp">Hiroyuki Wada</a>
|
||||
*/
|
||||
public class OAuth2DeviceCodeModel {
|
||||
|
||||
private static final String REALM_ID = "rid";
|
||||
private static final String CLIENT_ID = "cid";
|
||||
private static final String EXPIRATION_NOTE = "exp";
|
||||
private static final String POLLING_INTERVAL_NOTE = "int";
|
||||
|
@ -46,15 +46,14 @@ public class OAuth2DeviceCodeModel {
|
|||
private final String scope;
|
||||
private final String nonce;
|
||||
private final String userSessionId;
|
||||
private final boolean denied;
|
||||
private final Boolean denied;
|
||||
private final Map<String, String> additionalParams;
|
||||
|
||||
public static OAuth2DeviceCodeModel create(RealmModel realm, ClientModel client,
|
||||
String deviceCode, String scope, String nonce, Map<String, String> additionalParams) {
|
||||
int expiresIn = realm.getOAuth2DeviceCodeLifespan();
|
||||
String deviceCode, String scope, String nonce, int expiresIn, int pollingInterval, Map<String, String> additionalParams) {
|
||||
|
||||
int expiration = Time.currentTime() + expiresIn;
|
||||
int pollingInterval = realm.getOAuth2DevicePollingInterval();
|
||||
return new OAuth2DeviceCodeModel(realm, client.getClientId(), deviceCode, scope, nonce, expiration, pollingInterval, null, false, additionalParams);
|
||||
return new OAuth2DeviceCodeModel(realm, client.getClientId(), deviceCode, scope, nonce, expiration, pollingInterval, null, null, additionalParams);
|
||||
}
|
||||
|
||||
public OAuth2DeviceCodeModel approve(String userSessionId) {
|
||||
|
@ -67,7 +66,7 @@ public class OAuth2DeviceCodeModel {
|
|||
|
||||
private OAuth2DeviceCodeModel(RealmModel realm, String clientId,
|
||||
String deviceCode, String scope, String nonce, int expiration, int pollingInterval,
|
||||
String userSessionId, boolean denied, Map<String, String> additionalParams) {
|
||||
String userSessionId, Boolean denied, Map<String, String> additionalParams) {
|
||||
this.realm = realm;
|
||||
this.clientId = clientId;
|
||||
this.deviceCode = deviceCode;
|
||||
|
@ -81,7 +80,13 @@ public class OAuth2DeviceCodeModel {
|
|||
}
|
||||
|
||||
public static OAuth2DeviceCodeModel fromCache(RealmModel realm, String deviceCode, Map<String, String> data) {
|
||||
return new OAuth2DeviceCodeModel(realm, deviceCode, data);
|
||||
OAuth2DeviceCodeModel model = new OAuth2DeviceCodeModel(realm, deviceCode, data);
|
||||
|
||||
if (!realm.getId().equals(data.get(REALM_ID))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
private OAuth2DeviceCodeModel(RealmModel realm, String deviceCode, Map<String, String> data) {
|
||||
|
@ -128,58 +133,51 @@ public class OAuth2DeviceCodeModel {
|
|||
return userSessionId == null;
|
||||
}
|
||||
|
||||
public boolean isApproved() {
|
||||
return userSessionId != null && !denied;
|
||||
}
|
||||
|
||||
public boolean isDenied() {
|
||||
return userSessionId != null && denied;
|
||||
return denied;
|
||||
}
|
||||
|
||||
public String getUserSessionId() {
|
||||
return userSessionId;
|
||||
}
|
||||
|
||||
public static String createKey(RealmModel realm, String deviceCode) {
|
||||
return String.format("%s.dc.%s", realm.getId(), deviceCode);
|
||||
public static String createKey(String deviceCode) {
|
||||
return String.format("dc.%s", deviceCode);
|
||||
}
|
||||
|
||||
public String serializeKey() {
|
||||
return createKey(realm, deviceCode);
|
||||
return createKey(deviceCode);
|
||||
}
|
||||
|
||||
public String serializePollingKey() {
|
||||
return createKey(realm, deviceCode) + ".polling";
|
||||
return createKey(deviceCode) + ".polling";
|
||||
}
|
||||
|
||||
public Map<String, String> serializeValue() {
|
||||
public Map<String, String> toMap() {
|
||||
Map<String, String> result = new HashMap<>();
|
||||
result.put(CLIENT_ID, clientId);
|
||||
result.put(EXPIRATION_NOTE, String.valueOf(expiration));
|
||||
result.put(POLLING_INTERVAL_NOTE, String.valueOf(pollingInterval));
|
||||
result.put(SCOPE_NOTE, scope);
|
||||
result.put(NONCE_NOTE, nonce);
|
||||
additionalParams.forEach((key, value) -> result.put(ADDITIONAL_PARAM_PREFIX + key, value));
|
||||
return result;
|
||||
}
|
||||
|
||||
public Map<String, String> serializeApprovedValue() {
|
||||
Map<String, String> result = new HashMap<>();
|
||||
result.put(EXPIRATION_NOTE, String.valueOf(expiration));
|
||||
result.put(POLLING_INTERVAL_NOTE, String.valueOf(pollingInterval));
|
||||
result.put(SCOPE_NOTE, scope);
|
||||
result.put(NONCE_NOTE, nonce);
|
||||
result.put(USER_SESSION_ID_NOTE, userSessionId);
|
||||
additionalParams.forEach((key, value) -> result.put(ADDITIONAL_PARAM_PREFIX + key, value));
|
||||
return result;
|
||||
}
|
||||
result.put(REALM_ID, realm.getId());
|
||||
|
||||
if (denied == null) {
|
||||
result.put(CLIENT_ID, clientId);
|
||||
result.put(EXPIRATION_NOTE, String.valueOf(expiration));
|
||||
result.put(POLLING_INTERVAL_NOTE, String.valueOf(pollingInterval));
|
||||
result.put(SCOPE_NOTE, scope);
|
||||
result.put(NONCE_NOTE, nonce);
|
||||
} else if (denied) {
|
||||
result.put(EXPIRATION_NOTE, String.valueOf(expiration));
|
||||
result.put(POLLING_INTERVAL_NOTE, String.valueOf(pollingInterval));
|
||||
result.put(DENIED_NOTE, String.valueOf(denied));
|
||||
} else {
|
||||
result.put(EXPIRATION_NOTE, String.valueOf(expiration));
|
||||
result.put(POLLING_INTERVAL_NOTE, String.valueOf(pollingInterval));
|
||||
result.put(SCOPE_NOTE, scope);
|
||||
result.put(NONCE_NOTE, nonce);
|
||||
result.put(USER_SESSION_ID_NOTE, userSessionId);
|
||||
}
|
||||
|
||||
public Map<String, String> serializeDeniedValue() {
|
||||
Map<String, String> result = new HashMap<>();
|
||||
result.put(EXPIRATION_NOTE, String.valueOf(expiration));
|
||||
result.put(POLLING_INTERVAL_NOTE, String.valueOf(pollingInterval));
|
||||
result.put(DENIED_NOTE, String.valueOf(denied));
|
||||
additionalParams.forEach((key, value) -> result.put(ADDITIONAL_PARAM_PREFIX + key, value));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
@ -192,4 +190,8 @@ public class OAuth2DeviceCodeModel {
|
|||
this.additionalParams.forEach(params::putSingle);
|
||||
return params;
|
||||
}
|
||||
|
||||
public boolean isExpired() {
|
||||
return getExpiration() - Time.currentTime() < 0;
|
||||
}
|
||||
}
|
|
@ -352,8 +352,8 @@ public class ModelToRepresentation {
|
|||
rep.setAccessCodeLifespanLogin(realm.getAccessCodeLifespanLogin());
|
||||
rep.setActionTokenGeneratedByAdminLifespan(realm.getActionTokenGeneratedByAdminLifespan());
|
||||
rep.setActionTokenGeneratedByUserLifespan(realm.getActionTokenGeneratedByUserLifespan());
|
||||
rep.setOAuth2DeviceCodeLifespan(realm.getOAuth2DeviceCodeLifespan());
|
||||
rep.setOAuth2DevicePollingInterval(realm.getOAuth2DevicePollingInterval());
|
||||
rep.setOAuth2DeviceCodeLifespan(realm.getOAuth2DeviceConfig().getLifespan());
|
||||
rep.setOAuth2DevicePollingInterval(realm.getOAuth2DeviceConfig().getPoolingInterval());
|
||||
rep.setSmtpServer(new HashMap<>(realm.getSmtpConfig()));
|
||||
rep.setBrowserSecurityHeaders(realm.getBrowserSecurityHeaders());
|
||||
rep.setAccountTheme(realm.getAccountTheme());
|
||||
|
@ -585,7 +585,6 @@ public class ModelToRepresentation {
|
|||
rep.setImplicitFlowEnabled(clientModel.isImplicitFlowEnabled());
|
||||
rep.setDirectAccessGrantsEnabled(clientModel.isDirectAccessGrantsEnabled());
|
||||
rep.setServiceAccountsEnabled(clientModel.isServiceAccountsEnabled());
|
||||
rep.setOAuth2DeviceAuthorizationGrantEnabled(clientModel.isOAuth2DeviceAuthorizationGrantEnabled());
|
||||
rep.setSurrogateAuthRequired(clientModel.isSurrogateAuthRequired());
|
||||
rep.setRootUrl(clientModel.getRootUrl());
|
||||
rep.setBaseUrl(clientModel.getBaseUrl());
|
||||
|
|
|
@ -76,6 +76,7 @@ import org.keycloak.models.IdentityProviderModel;
|
|||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.LDAPConstants;
|
||||
import org.keycloak.models.ModelException;
|
||||
import org.keycloak.models.OAuth2DeviceConfig;
|
||||
import org.keycloak.models.OTPPolicy;
|
||||
import org.keycloak.models.PasswordPolicy;
|
||||
import org.keycloak.models.ProtocolMapperModel;
|
||||
|
@ -250,12 +251,10 @@ public class RepresentationToModel {
|
|||
else newRealm.setActionTokenGeneratedByUserLifespan(newRealm.getAccessCodeLifespanUserAction());
|
||||
|
||||
// OAuth 2.0 Device Authorization Grant
|
||||
if (rep.getOAuth2DeviceCodeLifespan() != null)
|
||||
newRealm.setOAuth2DeviceCodeLifespan(rep.getOAuth2DeviceCodeLifespan());
|
||||
else newRealm.setOAuth2DeviceCodeLifespan(Constants.DEFAULT_OAUTH2_DEVICE_CODE_LIFESPAN);
|
||||
if (rep.getOAuth2DevicePollingInterval() != null)
|
||||
newRealm.setOAuth2DevicePollingInterval(rep.getOAuth2DevicePollingInterval());
|
||||
else newRealm.setOAuth2DevicePollingInterval(Constants.DEFAULT_OAUTH2_DEVICE_POLLING_INTERVAL);
|
||||
OAuth2DeviceConfig deviceConfig = newRealm.getOAuth2DeviceConfig();
|
||||
|
||||
deviceConfig.setOAuth2DeviceCodeLifespan(rep.getOAuth2DeviceCodeLifespan());
|
||||
deviceConfig.setOAuth2DevicePollingInterval(rep.getOAuth2DevicePollingInterval());
|
||||
|
||||
if (rep.getSslRequired() != null)
|
||||
newRealm.setSslRequired(SslRequired.valueOf(rep.getSslRequired().toUpperCase()));
|
||||
|
@ -1118,10 +1117,12 @@ public class RepresentationToModel {
|
|||
realm.setActionTokenGeneratedByAdminLifespan(rep.getActionTokenGeneratedByAdminLifespan());
|
||||
if (rep.getActionTokenGeneratedByUserLifespan() != null)
|
||||
realm.setActionTokenGeneratedByUserLifespan(rep.getActionTokenGeneratedByUserLifespan());
|
||||
if (rep.getOAuth2DeviceCodeLifespan() != null)
|
||||
realm.setOAuth2DeviceCodeLifespan(rep.getOAuth2DeviceCodeLifespan());
|
||||
if (rep.getOAuth2DevicePollingInterval() != null)
|
||||
realm.setOAuth2DevicePollingInterval(rep.getOAuth2DevicePollingInterval());
|
||||
|
||||
OAuth2DeviceConfig deviceConfig = realm.getOAuth2DeviceConfig();
|
||||
|
||||
deviceConfig.setOAuth2DeviceCodeLifespan(rep.getOAuth2DeviceCodeLifespan());
|
||||
deviceConfig.setOAuth2DevicePollingInterval(rep.getOAuth2DevicePollingInterval());
|
||||
|
||||
if (rep.getNotBefore() != null) realm.setNotBefore(rep.getNotBefore());
|
||||
if (rep.getDefaultSignatureAlgorithm() != null) realm.setDefaultSignatureAlgorithm(rep.getDefaultSignatureAlgorithm());
|
||||
if (rep.getRevokeRefreshToken() != null) realm.setRevokeRefreshToken(rep.getRevokeRefreshToken());
|
||||
|
@ -1487,10 +1488,6 @@ public class RepresentationToModel {
|
|||
client.setFullScopeAllowed(!client.isConsentRequired());
|
||||
}
|
||||
|
||||
if (resourceRep.isOAuth2DeviceAuthorizationGrantEnabled() != null) {
|
||||
client.setOAuth2DeviceAuthorizationGrantEnabled(resourceRep.isOAuth2DeviceAuthorizationGrantEnabled());
|
||||
}
|
||||
|
||||
client.updateClient();
|
||||
resourceRep.setId(client.getId());
|
||||
|
||||
|
@ -1556,6 +1553,7 @@ public class RepresentationToModel {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (rep.getNotBefore() != null) {
|
||||
resource.setNotBefore(rep.getNotBefore());
|
||||
}
|
||||
|
@ -1589,10 +1587,6 @@ public class RepresentationToModel {
|
|||
}
|
||||
}
|
||||
|
||||
if (rep.isOAuth2DeviceAuthorizationGrantEnabled() != null) {
|
||||
resource.setOAuth2DeviceAuthorizationGrantEnabled(rep.isOAuth2DeviceAuthorizationGrantEnabled());
|
||||
}
|
||||
|
||||
resource.updateClient();
|
||||
}
|
||||
|
||||
|
|
|
@ -36,7 +36,6 @@ public interface ClientModel extends ClientScopeModel, RoleContainerModel, Prot
|
|||
String PRIVATE_KEY = "privateKey";
|
||||
String PUBLIC_KEY = "publicKey";
|
||||
String X509CERTIFICATE = "X509Certificate";
|
||||
String OAUTH2_DEVICE_AUTHORIZATION_GRANT_ENABLED = "oauth2.device.authorization.grant.enabled";
|
||||
|
||||
public static class SearchableFields {
|
||||
public static final SearchableModelField<ClientModel> ID = new SearchableModelField<>("id", String.class);
|
||||
|
@ -200,15 +199,6 @@ public interface ClientModel extends ClientScopeModel, RoleContainerModel, Prot
|
|||
boolean isServiceAccountsEnabled();
|
||||
void setServiceAccountsEnabled(boolean serviceAccountsEnabled);
|
||||
|
||||
default boolean isOAuth2DeviceAuthorizationGrantEnabled() {
|
||||
String enabled = getAttribute(OAUTH2_DEVICE_AUTHORIZATION_GRANT_ENABLED);
|
||||
return Boolean.parseBoolean(enabled);
|
||||
}
|
||||
|
||||
default void setOAuth2DeviceAuthorizationGrantEnabled(boolean oauth2DeviceAuthorizationGrantEnabled) {
|
||||
setAttribute(OAUTH2_DEVICE_AUTHORIZATION_GRANT_ENABLED, Boolean.toString(oauth2DeviceAuthorizationGrantEnabled));
|
||||
}
|
||||
|
||||
RealmModel getRealm();
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,126 @@
|
|||
/*
|
||||
*
|
||||
* * Copyright 2021 Red Hat, Inc. and/or its affiliates
|
||||
* * and other contributors as indicated by the @author tags.
|
||||
* *
|
||||
* * 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
|
||||
* *
|
||||
* * http://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.keycloak.models;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||
*/
|
||||
public final class OAuth2DeviceConfig implements Serializable {
|
||||
|
||||
// 10 minutes
|
||||
public static final int DEFAULT_OAUTH2_DEVICE_CODE_LIFESPAN = 600;
|
||||
// 5 seconds
|
||||
public static final int DEFAULT_OAUTH2_DEVICE_POLLING_INTERVAL = 5;
|
||||
|
||||
// realm attribute names
|
||||
public static String OAUTH2_DEVICE_CODE_LIFESPAN = "oauth2DeviceCodeLifespan";
|
||||
public static String OAUTH2_DEVICE_POLLING_INTERVAL = "oauth2DevicePollingInterval";
|
||||
|
||||
// client attribute names
|
||||
public static String OAUTH2_DEVICE_CODE_LIFESPAN_PER_CLIENT = "oauth2.device.code.lifespan";
|
||||
public static String OAUTH2_DEVICE_POLLING_INTERVAL_PER_CLIENT = "oauth2.device.polling.interval";
|
||||
public static final String OAUTH2_DEVICE_AUTHORIZATION_GRANT_ENABLED = "oauth2.device.authorization.grant.enabled";
|
||||
|
||||
private transient Supplier<RealmModel> realm;
|
||||
|
||||
private int lifespan = DEFAULT_OAUTH2_DEVICE_CODE_LIFESPAN;
|
||||
private int poolingInterval = DEFAULT_OAUTH2_DEVICE_CODE_LIFESPAN;
|
||||
|
||||
public OAuth2DeviceConfig(RealmModel realm) {
|
||||
this.realm = () -> realm;
|
||||
|
||||
String lifespan = realm.getAttribute(OAUTH2_DEVICE_CODE_LIFESPAN);
|
||||
|
||||
if (lifespan != null && !lifespan.trim().isEmpty()) {
|
||||
setOAuth2DeviceCodeLifespan(Integer.parseInt(lifespan));
|
||||
}
|
||||
|
||||
String pooling = realm.getAttribute(OAUTH2_DEVICE_POLLING_INTERVAL);
|
||||
|
||||
if (pooling != null && !pooling.trim().isEmpty()) {
|
||||
setOAuth2DevicePollingInterval(Integer.parseInt(pooling));
|
||||
}
|
||||
}
|
||||
|
||||
public int getLifespan() {
|
||||
return lifespan;
|
||||
}
|
||||
|
||||
public void setOAuth2DeviceCodeLifespan(Integer seconds) {
|
||||
if (seconds == null) {
|
||||
seconds = DEFAULT_OAUTH2_DEVICE_CODE_LIFESPAN;
|
||||
}
|
||||
this.lifespan = seconds;
|
||||
realm.get().setAttribute(OAUTH2_DEVICE_CODE_LIFESPAN, lifespan);
|
||||
}
|
||||
|
||||
public int getPoolingInterval() {
|
||||
return poolingInterval;
|
||||
}
|
||||
|
||||
public void setOAuth2DevicePollingInterval(Integer seconds) {
|
||||
if (seconds == null) {
|
||||
seconds = DEFAULT_OAUTH2_DEVICE_POLLING_INTERVAL;
|
||||
}
|
||||
this.poolingInterval = seconds;
|
||||
|
||||
RealmModel model = getRealm();
|
||||
|
||||
model.setAttribute(OAUTH2_DEVICE_POLLING_INTERVAL, poolingInterval);
|
||||
}
|
||||
|
||||
public int getLifespan(ClientModel client) {
|
||||
String lifespan = client.getAttribute(OAUTH2_DEVICE_CODE_LIFESPAN_PER_CLIENT);
|
||||
|
||||
if (lifespan != null && !lifespan.trim().isEmpty()) {
|
||||
return Integer.parseInt(lifespan);
|
||||
}
|
||||
|
||||
return getLifespan();
|
||||
}
|
||||
|
||||
public int getPoolingInterval(ClientModel client) {
|
||||
String interval = client.getAttribute(OAUTH2_DEVICE_POLLING_INTERVAL_PER_CLIENT);
|
||||
|
||||
if (interval != null && !interval.trim().isEmpty()) {
|
||||
return Integer.parseInt(interval);
|
||||
}
|
||||
|
||||
return getPoolingInterval();
|
||||
}
|
||||
|
||||
public boolean isOAuth2DeviceAuthorizationGrantEnabled(ClientModel client) {
|
||||
String enabled = client.getAttribute(OAUTH2_DEVICE_AUTHORIZATION_GRANT_ENABLED);
|
||||
return Boolean.parseBoolean(enabled);
|
||||
}
|
||||
|
||||
private RealmModel getRealm() {
|
||||
RealmModel model = realm.get();
|
||||
|
||||
if (model == null) {
|
||||
throw new RuntimeException("Can only update after invalidating the realm");
|
||||
}
|
||||
|
||||
return model;
|
||||
}
|
||||
}
|
|
@ -213,11 +213,7 @@ public interface RealmModel extends RoleContainerModel {
|
|||
|
||||
void setAccessCodeLifespanUserAction(int seconds);
|
||||
|
||||
int getOAuth2DeviceCodeLifespan();
|
||||
void setOAuth2DeviceCodeLifespan(int seconds);
|
||||
|
||||
int getOAuth2DevicePollingInterval();
|
||||
void setOAuth2DevicePollingInterval(int seconds);
|
||||
OAuth2DeviceConfig getOAuth2DeviceConfig();
|
||||
|
||||
/**
|
||||
* This method will return a map with all the lifespans available
|
||||
|
|
|
@ -16,6 +16,8 @@
|
|||
*/
|
||||
package org.keycloak.forms.login.freemarker.model;
|
||||
|
||||
import static org.keycloak.protocol.oidc.grants.device.DeviceGrantType.realmOAuth2DeviceVerificationAction;
|
||||
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.services.Urls;
|
||||
import org.keycloak.theme.Theme;
|
||||
|
@ -110,7 +112,7 @@ public class UrlBean {
|
|||
return this.actionuri.getPath();
|
||||
}
|
||||
|
||||
return Urls.realmOAuth2DeviceVerificationAction(baseURI, realm).toString();
|
||||
return realmOAuth2DeviceVerificationAction(baseURI, realm).toString();
|
||||
}
|
||||
|
||||
public String getResourcesPath() {
|
||||
|
|
|
@ -16,6 +16,10 @@
|
|||
*/
|
||||
package org.keycloak.protocol.oidc;
|
||||
|
||||
import static org.keycloak.protocol.oidc.grants.device.DeviceGrantType.approveOAuth2DeviceAuthorization;
|
||||
import static org.keycloak.protocol.oidc.grants.device.DeviceGrantType.denyOAuth2DeviceAuthorization;
|
||||
import static org.keycloak.protocol.oidc.grants.device.DeviceGrantType.isOAuth2DeviceVerificationFlow;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.OAuthErrorException;
|
||||
|
@ -33,10 +37,10 @@ import org.keycloak.models.ClientModel;
|
|||
import org.keycloak.models.ClientSessionContext;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.OAuth2DeviceTokenStoreProvider;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.protocol.LoginProtocol;
|
||||
import org.keycloak.protocol.oidc.grants.device.DeviceGrantType;
|
||||
import org.keycloak.protocol.oidc.utils.OIDCRedirectUriBuilder;
|
||||
import org.keycloak.protocol.oidc.utils.OIDCResponseMode;
|
||||
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
|
||||
|
@ -118,10 +122,6 @@ public class OIDCLoginProtocol implements LoginProtocol {
|
|||
public static final String PKCE_METHOD_PLAIN = "plain";
|
||||
public static final String PKCE_METHOD_S256 = "S256";
|
||||
|
||||
// OAuth 2.0 Device Authorization Grant
|
||||
public static final String OAUTH2_DEVICE_VERIFIED_USER_CODE = "OAUTH2_DEVICE_VERIFIED_USER_CODE";
|
||||
public static final String OAUTH2_DEVICE_USER_CODE_EXPIRATION = "OAUTH2_DEVICE_USER_CODE_EXPIRATION";
|
||||
|
||||
private static final Logger logger = Logger.getLogger(OIDCLoginProtocol.class);
|
||||
|
||||
protected KeycloakSession session;
|
||||
|
@ -191,8 +191,8 @@ public class OIDCLoginProtocol implements LoginProtocol {
|
|||
public Response authenticated(AuthenticationSessionModel authSession, UserSessionModel userSession, ClientSessionContext clientSessionCtx) {
|
||||
AuthenticatedClientSessionModel clientSession = clientSessionCtx.getClientSession();
|
||||
|
||||
if (AuthenticationManager.isOAuth2DeviceVerificationFlow(authSession)) {
|
||||
return approveOAuth2DeviceAuthorization(authSession, clientSession);
|
||||
if (isOAuth2DeviceVerificationFlow(authSession)) {
|
||||
return approveOAuth2DeviceAuthorization(authSession, clientSession, session);
|
||||
}
|
||||
|
||||
String responseTypeParam = authSession.getClientNote(OIDCLoginProtocol.RESPONSE_TYPE_PARAM);
|
||||
|
@ -278,8 +278,8 @@ public class OIDCLoginProtocol implements LoginProtocol {
|
|||
|
||||
@Override
|
||||
public Response sendError(AuthenticationSessionModel authSession, Error error) {
|
||||
if (AuthenticationManager.isOAuth2DeviceVerificationFlow(authSession)) {
|
||||
return denyOAuth2DeviceAuthorization(authSession, error);
|
||||
if (isOAuth2DeviceVerificationFlow(authSession)) {
|
||||
return denyOAuth2DeviceAuthorization(authSession, error, session);
|
||||
}
|
||||
|
||||
String responseTypeParam = authSession.getClientNote(OIDCLoginProtocol.RESPONSE_TYPE_PARAM);
|
||||
|
@ -289,7 +289,7 @@ public class OIDCLoginProtocol implements LoginProtocol {
|
|||
String redirect = authSession.getRedirectUri();
|
||||
String state = authSession.getClientNote(OIDCLoginProtocol.STATE_PARAM);
|
||||
OIDCRedirectUriBuilder redirectUri = OIDCRedirectUriBuilder.fromUri(redirect, responseMode);
|
||||
|
||||
|
||||
if (error != Error.CANCELLED_AIA_SILENT) {
|
||||
redirectUri.addParam(OAuth2Constants.ERROR, translateError(error));
|
||||
}
|
||||
|
@ -304,47 +304,6 @@ public class OIDCLoginProtocol implements LoginProtocol {
|
|||
return redirectUri.build();
|
||||
}
|
||||
|
||||
private Response approveOAuth2DeviceAuthorization(AuthenticationSessionModel authSession, AuthenticatedClientSessionModel clientSession) {
|
||||
UriBuilder uriBuilder = OIDCLoginProtocolService.oauth2DeviceVerificationCompletedUrl(uriInfo);
|
||||
|
||||
String verifiedUserCode = authSession.getClientNote(OIDCLoginProtocol.OAUTH2_DEVICE_VERIFIED_USER_CODE);
|
||||
String userSessionId = clientSession.getUserSession().getId();
|
||||
OAuth2DeviceTokenStoreProvider store = session.getProvider(OAuth2DeviceTokenStoreProvider.class);
|
||||
if (!store.approve(realm, verifiedUserCode, userSessionId)) {
|
||||
// Already expired and removed in the store
|
||||
return Response.status(302).location(
|
||||
uriBuilder.queryParam(OAuth2Constants.ERROR, OAuthErrorException.EXPIRED_TOKEN)
|
||||
.build(realm.getName())
|
||||
).build();
|
||||
}
|
||||
|
||||
// Now, remove the verified user code
|
||||
store.removeUserCode(realm, verifiedUserCode);
|
||||
|
||||
return Response.status(302).location(
|
||||
uriBuilder.build(realm.getName())
|
||||
).build();
|
||||
}
|
||||
|
||||
private Response denyOAuth2DeviceAuthorization(AuthenticationSessionModel authSession, Error error) {
|
||||
UriBuilder uriBuilder = OIDCLoginProtocolService.oauth2DeviceVerificationCompletedUrl(uriInfo);
|
||||
String errorType = OAuthErrorException.SERVER_ERROR;
|
||||
if (error == Error.CONSENT_DENIED) {
|
||||
String verifiedUserCode = authSession.getClientNote(OIDCLoginProtocol.OAUTH2_DEVICE_VERIFIED_USER_CODE);
|
||||
OAuth2DeviceTokenStoreProvider store = session.getProvider(OAuth2DeviceTokenStoreProvider.class);
|
||||
if (!store.deny(realm, verifiedUserCode)) {
|
||||
// Already expired and removed in the store
|
||||
errorType = OAuthErrorException.EXPIRED_TOKEN;
|
||||
} else {
|
||||
errorType = OAuthErrorException.ACCESS_DENIED;
|
||||
}
|
||||
}
|
||||
return Response.status(302).location(
|
||||
uriBuilder.queryParam(OAuth2Constants.ERROR, errorType)
|
||||
.build(realm.getName())
|
||||
).build();
|
||||
}
|
||||
|
||||
private String translateError(Error error) {
|
||||
switch (error) {
|
||||
case CANCELLED_BY_USER:
|
||||
|
|
|
@ -37,13 +37,11 @@ import org.keycloak.protocol.oidc.endpoints.AuthorizationEndpoint;
|
|||
import org.keycloak.protocol.oidc.endpoints.LoginStatusIframeEndpoint;
|
||||
import org.keycloak.protocol.oidc.endpoints.LogoutEndpoint;
|
||||
import org.keycloak.protocol.oidc.endpoints.ThirdPartyCookiesIframeEndpoint;
|
||||
import org.keycloak.protocol.oidc.endpoints.OAuth2DeviceAuthorizationEndpoint;
|
||||
import org.keycloak.protocol.oidc.endpoints.TokenEndpoint;
|
||||
import org.keycloak.protocol.oidc.endpoints.TokenRevocationEndpoint;
|
||||
import org.keycloak.protocol.oidc.endpoints.UserInfoEndpoint;
|
||||
import org.keycloak.protocol.oidc.ext.OIDCExtProvider;
|
||||
import org.keycloak.services.CorsErrorResponseException;
|
||||
import org.keycloak.saml.common.util.StringUtil;
|
||||
import org.keycloak.services.managers.AuthenticationManager;
|
||||
import org.keycloak.services.messages.Messages;
|
||||
import org.keycloak.services.resources.Cors;
|
||||
|
@ -52,7 +50,6 @@ import org.keycloak.services.util.CacheControlUtil;
|
|||
|
||||
import java.util.Objects;
|
||||
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.NotFoundException;
|
||||
import javax.ws.rs.OPTIONS;
|
||||
|
@ -118,16 +115,6 @@ public class OIDCLoginProtocolService {
|
|||
return uriBuilder.path(OIDCLoginProtocolService.class, "auth");
|
||||
}
|
||||
|
||||
public static UriBuilder oauth2DeviceAuthUrl(UriBuilder baseUriBuilder) {
|
||||
UriBuilder uriBuilder = tokenServiceBaseUrl(baseUriBuilder);
|
||||
return uriBuilder.path(OIDCLoginProtocolService.class, "oauth2DeviceAuth");
|
||||
}
|
||||
|
||||
public static UriBuilder oauth2DeviceVerificationCompletedUrl(UriInfo uriInfo) {
|
||||
UriBuilder uriBuilder = tokenServiceBaseUrl(uriInfo);
|
||||
return uriBuilder.path(OIDCLoginProtocolService.class, "oauth2DeviceVerificationCompleted");
|
||||
}
|
||||
|
||||
public static UriBuilder delegatedUrl(UriInfo uriInfo) {
|
||||
UriBuilder uriBuilder = tokenServiceBaseUrl(uriInfo);
|
||||
return uriBuilder.path(OIDCLoginProtocolService.class, "kcinitBrowserLoginComplete");
|
||||
|
@ -177,58 +164,6 @@ public class OIDCLoginProtocolService {
|
|||
return endpoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* OAuth 2.0 Device Authorization endpoint
|
||||
*/
|
||||
@Path("device/auth")
|
||||
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Object oauth2DeviceAuth() {
|
||||
OAuth2DeviceAuthorizationEndpoint endpoint = new OAuth2DeviceAuthorizationEndpoint(realm, event);
|
||||
ResteasyProviderFactory.getInstance().injectProperties(endpoint);
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Showing the result of verification process for OAuth 2.0 Device Authorization Grant.
|
||||
* This outputs login success or failure messages.
|
||||
*
|
||||
* @param error
|
||||
* @return
|
||||
*/
|
||||
@GET
|
||||
@Path("device/verification")
|
||||
public Response oauth2DeviceVerificationCompleted(@QueryParam("error") String error) {
|
||||
if (!StringUtil.isNullOrEmpty(error)) {
|
||||
String message;
|
||||
switch (error) {
|
||||
case OAuthErrorException.ACCESS_DENIED:
|
||||
// cased by CANCELLED_BY_USER or CONSENT_DENIED:
|
||||
message = Messages.OAUTH2_DEVICE_CONSENT_DENIED;
|
||||
break;
|
||||
case OAuthErrorException.EXPIRED_TOKEN:
|
||||
message = Messages.OAUTH2_DEVICE_EXPIRED_USER_CODE;
|
||||
break;
|
||||
default:
|
||||
message = Messages.OAUTH2_DEVICE_VERIFICATION_FAILED;
|
||||
}
|
||||
LoginFormsProvider forms = session.getProvider(LoginFormsProvider.class);
|
||||
String restartUri = RealmsResource.oauth2DeviceVerificationUrl(session.getContext().getUri()).build(realm.getName()).toString();
|
||||
return forms
|
||||
.setAttribute("messageHeader", forms.getMessage(Messages.OAUTH2_DEVICE_VERIFICATION_FAILED_HEADER))
|
||||
.setAttribute(Constants.TEMPLATE_ATTR_ACTION_URI, restartUri)
|
||||
.setError(message)
|
||||
.createInfoPage();
|
||||
} else {
|
||||
LoginFormsProvider forms = session.getProvider(LoginFormsProvider.class);
|
||||
return forms
|
||||
.setAttribute("messageHeader", forms.getMessage(Messages.OAUTH2_DEVICE_VERIFICATION_COMPLETE_HEADER))
|
||||
.setAttribute(Constants.SKIP_LINK, true)
|
||||
.setSuccess(Messages.OAUTH2_DEVICE_VERIFICATION_COMPLETE)
|
||||
.createInfoPage();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registration endpoint
|
||||
*/
|
||||
|
|
|
@ -29,7 +29,9 @@ import org.keycloak.jose.jws.Algorithm;
|
|||
import org.keycloak.models.ClientScopeModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.protocol.oidc.endpoints.AuthorizationEndpoint;
|
||||
import org.keycloak.protocol.oidc.endpoints.TokenEndpoint;
|
||||
import org.keycloak.protocol.oidc.grants.device.endpoints.DeviceEndpoint;
|
||||
import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation;
|
||||
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
|
||||
import org.keycloak.provider.Provider;
|
||||
|
@ -58,7 +60,9 @@ import java.util.stream.Stream;
|
|||
*/
|
||||
public class OIDCWellKnownProvider implements WellKnownProvider {
|
||||
|
||||
public static final List<String> DEFAULT_GRANT_TYPES_SUPPORTED = list(OAuth2Constants.AUTHORIZATION_CODE, OAuth2Constants.IMPLICIT, OAuth2Constants.REFRESH_TOKEN, OAuth2Constants.PASSWORD, OAuth2Constants.CLIENT_CREDENTIALS);
|
||||
public static final List<String> DEFAULT_GRANT_TYPES_SUPPORTED = list(OAuth2Constants.AUTHORIZATION_CODE,
|
||||
OAuth2Constants.IMPLICIT, OAuth2Constants.REFRESH_TOKEN, OAuth2Constants.PASSWORD, OAuth2Constants.CLIENT_CREDENTIALS,
|
||||
OAuth2Constants.DEVICE_CODE_GRANT_TYPE);
|
||||
|
||||
public static final List<String> DEFAULT_RESPONSE_TYPES_SUPPORTED = list(OAuth2Constants.CODE, OIDCResponseType.NONE, OIDCResponseType.ID_TOKEN, OIDCResponseType.TOKEN, "id_token token", "code id_token", "code token", "code id_token token");
|
||||
|
||||
|
@ -99,6 +103,9 @@ public class OIDCWellKnownProvider implements WellKnownProvider {
|
|||
config.setIntrospectionEndpoint(backendUriBuilder.clone().path(OIDCLoginProtocolService.class, "token").path(TokenEndpoint.class, "introspect").build(realm.getName(), OIDCLoginProtocol.LOGIN_PROTOCOL).toString());
|
||||
config.setUserinfoEndpoint(backendUriBuilder.clone().path(OIDCLoginProtocolService.class, "issueUserInfo").build(realm.getName(), OIDCLoginProtocol.LOGIN_PROTOCOL).toString());
|
||||
config.setLogoutEndpoint(frontendUriBuilder.clone().path(OIDCLoginProtocolService.class, "logout").build(realm.getName(), OIDCLoginProtocol.LOGIN_PROTOCOL).toString());
|
||||
config.setDeviceAuthorizationEndpoint(frontendUriBuilder.clone().path(OIDCLoginProtocolService.class, "auth")
|
||||
.path(AuthorizationEndpoint.class, "authorizeDevice").path(DeviceEndpoint.class, "handleDeviceRequest")
|
||||
.build(realm.getName(), OIDCLoginProtocol.LOGIN_PROTOCOL).toString());
|
||||
URI jwksUri = backendUriBuilder.clone().path(OIDCLoginProtocolService.class, "certs").build(realm.getName(),
|
||||
OIDCLoginProtocol.LOGIN_PROTOCOL);
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
package org.keycloak.protocol.oidc.endpoints;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.jboss.resteasy.spi.ResteasyProviderFactory;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.OAuthErrorException;
|
||||
import org.keycloak.authentication.AuthenticationProcessor;
|
||||
|
@ -37,6 +38,7 @@ import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
|||
import org.keycloak.protocol.oidc.TokenManager;
|
||||
import org.keycloak.protocol.oidc.endpoints.request.AuthorizationEndpointRequest;
|
||||
import org.keycloak.protocol.oidc.endpoints.request.AuthorizationEndpointRequestParserProcessor;
|
||||
import org.keycloak.protocol.oidc.grants.device.endpoints.DeviceEndpoint;
|
||||
import org.keycloak.protocol.oidc.utils.OIDCRedirectUriBuilder;
|
||||
import org.keycloak.protocol.oidc.utils.OIDCResponseMode;
|
||||
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
|
||||
|
@ -57,6 +59,7 @@ import org.keycloak.util.TokenUtil;
|
|||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.MultivaluedMap;
|
||||
import javax.ws.rs.core.Response;
|
||||
|
@ -116,6 +119,16 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
|
|||
return process(session.getContext().getUri().getQueryParameters());
|
||||
}
|
||||
|
||||
/**
|
||||
* OAuth 2.0 Device Authorization endpoint
|
||||
*/
|
||||
@Path("device")
|
||||
public Object authorizeDevice() {
|
||||
DeviceEndpoint endpoint = new DeviceEndpoint(realm, event);
|
||||
ResteasyProviderFactory.getInstance().injectProperties(endpoint);
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
private Response process(MultivaluedMap<String, String> params) {
|
||||
String clientId = AuthorizationEndpointRequestParserProcessor.getClientId(event, session, params);
|
||||
|
||||
|
|
|
@ -1,346 +0,0 @@
|
|||
/*
|
||||
* Copyright 2019 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* http://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.keycloak.protocol.oidc.endpoints;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.OAuthErrorException;
|
||||
import org.keycloak.authentication.AuthenticationProcessor;
|
||||
import org.keycloak.common.util.Base64Url;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.constants.AdapterConstants;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.Errors;
|
||||
import org.keycloak.events.EventBuilder;
|
||||
import org.keycloak.events.EventType;
|
||||
import org.keycloak.forms.login.LoginFormsProvider;
|
||||
import org.keycloak.models.AuthenticatedClientSessionModel;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.OAuth2DeviceCodeModel;
|
||||
import org.keycloak.models.OAuth2DeviceTokenStoreProvider;
|
||||
import org.keycloak.models.OAuth2DeviceUserCodeModel;
|
||||
import org.keycloak.models.OAuth2DeviceUserCodeProvider;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.models.utils.SystemClientUtil;
|
||||
import org.keycloak.protocol.AuthorizationEndpointBase;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.protocol.oidc.endpoints.request.AuthorizationEndpointRequest;
|
||||
import org.keycloak.protocol.oidc.endpoints.request.AuthorizationEndpointRequestParserProcessor;
|
||||
import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil;
|
||||
import org.keycloak.representations.OAuth2DeviceAuthorizationResponse;
|
||||
import org.keycloak.saml.common.util.StringUtil;
|
||||
import org.keycloak.services.CorsErrorResponseException;
|
||||
import org.keycloak.services.ErrorPageException;
|
||||
import org.keycloak.services.ServicesLogger;
|
||||
import org.keycloak.services.Urls;
|
||||
import org.keycloak.services.managers.AuthenticationManager;
|
||||
import org.keycloak.services.managers.ClientSessionCode;
|
||||
import org.keycloak.services.messages.Messages;
|
||||
import org.keycloak.services.resources.Cors;
|
||||
import org.keycloak.services.resources.LoginActionsService;
|
||||
import org.keycloak.services.resources.RealmsResource;
|
||||
import org.keycloak.services.util.CacheControlUtil;
|
||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
import org.keycloak.util.TokenUtil;
|
||||
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.MultivaluedMap;
|
||||
import javax.ws.rs.core.Response;
|
||||
|
||||
import static org.keycloak.protocol.oidc.endpoints.AuthorizationEndpoint.LOGIN_SESSION_NOTE_ADDITIONAL_REQ_PARAMS_PREFIX;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:h2-wada@nri.co.jp">Hiroyuki Wada</a>
|
||||
*/
|
||||
public class OAuth2DeviceAuthorizationEndpoint extends AuthorizationEndpointBase {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(OAuth2DeviceAuthorizationEndpoint.class);
|
||||
|
||||
private enum Action {
|
||||
OAUTH2_DEVICE_AUTH, OAUTH2_DEVICE_VERIFY_USER_CODE
|
||||
}
|
||||
|
||||
private ClientModel client;
|
||||
private AuthenticationSessionModel authenticationSession;
|
||||
private Action action;
|
||||
private AuthorizationEndpointRequest request;
|
||||
private Cors cors;
|
||||
|
||||
public OAuth2DeviceAuthorizationEndpoint(RealmModel realm, EventBuilder event) {
|
||||
super(realm, event);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
public Response buildPost() {
|
||||
logger.trace("Processing @POST request");
|
||||
cors = Cors.add(httpRequest).auth().allowedMethods("POST").auth().exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS);
|
||||
action = Action.OAUTH2_DEVICE_AUTH;
|
||||
event.event(EventType.OAUTH2_DEVICE_AUTH);
|
||||
return process(httpRequest.getDecodedFormParameters());
|
||||
}
|
||||
|
||||
private Response process(MultivaluedMap<String, String> params) {
|
||||
checkSsl();
|
||||
checkRealm();
|
||||
checkClient(null);
|
||||
|
||||
request = AuthorizationEndpointRequestParserProcessor.parseRequest(event, session, client, params);
|
||||
|
||||
if (!TokenUtil.isOIDCRequest(request.getScope())) {
|
||||
ServicesLogger.LOGGER.oidcScopeMissing();
|
||||
}
|
||||
|
||||
authenticationSession = createAuthenticationSession(client, request.getState());
|
||||
updateAuthenticationSession();
|
||||
|
||||
// So back button doesn't work
|
||||
CacheControlUtil.noBackButtonCacheControlHeader();
|
||||
switch (action) {
|
||||
case OAUTH2_DEVICE_AUTH:
|
||||
return buildDeviceAuthorizationResponse();
|
||||
}
|
||||
|
||||
throw new RuntimeException("Unknown action " + action);
|
||||
}
|
||||
|
||||
public Response buildDeviceAuthorizationResponse() {
|
||||
if (!client.isOAuth2DeviceAuthorizationGrantEnabled()) {
|
||||
event.error(Errors.NOT_ALLOWED);
|
||||
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, "Client not allowed for OAuth 2.0 Device Authorization Grant", Response.Status.BAD_REQUEST);
|
||||
}
|
||||
|
||||
int expiresIn = realm.getOAuth2DeviceCodeLifespan();
|
||||
int interval = realm.getOAuth2DevicePollingInterval();
|
||||
|
||||
OAuth2DeviceCodeModel deviceCode = OAuth2DeviceCodeModel.create(realm, client,
|
||||
Base64Url.encode(KeycloakModelUtils.generateSecret()), request.getScope(), request.getNonce(), request.getAdditionalReqParams());
|
||||
|
||||
OAuth2DeviceUserCodeProvider userCodeProvider = session.getProvider(OAuth2DeviceUserCodeProvider.class);
|
||||
String secret = userCodeProvider.generate();
|
||||
OAuth2DeviceUserCodeModel userCode = new OAuth2DeviceUserCodeModel(realm,
|
||||
deviceCode.getDeviceCode(),
|
||||
secret
|
||||
);
|
||||
|
||||
// To inform "expired_token" to the client, the lifespan of the cache provider is longer than device code
|
||||
int lifespanSeconds = expiresIn + interval + 10;
|
||||
|
||||
OAuth2DeviceTokenStoreProvider store = session.getProvider(OAuth2DeviceTokenStoreProvider.class);
|
||||
store.put(deviceCode, userCode, lifespanSeconds);
|
||||
|
||||
try {
|
||||
String deviceUrl = RealmsResource.oauth2DeviceVerificationUrl(session.getContext().getUri()).build(realm.getName()).toString();
|
||||
|
||||
OAuth2DeviceAuthorizationResponse response = new OAuth2DeviceAuthorizationResponse();
|
||||
response.setDeviceCode(deviceCode.getDeviceCode());
|
||||
response.setUserCode(userCodeProvider.display(secret));
|
||||
response.setExpiresIn(expiresIn);
|
||||
response.setInterval(interval);
|
||||
response.setVerificationUri(deviceUrl);
|
||||
response.setVerificationUriComplete(deviceUrl + "?user_code=" + response.getUserCode());
|
||||
|
||||
return Response.ok(JsonSerialization.writeValueAsBytes(response)).type(MediaType.APPLICATION_JSON_TYPE).build();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Error creating OAuth 2.0 Device Authorization Response.", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void checkClient(String clientId) {
|
||||
// https://tools.ietf.org/html/draft-ietf-oauth-device-flow-15#section-3.1
|
||||
// The spec says "The client authentication requirements of Section 3.2.1 of [RFC6749]
|
||||
// apply to requests on this endpoint".
|
||||
if (action == Action.OAUTH2_DEVICE_AUTH) {
|
||||
AuthorizeClientUtil.ClientAuthResult clientAuth = AuthorizeClientUtil.authorizeClient(session, event);
|
||||
client = clientAuth.getClient();
|
||||
clientId = client.getClientId();
|
||||
}
|
||||
|
||||
if (clientId == null) {
|
||||
event.error(Errors.INVALID_REQUEST);
|
||||
throw new ErrorPageException(session, authenticationSession, Response.Status.BAD_REQUEST, Messages.MISSING_PARAMETER, OIDCLoginProtocol.CLIENT_ID_PARAM);
|
||||
}
|
||||
|
||||
event.client(clientId);
|
||||
|
||||
client = realm.getClientByClientId(clientId);
|
||||
if (client == null) {
|
||||
event.error(Errors.CLIENT_NOT_FOUND);
|
||||
throw new ErrorPageException(session, authenticationSession, Response.Status.BAD_REQUEST, Messages.CLIENT_NOT_FOUND);
|
||||
}
|
||||
|
||||
if (!client.isEnabled()) {
|
||||
event.error(Errors.CLIENT_DISABLED);
|
||||
throw new ErrorPageException(session, authenticationSession, Response.Status.BAD_REQUEST, Messages.CLIENT_DISABLED);
|
||||
}
|
||||
|
||||
if (!client.isOAuth2DeviceAuthorizationGrantEnabled()) {
|
||||
event.error(Errors.NOT_ALLOWED);
|
||||
throw new ErrorPageException(session, authenticationSession, Response.Status.BAD_REQUEST, Messages.OAUTH2_DEVICE_AUTHORIZATION_GRANT_DISABLED);
|
||||
}
|
||||
|
||||
if (client.isBearerOnly()) {
|
||||
event.error(Errors.NOT_ALLOWED);
|
||||
throw new ErrorPageException(session, authenticationSession, Response.Status.FORBIDDEN, Messages.BEARER_ONLY);
|
||||
}
|
||||
|
||||
String protocol = client.getProtocol();
|
||||
if (protocol == null) {
|
||||
logger.warnf("Client '%s' doesn't have protocol set. Fallback to openid-connect. Please fix client configuration", clientId);
|
||||
protocol = OIDCLoginProtocol.LOGIN_PROTOCOL;
|
||||
}
|
||||
if (!protocol.equals(OIDCLoginProtocol.LOGIN_PROTOCOL)) {
|
||||
event.error(Errors.INVALID_CLIENT);
|
||||
throw new ErrorPageException(session, authenticationSession, Response.Status.BAD_REQUEST, "Wrong client protocol.");
|
||||
}
|
||||
|
||||
session.getContext().setClient(client);
|
||||
}
|
||||
|
||||
private void updateAuthenticationSession() {
|
||||
authenticationSession.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
|
||||
authenticationSession.setAction(AuthenticationSessionModel.Action.AUTHENTICATE.name());
|
||||
authenticationSession.setClientNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(session.getContext().getUri().getBaseUri(), realm.getName()));
|
||||
|
||||
if (request.getNonce() != null) authenticationSession.setClientNote(OIDCLoginProtocol.NONCE_PARAM, request.getNonce());
|
||||
if (request.getMaxAge() != null) authenticationSession.setClientNote(OIDCLoginProtocol.MAX_AGE_PARAM, String.valueOf(request.getMaxAge()));
|
||||
if (request.getScope() != null) authenticationSession.setClientNote(OIDCLoginProtocol.SCOPE_PARAM, request.getScope());
|
||||
if (request.getLoginHint() != null) authenticationSession.setClientNote(OIDCLoginProtocol.LOGIN_HINT_PARAM, request.getLoginHint());
|
||||
if (request.getPrompt() != null) authenticationSession.setClientNote(OIDCLoginProtocol.PROMPT_PARAM, request.getPrompt());
|
||||
if (request.getIdpHint() != null) authenticationSession.setClientNote(AdapterConstants.KC_IDP_HINT, request.getIdpHint());
|
||||
if (request.getClaims()!= null) authenticationSession.setClientNote(OIDCLoginProtocol.CLAIMS_PARAM, request.getClaims());
|
||||
if (request.getAcr() != null) authenticationSession.setClientNote(OIDCLoginProtocol.ACR_PARAM, request.getAcr());
|
||||
if (request.getDisplay() != null) authenticationSession.setAuthNote(OAuth2Constants.DISPLAY, request.getDisplay());
|
||||
|
||||
if (request.getAdditionalReqParams() != null) {
|
||||
for (String paramName : request.getAdditionalReqParams().keySet()) {
|
||||
authenticationSession.setClientNote(LOGIN_SESSION_NOTE_ADDITIONAL_REQ_PARAMS_PREFIX + paramName, request.getAdditionalReqParams().get(paramName));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Response buildVerificationResponse(String userCode) {
|
||||
this.event.event(EventType.LOGIN);
|
||||
action = Action.OAUTH2_DEVICE_VERIFY_USER_CODE;
|
||||
|
||||
checkSsl();
|
||||
checkRealm();
|
||||
|
||||
// So back button doesn't work
|
||||
CacheControlUtil.noBackButtonCacheControlHeader();
|
||||
|
||||
client = SystemClientUtil.getSystemClient(realm);
|
||||
authenticationSession = createAuthenticationSession(client, null);
|
||||
authenticationSession.setClientNote(APP_INITIATED_FLOW, LoginActionsService.OAUTH2_DEVICE_VERIFICATION_PATH);
|
||||
|
||||
if (StringUtil.isNullOrEmpty(userCode)) {
|
||||
return createVerificationPage(null);
|
||||
} else {
|
||||
return processVerification(userCode);
|
||||
}
|
||||
}
|
||||
|
||||
public Response processVerification(AuthenticationSessionModel authSessionWithSystemClient, String userCode) {
|
||||
authenticationSession = authSessionWithSystemClient;
|
||||
return processVerification(userCode);
|
||||
}
|
||||
|
||||
public Response processVerification(String userCode) {
|
||||
if (userCode == null) {
|
||||
event.error(Errors.INVALID_OAUTH2_USER_CODE);
|
||||
return createVerificationPage(Messages.OAUTH2_DEVICE_INVALID_USER_CODE);
|
||||
}
|
||||
|
||||
// Format inputted user code
|
||||
OAuth2DeviceUserCodeProvider userCodeProvider = session.getProvider(OAuth2DeviceUserCodeProvider.class);
|
||||
String formattedUserCode = userCodeProvider.format(userCode);
|
||||
|
||||
// Find the token from store
|
||||
OAuth2DeviceTokenStoreProvider store = session.getProvider(OAuth2DeviceTokenStoreProvider.class);
|
||||
OAuth2DeviceCodeModel deviceCodeModel = store.getByUserCode(realm, formattedUserCode);
|
||||
if (deviceCodeModel == null) {
|
||||
event.error(Errors.INVALID_OAUTH2_USER_CODE);
|
||||
return createVerificationPage(Messages.OAUTH2_DEVICE_INVALID_USER_CODE);
|
||||
}
|
||||
|
||||
int expiresIn = deviceCodeModel.getExpiration() - Time.currentTime();
|
||||
if (expiresIn < 0) {
|
||||
event.error(Errors.EXPIRED_OAUTH2_USER_CODE);
|
||||
return createVerificationPage(Messages.OAUTH2_DEVICE_INVALID_USER_CODE);
|
||||
}
|
||||
|
||||
// Update authentication session with requested clientId and scope from the device
|
||||
updateAuthenticationSession(deviceCodeModel);
|
||||
|
||||
// Verification OK
|
||||
authenticationSession.setClientNote(OIDCLoginProtocol.OAUTH2_DEVICE_VERIFIED_USER_CODE, formattedUserCode);
|
||||
|
||||
// Event logging for the verification
|
||||
event.client(deviceCodeModel.getClientId())
|
||||
.detail(Details.SCOPE, deviceCodeModel.getScope())
|
||||
.success();
|
||||
|
||||
return redirectToBrowserAuthentication();
|
||||
}
|
||||
|
||||
private AuthenticationSessionModel updateAuthenticationSession(OAuth2DeviceCodeModel deviceCode) {
|
||||
checkClient(deviceCode.getClientId());
|
||||
|
||||
// Create request object using parameters which is used in device authorization request from the device
|
||||
request = AuthorizationEndpointRequestParserProcessor.parseRequest(event, session, client, deviceCode.getParams());
|
||||
|
||||
// Re-create authentication session because current session doesn't have relation with the target device client
|
||||
authenticationSession = createAuthenticationSession(client, null);
|
||||
authenticationSession.setClientNote(APP_INITIATED_FLOW, LoginActionsService.OAUTH2_DEVICE_VERIFICATION_PATH);
|
||||
|
||||
updateAuthenticationSession();
|
||||
|
||||
AuthenticationManager.setClientScopesInSession(authenticationSession);
|
||||
|
||||
return authenticationSession;
|
||||
}
|
||||
|
||||
public Response createVerificationPage(String errorMessage) {
|
||||
String execution = AuthenticatedClientSessionModel.Action.USER_CODE_VERIFICATION.name();
|
||||
|
||||
ClientSessionCode<AuthenticationSessionModel> accessCode = new ClientSessionCode<>(session, realm, authenticationSession);
|
||||
authenticationSession.getParentSession().setTimestamp(Time.currentTime());
|
||||
|
||||
accessCode.setAction(AuthenticationSessionModel.Action.USER_CODE_VERIFICATION.name());
|
||||
authenticationSession.setAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION, execution);
|
||||
|
||||
LoginFormsProvider provider = session.getProvider(LoginFormsProvider.class)
|
||||
.setAuthenticationSession(authenticationSession)
|
||||
.setExecution(execution)
|
||||
.setClientSessionCode(accessCode.getOrGenerateCode());
|
||||
|
||||
if (errorMessage != null) {
|
||||
provider = provider.setError(errorMessage);
|
||||
}
|
||||
|
||||
return provider.createOAuth2DeviceVerifyUserCodePage();
|
||||
}
|
||||
|
||||
public Response redirectToBrowserAuthentication() {
|
||||
return handleBrowserAuthenticationRequest(authenticationSession, new OIDCLoginProtocol(session, realm, session.getContext().getUri(), headers, event), false, true);
|
||||
}
|
||||
}
|
|
@ -39,7 +39,6 @@ import org.keycloak.common.Profile;
|
|||
import org.keycloak.common.constants.ServiceAccountConstants;
|
||||
import org.keycloak.common.util.Base64Url;
|
||||
import org.keycloak.common.util.KeycloakUriBuilder;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.constants.AdapterConstants;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.Errors;
|
||||
|
@ -57,14 +56,13 @@ import org.keycloak.models.IdentityProviderMapperModel;
|
|||
import org.keycloak.models.IdentityProviderModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.models.OAuth2DeviceCodeModel;
|
||||
import org.keycloak.models.OAuth2DeviceTokenStoreProvider;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.models.utils.AuthenticationFlowResolver;
|
||||
import org.keycloak.protocol.LoginProtocol;
|
||||
import org.keycloak.protocol.LoginProtocolFactory;
|
||||
import org.keycloak.protocol.oidc.grants.device.DeviceGrantType;
|
||||
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.protocol.oidc.TokenManager;
|
||||
|
@ -74,7 +72,6 @@ import org.keycloak.protocol.saml.JaxrsSAML2BindingBuilder;
|
|||
import org.keycloak.protocol.saml.SamlClient;
|
||||
import org.keycloak.protocol.saml.SamlProtocol;
|
||||
import org.keycloak.protocol.saml.SamlService;
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
import org.keycloak.representations.AccessToken;
|
||||
import org.keycloak.representations.AccessTokenResponse;
|
||||
import org.keycloak.representations.JsonWebToken;
|
||||
|
@ -99,7 +96,6 @@ import org.keycloak.services.managers.ClientManager;
|
|||
import org.keycloak.protocol.oidc.utils.OAuth2Code;
|
||||
import org.keycloak.protocol.oidc.utils.OAuth2CodeParser;
|
||||
import org.keycloak.services.managers.RealmManager;
|
||||
import org.keycloak.services.managers.UserSessionCrossDCManager;
|
||||
import org.keycloak.services.resources.Cors;
|
||||
import org.keycloak.services.resources.IdentityBrokerService;
|
||||
import org.keycloak.services.resources.admin.AdminAuth;
|
||||
|
@ -453,11 +449,16 @@ public class TokenEndpoint {
|
|||
// Set nonce as an attribute in the ClientSessionContext. Will be used for the token generation
|
||||
clientSessionCtx.setAttribute(OIDCLoginProtocol.NONCE_PARAM, codeData.getNonce());
|
||||
|
||||
return codeOrDeviceCodeToToken(user, userSession, clientSessionCtx, scopeParam, true);
|
||||
}
|
||||
|
||||
public Response codeOrDeviceCodeToToken(UserModel user, UserSessionModel userSession, ClientSessionContext clientSessionCtx,
|
||||
String scopeParam, boolean code) {
|
||||
AccessToken token = tokenManager.createClientAccessToken(session, realm, client, user, userSession, clientSessionCtx);
|
||||
|
||||
TokenManager.AccessTokenResponseBuilder responseBuilder = tokenManager.responseBuilder(realm, client, event, session, userSession, clientSessionCtx)
|
||||
.accessToken(token)
|
||||
.generateRefreshToken();
|
||||
TokenManager.AccessTokenResponseBuilder responseBuilder = tokenManager
|
||||
.responseBuilder(realm, client, event, session, userSession, clientSessionCtx).accessToken(token)
|
||||
.generateRefreshToken();
|
||||
|
||||
// KEYCLOAK-6771 Certificate Bound Token
|
||||
// https://tools.ietf.org/html/draft-ietf-oauth-mtls-08#section-3
|
||||
|
@ -468,25 +469,30 @@ public class TokenEndpoint {
|
|||
responseBuilder.getRefreshToken().setCertConf(certConf);
|
||||
} else {
|
||||
event.error(Errors.INVALID_REQUEST);
|
||||
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST, "Client Certification missing for MTLS HoK Token Binding", Response.Status.BAD_REQUEST);
|
||||
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST,
|
||||
"Client Certification missing for MTLS HoK Token Binding", Response.Status.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
if (TokenUtil.isOIDCRequest(scopeParam)) {
|
||||
responseBuilder.generateIDToken().generateAccessTokenHash();
|
||||
}
|
||||
|
||||
|
||||
AccessTokenResponse res = null;
|
||||
try {
|
||||
res = responseBuilder.build();
|
||||
} catch (RuntimeException re) {
|
||||
if ("can not get encryption KEK".equals(re.getMessage())) {
|
||||
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST, "can not get encryption KEK", Response.Status.BAD_REQUEST);
|
||||
} else {
|
||||
throw re;
|
||||
if (code) {
|
||||
try {
|
||||
res = responseBuilder.build();
|
||||
} catch (RuntimeException re) {
|
||||
if ("can not get encryption KEK".equals(re.getMessage())) {
|
||||
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST,
|
||||
"can not get encryption KEK", Response.Status.BAD_REQUEST);
|
||||
} else {
|
||||
throw re;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
res = responseBuilder.build();
|
||||
}
|
||||
|
||||
event.success();
|
||||
|
||||
return cors.builder(Response.ok(res).type(MediaType.APPLICATION_JSON_TYPE)).build();
|
||||
|
@ -1375,134 +1381,8 @@ public class TokenEndpoint {
|
|||
}
|
||||
|
||||
public Response oauth2DeviceCodeToToken() {
|
||||
if (!client.isOAuth2DeviceAuthorizationGrantEnabled()) {
|
||||
event.error(Errors.NOT_ALLOWED);
|
||||
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, "Client not allowed OAuth 2.0 Device Authorization Grant", Response.Status.BAD_REQUEST);
|
||||
}
|
||||
|
||||
String deviceCode = formParams.getFirst(OAuth2Constants.DEVICE_CODE);
|
||||
if (deviceCode == null) {
|
||||
event.error(Errors.INVALID_OAUTH2_DEVICE_CODE);
|
||||
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST, "Missing parameter: " + OAuth2Constants.DEVICE_CODE, Response.Status.BAD_REQUEST);
|
||||
}
|
||||
|
||||
OAuth2DeviceTokenStoreProvider store = session.getProvider(OAuth2DeviceTokenStoreProvider.class);
|
||||
OAuth2DeviceCodeModel deviceCodeModel = store.getByDeviceCode(realm, deviceCode);
|
||||
|
||||
if (deviceCodeModel == null) {
|
||||
event.error(Errors.INVALID_OAUTH2_DEVICE_CODE);
|
||||
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, "Device code not valid", Response.Status.BAD_REQUEST);
|
||||
}
|
||||
|
||||
if (!store.isPollingAllowed(deviceCodeModel)) {
|
||||
event.error(Errors.SLOW_DOWN);
|
||||
throw new CorsErrorResponseException(cors, OAuthErrorException.SLOW_DOWN, "Slow down", Response.Status.BAD_REQUEST);
|
||||
}
|
||||
|
||||
int expiresIn = deviceCodeModel.getExpiration() - Time.currentTime();
|
||||
if (expiresIn < 0) {
|
||||
event.error(Errors.EXPIRED_OAUTH2_DEVICE_CODE);
|
||||
throw new CorsErrorResponseException(cors, OAuthErrorException.EXPIRED_TOKEN, "Device code is expired", Response.Status.BAD_REQUEST);
|
||||
}
|
||||
|
||||
if (deviceCodeModel.isPending()) {
|
||||
throw new CorsErrorResponseException(cors, OAuthErrorException.AUTHORIZATION_PENDING, "The authorization request is still pending", Response.Status.BAD_REQUEST);
|
||||
}
|
||||
|
||||
if (deviceCodeModel.isDenied()) {
|
||||
event.error(Errors.ACCESS_DENIED);
|
||||
throw new CorsErrorResponseException(cors, OAuthErrorException.ACCESS_DENIED, "The end user denied the authorization request", Response.Status.BAD_REQUEST);
|
||||
}
|
||||
|
||||
// Approved
|
||||
|
||||
String userSessionId = deviceCodeModel.getUserSessionId();
|
||||
event.detail(Details.CODE_ID, userSessionId);
|
||||
event.session(userSessionId);
|
||||
|
||||
// Retrieve UserSession
|
||||
UserSessionModel userSession = new UserSessionCrossDCManager(session)
|
||||
.getUserSessionWithClient(realm, userSessionId, client.getId());
|
||||
|
||||
if (userSession == null) {
|
||||
userSession = session.sessions().getUserSession(realm, userSessionId);
|
||||
if (userSession == null) {
|
||||
throw new CorsErrorResponseException(cors, OAuthErrorException.AUTHORIZATION_PENDING, "The authorization request is verified but can not lookup the user session yet", Response.Status.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
// Now, remove the device code
|
||||
store.removeDeviceCode(realm, deviceCode);
|
||||
|
||||
UserModel user = userSession.getUser();
|
||||
if (user == null) {
|
||||
event.error(Errors.USER_NOT_FOUND);
|
||||
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, "User not found", Response.Status.BAD_REQUEST);
|
||||
}
|
||||
|
||||
event.user(userSession.getUser());
|
||||
|
||||
if (!user.isEnabled()) {
|
||||
event.error(Errors.USER_DISABLED);
|
||||
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, "User disabled", Response.Status.BAD_REQUEST);
|
||||
}
|
||||
|
||||
AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessionByClient(client.getId());
|
||||
if (!client.getClientId().equals(clientSession.getClient().getClientId())) {
|
||||
event.error(Errors.INVALID_CODE);
|
||||
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, "Auth error", Response.Status.BAD_REQUEST);
|
||||
}
|
||||
|
||||
if (!AuthenticationManager.isSessionValid(realm, userSession)) {
|
||||
event.error(Errors.USER_SESSION_NOT_FOUND);
|
||||
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, "Session not active", Response.Status.BAD_REQUEST);
|
||||
}
|
||||
|
||||
updateClientSession(clientSession);
|
||||
updateUserSessionFromClientAuth(userSession);
|
||||
|
||||
// Compute client scopes again from scope parameter. Check if user still has them granted
|
||||
// (but in device_code-to-token request, it could just theoretically happen that they are not available)
|
||||
String scopeParam = deviceCodeModel.getScope();
|
||||
Stream<ClientScopeModel> clientScopes = TokenManager.getRequestedClientScopes(scopeParam, client);
|
||||
if (!TokenManager.verifyConsentStillAvailable(session, user, client, clientScopes)) {
|
||||
event.error(Errors.NOT_ALLOWED);
|
||||
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_SCOPE, "Client no longer has requested consent from user", Response.Status.BAD_REQUEST);
|
||||
}
|
||||
|
||||
ClientSessionContext clientSessionCtx = DefaultClientSessionContext.fromClientSessionAndClientScopes(clientSession, clientScopes, session);
|
||||
|
||||
// Set nonce as an attribute in the ClientSessionContext. Will be used for the token generation
|
||||
clientSessionCtx.setAttribute(OIDCLoginProtocol.NONCE_PARAM, deviceCodeModel.getNonce());
|
||||
|
||||
AccessToken token = tokenManager.createClientAccessToken(session, realm, client, user, userSession, clientSessionCtx);
|
||||
|
||||
TokenManager.AccessTokenResponseBuilder responseBuilder = tokenManager.responseBuilder(realm, client, event, session, userSession, clientSessionCtx)
|
||||
.accessToken(token)
|
||||
.generateRefreshToken();
|
||||
|
||||
// KEYCLOAK-6771 Certificate Bound Token
|
||||
// https://tools.ietf.org/html/draft-ietf-oauth-mtls-08#section-3
|
||||
if (OIDCAdvancedConfigWrapper.fromClientModel(client).isUseMtlsHokToken()) {
|
||||
AccessToken.CertConf certConf = MtlsHoKTokenUtil.bindTokenWithClientCertificate(request, session);
|
||||
if (certConf != null) {
|
||||
responseBuilder.getAccessToken().setCertConf(certConf);
|
||||
responseBuilder.getRefreshToken().setCertConf(certConf);
|
||||
} else {
|
||||
event.error(Errors.INVALID_REQUEST);
|
||||
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST, "Client Certification missing for MTLS HoK Token Binding", Response.Status.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
if (TokenUtil.isOIDCRequest(scopeParam)) {
|
||||
responseBuilder.generateIDToken();
|
||||
}
|
||||
|
||||
AccessTokenResponse res = responseBuilder.build();
|
||||
|
||||
event.success();
|
||||
|
||||
return cors.builder(Response.ok(res).type(MediaType.APPLICATION_JSON_TYPE)).build();
|
||||
DeviceGrantType deviceGrantType = new DeviceGrantType(formParams, client, session, this, realm, event, cors);
|
||||
return deviceGrantType.oauth2DeviceFlow();
|
||||
}
|
||||
|
||||
// https://tools.ietf.org/html/rfc7636#section-4.1
|
||||
|
|
|
@ -0,0 +1,290 @@
|
|||
/*
|
||||
* Copyright 2019 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* http://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.keycloak.protocol.oidc.grants.device;
|
||||
|
||||
import static org.keycloak.protocol.oidc.OIDCLoginProtocolService.tokenServiceBaseUrl;
|
||||
|
||||
import org.jboss.resteasy.spi.HttpRequest;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.OAuthErrorException;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.Errors;
|
||||
import org.keycloak.events.EventBuilder;
|
||||
import org.keycloak.models.AuthenticatedClientSessionModel;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.ClientScopeModel;
|
||||
import org.keycloak.models.ClientSessionContext;
|
||||
import org.keycloak.models.KeycloakContext;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakUriInfo;
|
||||
import org.keycloak.models.OAuth2DeviceCodeModel;
|
||||
import org.keycloak.models.OAuth2DeviceTokenStoreProvider;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.protocol.LoginProtocol;
|
||||
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
|
||||
import org.keycloak.protocol.oidc.TokenManager;
|
||||
import org.keycloak.protocol.oidc.endpoints.AuthorizationEndpoint;
|
||||
import org.keycloak.protocol.oidc.endpoints.TokenEndpoint;
|
||||
import org.keycloak.protocol.oidc.grants.device.endpoints.DeviceEndpoint;
|
||||
import org.keycloak.representations.AccessToken;
|
||||
import org.keycloak.representations.AccessTokenResponse;
|
||||
import org.keycloak.services.CorsErrorResponseException;
|
||||
import org.keycloak.services.Urls;
|
||||
import org.keycloak.services.managers.AuthenticationManager;
|
||||
import org.keycloak.services.managers.UserSessionCrossDCManager;
|
||||
import org.keycloak.services.resources.Cors;
|
||||
import org.keycloak.services.resources.LoginActionsService;
|
||||
import org.keycloak.services.resources.RealmsResource;
|
||||
import org.keycloak.services.util.DefaultClientSessionContext;
|
||||
import org.keycloak.services.util.MtlsHoKTokenUtil;
|
||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||
import org.keycloak.util.TokenUtil;
|
||||
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.MultivaluedMap;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.UriBuilder;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:h2-wada@nri.co.jp">Hiroyuki Wada</a>
|
||||
* @author <a href="mailto:michito.okai.zn@hitachi.com">Michito Okai</a>
|
||||
*/
|
||||
public class DeviceGrantType {
|
||||
|
||||
// OAuth 2.0 Device Authorization Grant
|
||||
public static final String OAUTH2_DEVICE_VERIFIED_USER_CODE = "OAUTH2_DEVICE_VERIFIED_USER_CODE";
|
||||
public static final String OAUTH2_DEVICE_USER_CODE = "device_user_code";
|
||||
public static final String OAUTH2_USER_CODE_VERIFY = "device/verify";
|
||||
|
||||
public static UriBuilder oauth2DeviceVerificationUrl(UriInfo uriInfo) {
|
||||
UriBuilder baseUriBuilder = uriInfo.getBaseUriBuilder();
|
||||
return baseUriBuilder.path(RealmsResource.class).path("{realm}").path("device");
|
||||
}
|
||||
|
||||
public static URI realmOAuth2DeviceVerificationAction(URI baseUri, String realmName) {
|
||||
return UriBuilder.fromUri(baseUri).path(RealmsResource.class).path("{realm}").path("device")
|
||||
.build(realmName);
|
||||
}
|
||||
|
||||
public static UriBuilder oauth2DeviceAuthUrl(UriBuilder baseUriBuilder) {
|
||||
UriBuilder uriBuilder = tokenServiceBaseUrl(baseUriBuilder);
|
||||
return uriBuilder.path(OIDCLoginProtocolService.class, "auth").path(AuthorizationEndpoint.class, "authorizeDevice")
|
||||
.path(DeviceEndpoint.class, "handleDeviceRequest");
|
||||
}
|
||||
|
||||
public static UriBuilder oauth2DeviceVerificationCompletedUrl(UriInfo baseUri) {
|
||||
return baseUri.getBaseUriBuilder().path(RealmsResource.class).path("{realm}").path("device").path("status");
|
||||
}
|
||||
|
||||
public static Response denyOAuth2DeviceAuthorization(AuthenticationSessionModel authSession, LoginProtocol.Error error, KeycloakSession session) {
|
||||
KeycloakContext context = session.getContext();
|
||||
RealmModel realm = context.getRealm();
|
||||
KeycloakUriInfo uri = context.getUri();
|
||||
UriBuilder uriBuilder = DeviceGrantType.oauth2DeviceVerificationCompletedUrl(uri);
|
||||
String errorType = OAuthErrorException.SERVER_ERROR;
|
||||
if (error == LoginProtocol.Error.CONSENT_DENIED) {
|
||||
String verifiedUserCode = authSession.getClientNote(DeviceGrantType.OAUTH2_DEVICE_VERIFIED_USER_CODE);
|
||||
OAuth2DeviceTokenStoreProvider store = session.getProvider(OAuth2DeviceTokenStoreProvider.class);
|
||||
if (!store.deny(realm, verifiedUserCode)) {
|
||||
// Already expired and removed in the store
|
||||
errorType = OAuthErrorException.EXPIRED_TOKEN;
|
||||
} else {
|
||||
errorType = OAuthErrorException.ACCESS_DENIED;
|
||||
}
|
||||
}
|
||||
return Response.status(302).location(
|
||||
uriBuilder.queryParam(OAuth2Constants.ERROR, errorType)
|
||||
.build(realm.getName())
|
||||
).build();
|
||||
}
|
||||
|
||||
public static Response approveOAuth2DeviceAuthorization(AuthenticationSessionModel authSession, AuthenticatedClientSessionModel clientSession, KeycloakSession session) {
|
||||
KeycloakContext context = session.getContext();
|
||||
RealmModel realm = context.getRealm();
|
||||
KeycloakUriInfo uriInfo = context.getUri();
|
||||
UriBuilder uriBuilder = DeviceGrantType.oauth2DeviceVerificationCompletedUrl(uriInfo);
|
||||
|
||||
String verifiedUserCode = authSession.getClientNote(DeviceGrantType.OAUTH2_DEVICE_VERIFIED_USER_CODE);
|
||||
String userSessionId = clientSession.getUserSession().getId();
|
||||
OAuth2DeviceTokenStoreProvider store = session.getProvider(OAuth2DeviceTokenStoreProvider.class);
|
||||
if (!store.approve(realm, verifiedUserCode, userSessionId)) {
|
||||
// Already expired and removed in the store
|
||||
return Response.status(302).location(
|
||||
uriBuilder.queryParam(OAuth2Constants.ERROR, OAuthErrorException.EXPIRED_TOKEN)
|
||||
.build(realm.getName())
|
||||
).build();
|
||||
}
|
||||
|
||||
// Now, remove the verified user code
|
||||
store.removeUserCode(realm, verifiedUserCode);
|
||||
|
||||
return Response.status(302).location(
|
||||
uriBuilder.build(realm.getName())
|
||||
).build();
|
||||
}
|
||||
|
||||
public static boolean isOAuth2DeviceVerificationFlow(final AuthenticationSessionModel authSession) {
|
||||
String flow = authSession.getClientNote(DeviceGrantType.OAUTH2_DEVICE_VERIFIED_USER_CODE);
|
||||
return flow != null;
|
||||
}
|
||||
|
||||
private MultivaluedMap<String, String> formParams;
|
||||
private ClientModel client;
|
||||
|
||||
private KeycloakSession session;
|
||||
|
||||
private TokenEndpoint tokenEndpoint;
|
||||
|
||||
private final RealmModel realm;
|
||||
private final EventBuilder event;
|
||||
|
||||
private Cors cors;
|
||||
|
||||
public DeviceGrantType(MultivaluedMap<String, String> formParams, ClientModel client, KeycloakSession session,
|
||||
TokenEndpoint tokenEndpoint, RealmModel realm, EventBuilder event, Cors cors) {
|
||||
this.formParams = formParams;
|
||||
this.client = client;
|
||||
this.session = session;
|
||||
this.tokenEndpoint = tokenEndpoint;
|
||||
this.realm = realm;
|
||||
this.event = event;
|
||||
this.cors = cors;
|
||||
}
|
||||
|
||||
public Response oauth2DeviceFlow() {
|
||||
if (!realm.getOAuth2DeviceConfig().isOAuth2DeviceAuthorizationGrantEnabled(client)) {
|
||||
event.error(Errors.NOT_ALLOWED);
|
||||
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT,
|
||||
"Client not allowed OAuth 2.0 Device Authorization Grant", Response.Status.BAD_REQUEST);
|
||||
}
|
||||
|
||||
String deviceCode = formParams.getFirst(OAuth2Constants.DEVICE_CODE);
|
||||
if (deviceCode == null) {
|
||||
event.error(Errors.INVALID_OAUTH2_DEVICE_CODE);
|
||||
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST,
|
||||
"Missing parameter: " + OAuth2Constants.DEVICE_CODE, Response.Status.BAD_REQUEST);
|
||||
}
|
||||
|
||||
OAuth2DeviceTokenStoreProvider store = session.getProvider(OAuth2DeviceTokenStoreProvider.class);
|
||||
OAuth2DeviceCodeModel deviceCodeModel = store.getByDeviceCode(realm, deviceCode);
|
||||
|
||||
if (deviceCodeModel == null) {
|
||||
event.error(Errors.INVALID_OAUTH2_DEVICE_CODE);
|
||||
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, "Device code not valid",
|
||||
Response.Status.BAD_REQUEST);
|
||||
}
|
||||
|
||||
if (deviceCodeModel.isExpired()) {
|
||||
event.error(Errors.EXPIRED_OAUTH2_DEVICE_CODE);
|
||||
throw new CorsErrorResponseException(cors, OAuthErrorException.EXPIRED_TOKEN, "Device code is expired",
|
||||
Response.Status.BAD_REQUEST);
|
||||
}
|
||||
|
||||
if (!store.isPollingAllowed(deviceCodeModel)) {
|
||||
event.error(Errors.SLOW_DOWN);
|
||||
throw new CorsErrorResponseException(cors, OAuthErrorException.SLOW_DOWN, "Slow down", Response.Status.BAD_REQUEST);
|
||||
}
|
||||
|
||||
if (deviceCodeModel.isDenied()) {
|
||||
event.error(Errors.ACCESS_DENIED);
|
||||
throw new CorsErrorResponseException(cors, OAuthErrorException.ACCESS_DENIED,
|
||||
"The end user denied the authorization request", Response.Status.BAD_REQUEST);
|
||||
}
|
||||
|
||||
if (deviceCodeModel.isPending()) {
|
||||
throw new CorsErrorResponseException(cors, OAuthErrorException.AUTHORIZATION_PENDING,
|
||||
"The authorization request is still pending", Response.Status.BAD_REQUEST);
|
||||
}
|
||||
|
||||
// Approved
|
||||
|
||||
String userSessionId = deviceCodeModel.getUserSessionId();
|
||||
event.detail(Details.CODE_ID, userSessionId);
|
||||
event.session(userSessionId);
|
||||
|
||||
// Retrieve UserSession
|
||||
UserSessionModel userSession = new UserSessionCrossDCManager(session).getUserSessionWithClient(realm, userSessionId,
|
||||
client.getId());
|
||||
|
||||
if (userSession == null) {
|
||||
userSession = session.sessions().getUserSession(realm, userSessionId);
|
||||
if (userSession == null) {
|
||||
throw new CorsErrorResponseException(cors, OAuthErrorException.AUTHORIZATION_PENDING,
|
||||
"The authorization request is verified but can not lookup the user session yet",
|
||||
Response.Status.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
// Now, remove the device code
|
||||
store.removeDeviceCode(realm, deviceCode);
|
||||
|
||||
UserModel user = userSession.getUser();
|
||||
if (user == null) {
|
||||
event.error(Errors.USER_NOT_FOUND);
|
||||
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, "User not found",
|
||||
Response.Status.BAD_REQUEST);
|
||||
}
|
||||
|
||||
event.user(userSession.getUser());
|
||||
|
||||
if (!user.isEnabled()) {
|
||||
event.error(Errors.USER_DISABLED);
|
||||
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, "User disabled",
|
||||
Response.Status.BAD_REQUEST);
|
||||
}
|
||||
|
||||
AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessionByClient(client.getId());
|
||||
if (!client.getClientId().equals(clientSession.getClient().getClientId())) {
|
||||
event.error(Errors.INVALID_CODE);
|
||||
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, "Auth error",
|
||||
Response.Status.BAD_REQUEST);
|
||||
}
|
||||
|
||||
if (!AuthenticationManager.isSessionValid(realm, userSession)) {
|
||||
event.error(Errors.USER_SESSION_NOT_FOUND);
|
||||
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, "Session not active",
|
||||
Response.Status.BAD_REQUEST);
|
||||
}
|
||||
|
||||
// Compute client scopes again from scope parameter. Check if user still has them granted
|
||||
// (but in device_code-to-token request, it could just theoretically happen that they are not available)
|
||||
String scopeParam = deviceCodeModel.getScope();
|
||||
Stream<ClientScopeModel> clientScopes = TokenManager.getRequestedClientScopes(scopeParam, client);
|
||||
if (!TokenManager.verifyConsentStillAvailable(session, user, client, clientScopes)) {
|
||||
event.error(Errors.NOT_ALLOWED);
|
||||
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_SCOPE,
|
||||
"Client no longer has requested consent from user", Response.Status.BAD_REQUEST);
|
||||
}
|
||||
|
||||
ClientSessionContext clientSessionCtx = DefaultClientSessionContext.fromClientSessionAndClientScopes(clientSession,
|
||||
clientScopes, session);
|
||||
|
||||
// Set nonce as an attribute in the ClientSessionContext. Will be used for the token generation
|
||||
clientSessionCtx.setAttribute(OIDCLoginProtocol.NONCE_PARAM, deviceCodeModel.getNonce());
|
||||
|
||||
return tokenEndpoint.codeOrDeviceCodeToToken(user, userSession, clientSessionCtx, scopeParam, false);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,367 @@
|
|||
/*
|
||||
* Copyright 2019 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* http://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.keycloak.protocol.oidc.grants.device.endpoints;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.OAuthErrorException;
|
||||
import org.keycloak.authentication.AuthenticationProcessor;
|
||||
import org.keycloak.common.util.Base64Url;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.constants.AdapterConstants;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.Errors;
|
||||
import org.keycloak.events.EventBuilder;
|
||||
import org.keycloak.events.EventType;
|
||||
import org.keycloak.forms.login.LoginFormsProvider;
|
||||
import org.keycloak.models.AuthenticatedClientSessionModel;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.OAuth2DeviceCodeModel;
|
||||
import org.keycloak.models.OAuth2DeviceTokenStoreProvider;
|
||||
import org.keycloak.models.OAuth2DeviceUserCodeModel;
|
||||
import org.keycloak.models.OAuth2DeviceUserCodeProvider;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.models.utils.SystemClientUtil;
|
||||
import org.keycloak.protocol.AuthorizationEndpointBase;
|
||||
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.protocol.oidc.endpoints.request.AuthorizationEndpointRequest;
|
||||
import org.keycloak.protocol.oidc.endpoints.request.AuthorizationEndpointRequestParserProcessor;
|
||||
import org.keycloak.protocol.oidc.grants.device.DeviceGrantType;
|
||||
import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil;
|
||||
import org.keycloak.representations.OAuth2DeviceAuthorizationResponse;
|
||||
import org.keycloak.saml.common.util.StringUtil;
|
||||
import org.keycloak.services.ErrorPageException;
|
||||
import org.keycloak.services.ErrorResponseException;
|
||||
import org.keycloak.services.ServicesLogger;
|
||||
import org.keycloak.services.Urls;
|
||||
import org.keycloak.services.messages.Messages;
|
||||
import org.keycloak.services.resource.RealmResourceProvider;
|
||||
import org.keycloak.services.resources.SessionCodeChecks;
|
||||
import org.keycloak.services.util.CacheControlUtil;
|
||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
import org.keycloak.util.TokenUtil;
|
||||
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.HttpMethod;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.MultivaluedMap;
|
||||
import javax.ws.rs.core.Response;
|
||||
|
||||
import static org.keycloak.protocol.oidc.endpoints.AuthorizationEndpoint.LOGIN_SESSION_NOTE_ADDITIONAL_REQ_PARAMS_PREFIX;
|
||||
import static org.keycloak.protocol.oidc.grants.device.DeviceGrantType.OAUTH2_DEVICE_USER_CODE;
|
||||
import static org.keycloak.protocol.oidc.grants.device.DeviceGrantType.OAUTH2_USER_CODE_VERIFY;
|
||||
import static org.keycloak.services.resources.LoginActionsService.SESSION_CODE;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:h2-wada@nri.co.jp">Hiroyuki Wada</a>
|
||||
*/
|
||||
public class DeviceEndpoint extends AuthorizationEndpointBase implements RealmResourceProvider {
|
||||
|
||||
protected static final Logger logger = Logger.getLogger(DeviceEndpoint.class);
|
||||
|
||||
public DeviceEndpoint(RealmModel realm, EventBuilder event) {
|
||||
super(realm, event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles device authorization requests.
|
||||
*
|
||||
* @return the device authorization response.
|
||||
*/
|
||||
@Path("")
|
||||
@POST
|
||||
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Response handleDeviceRequest() {
|
||||
logger.trace("Processing @POST request");
|
||||
event.event(EventType.OAUTH2_DEVICE_AUTH);
|
||||
|
||||
checkSsl();
|
||||
checkRealm();
|
||||
|
||||
ClientModel client = authenticateClient();
|
||||
|
||||
AuthorizationEndpointRequest request = AuthorizationEndpointRequestParserProcessor.parseRequest(event, session, client,
|
||||
httpRequest.getDecodedFormParameters());
|
||||
|
||||
if (!TokenUtil.isOIDCRequest(request.getScope())) {
|
||||
ServicesLogger.LOGGER.oidcScopeMissing();
|
||||
}
|
||||
|
||||
// So back button doesn't work
|
||||
CacheControlUtil.noBackButtonCacheControlHeader();
|
||||
|
||||
if (!realm.getOAuth2DeviceConfig().isOAuth2DeviceAuthorizationGrantEnabled(client)) {
|
||||
event.error(Errors.NOT_ALLOWED);
|
||||
throw new ErrorResponseException(OAuthErrorException.INVALID_GRANT,
|
||||
"Client not allowed for OAuth 2.0 Device Authorization Grant", Response.Status.BAD_REQUEST);
|
||||
}
|
||||
|
||||
int expiresIn = realm.getOAuth2DeviceConfig().getLifespan(client);
|
||||
int interval = realm.getOAuth2DeviceConfig().getPoolingInterval(client);
|
||||
|
||||
OAuth2DeviceCodeModel deviceCode = OAuth2DeviceCodeModel.create(realm, client,
|
||||
Base64Url.encode(KeycloakModelUtils.generateSecret()), request.getScope(), request.getNonce(), expiresIn, interval,
|
||||
request.getAdditionalReqParams());
|
||||
OAuth2DeviceUserCodeProvider userCodeProvider = session.getProvider(OAuth2DeviceUserCodeProvider.class);
|
||||
String secret = userCodeProvider.generate();
|
||||
OAuth2DeviceUserCodeModel userCode = new OAuth2DeviceUserCodeModel(realm, deviceCode.getDeviceCode(), secret);
|
||||
|
||||
// To inform "expired_token" to the client, the lifespan of the cache provider is longer than device code
|
||||
int lifespanSeconds = expiresIn + interval + 10;
|
||||
|
||||
OAuth2DeviceTokenStoreProvider store = session.getProvider(OAuth2DeviceTokenStoreProvider.class);
|
||||
store.put(deviceCode, userCode, lifespanSeconds);
|
||||
|
||||
try {
|
||||
String deviceUrl = DeviceGrantType.oauth2DeviceVerificationUrl(session.getContext().getUri()).build(realm.getName())
|
||||
.toString();
|
||||
|
||||
OAuth2DeviceAuthorizationResponse response = new OAuth2DeviceAuthorizationResponse();
|
||||
response.setDeviceCode(deviceCode.getDeviceCode());
|
||||
response.setUserCode(userCodeProvider.display(secret));
|
||||
response.setExpiresIn(expiresIn);
|
||||
response.setInterval(interval);
|
||||
response.setVerificationUri(deviceUrl);
|
||||
response.setVerificationUriComplete(deviceUrl + "?user_code=" + response.getUserCode());
|
||||
|
||||
return Response.ok(JsonSerialization.writeValueAsBytes(response)).type(MediaType.APPLICATION_JSON_TYPE).build();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Error creating OAuth 2.0 Device Authorization Response.", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This endpoint is used by end-users to start the flow to authorize a device.
|
||||
*
|
||||
* @param userCode the user code to authorize
|
||||
* @return
|
||||
*/
|
||||
@GET
|
||||
public Response verifyUserCode(@QueryParam("user_code") String userCode) {
|
||||
event.event(EventType.OAUTH2_DEVICE_VERIFY_USER_CODE);
|
||||
|
||||
checkSsl();
|
||||
checkRealm();
|
||||
|
||||
// So back button doesn't work
|
||||
CacheControlUtil.noBackButtonCacheControlHeader();
|
||||
|
||||
// code is not known, we can infer the client neither. ask the user to provide the code.
|
||||
if (StringUtil.isNullOrEmpty(userCode)) {
|
||||
return createVerificationPage(null);
|
||||
} else {
|
||||
// code exists, probably due to using a verification_uri_complete. Start the authentication considering the client
|
||||
// that started the flow.
|
||||
OAuth2DeviceTokenStoreProvider store = session.getProvider(OAuth2DeviceTokenStoreProvider.class);
|
||||
OAuth2DeviceUserCodeProvider userCodeProvider = session.getProvider(OAuth2DeviceUserCodeProvider.class);
|
||||
String formattedUserCode = userCodeProvider.format(userCode);
|
||||
OAuth2DeviceCodeModel deviceCode = store.getByUserCode(realm, formattedUserCode);
|
||||
|
||||
if (deviceCode == null) {
|
||||
event.error(Errors.INVALID_OAUTH2_USER_CODE);
|
||||
return createVerificationPage(Messages.OAUTH2_DEVICE_INVALID_USER_CODE);
|
||||
}
|
||||
|
||||
if (deviceCode.isExpired()) {
|
||||
event.error(Errors.INVALID_OAUTH2_USER_CODE);
|
||||
return createVerificationPage(Messages.OAUTH2_DEVICE_EXPIRED_USER_CODE);
|
||||
}
|
||||
|
||||
return processVerification(deviceCode, formattedUserCode);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies the code provided by the end-user and start the authentication.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Path("/")
|
||||
@POST
|
||||
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
public Response verifyUserCode() {
|
||||
MultivaluedMap<String, String> formData = httpRequest.getDecodedFormParameters();
|
||||
return verifyUserCode(formData.getFirst(OAUTH2_DEVICE_USER_CODE));
|
||||
}
|
||||
|
||||
/**
|
||||
* Showing the result of verification process for OAuth 2.0 Device Authorization Grant. This outputs login success or
|
||||
* failure messages.
|
||||
*
|
||||
* @param error
|
||||
* @return
|
||||
*/
|
||||
@Path("status")
|
||||
@GET
|
||||
public Response status(@QueryParam("error") String error) {
|
||||
if (!StringUtil.isNullOrEmpty(error)) {
|
||||
String message;
|
||||
switch (error) {
|
||||
case OAuthErrorException.ACCESS_DENIED:
|
||||
// cased by CANCELLED_BY_USER or CONSENT_DENIED:
|
||||
message = Messages.OAUTH2_DEVICE_CONSENT_DENIED;
|
||||
break;
|
||||
case OAuthErrorException.EXPIRED_TOKEN:
|
||||
message = Messages.OAUTH2_DEVICE_EXPIRED_USER_CODE;
|
||||
break;
|
||||
default:
|
||||
message = Messages.OAUTH2_DEVICE_VERIFICATION_FAILED;
|
||||
}
|
||||
LoginFormsProvider forms = session.getProvider(LoginFormsProvider.class);
|
||||
String restartUri = DeviceGrantType.oauth2DeviceVerificationUrl(session.getContext().getUri())
|
||||
.build(realm.getName()).toString();
|
||||
return forms.setAttribute("messageHeader", forms.getMessage(Messages.OAUTH2_DEVICE_VERIFICATION_FAILED_HEADER))
|
||||
.setAttribute(Constants.TEMPLATE_ATTR_ACTION_URI, restartUri).setError(message).createInfoPage();
|
||||
} else {
|
||||
LoginFormsProvider forms = session.getProvider(LoginFormsProvider.class);
|
||||
return forms.setAttribute("messageHeader", forms.getMessage(Messages.OAUTH2_DEVICE_VERIFICATION_COMPLETE_HEADER))
|
||||
.setAttribute(Constants.SKIP_LINK, true).setSuccess(Messages.OAUTH2_DEVICE_VERIFICATION_COMPLETE)
|
||||
.createInfoPage();
|
||||
}
|
||||
}
|
||||
|
||||
private Response createVerificationPage(String errorMessage) {
|
||||
String execution = AuthenticatedClientSessionModel.Action.USER_CODE_VERIFICATION.name();
|
||||
|
||||
LoginFormsProvider provider = session.getProvider(LoginFormsProvider.class)
|
||||
.setExecution(execution);
|
||||
|
||||
if (errorMessage != null) {
|
||||
provider = provider.setError(errorMessage);
|
||||
}
|
||||
|
||||
return provider.createOAuth2DeviceVerifyUserCodePage();
|
||||
}
|
||||
|
||||
private Response processVerification(OAuth2DeviceCodeModel deviceCode, String userCode) {
|
||||
int expiresIn = deviceCode.getExpiration() - Time.currentTime();
|
||||
if (expiresIn < 0) {
|
||||
event.error(Errors.EXPIRED_OAUTH2_USER_CODE);
|
||||
return createVerificationPage(Messages.OAUTH2_DEVICE_INVALID_USER_CODE);
|
||||
}
|
||||
|
||||
ClientModel client = realm.getClientByClientId(deviceCode.getClientId());
|
||||
AuthenticationSessionModel authenticationSession = createAuthenticationSession(client);
|
||||
|
||||
// Verification OK
|
||||
authenticationSession.setClientNote(DeviceGrantType.OAUTH2_DEVICE_VERIFIED_USER_CODE, userCode);
|
||||
|
||||
// Event logging for the verification
|
||||
event.client(deviceCode.getClientId()).detail(Details.SCOPE, deviceCode.getScope()).success();
|
||||
|
||||
OIDCLoginProtocol protocol = new OIDCLoginProtocol(session, realm, session.getContext().getUri(), headers, event);
|
||||
return handleBrowserAuthenticationRequest(authenticationSession, protocol, false, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getResource() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
}
|
||||
|
||||
private ClientModel authenticateClient() {
|
||||
// https://tools.ietf.org/html/draft-ietf-oauth-device-flow-15#section-3.1
|
||||
// The spec says "The client authentication requirements of Section 3.2.1 of [RFC6749]
|
||||
// apply to requests on this endpoint".
|
||||
AuthorizeClientUtil.ClientAuthResult clientAuth = AuthorizeClientUtil.authorizeClient(session, event, null);
|
||||
ClientModel client = clientAuth.getClient();
|
||||
|
||||
if (client == null) {
|
||||
event.error(Errors.INVALID_REQUEST);
|
||||
throw new ErrorPageException(session, null, Response.Status.BAD_REQUEST, Messages.MISSING_PARAMETER,
|
||||
OIDCLoginProtocol.CLIENT_ID_PARAM);
|
||||
}
|
||||
|
||||
checkClient(client.getClientId());
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
private ClientModel checkClient(String clientId) {
|
||||
if (clientId == null) {
|
||||
event.error(Errors.INVALID_REQUEST);
|
||||
throw new ErrorPageException(session, null, Response.Status.BAD_REQUEST,
|
||||
Messages.MISSING_PARAMETER, OIDCLoginProtocol.CLIENT_ID_PARAM);
|
||||
}
|
||||
|
||||
event.client(clientId);
|
||||
|
||||
ClientModel client = realm.getClientByClientId(clientId);
|
||||
if (client == null) {
|
||||
event.error(Errors.CLIENT_NOT_FOUND);
|
||||
throw new ErrorPageException(session, null, Response.Status.BAD_REQUEST,
|
||||
Messages.CLIENT_NOT_FOUND);
|
||||
}
|
||||
|
||||
if (!client.isEnabled()) {
|
||||
event.error(Errors.CLIENT_DISABLED);
|
||||
throw new ErrorPageException(session, null, Response.Status.BAD_REQUEST, Messages.CLIENT_DISABLED);
|
||||
}
|
||||
|
||||
if (!realm.getOAuth2DeviceConfig().isOAuth2DeviceAuthorizationGrantEnabled(client)) {
|
||||
event.error(Errors.NOT_ALLOWED);
|
||||
throw new ErrorPageException(session, null, Response.Status.BAD_REQUEST,
|
||||
Messages.OAUTH2_DEVICE_AUTHORIZATION_GRANT_DISABLED);
|
||||
}
|
||||
|
||||
if (client.isBearerOnly()) {
|
||||
event.error(Errors.NOT_ALLOWED);
|
||||
throw new ErrorPageException(session, null, Response.Status.FORBIDDEN, Messages.BEARER_ONLY);
|
||||
}
|
||||
|
||||
String protocol = client.getProtocol();
|
||||
if (protocol == null) {
|
||||
logger.warnf("Client '%s' doesn't have protocol set. Fallback to openid-connect. Please fix client configuration",
|
||||
clientId);
|
||||
protocol = OIDCLoginProtocol.LOGIN_PROTOCOL;
|
||||
}
|
||||
if (!protocol.equals(OIDCLoginProtocol.LOGIN_PROTOCOL)) {
|
||||
event.error(Errors.INVALID_CLIENT);
|
||||
throw new ErrorPageException(session, null, Response.Status.BAD_REQUEST, "Wrong client protocol.");
|
||||
}
|
||||
|
||||
session.getContext().setClient(client);
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
protected AuthenticationSessionModel createAuthenticationSession(ClientModel client) {
|
||||
AuthenticationSessionModel authenticationSession = super.createAuthenticationSession(client, null);
|
||||
|
||||
authenticationSession.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
|
||||
authenticationSession.setAction(AuthenticationSessionModel.Action.AUTHENTICATE.name());
|
||||
authenticationSession.setClientNote(OIDCLoginProtocol.ISSUER,
|
||||
Urls.realmIssuer(session.getContext().getUri().getBaseUri(), realm.getName()));
|
||||
|
||||
return authenticationSession;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
*
|
||||
* * Copyright 2021 Red Hat, Inc. and/or its affiliates
|
||||
* * and other contributors as indicated by the @author tags.
|
||||
* *
|
||||
* * 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
|
||||
* *
|
||||
* * http://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.keycloak.protocol.oidc.grants.device.endpoints;
|
||||
|
||||
import org.jboss.resteasy.spi.ResteasyProviderFactory;
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.events.EventBuilder;
|
||||
import org.keycloak.models.KeycloakContext;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.services.resource.RealmResourceProvider;
|
||||
import org.keycloak.services.resource.RealmResourceProviderFactory;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||
*/
|
||||
public class DeviceEndpointFactory implements RealmResourceProviderFactory {
|
||||
|
||||
@Override
|
||||
public RealmResourceProvider create(KeycloakSession session) {
|
||||
KeycloakContext context = session.getContext();
|
||||
RealmModel realm = context.getRealm();
|
||||
EventBuilder event = new EventBuilder(realm, session, context.getConnection());
|
||||
DeviceEndpoint provider = new DeviceEndpoint(realm, event);
|
||||
ResteasyProviderFactory.getInstance().injectProperties(provider);
|
||||
return provider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(Config.Scope config) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postInit(KeycloakSessionFactory factory) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "device";
|
||||
}
|
||||
}
|
|
@ -245,10 +245,6 @@ public class Urls {
|
|||
return loginActionsBase(baseUri).path(LoginActionsService.class, "processConsent").build(realmName);
|
||||
}
|
||||
|
||||
public static URI realmOAuth2DeviceVerificationAction(URI baseUri, String realmName) {
|
||||
return loginActionsBase(baseUri).path(LoginActionsService.class, "processOAuth2DeviceVerification").build(realmName);
|
||||
}
|
||||
|
||||
public static URI firstBrokerLoginProcessor(URI baseUri, String realmName) {
|
||||
return loginActionsBase(baseUri).path(LoginActionsService.class, "firstBrokerLoginGet")
|
||||
.build(realmName);
|
||||
|
@ -262,7 +258,7 @@ public class Urls {
|
|||
return themeBase(baseUri).path(Version.RESOURCES_VERSION).build();
|
||||
}
|
||||
|
||||
private static UriBuilder loginActionsBase(URI baseUri) {
|
||||
public static UriBuilder loginActionsBase(URI baseUri) {
|
||||
return realmBase(baseUri).path(RealmsResource.class, "getLoginActionsService");
|
||||
}
|
||||
|
||||
|
|
|
@ -52,6 +52,8 @@ import java.util.List;
|
|||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.keycloak.models.OAuth2DeviceConfig.OAUTH2_DEVICE_AUTHORIZATION_GRANT_ENABLED;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
|
@ -312,7 +314,8 @@ public class DescriptionConverter {
|
|||
if (client.isServiceAccountsEnabled()) {
|
||||
grantTypes.add(OAuth2Constants.CLIENT_CREDENTIALS);
|
||||
}
|
||||
if (client.isOAuth2DeviceAuthorizationGrantEnabled()) {
|
||||
boolean oauth2DeviceEnabled = client.getAttributes() != null && Boolean.parseBoolean(client.getAttributes().get(OAUTH2_DEVICE_AUTHORIZATION_GRANT_ENABLED));
|
||||
if (oauth2DeviceEnabled) {
|
||||
grantTypes.add(OAuth2Constants.DEVICE_CODE_GRANT_TYPE);
|
||||
}
|
||||
if (client.getAuthorizationServicesEnabled() != null && client.getAuthorizationServicesEnabled()) {
|
||||
|
|
|
@ -81,8 +81,6 @@ public class ApplianceBootstrap {
|
|||
realm.setAccessCodeLifespan(60);
|
||||
realm.setAccessCodeLifespanUserAction(300);
|
||||
realm.setAccessCodeLifespanLogin(1800);
|
||||
realm.setOAuth2DeviceCodeLifespan(Constants.DEFAULT_OAUTH2_DEVICE_CODE_LIFESPAN);
|
||||
realm.setOAuth2DevicePollingInterval(Constants.DEFAULT_OAUTH2_DEVICE_POLLING_INTERVAL);
|
||||
realm.setSslRequired(SslRequired.EXTERNAL);
|
||||
realm.setRegistrationAllowed(false);
|
||||
realm.setRegistrationEmailAsUsername(false);
|
||||
|
|
|
@ -62,7 +62,6 @@ import org.keycloak.models.UserSessionModel;
|
|||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.models.utils.SessionTimeoutHelper;
|
||||
import org.keycloak.models.utils.SystemClientUtil;
|
||||
import org.keycloak.protocol.AuthorizationEndpointBase;
|
||||
import org.keycloak.protocol.LoginProtocol;
|
||||
import org.keycloak.protocol.LoginProtocol.Error;
|
||||
import org.keycloak.protocol.oidc.BackchannelLogoutResponse;
|
||||
|
@ -90,7 +89,6 @@ import javax.ws.rs.core.Response;
|
|||
import javax.ws.rs.core.UriBuilder;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
import java.net.URI;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
@ -102,6 +100,7 @@ import java.util.stream.Collectors;
|
|||
import java.util.stream.Stream;
|
||||
|
||||
import static org.keycloak.common.util.ServerCookie.SameSiteAttributeValue;
|
||||
import static org.keycloak.protocol.oidc.grants.device.DeviceGrantType.isOAuth2DeviceVerificationFlow;
|
||||
import static org.keycloak.services.util.CookieHelper.getCookie;
|
||||
|
||||
/**
|
||||
|
@ -1080,11 +1079,6 @@ public class AuthenticationManager {
|
|||
|
||||
}
|
||||
|
||||
public static boolean isOAuth2DeviceVerificationFlow(final AuthenticationSessionModel authSession) {
|
||||
String flow = authSession.getClientNote(AuthorizationEndpointBase.APP_INITIATED_FLOW);
|
||||
return flow != null && flow.equals(LoginActionsService.OAUTH2_DEVICE_VERIFICATION_PATH);
|
||||
}
|
||||
|
||||
private static List<ClientScopeModel> getClientScopesToApproveOnConsentScreen(RealmModel realm, UserConsentModel grantedConsent,
|
||||
AuthenticationSessionModel authSession) {
|
||||
// Client Scopes to be displayed on consent screen
|
||||
|
|
|
@ -18,7 +18,6 @@ package org.keycloak.services.resources;
|
|||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.jboss.resteasy.spi.HttpRequest;
|
||||
import org.jboss.resteasy.spi.ResteasyProviderFactory;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.TokenVerifier;
|
||||
import org.keycloak.authentication.AuthenticationFlowException;
|
||||
|
@ -68,7 +67,6 @@ import org.keycloak.protocol.AuthorizationEndpointBase;
|
|||
import org.keycloak.protocol.LoginProtocol;
|
||||
import org.keycloak.protocol.LoginProtocol.Error;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.protocol.oidc.endpoints.OAuth2DeviceAuthorizationEndpoint;
|
||||
import org.keycloak.protocol.oidc.utils.OIDCResponseMode;
|
||||
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
|
||||
import org.keycloak.protocol.oidc.utils.RedirectUtils;
|
||||
|
@ -80,6 +78,7 @@ import org.keycloak.services.managers.AuthenticationManager;
|
|||
import org.keycloak.services.managers.AuthenticationSessionManager;
|
||||
import org.keycloak.services.managers.ClientSessionCode;
|
||||
import org.keycloak.services.messages.Messages;
|
||||
import org.keycloak.services.resource.RealmResourceProvider;
|
||||
import org.keycloak.services.util.AuthenticationFlowURLHelper;
|
||||
import org.keycloak.services.util.BrowserHistoryHelper;
|
||||
import org.keycloak.services.util.CacheControlUtil;
|
||||
|
@ -120,7 +119,6 @@ public class LoginActionsService {
|
|||
public static final String REQUIRED_ACTION = "required-action";
|
||||
public static final String FIRST_BROKER_LOGIN_PATH = "first-broker-login";
|
||||
public static final String POST_BROKER_LOGIN_PATH = "post-broker-login";
|
||||
public static final String OAUTH2_DEVICE_VERIFICATION_PATH = "verification";
|
||||
|
||||
public static final String RESTART_PATH = "restart";
|
||||
|
||||
|
@ -131,8 +129,6 @@ public class LoginActionsService {
|
|||
|
||||
public static final String CANCEL_AIA = "cancel-aia";
|
||||
|
||||
public static final String OAUTH2_DEVICE_USER_CODE = "device_user_code";
|
||||
|
||||
private RealmModel realm;
|
||||
|
||||
@Context
|
||||
|
@ -850,36 +846,6 @@ public class LoginActionsService {
|
|||
return Response.status(302).location(redirect).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifying user code page. You should not invoked this directly!
|
||||
*
|
||||
* @param formData
|
||||
* @return
|
||||
*/
|
||||
@Path("verification")
|
||||
@POST
|
||||
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
public Response processOAuth2DeviceVerification(final MultivaluedMap<String, String> formData) {
|
||||
event.event(EventType.OAUTH2_DEVICE_VERIFY_USER_CODE);
|
||||
|
||||
String code = formData.getFirst(SESSION_CODE);
|
||||
String tabId = session.getContext().getUri().getQueryParameters().getFirst(Constants.TAB_ID);
|
||||
|
||||
String systemClientId = SystemClientUtil.getSystemClient(realm).getClientId();
|
||||
|
||||
SessionCodeChecks checks = checksForCode(null, code, null, systemClientId, tabId, OAUTH2_DEVICE_VERIFICATION_PATH);
|
||||
if (!checks.verifyActiveAndValidAction(AuthenticationSessionModel.Action.USER_CODE_VERIFICATION.name(), ClientSessionCode.ActionType.LOGIN)) {
|
||||
return checks.getResponse();
|
||||
}
|
||||
|
||||
OAuth2DeviceAuthorizationEndpoint endpoint = new OAuth2DeviceAuthorizationEndpoint(realm, event);
|
||||
ResteasyProviderFactory.getInstance().injectProperties(endpoint);
|
||||
|
||||
AuthenticationSessionModel authSessionWithSystemClient = checks.getAuthenticationSession();
|
||||
String userCode = formData.getFirst(OAUTH2_DEVICE_USER_CODE);
|
||||
return endpoint.processVerification(authSessionWithSystemClient, userCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* OAuth grant page. You should not invoked this directly!
|
||||
*
|
||||
|
|
|
@ -31,7 +31,6 @@ import org.keycloak.models.RealmModel;
|
|||
import org.keycloak.protocol.LoginProtocol;
|
||||
import org.keycloak.protocol.LoginProtocolFactory;
|
||||
import org.keycloak.services.CorsErrorResponseException;
|
||||
import org.keycloak.protocol.oidc.endpoints.OAuth2DeviceAuthorizationEndpoint;
|
||||
import org.keycloak.services.clientregistration.ClientRegistrationService;
|
||||
import org.keycloak.services.managers.RealmManager;
|
||||
import org.keycloak.services.resource.RealmResourceProvider;
|
||||
|
@ -101,15 +100,6 @@ public class RealmsResource {
|
|||
return uriInfo.getBaseUriBuilder().path(RealmsResource.class).path(RealmsResource.class, "getBrokerService");
|
||||
}
|
||||
|
||||
public static UriBuilder oauth2DeviceVerificationUrl(UriInfo uriInfo) {
|
||||
UriBuilder baseUriBuilder = uriInfo.getBaseUriBuilder();
|
||||
return oauth2DeviceVerificationUrl(baseUriBuilder);
|
||||
}
|
||||
|
||||
public static UriBuilder oauth2DeviceVerificationUrl(UriBuilder baseUriBuilder) {
|
||||
return baseUriBuilder.path(RealmsResource.class).path(RealmsResource.class, "getOAuth2DeviceVerificationService");
|
||||
}
|
||||
|
||||
public static UriBuilder wellKnownProviderUrl(UriBuilder builder) {
|
||||
return builder.path(RealmsResource.class).path(RealmsResource.class, "getWellKnown");
|
||||
}
|
||||
|
@ -279,18 +269,6 @@ public class RealmsResource {
|
|||
return service;
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("{realm}/device")
|
||||
public Object getOAuth2DeviceVerificationService(@PathParam("realm") String realmName, @QueryParam("user_code") String userCode) {
|
||||
RealmModel realm = init(realmName);
|
||||
EventBuilder event = new EventBuilder(realm, session, clientConnection);
|
||||
OAuth2DeviceAuthorizationEndpoint endpoint = new OAuth2DeviceAuthorizationEndpoint(realm, event);
|
||||
|
||||
ResteasyProviderFactory.getInstance().injectProperties(endpoint);
|
||||
|
||||
return endpoint.buildVerificationResponse(userCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* A JAX-RS sub-resource locator that uses the {@link org.keycloak.services.resource.RealmResourceSPI} to resolve sub-resources instances given an <code>unknownPath</code>.
|
||||
*
|
||||
|
@ -299,9 +277,9 @@ public class RealmsResource {
|
|||
*/
|
||||
@Path("{realm}/{extension}")
|
||||
public Object resolveRealmExtension(@PathParam("realm") String realmName, @PathParam("extension") String extension) {
|
||||
init(realmName);
|
||||
RealmResourceProvider provider = session.getProvider(RealmResourceProvider.class, extension);
|
||||
if (provider != null) {
|
||||
init(realmName);
|
||||
Object resource = provider.getResource();
|
||||
if (resource != null) {
|
||||
return resource;
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
#
|
||||
# Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
# and other contributors as indicated by the @author tags.
|
||||
#
|
||||
# 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
|
||||
#
|
||||
# http://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.
|
||||
#
|
||||
org.keycloak.protocol.oidc.grants.device.endpoints.DeviceEndpointFactory
|
|
@ -49,7 +49,7 @@ public class OAuth2DeviceVerificationPage extends LanguageComboboxAwarePage {
|
|||
|
||||
@Override
|
||||
public boolean isCurrent() {
|
||||
if (driver.getTitle().startsWith("Log in to ")) {
|
||||
if (driver.getTitle().startsWith("Sign in to ")) {
|
||||
try {
|
||||
driver.findElement(By.id("device-user-code"));
|
||||
return true;
|
||||
|
@ -59,38 +59,23 @@ public class OAuth2DeviceVerificationPage extends LanguageComboboxAwarePage {
|
|||
return false;
|
||||
}
|
||||
|
||||
public void assertLoginPage() {
|
||||
String name = getClass().getSimpleName();
|
||||
Assert.assertTrue("Expected device login page but was " + driver.getTitle() + " (" + driver.getCurrentUrl() + ")",
|
||||
isLoginPage());
|
||||
}
|
||||
|
||||
public void assertApprovedPage() {
|
||||
String name = getClass().getSimpleName();
|
||||
Assert.assertTrue("Expected device approved page but was " + driver.getTitle() + " (" + driver.getCurrentUrl() + ")",
|
||||
isApprovedPage());
|
||||
}
|
||||
|
||||
public void assertDeniedPage() {
|
||||
String name = getClass().getSimpleName();
|
||||
Assert.assertTrue("Expected device denied page but was " + driver.getTitle() + " (" + driver.getCurrentUrl() + ")",
|
||||
isDeniedPage());
|
||||
}
|
||||
|
||||
private boolean isLoginPage() {
|
||||
if (driver.getTitle().startsWith("Log in to ")) {
|
||||
try {
|
||||
driver.findElement(By.id("username"));
|
||||
driver.findElement(By.id("password"));
|
||||
return true;
|
||||
} catch (Throwable t) {
|
||||
}
|
||||
}
|
||||
return false;
|
||||
public void assertInvalidUserCodePage() {
|
||||
Assert.assertTrue("Expected invalid user code page but was " + driver.getTitle() + " (" + driver.getCurrentUrl() + ")",
|
||||
isInvalidUserCodePage());
|
||||
}
|
||||
|
||||
private boolean isApprovedPage() {
|
||||
if (driver.getTitle().startsWith("Log in to ")) {
|
||||
if (driver.getTitle().startsWith("Sign in to ")) {
|
||||
try {
|
||||
driver.findElement(By.id("kc-page-title")).getText().equals("Device Login Successful");
|
||||
return true;
|
||||
|
@ -101,7 +86,7 @@ public class OAuth2DeviceVerificationPage extends LanguageComboboxAwarePage {
|
|||
}
|
||||
|
||||
private boolean isDeniedPage() {
|
||||
if (driver.getTitle().startsWith("Log in to ")) {
|
||||
if (driver.getTitle().startsWith("Sign in to ")) {
|
||||
try {
|
||||
driver.findElement(By.id("kc-page-title")).getText().equals("Device Login Failed");
|
||||
driver.findElement(By.className("instruction")).getText().equals("Consent denied for connecting the device.");
|
||||
|
@ -112,6 +97,19 @@ public class OAuth2DeviceVerificationPage extends LanguageComboboxAwarePage {
|
|||
return false;
|
||||
}
|
||||
|
||||
private boolean isInvalidUserCodePage() {
|
||||
if (driver.getTitle().startsWith("Sign in to ")) {
|
||||
try {
|
||||
driver.findElement(By.id("device-user-code"));
|
||||
driver.findElement(By.id("kc-page-title")).getText().equals("Device Login");
|
||||
driver.findElement(By.className("kc-feedback-text")).getText().equals("Invalid code, please try again.");
|
||||
return true;
|
||||
} catch (Throwable t) {
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void open() {
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
|
||||
package org.keycloak.testsuite.util;
|
||||
|
||||
import static org.keycloak.protocol.oidc.grants.device.DeviceGrantType.oauth2DeviceAuthUrl;
|
||||
import static org.keycloak.testsuite.admin.Users.getPasswordOf;
|
||||
import static org.keycloak.testsuite.util.ServerURLs.getAuthServerContextRoot;
|
||||
import static org.keycloak.testsuite.util.ServerURLs.removeDefaultPorts;
|
||||
|
@ -60,6 +61,7 @@ import org.keycloak.models.Constants;
|
|||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
|
||||
import org.keycloak.protocol.oidc.grants.device.DeviceGrantType;
|
||||
import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation;
|
||||
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
|
||||
import org.keycloak.representations.AccessToken;
|
||||
|
@ -68,8 +70,6 @@ import org.keycloak.representations.JsonWebToken;
|
|||
import org.keycloak.representations.RefreshToken;
|
||||
import org.keycloak.representations.UserInfo;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.services.resources.RealmsResource;
|
||||
import org.keycloak.testsuite.arquillian.AuthServerTestEnricher;
|
||||
import org.keycloak.testsuite.runonserver.RunOnServerException;
|
||||
import org.keycloak.util.BasicAuthHelper;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
@ -1058,11 +1058,6 @@ public class OAuthClient {
|
|||
driver.navigate().to(getLoginFormUrl());
|
||||
}
|
||||
|
||||
public void openOAuth2DeviceVerificationForm() {
|
||||
UriBuilder b = RealmsResource.oauth2DeviceVerificationUrl(UriBuilder.fromUri(baseUrl));
|
||||
openOAuth2DeviceVerificationForm(b.build(realm).toString());
|
||||
}
|
||||
|
||||
public void openOAuth2DeviceVerificationForm(String verificationUri) {
|
||||
driver.navigate().to(verificationUri);
|
||||
}
|
||||
|
@ -1206,7 +1201,7 @@ public class OAuthClient {
|
|||
}
|
||||
|
||||
public String getDeviceAuthorizationUrl() {
|
||||
UriBuilder b = OIDCLoginProtocolService.oauth2DeviceAuthUrl(UriBuilder.fromUri(baseUrl));
|
||||
UriBuilder b = oauth2DeviceAuthUrl(UriBuilder.fromUri(baseUrl));
|
||||
return b.build(realm).toString();
|
||||
}
|
||||
|
||||
|
|
|
@ -31,6 +31,7 @@ import org.keycloak.events.admin.OperationType;
|
|||
import org.keycloak.events.admin.ResourceType;
|
||||
import org.keycloak.events.log.JBossLoggingEventListenerProviderFactory;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.OAuth2DeviceConfig;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.protocol.saml.SamlProtocol;
|
||||
import org.keycloak.representations.adapters.action.GlobalRequestResult;
|
||||
|
@ -181,7 +182,8 @@ public class RealmTest extends AbstractAdminTest {
|
|||
|
||||
Map<String, String> attributes = rep2.getAttributes();
|
||||
assertTrue("Attributes expected to be present oauth2DeviceCodeLifespan, oauth2DevicePollingInterval, found: " + String.join(", ", attributes.keySet()),
|
||||
attributes.size() == 2 && attributes.containsKey("oauth2DeviceCodeLifespan") && attributes.containsKey("oauth2DevicePollingInterval"));
|
||||
attributes.size() == 2 && attributes.containsKey(OAuth2DeviceConfig.OAUTH2_DEVICE_CODE_LIFESPAN)
|
||||
&& attributes.containsKey(OAuth2DeviceConfig.OAUTH2_DEVICE_POLLING_INTERVAL));
|
||||
} finally {
|
||||
adminClient.realm("attributes").remove();
|
||||
}
|
||||
|
|
|
@ -16,17 +16,27 @@
|
|||
*/
|
||||
package org.keycloak.testsuite.oauth;
|
||||
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.keycloak.models.OAuth2DeviceConfig.DEFAULT_OAUTH2_DEVICE_CODE_LIFESPAN;
|
||||
import static org.keycloak.models.OAuth2DeviceConfig.DEFAULT_OAUTH2_DEVICE_POLLING_INTERVAL;
|
||||
|
||||
import org.jboss.arquillian.graphene.page.Page;
|
||||
import org.junit.*;
|
||||
import org.keycloak.events.Details;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.admin.client.resource.ClientResource;
|
||||
import org.keycloak.admin.client.resource.RealmResource;
|
||||
import org.keycloak.models.OAuth2DeviceConfig;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
|
||||
import org.keycloak.representations.AccessToken;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.representations.idm.EventRepresentation;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.testsuite.AbstractKeycloakTest;
|
||||
import org.keycloak.testsuite.Assert;
|
||||
import org.keycloak.testsuite.AssertEvents;
|
||||
import org.keycloak.testsuite.admin.ApiUtil;
|
||||
import org.keycloak.testsuite.pages.ErrorPage;
|
||||
import org.keycloak.testsuite.pages.OAuth2DeviceVerificationPage;
|
||||
import org.keycloak.testsuite.pages.OAuthGrantPage;
|
||||
|
@ -34,7 +44,6 @@ import org.keycloak.testsuite.util.ClientBuilder;
|
|||
import org.keycloak.testsuite.util.OAuthClient;
|
||||
import org.keycloak.testsuite.util.RealmBuilder;
|
||||
import org.keycloak.testsuite.util.UserBuilder;
|
||||
import org.keycloak.testsuite.util.WaitUtils;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
|
@ -72,19 +81,11 @@ public class OAuth2DeviceAuthorizationGrantTest extends AbstractKeycloakTest {
|
|||
ClientRepresentation app = ClientBuilder.create()
|
||||
.id(KeycloakModelUtils.generateId())
|
||||
.clientId("test-device")
|
||||
.oauth2DeviceAuthorizationGrant()
|
||||
.secret("secret")
|
||||
.attribute(OAuth2DeviceConfig.OAUTH2_DEVICE_AUTHORIZATION_GRANT_ENABLED, "true")
|
||||
.build();
|
||||
realm.client(app);
|
||||
|
||||
ClientRepresentation app2 = ClientBuilder.create()
|
||||
.id(KeycloakModelUtils.generateId())
|
||||
.clientId("test-device-public")
|
||||
.oauth2DeviceAuthorizationGrant()
|
||||
.publicClient()
|
||||
.build();
|
||||
realm.client(app2);
|
||||
|
||||
userId = KeycloakModelUtils.generateId();
|
||||
UserRepresentation user = UserBuilder.create()
|
||||
.id(userId)
|
||||
|
@ -98,26 +99,26 @@ public class OAuth2DeviceAuthorizationGrantTest extends AbstractKeycloakTest {
|
|||
}
|
||||
|
||||
@Before
|
||||
public void resetConifg() {
|
||||
public void resetConfig() {
|
||||
RealmRepresentation realm = getAdminClient().realm(REALM_NAME).toRepresentation();
|
||||
realm.setOAuth2DeviceCodeLifespan(600);
|
||||
realm.setOAuth2DeviceCodeLifespan(60);
|
||||
realm.setOAuth2DevicePollingInterval(5);
|
||||
getAdminClient().realm(REALM_NAME).update(realm);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void publicClientTest() throws Exception {
|
||||
public void testConfidentialClient() throws Exception {
|
||||
// Device Authorization Request from device
|
||||
oauth.realm(REALM_NAME);
|
||||
oauth.clientId(DEVICE_APP_PUBLIC);
|
||||
OAuthClient.DeviceAuthorizationResponse response = oauth.doDeviceAuthorizationRequest(DEVICE_APP_PUBLIC, null);
|
||||
oauth.clientId(DEVICE_APP);
|
||||
OAuthClient.DeviceAuthorizationResponse response = oauth.doDeviceAuthorizationRequest(DEVICE_APP, "secret");
|
||||
|
||||
Assert.assertEquals(200, response.getStatusCode());
|
||||
Assert.assertNotNull(response.getDeviceCode());
|
||||
Assert.assertNotNull(response.getUserCode());
|
||||
Assert.assertNotNull(response.getVerificationUri());
|
||||
Assert.assertNotNull(response.getVerificationUriComplete());
|
||||
Assert.assertEquals(600, response.getExpiresIn());
|
||||
assertNotNull(response.getDeviceCode());
|
||||
assertNotNull(response.getUserCode());
|
||||
assertNotNull(response.getVerificationUri());
|
||||
assertNotNull(response.getVerificationUriComplete());
|
||||
Assert.assertEquals(60, response.getExpiresIn());
|
||||
Assert.assertEquals(5, response.getInterval());
|
||||
|
||||
// Verify user code from verification page using browser
|
||||
|
@ -125,10 +126,7 @@ public class OAuth2DeviceAuthorizationGrantTest extends AbstractKeycloakTest {
|
|||
verificationPage.assertCurrent();
|
||||
verificationPage.submit(response.getUserCode());
|
||||
|
||||
EventRepresentation verifyEvent = events.expectDeviceVerifyUserCode(DEVICE_APP_PUBLIC).assertEvent();
|
||||
String codeId = verifyEvent.getDetails().get(Details.CODE_ID);
|
||||
|
||||
verificationPage.assertLoginPage();
|
||||
loginPage.assertCurrent();
|
||||
|
||||
// Do Login
|
||||
oauth.fillLoginForm("device-login", "password");
|
||||
|
@ -140,47 +138,148 @@ public class OAuth2DeviceAuthorizationGrantTest extends AbstractKeycloakTest {
|
|||
|
||||
verificationPage.assertApprovedPage();
|
||||
|
||||
events.expectDeviceLogin(DEVICE_APP_PUBLIC, codeId, userId).assertEvent();
|
||||
|
||||
// Token request from device
|
||||
OAuthClient.AccessTokenResponse tokenResponse = oauth.doDeviceTokenRequest(DEVICE_APP_PUBLIC, null, response.getDeviceCode());
|
||||
OAuthClient.AccessTokenResponse tokenResponse = oauth.doDeviceTokenRequest(DEVICE_APP, "secret", response.getDeviceCode());
|
||||
|
||||
Assert.assertEquals(200, tokenResponse.getStatusCode());
|
||||
|
||||
String tokenString = tokenResponse.getAccessToken();
|
||||
Assert.assertNotNull(tokenString);
|
||||
assertNotNull(tokenString);
|
||||
AccessToken token = oauth.verifyToken(tokenString);
|
||||
|
||||
// Check receiving access token which is bound to the user session of the verification process
|
||||
Assert.assertTrue(codeId.equals(token.getSessionState()));
|
||||
|
||||
events.expectDeviceCodeToToken(DEVICE_APP_PUBLIC, codeId, userId).assertEvent();
|
||||
assertNotNull(token);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void confidentialClientTest() throws Exception {
|
||||
public void testConsentCancel() throws Exception {
|
||||
// Device Authorization Request from device
|
||||
oauth.realm(REALM_NAME);
|
||||
oauth.clientId(DEVICE_APP);
|
||||
OAuthClient.DeviceAuthorizationResponse response = oauth.doDeviceAuthorizationRequest(DEVICE_APP, "secret");
|
||||
|
||||
Assert.assertEquals(200, response.getStatusCode());
|
||||
Assert.assertNotNull(response.getDeviceCode());
|
||||
Assert.assertNotNull(response.getUserCode());
|
||||
Assert.assertNotNull(response.getVerificationUri());
|
||||
Assert.assertNotNull(response.getVerificationUriComplete());
|
||||
Assert.assertEquals(600, response.getExpiresIn());
|
||||
assertNotNull(response.getDeviceCode());
|
||||
assertNotNull(response.getUserCode());
|
||||
assertNotNull(response.getVerificationUri());
|
||||
assertNotNull(response.getVerificationUriComplete());
|
||||
Assert.assertEquals(60, response.getExpiresIn());
|
||||
Assert.assertEquals(5, response.getInterval());
|
||||
|
||||
openVerificationPage(response.getVerificationUriComplete());
|
||||
loginPage.assertCurrent();
|
||||
|
||||
// Do Login
|
||||
oauth.fillLoginForm("device-login", "password");
|
||||
|
||||
// Consent
|
||||
grantPage.assertCurrent();
|
||||
grantPage.assertGrants(OAuthGrantPage.PROFILE_CONSENT_TEXT, OAuthGrantPage.EMAIL_CONSENT_TEXT, OAuthGrantPage.ROLES_CONSENT_TEXT);
|
||||
grantPage.cancel();
|
||||
|
||||
verificationPage.assertDeniedPage();
|
||||
|
||||
OAuthClient.AccessTokenResponse tokenResponse = oauth.doDeviceTokenRequest(DEVICE_APP, "secret", response.getDeviceCode());
|
||||
|
||||
Assert.assertEquals(400, tokenResponse.getStatusCode());
|
||||
Assert.assertEquals("access_denied", tokenResponse.getError());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInvalidUserCode() throws Exception {
|
||||
// Device Authorization Request from device
|
||||
oauth.realm(REALM_NAME);
|
||||
oauth.clientId(DEVICE_APP);
|
||||
OAuthClient.DeviceAuthorizationResponse response = oauth.doDeviceAuthorizationRequest(DEVICE_APP, "secret");
|
||||
|
||||
Assert.assertEquals(200, response.getStatusCode());
|
||||
assertNotNull(response.getDeviceCode());
|
||||
assertNotNull(response.getUserCode());
|
||||
assertNotNull(response.getVerificationUri());
|
||||
assertNotNull(response.getVerificationUriComplete());
|
||||
Assert.assertEquals(60, response.getExpiresIn());
|
||||
Assert.assertEquals(5, response.getInterval());
|
||||
|
||||
// Verify user code from verification page using browser
|
||||
openVerificationPage(response.getVerificationUri());
|
||||
verificationPage.assertCurrent();
|
||||
verificationPage.submit(response.getUserCode());
|
||||
verificationPage.submit("x");
|
||||
|
||||
EventRepresentation verifyEvent = events.expectDeviceVerifyUserCode(DEVICE_APP).assertEvent();
|
||||
String codeId = verifyEvent.getDetails().get(Details.CODE_ID);
|
||||
verificationPage.assertInvalidUserCodePage();
|
||||
}
|
||||
|
||||
verificationPage.assertLoginPage();
|
||||
@Test
|
||||
public void testExpiredUserCodeTest() throws Exception {
|
||||
// Device Authorization Request from device
|
||||
oauth.realm(REALM_NAME);
|
||||
oauth.clientId(DEVICE_APP);
|
||||
OAuthClient.DeviceAuthorizationResponse response = oauth.doDeviceAuthorizationRequest(DEVICE_APP, "secret");
|
||||
|
||||
Assert.assertEquals(200, response.getStatusCode());
|
||||
assertNotNull(response.getDeviceCode());
|
||||
assertNotNull(response.getUserCode());
|
||||
assertNotNull(response.getVerificationUri());
|
||||
assertNotNull(response.getVerificationUriComplete());
|
||||
Assert.assertEquals(60, response.getExpiresIn());
|
||||
Assert.assertEquals(5, response.getInterval());
|
||||
|
||||
try {
|
||||
setTimeOffset(610);
|
||||
openVerificationPage(response.getVerificationUriComplete());
|
||||
} finally {
|
||||
resetTimeOffset();
|
||||
}
|
||||
|
||||
verificationPage.assertInvalidUserCodePage();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInvalidDeviceCode() throws Exception {
|
||||
// Device Authorization Request from device
|
||||
oauth.realm(REALM_NAME);
|
||||
oauth.clientId(DEVICE_APP);
|
||||
OAuthClient.DeviceAuthorizationResponse response = oauth.doDeviceAuthorizationRequest(DEVICE_APP, "secret");
|
||||
|
||||
Assert.assertEquals(200, response.getStatusCode());
|
||||
assertNotNull(response.getDeviceCode());
|
||||
assertNotNull(response.getUserCode());
|
||||
assertNotNull(response.getVerificationUri());
|
||||
assertNotNull(response.getVerificationUriComplete());
|
||||
Assert.assertEquals(60, response.getExpiresIn());
|
||||
Assert.assertEquals(5, response.getInterval());
|
||||
|
||||
openVerificationPage(response.getVerificationUriComplete());
|
||||
loginPage.assertCurrent();
|
||||
|
||||
// Do Login
|
||||
oauth.fillLoginForm("device-login", "password");
|
||||
|
||||
// Consent
|
||||
grantPage.assertCurrent();
|
||||
grantPage.assertGrants(OAuthGrantPage.PROFILE_CONSENT_TEXT, OAuthGrantPage.EMAIL_CONSENT_TEXT, OAuthGrantPage.ROLES_CONSENT_TEXT);
|
||||
grantPage.accept();
|
||||
|
||||
// Token request from device
|
||||
OAuthClient.AccessTokenResponse tokenResponse = oauth.doDeviceTokenRequest(DEVICE_APP, "secret", "x");
|
||||
|
||||
Assert.assertEquals(400, tokenResponse.getStatusCode());
|
||||
Assert.assertEquals("invalid_grant", tokenResponse.getError());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccessVerificationUriComplete() throws Exception {
|
||||
// Device Authorization Request from device
|
||||
oauth.realm(REALM_NAME);
|
||||
oauth.clientId(DEVICE_APP);
|
||||
OAuthClient.DeviceAuthorizationResponse response = oauth.doDeviceAuthorizationRequest(DEVICE_APP, "secret");
|
||||
|
||||
Assert.assertEquals(200, response.getStatusCode());
|
||||
assertNotNull(response.getDeviceCode());
|
||||
assertNotNull(response.getUserCode());
|
||||
assertNotNull(response.getVerificationUri());
|
||||
assertNotNull(response.getVerificationUriComplete());
|
||||
Assert.assertEquals(60, response.getExpiresIn());
|
||||
Assert.assertEquals(5, response.getInterval());
|
||||
|
||||
openVerificationPage(response.getVerificationUriComplete());
|
||||
loginPage.assertCurrent();
|
||||
|
||||
// Do Login
|
||||
oauth.fillLoginForm("device-login", "password");
|
||||
|
@ -192,95 +291,251 @@ public class OAuth2DeviceAuthorizationGrantTest extends AbstractKeycloakTest {
|
|||
|
||||
verificationPage.assertApprovedPage();
|
||||
|
||||
events.expectDeviceLogin(DEVICE_APP, codeId, userId).assertEvent();
|
||||
|
||||
// Token request from device
|
||||
OAuthClient.AccessTokenResponse tokenResponse = oauth.doDeviceTokenRequest(DEVICE_APP, "secret", response.getDeviceCode());
|
||||
|
||||
Assert.assertEquals(200, tokenResponse.getStatusCode());
|
||||
|
||||
String tokenString = tokenResponse.getAccessToken();
|
||||
Assert.assertNotNull(tokenString);
|
||||
AccessToken token = oauth.verifyToken(tokenString);
|
||||
|
||||
// Check receiving access token which is bound to the user session of the verification process
|
||||
Assert.assertTrue(codeId.equals(token.getSessionState()));
|
||||
|
||||
events.expectDeviceCodeToToken(DEVICE_APP, codeId, userId).assertEvent();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pollingTest() throws Exception {
|
||||
public void testExpiredDeviceCode() throws Exception {
|
||||
// Device Authorization Request from device
|
||||
oauth.realm(REALM_NAME);
|
||||
oauth.clientId(DEVICE_APP);
|
||||
OAuthClient.DeviceAuthorizationResponse response = oauth.doDeviceAuthorizationRequest(DEVICE_APP, "secret");
|
||||
|
||||
Assert.assertEquals(600, response.getExpiresIn());
|
||||
Assert.assertEquals(200, response.getStatusCode());
|
||||
assertNotNull(response.getDeviceCode());
|
||||
assertNotNull(response.getUserCode());
|
||||
assertNotNull(response.getVerificationUri());
|
||||
assertNotNull(response.getVerificationUriComplete());
|
||||
Assert.assertEquals(60, response.getExpiresIn());
|
||||
Assert.assertEquals(5, response.getInterval());
|
||||
|
||||
// Polling token request from device
|
||||
OAuthClient.AccessTokenResponse tokenResponse = oauth.doDeviceTokenRequest(DEVICE_APP, "secret", response.getDeviceCode());
|
||||
try {
|
||||
setTimeOffset(610);
|
||||
// Token request from device
|
||||
OAuthClient.AccessTokenResponse tokenResponse = oauth.doDeviceTokenRequest(DEVICE_APP, "secret",
|
||||
response.getDeviceCode());
|
||||
|
||||
// Not approved yet
|
||||
Assert.assertEquals(400, tokenResponse.getStatusCode());
|
||||
Assert.assertEquals("authorization_pending", tokenResponse.getError());
|
||||
Assert.assertEquals(400, tokenResponse.getStatusCode());
|
||||
Assert.assertEquals("expired_token", tokenResponse.getError());
|
||||
} finally {
|
||||
resetTimeOffset();
|
||||
}
|
||||
}
|
||||
|
||||
// Polling again without waiting
|
||||
tokenResponse = oauth.doDeviceTokenRequest(DEVICE_APP, "secret", response.getDeviceCode());
|
||||
@Test
|
||||
public void testDeviceCodeLifespanPerClient() throws Exception {
|
||||
ClientResource client = ApiUtil.findClientByClientId(adminClient.realm(REALM_NAME), DEVICE_APP);
|
||||
ClientRepresentation clientRepresentation = client.toRepresentation();
|
||||
// Device Authorization Request from device
|
||||
oauth.realm(REALM_NAME);
|
||||
oauth.clientId(DEVICE_APP);
|
||||
OAuthClient.DeviceAuthorizationResponse response = oauth.doDeviceAuthorizationRequest(DEVICE_APP, "secret");
|
||||
|
||||
// Slow down
|
||||
Assert.assertEquals(400, tokenResponse.getStatusCode());
|
||||
Assert.assertEquals("slow_down", tokenResponse.getError());
|
||||
Assert.assertEquals(200, response.getStatusCode());
|
||||
assertNotNull(response.getDeviceCode());
|
||||
assertNotNull(response.getUserCode());
|
||||
assertNotNull(response.getVerificationUri());
|
||||
assertNotNull(response.getVerificationUriComplete());
|
||||
Assert.assertEquals(60, response.getExpiresIn());
|
||||
Assert.assertEquals(5, response.getInterval());
|
||||
|
||||
// Wait the interval
|
||||
WaitUtils.pause(5000);
|
||||
clientRepresentation.getAttributes().put(OAuth2DeviceConfig.OAUTH2_DEVICE_CODE_LIFESPAN_PER_CLIENT, "120");
|
||||
clientRepresentation.getAttributes().put(OAuth2DeviceConfig.OAUTH2_DEVICE_POLLING_INTERVAL_PER_CLIENT, "600000");
|
||||
client.update(clientRepresentation);
|
||||
|
||||
// Polling again
|
||||
tokenResponse = oauth.doDeviceTokenRequest(DEVICE_APP, "secret", response.getDeviceCode());
|
||||
|
||||
// Not approved yet
|
||||
Assert.assertEquals(400, tokenResponse.getStatusCode());
|
||||
Assert.assertEquals("authorization_pending", tokenResponse.getError());
|
||||
|
||||
|
||||
// Change the interval setting of the realm from 5 seconds to 10 seconds.
|
||||
RealmRepresentation realm = getAdminClient().realm(REALM_NAME).toRepresentation();
|
||||
realm.setOAuth2DevicePollingInterval(10);
|
||||
getAdminClient().realm(REALM_NAME).update(realm);
|
||||
|
||||
// Checking the new interval is applied
|
||||
response = oauth.doDeviceAuthorizationRequest(DEVICE_APP, "secret");
|
||||
Assert.assertEquals(120, response.getExpiresIn());
|
||||
OAuthClient.AccessTokenResponse tokenResponse;
|
||||
|
||||
Assert.assertEquals(600, response.getExpiresIn());
|
||||
try {
|
||||
setTimeOffset(100);
|
||||
// Token request from device
|
||||
tokenResponse = oauth.doDeviceTokenRequest(DEVICE_APP, "secret", response.getDeviceCode());
|
||||
|
||||
Assert.assertEquals(400, tokenResponse.getStatusCode());
|
||||
Assert.assertEquals("authorization_pending", tokenResponse.getError());
|
||||
|
||||
setTimeOffset(125);
|
||||
// Token request from device
|
||||
tokenResponse = oauth.doDeviceTokenRequest(DEVICE_APP, "secret", response.getDeviceCode());
|
||||
|
||||
Assert.assertEquals(400, tokenResponse.getStatusCode());
|
||||
Assert.assertEquals("expired_token", tokenResponse.getError());
|
||||
} finally {
|
||||
resetTimeOffset();
|
||||
}
|
||||
|
||||
clientRepresentation.getAttributes().put(OAuth2DeviceConfig.OAUTH2_DEVICE_CODE_LIFESPAN_PER_CLIENT, "");
|
||||
clientRepresentation.getAttributes().put(OAuth2DeviceConfig.OAUTH2_DEVICE_POLLING_INTERVAL_PER_CLIENT, "");
|
||||
client.update(clientRepresentation);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDevicePollingIntervalPerClient() throws Exception {
|
||||
getTestingClient().testing().setTestingInfinispanTimeService();
|
||||
ClientResource client = ApiUtil.findClientByClientId(adminClient.realm(REALM_NAME), DEVICE_APP);
|
||||
ClientRepresentation clientRepresentation = client.toRepresentation();
|
||||
// Device Authorization Request from device
|
||||
oauth.realm(REALM_NAME);
|
||||
oauth.clientId(DEVICE_APP);
|
||||
OAuthClient.DeviceAuthorizationResponse response = oauth.doDeviceAuthorizationRequest(DEVICE_APP, "secret");
|
||||
|
||||
Assert.assertEquals(200, response.getStatusCode());
|
||||
assertNotNull(response.getDeviceCode());
|
||||
assertNotNull(response.getUserCode());
|
||||
assertNotNull(response.getVerificationUri());
|
||||
assertNotNull(response.getVerificationUriComplete());
|
||||
Assert.assertEquals(60, response.getExpiresIn());
|
||||
Assert.assertEquals(5, response.getInterval());
|
||||
|
||||
clientRepresentation.getAttributes().put(OAuth2DeviceConfig.OAUTH2_DEVICE_POLLING_INTERVAL_PER_CLIENT, "10");
|
||||
client.update(clientRepresentation);
|
||||
|
||||
response = oauth.doDeviceAuthorizationRequest(DEVICE_APP, "secret");
|
||||
Assert.assertEquals(10, response.getInterval());
|
||||
|
||||
// Polling token request from device
|
||||
tokenResponse = oauth.doDeviceTokenRequest(DEVICE_APP, "secret", response.getDeviceCode());
|
||||
try {
|
||||
// Token request from device
|
||||
OAuthClient.AccessTokenResponse tokenResponse = oauth.doDeviceTokenRequest(DEVICE_APP, "secret",
|
||||
response.getDeviceCode());
|
||||
|
||||
// Not approved yet
|
||||
Assert.assertEquals(400, tokenResponse.getStatusCode());
|
||||
Assert.assertEquals("authorization_pending", tokenResponse.getError());
|
||||
Assert.assertEquals(400, tokenResponse.getStatusCode());
|
||||
Assert.assertEquals("authorization_pending", tokenResponse.getError());
|
||||
|
||||
// Wait
|
||||
WaitUtils.pause(5000);
|
||||
// Token request from device
|
||||
tokenResponse = oauth.doDeviceTokenRequest(DEVICE_APP, "secret", response.getDeviceCode());
|
||||
|
||||
// Polling again without waiting
|
||||
tokenResponse = oauth.doDeviceTokenRequest(DEVICE_APP, "secret", response.getDeviceCode());
|
||||
Assert.assertEquals(400, tokenResponse.getStatusCode());
|
||||
Assert.assertEquals("slow_down", tokenResponse.getError());
|
||||
|
||||
// Slow down
|
||||
Assert.assertEquals(400, tokenResponse.getStatusCode());
|
||||
Assert.assertEquals("slow_down", tokenResponse.getError());
|
||||
setTimeOffset(7);
|
||||
|
||||
// Wait
|
||||
WaitUtils.pause(5000);
|
||||
// Token request from device
|
||||
tokenResponse = oauth.doDeviceTokenRequest(DEVICE_APP, "secret", response.getDeviceCode());
|
||||
|
||||
// Polling again
|
||||
tokenResponse = oauth.doDeviceTokenRequest(DEVICE_APP, "secret", response.getDeviceCode());
|
||||
Assert.assertEquals(400, tokenResponse.getStatusCode());
|
||||
Assert.assertEquals("slow_down", tokenResponse.getError());
|
||||
|
||||
// Not approved yet
|
||||
Assert.assertEquals(400, tokenResponse.getStatusCode());
|
||||
Assert.assertEquals("authorization_pending", tokenResponse.getError());
|
||||
setTimeOffset(10);
|
||||
|
||||
// Token request from device
|
||||
tokenResponse = oauth.doDeviceTokenRequest(DEVICE_APP, "secret", response.getDeviceCode());
|
||||
|
||||
Assert.assertEquals(400, tokenResponse.getStatusCode());
|
||||
Assert.assertEquals("authorization_pending", tokenResponse.getError());
|
||||
} finally {
|
||||
getTestingClient().testing().revertTestingInfinispanTimeService();
|
||||
clientRepresentation.getAttributes().put(OAuth2DeviceConfig.OAUTH2_DEVICE_POLLING_INTERVAL_PER_CLIENT, "");
|
||||
client.update(clientRepresentation);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPooling() throws Exception {
|
||||
getTestingClient().testing().setTestingInfinispanTimeService();
|
||||
|
||||
try {
|
||||
RealmRepresentation realm = getAdminClient().realm(REALM_NAME).toRepresentation();
|
||||
realm.setOAuth2DeviceCodeLifespan(600);
|
||||
getAdminClient().realm(REALM_NAME).update(realm);
|
||||
// Device Authorization Request from device
|
||||
oauth.realm(REALM_NAME);
|
||||
oauth.clientId(DEVICE_APP);
|
||||
OAuthClient.DeviceAuthorizationResponse response = oauth.doDeviceAuthorizationRequest(DEVICE_APP, "secret");
|
||||
|
||||
Assert.assertEquals(600, response.getExpiresIn());
|
||||
Assert.assertEquals(5, response.getInterval());
|
||||
|
||||
// Polling token request from device
|
||||
OAuthClient.AccessTokenResponse tokenResponse = oauth.doDeviceTokenRequest(DEVICE_APP, "secret", response.getDeviceCode());
|
||||
|
||||
// Not approved yet
|
||||
Assert.assertEquals(400, tokenResponse.getStatusCode());
|
||||
Assert.assertEquals("authorization_pending", tokenResponse.getError());
|
||||
|
||||
// Polling again without waiting
|
||||
tokenResponse = oauth.doDeviceTokenRequest(DEVICE_APP, "secret", response.getDeviceCode());
|
||||
|
||||
// Slow down
|
||||
Assert.assertEquals(400, tokenResponse.getStatusCode());
|
||||
Assert.assertEquals("slow_down", tokenResponse.getError());
|
||||
|
||||
// Wait the interval
|
||||
setTimeOffset(5);
|
||||
|
||||
// Polling again
|
||||
tokenResponse = oauth.doDeviceTokenRequest(DEVICE_APP, "secret", response.getDeviceCode());
|
||||
|
||||
// Not approved yet
|
||||
Assert.assertEquals(400, tokenResponse.getStatusCode());
|
||||
Assert.assertEquals("authorization_pending", tokenResponse.getError());
|
||||
|
||||
// Change the interval setting of the realm from 5 seconds to 10 seconds.
|
||||
realm.setOAuth2DevicePollingInterval(10);
|
||||
getAdminClient().realm(REALM_NAME).update(realm);
|
||||
|
||||
// Checking the new interval is applied
|
||||
response = oauth.doDeviceAuthorizationRequest(DEVICE_APP, "secret");
|
||||
|
||||
Assert.assertEquals(600, response.getExpiresIn());
|
||||
Assert.assertEquals(10, response.getInterval());
|
||||
|
||||
// Polling token request from device
|
||||
tokenResponse = oauth.doDeviceTokenRequest(DEVICE_APP, "secret", response.getDeviceCode());
|
||||
|
||||
// Not approved yet
|
||||
Assert.assertEquals(400, tokenResponse.getStatusCode());
|
||||
Assert.assertEquals("authorization_pending", tokenResponse.getError());
|
||||
|
||||
// Wait
|
||||
setTimeOffset(10);
|
||||
|
||||
// Polling again without waiting
|
||||
tokenResponse = oauth.doDeviceTokenRequest(DEVICE_APP, "secret", response.getDeviceCode());
|
||||
|
||||
// Slow down
|
||||
Assert.assertEquals(400, tokenResponse.getStatusCode());
|
||||
Assert.assertEquals("slow_down", tokenResponse.getError());
|
||||
|
||||
// Wait
|
||||
setTimeOffset(15);
|
||||
|
||||
// Polling again
|
||||
tokenResponse = oauth.doDeviceTokenRequest(DEVICE_APP, "secret", response.getDeviceCode());
|
||||
|
||||
// Not approved yet
|
||||
Assert.assertEquals(400, tokenResponse.getStatusCode());
|
||||
Assert.assertEquals("authorization_pending", tokenResponse.getError());
|
||||
} finally {
|
||||
getTestingClient().testing().revertTestingInfinispanTimeService();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateConfig() {
|
||||
RealmResource realm = getAdminClient().realm(REALM_NAME);
|
||||
RealmRepresentation rep = realm.toRepresentation();
|
||||
|
||||
rep.setOAuth2DevicePollingInterval(DEFAULT_OAUTH2_DEVICE_POLLING_INTERVAL);
|
||||
rep.setOAuth2DeviceCodeLifespan(DEFAULT_OAUTH2_DEVICE_CODE_LIFESPAN);
|
||||
|
||||
realm.update(rep);
|
||||
rep = realm.toRepresentation();
|
||||
|
||||
Assert.assertEquals(DEFAULT_OAUTH2_DEVICE_POLLING_INTERVAL, rep.getOAuth2DevicePollingInterval().intValue());
|
||||
Assert.assertEquals(DEFAULT_OAUTH2_DEVICE_CODE_LIFESPAN, rep.getOAuth2DeviceCodeLifespan().intValue());
|
||||
|
||||
rep.setOAuth2DevicePollingInterval(10);
|
||||
rep.setOAuth2DeviceCodeLifespan(15);
|
||||
|
||||
realm.update(rep);
|
||||
rep = realm.toRepresentation();
|
||||
|
||||
Assert.assertEquals(10, rep.getOAuth2DevicePollingInterval().intValue());
|
||||
Assert.assertEquals(15, rep.getOAuth2DeviceCodeLifespan().intValue());
|
||||
}
|
||||
|
||||
private void openVerificationPage(String verificationUri) {
|
||||
|
|
|
@ -121,7 +121,8 @@ public class OIDCWellKnownProviderTest extends AbstractKeycloakTest {
|
|||
|
||||
// Support standard + implicit + hybrid flow
|
||||
assertContains(oidcConfig.getResponseTypesSupported(), OAuth2Constants.CODE, OIDCResponseType.ID_TOKEN, "id_token token", "code id_token", "code token", "code id_token token");
|
||||
assertContains(oidcConfig.getGrantTypesSupported(), OAuth2Constants.AUTHORIZATION_CODE, OAuth2Constants.IMPLICIT);
|
||||
assertContains(oidcConfig.getGrantTypesSupported(), OAuth2Constants.AUTHORIZATION_CODE, OAuth2Constants.IMPLICIT,
|
||||
OAuth2Constants.DEVICE_CODE_GRANT_TYPE);
|
||||
assertContains(oidcConfig.getResponseModesSupported(), "query", "fragment");
|
||||
|
||||
Assert.assertNames(oidcConfig.getSubjectTypesSupported(), "pairwise", "public");
|
||||
|
@ -177,6 +178,8 @@ public class OIDCWellKnownProviderTest extends AbstractKeycloakTest {
|
|||
Assert.assertNames(oidcConfig.getRevocationEndpointAuthSigningAlgValuesSupported(), Algorithm.PS256,
|
||||
Algorithm.PS384, Algorithm.PS512, Algorithm.RS256, Algorithm.RS384, Algorithm.RS512, Algorithm.ES256,
|
||||
Algorithm.ES384, Algorithm.ES512, Algorithm.HS256, Algorithm.HS384, Algorithm.HS512);
|
||||
|
||||
assertEquals(oidcConfig.getDeviceAuthorizationEndpoint(), oauth.getDeviceAuthorizationUrl());
|
||||
} finally {
|
||||
client.close();
|
||||
}
|
||||
|
|
|
@ -109,11 +109,6 @@ public class ClientBuilder {
|
|||
return this;
|
||||
}
|
||||
|
||||
public ClientBuilder oauth2DeviceAuthorizationGrant() {
|
||||
rep.setOAuth2DeviceAuthorizationGrantEnabled(true);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ClientRepresentation build() {
|
||||
return rep;
|
||||
}
|
||||
|
|
|
@ -1108,6 +1108,7 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, flows, $ro
|
|||
$scope.disableAuthorizationTab = !client.authorizationServicesEnabled;
|
||||
$scope.disableServiceAccountRolesTab = !client.serviceAccountsEnabled;
|
||||
$scope.disableCredentialsTab = client.publicClient;
|
||||
$scope.oauth2DeviceAuthorizationGrantEnabled = false;
|
||||
// KEYCLOAK-6771 Certificate Bound Token
|
||||
// https://tools.ietf.org/html/draft-ietf-oauth-mtls-08#section-3
|
||||
$scope.tlsClientCertificateBoundAccessTokens = false;
|
||||
|
@ -1118,6 +1119,8 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, flows, $ro
|
|||
$scope.clientSessionMaxLifespan = TimeUnit2.asUnit(client.attributes['client.session.max.lifespan']);
|
||||
$scope.clientOfflineSessionIdleTimeout = TimeUnit2.asUnit(client.attributes['client.offline.session.idle.timeout']);
|
||||
$scope.clientOfflineSessionMaxLifespan = TimeUnit2.asUnit(client.attributes['client.offline.session.max.lifespan']);
|
||||
$scope.oauth2DeviceCodeLifespan = TimeUnit2.asUnit(client.attributes['oauth2.device.code.lifespan']);
|
||||
$scope.oauth2DevicePollingInterval = parseInt(client.attributes['oauth2.device.polling.interval']);
|
||||
|
||||
if(client.origin) {
|
||||
if ($scope.access.viewRealm) {
|
||||
|
@ -1279,6 +1282,14 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, flows, $ro
|
|||
}
|
||||
}
|
||||
|
||||
if ($scope.client.attributes["oauth2.device.authorization.grant.enabled"]) {
|
||||
if ($scope.client.attributes["oauth2.device.authorization.grant.enabled"] == "true") {
|
||||
$scope.oauth2DeviceAuthorizationGrantEnabled = true;
|
||||
} else {
|
||||
$scope.oauth2DeviceAuthorizationGrantEnabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// KEYCLOAK-6771 Certificate Bound Token
|
||||
// https://tools.ietf.org/html/draft-ietf-oauth-mtls-08#section-3
|
||||
if ($scope.client.attributes["tls.client.certificate.bound.access.tokens"]) {
|
||||
|
@ -1521,6 +1532,22 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, flows, $ro
|
|||
}
|
||||
}
|
||||
|
||||
$scope.updateOauth2DeviceCodeLifespan = function() {
|
||||
if ($scope.oauth2DeviceCodeLifespan.time) {
|
||||
$scope.clientEdit.attributes['oauth2.device.code.lifespan'] = $scope.oauth2DeviceCodeLifespan.toSeconds();
|
||||
} else {
|
||||
$scope.clientEdit.attributes['oauth2.device.code.lifespan'] = null;
|
||||
}
|
||||
}
|
||||
|
||||
$scope.updateOauth2DevicePollingInterval = function() {
|
||||
if ($scope.oauth2DevicePollingInterval) {
|
||||
$scope.clientEdit.attributes['oauth2.device.polling.interval'] = $scope.oauth2DevicePollingInterval;
|
||||
} else {
|
||||
$scope.clientEdit.attributes['oauth2.device.polling.interval'] = null;
|
||||
}
|
||||
}
|
||||
|
||||
function configureAuthorizationServices() {
|
||||
if ($scope.clientEdit.authorizationServicesEnabled) {
|
||||
if ($scope.accessType == 'public') {
|
||||
|
@ -1664,6 +1691,12 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, flows, $ro
|
|||
|
||||
}
|
||||
|
||||
if ($scope.oauth2DeviceAuthorizationGrantEnabled == true) {
|
||||
$scope.clientEdit.attributes["oauth2.device.authorization.grant.enabled"] = "true";
|
||||
} else {
|
||||
$scope.clientEdit.attributes["oauth2.device.authorization.grant.enabled"] = "false";
|
||||
}
|
||||
|
||||
// KEYCLOAK-6771 Certificate Bound Token
|
||||
// https://tools.ietf.org/html/draft-ietf-oauth-mtls-08#section-3
|
||||
if ($scope.tlsClientCertificateBoundAccessTokens == true) {
|
||||
|
|
|
@ -139,11 +139,15 @@
|
|||
<input ng-model="clientEdit.serviceAccountsEnabled" name="serviceAccountsEnabled" id="serviceAccountsEnabled" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" data-ng-show="protocol == 'openid-connect' && !clientEdit.publicClient && !clientEdit.bearerOnly">
|
||||
<label class="col-md-2 control-label" for="oauth2DeviceAuthorizationGrantEnabled">{{:: 'oauth2-device-authorization-grant-enabled' | translate}}</label>
|
||||
<div class="form-group"
|
||||
data-ng-show="protocol == 'openid-connect' && !clientEdit.publicClient && !clientEdit.bearerOnly">
|
||||
<label class="col-md-2 control-label" for="oauth2DeviceAuthorizationGrantEnabled">{{::
|
||||
'oauth2-device-authorization-grant-enabled' | translate}}</label>
|
||||
<kc-tooltip>{{:: 'oauth2-device-authorization-grant-enabled.tooltip' | translate}}</kc-tooltip>
|
||||
<div class="col-md-6">
|
||||
<input ng-model="clientEdit.oauth2DeviceAuthorizationGrantEnabled" name="oauth2DeviceAuthorizationGrantEnabled" id="oauth2DeviceAuthorizationGrantEnabled" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
|
||||
<input ng-model="oauth2DeviceAuthorizationGrantEnabled" ng-click="switchChange()"
|
||||
name="oauth2DeviceAuthorizationGrantEnabled" id="oauth2DeviceAuthorizationGrantEnabled" onoffswitch
|
||||
on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" data-ng-show="protocol == 'openid-connect' && !clientEdit.publicClient && !clientEdit.bearerOnly">
|
||||
|
@ -669,6 +673,36 @@
|
|||
<kc-tooltip>{{:: 'client-offline-session-max.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="form-group"
|
||||
data-ng-show="protocol == 'openid-connect' && !clientEdit.publicClient && !clientEdit.bearerOnly && oauth2DeviceAuthorizationGrantEnabled == true">
|
||||
<label class="col-md-2 control-label" for="oauth2DeviceCodeLifespan">{{:: 'oauth2-device-code-lifespan'
|
||||
| translate}}</label>
|
||||
<div class="col-md-6 time-selector">
|
||||
<input class="form-control" type="number" min="1" max="31536000"
|
||||
data-ng-model="oauth2DeviceCodeLifespan.time" id="oauth2DeviceCodeLifespan"
|
||||
name="oauth2DeviceCodeLifespan" data-ng-change="updateOauth2DeviceCodeLifespan()" /> <select
|
||||
class="form-control" name="oauth2DeviceCodeLifespanUnit" data-ng-model="oauth2DeviceCodeLifespan.unit"
|
||||
data-ng-change="updateOauth2DeviceCodeLifespan()">
|
||||
<option value="Minutes">{{:: 'minutes' | translate}}</option>
|
||||
<option value="Hours">{{:: 'hours' | translate}}</option>
|
||||
<option value="Days">{{:: 'days' | translate}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<kc-tooltip>{{:: 'oauth2-device-code-lifespan.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="form-group"
|
||||
data-ng-show="protocol == 'openid-connect' && !clientEdit.publicClient && !clientEdit.bearerOnly && oauth2DeviceAuthorizationGrantEnabled == true">
|
||||
<label class="col-md-2 control-label" for="oauth2DevicePollingInterval">{{::
|
||||
'oauth2-device-polling-interval' | translate}}</label>
|
||||
<div class="col-md-6 time-selector">
|
||||
<input class="form-control" type="number" min="1" max="31536000" data-ng-model="oauth2DevicePollingInterval"
|
||||
id="oauth2DevicePollingInterval" name="oauth2DevicePollingInterval"
|
||||
data-ng-change="updateOauth2DevicePollingInterval()" />
|
||||
</div>
|
||||
<kc-tooltip>{{:: 'oauth2-device-polling-interval.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="form-group clearfix block" data-ng-show="protocol == 'openid-connect'">
|
||||
<label class="col-md-2 control-label" for="tlsClientCertificateBoundAccessTokens">{{:: 'tls-client-certificate-bound-access-tokens' | translate}}</label>
|
||||
<div class="col-sm-6">
|
||||
|
|
Loading…
Reference in a new issue