KEYCLOAK-389 Added AuditListener SPI

KEYCLOAK-390 Added JBoss Logging AuditListener
KEYCLOAK-391 Audit Token events
This commit is contained in:
Stian Thorgersen 2014-03-25 10:36:15 +00:00
parent 911cf2ae45
commit 225307e855
58 changed files with 1859 additions and 165 deletions

39
audit/api/pom.xml Executable file
View file

@ -0,0 +1,39 @@
<?xml version="1.0"?>
<project>
<parent>
<artifactId>keycloak-audit-parent</artifactId>
<groupId>org.keycloak</groupId>
<version>1.0-beta-1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>keycloak-audit-api</artifactId>
<name>Keycloak Audit API</name>
<description/>
<dependencies>
<dependency>
<groupId>org.jboss.logging</groupId>
<artifactId>jboss-logging</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-core</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-model-api</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View file

@ -0,0 +1,141 @@
package org.keycloak.audit;
import org.jboss.logging.Logger;
import org.keycloak.models.ClientModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class Audit {
private static final Logger log = Logger.getLogger(Audit.class);
private List<AuditListener> listeners;
private Event event;
public static Audit create(RealmModel realm, String ipAddress) {
List<AuditListener> listeners = null;
if (realm.getAuditListeners() != null) {
listeners = new LinkedList<AuditListener>();
for (String id : realm.getAuditListeners()) {
listeners.add(AuditLoader.load(id));
}
}
return new Audit(listeners, new Event()).realm(realm).ipAddress(ipAddress);
}
private Audit(List<AuditListener> listeners, Event event) {
this.listeners = listeners;
this.event = event;
}
public Audit realm(RealmModel realm) {
event.setRealmId(realm.getId());
return this;
}
public Audit realm(String realmId) {
event.setRealmId(realmId);
return this;
}
public Audit client(ClientModel client) {
event.setClientId(client.getClientId());
return this;
}
public Audit client(String clientId) {
event.setClientId(clientId);
return this;
}
public Audit user(UserModel user) {
event.setUserId(user.getId());
return this;
}
public Audit user(String userId) {
event.setUserId(userId);
return this;
}
public Audit ipAddress(String ipAddress) {
event.setIpAddress(ipAddress);
return this;
}
public Audit event(String e) {
event.setEvent(e);
return this;
}
public Audit detail(String key, String value) {
if (value == null || value.equals("")) {
return this;
}
if (event.getDetails() == null) {
event.setDetails(new HashMap<String, String>());
}
event.getDetails().put(key, value);
return this;
}
public Audit removeDetail(String key) {
if (event.getDetails() != null) {
event.getDetails().remove(key);
}
return this;
}
public Event getEvent() {
return event;
}
public void success() {
send();
}
public void error(String error) {
event.setError(error);
send();
}
public Audit clone() {
return new Audit(listeners, event.clone());
}
public Audit reset() {
Event old = event;
event = new Event();
event.setRealmId(old.getRealmId());
event.setIpAddress(old.getIpAddress());
event.setClientId(old.getClientId());
event.setUserId(old.getUserId());
return this;
}
private void send() {
event.setTime(System.currentTimeMillis());
if (listeners != null) {
for (AuditListener l : listeners) {
try {
l.onEvent(event);
} catch (Throwable t) {
log.error("Failed to send event to " + l, t);
}
}
}
}
}

View file

@ -0,0 +1,12 @@
package org.keycloak.audit;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public interface AuditListener {
public String getId();
public void onEvent(Event event);
}

View file

@ -0,0 +1,31 @@
package org.keycloak.audit;
import org.keycloak.util.ProviderLoader;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class AuditLoader {
private AuditLoader() {
}
public static AuditListener load(String id) {
if (id == null) {
throw new NullPointerException();
}
for (AuditListener l : load()) {
if (id.equals(l.getId())) {
return l;
}
}
return null;
}
public static Iterable<AuditListener> load() {
return ProviderLoader.load(AuditListener.class);
}
}

View file

@ -0,0 +1,10 @@
package org.keycloak.audit;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public interface AuditProvider extends AuditListener {
public EventQuery createQuery();
}

View file

@ -0,0 +1,22 @@
package org.keycloak.audit;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public interface Details {
String EMAIL = "email";
String PREVIOUS_EMAIL = "previous_email";
String UPDATED_EMAIL = "updated_email";
String CODE_ID = "code_id";
String REDIRECT_URI = "redirect_uri";
String RESPONSE_TYPE = "response_type";
String AUTH_METHOD = "auth_method";
String REGISTER_METHOD = "register_method";
String USERNAME = "username";
String REMEMBER_ME = "remember_me";
String TOKEN_ID = "token_id";
String REFRESH_TOKEN_ID = "refresh_token_id";
String UPDATED_REFRESH_TOKEN_ID = "updated_refresh_token_id";
}

View file

@ -0,0 +1,36 @@
package org.keycloak.audit;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public interface Errors {
String REALM_DISABLED = "realm_disabled";
String CLIENT_NOT_FOUND = "client_not_found";
String CLIENT_DISABLED = "client_disabled";
String INVALID_CLIENT_CREDENTIALS = "invalid_client_credentials";
String USER_NOT_FOUND = "user_not_found";
String USER_DISABLED = "user_disabled";
String INVALID_USER_CREDENTIALS = "invalid_user_credentials";
String USERNAME_MISSING = "username_missing";
String USERNAME_IN_USE = "username_in_use";
String INVALID_REDIRECT_URI = "invalid_redirect_uri";
String INVALID_CODE = "invalid_code";
String INVALID_TOKEN = "invalid_token";
String INVALID_REGISTRATION = "invalid_registration";
String INVALID_FORM = "invalid_form";
String REGISTRATION_DISABLED = "registration_disabled";
String REJECTED_BY_USER = "rejected_by_user";
String NOT_ALLOWED = "not_allowed";
String SOCIAL_PROVIDER_NOT_FOUND = "social_provider_not_found";
String SOCIAL_ID_IN_USE = "social_id_in_use";
}

View file

@ -0,0 +1,108 @@
package org.keycloak.audit;
import java.util.HashMap;
import java.util.Map;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class Event {
private long time;
private String event;
private String realmId;
private String clientId;
private String userId;
private String ipAddress;
private String error;
private Map<String, String> details;
public long getTime() {
return time;
}
public void setTime(long time) {
this.time = time;
}
public String getEvent() {
return event;
}
public void setEvent(String event) {
this.event = event;
}
public String getRealmId() {
return realmId;
}
public void setRealmId(String realmId) {
this.realmId = realmId;
}
public String getClientId() {
return clientId;
}
public void setClientId(String clientId) {
this.clientId = clientId;
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getIpAddress() {
return ipAddress;
}
public void setIpAddress(String ipAddress) {
this.ipAddress = ipAddress;
}
public boolean isError() {
return error != null;
}
public String getError() {
return error;
}
public void setError(String error) {
this.error = error;
}
public Map<String, String> getDetails() {
return details;
}
public void setDetails(Map<String, String> details) {
this.details = details;
}
public Event clone() {
Event clone = new Event();
clone.time = time;
clone.event = event;
clone.realmId = realmId;
clone.clientId = clientId;
clone.userId = userId;
clone.ipAddress = ipAddress;
clone.error = error;
clone.details = details != null ? new HashMap<String, String>(details) : null;
return clone;
}
}

View file

@ -0,0 +1,24 @@
package org.keycloak.audit;
import java.util.List;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public interface EventQuery {
public EventQuery event(String event);
public EventQuery realm(String realmId);
public EventQuery client(String clientId);
public EventQuery user(String userId);
public EventQuery firstResult(int result);
public EventQuery maxResults(int results);
public List<Event> getResultList();
}

View file

@ -0,0 +1,29 @@
package org.keycloak.audit;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public interface Events {
String LOGIN = "login";
String REGISTER = "register";
String LOGOUT = "logout";
String CODE_TO_TOKEN = "code_to_token";
String REFRESH_TOKEN = "refresh_token";
String SOCIAL_LINK = "social_link";
String REMOVE_SOCIAL_LINK = "remove_social_link";
String UPDATE_EMAIL = "update_email";
String UPDATE_PROFILE = "update_profile";
String UPDATE_PASSWORD = "update_password";
String UPDATE_TOTP = "update_totp";
String VERIFY_EMAIL = "verify_email";
String REMOVE_TOTP = "remove_totp";
String SEND_VERIFY_EMAIL = "send_verify_email";
String SEND_RESET_PASSWORD = "send_reset_password";
}

33
audit/jboss-logging/pom.xml Executable file
View file

@ -0,0 +1,33 @@
<?xml version="1.0"?>
<project>
<parent>
<artifactId>keycloak-audit-parent</artifactId>
<groupId>org.keycloak</groupId>
<version>1.0-beta-1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>keycloak-audit-jboss-logging</artifactId>
<name>Keycloak Audit JBoss Logging Provider</name>
<description/>
<dependencies>
<dependency>
<groupId>org.jboss.logging</groupId>
<artifactId>jboss-logging</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-audit-api</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View file

@ -0,0 +1,63 @@
package org.keycloak.audit.log;
import org.jboss.logging.Logger;
import org.keycloak.audit.AuditListener;
import org.keycloak.audit.Event;
import java.util.Map;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class JBossLoggingAuditListener implements AuditListener {
private static final Logger logger = Logger.getLogger("org.keycloak.audit");
@Override
public String getId() {
return "jboss-logging";
}
@Override
public void onEvent(Event event) {
Logger.Level level = event.isError() ? Logger.Level.WARN : Logger.Level.INFO;
if (logger.isEnabled(level)) {
StringBuilder sb = new StringBuilder();
sb.append("event=");
sb.append(event.getEvent());
sb.append(", realmId=");
sb.append(event.getRealmId());
sb.append(", clientId=");
sb.append(event.getClientId());
sb.append(", userId=");
sb.append(event.getUserId());
sb.append(", ipAddress=");
sb.append(event.getIpAddress());
if (event.isError()) {
sb.append(", error=");
sb.append(event.getError());
}
if (event.getDetails() != null) {
for (Map.Entry<String, String> e : event.getDetails().entrySet()) {
sb.append(", ");
sb.append(e.getKey());
if (e.getValue() == null || e.getValue().indexOf(' ') == -1) {
sb.append("=");
sb.append(e.getValue());
} else {
sb.append("='");
sb.append(e.getValue());
sb.append("'");
}
}
}
logger.log(level, sb.toString());
}
}
}

View file

@ -0,0 +1 @@
org.keycloak.audit.log.JBossLoggingAuditListener

29
audit/jpa/pom.xml Executable file
View file

@ -0,0 +1,29 @@
<?xml version="1.0"?>
<project>
<parent>
<artifactId>keycloak-audit-parent</artifactId>
<groupId>org.keycloak</groupId>
<version>1.0-beta-1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>keycloak-audit-jpa</artifactId>
<name>Keycloak Audit JPA Provider</name>
<description/>
<dependencies>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-core</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

23
audit/pom.xml Executable file
View file

@ -0,0 +1,23 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
<version>1.0-beta-1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<name>Audit Parent</name>
<description/>
<modelVersion>4.0.0</modelVersion>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-audit-parent</artifactId>
<packaging>pom</packaging>
<modules>
<module>api</module>
<module>jpa</module>
<module>jboss-logging</module>
</modules>
</project>

View file

@ -202,4 +202,8 @@ public interface RealmModel extends RoleContainerModel, RoleMapperModel, ScopeMa
void setNotBefore(int notBefore);
boolean removeRoleById(String id);
Set<String> getAuditListeners();
void setAuditListeners(Set<String> listeners);
}

View file

@ -1154,4 +1154,15 @@ public class RealmAdapter implements RealmModel {
realm.setAccountTheme(name);
em.flush();
}
@Override
public Set<String> getAuditListeners() {
return realm.getAuditListeners();
}
@Override
public void setAuditListeners(Set<String> listeners) {
realm.setAuditListeners(listeners);
em.flush();
}
}

View file

