From 02a59812e62eb03ceb4383af72818c521230d7a4 Mon Sep 17 00:00:00 2001 From: Stian Thorgersen Date: Fri, 25 Jul 2014 16:18:27 +0100 Subject: [PATCH] Added ClientSessionModel to UserSessionProvider --- .../main/resources/META-INF/persistence.xml | 3 +- ...DefaultMongoConnectionFactoryProvider.java | 1 + .../keycloak/representations/AccessCode.java | 101 ----------- .../freemarker/model/SessionsBean.java | 13 +- .../keycloak/models/ClientSessionModel.java | 39 ++++ .../org/keycloak/models/UserSessionModel.java | 6 +- .../keycloak/models/UserSessionProvider.java | 4 + .../models/utils/ModelToRepresentation.java | 4 +- .../sessions/jpa/ClientSessionAdapter.java | 87 +++++++++ .../sessions/jpa/JpaUserSessionProvider.java | 67 ++++++- .../sessions/jpa/UserSessionAdapter.java | 34 +--- .../jpa/entities/ClientSessionEntity.java | 127 +++++++++++++ .../jpa/entities/ClientSessionRoleEntity.java | 97 ++++++++++ .../ClientUserSessionAssociationEntity.java | 97 ---------- .../jpa/entities/UserSessionEntity.java | 12 +- .../sessions/mem/ClientSessionAdapter.java | 79 ++++++++ .../sessions/mem/MemUserSessionProvider.java | 125 +++++++++---- .../mem/MemUserSessionProviderFactory.java | 10 +- .../sessions/mem/UserSessionAdapter.java | 30 ++- .../mem/entities/ClientSessionEntity.java | 87 +++++++++ .../mem/entities/UserSessionEntity.java | 20 +- .../sessions/mem/entities/UserSessionKey.java | 36 ---- .../sessions/mongo/ClientSessionAdapter.java | 88 +++++++++ .../mongo/MongoUserSessionProvider.java | 80 ++++++-- .../sessions/mongo/UserSessionAdapter.java | 46 ++--- .../entities/MongoClientSessionEntity.java | 80 ++++++++ .../entities/MongoUserSessionEntity.java | 11 +- .../services/managers/AccessCode.java | 171 ++++++++++++++++++ .../services/managers/AccessCodeEntry.java | 125 ------------- .../services/managers/TokenManager.java | 41 +---- .../services/resources/AccountService.java | 17 +- .../resources/RequiredActionsService.java | 49 +++-- .../services/resources/TokenService.java | 54 +++--- .../resources/admin/UsersResource.java | 4 +- .../services/resources/flows/OAuthFlows.java | 12 +- .../org/keycloak/testsuite/AssertEvents.java | 13 +- .../org/keycloak/testsuite/OAuthClient.java | 9 +- .../model/UserSessionProviderTest.java | 163 +++++++++++++++-- .../testsuite/oauth/AccessTokenTest.java | 127 ++++++++++--- .../oauth/AuthorizationCodeTest.java | 34 ++-- .../keycloak/testsuite/rule/KeycloakRule.java | 20 ++ 41 files changed, 1529 insertions(+), 694 deletions(-) delete mode 100755 core/src/main/java/org/keycloak/representations/AccessCode.java create mode 100644 model/api/src/main/java/org/keycloak/models/ClientSessionModel.java create mode 100644 model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/ClientSessionAdapter.java create mode 100755 model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/entities/ClientSessionEntity.java create mode 100755 model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/entities/ClientSessionRoleEntity.java delete mode 100755 model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/entities/ClientUserSessionAssociationEntity.java create mode 100644 model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/ClientSessionAdapter.java create mode 100644 model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/entities/ClientSessionEntity.java delete mode 100644 model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/entities/UserSessionKey.java create mode 100644 model/sessions-mongo/src/main/java/org/keycloak/models/sessions/mongo/ClientSessionAdapter.java create mode 100644 model/sessions-mongo/src/main/java/org/keycloak/models/sessions/mongo/entities/MongoClientSessionEntity.java create mode 100755 services/src/main/java/org/keycloak/services/managers/AccessCode.java delete mode 100755 services/src/main/java/org/keycloak/services/managers/AccessCodeEntry.java diff --git a/connections/jpa/src/main/resources/META-INF/persistence.xml b/connections/jpa/src/main/resources/META-INF/persistence.xml index 7511ed2500..2463c4ca36 100755 --- a/connections/jpa/src/main/resources/META-INF/persistence.xml +++ b/connections/jpa/src/main/resources/META-INF/persistence.xml @@ -20,7 +20,8 @@ org.keycloak.models.jpa.entities.ScopeMappingEntity - org.keycloak.models.sessions.jpa.entities.ClientUserSessionAssociationEntity + org.keycloak.models.sessions.jpa.entities.ClientSessionEntity + org.keycloak.models.sessions.jpa.entities.ClientSessionRoleEntity org.keycloak.models.sessions.jpa.entities.UserSessionEntity org.keycloak.models.sessions.jpa.entities.UsernameLoginFailureEntity diff --git a/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java b/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java index cea49fffd9..70135fbd0c 100644 --- a/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java +++ b/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java @@ -32,6 +32,7 @@ public class DefaultMongoConnectionFactoryProvider implements MongoConnectionPro "org.keycloak.models.mongo.keycloak.entities.MongoOAuthClientEntity", "org.keycloak.models.sessions.mongo.entities.MongoUsernameLoginFailureEntity", "org.keycloak.models.sessions.mongo.entities.MongoUserSessionEntity", + "org.keycloak.models.sessions.mongo.entities.MongoClientSessionEntity", "org.keycloak.models.entities.FederationProviderEntity" }; diff --git a/core/src/main/java/org/keycloak/representations/AccessCode.java b/core/src/main/java/org/keycloak/representations/AccessCode.java deleted file mode 100755 index ef6120262a..0000000000 --- a/core/src/main/java/org/keycloak/representations/AccessCode.java +++ /dev/null @@ -1,101 +0,0 @@ -package org.keycloak.representations; - -import java.util.Set; - -/** - * - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class AccessCode { - protected String id; - protected String clientId; - protected String userId; - protected String state; - protected String sessionState; - protected String redirectUri; - protected int timestamp; - protected Action action; - protected Set requestedRoles; - - public String getId() { - return id; - } - - public void setId(String id) { - this.id = id; - } - - public String getClientId() { - return clientId; - } - - public void setClientId(String clientId) { - this.clientId = clientId; - } - - public String getUserId() { - return userId; - } - - public void setUserId(String userId) { - this.userId = userId; - } - - public String getState() { - return state; - } - - public void setState(String state) { - this.state = state; - } - - public String getSessionState() { - return sessionState; - } - - public void setSessionState(String sessionState) { - this.sessionState = sessionState; - } - - public String getRedirectUri() { - return redirectUri; - } - - public void setRedirectUri(String redirectUri) { - this.redirectUri = redirectUri; - } - - public int getTimestamp() { - return timestamp; - } - - public void setTimestamp(int timestamp) { - this.timestamp = timestamp; - } - - public Action getAction() { - return action; - } - - public void setAction(Action action) { - this.action = action; - } - - public Set getRequestedRoles() { - return requestedRoles; - } - - public void setRequestedRoles(Set requestedRoles) { - this.requestedRoles = requestedRoles; - } - - public static enum Action { - OAUTH_GRANT, - VERIFY_EMAIL, - UPDATE_PROFILE, - CONFIGURE_TOTP, - UPDATE_PASSWORD - } - -} diff --git a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/SessionsBean.java b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/SessionsBean.java index 403512f5eb..de2f6e4d72 100755 --- a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/SessionsBean.java +++ b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/SessionsBean.java @@ -2,6 +2,7 @@ package org.keycloak.account.freemarker.model; import org.keycloak.models.ApplicationModel; import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientSessionModel; import org.keycloak.models.OAuthClientModel; import org.keycloak.models.RealmModel; import org.keycloak.models.UserSessionModel; @@ -9,8 +10,10 @@ import org.keycloak.util.Time; import java.util.ArrayList; import java.util.Date; +import java.util.HashSet; import java.util.LinkedList; import java.util.List; +import java.util.Set; /** * @author Stian Thorgersen @@ -58,16 +61,18 @@ public class SessionsBean { return Time.toDate(max); } - public List getApplications() { - List apps = new ArrayList(); - for (ClientModel client : session.getClientAssociations()) { + public Set getApplications() { + Set apps = new HashSet(); + for (ClientSessionModel clientSession : session.getClientSessions()) { + ClientModel client = clientSession.getClient(); if (client instanceof ApplicationModel) apps.add(client.getClientId()); } return apps; } public List getClients() { List apps = new ArrayList(); - for (ClientModel client : session.getClientAssociations()) { + for (ClientSessionModel clientSession : session.getClientSessions()) { + ClientModel client = clientSession.getClient(); if (client instanceof OAuthClientModel) apps.add(client.getClientId()); } return apps; diff --git a/model/api/src/main/java/org/keycloak/models/ClientSessionModel.java b/model/api/src/main/java/org/keycloak/models/ClientSessionModel.java new file mode 100644 index 0000000000..66f21318f2 --- /dev/null +++ b/model/api/src/main/java/org/keycloak/models/ClientSessionModel.java @@ -0,0 +1,39 @@ +package org.keycloak.models; + +import java.util.Set; + +/** + * @author Stian Thorgersen + */ +public interface ClientSessionModel { + + public String getId(); + + public ClientModel getClient(); + + public String getState(); + + public UserSessionModel getUserSession(); + + public String getRedirectUri(); + + public int getTimestamp(); + + public void setTimestamp(int timestamp); + + public Action getAction(); + + public void setAction(Action action); + + public Set getRoles(); + + public static enum Action { + OAUTH_GRANT, + CODE_TO_TOKEN, + VERIFY_EMAIL, + UPDATE_PROFILE, + CONFIGURE_TOTP, + UPDATE_PASSWORD + } + +} diff --git a/model/api/src/main/java/org/keycloak/models/UserSessionModel.java b/model/api/src/main/java/org/keycloak/models/UserSessionModel.java index e7ce35c394..d299a99b87 100755 --- a/model/api/src/main/java/org/keycloak/models/UserSessionModel.java +++ b/model/api/src/main/java/org/keycloak/models/UserSessionModel.java @@ -39,10 +39,6 @@ public interface UserSessionModel { void setLastSessionRefresh(int seconds); - void associateClient(ClientModel client); - - List getClientAssociations(); - - void removeAssociatedClient(ClientModel client); + List getClientSessions(); } diff --git a/model/api/src/main/java/org/keycloak/models/UserSessionProvider.java b/model/api/src/main/java/org/keycloak/models/UserSessionProvider.java index d91061d65b..d2c66d0e49 100755 --- a/model/api/src/main/java/org/keycloak/models/UserSessionProvider.java +++ b/model/api/src/main/java/org/keycloak/models/UserSessionProvider.java @@ -3,6 +3,7 @@ package org.keycloak.models; import org.keycloak.provider.Provider; import java.util.List; +import java.util.Set; /** * @author Bill Burke @@ -10,6 +11,9 @@ import java.util.List; */ public interface UserSessionProvider extends Provider { + ClientSessionModel createClientSession(RealmModel realm, ClientModel client, UserSessionModel userSession, String redirectUri, String state, Set roles); + ClientSessionModel getClientSession(RealmModel realm, String id); + UserSessionModel createUserSession(RealmModel realm, UserModel user, String loginUsername, String ipAddress, String authMethod, boolean rememberMe); UserSessionModel getUserSession(RealmModel realm, String id); List getUserSessions(RealmModel realm, UserModel user); diff --git a/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java b/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java index 13e3a8965b..b28f71ce70 100755 --- a/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java +++ b/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java @@ -4,6 +4,7 @@ import org.keycloak.models.ApplicationModel; import org.keycloak.models.AuthenticationProviderModel; import org.keycloak.models.ClaimMask; import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientSessionModel; import org.keycloak.models.Constants; import org.keycloak.models.UserFederationProviderModel; import org.keycloak.models.OAuthClientModel; @@ -209,7 +210,8 @@ public class ModelToRepresentation { rep.setLastAccess(((long)session.getLastSessionRefresh())* 1000L); rep.setUser(session.getUser().getUsername()); rep.setIpAddress(session.getIpAddress()); - for (ClientModel client : session.getClientAssociations()) { + for (ClientSessionModel clientSession : session.getClientSessions()) { + ClientModel client = clientSession.getClient(); if (client instanceof ApplicationModel) { rep.getApplications().add(client.getClientId()); } else if (client instanceof OAuthClientModel) { diff --git a/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/ClientSessionAdapter.java b/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/ClientSessionAdapter.java new file mode 100644 index 0000000000..ebcef17cd5 --- /dev/null +++ b/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/ClientSessionAdapter.java @@ -0,0 +1,87 @@ +package org.keycloak.models.sessions.jpa; + +import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientSessionModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserSessionModel; +import org.keycloak.models.sessions.jpa.entities.ClientSessionEntity; +import org.keycloak.models.sessions.jpa.entities.ClientSessionRoleEntity; + +import javax.persistence.EntityManager; +import java.util.HashSet; +import java.util.Set; + +/** + * @author Stian Thorgersen + */ +public class ClientSessionAdapter implements ClientSessionModel { + + private KeycloakSession session; + private ClientSessionEntity entity; + private EntityManager em; + private RealmModel realm; + + public ClientSessionAdapter(KeycloakSession session, EntityManager em, RealmModel realm, ClientSessionEntity entity) { + this.session = session; + this.em = em; + this.realm = realm; + this.entity = entity; + } + + @Override + public String getId() { + return entity.getId(); + } + + @Override + public ClientModel getClient() { + return realm.findClientById(entity.getClientId()); + } + + @Override + public String getState() { + return entity.getState(); + } + + @Override + public UserSessionModel getUserSession() { + return new UserSessionAdapter(session, em, realm, entity.getSession()); + } + + @Override + public String getRedirectUri() { + return entity.getRedirectUri(); + } + + @Override + public int getTimestamp() { + return entity.getTimestamp(); + } + + @Override + public void setTimestamp(int timestamp) { + entity.setTimestamp(timestamp); + } + + @Override + public Action getAction() { + return entity.getAction(); + } + + @Override + public void setAction(Action action) { + entity.setAction(action); + } + + @Override + public Set getRoles() { + Set roles = new HashSet(); + if (entity.getRoles() != null) { + for (ClientSessionRoleEntity e : entity.getRoles()) { + roles.add(e.getRoleId()); + } + } + return roles; + } +} diff --git a/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/JpaUserSessionProvider.java b/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/JpaUserSessionProvider.java index 640af9938b..92e416b3b5 100644 --- a/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/JpaUserSessionProvider.java +++ b/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/JpaUserSessionProvider.java @@ -1,12 +1,15 @@ package org.keycloak.models.sessions.jpa; import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientSessionModel; import org.keycloak.models.KeycloakSession; 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.sessions.jpa.entities.ClientSessionEntity; +import org.keycloak.models.sessions.jpa.entities.ClientSessionRoleEntity; import org.keycloak.models.sessions.jpa.entities.UserSessionEntity; import org.keycloak.models.sessions.jpa.entities.UsernameLoginFailureEntity; import org.keycloak.models.utils.KeycloakModelUtils; @@ -17,6 +20,7 @@ import javax.persistence.TypedQuery; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; +import java.util.Set; /** * @author Stian Thorgersen @@ -32,6 +36,46 @@ public class JpaUserSessionProvider implements UserSessionProvider { this.em = em; } + @Override + public ClientSessionModel createClientSession(RealmModel realm, ClientModel client, UserSessionModel userSession, String redirectUri, String state, Set roles) { + UserSessionEntity userSessionEntity = em.find(UserSessionEntity.class, userSession.getId()); + + ClientSessionEntity entity = new ClientSessionEntity(); + entity.setId(KeycloakModelUtils.generateId()); + entity.setTimestamp(Time.currentTime()); + entity.setClientId(client.getId()); + entity.setSession(userSessionEntity); + entity.setRedirectUri(redirectUri); + entity.setState(state); + em.persist(entity); + + if (roles != null) { + List roleEntities = new LinkedList(); + for (String r : roles) { + ClientSessionRoleEntity roleEntity = new ClientSessionRoleEntity(); + roleEntity.setClientSession(entity); + roleEntity.setRoleId(r); + em.persist(roleEntity); + + roleEntities.add(roleEntity); + } + entity.setRoles(roleEntities); + } + + userSessionEntity.getClientSessions().add(entity); + + return new ClientSessionAdapter(session, em, realm, entity); + } + + @Override + public ClientSessionModel getClientSession(RealmModel realm, String id) { + ClientSessionEntity clientSession = em.find(ClientSessionEntity.class, id); + if (clientSession != null && clientSession.getSession().getRealmId().equals(realm.getId())) { + return new ClientSessionAdapter(session, em, realm, clientSession); + } + return null; + } + @Override public UsernameLoginFailureModel getUserLoginFailure(RealmModel realm, String username) { String id = username + "-" + realm; @@ -109,7 +153,7 @@ public class JpaUserSessionProvider implements UserSessionProvider { List list = new LinkedList(); TypedQuery query = em.createNamedQuery("getUserSessionByClient", UserSessionEntity.class) .setParameter("realmId", realm.getId()) - .setParameter("clientId", client.getClientId()); + .setParameter("clientId", client.getId()); if (firstResult != -1) { query.setFirstResult(firstResult); } @@ -126,7 +170,7 @@ public class JpaUserSessionProvider implements UserSessionProvider { public int getActiveUserSessions(RealmModel realm, ClientModel client) { Object count = em.createNamedQuery("getActiveUserSessionByClient") .setParameter("realmId", realm.getId()) - .setParameter("clientId", client.getClientId()) + .setParameter("clientId", client.getId()) .getSingleResult(); return ((Number)count).intValue(); } @@ -141,7 +185,11 @@ public class JpaUserSessionProvider implements UserSessionProvider { @Override public void removeUserSessions(RealmModel realm, UserModel user) { - em.createNamedQuery("removeClientUserSessionByUser") + em.createNamedQuery("removeClientSessionRoleByUser") + .setParameter("realmId", realm.getId()) + .setParameter("userId", user.getId()) + .executeUpdate(); + em.createNamedQuery("removeClientSessionByUser") .setParameter("realmId", realm.getId()) .setParameter("userId", user.getId()) .executeUpdate(); @@ -156,7 +204,12 @@ public class JpaUserSessionProvider implements UserSessionProvider { int maxTime = Time.currentTime() - realm.getSsoSessionMaxLifespan(); int idleTime = Time.currentTime() - realm.getSsoSessionIdleTimeout(); - em.createNamedQuery("removeClientUserSessionByExpired") + em.createNamedQuery("removeClientSessionRoleByExpired") + .setParameter("realmId", realm.getId()) + .setParameter("maxTime", maxTime) + .setParameter("idleTime", idleTime) + .executeUpdate(); + em.createNamedQuery("removeClientSessionByExpired") .setParameter("realmId", realm.getId()) .setParameter("maxTime", maxTime) .setParameter("idleTime", idleTime) @@ -170,7 +223,8 @@ public class JpaUserSessionProvider implements UserSessionProvider { @Override public void removeUserSessions(RealmModel realm) { - em.createNamedQuery("removeClientUserSessionByRealm").setParameter("realmId", realm.getId()).executeUpdate(); + em.createNamedQuery("removeClientSessionRoleByRealm").setParameter("realmId", realm.getId()).executeUpdate(); + em.createNamedQuery("removeClientSessionByRealm").setParameter("realmId", realm.getId()).executeUpdate(); em.createNamedQuery("removeUserSessionByRealm").setParameter("realmId", realm.getId()).executeUpdate(); } @@ -182,7 +236,8 @@ public class JpaUserSessionProvider implements UserSessionProvider { @Override public void onClientRemoved(RealmModel realm, ClientModel client) { - em.createNamedQuery("removeClientUserSessionByClient").setParameter("realmId", realm.getId()).setParameter("clientId", client.getClientId()).executeUpdate(); + em.createNamedQuery("removeClientSessionRoleByClient").setParameter("realmId", realm.getId()).setParameter("clientId", client.getId()).executeUpdate(); + em.createNamedQuery("removeClientSessionByClient").setParameter("realmId", realm.getId()).setParameter("clientId", client.getId()).executeUpdate(); } @Override diff --git a/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/UserSessionAdapter.java b/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/UserSessionAdapter.java index 01bda8ee90..3261b212c1 100755 --- a/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/UserSessionAdapter.java +++ b/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/UserSessionAdapter.java @@ -1,15 +1,15 @@ package org.keycloak.models.sessions.jpa; -import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientSessionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; -import org.keycloak.models.sessions.jpa.entities.ClientUserSessionAssociationEntity; +import org.keycloak.models.sessions.jpa.entities.ClientSessionEntity; import org.keycloak.models.sessions.jpa.entities.UserSessionEntity; import javax.persistence.EntityManager; -import java.util.ArrayList; +import java.util.LinkedList; import java.util.List; /** @@ -114,30 +114,12 @@ public class UserSessionAdapter implements UserSessionModel { } @Override - public void associateClient(ClientModel client) { - for (ClientUserSessionAssociationEntity ass : entity.getClients()) { - if (ass.getClientId().equals(client.getClientId())) return; + public List getClientSessions() { + List clientSessions = new LinkedList(); + for (ClientSessionEntity e : entity.getClientSessions()) { + clientSessions.add(new ClientSessionAdapter(session, em, realm, e)); } - - ClientUserSessionAssociationEntity association = new ClientUserSessionAssociationEntity(); - association.setClientId(client.getClientId()); - association.setSession(entity); - em.persist(association); - entity.getClients().add(association); - } - - @Override - public void removeAssociatedClient(ClientModel client) { - em.createNamedQuery("removeClientUserSessionByClient").setParameter("clientId", client.getClientId()).executeUpdate(); - } - - @Override - public List getClientAssociations() { - List clients = new ArrayList(); - for (ClientUserSessionAssociationEntity association : entity.getClients()) { - clients.add(realm.findClient(association.getClientId())); - } - return clients; + return clientSessions; } @Override diff --git a/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/entities/ClientSessionEntity.java b/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/entities/ClientSessionEntity.java new file mode 100755 index 0000000000..65df66396d --- /dev/null +++ b/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/entities/ClientSessionEntity.java @@ -0,0 +1,127 @@ +package org.keycloak.models.sessions.jpa.entities; + +import org.keycloak.models.ClientSessionModel; + +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.ElementCollection; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.Id; +import javax.persistence.IdClass; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.OneToMany; +import javax.persistence.Table; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +@Entity +@Table(name = "CLIENT_SESSION") +@NamedQueries({ + @NamedQuery(name = "removeClientSessionByRealm", query = "delete from ClientSessionEntity a where a.session IN (select s from UserSessionEntity s where s.realmId = :realmId)"), + @NamedQuery(name = "removeClientSessionByUser", query = "delete from ClientSessionEntity a where a.session IN (select s from UserSessionEntity s where s.realmId = :realmId and s.userId = :userId)"), + @NamedQuery(name = "removeClientSessionByClient", query = "delete from ClientSessionEntity a where a.clientId = :clientId and a.session IN (select s from UserSessionEntity s where s.realmId = :realmId)"), + @NamedQuery(name = "removeClientSessionByExpired", query = "delete from ClientSessionEntity a where a.session IN (select s from UserSessionEntity s where s.realmId = :realmId and (s.started < :maxTime or s.lastSessionRefresh < :idleTime))") +}) +public class ClientSessionEntity { + + @Id + @Column(name = "ID", length = 36) + protected String id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "SESSION_ID") + protected UserSessionEntity session; + + @Column(name="CLIENT_ID",length = 36) + protected String clientId; + + @Column(name="TIMESTAMP") + protected int timestamp; + + @Column(name="REDIRECT_URI") + protected String redirectUri; + + @Column(name="STATE") + protected String state; + + @Column(name="ACTION") + protected ClientSessionModel.Action action; + + @OneToMany(cascade = CascadeType.REMOVE, orphanRemoval = true, mappedBy="clientSession") + protected Collection roles = new ArrayList(); + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public UserSessionEntity getSession() { + return session; + } + + public void setSession(UserSessionEntity session) { + this.session = session; + } + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public int getTimestamp() { + return timestamp; + } + + public void setTimestamp(int timestamp) { + this.timestamp = timestamp; + } + + public String getRedirectUri() { + return redirectUri; + } + + public void setRedirectUri(String redirectUri) { + this.redirectUri = redirectUri; + } + + public String getState() { + return state; + } + + public void setState(String state) { + this.state = state; + } + + public ClientSessionModel.Action getAction() { + return action; + } + + public void setAction(ClientSessionModel.Action action) { + this.action = action; + } + + public Collection getRoles() { + return roles; + } + + public void setRoles(Collection roles) { + this.roles = roles; + } + +} diff --git a/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/entities/ClientSessionRoleEntity.java b/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/entities/ClientSessionRoleEntity.java new file mode 100755 index 0000000000..e1be9d3c84 --- /dev/null +++ b/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/entities/ClientSessionRoleEntity.java @@ -0,0 +1,97 @@ +package org.keycloak.models.sessions.jpa.entities; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.Id; +import javax.persistence.IdClass; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.Table; +import java.io.Serializable; + +/** + * @author Stian Thorgersen + */ +@NamedQueries({ + @NamedQuery(name = "removeClientSessionRoleByUser", query="delete from ClientSessionRoleEntity r where r.clientSession IN (select c from ClientSessionEntity c where c.session IN (select s from UserSessionEntity s where s.realmId = :realmId and s.userId = :userId))"), + @NamedQuery(name = "removeClientSessionRoleByClient", query="delete from ClientSessionRoleEntity r where r.clientSession IN (select c from ClientSessionEntity c where c.clientId = :clientId and c.session IN (select s from UserSessionEntity s where s.realmId = :realmId))"), + @NamedQuery(name = "removeClientSessionRoleByRealm", query="delete from ClientSessionRoleEntity r where r.clientSession IN (select c from ClientSessionEntity c where c.session IN (select s from UserSessionEntity s where s.realmId = :realmId))"), + @NamedQuery(name = "removeClientSessionRoleByExpired", query = "delete from ClientSessionRoleEntity r where r.clientSession IN (select c from ClientSessionEntity c where c.session IN (select s from UserSessionEntity s where s.realmId = :realmId and (s.started < :maxTime or s.lastSessionRefresh < :idleTime)))") +}) +@Table(name="CLIENT_SESSION_ROLE") +@Entity +@IdClass(ClientSessionRoleEntity.Key.class) +public class ClientSessionRoleEntity { + + @Id + @ManyToOne(fetch= FetchType.LAZY) + @JoinColumn(name="CLIENT_SESSION") + protected ClientSessionEntity clientSession; + + @Id + @Column(name = "ROLE_ID") + protected String roleId; + + public ClientSessionEntity getClientSession() { + return clientSession; + } + + public void setClientSession(ClientSessionEntity clientSession) { + this.clientSession = clientSession; + } + + public String getRoleId() { + return roleId; + } + + public void setRoleId(String roleId) { + this.roleId = roleId; + } + + + public static class Key implements Serializable { + + protected ClientSessionEntity clientSession; + + protected String roleId; + + public Key() { + } + + public Key(ClientSessionEntity clientSession, String roleId) { + this.clientSession = clientSession; + this.roleId = roleId; + } + + public ClientSessionEntity getClientSession() { + return clientSession; + } + + public String getRoleId() { + return roleId; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Key key = (Key) o; + + if (!roleId.equals(key.roleId)) return false; + if (!clientSession.getId().equals(key.clientSession.getId())) return false; + + return true; + } + + @Override + public int hashCode() { + int result = clientSession.getId().hashCode(); + result = 31 * result + roleId.hashCode(); + return result; + } + } +} diff --git a/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/entities/ClientUserSessionAssociationEntity.java b/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/entities/ClientUserSessionAssociationEntity.java deleted file mode 100755 index 99eb40ee5c..0000000000 --- a/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/entities/ClientUserSessionAssociationEntity.java +++ /dev/null @@ -1,97 +0,0 @@ -package org.keycloak.models.sessions.jpa.entities; - -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.FetchType; -import javax.persistence.Id; -import javax.persistence.IdClass; -import javax.persistence.JoinColumn; -import javax.persistence.ManyToOne; -import javax.persistence.NamedQueries; -import javax.persistence.NamedQuery; -import javax.persistence.Table; -import java.io.Serializable; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -@Entity -@Table(name = "CLIENT_USERSESSION") -@NamedQueries({ - @NamedQuery(name = "removeClientUserSessionByRealm", query = "delete from ClientUserSessionAssociationEntity a where a.session IN (select s from UserSessionEntity s where s.realmId = :realmId)"), - @NamedQuery(name = "removeClientUserSessionByUser", query = "delete from ClientUserSessionAssociationEntity a where a.session IN (select s from UserSessionEntity s where s.realmId = :realmId and s.userId = :userId)"), - @NamedQuery(name = "removeClientUserSessionByClient", query = "delete from ClientUserSessionAssociationEntity a where a.clientId = :clientId and a.session IN (select s from UserSessionEntity s where s.realmId = :realmId)"), - @NamedQuery(name = "removeClientUserSessionByExpired", query = "delete from ClientUserSessionAssociationEntity a where a.session IN (select s from UserSessionEntity s where s.realmId = :realmId and (s.started < :maxTime or s.lastSessionRefresh < :idleTime))") -}) -@IdClass(ClientUserSessionAssociationEntity.Key.class) -public class ClientUserSessionAssociationEntity { - - @Id - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "SESSION_ID") - protected UserSessionEntity session; - - @Id - @Column(name="CLIENT_ID",length = 36) - protected String clientId; - - public UserSessionEntity getSession() { - return session; - } - - public void setSession(UserSessionEntity session) { - this.session = session; - } - - public String getClientId() { - return clientId; - } - - public void setClientId(String clientId) { - this.clientId = clientId; - } - - public static class Key implements Serializable { - - private String clientId; - private UserSessionEntity session; - - public Key() { - } - - public Key(String clientId, UserSessionEntity session) { - this.clientId = clientId; - this.session = session; - } - - public String getClientId() { - return clientId; - } - - public UserSessionEntity getSession() { - return session; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - Key key = (Key) o; - - if (clientId != null ? !clientId.equals(key.clientId) : key.clientId != null) return false; - if (session != null ? !session.getId().equals(key.session != null ? key.session.getId() : null) : key.session != null) return false; - - return true; - } - - @Override - public int hashCode() { - int result = clientId != null ? clientId.hashCode() : 0; - result = 31 * result + (session != null ? session.getId().hashCode() : 0); - return result; - } - } - -} diff --git a/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/entities/UserSessionEntity.java b/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/entities/UserSessionEntity.java index 9094a908f1..3716c3ca64 100755 --- a/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/entities/UserSessionEntity.java +++ b/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/entities/UserSessionEntity.java @@ -20,8 +20,8 @@ import java.util.Collection; @Table(name = "USER_SESSION") @NamedQueries({ @NamedQuery(name = "getUserSessionByUser", query = "select s from UserSessionEntity s where s.realmId = :realmId and s.userId = :userId order by s.started, s.id"), - @NamedQuery(name = "getUserSessionByClient", query = "select s from UserSessionEntity s join s.clients c where s.realmId = :realmId and c.clientId = :clientId order by s.started, s.id"), - @NamedQuery(name = "getActiveUserSessionByClient", query = "select count(s) from UserSessionEntity s join s.clients c where s.realmId = :realmId and c.clientId = :clientId"), + @NamedQuery(name = "getUserSessionByClient", query = "select s from UserSessionEntity s join s.clientSessions c where s.realmId = :realmId and c.clientId = :clientId order by s.started, s.id"), + @NamedQuery(name = "getActiveUserSessionByClient", query = "select count(s) from UserSessionEntity s join s.clientSessions c where s.realmId = :realmId and c.clientId = :clientId"), @NamedQuery(name = "removeUserSessionByRealm", query = "delete from UserSessionEntity s where s.realmId = :realmId"), @NamedQuery(name = "removeUserSessionByUser", query = "delete from UserSessionEntity s where s.realmId = :realmId and s.userId = :userId"), @NamedQuery(name = "removeUserSessionByExpired", query = "delete from UserSessionEntity s where s.realmId = :realmId and (s.started < :maxTime or s.lastSessionRefresh < :idleTime)") @@ -56,8 +56,8 @@ public class UserSessionEntity { @Column(name="LAST_SESSION_REFRESH") protected int lastSessionRefresh; - @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.REMOVE, orphanRemoval = true, mappedBy="session") - protected Collection clients = new ArrayList(); + @OneToMany(cascade = CascadeType.REMOVE, orphanRemoval = true, mappedBy="session") + protected Collection clientSessions = new ArrayList(); public String getId() { return id; @@ -131,8 +131,8 @@ public class UserSessionEntity { this.lastSessionRefresh = lastSessionRefresh; } - public Collection getClients() { - return clients; + public Collection getClientSessions() { + return clientSessions; } } diff --git a/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/ClientSessionAdapter.java b/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/ClientSessionAdapter.java new file mode 100644 index 0000000000..81918b1e47 --- /dev/null +++ b/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/ClientSessionAdapter.java @@ -0,0 +1,79 @@ +package org.keycloak.models.sessions.mem; + +import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientSessionModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserSessionModel; +import org.keycloak.models.sessions.mem.entities.ClientSessionEntity; + +import java.util.Set; + +/** + * @author Stian Thorgersen + */ +public class ClientSessionAdapter implements ClientSessionModel { + + private KeycloakSession session; + private MemUserSessionProvider provider; + private RealmModel realm; + private ClientSessionEntity entity; + + public ClientSessionAdapter(KeycloakSession session, MemUserSessionProvider provider, RealmModel realm, ClientSessionEntity entity) { + this.session = session; + this.provider = provider; + this.realm = realm; + this.entity = entity; + } + + @Override + public String getId() { + return entity.getId(); + } + + @Override + public ClientModel getClient() { + return realm.findClientById(entity.getClientId()); + } + + @Override + public String getState() { + return entity.getState(); + } + + @Override + public UserSessionModel getUserSession() { + return new UserSessionAdapter(session, provider, realm, entity.getSession()); + } + + @Override + public String getRedirectUri() { + return entity.getRedirectUri(); + } + + @Override + public int getTimestamp() { + return entity.getTimestamp(); + } + + @Override + public void setTimestamp(int timestamp) { + entity.setTimestamp(timestamp); + } + + @Override + public ClientSessionModel.Action getAction() { + return entity.getAction(); + } + + @Override + public void setAction(ClientSessionModel.Action action) { + entity.setAction(action); + } + + @Override + public Set getRoles() { + return entity.getRoles(); + } + +} diff --git a/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/MemUserSessionProvider.java b/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/MemUserSessionProvider.java index 7021046b7d..8650397229 100755 --- a/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/MemUserSessionProvider.java +++ b/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/MemUserSessionProvider.java @@ -1,14 +1,15 @@ package org.keycloak.models.sessions.mem; import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientSessionModel; import org.keycloak.models.KeycloakSession; 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.sessions.mem.entities.ClientSessionEntity; import org.keycloak.models.sessions.mem.entities.UserSessionEntity; -import org.keycloak.models.sessions.mem.entities.UserSessionKey; import org.keycloak.models.sessions.mem.entities.UsernameLoginFailureEntity; import org.keycloak.models.sessions.mem.entities.UsernameLoginFailureKey; import org.keycloak.models.utils.KeycloakModelUtils; @@ -19,6 +20,7 @@ import java.util.Comparator; import java.util.Iterator; import java.util.LinkedList; import java.util.List; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; /** @@ -27,15 +29,42 @@ import java.util.concurrent.ConcurrentHashMap; public class MemUserSessionProvider implements UserSessionProvider { private final KeycloakSession session; - private final ConcurrentHashMap sessions; + private final ConcurrentHashMap userSessions; + private final ConcurrentHashMap clientSessions; private final ConcurrentHashMap loginFailures; - public MemUserSessionProvider(KeycloakSession session, ConcurrentHashMap sessions, ConcurrentHashMap loginFailures) { + public MemUserSessionProvider(KeycloakSession session, ConcurrentHashMap userSessions, ConcurrentHashMap clientSessions, ConcurrentHashMap loginFailures) { this.session = session; - this.sessions = sessions; + this.userSessions = userSessions; + this.clientSessions = clientSessions; this.loginFailures = loginFailures; } + @Override + public ClientSessionModel createClientSession(RealmModel realm, ClientModel client, UserSessionModel userSession, String redirectUri, String state, Set roles) { + UserSessionEntity userSessionEntity = getUserSessionEntity(realm, userSession.getId()); + + ClientSessionEntity entity = new ClientSessionEntity(); + entity.setId(KeycloakModelUtils.generateId()); + entity.setTimestamp(Time.currentTime()); + entity.setClientId(client.getId()); + entity.setSession(userSessionEntity); + entity.setRedirectUri(redirectUri); + entity.setState(state); + entity.setRoles(roles); + + userSessionEntity.addClientSession(entity); + + clientSessions.put(entity.getId(), entity); + return new ClientSessionAdapter(session, this, realm, entity); + } + + @Override + public ClientSessionModel getClientSession(RealmModel realm, String id) { + ClientSessionEntity entity = clientSessions.get(id); + return entity != null ? new ClientSessionAdapter(session, this, realm, entity) : null; + } + @Override public UserSessionModel createUserSession(RealmModel realm, UserModel user, String loginUsername, String ipAddress, String authMethod, boolean rememberMe) { String id = KeycloakModelUtils.generateId(); @@ -54,23 +83,31 @@ public class MemUserSessionProvider implements UserSessionProvider { entity.setStarted(currentTime); entity.setLastSessionRefresh(currentTime); - sessions.put(new UserSessionKey(realm.getId(), id), entity); + userSessions.put(id, entity); - return new UserSessionAdapter(session, realm, entity); + return new UserSessionAdapter(session, this, realm, entity); } @Override public UserSessionModel getUserSession(RealmModel realm, String id) { - UserSessionEntity entity = sessions.get(new UserSessionKey(realm.getId(), id)); - return entity != null ? new UserSessionAdapter(session, realm, entity) : null; + UserSessionEntity entity = getUserSessionEntity(realm, id); + return entity != null ? new UserSessionAdapter(session, this, realm, entity) : null; + } + + UserSessionEntity getUserSessionEntity(RealmModel realm, String id) { + UserSessionEntity entity = userSessions.get(id); + if (entity != null && entity.getRealm().equals(realm.getId())) { + return entity; + } + return null; } @Override public List getUserSessions(RealmModel realm, UserModel user) { List userSessions = new LinkedList(); - for (UserSessionEntity s : sessions.values()) { + for (UserSessionEntity s : this.userSessions.values()) { if (s.getRealm().equals(realm.getId()) && s.getUser().equals(user.getId())) { - userSessions.add(new UserSessionAdapter(session, realm, s)); + userSessions.add(new UserSessionAdapter(session, this, realm, s)); } } return userSessions; @@ -78,14 +115,21 @@ public class MemUserSessionProvider implements UserSessionProvider { @Override public List getUserSessions(RealmModel realm, ClientModel client) { - List clientSessions = new LinkedList(); - for (UserSessionEntity s : sessions.values()) { - if (s.getRealm().equals(realm.getId()) && s.getClients().contains(client.getClientId())) { - clientSessions.add(new UserSessionAdapter(session, realm, s)); + List userSessionEntities = new LinkedList(); + for (ClientSessionEntity s : clientSessions.values()) { + if (s.getSession().getRealm().equals(realm.getId()) && s.getClientId().equals(client.getId())) { + if (!userSessionEntities.contains(s.getSession())) { + userSessionEntities.add(s.getSession()); + } } } - Collections.sort(clientSessions, new UserSessionSort()); - return clientSessions; + + List userSessions = new LinkedList(); + for (UserSessionEntity e : userSessionEntities) { + userSessions.add(new UserSessionAdapter(session, this, realm, e)); + } + Collections.sort(userSessions, new UserSessionSort()); + return userSessions; } @Override @@ -101,49 +145,61 @@ public class MemUserSessionProvider implements UserSessionProvider { @Override public int getActiveUserSessions(RealmModel realm, ClientModel client) { - int count = 0; - for (UserSessionEntity s : sessions.values()) { - if (s.getRealm().equals(realm.getId()) && s.getClients().contains(client.getClientId())) { - count++; - } - } - return count; + return getUserSessions(realm, client).size(); } @Override public void removeUserSession(RealmModel realm, UserSessionModel session) { - sessions.remove(new UserSessionKey(realm.getId(), session.getId())); + UserSessionEntity entity = getUserSessionEntity(realm, session.getId()); + if (entity != null) { + userSessions.remove(entity.getId()); + for (ClientSessionEntity clientSession : entity.getClientSessions()) { + clientSessions.remove(clientSession.getId()); + } + } } @Override public void removeUserSessions(RealmModel realm, UserModel user) { - Iterator itr = sessions.values().iterator(); + Iterator itr = userSessions.values().iterator(); while (itr.hasNext()) { UserSessionEntity s = itr.next(); if (s.getRealm().equals(realm.getId()) && s.getUser().equals(user.getId())) { itr.remove(); + + for (ClientSessionEntity clientSession : s.getClientSessions()) { + clientSessions.remove(clientSession.getId()); + } } } } @Override public void removeExpiredUserSessions(RealmModel realm) { - Iterator itr = sessions.values().iterator(); + Iterator itr = userSessions.values().iterator(); while (itr.hasNext()) { UserSessionEntity s = itr.next(); if (s.getRealm().equals(realm.getId()) && (s.getLastSessionRefresh() < Time.currentTime() - realm.getSsoSessionIdleTimeout() || s.getStarted() < Time.currentTime() - realm.getSsoSessionMaxLifespan())) { itr.remove(); + + for (ClientSessionEntity clientSession : s.getClientSessions()) { + clientSessions.remove(clientSession.getId()); + } } } } @Override public void removeUserSessions(RealmModel realm) { - Iterator itr = sessions.values().iterator(); + Iterator itr = userSessions.values().iterator(); while (itr.hasNext()) { UserSessionEntity s = itr.next(); if (s.getRealm().equals(realm.getId())) { itr.remove(); + + for (ClientSessionEntity clientSession : s.getClientSessions()) { + clientSessions.remove(clientSession.getId()); + } } } } @@ -185,17 +241,10 @@ public class MemUserSessionProvider implements UserSessionProvider { @Override public void onClientRemoved(RealmModel realm, ClientModel client) { - Iterator itr = sessions.values().iterator(); - while (itr.hasNext()) { - UserSessionEntity s = itr.next(); - if (s.getRealm().equals(realm.getId())) { - itr.remove(); - } - } - - for (UserSessionEntity s : sessions.values()) { - if (s.getRealm().equals(realm.getId())) { - s.getClients().remove(client.getClientId()); + for (ClientSessionEntity e : clientSessions.values()) { + if (e.getSession().getRealm().equals(realm.getId()) && e.getClientId().equals(client.getId())) { + clientSessions.remove(e.getId()); + e.getSession().removeClientSession(e); } } } diff --git a/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/MemUserSessionProviderFactory.java b/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/MemUserSessionProviderFactory.java index c1ae6a3032..27d11e737d 100644 --- a/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/MemUserSessionProviderFactory.java +++ b/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/MemUserSessionProviderFactory.java @@ -4,8 +4,8 @@ import org.keycloak.Config; import org.keycloak.models.KeycloakSession; import org.keycloak.models.UserSessionProvider; import org.keycloak.models.UserSessionProviderFactory; +import org.keycloak.models.sessions.mem.entities.ClientSessionEntity; import org.keycloak.models.sessions.mem.entities.UserSessionEntity; -import org.keycloak.models.sessions.mem.entities.UserSessionKey; import org.keycloak.models.sessions.mem.entities.UsernameLoginFailureEntity; import org.keycloak.models.sessions.mem.entities.UsernameLoginFailureKey; @@ -18,13 +18,15 @@ public class MemUserSessionProviderFactory implements UserSessionProviderFactory public static final String ID = "mem"; - private ConcurrentHashMap sessions = new ConcurrentHashMap(); + private ConcurrentHashMap userSessions = new ConcurrentHashMap(); + + private ConcurrentHashMap clientSessions = new ConcurrentHashMap(); private ConcurrentHashMap loginFailures = new ConcurrentHashMap(); @Override public UserSessionProvider create(KeycloakSession session) { - return new MemUserSessionProvider(session, sessions, loginFailures); + return new MemUserSessionProvider(session, userSessions, clientSessions, loginFailures); } @Override @@ -33,7 +35,7 @@ public class MemUserSessionProviderFactory implements UserSessionProviderFactory @Override public void close() { - sessions.clear(); + userSessions.clear(); loginFailures.clear(); } diff --git a/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/UserSessionAdapter.java b/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/UserSessionAdapter.java index a9008cfc42..2d1b16aac4 100755 --- a/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/UserSessionAdapter.java +++ b/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/UserSessionAdapter.java @@ -1,10 +1,12 @@ package org.keycloak.models.sessions.mem; import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientSessionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; +import org.keycloak.models.sessions.mem.entities.ClientSessionEntity; import org.keycloak.models.sessions.mem.entities.UserSessionEntity; import java.util.LinkedList; @@ -17,12 +19,14 @@ public class UserSessionAdapter implements UserSessionModel { private final KeycloakSession session; + private MemUserSessionProvider provider; private final RealmModel realm; private final UserSessionEntity entity; - public UserSessionAdapter(KeycloakSession session, RealmModel realm, UserSessionEntity entity) { + public UserSessionAdapter(KeycloakSession session, MemUserSessionProvider provider, RealmModel realm, UserSessionEntity entity) { this.session = session; + this.provider = provider; this.realm = realm; this.entity = entity; } @@ -98,24 +102,14 @@ public class UserSessionAdapter implements UserSessionModel { } @Override - public void associateClient(ClientModel client) { - if (!entity.getClients().contains(client.getClientId())) { - entity.getClients().add(client.getClientId()); + public List getClientSessions() { + List clientSessionModels = new LinkedList(); + if (entity.getClientSessions() != null) { + for (ClientSessionEntity e : entity.getClientSessions()) { + clientSessionModels.add(new ClientSessionAdapter(session, provider, realm, e)); + } } - } - - @Override - public List getClientAssociations() { - List models = new LinkedList(); - for (String clientId : entity.getClients()) { - models.add(realm.findClient(clientId)); - } - return models; - } - - @Override - public void removeAssociatedClient(ClientModel client) { - entity.getClients().remove(client.getClientId()); + return clientSessionModels; } @Override diff --git a/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/entities/ClientSessionEntity.java b/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/entities/ClientSessionEntity.java new file mode 100644 index 0000000000..f56829d25c --- /dev/null +++ b/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/entities/ClientSessionEntity.java @@ -0,0 +1,87 @@ +package org.keycloak.models.sessions.mem.entities; + +import org.keycloak.models.ClientSessionModel; + +import java.util.Set; + +/** + * @author Stian Thorgersen + */ +public class ClientSessionEntity { + + private String id; + private String clientId; + + private UserSessionEntity session; + + private String redirectUri; + private String state; + + private int timestamp; + private ClientSessionModel.Action action; + private Set roles; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public UserSessionEntity getSession() { + return session; + } + + public void setSession(UserSessionEntity session) { + this.session = session; + } + + public String getRedirectUri() { + return redirectUri; + } + + public void setRedirectUri(String redirectUri) { + this.redirectUri = redirectUri; + } + + public String getState() { + return state; + } + + public void setState(String state) { + this.state = state; + } + + public int getTimestamp() { + return timestamp; + } + + public void setTimestamp(int timestamp) { + this.timestamp = timestamp; + } + + public ClientSessionModel.Action getAction() { + return action; + } + + public void setAction(ClientSessionModel.Action action) { + this.action = action; + } + + public Set getRoles() { + return roles; + } + + public void setRoles(Set roles) { + this.roles = roles; + } +} diff --git a/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/entities/UserSessionEntity.java b/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/entities/UserSessionEntity.java index 25961371ba..30cb920145 100644 --- a/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/entities/UserSessionEntity.java +++ b/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/entities/UserSessionEntity.java @@ -1,5 +1,6 @@ package org.keycloak.models.sessions.mem.entities; +import java.util.Collections; import java.util.LinkedList; import java.util.List; @@ -17,7 +18,7 @@ public class UserSessionEntity { private boolean rememberMe; private int started; private int lastSessionRefresh; - private List clients = new LinkedList(); + private List clientSessions = Collections.synchronizedList(new LinkedList()); public String getId() { return id; @@ -91,12 +92,21 @@ public class UserSessionEntity { this.lastSessionRefresh = lastSessionRefresh; } - public List getClients() { - return clients; + public void addClientSession(ClientSessionEntity clientSession) { + if (clientSessions == null) { + clientSessions = new LinkedList(); + } + clientSessions.add(clientSession); } - public void setClients(List clients) { - this.clients = clients; + public void removeClientSession(ClientSessionEntity clientSession) { + if (clientSessions != null) { + clientSessions.remove(clientSession); + } + } + + public List getClientSessions() { + return clientSessions; } } diff --git a/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/entities/UserSessionKey.java b/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/entities/UserSessionKey.java deleted file mode 100644 index 0fcb86cee8..0000000000 --- a/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/entities/UserSessionKey.java +++ /dev/null @@ -1,36 +0,0 @@ -package org.keycloak.models.sessions.mem.entities; - -/** - * @author Stian Thorgersen - */ -public class UserSessionKey { - - private final String realm; - private final String id; - - public UserSessionKey(String realm, String id) { - this.realm = realm; - this.id = id; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - UserSessionKey key = (UserSessionKey) o; - - if (realm != null ? !realm.equals(key.realm) : key.realm != null) return false; - if (id != null ? !id.equals(key.id) : key.id != null) return false; - - return true; - } - - @Override - public int hashCode() { - int result = realm != null ? realm.hashCode() : 0; - result = 31 * result + (id != null ? id.hashCode() : 0); - return result; - } - -} diff --git a/model/sessions-mongo/src/main/java/org/keycloak/models/sessions/mongo/ClientSessionAdapter.java b/model/sessions-mongo/src/main/java/org/keycloak/models/sessions/mongo/ClientSessionAdapter.java new file mode 100644 index 0000000000..c21ae096bf --- /dev/null +++ b/model/sessions-mongo/src/main/java/org/keycloak/models/sessions/mongo/ClientSessionAdapter.java @@ -0,0 +1,88 @@ +package org.keycloak.models.sessions.mongo; + +import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext; +import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientSessionModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserSessionModel; +import org.keycloak.models.sessions.mongo.entities.MongoClientSessionEntity; +import org.keycloak.models.sessions.mongo.entities.MongoUserSessionEntity; + +import java.util.HashSet; +import java.util.Set; + +/** + * @author Stian Thorgersen + */ +public class ClientSessionAdapter implements ClientSessionModel { + + private KeycloakSession session; + private MongoUserSessionProvider provider; + private RealmModel realm; + private MongoClientSessionEntity entity; + private MongoUserSessionEntity userSessionEntity; + private MongoStoreInvocationContext invContext; + + public ClientSessionAdapter(KeycloakSession session, MongoUserSessionProvider provider, RealmModel realm, MongoClientSessionEntity entity, MongoUserSessionEntity userSessionEntity, MongoStoreInvocationContext invContext) { + this.session = session; + this.provider = provider; + this.realm = realm; + this.entity = entity; + this.userSessionEntity = userSessionEntity; + this.invContext = invContext; + } + + @Override + public String getId() { + return entity.getId(); + } + + @Override + public ClientModel getClient() { + return realm.findClientById(entity.getClientId()); + } + + @Override + public String getState() { + return entity.getState(); + } + + @Override + public UserSessionModel getUserSession() { + return new UserSessionAdapter(session, provider, userSessionEntity, realm, invContext); + } + + @Override + public String getRedirectUri() { + return entity.getRedirectUri(); + } + + @Override + public int getTimestamp() { + return entity.getTimestamp(); + } + + @Override + public void setTimestamp(int timestamp) { + entity.setTimestamp(timestamp); + invContext.getMongoStore().updateEntity(userSessionEntity, invContext); + } + + @Override + public Action getAction() { + return entity.getAction(); + } + + @Override + public void setAction(Action action) { + entity.setAction(action); + invContext.getMongoStore().updateEntity(userSessionEntity, invContext); + } + + @Override + public Set getRoles() { + return entity.getRoles() != null ? new HashSet(entity.getRoles()) : null; + } + +} diff --git a/model/sessions-mongo/src/main/java/org/keycloak/models/sessions/mongo/MongoUserSessionProvider.java b/model/sessions-mongo/src/main/java/org/keycloak/models/sessions/mongo/MongoUserSessionProvider.java index b555407ad1..e4e75c5c73 100755 --- a/model/sessions-mongo/src/main/java/org/keycloak/models/sessions/mongo/MongoUserSessionProvider.java +++ b/model/sessions-mongo/src/main/java/org/keycloak/models/sessions/mongo/MongoUserSessionProvider.java @@ -6,18 +6,22 @@ import com.mongodb.QueryBuilder; import org.keycloak.connections.mongo.api.MongoStore; import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext; import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientSessionModel; import org.keycloak.models.KeycloakSession; 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.sessions.mongo.entities.MongoClientSessionEntity; import org.keycloak.models.sessions.mongo.entities.MongoUserSessionEntity; import org.keycloak.models.sessions.mongo.entities.MongoUsernameLoginFailureEntity; +import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.util.Time; import java.util.LinkedList; import java.util.List; +import java.util.Set; /** * @author Stian Thorgersen @@ -34,6 +38,47 @@ public class MongoUserSessionProvider implements UserSessionProvider { this.invocationContext = invocationContext; } + @Override + public ClientSessionModel createClientSession(RealmModel realm, ClientModel client, UserSessionModel userSession, String redirectUri, String state, Set roles) { + MongoUserSessionEntity userSessionEntity = getUserSessionEntity(realm, userSession.getId()); + + MongoClientSessionEntity entity = new MongoClientSessionEntity(); + entity.setId(KeycloakModelUtils.generateId()); + entity.setTimestamp(Time.currentTime()); + entity.setClientId(client.getId()); + entity.setRedirectUri(redirectUri); + entity.setState(state); + if (roles != null) { + entity.setRoles(new LinkedList(roles)); + } + + mongoStore.pushItemToList(userSessionEntity, "clientSessions", entity, false, invocationContext); + + return new ClientSessionAdapter(session, this, realm, entity, userSessionEntity, invocationContext); + } + + @Override + public ClientSessionModel getClientSession(RealmModel realm, String id) { + DBObject query = new QueryBuilder() + .and("realmId").is(realm.getId()) + .and("clientSessions.id").is(id).get(); + + List entities = mongoStore.loadEntities(MongoUserSessionEntity.class, query, invocationContext); + if (entities.isEmpty()) { + return null; + } + + MongoUserSessionEntity userSessionEntity = entities.get(0); + List sessions = userSessionEntity.getClientSessions(); + for (MongoClientSessionEntity s : sessions) { + if (s.getId().equals(id)) { + return new ClientSessionAdapter(session, this, realm, s, userSessionEntity, invocationContext); + } + } + + return null; + } + @Override public UserSessionModel createUserSession(RealmModel realm, UserModel user, String loginUsername, String ipAddress, String authMethod, boolean rememberMe) { MongoUserSessionEntity entity = new MongoUserSessionEntity(); @@ -50,25 +95,29 @@ public class MongoUserSessionProvider implements UserSessionProvider { entity.setLastSessionRefresh(currentTime); mongoStore.insertEntity(entity, invocationContext); - return new UserSessionAdapter(session, entity, realm, invocationContext); + return new UserSessionAdapter(session, this, entity, realm, invocationContext); } @Override public UserSessionModel getUserSession(RealmModel realm, String id) { - MongoUserSessionEntity entity = mongoStore.loadEntity(MongoUserSessionEntity.class, id, invocationContext); + MongoUserSessionEntity entity = getUserSessionEntity(realm, id); if (entity == null) { return null; } else { - return new UserSessionAdapter(session, entity, realm, invocationContext); + return new UserSessionAdapter(session, this, entity, realm, invocationContext); } } + MongoUserSessionEntity getUserSessionEntity(RealmModel realm, String id) { + return mongoStore.loadEntity(MongoUserSessionEntity.class, id, invocationContext); + } + @Override public List getUserSessions(RealmModel realm, UserModel user) { DBObject query = new BasicDBObject("user", user.getId()); List sessions = new LinkedList(); for (MongoUserSessionEntity e : mongoStore.loadEntities(MongoUserSessionEntity.class, query, invocationContext)) { - sessions.add(new UserSessionAdapter(session, e, realm, invocationContext)); + sessions.add(new UserSessionAdapter(session, this, e, realm, invocationContext)); } return sessions; } @@ -80,14 +129,14 @@ public class MongoUserSessionProvider implements UserSessionProvider { public List getUserSessions(RealmModel realm, ClientModel client, int firstResult, int maxResults) { DBObject query = new QueryBuilder() - .and("associatedClientIds").is(client.getId()) + .and("clientSessions.clientId").is(client.getId()) .get(); DBObject sort = new BasicDBObject("started", 1).append("id", 1); List sessions = mongoStore.loadEntities(MongoUserSessionEntity.class, query, sort, firstResult, maxResults, invocationContext); List result = new LinkedList(); for (MongoUserSessionEntity session : sessions) { - result.add(new UserSessionAdapter(this.session, session, realm, invocationContext)); + result.add(new UserSessionAdapter(this.session, this, session, realm, invocationContext)); } return result; } @@ -95,7 +144,7 @@ public class MongoUserSessionProvider implements UserSessionProvider { @Override public int getActiveUserSessions(RealmModel realm, ClientModel client) { DBObject query = new QueryBuilder() - .and("associatedClientIds").is(client.getId()) + .and("clientSessions.clientId").is(client.getId()) .get(); return mongoStore.countEntities(MongoUserSessionEntity.class, query, invocationContext); } @@ -184,13 +233,22 @@ public class MongoUserSessionProvider implements UserSessionProvider { } @Override + // TODO Not very efficient, should use Mongo $pull to remove directly public void onClientRemoved(RealmModel realm, ClientModel client) { DBObject query = new QueryBuilder() - .and("realmId").is(realm.getId()) + .and("clientSessions.clientId").is(client.getId()) .get(); - List sessions = invocationContext.getMongoStore().loadEntities(MongoUserSessionEntity.class, query, invocationContext); - for (MongoUserSessionEntity session : sessions) { - invocationContext.getMongoStore().pullItemFromList(session, "associatedClientIds", client.getClientId(), invocationContext); + List userSessionEntities = mongoStore.loadEntities(MongoUserSessionEntity.class, query, invocationContext); + for (MongoUserSessionEntity e : userSessionEntities) { + List remove = new LinkedList(); + for (MongoClientSessionEntity c : e.getClientSessions()) { + if (c.getClientId().equals(client.getId())) { + remove.add(c); + } + } + for (MongoClientSessionEntity c : remove) { + mongoStore.pullItemFromList(e, "clientSessions", c, invocationContext); + } } } diff --git a/model/sessions-mongo/src/main/java/org/keycloak/models/sessions/mongo/UserSessionAdapter.java b/model/sessions-mongo/src/main/java/org/keycloak/models/sessions/mongo/UserSessionAdapter.java index d7a3223709..633e1e65e1 100755 --- a/model/sessions-mongo/src/main/java/org/keycloak/models/sessions/mongo/UserSessionAdapter.java +++ b/model/sessions-mongo/src/main/java/org/keycloak/models/sessions/mongo/UserSessionAdapter.java @@ -2,14 +2,15 @@ package org.keycloak.models.sessions.mongo; import org.jboss.logging.Logger; import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext; -import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientSessionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; +import org.keycloak.models.sessions.mongo.entities.MongoClientSessionEntity; import org.keycloak.models.sessions.mongo.entities.MongoUserSessionEntity; -import java.util.ArrayList; +import java.util.LinkedList; import java.util.List; /** @@ -19,16 +20,19 @@ public class UserSessionAdapter extends AbstractMongoAdapter getClientAssociations() { - List associatedClientIds = getMongoEntity().getAssociatedClientIds(); - - List clients = new ArrayList(); - for (String clientId : associatedClientIds) { - // Try application first - ClientModel client = realm.getApplicationById(clientId); - - // And then OAuthClient - if (client == null) { - client = realm.getOAuthClientById(clientId); - } - - if (client != null) { - clients.add(client); - } else { - logger.warnf("Not found associated client with Id: %s", clientId); - } + public List getClientSessions() { + List sessions = new LinkedList(); + for (MongoClientSessionEntity e : entity.getClientSessions()) { + sessions.add(new ClientSessionAdapter(keycloakSession, provider, realm, e, entity, invocationContext)); } - return clients; - } - - @Override - public void removeAssociatedClient(ClientModel client) { - getMongoStore().pullItemFromList(entity, "associatedClientIds", client.getId(), invocationContext); + return sessions; } @Override diff --git a/model/sessions-mongo/src/main/java/org/keycloak/models/sessions/mongo/entities/MongoClientSessionEntity.java b/model/sessions-mongo/src/main/java/org/keycloak/models/sessions/mongo/entities/MongoClientSessionEntity.java new file mode 100644 index 0000000000..5bd9ac2d47 --- /dev/null +++ b/model/sessions-mongo/src/main/java/org/keycloak/models/sessions/mongo/entities/MongoClientSessionEntity.java @@ -0,0 +1,80 @@ +package org.keycloak.models.sessions.mongo.entities; + +import org.keycloak.models.ClientSessionModel; + +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +/** + * @author Stian Thorgersen + */ +public class MongoClientSessionEntity { + + private String id; + private String clientId; + + private String redirectUri; + private String state; + + private int timestamp; + private ClientSessionModel.Action action; + private List roles; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public String getRedirectUri() { + return redirectUri; + } + + public void setRedirectUri(String redirectUri) { + this.redirectUri = redirectUri; + } + + public String getState() { + return state; + } + + public void setState(String state) { + this.state = state; + } + + public int getTimestamp() { + return timestamp; + } + + public void setTimestamp(int timestamp) { + this.timestamp = timestamp; + } + + public ClientSessionModel.Action getAction() { + return action; + } + + public void setAction(ClientSessionModel.Action action) { + this.action = action; + } + + public List getRoles() { + return roles; + } + + public void setRoles(List roles) { + this.roles = roles; + } + +} diff --git a/model/sessions-mongo/src/main/java/org/keycloak/models/sessions/mongo/entities/MongoUserSessionEntity.java b/model/sessions-mongo/src/main/java/org/keycloak/models/sessions/mongo/entities/MongoUserSessionEntity.java index 8d0e946f9e..89679def33 100755 --- a/model/sessions-mongo/src/main/java/org/keycloak/models/sessions/mongo/entities/MongoUserSessionEntity.java +++ b/model/sessions-mongo/src/main/java/org/keycloak/models/sessions/mongo/entities/MongoUserSessionEntity.java @@ -6,6 +6,7 @@ import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext; import org.keycloak.models.entities.AbstractIdentifiableEntity; import java.util.ArrayList; +import java.util.LinkedList; import java.util.List; /** @@ -30,7 +31,7 @@ public class MongoUserSessionEntity extends AbstractIdentifiableEntity implement private int lastSessionRefresh; - private List associatedClientIds = new ArrayList(); + private List clientSessions; public String getRealmId() { return realmId; @@ -96,12 +97,12 @@ public class MongoUserSessionEntity extends AbstractIdentifiableEntity implement this.lastSessionRefresh = lastSessionRefresh; } - public List getAssociatedClientIds() { - return associatedClientIds; + public List getClientSessions() { + return clientSessions; } - public void setAssociatedClientIds(List associatedClientIds) { - this.associatedClientIds = associatedClientIds; + public void setClientSessions(List clientSessions) { + this.clientSessions = clientSessions; } @Override diff --git a/services/src/main/java/org/keycloak/services/managers/AccessCode.java b/services/src/main/java/org/keycloak/services/managers/AccessCode.java new file mode 100755 index 0000000000..edea2556ad --- /dev/null +++ b/services/src/main/java/org/keycloak/services/managers/AccessCode.java @@ -0,0 +1,171 @@ +package org.keycloak.services.managers; + +import org.keycloak.OAuthErrorException; +import org.keycloak.jose.jws.Algorithm; +import org.keycloak.jose.jws.crypto.RSAProvider; +import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientSessionModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.RoleModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.UserModel.RequiredAction; +import org.keycloak.util.Base64Url; +import org.keycloak.util.Time; + +import java.nio.ByteBuffer; +import java.security.MessageDigest; +import java.security.Signature; +import java.util.HashSet; +import java.util.Set; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class AccessCode { + + private final RealmModel realm; + private final ClientSessionModel clientSession; + + public AccessCode(RealmModel realm, ClientSessionModel clientSession) { + this.realm = realm; + this.clientSession = clientSession; + } + + public static AccessCode parse(String code, KeycloakSession session, RealmModel realm) { + try { + String[] parts = code.split("\\."); + String id = new String(Base64Url.decode(parts[1])); + + ClientSessionModel clientSession = session.sessions().getClientSession(realm, id); + if (clientSession == null) { + return null; + } + + String hash = createSignatureHash(realm, clientSession); + if (!hash.equals(parts[0])) { + return null; + } + + return new AccessCode(realm, clientSession); + } catch (RuntimeException e) { + return null; + } + } + + public String getCodeId() { + return clientSession.getId(); + } + + public UserModel getUser() { + return clientSession.getUserSession().getUser(); + } + + public String getSessionState() { + return clientSession.getUserSession().getId(); + } + + public boolean isValid(RequiredAction requiredAction) { + return isValid(convertToAction(requiredAction)); + } + + public boolean isValid(ClientSessionModel.Action requestedAction) { + ClientSessionModel.Action action = clientSession.getAction(); + if (action == null) { + return false; + } + + int timestamp = clientSession.getTimestamp(); + + if (!action.equals(requestedAction)) { + return false; + } + + int lifespan = action.equals(ClientSessionModel.Action.CODE_TO_TOKEN) ? realm.getAccessCodeLifespan() : realm.getAccessCodeLifespanUserAction(); + return timestamp + lifespan > Time.currentTime(); + } + + public Set getRequestedRoles() { + Set requestedRoles = new HashSet(); + for (String roleId : clientSession.getRoles()) { + RoleModel role = realm.getRoleById(roleId); + if (role == null) { + new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Invalid role " + roleId); + } + requestedRoles.add(realm.getRoleById(roleId)); + } + return requestedRoles; + } + + public ClientModel getClient() { + return clientSession.getClient(); + } + + public String getState() { + return clientSession.getState(); + } + + public String getRedirectUri() { + return clientSession.getRedirectUri(); + } + + public ClientSessionModel.Action getAction() { + return clientSession.getAction(); + } + + public void setAction(ClientSessionModel.Action action) { + clientSession.setAction(action); + clientSession.setTimestamp(Time.currentTime()); + } + + public void setRequiredAction(RequiredAction requiredAction) { + setAction(convertToAction(requiredAction)); + } + + private ClientSessionModel.Action convertToAction(RequiredAction requiredAction) { + switch (requiredAction) { + case CONFIGURE_TOTP: + return ClientSessionModel.Action.CONFIGURE_TOTP; + case UPDATE_PASSWORD: + return ClientSessionModel.Action.UPDATE_PASSWORD; + case UPDATE_PROFILE: + return ClientSessionModel.Action.UPDATE_PROFILE; + case VERIFY_EMAIL: + return ClientSessionModel.Action.VERIFY_EMAIL; + default: + throw new IllegalArgumentException("Unknown required action " + requiredAction); + } + } + + public String getCode() { + String hash = createSignatureHash(realm, clientSession); + + StringBuilder sb = new StringBuilder(); + sb.append(hash); + sb.append("."); + sb.append(Base64Url.encode(clientSession.getId().getBytes())); + + return sb.toString(); + } + + private static String createSignatureHash(RealmModel realm, ClientSessionModel clientSession) { + try { + Signature signature = Signature.getInstance(RSAProvider.getJavaAlgorithm(Algorithm.RS256)); + signature.initSign(realm.getPrivateKey()); + signature.update(clientSession.getId().getBytes()); + signature.update(ByteBuffer.allocate(4).putInt(clientSession.getTimestamp())); + if (clientSession.getAction() != null) { + signature.update(clientSession.getAction().toString().getBytes()); + } + byte[] sign = signature.sign(); + + MessageDigest digest = MessageDigest.getInstance("sha-1"); + digest.update(sign); + return Base64Url.encode(digest.digest()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + +} diff --git a/services/src/main/java/org/keycloak/services/managers/AccessCodeEntry.java b/services/src/main/java/org/keycloak/services/managers/AccessCodeEntry.java deleted file mode 100755 index 68e1cf8cd3..0000000000 --- a/services/src/main/java/org/keycloak/services/managers/AccessCodeEntry.java +++ /dev/null @@ -1,125 +0,0 @@ -package org.keycloak.services.managers; - -import org.keycloak.OAuthErrorException; -import org.keycloak.jose.jws.JWSBuilder; -import org.keycloak.models.ClientModel; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.RealmModel; -import org.keycloak.models.RoleModel; -import org.keycloak.models.UserModel; -import org.keycloak.models.UserModel.RequiredAction; -import org.keycloak.representations.AccessCode; -import org.keycloak.util.Time; - -import java.util.HashSet; -import java.util.Set; - -/** -* @author Bill Burke -* @version $Revision: 1 $ -*/ -public class AccessCodeEntry { - protected AccessCode accessCode; - protected RealmModel realm; - KeycloakSession keycloakSession; - - public AccessCodeEntry(KeycloakSession keycloakSession, RealmModel realm, AccessCode accessCode) { - this.realm = realm; - this.accessCode = accessCode; - this.keycloakSession = keycloakSession; - } - - public String getCodeId() { - return this.accessCode.getId(); - } - - public UserModel getUser() { - return keycloakSession.users().getUserById(accessCode.getUserId(), realm); - } - - public String getSessionState() { - return accessCode.getSessionState(); - } - - public boolean isExpired() { - int lifespan = accessCode.getAction() == null ? realm.getAccessCodeLifespan() : realm.getAccessCodeLifespanUserAction(); - return accessCode.getTimestamp() + lifespan < Time.currentTime(); - } - - public Set getRequestedRoles() { - Set requestedRoles = new HashSet(); - for (String roleId : accessCode.getRequestedRoles()) { - RoleModel role = realm.getRoleById(roleId); - if (role == null) { - new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Invalid role " + roleId); - } - requestedRoles.add(realm.getRoleById(roleId)); - } - return requestedRoles; - } - - public ClientModel getClient() { - return realm.findClient(accessCode.getClientId()); - } - - public String getState() { - return accessCode.getState(); - } - - public void setState(String state) { - accessCode.setState(state); - } - - public String getRedirectUri() { - return accessCode.getRedirectUri(); - } - - public AccessCode.Action getAction() { - return accessCode.getAction(); - } - - public void setAction(AccessCode.Action action) { - accessCode.setAction(action); - accessCode.setTimestamp(Time.currentTime()); - } - - public RequiredAction getRequiredAction() { - AccessCode.Action action = accessCode.getAction(); - if (action != null) { - switch (action) { - case CONFIGURE_TOTP: - return RequiredAction.CONFIGURE_TOTP; - case UPDATE_PASSWORD: - return RequiredAction.UPDATE_PASSWORD; - case UPDATE_PROFILE: - return RequiredAction.UPDATE_PROFILE; - case VERIFY_EMAIL: - return RequiredAction.VERIFY_EMAIL; - } - } - return null; - } - - public void setRequiredAction(RequiredAction requiredAction) { - switch (requiredAction) { - case CONFIGURE_TOTP: - setAction(AccessCode.Action.CONFIGURE_TOTP); - break; - case UPDATE_PASSWORD: - setAction(AccessCode.Action.UPDATE_PASSWORD); - break; - case UPDATE_PROFILE: - setAction(AccessCode.Action.UPDATE_PROFILE); - break; - case VERIFY_EMAIL: - setAction(AccessCode.Action.VERIFY_EMAIL); - break; - default: - throw new IllegalArgumentException("Unknown required action " + requiredAction); - } - } - - public String getCode() { - return new JWSBuilder().jsonContent(accessCode).rsa256(realm.getPrivateKey()); - } -} diff --git a/services/src/main/java/org/keycloak/services/managers/TokenManager.java b/services/src/main/java/org/keycloak/services/managers/TokenManager.java index 727679ecf3..16a8fca5ed 100755 --- a/services/src/main/java/org/keycloak/services/managers/TokenManager.java +++ b/services/src/main/java/org/keycloak/services/managers/TokenManager.java @@ -10,13 +10,13 @@ import org.keycloak.jose.jws.crypto.RSAProvider; import org.keycloak.models.ApplicationModel; import org.keycloak.models.ClaimMask; import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientSessionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; import org.keycloak.models.utils.KeycloakModelUtils; -import org.keycloak.representations.AccessCode; import org.keycloak.representations.AccessToken; import org.keycloak.representations.AccessTokenResponse; import org.keycloak.representations.IDToken; @@ -28,7 +28,6 @@ import java.io.IOException; import java.util.HashSet; import java.util.Map; import java.util.Set; -import java.util.UUID; /** * Stateful object that creates tokens and manages oauth access codes @@ -39,22 +38,6 @@ import java.util.UUID; public class TokenManager { protected static final Logger logger = Logger.getLogger(TokenManager.class); - public AccessCodeEntry parseCode(String code, KeycloakSession session, RealmModel realm) { - try { - JWSInput input = new JWSInput(code); - if (!RSAProvider.verify(input, realm.getPublicKey())) { - logger.error("Could not verify access code"); - return null; - } - AccessCode accessCode = input.readJsonContent(AccessCode.class); - return new AccessCodeEntry(session, realm, accessCode); - } catch (Exception e) { - logger.error("error parsing access code", e); - return null; - } - - } - public static void applyScope(RoleModel role, RoleModel scope, Set visited, Set requested) { if (visited.contains(scope)) return; visited.add(scope); @@ -69,30 +52,14 @@ public class TokenManager { } } - - - public AccessCodeEntry createAccessCode(String scopeParam, String state, String redirect, KeycloakSession keycloakSession, RealmModel realm, ClientModel client, UserModel user, UserSessionModel session) { - return createAccessCodeEntry(scopeParam, state, redirect, keycloakSession, realm, client, user, session); - } - - private AccessCodeEntry createAccessCodeEntry(String scopeParam, String state, String redirect, KeycloakSession keycloakSession, RealmModel realm, ClientModel client, UserModel user, UserSessionModel session) { - AccessCode code = new AccessCode(); - code.setId(UUID.randomUUID().toString() + System.currentTimeMillis()); - code.setClientId(client.getClientId()); - code.setUserId(user.getId()); - code.setTimestamp(Time.currentTime()); - code.setSessionState(session != null ? session.getId() : null); - code.setRedirectUri(redirect); - code.setState(state); - + public AccessCode createAccessCode(String scopeParam, String state, String redirect, KeycloakSession session, RealmModel realm, ClientModel client, UserModel user, UserSessionModel userSession) { Set requestedRoles = new HashSet(); for (RoleModel r : getAccess(scopeParam, client, user)) { requestedRoles.add(r.getId()); } - code.setRequestedRoles(requestedRoles); - AccessCodeEntry entry = new AccessCodeEntry(keycloakSession, realm, code); - return entry; + ClientSessionModel clientSession = session.sessions().createClientSession(realm, client, userSession, redirect, state, requestedRoles); + return new AccessCode(realm, clientSession); } public AccessToken refreshAccessToken(KeycloakSession session, UriInfo uriInfo, RealmModel realm, ClientModel client, String encodedRefreshToken, Audit audit) throws OAuthErrorException { diff --git a/services/src/main/java/org/keycloak/services/resources/AccountService.java b/services/src/main/java/org/keycloak/services/resources/AccountService.java index f3b751e260..f0fddfa6eb 100755 --- a/services/src/main/java/org/keycloak/services/resources/AccountService.java +++ b/services/src/main/java/org/keycloak/services/resources/AccountService.java @@ -40,12 +40,14 @@ import org.keycloak.models.ApplicationModel; import org.keycloak.models.AuthenticationLinkModel; import org.keycloak.models.AuthenticationProviderModel; import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientSessionModel; import org.keycloak.models.Constants; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.SocialLinkModel; import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserModel; +import org.keycloak.models.UserSessionModel; import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.models.utils.TimeBasedOTP; import org.keycloak.representations.idm.CredentialRepresentation; @@ -151,9 +153,20 @@ public class AccountService { } } if (authResult != null) { - if (authResult.getSession() != null) { - authResult.getSession().associateClient(application); + UserSessionModel userSession = authResult.getSession(); + if (userSession != null) { + boolean associated = false; + for (ClientSessionModel c : userSession.getClientSessions()) { + if (c.getClient().equals(application)) { + associated = true; + break; + } + } + if (!associated) { + session.sessions().createClientSession(realm, application, userSession, null, null, null); + } } + account.setUser(auth.getUser()); AuthenticationLinkModel authLinkModel = auth.getUser().getAuthenticationLink(); diff --git a/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java b/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java index 60988ca438..904004568f 100755 --- a/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java +++ b/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java @@ -34,6 +34,7 @@ import org.keycloak.email.EmailException; import org.keycloak.email.EmailProvider; import org.keycloak.login.LoginFormsProvider; import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientSessionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserCredentialModel; @@ -43,7 +44,7 @@ import org.keycloak.models.UserSessionModel; import org.keycloak.models.utils.TimeBasedOTP; import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.services.ClientConnection; -import org.keycloak.services.managers.AccessCodeEntry; +import org.keycloak.services.managers.AccessCode; import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.TokenManager; import org.keycloak.services.messages.Messages; @@ -106,7 +107,7 @@ public class RequiredActionsService { @POST @Consumes(MediaType.APPLICATION_FORM_URLENCODED) public Response updateProfile(final MultivaluedMap formData) { - AccessCodeEntry accessCode = getAccessCodeEntry(RequiredAction.UPDATE_PROFILE); + AccessCode accessCode = getAccessCodeEntry(RequiredAction.UPDATE_PROFILE); if (accessCode == null) { return unauthorized(); } @@ -144,7 +145,7 @@ public class RequiredActionsService { @POST @Consumes(MediaType.APPLICATION_FORM_URLENCODED) public Response updateTotp(final MultivaluedMap formData) { - AccessCodeEntry accessCode = getAccessCodeEntry(RequiredAction.CONFIGURE_TOTP); + AccessCode accessCode = getAccessCodeEntry(RequiredAction.CONFIGURE_TOTP); if (accessCode == null) { return unauthorized(); } @@ -182,7 +183,7 @@ public class RequiredActionsService { @Consumes(MediaType.APPLICATION_FORM_URLENCODED) public Response updatePassword(final MultivaluedMap formData) { logger.debug("updatePassword"); - AccessCodeEntry accessCode = getAccessCodeEntry(RequiredAction.UPDATE_PASSWORD); + AccessCode accessCode = getAccessCodeEntry(RequiredAction.UPDATE_PASSWORD); if (accessCode == null) { logger.debug("updatePassword access code is null"); return unauthorized(); @@ -231,8 +232,8 @@ public class RequiredActionsService { @GET public Response emailVerification() { if (uriInfo.getQueryParameters().containsKey("key")) { - AccessCodeEntry accessCode = tokenManager.parseCode(uriInfo.getQueryParameters().getFirst("key"), session, realm); - if (accessCode == null || accessCode.isExpired() || !RequiredAction.VERIFY_EMAIL.equals(accessCode.getRequiredAction())) { + AccessCode accessCode = AccessCode.parse(uriInfo.getQueryParameters().getFirst("key"), session, realm); + if (accessCode == null || !accessCode.isValid(RequiredAction.VERIFY_EMAIL)) { return unauthorized(); } @@ -248,13 +249,12 @@ public class RequiredActionsService { return redirectOauth(user, accessCode); } else { - AccessCodeEntry accessCode = getAccessCodeEntry(RequiredAction.VERIFY_EMAIL); + AccessCode accessCode = getAccessCodeEntry(RequiredAction.VERIFY_EMAIL); if (accessCode == null) { return unauthorized(); } initAudit(accessCode); - //audit.clone().event(EventType.SEND_VERIFY_EMAIL).detail(Details.EMAIL, accessCode.getUser().getEmail()).success(); return Flows.forms(session, realm, uriInfo).setAccessCode(accessCode.getCode()).setUser(accessCode.getUser()) .createResponse(RequiredAction.VERIFY_EMAIL); @@ -265,8 +265,8 @@ public class RequiredActionsService { @GET public Response passwordReset() { if (uriInfo.getQueryParameters().containsKey("key")) { - AccessCodeEntry accessCode = tokenManager.parseCode(uriInfo.getQueryParameters().getFirst("key"), session, realm); - if (accessCode == null || accessCode.isExpired() || !RequiredAction.UPDATE_PASSWORD.equals(accessCode.getRequiredAction())) { + AccessCode accessCode = AccessCode.parse(uriInfo.getQueryParameters().getFirst("key"), session, realm); + if (accessCode == null || !accessCode.isValid(RequiredAction.UPDATE_PASSWORD)) { return unauthorized(); } @@ -317,7 +317,7 @@ public class RequiredActionsService { UserSessionModel userSession = session.sessions().createUserSession(realm, user, username, clientConnection.getRemoteAddr(), "form", false); audit.session(userSession); - AccessCodeEntry accessCode = tokenManager.createAccessCode(scopeParam, state, redirect, session, realm, client, user, userSession); + AccessCode accessCode = tokenManager.createAccessCode(scopeParam, state, redirect, session, realm, client, user, userSession); accessCode.setRequiredAction(RequiredAction.UPDATE_PASSWORD); try { @@ -339,38 +339,33 @@ public class RequiredActionsService { return Flows.forms(session, realm, uriInfo).setSuccess("emailSent").createPasswordReset(); } - private AccessCodeEntry getAccessCodeEntry(RequiredAction requiredAction) { + private AccessCode getAccessCodeEntry(RequiredAction requiredAction) { String code = uriInfo.getQueryParameters().getFirst(OAuth2Constants.CODE); if (code == null) { logger.debug("getAccessCodeEntry code as not in query param"); return null; } - AccessCodeEntry accessCodeEntry = tokenManager.parseCode(code, session, realm); - if (accessCodeEntry == null) { + AccessCode accessCode = AccessCode.parse(code, session, realm); + if (accessCode == null) { logger.debug("getAccessCodeEntry access code entry null"); return null; } - if (accessCodeEntry.isExpired()) { - logger.debugv("getAccessCodeEntry: access code id: {0}", accessCodeEntry.getCodeId()); - logger.debugv("getAccessCodeEntry access code entry expired"); + if (!accessCode.isValid(requiredAction)) { + logger.debugv("getAccessCodeEntry: access code id: {0}", accessCode.getCodeId()); + logger.debugv("getAccessCodeEntry access code not valid"); return null; } - if (!requiredAction.equals(accessCodeEntry.getRequiredAction())) { - logger.debugv("Invalid access code action: {0}", requiredAction); - return null; - } - - return accessCodeEntry; + return accessCode; } - private UserModel getUser(AccessCodeEntry accessCode) { + private UserModel getUser(AccessCode accessCode) { return session.users().getUserByUsername(accessCode.getUser().getUsername(), realm); } - private Response redirectOauth(UserModel user, AccessCodeEntry accessCode) { + private Response redirectOauth(UserModel user, AccessCode accessCode) { if (accessCode == null) { return null; } @@ -382,7 +377,7 @@ public class RequiredActionsService { .createResponse(requiredActions.iterator().next()); } else { logger.debugv("redirectOauth: redirecting to: {0}", accessCode.getRedirectUri()); - accessCode.setAction(null); + accessCode.setAction(ClientSessionModel.Action.CODE_TO_TOKEN); AuthenticationManager authManager = new AuthenticationManager(); @@ -400,7 +395,7 @@ public class RequiredActionsService { } } - private void initAudit(AccessCodeEntry accessCode) { + private void initAudit(AccessCode accessCode) { audit.event(EventType.LOGIN).client(accessCode.getClient()) .user(accessCode.getUser()) .session(accessCode.getSessionState()) diff --git a/services/src/main/java/org/keycloak/services/resources/TokenService.java b/services/src/main/java/org/keycloak/services/resources/TokenService.java index 917dd2d9aa..926cb39d6e 100755 --- a/services/src/main/java/org/keycloak/services/resources/TokenService.java +++ b/services/src/main/java/org/keycloak/services/resources/TokenService.java @@ -19,21 +19,20 @@ import org.keycloak.authentication.AuthenticationProviderManager; import org.keycloak.login.LoginFormsProvider; import org.keycloak.models.ApplicationModel; import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientSessionModel; import org.keycloak.models.Constants; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.RequiredCredentialModel; -import org.keycloak.models.RoleModel; import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; import org.keycloak.models.utils.KeycloakModelUtils; -import org.keycloak.representations.AccessCode; import org.keycloak.representations.AccessToken; import org.keycloak.representations.AccessTokenResponse; import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.services.ClientConnection; -import org.keycloak.services.managers.AccessCodeEntry; +import org.keycloak.services.managers.AccessCode; import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationManager.AuthenticationStatus; import org.keycloak.services.managers.ResourceAdminManager; @@ -43,6 +42,7 @@ import org.keycloak.services.resources.flows.Flows; import org.keycloak.services.resources.flows.OAuthFlows; import org.keycloak.services.resources.flows.Urls; import org.keycloak.services.validation.Validation; +import org.keycloak.util.Base64Url; import org.keycloak.util.BasicAuthHelper; import javax.ws.rs.Consumes; @@ -281,7 +281,6 @@ public class TokenService { String scope = form.getFirst(OAuth2Constants.SCOPE); UserSessionModel userSession = session.sessions().createUserSession(realm, user, username, clientConnection.getRemoteAddr(), "oauth_credentials", false); - userSession.associateClient(client); audit.session(userSession); AccessTokenResponse res = tokenManager.responseBuilder(realm, client, audit) @@ -623,10 +622,15 @@ public class TokenService { throw new BadRequestException("Code not specified", Response.status(Response.Status.BAD_REQUEST).entity(error).type("application/json").build()); } - - - AccessCodeEntry accessCode = tokenManager.parseCode(code, session, realm); + AccessCode accessCode = AccessCode.parse(code, session, realm); if (accessCode == null) { + String[] parts = code.split("\\."); + if (parts.length == 2) { + try { + audit.detail(Details.CODE_ID, new String(Base64Url.decode(parts[1]))); + } catch (Throwable t) { + } + } Map res = new HashMap(); res.put(OAuth2Constants.ERROR, "invalid_grant"); res.put(OAuth2Constants.ERROR_DESCRIPTION, "Code not found"); @@ -635,7 +639,7 @@ public class TokenService { .build(); } audit.detail(Details.CODE_ID, accessCode.getCodeId()); - if (accessCode.isExpired()) { + if (!accessCode.isValid(ClientSessionModel.Action.CODE_TO_TOKEN)) { Map res = new HashMap(); res.put(OAuth2Constants.ERROR, "invalid_grant"); res.put(OAuth2Constants.ERROR_DESCRIPTION, "Code is expired"); @@ -643,14 +647,8 @@ public class TokenService { return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(res) .build(); } - if (accessCode.getAction() != null) { - Map res = new HashMap(); - res.put(OAuth2Constants.ERROR, "invalid_grant"); - res.put(OAuth2Constants.ERROR_DESCRIPTION, "Code is not active"); - audit.error(Errors.INVALID_CODE); - return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(res) - .build(); - } + + accessCode.setAction(null); audit.user(accessCode.getUser()); audit.session(accessCode.getSessionState()); @@ -698,8 +696,6 @@ public class TokenService { logger.debug("accessRequest SUCCESS"); - userSession.associateClient(client); - AccessToken token = tokenManager.createClientAccessToken(accessCode.getRequestedRoles(), realm, client, user, userSession); try { @@ -982,22 +978,22 @@ public class TokenService { String code = formData.getFirst(OAuth2Constants.CODE); - AccessCodeEntry accessCodeEntry = tokenManager.parseCode(code, session, realm); - if (accessCodeEntry == null || !AccessCode.Action.OAUTH_GRANT.equals(accessCodeEntry.getAction())) { + AccessCode accessCode = AccessCode.parse(code, session, realm); + if (accessCode == null || !accessCode.isValid(ClientSessionModel.Action.OAUTH_GRANT)) { audit.error(Errors.INVALID_CODE); - return oauth.forwardToSecurityFailure("Unknown access code."); + return oauth.forwardToSecurityFailure("Invalid access code."); } - audit.detail(Details.CODE_ID, accessCodeEntry.getCodeId()); + audit.detail(Details.CODE_ID, accessCode.getCodeId()); - String redirect = accessCodeEntry.getRedirectUri(); - String state = accessCodeEntry.getState(); + String redirect = accessCode.getRedirectUri(); + String state = accessCode.getState(); - audit.client(accessCodeEntry.getClient()) - .user(accessCodeEntry.getUser()) + audit.client(accessCode.getClient()) + .user(accessCode.getUser()) .detail(Details.RESPONSE_TYPE, "code") .detail(Details.REDIRECT_URI, redirect); - UserSessionModel userSession = session.sessions().getUserSession(realm, accessCodeEntry.getSessionState()); + UserSessionModel userSession = session.sessions().getUserSession(realm, accessCode.getSessionState()); if (userSession != null) { audit.detail(Details.AUTH_METHOD, userSession.getAuthMethod()); audit.detail(Details.USERNAME, userSession.getLoginUsername()); @@ -1020,8 +1016,8 @@ public class TokenService { audit.success(); - accessCodeEntry.setAction(null); - return oauth.redirectAccessCode(accessCodeEntry, userSession, state, redirect); + accessCode.setAction(ClientSessionModel.Action.CODE_TO_TOKEN); + return oauth.redirectAccessCode(accessCode, userSession, state, redirect); } @Path("oauth/oob") diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java index 8c1cffa285..52f8436f9c 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java @@ -27,7 +27,7 @@ import org.keycloak.representations.idm.RoleRepresentation; import org.keycloak.representations.idm.SocialLinkRepresentation; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.idm.UserSessionRepresentation; -import org.keycloak.services.managers.AccessCodeEntry; +import org.keycloak.services.managers.AccessCode; import org.keycloak.services.managers.RealmManager; import org.keycloak.services.managers.ResourceAdminManager; import org.keycloak.services.managers.TokenManager; @@ -820,7 +820,7 @@ public class UsersResource { return Flows.errors().error("AccountProvider management not enabled", Response.Status.INTERNAL_SERVER_ERROR); } - AccessCodeEntry accessCode = tokenManager.createAccessCode(scope, state, redirect, session, realm, client, user, null); + AccessCode accessCode = tokenManager.createAccessCode(scope, state, redirect, session, realm, client, user, null); accessCode.setRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD); try { diff --git a/services/src/main/java/org/keycloak/services/resources/flows/OAuthFlows.java b/services/src/main/java/org/keycloak/services/resources/flows/OAuthFlows.java index b73c0fcb79..93cd4897b6 100755 --- a/services/src/main/java/org/keycloak/services/resources/flows/OAuthFlows.java +++ b/services/src/main/java/org/keycloak/services/resources/flows/OAuthFlows.java @@ -30,6 +30,7 @@ import org.keycloak.audit.Details; import org.keycloak.audit.EventType; import org.keycloak.models.ApplicationModel; import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientSessionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.RequiredCredentialModel; @@ -37,9 +38,8 @@ import org.keycloak.models.RoleModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserModel.RequiredAction; import org.keycloak.models.UserSessionModel; -import org.keycloak.representations.AccessCode; import org.keycloak.representations.idm.CredentialRepresentation; -import org.keycloak.services.managers.AccessCodeEntry; +import org.keycloak.services.managers.AccessCode; import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.TokenManager; @@ -82,7 +82,7 @@ public class OAuthFlows { this.tokenManager = tokenManager; } - public Response redirectAccessCode(AccessCodeEntry accessCode, UserSessionModel userSession, String state, String redirect) { + public Response redirectAccessCode(AccessCode accessCode, UserSessionModel userSession, String state, String redirect) { String code = accessCode.getCode(); UriBuilder redirectUri = UriBuilder.fromUri(redirect).queryParam(OAuth2Constants.CODE, code); log.debugv("redirectAccessCode: state: {0}", state); @@ -122,7 +122,7 @@ public class OAuthFlows { isEmailVerificationRequired(user); boolean isResource = client instanceof ApplicationModel; - AccessCodeEntry accessCode = tokenManager.createAccessCode(scopeParam, state, redirect, this.session, realm, client, user, session); + AccessCode accessCode = tokenManager.createAccessCode(scopeParam, state, redirect, this.session, realm, client, user, session); log.debugv("processAccessCode: isResource: {0}", isResource); log.debugv("processAccessCode: go to oauth page?: {0}", @@ -144,7 +144,7 @@ public class OAuthFlows { } if (!isResource) { - accessCode.setAction(AccessCode.Action.OAUTH_GRANT); + accessCode.setAction(ClientSessionModel.Action.OAUTH_GRANT); List realmRoles = new LinkedList(); MultivaluedMap resourceRoles = new MultivaluedMapImpl(); @@ -165,6 +165,8 @@ public class OAuthFlows { if (redirect != null) { audit.success(); + + accessCode.setAction(ClientSessionModel.Action.CODE_TO_TOKEN); return redirectAccessCode(accessCode, session, state, redirect); } else { return null; diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/AssertEvents.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/AssertEvents.java index c51e89e811..282823d0b3 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/AssertEvents.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/AssertEvents.java @@ -22,6 +22,7 @@ import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.services.managers.RealmManager; import org.keycloak.testsuite.rule.KeycloakRule; +import org.keycloak.util.Time; import java.util.HashMap; import java.util.HashSet; @@ -328,17 +329,7 @@ public class AssertEvents implements TestRule, AuditListenerFactory { } public static Matcher isCodeId() { - return new TypeSafeMatcher() { - @Override - protected boolean matchesSafely(String item) { - return (UUID.randomUUID().toString() + System.currentTimeMillis()).length() == item.length(); - } - - @Override - public void describeTo(Description description) { - description.appendText("Not an Code ID"); - } - }; + return isUUID(); } public static Matcher isUUID() { diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java index 4d7c211c87..3a3f6c84ee 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java @@ -38,8 +38,11 @@ import org.keycloak.RSATokenVerifier; import org.keycloak.VerificationException; import org.keycloak.jose.jws.JWSInput; import org.keycloak.jose.jws.crypto.RSAProvider; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; import org.keycloak.representations.AccessToken; import org.keycloak.representations.RefreshToken; +import org.keycloak.services.managers.AccessCode; import org.keycloak.services.resources.TokenService; import org.keycloak.util.BasicAuthHelper; import org.keycloak.util.PemUtils; @@ -217,12 +220,6 @@ public class OAuthClient { } } - public void verifyCode(String code) { - if (!RSAProvider.verify(new JWSInput(code), realmPublicKey)) { - throw new RuntimeException("Failed to verify code"); - } - } - public RefreshToken verifyRefreshToken(String refreshToken) { try { JWSInput jws = new JWSInput(refreshToken); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java index c089781306..d7a29bed9b 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java @@ -5,6 +5,7 @@ import org.junit.Before; import org.junit.ClassRule; import org.junit.Test; import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientSessionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; @@ -13,7 +14,11 @@ import org.keycloak.testsuite.rule.KeycloakRule; import org.keycloak.util.Time; import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedList; import java.util.List; +import java.util.Set; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; @@ -59,7 +64,47 @@ public class UserSessionProviderTest { assertSession(session.sessions().getUserSession(realm, sessions[0].getId()), session.users().getUserByUsername("user1", realm), "127.0.0.1", started, started, "test-app", "third-party"); assertSession(session.sessions().getUserSession(realm, sessions[1].getId()), session.users().getUserByUsername("user1", realm), "127.0.0.2", started, started, "test-app"); - assertSession(session.sessions().getUserSession(realm, sessions[2].getId()), session.users().getUserByUsername("user2", realm), "127.0.0.3", started, started); + assertSession(session.sessions().getUserSession(realm, sessions[2].getId()), session.users().getUserByUsername("user2", realm), "127.0.0.3", started, started, "test-app"); + } + + @Test + public void testCreateClientSession() { + UserSessionModel[] sessions = createSessions(); + + List clientSessions = sessions[0].getClientSessions(); + assertEquals(2, clientSessions.size()); + ClientSessionModel session = clientSessions.get(0); + + assertEquals(null, session.getAction()); + assertEquals(realm.findClient("test-app").getClientId(), session.getClient().getClientId()); + assertEquals(sessions[0].getId(), session.getUserSession().getId()); + assertEquals("http://redirect", session.getRedirectUri()); + assertEquals("state", session.getState()); + assertEquals(2, session.getRoles().size()); + assertTrue(session.getRoles().contains("one")); + assertTrue(session.getRoles().contains("two")); + } + + @Test + public void testUpdateClientSession() { + UserSessionModel[] sessions = createSessions(); + + String id = sessions[0].getClientSessions().get(0).getId(); + + ClientSessionModel clientSession = session.sessions().getClientSession(realm, id); + + int time = clientSession.getTimestamp(); + assertEquals(null, clientSession.getAction()); + + clientSession.setAction(ClientSessionModel.Action.CODE_TO_TOKEN); + clientSession.setTimestamp(time + 10); + + kc.stopSession(session, true); + session = kc.startSession(); + + ClientSessionModel updated = session.sessions().getClientSession(realm, id); + assertEquals(ClientSessionModel.Action.CODE_TO_TOKEN, updated.getAction()); + assertEquals(time + 10, updated.getTimestamp()); } @Test @@ -72,22 +117,108 @@ public class UserSessionProviderTest { @Test public void testRemoveUserSessionsByUser() { - createSessions(); + UserSessionModel[] sessions = createSessions(); + + List clientSessionsRemoved = new LinkedList(); + List clientSessionsKept = new LinkedList(); + for (UserSessionModel s : sessions) { + for (ClientSessionModel c : s.getClientSessions()) { + if (c.getUserSession().getUser().getUsername().equals("user1")) { + clientSessionsRemoved.add(c.getId()); + } else { + clientSessionsKept.add(c.getId()); + } + } + } + session.sessions().removeUserSessions(realm, session.users().getUserByUsername("user1", realm)); resetSession(); assertTrue(session.sessions().getUserSessions(realm, session.users().getUserByUsername("user1", realm)).isEmpty()); assertFalse(session.sessions().getUserSessions(realm, session.users().getUserByUsername("user2", realm)).isEmpty()); + + for (String c : clientSessionsRemoved) { + assertNull(session.sessions().getClientSession(realm, c)); + } + for (String c : clientSessionsKept) { + assertNotNull(session.sessions().getClientSession(realm, c)); + } + } + + @Test + public void testRemoveUserSession() { + UserSessionModel userSession = createSessions()[0]; + + List clientSessionsRemoved = new LinkedList(); + for (ClientSessionModel c : userSession.getClientSessions()) { + clientSessionsRemoved.add(c.getId()); + } + + session.sessions().removeUserSession(realm, userSession); + resetSession(); + + assertNull(session.sessions().getUserSession(realm, userSession.getId())); + for (String c : clientSessionsRemoved) { + assertNull(session.sessions().getClientSession(realm, c)); + } } @Test public void testRemoveUserSessionsByRealm() { - createSessions(); + UserSessionModel[] sessions = createSessions(); + + List clientSessions = new LinkedList(); + for (UserSessionModel s : sessions) { + clientSessions.addAll(s.getClientSessions()); + } + session.sessions().removeUserSessions(realm); resetSession(); assertTrue(session.sessions().getUserSessions(realm, session.users().getUserByUsername("user1", realm)).isEmpty()); assertTrue(session.sessions().getUserSessions(realm, session.users().getUserByUsername("user2", realm)).isEmpty()); + + for (ClientSessionModel c : clientSessions) { + assertNull(session.sessions().getClientSession(realm, c.getId())); + } + } + + @Test + public void testOnClientRemoved() { + UserSessionModel[] sessions = createSessions(); + + List clientSessionsRemoved = new LinkedList(); + List clientSessionsKept = new LinkedList(); + for (UserSessionModel s : sessions) { + s = session.sessions().getUserSession(realm, s.getId()); + for (ClientSessionModel c : s.getClientSessions()) { + if (c.getClient().getClientId().equals("third-party")) { + clientSessionsRemoved.add(c.getId()); + } else { + clientSessionsKept.add(c.getId()); + } + } + } + + session.sessions().onClientRemoved(realm, realm.findClient("third-party")); + resetSession(); + + for (String c : clientSessionsRemoved) { + assertNull(session.sessions().getClientSession(realm, c)); + } + for (String c : clientSessionsKept) { + assertNotNull(session.sessions().getClientSession(realm, c)); + } + + session.sessions().onClientRemoved(realm, realm.findClient("test-app")); + resetSession(); + + for (String c : clientSessionsRemoved) { + assertNull(session.sessions().getClientSession(realm, c)); + } + for (String c : clientSessionsKept) { + assertNull(session.sessions().getClientSession(realm, c)); + } } @Test @@ -111,7 +242,7 @@ public class UserSessionProviderTest { public void testGetByClient() { UserSessionModel[] sessions = createSessions(); - assertSessions(session.sessions().getUserSessions(realm, realm.findClient("test-app")), sessions[0], sessions[1]); + assertSessions(session.sessions().getUserSessions(realm, realm.findClient("test-app")), sessions[0], sessions[1], sessions[2]); assertSessions(session.sessions().getUserSessions(realm, realm.findClient("third-party")), sessions[0]); } @@ -120,7 +251,7 @@ public class UserSessionProviderTest { for (int i = 0; i < 25; i++) { UserSessionModel userSession = session.sessions().createUserSession(realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0." + i, "form", false); userSession.setStarted(Time.currentTime() + i); - userSession.associateClient(realm.findClient("test-app")); + session.sessions().createClientSession(realm, realm.findClient("test-app"), userSession, "http://redirect", "state", new HashSet()); } resetSession(); @@ -147,26 +278,30 @@ public class UserSessionProviderTest { assertArrayEquals(expectedIps, actualIps); } - - @Test public void testGetCountByClient() { createSessions(); - assertEquals(2, session.sessions().getActiveUserSessions(realm, realm.findClient("test-app"))); + assertEquals(3, session.sessions().getActiveUserSessions(realm, realm.findClient("test-app"))); assertEquals(1, session.sessions().getActiveUserSessions(realm, realm.findClient("third-party"))); } private UserSessionModel[] createSessions() { - UserSessionModel[] sessions = new UserSessionModel[4]; + UserSessionModel[] sessions = new UserSessionModel[3]; sessions[0] = session.sessions().createUserSession(realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.1", "form", true); - sessions[0].associateClient(realm.findClient("test-app")); - sessions[0].associateClient(realm.findClient("third-party")); + + Set roles = new HashSet(); + roles.add("one"); + roles.add("two"); + + session.sessions().createClientSession(realm, realm.findClient("test-app"), sessions[0], "http://redirect", "state", roles); + session.sessions().createClientSession(realm, realm.findClient("third-party"), sessions[0], "http://redirect", "state", new HashSet()); sessions[1] = session.sessions().createUserSession(realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.2", "form", true); - sessions[1].associateClient(realm.findClient("test-app")); + session.sessions().createClientSession(realm, realm.findClient("test-app"), sessions[1], "http://redirect", "state", new HashSet()); sessions[2] = session.sessions().createUserSession(realm, session.users().getUserByUsername("user2", realm), "user2", "127.0.0.3", "form", true); + session.sessions().createClientSession(realm, realm.findClient("test-app"), sessions[2], "http://redirect", "state", new HashSet()); resetSession(); @@ -205,9 +340,9 @@ public class UserSessionProviderTest { assertTrue(session.getStarted() >= started - 1 && session.getStarted() <= started + 1); assertTrue(session.getLastSessionRefresh() >= lastRefresh - 1 && session.getLastSessionRefresh() <= lastRefresh + 1); - String[] actualClients = new String[session.getClientAssociations().size()]; + String[] actualClients = new String[session.getClientSessions().size()]; for (int i = 0; i < actualClients.length; i++) { - actualClients[i] = session.getClientAssociations().get(i).getClientId(); + actualClients[i] = session.getClientSessions().get(i).getClient().getClientId(); } Arrays.sort(clients); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java index b47a84bdc0..ba7c41d218 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java @@ -29,7 +29,9 @@ import org.keycloak.OAuth2Constants; import org.keycloak.audit.Details; import org.keycloak.audit.Errors; import org.keycloak.audit.Event; +import org.keycloak.audit.EventType; import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; import org.keycloak.representations.AccessToken; import org.keycloak.services.managers.RealmManager; import org.keycloak.testsuite.AssertEvents; @@ -45,6 +47,7 @@ import static org.hamcrest.Matchers.allOf; import static org.hamcrest.Matchers.greaterThanOrEqualTo; import static org.hamcrest.Matchers.lessThanOrEqualTo; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; /** @@ -72,13 +75,6 @@ public class AccessTokenTest { @Test public void accessTokenRequest() throws Exception { - keycloakRule.update(new KeycloakRule.KeycloakSetup() { - @Override - public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { - appRealm.setAccessCodeLifespan(1); - } - }); - oauth.doLogin("test-user@localhost", "password"); Event loginEvent = events.expectLogin().assertEvent(); @@ -112,22 +108,6 @@ public class AccessTokenTest { Assert.assertEquals(token.getId(), event.getDetails().get(Details.TOKEN_ID)); Assert.assertEquals(oauth.verifyRefreshToken(response.getRefreshToken()).getId(), event.getDetails().get(Details.REFRESH_TOKEN_ID)); Assert.assertEquals(sessionId, token.getSessionState()); - - Thread.sleep(2000); - response = oauth.doAccessTokenRequest(code, "password"); - Assert.assertEquals(400, response.getStatusCode()); - - AssertEvents.ExpectedEvent expectedEvent = events.expectCodeToToken(codeId, null); - expectedEvent.error("invalid_code").removeDetail(Details.TOKEN_ID).removeDetail(Details.REFRESH_TOKEN_ID).client((String) null).user((String) null); - expectedEvent.assertEvent(); - - keycloakRule.update(new KeycloakRule.KeycloakSetup() { - @Override - public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { - appRealm.setAccessCodeLifespan(60); - } - }); - } @Test @@ -162,10 +142,109 @@ public class AccessTokenTest { assertNull(tokenResponse.getAccessToken()); assertNull(tokenResponse.getRefreshToken()); - events.expectCodeToToken(codeId, sessionId).removeDetail(Details.TOKEN_ID).removeDetail(Details.REFRESH_TOKEN_ID).error(Errors.INVALID_CODE).assertEvent(); + events.expectCodeToToken(codeId, sessionId).removeDetail(Details.TOKEN_ID).client((String) null).user((String) null).session((String) null).removeDetail(Details.REFRESH_TOKEN_ID).error(Errors.INVALID_CODE).assertEvent(); events.clear(); } + @Test + public void accessTokenCodeExpired() { + keycloakRule.update(new KeycloakRule.KeycloakSetup() { + @Override + public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { + appRealm.setAccessCodeLifespan(1); + } + }); + + oauth.doLogin("test-user@localhost", "password"); + + Event loginEvent = events.expectLogin().assertEvent(); + + String codeId = loginEvent.getDetails().get(Details.CODE_ID); + loginEvent.getSessionId(); + + String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); + + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + } + + OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, "password"); + Assert.assertEquals(400, response.getStatusCode()); + + AssertEvents.ExpectedEvent expectedEvent = events.expectCodeToToken(codeId, null); + expectedEvent.error("invalid_code").removeDetail(Details.TOKEN_ID).removeDetail(Details.REFRESH_TOKEN_ID).client((String) null).user((String) null); + expectedEvent.assertEvent(); + + events.clear(); + + keycloakRule.update(new KeycloakRule.KeycloakSetup() { + @Override + public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { + appRealm.setAccessCodeLifespan(60); + } + }); + } + + @Test + public void accessTokenCodeUsed() { + oauth.doLogin("test-user@localhost", "password"); + + Event loginEvent = events.expectLogin().assertEvent(); + + String codeId = loginEvent.getDetails().get(Details.CODE_ID); + loginEvent.getSessionId(); + + String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); + OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, "password"); + Assert.assertEquals(200, response.getStatusCode()); + + events.clear(); + + response = oauth.doAccessTokenRequest(code, "password"); + Assert.assertEquals(400, response.getStatusCode()); + + AssertEvents.ExpectedEvent expectedEvent = events.expectCodeToToken(codeId, null); + expectedEvent.error("invalid_code").removeDetail(Details.TOKEN_ID).removeDetail(Details.REFRESH_TOKEN_ID).client((String) null).user((String) null); + expectedEvent.assertEvent(); + + events.clear(); + + keycloakRule.update(new KeycloakRule.KeycloakSetup() { + @Override + public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { + appRealm.setAccessCodeLifespan(60); + } + }); + } + + @Test + public void accessTokenCodeHasRequiredAction() { + keycloakRule.configure(new KeycloakRule.KeycloakSetup() { + @Override + public void config(RealmManager manager, RealmModel defaultRealm, RealmModel appRealm) { + UserModel user = manager.getSession().users().getUserByUsername("test-user@localhost", appRealm); + user.addRequiredAction(UserModel.RequiredAction.UPDATE_PROFILE); + } + }); + + oauth.doLogin("test-user@localhost", "password"); + + String code = driver.getPageSource().split("code=")[1].split("&")[0].split("\"")[0]; + + OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, "password"); + Assert.assertEquals(400, response.getStatusCode()); + + Event event = events.poll(); + assertNotNull(event.getDetails().get(Details.CODE_ID)); + + keycloakRule.update(new KeycloakRule.KeycloakSetup() { + @Override + public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { + manager.getSession().users().getUserByUsername("test-user@localhost", appRealm).removeRequiredAction(UserModel.RequiredAction.UPDATE_PROFILE); + } + }); + } } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AuthorizationCodeTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AuthorizationCodeTest.java index 41f11f0c3d..98c2270bf8 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AuthorizationCodeTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AuthorizationCodeTest.java @@ -23,6 +23,7 @@ package org.keycloak.testsuite.oauth; import org.junit.Assert; import org.junit.ClassRule; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.keycloak.OAuth2Constants; @@ -30,7 +31,7 @@ import org.keycloak.audit.Details; import org.keycloak.jose.jws.JWSInput; import org.keycloak.models.Constants; import org.keycloak.models.RealmModel; -import org.keycloak.representations.AccessCode; +import org.keycloak.services.managers.AccessCode; import org.keycloak.services.managers.RealmManager; import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.OAuthClient; @@ -44,6 +45,8 @@ import org.openqa.selenium.WebDriver; import java.io.IOException; +import static org.junit.Assert.assertEquals; + /** * @author Stian Thorgersen */ @@ -75,14 +78,13 @@ public class AuthorizationCodeTest { Assert.assertTrue(response.isRedirected()); Assert.assertNotNull(response.getCode()); - Assert.assertEquals("mystate", response.getState()); + assertEquals("mystate", response.getState()); Assert.assertNull(response.getError()); - oauth.verifyCode(response.getCode()); + keycloakRule.verifyCode(response.getCode()); String codeId = events.expectLogin().assertEvent().getDetails().get(Details.CODE_ID); - AccessCode accessCode = new JWSInput(response.getCode()).readJsonContent(AccessCode.class); - Assert.assertEquals(codeId,accessCode.getId()); + assertCode(codeId, response.getCode()); } @Test @@ -101,11 +103,10 @@ public class AuthorizationCodeTest { Assert.assertTrue(title.startsWith("Success code=")); String code = driver.findElement(By.id(OAuth2Constants.CODE)).getText(); - oauth.verifyCode(code); + keycloakRule.verifyCode(code); String codeId = events.expectLogin().detail(Details.REDIRECT_URI, Constants.INSTALLED_APP_URN).assertEvent().getDetails().get(Details.CODE_ID); - AccessCode accessCode = new JWSInput(code).readJsonContent(AccessCode.class); - Assert.assertEquals(codeId,accessCode.getId()); + assertCode(codeId, code); keycloakRule.update(new KeycloakRule.KeycloakSetup() { @Override @@ -132,7 +133,7 @@ public class AuthorizationCodeTest { Assert.assertTrue(title.equals("Error error=access_denied")); String error = driver.findElement(By.id(OAuth2Constants.ERROR)).getText(); - Assert.assertEquals("access_denied", error); + assertEquals("access_denied", error); events.expectLogin().error("rejected_by_user").user((String) null).session((String) null).removeDetail(Details.USERNAME).removeDetail(Details.CODE_ID).detail(Details.REDIRECT_URI, Constants.INSTALLED_APP_URN).assertEvent().getDetails().get(Details.CODE_ID); @@ -160,11 +161,10 @@ public class AuthorizationCodeTest { Assert.assertTrue(response.isRedirected()); Assert.assertNotNull(response.getCode()); - oauth.verifyCode(response.getCode()); + keycloakRule.verifyCode(response.getCode()); String codeId = events.expectLogin().assertEvent().getDetails().get(Details.CODE_ID); - AccessCode accessCode = new JWSInput(response.getCode()).readJsonContent(AccessCode.class); - Assert.assertEquals(codeId,accessCode.getId()); + assertCode(codeId, response.getCode()); } @Test @@ -176,11 +176,15 @@ public class AuthorizationCodeTest { Assert.assertNull(response.getState()); Assert.assertNull(response.getError()); - oauth.verifyCode(response.getCode()); + keycloakRule.verifyCode(response.getCode()); String codeId = events.expectLogin().assertEvent().getDetails().get(Details.CODE_ID); - AccessCode accessCode = new JWSInput(response.getCode()).readJsonContent(AccessCode.class); - Assert.assertEquals(codeId,accessCode.getId()); + assertCode(codeId, response.getCode()); + } + + private void assertCode(String expectedCodeId, String actualCode) { + AccessCode code = keycloakRule.verifyCode(actualCode); + assertEquals(expectedCodeId, code.getCodeId()); } } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/KeycloakRule.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/KeycloakRule.java index 1e30a4b028..f108a631b8 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/KeycloakRule.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/KeycloakRule.java @@ -21,10 +21,12 @@ */ package org.keycloak.testsuite.rule; +import org.junit.Assert; import org.keycloak.Config; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserSessionModel; +import org.keycloak.services.managers.AccessCode; import org.keycloak.services.managers.RealmManager; import org.keycloak.testsuite.ApplicationServlet; @@ -107,6 +109,24 @@ public class KeycloakRule extends AbstractKeycloakRule { stopSession(session, true); } + public AccessCode verifyCode(String code) { + KeycloakSession session = startSession(); + try { + RealmModel realm = session.realms().getRealm("test"); + try { + AccessCode accessCode = AccessCode.parse(code, session, realm); + if (accessCode == null) { + Assert.fail("Invalid code"); + } + return accessCode; + } catch (Throwable t) { + throw new AssertionError("Failed to parse code", t); + } + } finally { + stopSession(session, false); + } + } + public abstract static class KeycloakSetup { protected KeycloakSession session;