From 524b4237333ba468807b2f124dc1a3b093e2c272 Mon Sep 17 00:00:00 2001 From: Bill Burke Date: Fri, 26 Sep 2014 17:44:33 -0400 Subject: [PATCH] ClientSession everywhere refactor phase1 --- .../main/java/org/keycloak/events/Errors.java | 2 + .../java/org/keycloak/events/EventType.java | 4 +- .../keycloak/login/LoginFormsProvider.java | 2 + .../FreeMarkerLoginFormsProvider.java | 13 +- .../keycloak/models/ClientSessionModel.java | 11 +- .../keycloak/models/UserSessionProvider.java | 3 +- .../sessions/jpa/ClientSessionAdapter.java | 37 +- .../sessions/jpa/JpaUserSessionProvider.java | 45 +- .../jpa/entities/ClientSessionEntity.java | 29 +- .../jpa/entities/ClientSessionNoteEntity.java | 7 +- .../jpa/entities/ClientSessionRoleEntity.java | 7 +- .../sessions/mem/ClientSessionAdapter.java | 30 +- .../sessions/mem/MemUserSessionProvider.java | 40 +- .../sessions/mem/UserSessionAdapter.java | 4 + .../mem/entities/ClientSessionEntity.java | 18 +- .../sessions/mongo/ClientSessionAdapter.java | 37 +- .../mongo/MongoUserSessionProvider.java | 86 ++-- .../sessions/mongo/UserSessionAdapter.java | 6 +- .../entities/MongoClientSessionEntity.java | 37 +- .../entities/MongoUserSessionEntity.java | 13 +- .../services/managers/AccessCode.java | 2 +- .../org/keycloak/services/managers/Auth.java | 10 + .../services/managers/ClientSessionCode.java | 173 +++++++ .../services/managers/TokenManager.java | 26 +- .../protocol/OpenIdConnectProtocol.java | 17 + .../services/resources/AccountService.java | 18 +- .../resources/RequiredActionsService.java | 307 ++++++++----- .../services/resources/SocialResource.java | 207 ++++----- .../services/resources/TokenService.java | 425 +++++++++++------- .../resources/admin/UsersResource.java | 17 +- .../services/resources/flows/OAuthFlows.java | 78 +++- .../resources/flows/SocialRedirectFlows.java | 35 +- social/core/pom.xml | 6 + .../social/AbstractOAuth2Provider.java | 5 +- .../org/keycloak/social/AuthCallback.java | 7 +- .../java/org/keycloak/social/AuthRequest.java | 21 +- .../org/keycloak/social/SocialProvider.java | 6 +- social/facebook/pom.xml | 6 + social/github/pom.xml | 6 + social/google/pom.xml | 6 + social/twitter/pom.xml | 6 + .../social/twitter/TwitterProvider.java | 12 +- .../org/keycloak/testsuite/AssertEvents.java | 14 + .../org/keycloak/testsuite/DummySocial.java | 5 +- .../org/keycloak/testsuite/OAuthClient.java | 2 +- .../testsuite/account/AccountTest.java | 12 + .../RequiredActionEmailVerificationTest.java | 4 +- .../RequiredActionResetPasswordTest.java | 7 + .../testsuite/forms/LoginTotpTest.java | 13 +- .../model/UserSessionProviderTest.java | 26 +- .../testsuite/oauth/AccessTokenTest.java | 3 +- .../oauth/AuthorizationCodeTest.java | 39 +- .../testsuite/perf/AccessTokenPerfTest.java | 27 +- .../keycloak/testsuite/rule/KeycloakRule.java | 5 +- .../testsuite/social/SocialLoginTest.java | 13 +- 55 files changed, 1330 insertions(+), 667 deletions(-) create mode 100755 services/src/main/java/org/keycloak/services/managers/ClientSessionCode.java create mode 100755 services/src/main/java/org/keycloak/services/protocol/OpenIdConnectProtocol.java mode change 100644 => 100755 social/core/src/main/java/org/keycloak/social/AuthCallback.java mode change 100644 => 100755 social/core/src/main/java/org/keycloak/social/AuthRequest.java mode change 100644 => 100755 social/core/src/main/java/org/keycloak/social/SocialProvider.java diff --git a/events/api/src/main/java/org/keycloak/events/Errors.java b/events/api/src/main/java/org/keycloak/events/Errors.java index a6932ec0ad..e7d7e1a203 100755 --- a/events/api/src/main/java/org/keycloak/events/Errors.java +++ b/events/api/src/main/java/org/keycloak/events/Errors.java @@ -35,6 +35,8 @@ public interface Errors { String SOCIAL_PROVIDER_NOT_FOUND = "social_provider_not_found"; String SOCIAL_ID_IN_USE = "social_id_in_use"; + String STATE_PARAM_NOT_FOUND = "state_param_not_found"; + String SSL_REQUIRED = "ssl_required"; String USER_NOT_LOGGED_IN = "user_not_logged_in"; String USER_SESSION_NOT_FOUND = "user_session_not_found"; diff --git a/events/api/src/main/java/org/keycloak/events/EventType.java b/events/api/src/main/java/org/keycloak/events/EventType.java index 1e7a2c0730..917b772aba 100755 --- a/events/api/src/main/java/org/keycloak/events/EventType.java +++ b/events/api/src/main/java/org/keycloak/events/EventType.java @@ -39,6 +39,8 @@ public enum EventType { SEND_VERIFY_EMAIL, SEND_VERIFY_EMAIL_ERROR, SEND_RESET_PASSWORD, - SEND_RESET_PASSWORD_ERROR + SEND_RESET_PASSWORD_ERROR, + SOCIAL_LOGIN, + SOCIAL_LOGIN_ERROR } diff --git a/forms/login-api/src/main/java/org/keycloak/login/LoginFormsProvider.java b/forms/login-api/src/main/java/org/keycloak/login/LoginFormsProvider.java index 1e0fb4d243..1aede52b32 100755 --- a/forms/login-api/src/main/java/org/keycloak/login/LoginFormsProvider.java +++ b/forms/login-api/src/main/java/org/keycloak/login/LoginFormsProvider.java @@ -50,6 +50,8 @@ public interface LoginFormsProvider extends Provider { public LoginFormsProvider setClient(ClientModel client); + LoginFormsProvider setVerifyCode(String code); + public LoginFormsProvider setQueryParams(MultivaluedMap queryParams); public LoginFormsProvider setFormData(MultivaluedMap formData); diff --git a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java index 34168cda67..343c3c194e 100755 --- a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java +++ b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java @@ -1,6 +1,7 @@ package org.keycloak.login.freemarker; import org.jboss.logging.Logger; +import org.jboss.resteasy.specimpl.MultivaluedMapImpl; import org.keycloak.OAuth2Constants; import org.keycloak.email.EmailException; import org.keycloak.email.EmailProvider; @@ -50,6 +51,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { private static final Logger logger = Logger.getLogger(FreeMarkerLoginFormsProvider.class); + private String verifyCode; private String message; private String accessCode; private Response.Status status = Response.Status.OK; @@ -108,7 +110,8 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { case VERIFY_EMAIL: try { UriBuilder builder = Urls.loginActionEmailVerificationBuilder(uriInfo.getBaseUri()); - builder.queryParam("key", accessCode); + builder.queryParam("code", accessCode); + builder.queryParam("key", verifyCode); String link = builder.build(realm.getName()).toString(); long expiration = TimeUnit.SECONDS.toMinutes(realm.getAccessCodeLifespanUserAction()); @@ -134,7 +137,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { } private Response createResponse(LoginFormsPages page) { - MultivaluedMap queryParameterMap = queryParams != null ? queryParams : uriInfo.getQueryParameters(); + MultivaluedMap queryParameterMap = queryParams != null ? queryParams : new MultivaluedMapImpl(); String requestURI = uriInfo.getBaseUri().getPath(); UriBuilder uriBuilder = UriBuilder.fromUri(requestURI); @@ -308,6 +311,12 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { return this; } + @Override + public LoginFormsProvider setVerifyCode(String code) { + this.verifyCode = code; + return this; + } + @Override public LoginFormsProvider setQueryParams(MultivaluedMap queryParams) { this.queryParams = queryParams; diff --git a/model/api/src/main/java/org/keycloak/models/ClientSessionModel.java b/model/api/src/main/java/org/keycloak/models/ClientSessionModel.java index 7ecd68e21d..2c0f9dd8f6 100755 --- a/model/api/src/main/java/org/keycloak/models/ClientSessionModel.java +++ b/model/api/src/main/java/org/keycloak/models/ClientSessionModel.java @@ -8,14 +8,14 @@ import java.util.Set; public interface ClientSessionModel { public String getId(); - + public RealmModel getRealm(); public ClientModel getClient(); - public String getState(); - public UserSessionModel getUserSession(); + public void setUserSession(UserSessionModel userSession); public String getRedirectUri(); + public void setRedirectUri(String uri); public int getTimestamp(); @@ -26,6 +26,7 @@ public interface ClientSessionModel { public void setAction(Action action); public Set getRoles(); + public void setRoles(Set roles); /** * Authentication request type, i.e. OAUTH, SAML 2.0, SAML 1.1, etc. @@ -45,7 +46,9 @@ public interface ClientSessionModel { VERIFY_EMAIL, UPDATE_PROFILE, CONFIGURE_TOTP, - UPDATE_PASSWORD + UPDATE_PASSWORD, + AUTHENTICATE, + SOCIAL_CALLBACK } } 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 d2c66d0e49..466717d05f 100755 --- a/model/api/src/main/java/org/keycloak/models/UserSessionProvider.java +++ b/model/api/src/main/java/org/keycloak/models/UserSessionProvider.java @@ -11,8 +11,9 @@ import java.util.Set; */ public interface UserSessionProvider extends Provider { - ClientSessionModel createClientSession(RealmModel realm, ClientModel client, UserSessionModel userSession, String redirectUri, String state, Set roles); + ClientSessionModel createClientSession(RealmModel realm, ClientModel client); ClientSessionModel getClientSession(RealmModel realm, String id); + ClientSessionModel getClientSession(String id); UserSessionModel createUserSession(RealmModel realm, UserModel user, String loginUsername, String ipAddress, String authMethod, boolean rememberMe); UserSessionModel getUserSession(RealmModel realm, String id); 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 index f1f5ea49fc..8dd976392d 100755 --- 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 @@ -8,10 +8,13 @@ import org.keycloak.models.UserSessionModel; import org.keycloak.models.sessions.jpa.entities.ClientSessionEntity; import org.keycloak.models.sessions.jpa.entities.ClientSessionNoteEntity; import org.keycloak.models.sessions.jpa.entities.ClientSessionRoleEntity; +import org.keycloak.models.sessions.jpa.entities.UserSessionEntity; import javax.persistence.EntityManager; import java.util.HashSet; import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; import java.util.Set; /** @@ -31,6 +34,11 @@ public class ClientSessionAdapter implements ClientSessionModel { this.entity = entity; } + @Override + public RealmModel getRealm() { + return session.realms().getRealm(entity.getRealmId()); + } + @Override public void setNote(String name, String value) { for (ClientSessionNoteEntity attr : entity.getNotes()) { @@ -80,8 +88,32 @@ public class ClientSessionAdapter implements ClientSessionModel { } @Override - public String getState() { - return entity.getState(); + public void setUserSession(UserSessionModel userSession) { + UserSessionAdapter adapter = (UserSessionAdapter)userSession; + UserSessionEntity userSessionEntity = adapter.getEntity(); + entity.setSession(userSessionEntity); + userSessionEntity.getClientSessions().add(entity); + } + + @Override + public void setRedirectUri(String uri) { + entity.setRedirectUri(uri); + } + + @Override + public void setRoles(Set roles) { + 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); + } } @Override @@ -96,6 +128,7 @@ public class ClientSessionAdapter implements ClientSessionModel { @Override public UserSessionModel getUserSession() { + if (entity.getSession() == null) return null; return new UserSessionAdapter(session, em, realm, entity.getSession()); } 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 5e04193c31..335a48f770 100755 --- 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 @@ -37,33 +37,14 @@ public class JpaUserSessionProvider implements UserSessionProvider { } @Override - public ClientSessionModel createClientSession(RealmModel realm, ClientModel client, UserSessionModel userSession, String redirectUri, String state, Set roles) { - UserSessionEntity userSessionEntity = em.find(UserSessionEntity.class, userSession.getId()); - + public ClientSessionModel createClientSession(RealmModel realm, ClientModel client) { 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.setRealmId(realm.getId()); 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); } @@ -76,6 +57,16 @@ public class JpaUserSessionProvider implements UserSessionProvider { return null; } + @Override + public ClientSessionModel getClientSession(String id) { + ClientSessionEntity clientSession = em.find(ClientSessionEntity.class, id); + if (clientSession != null) { + RealmModel realm = session.realms().getRealm(clientSession.getRealmId()); + return new ClientSessionAdapter(session, em, realm, clientSession); + } + return null; + } + @Override public UsernameLoginFailureModel getUserLoginFailure(RealmModel realm, String username) { String id = username + "-" + realm; @@ -208,6 +199,18 @@ public class JpaUserSessionProvider implements UserSessionProvider { int maxTime = Time.currentTime() - realm.getSsoSessionMaxLifespan(); int idleTime = Time.currentTime() - realm.getSsoSessionIdleTimeout(); + em.createNamedQuery("removeDetachedClientSessionRoleByExpired") + .setParameter("realmId", realm.getId()) + .setParameter("maxTime", idleTime) + .executeUpdate(); + em.createNamedQuery("removeDetachedClientSessionNoteByExpired") + .setParameter("realmId", realm.getId()) + .setParameter("maxTime", idleTime) + .executeUpdate(); + em.createNamedQuery("removeDetachedClientSessionByExpired") + .setParameter("realmId", realm.getId()) + .setParameter("maxTime", idleTime) + .executeUpdate(); em.createNamedQuery("removeClientSessionRoleByExpired") .setParameter("realmId", realm.getId()) .setParameter("maxTime", maxTime) 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 index 8415499645..c4bbe06cdf 100755 --- 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 @@ -23,10 +23,11 @@ import java.util.Collection; @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 = "removeClientSessionByRealm", query = "delete from ClientSessionEntity a where a.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))") + @NamedQuery(name = "removeClientSessionByClient", query = "delete from ClientSessionEntity a where a.clientId = :clientId and a.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))"), + @NamedQuery(name = "removeDetachedClientSessionByExpired", query = "delete from ClientSessionEntity a where a.session IS NULL and a.timestamp < :maxTime and a.realmId = :realmId") }) public class ClientSessionEntity { @@ -41,6 +42,9 @@ public class ClientSessionEntity { @Column(name="CLIENT_ID",length = 36) protected String clientId; + @Column(name="REALM_ID") + protected String realmId; + @Column(name="TIMESTAMP") protected int timestamp; @@ -50,9 +54,6 @@ public class ClientSessionEntity { @Column(name="AUTH_METHOD") protected String authMethod; - @Column(name="STATE") - protected String state; - @Column(name="ACTION") protected ClientSessionModel.Action action; @@ -86,6 +87,14 @@ public class ClientSessionEntity { this.clientId = clientId; } + public String getRealmId() { + return realmId; + } + + public void setRealmId(String realmId) { + this.realmId = realmId; + } + public int getTimestamp() { return timestamp; } @@ -102,14 +111,6 @@ public class ClientSessionEntity { this.redirectUri = redirectUri; } - public String getState() { - return state; - } - - public void setState(String state) { - this.state = state; - } - public ClientSessionModel.Action getAction() { return action; } diff --git a/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/entities/ClientSessionNoteEntity.java b/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/entities/ClientSessionNoteEntity.java index a08c9d2dd3..d1025e0ccd 100755 --- a/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/entities/ClientSessionNoteEntity.java +++ b/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/entities/ClientSessionNoteEntity.java @@ -18,9 +18,10 @@ import java.io.Serializable; */ @NamedQueries({ @NamedQuery(name = "removeClientSessionNoteByUser", query="delete from ClientSessionNoteEntity 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 = "removeClientSessionNoteByClient", query="delete from ClientSessionNoteEntity 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 = "removeClientSessionNoteByRealm", query="delete from ClientSessionNoteEntity 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 = "removeClientSessionNoteByExpired", query = "delete from ClientSessionNoteEntity 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)))") + @NamedQuery(name = "removeClientSessionNoteByClient", query="delete from ClientSessionNoteEntity r where r.clientSession IN (select c from ClientSessionEntity c where c.clientId = :clientId and c.realmId = :realmId)"), + @NamedQuery(name = "removeClientSessionNoteByRealm", query="delete from ClientSessionNoteEntity r where r.clientSession IN (select c from ClientSessionEntity c where c.realmId = :realmId)"), + @NamedQuery(name = "removeClientSessionNoteByExpired", query = "delete from ClientSessionNoteEntity 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)))"), + @NamedQuery(name = "removeDetachedClientSessionNoteByExpired", query = "delete from ClientSessionNoteEntity r where r.clientSession IN (select c from ClientSessionEntity c where c.session IS NULL and c.realmId = :realmId and c.timestamp < :maxTime )") }) @Table(name="CLIENT_SESSION_NOTE") @Entity 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 index e1be9d3c84..fe79cadcd9 100755 --- 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 @@ -17,9 +17,10 @@ import java.io.Serializable; */ @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)))") + @NamedQuery(name = "removeClientSessionRoleByClient", query="delete from ClientSessionRoleEntity r where r.clientSession IN (select c from ClientSessionEntity c where c.clientId = :clientId and c.realmId = :realmId)"), + @NamedQuery(name = "removeClientSessionRoleByRealm", query="delete from ClientSessionRoleEntity r where r.clientSession IN (select c from ClientSessionEntity c where c.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)))"), + @NamedQuery(name = "removeDetachedClientSessionRoleByExpired", query = "delete from ClientSessionRoleEntity r where r.clientSession IN (select c from ClientSessionEntity c where c.session IS NULL and c.realmId = :realmId and c.timestamp < :maxTime )") }) @Table(name="CLIENT_SESSION_ROLE") @Entity 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 index 8ae392fc42..1328171b9a 100755 --- 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 @@ -6,6 +6,7 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserSessionModel; import org.keycloak.models.sessions.mem.entities.ClientSessionEntity; +import org.keycloak.models.sessions.mem.entities.UserSessionEntity; import java.util.Set; @@ -31,19 +32,40 @@ public class ClientSessionAdapter implements ClientSessionModel { return entity.getId(); } + @Override + public RealmModel getRealm() { + return session.realms().getRealm(entity.getRealmId()); + } + + + @Override public ClientModel getClient() { return realm.findClientById(entity.getClientId()); } @Override - public String getState() { - return entity.getState(); + public UserSessionModel getUserSession() { + if (entity.getSession() == null) return null; + return new UserSessionAdapter(session, provider, realm, entity.getSession()); } @Override - public UserSessionModel getUserSession() { - return new UserSessionAdapter(session, provider, realm, entity.getSession()); + public void setUserSession(UserSessionModel userSession) { + UserSessionAdapter adapter = (UserSessionAdapter)userSession; + UserSessionEntity userSessionEntity = adapter.getEntity(); + entity.setSession(userSessionEntity); + userSessionEntity.getClientSessions().add(entity); + } + + @Override + public void setRedirectUri(String uri) { + entity.setRedirectUri(uri); + } + + @Override + public void setRoles(Set roles) { + entity.setRoles(roles); } @Override 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 56d475586a..7ef7e774d2 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 @@ -42,20 +42,12 @@ public class MemUserSessionProvider implements UserSessionProvider { } @Override - public ClientSessionModel createClientSession(RealmModel realm, ClientModel client, UserSessionModel userSession, String redirectUri, String state, Set roles) { - UserSessionEntity userSessionEntity = getUserSessionEntity(realm, userSession.getId()); - + public ClientSessionModel createClientSession(RealmModel realm, ClientModel client) { 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); - + entity.setRealmId(realm.getId()); clientSessions.put(entity.getId(), entity); return new ClientSessionAdapter(session, this, realm, entity); } @@ -66,6 +58,16 @@ public class MemUserSessionProvider implements UserSessionProvider { return entity != null ? new ClientSessionAdapter(session, this, realm, entity) : null; } + @Override + public ClientSessionModel getClientSession(String id) { + ClientSessionEntity entity = clientSessions.get(id); + if (entity != null) { + RealmModel realm = session.realms().getRealm(entity.getRealmId()); + return new ClientSessionAdapter(session, this, realm, entity); + } + return null; + } + @Override public UserSessionModel createUserSession(RealmModel realm, UserModel user, String loginUsername, String ipAddress, String authMethod, boolean rememberMe) { String id = KeycloakModelUtils.generateId(); @@ -118,7 +120,9 @@ public class MemUserSessionProvider implements UserSessionProvider { public List getUserSessions(RealmModel realm, ClientModel client) { List userSessionEntities = new LinkedList(); for (ClientSessionEntity s : clientSessions.values()) { - if (s.getSession().getRealm().equals(realm.getId()) && s.getClientId().equals(client.getId())) { + String realmId = realm.getId(); + String clientId = client.getId(); + if (s.getSession().getRealm().equals(realmId) && s.getClientId().equals(clientId)) { if (!userSessionEntities.contains(s.getSession())) { userSessionEntities.add(s.getSession()); } @@ -188,6 +192,13 @@ public class MemUserSessionProvider implements UserSessionProvider { } } } + Iterator citr = clientSessions.values().iterator(); + while (citr.hasNext()) { + ClientSessionEntity c = citr.next(); + if (c.getSession() == null && c.getTimestamp() < Time.currentTime() - realm.getSsoSessionIdleTimeout()) { + citr.remove(); + } + } } @Override @@ -203,6 +214,13 @@ public class MemUserSessionProvider implements UserSessionProvider { } } } + Iterator citr = clientSessions.values().iterator(); + while (citr.hasNext()) { + ClientSessionEntity c = citr.next(); + if (c.getSession() == null && c.getRealmId().equals(realm.getId())) { + citr.remove(); + } + } } @Override 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 6a3a963cd0..3b8e8674c3 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 @@ -30,6 +30,10 @@ public class UserSessionAdapter implements UserSessionModel { this.entity = entity; } + public UserSessionEntity getEntity() { + return entity; + } + public String getId() { return entity.getId(); } 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 index 7664a9b20c..dd945de793 100755 --- 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 @@ -13,11 +13,11 @@ public class ClientSessionEntity { private String id; private String clientId; + private String realmId; private UserSessionEntity session; private String redirectUri; - private String state; private String authMethod; private int timestamp; @@ -41,6 +41,14 @@ public class ClientSessionEntity { this.clientId = clientId; } + public String getRealmId() { + return realmId; + } + + public void setRealmId(String realmId) { + this.realmId = realmId; + } + public UserSessionEntity getSession() { return session; } @@ -57,14 +65,6 @@ public class ClientSessionEntity { this.redirectUri = redirectUri; } - public String getState() { - return state; - } - - public void setState(String state) { - this.state = state; - } - public int getTimestamp() { return timestamp; } 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 index 248823917a..4e0f53ca08 100755 --- 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 @@ -10,6 +10,8 @@ import org.keycloak.models.sessions.mongo.entities.MongoClientSessionEntity; import org.keycloak.models.sessions.mongo.entities.MongoUserSessionEntity; import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; import java.util.Set; /** @@ -21,15 +23,13 @@ public class ClientSessionAdapter implements ClientSessionModel { 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) { + public ClientSessionAdapter(KeycloakSession session, MongoUserSessionProvider provider, RealmModel realm, MongoClientSessionEntity entity, MongoStoreInvocationContext invContext) { this.session = session; this.provider = provider; this.realm = realm; this.entity = entity; - this.userSessionEntity = userSessionEntity; this.invContext = invContext; } @@ -38,19 +38,40 @@ public class ClientSessionAdapter implements ClientSessionModel { return entity.getId(); } + @Override + public RealmModel getRealm() { + return session.realms().getRealm(entity.getRealmId()); + } + @Override public ClientModel getClient() { return realm.findClientById(entity.getClientId()); } @Override - public String getState() { - return entity.getState(); + public UserSessionModel getUserSession() { + if (entity.getSessionId() == null) return null; + return provider.getUserSession(realm, entity.getSessionId()); } @Override - public UserSessionModel getUserSession() { - return new UserSessionAdapter(session, provider, userSessionEntity, realm, invContext); + public void setUserSession(UserSessionModel userSession) { + MongoUserSessionEntity userSessionEntity = provider.getUserSessionEntity(realm, userSession.getId()); + entity.setSessionId(userSessionEntity.getId()); + provider.getMongoStore().pushItemToList(userSessionEntity, "clientSessions", entity.getId(), true, invContext); + } + + @Override + public void setRedirectUri(String uri) { + entity.setRedirectUri(uri); + + } + + @Override + public void setRoles(Set roles) { + List list = new LinkedList(); + list.addAll(roles); + entity.setRoles(list); } @Override @@ -66,7 +87,6 @@ public class ClientSessionAdapter implements ClientSessionModel { @Override public void setTimestamp(int timestamp) { entity.setTimestamp(timestamp); - invContext.getMongoStore().updateEntity(userSessionEntity, invContext); } @Override @@ -77,7 +97,6 @@ public class ClientSessionAdapter implements ClientSessionModel { @Override public void setAction(Action action) { entity.setAction(action); - invContext.getMongoStore().updateEntity(userSessionEntity, invContext); } @Override 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 3fdfdba3fc..a508c3490c 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 @@ -38,50 +38,38 @@ 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()); + public MongoStore getMongoStore() { + return mongoStore; + } + @Override + public ClientSessionModel createClientSession(RealmModel realm, ClientModel client) { 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); + entity.setRealmId(realm.getId()); + return new ClientSessionAdapter(session, this, realm, entity, invocationContext); } @Override public ClientSessionModel getClientSession(RealmModel realm, String id) { - DBObject query = new QueryBuilder() - .and("realmId").is(realm.getId()) - .and("clientSessions.id").is(id).get(); + MongoClientSessionEntity entity = getClientSessionEntity(id); + if (entity == null) return null; + return new ClientSessionAdapter(session, this, realm, entity, invocationContext); + } - List entities = mongoStore.loadEntities(MongoUserSessionEntity.class, query, invocationContext); - if (entities.isEmpty()) { - return null; + @Override + public ClientSessionModel getClientSession(String id) { + MongoClientSessionEntity entity = getClientSessionEntity(id); + if (entity != null) { + RealmModel realm = session.realms().getRealm(entity.getRealmId()); + return new ClientSessionAdapter(session, this, realm, entity, invocationContext); } - - MongoUserSessionEntity userSessionEntity = entities.get(0); - List sessions = userSessionEntity.getClientSessions(); - if (sessions == null) { - return null; - } - 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(); @@ -91,6 +79,7 @@ public class MongoUserSessionProvider implements UserSessionProvider { entity.setIpAddress(ipAddress); entity.setAuthMethod(authMethod); entity.setRememberMe(rememberMe); + entity.setRealmId(realm.getId()); int currentTime = Time.currentTime(); @@ -115,6 +104,10 @@ public class MongoUserSessionProvider implements UserSessionProvider { return mongoStore.loadEntity(MongoUserSessionEntity.class, id, invocationContext); } + MongoClientSessionEntity getClientSessionEntity(String id) { + return mongoStore.loadEntity(MongoClientSessionEntity.class, id, invocationContext); + } + @Override public List getUserSessions(RealmModel realm, UserModel user) { DBObject query = new BasicDBObject("user", user.getId()); @@ -167,21 +160,35 @@ public class MongoUserSessionProvider implements UserSessionProvider { public void removeUserSessions(RealmModel realm) { DBObject query = new BasicDBObject("realmId", realm.getId()); mongoStore.removeEntities(MongoUserSessionEntity.class, query, invocationContext); + query = new QueryBuilder() + .and("realmId").is(realm.getId()) + .get(); + + mongoStore.removeEntities(MongoClientSessionEntity.class, query, invocationContext); } @Override public void removeExpiredUserSessions(RealmModel realm) { int currentTime = Time.currentTime(); DBObject query = new QueryBuilder() + .and("realmId").is(realm.getId()) .and("started").lessThan(currentTime - realm.getSsoSessionMaxLifespan()) .get(); mongoStore.removeEntities(MongoUserSessionEntity.class, query, invocationContext); query = new QueryBuilder() + .and("realmId").is(realm.getId()) .and("lastSessionRefresh").lessThan(currentTime - realm.getSsoSessionIdleTimeout()) .get(); mongoStore.removeEntities(MongoUserSessionEntity.class, query, invocationContext); + query = new QueryBuilder() + .and("sessionId").is(null) + .and("realmId").is(realm.getId()) + .and("timestamp").lessThan(currentTime - realm.getSsoSessionIdleTimeout()) + .get(); + + mongoStore.removeEntities(MongoClientSessionEntity.class, query, invocationContext); } @Override @@ -239,24 +246,9 @@ public class MongoUserSessionProvider implements UserSessionProvider { // TODO Not very efficient, should use Mongo $pull to remove directly public void onClientRemoved(RealmModel realm, ClientModel client) { DBObject query = new QueryBuilder() - .and("clientSessions.clientId").is(client.getId()) + .and("clientId").is(client.getId()) .get(); - List userSessionEntities = mongoStore.loadEntities(MongoUserSessionEntity.class, query, invocationContext); - for (MongoUserSessionEntity e : userSessionEntities) { - if (e.getClientSessions() == null) { - continue; - } - - 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); - } - } + mongoStore.removeEntities(MongoUserSessionEntity.class, query, invocationContext); } @Override 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 6a6658550d..e47833d98a 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 @@ -135,8 +135,10 @@ public class UserSessionAdapter extends AbstractMongoAdapterStian Thorgersen */ -public class MongoClientSessionEntity { +public class MongoClientSessionEntity extends AbstractIdentifiableEntity implements MongoIdentifiableEntity { private String id; private String clientId; + private String realmId; + private String sessionId; private String redirectUri; - private String state; private String authMethod; private int timestamp; @@ -39,6 +43,14 @@ public class MongoClientSessionEntity { this.clientId = clientId; } + public String getRealmId() { + return realmId; + } + + public void setRealmId(String realmId) { + this.realmId = realmId; + } + public String getRedirectUri() { return redirectUri; } @@ -47,14 +59,6 @@ public class MongoClientSessionEntity { this.redirectUri = redirectUri; } - public String getState() { - return state; - } - - public void setState(String state) { - this.state = state; - } - public String getAuthMethod() { return authMethod; } @@ -94,4 +98,17 @@ public class MongoClientSessionEntity { public void setNotes(Map notes) { this.notes = notes; } + + public String getSessionId() { + return sessionId; + } + + public void setSessionId(String sessionId) { + this.sessionId = sessionId; + } + + @Override + public void afterRemove(MongoStoreInvocationContext context) { + } + } 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 5f2bd28e52..141297b6dc 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 @@ -1,10 +1,13 @@ package org.keycloak.models.sessions.mongo.entities; +import com.mongodb.DBObject; +import com.mongodb.QueryBuilder; import org.keycloak.connections.mongo.api.MongoCollection; import org.keycloak.connections.mongo.api.MongoIdentifiableEntity; import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext; import org.keycloak.models.entities.AbstractIdentifiableEntity; +import java.util.ArrayList; import java.util.List; /** @@ -29,7 +32,7 @@ public class MongoUserSessionEntity extends AbstractIdentifiableEntity implement private int lastSessionRefresh; - private List clientSessions; + private List clientSessions = new ArrayList(); public String getRealmId() { return realmId; @@ -95,16 +98,20 @@ public class MongoUserSessionEntity extends AbstractIdentifiableEntity implement this.lastSessionRefresh = lastSessionRefresh; } - public List getClientSessions() { + public List getClientSessions() { return clientSessions; } - public void setClientSessions(List clientSessions) { + public void setClientSessions(List clientSessions) { this.clientSessions = clientSessions; } @Override public void afterRemove(MongoStoreInvocationContext context) { + DBObject query = new QueryBuilder() + .and("sessionId").is(getId()) + .get(); + context.getMongoStore().removeEntities(MongoClientSessionEntity.class, query, context); } } diff --git a/services/src/main/java/org/keycloak/services/managers/AccessCode.java b/services/src/main/java/org/keycloak/services/managers/AccessCode.java index edea2556ad..8b9d3f4bdf 100755 --- a/services/src/main/java/org/keycloak/services/managers/AccessCode.java +++ b/services/src/main/java/org/keycloak/services/managers/AccessCode.java @@ -103,7 +103,7 @@ public class AccessCode { } public String getState() { - return clientSession.getState(); + throw new RuntimeException("REFACTORING, TODO REMOVE ACCESS CODE"); } public String getRedirectUri() { diff --git a/services/src/main/java/org/keycloak/services/managers/Auth.java b/services/src/main/java/org/keycloak/services/managers/Auth.java index 7568c1b8bc..1527ea5153 100755 --- a/services/src/main/java/org/keycloak/services/managers/Auth.java +++ b/services/src/main/java/org/keycloak/services/managers/Auth.java @@ -2,6 +2,7 @@ package org.keycloak.services.managers; import org.keycloak.models.ApplicationModel; import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientSessionModel; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; @@ -18,6 +19,7 @@ public class Auth { private final UserModel user; private final ClientModel client; private final UserSessionModel session; + private ClientSessionModel clientSession; public Auth(RealmModel realm, AccessToken token, UserModel user, ClientModel client, UserSessionModel session, boolean cookie) { this.cookie = cookie; @@ -53,6 +55,14 @@ public class Auth { return session; } + public ClientSessionModel getClientSession() { + return clientSession; + } + + public void setClientSession(ClientSessionModel clientSession) { + this.clientSession = clientSession; + } + public boolean hasRealmRole(String role) { if (cookie) { return user.hasRole(realm.getRole(role)); diff --git a/services/src/main/java/org/keycloak/services/managers/ClientSessionCode.java b/services/src/main/java/org/keycloak/services/managers/ClientSessionCode.java new file mode 100755 index 0000000000..217675a581 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/managers/ClientSessionCode.java @@ -0,0 +1,173 @@ +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 ClientSessionCode { + + private final RealmModel realm; + private final ClientSessionModel clientSession; + + public ClientSessionCode(RealmModel realm, ClientSessionModel clientSession) { + this.realm = realm; + this.clientSession = clientSession; + } + + public static ClientSessionCode parse(String code, KeycloakSession session) { + try { + String[] parts = code.split("\\."); + String id = new String(Base64Url.decode(parts[1])); + + ClientSessionModel clientSession = session.sessions().getClientSession(id); + if (clientSession == null) { + return null; + } + + String hash = createSignatureHash(clientSession.getRealm(), clientSession); + if (!hash.equals(parts[0])) { + return null; + } + + return new ClientSessionCode(clientSession.getRealm(), clientSession); + } catch (RuntimeException e) { + return null; + } + } + + + public static ClientSessionCode 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 ClientSessionCode(realm, clientSession); + } catch (RuntimeException e) { + return null; + } + } + + public ClientSessionModel getClientSession() { + return clientSession; + } + + 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 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() { + return generateCode(realm, clientSession); + } + + private static String generateCode(RealmModel realm, ClientSessionModel clientSession) { + 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/TokenManager.java b/services/src/main/java/org/keycloak/services/managers/TokenManager.java index 112f6e6536..e7de7172d7 100755 --- a/services/src/main/java/org/keycloak/services/managers/TokenManager.java +++ b/services/src/main/java/org/keycloak/services/managers/TokenManager.java @@ -53,16 +53,6 @@ public class TokenManager { } } - 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()); - } - - ClientSessionModel clientSession = session.sessions().createClientSession(realm, client, userSession, redirect, state, requestedRoles); - return new AccessCode(realm, clientSession); - } - public AccessToken refreshAccessToken(KeycloakSession session, UriInfo uriInfo, ClientConnection connection, RealmModel realm, ClientModel client, String encodedRefreshToken, EventBuilder event) throws OAuthErrorException { RefreshToken refreshToken = verifyRefreshToken(realm, encodedRefreshToken); @@ -132,7 +122,21 @@ public class TokenManager { return token; } - public Set getAccess(String scopeParam, ClientModel client, UserModel user) { + public static void attachClientSession(UserSessionModel session, ClientSessionModel clientSession) { + UserModel user = session.getUser(); + clientSession.setUserSession(session); + Set requestedRoles = new HashSet(); + // todo scope param protocol independent + for (RoleModel r : TokenManager.getAccess(null, clientSession.getClient(), user)) { + requestedRoles.add(r.getId()); + } + clientSession.setRoles(requestedRoles); + + + } + + + public static Set getAccess(String scopeParam, ClientModel client, UserModel user) { // todo scopeParam is ignored until we figure out a scheme that fits with openid connect Set requestedRoles = new HashSet(); diff --git a/services/src/main/java/org/keycloak/services/protocol/OpenIdConnectProtocol.java b/services/src/main/java/org/keycloak/services/protocol/OpenIdConnectProtocol.java new file mode 100755 index 0000000000..79cf76c3d4 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/protocol/OpenIdConnectProtocol.java @@ -0,0 +1,17 @@ +package org.keycloak.services.protocol; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class OpenIdConnectProtocol { + public static final String LOGIN_PAGE_PROTOCOL = "oidc_login_page"; + + public static final String STATE_PARAM = "state"; + public static final String SCOPE_PARAM = "scope"; + public static final String RESPONSE_TYPE_PARAM = "response_type"; + public static final String REDIRECT_URI_PARAM = "redirect_uri"; + public static final String CLIENT_ID_PARAM = "client_id"; + public static final String PROMPT_PARAM = "prompt"; + public static final String LOGIN_HINT_PARAM = "login_hint"; +} 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 4ceb9f3e60..d343af64f8 100755 --- a/services/src/main/java/org/keycloak/services/resources/AccountService.java +++ b/services/src/main/java/org/keycloak/services/resources/AccountService.java @@ -54,7 +54,9 @@ import org.keycloak.services.ForbiddenException; import org.keycloak.services.managers.AppAuthManager; import org.keycloak.services.managers.Auth; import org.keycloak.services.managers.AuthenticationManager; +import org.keycloak.services.managers.ClientSessionCode; import org.keycloak.services.messages.Messages; +import org.keycloak.services.protocol.OpenIdConnectProtocol; import org.keycloak.services.resources.flows.Flows; import org.keycloak.services.resources.flows.OAuthRedirect; import org.keycloak.services.resources.flows.Urls; @@ -191,12 +193,15 @@ public class AccountService { boolean associated = false; for (ClientSessionModel c : userSession.getClientSessions()) { if (c.getClient().equals(application)) { + auth.setClientSession(c); associated = true; break; } } if (!associated) { - session.sessions().createClientSession(realm, application, userSession, null, null, null); + ClientSessionModel clientSession = session.sessions().createClientSession(realm, application); + clientSession.setUserSession(userSession); + auth.setClientSession(clientSession); } } @@ -650,12 +655,13 @@ public class AccountService { String redirectUri = UriBuilder.fromUri(Urls.accountSocialPage(uriInfo.getBaseUri(), realm.getName())).build().toString(); try { + ClientSessionModel clientSession = auth.getClientSession(); + clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE); + clientSession.setRedirectUri(redirectUri); + clientSession.setNote(OpenIdConnectProtocol.STATE_PARAM, UUID.randomUUID().toString()); + ClientSessionCode clientSessionCode = new ClientSessionCode(realm, clientSession); return Flows.social(realm, uriInfo, clientConnection, provider) - .user(user) - .putClientAttribute(OAuth2Constants.CLIENT_ID, Constants.ACCOUNT_MANAGEMENT_APP) - .putClientAttribute(OAuth2Constants.STATE, UUID.randomUUID().toString()) - .putClientAttribute(OAuth2Constants.REDIRECT_URI, redirectUri) - .redirectToSocialProvider(); + .redirectToSocialProvider(clientSessionCode); } catch (SocialProviderException spe) { setReferrerOnPage(); return account.setError(Messages.SOCIAL_REDIRECT_ERROR).createResponse(AccountPages.SOCIAL); 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 3b34ed1e8a..ca5f72f4a2 100755 --- a/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java +++ b/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java @@ -25,6 +25,7 @@ import org.jboss.logging.Logger; import org.jboss.resteasy.spi.HttpRequest; import org.keycloak.ClientConnection; import org.keycloak.OAuth2Constants; +import org.keycloak.events.Event; import org.keycloak.events.EventBuilder; import org.keycloak.events.Details; import org.keycloak.events.Errors; @@ -44,8 +45,10 @@ import org.keycloak.models.utils.TimeBasedOTP; import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.services.managers.AccessCode; import org.keycloak.services.managers.AuthenticationManager; +import org.keycloak.services.managers.ClientSessionCode; import org.keycloak.services.managers.TokenManager; import org.keycloak.services.messages.Messages; +import org.keycloak.services.protocol.OpenIdConnectProtocol; import org.keycloak.services.resources.flows.Flows; import org.keycloak.services.resources.flows.Urls; import org.keycloak.services.validation.Validation; @@ -54,6 +57,7 @@ import javax.ws.rs.Consumes; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.Path; +import javax.ws.rs.QueryParam; import javax.ws.rs.core.Context; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; @@ -63,6 +67,7 @@ import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriInfo; import javax.ws.rs.ext.Providers; import java.util.Set; +import java.util.UUID; import java.util.concurrent.TimeUnit; /** @@ -102,22 +107,66 @@ public class RequiredActionsService { this.event = event; } + private boolean checkSsl() { + if (uriInfo.getBaseUri().getScheme().equals("https")) { + return true; + } else { + return !realm.getSslRequired().isRequired(clientConnection); + } + } + + + private class Checks { + ClientSessionCode clientCode; + Response response; + + boolean check(String code, ClientSessionModel.Action requiredAction) { + if (!checkSsl()) { + event.error(Errors.SSL_REQUIRED); + response = Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "HTTPS required"); + return false; + } + if (!realm.isEnabled()) { + event.error(Errors.REALM_DISABLED); + response = Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Realm not enabled."); + return false; + } + clientCode = ClientSessionCode.parse(code, session, realm); + if (clientCode == null) { + event.error(Errors.INVALID_CODE); + response = Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Unknown code, please login again through your application."); + return false; + } + if (!clientCode.isValid(requiredAction)) { + event.error(Errors.INVALID_CODE); + response = Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Invalid code, please login again through your application."); + } + return true; + } + } + @Path("profile") @POST @Consumes(MediaType.APPLICATION_FORM_URLENCODED) - public Response updateProfile(final MultivaluedMap formData) { - AccessCode accessCode = getAccessCodeEntry(RequiredAction.UPDATE_PROFILE); - if (accessCode == null) { - return unauthorized(); + public Response updateProfile(@QueryParam("code") String code, + final MultivaluedMap formData) { + event.event(EventType.UPDATE_PROFILE); + Checks checks = new Checks(); + if (!checks.check(code, ClientSessionModel.Action.UPDATE_PROFILE)) { + return checks.response; } + ClientSessionCode accessCode = checks.clientCode; + ClientSessionModel clientSession = accessCode.getClientSession(); + UserSessionModel userSession = clientSession.getUserSession(); + UserModel user = userSession.getUser(); - UserModel user = getUser(accessCode); - - initEvent(accessCode); + initEvent(clientSession); String error = Validation.validateUpdateProfileForm(formData); if (error != null) { - return Flows.forms(session, realm, null, uriInfo).setUser(user).setError(error).createResponse(RequiredAction.UPDATE_PROFILE); + return Flows.forms(session, realm, null, uriInfo).setUser(user).setError(error) + .setAccessCode(accessCode.getCode()) + .createResponse(RequiredAction.UPDATE_PROFILE); } user.setFirstName(formData.getFirst("firstName")); @@ -137,30 +186,38 @@ public class RequiredActionsService { event.clone().event(EventType.UPDATE_EMAIL).detail(Details.PREVIOUS_EMAIL, oldEmail).detail(Details.UPDATED_EMAIL, email).success(); } - return redirectOauth(user, accessCode); + return redirectOauth(user, accessCode, clientSession, userSession); } @Path("totp") @POST @Consumes(MediaType.APPLICATION_FORM_URLENCODED) - public Response updateTotp(final MultivaluedMap formData) { - AccessCode accessCode = getAccessCodeEntry(RequiredAction.CONFIGURE_TOTP); - if (accessCode == null) { - return unauthorized(); + public Response updateTotp(@QueryParam("code") String code, + final MultivaluedMap formData) { + event.event(EventType.UPDATE_TOTP); + Checks checks = new Checks(); + if (!checks.check(code, ClientSessionModel.Action.CONFIGURE_TOTP)) { + return checks.response; } + ClientSessionCode accessCode = checks.clientCode; + ClientSessionModel clientSession = accessCode.getClientSession(); + UserSessionModel userSession = clientSession.getUserSession(); + UserModel user = userSession.getUser(); - UserModel user = getUser(accessCode); - - initEvent(accessCode); + initEvent(clientSession); String totp = formData.getFirst("totp"); String totpSecret = formData.getFirst("totpSecret"); LoginFormsProvider loginForms = Flows.forms(session, realm, null, uriInfo).setUser(user); if (Validation.isEmpty(totp)) { - return loginForms.setError(Messages.MISSING_TOTP).createResponse(RequiredAction.CONFIGURE_TOTP); + return loginForms.setError(Messages.MISSING_TOTP) + .setAccessCode(accessCode.getCode()) + .createResponse(RequiredAction.CONFIGURE_TOTP); } else if (!new TimeBasedOTP().validate(totp, totpSecret.getBytes())) { - return loginForms.setError(Messages.INVALID_TOTP).createResponse(RequiredAction.CONFIGURE_TOTP); + return loginForms.setError(Messages.INVALID_TOTP) + .setAccessCode(accessCode.getCode()) + .createResponse(RequiredAction.CONFIGURE_TOTP); } UserCredentialModel credentials = new UserCredentialModel(); @@ -174,113 +231,163 @@ public class RequiredActionsService { event.clone().event(EventType.UPDATE_TOTP).success(); - return redirectOauth(user, accessCode); + return redirectOauth(user, accessCode, clientSession, userSession); } @Path("password") @POST @Consumes(MediaType.APPLICATION_FORM_URLENCODED) - public Response updatePassword(final MultivaluedMap formData) { - AccessCode accessCode = getAccessCodeEntry(RequiredAction.UPDATE_PASSWORD); - if (accessCode == null) { - return unauthorized(); + public Response updatePassword(@QueryParam("code") String code, + final MultivaluedMap formData) { + event.event(EventType.UPDATE_PASSWORD); + Checks checks = new Checks(); + if (!checks.check(code, ClientSessionModel.Action.UPDATE_PASSWORD)) { + return checks.response; } + ClientSessionCode accessCode = checks.clientCode; + ClientSessionModel clientSession = accessCode.getClientSession(); + UserSessionModel userSession = clientSession.getUserSession(); + UserModel user = userSession.getUser(); - UserModel user = getUser(accessCode); - - initEvent(accessCode); + initEvent(clientSession); String passwordNew = formData.getFirst("password-new"); String passwordConfirm = formData.getFirst("password-confirm"); LoginFormsProvider loginForms = Flows.forms(session, realm, null, uriInfo).setUser(user); if (Validation.isEmpty(passwordNew)) { - return loginForms.setError(Messages.MISSING_PASSWORD).createResponse(RequiredAction.UPDATE_PASSWORD); + return loginForms.setError(Messages.MISSING_PASSWORD) + .setAccessCode(accessCode.getCode()) + .createResponse(RequiredAction.UPDATE_PASSWORD); } else if (!passwordNew.equals(passwordConfirm)) { - return loginForms.setError(Messages.NOTMATCH_PASSWORD).createResponse(RequiredAction.UPDATE_PASSWORD); + return loginForms.setError(Messages.NOTMATCH_PASSWORD) + .setAccessCode(accessCode.getCode()) + .createResponse(RequiredAction.UPDATE_PASSWORD); } try { session.users().updateCredential(realm, user, UserCredentialModel.password(passwordNew)); } catch (Exception ape) { - return loginForms.setError(ape.getMessage()).createResponse(RequiredAction.UPDATE_PASSWORD); + return loginForms.setError(ape.getMessage()) + .setAccessCode(accessCode.getCode()) + .createResponse(RequiredAction.UPDATE_PASSWORD); } user.removeRequiredAction(RequiredAction.UPDATE_PASSWORD); event.clone().event(EventType.UPDATE_PASSWORD).success(); + return redirectOauth(user, accessCode, clientSession, userSession); // Redirect to account management to login if password reset was initiated by admin + /* here while refactoring, ok to remove when you want if (accessCode.getSessionState() == null) { return Response.seeOther(Urls.accountPage(uriInfo.getBaseUri(), realm.getId())).build(); } else { return redirectOauth(user, accessCode); } + */ } @Path("email-verification") @GET - public Response emailVerification() { + public Response emailVerification(@QueryParam("code") String code) { + event.event(EventType.VERIFY_EMAIL); if (uriInfo.getQueryParameters().containsKey("key")) { - AccessCode accessCode = AccessCode.parse(uriInfo.getQueryParameters().getFirst("key"), session, realm); - if (accessCode == null || !accessCode.isValid(RequiredAction.VERIFY_EMAIL)) { - return unauthorized(); + Checks checks = new Checks(); + if (!checks.check(code, ClientSessionModel.Action.VERIFY_EMAIL)) { + return checks.response; } - - UserModel user = getUser(accessCode); - - initEvent(accessCode); - + ClientSessionCode accessCode = checks.clientCode; + ClientSessionModel clientSession = accessCode.getClientSession(); + UserSessionModel userSession = clientSession.getUserSession(); + UserModel user = userSession.getUser(); + String key = uriInfo.getQueryParameters().getFirst("key"); + String keyNote = clientSession.getNote("key"); + if (key == null || !key.equals(keyNote)) { + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Somebody is trying to illegally change your email."); + } + initEvent(clientSession); user.setEmailVerified(true); user.removeRequiredAction(RequiredAction.VERIFY_EMAIL); - event.clone().event(EventType.VERIFY_EMAIL).detail(Details.EMAIL, accessCode.getUser().getEmail()).success(); + event.clone().event(EventType.VERIFY_EMAIL).detail(Details.EMAIL, user.getEmail()).success(); - return redirectOauth(user, accessCode); + return redirectOauth(user, accessCode, clientSession, userSession); } else { - AccessCode accessCode = getAccessCodeEntry(RequiredAction.VERIFY_EMAIL); - if (accessCode == null) { - return unauthorized(); + Checks checks = new Checks(); + if (!checks.check(code, ClientSessionModel.Action.VERIFY_EMAIL)) { + return checks.response; } + ClientSessionCode accessCode = checks.clientCode; + ClientSessionModel clientSession = accessCode.getClientSession(); + String verifyCode = UUID.randomUUID().toString(); + clientSession.setNote("key", verifyCode); + UserSessionModel userSession = clientSession.getUserSession(); + UserModel user = userSession.getUser(); - initEvent(accessCode); + initEvent(clientSession); - return Flows.forms(session, realm, null, uriInfo).setAccessCode(accessCode.getCode()).setUser(accessCode.getUser()) + return Flows.forms(session, realm, null, uriInfo) + .setAccessCode(accessCode.getCode()) + .setVerifyCode(verifyCode) + .setUser(userSession.getUser()) .createResponse(RequiredAction.VERIFY_EMAIL); } } @Path("password-reset") @GET - public Response passwordReset() { + public Response passwordReset(@QueryParam("code") String code) { + event.event(EventType.SEND_RESET_PASSWORD); if (uriInfo.getQueryParameters().containsKey("key")) { - AccessCode accessCode = AccessCode.parse(uriInfo.getQueryParameters().getFirst("key"), session, realm); - if (accessCode == null || !accessCode.isValid(RequiredAction.UPDATE_PASSWORD)) { - return unauthorized(); + Checks checks = new Checks(); + if (!checks.check(code, ClientSessionModel.Action.UPDATE_PASSWORD)) { + return checks.response; } - - return Flows.forms(session, realm, null, uriInfo).setAccessCode(accessCode.getCode()).createResponse(RequiredAction.UPDATE_PASSWORD); + ClientSessionCode accessCode = checks.clientCode; + ClientSessionModel clientSession = accessCode.getClientSession(); + UserSessionModel userSession = clientSession.getUserSession(); + UserModel user = userSession.getUser(); + String key = uriInfo.getQueryParameters().getFirst("key"); + String keyNote = clientSession.getNote("key"); + if (key == null || !key.equals(keyNote)) { + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Somebody is trying to illegally change your password."); + } + return Flows.forms(session, realm, null, uriInfo) + .setAccessCode(accessCode.getCode()) + .createResponse(RequiredAction.UPDATE_PASSWORD); } else { - return Flows.forms(session, realm, null, uriInfo).createPasswordReset(); + return Flows.forms(session, realm, null, uriInfo) + .setAccessCode(code) + .createPasswordReset(); } } @Path("password-reset") @POST @Consumes(MediaType.APPLICATION_FORM_URLENCODED) - public Response sendPasswordReset(final MultivaluedMap formData) { + public Response sendPasswordReset(@QueryParam("code") String code, + final MultivaluedMap formData) { + event.event(EventType.SEND_RESET_PASSWORD); + if (!checkSsl()) { + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "HTTPS required"); + } + if (!realm.isEnabled()) { + event.error(Errors.REALM_DISABLED); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Realm not enabled."); + } + ClientSessionCode accessCode = ClientSessionCode.parse(code, session, realm); + if (accessCode == null) { + event.error(Errors.INVALID_CODE); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Unknown code, please login again through your application."); + } + ClientSessionModel clientSession = accessCode.getClientSession(); + String username = formData.getFirst("username"); - String scopeParam = uriInfo.getQueryParameters().getFirst(OAuth2Constants.SCOPE); - String state = uriInfo.getQueryParameters().getFirst(OAuth2Constants.STATE); - String redirect = uriInfo.getQueryParameters().getFirst(OAuth2Constants.REDIRECT_URI); - String clientId = uriInfo.getQueryParameters().getFirst(OAuth2Constants.CLIENT_ID); - - AuthenticationManager authManager = new AuthenticationManager(); - - ClientModel client = realm.findClient(clientId); + ClientModel client = clientSession.getClient(); if (client == null) { return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Unknown login requester."); @@ -290,8 +397,8 @@ public class RequiredActionsService { "Login requester not enabled."); } - event.event(EventType.SEND_RESET_PASSWORD).client(clientId) - .detail(Details.REDIRECT_URI, redirect) + event.client(client.getClientId()) + .detail(Details.REDIRECT_URI, clientSession.getRedirectUri()) .detail(Details.RESPONSE_TYPE, "code") .detail(Details.AUTH_METHOD, "form") .detail(Details.USERNAME, username); @@ -306,55 +413,35 @@ public class RequiredActionsService { } else { UserSessionModel userSession = session.sessions().createUserSession(realm, user, username, clientConnection.getRemoteAddr(), "form", false); event.session(userSession); + TokenManager.attachClientSession(userSession, clientSession); - AccessCode accessCode = tokenManager.createAccessCode(scopeParam, state, redirect, session, realm, client, user, userSession); accessCode.setRequiredAction(RequiredAction.UPDATE_PASSWORD); try { UriBuilder builder = Urls.loginPasswordResetBuilder(uriInfo.getBaseUri()); - builder.queryParam("key", accessCode.getCode()); + builder.queryParam("code", accessCode.getCode()); + String verifyCode = UUID.randomUUID().toString(); + clientSession.setNote("key", verifyCode); + builder.queryParam("key", verifyCode); String link = builder.build(realm.getName()).toString(); long expiration = TimeUnit.SECONDS.toMinutes(realm.getAccessCodeLifespanUserAction()); this.session.getProvider(EmailProvider.class).setRealm(realm).setUser(user).sendPasswordReset(link, expiration); - event.user(user).detail(Details.EMAIL, user.getEmail()).detail(Details.CODE_ID, accessCode.getCodeId()).success(); + event.user(user).detail(Details.EMAIL, user.getEmail()).detail(Details.CODE_ID, clientSession.getId()).success(); } catch (EmailException e) { logger.error("Failed to send password reset email", e); - return Flows.forms(this.session, realm, client, uriInfo).setError("emailSendError").createErrorPage(); + return Flows.forms(this.session, realm, client, uriInfo).setError("emailSendError") + .setAccessCode(accessCode.getCode()) + .createErrorPage(); } } return Flows.forms(session, realm, client, uriInfo).setSuccess("emailSent").createPasswordReset(); } - private AccessCode getAccessCodeEntry(RequiredAction requiredAction) { - String code = uriInfo.getQueryParameters().getFirst(OAuth2Constants.CODE); - if (code == null) { - logger.debug("Code query param not found"); - return null; - } - - AccessCode accessCode = AccessCode.parse(code, session, realm); - if (accessCode == null) { - logger.debug("Access code not found"); - return null; - } - - if (!accessCode.isValid(requiredAction)) { - logger.debugv("Invalid access code"); - return null; - } - - return accessCode; - } - - private UserModel getUser(AccessCode accessCode) { - return session.users().getUserByUsername(accessCode.getUser().getUsername(), realm); - } - - private Response redirectOauth(UserModel user, AccessCode accessCode) { + private Response redirectOauth(UserModel user, ClientSessionCode accessCode, ClientSessionModel clientSession, UserSessionModel userSession) { if (accessCode == null) { return null; } @@ -362,37 +449,40 @@ public class RequiredActionsService { Set requiredActions = user.getRequiredActions(); if (!requiredActions.isEmpty()) { accessCode.setRequiredAction(requiredActions.iterator().next()); - return Flows.forms(session, realm, null, uriInfo).setAccessCode(accessCode.getCode()).setUser(user) + return Flows.forms(session, realm, null, uriInfo) + .setAccessCode(accessCode.getCode()) + .setUser(user) .createResponse(requiredActions.iterator().next()); } else { - logger.debugv("Redirecting to: {0}", accessCode.getRedirectUri()); + logger.debugv("Redirecting to: {0}", clientSession.getRedirectUri()); accessCode.setAction(ClientSessionModel.Action.CODE_TO_TOKEN); AuthenticationManager authManager = new AuthenticationManager(); - UserSessionModel userSession = session.sessions().getUserSession(realm, accessCode.getSessionState()); if (!AuthenticationManager.isSessionValid(realm, userSession)) { AuthenticationManager.logout(session, realm, userSession, uriInfo, clientConnection); - return Flows.oauth(this.session, realm, request, uriInfo, clientConnection, authManager, tokenManager).redirectError(accessCode.getClient(), "access_denied", accessCode.getState(), accessCode.getRedirectUri()); + return Flows.oauth(this.session, realm, request, uriInfo, clientConnection, authManager, tokenManager) + .redirectError(clientSession.getClient(), "access_denied", clientSession.getNote(OpenIdConnectProtocol.STATE_PARAM), clientSession.getRedirectUri()); } event.session(userSession); event.success(); - return Flows.oauth(this.session, realm, request, uriInfo, clientConnection, authManager, tokenManager).redirectAccessCode(accessCode, - userSession, accessCode.getState(), accessCode.getRedirectUri()); + return Flows.oauth(this.session, realm, request, uriInfo, clientConnection, authManager, tokenManager) + .redirectAccessCode(accessCode, + userSession, clientSession.getNote(OpenIdConnectProtocol.STATE_PARAM), clientSession.getRedirectUri()); } } - private void initEvent(AccessCode accessCode) { - event.event(EventType.LOGIN).client(accessCode.getClient()) - .user(accessCode.getUser()) - .session(accessCode.getSessionState()) - .detail(Details.CODE_ID, accessCode.getCodeId()) - .detail(Details.REDIRECT_URI, accessCode.getRedirectUri()) + private void initEvent(ClientSessionModel clientSession) { + event.event(EventType.LOGIN).client(clientSession.getClient()) + .user(clientSession.getUserSession().getUser()) + .session(clientSession.getUserSession().getId()) + .detail(Details.CODE_ID, clientSession.getId()) + .detail(Details.REDIRECT_URI, clientSession.getRedirectUri()) .detail(Details.RESPONSE_TYPE, "code"); - UserSessionModel userSession = accessCode.getSessionState() != null ? session.sessions().getUserSession(realm, accessCode.getSessionState()) : null; + UserSessionModel userSession = clientSession.getUserSession(); if (userSession != null) { event.detail(Details.AUTH_METHOD, userSession.getAuthMethod()); @@ -402,9 +492,4 @@ public class RequiredActionsService { } } } - - private Response unauthorized() { - return Flows.forms(session, realm, null, uriInfo).setError("Unauthorized request").createErrorPage(); - } - } diff --git a/services/src/main/java/org/keycloak/services/resources/SocialResource.java b/services/src/main/java/org/keycloak/services/resources/SocialResource.java index 07a5b9dfe8..7a8733a504 100755 --- a/services/src/main/java/org/keycloak/services/resources/SocialResource.java +++ b/services/src/main/java/org/keycloak/services/resources/SocialResource.java @@ -33,6 +33,7 @@ import org.keycloak.events.EventType; import org.keycloak.jose.jws.JWSInput; import org.keycloak.models.AccountRoles; import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientSessionModel; import org.keycloak.models.Constants; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ModelDuplicateException; @@ -41,6 +42,7 @@ import org.keycloak.models.SocialLinkModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.services.managers.ClientSessionCode; import org.keycloak.services.managers.EventsManager; import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.RealmManager; @@ -106,76 +108,55 @@ public class SocialResource { @GET @Path("callback") public Response callback(@QueryParam("state") String encodedState) throws URISyntaxException, IOException { - State initialRequest; + ClientSessionCode clientCode = null; + ClientSessionModel clientSession = null; try { - initialRequest = new JWSInput(encodedState).readJsonContent(State.class); + clientCode = ClientSessionCode.parse(encodedState, session); + if (clientCode == null) { + return Flows.forms(session, null, null, uriInfo).setError("Unexpected callback").createErrorPage(); + } + clientSession = clientCode.getClientSession(); + if (!clientCode.isValid(ClientSessionModel.Action.SOCIAL_CALLBACK)) { + return Flows.forwardToSecurityFailurePage(session, clientSession.getRealm(), uriInfo, "Invalid code, please login again through your application."); + } } catch (Throwable t) { logger.error("Invalid social callback", t); return Flows.forms(session, null, null, uriInfo).setError("Unexpected callback").createErrorPage(); } + String providerId = clientSession.getNote("social_provider"); + SocialProvider provider = SocialLoader.load(providerId); - SocialProvider provider = SocialLoader.load(initialRequest.getProvider()); - - String realmName = initialRequest.getRealm(); String authMethod = "social@" + provider.getId(); - RealmManager realmManager = new RealmManager(session); - RealmModel realm = realmManager.getRealmByName(realmName); + RealmModel realm = clientSession.getRealm(); EventBuilder event = new EventsManager(realm, session, clientConnection).createEventBuilder() - .event(EventType.LOGIN) - .detail(Details.RESPONSE_TYPE, initialRequest.get(OAuth2Constants.RESPONSE_TYPE)) + .event(EventType.SOCIAL_LOGIN) + .client(clientSession.getClient()) + .detail(Details.REDIRECT_URI, clientSession.getRedirectUri()) .detail(Details.AUTH_METHOD, authMethod); - AuthenticationManager authManager = new AuthenticationManager(); - OAuthFlows oauth = Flows.oauth(session, realm, request, uriInfo, clientConnection, authManager, tokenManager); - if (!realm.isEnabled()) { event.error(Errors.REALM_DISABLED); return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Realm not enabled."); } - String clientId = initialRequest.get(OAuth2Constants.CLIENT_ID); - String redirectUri = initialRequest.get(OAuth2Constants.REDIRECT_URI); - String scope = initialRequest.get(OAuth2Constants.SCOPE); - String state = initialRequest.get(OAuth2Constants.STATE); - String responseType = initialRequest.get(OAuth2Constants.RESPONSE_TYPE); - - event.client(clientId).detail(Details.REDIRECT_URI, redirectUri); - - ClientModel client = realm.findClient(clientId); - if (client == null) { - event.error(Errors.CLIENT_NOT_FOUND); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Unknown login requester."); - } - if (!client.isEnabled()) { - event.error(Errors.CLIENT_DISABLED); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Login requester not enabled."); - } - String key = realm.getSocialConfig().get(provider.getId() + ".key"); String secret = realm.getSocialConfig().get(provider.getId() + ".secret"); String callbackUri = Urls.socialCallback(uriInfo.getBaseUri()).toString(); SocialProviderConfig config = new SocialProviderConfig(key, secret, callbackUri); Map queryParams = getQueryParams(); - Map attributes = getAttributes(); - AuthCallback callback = new AuthCallback(queryParams, attributes); + AuthCallback callback = new AuthCallback(queryParams); SocialUser socialUser; try { - socialUser = provider.processCallback(config, callback); + socialUser = provider.processCallback(clientSession, config, callback); } catch (SocialAccessDeniedException e) { - MultivaluedMap queryParms = new MultivaluedMapImpl(); - queryParms.putSingle(OAuth2Constants.CLIENT_ID, clientId); - queryParms.putSingle(OAuth2Constants.STATE, state); - queryParms.putSingle(OAuth2Constants.SCOPE, scope); - queryParms.putSingle(OAuth2Constants.REDIRECT_URI, redirectUri); - queryParms.putSingle(OAuth2Constants.RESPONSE_TYPE, responseType); - event.error(Errors.REJECTED_BY_USER); - return Flows.forms(session, realm, client, uriInfo).setQueryParams(queryParms).setWarning("Access denied").createLogin(); + clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE); + return Flows.forms(session, realm, clientSession.getClient(), uriInfo).setAccessCode(clientCode.getCode()).setWarning("Access denied").createLogin(); } catch (SocialProviderException e) { logger.error("Failed to process social callback", e); return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Failed to process social callback"); @@ -188,11 +169,11 @@ public class SocialResource { UserModel user = session.users().getUserBySocialLink(socialLink, realm); // Check if user is already authenticated (this means linking social into existing user account) - String userId = initialRequest.getUser(); - if (userId != null) { - UserModel authenticatedUser = session.users().getUserById(userId, realm); + if (clientSession.getUserSession() != null) { - event.event(EventType.SOCIAL_LINK).user(userId); + UserModel authenticatedUser = clientSession.getUserSession().getUser(); + + event.event(EventType.SOCIAL_LINK).user(authenticatedUser.getId()); if (user != null) { event.error(Errors.SOCIAL_ID_IN_USE); @@ -209,16 +190,11 @@ public class SocialResource { return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Insufficient permissions to link social account"); } - if (redirectUri == null) { - event.error(Errors.INVALID_REDIRECT_URI); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Unknown redirectUri"); - } - session.users().addSocialLink(realm, authenticatedUser, socialLink); logger.debugv("Social provider {0} linked with user {1}", provider.getId(), authenticatedUser.getUsername()); event.success(); - return Response.status(302).location(UriBuilder.fromUri(redirectUri).build()).build(); + return Response.status(302).location(UriBuilder.fromUri(clientSession.getRedirectUri()).build()).build(); } if (user == null) { @@ -252,31 +228,72 @@ public class SocialResource { UserSessionModel userSession = session.sessions().createUserSession(realm, user, username, clientConnection.getRemoteAddr(), authMethod, false); event.session(userSession); + TokenManager.attachClientSession(userSession, clientSession); - Response response = oauth.processAccessCode(scope, state, redirectUri, client, user, userSession, event); + OAuthFlows oauth = Flows.oauth(session, realm, request, uriInfo, clientConnection, new AuthenticationManager(), tokenManager); + Response response = oauth.processAccessCode(clientSession, userSession, event); if (session.getTransaction().isActive()) { session.getTransaction().commit(); } return response; } catch (ModelDuplicateException e) { // Assume email is the duplicate as there's nothing else atm - return returnToLogin(realm, client, initialRequest.getAttributes(), "socialEmailExists"); + return Flows.forms(session, realm, clientSession.getClient(), uriInfo) + .setAccessCode(clientCode.getCode()) + .setError("socialEmailExists") + .createLogin(); } } + private class Checks { + ClientSessionCode clientCode; + Response response; + + private boolean checkSsl(RealmModel realm) { + if (uriInfo.getBaseUri().getScheme().equals("https")) { + return true; + } else { + return !realm.getSslRequired().isRequired(clientConnection); + } + } + + + boolean check(EventBuilder event, RealmModel realm, String code, ClientSessionModel.Action requiredAction) { + if (!checkSsl(realm)) { + event.error(Errors.SSL_REQUIRED); + response = Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "HTTPS required"); + return false; + } + if (!realm.isEnabled()) { + event.error(Errors.REALM_DISABLED); + response = Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Realm not enabled."); + return false; + } + clientCode = ClientSessionCode.parse(code, session, realm); + if (clientCode == null) { + event.error(Errors.INVALID_CODE); + response = Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Unknown code, please login again through your application."); + return false; + } + if (!clientCode.isValid(requiredAction)) { + event.error(Errors.INVALID_CODE); + response = Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Invalid code, please login again through your application."); + } + return true; + } + } + + @GET @Path("{realm}/login") public Response redirectToProviderAuth(@PathParam("realm") final String realmName, - @QueryParam("provider_id") final String providerId, @QueryParam(OAuth2Constants.CLIENT_ID) final String clientId, - @QueryParam("scope") final String scope, @QueryParam("state") final String state, - @QueryParam("redirect_uri") String redirectUri, @QueryParam("response_type") String responseType) { + @QueryParam("provider_id") final String providerId, + @QueryParam("code") String code) { RealmManager realmManager = new RealmManager(session); RealmModel realm = realmManager.getRealmByName(realmName); EventBuilder event = new EventsManager(realm, session, clientConnection).createEventBuilder() - .event(EventType.LOGIN).client(clientId) - .detail(Details.REDIRECT_URI, redirectUri) - .detail(Details.RESPONSE_TYPE, "code") + .event(EventType.LOGIN) .detail(Details.AUTH_METHOD, "social@" + providerId); SocialProvider provider = SocialLoader.load(providerId); @@ -285,30 +302,16 @@ public class SocialResource { return Flows.forms(session, realm, null, uriInfo).setError("Social provider not found").createErrorPage(); } - ClientModel client = realm.findClient(clientId); - if (client == null) { - event.error(Errors.CLIENT_NOT_FOUND); - return Flows.forms(session, realm, null, uriInfo).setError("Unknown login requester.").createErrorPage(); + Checks checks = new Checks(); + if (!checks.check(event, realm, code, ClientSessionModel.Action.AUTHENTICATE)) { + return checks.response; } - if (!client.isEnabled()) { - event.error(Errors.CLIENT_DISABLED); - return Flows.forms(session, realm, null, uriInfo).setError("Login requester not enabled.").createErrorPage(); - } - redirectUri = TokenService.verifyRedirectUri(uriInfo, redirectUri, realm, client); - if (redirectUri == null) { - event.error(Errors.INVALID_REDIRECT_URI); - return Flows.forms(session, realm, null, uriInfo).setError("Invalid redirect_uri.").createErrorPage(); - } + ClientSessionCode clientSessionCode = checks.clientCode; try { return Flows.social(realm, uriInfo, clientConnection, provider) - .putClientAttribute(OAuth2Constants.CLIENT_ID, clientId) - .putClientAttribute(OAuth2Constants.SCOPE, scope) - .putClientAttribute(OAuth2Constants.STATE, state) - .putClientAttribute(OAuth2Constants.REDIRECT_URI, redirectUri) - .putClientAttribute(OAuth2Constants.RESPONSE_TYPE, responseType) - .redirectToSocialProvider(); + .redirectToSocialProvider(clientSessionCode); } catch (Throwable t) { logger.error("Failed to redirect to social auth", t); return Flows.forms(session, realm, null, uriInfo).setError("Failed to redirect to social auth").createErrorPage(); @@ -334,52 +337,4 @@ public class SocialResource { return queryParams; } - private Map getAttributes() throws IOException { - Cookie cookie = headers.getCookies().get("KEYCLOAK_SOCIAL"); - return cookie != null ? new JWSInput(cookie.getValue()).readJsonContent(HashMap.class) : null; - } - - public static class State { - private String realm; - private String provider; - private String user; - private Map attributes = new HashMap(); - - public String getRealm() { - return realm; - } - - public void setRealm(String realm) { - this.realm = realm; - } - - public String getProvider() { - return provider; - } - - public void setProvider(String provider) { - this.provider = provider; - } - - public String getUser() { - return user; - } - - public void setUser(String user) { - this.user = user; - } - - public Map getAttributes() { - return attributes; - } - - public String get(String key) { - return attributes.get(key); - } - - public void set(String key, String value) { - attributes.put(key, value); - } - } - } 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 52e3704c10..55a320c9ab 100755 --- a/services/src/main/java/org/keycloak/services/resources/TokenService.java +++ b/services/src/main/java/org/keycloak/services/resources/TokenService.java @@ -36,13 +36,14 @@ import org.keycloak.representations.AccessTokenResponse; import org.keycloak.representations.RefreshToken; import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.services.ForbiddenException; -import org.keycloak.services.managers.AccessCode; import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationManager.AuthenticationStatus; import org.keycloak.representations.PasswordToken; +import org.keycloak.services.managers.ClientSessionCode; import org.keycloak.services.managers.ResourceAdminManager; import org.keycloak.services.managers.TokenManager; import org.keycloak.services.messages.Messages; +import org.keycloak.services.protocol.OpenIdConnectProtocol; import org.keycloak.services.resources.flows.Flows; import org.keycloak.services.resources.flows.OAuthFlows; import org.keycloak.services.resources.flows.Urls; @@ -462,26 +463,46 @@ public class TokenService { /** * URL called after login page. YOU SHOULD NEVER INVOKE THIS DIRECTLY! * - * @param clientId - * @param scopeParam - * @param state - * @param redirect + * @param code * @param formData * @return */ @Path("auth/request/login") @POST @Consumes(MediaType.APPLICATION_FORM_URLENCODED) - public Response processLogin(@QueryParam("client_id") final String clientId, @QueryParam("scope") final String scopeParam, - @QueryParam("state") final String state, @QueryParam("redirect_uri") String redirect, + public Response processLogin(@QueryParam("code") String code, final MultivaluedMap formData) { + event.event(EventType.LOGIN); + if (!checkSsl()) { + event.error(Errors.SSL_REQUIRED); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "HTTPS required"); + } + + if (!realm.isEnabled()) { + event.error(Errors.REALM_DISABLED); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Realm not enabled."); + } + ClientSessionCode clientCode = ClientSessionCode.parse(code, session, realm); + if (clientCode == null) { + event.error(Errors.INVALID_CODE); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Unknown code, please login again through your application."); + } + ClientSessionModel clientSession = clientCode.getClientSession(); + if (!clientCode.isValid(ClientSessionModel.Action.AUTHENTICATE)) { + clientCode.setAction(ClientSessionModel.Action.AUTHENTICATE); + event.client(clientSession.getClient()).error(Errors.INVALID_USER_CREDENTIALS); + return Flows.forms(this.session, realm, clientSession.getClient(), uriInfo).setError(Messages.INVALID_USER) + .setAccessCode(clientCode.getCode()) + .createLogin(); + } + String username = formData.getFirst(AuthenticationManager.FORM_USERNAME); String rememberMe = formData.getFirst("rememberMe"); boolean remember = rememberMe != null && rememberMe.equalsIgnoreCase("on"); - event.event(EventType.LOGIN).client(clientId) - .detail(Details.REDIRECT_URI, redirect) + event.client(clientSession.getClient().getClientId()) + .detail(Details.REDIRECT_URI, clientSession.getRedirectUri()) .detail(Details.RESPONSE_TYPE, "code") .detail(Details.AUTH_METHOD, "form") .detail(Details.USERNAME, username); @@ -492,15 +513,7 @@ public class TokenService { OAuthFlows oauth = Flows.oauth(session, realm, request, uriInfo, clientConnection, authManager, tokenManager); - if (!checkSsl()) { - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "HTTPS required"); - } - - if (!realm.isEnabled()) { - event.error(Errors.REALM_DISABLED); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Realm not enabled."); - } - ClientModel client = realm.findClient(clientId); + ClientModel client = clientSession.getClient(); if (client == null) { event.error(Errors.CLIENT_NOT_FOUND); return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Unknown login requester."); @@ -510,15 +523,9 @@ public class TokenService { return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Login requester not enabled."); } - redirect = verifyRedirectUri(uriInfo, redirect, realm, client); - if (redirect == null) { - event.error(Errors.INVALID_REDIRECT_URI); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Invalid redirect_uri."); - } - if (formData.containsKey("cancel")) { event.error(Errors.REJECTED_BY_USER); - return oauth.redirectError(client, "access_denied", state, redirect); + return oauth.redirectError(client, "access_denied", clientSession.getNote(OpenIdConnectProtocol.STATE_PARAM), clientSession.getRedirectUri()); } AuthenticationStatus status = authManager.authenticateForm(session, clientConnection, realm, formData); @@ -538,28 +545,45 @@ public class TokenService { case SUCCESS: case ACTIONS_REQUIRED: UserSessionModel userSession = session.sessions().createUserSession(realm, user, username, clientConnection.getRemoteAddr(), "form", remember); + TokenManager.attachClientSession(userSession, clientSession); event.session(userSession); - return oauth.processAccessCode(scopeParam, state, redirect, client, user, userSession, event); + return oauth.processAccessCode(clientSession, userSession, event); case ACCOUNT_TEMPORARILY_DISABLED: event.error(Errors.USER_TEMPORARILY_DISABLED); - return Flows.forms(this.session, realm, client, uriInfo).setError(Messages.ACCOUNT_TEMPORARILY_DISABLED).setFormData(formData).createLogin(); + return Flows.forms(this.session, realm, client, uriInfo) + .setError(Messages.ACCOUNT_TEMPORARILY_DISABLED) + .setFormData(formData) + .setAccessCode(clientCode.getCode()) + .createLogin(); case ACCOUNT_DISABLED: event.error(Errors.USER_DISABLED); - return Flows.forms(this.session, realm, client, uriInfo).setError(Messages.ACCOUNT_DISABLED).setFormData(formData).createLogin(); + return Flows.forms(this.session, realm, client, uriInfo) + .setError(Messages.ACCOUNT_DISABLED) + .setAccessCode(clientCode.getCode()) + .setFormData(formData).createLogin(); case MISSING_TOTP: formData.remove(CredentialRepresentation.PASSWORD); String passwordToken = new JWSBuilder().jsonContent(new PasswordToken(realm.getName(), user.getId())).rsa256(realm.getPrivateKey()); formData.add(CredentialRepresentation.PASSWORD_TOKEN, passwordToken); - return Flows.forms(this.session, realm, client, uriInfo).setFormData(formData).createLoginTotp(); + return Flows.forms(this.session, realm, client, uriInfo) + .setFormData(formData) + .setAccessCode(clientCode.getCode()) + .createLoginTotp(); case INVALID_USER: event.error(Errors.USER_NOT_FOUND); - return Flows.forms(this.session, realm, client, uriInfo).setError(Messages.INVALID_USER).setFormData(formData).createLogin(); + return Flows.forms(this.session, realm, client, uriInfo).setError(Messages.INVALID_USER) + .setFormData(formData) + .setAccessCode(clientCode.getCode()) + .createLogin(); default: event.error(Errors.INVALID_USER_CREDENTIALS); - return Flows.forms(this.session, realm, client, uriInfo).setError(Messages.INVALID_USER).setFormData(formData).createLogin(); + return Flows.forms(this.session, realm, client, uriInfo).setError(Messages.INVALID_USER) + .setFormData(formData) + .setAccessCode(clientCode.getCode()) + .createLogin(); } } @@ -575,25 +599,44 @@ public class TokenService { /** * Registration * - * @param clientId - * @param scopeParam - * @param state - * @param redirect + * @param code * @param formData * @return */ @Path("registrations") @POST @Consumes(MediaType.APPLICATION_FORM_URLENCODED) - public Response processRegister(@QueryParam("client_id") final String clientId, - @QueryParam("scope") final String scopeParam, @QueryParam("state") final String state, - @QueryParam("redirect_uri") String redirect, final MultivaluedMap formData) { + public Response processRegister(@QueryParam("code") String code, + final MultivaluedMap formData) { + event.event(EventType.REGISTER); + if (!checkSsl()) { + event.error(Errors.SSL_REQUIRED); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "HTTPS required"); + } + + if (!realm.isEnabled()) { + event.error(Errors.REALM_DISABLED); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Realm not enabled."); + } + if (!realm.isRegistrationAllowed()) { + event.error(Errors.REGISTRATION_DISABLED); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Registration not allowed"); + } + ClientSessionCode clientCode = ClientSessionCode.parse(code, session, realm); + if (clientCode == null) { + event.error(Errors.INVALID_CODE); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Unknown code, please login again through your application."); + } + if (!clientCode.isValid(ClientSessionModel.Action.AUTHENTICATE)) { + event.error(Errors.INVALID_CODE); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Invalid code, please login again through your application."); + } String username = formData.getFirst("username"); String email = formData.getFirst("email"); - - event.event(EventType.REGISTER).client(clientId) - .detail(Details.REDIRECT_URI, redirect) + ClientSessionModel clientSession = clientCode.getClientSession(); + event.client(clientSession.getClient()) + .detail(Details.REDIRECT_URI, clientSession.getRedirectUri()) .detail(Details.RESPONSE_TYPE, "code") .detail(Details.USERNAME, username) .detail(Details.EMAIL, email) @@ -605,7 +648,7 @@ public class TokenService { event.error(Errors.REALM_DISABLED); return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Realm not enabled"); } - ClientModel client = realm.findClient(clientId); + ClientModel client = clientSession.getClient(); if (client == null) { event.error(Errors.CLIENT_NOT_FOUND); return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Unknown login requester."); @@ -616,16 +659,6 @@ public class TokenService { return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Login requester not enabled."); } - redirect = verifyRedirectUri(uriInfo, redirect, realm, client); - if (redirect == null) { - event.error(Errors.INVALID_REDIRECT_URI); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Invalid redirect_uri."); - } - - if (!realm.isRegistrationAllowed()) { - event.error(Errors.REGISTRATION_DISABLED); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Registration not allowed"); - } List requiredCredentialTypes = new LinkedList(); for (RequiredCredentialModel m : realm.getRequiredCredentials()) { @@ -640,19 +673,31 @@ public class TokenService { if (error != null) { event.error(Errors.INVALID_REGISTRATION); - return Flows.forms(session, realm, client, uriInfo).setError(error).setFormData(formData).createRegistration(); + return Flows.forms(session, realm, client, uriInfo) + .setError(error) + .setFormData(formData) + .setAccessCode(clientCode.getCode()) + .createRegistration(); } // Validate that user with this username doesn't exist in realm or any federation provider if (session.users().getUserByUsername(username, realm) != null) { event.error(Errors.USERNAME_IN_USE); - return Flows.forms(session, realm, client, uriInfo).setError(Messages.USERNAME_EXISTS).setFormData(formData).createRegistration(); + return Flows.forms(session, realm, client, uriInfo) + .setError(Messages.USERNAME_EXISTS) + .setFormData(formData) + .setAccessCode(clientCode.getCode()) + .createRegistration(); } // Validate that user with this email doesn't exist in realm or any federation provider if (session.users().getUserByEmail(email, realm) != null) { event.error(Errors.EMAIL_IN_USE); - return Flows.forms(session, realm, client, uriInfo).setError(Messages.EMAIL_EXISTS).setFormData(formData).createRegistration(); + return Flows.forms(session, realm, client, uriInfo) + .setError(Messages.EMAIL_EXISTS) + .setFormData(formData) + .setAccessCode(clientCode.getCode()) + .createRegistration(); } UserModel user = session.users().addUser(realm, username); @@ -680,14 +725,17 @@ public class TokenService { // User already registered, but force him to update password if (!passwordUpdateSuccessful) { user.addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD); - return Flows.forms(session, realm, client, uriInfo).setError(passwordUpdateError).createResponse(UserModel.RequiredAction.UPDATE_PASSWORD); + return Flows.forms(session, realm, client, uriInfo) + .setError(passwordUpdateError) + .setAccessCode(clientCode.getCode()) + .createResponse(UserModel.RequiredAction.UPDATE_PASSWORD); } } event.user(user).success(); event.reset(); - return processLogin(clientId, scopeParam, state, redirect, formData); + return processLogin(code, formData); } /** @@ -737,8 +785,7 @@ public class TokenService { event.error(Errors.INVALID_CODE); throw new BadRequestException("Code not specified", Response.status(Response.Status.BAD_REQUEST).entity(error).type("application/json").build()); } - - AccessCode accessCode = AccessCode.parse(code, session, realm); + ClientSessionCode accessCode = ClientSessionCode.parse(code, session, realm); if (accessCode == null) { String[] parts = code.split("\\."); if (parts.length == 2) { @@ -754,7 +801,8 @@ public class TokenService { return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(res) .build(); } - event.detail(Details.CODE_ID, accessCode.getCodeId()); + ClientSessionModel clientSession = accessCode.getClientSession(); + event.detail(Details.CODE_ID, clientSession.getId()); if (!accessCode.isValid(ClientSessionModel.Action.CODE_TO_TOKEN)) { Map res = new HashMap(); res.put(OAuth2Constants.ERROR, "invalid_grant"); @@ -765,13 +813,13 @@ public class TokenService { } accessCode.setAction(null); - - event.user(accessCode.getUser()); - event.session(accessCode.getSessionState()); + UserSessionModel userSession = clientSession.getUserSession(); + event.user(userSession.getUser()); + event.session(userSession.getId()); ClientModel client = authorizeClient(authorizationHeader, formData, event); - if (!client.getClientId().equals(accessCode.getClient().getClientId())) { + if (!client.getClientId().equals(clientSession.getClient().getClientId())) { Map res = new HashMap(); res.put(OAuth2Constants.ERROR, "invalid_grant"); res.put(OAuth2Constants.ERROR_DESCRIPTION, "Auth error"); @@ -780,7 +828,7 @@ public class TokenService { .build(); } - UserModel user = session.users().getUserById(accessCode.getUser().getId(), realm); + UserModel user = session.users().getUserById(userSession.getUser().getId(), realm); if (user == null) { Map res = new HashMap(); res.put(OAuth2Constants.ERROR, "invalid_grant"); @@ -799,7 +847,6 @@ public class TokenService { .build(); } - UserSessionModel userSession = session.sessions().getUserSession(realm, accessCode.getSessionState()); if (!AuthenticationManager.isSessionValid(realm, userSession)) { AuthenticationManager.logout(session, realm, userSession, uriInfo, clientConnection); Map res = new HashMap(); @@ -892,12 +939,107 @@ public class TokenService { return client; } + /** + * checks input and initializes variables based on a front page action like the login page or registration page + * + */ + private class FrontPageInitializer { + protected String code; + protected String clientId; + protected String redirect; + protected String state; + protected String scopeParam; + protected String responseType; + protected String loginHint; + protected String prompt; + protected ClientSessionModel clientSession; + + public Response processInput() { + if (code != null) { + event.detail(Details.CODE_ID, code); + } else { + event.client(clientId).detail(Details.REDIRECT_URI, redirect).detail(Details.RESPONSE_TYPE, "code"); + } + if (!checkSsl()) { + event.error(Errors.SSL_REQUIRED); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "HTTPS required"); + } + if (!realm.isEnabled()) { + event.error(Errors.REALM_DISABLED); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Realm not enabled"); + } + + clientSession = null; + if (code != null) { + ClientSessionCode clientCode = ClientSessionCode.parse(code, session, realm); + if (clientCode == null) { + event.error(Errors.INVALID_CODE); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Unknown code, please login again through your application."); + } + if (!clientCode.isValid(ClientSessionModel.Action.AUTHENTICATE)) { + event.error(Errors.INVALID_CODE); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Invalid code, please login again through your application."); + } + clientSession = clientCode.getClientSession(); + if (!clientSession.getAuthMethod().equals(OpenIdConnectProtocol.LOGIN_PAGE_PROTOCOL)) { + event.error(Errors.INVALID_CODE); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Invalid protocol, please login again through your application."); + } + state = clientSession.getNote(OpenIdConnectProtocol.STATE_PARAM); + scopeParam = clientSession.getNote(OpenIdConnectProtocol.SCOPE_PARAM); + responseType = clientSession.getNote(OpenIdConnectProtocol.RESPONSE_TYPE_PARAM); + loginHint = clientSession.getNote(OpenIdConnectProtocol.LOGIN_HINT_PARAM); + prompt = clientSession.getNote(OpenIdConnectProtocol.PROMPT_PARAM); + } else { + if (state == null) { + event.error(Errors.STATE_PARAM_NOT_FOUND); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Invalid state param."); + + } + ClientModel client = realm.findClient(clientId); + if (client == null) { + event.error(Errors.CLIENT_NOT_FOUND); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Unknown login requester."); + } + + if (!client.isEnabled()) { + event.error(Errors.CLIENT_DISABLED); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Login requester not enabled."); + } + if ((client instanceof ApplicationModel) && ((ApplicationModel)client).isBearerOnly()) { + event.error(Errors.NOT_ALLOWED); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Bearer-only applications are not allowed to initiate browser login"); + } + if (client.isDirectGrantsOnly()) { + event.error(Errors.NOT_ALLOWED); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "direct-grants-only clients are not allowed to initiate browser login"); + } + redirect = verifyRedirectUri(uriInfo, redirect, realm, client); + if (redirect == null) { + event.error(Errors.INVALID_REDIRECT_URI); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Invalid redirect_uri."); + } + clientSession = session.sessions().createClientSession(realm, client); + clientSession.setAuthMethod(OpenIdConnectProtocol.LOGIN_PAGE_PROTOCOL); + clientSession.setRedirectUri(redirect); + clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE); + clientSession.setNote(OpenIdConnectProtocol.STATE_PARAM, state); + if (scopeParam != null) clientSession.setNote(OpenIdConnectProtocol.SCOPE_PARAM, scopeParam); + if (responseType != null) clientSession.setNote(OpenIdConnectProtocol.RESPONSE_TYPE_PARAM, responseType); + if (loginHint != null) clientSession.setNote(OpenIdConnectProtocol.LOGIN_HINT_PARAM, loginHint); + if (prompt != null) clientSession.setNote(OpenIdConnectProtocol.PROMPT_PARAM, prompt); + } + return null; + } + } + /** * Login page. Must be redirected to from the application or oauth client. * * @See http://tools.ietf.org/html/rfc6749#section-4.1 * * + * @param code * @param responseType * @param redirect * @param clientId @@ -908,60 +1050,54 @@ public class TokenService { */ @Path("login") @GET - public Response loginPage(final @QueryParam("response_type") String responseType, - @QueryParam("redirect_uri") String redirect, final @QueryParam("client_id") String clientId, - final @QueryParam("scope") String scopeParam, final @QueryParam("state") String state, final @QueryParam("prompt") String prompt, - final @QueryParam("login_hint") String loginHint) { - event.event(EventType.LOGIN).client(clientId).detail(Details.REDIRECT_URI, redirect).detail(Details.RESPONSE_TYPE, "code"); + public Response loginPage(@QueryParam("code") String code, + @QueryParam(OpenIdConnectProtocol.RESPONSE_TYPE_PARAM) String responseType, + @QueryParam(OpenIdConnectProtocol.REDIRECT_URI_PARAM) String redirect, + @QueryParam(OpenIdConnectProtocol.CLIENT_ID_PARAM) String clientId, + @QueryParam(OpenIdConnectProtocol.SCOPE_PARAM) String scopeParam, + @QueryParam(OpenIdConnectProtocol.STATE_PARAM) String state, + @QueryParam(OpenIdConnectProtocol.PROMPT_PARAM) String prompt, + @QueryParam(OpenIdConnectProtocol.LOGIN_HINT_PARAM) String loginHint) { + event.event(EventType.LOGIN); + FrontPageInitializer pageInitializer = new FrontPageInitializer(); + pageInitializer.code = code; + pageInitializer.responseType = responseType; + pageInitializer.redirect = redirect; + pageInitializer.clientId = clientId; + pageInitializer.scopeParam = scopeParam; + pageInitializer.state = state; + pageInitializer.prompt = prompt; + pageInitializer.loginHint = loginHint; + Response response = pageInitializer.processInput(); + if (response != null) return response; + ClientSessionModel clientSession = pageInitializer.clientSession; + code = pageInitializer.code; + responseType = pageInitializer.responseType; + redirect = pageInitializer.redirect; + clientId = pageInitializer.clientId ; + scopeParam = pageInitializer.scopeParam; + state = pageInitializer.state; + prompt = pageInitializer.prompt; + loginHint = pageInitializer.loginHint; + OAuthFlows oauth = Flows.oauth(session, realm, request, uriInfo, clientConnection, authManager, tokenManager); - if (!checkSsl()) { - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "HTTPS required"); - } - - if (!realm.isEnabled()) { - event.error(Errors.REALM_DISABLED); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Realm not enabled"); - } - ClientModel client = realm.findClient(clientId); - if (client == null) { - event.error(Errors.CLIENT_NOT_FOUND); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Unknown login requester."); - } - - if (!client.isEnabled()) { - event.error(Errors.CLIENT_DISABLED); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Login requester not enabled."); - } - if ( (client instanceof ApplicationModel) && ((ApplicationModel)client).isBearerOnly()) { - event.error(Errors.NOT_ALLOWED); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Bearer-only applications are not allowed to initiate browser login"); - } - if (client.isDirectGrantsOnly()) { - event.error(Errors.NOT_ALLOWED); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "direct-grants-only clients are not allowed to initiate browser login"); - } - redirect = verifyRedirectUri(uriInfo, redirect, realm, client); - if (redirect == null) { - event.error(Errors.INVALID_REDIRECT_URI); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Invalid redirect_uri."); - } - AuthenticationManager.AuthResult authResult = authManager.authenticateIdentityCookie(session, realm, uriInfo, clientConnection, headers); if (authResult != null) { UserModel user = authResult.getUser(); UserSessionModel session = authResult.getSession(); - + TokenManager.attachClientSession(session, clientSession); event.user(user).session(session).detail(Details.AUTH_METHOD, "sso"); - return oauth.processAccessCode(scopeParam, state, redirect, client, user, session, event); + return oauth.processAccessCode(clientSession, session, event); } if (prompt != null && prompt.equals("none")) { - return oauth.redirectError(client, "access_denied", state, redirect); + return oauth.redirectError(clientSession.getClient(), "access_denied", state, redirect); } - LoginFormsProvider forms = Flows.forms(session, realm, client, uriInfo); + LoginFormsProvider forms = Flows.forms(session, realm, clientSession.getClient(), uriInfo) + .setAccessCode(new ClientSessionCode(realm, clientSession).getCode()); String rememberMeUsername = null; if (realm.isRememberMe()) { @@ -999,46 +1135,35 @@ public class TokenService { */ @Path("registrations") @GET - public Response registerPage(final @QueryParam("response_type") String responseType, - @QueryParam("redirect_uri") String redirect, final @QueryParam("client_id") String clientId, - final @QueryParam("scope") String scopeParam, final @QueryParam("state") String state) { - event.event(EventType.REGISTER).client(clientId).detail(Details.REDIRECT_URI, redirect).detail(Details.RESPONSE_TYPE, "code"); - - OAuthFlows oauth = Flows.oauth(session, realm, request, uriInfo, clientConnection, authManager, tokenManager); - - if (!checkSsl()) { - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "HTTPS required"); - } - - if (!realm.isEnabled()) { - event.error(Errors.REALM_DISABLED); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Realm not enabled"); - } - ClientModel client = realm.findClient(clientId); - if (client == null) { - event.error(Errors.CLIENT_NOT_FOUND); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Unknown login requester."); - } - - if (!client.isEnabled()) { - event.error(Errors.CLIENT_DISABLED); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Login requester not enabled."); - } - - redirect = verifyRedirectUri(uriInfo, redirect, realm, client); - if (redirect == null) { - event.error(Errors.INVALID_REDIRECT_URI); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Invalid redirect_uri."); - } - + public Response registerPage(@QueryParam("code") String code, + @QueryParam("response_type") String responseType, + @QueryParam(OpenIdConnectProtocol.REDIRECT_URI_PARAM) String redirect, + @QueryParam(OpenIdConnectProtocol.CLIENT_ID_PARAM) String clientId, + @QueryParam("scope") String scopeParam, + @QueryParam("state") String state) { + event.event(EventType.REGISTER); if (!realm.isRegistrationAllowed()) { event.error(Errors.REGISTRATION_DISABLED); return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Registration not allowed"); } + FrontPageInitializer pageInitializer = new FrontPageInitializer(); + pageInitializer.code = code; + pageInitializer.responseType = responseType; + pageInitializer.redirect = redirect; + pageInitializer.clientId = clientId; + pageInitializer.scopeParam = scopeParam; + pageInitializer.state = state; + Response response = pageInitializer.processInput(); + if (response != null) return response; + ClientSessionModel clientSession = pageInitializer.clientSession; + + authManager.expireIdentityCookie(realm, uriInfo, clientConnection); - return Flows.forms(session, realm, client, uriInfo).createRegistration(); + return Flows.forms(session, realm, clientSession.getClient(), uriInfo) + .setAccessCode(new ClientSessionCode(realm, clientSession).getCode()) + .createRegistration(); } /** @@ -1050,7 +1175,7 @@ public class TokenService { @Path("logout") @GET @NoCache - public Response logout(final @QueryParam("redirect_uri") String redirectUri) { + public Response logout(final @QueryParam(OpenIdConnectProtocol.REDIRECT_URI_PARAM) String redirectUri) { event.event(EventType.LOGOUT); if (redirectUri != null) { event.detail(Details.REDIRECT_URI, redirectUri); @@ -1150,22 +1275,22 @@ public class TokenService { String code = formData.getFirst(OAuth2Constants.CODE); - AccessCode accessCode = AccessCode.parse(code, session, realm); + ClientSessionCode accessCode = ClientSessionCode.parse(code, session, realm); if (accessCode == null || !accessCode.isValid(ClientSessionModel.Action.OAUTH_GRANT)) { event.error(Errors.INVALID_CODE); return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Invalid access code."); } - event.detail(Details.CODE_ID, accessCode.getCodeId()); + ClientSessionModel clientSession = accessCode.getClientSession(); + event.detail(Details.CODE_ID, clientSession.getId()); - String redirect = accessCode.getRedirectUri(); - String state = accessCode.getState(); + String redirect = clientSession.getRedirectUri(); - event.client(accessCode.getClient()) - .user(accessCode.getUser()) + event.client(clientSession.getClient()) + .user(clientSession.getUserSession().getUser()) .detail(Details.RESPONSE_TYPE, "code") .detail(Details.REDIRECT_URI, redirect); - UserSessionModel userSession = session.sessions().getUserSession(realm, accessCode.getSessionState()); + UserSessionModel userSession = clientSession.getUserSession(); if (userSession != null) { event.detail(Details.AUTH_METHOD, userSession.getAuthMethod()); event.detail(Details.USERNAME, userSession.getLoginUsername()); @@ -1183,13 +1308,13 @@ public class TokenService { if (formData.containsKey("cancel")) { event.error(Errors.REJECTED_BY_USER); - return redirectAccessDenied(redirect, state); + return redirectAccessDenied(redirect, clientSession.getNote(OpenIdConnectProtocol.STATE_PARAM)); } event.success(); accessCode.setAction(ClientSessionModel.Action.CODE_TO_TOKEN); - return oauth.redirectAccessCode(accessCode, userSession, state, redirect); + return oauth.redirectAccessCode(accessCode, userSession, clientSession.getNote(OpenIdConnectProtocol.STATE_PARAM), 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 b04ef18297..892d0b6b80 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 @@ -9,6 +9,7 @@ import org.keycloak.email.EmailException; import org.keycloak.email.EmailProvider; 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.ModelDuplicateException; @@ -31,10 +32,12 @@ import org.keycloak.representations.idm.SocialLinkRepresentation; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.idm.UserSessionRepresentation; import org.keycloak.services.managers.AccessCode; +import org.keycloak.services.managers.ClientSessionCode; import org.keycloak.services.managers.RealmManager; import org.keycloak.services.managers.ResourceAdminManager; import org.keycloak.services.managers.TokenManager; import org.keycloak.services.managers.UserManager; +import org.keycloak.services.protocol.OpenIdConnectProtocol; import org.keycloak.services.resources.flows.Flows; import org.keycloak.services.resources.flows.Urls; @@ -58,6 +61,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.UUID; import java.util.concurrent.TimeUnit; /** @@ -878,8 +882,6 @@ public class UsersResource { String redirect = Urls.accountBase(uriInfo.getBaseUri()).path("/").build(realm.getName()).toString(); String clientId = Constants.ACCOUNT_MANAGEMENT_APP; - String state = null; - String scope = null; ClientModel client = realm.findClient(clientId); if (client == null || !client.isEnabled()) { @@ -888,13 +890,20 @@ public class UsersResource { UserSessionModel userSession = session.sessions().createUserSession(realm, user, username, clientConnection.getRemoteAddr(), "form", false); //audit.session(userSession); + ClientSessionModel clientSession = session.sessions().createClientSession(realm, client); + clientSession.setAuthMethod(OpenIdConnectProtocol.LOGIN_PAGE_PROTOCOL); + clientSession.setRedirectUri(redirect); + clientSession.setUserSession(userSession); + ClientSessionCode accessCode = new ClientSessionCode(realm, clientSession); - AccessCode accessCode = tokenManager.createAccessCode(null, state, redirect, session, realm, client, user, userSession); accessCode.setRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD); try { UriBuilder builder = Urls.loginPasswordResetBuilder(uriInfo.getBaseUri()); - builder.queryParam("key", accessCode.getCode()); + builder.queryParam("code", accessCode.getCode()); + String key = UUID.randomUUID().toString(); + clientSession.setNote("key", key); + builder.queryParam("key", key); String link = builder.build(realm.getName()).toString(); long expiration = TimeUnit.SECONDS.toMinutes(realm.getAccessCodeLifespanUserAction()); 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 1675fb7bbc..6482ff8822 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 @@ -29,6 +29,7 @@ import org.keycloak.OAuth2Constants; import org.keycloak.events.EventBuilder; import org.keycloak.events.Details; import org.keycloak.events.EventType; +import org.keycloak.login.LoginFormsProvider; import org.keycloak.models.ApplicationModel; import org.keycloak.models.ClientModel; import org.keycloak.models.ClientSessionModel; @@ -42,16 +43,20 @@ import org.keycloak.models.UserSessionModel; import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.services.managers.AccessCode; import org.keycloak.services.managers.AuthenticationManager; +import org.keycloak.services.managers.ClientSessionCode; import org.keycloak.services.managers.TokenManager; +import org.keycloak.services.protocol.OpenIdConnectProtocol; import javax.ws.rs.core.Cookie; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriInfo; +import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Set; +import java.util.UUID; /** * @author Bill Burke @@ -85,7 +90,7 @@ public class OAuthFlows { this.tokenManager = tokenManager; } - public Response redirectAccessCode(AccessCode accessCode, UserSessionModel userSession, String state, String redirect) { + public Response redirectAccessCode(ClientSessionCode 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); @@ -110,8 +115,8 @@ public class OAuthFlows { } // refresh the cookies! - authManager.createLoginCookie(realm, accessCode.getUser(), userSession, uriInfo, clientConnection); - if (userSession.isRememberMe()) authManager.createRememberMeCookie(realm, accessCode.getUser().getUsername(), uriInfo, clientConnection); + authManager.createLoginCookie(realm, userSession.getUser(), userSession, uriInfo, clientConnection); + if (userSession.isRememberMe()) authManager.createRememberMeCookie(realm, userSession.getUser().getUsername(), uriInfo, clientConnection); return location.build(); } @@ -123,6 +128,72 @@ public class OAuthFlows { return Response.status(302).location(redirectUri.build()).build(); } + + public Response processAccessCode(ClientSessionModel clientSession, UserSessionModel session, EventBuilder event) { + UserModel user = session.getUser(); + isTotpConfigurationRequired(user); + isEmailVerificationRequired(user); + ClientModel client = clientSession.getClient(); + + boolean isResource = client instanceof ApplicationModel; + ClientSessionCode accessCode = new ClientSessionCode(realm, clientSession); + + + log.debugv("processAccessCode: isResource: {0}", isResource); + log.debugv("processAccessCode: go to oauth page?: {0}", + !isResource); + + event.detail(Details.CODE_ID, clientSession.getId()); + + Set requiredActions = user.getRequiredActions(); + if (!requiredActions.isEmpty()) { + RequiredAction action = user.getRequiredActions().iterator().next(); + accessCode.setRequiredAction(action); + + LoginFormsProvider loginFormsProvider = Flows.forms(this.session, realm, client, uriInfo).setAccessCode(accessCode.getCode()).setUser(user); + if (action.equals(RequiredAction.VERIFY_EMAIL)) { + String key = UUID.randomUUID().toString(); + clientSession.setNote("key", key); + loginFormsProvider.setVerifyCode(key); + event.clone().event(EventType.SEND_VERIFY_EMAIL).detail(Details.EMAIL, user.getEmail()).success(); + } + + return loginFormsProvider + .createResponse(action); + } + + if (!isResource) { + accessCode.setAction(ClientSessionModel.Action.OAUTH_GRANT); + + List realmRoles = new LinkedList(); + MultivaluedMap resourceRoles = new MultivaluedMapImpl(); + for (RoleModel r : accessCode.getRequestedRoles()) { + if (r.getContainer() instanceof RealmModel) { + realmRoles.add(r); + } else { + resourceRoles.add(((ApplicationModel) r.getContainer()).getName(), r); + } + } + + return Flows.forms(this.session, realm, client, uriInfo) + .setAccessCode(accessCode.getCode()) + .setAccessRequest(realmRoles, resourceRoles) + .setClient(client) + .createOAuthGrant(); + } + + String redirect = clientSession.getRedirectUri(); + + if (redirect != null) { + event.success(); + String state = clientSession.getNote(OpenIdConnectProtocol.STATE_PARAM); + accessCode.setAction(ClientSessionModel.Action.CODE_TO_TOKEN); + return redirectAccessCode(accessCode, session, state, redirect); + } else { + return null; + } + } + /* public Response processAccessCode(String scopeParam, String state, String redirect, ClientModel client, UserModel user, UserSessionModel session, EventBuilder event) { isTotpConfigurationRequired(user); isEmailVerificationRequired(user); @@ -178,6 +249,7 @@ public class OAuthFlows { return null; } } + */ /* public Response forwardToSecurityFailure(String message) { diff --git a/services/src/main/java/org/keycloak/services/resources/flows/SocialRedirectFlows.java b/services/src/main/java/org/keycloak/services/resources/flows/SocialRedirectFlows.java index 93eb48aa8a..8aa642a068 100755 --- a/services/src/main/java/org/keycloak/services/resources/flows/SocialRedirectFlows.java +++ b/services/src/main/java/org/keycloak/services/resources/flows/SocialRedirectFlows.java @@ -2,8 +2,10 @@ package org.keycloak.services.resources.flows; import org.keycloak.ClientConnection; import org.keycloak.jose.jws.JWSBuilder; +import org.keycloak.models.ClientSessionModel; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; +import org.keycloak.services.managers.ClientSessionCode; import org.keycloak.services.resources.SocialResource; import org.keycloak.services.util.CookieHelper; import org.keycloak.social.AuthRequest; @@ -23,30 +25,17 @@ public class SocialRedirectFlows { private final UriInfo uriInfo; private ClientConnection clientConnection; private final SocialProvider socialProvider; - private final SocialResource.State state; SocialRedirectFlows(RealmModel realm, UriInfo uriInfo, ClientConnection clientConnection, SocialProvider provider) { this.realm = realm; this.uriInfo = uriInfo; this.clientConnection = clientConnection; this.socialProvider = provider; - - state = new SocialResource.State(); - state.setRealm(realm.getName()); - state.setProvider(provider.getId()); } - public SocialRedirectFlows putClientAttribute(String key, String value) { - state.set(key, value); - return this; - } - - public SocialRedirectFlows user(UserModel user) { - state.setUser(user.getId()); - return this; - } - - public Response redirectToSocialProvider() throws SocialProviderException { + public Response redirectToSocialProvider(ClientSessionCode code) throws SocialProviderException { + code.setAction(ClientSessionModel.Action.SOCIAL_CALLBACK); + code.getClientSession().setNote("social_provider", socialProvider.getId()); String socialProviderId = socialProvider.getId(); String key = realm.getSocialConfig().get(socialProviderId + ".key"); @@ -54,20 +43,8 @@ public class SocialRedirectFlows { String callbackUri = Urls.socialCallback(uriInfo.getBaseUri()).toString(); SocialProviderConfig config = new SocialProviderConfig(key, secret, callbackUri); - String encodedState = new JWSBuilder().jsonContent(state).rsa256(realm.getPrivateKey()); - - AuthRequest authRequest = socialProvider.getAuthUrl(config, encodedState); - - if (authRequest.getAttributes() != null) { - String cookiePath = Urls.socialBase(uriInfo.getBaseUri()).build().getRawPath().toString(); - - String encoded = new JWSBuilder() - .jsonContent(authRequest.getAttributes()) - .rsa256(realm.getPrivateKey()); - - CookieHelper.addCookie("KEYCLOAK_SOCIAL", encoded, cookiePath, null, null, -1, realm.getSslRequired().isRequired(clientConnection), true); - } + AuthRequest authRequest = socialProvider.getAuthUrl(code.getClientSession(), config, code.getCode()); return Response.status(302).location(authRequest.getAuthUri()).build(); } diff --git a/social/core/pom.xml b/social/core/pom.xml index 895237e5c5..cf11cff8a4 100755 --- a/social/core/pom.xml +++ b/social/core/pom.xml @@ -20,6 +20,12 @@ ${project.version} provided + + org.keycloak + keycloak-model-api + ${project.version} + provided + org.codehaus.jackson jackson-core-asl diff --git a/social/core/src/main/java/org/keycloak/social/AbstractOAuth2Provider.java b/social/core/src/main/java/org/keycloak/social/AbstractOAuth2Provider.java index 43eefc16b5..161d1b4c62 100755 --- a/social/core/src/main/java/org/keycloak/social/AbstractOAuth2Provider.java +++ b/social/core/src/main/java/org/keycloak/social/AbstractOAuth2Provider.java @@ -2,6 +2,7 @@ package org.keycloak.social; import org.codehaus.jackson.map.ObjectMapper; import org.keycloak.OAuth2Constants; +import org.keycloak.models.ClientSessionModel; import org.keycloak.social.utils.SimpleHttp; import java.io.IOException; @@ -43,7 +44,7 @@ public abstract class AbstractOAuth2Provider implements SocialProvider { protected abstract SocialUser getProfile(String accessToken) throws SocialProviderException; @Override - public AuthRequest getAuthUrl(SocialProviderConfig config, String state) throws SocialProviderException { + public AuthRequest getAuthUrl(ClientSessionModel clientSession, SocialProviderConfig config, String state) throws SocialProviderException { return AuthRequest.create(getAuthUrl()) .setQueryParam(CLIENT_ID, config.getKey()) .setQueryParam(RESPONSE_TYPE, CODE) @@ -54,7 +55,7 @@ public abstract class AbstractOAuth2Provider implements SocialProvider { } @Override - public SocialUser processCallback(SocialProviderConfig config, AuthCallback callback) throws SocialProviderException { + public SocialUser processCallback(ClientSessionModel clientSession, SocialProviderConfig config, AuthCallback callback) throws SocialProviderException { String error = callback.getQueryParam(OAuth2Constants.ERROR); if (error != null) { if (error.equals("access_denied")) { diff --git a/social/core/src/main/java/org/keycloak/social/AuthCallback.java b/social/core/src/main/java/org/keycloak/social/AuthCallback.java old mode 100644 new mode 100755 index ebb166060b..58aee5b571 --- a/social/core/src/main/java/org/keycloak/social/AuthCallback.java +++ b/social/core/src/main/java/org/keycloak/social/AuthCallback.java @@ -29,11 +29,9 @@ import java.util.Map; public class AuthCallback { private Map queryParams; - private Map attributes; - public AuthCallback(Map queryParams, Map attributes) { + public AuthCallback(Map queryParams) { this.queryParams = queryParams; - this.attributes = attributes; } public String getQueryParam(String name) { @@ -44,8 +42,5 @@ public class AuthCallback { return null; } - public String getAttribute(String name) { - return attributes != null ? attributes.get(name) : null; - } } diff --git a/social/core/src/main/java/org/keycloak/social/AuthRequest.java b/social/core/src/main/java/org/keycloak/social/AuthRequest.java old mode 100644 new mode 100755 index ee89fbe473..1efce3560f --- a/social/core/src/main/java/org/keycloak/social/AuthRequest.java +++ b/social/core/src/main/java/org/keycloak/social/AuthRequest.java @@ -35,8 +35,6 @@ public class AuthRequest { private URI authUri; - private Map attributes; - public static AuthRequestBuilder create(String url) { AuthRequestBuilder req = new AuthRequestBuilder(); @@ -46,27 +44,20 @@ public class AuthRequest { return req; } - private AuthRequest(URI authUri, Map attributes) { + private AuthRequest(URI authUri) { this.authUri = authUri; - this.attributes = attributes; } public URI getAuthUri() { return authUri; } - public Map getAttributes() { - return attributes; - } - public static class AuthRequestBuilder { private StringBuilder b; private char sep; - private Map attributes; - private String id; private AuthRequestBuilder() { @@ -90,17 +81,9 @@ public class AuthRequest { } } - public AuthRequestBuilder setAttribute(String name, String value) { - if (attributes == null) { - attributes = new HashMap(); - } - attributes.put(name, value); - return this; - } - public AuthRequest build() { try { - return new AuthRequest(new URI(b.toString()), attributes); + return new AuthRequest(new URI(b.toString())); } catch (URISyntaxException e) { throw new IllegalArgumentException(e); } diff --git a/social/core/src/main/java/org/keycloak/social/SocialProvider.java b/social/core/src/main/java/org/keycloak/social/SocialProvider.java old mode 100644 new mode 100755 index b0980776c2..1f90622f78 --- a/social/core/src/main/java/org/keycloak/social/SocialProvider.java +++ b/social/core/src/main/java/org/keycloak/social/SocialProvider.java @@ -22,6 +22,8 @@ package org.keycloak.social; +import org.keycloak.models.ClientSessionModel; + /** * @author Stian Thorgersen */ @@ -29,10 +31,10 @@ public interface SocialProvider { String getId(); - AuthRequest getAuthUrl(SocialProviderConfig config, String state) throws SocialProviderException; + AuthRequest getAuthUrl(ClientSessionModel clientSession, SocialProviderConfig config, String state) throws SocialProviderException; String getName(); - SocialUser processCallback(SocialProviderConfig config, AuthCallback callback) throws SocialProviderException; + SocialUser processCallback(ClientSessionModel clientSession, SocialProviderConfig config, AuthCallback callback) throws SocialProviderException; } diff --git a/social/facebook/pom.xml b/social/facebook/pom.xml index bbf264cf0d..69c48c0e10 100755 --- a/social/facebook/pom.xml +++ b/social/facebook/pom.xml @@ -14,6 +14,12 @@ + + org.keycloak + keycloak-model-api + ${project.version} + provided + org.keycloak keycloak-social-core diff --git a/social/github/pom.xml b/social/github/pom.xml index 8746269e2c..351f4b142f 100755 --- a/social/github/pom.xml +++ b/social/github/pom.xml @@ -14,6 +14,12 @@ + + org.keycloak + keycloak-model-api + ${project.version} + provided + org.keycloak keycloak-social-core diff --git a/social/google/pom.xml b/social/google/pom.xml index 4f2bfb5589..a1ca18974e 100755 --- a/social/google/pom.xml +++ b/social/google/pom.xml @@ -14,6 +14,12 @@ + + org.keycloak + keycloak-model-api + ${project.version} + provided + org.keycloak keycloak-social-core diff --git a/social/twitter/pom.xml b/social/twitter/pom.xml index 601702401c..dd3f14e960 100755 --- a/social/twitter/pom.xml +++ b/social/twitter/pom.xml @@ -14,6 +14,12 @@ + + org.keycloak + keycloak-model-api + ${project.version} + provided + org.keycloak keycloak-social-core diff --git a/social/twitter/src/main/java/org/keycloak/social/twitter/TwitterProvider.java b/social/twitter/src/main/java/org/keycloak/social/twitter/TwitterProvider.java index 7a6c88b35f..0fd4ee36a6 100755 --- a/social/twitter/src/main/java/org/keycloak/social/twitter/TwitterProvider.java +++ b/social/twitter/src/main/java/org/keycloak/social/twitter/TwitterProvider.java @@ -21,6 +21,7 @@ */ package org.keycloak.social.twitter; +import org.keycloak.models.ClientSessionModel; import org.keycloak.social.AuthCallback; import org.keycloak.social.AuthRequest; import org.keycloak.social.SocialAccessDeniedException; @@ -45,7 +46,7 @@ public class TwitterProvider implements SocialProvider { } @Override - public AuthRequest getAuthUrl(SocialProviderConfig config, String state) throws SocialProviderException { + public AuthRequest getAuthUrl(ClientSessionModel clientSession, SocialProviderConfig config, String state) throws SocialProviderException { try { Twitter twitter = new TwitterFactory().getInstance(); twitter.setOAuthConsumer(config.getKey(), config.getSecret()); @@ -53,8 +54,9 @@ public class TwitterProvider implements SocialProvider { URI uri = new URI(config.getCallbackUrl() + "?state=" + state); RequestToken requestToken = twitter.getOAuthRequestToken(uri.toString()); + clientSession.setNote("twitter_token", requestToken.getToken()); + clientSession.setNote("twitter_tokenSecret", requestToken.getTokenSecret()); return AuthRequest.create(requestToken.getAuthenticationURL()) - .setAttribute("token", requestToken.getToken()).setAttribute("tokenSecret", requestToken.getTokenSecret()) .build(); } catch (Exception e) { throw new SocialProviderException(e); @@ -67,7 +69,7 @@ public class TwitterProvider implements SocialProvider { } @Override - public SocialUser processCallback(SocialProviderConfig config, AuthCallback callback) throws SocialProviderException { + public SocialUser processCallback(ClientSessionModel clientSession, SocialProviderConfig config, AuthCallback callback) throws SocialProviderException { if (callback.getQueryParam("denied") != null) { throw new SocialAccessDeniedException(); } @@ -78,8 +80,10 @@ public class TwitterProvider implements SocialProvider { String token = callback.getQueryParam("oauth_token"); String verifier = callback.getQueryParam("oauth_verifier"); + String twitterToken = clientSession.getNote("twitter_token"); + String twitterSecret = clientSession.getNote("twitter_tokenSecret"); - RequestToken requestToken = new RequestToken((String)callback.getAttribute("token"), (String)callback.getAttribute("tokenSecret")); + RequestToken requestToken = new RequestToken(twitterToken, twitterSecret); twitter.getOAuthAccessToken(requestToken, verifier); twitter4j.User twitterUser = twitter.verifyCredentials(); 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 0dd893bbbb..fc9b9fe8c0 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/AssertEvents.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/AssertEvents.java @@ -124,6 +124,15 @@ public class AssertEvents implements TestRule, EventListenerProviderFactory { .session(isUUID()); } + public ExpectedEvent expectSocialLogin() { + return expect(EventType.SOCIAL_LOGIN) + .detail(Details.CODE_ID, isCodeId()) + .detail(Details.USERNAME, DEFAULT_USERNAME) + .detail(Details.AUTH_METHOD, "form") + .detail(Details.REDIRECT_URI, DEFAULT_REDIRECT_URI) + .session(isUUID()); + } + public ExpectedEvent expectCodeToToken(String codeId, String sessionId) { return expect(EventType.CODE_TO_TOKEN) .detail(Details.CODE_ID, codeId) @@ -277,6 +286,11 @@ public class AssertEvents implements TestRule, EventListenerProviderFactory { return this; } + public ExpectedEvent clearDetails() { + if (details != null) details.clear(); + return this; + } + public ExpectedEvent error(String error) { expected.setError(error); return this; diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/DummySocial.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/DummySocial.java index f473cb9e9a..154bd195fa 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/DummySocial.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/DummySocial.java @@ -1,6 +1,7 @@ package org.keycloak.testsuite; import org.keycloak.OAuth2Constants; +import org.keycloak.models.ClientSessionModel; import org.keycloak.social.AuthCallback; import org.keycloak.social.AuthRequest; import org.keycloak.social.SocialAccessDeniedException; @@ -19,7 +20,7 @@ public class DummySocial implements SocialProvider { } @Override - public AuthRequest getAuthUrl(SocialProviderConfig config, String state) throws SocialProviderException { + public AuthRequest getAuthUrl(ClientSessionModel clientSession, SocialProviderConfig config, String state) throws SocialProviderException { return AuthRequest.create(AUTH_PATH) .setQueryParam(OAuth2Constants.RESPONSE_TYPE, "token") .setQueryParam(OAuth2Constants.REDIRECT_URI, config.getCallbackUrl()) @@ -32,7 +33,7 @@ public class DummySocial implements SocialProvider { } @Override - public SocialUser processCallback(SocialProviderConfig config, AuthCallback callback) throws SocialProviderException { + public SocialUser processCallback(ClientSessionModel clientSession, SocialProviderConfig config, AuthCallback callback) throws SocialProviderException { String error = callback.getQueryParam(OAuth2Constants.ERROR); if (error != null) { throw new SocialAccessDeniedException(); 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 a674407950..33d02c9ad8 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java @@ -75,7 +75,7 @@ public class OAuthClient { private String redirectUri = "http://localhost:8081/app/auth"; - private String state; + private String state = "mystate"; private PublicKey realmPublicKey; diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java index e22a6308eb..3eb3daead2 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java @@ -140,6 +140,7 @@ public class AccountTest { @Before public void before() { + oauth.state("mystate"); // keycloak enforces that a state param has been sent by client userId = keycloakRule.getUser("test", "test-user@localhost").getId(); } @@ -159,6 +160,16 @@ public class AccountTest { }); } + @Test + @Ignore + public void runit() throws Exception { + Thread.sleep(10000000); + + } + + + + @Test public void returnToAppFromQueryParam() { driver.navigate().to(AccountUpdateProfilePage.PATH + "?referrer=test-app"); @@ -446,6 +457,7 @@ public class AccountTest { // Create second session WebDriver driver2 = WebRule.createWebDriver(); OAuthClient oauth2 = new OAuthClient(driver2); + oauth2.state("mystate"); oauth2.doLogin("view-sessions", "password"); Event login2Event = events.expectLogin().user(userId).detail(Details.USERNAME, "view-sessions").assertEvent(); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java index 8fe4d7288d..f2b8b267bc 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java @@ -88,6 +88,7 @@ public class RequiredActionEmailVerificationTest { @Before public void before() { + oauth.state("mystate"); // have to set this as keycloak validates that state is sent keycloakRule.configure(new KeycloakSetup() { @Override @@ -115,7 +116,8 @@ public class RequiredActionEmailVerificationTest { String body = (String) message.getContent(); String verificationUrl = MailUtil.getLink(body); - Event sendEvent = events.expectRequiredAction(EventType.SEND_VERIFY_EMAIL).detail("email", "test-user@localhost").assertEvent(); + AssertEvents.ExpectedEvent emailEvent = events.expectRequiredAction(EventType.SEND_VERIFY_EMAIL).detail("email", "test-user@localhost"); + Event sendEvent = emailEvent.assertEvent(); String sessionId = sendEvent.getSessionId(); String mailCodeId = sendEvent.getDetails().get(Details.CODE_ID); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionResetPasswordTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionResetPasswordTest.java index a512795b82..26979ee253 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionResetPasswordTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionResetPasswordTest.java @@ -22,6 +22,7 @@ package org.keycloak.testsuite.actions; import org.junit.Assert; +import org.junit.Before; import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; @@ -86,6 +87,12 @@ public class RequiredActionResetPasswordTest { @WebResource protected LoginPasswordUpdatePage changePasswordPage; + @Before + public void before() { + oauth.state("mystate"); // have to set this as keycloak validates that state is sent + } + + @Test public void tempPassword() throws Exception { loginPage.open(); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTotpTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTotpTest.java index 78b52497e6..8a922af7c8 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTotpTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTotpTest.java @@ -145,6 +145,10 @@ public class LoginTotpTest { @Test public void loginWithTotpExpiredPasswordToken() throws Exception { try { + + loginPage.open(); + loginPage.login("test-user@localhost", "password"); + keycloakRule.configure(new KeycloakSetup() { @Override public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { @@ -153,9 +157,6 @@ public class LoginTotpTest { } }); - loginPage.open(); - loginPage.login("test-user@localhost", "password"); - loginTotpPage.assertCurrent(); Thread.sleep(2000); @@ -165,7 +166,11 @@ public class LoginTotpTest { loginPage.assertCurrent(); Assert.assertEquals("Invalid username or password.", loginPage.getError()); - events.expectLogin().error("invalid_user_credentials").removeDetail(Details.CODE_ID).session((String) null).assertEvent(); + AssertEvents.ExpectedEvent expectedEvent = events.expectLogin().error("invalid_user_credentials") + .user((String)null) + .clearDetails() + .session((String) null); + expectedEvent.assertEvent(); } finally { keycloakRule.configure(new KeycloakSetup() { @Override 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 f2c168cd3d..9cda36bbd3 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 @@ -10,6 +10,7 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; +import org.keycloak.services.protocol.OpenIdConnectProtocol; import org.keycloak.testsuite.rule.KeycloakRule; import org.keycloak.util.Time; @@ -73,7 +74,7 @@ public class UserSessionProviderTest { 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("state", session.getNote(OpenIdConnectProtocol.STATE_PARAM)); assertEquals(2, session.getRoles().size()); assertTrue(session.getRoles().contains("one")); assertTrue(session.getRoles().contains("two")); @@ -245,7 +246,11 @@ 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); - session.sessions().createClientSession(realm, realm.findClient("test-app"), userSession, "http://redirect", "state", new HashSet()); + ClientSessionModel clientSession = session.sessions().createClientSession(realm, realm.findClient("test-app")); + clientSession.setUserSession(userSession); + clientSession.setRedirectUri("http://redirect"); + clientSession.setRoles(new HashSet()); + clientSession.setNote(OpenIdConnectProtocol.STATE_PARAM, "state"); } resetSession(); @@ -280,6 +285,15 @@ public class UserSessionProviderTest { assertEquals(1, session.sessions().getActiveUserSessions(realm, realm.findClient("third-party"))); } + private ClientSessionModel createClientSession(ClientModel client, UserSessionModel userSession, String redirect, String state, Set roles) { + ClientSessionModel clientSession = session.sessions().createClientSession(realm, client); + if (userSession != null) clientSession.setUserSession(userSession); + clientSession.setRedirectUri(redirect); + if (state != null) clientSession.setNote(OpenIdConnectProtocol.STATE_PARAM, state); + if (roles != null) clientSession.setRoles(roles); + return clientSession; + } + private UserSessionModel[] createSessions() { UserSessionModel[] sessions = new UserSessionModel[3]; sessions[0] = session.sessions().createUserSession(realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.1", "form", true); @@ -288,14 +302,14 @@ public class UserSessionProviderTest { 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()); + createClientSession(realm.findClient("test-app"), sessions[0], "http://redirect", "state", roles); + createClientSession(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); - session.sessions().createClientSession(realm, realm.findClient("test-app"), sessions[1], "http://redirect", "state", new HashSet()); + createClientSession(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()); + createClientSession(realm.findClient("test-app"), sessions[2], "http://redirect", "state", new HashSet()); resetSession(); 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 98dd5ac0b6..410b012b9e 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 @@ -135,7 +135,8 @@ public class AccessTokenTest { AccessTokenResponse response = oauth.doAccessTokenRequest(code, "invalid"); Assert.assertEquals(400, response.getStatusCode()); - events.expectCodeToToken(codeId, loginEvent.getSessionId()).error("invalid_client_credentials").removeDetail(Details.TOKEN_ID).removeDetail(Details.REFRESH_TOKEN_ID).assertEvent(); + AssertEvents.ExpectedEvent expectedEvent = events.expectCodeToToken(codeId, loginEvent.getSessionId()).error("invalid_client_credentials").removeDetail(Details.TOKEN_ID).removeDetail(Details.REFRESH_TOKEN_ID); + expectedEvent.assertEvent(); } @Test 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 2ca2021303..a70cc85756 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 @@ -22,18 +22,23 @@ package org.keycloak.testsuite.oauth; import org.junit.Assert; +import org.junit.Before; import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; import org.keycloak.OAuth2Constants; import org.keycloak.events.Details; +import org.keycloak.events.Errors; +import org.keycloak.events.EventType; import org.keycloak.models.Constants; import org.keycloak.models.RealmModel; import org.keycloak.services.managers.AccessCode; +import org.keycloak.services.managers.ClientSessionCode; import org.keycloak.services.managers.RealmManager; import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.OAuthClient; import org.keycloak.testsuite.OAuthClient.AuthorizationCodeResponse; +import org.keycloak.testsuite.pages.ErrorPage; import org.keycloak.testsuite.pages.LoginPage; import org.keycloak.testsuite.rule.KeycloakRule; import org.keycloak.testsuite.rule.WebResource; @@ -65,6 +70,9 @@ public class AuthorizationCodeTest { @WebResource protected LoginPage loginPage; + @WebResource + protected ErrorPage errorPage; + @Rule public AssertEvents events = new AssertEvents(keycloakRule); @@ -103,7 +111,7 @@ public class AuthorizationCodeTest { String code = driver.findElement(By.id(OAuth2Constants.CODE)).getText(); keycloakRule.verifyCode(code); - String codeId = events.expectLogin().detail(Details.REDIRECT_URI, Constants.INSTALLED_APP_URN).assertEvent().getDetails().get(Details.CODE_ID); + String codeId = events.expectLogin().detail(Details.REDIRECT_URI, "http://localhost:8081/auth/realms/test/tokens/oauth/oob").assertEvent().getDetails().get(Details.CODE_ID); assertCode(codeId, code); keycloakRule.update(new KeycloakRule.KeycloakSetup() { @@ -133,7 +141,10 @@ public class AuthorizationCodeTest { String error = driver.findElement(By.id(OAuth2Constants.ERROR)).getText(); 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); + events.expectLogin().error("rejected_by_user").user((String) null).session((String) null) + .removeDetail(Details.USERNAME).removeDetail(Details.CODE_ID) + .detail(Details.REDIRECT_URI, "http://localhost:8081/auth/realms/test/tokens/oauth/oob") + .assertEvent().getDetails().get(Details.CODE_ID); keycloakRule.update(new KeycloakRule.KeycloakSetup() { @Override @@ -167,22 +178,22 @@ public class AuthorizationCodeTest { @Test public void authorizationRequestNoState() throws IOException { - AuthorizationCodeResponse response = oauth.doLogin("test-user@localhost", "password"); + oauth.state(null); + oauth.openLoginForm(); + Assert.assertTrue(errorPage.isCurrent()); - Assert.assertTrue(response.isRedirected()); - Assert.assertNotNull(response.getCode()); - Assert.assertNull(response.getState()); - Assert.assertNull(response.getError()); - - keycloakRule.verifyCode(response.getCode()); - - String codeId = events.expectLogin().assertEvent().getDetails().get(Details.CODE_ID); - assertCode(codeId, response.getCode()); + events.expect(EventType.LOGIN_ERROR) + .error(Errors.STATE_PARAM_NOT_FOUND) + .detail(Details.RESPONSE_TYPE, "code") + .detail(Details.REDIRECT_URI, oauth.getRedirectUri()) + .user((String)null) + .assertEvent(); + //assertCode(codeId, response.getCode()); } private void assertCode(String expectedCodeId, String actualCode) { - AccessCode code = keycloakRule.verifyCode(actualCode); - assertEquals(expectedCodeId, code.getCodeId()); + ClientSessionCode code = keycloakRule.verifyCode(actualCode); + assertEquals(expectedCodeId, code.getClientSession().getId()); } } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/perf/AccessTokenPerfTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/perf/AccessTokenPerfTest.java index 686b785e75..887762f8ef 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/perf/AccessTokenPerfTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/perf/AccessTokenPerfTest.java @@ -55,6 +55,8 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicLong; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * @author Stian Thorgersen @@ -163,21 +165,24 @@ public class AccessTokenPerfTest { return b.build(realm).toString(); } + static Pattern actionParser = Pattern.compile("action=\"([^\"]+)\""); - @Override public void run() { //this.client = new ResteasyClientBuilder().build(); String state = "42"; - Response response = client.target(getLoginFormUrl(state)).request().get(); - URI uri = null; - if (200 == response.getStatus()) { - response.close(); - Form form = new Form(); - form.param("username", "test-user@localhost"); - form.param("password", "password"); - response = client.target(getProcessLoginUrl(state)).request().post(Entity.form(form)); - + String loginFormUrl = getLoginFormUrl(state); + String html = client.target(loginFormUrl).request().get(String.class); + Matcher matcher = actionParser.matcher(html); + matcher.find(); + String actionUrl = matcher.group(1); + if (!actionUrl.startsWith("http")) { + actionUrl = UriBuilder.fromUri(actionUrl).scheme("http").host("localhost").port(8081).build().toString(); } + Form form = new Form(); + form.param("username", "test-user@localhost"); + form.param("password", "password"); + Response response = client.target(actionUrl).request().post(Entity.form(form)); + URI uri = null; Assert.assertEquals(302, response.getStatus()); uri = response.getLocation(); for (String header : response.getHeaders().keySet()) { @@ -191,7 +196,7 @@ public class AccessTokenPerfTest { String code = getCode(uri); Assert.assertNotNull(code); - Form form = new Form(); + form = new Form(); form.param(OAuth2Constants.GRANT_TYPE, grantType) .param(OAuth2Constants.CODE, code) .param(OAuth2Constants.REDIRECT_URI, redirectUri); 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 f108a631b8..33dc6eb19f 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 @@ -27,6 +27,7 @@ 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.ClientSessionCode; import org.keycloak.services.managers.RealmManager; import org.keycloak.testsuite.ApplicationServlet; @@ -109,12 +110,12 @@ public class KeycloakRule extends AbstractKeycloakRule { stopSession(session, true); } - public AccessCode verifyCode(String code) { + public ClientSessionCode verifyCode(String code) { KeycloakSession session = startSession(); try { RealmModel realm = session.realms().getRealm("test"); try { - AccessCode accessCode = AccessCode.parse(code, session, realm); + ClientSessionCode accessCode = ClientSessionCode.parse(code, session, realm); if (accessCode == null) { Assert.fail("Invalid code"); } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/social/SocialLoginTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/social/SocialLoginTest.java index f81b160074..658512e7a5 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/social/SocialLoginTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/social/SocialLoginTest.java @@ -114,14 +114,17 @@ public class SocialLoginTest { String userId = events.expect(EventType.REGISTER) .user(AssertEvents.isUUID()) .detail(Details.EMAIL, "bob@builder.com") - .detail(Details.RESPONSE_TYPE, "code") .detail(Details.REGISTER_METHOD, "social@dummy") .detail(Details.REDIRECT_URI, AssertEvents.DEFAULT_REDIRECT_URI) .detail(Details.USERNAME, "1@dummy") .session((String) null) .assertEvent().getUserId(); - Event loginEvent = events.expectLogin().user(userId).detail(Details.USERNAME, "1@dummy").detail(Details.AUTH_METHOD, "social@dummy").assertEvent(); + Event loginEvent = events.expectSocialLogin() + .user(userId) + .detail(Details.USERNAME, "1@dummy") + .detail(Details.AUTH_METHOD, "social@dummy") + .assertEvent(); String sessionId = loginEvent.getSessionId(); String codeId = loginEvent.getDetails().get(Details.CODE_ID); @@ -153,7 +156,7 @@ public class SocialLoginTest { driver.findElement(By.id("username")).sendKeys("dummy-user1"); driver.findElement(By.id("login")).click(); - events.expectLogin().user(userId).detail(Details.USERNAME, "1@dummy").detail(Details.AUTH_METHOD, "social@dummy").assertEvent(); + events.expectSocialLogin().user(userId).detail(Details.USERNAME, "1@dummy").detail(Details.AUTH_METHOD, "social@dummy").assertEvent(); } @Test @@ -199,8 +202,9 @@ public class SocialLoginTest { Assert.assertTrue(loginPage.isCurrent()); Assert.assertEquals("Access denied", loginPage.getWarning()); - events.expectLogin().error("rejected_by_user").user((String) null).session((String) null).detail(Details.AUTH_METHOD, "social@dummy").removeDetail(Details.USERNAME).removeDetail(Details.CODE_ID).assertEvent(); + events.expectSocialLogin().error("rejected_by_user").user((String) null).session((String) null).detail(Details.AUTH_METHOD, "social@dummy").removeDetail(Details.USERNAME).removeDetail(Details.CODE_ID).assertEvent(); + String src = driver.getPageSource(); loginPage.login("test-user@localhost", "password"); Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); @@ -238,7 +242,6 @@ public class SocialLoginTest { String userId = events.expect(EventType.REGISTER) .user(AssertEvents.isUUID()) .detail(Details.EMAIL, "bob@builder.com") - .detail(Details.RESPONSE_TYPE, "code") .detail(Details.REGISTER_METHOD, "social@dummy") .detail(Details.REDIRECT_URI, AssertEvents.DEFAULT_REDIRECT_URI) .detail(Details.USERNAME, "2@dummy")