@ -16,7 +16,9 @@ import javax.persistence.OneToMany;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -95,6 +97,9 @@ public class RealmEntity {
@JoinTable(name="RealmDefaultRoles")
Collection<RoleEntity> defaultRoles = new ArrayList<RoleEntity>();
@ElementCollection
protected Set<String> auditListeners= new HashSet<String>();
public String getId() {
return id;
}
@ -333,5 +338,13 @@ public class RealmEntity {
public void setNotBefore(int notBefore) {
this.notBefore = notBefore;
}
public Set<String> getAuditListeners() {
return auditListeners;
}
public void setAuditListeners(Set<String> auditListeners) {
this.auditListeners = auditListeners;
}
}

View file

@ -38,6 +38,7 @@ import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
@ -1113,6 +1114,20 @@ public class RealmAdapter extends AbstractMongoAdapter<RealmEntity> implements R
updateRealm();
}
@Override
public Set<String> getAuditListeners() {
return realm.getAuditListeners() != null ? new HashSet<String>(realm.getAuditListeners()) : null;
}
@Override
public void setAuditListeners(Set<String> listeners) {
if (listeners != null) {
realm.setAuditListeners(new LinkedList<String>(listeners));
} else {
realm.setAuditListeners(null);
}
}
@Override
public RealmEntity getMongoEntity() {
return realm;

View file

@ -10,8 +10,11 @@ import org.keycloak.models.mongo.api.context.MongoStoreInvocationContext;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@ -53,6 +56,8 @@ public class RealmEntity extends AbstractMongoIdentifiableEntity implements Mong
private Map<String, String> socialConfig = new HashMap<String, String>();
private Map<String, String> ldapServerConfig;
private List<String> auditListeners = new LinkedList<String>();
@MongoField
public String getName() {
return name;
@ -287,6 +292,15 @@ public class RealmEntity extends AbstractMongoIdentifiableEntity implements Mong
this.ldapServerConfig = ldapServerConfig;
}
@MongoField
public List<String> getAuditListeners() {
return auditListeners;
}
public void setAuditListeners(List<String> auditListeners) {
this.auditListeners = auditListeners;
}
@Override
public void afterRemove(MongoStoreInvocationContext context) {
DBObject query = new QueryBuilder()

View file

@ -6,7 +6,7 @@
<version>1.0-beta-1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<name>Examples</name>
<name>Model Parent</name>
<description/>
<modelVersion>4.0.0</modelVersion>

View file

@ -80,6 +80,7 @@
</contributors>
<modules>
<module>audit</module>
<module>core</module>
<module>core-jaxrs</module>
<module>model</module>

View file

@ -36,6 +36,12 @@
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-audit-api</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-account-api</artifactId>

View file

@ -25,6 +25,8 @@ public class AccessCodeEntry {
protected String state;
protected String redirectUri;
protected boolean rememberMe;
protected String authMethod;
protected String username;
protected int expiration;
protected RealmModel realm;
@ -130,4 +132,20 @@ public class AccessCodeEntry {
public void setRememberMe(boolean rememberMe) {
this.rememberMe = rememberMe;
}
public String getAuthMethod() {
return authMethod;
}
public void setAuthMethod(String authMethod) {
this.authMethod = authMethod;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
}

View file

@ -13,6 +13,8 @@ import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
import org.keycloak.representations.idm.CredentialRepresentation;
import java.util.Collections;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
@ -61,6 +63,8 @@ public class ApplianceBootstrap {
adminConsole.setBaseUrl("/auth/admin/index.html");
adminConsole.setEnabled(true);
realm.setAuditListeners(Collections.singleton("jboss-logging"));
RoleModel adminRole = realm.getRole(AdminRoles.ADMIN);
adminConsole.addScope(adminRole);

View file

@ -81,6 +81,8 @@ public class RealmManager {
setupAdminManagement(realm);
setupAccountManagement(realm);
realm.setAuditListeners(Collections.singleton("jboss-logging"));
return realm;
}

View file

@ -2,6 +2,8 @@ package org.keycloak.services.managers;
import org.jboss.resteasy.logging.Logger;
import org.keycloak.OAuthErrorException;
import org.keycloak.audit.Audit;
import org.keycloak.audit.Details;
import org.keycloak.jose.jws.JWSBuilder;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.crypto.RSAProvider;
@ -98,7 +100,7 @@ public class TokenManager {
return code;
}
public AccessToken refreshAccessToken(RealmModel realm, ClientModel client, String encodedRefreshToken) throws OAuthErrorException {
public AccessToken refreshAccessToken(RealmModel realm, ClientModel client, String encodedRefreshToken, Audit audit) throws OAuthErrorException {
JWSInput jws = new JWSInput(encodedRefreshToken);
RefreshToken refreshToken = null;
try {
@ -117,6 +119,8 @@ public class TokenManager {
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Stale refresh token");
}
audit.user(refreshToken.getSubject()).detail(Details.REFRESH_TOKEN_ID, refreshToken.getId());
UserModel user = realm.getUserById(refreshToken.getSubject());
if (user == null) {
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Invalid refresh token", "Unknown user");
@ -320,8 +324,8 @@ public class TokenManager {
return encodedToken;
}
public AccessTokenResponseBuilder responseBuilder(RealmModel realm, ClientModel client) {
return new AccessTokenResponseBuilder(realm, client);
public AccessTokenResponseBuilder responseBuilder(RealmModel realm, ClientModel client, Audit audit) {
return new AccessTokenResponseBuilder(realm, client, audit);
}
public class AccessTokenResponseBuilder {
@ -330,10 +334,12 @@ public class TokenManager {
AccessToken accessToken;
RefreshToken refreshToken;
IDToken idToken;
Audit audit;
public AccessTokenResponseBuilder(RealmModel realm, ClientModel client) {
public AccessTokenResponseBuilder(RealmModel realm, ClientModel client, Audit audit) {
this.realm = realm;
this.client = client;
this.audit = audit;
}
public AccessTokenResponseBuilder accessToken(AccessToken accessToken) {
@ -402,7 +408,21 @@ public class TokenManager {
return this;
}
public AccessTokenResponse build() {
if (accessToken != null) {
audit.detail(Details.TOKEN_ID, accessToken.getId());
}
if (refreshToken != null) {
if (audit.getEvent().getDetails().containsKey(Details.REFRESH_TOKEN_ID)) {
audit.detail(Details.UPDATED_REFRESH_TOKEN_ID, refreshToken.getId());
} else {
audit.detail(Details.REFRESH_TOKEN_ID, refreshToken.getId());
}
}
AccessTokenResponse res = new AccessTokenResponse();
if (idToken != null) {
String encodedToken = new JWSBuilder().jsonContent(idToken).rsa256(realm.getPrivateKey());

View file

@ -27,10 +27,14 @@ import org.keycloak.OAuth2Constants;
import org.keycloak.account.Account;
import org.keycloak.account.AccountLoader;
import org.keycloak.account.AccountPages;
import org.keycloak.audit.Audit;
import org.keycloak.audit.Details;
import org.keycloak.audit.Events;
import org.keycloak.jaxrs.JaxrsOAuthClient;
import org.keycloak.models.*;
import org.keycloak.models.utils.TimeBasedOTP;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.services.managers.AccessCodeEntry;
import org.keycloak.services.managers.AppAuthManager;
import org.keycloak.services.managers.Auth;
import org.keycloak.services.managers.ModelToRepresentation;
@ -75,11 +79,13 @@ public class AccountService {
private final AppAuthManager authManager;
private final ApplicationModel application;
private Audit audit;
private final SocialRequestManager socialRequestManager;
public AccountService(RealmModel realm, ApplicationModel application, TokenManager tokenManager, SocialRequestManager socialRequestManager) {
public AccountService(RealmModel realm, ApplicationModel application, TokenManager tokenManager, SocialRequestManager socialRequestManager, Audit audit) {
this.realm = realm;
this.application = application;
this.audit = audit;
this.authManager = new AppAuthManager(KEYCLOAK_ACCOUNT_IDENTITY_COOKIE, tokenManager);
this.socialRequestManager = socialRequestManager;
}
@ -170,8 +176,20 @@ public class AccountService {
user.setFirstName(formData.getFirst("firstName"));
user.setLastName(formData.getFirst("lastName"));
String email = formData.getFirst("email");
String oldEmail = user.getEmail();
boolean emailChanged = oldEmail != null ? !oldEmail.equals(email) : email != null;
user.setEmail(formData.getFirst("email"));
audit.event(Events.UPDATE_PROFILE).client(auth.getClient()).user(auth.getUser()).success();
if (emailChanged) {
user.setEmailVerified(false);
audit.clone().event(Events.UPDATE_EMAIL).detail(Details.PREVIOUS_EMAIL, oldEmail).detail(Details.UPDATED_EMAIL, email).success();
}
return account.setSuccess("accountUpdated").createResponse(AccountPages.ACCOUNT);
}
@ -184,6 +202,8 @@ public class AccountService {
UserModel user = auth.getUser();
user.setTotp(false);
audit.event(Events.REMOVE_TOTP).client(auth.getClient()).user(auth.getUser()).success();
Account account = AccountLoader.load().createAccount(uriInfo).setRealm(realm).setUser(auth.getUser());
return account.setSuccess("successTotpRemoved").createResponse(AccountPages.TOTP);
}
@ -215,6 +235,8 @@ public class AccountService {
user.setTotp(true);
audit.event(Events.UPDATE_TOTP).client(auth.getClient()).user(auth.getUser()).success();
return account.setSuccess("successTotp").createResponse(AccountPages.TOTP);
}
@ -253,6 +275,8 @@ public class AccountService {
return account.setError(ape.getMessage()).createResponse(AccountPages.PASSWORD);
}
audit.event(Events.UPDATE_PASSWORD).client(auth.getClient()).user(auth.getUser()).success();
return account.setSuccess("accountPasswordUpdated").createResponse(AccountPages.PASSWORD);
}
@ -298,8 +322,16 @@ public class AccountService {
return account.setError(Messages.SOCIAL_REDIRECT_ERROR).createResponse(AccountPages.SOCIAL);
}
case REMOVE:
if (realm.removeSocialLink(user, providerId)) {
SocialLinkModel link = realm.getSocialLink(user, providerId);
if (link != null) {
realm.removeSocialLink(user, providerId);
logger.debug("Social provider " + providerId + " removed successfully from user " + user.getLoginName());
audit.event(Events.REMOVE_SOCIAL_LINK).client(auth.getClient()).user(auth.getUser())
.detail(Details.USERNAME, link.getSocialUserId() + "@" + link.getSocialProvider())
.success();
return account.setSuccess(Messages.SOCIAL_PROVIDER_REMOVED).createResponse(AccountPages.SOCIAL);
} else {
return account.setError(Messages.SOCIAL_LINK_NOT_ACTIVE).createResponse(AccountPages.SOCIAL);

View file

@ -1,6 +1,7 @@
package org.keycloak.services.resources;
import org.jboss.resteasy.logging.Logger;
import org.keycloak.audit.Audit;
import org.keycloak.models.ApplicationModel;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
@ -9,6 +10,7 @@ import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.managers.SocialRequestManager;
import org.keycloak.services.managers.TokenManager;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.NotFoundException;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
@ -38,6 +40,9 @@ public class RealmsResource {
@Context
protected KeycloakSession session;
@Context
protected HttpServletRequest servletRequest;
protected TokenManager tokenManager;
protected SocialRequestManager socialRequestManager;
@ -54,7 +59,8 @@ public class RealmsResource {
public TokenService getTokenService(final @PathParam("realm") String name) {
RealmManager realmManager = new RealmManager(session);
RealmModel realm = locateRealm(name, realmManager);
TokenService tokenService = new TokenService(realm, tokenManager);
Audit audit = Audit.create(realm, servletRequest.getRemoteAddr());
TokenService tokenService = new TokenService(realm, tokenManager, audit);
resourceContext.initResource(tokenService);
return tokenService;
}
@ -78,7 +84,9 @@ public class RealmsResource {
throw new NotFoundException();
}
AccountService accountService = new AccountService(realm, application, tokenManager, socialRequestManager);
Audit audit = Audit.create(realm, servletRequest.getRemoteAddr());
AccountService accountService = new AccountService(realm, application, tokenManager, socialRequestManager, audit);
resourceContext.initResource(accountService);
return accountService;
}
@ -92,5 +100,4 @@ public class RealmsResource {
return realmResource;
}
}

View file

@ -24,6 +24,10 @@ package org.keycloak.services.resources;
import org.jboss.resteasy.logging.Logger;
import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.OAuth2Constants;
import org.keycloak.audit.Audit;
import org.keycloak.audit.Details;
import org.keycloak.audit.Errors;
import org.keycloak.audit.Events;
import org.keycloak.login.LoginForms;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.crypto.RSAProvider;
@ -84,9 +88,12 @@ public class RequiredActionsService {
private TokenManager tokenManager;
public RequiredActionsService(RealmModel realm, TokenManager tokenManager) {
private Audit audit;
public RequiredActionsService(RealmModel realm, TokenManager tokenManager, Audit audit) {
this.realm = realm;
this.tokenManager = tokenManager;
this.audit = audit;
}
@Path("profile")
@ -100,6 +107,8 @@ public class RequiredActionsService {
UserModel user = getUser(accessCode);
initAudit(accessCode);
String error = Validation.validateUpdateProfileForm(formData);
if (error != null) {
return Flows.forms(realm, request, uriInfo).setUser(user).setError(error).createResponse(RequiredAction.UPDATE_PROFILE);
@ -107,11 +116,22 @@ public class RequiredActionsService {
user.setFirstName(formData.getFirst("firstName"));
user.setLastName(formData.getFirst("lastName"));
user.setEmail(formData.getFirst("email"));
String email = formData.getFirst("email");
String oldEmail = user.getEmail();
boolean emailChanged = oldEmail != null ? !oldEmail.equals(email) : email != null;
user.setEmail(email);
user.removeRequiredAction(RequiredAction.UPDATE_PROFILE);
accessCode.getRequiredActions().remove(RequiredAction.UPDATE_PROFILE);
audit.clone().event(Events.UPDATE_PROFILE).success();
if (emailChanged) {
user.setEmailVerified(false);
audit.clone().event(Events.UPDATE_EMAIL).detail(Details.PREVIOUS_EMAIL, oldEmail).detail(Details.UPDATED_EMAIL, email).success();
}
return redirectOauth(user, accessCode);
}
@ -126,6 +146,8 @@ public class RequiredActionsService {
UserModel user = getUser(accessCode);
initAudit(accessCode);
String totp = formData.getFirst("totp");
String totpSecret = formData.getFirst("totpSecret");
@ -146,6 +168,8 @@ public class RequiredActionsService {
user.removeRequiredAction(RequiredAction.CONFIGURE_TOTP);
accessCode.getRequiredActions().remove(RequiredAction.CONFIGURE_TOTP);
audit.clone().event(Events.UPDATE_TOTP).success();
return redirectOauth(user, accessCode);
}
@ -163,6 +187,8 @@ public class RequiredActionsService {
UserModel user = getUser(accessCode);
initAudit(accessCode);
String passwordNew = formData.getFirst("password-new");
String passwordConfirm = formData.getFirst("password-confirm");
@ -186,6 +212,8 @@ public class RequiredActionsService {
accessCode.getRequiredActions().remove(RequiredAction.UPDATE_PASSWORD);
}
audit.clone().event(Events.UPDATE_PASSWORD).success();
return redirectOauth(user, accessCode);
}
@ -201,11 +229,16 @@ public class RequiredActionsService {
}
UserModel user = getUser(accessCode);
initAudit(accessCode);
user.setEmailVerified(true);
user.removeRequiredAction(RequiredAction.VERIFY_EMAIL);
accessCode.getRequiredActions().remove(RequiredAction.VERIFY_EMAIL);
audit.clone().event(Events.VERIFY_EMAIL).detail(Details.EMAIL, accessCode.getUser().getEmail()).success();
return redirectOauth(user, accessCode);
} else {
AccessCodeEntry accessCode = getAccessCodeEntry(RequiredAction.VERIFY_EMAIL);
@ -213,6 +246,9 @@ public class RequiredActionsService {
return unauthorized();
}
initAudit(accessCode);
//audit.clone().event(Events.SEND_VERIFY_EMAIL).detail(Details.EMAIL, accessCode.getUser().getEmail()).success();
return Flows.forms(realm, request, uriInfo).setAccessCode(accessCode.getId(), accessCode.getCode()).setUser(accessCode.getUser())
.createResponse(RequiredAction.VERIFY_EMAIL);
}
@ -223,10 +259,12 @@ public class RequiredActionsService {
public Response passwordReset() {
if (uriInfo.getQueryParameters().containsKey("key")) {
AccessCodeEntry accessCode = tokenManager.getAccessCode(uriInfo.getQueryParameters().getFirst("key"));
accessCode.setAuthMethod("form");
if (accessCode == null || accessCode.isExpired()
|| !accessCode.getRequiredActions().contains(RequiredAction.UPDATE_PASSWORD)) {
return unauthorized();
}
return Flows.forms(realm, request, uriInfo).setAccessCode(accessCode.getId(), accessCode.getCode()).createResponse(RequiredAction.UPDATE_PASSWORD);
} else {
return Flows.forms(realm, request, uriInfo).createPasswordReset();
@ -254,6 +292,12 @@ public class RequiredActionsService {
"Login requester not enabled.");
}
audit.event(Events.SEND_RESET_PASSWORD).client(clientId)
.detail(Details.REDIRECT_URI, redirect)
.detail(Details.RESPONSE_TYPE, "code")
.detail(Details.AUTH_METHOD, "form")
.detail(Details.USERNAME, username);
UserModel user = realm.getUser(username);
if (user == null && username.contains("@")) {
user = realm.getUserByEmail(username);
@ -261,6 +305,7 @@ public class RequiredActionsService {
if (user == null) {
logger.warn("Failed to send password reset email: user not found");
audit.error(Errors.USER_NOT_FOUND);
} else {
Set<RequiredAction> requiredActions = new HashSet<RequiredAction>(user.getRequiredActions());
requiredActions.add(RequiredAction.UPDATE_PASSWORD);
@ -268,9 +313,12 @@ public class RequiredActionsService {
AccessCodeEntry accessCode = tokenManager.createAccessCode(scopeParam, state, redirect, realm, client, user);
accessCode.setRequiredActions(requiredActions);
accessCode.setExpiration(Time.currentTime() + realm.getAccessCodeLifespanUserAction());
accessCode.setAuthMethod("form");
accessCode.setUsername(username);
try {
new EmailSender(realm.getSmtpConfig()).sendPasswordReset(user, realm, accessCode, uriInfo);
audit.user(user).detail(Details.EMAIL, user.getEmail()).detail(Details.CODE_ID, accessCode.getId()).success();
} catch (EmailException e) {
logger.error("Failed to send password reset email", e);
return Flows.forms(realm, request, uriInfo).setError("emailSendError").createErrorPage();
@ -339,11 +387,27 @@ public class RequiredActionsService {
} else {
logger.debug("redirectOauth: redirecting to: {0}", accessCode.getRedirectUri());
accessCode.setExpiration(Time.currentTime() + realm.getAccessCodeLifespan());
audit.success();
return Flows.oauth(realm, request, uriInfo, authManager, tokenManager).redirectAccessCode(accessCode,
accessCode.getState(), accessCode.getRedirectUri());
}
}
private void initAudit(AccessCodeEntry accessCode) {
audit.event(Events.LOGIN).client(accessCode.getClient())
.user(accessCode.getUser())
.detail(Details.CODE_ID, accessCode.getId())
.detail(Details.REDIRECT_URI, accessCode.getRedirectUri())
.detail(Details.RESPONSE_TYPE, "code")
.detail(Details.AUTH_METHOD, accessCode.getAuthMethod())
.detail(Details.USERNAME, accessCode.getUsername());
if (accessCode.isRememberMe()) {
audit.detail(Details.REMEMBER_ME, "true");
}
}
private Response unauthorized() {
return Flows.forms(realm, request, uriInfo).setError("Unauthorized request").createErrorPage();
}

View file

@ -24,6 +24,10 @@ package org.keycloak.services.resources;
import org.jboss.resteasy.logging.Logger;
import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.OAuth2Constants;
import org.keycloak.audit.Audit;
import org.keycloak.audit.Details;
import org.keycloak.audit.Errors;
import org.keycloak.audit.Events;
import org.keycloak.models.AccountRoles;
import org.keycloak.models.ClientModel;
import org.keycloak.models.Constants;
@ -48,6 +52,7 @@ import org.keycloak.social.SocialProviderConfig;
import org.keycloak.social.SocialProviderException;
import org.keycloak.social.SocialUser;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
@ -89,6 +94,8 @@ public class SocialResource {
@Context
protected KeycloakSession session;
@Context
protected HttpServletRequest servletRequest;
private SocialRequestManager socialRequestManager;
@ -114,19 +121,33 @@ public class SocialResource {
RealmManager realmManager = new RealmManager(session);
RealmModel realm = realmManager.getRealmByName(realmName);
Audit audit = Audit.create(realm, servletRequest.getRemoteAddr())
.event(Events.LOGIN)
.detail(Details.RESPONSE_TYPE, "code")
.detail(Details.AUTH_METHOD, "social");
OAuthFlows oauth = Flows.oauth(realm, request, uriInfo, authManager, tokenManager);
if (!realm.isEnabled()) {
audit.error(Errors.REALM_DISABLED);
return oauth.forwardToSecurityFailure("Realm not enabled.");
}
String clientId = requestData.getClientAttributes().get("clientId");
String redirectUri = requestData.getClientAttribute("redirectUri");
String scope = requestData.getClientAttributes().get(OAuth2Constants.SCOPE);
String state = requestData.getClientAttributes().get(OAuth2Constants.STATE);
String responseType = requestData.getClientAttribute("responseType");
audit.client(clientId).detail(Details.REDIRECT_URI, redirectUri);
ClientModel client = realm.findClient(clientId);
if (client == null) {
audit.error(Errors.CLIENT_NOT_FOUND);
return oauth.forwardToSecurityFailure("Unknown login requester.");
}
if (!client.isEnabled()) {
audit.error(Errors.CLIENT_DISABLED);
return oauth.forwardToSecurityFailure("Login requester not enabled.");
}
@ -142,17 +163,21 @@ public class SocialResource {
socialUser = provider.processCallback(config, callback);
} catch (SocialAccessDeniedException e) {
MultivaluedHashMap<String, String> queryParms = new MultivaluedHashMap<String, String>();
queryParms.putSingle(OAuth2Constants.CLIENT_ID, requestData.getClientAttribute("clientId"));
queryParms.putSingle(OAuth2Constants.STATE, requestData.getClientAttribute(OAuth2Constants.STATE));
queryParms.putSingle(OAuth2Constants.SCOPE, requestData.getClientAttribute(OAuth2Constants.SCOPE));
queryParms.putSingle(OAuth2Constants.REDIRECT_URI, requestData.getClientAttribute("redirectUri"));
queryParms.putSingle(OAuth2Constants.RESPONSE_TYPE, requestData.getClientAttribute("responseType"));
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);
audit.error(Errors.REJECTED_BY_USER);
return Flows.forms(realm, request, uriInfo).setQueryParams(queryParms).setWarning("Access denied").createLogin();
} catch (SocialProviderException e) {
logger.warn("Failed to process social callback", e);
logger.error("Failed to process social callback", e);
return oauth.forwardToSecurityFailure("Failed to process social callback");
}
audit.detail(Details.USERNAME, socialUser.getId() + "@" + provider.getId());
SocialLinkModel socialLink = new SocialLinkModel(provider.getId(), socialUser.getId(), socialUser.getUsername());
UserModel user = realm.getUserBySocialLink(socialLink);
@ -161,30 +186,39 @@ public class SocialResource {
if (userId != null) {
UserModel authenticatedUser = realm.getUserById(userId);
audit.event(Events.SOCIAL_LINK).user(userId);
if (user != null) {
audit.error(Errors.SOCIAL_ID_IN_USE);
return oauth.forwardToSecurityFailure("This social account is already linked to other user");
}
if (!authenticatedUser.isEnabled()) {
audit.error(Errors.USER_DISABLED);
return oauth.forwardToSecurityFailure("User is disabled");
}
if (!realm.hasRole(authenticatedUser, realm.getApplicationByName(Constants.ACCOUNT_MANAGEMENT_APP).getRole(AccountRoles.MANAGE_ACCOUNT))) {
audit.error(Errors.NOT_ALLOWED);
return oauth.forwardToSecurityFailure("Insufficient permissions to link social account");
}
if (redirectUri == null) {
audit.error(Errors.INVALID_REDIRECT_URI);
return oauth.forwardToSecurityFailure("Unknown redirectUri");
}
realm.addSocialLink(authenticatedUser, socialLink);
logger.debug("Social provider " + provider.getId() + " linked with user " + authenticatedUser.getLoginName());
String redirectUri = requestData.getClientAttributes().get("redirectUri");
if (redirectUri == null) {
return oauth.forwardToSecurityFailure("Unknown redirectUri");
}
audit.success();
return Response.status(Status.FOUND).location(UriBuilder.fromUri(redirectUri).build()).build();
}
if (user == null) {
if (!realm.isRegistrationAllowed()) {
audit.error(Errors.REGISTRATION_DISABLED);
return oauth.forwardToSecurityFailure("Registration not allowed");
}
@ -199,17 +233,22 @@ public class SocialResource {
}
realm.addSocialLink(user, socialLink);
audit.clone().user(user).event(Events.REGISTER)
.detail(Details.REGISTER_METHOD, "social")
.detail(Details.EMAIL, socialUser.getEmail())
.removeDetail("auth_method")
.success();
}
audit.user(user);
if (!user.isEnabled()) {
audit.error(Errors.USER_DISABLED);
return oauth.forwardToSecurityFailure("Your account is not enabled.");
}
String scope = requestData.getClientAttributes().get(OAuth2Constants.SCOPE);
String state = requestData.getClientAttributes().get(OAuth2Constants.STATE);
String redirectUri = requestData.getClientAttributes().get("redirectUri");
return oauth.processAccessCode(scope, state, redirectUri, client, user);
return oauth.processAccessCode(scope, state, redirectUri, client, user, socialLink.getSocialUserId() + "@" + socialLink.getSocialProvider(), false, "social", audit);
}
@GET
@ -221,23 +260,33 @@ public class SocialResource {
RealmManager realmManager = new RealmManager(session);
RealmModel realm = realmManager.getRealmByName(realmName);
Audit audit = Audit.create(realm, servletRequest.getRemoteAddr())
.event(Events.LOGIN).client(clientId)
.detail(Details.REDIRECT_URI, redirectUri)
.detail(Details.RESPONSE_TYPE, "code")
.detail(Details.AUTH_METHOD, "social");
SocialProvider provider = SocialLoader.load(providerId);
if (provider == null) {
audit.error(Errors.SOCIAL_PROVIDER_NOT_FOUND);
return Flows.forms(realm, request, uriInfo).setError("Social provider not found").createErrorPage();
}
ClientModel client = realm.findClient(clientId);
if (client == null) {
audit.error(Errors.CLIENT_NOT_FOUND);
logger.warn("Unknown login requester: " + clientId);
return Flows.forms(realm, request, uriInfo).setError("Unknown login requester.").createErrorPage();
}
if (!client.isEnabled()) {
audit.error(Errors.CLIENT_DISABLED);
logger.warn("Login requester not enabled.");
return Flows.forms(realm, request, uriInfo).setError("Login requester not enabled.").createErrorPage();
}
redirectUri = TokenService.verifyRedirectUri(redirectUri, client);
if (redirectUri == null) {
audit.error(Errors.INVALID_REDIRECT_URI);
return Flows.forms(realm, request, uriInfo).setError("Invalid redirect_uri.").createErrorPage();
}
@ -248,6 +297,7 @@ public class SocialResource {
.putClientAttribute(OAuth2Constants.STATE, state).putClientAttribute("redirectUri", redirectUri)
.putClientAttribute("responseType", responseType).redirectToSocialProvider();
} catch (Throwable t) {
logger.error("Failed to redirect to social auth", t);
return Flows.forms(realm, request, uriInfo).setError("Failed to redirect to social auth").createErrorPage();
}
}

View file

@ -6,6 +6,10 @@ import org.jboss.resteasy.spi.HttpRequest;
import org.jboss.resteasy.spi.HttpResponse;
import org.keycloak.OAuth2Constants;
import org.keycloak.OAuthErrorException;
import org.keycloak.audit.Audit;
import org.keycloak.audit.Details;
import org.keycloak.audit.Errors;
import org.keycloak.audit.Events;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.crypto.RSAProvider;
import org.keycloak.models.ClientModel;
@ -73,6 +77,7 @@ public class TokenService {
protected RealmModel realm;
protected TokenManager tokenManager;
private Audit audit;
protected AuthenticationManager authManager = new AuthenticationManager();
@Context
@ -97,9 +102,10 @@ public class TokenService {
private ResourceAdminManager resourceAdminManager = new ResourceAdminManager();
public TokenService(RealmModel realm, TokenManager tokenManager) {
public TokenService(RealmModel realm, TokenManager tokenManager, Audit audit) {
this.realm = realm;
this.tokenManager = tokenManager;
this.audit = audit;
}
public static UriBuilder tokenServiceBaseUrl(UriInfo uriInfo) {
@ -143,31 +149,42 @@ public class TokenService {
throw new NotAcceptableException("HTTPS required");
}
ClientModel client = authorizeClient(authorizationHeader, form);
audit.event(Events.LOGIN).detail(Details.AUTH_METHOD, "oauth_credentials").detail(Details.RESPONSE_TYPE, "token");
ClientModel client = authorizeClient(authorizationHeader, form, audit);
if (client.isPublicClient()) {
// we don't allow public clients to invoke grants/access to prevent phishing attacks
audit.error(Errors.NOT_ALLOWED);
throw new ForbiddenException("Public clients are not allowed to invoke grants/access");
}
if (form.getFirst(AuthenticationManager.FORM_USERNAME) == null) {
String username = form.getFirst(AuthenticationManager.FORM_USERNAME);
if (username == null) {
audit.error(Errors.USERNAME_MISSING);
throw new NotAuthorizedException("No username");
}
audit.detail(Details.USERNAME, username);
if (!realm.isEnabled()) {
audit.error(Errors.REALM_DISABLED);
throw new NotAuthorizedException("Disabled realm");
}
if (authManager.authenticateForm(realm, form) != AuthenticationStatus.SUCCESS) {
audit.error(Errors.INVALID_USER_CREDENTIALS);
throw new NotAuthorizedException("Auth failed");
}
UserModel user = realm.getUser(form.getFirst(AuthenticationManager.FORM_USERNAME));
String scope = form.getFirst(OAuth2Constants.SCOPE);
AccessTokenResponse res = tokenManager.responseBuilder(realm, client)
AccessTokenResponse res = tokenManager.responseBuilder(realm, client, audit)
.generateAccessToken(scope, client, user)
.generateIDToken()
.build();
audit.success();
return Response.ok(res, MediaType.APPLICATION_JSON_TYPE).build();
}
@ -182,22 +199,28 @@ public class TokenService {
throw new NotAcceptableException("HTTPS required");
}
ClientModel client = authorizeClient(authorizationHeader, form);
audit.event(Events.REFRESH_TOKEN);
ClientModel client = authorizeClient(authorizationHeader, form, audit);
String refreshToken = form.getFirst(OAuth2Constants.REFRESH_TOKEN);
AccessToken accessToken = null;
try {
accessToken = tokenManager.refreshAccessToken(realm, client, refreshToken);
accessToken = tokenManager.refreshAccessToken(realm, client, refreshToken, audit);
} catch (OAuthErrorException e) {
Map<String, String> error = new HashMap<String, String>();
error.put(OAuth2Constants.ERROR, e.getError());
if (e.getDescription() != null) error.put(OAuth2Constants.ERROR_DESCRIPTION, e.getDescription());
audit.error(Errors.INVALID_TOKEN);
throw new BadRequestException(Response.status(Response.Status.BAD_REQUEST).entity(error).type("application/json").build(), e);
}
AccessTokenResponse res = tokenManager.responseBuilder(realm, client)
AccessTokenResponse res = tokenManager.responseBuilder(realm, client, audit)
.accessToken(accessToken)
.generateIDToken()
.generateRefreshToken().build();
audit.success();
return Response.ok(res, MediaType.APPLICATION_JSON_TYPE).build();
}
@ -208,6 +231,23 @@ public class TokenService {
@QueryParam("state") final String state, @QueryParam("redirect_uri") String redirect,
final MultivaluedMap<String, String> formData) {
logger.debug("TokenService.processLogin");
String username = formData.getFirst(AuthenticationManager.FORM_USERNAME);
String rememberMe = formData.getFirst("rememberMe");
boolean remember = rememberMe != null && rememberMe.equalsIgnoreCase("on");
logger.debug("*** Remember me: " + remember);
audit.event(Events.LOGIN).client(clientId)
.detail(Details.REDIRECT_URI, redirect)
.detail(Details.RESPONSE_TYPE, "code")
.detail(Details.AUTH_METHOD, "form")
.detail(Details.USERNAME, username);
if (remember) {
audit.detail(Details.REMEMBER_ME, "true");
}
OAuthFlows oauth = Flows.oauth(realm, request, uriInfo, authManager, tokenManager);
if (!checkSsl()) {
@ -215,30 +255,32 @@ public class TokenService {
}
if (!realm.isEnabled()) {
audit.error(Errors.REALM_DISABLED);
return oauth.forwardToSecurityFailure("Realm not enabled.");
}
ClientModel client = realm.findClient(clientId);
if (client == null) {
audit.error(Errors.CLIENT_NOT_FOUND);
return oauth.forwardToSecurityFailure("Unknown login requester.");
}
if (!client.isEnabled()) {
audit.error(Errors.CLIENT_NOT_FOUND);
return oauth.forwardToSecurityFailure("Login requester not enabled.");
}
redirect = verifyRedirectUri(redirect, client);
if (redirect == null) {
audit.error(Errors.INVALID_REDIRECT_URI);
return oauth.forwardToSecurityFailure("Invalid redirect_uri.");
}
if (formData.containsKey("cancel")) {
audit.error(Errors.REJECTED_BY_USER);
return oauth.redirectError(client, "access_denied", state, redirect);
}
AuthenticationStatus status = authManager.authenticateForm(realm, formData);
String rememberMe = formData.getFirst("rememberMe");
boolean remember = rememberMe != null && rememberMe.equalsIgnoreCase("on");
logger.debug("*** Remember me: " + remember);
if (remember) {
NewCookie cookie = authManager.createRememberMeCookie(realm, uriInfo);
response.addNewCookie(cookie);
@ -249,20 +291,26 @@ public class TokenService {
switch (status) {
case SUCCESS:
case ACTIONS_REQUIRED:
UserModel user = KeycloakModelUtils.findUserByNameOrEmail(realm, formData.getFirst(AuthenticationManager.FORM_USERNAME));
return oauth.processAccessCode(scopeParam, state, redirect, client, user, remember);
UserModel user = KeycloakModelUtils.findUserByNameOrEmail(realm, username);
audit.user(user);
return oauth.processAccessCode(scopeParam, state, redirect, client, user, username, remember, "form", audit);
case ACCOUNT_DISABLED:
audit.error(Errors.USER_DISABLED);
return Flows.forms(realm, request, uriInfo).setError(Messages.ACCOUNT_DISABLED).setFormData(formData).createLogin();
case MISSING_TOTP:
return Flows.forms(realm, request, uriInfo).setFormData(formData).createLoginTotp();
case INVALID_USER:
audit.error(Errors.USER_NOT_FOUND);
return Flows.forms(realm, request, uriInfo).setError(Messages.INVALID_USER).setFormData(formData).createLogin();
default:
audit.error(Errors.INVALID_USER_CREDENTIALS);
return Flows.forms(realm, request, uriInfo).setError(Messages.INVALID_USER).setFormData(formData).createLogin();
}
}
@Path("auth/request/login-actions")
public RequiredActionsService getRequiredActionsService() {
RequiredActionsService service = new RequiredActionsService(realm, tokenManager);
RequiredActionsService service = new RequiredActionsService(realm, tokenManager, audit);
resourceContext.initResource(service);
return service;
}
@ -273,30 +321,46 @@ public class TokenService {
public Response processRegister(@QueryParam("client_id") final String clientId,
@QueryParam("scope") final String scopeParam, @QueryParam("state") final String state,
@QueryParam("redirect_uri") String redirect, final MultivaluedMap<String, String> formData) {
String username = formData.getFirst("username");
String email = formData.getFirst("email");
audit.event(Events.REGISTER).client(clientId)
.detail(Details.REDIRECT_URI, redirect)
.detail(Details.RESPONSE_TYPE, "code")
.detail(Details.USERNAME, username)
.detail(Details.EMAIL, email)
.detail(Details.REGISTER_METHOD, "form");
OAuthFlows oauth = Flows.oauth(realm, request, uriInfo, authManager, tokenManager);
if (!realm.isEnabled()) {
logger.warn("Realm not enabled");
audit.error(Errors.REALM_DISABLED);
return oauth.forwardToSecurityFailure("Realm not enabled");
}
ClientModel client = realm.findClient(clientId);
if (client == null) {
logger.warn("Unknown login requester.");
audit.error(Errors.CLIENT_NOT_FOUND);
return oauth.forwardToSecurityFailure("Unknown login requester.");
}
if (!client.isEnabled()) {
logger.warn("Login requester not enabled.");
audit.error(Errors.CLIENT_DISABLED);
return oauth.forwardToSecurityFailure("Login requester not enabled.");
}
redirect = verifyRedirectUri(redirect, client);
if (redirect == null) {
audit.error(Errors.INVALID_REDIRECT_URI);
return oauth.forwardToSecurityFailure("Invalid redirect_uri.");
}
if (!realm.isRegistrationAllowed()) {
logger.warn("Registration not allowed");
audit.error(Errors.REGISTRATION_DISABLED);
return oauth.forwardToSecurityFailure("Registration not allowed");
}
@ -312,13 +376,13 @@ public class TokenService {
}
if (error != null) {
audit.error(Errors.INVALID_REGISTRATION);
return Flows.forms(realm, request, uriInfo).setError(error).setFormData(formData).createRegistration();
}
String username = formData.getFirst("username");
UserModel user = realm.getUser(username);
if (user != null) {
audit.error(Errors.USERNAME_IN_USE);
return Flows.forms(realm, request, uriInfo).setError(Messages.USERNAME_EXISTS).setFormData(formData).createRegistration();
}
@ -327,7 +391,7 @@ public class TokenService {
user.setFirstName(formData.getFirst("firstName"));
user.setLastName(formData.getFirst("lastName"));
user.setEmail(formData.getFirst("email"));
user.setEmail(email);
if (requiredCredentialTypes.contains(CredentialRepresentation.PASSWORD)) {
UserCredentialModel credentials = new UserCredentialModel();
@ -342,6 +406,9 @@ public class TokenService {
}
}
audit.user(user).success();
audit.reset();
return processLogin(clientId, scopeParam, state, redirect, formData);
}
@ -362,19 +429,20 @@ public class TokenService {
throw new NotAcceptableException("HTTPS required");
}
audit.event(Events.CODE_TO_TOKEN);
if (!realm.isEnabled()) {
audit.error(Errors.REALM_DISABLED);
throw new NotAuthorizedException("Realm not enabled");
}
ClientModel client = authorizeClient(authorizationHeader, formData);
String code = formData.getFirst(OAuth2Constants.CODE);
if (code == null) {
Map<String, String> error = new HashMap<String, String>();
error.put(OAuth2Constants.ERROR, "invalid_request");
error.put(OAuth2Constants.ERROR_DESCRIPTION, "code not specified");
audit.error(Errors.INVALID_CODE);
throw new BadRequestException("Code not specified", Response.status(Response.Status.BAD_REQUEST).entity(error).type("application/json").build());
}
JWSInput input = new JWSInput(code);
@ -388,22 +456,33 @@ public class TokenService {
Map<String, String> res = new HashMap<String, String>();
res.put(OAuth2Constants.ERROR, "invalid_grant");
res.put(OAuth2Constants.ERROR_DESCRIPTION, "Unable to verify code signature");
audit.error(Errors.INVALID_CODE);
return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(res)
.build();
}
String key = input.readContentAsString();
audit.detail(Details.CODE_ID, key);
AccessCodeEntry accessCode = tokenManager.pullAccessCode(key);
if (accessCode == null) {
Map<String, String> res = new HashMap<String, String>();
res.put(OAuth2Constants.ERROR, "invalid_grant");
res.put(OAuth2Constants.ERROR_DESCRIPTION, "Code not found");
audit.error(Errors.INVALID_CODE);
return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(res)
.build();
}
audit.user(accessCode.getUser());
ClientModel client = authorizeClient(authorizationHeader, formData, audit);
if (accessCode.isExpired()) {
Map<String, String> res = new HashMap<String, String>();
res.put(OAuth2Constants.ERROR, "invalid_grant");
res.put(OAuth2Constants.ERROR_DESCRIPTION, "Code is expired");
audit.error(Errors.INVALID_CODE);
return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(res)
.build();
}
@ -411,6 +490,7 @@ public class TokenService {
Map<String, String> res = new HashMap<String, String>();
res.put(OAuth2Constants.ERROR, "invalid_grant");
res.put(OAuth2Constants.ERROR_DESCRIPTION, "Token expired");
audit.error(Errors.INVALID_CODE);
return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(res)
.build();
}
@ -418,19 +498,24 @@ public class TokenService {
Map<String, String> res = new HashMap<String, String>();
res.put(OAuth2Constants.ERROR, "invalid_grant");
res.put(OAuth2Constants.ERROR_DESCRIPTION, "Auth error");
audit.error(Errors.INVALID_CODE);
return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(res)
.build();
}
logger.debug("accessRequest SUCCESS");
AccessTokenResponse res = tokenManager.responseBuilder(realm, client)
AccessTokenResponse res = tokenManager.responseBuilder(realm, client, audit)
.accessToken(accessCode.getToken())
.generateIDToken()
.generateRefreshToken().build();
audit.success();
return Cors.add(request, Response.ok(res)).auth().allowedOrigins(client).allowedMethods("POST").build();
}
protected ClientModel authorizeClient(String authorizationHeader, MultivaluedMap<String, String> formData) {
protected ClientModel authorizeClient(String authorizationHeader, MultivaluedMap<String, String> formData, Audit audit) {
String client_id = null;
String clientSecret = null;
if (authorizationHeader != null) {
@ -453,11 +538,14 @@ public class TokenService {
throw new BadRequestException("Could not find client", Response.status(Response.Status.BAD_REQUEST).entity(error).type("application/json").build());
}
audit.client(client_id);
ClientModel client = realm.findClient(client_id);
if (client == null) {
Map<String, String> error = new HashMap<String, String>();
error.put(OAuth2Constants.ERROR, "invalid_client");
error.put(OAuth2Constants.ERROR_DESCRIPTION, "Could not find client");
audit.error(Errors.CLIENT_NOT_FOUND);
throw new BadRequestException("Could not find client", Response.status(Response.Status.BAD_REQUEST).entity(error).type("application/json").build());
}
@ -465,6 +553,7 @@ public class TokenService {
Map<String, String> error = new HashMap<String, String>();
error.put(OAuth2Constants.ERROR, "invalid_client");
error.put(OAuth2Constants.ERROR_DESCRIPTION, "Client is not enabled");
audit.error(Errors.CLIENT_DISABLED);
throw new BadRequestException("Client is not enabled", Response.status(Response.Status.BAD_REQUEST).entity(error).type("application/json").build());
}
@ -472,6 +561,7 @@ public class TokenService {
if (!client.validateSecret(clientSecret)) {
Map<String, String> error = new HashMap<String, String>();
error.put(OAuth2Constants.ERROR, "unauthorized_client");
audit.error(Errors.INVALID_CLIENT_CREDENTIALS);
throw new BadRequestException("Unauthorized Client", Response.status(Response.Status.BAD_REQUEST).entity(error).type("application/json").build());
}
}
@ -484,6 +574,9 @@ public class TokenService {
@QueryParam("redirect_uri") String redirect, final @QueryParam("client_id") String clientId,
final @QueryParam("scope") String scopeParam, final @QueryParam("state") String state, final @QueryParam("prompt") String prompt) {
logger.info("TokenService.loginPage");
audit.event(Events.LOGIN).client(clientId).detail(Details.REDIRECT_URI, redirect).detail(Details.RESPONSE_TYPE, "code");
OAuthFlows oauth = Flows.oauth(realm, request, uriInfo, authManager, tokenManager);
if (!checkSsl()) {
@ -492,20 +585,24 @@ public class TokenService {
if (!realm.isEnabled()) {
logger.warn("Realm not enabled");
audit.error(Errors.REALM_DISABLED);
return oauth.forwardToSecurityFailure("Realm not enabled");
}
ClientModel client = realm.findClient(clientId);
if (client == null) {
logger.warn("Unknown login requester: " + clientId);
audit.error(Errors.CLIENT_NOT_FOUND);
return oauth.forwardToSecurityFailure("Unknown login requester.");
}
if (!client.isEnabled()) {
logger.warn("Login requester not enabled.");
audit.error(Errors.CLIENT_DISABLED);
return oauth.forwardToSecurityFailure("Login requester not enabled.");
}
redirect = verifyRedirectUri(redirect, client);
if (redirect == null) {
audit.error(Errors.INVALID_REDIRECT_URI);
return oauth.forwardToSecurityFailure("Invalid redirect_uri.");
}
@ -513,7 +610,8 @@ public class TokenService {
UserModel user = authManager.authenticateIdentityCookie(realm, uriInfo, headers);
if (user != null) {
logger.debug(user.getLoginName() + " already logged in.");
return oauth.processAccessCode(scopeParam, state, redirect, client, user);
audit.user(user).detail(Details.AUTH_METHOD, "sso");
return oauth.processAccessCode(scopeParam, state, redirect, client, user, null, false, "sso", audit);
}
if (prompt != null && prompt.equals("none")) {
@ -529,6 +627,9 @@ public class TokenService {
@QueryParam("redirect_uri") String redirect, final @QueryParam("client_id") String clientId,
final @QueryParam("scope") String scopeParam, final @QueryParam("state") String state) {
logger.info("**********registerPage()");
audit.event(Events.REGISTER).client(clientId).detail(Details.REDIRECT_URI, redirect).detail(Details.RESPONSE_TYPE, "code");
OAuthFlows oauth = Flows.oauth(realm, request, uriInfo, authManager, tokenManager);
if (!checkSsl()) {
@ -537,26 +638,31 @@ public class TokenService {
if (!realm.isEnabled()) {
logger.warn("Realm not enabled");
audit.error(Errors.REALM_DISABLED);
return oauth.forwardToSecurityFailure("Realm not enabled");
}
ClientModel client = realm.findClient(clientId);
if (client == null) {
logger.warn("Unknown login requester.");
audit.error(Errors.CLIENT_NOT_FOUND);
return oauth.forwardToSecurityFailure("Unknown login requester.");
}
if (!client.isEnabled()) {
logger.warn("Login requester not enabled.");
audit.error(Errors.CLIENT_DISABLED);
return oauth.forwardToSecurityFailure("Login requester not enabled.");
}
redirect = verifyRedirectUri(redirect, client);
if (redirect == null) {
audit.error(Errors.INVALID_REDIRECT_URI);
return oauth.forwardToSecurityFailure("Invalid redirect_uri.");
}
if (!realm.isRegistrationAllowed()) {
logger.warn("Registration not allowed");
audit.error(Errors.REGISTRATION_DISABLED);
return oauth.forwardToSecurityFailure("Registration not allowed");
}
@ -571,6 +677,8 @@ public class TokenService {
public Response logout(final @QueryParam("redirect_uri") String redirectUri) {
// todo do we care if anybody can trigger this?
audit.event(Events.LOGOUT).detail(Details.REDIRECT_URI, redirectUri);
// authenticate identity cookie, but ignore an access token timeout as we're logging out anyways.
UserModel user = authManager.authenticateIdentityCookie(realm, uriInfo, headers, false);
if (user != null) {
@ -578,6 +686,8 @@ public class TokenService {
authManager.expireIdentityCookie(realm, uriInfo);
authManager.expireRememberMeCookie(realm, uriInfo);
resourceAdminManager.logoutUser(realm, user);
audit.user(user).success();
} else {
logger.info("No user logged in for logout");
}
@ -589,6 +699,8 @@ public class TokenService {
@POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public Response processOAuth(final MultivaluedMap<String, String> formData) {
audit.event(Events.LOGIN).detail(Details.RESPONSE_TYPE, "code");
OAuthFlows oauth = Flows.oauth(realm, request, uriInfo, authManager, tokenManager);
if (!checkSsl()) {
@ -604,21 +716,39 @@ public class TokenService {
logger.debug("Failed to verify signature", ignored);
}
if (!verifiedCode) {
audit.error(Errors.INVALID_CODE);
return oauth.forwardToSecurityFailure("Illegal access code.");
}
String key = input.readContentAsString();
audit.detail(Details.CODE_ID, key);
AccessCodeEntry accessCodeEntry = tokenManager.getAccessCode(key);
if (accessCodeEntry == null) {
audit.error(Errors.INVALID_CODE);
return oauth.forwardToSecurityFailure("Unknown access code.");
}
String redirect = accessCodeEntry.getRedirectUri();
String state = accessCodeEntry.getState();
audit.client(accessCodeEntry.getClient())
.user(accessCodeEntry.getUser())
.detail(Details.RESPONSE_TYPE, "code")
.detail(Details.AUTH_METHOD, accessCodeEntry.getAuthMethod())
.detail(Details.REDIRECT_URI, redirect)
.detail(Details.USERNAME, accessCodeEntry.getUsername());
if (accessCodeEntry.isRememberMe()) {
audit.detail(Details.REMEMBER_ME, "true");
}
if (formData.containsKey("cancel")) {
audit.error(Errors.REJECTED_BY_USER);
return redirectAccessDenied(redirect, state);
}
audit.success();
accessCodeEntry.setExpiration(Time.currentTime() + realm.getAccessCodeLifespan());
return oauth.redirectAccessCode(accessCodeEntry, state, redirect);
}

View file

@ -24,23 +24,22 @@ package org.keycloak.services.resources.flows;
import org.jboss.resteasy.logging.Logger;
import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.OAuth2Constants;
import org.keycloak.audit.Audit;
import org.keycloak.audit.Details;
import org.keycloak.audit.Events;
import org.keycloak.models.ApplicationModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.Constants;
import org.keycloak.models.OAuthClientModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RequiredCredentialModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserModel.RequiredAction;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.services.managers.AccessCodeEntry;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.TokenManager;
import org.keycloak.services.resources.TokenService;
import org.keycloak.util.Time;
import javax.ws.rs.Path;
import javax.ws.rs.core.Cookie;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
@ -56,15 +55,15 @@ public class OAuthFlows {
private static final Logger log = Logger.getLogger(OAuthFlows.class);
private RealmModel realm;
private final RealmModel realm;
private HttpRequest request;
private final HttpRequest request;
private UriInfo uriInfo;
private final UriInfo uriInfo;
private AuthenticationManager authManager;
private final AuthenticationManager authManager;
private TokenManager tokenManager;
private final TokenManager tokenManager;
OAuthFlows(RealmModel realm, HttpRequest request, UriInfo uriInfo, AuthenticationManager authManager,
TokenManager tokenManager) {
@ -110,28 +109,40 @@ public class OAuthFlows {
}
}
public Response processAccessCode(String scopeParam, String state, String redirect, ClientModel client, UserModel user) {
return processAccessCode(scopeParam, state, redirect, client, user, false);
public Response processAccessCode(String scopeParam, String state, String redirect, ClientModel client, UserModel user, Audit audit) {
return processAccessCode(scopeParam, state, redirect, client, user, null, false, "form", audit);
}
public Response processAccessCode(String scopeParam, String state, String redirect, ClientModel client, UserModel user, boolean rememberMe) {
public Response processAccessCode(String scopeParam, String state, String redirect, ClientModel client, UserModel user, String username, boolean rememberMe, String authMethod, Audit audit) {
isTotpConfigurationRequired(user);
isEmailVerificationRequired(user);
boolean isResource = client instanceof ApplicationModel;
AccessCodeEntry accessCode = tokenManager.createAccessCode(scopeParam, state, redirect, realm, client, user);
accessCode.setUsername(username);
accessCode.setRememberMe(rememberMe);
accessCode.setAuthMethod(authMethod);
log.debug("processAccessCode: isResource: {0}", isResource);
log.debug("processAccessCode: go to oauth page?: {0}",
(!isResource && (accessCode.getRealmRolesRequested().size() > 0 || accessCode.getResourceRolesRequested()
.size() > 0)));
audit.detail(Details.CODE_ID, accessCode.getId());
Set<RequiredAction> requiredActions = user.getRequiredActions();
if (!requiredActions.isEmpty()) {
accessCode.setRequiredActions(new HashSet<UserModel.RequiredAction>(requiredActions));
accessCode.setExpiration(Time.currentTime() + realm.getAccessCodeLifespanUserAction());
RequiredAction action = user.getRequiredActions().iterator().next();
if (action.equals(RequiredAction.VERIFY_EMAIL)) {
audit.clone().event(Events.SEND_VERIFY_EMAIL).detail(Details.EMAIL, accessCode.getUser().getEmail()).success();
}
return Flows.forms(realm, request, uriInfo).setAccessCode(accessCode.getId(), accessCode.getCode()).setUser(user)
.createResponse(user.getRequiredActions().iterator().next());
.createResponse(action);
}
if (!isResource
@ -143,6 +154,7 @@ public class OAuthFlows {
}
if (redirect != null) {
audit.success();
return redirectAccessCode(accessCode, state, redirect, rememberMe);
} else {
return null;

View file

@ -36,6 +36,16 @@
<artifactId>keycloak-admin-ui</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-audit-api</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-audit-jboss-logging</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-admin-ui-styles</artifactId>

View file

@ -21,18 +21,12 @@
*/
package org.keycloak.testsuite;
import org.apache.http.NameValuePair;
import org.apache.http.client.utils.URLEncodedUtils;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>

View file

@ -0,0 +1,310 @@
package org.keycloak.testsuite;
import org.hamcrest.CoreMatchers;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeMatcher;
import org.jboss.logging.Logger;
import org.junit.Assert;
import org.junit.rules.TestRule;
import org.junit.runners.model.Statement;
import org.keycloak.audit.AuditListener;
import org.keycloak.audit.Details;
import org.keycloak.audit.Event;
import org.keycloak.models.ClientModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.testsuite.rule.KeycloakRule;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class AssertEvents implements TestRule, AuditListener{
private static final Logger log = Logger.getLogger(AssertEvents.class);
public static String DEFAULT_CLIENT_ID = "test-app";
public static String DEFAULT_REDIRECT_URI = "http://localhost:8081/app/auth";
public static String DEFAULT_IP_ADDRESS = "127.0.0.1";
public static String DEFAULT_REALM = "test";
public static String DEFAULT_USERNAME = "test-user@localhost";
private KeycloakRule keycloak;
private static BlockingQueue<Event> events = new LinkedBlockingQueue<Event>();
public AssertEvents() {
}
public AssertEvents(KeycloakRule keycloak) {
this.keycloak = keycloak;
}
@Override
public String getId() {
return "assert-events";
}
@Override
public void onEvent(Event event) {
events.add(event);
}
@Override
public Statement apply(final Statement base, org.junit.runner.Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
events.clear();
keycloak.configure(new KeycloakRule.KeycloakSetup() {
@Override
public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
Set<String> listeners = new HashSet<String>();
listeners.add("jboss-logging");
listeners.add("assert-events");
appRealm.setAuditListeners(listeners);
}
});
try {
base.evaluate();
Event event = events.peek();
if (event != null) {
Assert.fail("Unexpected event after test: " + event.getEvent());
}
} finally {
keycloak.configure(new KeycloakRule.KeycloakSetup() {
@Override
public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
appRealm.setAuditListeners(null);
}
});
}
}
};
}
public void assertEmpty() {
Assert.assertTrue(events.isEmpty());
}
public Event poll() {
try {
return events.poll(10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
return null;
}
}
public void clear() {
events.clear();
}
public ExpectedEvent expectRequiredAction(String event) {
return expectLogin().event(event);
}
public ExpectedEvent expectLogin() {
return expect("login")
.detail(Details.CODE_ID, isCodeId())
.detail(Details.USERNAME, DEFAULT_USERNAME)
.detail(Details.RESPONSE_TYPE, "code")
.detail(Details.AUTH_METHOD, "form")
.detail(Details.REDIRECT_URI, DEFAULT_REDIRECT_URI);
}
public ExpectedEvent expectCodeToToken(String codeId) {
return expect("code_to_token")
.detail(Details.CODE_ID, codeId)
.detail(Details.TOKEN_ID, isUUID())
.detail(Details.REFRESH_TOKEN_ID, isUUID());
}
public ExpectedEvent expectRefresh(String refreshTokenId) {
return expect("refresh_token")
.detail(Details.TOKEN_ID, isUUID())
.detail(Details.REFRESH_TOKEN_ID, refreshTokenId)
.detail(Details.UPDATED_REFRESH_TOKEN_ID, isUUID());
}
public ExpectedEvent expectLogout() {
return expect("logout").client((String) null)
.detail(Details.REDIRECT_URI, DEFAULT_REDIRECT_URI);
}
public ExpectedEvent expectRegister(String username, String email) {
UserRepresentation user = keycloak.getUser("test", username);
return expect("register")
.user(user != null ? user.getId() : null)
.detail(Details.USERNAME, username)
.detail(Details.EMAIL, email)
.detail(Details.RESPONSE_TYPE, "code")
.detail(Details.REGISTER_METHOD, "form")
.detail(Details.REDIRECT_URI, DEFAULT_REDIRECT_URI);
}
public ExpectedEvent expectAccount(String event) {
return expect(event).client("account");
}
public ExpectedEvent expect(String event) {
return new ExpectedEvent().realm(DEFAULT_REALM).client(DEFAULT_CLIENT_ID).user(keycloak.getUser(DEFAULT_REALM, DEFAULT_USERNAME).getId()).ipAddress(DEFAULT_IP_ADDRESS).event(event);
}
public static class ExpectedEvent {
private Event expected = new Event();
private Matcher<String> userId;
private HashMap<String, Matcher<String>> details;
public ExpectedEvent realm(RealmModel realm) {
expected.setRealmId(realm.getId());
return this;
}
public ExpectedEvent realm(String realmId) {
expected.setRealmId(realmId);
return this;
}
public ExpectedEvent client(ClientModel client) {
expected.setClientId(client.getClientId());
return this;
}
public ExpectedEvent client(String clientId) {
expected.setClientId(clientId);
return this;
}
public ExpectedEvent user(UserModel user) {
return user(CoreMatchers.equalTo(user.getId()));
}
public ExpectedEvent user(String userId) {
return user(CoreMatchers.equalTo(userId));
}
public ExpectedEvent user(Matcher<String> userId) {
this.userId = userId;
return this;
}
public ExpectedEvent ipAddress(String ipAddress) {
expected.setIpAddress(ipAddress);
return this;
}
public ExpectedEvent event(String e) {
expected.setEvent(e);
return this;
}
public ExpectedEvent detail(String key, String value) {
return detail(key, CoreMatchers.equalTo(value));
}
public ExpectedEvent detail(String key, Matcher<String> matcher) {
if (details == null) {
details = new HashMap<String, Matcher<String>>();
}
details.put(key, matcher);
return this;
}
public ExpectedEvent removeDetail(String key) {
if (details != null) {
details.remove(key);
}
return this;
}
public ExpectedEvent error(String error) {
expected.setError(error);
return this;
}
public Event assertEvent() {
try {
return assertEvent(events.poll(10, TimeUnit.SECONDS));
} catch (InterruptedException e) {
throw new AssertionError("No event received within timeout");
}
}
public Event assertEvent(Event actual) {
Assert.assertEquals(expected.getEvent(), actual.getEvent());
Assert.assertEquals(expected.getRealmId(), actual.getRealmId());
Assert.assertEquals(expected.getClientId(), actual.getClientId());
Assert.assertEquals(expected.getError(), actual.getError());
Assert.assertEquals(expected.getIpAddress(), actual.getIpAddress());
Assert.assertThat(actual.getUserId(), userId);
if (details == null) {
Assert.assertNull(actual.getDetails());
} else {
Assert.assertNotNull(actual.getDetails());
for (Map.Entry<String, Matcher<String>> d : details.entrySet()) {
String actualValue = actual.getDetails().get(d.getKey());
if (!actual.getDetails().containsKey(d.getKey())) {
Assert.fail(d.getKey() + " missing");
}
if (!d.getValue().matches(actualValue)) {
Assert.fail(d.getKey() + " doesn't match");
}
}
for (String k : actual.getDetails().keySet()) {
if (!details.containsKey(k)) {
Assert.fail(k + " was not expected");
}
}
}
return actual;
}
}
public static Matcher<String> isCodeId() {
return new TypeSafeMatcher<String>() {
@Override
protected boolean matchesSafely(String item) {
return (UUID.randomUUID().toString() + System.currentTimeMillis()).length() == item.length();
}
@Override
public void describeTo(Description description) {
description.appendText("Not an Code ID");
}
};
}
public static Matcher<String> isUUID() {
return new TypeSafeMatcher<String>() {
@Override
protected boolean matchesSafely(String item) {
return KeycloakModelUtils.generateId().length() == item.length();
}
@Override
public void describeTo(Description description) {
description.appendText("Not an UUID");
}
};
}
}

View file

@ -11,7 +11,6 @@ import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.URI;
import java.nio.charset.Charset;
import java.util.List;
import java.util.UUID;

View file

@ -19,24 +19,38 @@
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.keycloak.testsuite.forms;
package org.keycloak.testsuite.account;
import org.junit.*;
import org.keycloak.models.*;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.audit.Details;
import org.keycloak.models.ApplicationModel;
import org.keycloak.models.PasswordPolicy;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.TimeBasedOTP;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.OAuthClient;
import org.keycloak.testsuite.pages.*;
import org.keycloak.testsuite.pages.AccountPasswordPage;
import org.keycloak.testsuite.pages.AccountTotpPage;
import org.keycloak.testsuite.pages.AccountUpdateProfilePage;
import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.AppPage.RequestType;
import org.keycloak.testsuite.pages.ErrorPage;
import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.rule.KeycloakRule;
import org.keycloak.testsuite.rule.KeycloakRule.KeycloakSetup;
import org.keycloak.testsuite.rule.WebResource;
import org.keycloak.testsuite.rule.WebRule;
import org.openqa.selenium.WebDriver;
import static org.junit.Assert.assertEquals;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
@ -62,6 +76,11 @@ public class AccountTest {
}
});
public static String ACCOUNT_REDIRECT = "http://localhost:8081/auth/rest/realms/test/account/login-redirect";
@Rule
public AssertEvents events = new AssertEvents(keycloakRule);
@Rule
public WebRule webRule = new WebRule(this);
@ -90,6 +109,12 @@ public class AccountTest {
protected ErrorPage errorPage;
private TimeBasedOTP totp = new TimeBasedOTP();
private String userId;
@Before
public void before() {
userId = keycloakRule.getUser("test", "test-user@localhost").getId();
}
@After
public void after() {
@ -122,6 +147,8 @@ public class AccountTest {
Assert.assertTrue(appPage.isCurrent());
Assert.assertEquals(appPage.baseUrl + "?test", driver.getCurrentUrl());
events.clear();
}
@Test
@ -129,6 +156,8 @@ public class AccountTest {
changePasswordPage.open();
loginPage.login("test-user@localhost", "password");
events.expectLogin().client("account").detail(Details.REDIRECT_URI, ACCOUNT_REDIRECT + "?path=password").assertEvent();
changePasswordPage.changePassword("", "new-password", "new-password");
Assert.assertEquals("Please specify password.", profilePage.getError());
@ -141,6 +170,8 @@ public class AccountTest {
Assert.assertEquals("Your password has been updated", profilePage.getSuccess());
events.expectAccount("update_password").assertEvent();
changePasswordPage.logout();
loginPage.open();
@ -148,10 +179,14 @@ public class AccountTest {
Assert.assertEquals("Invalid username or password.", loginPage.getError());
events.expectLogin().user((String) null).error("invalid_user_credentials").removeDetail(Details.CODE_ID).assertEvent();
loginPage.open();
loginPage.login("test-user@localhost", "new-password");
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
events.expectLogin().assertEvent();
}
@Test
@ -167,6 +202,8 @@ public class AccountTest {
changePasswordPage.open();
loginPage.login("test-user@localhost", "password");
events.expectLogin().client("account").detail(Details.REDIRECT_URI, ACCOUNT_REDIRECT + "?path=password").assertEvent();
changePasswordPage.changePassword("", "new", "new");
Assert.assertEquals("Please specify password.", profilePage.getError());
@ -174,6 +211,8 @@ public class AccountTest {
changePasswordPage.changePassword("password", "new-password", "new-password");
Assert.assertEquals("Your password has been updated", profilePage.getSuccess());
events.expectAccount("update_password").assertEvent();
} finally {
keycloakRule.configure(new KeycloakRule.KeycloakSetup() {
@Override
@ -189,6 +228,8 @@ public class AccountTest {
profilePage.open();
loginPage.login("test-user@localhost", "password");
events.expectLogin().client("account").detail(Details.REDIRECT_URI, ACCOUNT_REDIRECT).assertEvent();
Assert.assertEquals("", profilePage.getFirstName());
Assert.assertEquals("", profilePage.getLastName());
Assert.assertEquals("test-user@localhost", profilePage.getEmail());
@ -201,6 +242,8 @@ public class AccountTest {
Assert.assertEquals("", profilePage.getLastName());
Assert.assertEquals("test-user@localhost", profilePage.getEmail());
events.assertEmpty();
profilePage.updateProfile("New first", "", "new@email.com");
Assert.assertEquals("Please specify last name", profilePage.getError());
@ -208,6 +251,8 @@ public class AccountTest {
Assert.assertEquals("", profilePage.getLastName());
Assert.assertEquals("test-user@localhost", profilePage.getEmail());
events.assertEmpty();
profilePage.updateProfile("New first", "New last", "");
Assert.assertEquals("Please specify email", profilePage.getError());
@ -215,12 +260,17 @@ public class AccountTest {
Assert.assertEquals("", profilePage.getLastName());
Assert.assertEquals("test-user@localhost", profilePage.getEmail());
events.assertEmpty();
profilePage.updateProfile("New first", "New last", "new@email.com");
Assert.assertEquals("Your account has been updated", profilePage.getSuccess());
Assert.assertEquals("New first", profilePage.getFirstName());
Assert.assertEquals("New last", profilePage.getLastName());
Assert.assertEquals("new@email.com", profilePage.getEmail());
events.expectAccount("update_profile").assertEvent();
events.expectAccount("update_email").detail(Details.PREVIOUS_EMAIL, "test-user@localhost").detail(Details.UPDATED_EMAIL, "new@email.com").assertEvent();
}
@Test
@ -228,6 +278,8 @@ public class AccountTest {
totpPage.open();
loginPage.login("test-user@localhost", "password");
events.expectLogin().client("account").detail(Details.REDIRECT_URI, ACCOUNT_REDIRECT + "?path=totp").assertEvent();
Assert.assertTrue(totpPage.isCurrent());
Assert.assertFalse(driver.getPageSource().contains("Remove Google"));
@ -241,7 +293,13 @@ public class AccountTest {
Assert.assertEquals("Google authenticator configured.", profilePage.getSuccess());
events.expectAccount("update_totp").assertEvent();
Assert.assertTrue(driver.getPageSource().contains("pficon-delete"));
totpPage.removeTotp();
events.expectAccount("remove_totp").assertEvent();
}
@Test
@ -249,6 +307,10 @@ public class AccountTest {
profilePage.open();
loginPage.login("test-user-no-access@localhost", "password");
events.expectLogin().client("account").user(keycloakRule.getUser("test", "test-user-no-access@localhost").getId())
.detail(Details.USERNAME, "test-user-no-access@localhost")
.detail(Details.REDIRECT_URI, ACCOUNT_REDIRECT).assertEvent();
Assert.assertTrue(errorPage.isCurrent());
Assert.assertEquals("No access", errorPage.getError());
}

View file

@ -36,7 +36,6 @@ import java.io.IOException;
import java.net.URI;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
/**

View file

@ -22,13 +22,17 @@
package org.keycloak.testsuite.actions;
import org.junit.Assert;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.audit.Details;
import org.keycloak.audit.Event;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserModel.RequiredAction;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.OAuthClient;
import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.AppPage.RequestType;
import org.keycloak.testsuite.pages.LoginPage;
@ -53,17 +57,10 @@ import java.util.regex.Pattern;
public class RequiredActionEmailVerificationTest {
@ClassRule
public static KeycloakRule keycloakRule = new KeycloakRule(new KeycloakSetup() {
public static KeycloakRule keycloakRule = new KeycloakRule();
@Override
public void config(RealmManager manager, RealmModel defaultRealm, RealmModel appRealm) {
appRealm.setVerifyEmail(true);
UserModel user = appRealm.getUser("test-user@localhost");
user.addRequiredAction(RequiredAction.VERIFY_EMAIL);
}
});
@Rule
public AssertEvents events = new AssertEvents(keycloakRule);
@Rule
public WebRule webRule = new WebRule(this);
@ -74,6 +71,9 @@ public class RequiredActionEmailVerificationTest {
@WebResource
protected WebDriver driver;
@WebResource
protected OAuthClient oauth;
@WebResource
protected AppPage appPage;
@ -86,6 +86,21 @@ public class RequiredActionEmailVerificationTest {
@WebResource
protected RegisterPage registerPage;
@Before
public void before() {
keycloakRule.configure(new KeycloakSetup() {
@Override
public void config(RealmManager manager, RealmModel defaultRealm, RealmModel appRealm) {
appRealm.setVerifyEmail(true);
UserModel user = appRealm.getUser("test-user@localhost");
user.setEmailVerified(false);
}
});
}
@Test
public void verifyEmailExisting() throws IOException, MessagingException {
loginPage.open();
@ -105,9 +120,19 @@ public class RequiredActionEmailVerificationTest {
String verificationUrl = m.group(1);
Event sendEvent = events.expectRequiredAction("send_verify_email").detail("email", "test-user@localhost").assertEvent();
String mailCodeId = sendEvent.getDetails().get(Details.CODE_ID);
Assert.assertEquals(mailCodeId, verificationUrl.split("key=")[1]);
driver.navigate().to(verificationUrl.trim());
events.expectRequiredAction("verify_email").detail("email", "test-user@localhost").detail(Details.CODE_ID, mailCodeId).assertEvent();
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
events.expectLogin().detail(Details.CODE_ID, mailCodeId).assertEvent();
}
@Test
@ -116,6 +141,8 @@ public class RequiredActionEmailVerificationTest {
loginPage.clickRegister();
registerPage.register("firstName", "lastName", "email", "verifyEmail", "password", "password");
String userId = events.expectRegister("verifyEmail", "email").assertEvent().getUserId();
Assert.assertTrue(verifyEmailPage.isCurrent());
Assert.assertEquals(1, greenMail.getReceivedMessages().length);
@ -128,23 +155,34 @@ public class RequiredActionEmailVerificationTest {
Matcher m = p.matcher(body);
m.matches();
Event sendEvent = events.expectRequiredAction("send_verify_email").user(userId).detail("username", "verifyEmail").detail("email", "email").assertEvent();
String mailCodeId = sendEvent.getDetails().get(Details.CODE_ID);
String verificationUrl = m.group(1);
driver.navigate().to(verificationUrl.trim());
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
events.expectRequiredAction("verify_email").user(userId).detail("username", "verifyEmail").detail("email", "email").detail(Details.CODE_ID, mailCodeId).assertEvent();
events.expectLogin().user(userId).detail("username", "verifyEmail").detail(Details.CODE_ID, mailCodeId).assertEvent();
}
@Test
public void verifyEmailResend() throws IOException, MessagingException {
loginPage.open();
loginPage.clickRegister();
registerPage.register("firstName2", "lastName2", "email2", "verifyEmail2", "password2", "password2");
loginPage.login("test-user@localhost", "password");
Assert.assertTrue(verifyEmailPage.isCurrent());
Assert.assertEquals(1, greenMail.getReceivedMessages().length);
Event sendEvent = events.expectRequiredAction("send_verify_email").detail("email", "test-user@localhost").assertEvent();
String mailCodeId = sendEvent.getDetails().get(Details.CODE_ID);
verifyEmailPage.clickResendEmail();
Assert.assertEquals(2, greenMail.getReceivedMessages().length);
@ -157,11 +195,17 @@ public class RequiredActionEmailVerificationTest {
Matcher m = p.matcher(body);
m.matches();
events.expectRequiredAction("send_verify_email").detail("email", "test-user@localhost").assertEvent(sendEvent);
String verificationUrl = m.group(1);
driver.navigate().to(verificationUrl.trim());
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
events.expectRequiredAction("verify_email").detail("email", "test-user@localhost").detail(Details.CODE_ID, mailCodeId).assertEvent();
events.expectLogin().assertEvent();
}
}

View file

@ -25,10 +25,13 @@ import org.junit.Assert;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.audit.Details;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserModel.RequiredAction;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.OAuthClient;
import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.AppPage.RequestType;
import org.keycloak.testsuite.pages.LoginPage;
@ -60,9 +63,15 @@ public class RequiredActionMultipleActionsTest {
@Rule
public WebRule webRule = new WebRule(this);
@Rule
public AssertEvents events = new AssertEvents(keycloakRule);
@WebResource
protected WebDriver driver;
@WebResource
protected OAuthClient oauth;
@WebResource
protected AppPage appPage;
@ -95,14 +104,21 @@ public class RequiredActionMultipleActionsTest {
}
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
events.expectLogin().assertEvent();
}
public void updatePassword() {
changePasswordPage.changePassword("new-password", "new-password");
events.expectRequiredAction("update_password").assertEvent();
}
public void updateProfile() {
updateProfilePage.update("New first", "New last", "new@email.com");
events.expectRequiredAction("update_profile").assertEvent();
events.expectRequiredAction("update_email").detail(Details.PREVIOUS_EMAIL, "test-user@localhost").detail(Details.UPDATED_EMAIL, "new@email.com").assertEvent();
}
}

View file

@ -29,6 +29,7 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserModel.RequiredAction;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.OAuthClient;
import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.AppPage.RequestType;
@ -62,6 +63,9 @@ public class RequiredActionResetPasswordTest {
@Rule
public WebRule webRule = new WebRule(this);
@Rule
public AssertEvents events = new AssertEvents(keycloakRule);
@Rule
public GreenMailRule greenMail = new GreenMailRule();
@ -88,12 +92,20 @@ public class RequiredActionResetPasswordTest {
changePasswordPage.assertCurrent();
changePasswordPage.changePassword("new-password", "new-password");
events.expectRequiredAction("update_password").assertEvent();
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
events.expectLogin().assertEvent();
oauth.openLogout();
events.expectLogout().assertEvent();
loginPage.open();
loginPage.login("test-user@localhost", "new-password");
events.expectLogin().assertEvent();
}
}

View file

@ -25,10 +25,12 @@ import org.junit.Assert;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.audit.Details;
import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.TimeBasedOTP;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.OAuthClient;
import org.keycloak.testsuite.pages.AccountTotpPage;
import org.keycloak.testsuite.pages.AppPage;
@ -59,6 +61,9 @@ public class RequiredActionTotpSetupTest {
});
@Rule
public AssertEvents events = new AssertEvents(keycloakRule);
@Rule
public WebRule webRule = new WebRule(this);
@ -94,11 +99,17 @@ public class RequiredActionTotpSetupTest {
loginPage.clickRegister();
registerPage.register("firstName", "lastName", "email", "setupTotp", "password", "password");
String userId = events.expectRegister("setupTotp", "email").assertEvent().getUserId();
totpPage.assertCurrent();
totpPage.configure(totp.generate(totpPage.getTotpSecret()));
events.expectRequiredAction("update_totp").user(userId).detail(Details.USERNAME, "setupTotp").assertEvent();
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
events.expectLogin().user(userId).detail(Details.USERNAME, "setupTotp").assertEvent();
}
@Test
@ -112,15 +123,23 @@ public class RequiredActionTotpSetupTest {
totpPage.configure(totp.generate(totpSecret));
events.expectRequiredAction("update_totp").assertEvent();
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
events.expectLogin().assertEvent();
oauth.openLogout();
events.expectLogout().assertEvent();
loginPage.open();
loginPage.login("test-user@localhost", "password");
loginTotpPage.login(totp.generate(totpSecret));
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
events.expectLogin().assertEvent();
}
@Test
@ -130,6 +149,8 @@ public class RequiredActionTotpSetupTest {
loginPage.clickRegister();
registerPage.register("firstName2", "lastName2", "email2", "setupTotp2", "password2", "password2");
String userId = events.expectRegister("setupTotp2", "email2").assertEvent().getUserId();
// Configure totp
totpPage.assertCurrent();
@ -139,8 +160,13 @@ public class RequiredActionTotpSetupTest {
// After totp config, user should be on the app page
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
events.expectRequiredAction("update_totp").user(userId).detail(Details.USERNAME, "setupTotp2").assertEvent();
events.expectLogin().user(userId).detail(Details.USERNAME, "setupTotp2").assertEvent();
// Logout
oauth.openLogout();
events.expectLogout().user(userId).assertEvent();
// Try to login after logout
loginPage.open();
@ -153,15 +179,24 @@ public class RequiredActionTotpSetupTest {
// Login with one-time password
loginTotpPage.login(totp.generate(totpCode));
events.expectLogin().user(userId).detail(Details.USERNAME, "setupTotp2").assertEvent();
// Open account page
accountTotpPage.open();
accountTotpPage.assertCurrent();
events.expectLogin().user(userId).detail(Details.AUTH_METHOD, "sso").client("account")
.detail(Details.REDIRECT_URI, "http://localhost:8081/auth/rest/realms/test/account/login-redirect?path=totp")
.removeDetail(Details.USERNAME).assertEvent();
// Remove google authentificator
accountTotpPage.removeTotp();
events.expectAccount("remove_totp").user(userId).assertEvent();
// Logout
oauth.openLogout();
events.expectLogout().user(userId).assertEvent();
// Try to login
loginPage.open();
@ -171,7 +206,11 @@ public class RequiredActionTotpSetupTest {
totpPage.assertCurrent();
totpPage.configure(totp.generate(totpPage.getTotpSecret()));
events.expectRequiredAction("update_totp").user(userId).detail(Details.USERNAME, "setupTotp2").assertEvent();
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
events.expectLogin().user(userId).detail(Details.USERNAME, "setupTotp2").assertEvent();
}
}

View file

@ -22,18 +22,20 @@
package org.keycloak.testsuite.actions;
import org.junit.Assert;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.audit.Details;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserModel.RequiredAction;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.AppPage.RequestType;
import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.pages.LoginUpdateProfilePage;
import org.keycloak.testsuite.rule.KeycloakRule;
import org.keycloak.testsuite.rule.KeycloakRule.KeycloakSetup;
import org.keycloak.testsuite.rule.WebResource;
import org.keycloak.testsuite.rule.WebRule;
import org.openqa.selenium.WebDriver;
@ -43,20 +45,15 @@ import org.openqa.selenium.WebDriver;
*/
public class RequiredActionUpdateProfileTest {
@Rule
public KeycloakRule keycloakRule = new KeycloakRule(new KeycloakSetup() {
@Override
public void config(RealmManager manager, RealmModel defaultRealm, RealmModel appRealm) {
UserModel user = appRealm.getUser("test-user@localhost");
user.addRequiredAction(RequiredAction.UPDATE_PROFILE);
}
});
@ClassRule
public static KeycloakRule keycloakRule = new KeycloakRule();
@Rule
public WebRule webRule = new WebRule(this);
@Rule
public AssertEvents events = new AssertEvents(keycloakRule);
@WebResource
protected WebDriver driver;
@ -69,6 +66,17 @@ public class RequiredActionUpdateProfileTest {
@WebResource
protected LoginUpdateProfilePage updateProfilePage;
@Before
public void before() {
keycloakRule.configure(new KeycloakRule.KeycloakSetup() {
@Override
public void config(RealmManager manager, RealmModel defaultRealm, RealmModel appRealm) {
UserModel user = appRealm.getUser("test-user@localhost");
user.addRequiredAction(UserModel.RequiredAction.UPDATE_PROFILE);
}
});
}
@Test
public void updateProfile() {
loginPage.open();
@ -79,7 +87,12 @@ public class RequiredActionUpdateProfileTest {
updateProfilePage.update("New first", "New last", "new@email.com");
events.expectRequiredAction("update_profile").assertEvent();
events.expectRequiredAction("update_email").detail(Details.PREVIOUS_EMAIL, "test-user@localhost").detail(Details.UPDATED_EMAIL, "new@email.com").assertEvent();
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
events.expectLogin().assertEvent();
}
@Test
@ -95,6 +108,8 @@ public class RequiredActionUpdateProfileTest {
updateProfilePage.assertCurrent();
Assert.assertEquals("Please specify first name", updateProfilePage.getError());
events.assertEmpty();
}
@Test
@ -110,6 +125,8 @@ public class RequiredActionUpdateProfileTest {
updateProfilePage.assertCurrent();
Assert.assertEquals("Please specify last name", updateProfilePage.getError());
events.assertEmpty();
}
@Test
@ -125,7 +142,8 @@ public class RequiredActionUpdateProfileTest {
updateProfilePage.assertCurrent();
Assert.assertEquals("Please specify email", updateProfilePage.getError());
events.assertEmpty();
}
}

View file

@ -1,10 +1,5 @@
package org.keycloak.testsuite.forms;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.junit.Assert;
import org.junit.ClassRule;
import org.junit.FixMethodOrder;
@ -34,6 +29,11 @@ import org.keycloak.testsuite.rule.WebResource;
import org.keycloak.testsuite.rule.WebRule;
import org.openqa.selenium.WebDriver;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/

View file

@ -26,11 +26,13 @@ import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.audit.Details;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.OAuthClient;
import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.AppPage.RequestType;
@ -53,6 +55,8 @@ public class LoginTest {
user.setEmail("login@test.com");
user.setEnabled(true);
userId = user.getId();
UserCredentialModel creds = new UserCredentialModel();
creds.setType(CredentialRepresentation.PASSWORD);
creds.setValue("password");
@ -61,6 +65,9 @@ public class LoginTest {
}
});
@Rule
public AssertEvents events = new AssertEvents(keycloakRule);
@Rule
public WebRule webRule = new WebRule(this);
@ -76,6 +83,8 @@ public class LoginTest {
@WebResource
protected LoginPage loginPage;
private static String userId;
@Test
public void loginInvalidPassword() {
loginPage.open();
@ -84,6 +93,8 @@ public class LoginTest {
loginPage.assertCurrent();
Assert.assertEquals("Invalid username or password.", loginPage.getError());
events.expectLogin().user((String) null).error("invalid_user_credentials").detail(Details.USERNAME, "login-test").removeDetail(Details.CODE_ID).assertEvent();
}
@Test
@ -94,6 +105,8 @@ public class LoginTest {
loginPage.assertCurrent();
Assert.assertEquals("Invalid username or password.", loginPage.getError());
events.expectLogin().user((String) null).error("user_not_found").detail(Details.USERNAME, "invalid").removeDetail(Details.CODE_ID).assertEvent();
}
@Test
@ -103,6 +116,8 @@ public class LoginTest {
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
events.expectLogin().user(userId).detail(Details.USERNAME, "login-test").assertEvent();
}
@Test
@ -112,6 +127,8 @@ public class LoginTest {
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
events.expectLogin().user(userId).detail(Details.USERNAME, "login@test.com").assertEvent();
}
@Test
@ -120,8 +137,9 @@ public class LoginTest {
loginPage.cancel();
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
Assert.assertEquals("access_denied", oauth.getCurrentQuery().get(OAuth2Constants.ERROR));
events.expectLogin().error("rejected_by_user").user((String) null).removeDetail(Details.USERNAME).removeDetail(Details.CODE_ID).assertEvent();
}
}

View file

@ -26,12 +26,14 @@ import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.audit.Details;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.TimeBasedOTP;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.AppPage.RequestType;
import org.keycloak.testsuite.pages.LoginPage;
@ -44,6 +46,7 @@ import org.keycloak.testsuite.rule.WebRule;
import org.openqa.selenium.WebDriver;
import java.net.MalformedURLException;
import java.util.Collections;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -63,10 +66,14 @@ public class LoginTotpTest {
appRealm.updateCredential(user, credentials);
user.setTotp(true);
appRealm.setAuditListeners(Collections.singleton("dummy"));
}
});
@Rule
public AssertEvents events = new AssertEvents(keycloakRule);
@Rule
public WebRule webRule = new WebRule(this);
@ -83,7 +90,7 @@ public class LoginTotpTest {
protected LoginPage loginPage;
@WebResource
private LoginTotpPage loginTotpPage;
protected LoginTotpPage loginTotpPage;
private TimeBasedOTP totp = new TimeBasedOTP();
@ -103,6 +110,8 @@ public class LoginTotpTest {
loginPage.assertCurrent();
Assert.assertEquals("Invalid username or password.", loginPage.getError());
events.expectLogin().error("invalid_user_credentials").removeDetail(Details.CODE_ID).user((String) null).assertEvent();
}
@Test
@ -115,6 +124,8 @@ public class LoginTotpTest {
loginTotpPage.login(totp.generate("totpSecret"));
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
events.expectLogin().assertEvent();
}
}

View file

@ -25,9 +25,12 @@ import org.junit.Assert;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.audit.Details;
import org.keycloak.models.PasswordPolicy;
import org.keycloak.models.RealmModel;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.OAuthClient;
import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.AppPage.RequestType;
import org.keycloak.testsuite.pages.LoginPage;
@ -45,6 +48,9 @@ public class RegisterTest {
@ClassRule
public static KeycloakRule keycloakRule = new KeycloakRule();
@Rule
public AssertEvents events = new AssertEvents(keycloakRule);
@Rule
public WebRule webRule = new WebRule(this);
@ -60,6 +66,9 @@ public class RegisterTest {
@WebResource
protected RegisterPage registerPage;
@WebResource
protected OAuthClient oauth;
@Test
public void registerExistingUser() {
loginPage.open();
@ -70,6 +79,8 @@ public class RegisterTest {
registerPage.assertCurrent();
Assert.assertEquals("Username already exists", registerPage.getError());
events.expectRegister("test-user@localhost", "email").user((String) null).error("username_in_use").assertEvent();
}
@Test
@ -82,6 +93,8 @@ public class RegisterTest {
registerPage.assertCurrent();
Assert.assertEquals("Password confirmation doesn't match", registerPage.getError());
events.expectRegister("registerUserInvalidPasswordConfirm", "email").user((String) null).error("invalid_registration").assertEvent();
}
@Test
@ -94,6 +107,8 @@ public class RegisterTest {
registerPage.assertCurrent();
Assert.assertEquals("Please specify password.", registerPage.getError());
events.expectRegister("registerUserMissingPassword", "email").user((String) null).error("invalid_registration").assertEvent();
}
@Test
@ -115,8 +130,14 @@ public class RegisterTest {
registerPage.assertCurrent();
Assert.assertEquals("Invalid password: minimum length 8", registerPage.getError());
events.expectRegister("registerPasswordPolicy", "email").user((String) null).error("invalid_registration").assertEvent();
registerPage.register("firstName", "lastName", "email", "registerPasswordPolicy", "password", "password");
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
String userId = events.expectRegister("registerPasswordPolicy", "email").assertEvent().getUserId();
events.expectLogin().user(userId).detail(Details.USERNAME, "registerPasswordPolicy").assertEvent();
} finally {
keycloakRule.configure(new KeycloakRule.KeycloakSetup() {
@Override
@ -137,6 +158,8 @@ public class RegisterTest {
registerPage.assertCurrent();
Assert.assertEquals("Please specify username", registerPage.getError());
events.expectRegister(null, "email").removeDetail("username").error("invalid_registration").assertEvent();
}
@Test
@ -148,6 +171,9 @@ public class RegisterTest {
registerPage.register("firstName", "lastName", "email", "registerUserSuccess", "password", "password");
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
String userId = events.expectRegister("registerUserSuccess", "email").assertEvent().getUserId();
events.expectLogin().detail("username", "registerUserSuccess").user(userId).assertEvent();
}
}

View file

@ -25,12 +25,14 @@ import org.junit.Assert;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.audit.Details;
import org.keycloak.models.PasswordPolicy;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.OAuthClient;
import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.AppPage.RequestType;
@ -46,6 +48,7 @@ import org.openqa.selenium.WebDriver;
import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import java.io.IOException;
import java.util.Collections;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -60,14 +63,19 @@ public class ResetPasswordTest {
user.setEmail("login@test.com");
user.setEnabled(true);
userId = user.getId();
UserCredentialModel creds = new UserCredentialModel();
creds.setType(CredentialRepresentation.PASSWORD);
creds.setValue("password");
appRealm.updateCredential(user, creds);
appRealm.setAuditListeners(Collections.singleton("dummy"));
}
}));
private static String userId;
@Rule
public WebRule webRule = new WebRule(this);
@ -92,17 +100,31 @@ public class ResetPasswordTest {
@WebResource
protected LoginPasswordUpdatePage updatePasswordPage;
@Rule
public AssertEvents events = new AssertEvents(keycloakRule);
@Test
public void resetPassword() throws IOException, MessagingException {
resetPassword("login-test");
}
@Test
public void resetPasswordByEmail() throws IOException, MessagingException {
resetPassword("login@test.com");
}
private void resetPassword(String username) throws IOException, MessagingException {
loginPage.open();
loginPage.resetPassword();
resetPasswordPage.assertCurrent();
resetPasswordPage.changePassword("login-test");
resetPasswordPage.changePassword(username);
resetPasswordPage.assertCurrent();
events.expectRequiredAction("send_reset_password").user(userId).detail(Details.USERNAME, username).detail(Details.EMAIL, "login@test.com").assertEvent();
Assert.assertEquals("You should receive an email shortly with further instructions.", resetPasswordPage.getSuccessMessage());
Assert.assertEquals(1, greenMail.getReceivedMessages().length);
@ -118,50 +140,21 @@ public class ResetPasswordTest {
updatePasswordPage.changePassword("resetPassword", "resetPassword");
events.expectRequiredAction("update_password").user(userId).detail(Details.USERNAME, username).assertEvent();
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
events.expectLogin().user(userId).detail(Details.USERNAME, username).assertEvent();
oauth.openLogout();
events.expectLogout().user(userId).assertEvent();
loginPage.open();
loginPage.login("login-test", "resetPassword");
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
}
@Test
public void resetPasswordByEmail() throws IOException, MessagingException {
loginPage.open();
loginPage.resetPassword();
resetPasswordPage.assertCurrent();
resetPasswordPage.changePassword("login@test.com");
resetPasswordPage.assertCurrent();
Assert.assertEquals("You should receive an email shortly with further instructions.", resetPasswordPage.getSuccessMessage());
Assert.assertEquals(1, greenMail.getReceivedMessages().length);
MimeMessage message = greenMail.getReceivedMessages()[0];
String body = (String) message.getContent();
String changePasswordUrl = body.split("\n")[3];
driver.navigate().to(changePasswordUrl.trim());
updatePasswordPage.assertCurrent();
updatePasswordPage.changePassword("resetPassword", "resetPassword");
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
oauth.openLogout();
loginPage.open();
loginPage.login("login@test.com", "resetPassword");
events.expectLogin().user(userId).detail(Details.USERNAME, "login-test").assertEvent();
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
}
@ -182,6 +175,8 @@ public class ResetPasswordTest {
Thread.sleep(1000);
Assert.assertEquals(0, greenMail.getReceivedMessages().length);
events.expectRequiredAction("send_reset_password").user((String) null).detail(Details.USERNAME, "invalid").removeDetail(Details.EMAIL).removeDetail(Details.CODE_ID).error("user_not_found").assertEvent();
}
@Test
@ -211,6 +206,8 @@ public class ResetPasswordTest {
String body = (String) message.getContent();
String changePasswordUrl = body.split("\n")[3];
events.expectRequiredAction("send_reset_password").user(userId).detail(Details.USERNAME, "login-test").detail(Details.EMAIL, "login@test.com").assertEvent();
driver.navigate().to(changePasswordUrl.trim());
updatePasswordPage.assertCurrent();
@ -221,14 +218,23 @@ public class ResetPasswordTest {
updatePasswordPage.changePassword("resetPasswordWithPasswordPolicy", "resetPasswordWithPasswordPolicy");
events.expectRequiredAction("update_password").user(userId).detail(Details.USERNAME, "login-test").assertEvent();
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
events.expectLogin().user(userId).detail(Details.USERNAME, "login-test").assertEvent();
oauth.openLogout();
events.expectLogout().user(userId).assertEvent();
loginPage.open();
loginPage.login("login-test", "resetPasswordWithPasswordPolicy");
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
events.expectLogin().user(userId).detail(Details.USERNAME, "login-test").assertEvent();
}
}

View file

@ -26,6 +26,8 @@ import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.audit.Details;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.OAuthClient;
import org.keycloak.testsuite.pages.AccountUpdateProfilePage;
import org.keycloak.testsuite.pages.AppPage;
@ -63,6 +65,9 @@ public class SSOTest {
@WebResource
protected AccountUpdateProfilePage profilePage;
@Rule
public AssertEvents events = new AssertEvents(keycloakRule);
@Test
public void loginSuccess() {
loginPage.open();
@ -71,6 +76,8 @@ public class SSOTest {
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
events.expectLogin().assertEvent();
appPage.open();
oauth.openLoginForm();
@ -80,6 +87,9 @@ public class SSOTest {
profilePage.open();
Assert.assertTrue(profilePage.isCurrent());
events.expectLogin().detail(Details.AUTH_METHOD, "sso").removeDetail(Details.USERNAME).client("test-app").assertEvent();
events.expectLogin().detail(Details.AUTH_METHOD, "sso").removeDetail(Details.USERNAME).client("account").detail(Details.REDIRECT_URI, "http://localhost:8081/auth/rest/realms/test/account/login-redirect").assertEvent();
}
}

View file

@ -26,7 +26,10 @@ import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.audit.Details;
import org.keycloak.audit.Event;
import org.keycloak.representations.AccessToken;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.OAuthClient;
import org.keycloak.testsuite.OAuthClient.AccessTokenResponse;
import org.keycloak.testsuite.pages.LoginPage;
@ -59,10 +62,15 @@ public class AccessTokenTest {
@WebResource
protected LoginPage loginPage;
@Rule
public AssertEvents events = new AssertEvents(keycloakRule);
@Test
public void accessTokenRequest() throws Exception {
oauth.doLogin("test-user@localhost", "password");
String codeId = events.expectLogin().assertEvent().getDetails().get(Details.CODE_ID);
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
AccessTokenResponse response = oauth.doAccessTokenRequest(code, "password");
@ -82,6 +90,28 @@ public class AccessTokenTest {
Assert.assertEquals(1, token.getResourceAccess(oauth.getClientId()).getRoles().size());
Assert.assertTrue(token.getResourceAccess(oauth.getClientId()).isUserInRole("customer-user"));
Event event = events.expectCodeToToken(codeId).assertEvent();
Assert.assertEquals(token.getId(), event.getDetails().get(Details.TOKEN_ID));
Assert.assertEquals(oauth.verifyRefreshToken(response.getRefreshToken()).getId(), event.getDetails().get(Details.REFRESH_TOKEN_ID));
response = oauth.doAccessTokenRequest(code, "password");
Assert.assertEquals(400, response.getStatusCode());
events.expectCodeToToken(codeId).error("invalid_code").removeDetail(Details.TOKEN_ID).removeDetail(Details.REFRESH_TOKEN_ID).client((String) null).user((String) null).assertEvent();
}
@Test
public void accessTokenInvalidClientCredentials() throws Exception {
oauth.doLogin("test-user@localhost", "password");
String codeId = events.expectLogin().assertEvent().getDetails().get(Details.CODE_ID);
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
AccessTokenResponse response = oauth.doAccessTokenRequest(code, "invalid");
Assert.assertEquals(400, response.getStatusCode());
events.expectCodeToToken(codeId).error("invalid_client_credentials").removeDetail(Details.TOKEN_ID).removeDetail(Details.REFRESH_TOKEN_ID).assertEvent();
}
}

View file

@ -26,13 +26,14 @@ import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.models.ApplicationModel;
import org.keycloak.audit.Details;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.models.Constants;
import org.keycloak.models.RealmModel;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.OAuthClient;
import org.keycloak.testsuite.OAuthClient.AuthorizationCodeResponse;
import org.keycloak.testsuite.pages.ErrorPage;
import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.rule.KeycloakRule;
import org.keycloak.testsuite.rule.WebResource;
@ -62,8 +63,8 @@ public class AuthorizationCodeTest {
@WebResource
protected LoginPage loginPage;
@WebResource
protected ErrorPage errorPage;
@Rule
public AssertEvents events = new AssertEvents(keycloakRule);
@Test
public void authorizationRequest() throws IOException {
@ -77,6 +78,9 @@ public class AuthorizationCodeTest {
Assert.assertNull(response.getError());
oauth.verifyCode(response.getCode());
String codeId = events.expectLogin().assertEvent().getDetails().get(Details.CODE_ID);
Assert.assertEquals(codeId, new JWSInput(response.getCode()).readContentAsString());
}
@Test
@ -90,6 +94,9 @@ public class AuthorizationCodeTest {
String code = driver.findElement(By.id(OAuth2Constants.CODE)).getText();
oauth.verifyCode(code);
String codeId = events.expectLogin().detail(Details.REDIRECT_URI, Constants.INSTALLED_APP_URN).assertEvent().getDetails().get(Details.CODE_ID);
Assert.assertEquals(codeId, new JWSInput(code).readContentAsString());
}
@Test
@ -109,6 +116,9 @@ public class AuthorizationCodeTest {
Assert.assertNotNull(response.getCode());
oauth.verifyCode(response.getCode());
String codeId = events.expectLogin().assertEvent().getDetails().get(Details.CODE_ID);
Assert.assertEquals(codeId, new JWSInput(response.getCode()).readContentAsString());
}
@Test
@ -121,6 +131,9 @@ public class AuthorizationCodeTest {
Assert.assertNull(response.getError());
oauth.verifyCode(response.getCode());
String codeId = events.expectLogin().assertEvent().getDetails().get(Details.CODE_ID);
Assert.assertEquals(codeId, new JWSInput(response.getCode()).readContentAsString());
}
}

View file

@ -21,15 +21,14 @@
*/
package org.keycloak.testsuite.oauth;
import java.io.IOException;
import java.util.Map;
import org.junit.Assert;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.audit.Details;
import org.keycloak.representations.AccessToken;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.OAuthClient;
import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.pages.OAuthGrantPage;
@ -38,6 +37,9 @@ import org.keycloak.testsuite.rule.WebResource;
import org.keycloak.testsuite.rule.WebRule;
import org.openqa.selenium.WebDriver;
import java.io.IOException;
import java.util.Map;
/**
* @author <a href="mailto:vrockai@redhat.com">Viliam Rockai</a>
*/
@ -46,6 +48,9 @@ public class OAuthGrantTest {
@ClassRule
public static KeycloakRule keycloakRule = new KeycloakRule();
@Rule
public AssertEvents events = new AssertEvents(keycloakRule);
@Rule
public WebRule webRule = new WebRule(this);
@ -76,6 +81,9 @@ public class OAuthGrantTest {
grantPage.accept();
Assert.assertTrue(oauth.getCurrentQuery().containsKey(OAuth2Constants.CODE));
String codeId = events.expectLogin().client("third-party").assertEvent().getDetails().get(Details.CODE_ID);
OAuthClient.AccessTokenResponse accessToken = oauth.doAccessTokenRequest(oauth.getCurrentQuery().get(OAuth2Constants.CODE), "password");
AccessToken token = oauth.verifyToken(accessToken.getAccessToken());
@ -88,6 +96,8 @@ public class OAuthGrantTest {
Assert.assertEquals(1, resourceAccess.size());
Assert.assertEquals(1, resourceAccess.get("test-app").getRoles().size());
Assert.assertTrue(resourceAccess.get("test-app").isUserInRole("customer-user"));
events.expectCodeToToken(codeId).client("third-party").assertEvent();
}
@Test
@ -103,5 +113,8 @@ public class OAuthGrantTest {
Assert.assertTrue(oauth.getCurrentQuery().containsKey(OAuth2Constants.ERROR));
Assert.assertEquals("access_denied", oauth.getCurrentQuery().get(OAuth2Constants.ERROR));
events.expectLogin().client("third-party").error("rejected_by_user").assertEvent();
}
}

View file

@ -26,8 +26,11 @@ import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.audit.Details;
import org.keycloak.audit.Event;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.RefreshToken;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.OAuthClient;
import org.keycloak.testsuite.OAuthClient.AccessTokenResponse;
import org.keycloak.testsuite.pages.LoginPage;
@ -61,10 +64,15 @@ public class RefreshTokenTest {
@WebResource
protected LoginPage loginPage;
@Rule
public AssertEvents events = new AssertEvents(keycloakRule);
@Test
public void refreshTokenRequest() throws Exception {
oauth.doLogin("test-user@localhost", "password");
String codeId = events.expectLogin().assertEvent().getDetails().get(Details.CODE_ID);
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
@ -72,6 +80,8 @@ public class RefreshTokenTest {
String refreshTokenString = tokenResponse.getRefreshToken();
RefreshToken refreshToken = oauth.verifyRefreshToken(refreshTokenString);
Event tokenEvent = events.expectCodeToToken(codeId).assertEvent();
Assert.assertNotNull(refreshTokenString);
Assert.assertEquals("bearer", tokenResponse.getTokenType());
@ -106,6 +116,10 @@ public class RefreshTokenTest {
Assert.assertEquals(1, refreshedToken.getResourceAccess(oauth.getClientId()).getRoles().size());
Assert.assertTrue(refreshedToken.getResourceAccess(oauth.getClientId()).isUserInRole("customer-user"));
Event refreshEvent = events.expectRefresh(tokenEvent.getDetails().get(Details.REFRESH_TOKEN_ID)).assertEvent();
Assert.assertNotEquals(tokenEvent.getDetails().get(Details.TOKEN_ID), refreshEvent.getDetails().get(Details.TOKEN_ID));
Assert.assertNotEquals(tokenEvent.getDetails().get(Details.REFRESH_TOKEN_ID), refreshEvent.getDetails().get(Details.UPDATED_REFRESH_TOKEN_ID));
}
}

View file

@ -7,9 +7,9 @@ import io.undertow.servlet.api.ServletInfo;
import io.undertow.servlet.api.WebResourceCollection;
import org.junit.rules.ExternalResource;
import org.keycloak.models.Config;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.services.managers.ModelToRepresentation;
@ -40,7 +40,8 @@ public abstract class AbstractKeycloakRule extends ExternalResource {
public UserRepresentation getUser(String realm, String name) {
KeycloakSession session = server.getKeycloakSessionFactory().createSession();
try {
return ModelToRepresentation.toRepresentation(session.getRealmByName(realm).getUser(name));
UserModel user = session.getRealmByName(realm).getUser(name);
return user != null ? ModelToRepresentation.toRepresentation(user) : null;
} finally {
session.close();
}

View file

@ -22,12 +22,8 @@
package org.keycloak.testsuite.rule;
import org.keycloak.models.Config;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.services.managers.ModelToRepresentation;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.testsuite.ApplicationServlet;

View file

@ -27,13 +27,12 @@ import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.models.AccountRoles;
import org.keycloak.models.ApplicationModel;
import org.keycloak.models.Constants;
import org.keycloak.audit.Details;
import org.keycloak.models.RealmModel;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.DummySocialServlet;
import org.keycloak.testsuite.OAuthClient;
import org.keycloak.testsuite.OAuthClient.AccessTokenResponse;
@ -87,6 +86,9 @@ public class SocialLoginTest {
@WebResource
protected OAuthClient oauth;
@Rule
public AssertEvents events = new AssertEvents(keycloakRule);
@BeforeClass
public static void before() {
keycloakRule.deployServlet("dummy-social", "/dummy-social", DummySocialServlet.class);
@ -107,8 +109,21 @@ public class SocialLoginTest {
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
String userId = events.expect("register")
.user(AssertEvents.isUUID())
.detail(Details.EMAIL, "bob@builder.com")
.detail(Details.RESPONSE_TYPE, "code")
.detail(Details.REGISTER_METHOD, "social")
.detail(Details.REDIRECT_URI, AssertEvents.DEFAULT_REDIRECT_URI)
.detail(Details.USERNAME, "1@dummy")
.assertEvent().getUserId();
String codeId = events.expectLogin().user(userId).detail(Details.USERNAME, "1@dummy").detail(Details.AUTH_METHOD, "social").assertEvent().getDetails().get(Details.CODE_ID);
AccessTokenResponse response = oauth.doAccessTokenRequest(oauth.getCurrentQuery().get(OAuth2Constants.CODE), "password");
events.expectCodeToToken(codeId).user(userId).assertEvent();
AccessToken token = oauth.verifyToken(response.getAccessToken());
Assert.assertEquals(36, token.getSubject().length());
@ -118,8 +133,21 @@ public class SocialLoginTest {
Assert.assertEquals("Bob", profile.getFirstName());
Assert.assertEquals("Builder", profile.getLastName());
Assert.assertEquals("bob@builder.com", profile.getEmail());
}
oauth.openLogout();
events.expectLogout().user(userId).assertEvent();
loginPage.open();
loginPage.clickSocial("dummy");
driver.findElement(By.id("id")).sendKeys("1");
driver.findElement(By.id("username")).sendKeys("dummy-user1");
driver.findElement(By.id("login")).click();
events.expectLogin().user(userId).detail(Details.USERNAME, "1@dummy").detail(Details.AUTH_METHOD, "social").assertEvent();
}
@Test
public void loginCancelled() throws Exception {
@ -132,9 +160,13 @@ public class SocialLoginTest {
Assert.assertTrue(loginPage.isCurrent());
Assert.assertEquals("Access denied", loginPage.getWarning());
events.expectLogin().error("rejected_by_user").user((String) null).detail(Details.AUTH_METHOD, "social").removeDetail(Details.USERNAME).removeDetail(Details.CODE_ID).assertEvent();
loginPage.login("test-user@localhost", "password");
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
events.expectLogin().assertEvent();
}
@Test
@ -164,13 +196,29 @@ public class SocialLoginTest {
Assert.assertEquals("Builder", profilePage.getLastName());
Assert.assertEquals("bob@builder.com", profilePage.getEmail());
String userId = events.expect("register")
.user(AssertEvents.isUUID())
.detail(Details.EMAIL, "bob@builder.com")
.detail(Details.RESPONSE_TYPE, "code")
.detail(Details.REGISTER_METHOD, "social")
.detail(Details.REDIRECT_URI, AssertEvents.DEFAULT_REDIRECT_URI)
.detail(Details.USERNAME, "2@dummy")
.assertEvent().getUserId();
profilePage.update("Dummy", "User", "dummy-user-reg@dummy-social");
events.expectRequiredAction("update_profile").user(userId).detail(Details.AUTH_METHOD, "social").detail(Details.USERNAME, "2@dummy").assertEvent();
events.expectRequiredAction("update_email").user(userId).detail(Details.AUTH_METHOD, "social").detail(Details.USERNAME, "2@dummy").detail(Details.PREVIOUS_EMAIL, "bob@builder.com").detail(Details.UPDATED_EMAIL, "dummy-user-reg@dummy-social").assertEvent();
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
String codeId = events.expectLogin().user(userId).removeDetail(Details.USERNAME).detail(Details.AUTH_METHOD, "social").detail(Details.USERNAME, "2@dummy").assertEvent().getDetails().get(Details.CODE_ID);
AccessTokenResponse response = oauth.doAccessTokenRequest(oauth.getCurrentQuery().get(OAuth2Constants.CODE), "password");
AccessToken token = oauth.verifyToken(response.getAccessToken());
events.expectCodeToToken(codeId).user(userId).assertEvent();
UserRepresentation profile = keycloakRule.getUserById("test", token.getSubject());
Assert.assertEquals("Dummy", profile.getFirstName());

View file

@ -0,0 +1 @@
org.keycloak.testsuite.AssertEvents