KEYCLOAK-2085 Initial access tokens for client registration

This commit is contained in:
Stian Thorgersen 2015-11-17 20:35:09 +01:00
parent 67fca8f1f3
commit 764c20d748
46 changed files with 1292 additions and 315 deletions

View file

@ -3,6 +3,7 @@ package org.keycloak.client.registration;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpRequest;
import org.keycloak.common.util.Base64;
import org.keycloak.representations.idm.ClientInitialAccessPresentation;
import org.keycloak.representations.idm.ClientRepresentation;
/**
@ -16,6 +17,11 @@ public abstract class Auth {
return new BearerTokenAuth(token);
}
public static Auth token(ClientInitialAccessPresentation initialAccess) {
return new BearerTokenAuth(initialAccess.getToken());
}
public static Auth token(ClientRepresentation client) {
return new BearerTokenAuth(client.getRegistrationAccessToken());
}

View file

@ -2,6 +2,7 @@ package org.keycloak.client.registration;
import org.apache.http.client.HttpClient;
import org.apache.http.impl.client.HttpClients;
import org.codehaus.jackson.map.ObjectMapper;
import org.keycloak.representations.adapters.config.AdapterConfig;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.util.JsonSerialization;
@ -14,6 +15,11 @@ import java.io.InputStream;
*/
public class ClientRegistration {
public static final ObjectMapper outputMapper = new ObjectMapper();
static {
outputMapper.getSerializationConfig().addMixInAnnotations(ClientRepresentation.class, ClientRepresentationMixIn.class);
}
private final String DEFAULT = "default";
private final String INSTALLATION = "install";
@ -69,15 +75,16 @@ public class ClientRegistration {
httpUtil.doDelete(DEFAULT, clientId);
}
private String serialize(ClientRepresentation client) throws ClientRegistrationException {
public static String serialize(ClientRepresentation client) throws ClientRegistrationException {
try {
return JsonSerialization.writeValueAsString(client);
return outputMapper.writeValueAsString(client);
} catch (IOException e) {
throw new ClientRegistrationException("Failed to write json object", e);
}
}
private <T> T deserialize(InputStream inputStream, Class<T> clazz) throws ClientRegistrationException {
private static <T> T deserialize(InputStream inputStream, Class<T> clazz) throws ClientRegistrationException {
try {
return JsonSerialization.readValue(inputStream, clazz);
} catch (IOException e) {

View file

@ -0,0 +1,13 @@
package org.keycloak.client.registration;
import org.codehaus.jackson.annotate.JsonIgnore;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
abstract class ClientRepresentationMixIn {
@JsonIgnore
String registrationAccessToken;
}

View file

@ -59,7 +59,7 @@
<addForeignKeyConstraint baseColumnNames="ROLE_ID" baseTableName="GROUP_ROLE_MAPPING" constraintName="FK_GROUP_ROLE_ROLE" referencedColumnNames="ID" referencedTableName="KEYCLOAK_ROLE"/>
<addColumn tableName="CLIENT">
<column name="REGISTRATION_SECRET" type="VARCHAR(255)"/>
<column name="REGISTRATION_TOKEN" type="VARCHAR(255)"/>
</addColumn>
</changeSet>

View file

@ -0,0 +1,36 @@
package org.keycloak.representations.idm;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class ClientInitialAccessCreatePresentation {
private Integer expiration;
private Integer count;
public ClientInitialAccessCreatePresentation() {
}
public ClientInitialAccessCreatePresentation(Integer expiration, Integer count) {
this.expiration = expiration;
this.count = count;
}
public Integer getExpiration() {
return expiration;
}
public void setExpiration(Integer expiration) {
this.expiration = expiration;
}
public Integer getCount() {
return count;
}
public void setCount(Integer count) {
this.count = count;
}
}

View file

@ -0,0 +1,67 @@
package org.keycloak.representations.idm;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class ClientInitialAccessPresentation {
private String id;
private String token;
private Integer timestamp;
private Integer expiration;
private Integer count;
private Integer remainingCount;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public Integer getTimestamp() {
return timestamp;
}
public void setTimestamp(Integer timestamp) {
this.timestamp = timestamp;
}
public Integer getExpiration() {
return expiration;
}
public void setExpiration(Integer expiration) {
this.expiration = expiration;
}
public Integer getCount() {
return count;
}
public void setCount(Integer count) {
this.count = count;
}
public Integer getRemainingCount() {
return remainingCount;
}
public void setRemainingCount(Integer remainingCount) {
this.remainingCount = remainingCount;
}
}

View file

@ -19,6 +19,7 @@ public class TokenUtil {
public static final String TOKEN_TYPE_OFFLINE = "Offline";
public static boolean isOfflineTokenRequested(String scopeParam) {
if (scopeParam == null) {
return false;

View file

@ -0,0 +1,31 @@
package org.keycloak.admin.client.resource;
import org.keycloak.representations.idm.ClientInitialAccessCreatePresentation;
import org.keycloak.representations.idm.ClientInitialAccessPresentation;
import javax.ws.rs.*;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import java.util.List;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public interface ClientInitialAccessResource {
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
ClientInitialAccessPresentation create(ClientInitialAccessCreatePresentation rep);
@GET
@Produces(MediaType.APPLICATION_JSON)
List<ClientInitialAccessPresentation> list();
@DELETE
@Path("{id}")
void delete(final @PathParam("id") String id);
}

View file

@ -45,5 +45,8 @@ public interface RealmResource {
@Path("client-session-stats")
@GET
List<Map<String, String>> getClientSessionStats();
@Path("clients-initial-access")
ClientInitialAccessResource clientInitialAccess();
}

View file

@ -0,0 +1,22 @@
package org.keycloak.models;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public interface ClientInitialAccessModel {
String getId();
RealmModel getRealm();
int getTimestamp();
int getExpiration();
int getCount();
int getRemainingCount();
void decreaseRemainingCount();
}

View file

@ -90,8 +90,8 @@ public interface ClientModel extends RoleContainerModel {
String getSecret();
public void setSecret(String secret);
String getRegistrationSecret();
void setRegistrationSecret(String registrationSecret);
String getRegistrationToken();
void setRegistrationToken(String registrationToken);
boolean isFullScopeAllowed();
void setFullScopeAllowed(boolean value);

View file

@ -63,6 +63,11 @@ public interface UserSessionProvider extends Provider {
UserSessionModel importUserSession(UserSessionModel persistentUserSession, boolean offline);
ClientSessionModel importClientSession(ClientSessionModel persistentClientSession, boolean offline);
ClientInitialAccessModel createClientInitialAccessModel(RealmModel realm, int expiration, int count);
ClientInitialAccessModel getClientInitialAccessModel(RealmModel realm, String id);
void removeClientInitialAccessModel(RealmModel realm, String id);
List<ClientInitialAccessModel> listClientInitialAccess(RealmModel realm);
void close();
}

View file

@ -17,7 +17,7 @@ public class ClientEntity extends AbstractIdentifiableEntity {
private boolean enabled;
private String clientAuthenticatorType;
private String secret;
private String registrationSecret;
private String registrationToken;
private String protocol;
private int notBefore;
private boolean publicClient;
@ -91,12 +91,12 @@ public class ClientEntity extends AbstractIdentifiableEntity {
this.secret = secret;
}
public String getRegistrationSecret() {
return registrationSecret;
public String getRegistrationToken() {
return registrationToken;
}
public void setRegistrationSecret(String registrationSecret) {
this.registrationSecret = registrationSecret;
public void setRegistrationToken(String registrationToken) {
this.registrationToken = registrationToken;
}
public int getNotBefore() {

View file

@ -43,8 +43,6 @@ import java.util.UUID;
*/
public final class KeycloakModelUtils {
private static final int RANDOM_PASSWORD_BYTES = 32;
private KeycloakModelUtils() {
}
@ -182,16 +180,6 @@ public final class KeycloakModelUtils {
return secret;
}
public static void generateRegistrationAccessToken(ClientModel client) {
client.setRegistrationSecret(generatePassword());
}
public static String generatePassword() {
byte[] buf = new byte[RANDOM_PASSWORD_BYTES];
new SecureRandom().nextBytes(buf);
return Base64Url.encode(buf);
}
public static String getDefaultClientAuthenticatorType() {
return "client-secret";
}

View file

@ -387,7 +387,6 @@ public class ModelToRepresentation {
rep.setNotBefore(clientModel.getNotBefore());
rep.setNodeReRegistrationTimeout(clientModel.getNodeReRegistrationTimeout());
rep.setClientAuthenticatorType(clientModel.getClientAuthenticatorType());
rep.setRegistrationAccessToken(clientModel.getRegistrationSecret());
Set<String> redirectUris = clientModel.getRedirectUris();
if (redirectUris != null) {

View file

@ -737,8 +737,6 @@ public class RepresentationToModel {
KeycloakModelUtils.generateSecret(client);
}
client.setRegistrationSecret(resourceRep.getRegistrationAccessToken());
if (resourceRep.getAttributes() != null) {
for (Map.Entry<String, String> entry : resourceRep.getAttributes().entrySet()) {
client.setAttribute(entry.getKey(), entry.getValue());
@ -815,7 +813,6 @@ public class RepresentationToModel {
if (rep.isSurrogateAuthRequired() != null) resource.setSurrogateAuthRequired(rep.isSurrogateAuthRequired());
if (rep.getNodeReRegistrationTimeout() != null) resource.setNodeReRegistrationTimeout(rep.getNodeReRegistrationTimeout());
if (rep.getClientAuthenticatorType() != null) resource.setClientAuthenticatorType(rep.getClientAuthenticatorType());
if (rep.getRegistrationAccessToken() != null) resource.setRegistrationSecret(rep.getRegistrationAccessToken());
resource.updateClient();
if (rep.getProtocol() != null) resource.setProtocol(rep.getProtocol());

View file

@ -120,14 +120,14 @@ public class ClientAdapter implements ClientModel {
getDelegateForUpdate();
updated.setSecret(secret);
}
public String getRegistrationSecret() {
if (updated != null) return updated.getRegistrationSecret();
return cached.getRegistrationSecret();
public String getRegistrationToken() {
if (updated != null) return updated.getRegistrationToken();
return cached.getRegistrationToken();
}
public void setRegistrationSecret(String registrationsecret) {
public void setRegistrationToken(String registrationToken) {
getDelegateForUpdate();
updated.setRegistrationSecret(registrationsecret);
updated.setRegistrationToken(registrationToken);
}
public boolean isPublicClient() {

View file

@ -31,7 +31,7 @@ public class CachedClient implements Serializable {
private boolean enabled;
private String clientAuthenticatorType;
private String secret;
private String registrationSecret;
private String registrationToken;
private String protocol;
private Map<String, String> attributes = new HashMap<String, String>();
private boolean publicClient;
@ -58,7 +58,7 @@ public class CachedClient implements Serializable {
id = model.getId();
clientAuthenticatorType = model.getClientAuthenticatorType();
secret = model.getSecret();
registrationSecret = model.getRegistrationSecret();
registrationToken = model.getRegistrationToken();
clientId = model.getClientId();
name = model.getName();
description = model.getDescription();
@ -131,8 +131,8 @@ public class CachedClient implements Serializable {
return secret;
}
public String getRegistrationSecret() {
return registrationSecret;
public String getRegistrationToken() {
return registrationToken;
}
public boolean isPublicClient() {

View file

@ -178,13 +178,13 @@ public class ClientAdapter implements ClientModel {
}
@Override
public String getRegistrationSecret() {
return entity.getRegistrationSecret();
public String getRegistrationToken() {
return entity.getRegistrationToken();
}
@Override
public void setRegistrationSecret(String registrationSecret) {
entity.setRegistrationSecret(registrationSecret);
public void setRegistrationToken(String registrationToken) {
entity.setRegistrationToken(registrationToken);
}
@Override

View file

@ -42,8 +42,8 @@ public class ClientEntity {
private boolean enabled;
@Column(name="SECRET")
private String secret;
@Column(name="REGISTRATION_SECRET")
private String registrationSecret;
@Column(name="REGISTRATION_TOKEN")
private String registrationToken;
@Column(name="CLIENT_AUTHENTICATOR_TYPE")
private String clientAuthenticatorType;
@Column(name="NOT_BEFORE")
@ -203,12 +203,12 @@ public class ClientEntity {
this.secret = secret;
}
public String getRegistrationSecret() {
return registrationSecret;
public String getRegistrationToken() {
return registrationToken;
}
public void setRegistrationSecret(String registrationSecret) {
this.registrationSecret = registrationSecret;
public void setRegistrationToken(String registrationToken) {
this.registrationToken = registrationToken;
}
public int getNotBefore() {

View file

@ -178,13 +178,13 @@ public class ClientAdapter extends AbstractMongoAdapter<MongoClientEntity> imple
}
@Override
public String getRegistrationSecret() {
return getMongoEntity().getRegistrationSecret();
public String getRegistrationToken() {
return getMongoEntity().getRegistrationToken();
}
@Override
public void setRegistrationSecret(String registrationSecretsecret) {
getMongoEntity().setRegistrationSecret(registrationSecretsecret);
public void setRegistrationToken(String registrationToken) {
getMongoEntity().setRegistrationToken(registrationToken);
updateMongoEntity();
}

View file

@ -0,0 +1,69 @@
package org.keycloak.models.sessions.infinispan;
import org.infinispan.Cache;
import org.keycloak.models.ClientInitialAccessModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.sessions.infinispan.entities.ClientInitialAccessEntity;
import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class ClientInitialAccessAdapter implements ClientInitialAccessModel {
private final KeycloakSession session;
private final InfinispanUserSessionProvider provider;
private final Cache<String, SessionEntity> cache;
private final RealmModel realm;
private final ClientInitialAccessEntity entity;
public ClientInitialAccessAdapter(KeycloakSession session, InfinispanUserSessionProvider provider, Cache<String, SessionEntity> cache, RealmModel realm, ClientInitialAccessEntity entity) {
this.session = session;
this.provider = provider;
this.cache = cache;
this.realm = realm;
this.entity = entity;
}
@Override
public String getId() {
return entity.getId();
}
@Override
public RealmModel getRealm() {
return realm;
}
@Override
public int getTimestamp() {
return entity.getTimestamp();
}
@Override
public int getExpiration() {
return entity.getExpiration();
}
@Override
public int getCount() {
return entity.getCount();
}
@Override
public int getRemainingCount() {
return entity.getRemainingCount();
}
@Override
public void decreaseRemainingCount() {
entity.setRemainingCount(entity.getRemainingCount() - 1);
update();
}
void update() {
provider.getTx().replace(cache, entity.getId(), entity);
}
}

View file

@ -3,28 +3,10 @@ package org.keycloak.models.sessions.infinispan;
import org.infinispan.Cache;
import org.infinispan.distexec.mapreduce.MapReduceTask;
import org.jboss.logging.Logger;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakTransaction;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.UserSessionProvider;
import org.keycloak.models.UsernameLoginFailureModel;
import org.keycloak.models.*;
import org.keycloak.models.session.UserSessionPersisterProvider;
import org.keycloak.models.sessions.infinispan.entities.ClientSessionEntity;
import org.keycloak.models.sessions.infinispan.entities.LoginFailureEntity;
import org.keycloak.models.sessions.infinispan.entities.LoginFailureKey;
import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
import org.keycloak.models.sessions.infinispan.mapreduce.ClientSessionMapper;
import org.keycloak.models.sessions.infinispan.mapreduce.FirstResultReducer;
import org.keycloak.models.sessions.infinispan.mapreduce.LargestResultReducer;
import org.keycloak.models.sessions.infinispan.mapreduce.SessionMapper;
import org.keycloak.models.sessions.infinispan.mapreduce.UserLoginFailureMapper;
import org.keycloak.models.sessions.infinispan.mapreduce.UserSessionMapper;
import org.keycloak.models.sessions.infinispan.mapreduce.UserSessionNoteMapper;
import org.keycloak.models.sessions.infinispan.entities.*;
import org.keycloak.models.sessions.infinispan.mapreduce.*;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.RealmInfoUtil;
import org.keycloak.common.util.Time;
@ -355,6 +337,15 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
persister.removeClientSession(clientSessionId, true);
}
// Remove expired client initial access
map = new MapReduceTask(sessionCache)
.mappedWith(ClientInitialAccessMapper.create(realm.getId()).time(Time.currentTime()).remainingCount(0).emitKey())
.reducedWith(new FirstResultReducer())
.execute();
for (String id : map.keySet()) {
tx.remove(sessionCache, id);
}
}
@Override
@ -538,11 +529,24 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
return models;
}
List<ClientInitialAccessModel> wrapClientInitialAccess(RealmModel realm, Collection<ClientInitialAccessEntity> entities) {
List<ClientInitialAccessModel> models = new LinkedList<>();
for (ClientInitialAccessEntity e : entities) {
models.add(wrap(realm, e));
}
return models;
}
ClientSessionAdapter wrap(RealmModel realm, ClientSessionEntity entity, boolean offline) {
Cache<String, SessionEntity> cache = getCache(offline);
return entity != null ? new ClientSessionAdapter(session, this, cache, realm, entity, offline) : null;
}
ClientInitialAccessAdapter wrap(RealmModel realm, ClientInitialAccessEntity entity) {
Cache<String, SessionEntity> cache = getCache(false);
return entity != null ? new ClientInitialAccessAdapter(session, this, cache, realm, entity) : null;
}
UsernameLoginFailureModel wrap(LoginFailureKey key, LoginFailureEntity entity) {
return entity != null ? new UsernameLoginFailureAdapter(this, loginFailureCache, key, entity) : null;
@ -680,6 +684,50 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
return wrap(clientSession.getRealm(), entity, offline);
}
@Override
public ClientInitialAccessModel createClientInitialAccessModel(RealmModel realm, int expiration, int count) {
String id = KeycloakModelUtils.generateId();
ClientInitialAccessEntity entity = new ClientInitialAccessEntity();
entity.setId(id);
entity.setRealm(realm.getId());
entity.setTimestamp(Time.currentTime());
entity.setExpiration(expiration);
entity.setCount(count);
entity.setRemainingCount(count);
tx.put(sessionCache, id, entity);
return wrap(realm, entity);
}
@Override
public ClientInitialAccessModel getClientInitialAccessModel(RealmModel realm, String id) {
Cache<String, SessionEntity> cache = getCache(false);
ClientInitialAccessEntity entity = (ClientInitialAccessEntity) cache.get(id);
// If created in this transaction
if (entity == null) {
entity = (ClientInitialAccessEntity) tx.get(cache, id);
}
return wrap(realm, entity);
}
@Override
public void removeClientInitialAccessModel(RealmModel realm, String id) {
tx.remove(getCache(false), id);
}
@Override
public List<ClientInitialAccessModel> listClientInitialAccess(RealmModel realm) {
Map<String, ClientInitialAccessEntity> entities = new MapReduceTask(sessionCache)
.mappedWith(ClientInitialAccessMapper.create(realm.getId()))
.reducedWith(new FirstResultReducer())
.execute();
return wrapClientInitialAccess(realm, entities.values());
}
class InfinispanKeycloakTransaction implements KeycloakTransaction {
private boolean active;

View file

@ -0,0 +1,55 @@
package org.keycloak.models.sessions.infinispan.compat;
import org.keycloak.models.ClientInitialAccessModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.sessions.infinispan.compat.entities.ClientInitialAccessEntity;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class ClientInitialAccessAdapter implements ClientInitialAccessModel {
private final RealmModel realm;
private final ClientInitialAccessEntity entity;
public ClientInitialAccessAdapter(RealmModel realm, ClientInitialAccessEntity entity) {
this.realm = realm;
this.entity = entity;
}
@Override
public String getId() {
return entity.getId();
}
@Override
public RealmModel getRealm() {
return realm;
}
@Override
public int getTimestamp() {
return entity.getTimestamp();
}
@Override
public int getExpiration() {
return entity.getExpires();
}
@Override
public int getCount() {
return entity.getCount();
}
@Override
public int getRemainingCount() {
return entity.getRemainingCount();
}
@Override
public void decreaseRemainingCount() {
entity.setRemainingCount(entity.getRemainingCount() - 1);
}
}

View file

@ -1,19 +1,8 @@
package org.keycloak.models.sessions.infinispan.compat;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.UserSessionProvider;
import org.keycloak.models.UsernameLoginFailureModel;
import org.keycloak.models.*;
import org.keycloak.models.session.UserSessionPersisterProvider;
import org.keycloak.models.sessions.infinispan.compat.entities.ClientSessionEntity;
import org.keycloak.models.sessions.infinispan.compat.entities.UserSessionEntity;
import org.keycloak.models.sessions.infinispan.compat.entities.UsernameLoginFailureEntity;
import org.keycloak.models.sessions.infinispan.compat.entities.UsernameLoginFailureKey;
import org.keycloak.models.sessions.infinispan.compat.entities.*;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.RealmInfoUtil;
import org.keycloak.common.util.Time;
@ -41,11 +30,12 @@ public class MemUserSessionProvider implements UserSessionProvider {
private final ConcurrentHashMap<String, UserSessionEntity> offlineUserSessions;
private final ConcurrentHashMap<String, ClientSessionEntity> offlineClientSessions;
private ConcurrentHashMap<String, ClientInitialAccessEntity> clientInitialAccess;
public MemUserSessionProvider(KeycloakSession session, ConcurrentHashMap<String, UserSessionEntity> userSessions, ConcurrentHashMap<String, String> userSessionsByBrokerSessionId,
ConcurrentHashMap<String, Set<String>> userSessionsByBrokerUserId, ConcurrentHashMap<String, ClientSessionEntity> clientSessions,
ConcurrentHashMap<UsernameLoginFailureKey, UsernameLoginFailureEntity> loginFailures,
ConcurrentHashMap<String, UserSessionEntity> offlineUserSessions, ConcurrentHashMap<String, ClientSessionEntity> offlineClientSessions) {
ConcurrentHashMap<String, UserSessionEntity> offlineUserSessions, ConcurrentHashMap<String, ClientSessionEntity> offlineClientSessions, ConcurrentHashMap<String, ClientInitialAccessEntity> clientInitialAccess) {
this.session = session;
this.userSessions = userSessions;
this.clientSessions = clientSessions;
@ -54,6 +44,7 @@ public class MemUserSessionProvider implements UserSessionProvider {
this.userSessionsByBrokerUserId = userSessionsByBrokerUserId;
this.offlineUserSessions = offlineUserSessions;
this.offlineClientSessions = offlineClientSessions;
this.clientInitialAccess = clientInitialAccess;
}
@Override
@ -341,6 +332,15 @@ public class MemUserSessionProvider implements UserSessionProvider {
persister.removeClientSession(s.getId(), true);
}
}
// Remove expired initial access
Iterator<ClientInitialAccessEntity> iaitr = clientInitialAccess.values().iterator();
while (iaitr.hasNext()) {
ClientInitialAccessEntity e = iaitr.next();
if (e.getRealmId().equals(realm.getId()) && (e.getExpires() < Time.currentTime())) {
iaitr.remove();
}
}
}
@Override
@ -574,6 +574,43 @@ public class MemUserSessionProvider implements UserSessionProvider {
return getUserSessions(realm, client, first, max, true);
}
@Override
public ClientInitialAccessModel createClientInitialAccessModel(RealmModel realm, int expiration, int count) {
String id = KeycloakModelUtils.generateId();
ClientInitialAccessEntity entity = new ClientInitialAccessEntity();
entity.setId(id);
entity.setRealmId(realm.getId());
entity.setTimestamp(Time.currentTime());
entity.setExpiration(expiration);
entity.setCount(count);
entity.setRemainingCount(count);
clientInitialAccess.put(id, entity);
return new ClientInitialAccessAdapter(realm, entity);
}
@Override
public ClientInitialAccessModel getClientInitialAccessModel(RealmModel realm, String id) {
ClientInitialAccessEntity entity = clientInitialAccess.get(id);
return entity != null ? new ClientInitialAccessAdapter(realm, entity) : null;
}
@Override
public void removeClientInitialAccessModel(RealmModel realm, String id) {
clientInitialAccess.remove(id);
}
@Override
public List<ClientInitialAccessModel> listClientInitialAccess(RealmModel realm) {
List<ClientInitialAccessModel> models = new LinkedList<>();
for (ClientInitialAccessEntity e : clientInitialAccess.values()) {
models.add(new ClientInitialAccessAdapter(realm, e));
}
return models;
}
@Override
public void close() {
}

View file

@ -1,14 +1,12 @@
package org.keycloak.models.sessions.infinispan.compat;
import org.keycloak.Config;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.UserSessionProvider;
import org.keycloak.models.UserSessionProviderFactory;
import org.keycloak.models.sessions.infinispan.compat.entities.ClientSessionEntity;
import org.keycloak.models.sessions.infinispan.compat.entities.UserSessionEntity;
import org.keycloak.models.sessions.infinispan.compat.entities.UsernameLoginFailureEntity;
import org.keycloak.models.sessions.infinispan.compat.entities.UsernameLoginFailureKey;
import org.keycloak.models.sessions.infinispan.compat.entities.ClientInitialAccessEntity;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
@ -29,9 +27,11 @@ public class MemUserSessionProviderFactory {
private ConcurrentHashMap<String, UserSessionEntity> offlineUserSessions = new ConcurrentHashMap<String, UserSessionEntity>();
private ConcurrentHashMap<String, ClientSessionEntity> offlineClientSessions = new ConcurrentHashMap<String, ClientSessionEntity>();
private ConcurrentHashMap<String, ClientInitialAccessEntity> clientInitialAccess = new ConcurrentHashMap<>();
public UserSessionProvider create(KeycloakSession session) {
return new MemUserSessionProvider(session, userSessions, userSessionsByBrokerSessionId, userSessionsByBrokerUserId, clientSessions, loginFailures,
offlineUserSessions, offlineClientSessions);
offlineUserSessions, offlineClientSessions, clientInitialAccess);
}
public void close() {

View file

@ -0,0 +1,68 @@
package org.keycloak.models.sessions.infinispan.compat.entities;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class ClientInitialAccessEntity {
private String id;
private String realmId;
private int timestamp;
private int expires;
private int count;
private int remainingCount;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getRealmId() {
return realmId;
}
public void setRealmId(String realmId) {
this.realmId = realmId;
}
public int getTimestamp() {
return timestamp;
}
public void setTimestamp(int timestamp) {
this.timestamp = timestamp;
}
public int getExpires() {
return expires;
}
public void setExpiration(int expires) {
this.expires = expires;
}
public int getCount() {
return count;
}
public void setCount(int count) {
this.count = count;
}
public int getRemainingCount() {
return remainingCount;
}
public void setRemainingCount(int remainingCount) {
this.remainingCount = remainingCount;
}
}

View file

@ -0,0 +1,48 @@
package org.keycloak.models.sessions.infinispan.entities;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class ClientInitialAccessEntity extends SessionEntity {
private int timestamp;
private int expires;
private int count;
private int remainingCount;
public int getTimestamp() {
return timestamp;
}
public void setTimestamp(int timestamp) {
this.timestamp = timestamp;
}
public int getExpiration() {
return expires;
}
public void setExpiration(int expires) {
this.expires = expires;
}
public int getCount() {
return count;
}
public void setCount(int count) {
this.count = count;
}
public int getRemainingCount() {
return remainingCount;
}
public void setRemainingCount(int remainingCount) {
this.remainingCount = remainingCount;
}
}

View file

@ -0,0 +1,80 @@
package org.keycloak.models.sessions.infinispan.mapreduce;
import org.infinispan.distexec.mapreduce.Collector;
import org.infinispan.distexec.mapreduce.Mapper;
import org.keycloak.models.sessions.infinispan.entities.ClientInitialAccessEntity;
import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
import java.io.Serializable;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class ClientInitialAccessMapper implements Mapper<String, SessionEntity, String, Object>, Serializable {
public ClientInitialAccessMapper(String realm) {
this.realm = realm;
}
private enum EmitValue {
KEY, ENTITY
}
private String realm;
private EmitValue emit = EmitValue.ENTITY;
private Integer time;
private Integer remainingCount;
public static ClientInitialAccessMapper create(String realm) {
return new ClientInitialAccessMapper(realm);
}
public ClientInitialAccessMapper emitKey() {
emit = EmitValue.KEY;
return this;
}
public ClientInitialAccessMapper time(int time) {
this.time = time;
return this;
}
public ClientInitialAccessMapper remainingCount(int remainingCount) {
this.remainingCount = remainingCount;
return this;
}
@Override
public void map(String key, SessionEntity e, Collector collector) {
if (!realm.equals(e.getRealm())) {
return;
}
if (!(e instanceof ClientInitialAccessEntity)) {
return;
}
ClientInitialAccessEntity entity = (ClientInitialAccessEntity) e;
if (time != null && entity.getExpiration() > 0 && (entity.getTimestamp() + entity.getExpiration()) < time) {
return;
}
if (remainingCount != null && entity.getRemainingCount() == remainingCount) {
return;
}
switch (emit) {
case KEY:
collector.emit(key, key);
break;
case ENTITY:
collector.emit(key, entity);
break;
}
}
}

View file

@ -2,26 +2,10 @@ package org.keycloak.protocol.saml.clientregistration;
import org.jboss.logging.Logger;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.exportimport.ClientDescriptionConverter;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.models.utils.RepresentationToModel;
import org.keycloak.protocol.saml.EntityDescriptorDescriptionConverter;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.services.ErrorResponse;
import org.keycloak.services.clientregistration.ClientRegAuth;
import org.keycloak.services.clientregistration.ClientRegistrationAuth;
import org.keycloak.services.clientregistration.ClientRegistrationProvider;
import javax.ws.rs.Consumes;
import javax.ws.rs.POST;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.net.URI;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
@ -31,7 +15,7 @@ public class EntityDescriptorClientRegistrationProvider implements ClientRegistr
private KeycloakSession session;
private EventBuilder event;
private ClientRegAuth auth;
private ClientRegistrationAuth auth;
public EntityDescriptorClientRegistrationProvider(KeycloakSession session) {
this.session = session;
@ -67,7 +51,7 @@ public class EntityDescriptorClientRegistrationProvider implements ClientRegistr
}
@Override
public void setAuth(ClientRegAuth auth) {
public void setAuth(ClientRegistrationAuth auth) {
this.auth = auth;
}

View file

@ -1,6 +1,5 @@
package org.keycloak.services.clientregistration;
import org.jboss.resteasy.spi.UnauthorizedException;
import org.keycloak.authentication.AuthenticationProcessor;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
@ -23,7 +22,7 @@ public class AdapterInstallationClientRegistrationProvider implements ClientRegi
private KeycloakSession session;
private EventBuilder event;
private ClientRegAuth auth;
private ClientRegistrationAuth auth;
public AdapterInstallationClientRegistrationProvider(KeycloakSession session) {
this.session = session;
@ -51,7 +50,7 @@ public class AdapterInstallationClientRegistrationProvider implements ClientRegi
}
@Override
public void setAuth(ClientRegAuth auth) {
public void setAuth(ClientRegistrationAuth auth) {
this.auth = auth;
}

View file

@ -1,134 +0,0 @@
package org.keycloak.services.clientregistration;
import org.jboss.resteasy.spi.UnauthorizedException;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.models.*;
import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil;
import org.keycloak.representations.AccessToken;
import org.keycloak.services.ForbiddenException;
import org.keycloak.services.managers.AppAuthManager;
import org.keycloak.services.managers.AuthenticationManager;
import javax.ws.rs.core.HttpHeaders;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class ClientRegAuth {
private KeycloakSession session;
private EventBuilder event;
private String token;
private AccessToken.Access bearerRealmAccess;
private boolean authenticated = false;
private boolean registrationAccessToken = false;
public ClientRegAuth(KeycloakSession session, EventBuilder event) {
this.session = session;
this.event = event;
init();
}
private void init() {
RealmModel realm = session.getContext().getRealm();
String authorizationHeader = session.getContext().getRequestHeaders().getRequestHeaders().getFirst(HttpHeaders.AUTHORIZATION);
if (authorizationHeader == null) {
return;
}
String[] split = authorizationHeader.split(" ");
if (!split[0].equalsIgnoreCase("bearer")) {
return;
}
if (split[1].indexOf('.') == -1) {
token = split[1];
authenticated = true;
registrationAccessToken = true;
} else {
AuthenticationManager.AuthResult authResult = new AppAuthManager().authenticateBearerToken(session, realm);
bearerRealmAccess = authResult.getToken().getResourceAccess(Constants.REALM_MANAGEMENT_CLIENT_ID);
authenticated = true;
}
}
public boolean isAuthenticated() {
return authenticated;
}
public boolean isRegistrationAccessToken() {
return registrationAccessToken;
}
public void requireCreate() {
if (!authenticated) {
event.error(Errors.NOT_ALLOWED);
throw new UnauthorizedException();
}
if (bearerRealmAccess != null) {
if (bearerRealmAccess.isUserInRole(AdminRoles.MANAGE_CLIENTS) || bearerRealmAccess.isUserInRole(AdminRoles.CREATE_CLIENT)) {
return;
}
}
event.error(Errors.NOT_ALLOWED);
throw new ForbiddenException();
}
public void requireView(ClientModel client) {
if (!authenticated) {
event.error(Errors.NOT_ALLOWED);
throw new UnauthorizedException();
}
if (client == null) {
event.error(Errors.NOT_ALLOWED);
throw new ForbiddenException();
}
if (bearerRealmAccess != null) {
if (bearerRealmAccess.isUserInRole(AdminRoles.MANAGE_CLIENTS) || bearerRealmAccess.isUserInRole(AdminRoles.VIEW_CLIENTS)) {
return;
}
} else if (token != null) {
if (client.getRegistrationSecret() != null && client.getRegistrationSecret().equals(token)) {
return;
}
}
event.error(Errors.NOT_ALLOWED);
throw new ForbiddenException();
}
public void requireUpdate(ClientModel client) {
if (!authenticated) {
event.error(Errors.NOT_ALLOWED);
throw new UnauthorizedException();
}
if (client == null) {
event.error(Errors.NOT_ALLOWED);
throw new ForbiddenException();
}
if (bearerRealmAccess != null) {
if (bearerRealmAccess.isUserInRole(AdminRoles.MANAGE_CLIENTS) || bearerRealmAccess.isUserInRole(AdminRoles.VIEW_CLIENTS)) {
return;
}
} else if (token != null) {
if (client.getRegistrationSecret() != null && client.getRegistrationSecret().equals(token)) {
return;
}
}
event.error(Errors.NOT_ALLOWED);
throw new ForbiddenException();
}
}

View file

@ -0,0 +1,182 @@
package org.keycloak.services.clientregistration;
import org.jboss.resteasy.spi.UnauthorizedException;
import org.keycloak.common.util.Time;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.models.*;
import org.keycloak.representations.JsonWebToken;
import org.keycloak.services.ForbiddenException;
import org.keycloak.util.TokenUtil;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.UriInfo;
import java.util.List;
import java.util.Map;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class ClientRegistrationAuth {
private KeycloakSession session;
private EventBuilder event;
private JsonWebToken jwt;
private ClientInitialAccessModel initialAccessModel;
public ClientRegistrationAuth(KeycloakSession session, EventBuilder event) {
this.session = session;
this.event = event;
init();
}
private void init() {
RealmModel realm = session.getContext().getRealm();
UriInfo uri = session.getContext().getUri();
String authorizationHeader = session.getContext().getRequestHeaders().getRequestHeaders().getFirst(HttpHeaders.AUTHORIZATION);
if (authorizationHeader == null) {
return;
}
String[] split = authorizationHeader.split(" ");
if (!split[0].equalsIgnoreCase("bearer")) {
return;
}
jwt = ClientRegistrationTokenUtils.parseToken(realm, uri, split[1]);
if (isInitialAccessToken()) {
initialAccessModel = session.sessions().getClientInitialAccessModel(session.getContext().getRealm(), jwt.getId());
if (initialAccessModel == null) {
throw new ForbiddenException();
}
}
}
public boolean isAuthenticated() {
return jwt != null;
}
public boolean isBearerToken() {
return TokenUtil.TOKEN_TYPE_BEARER.equals(jwt.getType());
}
public boolean isInitialAccessToken() {
return ClientRegistrationTokenUtils.TYPE_INITIAL_ACCESS_TOKEN.equals(jwt.getType());
}
public boolean isRegistrationAccessToken() {
return ClientRegistrationTokenUtils.TYPE_REGISTRATION_ACCESS_TOKEN.equals(jwt.getType());
}
public void requireCreate() {
if (!isAuthenticated()) {
event.error(Errors.NOT_ALLOWED);
throw new UnauthorizedException();
}
if (isBearerToken()) {
if (hasRole(AdminRoles.MANAGE_CLIENTS, AdminRoles.CREATE_CLIENT)) {
return;
}
} else if (isInitialAccessToken()) {
if (initialAccessModel.getRemainingCount() > 0) {
if (initialAccessModel.getExpiration() == 0 || (initialAccessModel.getTimestamp() + initialAccessModel.getExpiration()) > Time.currentTime()) {
return;
}
}
}
event.error(Errors.NOT_ALLOWED);
throw new ForbiddenException();
}
public void requireView(ClientModel client) {
if (!isAuthenticated()) {
event.error(Errors.NOT_ALLOWED);
throw new UnauthorizedException();
}
if (client == null) {
event.error(Errors.NOT_ALLOWED);
throw new ForbiddenException();
}
if (isBearerToken()) {
if (hasRole(AdminRoles.MANAGE_CLIENTS, AdminRoles.VIEW_CLIENTS)) {
return;
}
} else if (isRegistrationAccessToken()) {
if (client.getRegistrationToken() != null && client.getRegistrationToken().equals(jwt.getId())) {
return;
}
}
event.error(Errors.NOT_ALLOWED);
throw new ForbiddenException();
}
public void requireUpdate(ClientModel client) {
if (!isAuthenticated()) {
event.error(Errors.NOT_ALLOWED);
throw new UnauthorizedException();
}
if (client == null) {
event.error(Errors.NOT_ALLOWED);
throw new ForbiddenException();
}
if (isBearerToken()) {
if (hasRole(AdminRoles.MANAGE_CLIENTS)) {
return;
}
} else if (isRegistrationAccessToken()) {
if (client.getRegistrationToken() != null && client.getRegistrationToken().equals(jwt.getId())) {
return;
}
}
event.error(Errors.NOT_ALLOWED);
throw new ForbiddenException();
}
public ClientInitialAccessModel getInitialAccessModel() {
return initialAccessModel;
}
private boolean hasRole(String... role) {
try {
Map<String, Object> otherClaims = jwt.getOtherClaims();
if (otherClaims != null) {
Map<String, Map<String, List<String>>> resourceAccess = (Map<String, Map<String, List<String>>>) jwt.getOtherClaims().get("resource_access");
if (resourceAccess == null) {
return false;
}
Map<String, List<String>> realmManagement = resourceAccess.get(Constants.REALM_MANAGEMENT_CLIENT_ID);
if (realmManagement == null) {
return false;
}
List<String> resources = realmManagement.get("roles");
if (resources == null) {
return false;
}
for (String r : role) {
if (resources.contains(r)) {
return true;
}
}
}
return false;
} catch (Throwable t) {
return false;
}
}
}

View file

@ -1,7 +1,6 @@
package org.keycloak.services.clientregistration;
import org.keycloak.events.EventBuilder;
import org.keycloak.models.RealmModel;
import org.keycloak.provider.Provider;
/**
@ -9,7 +8,7 @@ import org.keycloak.provider.Provider;
*/
public interface ClientRegistrationProvider extends Provider {
void setAuth(ClientRegAuth auth);
void setAuth(ClientRegistrationAuth auth);
void setEvent(EventBuilder event);

View file

@ -35,7 +35,7 @@ public class ClientRegistrationService {
}
provider.setEvent(event);
provider.setAuth(new ClientRegAuth(session, event));
provider.setAuth(new ClientRegistrationAuth(session, event));
return provider;
}

View file

@ -0,0 +1,99 @@
package org.keycloak.services.clientregistration;
import org.keycloak.common.util.Time;
import org.keycloak.jose.jws.JWSBuilder;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.crypto.RSAProvider;
import org.keycloak.models.ClientInitialAccessModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.representations.JsonWebToken;
import org.keycloak.services.ForbiddenException;
import org.keycloak.services.Urls;
import org.keycloak.util.TokenUtil;
import javax.ws.rs.core.UriInfo;
import java.io.IOException;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class ClientRegistrationTokenUtils {
public static final String TYPE_INITIAL_ACCESS_TOKEN = "InitialAccessToken";
public static final String TYPE_REGISTRATION_ACCESS_TOKEN = "RegistrationAccessToken";
public static String updateRegistrationAccessToken(KeycloakSession session, ClientModel client) {
return updateRegistrationAccessToken(session.getContext().getRealm(), session.getContext().getUri(), client);
}
public static String updateRegistrationAccessToken(RealmModel realm, UriInfo uri, ClientModel client) {
String id = KeycloakModelUtils.generateId();
client.setRegistrationToken(id);
String token = createToken(realm, uri, id, TYPE_REGISTRATION_ACCESS_TOKEN, 0);
return token;
}
public static String createInitialAccessToken(RealmModel realm, UriInfo uri, ClientInitialAccessModel model) {
return createToken(realm, uri, model.getId(), TYPE_INITIAL_ACCESS_TOKEN, model.getTimestamp() + model.getExpiration());
}
public static JsonWebToken parseToken(RealmModel realm, UriInfo uri, String token) {
JWSInput input;
try {
input = new JWSInput(token);
} catch (Exception e) {
throw new ForbiddenException(e);
}
if (!RSAProvider.verify(input, realm.getPublicKey())) {
throw new ForbiddenException("Invalid signature");
}
JsonWebToken jwt;
try {
jwt = input.readJsonContent(JsonWebToken.class);
} catch (IOException e) {
throw new ForbiddenException(e);
}
if (!getIssuer(realm, uri).equals(jwt.getIssuer())) {
throw new ForbiddenException("Issuer doesn't match");
}
if (!jwt.isActive()) {
throw new ForbiddenException("Expired token");
}
if (!(TokenUtil.TOKEN_TYPE_BEARER.equals(jwt.getType()) ||
TYPE_INITIAL_ACCESS_TOKEN.equals(jwt.getType()) ||
TYPE_REGISTRATION_ACCESS_TOKEN.equals(jwt.getType()))) {
throw new ForbiddenException("Invalid token type");
}
return jwt;
}
private static String createToken(RealmModel realm, UriInfo uri, String id, String type, int expiration) {
JsonWebToken jwt = new JsonWebToken();
String issuer = getIssuer(realm, uri);
jwt.type(type);
jwt.id(id);
jwt.issuedAt(Time.currentTime());
jwt.expiration(expiration);
jwt.issuer(issuer);
jwt.audience(issuer);
String token = new JWSBuilder().jsonContent(jwt).rsa256(realm.getPrivateKey());
return token;
}
private static String getIssuer(RealmModel realm, UriInfo uri) {
return Urls.realmIssuer(uri.getBaseUri(), realm.getName());
}
}

View file

@ -2,10 +2,10 @@ package org.keycloak.services.clientregistration;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.models.ClientInitialAccessModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.models.utils.RepresentationToModel;
import org.keycloak.representations.idm.ClientRepresentation;
@ -23,7 +23,7 @@ public class DefaultClientRegistrationProvider implements ClientRegistrationProv
private KeycloakSession session;
private EventBuilder event;
private ClientRegAuth auth;
private ClientRegistrationAuth auth;
public DefaultClientRegistrationProvider(KeycloakSession session) {
this.session = session;
@ -39,11 +39,19 @@ public class DefaultClientRegistrationProvider implements ClientRegistrationProv
try {
ClientModel clientModel = RepresentationToModel.createClient(session, session.getContext().getRealm(), client, true);
KeycloakModelUtils.generateRegistrationAccessToken(clientModel);
client = ModelToRepresentation.toRepresentation(clientModel);
String registrationAccessToken = ClientRegistrationTokenUtils.updateRegistrationAccessToken(session, clientModel);
client.setRegistrationAccessToken(registrationAccessToken);
URI uri = session.getContext().getUri().getAbsolutePathBuilder().path(clientModel.getId()).build();
if (auth.isInitialAccessToken()) {
ClientInitialAccessModel initialAccessModel = auth.getInitialAccessModel();
initialAccessModel.decreaseRemainingCount();
}
event.client(client.getClientId()).success();
return Response.created(uri).entity(client).build();
} catch (ModelDuplicateException e) {
@ -60,12 +68,15 @@ public class DefaultClientRegistrationProvider implements ClientRegistrationProv
ClientModel client = session.getContext().getRealm().getClientByClientId(clientId);
auth.requireView(client);
ClientRepresentation rep = ModelToRepresentation.toRepresentation(client);
if (auth.isRegistrationAccessToken()) {
KeycloakModelUtils.generateRegistrationAccessToken(client);
String registrationAccessToken = ClientRegistrationTokenUtils.updateRegistrationAccessToken(session, client);
rep.setRegistrationAccessToken(registrationAccessToken);
}
event.client(client.getClientId()).success();
return Response.ok(ModelToRepresentation.toRepresentation(client)).build();
return Response.ok(rep).build();
}
@PUT
@ -78,13 +89,13 @@ public class DefaultClientRegistrationProvider implements ClientRegistrationProv
auth.requireUpdate(client);
RepresentationToModel.updateClient(rep, client);
rep = ModelToRepresentation.toRepresentation(client);
if (auth.isRegistrationAccessToken()) {
KeycloakModelUtils.generateRegistrationAccessToken(client);
String registrationAccessToken = ClientRegistrationTokenUtils.updateRegistrationAccessToken(session, client);
rep.setRegistrationAccessToken(registrationAccessToken);
}
rep = ModelToRepresentation.toRepresentation(client);
event.client(client.getClientId()).success();
return Response.ok(rep).build();
}
@ -106,7 +117,7 @@ public class DefaultClientRegistrationProvider implements ClientRegistrationProv
}
@Override
public void setAuth(ClientRegAuth auth) {
public void setAuth(ClientRegistrationAuth auth) {
this.auth = auth;
}

View file

@ -1,28 +1,11 @@
package org.keycloak.services.clientregistration.oidc;
import org.jboss.logging.Logger;
import org.keycloak.common.util.Time;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.models.utils.RepresentationToModel;
import org.keycloak.protocol.oidc.OIDCClientDescriptionConverter;
import org.keycloak.protocol.oidc.representations.OIDCClientRepresentation;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.services.ErrorResponse;
import org.keycloak.services.clientregistration.ClientRegAuth;
import org.keycloak.services.clientregistration.ClientRegistrationAuth;
import org.keycloak.services.clientregistration.ClientRegistrationProvider;
import javax.ws.rs.Consumes;
import javax.ws.rs.POST;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.net.URI;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
@ -32,7 +15,7 @@ public class OIDCClientRegistrationProvider implements ClientRegistrationProvide
private KeycloakSession session;
private EventBuilder event;
private ClientRegAuth auth;
private ClientRegistrationAuth auth;
public OIDCClientRegistrationProvider(KeycloakSession session) {
this.session = session;
@ -55,7 +38,7 @@ public class OIDCClientRegistrationProvider implements ClientRegistrationProvide
//
// String registrationAccessToken = TokenGenerator.createRegistrationAccessToken();
//
// clientModel.setRegistrationSecret(registrationAccessToken);
// clientModel.setRegistrationToken(registrationAccessToken);
//
// URI uri = session.getContext().getUri().getAbsolutePathBuilder().path(clientModel.getId()).build();
//
@ -87,7 +70,7 @@ public class OIDCClientRegistrationProvider implements ClientRegistrationProvide
}
@Override
public void setAuth(ClientRegAuth auth) {
public void setAuth(ClientRegistrationAuth auth) {
this.auth = auth;
}

View file

@ -0,0 +1,102 @@
package org.keycloak.services.resources.admin;
import org.keycloak.events.admin.OperationType;
import org.keycloak.models.ClientInitialAccessModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.representations.idm.ClientInitialAccessCreatePresentation;
import org.keycloak.representations.idm.ClientInitialAccessPresentation;
import org.keycloak.services.clientregistration.ClientRegistrationTokenUtils;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.*;
import javax.ws.rs.core.*;
import java.util.LinkedList;
import java.util.List;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class ClientInitialAccessResource {
private final RealmAuth auth;
private final RealmModel realm;
private final AdminEventBuilder adminEvent;
@Context
protected KeycloakSession session;
@Context
protected UriInfo uriInfo;
public ClientInitialAccessResource(RealmModel realm, RealmAuth auth, AdminEventBuilder adminEvent) {
this.auth = auth;
this.realm = realm;
this.adminEvent = adminEvent;
auth.init(RealmAuth.Resource.CLIENT);
}
/**
* Create a new initial access token.
*
* @param config
* @return
*/
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public ClientInitialAccessPresentation create(ClientInitialAccessCreatePresentation config, @Context final HttpServletResponse response) {
auth.requireManage();
int expiration = config.getExpiration() != null ? config.getExpiration() : 0;
int count = config.getCount() != null ? config.getCount() : 1;
ClientInitialAccessModel clientInitialAccessModel = session.sessions().createClientInitialAccessModel(realm, expiration, count);
adminEvent.operation(OperationType.CREATE).resourcePath(uriInfo, clientInitialAccessModel.getId()).representation(config).success();
if (session.getTransaction().isActive()) {
session.getTransaction().commit();
}
ClientInitialAccessPresentation rep = wrap(clientInitialAccessModel);
String token = ClientRegistrationTokenUtils.createInitialAccessToken(realm, uriInfo, clientInitialAccessModel);
rep.setToken(token);
response.setStatus(Response.Status.CREATED.getStatusCode());
response.setHeader(HttpHeaders.LOCATION, uriInfo.getAbsolutePathBuilder().path(clientInitialAccessModel.getId()).build().toString());
return rep;
}
@GET
@Produces(MediaType.APPLICATION_JSON)
public List<ClientInitialAccessPresentation> list() {
List<ClientInitialAccessModel> models = session.sessions().listClientInitialAccess(realm);
List<ClientInitialAccessPresentation> reps = new LinkedList<>();
for (ClientInitialAccessModel m : models) {
ClientInitialAccessPresentation r = wrap(m);
reps.add(r);
}
return reps;
}
@DELETE
@Path("{id}")
public void delete(final @PathParam("id") String id) {
session.sessions().removeClientInitialAccessModel(realm, id);
}
private ClientInitialAccessPresentation wrap(ClientInitialAccessModel model) {
ClientInitialAccessPresentation rep = new ClientInitialAccessPresentation();
rep.setId(model.getId());
rep.setTimestamp(model.getTimestamp());
rep.setExpiration(model.getExpiration());
rep.setCount(model.getCount());
rep.setRemainingCount(model.getRemainingCount());
return rep;
}
}

View file

@ -22,6 +22,7 @@ import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.representations.idm.UserSessionRepresentation;
import org.keycloak.services.clientregistration.ClientRegistrationTokenUtils;
import org.keycloak.services.managers.ClientManager;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.managers.ResourceAdminManager;
@ -228,8 +229,11 @@ public class ClientResource {
public ClientRepresentation regenerateRegistrationAccessToken() {
auth.requireManage();
KeycloakModelUtils.generateRegistrationAccessToken(client);
String token = ClientRegistrationTokenUtils.updateRegistrationAccessToken(realm, uriInfo, client);
ClientRepresentation rep = ModelToRepresentation.toRepresentation(client);
rep.setRegistrationAccessToken(token);
adminEvent.operation(OperationType.ACTION).resourcePath(uriInfo).representation(rep).success();
return rep;
}

View file

@ -140,6 +140,18 @@ public class RealmAdminResource {
return clientsResource;
}
/**
* Base path for managing client initial access tokens
*
* @return
*/
@Path("clients-initial-access")
public ClientInitialAccessResource getClientInitialAccess() {
ClientInitialAccessResource resource = new ClientInitialAccessResource(realm, auth, adminEvent);
ResteasyProviderFactory.getInstance().injectProperties(resource);
return resource;
}
/**
* base path for managing realm-level roles of this realm
*

View file

@ -2,6 +2,7 @@ package org.keycloak.testsuite.client;
import org.junit.After;
import org.junit.Before;
import org.keycloak.client.registration.Auth;
import org.keycloak.client.registration.ClientRegistration;
import org.keycloak.client.registration.ClientRegistrationException;
import org.keycloak.models.AdminRoles;
@ -13,7 +14,6 @@ import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.AbstractKeycloakTest;
import javax.ws.rs.NotFoundException;
import javax.ws.rs.core.Response;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
@ -76,13 +76,11 @@ public abstract class AbstractClientRegistrationTest extends AbstractKeycloakTes
testRealms.add(rep);
}
public ClientRepresentation createClient(ClientRepresentation client) {
Response response = adminClient.realm(REALM_NAME).clients().create(client);
String id = response.getLocation().toString();
id = id.substring(id.lastIndexOf('/') + 1);
client.setId(id);
response.close();
return client;
public ClientRepresentation createClient(ClientRepresentation client) throws ClientRegistrationException {
authManageClients();
ClientRepresentation response = reg.create(client);
reg.auth(null);
return response;
}
public ClientRepresentation getClient(String clientId) {
@ -93,4 +91,20 @@ public abstract class AbstractClientRegistrationTest extends AbstractKeycloakTes
}
}
void authCreateClients() {
reg.auth(Auth.token(getToken("create-clients", "password")));
}
void authManageClients() {
reg.auth(Auth.token(getToken("manage-clients", "password")));
}
void authNoAccess() {
reg.auth(Auth.token(getToken("no-access", "password")));
}
private String getToken(String username, String password) {
return oauthClient.getToken(REALM_NAME, "security-admin-console", null, username, password).getToken();
}
}

View file

@ -196,20 +196,4 @@ public class ClientRegistrationTest extends AbstractClientRegistrationTest {
}
}
private void authCreateClients() {
reg.auth(Auth.token(getToken("create-clients", "password")));
}
private void authManageClients() {
reg.auth(Auth.token(getToken("manage-clients", "password")));
}
private void authNoAccess() {
reg.auth(Auth.token(getToken("no-access", "password")));
}
private String getToken(String username, String password) {
return oauthClient.getToken(REALM_NAME, "security-admin-console", null, username, password).getToken();
}
}

View file

@ -0,0 +1,101 @@
package org.keycloak.testsuite.client;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.keycloak.admin.client.resource.ClientInitialAccessResource;
import org.keycloak.client.registration.Auth;
import org.keycloak.client.registration.ClientRegistrationException;
import org.keycloak.client.registration.HttpErrorException;
import org.keycloak.representations.idm.ClientInitialAccessCreatePresentation;
import org.keycloak.representations.idm.ClientInitialAccessPresentation;
import org.keycloak.representations.idm.ClientRepresentation;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class InitialAccessTokenTest extends AbstractClientRegistrationTest {
private ClientInitialAccessResource resource;
@Before
public void before() throws Exception {
super.before();
resource = adminClient.realm(REALM_NAME).clientInitialAccess();
}
@Test
public void create() throws ClientRegistrationException {
ClientInitialAccessPresentation response = resource.create(new ClientInitialAccessCreatePresentation());
reg.auth(Auth.token(response));
ClientRepresentation rep = new ClientRepresentation();
ClientRepresentation created = reg.create(rep);
Assert.assertNotNull(created);
try {
reg.create(rep);
} catch (ClientRegistrationException e) {
Assert.assertEquals(403, ((HttpErrorException) e.getCause()).getStatusLine().getStatusCode());
}
}
@Test
public void createMultiple() throws ClientRegistrationException {
ClientInitialAccessPresentation response = resource.create(new ClientInitialAccessCreatePresentation(0, 2));
reg.auth(Auth.token(response));
ClientRepresentation rep = new ClientRepresentation();
ClientRepresentation created = reg.create(rep);
Assert.assertNotNull(created);
created = reg.create(rep);
Assert.assertNotNull(created);
try {
reg.create(rep);
} catch (ClientRegistrationException e) {
Assert.assertEquals(403, ((HttpErrorException) e.getCause()).getStatusLine().getStatusCode());
}
}
@Test
public void createExpired() throws ClientRegistrationException, InterruptedException {
ClientInitialAccessPresentation response = resource.create(new ClientInitialAccessCreatePresentation(1, 1));
reg.auth(Auth.token(response));
ClientRepresentation rep = new ClientRepresentation();
Thread.sleep(2);
try {
reg.create(rep);
} catch (ClientRegistrationException e) {
Assert.assertEquals(403, ((HttpErrorException) e.getCause()).getStatusLine().getStatusCode());
}
}
@Test
public void createDeleted() throws ClientRegistrationException, InterruptedException {
ClientInitialAccessPresentation response = resource.create(new ClientInitialAccessCreatePresentation());
reg.auth(Auth.token(response));
resource.delete(response.getId());
ClientRepresentation rep = new ClientRepresentation();
try {
reg.create(rep);
} catch (ClientRegistrationException e) {
Assert.assertEquals(403, ((HttpErrorException) e.getCause()).getStatusLine().getStatusCode());
}
}
}

View file

@ -7,8 +7,6 @@ import org.keycloak.client.registration.ClientRegistrationException;
import org.keycloak.client.registration.HttpErrorException;
import org.keycloak.representations.idm.ClientRepresentation;
import javax.ws.rs.core.Response;
import static org.junit.Assert.*;
/**
@ -22,13 +20,13 @@ public class RegistrationAccessTokenTest extends AbstractClientRegistrationTest
public void before() throws Exception {
super.before();
client = new ClientRepresentation();
client.setEnabled(true);
client.setClientId("RegistrationAccessTokenTest");
client.setSecret("RegistrationAccessTokenTestClientSecret");
client.setRegistrationAccessToken("RegistrationAccessTokenTestRegistrationAccessToken");
client.setRootUrl("http://root");
client = createClient(client);
ClientRepresentation c = new ClientRepresentation();
c.setEnabled(true);
c.setClientId("RegistrationAccessTokenTest");
c.setSecret("RegistrationAccessTokenTestClientSecret");
c.setRootUrl("http://root");
client = createClient(c);
reg.auth(Auth.token(client.getRegistrationAccessToken()));
}
@ -36,7 +34,7 @@ public class RegistrationAccessTokenTest extends AbstractClientRegistrationTest
private ClientRepresentation assertRead(String id, String registrationAccess, boolean expectSuccess) throws ClientRegistrationException {
if (expectSuccess) {
reg.auth(Auth.token(registrationAccess));
ClientRepresentation rep = reg.get(client.getClientId());
ClientRepresentation rep = reg.get(id);
assertNotNull(rep);
return rep;
} else {
@ -76,6 +74,7 @@ public class RegistrationAccessTokenTest extends AbstractClientRegistrationTest
@Test
public void updateClientWithRegistrationToken() throws ClientRegistrationException {
client.setRootUrl("http://newroot");
ClientRepresentation rep = reg.update(client);
assertEquals("http://newroot", getClient(client.getId()).getRootUrl());

View file

@ -0,0 +1,58 @@
package org.keycloak.testsuite.admin;
import org.junit.Assert;
import org.junit.Test;
import org.keycloak.admin.client.resource.ClientInitialAccessResource;
import org.keycloak.representations.idm.ClientInitialAccessCreatePresentation;
import org.keycloak.representations.idm.ClientInitialAccessPresentation;
import javax.ws.rs.core.Response;
import java.net.URI;
import java.util.List;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class ClientInitialAccessTest extends AbstractClientTest {
@Test
public void create() {
ClientInitialAccessResource resource = keycloak.realm(REALM_NAME).clientInitialAccess();
ClientInitialAccessPresentation access = resource.create(new ClientInitialAccessCreatePresentation(1000, 2));
Assert.assertEquals(new Integer(2), access.getCount());
Assert.assertEquals(new Integer(2), access.getRemainingCount());
Assert.assertEquals(new Integer(1000), access.getExpiration());
Assert.assertNotNull(access.getTimestamp());
Assert.assertNotNull(access.getToken());
ClientInitialAccessPresentation access2 = resource.create(new ClientInitialAccessCreatePresentation());
List<ClientInitialAccessPresentation> list = resource.list();
Assert.assertEquals(2, list.size());
for (ClientInitialAccessPresentation r : list) {
if (r.getId().equals(access.getId())) {
Assert.assertEquals(new Integer(2), r.getCount());
Assert.assertEquals(new Integer(2), r.getRemainingCount());
Assert.assertEquals(new Integer(1000), r.getExpiration());
Assert.assertNotNull(r.getTimestamp());
Assert.assertNull(r.getToken());
} else if(r.getId().equals(access2.getId())) {
Assert.assertEquals(new Integer(1), r.getCount());
Assert.assertEquals(new Integer(1), r.getRemainingCount());
Assert.assertEquals(new Integer(0), r.getExpiration());
Assert.assertNotNull(r.getTimestamp());
Assert.assertNull(r.getToken());
} else {
Assert.fail("Unexpected id");
}
}
resource.delete(access.getId());
resource.delete(access2.getId());
Assert.assertTrue(resource.list().isEmpty());
}
}