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 f7968a0357..f3fe77f4ca 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 5e9fefda63..1d6a6be925 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 e9da897795..5b4ae7f745 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 @@ A disabled user cannot login. +
+ +
+ +
+ The user may have been locked due to failing to login too many times. +
+ +
+
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 508e07f4e6..0d069e04dc 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 @@
+ Add User
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 2a874a80b9..3b4d1472d7 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 f865ca3d19..2202009c20 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; @@ -293,9 +294,30 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { return wrap(key, entity); } + @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 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 0000000000..766a863a6e --- /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 Stian Thorgersen + */ +public class UserLoginFailureMapper implements Mapper, 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 8614ec06fa..20ac9675ed 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 @@ -92,6 +92,18 @@ public class JpaUserSessionProvider implements UserSessionProvider { return new UsernameLoginFailureAdapter(entity); } + @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(); 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 ac6655f5c7..c32c4dba7b 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 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 82045fd1f7..d75da9a9b4 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 @@ -271,9 +271,28 @@ public class MongoUserSessionProvider implements UserSessionProvider { return new UsernameLoginFailureAdapter(invocationContext, userEntity); } + @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 1508746222..fbe37b482f 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 c9ca6dee28..ea95c14310 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 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 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 types = new HashSet(); - - for (RequiredCredentialModel credential : realm.getRequiredCredentials()) { - types.add(credential.getType()); - } - - if (types.contains(CredentialRepresentation.PASSWORD)) { - List credentials = new LinkedList(); - - 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 0000000000..38eec9cd77 --- /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 Bill Burke + * @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 bruteForceUserStatus(@PathParam("username") String username) { + auth.hasView(); + Map 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 85779bce94..40710a1a11 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 @@ -108,6 +108,18 @@ public class RealmAdminResource { return importer.createJaxrsService(realm, auth); } + /** + * 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. * 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 1bac62790e..abfe3c30be 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 parameters = new LinkedList(); 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/forms/BruteForceTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/BruteForceTest.java new file mode 100755 index 0000000000..a5eaa648cc --- /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 Stian Thorgersen + */ +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 1799a644f0..745a5a2e1d 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 @@ -137,6 +137,21 @@ public class LoginTest { .assertEvent(); } + @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() { @@ -214,6 +229,20 @@ public class LoginTest { .assertEvent(); } + @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(); 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 3ea0915d1a..03747150d3 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 @@ -121,6 +121,25 @@ public class LoginTotpTest { .assertEvent(); } + @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(); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/AuthenticationManagerTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/AuthenticationManagerTest.java deleted file mode 100755 index 11e1887cbd..0000000000 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/AuthenticationManagerTest.java +++ /dev/null @@ -1,288 +0,0 @@ -package org.keycloak.testsuite.model; - -import org.jboss.resteasy.specimpl.MultivaluedMapImpl; -import org.jboss.resteasy.spi.ResteasyProviderFactory; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; -import org.keycloak.ClientConnection; -import org.keycloak.jose.jws.JWSBuilder; -import org.keycloak.models.RealmModel; -import org.keycloak.models.UserCredentialModel; -import org.keycloak.models.UserModel; -import org.keycloak.models.UserModel.RequiredAction; -import org.keycloak.models.utils.KeycloakModelUtils; -import org.keycloak.models.utils.TimeBasedOTP; -import org.keycloak.representations.PasswordToken; -import org.keycloak.representations.idm.CredentialRepresentation; -import org.keycloak.services.managers.AuthenticationManager; -import org.keycloak.services.managers.AuthenticationManager.AuthenticationStatus; -import org.keycloak.services.managers.BruteForceProtector; -import org.keycloak.util.Time; - -import javax.ws.rs.core.MultivaluedMap; -import java.util.UUID; - -public class AuthenticationManagerTest extends AbstractModelTest { - - private AuthenticationManager am; - private MultivaluedMap formData; - private TimeBasedOTP otp; - private RealmModel realm; - private UserModel user; - private BruteForceProtector protector; - private ClientConnection dummyConnection = new ClientConnection() { - @Override - public String getRemoteAddr() { - return "127.0.0.1"; - - } - - @Override - public String getRemoteHost() { - return "localhost"; - - } - - @Override - public int getReportPort() { - return 8080; - } - }; - - @Test - public void authForm() { - AuthenticationStatus status = am.authenticateForm(session, dummyConnection, realm, formData); - Assert.assertEquals(AuthenticationStatus.SUCCESS, status); - } - - @Test - public void authFormInvalidPassword() { - formData.remove(CredentialRepresentation.PASSWORD); - formData.add(CredentialRepresentation.PASSWORD, "invalid"); - - AuthenticationStatus status = am.authenticateForm(session, dummyConnection, realm, formData); - Assert.assertEquals(AuthenticationStatus.INVALID_CREDENTIALS, status); - } - - @Test - public void authFormMissingUsername() { - formData.remove("username"); - - AuthenticationStatus status = am.authenticateForm(session, dummyConnection, realm, formData); - Assert.assertEquals(AuthenticationStatus.INVALID_USER, status); - } - - @Test - public void authFormMissingPassword() { - formData.remove(CredentialRepresentation.PASSWORD); - - AuthenticationStatus status = am.authenticateForm(session, dummyConnection, realm, formData); - Assert.assertEquals(AuthenticationStatus.MISSING_PASSWORD, status); - } - - @Test - public void authFormRequiredAction() { - realm.addRequiredCredential(CredentialRepresentation.TOTP); - user.addRequiredAction(RequiredAction.CONFIGURE_TOTP); - - AuthenticationStatus status = am.authenticateForm(session, dummyConnection, realm, formData); - Assert.assertEquals(AuthenticationStatus.ACTIONS_REQUIRED, status); - } - - @Test - public void authFormUserDisabled() { - user.setEnabled(false); - - AuthenticationStatus status = am.authenticateForm(session, dummyConnection, realm, formData); - Assert.assertEquals(AuthenticationStatus.ACCOUNT_DISABLED, status); - } - - @Test - public void authFormWithTotp() { - realm.addRequiredCredential(CredentialRepresentation.TOTP); - - String totpSecret = UUID.randomUUID().toString(); - - UserCredentialModel credential = new UserCredentialModel(); - credential.setType(CredentialRepresentation.TOTP); - credential.setValue(totpSecret); - - user.updateCredential(credential); - - user.setTotp(true); - - String token = otp.generate(totpSecret); - - formData.add(CredentialRepresentation.TOTP, token); - - AuthenticationStatus status = am.authenticateForm(session, dummyConnection, realm, formData); - Assert.assertEquals(AuthenticationStatus.SUCCESS, status); - } - - @Test - public void authFormWithTotpInvalidPassword() { - authFormWithTotp(); - - formData.remove(CredentialRepresentation.PASSWORD); - formData.add(CredentialRepresentation.PASSWORD, "invalid"); - - AuthenticationStatus status = am.authenticateForm(session, dummyConnection, realm, formData); - Assert.assertEquals(AuthenticationStatus.INVALID_CREDENTIALS, status); - } - - @Test - public void authFormWithTotpMissingPassword() { - authFormWithTotp(); - - formData.remove(CredentialRepresentation.PASSWORD); - - AuthenticationStatus status = am.authenticateForm(session, dummyConnection, realm, formData); - Assert.assertEquals(AuthenticationStatus.MISSING_PASSWORD, status); - } - - @Test - public void authFormWithTotpInvalidTotp() { - authFormWithTotp(); - - formData.remove(CredentialRepresentation.TOTP); - formData.add(CredentialRepresentation.TOTP, "invalid"); - - AuthenticationStatus status = am.authenticateForm(session, dummyConnection, realm, formData); - Assert.assertEquals(AuthenticationStatus.INVALID_CREDENTIALS, status); - } - - @Test - public void authFormWithTotpMissingTotp() { - authFormWithTotp(); - - formData.remove(CredentialRepresentation.TOTP); - - AuthenticationStatus status = am.authenticateForm(session, dummyConnection, realm, formData); - Assert.assertEquals(AuthenticationStatus.MISSING_TOTP, status); - } - - @Test - public void authFormWithTotpPasswordToken() { - realm.addRequiredCredential(CredentialRepresentation.TOTP); - - String totpSecret = UUID.randomUUID().toString(); - - UserCredentialModel credential = new UserCredentialModel(); - credential.setType(CredentialRepresentation.TOTP); - credential.setValue(totpSecret); - - user.updateCredential(credential); - - user.setTotp(true); - - String token = otp.generate(totpSecret); - - formData.add(CredentialRepresentation.TOTP, token); - formData.remove(CredentialRepresentation.PASSWORD); - - String passwordToken = new JWSBuilder().jsonContent(new PasswordToken(realm.getName(), user.getId())).rsa256(realm.getPrivateKey()); - formData.add(CredentialRepresentation.PASSWORD_TOKEN, passwordToken); - - AuthenticationStatus status = am.authenticateForm(session, dummyConnection, realm, formData); - Assert.assertEquals(AuthenticationStatus.SUCCESS, status); - } - - @Test - public void authFormWithTotpPasswordTokenInvalidKey() { - authFormWithTotpPasswordToken(); - - formData.remove(CredentialRepresentation.PASSWORD_TOKEN); - String passwordToken = new JWSBuilder().jsonContent(new PasswordToken(realm.getName(), user.getId())).rsa256(realm.getPrivateKey()); - formData.add(CredentialRepresentation.PASSWORD_TOKEN, passwordToken); - - KeycloakModelUtils.generateRealmKeys(realm); - - AuthenticationStatus status = am.authenticateForm(session, dummyConnection, realm, formData); - Assert.assertEquals(AuthenticationStatus.INVALID_CREDENTIALS, status); - } - - @Test - public void authFormWithTotpPasswordTokenInvalidRealm() { - authFormWithTotpPasswordToken(); - - formData.remove(CredentialRepresentation.PASSWORD_TOKEN); - String passwordToken = new JWSBuilder().jsonContent(new PasswordToken("invalid", user.getId())).rsa256(realm.getPrivateKey()); - formData.add(CredentialRepresentation.PASSWORD_TOKEN, passwordToken); - - AuthenticationStatus status = am.authenticateForm(session, dummyConnection, realm, formData); - Assert.assertEquals(AuthenticationStatus.INVALID_CREDENTIALS, status); - } - - @Test - public void authFormWithTotpPasswordTokenInvalidUser() { - authFormWithTotpPasswordToken(); - - formData.remove(CredentialRepresentation.PASSWORD_TOKEN); - String passwordToken = new JWSBuilder().jsonContent(new PasswordToken(realm.getName(), "invalid")).rsa256(realm.getPrivateKey()); - formData.add(CredentialRepresentation.PASSWORD_TOKEN, passwordToken); - - AuthenticationStatus status = am.authenticateForm(session, dummyConnection, realm, formData); - Assert.assertEquals(AuthenticationStatus.INVALID_CREDENTIALS, status); - } - - @Test - public void authFormWithTotpPasswordTokenExpired() throws InterruptedException { - int lifespan = realm.getAccessCodeLifespanUserAction(); - - try { - authFormWithTotpPasswordToken(); - - realm.setAccessCodeLifespanUserAction(1); - - formData.remove(CredentialRepresentation.PASSWORD_TOKEN); - String passwordToken = new JWSBuilder().jsonContent(new PasswordToken(realm.getName(), "invalid")).rsa256(realm.getPrivateKey()); - formData.add(CredentialRepresentation.PASSWORD_TOKEN, passwordToken); - - Time.setOffset(2); - - AuthenticationStatus status = am.authenticateForm(session, dummyConnection, realm, formData); - Assert.assertEquals(AuthenticationStatus.INVALID_CREDENTIALS, status); - - Time.setOffset(0); - } finally { - realm.setAccessCodeLifespanUserAction(lifespan); - } - } - - @Before - @Override - public void before() throws Exception { - super.before(); - - realm = realmManager.createRealm("TestAuth"); - realm.setAccessCodeLifespan(100); - realm.setAccessCodeLifespanUserAction(100); - realm.setEnabled(true); - realm.setName("TestAuth"); - - KeycloakModelUtils.generateRealmKeys(realm); - - realm.setAccessTokenLifespan(1000); - realm.addRequiredCredential(CredentialRepresentation.PASSWORD); - - protector = ResteasyProviderFactory.getContextData(BruteForceProtector.class); - am = new AuthenticationManager(protector); - - user = realmManager.getSession().users().addUser(realm, "test"); - user.setEnabled(true); - - UserCredentialModel credential = new UserCredentialModel(); - credential.setType(CredentialRepresentation.PASSWORD); - credential.setValue("password"); - - user.updateCredential(credential); - - formData = new MultivaluedMapImpl(); - formData.add("username", "test"); - formData.add(CredentialRepresentation.PASSWORD, "password"); - - otp = new TimeBasedOTP(); - } - -} 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 7fa6c9e44a..82d696ee31 --- 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 Stian Thorgersen */ 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 2be207c5c8..2ea8b62ffe --- 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 e1a934a61a..b725a169b9 --- 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(); }