Skip to content

Commit

Permalink
KEYCLOAK-7675 Support for Device Authorization Grant
Browse files Browse the repository at this point in the history
  • Loading branch information
Michito-Okai authored and pedroigor committed Mar 15, 2021
1 parent f58bf0d commit 298ab0b
Show file tree
Hide file tree
Showing 41 changed files with 1,491 additions and 1,000 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -445,4 +448,12 @@ public Map<String, Object> getOtherClaims() {
public void setOtherClaims(String name, Object value) {
otherClaims.put(name, value);
}

public void setDeviceAuthorizationEndpoint(String deviceAuthorizationEndpoint) {
this.deviceAuthorizationEndpoint = deviceAuthorizationEndpoint;
}

public String getDeviceAuthorizationEndpoint() {
return deviceAuthorizationEndpoint;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -268,14 +268,6 @@ public void setServiceAccountsEnabled(Boolean serviceAccountsEnabled) {
this.serviceAccountsEnabled = serviceAccountsEnabled;
}

public Boolean isOAuth2DeviceAuthorizationGrantEnabled() {
return oauth2DeviceAuthorizationGrantEnabled;
}

public void setOAuth2DeviceAuthorizationGrantEnabled(Boolean oauth2DeviceAuthorizationGrantEnabled) {
this.oauth2DeviceAuthorizationGrantEnabled = oauth2DeviceAuthorizationGrantEnabled;
}

public Boolean getAuthorizationServicesEnabled() {
if (authorizationSettings != null) {
return true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;
import java.util.stream.Stream;

/**
Expand All @@ -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;
Expand Down Expand Up @@ -643,27 +646,11 @@ public Stream<RequiredCredentialModel> getRequiredCredentialsStream() {
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
Expand Down Expand Up @@ -1721,6 +1708,10 @@ public Map<String, String> getRealmLocalizationTextsByLocale(String locale) {
return Collections.unmodifiableMap(localizationTexts);
}

private RealmModel getRealm() {
return cacheSession.getRealmDelegate().getRealm(cached.getId());
}

@Override
public String toString() {
return String.format("%s@%08x", getId(), hashCode());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,15 @@
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;
Expand All @@ -43,6 +46,7 @@
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;

/**
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -213,8 +216,7 @@ public CachedRealm(Long revision, RealmModel model) {
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();
Expand Down Expand Up @@ -491,12 +493,8 @@ public int getAccessCodeLifespanLogin() {
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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public InfinispanOAuth2DeviceTokenStoreProvider(KeycloakSession session, Supplie
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;
Expand All @@ -74,7 +74,7 @@ public void close() {

@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 {
Expand Down Expand Up @@ -142,7 +142,7 @@ private OAuth2DeviceCodeModel findDeviceCodeByUserCode(RealmModel realm, String
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) {
Expand All @@ -164,7 +164,7 @@ public boolean approve(RealmModel realm, String userCode, String userSessionId)

// 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) {
Expand All @@ -189,7 +189,7 @@ public boolean deny(RealmModel realm, String userCode) {
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) {
Expand All @@ -207,7 +207,7 @@ public boolean deny(RealmModel realm, String userCode) {
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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,27 +17,21 @@

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;

/**
* @author <a href="mailto:h2-wada@nri.co.jp">Hiroyuki Wada</a>
*/
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;

Expand All @@ -50,25 +44,7 @@ public OAuth2DeviceTokenStoreProvider create(KeycloakSession session) {
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);
}
}
}
Expand Down
19 changes: 2 additions & 17 deletions model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
Original file line number Diff line number Diff line change
Expand Up @@ -588,23 +588,8 @@ public void setAccessCodeLifespanUserAction(int accessCodeLifespanUserAction) {
}

@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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Loading

0 comments on commit 298ab0b

Please sign in to comment.