keycloak-uncached
Changes
forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js 28(+26 -2)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-detail.html 10(+10 -0)
model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java 24(+23 -1)
model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/UserLoginFailureMapper.java 53(+53 -0)
model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/JpaUserSessionProvider.java 12(+12 -0)
model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/MemUserSessionProvider.java 14(+12 -2)
model/sessions-mongo/src/main/java/org/keycloak/models/sessions/mongo/MongoUserSessionProvider.java 19(+19 -0)
services/src/main/java/org/keycloak/services/resources/admin/AttackDetectionResource.java 158(+158 -0)
Details
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js
index f7968a0..f3fe77f 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js
@@ -216,7 +216,7 @@ module.controller('UserConsentsCtrl', function($scope, realm, user, userConsents
});
-module.controller('UserListCtrl', function($scope, realm, User, UserImpersonation) {
+module.controller('UserListCtrl', function($scope, realm, User, UserImpersonation, BruteForce, Notifications) {
$scope.realm = realm;
$scope.page = 0;
@@ -236,6 +236,13 @@ module.controller('UserListCtrl', function($scope, realm, User, UserImpersonatio
});
};
+ $scope.unlockUsers = function() {
+ BruteForce.delete({realm: realm.realm}, function(data) {
+ Notifications.success("Any temporarily locked users are now unlocked.");
+ });
+ }
+
+
$scope.firstPage = function() {
$scope.query.first = 0;
$scope.searchQuery();
@@ -282,7 +289,7 @@ module.controller('UserTabCtrl', function($scope, $location, Dialog, Notificatio
};
});
-module.controller('UserDetailCtrl', function($scope, realm, user, User, UserFederationInstances, UserImpersonation, RequiredActions, $location, Dialog, Notifications) {
+module.controller('UserDetailCtrl', function($scope, realm, user, BruteForceUser, User, UserFederationInstances, UserImpersonation, RequiredActions, $location, Dialog, Notifications) {
$scope.realm = realm;
$scope.create = !user.id;
$scope.editUsername = $scope.create || $scope.realm.editUsernameAllowed;
@@ -315,6 +322,23 @@ module.controller('UserDetailCtrl', function($scope, realm, user, User, UserFede
} else {
console.log("federationLink is null");
}
+ console.log('realm brute force? ' + realm.bruteForceProtected)
+ $scope.temporarilyDisabled = false;
+ var isDisabled = function () {
+ BruteForceUser.get({realm: realm.realm, username: user.username}, function(data) {
+ console.log('here in isDisabled ' + data.disabled);
+ $scope.temporarilyDisabled = data.disabled;
+ });
+ };
+
+ console.log("check if disabled");
+ isDisabled();
+
+ $scope.unlockUser = function() {
+ BruteForceUser.delete({realm: realm.realm, username: user.username}, function(data) {
+ isDisabled();
+ });
+ }
}
$scope.changed = false; // $scope.create;
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js
index 5e9fefd..1d6a6be 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js
@@ -186,6 +186,20 @@ module.factory('RealmAdminEvents', function($resource) {
});
});
+module.factory('BruteForce', function($resource) {
+ return $resource(authUrl + '/admin/realms/:realm/attack-detection/brute-force/usernames', {
+ realm : '@realm'
+ });
+});
+
+module.factory('BruteForceUser', function($resource) {
+ return $resource(authUrl + '/admin/realms/:realm/attack-detection/brute-force/usernames/:username', {
+ realm : '@realm',
+ username : '@username'
+ });
+});
+
+
module.factory('RequiredActions', function($resource) {
return $resource(authUrl + '/admin/realms/:id/authentication/required-actions/:alias', {
realm : '@realm',
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-detail.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-detail.html
index e9da897..5b4ae7f 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-detail.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-detail.html
@@ -66,6 +66,16 @@
</div>
<kc-tooltip>A disabled user cannot login.</kc-tooltip>
</div>
+ <div class="form-group clearfix block" data-ng-show="realm.bruteForceProtected && !create">
+ <label class="col-md-2 control-label" for="temporarilyDisabled">User Temporarily Locked</label>
+ <div class="col-md-1">
+ <input ng-model="temporarilyDisabled" name="temporarilyDisabled" id="temporarilyDisabled" data-ng-readonly="true" data-ng-disabled="true" onoffswitch />
+ </div>
+ <kc-tooltip>The user may have been locked due to failing to login too many times.</kc-tooltip>
+ <div class="col-sm-2">
+ <button type="submit" data-ng-click="unlockUser()" data-ng-show="temporarilyDisabled" class="btn btn-default">Unlock User</button>
+ </div>
+ </div>
<div class="form-group clearfix block" data-ng-show="!create && user.federationLink">
<label class="col-md-2 control-label" for="userEnabled">Federation Link</label>
<div class="col-md-6">
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-list.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-list.html
index 508e07f..0d069e0 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-list.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-list.html
@@ -18,6 +18,7 @@
<button id="viewAllUsers" class="btn btn-default" ng-click="query.search = null; firstPage()">View all users</button>
<div class="pull-right" data-ng-show="access.manageUsers">
+ <button data-ng-click="unlockUsers()" class="btn btn-default">Unlock Users</button>
<a id="createUser" class="btn btn-default" href="#/create/user/{{realm.realm}}">Add User</a>
</div>
</div>
diff --git a/model/api/src/main/java/org/keycloak/models/UserSessionProvider.java b/model/api/src/main/java/org/keycloak/models/UserSessionProvider.java
index 2a874a8..3b4d147 100755
--- a/model/api/src/main/java/org/keycloak/models/UserSessionProvider.java
+++ b/model/api/src/main/java/org/keycloak/models/UserSessionProvider.java
@@ -33,6 +33,8 @@ public interface UserSessionProvider extends Provider {
UsernameLoginFailureModel getUserLoginFailure(RealmModel realm, String username);
UsernameLoginFailureModel addUserLoginFailure(RealmModel realm, String username);
+ void removeUserLoginFailure(RealmModel realm, String username);
+ void removeAllUserLoginFailures(RealmModel realm);
void onRealmRemoved(RealmModel realm);
void onClientRemoved(RealmModel realm, ClientModel client);
diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java
index f865ca3..2202009 100755
--- a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java
+++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java
@@ -20,6 +20,7 @@ import org.keycloak.models.sessions.infinispan.mapreduce.ClientSessionMapper;
import org.keycloak.models.sessions.infinispan.mapreduce.FirstResultReducer;
import org.keycloak.models.sessions.infinispan.mapreduce.LargestResultReducer;
import org.keycloak.models.sessions.infinispan.mapreduce.SessionMapper;
+import org.keycloak.models.sessions.infinispan.mapreduce.UserLoginFailureMapper;
import org.keycloak.models.sessions.infinispan.mapreduce.UserSessionMapper;
import org.keycloak.models.sessions.infinispan.mapreduce.UserSessionNoteMapper;
import org.keycloak.models.utils.KeycloakModelUtils;
@@ -294,8 +295,29 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
}
@Override
+ public void removeUserLoginFailure(RealmModel realm, String username) {
+ LoginFailureKey key = new LoginFailureKey(realm.getId(), username);
+ tx.remove(loginFailureCache, key);
+ }
+
+ @Override
+ public void removeAllUserLoginFailures(RealmModel realm) {
+ Map<LoginFailureKey, Object> sessions = new MapReduceTask(loginFailureCache)
+ .mappedWith(UserLoginFailureMapper.create(realm.getId()).emitKey())
+ .reducedWith(new FirstResultReducer())
+ .execute();
+
+ for (LoginFailureKey id : sessions.keySet()) {
+ tx.remove(loginFailureCache, id);
+ }
+ }
+
+
+
+ @Override
public void onRealmRemoved(RealmModel realm) {
removeUserSessions(realm);
+ removeAllUserLoginFailures(realm);
}
@Override
@@ -474,7 +496,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
}
}
- public void remove(Cache cache, String key) {
+ public void remove(Cache cache, Object key) {
tasks.put(key, new CacheTask(cache, CacheOperation.REMOVE, key, null));
}
diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/UserLoginFailureMapper.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/UserLoginFailureMapper.java
new file mode 100755
index 0000000..766a863
--- /dev/null
+++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/UserLoginFailureMapper.java
@@ -0,0 +1,53 @@
+package org.keycloak.models.sessions.infinispan.mapreduce;
+
+import org.infinispan.distexec.mapreduce.Collector;
+import org.infinispan.distexec.mapreduce.Mapper;
+import org.keycloak.models.sessions.infinispan.entities.LoginFailureEntity;
+import org.keycloak.models.sessions.infinispan.entities.LoginFailureKey;
+import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
+
+import java.io.Serializable;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class UserLoginFailureMapper implements Mapper<LoginFailureKey, LoginFailureEntity, LoginFailureKey, Object>, Serializable {
+
+ public UserLoginFailureMapper(String realm) {
+ this.realm = realm;
+ }
+
+ private enum EmitValue {
+ KEY, ENTITY
+ }
+
+ private String realm;
+
+ private EmitValue emit = EmitValue.ENTITY;
+
+ public static UserLoginFailureMapper create(String realm) {
+ return new UserLoginFailureMapper(realm);
+ }
+
+ public UserLoginFailureMapper emitKey() {
+ emit = EmitValue.KEY;
+ return this;
+ }
+
+ @Override
+ public void map(LoginFailureKey key, LoginFailureEntity e, Collector collector) {
+ if (!realm.equals(e.getRealm())) {
+ return;
+ }
+
+ switch (emit) {
+ case KEY:
+ collector.emit(key, key);
+ break;
+ case ENTITY:
+ collector.emit(key, e);
+ break;
+ }
+ }
+
+}
diff --git a/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/JpaUserSessionProvider.java b/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/JpaUserSessionProvider.java
index 8614ec0..20ac967 100755
--- a/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/JpaUserSessionProvider.java
+++ b/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/JpaUserSessionProvider.java
@@ -93,6 +93,18 @@ public class JpaUserSessionProvider implements UserSessionProvider {
}
@Override
+ public void removeUserLoginFailure(RealmModel realm, String username) {
+ UsernameLoginFailureEntity entity = em.find(UsernameLoginFailureEntity.class, new UsernameLoginFailureEntity.Key(realm.getId(), username));
+ if (entity == null) return;
+ em.remove(entity);
+ }
+
+ @Override
+ public void removeAllUserLoginFailures(RealmModel realm) {
+ em.createNamedQuery("removeLoginFailuresByRealm").setParameter("realmId", realm.getId()).executeUpdate();
+ }
+
+ @Override
public UserSessionModel createUserSession(RealmModel realm, UserModel user, String loginUsername, String ipAddress, String authMethod, boolean rememberMe, String brokerSessionId, String brokerUserId) {
UserSessionEntity entity = new UserSessionEntity();
entity.setId(KeycloakModelUtils.generateId());
diff --git a/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/MemUserSessionProvider.java b/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/MemUserSessionProvider.java
index ac6655f..c32c4db 100755
--- a/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/MemUserSessionProvider.java
+++ b/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/MemUserSessionProvider.java
@@ -323,15 +323,25 @@ public class MemUserSessionProvider implements UserSessionProvider {
}
@Override
- public void onRealmRemoved(RealmModel realm) {
- removeUserSessions(realm);
+ public void removeUserLoginFailure(RealmModel realm, String username) {
+ loginFailures.remove(new UsernameLoginFailureKey(realm.getId(), username));
+ }
+ @Override
+ public void removeAllUserLoginFailures(RealmModel realm) {
Iterator<UsernameLoginFailureEntity> itr = loginFailures.values().iterator();
while (itr.hasNext()) {
if (itr.next().getRealm().equals(realm.getId())) {
itr.remove();
}
}
+
+ }
+
+ @Override
+ public void onRealmRemoved(RealmModel realm) {
+ removeUserSessions(realm);
+ removeAllUserLoginFailures(realm);
}
@Override
diff --git a/model/sessions-mongo/src/main/java/org/keycloak/models/sessions/mongo/MongoUserSessionProvider.java b/model/sessions-mongo/src/main/java/org/keycloak/models/sessions/mongo/MongoUserSessionProvider.java
index 82045fd..d75da9a 100755
--- a/model/sessions-mongo/src/main/java/org/keycloak/models/sessions/mongo/MongoUserSessionProvider.java
+++ b/model/sessions-mongo/src/main/java/org/keycloak/models/sessions/mongo/MongoUserSessionProvider.java
@@ -272,8 +272,27 @@ public class MongoUserSessionProvider implements UserSessionProvider {
}
@Override
+ public void removeUserLoginFailure(RealmModel realm, String username) {
+ DBObject query = new QueryBuilder()
+ .and("username").is(username)
+ .and("realmId").is(realm.getId())
+ .get();
+ mongoStore.removeEntities(MongoUsernameLoginFailureEntity.class, query, false, invocationContext);
+ }
+
+ @Override
+ public void removeAllUserLoginFailures(RealmModel realm) {
+ DBObject query = new QueryBuilder()
+ .and("realmId").is(realm.getId())
+ .get();
+ mongoStore.removeEntities(MongoUsernameLoginFailureEntity.class, query, false, invocationContext);
+
+ }
+
+ @Override
public void onRealmRemoved(RealmModel realm) {
removeUserSessions(realm);
+ removeAllUserLoginFailures(realm);
}
@Override
diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java
index 1508746..fbe37b4 100755
--- a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java
+++ b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java
@@ -596,9 +596,8 @@ public class AuthenticationProcessor {
}
public void validateUser(UserModel authenticatedUser) {
- if (authenticatedUser != null) {
- if (!authenticatedUser.isEnabled()) throw new AuthException(Error.USER_DISABLED);
- }
+ if (authenticatedUser == null) return;
+ if (!authenticatedUser.isEnabled()) throw new AuthException(Error.USER_DISABLED);
if (realm.isBruteForceProtected()) {
if (protector.isTemporarilyDisabled(session, realm, authenticatedUser.getUsername())) {
throw new AuthException(Error.USER_TEMPORARILY_DISABLED);
diff --git a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
index c9ca6de..ea95c14 100755
--- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
@@ -592,121 +592,6 @@ public class AuthenticationManager {
return null;
}
- public AuthenticationStatus authenticateForm(KeycloakSession session, ClientConnection clientConnection, RealmModel realm, MultivaluedMap<String, String> formData) {
- String username = formData.getFirst(FORM_USERNAME);
- if (username == null) {
- logger.debug("Username not provided");
- return AuthenticationStatus.INVALID_USER;
- }
-
- if (realm.isBruteForceProtected()) {
- if (protector.isTemporarilyDisabled(session, realm, username)) {
- return AuthenticationStatus.ACCOUNT_TEMPORARILY_DISABLED;
- }
- }
-
- AuthenticationStatus status = authenticateInternal(session, realm, formData, username);
- if (realm.isBruteForceProtected()) {
- switch (status) {
- case SUCCESS:
- protector.successfulLogin(realm, username, clientConnection);
- break;
- case FAILED:
- case MISSING_TOTP:
- case MISSING_PASSWORD:
- case INVALID_CREDENTIALS:
- protector.failedLogin(realm, username, clientConnection);
- break;
- case INVALID_USER:
- protector.invalidUser(realm, username, clientConnection);
- break;
- default:
- break;
- }
- }
-
- return status;
- }
-
- protected AuthenticationStatus authenticateInternal(KeycloakSession session, RealmModel realm, MultivaluedMap<String, String> formData, String username) {
- UserModel user = KeycloakModelUtils.findUserByNameOrEmail(session, realm, username);
-
- if (user == null) {
- logger.debugv("User {0} not found", username);
- return AuthenticationStatus.INVALID_USER;
- }
-
- Set<String> types = new HashSet<String>();
-
- for (RequiredCredentialModel credential : realm.getRequiredCredentials()) {
- types.add(credential.getType());
- }
-
- if (types.contains(CredentialRepresentation.PASSWORD)) {
- List<UserCredentialModel> credentials = new LinkedList<UserCredentialModel>();
-
- String password = formData.getFirst(CredentialRepresentation.PASSWORD);
- if (password != null) {
- credentials.add(UserCredentialModel.password(password));
- }
-
- String passwordToken = formData.getFirst(CredentialRepresentation.PASSWORD_TOKEN);
- if (passwordToken != null) {
- credentials.add(UserCredentialModel.passwordToken(passwordToken));
- }
-
- String totp = formData.getFirst(CredentialRepresentation.TOTP);
- if (totp != null) {
- credentials.add(UserCredentialModel.totp(totp));
- }
-
- if ((password == null || password.isEmpty()) && (passwordToken == null || passwordToken.isEmpty())) {
- logger.debug("Password not provided");
- return AuthenticationStatus.MISSING_PASSWORD;
- }
-
- logger.debugv("validating password for user: {0}", username);
-
- if (!session.users().validCredentials(realm, user, credentials)) {
- return AuthenticationStatus.INVALID_CREDENTIALS;
- }
-
- if (!user.isEnabled()) {
- return AuthenticationStatus.ACCOUNT_DISABLED;
- }
-
- if (user.isTotp() && totp == null) {
- return AuthenticationStatus.MISSING_TOTP;
- }
-
- if (!user.getRequiredActions().isEmpty()) {
- return AuthenticationStatus.ACTIONS_REQUIRED;
- } else {
- return AuthenticationStatus.SUCCESS;
- }
- } else if (types.contains(CredentialRepresentation.SECRET)) {
- String secret = formData.getFirst(CredentialRepresentation.SECRET);
- if (secret == null) {
- logger.debug("Secret not provided");
- return AuthenticationStatus.MISSING_PASSWORD;
- }
- if (!session.users().validCredentials(realm, user, UserCredentialModel.secret(secret))) {
- return AuthenticationStatus.INVALID_CREDENTIALS;
- }
- if (!user.isEnabled()) {
- return AuthenticationStatus.ACCOUNT_DISABLED;
- }
- if (!user.getRequiredActions().isEmpty()) {
- return AuthenticationStatus.ACTIONS_REQUIRED;
- } else {
- return AuthenticationStatus.SUCCESS;
- }
- } else {
- logger.warn("Do not know how to authenticate user");
- return AuthenticationStatus.FAILED;
- }
- }
-
public enum AuthenticationStatus {
SUCCESS, ACCOUNT_TEMPORARILY_DISABLED, ACCOUNT_DISABLED, ACTIONS_REQUIRED, INVALID_USER, INVALID_CREDENTIALS, MISSING_PASSWORD, MISSING_TOTP, FAILED
}
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/AttackDetectionResource.java b/services/src/main/java/org/keycloak/services/resources/admin/AttackDetectionResource.java
new file mode 100755
index 0000000..38eec9c
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/resources/admin/AttackDetectionResource.java
@@ -0,0 +1,158 @@
+package org.keycloak.services.resources.admin;
+
+import org.jboss.logging.Logger;
+import org.jboss.resteasy.annotations.cache.NoCache;
+import org.jboss.resteasy.spi.BadRequestException;
+import org.jboss.resteasy.spi.NotFoundException;
+import org.jboss.resteasy.spi.ResteasyProviderFactory;
+import org.keycloak.ClientConnection;
+import org.keycloak.events.Event;
+import org.keycloak.events.EventQuery;
+import org.keycloak.events.EventStoreProvider;
+import org.keycloak.events.EventType;
+import org.keycloak.events.admin.AdminEvent;
+import org.keycloak.events.admin.AdminEventQuery;
+import org.keycloak.events.admin.OperationType;
+import org.keycloak.exportimport.ClientImporter;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.ModelDuplicateException;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserFederationProviderModel;
+import org.keycloak.models.UserSessionModel;
+import org.keycloak.models.UsernameLoginFailureModel;
+import org.keycloak.models.cache.CacheRealmProvider;
+import org.keycloak.models.cache.CacheUserProvider;
+import org.keycloak.models.utils.ModelToRepresentation;
+import org.keycloak.models.utils.RepresentationToModel;
+import org.keycloak.protocol.oidc.TokenManager;
+import org.keycloak.representations.adapters.action.GlobalRequestResult;
+import org.keycloak.representations.idm.RealmEventsConfigRepresentation;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.services.ErrorResponse;
+import org.keycloak.services.managers.AuthenticationManager;
+import org.keycloak.services.managers.BruteForceProtector;
+import org.keycloak.services.managers.LDAPConnectionTestManager;
+import org.keycloak.services.managers.RealmManager;
+import org.keycloak.services.managers.ResourceAdminManager;
+import org.keycloak.services.managers.UsersSyncManager;
+import org.keycloak.timer.TimerProvider;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.PatternSyntaxException;
+
+/**
+ * Base resource class for the admin REST api of one realm
+ *
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class AttackDetectionResource {
+ protected static final Logger logger = Logger.getLogger(AttackDetectionResource.class);
+ protected RealmAuth auth;
+ protected RealmModel realm;
+ private AdminEventBuilder adminEvent;
+
+ @Context
+ protected KeycloakSession session;
+
+ @Context
+ protected UriInfo uriInfo;
+
+ @Context
+ protected ClientConnection connection;
+
+ @Context
+ protected HttpHeaders headers;
+
+ @Context
+ protected BruteForceProtector protector;
+
+ public AttackDetectionResource(RealmAuth auth, RealmModel realm, AdminEventBuilder adminEvent) {
+ this.auth = auth;
+ this.realm = realm;
+ this.adminEvent = adminEvent.realm(realm);
+
+ auth.init(RealmAuth.Resource.REALM);
+ }
+
+ /**
+ * Get status of a username in brute force detection
+ *
+ * @param username
+ * @return
+ */
+ @GET
+ @Path("brute-force/usernames/{username}")
+ @NoCache
+ @Produces(MediaType.APPLICATION_JSON)
+ public Map<String, Object> bruteForceUserStatus(@PathParam("username") String username) {
+ auth.hasView();
+ Map<String, Object> data = new HashMap<>();
+ data.put("disabled", false);
+ data.put("numFailures", 0);
+ data.put("lastFailure", 0);
+ data.put("lastIPFailure", "n/a");
+ if (!realm.isBruteForceProtected()) return data;
+
+ UsernameLoginFailureModel model = session.sessions().getUserLoginFailure(realm, username);
+ if (model == null) return data;
+ if (protector.isTemporarilyDisabled(session, realm, username)) {
+ data.put("disabled", true);
+ }
+ data.put("numFailures", model.getNumFailures());
+ data.put("lastFailure", model.getLastFailure());
+ data.put("lastIPFailure", model.getLastIPFailure());
+ return data;
+ }
+
+ /**
+ * Clear any user login failures for the user. This can release temporary disabled user
+ *
+ * @param username
+ */
+ @Path("brute-force/usernames/{username}")
+ @DELETE
+ public void clearBruteForceForUser(@PathParam("username") String username) {
+ auth.requireManage();
+ UsernameLoginFailureModel model = session.sessions().getUserLoginFailure(realm, username);
+ if (model != null) {
+ session.sessions().removeUserLoginFailure(realm, username);
+ adminEvent.operation(OperationType.DELETE).success();
+ }
+ }
+
+ /**
+ * Clear any user login failures for all users. This can release temporary disabled users
+ *
+ */
+ @Path("brute-force/usernames")
+ @DELETE
+ public void clearAllBruteForce() {
+ auth.requireManage();
+ session.sessions().removeAllUserLoginFailures(realm);
+ adminEvent.operation(OperationType.DELETE).success();
+ }
+
+
+}
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
index 85779bc..40710a1 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
@@ -109,6 +109,18 @@ public class RealmAdminResource {
}
/**
+ * Base path for managing attack detection.
+ *
+ * @return
+ */
+ @Path("attack-detection")
+ public AttackDetectionResource getClientImporter() {
+ AttackDetectionResource resource = new AttackDetectionResource(auth, realm, adminEvent);
+ ResteasyProviderFactory.getInstance().injectProperties(resource);
+ return resource;
+ }
+
+ /**
* Base path for managing clients under this realm.
*
* @return
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/BruteForceTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/BruteForceTest.java
new file mode 100755
index 0000000..a5eaa64
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/BruteForceTest.java
@@ -0,0 +1,375 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2012, Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * 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;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.keycloak.events.Details;
+import org.keycloak.events.Errors;
+import org.keycloak.models.Constants;
+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.AppPage;
+import org.keycloak.testsuite.pages.AppPage.RequestType;
+import org.keycloak.testsuite.pages.LoginPage;
+import org.keycloak.testsuite.pages.LoginTotpPage;
+import org.keycloak.testsuite.rule.GreenMailRule;
+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 javax.ws.rs.client.Client;
+import javax.ws.rs.client.ClientBuilder;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.Response;
+import java.net.MalformedURLException;
+import java.util.Collections;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class BruteForceTest {
+
+ @ClassRule
+ public static KeycloakRule keycloakRule = new KeycloakRule(new KeycloakSetup() {
+
+ @Override
+ public void config(RealmManager manager, RealmModel defaultRealm, RealmModel appRealm) {
+ UserModel user = manager.getSession().users().getUserByUsername("test-user@localhost", appRealm);
+
+ UserCredentialModel credentials = new UserCredentialModel();
+ credentials.setType(CredentialRepresentation.TOTP);
+ credentials.setValue("totpSecret");
+ user.updateCredential(credentials);
+
+ user.setTotp(true);
+ appRealm.setEventsListeners(Collections.singleton("dummy"));
+
+ appRealm.setBruteForceProtected(true);
+ appRealm.setFailureFactor(2);
+ }
+
+ });
+
+ @Rule
+ public AssertEvents events = new AssertEvents(keycloakRule);
+
+ @Rule
+ public WebRule webRule = new WebRule(this);
+
+ @Rule
+ public GreenMailRule greenMail = new GreenMailRule();
+
+ @WebResource
+ protected WebDriver driver;
+
+ @WebResource
+ protected AppPage appPage;
+
+ @WebResource
+ protected LoginPage loginPage;
+
+ @WebResource
+ protected LoginTotpPage loginTotpPage;
+
+ @WebResource
+ protected OAuthClient oauth;
+
+
+ private TimeBasedOTP totp = new TimeBasedOTP();
+
+ private int lifespan;
+
+ @Before
+ public void before() throws MalformedURLException {
+ totp = new TimeBasedOTP();
+ }
+
+ public String getAdminToken() throws Exception {
+ String clientId = Constants.ADMIN_CONSOLE_CLIENT_ID;
+ return oauth.doGrantAccessTokenRequest("master", "admin", "admin", null, clientId, null).getAccessToken();
+ }
+
+ public OAuthClient.AccessTokenResponse getTestToken(String password, String totp) throws Exception {
+ return oauth.doGrantAccessTokenRequest("test", "test-user@localhost", password, totp, oauth.getClientId(), "password");
+
+ }
+
+ protected void clearUserFailures() throws Exception {
+ String token = getAdminToken();
+ Client client = ClientBuilder.newClient();
+ Response response = client.target(AppPage.AUTH_SERVER_URL)
+ .path("admin/realms/test/attack-detection/brute-force/usernames/test-user@localhost")
+ .request()
+ .header(HttpHeaders.AUTHORIZATION, "Bearer " + token)
+ .delete();
+ Assert.assertEquals(204, response.getStatus());
+ response.close();
+ client.close();
+
+
+ }
+
+ protected void clearAllUserFailures() throws Exception {
+ String token = getAdminToken();
+ Client client = ClientBuilder.newClient();
+ Response response = client.target(AppPage.AUTH_SERVER_URL)
+ .path("admin/realms/test/attack-detection/brute-force/usernames")
+ .request()
+ .header(HttpHeaders.AUTHORIZATION, "Bearer " + token)
+ .delete();
+ Assert.assertEquals(204, response.getStatus());
+ response.close();
+ client.close();
+
+
+ }
+
+ @Test
+ public void testGrantInvalidPassword() throws Exception {
+ {
+ String totpSecret = totp.generate("totpSecret");
+ OAuthClient.AccessTokenResponse response = getTestToken("password", totpSecret);
+ Assert.assertNotNull(response.getAccessToken());
+ Assert.assertNull(response.getError());
+ events.clear();
+ }
+ {
+ String totpSecret = totp.generate("totpSecret");
+ OAuthClient.AccessTokenResponse response = getTestToken("invalid", totpSecret);
+ Assert.assertNull(response.getAccessToken());
+ Assert.assertEquals(response.getError(), "invalid_grant");
+ Assert.assertEquals(response.getErrorDescription(), "Invalid user credentials");
+ events.clear();
+ }
+ {
+ String totpSecret = totp.generate("totpSecret");
+ OAuthClient.AccessTokenResponse response = getTestToken("invalid", totpSecret);
+ Assert.assertNull(response.getAccessToken());
+ Assert.assertEquals(response.getError(), "invalid_grant");
+ Assert.assertEquals(response.getErrorDescription(), "Invalid user credentials");
+ events.clear();
+ }
+ {
+ String totpSecret = totp.generate("totpSecret");
+ OAuthClient.AccessTokenResponse response = getTestToken("password", totpSecret);
+ Assert.assertNull(response.getAccessToken());
+ Assert.assertNotNull(response.getError());
+ Assert.assertEquals(response.getError(), "invalid_grant");
+ Assert.assertEquals(response.getErrorDescription(), "Account temporarily disabled");
+ events.clear();
+ }
+ clearUserFailures();
+ {
+ String totpSecret = totp.generate("totpSecret");
+ OAuthClient.AccessTokenResponse response = getTestToken("password", totpSecret);
+ Assert.assertNotNull(response.getAccessToken());
+ Assert.assertNull(response.getError());
+ events.clear();
+ }
+
+ }
+
+ @Test
+ public void testGrantInvalidOtp() throws Exception {
+ {
+ String totpSecret = totp.generate("totpSecret");
+ OAuthClient.AccessTokenResponse response = getTestToken("password", totpSecret);
+ Assert.assertNotNull(response.getAccessToken());
+ Assert.assertNull(response.getError());
+ events.clear();
+ }
+ {
+ OAuthClient.AccessTokenResponse response = getTestToken("password", "shite");
+ Assert.assertNull(response.getAccessToken());
+ Assert.assertEquals(response.getError(), "invalid_grant");
+ Assert.assertEquals(response.getErrorDescription(), "Invalid user credentials");
+ events.clear();
+ }
+ {
+ OAuthClient.AccessTokenResponse response = getTestToken("password", "shite");
+ Assert.assertNull(response.getAccessToken());
+ Assert.assertEquals(response.getError(), "invalid_grant");
+ Assert.assertEquals(response.getErrorDescription(), "Invalid user credentials");
+ events.clear();
+ }
+ {
+ String totpSecret = totp.generate("totpSecret");
+ OAuthClient.AccessTokenResponse response = getTestToken("password", totpSecret);
+ Assert.assertNull(response.getAccessToken());
+ Assert.assertNotNull(response.getError());
+ Assert.assertEquals(response.getError(), "invalid_grant");
+ Assert.assertEquals(response.getErrorDescription(), "Account temporarily disabled");
+ events.clear();
+ }
+ clearUserFailures();
+ {
+ String totpSecret = totp.generate("totpSecret");
+ OAuthClient.AccessTokenResponse response = getTestToken("password", totpSecret);
+ Assert.assertNotNull(response.getAccessToken());
+ Assert.assertNull(response.getError());
+ events.clear();
+ }
+
+ }
+
+
+
+
+ @Test
+ public void testBrowserInvalidPassword() throws Exception {
+ loginSuccess();
+ loginInvalidPassword();
+ loginInvalidPassword();
+ expectTemporarilyDisabled();
+ clearUserFailures();
+ loginSuccess();
+ loginInvalidPassword();
+ loginInvalidPassword();
+ expectTemporarilyDisabled();
+ clearAllUserFailures();
+ loginSuccess();
+ }
+
+ @Test
+ public void testBrowserMissingPassword() throws Exception {
+ loginSuccess();
+ loginMissingPassword();
+ loginMissingPassword();
+ expectTemporarilyDisabled();
+ clearUserFailures();
+ loginSuccess();
+ }
+
+ @Test
+ public void testBrowserInvalidTotp() throws Exception {
+ loginSuccess();
+ loginWithTotpFailure();
+ loginWithTotpFailure();
+ expectTemporarilyDisabled();
+ clearUserFailures();
+ loginSuccess();
+ }
+
+ @Test
+ public void testBrowserMissingTotp() throws Exception {
+ loginSuccess();
+ loginWithMissingTotp();
+ loginWithMissingTotp();
+ expectTemporarilyDisabled();
+ clearUserFailures();
+ loginSuccess();
+ }
+
+ public void expectTemporarilyDisabled() throws Exception {
+ loginPage.open();
+ loginPage.login("test-user@localhost", "password");
+
+ loginPage.assertCurrent();
+ String src = driver.getPageSource();
+ Assert.assertEquals("Account is temporarily disabled, contact admin or try again later.", loginPage.getError());
+ events.expectLogin().session((String) null).error(Errors.USER_TEMPORARILY_DISABLED)
+ .detail(Details.USERNAME, "test-user@localhost")
+ .removeDetail(Details.CONSENT)
+ .assertEvent();
+ }
+
+
+
+ public void loginSuccess() throws Exception {
+ loginPage.open();
+ loginPage.login("test-user@localhost", "password");
+
+ loginTotpPage.assertCurrent();
+
+ String totpSecret = totp.generate("totpSecret");
+ loginTotpPage.login(totpSecret);
+
+ Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
+
+ events.expectLogin().assertEvent();
+
+ appPage.logout();
+ events.clear();
+
+
+ }
+
+ public void loginWithTotpFailure() throws Exception {
+ loginPage.open();
+ loginPage.login("test-user@localhost", "password");
+
+ loginTotpPage.assertCurrent();
+
+ loginTotpPage.login("123456");
+ loginTotpPage.assertCurrent();
+ Assert.assertEquals("Invalid authenticator code.", loginPage.getError());
+ events.clear();
+ }
+
+ public void loginWithMissingTotp() throws Exception {
+ loginPage.open();
+ loginPage.login("test-user@localhost", "password");
+
+ loginTotpPage.assertCurrent();
+
+ loginTotpPage.login(null);
+ loginTotpPage.assertCurrent();
+ Assert.assertEquals("Invalid authenticator code.", loginPage.getError());
+
+ events.clear();
+ }
+
+
+ public void loginInvalidPassword() throws Exception {
+ loginPage.open();
+ loginPage.login("test-user@localhost", "invalid");
+
+ loginPage.assertCurrent();
+
+ Assert.assertEquals("Invalid username or password.", loginPage.getError());
+
+ events.clear();
+ }
+
+ public void loginMissingPassword() {
+ loginPage.open();
+ loginPage.missingPassword("test-user@localhost");
+
+ loginPage.assertCurrent();
+
+ Assert.assertEquals("Invalid username or password.", loginPage.getError());
+ events.clear();
+ }
+
+}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTest.java
index 1799a64..745a5a2 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTest.java
@@ -138,6 +138,21 @@ public class LoginTest {
}
@Test
+ public void loginMissingPassword() {
+ loginPage.open();
+ loginPage.missingPassword("login-test");
+
+ loginPage.assertCurrent();
+
+ Assert.assertEquals("Invalid username or password.", loginPage.getError());
+
+ events.expectLogin().user(userId).session((String) null).error("invalid_user_credentials")
+ .detail(Details.USERNAME, "login-test")
+ .removeDetail(Details.CONSENT)
+ .assertEvent();
+ }
+
+ @Test
public void loginInvalidPasswordDisabledUser() {
keycloakRule.configure(new KeycloakRule.KeycloakSetup() {
@Override
@@ -215,6 +230,20 @@ public class LoginTest {
}
@Test
+ public void loginMissingUsername() {
+ loginPage.open();
+ loginPage.missingUsername();
+
+ loginPage.assertCurrent();
+
+ Assert.assertEquals("Invalid username or password.", loginPage.getError());
+
+ events.expectLogin().user((String) null).session((String) null).error("user_not_found")
+ .removeDetail(Details.CONSENT)
+ .assertEvent();
+ }
+
+ @Test
public void loginSuccess() {
loginPage.open();
loginPage.login("login-test", "password");
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTotpTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTotpTest.java
index 3ea0915..0374715 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTotpTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTotpTest.java
@@ -122,6 +122,25 @@ public class LoginTotpTest {
}
@Test
+ public void loginWithMissingTotp() throws Exception {
+ loginPage.open();
+ loginPage.login("test-user@localhost", "password");
+
+ loginTotpPage.assertCurrent();
+
+ loginTotpPage.login(null);
+ loginTotpPage.assertCurrent();
+ Assert.assertEquals("Invalid authenticator code.", loginPage.getError());
+
+ //loginPage.assertCurrent(); // Invalid authenticator code.
+ //Assert.assertEquals("Invalid username or password.", loginPage.getError());
+
+ events.expectLogin().error("invalid_user_credentials").session((String) null)
+ .removeDetail(Details.CONSENT)
+ .assertEvent();
+ }
+
+ @Test
public void loginWithTotpSuccess() throws Exception {
loginPage.open();
loginPage.login("test-user@localhost", "password");
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java
index 1bac627..abfe3c3 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java
@@ -161,17 +161,30 @@ public class OAuthClient {
}
public AccessTokenResponse doGrantAccessTokenRequest(String clientSecret, String username, String password) throws Exception {
+ return doGrantAccessTokenRequest(realm, username, password, null, clientId, clientSecret);
+ }
+
+ public AccessTokenResponse doGrantAccessTokenRequest(String realm, String username, String password, String totp,
+ String clientId, String clientSecret) throws Exception {
CloseableHttpClient client = new DefaultHttpClient();
try {
- HttpPost post = new HttpPost(getResourceOwnerPasswordCredentialGrantUrl());
-
- String authorization = BasicAuthHelper.createHeader(clientId, clientSecret);
- post.setHeader("Authorization", authorization);
+ HttpPost post = new HttpPost(getResourceOwnerPasswordCredentialGrantUrl(realm));
List<NameValuePair> parameters = new LinkedList<NameValuePair>();
parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.PASSWORD));
parameters.add(new BasicNameValuePair("username", username));
parameters.add(new BasicNameValuePair("password", password));
+ if (totp != null) {
+ parameters.add(new BasicNameValuePair("totp", totp));
+
+ }
+ if (clientSecret != null) {
+ String authorization = BasicAuthHelper.createHeader(clientId, clientSecret);
+ post.setHeader("Authorization", authorization);
+ } else {
+ parameters.add(new BasicNameValuePair("client_id", clientId));
+
+ }
if (clientSessionState != null) {
parameters.add(new BasicNameValuePair(AdapterConstants.CLIENT_SESSION_STATE, clientSessionState));
@@ -219,6 +232,7 @@ public class OAuthClient {
}
}
+
public HttpResponse doLogout(String refreshToken, String clientSecret) throws IOException {
CloseableHttpClient client = new DefaultHttpClient();
try {
@@ -400,6 +414,11 @@ public class OAuthClient {
return b.build(realm).toString();
}
+ public String getResourceOwnerPasswordCredentialGrantUrl(String realm) {
+ UriBuilder b = OIDCLoginProtocolService.tokenUrl(UriBuilder.fromUri(baseUrl));
+ return b.build(realm).toString();
+ }
+
public String getServiceAccountUrl() {
return getResourceOwnerPasswordCredentialGrantUrl();
}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AppPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AppPage.java
old mode 100644
new mode 100755
index 7fa6c9e..82d696e
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AppPage.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AppPage.java
@@ -22,14 +22,20 @@
package org.keycloak.testsuite.pages;
+import org.keycloak.OAuth2Constants;
+import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
+import org.keycloak.testsuite.OAuthClient;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
+import javax.ws.rs.core.UriBuilder;
+
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class AppPage extends AbstractPage {
+ public static final String AUTH_SERVER_URL = "http://localhost:8081/auth";
public static final String baseUrl = "http://localhost:8081/app";
@FindBy(id = "account")
@@ -57,4 +63,11 @@ public class AppPage extends AbstractPage {
AUTH_RESPONSE, LOGOUT_REQUEST, APP_REQUEST
}
+ public void logout() {
+ String logoutUri = OIDCLoginProtocolService.logoutUrl(UriBuilder.fromUri(AUTH_SERVER_URL))
+ .queryParam(OAuth2Constants.REDIRECT_URI,baseUrl).build("test").toString();
+ driver.navigate().to(logoutUri);
+
+ }
+
}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginPage.java
old mode 100644
new mode 100755
index 2be207c..2ea8b62
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginPage.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginPage.java
@@ -91,6 +91,19 @@ public class LoginPage extends AbstractPage {
submitButton.click();
}
+ public void missingPassword(String username) {
+ usernameInput.clear();
+ usernameInput.sendKeys(username);
+ passwordInput.clear();
+ submitButton.click();
+
+ }
+ public void missingUsername() {
+ usernameInput.clear();
+ submitButton.click();
+
+ }
+
public String getUsername() {
return usernameInput.getAttribute("value");
}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginTotpPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginTotpPage.java
old mode 100644
new mode 100755
index e1a934a..b725a16
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginTotpPage.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginTotpPage.java
@@ -43,7 +43,8 @@ public class LoginTotpPage extends AbstractPage {
private WebElement loginErrorMessage;
public void login(String totp) {
- totpInput.sendKeys(totp);
+ totpInput.clear();
+ if (totp != null) totpInput.sendKeys(totp);
submitButton.click();
}