ClientSession everywhere refactor phase1

This commit is contained in:
Bill Burke 2014-09-26 17:44:33 -04:00
parent 0bf6c36ca7
commit 524b423733
55 changed files with 1330 additions and 667 deletions

View file

@ -35,6 +35,8 @@ public interface Errors {
String SOCIAL_PROVIDER_NOT_FOUND = "social_provider_not_found"; String SOCIAL_PROVIDER_NOT_FOUND = "social_provider_not_found";
String SOCIAL_ID_IN_USE = "social_id_in_use"; 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_NOT_LOGGED_IN = "user_not_logged_in";
String USER_SESSION_NOT_FOUND = "user_session_not_found"; String USER_SESSION_NOT_FOUND = "user_session_not_found";

View file

@ -39,6 +39,8 @@ public enum EventType {
SEND_VERIFY_EMAIL, SEND_VERIFY_EMAIL,
SEND_VERIFY_EMAIL_ERROR, SEND_VERIFY_EMAIL_ERROR,
SEND_RESET_PASSWORD, SEND_RESET_PASSWORD,
SEND_RESET_PASSWORD_ERROR SEND_RESET_PASSWORD_ERROR,
SOCIAL_LOGIN,
SOCIAL_LOGIN_ERROR
} }

View file

@ -50,6 +50,8 @@ public interface LoginFormsProvider extends Provider {
public LoginFormsProvider setClient(ClientModel client); public LoginFormsProvider setClient(ClientModel client);
LoginFormsProvider setVerifyCode(String code);
public LoginFormsProvider setQueryParams(MultivaluedMap<String, String> queryParams); public LoginFormsProvider setQueryParams(MultivaluedMap<String, String> queryParams);
public LoginFormsProvider setFormData(MultivaluedMap<String, String> formData); public LoginFormsProvider setFormData(MultivaluedMap<String, String> formData);

View file

@ -1,6 +1,7 @@
package org.keycloak.login.freemarker; package org.keycloak.login.freemarker;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
import org.keycloak.OAuth2Constants; import org.keycloak.OAuth2Constants;
import org.keycloak.email.EmailException; import org.keycloak.email.EmailException;
import org.keycloak.email.EmailProvider; import org.keycloak.email.EmailProvider;
@ -50,6 +51,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
private static final Logger logger = Logger.getLogger(FreeMarkerLoginFormsProvider.class); private static final Logger logger = Logger.getLogger(FreeMarkerLoginFormsProvider.class);
private String verifyCode;
private String message; private String message;
private String accessCode; private String accessCode;
private Response.Status status = Response.Status.OK; private Response.Status status = Response.Status.OK;
@ -108,7 +110,8 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
case VERIFY_EMAIL: case VERIFY_EMAIL:
try { try {
UriBuilder builder = Urls.loginActionEmailVerificationBuilder(uriInfo.getBaseUri()); 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(); String link = builder.build(realm.getName()).toString();
long expiration = TimeUnit.SECONDS.toMinutes(realm.getAccessCodeLifespanUserAction()); long expiration = TimeUnit.SECONDS.toMinutes(realm.getAccessCodeLifespanUserAction());
@ -134,7 +137,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
} }
private Response createResponse(LoginFormsPages page) { private Response createResponse(LoginFormsPages page) {
MultivaluedMap<String, String> queryParameterMap = queryParams != null ? queryParams : uriInfo.getQueryParameters(); MultivaluedMap<String, String> queryParameterMap = queryParams != null ? queryParams : new MultivaluedMapImpl<String, String>();
String requestURI = uriInfo.getBaseUri().getPath(); String requestURI = uriInfo.getBaseUri().getPath();
UriBuilder uriBuilder = UriBuilder.fromUri(requestURI); UriBuilder uriBuilder = UriBuilder.fromUri(requestURI);
@ -308,6 +311,12 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
return this; return this;
} }
@Override
public LoginFormsProvider setVerifyCode(String code) {
this.verifyCode = code;
return this;
}
@Override @Override
public LoginFormsProvider setQueryParams(MultivaluedMap<String, String> queryParams) { public LoginFormsProvider setQueryParams(MultivaluedMap<String, String> queryParams) {
this.queryParams = queryParams; this.queryParams = queryParams;

View file

@ -8,14 +8,14 @@ import java.util.Set;
public interface ClientSessionModel { public interface ClientSessionModel {
public String getId(); public String getId();
public RealmModel getRealm();
public ClientModel getClient(); public ClientModel getClient();
public String getState();
public UserSessionModel getUserSession(); public UserSessionModel getUserSession();
public void setUserSession(UserSessionModel userSession);
public String getRedirectUri(); public String getRedirectUri();
public void setRedirectUri(String uri);
public int getTimestamp(); public int getTimestamp();
@ -26,6 +26,7 @@ public interface ClientSessionModel {
public void setAction(Action action); public void setAction(Action action);
public Set<String> getRoles(); public Set<String> getRoles();
public void setRoles(Set<String> roles);
/** /**
* Authentication request type, i.e. OAUTH, SAML 2.0, SAML 1.1, etc. * Authentication request type, i.e. OAUTH, SAML 2.0, SAML 1.1, etc.
@ -45,7 +46,9 @@ public interface ClientSessionModel {
VERIFY_EMAIL, VERIFY_EMAIL,
UPDATE_PROFILE, UPDATE_PROFILE,
CONFIGURE_TOTP, CONFIGURE_TOTP,
UPDATE_PASSWORD UPDATE_PASSWORD,
AUTHENTICATE,
SOCIAL_CALLBACK
} }
} }

View file

@ -11,8 +11,9 @@ import java.util.Set;
*/ */
public interface UserSessionProvider extends Provider { public interface UserSessionProvider extends Provider {
ClientSessionModel createClientSession(RealmModel realm, ClientModel client, UserSessionModel userSession, String redirectUri, String state, Set<String> roles); ClientSessionModel createClientSession(RealmModel realm, ClientModel client);
ClientSessionModel getClientSession(RealmModel realm, String id); 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 createUserSession(RealmModel realm, UserModel user, String loginUsername, String ipAddress, String authMethod, boolean rememberMe);
UserSessionModel getUserSession(RealmModel realm, String id); UserSessionModel getUserSession(RealmModel realm, String id);

View file

@ -8,10 +8,13 @@ import org.keycloak.models.UserSessionModel;
import org.keycloak.models.sessions.jpa.entities.ClientSessionEntity; import org.keycloak.models.sessions.jpa.entities.ClientSessionEntity;
import org.keycloak.models.sessions.jpa.entities.ClientSessionNoteEntity; import org.keycloak.models.sessions.jpa.entities.ClientSessionNoteEntity;
import org.keycloak.models.sessions.jpa.entities.ClientSessionRoleEntity; import org.keycloak.models.sessions.jpa.entities.ClientSessionRoleEntity;
import org.keycloak.models.sessions.jpa.entities.UserSessionEntity;
import javax.persistence.EntityManager; import javax.persistence.EntityManager;
import java.util.HashSet; import java.util.HashSet;
import java.util.Iterator; import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set; import java.util.Set;
/** /**
@ -31,6 +34,11 @@ public class ClientSessionAdapter implements ClientSessionModel {
this.entity = entity; this.entity = entity;
} }
@Override
public RealmModel getRealm() {
return session.realms().getRealm(entity.getRealmId());
}
@Override @Override
public void setNote(String name, String value) { public void setNote(String name, String value) {
for (ClientSessionNoteEntity attr : entity.getNotes()) { for (ClientSessionNoteEntity attr : entity.getNotes()) {
@ -80,8 +88,32 @@ public class ClientSessionAdapter implements ClientSessionModel {
} }
@Override @Override
public String getState() { public void setUserSession(UserSessionModel userSession) {
return entity.getState(); 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<String> roles) {
if (roles != null) {
List<ClientSessionRoleEntity> roleEntities = new LinkedList<ClientSessionRoleEntity>();
for (String r : roles) {
ClientSessionRoleEntity roleEntity = new ClientSessionRoleEntity();
roleEntity.setClientSession(entity);
roleEntity.setRoleId(r);
em.persist(roleEntity);
roleEntities.add(roleEntity);
}
entity.setRoles(roleEntities);
}
} }
@Override @Override
@ -96,6 +128,7 @@ public class ClientSessionAdapter implements ClientSessionModel {
@Override @Override
public UserSessionModel getUserSession() { public UserSessionModel getUserSession() {
if (entity.getSession() == null) return null;
return new UserSessionAdapter(session, em, realm, entity.getSession()); return new UserSessionAdapter(session, em, realm, entity.getSession());
} }

View file

@ -37,33 +37,14 @@ public class JpaUserSessionProvider implements UserSessionProvider {
} }
@Override @Override
public ClientSessionModel createClientSession(RealmModel realm, ClientModel client, UserSessionModel userSession, String redirectUri, String state, Set<String> roles) { public ClientSessionModel createClientSession(RealmModel realm, ClientModel client) {
UserSessionEntity userSessionEntity = em.find(UserSessionEntity.class, userSession.getId());
ClientSessionEntity entity = new ClientSessionEntity(); ClientSessionEntity entity = new ClientSessionEntity();
entity.setId(KeycloakModelUtils.generateId()); entity.setId(KeycloakModelUtils.generateId());
entity.setTimestamp(Time.currentTime()); entity.setTimestamp(Time.currentTime());
entity.setClientId(client.getId()); entity.setClientId(client.getId());
entity.setSession(userSessionEntity); entity.setRealmId(realm.getId());
entity.setRedirectUri(redirectUri);
entity.setState(state);
em.persist(entity); em.persist(entity);
if (roles != null) {
List<ClientSessionRoleEntity> roleEntities = new LinkedList<ClientSessionRoleEntity>();
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); return new ClientSessionAdapter(session, em, realm, entity);
} }
@ -76,6 +57,16 @@ public class JpaUserSessionProvider implements UserSessionProvider {
return null; 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 @Override
public UsernameLoginFailureModel getUserLoginFailure(RealmModel realm, String username) { public UsernameLoginFailureModel getUserLoginFailure(RealmModel realm, String username) {
String id = username + "-" + realm; String id = username + "-" + realm;
@ -208,6 +199,18 @@ public class JpaUserSessionProvider implements UserSessionProvider {
int maxTime = Time.currentTime() - realm.getSsoSessionMaxLifespan(); int maxTime = Time.currentTime() - realm.getSsoSessionMaxLifespan();
int idleTime = Time.currentTime() - realm.getSsoSessionIdleTimeout(); 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") em.createNamedQuery("removeClientSessionRoleByExpired")
.setParameter("realmId", realm.getId()) .setParameter("realmId", realm.getId())
.setParameter("maxTime", maxTime) .setParameter("maxTime", maxTime)

View file

@ -23,10 +23,11 @@ import java.util.Collection;
@Entity @Entity
@Table(name = "CLIENT_SESSION") @Table(name = "CLIENT_SESSION")
@NamedQueries({ @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 = "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 = "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 = "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 { public class ClientSessionEntity {
@ -41,6 +42,9 @@ public class ClientSessionEntity {
@Column(name="CLIENT_ID",length = 36) @Column(name="CLIENT_ID",length = 36)
protected String clientId; protected String clientId;
@Column(name="REALM_ID")
protected String realmId;
@Column(name="TIMESTAMP") @Column(name="TIMESTAMP")
protected int timestamp; protected int timestamp;
@ -50,9 +54,6 @@ public class ClientSessionEntity {
@Column(name="AUTH_METHOD") @Column(name="AUTH_METHOD")
protected String authMethod; protected String authMethod;
@Column(name="STATE")
protected String state;
@Column(name="ACTION") @Column(name="ACTION")
protected ClientSessionModel.Action action; protected ClientSessionModel.Action action;
@ -86,6 +87,14 @@ public class ClientSessionEntity {
this.clientId = clientId; this.clientId = clientId;
} }
public String getRealmId() {
return realmId;
}
public void setRealmId(String realmId) {
this.realmId = realmId;
}
public int getTimestamp() { public int getTimestamp() {
return timestamp; return timestamp;
} }
@ -102,14 +111,6 @@ public class ClientSessionEntity {
this.redirectUri = redirectUri; this.redirectUri = redirectUri;
} }
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}
public ClientSessionModel.Action getAction() { public ClientSessionModel.Action getAction() {
return action; return action;
} }

View file

@ -18,9 +18,10 @@ import java.io.Serializable;
*/ */
@NamedQueries({ @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 = "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 = "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.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.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 = "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") @Table(name="CLIENT_SESSION_NOTE")
@Entity @Entity

View file

@ -17,9 +17,10 @@ import java.io.Serializable;
*/ */
@NamedQueries({ @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 = "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 = "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.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.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 = "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") @Table(name="CLIENT_SESSION_ROLE")
@Entity @Entity

View file

@ -6,6 +6,7 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionModel;
import org.keycloak.models.sessions.mem.entities.ClientSessionEntity; import org.keycloak.models.sessions.mem.entities.ClientSessionEntity;
import org.keycloak.models.sessions.mem.entities.UserSessionEntity;
import java.util.Set; import java.util.Set;
@ -31,19 +32,40 @@ public class ClientSessionAdapter implements ClientSessionModel {
return entity.getId(); return entity.getId();
} }
@Override
public RealmModel getRealm() {
return session.realms().getRealm(entity.getRealmId());
}
@Override @Override
public ClientModel getClient() { public ClientModel getClient() {
return realm.findClientById(entity.getClientId()); return realm.findClientById(entity.getClientId());
} }
@Override @Override
public String getState() { public UserSessionModel getUserSession() {
return entity.getState(); if (entity.getSession() == null) return null;
return new UserSessionAdapter(session, provider, realm, entity.getSession());
} }
@Override @Override
public UserSessionModel getUserSession() { public void setUserSession(UserSessionModel userSession) {
return new UserSessionAdapter(session, provider, realm, entity.getSession()); 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<String> roles) {
entity.setRoles(roles);
} }
@Override @Override

View file

@ -42,20 +42,12 @@ public class MemUserSessionProvider implements UserSessionProvider {
} }
@Override @Override
public ClientSessionModel createClientSession(RealmModel realm, ClientModel client, UserSessionModel userSession, String redirectUri, String state, Set<String> roles) { public ClientSessionModel createClientSession(RealmModel realm, ClientModel client) {
UserSessionEntity userSessionEntity = getUserSessionEntity(realm, userSession.getId());
ClientSessionEntity entity = new ClientSessionEntity(); ClientSessionEntity entity = new ClientSessionEntity();
entity.setId(KeycloakModelUtils.generateId()); entity.setId(KeycloakModelUtils.generateId());
entity.setTimestamp(Time.currentTime()); entity.setTimestamp(Time.currentTime());
entity.setClientId(client.getId()); entity.setClientId(client.getId());
entity.setSession(userSessionEntity); entity.setRealmId(realm.getId());
entity.setRedirectUri(redirectUri);
entity.setState(state);
entity.setRoles(roles);
userSessionEntity.addClientSession(entity);
clientSessions.put(entity.getId(), entity); clientSessions.put(entity.getId(), entity);
return new ClientSessionAdapter(session, this, realm, 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; 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 @Override
public UserSessionModel createUserSession(RealmModel realm, UserModel user, String loginUsername, String ipAddress, String authMethod, boolean rememberMe) { public UserSessionModel createUserSession(RealmModel realm, UserModel user, String loginUsername, String ipAddress, String authMethod, boolean rememberMe) {
String id = KeycloakModelUtils.generateId(); String id = KeycloakModelUtils.generateId();
@ -118,7 +120,9 @@ public class MemUserSessionProvider implements UserSessionProvider {
public List<UserSessionModel> getUserSessions(RealmModel realm, ClientModel client) { public List<UserSessionModel> getUserSessions(RealmModel realm, ClientModel client) {
List<UserSessionEntity> userSessionEntities = new LinkedList<UserSessionEntity>(); List<UserSessionEntity> userSessionEntities = new LinkedList<UserSessionEntity>();
for (ClientSessionEntity s : clientSessions.values()) { 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())) { if (!userSessionEntities.contains(s.getSession())) {
userSessionEntities.add(s.getSession()); userSessionEntities.add(s.getSession());
} }
@ -188,6 +192,13 @@ public class MemUserSessionProvider implements UserSessionProvider {
} }
} }
} }
Iterator<ClientSessionEntity> citr = clientSessions.values().iterator();
while (citr.hasNext()) {
ClientSessionEntity c = citr.next();
if (c.getSession() == null && c.getTimestamp() < Time.currentTime() - realm.getSsoSessionIdleTimeout()) {
citr.remove();
}
}
} }
@Override @Override
@ -203,6 +214,13 @@ public class MemUserSessionProvider implements UserSessionProvider {
} }
} }
} }
Iterator<ClientSessionEntity> citr = clientSessions.values().iterator();
while (citr.hasNext()) {
ClientSessionEntity c = citr.next();
if (c.getSession() == null && c.getRealmId().equals(realm.getId())) {
citr.remove();
}
}
} }
@Override @Override

View file

@ -30,6 +30,10 @@ public class UserSessionAdapter implements UserSessionModel {
this.entity = entity; this.entity = entity;
} }
public UserSessionEntity getEntity() {
return entity;
}
public String getId() { public String getId() {
return entity.getId(); return entity.getId();
} }

