KEYCLOAK-2085 Initial access tokens for client registration
This commit is contained in:
parent
67fca8f1f3
commit
764c20d748
46 changed files with 1292 additions and 315 deletions
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
||||
}
|
|
@ -45,5 +45,8 @@ public interface RealmResource {
|
|||
@Path("client-session-stats")
|
||||
@GET
|
||||
List<Map<String, String>> getClientSessionStats();
|
||||
|
||||
|
||||
@Path("clients-initial-access")
|
||||
ClientInitialAccessResource clientInitialAccess();
|
||||
|
||||
}
|
||||
|
|
22
model/api/src/main/java/org/keycloak/models/ClientInitialAccessModel.java
Executable file
22
model/api/src/main/java/org/keycloak/models/ClientInitialAccessModel.java
Executable 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();
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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() {
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -35,7 +35,7 @@ public class ClientRegistrationService {
|
|||
}
|
||||
|
||||
provider.setEvent(event);
|
||||
provider.setAuth(new ClientRegAuth(session, event));
|
||||
provider.setAuth(new ClientRegistrationAuth(session, event));
|
||||
return provider;
|
||||
}
|
||||
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
*
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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());
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in a new issue