View file

@ -13,11 +13,11 @@ public class ClientSessionEntity {
private String id; private String id;
private String clientId; private String clientId;
private String realmId;
private UserSessionEntity session; private UserSessionEntity session;
private String redirectUri; private String redirectUri;
private String state;
private String authMethod; private String authMethod;
private int timestamp; private int timestamp;
@ -41,6 +41,14 @@ public class ClientSessionEntity {
this.clientId = clientId; this.clientId = clientId;
} }
public String getRealmId() {
return realmId;
}
public void setRealmId(String realmId) {
this.realmId = realmId;
}
public UserSessionEntity getSession() { public UserSessionEntity getSession() {
return session; return session;
} }
@ -57,14 +65,6 @@ public class ClientSessionEntity {
this.redirectUri = redirectUri; this.redirectUri = redirectUri;
} }
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}
public int getTimestamp() { public int getTimestamp() {
return timestamp; return timestamp;
} }

View file

@ -10,6 +10,8 @@ import org.keycloak.models.sessions.mongo.entities.MongoClientSessionEntity;
import org.keycloak.models.sessions.mongo.entities.MongoUserSessionEntity; import org.keycloak.models.sessions.mongo.entities.MongoUserSessionEntity;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set; import java.util.Set;
/** /**
@ -21,15 +23,13 @@ public class ClientSessionAdapter implements ClientSessionModel {
private MongoUserSessionProvider provider; private MongoUserSessionProvider provider;
private RealmModel realm; private RealmModel realm;
private MongoClientSessionEntity entity; private MongoClientSessionEntity entity;
private MongoUserSessionEntity userSessionEntity;
private MongoStoreInvocationContext invContext; 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.session = session;
this.provider = provider; this.provider = provider;
this.realm = realm; this.realm = realm;
this.entity = entity; this.entity = entity;
this.userSessionEntity = userSessionEntity;
this.invContext = invContext; this.invContext = invContext;
} }
@ -38,19 +38,40 @@ public class ClientSessionAdapter implements ClientSessionModel {
return entity.getId(); return entity.getId();
} }
@Override
public RealmModel getRealm() {
return session.realms().getRealm(entity.getRealmId());
}
@Override @Override
public ClientModel getClient() { public ClientModel getClient() {
return realm.findClientById(entity.getClientId()); return realm.findClientById(entity.getClientId());
} }
@Override @Override
public String getState() { public UserSessionModel getUserSession() {
return entity.getState(); if (entity.getSessionId() == null) return null;
return provider.getUserSession(realm, entity.getSessionId());
} }
@Override @Override
public UserSessionModel getUserSession() { public void setUserSession(UserSessionModel userSession) {
return new UserSessionAdapter(session, provider, userSessionEntity, realm, invContext); 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<String> roles) {
List<String> list = new LinkedList<String>();
list.addAll(roles);
entity.setRoles(list);
} }
@Override @Override
@ -66,7 +87,6 @@ public class ClientSessionAdapter implements ClientSessionModel {
@Override @Override
public void setTimestamp(int timestamp) { public void setTimestamp(int timestamp) {
entity.setTimestamp(timestamp); entity.setTimestamp(timestamp);
invContext.getMongoStore().updateEntity(userSessionEntity, invContext);
} }
@Override @Override
@ -77,7 +97,6 @@ public class ClientSessionAdapter implements ClientSessionModel {
@Override @Override
public void setAction(Action action) { public void setAction(Action action) {
entity.setAction(action); entity.setAction(action);
invContext.getMongoStore().updateEntity(userSessionEntity, invContext);
} }
@Override @Override

View file

@ -38,49 +38,37 @@ public class MongoUserSessionProvider implements UserSessionProvider {
this.invocationContext = invocationContext; this.invocationContext = invocationContext;
} }
@Override public MongoStore getMongoStore() {
public ClientSessionModel createClientSession(RealmModel realm, ClientModel client, UserSessionModel userSession, String redirectUri, String state, Set<String> roles) { return mongoStore;
MongoUserSessionEntity userSessionEntity = getUserSessionEntity(realm, userSession.getId()); }
@Override
public ClientSessionModel createClientSession(RealmModel realm, ClientModel client) {
MongoClientSessionEntity entity = new MongoClientSessionEntity(); MongoClientSessionEntity entity = new MongoClientSessionEntity();
entity.setId(KeycloakModelUtils.generateId()); entity.setId(KeycloakModelUtils.generateId());
entity.setTimestamp(Time.currentTime()); entity.setTimestamp(Time.currentTime());
entity.setClientId(client.getId()); entity.setClientId(client.getId());
entity.setRedirectUri(redirectUri); entity.setRealmId(realm.getId());
entity.setState(state); return new ClientSessionAdapter(session, this, realm, entity, invocationContext);
if (roles != null) {
entity.setRoles(new LinkedList<String>(roles));
}
mongoStore.pushItemToList(userSessionEntity, "clientSessions", entity, false, invocationContext);
return new ClientSessionAdapter(session, this, realm, entity, userSessionEntity, invocationContext);
} }
@Override @Override
public ClientSessionModel getClientSession(RealmModel realm, String id) { public ClientSessionModel getClientSession(RealmModel realm, String id) {
DBObject query = new QueryBuilder() MongoClientSessionEntity entity = getClientSessionEntity(id);
.and("realmId").is(realm.getId()) if (entity == null) return null;
.and("clientSessions.id").is(id).get(); return new ClientSessionAdapter(session, this, realm, entity, invocationContext);
}
List<MongoUserSessionEntity> entities = mongoStore.loadEntities(MongoUserSessionEntity.class, query, invocationContext); @Override
if (entities.isEmpty()) { 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);
}
return null; return null;
} }
MongoUserSessionEntity userSessionEntity = entities.get(0);
List<MongoClientSessionEntity> 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 @Override
public UserSessionModel createUserSession(RealmModel realm, UserModel user, String loginUsername, String ipAddress, String authMethod, boolean rememberMe) { public UserSessionModel createUserSession(RealmModel realm, UserModel user, String loginUsername, String ipAddress, String authMethod, boolean rememberMe) {
@ -91,6 +79,7 @@ public class MongoUserSessionProvider implements UserSessionProvider {
entity.setIpAddress(ipAddress); entity.setIpAddress(ipAddress);
entity.setAuthMethod(authMethod); entity.setAuthMethod(authMethod);
entity.setRememberMe(rememberMe); entity.setRememberMe(rememberMe);
entity.setRealmId(realm.getId());
int currentTime = Time.currentTime(); int currentTime = Time.currentTime();
@ -115,6 +104,10 @@ public class MongoUserSessionProvider implements UserSessionProvider {
return mongoStore.loadEntity(MongoUserSessionEntity.class, id, invocationContext); return mongoStore.loadEntity(MongoUserSessionEntity.class, id, invocationContext);
} }
MongoClientSessionEntity getClientSessionEntity(String id) {
return mongoStore.loadEntity(MongoClientSessionEntity.class, id, invocationContext);
}
@Override @Override
public List<UserSessionModel> getUserSessions(RealmModel realm, UserModel user) { public List<UserSessionModel> getUserSessions(RealmModel realm, UserModel user) {
DBObject query = new BasicDBObject("user", user.getId()); DBObject query = new BasicDBObject("user", user.getId());
@ -167,21 +160,35 @@ public class MongoUserSessionProvider implements UserSessionProvider {
public void removeUserSessions(RealmModel realm) { public void removeUserSessions(RealmModel realm) {
DBObject query = new BasicDBObject("realmId", realm.getId()); DBObject query = new BasicDBObject("realmId", realm.getId());
mongoStore.removeEntities(MongoUserSessionEntity.class, query, invocationContext); mongoStore.removeEntities(MongoUserSessionEntity.class, query, invocationContext);
query = new QueryBuilder()
.and("realmId").is(realm.getId())
.get();
mongoStore.removeEntities(MongoClientSessionEntity.class, query, invocationContext);
} }
@Override @Override
public void removeExpiredUserSessions(RealmModel realm) { public void removeExpiredUserSessions(RealmModel realm) {
int currentTime = Time.currentTime(); int currentTime = Time.currentTime();
DBObject query = new QueryBuilder() DBObject query = new QueryBuilder()
.and("realmId").is(realm.getId())
.and("started").lessThan(currentTime - realm.getSsoSessionMaxLifespan()) .and("started").lessThan(currentTime - realm.getSsoSessionMaxLifespan())
.get(); .get();
mongoStore.removeEntities(MongoUserSessionEntity.class, query, invocationContext); mongoStore.removeEntities(MongoUserSessionEntity.class, query, invocationContext);
query = new QueryBuilder() query = new QueryBuilder()
.and("realmId").is(realm.getId())
.and("lastSessionRefresh").lessThan(currentTime - realm.getSsoSessionIdleTimeout()) .and("lastSessionRefresh").lessThan(currentTime - realm.getSsoSessionIdleTimeout())
.get(); .get();
mongoStore.removeEntities(MongoUserSessionEntity.class, query, invocationContext); 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 @Override
@ -239,24 +246,9 @@ public class MongoUserSessionProvider implements UserSessionProvider {
// TODO Not very efficient, should use Mongo $pull to remove directly // TODO Not very efficient, should use Mongo $pull to remove directly
public void onClientRemoved(RealmModel realm, ClientModel client) { public void onClientRemoved(RealmModel realm, ClientModel client) {
DBObject query = new QueryBuilder() DBObject query = new QueryBuilder()
.and("clientSessions.clientId").is(client.getId()) .and("clientId").is(client.getId())
.get(); .get();
List<MongoUserSessionEntity> userSessionEntities = mongoStore.loadEntities(MongoUserSessionEntity.class, query, invocationContext); mongoStore.removeEntities(MongoUserSessionEntity.class, query, invocationContext);
for (MongoUserSessionEntity e : userSessionEntities) {
if (e.getClientSessions() == null) {
continue;
}
List<MongoClientSessionEntity> remove = new LinkedList<MongoClientSessionEntity>();
for (MongoClientSessionEntity c : e.getClientSessions()) {
if (c.getClientId().equals(client.getId())) {
remove.add(c);
}
}
for (MongoClientSessionEntity c : remove) {
mongoStore.pullItemFromList(e, "clientSessions", c, invocationContext);
}
}
} }
@Override @Override

View file

@ -135,8 +135,10 @@ public class UserSessionAdapter extends AbstractMongoAdapter<MongoUserSessionEnt
return sessions; return sessions;
} }
for (MongoClientSessionEntity e : entity.getClientSessions()) { for (String id : entity.getClientSessions()) {
sessions.add(new ClientSessionAdapter(keycloakSession, provider, realm, e, entity, invocationContext)); ClientSessionModel clientSession = provider.getClientSession(realm, id);
if (clientSession == null) continue;
sessions.add(clientSession);
} }
return sessions; return sessions;
} }

View file

@ -1,6 +1,9 @@
package org.keycloak.models.sessions.mongo.entities; package org.keycloak.models.sessions.mongo.entities;
import org.keycloak.connections.mongo.api.MongoIdentifiableEntity;
import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext;
import org.keycloak.models.ClientSessionModel; import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.entities.AbstractIdentifiableEntity;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
@ -9,13 +12,14 @@ import java.util.Map;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/ */
public class MongoClientSessionEntity { public class MongoClientSessionEntity extends AbstractIdentifiableEntity implements MongoIdentifiableEntity {
private String id; private String id;
private String clientId; private String clientId;
private String realmId;
private String sessionId;
private String redirectUri; private String redirectUri;
private String state;
private String authMethod; private String authMethod;
private int timestamp; private int timestamp;
@ -39,6 +43,14 @@ public class MongoClientSessionEntity {
this.clientId = clientId; this.clientId = clientId;
} }
public String getRealmId() {
return realmId;
}
public void setRealmId(String realmId) {
this.realmId = realmId;
}
public String getRedirectUri() { public String getRedirectUri() {
return redirectUri; return redirectUri;
} }
@ -47,14 +59,6 @@ public class MongoClientSessionEntity {
this.redirectUri = redirectUri; this.redirectUri = redirectUri;
} }
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}
public String getAuthMethod() { public String getAuthMethod() {
return authMethod; return authMethod;
} }
@ -94,4 +98,17 @@ public class MongoClientSessionEntity {
public void setNotes(Map<String, String> notes) { public void setNotes(Map<String, String> notes) {
this.notes = notes; this.notes = notes;
} }
public String getSessionId() {
return sessionId;
}
public void setSessionId(String sessionId) {
this.sessionId = sessionId;
}
@Override
public void afterRemove(MongoStoreInvocationContext context) {
}
} }

View file

@ -1,10 +1,13 @@
package org.keycloak.models.sessions.mongo.entities; 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.MongoCollection;
import org.keycloak.connections.mongo.api.MongoIdentifiableEntity; import org.keycloak.connections.mongo.api.MongoIdentifiableEntity;
import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext; import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext;
import org.keycloak.models.entities.AbstractIdentifiableEntity; import org.keycloak.models.entities.AbstractIdentifiableEntity;
import java.util.ArrayList;
import java.util.List; import java.util.List;
/** /**
@ -29,7 +32,7 @@ public class MongoUserSessionEntity extends AbstractIdentifiableEntity implement
private int lastSessionRefresh; private int lastSessionRefresh;
private List<MongoClientSessionEntity> clientSessions; private List<String> clientSessions = new ArrayList<String>();
public String getRealmId() { public String getRealmId() {
return realmId; return realmId;
@ -95,16 +98,20 @@ public class MongoUserSessionEntity extends AbstractIdentifiableEntity implement
this.lastSessionRefresh = lastSessionRefresh; this.lastSessionRefresh = lastSessionRefresh;
} }
public List<MongoClientSessionEntity> getClientSessions() { public List<String> getClientSessions() {
return clientSessions; return clientSessions;
} }
public void setClientSessions(List<MongoClientSessionEntity> clientSessions) { public void setClientSessions(List<String> clientSessions) {
this.clientSessions = clientSessions; this.clientSessions = clientSessions;
} }
@Override @Override
public void afterRemove(MongoStoreInvocationContext context) { public void afterRemove(MongoStoreInvocationContext context) {
DBObject query = new QueryBuilder()
.and("sessionId").is(getId())
.get();
context.getMongoStore().removeEntities(MongoClientSessionEntity.class, query, context);
} }
} }

View file

@ -103,7 +103,7 @@ public class AccessCode {
} }
public String getState() { public String getState() {
return clientSession.getState(); throw new RuntimeException("REFACTORING, TODO REMOVE ACCESS CODE");
} }
public String getRedirectUri() { public String getRedirectUri() {

View file

@ -2,6 +2,7 @@ package org.keycloak.services.managers;
import org.keycloak.models.ApplicationModel; import org.keycloak.models.ApplicationModel;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionModel;
@ -18,6 +19,7 @@ public class Auth {
private final UserModel user; private final UserModel user;
private final ClientModel client; private final ClientModel client;
private final UserSessionModel session; private final UserSessionModel session;
private ClientSessionModel clientSession;
public Auth(RealmModel realm, AccessToken token, UserModel user, ClientModel client, UserSessionModel session, boolean cookie) { public Auth(RealmModel realm, AccessToken token, UserModel user, ClientModel client, UserSessionModel session, boolean cookie) {
this.cookie = cookie; this.cookie = cookie;
@ -53,6 +55,14 @@ public class Auth {
return session; return session;
} }
public ClientSessionModel getClientSession() {
return clientSession;
}
public void setClientSession(ClientSessionModel clientSession) {
this.clientSession = clientSession;
}
public boolean hasRealmRole(String role) { public boolean hasRealmRole(String role) {
if (cookie) { if (cookie) {
return user.hasRole(realm.getRole(role)); return user.hasRole(realm.getRole(role));

View file

@ -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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @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<RoleModel> getRequestedRoles() {
Set<RoleModel> requestedRoles = new HashSet<RoleModel>();
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);
}
}
}

View file

@ -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<String> requestedRoles = new HashSet<String>();
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 { public AccessToken refreshAccessToken(KeycloakSession session, UriInfo uriInfo, ClientConnection connection, RealmModel realm, ClientModel client, String encodedRefreshToken, EventBuilder event) throws OAuthErrorException {
RefreshToken refreshToken = verifyRefreshToken(realm, encodedRefreshToken); RefreshToken refreshToken = verifyRefreshToken(realm, encodedRefreshToken);
@ -132,7 +122,21 @@ public class TokenManager {
return token; return token;
} }
public Set<RoleModel> getAccess(String scopeParam, ClientModel client, UserModel user) { public static void attachClientSession(UserSessionModel session, ClientSessionModel clientSession) {
UserModel user = session.getUser();
clientSession.setUserSession(session);
Set<String> requestedRoles = new HashSet<String>();
// todo scope param protocol independent
for (RoleModel r : TokenManager.getAccess(null, clientSession.getClient(), user)) {
requestedRoles.add(r.getId());
}
clientSession.setRoles(requestedRoles);
}
public static Set<RoleModel> getAccess(String scopeParam, ClientModel client, UserModel user) {
// todo scopeParam is ignored until we figure out a scheme that fits with openid connect // todo scopeParam is ignored until we figure out a scheme that fits with openid connect
Set<RoleModel> requestedRoles = new HashSet<RoleModel>(); Set<RoleModel> requestedRoles = new HashSet<RoleModel>();

View file

@ -0,0 +1,17 @@
package org.keycloak.services.protocol;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @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";
}

View file

@ -54,7 +54,9 @@ import org.keycloak.services.ForbiddenException;
import org.keycloak.services.managers.AppAuthManager; import org.keycloak.services.managers.AppAuthManager;
import org.keycloak.services.managers.Auth; import org.keycloak.services.managers.Auth;
import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.messages.Messages; 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.Flows;
import org.keycloak.services.resources.flows.OAuthRedirect; import org.keycloak.services.resources.flows.OAuthRedirect;
import org.keycloak.services.resources.flows.Urls; import org.keycloak.services.resources.flows.Urls;
@ -191,12 +193,15 @@ public class AccountService {
boolean associated = false; boolean associated = false;
for (ClientSessionModel c : userSession.getClientSessions()) { for (ClientSessionModel c : userSession.getClientSessions()) {
if (c.getClient().equals(application)) { if (c.getClient().equals(application)) {
auth.setClientSession(c);
associated = true; associated = true;
break; break;
} }
} }
if (!associated) { 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(); String redirectUri = UriBuilder.fromUri(Urls.accountSocialPage(uriInfo.getBaseUri(), realm.getName())).build().toString();
try { 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) return Flows.social(realm, uriInfo, clientConnection, provider)
.user(user) .redirectToSocialProvider(clientSessionCode);
.putClientAttribute(OAuth2Constants.CLIENT_ID, Constants.ACCOUNT_MANAGEMENT_APP)
.putClientAttribute(OAuth2Constants.STATE, UUID.randomUUID().toString())
.putClientAttribute(OAuth2Constants.REDIRECT_URI, redirectUri)
.redirectToSocialProvider();
} catch (SocialProviderException spe) { } catch (SocialProviderException spe) {
setReferrerOnPage(); setReferrerOnPage();
return account.setError(Messages.SOCIAL_REDIRECT_ERROR).createResponse(AccountPages.SOCIAL); return account.setError(Messages.SOCIAL_REDIRECT_ERROR).createResponse(AccountPages.SOCIAL);

View file

@ -25,6 +25,7 @@ import org.jboss.logging.Logger;
import org.jboss.resteasy.spi.HttpRequest; import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.ClientConnection; import org.keycloak.ClientConnection;
import org.keycloak.OAuth2Constants; import org.keycloak.OAuth2Constants;
import org.keycloak.events.Event;
import org.keycloak.events.EventBuilder; import org.keycloak.events.EventBuilder;
import org.keycloak.events.Details; import org.keycloak.events.Details;
import org.keycloak.events.Errors; import org.keycloak.events.Errors;
@ -44,8 +45,10 @@ import org.keycloak.models.utils.TimeBasedOTP;
import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.services.managers.AccessCode; import org.keycloak.services.managers.AccessCode;
import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.managers.TokenManager; import org.keycloak.services.managers.TokenManager;
import org.keycloak.services.messages.Messages; 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.Flows;
import org.keycloak.services.resources.flows.Urls; import org.keycloak.services.resources.flows.Urls;
import org.keycloak.services.validation.Validation; import org.keycloak.services.validation.Validation;
@ -54,6 +57,7 @@ import javax.ws.rs.Consumes;
import javax.ws.rs.GET; import javax.ws.rs.GET;
import javax.ws.rs.POST; import javax.ws.rs.POST;
import javax.ws.rs.Path; import javax.ws.rs.Path;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context; import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType; 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.core.UriInfo;
import javax.ws.rs.ext.Providers; import javax.ws.rs.ext.Providers;
import java.util.Set; import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
/** /**
@ -102,22 +107,66 @@ public class RequiredActionsService {
this.event = event; 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") @Path("profile")
@POST @POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED) @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public Response updateProfile(final MultivaluedMap<String, String> formData) { public Response updateProfile(@QueryParam("code") String code,
AccessCode accessCode = getAccessCodeEntry(RequiredAction.UPDATE_PROFILE); final MultivaluedMap<String, String> formData) {
if (accessCode == null) { event.event(EventType.UPDATE_PROFILE);
return unauthorized(); 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(clientSession);
initEvent(accessCode);
String error = Validation.validateUpdateProfileForm(formData); String error = Validation.validateUpdateProfileForm(formData);
if (error != null) { 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")); 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(); 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") @Path("totp")
@POST @POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED) @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public Response updateTotp(final MultivaluedMap<String, String> formData) { public Response updateTotp(@QueryParam("code") String code,
AccessCode accessCode = getAccessCodeEntry(RequiredAction.CONFIGURE_TOTP); final MultivaluedMap<String, String> formData) {
if (accessCode == null) { event.event(EventType.UPDATE_TOTP);
return unauthorized(); 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(clientSession);
initEvent(accessCode);
String totp = formData.getFirst("totp"); String totp = formData.getFirst("totp");
String totpSecret = formData.getFirst("totpSecret"); String totpSecret = formData.getFirst("totpSecret");
LoginFormsProvider loginForms = Flows.forms(session, realm, null, uriInfo).setUser(user); LoginFormsProvider loginForms = Flows.forms(session, realm, null, uriInfo).setUser(user);
if (Validation.isEmpty(totp)) { 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())) { } 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(); UserCredentialModel credentials = new UserCredentialModel();
@ -174,113 +231,163 @@ public class RequiredActionsService {
event.clone().event(EventType.UPDATE_TOTP).success(); event.clone().event(EventType.UPDATE_TOTP).success();
return redirectOauth(user, accessCode); return redirectOauth(user, accessCode, clientSession, userSession);
} }
@Path("password") @Path("password")
@POST @POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED) @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public Response updatePassword(final MultivaluedMap<String, String> formData) { public Response updatePassword(@QueryParam("code") String code,
AccessCode accessCode = getAccessCodeEntry(RequiredAction.UPDATE_PASSWORD); final MultivaluedMap<String, String> formData) {
if (accessCode == null) { event.event(EventType.UPDATE_PASSWORD);
return unauthorized(); 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(clientSession);
initEvent(accessCode);
String passwordNew = formData.getFirst("password-new"); String passwordNew = formData.getFirst("password-new");
String passwordConfirm = formData.getFirst("password-confirm"); String passwordConfirm = formData.getFirst("password-confirm");
LoginFormsProvider loginForms = Flows.forms(session, realm, null, uriInfo).setUser(user); LoginFormsProvider loginForms = Flows.forms(session, realm, null, uriInfo).setUser(user);
if (Validation.isEmpty(passwordNew)) { 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)) { } 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 { try {
session.users().updateCredential(realm, user, UserCredentialModel.password(passwordNew)); session.users().updateCredential(realm, user, UserCredentialModel.password(passwordNew));
} catch (Exception ape) { } 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); user.removeRequiredAction(RequiredAction.UPDATE_PASSWORD);
event.clone().event(EventType.UPDATE_PASSWORD).success(); 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 // 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) { if (accessCode.getSessionState() == null) {
return Response.seeOther(Urls.accountPage(uriInfo.getBaseUri(), realm.getId())).build(); return Response.seeOther(Urls.accountPage(uriInfo.getBaseUri(), realm.getId())).build();
} else { } else {
return redirectOauth(user, accessCode); return redirectOauth(user, accessCode);
} }
*/
} }
@Path("email-verification") @Path("email-verification")
@GET @GET
public Response emailVerification() { public Response emailVerification(@QueryParam("code") String code) {
event.event(EventType.VERIFY_EMAIL);
if (uriInfo.getQueryParameters().containsKey("key")) { if (uriInfo.getQueryParameters().containsKey("key")) {
AccessCode accessCode = AccessCode.parse(uriInfo.getQueryParameters().getFirst("key"), session, realm); Checks checks = new Checks();
if (accessCode == null || !accessCode.isValid(RequiredAction.VERIFY_EMAIL)) { if (!checks.check(code, ClientSessionModel.Action.VERIFY_EMAIL)) {
return unauthorized(); return checks.response;
} }
ClientSessionCode accessCode = checks.clientCode;
UserModel user = getUser(accessCode); ClientSessionModel clientSession = accessCode.getClientSession();
UserSessionModel userSession = clientSession.getUserSession();
initEvent(accessCode); 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.setEmailVerified(true);
user.removeRequiredAction(RequiredAction.VERIFY_EMAIL); 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 { } else {
AccessCode accessCode = getAccessCodeEntry(RequiredAction.VERIFY_EMAIL); Checks checks = new Checks();
if (accessCode == null) { if (!checks.check(code, ClientSessionModel.Action.VERIFY_EMAIL)) {
return unauthorized(); 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); .createResponse(RequiredAction.VERIFY_EMAIL);
} }
} }
@Path("password-reset") @Path("password-reset")
@GET @GET
public Response passwordReset() { public Response passwordReset(@QueryParam("code") String code) {
event.event(EventType.SEND_RESET_PASSWORD);
if (uriInfo.getQueryParameters().containsKey("key")) { if (uriInfo.getQueryParameters().containsKey("key")) {
AccessCode accessCode = AccessCode.parse(uriInfo.getQueryParameters().getFirst("key"), session, realm); Checks checks = new Checks();
if (accessCode == null || !accessCode.isValid(RequiredAction.UPDATE_PASSWORD)) { if (!checks.check(code, ClientSessionModel.Action.UPDATE_PASSWORD)) {
return unauthorized(); return checks.response;
} }
ClientSessionCode accessCode = checks.clientCode;
return Flows.forms(session, realm, null, uriInfo).setAccessCode(accessCode.getCode()).createResponse(RequiredAction.UPDATE_PASSWORD); 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 { } else {
return Flows.forms(session, realm, null, uriInfo).createPasswordReset(); return Flows.forms(session, realm, null, uriInfo)
.setAccessCode(code)
.createPasswordReset();
} }
} }
@Path("password-reset") @Path("password-reset")
@POST @POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED) @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public Response sendPasswordReset(final MultivaluedMap<String, String> formData) { public Response sendPasswordReset(@QueryParam("code") String code,
final MultivaluedMap<String, String> 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 username = formData.getFirst("username");
String scopeParam = uriInfo.getQueryParameters().getFirst(OAuth2Constants.SCOPE); ClientModel client = clientSession.getClient();
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);
if (client == null) { if (client == null) {
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, return Flows.forwardToSecurityFailurePage(session, realm, uriInfo,
"Unknown login requester."); "Unknown login requester.");
@ -290,8 +397,8 @@ public class RequiredActionsService {
"Login requester not enabled."); "Login requester not enabled.");
} }
event.event(EventType.SEND_RESET_PASSWORD).client(clientId) event.client(client.getClientId())
.detail(Details.REDIRECT_URI, redirect) .detail(Details.REDIRECT_URI, clientSession.getRedirectUri())
.detail(Details.RESPONSE_TYPE, "code") .detail(Details.RESPONSE_TYPE, "code")
.detail(Details.AUTH_METHOD, "form") .detail(Details.AUTH_METHOD, "form")
.detail(Details.USERNAME, username); .detail(Details.USERNAME, username);
@ -306,55 +413,35 @@ public class RequiredActionsService {
} else { } else {
UserSessionModel userSession = session.sessions().createUserSession(realm, user, username, clientConnection.getRemoteAddr(), "form", false); UserSessionModel userSession = session.sessions().createUserSession(realm, user, username, clientConnection.getRemoteAddr(), "form", false);
event.session(userSession); event.session(userSession);
TokenManager.attachClientSession(userSession, clientSession);
AccessCode accessCode = tokenManager.createAccessCode(scopeParam, state, redirect, session, realm, client, user, userSession);
accessCode.setRequiredAction(RequiredAction.UPDATE_PASSWORD); accessCode.setRequiredAction(RequiredAction.UPDATE_PASSWORD);
try { try {
UriBuilder builder = Urls.loginPasswordResetBuilder(uriInfo.getBaseUri()); 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(); String link = builder.build(realm.getName()).toString();
long expiration = TimeUnit.SECONDS.toMinutes(realm.getAccessCodeLifespanUserAction()); long expiration = TimeUnit.SECONDS.toMinutes(realm.getAccessCodeLifespanUserAction());
this.session.getProvider(EmailProvider.class).setRealm(realm).setUser(user).sendPasswordReset(link, expiration); 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) { } catch (EmailException e) {
logger.error("Failed to send password reset email", 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(); return Flows.forms(session, realm, client, uriInfo).setSuccess("emailSent").createPasswordReset();
} }
private AccessCode getAccessCodeEntry(RequiredAction requiredAction) { private Response redirectOauth(UserModel user, ClientSessionCode accessCode, ClientSessionModel clientSession, UserSessionModel userSession) {
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) {
if (accessCode == null) { if (accessCode == null) {
return null; return null;
} }
@ -362,37 +449,40 @@ public class RequiredActionsService {
Set<RequiredAction> requiredActions = user.getRequiredActions(); Set<RequiredAction> requiredActions = user.getRequiredActions();
if (!requiredActions.isEmpty()) { if (!requiredActions.isEmpty()) {
accessCode.setRequiredAction(requiredActions.iterator().next()); 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()); .createResponse(requiredActions.iterator().next());
} else { } else {
logger.debugv("Redirecting to: {0}", accessCode.getRedirectUri()); logger.debugv("Redirecting to: {0}", clientSession.getRedirectUri());
accessCode.setAction(ClientSessionModel.Action.CODE_TO_TOKEN); accessCode.setAction(ClientSessionModel.Action.CODE_TO_TOKEN);
AuthenticationManager authManager = new AuthenticationManager(); AuthenticationManager authManager = new AuthenticationManager();
UserSessionModel userSession = session.sessions().getUserSession(realm, accessCode.getSessionState());
if (!AuthenticationManager.isSessionValid(realm, userSession)) { if (!AuthenticationManager.isSessionValid(realm, userSession)) {
AuthenticationManager.logout(session, realm, userSession, uriInfo, clientConnection); 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.session(userSession);
event.success(); event.success();
return Flows.oauth(this.session, realm, request, uriInfo, clientConnection, authManager, tokenManager).redirectAccessCode(accessCode, return Flows.oauth(this.session, realm, request, uriInfo, clientConnection, authManager, tokenManager)
userSession, accessCode.getState(), accessCode.getRedirectUri()); .redirectAccessCode(accessCode,
userSession, clientSession.getNote(OpenIdConnectProtocol.STATE_PARAM), clientSession.getRedirectUri());
} }
} }
private void initEvent(AccessCode accessCode) { private void initEvent(ClientSessionModel clientSession) {
event.event(EventType.LOGIN).client(accessCode.getClient()) event.event(EventType.LOGIN).client(clientSession.getClient())
.user(accessCode.getUser()) .user(clientSession.getUserSession().getUser())
.session(accessCode.getSessionState()) .session(clientSession.getUserSession().getId())
.detail(Details.CODE_ID, accessCode.getCodeId()) .detail(Details.CODE_ID, clientSession.getId())
.detail(Details.REDIRECT_URI, accessCode.getRedirectUri()) .detail(Details.REDIRECT_URI, clientSession.getRedirectUri())
.detail(Details.RESPONSE_TYPE, "code"); .detail(Details.RESPONSE_TYPE, "code");
UserSessionModel userSession = accessCode.getSessionState() != null ? session.sessions().getUserSession(realm, accessCode.getSessionState()) : null; UserSessionModel userSession = clientSession.getUserSession();
if (userSession != null) { if (userSession != null) {
event.detail(Details.AUTH_METHOD, userSession.getAuthMethod()); 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();
}
} }

View file

@ -33,6 +33,7 @@ import org.keycloak.events.EventType;
import org.keycloak.jose.jws.JWSInput; import org.keycloak.jose.jws.JWSInput;
import org.keycloak.models.AccountRoles; import org.keycloak.models.AccountRoles;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.Constants; import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelDuplicateException; import org.keycloak.models.ModelDuplicateException;
@ -41,6 +42,7 @@ import org.keycloak.models.SocialLinkModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.managers.EventsManager; import org.keycloak.services.managers.EventsManager;
import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.RealmManager; import org.keycloak.services.managers.RealmManager;
@ -106,76 +108,55 @@ public class SocialResource {
@GET @GET
@Path("callback") @Path("callback")
public Response callback(@QueryParam("state") String encodedState) throws URISyntaxException, IOException { public Response callback(@QueryParam("state") String encodedState) throws URISyntaxException, IOException {
State initialRequest; ClientSessionCode clientCode = null;
ClientSessionModel clientSession = null;
try { 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) { } catch (Throwable t) {
logger.error("Invalid social callback", t); logger.error("Invalid social callback", t);
return Flows.forms(session, null, null, uriInfo).setError("Unexpected callback").createErrorPage(); 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(); String authMethod = "social@" + provider.getId();
RealmManager realmManager = new RealmManager(session); RealmModel realm = clientSession.getRealm();
RealmModel realm = realmManager.getRealmByName(realmName);
EventBuilder event = new EventsManager(realm, session, clientConnection).createEventBuilder() EventBuilder event = new EventsManager(realm, session, clientConnection).createEventBuilder()
.event(EventType.LOGIN) .event(EventType.SOCIAL_LOGIN)
.detail(Details.RESPONSE_TYPE, initialRequest.get(OAuth2Constants.RESPONSE_TYPE)) .client(clientSession.getClient())
.detail(Details.REDIRECT_URI, clientSession.getRedirectUri())
.detail(Details.AUTH_METHOD, authMethod); .detail(Details.AUTH_METHOD, authMethod);
AuthenticationManager authManager = new AuthenticationManager();
OAuthFlows oauth = Flows.oauth(session, realm, request, uriInfo, clientConnection, authManager, tokenManager);
if (!realm.isEnabled()) { if (!realm.isEnabled()) {
event.error(Errors.REALM_DISABLED); event.error(Errors.REALM_DISABLED);
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Realm not enabled."); 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 key = realm.getSocialConfig().get(provider.getId() + ".key");
String secret = realm.getSocialConfig().get(provider.getId() + ".secret"); String secret = realm.getSocialConfig().get(provider.getId() + ".secret");
String callbackUri = Urls.socialCallback(uriInfo.getBaseUri()).toString(); String callbackUri = Urls.socialCallback(uriInfo.getBaseUri()).toString();
SocialProviderConfig config = new SocialProviderConfig(key, secret, callbackUri); SocialProviderConfig config = new SocialProviderConfig(key, secret, callbackUri);
Map<String, String[]> queryParams = getQueryParams(); Map<String, String[]> queryParams = getQueryParams();
Map<String, String> attributes = getAttributes();
AuthCallback callback = new AuthCallback(queryParams, attributes); AuthCallback callback = new AuthCallback(queryParams);
SocialUser socialUser; SocialUser socialUser;
try { try {
socialUser = provider.processCallback(config, callback); socialUser = provider.processCallback(clientSession, config, callback);
} catch (SocialAccessDeniedException e) { } catch (SocialAccessDeniedException e) {
MultivaluedMap<String, String> queryParms = new MultivaluedMapImpl<String, String>();
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); 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) { } catch (SocialProviderException e) {
logger.error("Failed to process social callback", e); logger.error("Failed to process social callback", e);
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Failed to process social callback"); 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); UserModel user = session.users().getUserBySocialLink(socialLink, realm);
// Check if user is already authenticated (this means linking social into existing user account) // Check if user is already authenticated (this means linking social into existing user account)
String userId = initialRequest.getUser(); if (clientSession.getUserSession() != null) {
if (userId != null) {
UserModel authenticatedUser = session.users().getUserById(userId, realm);
event.event(EventType.SOCIAL_LINK).user(userId); UserModel authenticatedUser = clientSession.getUserSession().getUser();
event.event(EventType.SOCIAL_LINK).user(authenticatedUser.getId());
if (user != null) { if (user != null) {
event.error(Errors.SOCIAL_ID_IN_USE); 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"); 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); session.users().addSocialLink(realm, authenticatedUser, socialLink);
logger.debugv("Social provider {0} linked with user {1}", provider.getId(), authenticatedUser.getUsername()); logger.debugv("Social provider {0} linked with user {1}", provider.getId(), authenticatedUser.getUsername());
event.success(); 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) { if (user == null) {
@ -252,31 +228,72 @@ public class SocialResource {
UserSessionModel userSession = session.sessions().createUserSession(realm, user, username, clientConnection.getRemoteAddr(), authMethod, false); UserSessionModel userSession = session.sessions().createUserSession(realm, user, username, clientConnection.getRemoteAddr(), authMethod, false);
event.session(userSession); 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()) { if (session.getTransaction().isActive()) {
session.getTransaction().commit(); session.getTransaction().commit();
} }
return response; return response;
} catch (ModelDuplicateException e) { } catch (ModelDuplicateException e) {
// Assume email is the duplicate as there's nothing else atm // 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 @GET
@Path("{realm}/login") @Path("{realm}/login")
public Response redirectToProviderAuth(@PathParam("realm") final String realmName, public Response redirectToProviderAuth(@PathParam("realm") final String realmName,
@QueryParam("provider_id") final String providerId, @QueryParam(OAuth2Constants.CLIENT_ID) final String clientId, @QueryParam("provider_id") final String providerId,
@QueryParam("scope") final String scope, @QueryParam("state") final String state, @QueryParam("code") String code) {
@QueryParam("redirect_uri") String redirectUri, @QueryParam("response_type") String responseType) {
RealmManager realmManager = new RealmManager(session); RealmManager realmManager = new RealmManager(session);
RealmModel realm = realmManager.getRealmByName(realmName); RealmModel realm = realmManager.getRealmByName(realmName);
EventBuilder event = new EventsManager(realm, session, clientConnection).createEventBuilder() EventBuilder event = new EventsManager(realm, session, clientConnection).createEventBuilder()
.event(EventType.LOGIN).client(clientId) .event(EventType.LOGIN)
.detail(Details.REDIRECT_URI, redirectUri)
.detail(Details.RESPONSE_TYPE, "code")
.detail(Details.AUTH_METHOD, "social@" + providerId); .detail(Details.AUTH_METHOD, "social@" + providerId);
SocialProvider provider = SocialLoader.load(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(); return Flows.forms(session, realm, null, uriInfo).setError("Social provider not found").createErrorPage();
} }
ClientModel client = realm.findClient(clientId); Checks checks = new Checks();
if (client == null) { if (!checks.check(event, realm, code, ClientSessionModel.Action.AUTHENTICATE)) {
event.error(Errors.CLIENT_NOT_FOUND); return checks.response;
return Flows.forms(session, realm, null, uriInfo).setError("Unknown login requester.").createErrorPage();
} }
if (!client.isEnabled()) { ClientSessionCode clientSessionCode = checks.clientCode;
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();
}
try { try {
return Flows.social(realm, uriInfo, clientConnection, provider) return Flows.social(realm, uriInfo, clientConnection, provider)
.putClientAttribute(OAuth2Constants.CLIENT_ID, clientId) .redirectToSocialProvider(clientSessionCode);
.putClientAttribute(OAuth2Constants.SCOPE, scope)
.putClientAttribute(OAuth2Constants.STATE, state)
.putClientAttribute(OAuth2Constants.REDIRECT_URI, redirectUri)
.putClientAttribute(OAuth2Constants.RESPONSE_TYPE, responseType)
.redirectToSocialProvider();
} catch (Throwable t) { } catch (Throwable t) {
logger.error("Failed to redirect to social auth", 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(); return Flows.forms(session, realm, null, uriInfo).setError("Failed to redirect to social auth").createErrorPage();
@ -334,52 +337,4 @@ public class SocialResource {
return queryParams; return queryParams;
} }
private Map<String, String> 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<String, String> attributes = new HashMap<String, String>();
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<String, String> getAttributes() {
return attributes;
}
public String get(String key) {
return attributes.get(key);
}
public void set(String key, String value) {
attributes.put(key, value);
}
}
} }

View file

@ -36,13 +36,14 @@ import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.RefreshToken; import org.keycloak.representations.RefreshToken;
import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.services.ForbiddenException; import org.keycloak.services.ForbiddenException;
import org.keycloak.services.managers.AccessCode;
import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.AuthenticationManager.AuthenticationStatus; import org.keycloak.services.managers.AuthenticationManager.AuthenticationStatus;
import org.keycloak.representations.PasswordToken; import org.keycloak.representations.PasswordToken;
import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.managers.ResourceAdminManager; import org.keycloak.services.managers.ResourceAdminManager;
import org.keycloak.services.managers.TokenManager; import org.keycloak.services.managers.TokenManager;
import org.keycloak.services.messages.Messages; 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.Flows;
import org.keycloak.services.resources.flows.OAuthFlows; import org.keycloak.services.resources.flows.OAuthFlows;
import org.keycloak.services.resources.flows.Urls; 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! * URL called after login page. YOU SHOULD NEVER INVOKE THIS DIRECTLY!
* *
* @param clientId * @param code
* @param scopeParam
* @param state
* @param redirect
* @param formData * @param formData
* @return * @return
*/ */
@Path("auth/request/login") @Path("auth/request/login")
@POST @POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED) @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public Response processLogin(@QueryParam("client_id") final String clientId, @QueryParam("scope") final String scopeParam, public Response processLogin(@QueryParam("code") String code,
@QueryParam("state") final String state, @QueryParam("redirect_uri") String redirect,
final MultivaluedMap<String, String> formData) { final MultivaluedMap<String, String> 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 username = formData.getFirst(AuthenticationManager.FORM_USERNAME);
String rememberMe = formData.getFirst("rememberMe"); String rememberMe = formData.getFirst("rememberMe");
boolean remember = rememberMe != null && rememberMe.equalsIgnoreCase("on"); boolean remember = rememberMe != null && rememberMe.equalsIgnoreCase("on");
event.event(EventType.LOGIN).client(clientId) event.client(clientSession.getClient().getClientId())
.detail(Details.REDIRECT_URI, redirect) .detail(Details.REDIRECT_URI, clientSession.getRedirectUri())
.detail(Details.RESPONSE_TYPE, "code") .detail(Details.RESPONSE_TYPE, "code")
.detail(Details.AUTH_METHOD, "form") .detail(Details.AUTH_METHOD, "form")
.detail(Details.USERNAME, username); .detail(Details.USERNAME, username);
@ -492,15 +513,7 @@ public class TokenService {
OAuthFlows oauth = Flows.oauth(session, realm, request, uriInfo, clientConnection, authManager, tokenManager); OAuthFlows oauth = Flows.oauth(session, realm, request, uriInfo, clientConnection, authManager, tokenManager);
if (!checkSsl()) { ClientModel client = clientSession.getClient();
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) { if (client == null) {
event.error(Errors.CLIENT_NOT_FOUND); event.error(Errors.CLIENT_NOT_FOUND);
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Unknown login requester."); 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."); 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")) { if (formData.containsKey("cancel")) {
event.error(Errors.REJECTED_BY_USER); 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); AuthenticationStatus status = authManager.authenticateForm(session, clientConnection, realm, formData);
@ -538,28 +545,45 @@ public class TokenService {
case SUCCESS: case SUCCESS:
case ACTIONS_REQUIRED: case ACTIONS_REQUIRED:
UserSessionModel userSession = session.sessions().createUserSession(realm, user, username, clientConnection.getRemoteAddr(), "form", remember); UserSessionModel userSession = session.sessions().createUserSession(realm, user, username, clientConnection.getRemoteAddr(), "form", remember);
TokenManager.attachClientSession(userSession, clientSession);
event.session(userSession); event.session(userSession);
return oauth.processAccessCode(scopeParam, state, redirect, client, user, userSession, event); return oauth.processAccessCode(clientSession, userSession, event);
case ACCOUNT_TEMPORARILY_DISABLED: case ACCOUNT_TEMPORARILY_DISABLED:
event.error(Errors.USER_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: case ACCOUNT_DISABLED:
event.error(Errors.USER_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: case MISSING_TOTP:
formData.remove(CredentialRepresentation.PASSWORD); formData.remove(CredentialRepresentation.PASSWORD);
String passwordToken = new JWSBuilder().jsonContent(new PasswordToken(realm.getName(), user.getId())).rsa256(realm.getPrivateKey()); String passwordToken = new JWSBuilder().jsonContent(new PasswordToken(realm.getName(), user.getId())).rsa256(realm.getPrivateKey());
formData.add(CredentialRepresentation.PASSWORD_TOKEN, passwordToken); 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: case INVALID_USER:
event.error(Errors.USER_NOT_FOUND); 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: default:
event.error(Errors.INVALID_USER_CREDENTIALS); 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 * Registration
* *
* @param clientId * @param code
* @param scopeParam
* @param state
* @param redirect
* @param formData * @param formData
* @return * @return
*/ */
@Path("registrations") @Path("registrations")
@POST @POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED) @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public Response processRegister(@QueryParam("client_id") final String clientId, public Response processRegister(@QueryParam("code") String code,
@QueryParam("scope") final String scopeParam, @QueryParam("state") final String state, final MultivaluedMap<String, String> formData) {
@QueryParam("redirect_uri") String redirect, final MultivaluedMap<String, String> 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 username = formData.getFirst("username");
String email = formData.getFirst("email"); String email = formData.getFirst("email");
ClientSessionModel clientSession = clientCode.getClientSession();
event.event(EventType.REGISTER).client(clientId) event.client(clientSession.getClient())
.detail(Details.REDIRECT_URI, redirect) .detail(Details.REDIRECT_URI, clientSession.getRedirectUri())
.detail(Details.RESPONSE_TYPE, "code") .detail(Details.RESPONSE_TYPE, "code")
.detail(Details.USERNAME, username) .detail(Details.USERNAME, username)
.detail(Details.EMAIL, email) .detail(Details.EMAIL, email)
@ -605,7 +648,7 @@ public class TokenService {
event.error(Errors.REALM_DISABLED); event.error(Errors.REALM_DISABLED);
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Realm not enabled"); return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Realm not enabled");
} }
ClientModel client = realm.findClient(clientId); ClientModel client = clientSession.getClient();
if (client == null) { if (client == null) {
event.error(Errors.CLIENT_NOT_FOUND); event.error(Errors.CLIENT_NOT_FOUND);
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Unknown login requester."); 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."); 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<String> requiredCredentialTypes = new LinkedList<String>(); List<String> requiredCredentialTypes = new LinkedList<String>();
for (RequiredCredentialModel m : realm.getRequiredCredentials()) { for (RequiredCredentialModel m : realm.getRequiredCredentials()) {
@ -640,19 +673,31 @@ public class TokenService {
if (error != null) { if (error != null) {
event.error(Errors.INVALID_REGISTRATION); 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 // Validate that user with this username doesn't exist in realm or any federation provider
if (session.users().getUserByUsername(username, realm) != null) { if (session.users().getUserByUsername(username, realm) != null) {
event.error(Errors.USERNAME_IN_USE); 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 // Validate that user with this email doesn't exist in realm or any federation provider
if (session.users().getUserByEmail(email, realm) != null) { if (session.users().getUserByEmail(email, realm) != null) {
event.error(Errors.EMAIL_IN_USE); 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); UserModel user = session.users().addUser(realm, username);
@ -680,14 +725,17 @@ public class TokenService {
// User already registered, but force him to update password // User already registered, but force him to update password
if (!passwordUpdateSuccessful) { if (!passwordUpdateSuccessful) {
user.addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD); 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.user(user).success();
event.reset(); 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); event.error(Errors.INVALID_CODE);
throw new BadRequestException("Code not specified", Response.status(Response.Status.BAD_REQUEST).entity(error).type("application/json").build()); throw new BadRequestException("Code not specified", Response.status(Response.Status.BAD_REQUEST).entity(error).type("application/json").build());
} }
ClientSessionCode accessCode = ClientSessionCode.parse(code, session, realm);
AccessCode accessCode = AccessCode.parse(code, session, realm);
if (accessCode == null) { if (accessCode == null) {
String[] parts = code.split("\\."); String[] parts = code.split("\\.");
if (parts.length == 2) { 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) return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(res)
.build(); .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)) { if (!accessCode.isValid(ClientSessionModel.Action.CODE_TO_TOKEN)) {
Map<String, String> res = new HashMap<String, String>(); Map<String, String> res = new HashMap<String, String>();
res.put(OAuth2Constants.ERROR, "invalid_grant"); res.put(OAuth2Constants.ERROR, "invalid_grant");
@ -765,13 +813,13 @@ public class TokenService {
} }
accessCode.setAction(null); accessCode.setAction(null);
UserSessionModel userSession = clientSession.getUserSession();
event.user(accessCode.getUser()); event.user(userSession.getUser());
event.session(accessCode.getSessionState()); event.session(userSession.getId());
ClientModel client = authorizeClient(authorizationHeader, formData, event); ClientModel client = authorizeClient(authorizationHeader, formData, event);
if (!client.getClientId().equals(accessCode.getClient().getClientId())) { if (!client.getClientId().equals(clientSession.getClient().getClientId())) {
Map<String, String> res = new HashMap<String, String>(); Map<String, String> res = new HashMap<String, String>();
res.put(OAuth2Constants.ERROR, "invalid_grant"); res.put(OAuth2Constants.ERROR, "invalid_grant");
res.put(OAuth2Constants.ERROR_DESCRIPTION, "Auth error"); res.put(OAuth2Constants.ERROR_DESCRIPTION, "Auth error");
@ -780,7 +828,7 @@ public class TokenService {
.build(); .build();
} }
UserModel user = session.users().getUserById(accessCode.getUser().getId(), realm); UserModel user = session.users().getUserById(userSession.getUser().getId(), realm);
if (user == null) { if (user == null) {
Map<String, String> res = new HashMap<String, String>(); Map<String, String> res = new HashMap<String, String>();
res.put(OAuth2Constants.ERROR, "invalid_grant"); res.put(OAuth2Constants.ERROR, "invalid_grant");
@ -799,7 +847,6 @@ public class TokenService {
.build(); .build();
} }
UserSessionModel userSession = session.sessions().getUserSession(realm, accessCode.getSessionState());
if (!AuthenticationManager.isSessionValid(realm, userSession)) { if (!AuthenticationManager.isSessionValid(realm, userSession)) {
AuthenticationManager.logout(session, realm, userSession, uriInfo, clientConnection); AuthenticationManager.logout(session, realm, userSession, uriInfo, clientConnection);
Map<String, String> res = new HashMap<String, String>(); Map<String, String> res = new HashMap<String, String>();
@ -893,37 +940,62 @@ public class TokenService {
} }
/** /**
* Login page. Must be redirected to from the application or oauth client. * checks input and initializes variables based on a front page action like the login page or registration page
* *
* @See <a href="http://tools.ietf.org/html/rfc6749#section-4.1">http://tools.ietf.org/html/rfc6749#section-4.1</a>
*
*
* @param responseType
* @param redirect
* @param clientId
* @param scopeParam
* @param state
* @param prompt
* @return
*/ */
@Path("login") private class FrontPageInitializer {
@GET protected String code;
public Response loginPage(final @QueryParam("response_type") String responseType, protected String clientId;
@QueryParam("redirect_uri") String redirect, final @QueryParam("client_id") String clientId, protected String redirect;
final @QueryParam("scope") String scopeParam, final @QueryParam("state") String state, final @QueryParam("prompt") String prompt, protected String state;
final @QueryParam("login_hint") String loginHint) { protected String scopeParam;
event.event(EventType.LOGIN).client(clientId).detail(Details.REDIRECT_URI, redirect).detail(Details.RESPONSE_TYPE, "code"); protected String responseType;
protected String loginHint;
OAuthFlows oauth = Flows.oauth(session, realm, request, uriInfo, clientConnection, authManager, tokenManager); 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()) { if (!checkSsl()) {
event.error(Errors.SSL_REQUIRED);
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "HTTPS required"); return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "HTTPS required");
} }
if (!realm.isEnabled()) { if (!realm.isEnabled()) {
event.error(Errors.REALM_DISABLED); event.error(Errors.REALM_DISABLED);
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Realm not enabled"); 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); ClientModel client = realm.findClient(clientId);
if (client == null) { if (client == null) {
event.error(Errors.CLIENT_NOT_FOUND); event.error(Errors.CLIENT_NOT_FOUND);
@ -947,21 +1019,85 @@ public class TokenService {
event.error(Errors.INVALID_REDIRECT_URI); event.error(Errors.INVALID_REDIRECT_URI);
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "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 <a href="http://tools.ietf.org/html/rfc6749#section-4.1">http://tools.ietf.org/html/rfc6749#section-4.1</a>
*
*
* @param code
* @param responseType
* @param redirect
* @param clientId
* @param scopeParam
* @param state
* @param prompt
* @return
*/
@Path("login")
@GET
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);
AuthenticationManager.AuthResult authResult = authManager.authenticateIdentityCookie(session, realm, uriInfo, clientConnection, headers); AuthenticationManager.AuthResult authResult = authManager.authenticateIdentityCookie(session, realm, uriInfo, clientConnection, headers);
if (authResult != null) { if (authResult != null) {
UserModel user = authResult.getUser(); UserModel user = authResult.getUser();
UserSessionModel session = authResult.getSession(); UserSessionModel session = authResult.getSession();
TokenManager.attachClientSession(session, clientSession);
event.user(user).session(session).detail(Details.AUTH_METHOD, "sso"); 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")) { 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; String rememberMeUsername = null;
if (realm.isRememberMe()) { if (realm.isRememberMe()) {
@ -999,46 +1135,35 @@ public class TokenService {
*/ */
@Path("registrations") @Path("registrations")
@GET @GET
public Response registerPage(final @QueryParam("response_type") String responseType, public Response registerPage(@QueryParam("code") String code,
@QueryParam("redirect_uri") String redirect, final @QueryParam("client_id") String clientId, @QueryParam("response_type") String responseType,
final @QueryParam("scope") String scopeParam, final @QueryParam("state") String state) { @QueryParam(OpenIdConnectProtocol.REDIRECT_URI_PARAM) String redirect,
event.event(EventType.REGISTER).client(clientId).detail(Details.REDIRECT_URI, redirect).detail(Details.RESPONSE_TYPE, "code"); @QueryParam(OpenIdConnectProtocol.CLIENT_ID_PARAM) String clientId,
@QueryParam("scope") String scopeParam,
OAuthFlows oauth = Flows.oauth(session, realm, request, uriInfo, clientConnection, authManager, tokenManager); @QueryParam("state") String state) {
event.event(EventType.REGISTER);
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.");
}
if (!realm.isRegistrationAllowed()) { if (!realm.isRegistrationAllowed()) {
event.error(Errors.REGISTRATION_DISABLED); event.error(Errors.REGISTRATION_DISABLED);
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Registration not allowed"); 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); 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") @Path("logout")
@GET @GET
@NoCache @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); event.event(EventType.LOGOUT);
if (redirectUri != null) { if (redirectUri != null) {
event.detail(Details.REDIRECT_URI, redirectUri); event.detail(Details.REDIRECT_URI, redirectUri);
@ -1150,22 +1275,22 @@ public class TokenService {
String code = formData.getFirst(OAuth2Constants.CODE); 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)) { if (accessCode == null || !accessCode.isValid(ClientSessionModel.Action.OAUTH_GRANT)) {
event.error(Errors.INVALID_CODE); event.error(Errors.INVALID_CODE);
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Invalid access 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 redirect = clientSession.getRedirectUri();
String state = accessCode.getState();
event.client(accessCode.getClient()) event.client(clientSession.getClient())
.user(accessCode.getUser()) .user(clientSession.getUserSession().getUser())
.detail(Details.RESPONSE_TYPE, "code") .detail(Details.RESPONSE_TYPE, "code")
.detail(Details.REDIRECT_URI, redirect); .detail(Details.REDIRECT_URI, redirect);
UserSessionModel userSession = session.sessions().getUserSession(realm, accessCode.getSessionState()); UserSessionModel userSession = clientSession.getUserSession();
if (userSession != null) { if (userSession != null) {
event.detail(Details.AUTH_METHOD, userSession.getAuthMethod()); event.detail(Details.AUTH_METHOD, userSession.getAuthMethod());
event.detail(Details.USERNAME, userSession.getLoginUsername()); event.detail(Details.USERNAME, userSession.getLoginUsername());
@ -1183,13 +1308,13 @@ public class TokenService {
if (formData.containsKey("cancel")) { if (formData.containsKey("cancel")) {
event.error(Errors.REJECTED_BY_USER); event.error(Errors.REJECTED_BY_USER);
return redirectAccessDenied(redirect, state); return redirectAccessDenied(redirect, clientSession.getNote(OpenIdConnectProtocol.STATE_PARAM));
} }
event.success(); event.success();
accessCode.setAction(ClientSessionModel.Action.CODE_TO_TOKEN); 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") @Path("oauth/oob")

View file

@ -9,6 +9,7 @@ import org.keycloak.email.EmailException;
import org.keycloak.email.EmailProvider; import org.keycloak.email.EmailProvider;
import org.keycloak.models.ApplicationModel; import org.keycloak.models.ApplicationModel;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.Constants; import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelDuplicateException; 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.UserRepresentation;
import org.keycloak.representations.idm.UserSessionRepresentation; import org.keycloak.representations.idm.UserSessionRepresentation;
import org.keycloak.services.managers.AccessCode; import org.keycloak.services.managers.AccessCode;
import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.managers.RealmManager; import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.managers.ResourceAdminManager; import org.keycloak.services.managers.ResourceAdminManager;
import org.keycloak.services.managers.TokenManager; import org.keycloak.services.managers.TokenManager;
import org.keycloak.services.managers.UserManager; 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.Flows;
import org.keycloak.services.resources.flows.Urls; import org.keycloak.services.resources.flows.Urls;
@ -58,6 +61,7 @@ import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
/** /**
@ -878,8 +882,6 @@ public class UsersResource {
String redirect = Urls.accountBase(uriInfo.getBaseUri()).path("/").build(realm.getName()).toString(); String redirect = Urls.accountBase(uriInfo.getBaseUri()).path("/").build(realm.getName()).toString();
String clientId = Constants.ACCOUNT_MANAGEMENT_APP; String clientId = Constants.ACCOUNT_MANAGEMENT_APP;
String state = null;
String scope = null;
ClientModel client = realm.findClient(clientId); ClientModel client = realm.findClient(clientId);
if (client == null || !client.isEnabled()) { if (client == null || !client.isEnabled()) {
@ -888,13 +890,20 @@ public class UsersResource {
UserSessionModel userSession = session.sessions().createUserSession(realm, user, username, clientConnection.getRemoteAddr(), "form", false); UserSessionModel userSession = session.sessions().createUserSession(realm, user, username, clientConnection.getRemoteAddr(), "form", false);
//audit.session(userSession); //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); accessCode.setRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD);
try { try {
UriBuilder builder = Urls.loginPasswordResetBuilder(uriInfo.getBaseUri()); 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(); String link = builder.build(realm.getName()).toString();
long expiration = TimeUnit.SECONDS.toMinutes(realm.getAccessCodeLifespanUserAction()); long expiration = TimeUnit.SECONDS.toMinutes(realm.getAccessCodeLifespanUserAction());

View file

@ -29,6 +29,7 @@ import org.keycloak.OAuth2Constants;
import org.keycloak.events.EventBuilder; import org.keycloak.events.EventBuilder;
import org.keycloak.events.Details; import org.keycloak.events.Details;
import org.keycloak.events.EventType; import org.keycloak.events.EventType;
import org.keycloak.login.LoginFormsProvider;
import org.keycloak.models.ApplicationModel; import org.keycloak.models.ApplicationModel;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionModel; import org.keycloak.models.ClientSessionModel;
@ -42,16 +43,20 @@ import org.keycloak.models.UserSessionModel;
import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.services.managers.AccessCode; import org.keycloak.services.managers.AccessCode;
import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.managers.TokenManager; import org.keycloak.services.managers.TokenManager;
import org.keycloak.services.protocol.OpenIdConnectProtocol;
import javax.ws.rs.core.Cookie; import javax.ws.rs.core.Cookie;
import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo; import javax.ws.rs.core.UriInfo;
import java.util.HashSet;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.UUID;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -85,7 +90,7 @@ public class OAuthFlows {
this.tokenManager = tokenManager; 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(); String code = accessCode.getCode();
UriBuilder redirectUri = UriBuilder.fromUri(redirect).queryParam(OAuth2Constants.CODE, code); UriBuilder redirectUri = UriBuilder.fromUri(redirect).queryParam(OAuth2Constants.CODE, code);
log.debugv("redirectAccessCode: state: {0}", state); log.debugv("redirectAccessCode: state: {0}", state);
@ -110,8 +115,8 @@ public class OAuthFlows {
} }
// refresh the cookies! // refresh the cookies!
authManager.createLoginCookie(realm, accessCode.getUser(), userSession, uriInfo, clientConnection); authManager.createLoginCookie(realm, userSession.getUser(), userSession, uriInfo, clientConnection);
if (userSession.isRememberMe()) authManager.createRememberMeCookie(realm, accessCode.getUser().getUsername(), uriInfo, clientConnection); if (userSession.isRememberMe()) authManager.createRememberMeCookie(realm, userSession.getUser().getUsername(), uriInfo, clientConnection);
return location.build(); return location.build();
} }
@ -123,6 +128,72 @@ public class OAuthFlows {
return Response.status(302).location(redirectUri.build()).build(); 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<RequiredAction> 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<RoleModel> realmRoles = new LinkedList<RoleModel>();
MultivaluedMap<String, RoleModel> resourceRoles = new MultivaluedMapImpl<String, RoleModel>();
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) { public Response processAccessCode(String scopeParam, String state, String redirect, ClientModel client, UserModel user, UserSessionModel session, EventBuilder event) {
isTotpConfigurationRequired(user); isTotpConfigurationRequired(user);
isEmailVerificationRequired(user); isEmailVerificationRequired(user);
@ -178,6 +249,7 @@ public class OAuthFlows {
return null; return null;
} }
} }
*/
/* /*
public Response forwardToSecurityFailure(String message) { public Response forwardToSecurityFailure(String message) {

View file

@ -2,8 +2,10 @@ package org.keycloak.services.resources.flows;
import org.keycloak.ClientConnection; import org.keycloak.ClientConnection;
import org.keycloak.jose.jws.JWSBuilder; import org.keycloak.jose.jws.JWSBuilder;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.resources.SocialResource; import org.keycloak.services.resources.SocialResource;
import org.keycloak.services.util.CookieHelper; import org.keycloak.services.util.CookieHelper;
import org.keycloak.social.AuthRequest; import org.keycloak.social.AuthRequest;
@ -23,30 +25,17 @@ public class SocialRedirectFlows {
private final UriInfo uriInfo; private final UriInfo uriInfo;
private ClientConnection clientConnection; private ClientConnection clientConnection;
private final SocialProvider socialProvider; private final SocialProvider socialProvider;
private final SocialResource.State state;
SocialRedirectFlows(RealmModel realm, UriInfo uriInfo, ClientConnection clientConnection, SocialProvider provider) { SocialRedirectFlows(RealmModel realm, UriInfo uriInfo, ClientConnection clientConnection, SocialProvider provider) {
this.realm = realm; this.realm = realm;
this.uriInfo = uriInfo; this.uriInfo = uriInfo;
this.clientConnection = clientConnection; this.clientConnection = clientConnection;
this.socialProvider = provider; this.socialProvider = provider;
state = new SocialResource.State();
state.setRealm(realm.getName());
state.setProvider(provider.getId());
} }
public SocialRedirectFlows putClientAttribute(String key, String value) { public Response redirectToSocialProvider(ClientSessionCode code) throws SocialProviderException {
state.set(key, value); code.setAction(ClientSessionModel.Action.SOCIAL_CALLBACK);
return this; code.getClientSession().setNote("social_provider", socialProvider.getId());
}
public SocialRedirectFlows user(UserModel user) {
state.setUser(user.getId());
return this;
}
public Response redirectToSocialProvider() throws SocialProviderException {
String socialProviderId = socialProvider.getId(); String socialProviderId = socialProvider.getId();
String key = realm.getSocialConfig().get(socialProviderId + ".key"); String key = realm.getSocialConfig().get(socialProviderId + ".key");
@ -54,20 +43,8 @@ public class SocialRedirectFlows {
String callbackUri = Urls.socialCallback(uriInfo.getBaseUri()).toString(); String callbackUri = Urls.socialCallback(uriInfo.getBaseUri()).toString();
SocialProviderConfig config = new SocialProviderConfig(key, secret, callbackUri); 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(); return Response.status(302).location(authRequest.getAuthUri()).build();
} }

View file

@ -20,6 +20,12 @@
<version>${project.version}</version> <version>${project.version}</version>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-model-api</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<dependency> <dependency>
<groupId>org.codehaus.jackson</groupId> <groupId>org.codehaus.jackson</groupId>
<artifactId>jackson-core-asl</artifactId> <artifactId>jackson-core-asl</artifactId>

View file

@ -2,6 +2,7 @@ package org.keycloak.social;
import org.codehaus.jackson.map.ObjectMapper; import org.codehaus.jackson.map.ObjectMapper;
import org.keycloak.OAuth2Constants; import org.keycloak.OAuth2Constants;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.social.utils.SimpleHttp; import org.keycloak.social.utils.SimpleHttp;
import java.io.IOException; import java.io.IOException;
@ -43,7 +44,7 @@ public abstract class AbstractOAuth2Provider implements SocialProvider {
protected abstract SocialUser getProfile(String accessToken) throws SocialProviderException; protected abstract SocialUser getProfile(String accessToken) throws SocialProviderException;
@Override @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()) return AuthRequest.create(getAuthUrl())
.setQueryParam(CLIENT_ID, config.getKey()) .setQueryParam(CLIENT_ID, config.getKey())
.setQueryParam(RESPONSE_TYPE, CODE) .setQueryParam(RESPONSE_TYPE, CODE)
@ -54,7 +55,7 @@ public abstract class AbstractOAuth2Provider implements SocialProvider {
} }
@Override @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); String error = callback.getQueryParam(OAuth2Constants.ERROR);
if (error != null) { if (error != null) {
if (error.equals("access_denied")) { if (error.equals("access_denied")) {

View file

@ -29,11 +29,9 @@ import java.util.Map;
public class AuthCallback { public class AuthCallback {
private Map<String, String[]> queryParams; private Map<String, String[]> queryParams;
private Map<String, String> attributes;
public AuthCallback(Map<String, String[]> queryParams, Map<String, String> attributes) { public AuthCallback(Map<String, String[]> queryParams) {
this.queryParams = queryParams; this.queryParams = queryParams;
this.attributes = attributes;
} }
public String getQueryParam(String name) { public String getQueryParam(String name) {
@ -44,8 +42,5 @@ public class AuthCallback {
return null; return null;
} }
public String getAttribute(String name) {
return attributes != null ? attributes.get(name) : null;
}
} }

View file

@ -35,8 +35,6 @@ public class AuthRequest {
private URI authUri; private URI authUri;
private Map<String, String> attributes;
public static AuthRequestBuilder create(String url) { public static AuthRequestBuilder create(String url) {
AuthRequestBuilder req = new AuthRequestBuilder(); AuthRequestBuilder req = new AuthRequestBuilder();
@ -46,27 +44,20 @@ public class AuthRequest {
return req; return req;
} }
private AuthRequest(URI authUri, Map<String, String> attributes) { private AuthRequest(URI authUri) {
this.authUri = authUri; this.authUri = authUri;
this.attributes = attributes;
} }
public URI getAuthUri() { public URI getAuthUri() {
return authUri; return authUri;
} }
public Map<String, String> getAttributes() {
return attributes;
}
public static class AuthRequestBuilder { public static class AuthRequestBuilder {
private StringBuilder b; private StringBuilder b;
private char sep; private char sep;
private Map<String, String> attributes;
private String id; private String id;
private AuthRequestBuilder() { private AuthRequestBuilder() {
@ -90,17 +81,9 @@ public class AuthRequest {
} }
} }
public AuthRequestBuilder setAttribute(String name, String value) {
if (attributes == null) {
attributes = new HashMap<String, String>();
}
attributes.put(name, value);
return this;
}
public AuthRequest build() { public AuthRequest build() {
try { try {
return new AuthRequest(new URI(b.toString()), attributes); return new AuthRequest(new URI(b.toString()));
} catch (URISyntaxException e) { } catch (URISyntaxException e) {
throw new IllegalArgumentException(e); throw new IllegalArgumentException(e);
} }

View file

@ -22,6 +22,8 @@
package org.keycloak.social; package org.keycloak.social;
import org.keycloak.models.ClientSessionModel;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/ */
@ -29,10 +31,10 @@ public interface SocialProvider {
String getId(); String getId();
AuthRequest getAuthUrl(SocialProviderConfig config, String state) throws SocialProviderException; AuthRequest getAuthUrl(ClientSessionModel clientSession, SocialProviderConfig config, String state) throws SocialProviderException;
String getName(); String getName();
SocialUser processCallback(SocialProviderConfig config, AuthCallback callback) throws SocialProviderException; SocialUser processCallback(ClientSessionModel clientSession, SocialProviderConfig config, AuthCallback callback) throws SocialProviderException;
} }

View file

@ -14,6 +14,12 @@
<description/> <description/>
<dependencies> <dependencies>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-model-api</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<dependency> <dependency>
<groupId>org.keycloak</groupId> <groupId>org.keycloak</groupId>
<artifactId>keycloak-social-core</artifactId> <artifactId>keycloak-social-core</artifactId>

View file

@ -14,6 +14,12 @@
<description/> <description/>
<dependencies> <dependencies>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-model-api</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<dependency> <dependency>
<groupId>org.keycloak</groupId> <groupId>org.keycloak</groupId>
<artifactId>keycloak-social-core</artifactId> <artifactId>keycloak-social-core</artifactId>

View file

@ -14,6 +14,12 @@
<description/> <description/>
<dependencies> <dependencies>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-model-api</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<dependency> <dependency>
<groupId>org.keycloak</groupId> <groupId>org.keycloak</groupId>
<artifactId>keycloak-social-core</artifactId> <artifactId>keycloak-social-core</artifactId>

View file

@ -14,6 +14,12 @@
<description/> <description/>
<dependencies> <dependencies>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-model-api</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<dependency> <dependency>
<groupId>org.keycloak</groupId> <groupId>org.keycloak</groupId>
<artifactId>keycloak-social-core</artifactId> <artifactId>keycloak-social-core</artifactId>

View file

@ -21,6 +21,7 @@
*/ */
package org.keycloak.social.twitter; package org.keycloak.social.twitter;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.social.AuthCallback; import org.keycloak.social.AuthCallback;
import org.keycloak.social.AuthRequest; import org.keycloak.social.AuthRequest;
import org.keycloak.social.SocialAccessDeniedException; import org.keycloak.social.SocialAccessDeniedException;
@ -45,7 +46,7 @@ public class TwitterProvider implements SocialProvider {
} }
@Override @Override
public AuthRequest getAuthUrl(SocialProviderConfig config, String state) throws SocialProviderException { public AuthRequest getAuthUrl(ClientSessionModel clientSession, SocialProviderConfig config, String state) throws SocialProviderException {
try { try {
Twitter twitter = new TwitterFactory().getInstance(); Twitter twitter = new TwitterFactory().getInstance();
twitter.setOAuthConsumer(config.getKey(), config.getSecret()); twitter.setOAuthConsumer(config.getKey(), config.getSecret());
@ -53,8 +54,9 @@ public class TwitterProvider implements SocialProvider {
URI uri = new URI(config.getCallbackUrl() + "?state=" + state); URI uri = new URI(config.getCallbackUrl() + "?state=" + state);
RequestToken requestToken = twitter.getOAuthRequestToken(uri.toString()); RequestToken requestToken = twitter.getOAuthRequestToken(uri.toString());
clientSession.setNote("twitter_token", requestToken.getToken());
clientSession.setNote("twitter_tokenSecret", requestToken.getTokenSecret());
return AuthRequest.create(requestToken.getAuthenticationURL()) return AuthRequest.create(requestToken.getAuthenticationURL())
.setAttribute("token", requestToken.getToken()).setAttribute("tokenSecret", requestToken.getTokenSecret())
.build(); .build();
} catch (Exception e) { } catch (Exception e) {
throw new SocialProviderException(e); throw new SocialProviderException(e);
@ -67,7 +69,7 @@ public class TwitterProvider implements SocialProvider {
} }
@Override @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) { if (callback.getQueryParam("denied") != null) {
throw new SocialAccessDeniedException(); throw new SocialAccessDeniedException();
} }
@ -78,8 +80,10 @@ public class TwitterProvider implements SocialProvider {
String token = callback.getQueryParam("oauth_token"); String token = callback.getQueryParam("oauth_token");
String verifier = callback.getQueryParam("oauth_verifier"); 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); twitter.getOAuthAccessToken(requestToken, verifier);
twitter4j.User twitterUser = twitter.verifyCredentials(); twitter4j.User twitterUser = twitter.verifyCredentials();

View file

@ -124,6 +124,15 @@ public class AssertEvents implements TestRule, EventListenerProviderFactory {
.session(isUUID()); .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) { public ExpectedEvent expectCodeToToken(String codeId, String sessionId) {
return expect(EventType.CODE_TO_TOKEN) return expect(EventType.CODE_TO_TOKEN)
.detail(Details.CODE_ID, codeId) .detail(Details.CODE_ID, codeId)
@ -277,6 +286,11 @@ public class AssertEvents implements TestRule, EventListenerProviderFactory {
return this; return this;
} }
public ExpectedEvent clearDetails() {
if (details != null) details.clear();
return this;
}
public ExpectedEvent error(String error) { public ExpectedEvent error(String error) {
expected.setError(error); expected.setError(error);
return this; return this;

View file

@ -1,6 +1,7 @@
package org.keycloak.testsuite; package org.keycloak.testsuite;
import org.keycloak.OAuth2Constants; import org.keycloak.OAuth2Constants;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.social.AuthCallback; import org.keycloak.social.AuthCallback;
import org.keycloak.social.AuthRequest; import org.keycloak.social.AuthRequest;
import org.keycloak.social.SocialAccessDeniedException; import org.keycloak.social.SocialAccessDeniedException;
@ -19,7 +20,7 @@ public class DummySocial implements SocialProvider {
} }
@Override @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) return AuthRequest.create(AUTH_PATH)
.setQueryParam(OAuth2Constants.RESPONSE_TYPE, "token") .setQueryParam(OAuth2Constants.RESPONSE_TYPE, "token")
.setQueryParam(OAuth2Constants.REDIRECT_URI, config.getCallbackUrl()) .setQueryParam(OAuth2Constants.REDIRECT_URI, config.getCallbackUrl())
@ -32,7 +33,7 @@ public class DummySocial implements SocialProvider {
} }
@Override @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); String error = callback.getQueryParam(OAuth2Constants.ERROR);
if (error != null) { if (error != null) {
throw new SocialAccessDeniedException(); throw new SocialAccessDeniedException();

View file

@ -75,7 +75,7 @@ public class OAuthClient {
private String redirectUri = "http://localhost:8081/app/auth"; private String redirectUri = "http://localhost:8081/app/auth";
private String state; private String state = "mystate";
private PublicKey realmPublicKey; private PublicKey realmPublicKey;

View file

@ -140,6 +140,7 @@ public class AccountTest {
@Before @Before
public void 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(); 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 @Test
public void returnToAppFromQueryParam() { public void returnToAppFromQueryParam() {
driver.navigate().to(AccountUpdateProfilePage.PATH + "?referrer=test-app"); driver.navigate().to(AccountUpdateProfilePage.PATH + "?referrer=test-app");
@ -446,6 +457,7 @@ public class AccountTest {
// Create second session // Create second session
WebDriver driver2 = WebRule.createWebDriver(); WebDriver driver2 = WebRule.createWebDriver();
OAuthClient oauth2 = new OAuthClient(driver2); OAuthClient oauth2 = new OAuthClient(driver2);
oauth2.state("mystate");
oauth2.doLogin("view-sessions", "password"); oauth2.doLogin("view-sessions", "password");
Event login2Event = events.expectLogin().user(userId).detail(Details.USERNAME, "view-sessions").assertEvent(); Event login2Event = events.expectLogin().user(userId).detail(Details.USERNAME, "view-sessions").assertEvent();

View file

@ -88,6 +88,7 @@ public class RequiredActionEmailVerificationTest {
@Before @Before
public void before() { public void before() {
oauth.state("mystate"); // have to set this as keycloak validates that state is sent
keycloakRule.configure(new KeycloakSetup() { keycloakRule.configure(new KeycloakSetup() {
@Override @Override
@ -115,7 +116,8 @@ public class RequiredActionEmailVerificationTest {
String body = (String) message.getContent(); String body = (String) message.getContent();
String verificationUrl = MailUtil.getLink(body); 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 sessionId = sendEvent.getSessionId();
String mailCodeId = sendEvent.getDetails().get(Details.CODE_ID); String mailCodeId = sendEvent.getDetails().get(Details.CODE_ID);

View file

@ -22,6 +22,7 @@
package org.keycloak.testsuite.actions; package org.keycloak.testsuite.actions;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Before;
import org.junit.ClassRule; import org.junit.ClassRule;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
@ -86,6 +87,12 @@ public class RequiredActionResetPasswordTest {
@WebResource @WebResource
protected LoginPasswordUpdatePage changePasswordPage; protected LoginPasswordUpdatePage changePasswordPage;
@Before
public void before() {
oauth.state("mystate"); // have to set this as keycloak validates that state is sent
}
@Test @Test
public void tempPassword() throws Exception { public void tempPassword() throws Exception {
loginPage.open(); loginPage.open();

View file

@ -145,6 +145,10 @@ public class LoginTotpTest {
@Test @Test
public void loginWithTotpExpiredPasswordToken() throws Exception { public void loginWithTotpExpiredPasswordToken() throws Exception {
try { try {
loginPage.open();
loginPage.login("test-user@localhost", "password");
keycloakRule.configure(new KeycloakSetup() { keycloakRule.configure(new KeycloakSetup() {
@Override @Override
public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { 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(); loginTotpPage.assertCurrent();
Thread.sleep(2000); Thread.sleep(2000);
@ -165,7 +166,11 @@ public class LoginTotpTest {
loginPage.assertCurrent(); loginPage.assertCurrent();
Assert.assertEquals("Invalid username or password.", loginPage.getError()); 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 { } finally {
keycloakRule.configure(new KeycloakSetup() { keycloakRule.configure(new KeycloakSetup() {
@Override @Override

View file

@ -10,6 +10,7 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionModel;
import org.keycloak.services.protocol.OpenIdConnectProtocol;
import org.keycloak.testsuite.rule.KeycloakRule; import org.keycloak.testsuite.rule.KeycloakRule;
import org.keycloak.util.Time; import org.keycloak.util.Time;
@ -73,7 +74,7 @@ public class UserSessionProviderTest {
assertEquals(realm.findClient("test-app").getClientId(), session.getClient().getClientId()); assertEquals(realm.findClient("test-app").getClientId(), session.getClient().getClientId());
assertEquals(sessions[0].getId(), session.getUserSession().getId()); assertEquals(sessions[0].getId(), session.getUserSession().getId());
assertEquals("http://redirect", session.getRedirectUri()); assertEquals("http://redirect", session.getRedirectUri());
assertEquals("state", session.getState()); assertEquals("state", session.getNote(OpenIdConnectProtocol.STATE_PARAM));
assertEquals(2, session.getRoles().size()); assertEquals(2, session.getRoles().size());
assertTrue(session.getRoles().contains("one")); assertTrue(session.getRoles().contains("one"));
assertTrue(session.getRoles().contains("two")); assertTrue(session.getRoles().contains("two"));
@ -245,7 +246,11 @@ public class UserSessionProviderTest {
for (int i = 0; i < 25; i++) { 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); UserSessionModel userSession = session.sessions().createUserSession(realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0." + i, "form", false);
userSession.setStarted(Time.currentTime() + i); userSession.setStarted(Time.currentTime() + i);
session.sessions().createClientSession(realm, realm.findClient("test-app"), userSession, "http://redirect", "state", new HashSet<String>()); ClientSessionModel clientSession = session.sessions().createClientSession(realm, realm.findClient("test-app"));
clientSession.setUserSession(userSession);
clientSession.setRedirectUri("http://redirect");
clientSession.setRoles(new HashSet<String>());
clientSession.setNote(OpenIdConnectProtocol.STATE_PARAM, "state");
} }
resetSession(); resetSession();
@ -280,6 +285,15 @@ public class UserSessionProviderTest {
assertEquals(1, session.sessions().getActiveUserSessions(realm, realm.findClient("third-party"))); assertEquals(1, session.sessions().getActiveUserSessions(realm, realm.findClient("third-party")));
} }
private ClientSessionModel createClientSession(ClientModel client, UserSessionModel userSession, String redirect, String state, Set<String> 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() { private UserSessionModel[] createSessions() {
UserSessionModel[] sessions = new UserSessionModel[3]; UserSessionModel[] sessions = new UserSessionModel[3];
sessions[0] = session.sessions().createUserSession(realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.1", "form", true); sessions[0] = 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("one");
roles.add("two"); roles.add("two");
session.sessions().createClientSession(realm, realm.findClient("test-app"), sessions[0], "http://redirect", "state", roles); createClientSession(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<String>()); createClientSession(realm.findClient("third-party"), sessions[0], "http://redirect", "state", new HashSet<String>());
sessions[1] = session.sessions().createUserSession(realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.2", "form", true); 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<String>()); createClientSession(realm.findClient("test-app"), sessions[1], "http://redirect", "state", new HashSet<String>());
sessions[2] = session.sessions().createUserSession(realm, session.users().getUserByUsername("user2", realm), "user2", "127.0.0.3", "form", true); 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<String>()); createClientSession(realm.findClient("test-app"), sessions[2], "http://redirect", "state", new HashSet<String>());
resetSession(); resetSession();

View file

@ -135,7 +135,8 @@ public class AccessTokenTest {
AccessTokenResponse response = oauth.doAccessTokenRequest(code, "invalid"); AccessTokenResponse response = oauth.doAccessTokenRequest(code, "invalid");
Assert.assertEquals(400, response.getStatusCode()); 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 @Test

View file

@ -22,18 +22,23 @@
package org.keycloak.testsuite.oauth; package org.keycloak.testsuite.oauth;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Before;
import org.junit.ClassRule; import org.junit.ClassRule;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.keycloak.OAuth2Constants; import org.keycloak.OAuth2Constants;
import org.keycloak.events.Details; import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventType;
import org.keycloak.models.Constants; import org.keycloak.models.Constants;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.services.managers.AccessCode; import org.keycloak.services.managers.AccessCode;
import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.managers.RealmManager; import org.keycloak.services.managers.RealmManager;
import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.OAuthClient; import org.keycloak.testsuite.OAuthClient;
import org.keycloak.testsuite.OAuthClient.AuthorizationCodeResponse; import org.keycloak.testsuite.OAuthClient.AuthorizationCodeResponse;
import org.keycloak.testsuite.pages.ErrorPage;
import org.keycloak.testsuite.pages.LoginPage; import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.rule.KeycloakRule; import org.keycloak.testsuite.rule.KeycloakRule;
import org.keycloak.testsuite.rule.WebResource; import org.keycloak.testsuite.rule.WebResource;
@ -65,6 +70,9 @@ public class AuthorizationCodeTest {
@WebResource @WebResource
protected LoginPage loginPage; protected LoginPage loginPage;
@WebResource
protected ErrorPage errorPage;
@Rule @Rule
public AssertEvents events = new AssertEvents(keycloakRule); public AssertEvents events = new AssertEvents(keycloakRule);
@ -103,7 +111,7 @@ public class AuthorizationCodeTest {
String code = driver.findElement(By.id(OAuth2Constants.CODE)).getText(); String code = driver.findElement(By.id(OAuth2Constants.CODE)).getText();
keycloakRule.verifyCode(code); 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); assertCode(codeId, code);
keycloakRule.update(new KeycloakRule.KeycloakSetup() { keycloakRule.update(new KeycloakRule.KeycloakSetup() {
@ -133,7 +141,10 @@ public class AuthorizationCodeTest {
String error = driver.findElement(By.id(OAuth2Constants.ERROR)).getText(); String error = driver.findElement(By.id(OAuth2Constants.ERROR)).getText();
assertEquals("access_denied", error); assertEquals("access_denied", error);
events.expectLogin().error("rejected_by_user").user((String) null).session((String) null).removeDetail(Details.USERNAME).removeDetail(Details.CODE_ID).detail(Details.REDIRECT_URI, Constants.INSTALLED_APP_URN).assertEvent().getDetails().get(Details.CODE_ID); 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() { keycloakRule.update(new KeycloakRule.KeycloakSetup() {
@Override @Override
@ -167,22 +178,22 @@ public class AuthorizationCodeTest {
@Test @Test
public void authorizationRequestNoState() throws IOException { 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()); events.expect(EventType.LOGIN_ERROR)
Assert.assertNotNull(response.getCode()); .error(Errors.STATE_PARAM_NOT_FOUND)
Assert.assertNull(response.getState()); .detail(Details.RESPONSE_TYPE, "code")
Assert.assertNull(response.getError()); .detail(Details.REDIRECT_URI, oauth.getRedirectUri())
.user((String)null)
keycloakRule.verifyCode(response.getCode()); .assertEvent();
//assertCode(codeId, response.getCode());
String codeId = events.expectLogin().assertEvent().getDetails().get(Details.CODE_ID);
assertCode(codeId, response.getCode());
} }
private void assertCode(String expectedCodeId, String actualCode) { private void assertCode(String expectedCodeId, String actualCode) {
AccessCode code = keycloakRule.verifyCode(actualCode); ClientSessionCode code = keycloakRule.verifyCode(actualCode);
assertEquals(expectedCodeId, code.getCodeId()); assertEquals(expectedCodeId, code.getClientSession().getId());
} }
} }

View file

@ -55,6 +55,8 @@ import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLong;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -163,21 +165,24 @@ public class AccessTokenPerfTest {
return b.build(realm).toString(); return b.build(realm).toString();
} }
static Pattern actionParser = Pattern.compile("action=\"([^\"]+)\"");
@Override
public void run() { public void run() {
//this.client = new ResteasyClientBuilder().build(); //this.client = new ResteasyClientBuilder().build();
String state = "42"; String state = "42";
Response response = client.target(getLoginFormUrl(state)).request().get(); String loginFormUrl = getLoginFormUrl(state);
URI uri = null; String html = client.target(loginFormUrl).request().get(String.class);
if (200 == response.getStatus()) { Matcher matcher = actionParser.matcher(html);
response.close(); 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 form = new Form();
form.param("username", "test-user@localhost"); form.param("username", "test-user@localhost");
form.param("password", "password"); form.param("password", "password");
response = client.target(getProcessLoginUrl(state)).request().post(Entity.form(form)); Response response = client.target(actionUrl).request().post(Entity.form(form));
URI uri = null;
}
Assert.assertEquals(302, response.getStatus()); Assert.assertEquals(302, response.getStatus());
uri = response.getLocation(); uri = response.getLocation();
for (String header : response.getHeaders().keySet()) { for (String header : response.getHeaders().keySet()) {
@ -191,7 +196,7 @@ public class AccessTokenPerfTest {
String code = getCode(uri); String code = getCode(uri);
Assert.assertNotNull(code); Assert.assertNotNull(code);
Form form = new Form(); form = new Form();
form.param(OAuth2Constants.GRANT_TYPE, grantType) form.param(OAuth2Constants.GRANT_TYPE, grantType)
.param(OAuth2Constants.CODE, code) .param(OAuth2Constants.CODE, code)
.param(OAuth2Constants.REDIRECT_URI, redirectUri); .param(OAuth2Constants.REDIRECT_URI, redirectUri);

View file

@ -27,6 +27,7 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionModel;
import org.keycloak.services.managers.AccessCode; import org.keycloak.services.managers.AccessCode;
import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.managers.RealmManager; import org.keycloak.services.managers.RealmManager;
import org.keycloak.testsuite.ApplicationServlet; import org.keycloak.testsuite.ApplicationServlet;
@ -109,12 +110,12 @@ public class KeycloakRule extends AbstractKeycloakRule {
stopSession(session, true); stopSession(session, true);
} }
public AccessCode verifyCode(String code) { public ClientSessionCode verifyCode(String code) {
KeycloakSession session = startSession(); KeycloakSession session = startSession();
try { try {
RealmModel realm = session.realms().getRealm("test"); RealmModel realm = session.realms().getRealm("test");
try { try {
AccessCode accessCode = AccessCode.parse(code, session, realm); ClientSessionCode accessCode = ClientSessionCode.parse(code, session, realm);
if (accessCode == null) { if (accessCode == null) {
Assert.fail("Invalid code"); Assert.fail("Invalid code");
} }

View file

@ -114,14 +114,17 @@ public class SocialLoginTest {
String userId = events.expect(EventType.REGISTER) String userId = events.expect(EventType.REGISTER)
.user(AssertEvents.isUUID()) .user(AssertEvents.isUUID())
.detail(Details.EMAIL, "bob@builder.com") .detail(Details.EMAIL, "bob@builder.com")
.detail(Details.RESPONSE_TYPE, "code")
.detail(Details.REGISTER_METHOD, "social@dummy") .detail(Details.REGISTER_METHOD, "social@dummy")
.detail(Details.REDIRECT_URI, AssertEvents.DEFAULT_REDIRECT_URI) .detail(Details.REDIRECT_URI, AssertEvents.DEFAULT_REDIRECT_URI)
.detail(Details.USERNAME, "1@dummy") .detail(Details.USERNAME, "1@dummy")
.session((String) null) .session((String) null)
.assertEvent().getUserId(); .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 sessionId = loginEvent.getSessionId();
String codeId = loginEvent.getDetails().get(Details.CODE_ID); 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("username")).sendKeys("dummy-user1");
driver.findElement(By.id("login")).click(); 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 @Test
@ -199,8 +202,9 @@ public class SocialLoginTest {
Assert.assertTrue(loginPage.isCurrent()); Assert.assertTrue(loginPage.isCurrent());
Assert.assertEquals("Access denied", loginPage.getWarning()); 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"); loginPage.login("test-user@localhost", "password");
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
@ -238,7 +242,6 @@ public class SocialLoginTest {
String userId = events.expect(EventType.REGISTER) String userId = events.expect(EventType.REGISTER)
.user(AssertEvents.isUUID()) .user(AssertEvents.isUUID())
.detail(Details.EMAIL, "bob@builder.com") .detail(Details.EMAIL, "bob@builder.com")
.detail(Details.RESPONSE_TYPE, "code")
.detail(Details.REGISTER_METHOD, "social@dummy") .detail(Details.REGISTER_METHOD, "social@dummy")
.detail(Details.REDIRECT_URI, AssertEvents.DEFAULT_REDIRECT_URI) .detail(Details.REDIRECT_URI, AssertEvents.DEFAULT_REDIRECT_URI)
.detail(Details.USERNAME, "2@dummy") .detail(Details.USERNAME, "2@dummy")