Merge pull request #1481 from patriot1burke/master
brute force fixes and enhancements
This commit is contained in:
commit
251ad6727a
22 changed files with 818 additions and 416 deletions
|
@ -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.realm = realm;
|
||||||
$scope.page = 0;
|
$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.firstPage = function() {
|
||||||
$scope.query.first = 0;
|
$scope.query.first = 0;
|
||||||
$scope.searchQuery();
|
$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.realm = realm;
|
||||||
$scope.create = !user.id;
|
$scope.create = !user.id;
|
||||||
$scope.editUsername = $scope.create || $scope.realm.editUsernameAllowed;
|
$scope.editUsername = $scope.create || $scope.realm.editUsernameAllowed;
|
||||||
|
@ -315,6 +322,23 @@ module.controller('UserDetailCtrl', function($scope, realm, user, User, UserFede
|
||||||
} else {
|
} else {
|
||||||
console.log("federationLink is null");
|
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;
|
$scope.changed = false; // $scope.create;
|
||||||
|
|
|
@ -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) {
|
module.factory('RequiredActions', function($resource) {
|
||||||
return $resource(authUrl + '/admin/realms/:id/authentication/required-actions/:alias', {
|
return $resource(authUrl + '/admin/realms/:id/authentication/required-actions/:alias', {
|
||||||
realm : '@realm',
|
realm : '@realm',
|
||||||
|
|
|
@ -66,6 +66,16 @@
|
||||||
</div>
|
</div>
|
||||||
<kc-tooltip>A disabled user cannot login.</kc-tooltip>
|
<kc-tooltip>A disabled user cannot login.</kc-tooltip>
|
||||||
</div>
|
</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">
|
<div class="form-group clearfix block" data-ng-show="!create && user.federationLink">
|
||||||
<label class="col-md-2 control-label" for="userEnabled">Federation Link</label>
|
<label class="col-md-2 control-label" for="userEnabled">Federation Link</label>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
<button id="viewAllUsers" class="btn btn-default" ng-click="query.search = null; firstPage()">View all users</button>
|
<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">
|
<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>
|
<a id="createUser" class="btn btn-default" href="#/create/user/{{realm.realm}}">Add User</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -33,6 +33,8 @@ public interface UserSessionProvider extends Provider {
|
||||||
|
|
||||||
UsernameLoginFailureModel getUserLoginFailure(RealmModel realm, String username);
|
UsernameLoginFailureModel getUserLoginFailure(RealmModel realm, String username);
|
||||||
UsernameLoginFailureModel addUserLoginFailure(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 onRealmRemoved(RealmModel realm);
|
||||||
void onClientRemoved(RealmModel realm, ClientModel client);
|
void onClientRemoved(RealmModel realm, ClientModel client);
|
||||||
|
|
|
@ -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.FirstResultReducer;
|
||||||
import org.keycloak.models.sessions.infinispan.mapreduce.LargestResultReducer;
|
import org.keycloak.models.sessions.infinispan.mapreduce.LargestResultReducer;
|
||||||
import org.keycloak.models.sessions.infinispan.mapreduce.SessionMapper;
|
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.UserSessionMapper;
|
||||||
import org.keycloak.models.sessions.infinispan.mapreduce.UserSessionNoteMapper;
|
import org.keycloak.models.sessions.infinispan.mapreduce.UserSessionNoteMapper;
|
||||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||||
|
@ -293,9 +294,30 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
|
||||||
return wrap(key, entity);
|
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<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
|
@Override
|
||||||
public void onRealmRemoved(RealmModel realm) {
|
public void onRealmRemoved(RealmModel realm) {
|
||||||
removeUserSessions(realm);
|
removeUserSessions(realm);
|
||||||
|
removeAllUserLoginFailures(realm);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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));
|
tasks.put(key, new CacheTask(cache, CacheOperation.REMOVE, key, null));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -92,6 +92,18 @@ public class JpaUserSessionProvider implements UserSessionProvider {
|
||||||
return new UsernameLoginFailureAdapter(entity);
|
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
|
@Override
|
||||||
public UserSessionModel createUserSession(RealmModel realm, UserModel user, String loginUsername, String ipAddress, String authMethod, boolean rememberMe, String brokerSessionId, String brokerUserId) {
|
public UserSessionModel createUserSession(RealmModel realm, UserModel user, String loginUsername, String ipAddress, String authMethod, boolean rememberMe, String brokerSessionId, String brokerUserId) {
|
||||||
UserSessionEntity entity = new UserSessionEntity();
|
UserSessionEntity entity = new UserSessionEntity();
|
||||||
|
|
|
@ -323,15 +323,25 @@ public class MemUserSessionProvider implements UserSessionProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onRealmRemoved(RealmModel realm) {
|
public void removeUserLoginFailure(RealmModel realm, String username) {
|
||||||
removeUserSessions(realm);
|
loginFailures.remove(new UsernameLoginFailureKey(realm.getId(), username));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void removeAllUserLoginFailures(RealmModel realm) {
|
||||||
Iterator<UsernameLoginFailureEntity> itr = loginFailures.values().iterator();
|
Iterator<UsernameLoginFailureEntity> itr = loginFailures.values().iterator();
|
||||||
while (itr.hasNext()) {
|
while (itr.hasNext()) {
|
||||||
if (itr.next().getRealm().equals(realm.getId())) {
|
if (itr.next().getRealm().equals(realm.getId())) {
|
||||||
itr.remove();
|
itr.remove();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRealmRemoved(RealmModel realm) {
|
||||||
|
removeUserSessions(realm);
|
||||||
|
removeAllUserLoginFailures(realm);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -271,9 +271,28 @@ public class MongoUserSessionProvider implements UserSessionProvider {
|
||||||
return new UsernameLoginFailureAdapter(invocationContext, userEntity);
|
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
|
@Override
|
||||||
public void onRealmRemoved(RealmModel realm) {
|
public void onRealmRemoved(RealmModel realm) {
|
||||||
removeUserSessions(realm);
|
removeUserSessions(realm);
|
||||||
|
removeAllUserLoginFailures(realm);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -596,9 +596,8 @@ public class AuthenticationProcessor {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void validateUser(UserModel authenticatedUser) {
|
public void validateUser(UserModel authenticatedUser) {
|
||||||
if (authenticatedUser != null) {
|
if (authenticatedUser == null) return;
|
||||||
if (!authenticatedUser.isEnabled()) throw new AuthException(Error.USER_DISABLED);
|
if (!authenticatedUser.isEnabled()) throw new AuthException(Error.USER_DISABLED);
|
||||||
}
|
|
||||||
if (realm.isBruteForceProtected()) {
|
if (realm.isBruteForceProtected()) {
|
||||||
if (protector.isTemporarilyDisabled(session, realm, authenticatedUser.getUsername())) {
|
if (protector.isTemporarilyDisabled(session, realm, authenticatedUser.getUsername())) {
|
||||||
throw new AuthException(Error.USER_TEMPORARILY_DISABLED);
|
throw new AuthException(Error.USER_TEMPORARILY_DISABLED);
|
||||||
|
|
|
@ -592,121 +592,6 @@ public class AuthenticationManager {
|
||||||
return null;
|
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 {
|
public enum AuthenticationStatus {
|
||||||
SUCCESS, ACCOUNT_TEMPORARILY_DISABLED, ACCOUNT_DISABLED, ACTIONS_REQUIRED, INVALID_USER, INVALID_CREDENTIALS, MISSING_PASSWORD, MISSING_TOTP, FAILED
|
SUCCESS, ACCOUNT_TEMPORARILY_DISABLED, ACCOUNT_DISABLED, ACTIONS_REQUIRED, INVALID_USER, INVALID_CREDENTIALS, MISSING_PASSWORD, MISSING_TOTP, FAILED
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -108,6 +108,18 @@ public class RealmAdminResource {
|
||||||
return importer.createJaxrsService(realm, auth);
|
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.
|
* Base path for managing clients under this realm.
|
||||||
*
|
*
|
||||||
|
|
|
@ -161,17 +161,30 @@ public class OAuthClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
public AccessTokenResponse doGrantAccessTokenRequest(String clientSecret, String username, String password) throws Exception {
|
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();
|
CloseableHttpClient client = new DefaultHttpClient();
|
||||||
try {
|
try {
|
||||||
HttpPost post = new HttpPost(getResourceOwnerPasswordCredentialGrantUrl());
|
HttpPost post = new HttpPost(getResourceOwnerPasswordCredentialGrantUrl(realm));
|
||||||
|
|
||||||
String authorization = BasicAuthHelper.createHeader(clientId, clientSecret);
|
|
||||||
post.setHeader("Authorization", authorization);
|
|
||||||
|
|
||||||
List<NameValuePair> parameters = new LinkedList<NameValuePair>();
|
List<NameValuePair> parameters = new LinkedList<NameValuePair>();
|
||||||
parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.PASSWORD));
|
parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.PASSWORD));
|
||||||
parameters.add(new BasicNameValuePair("username", username));
|
parameters.add(new BasicNameValuePair("username", username));
|
||||||
parameters.add(new BasicNameValuePair("password", password));
|
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) {
|
if (clientSessionState != null) {
|
||||||
parameters.add(new BasicNameValuePair(AdapterConstants.CLIENT_SESSION_STATE, clientSessionState));
|
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 {
|
public HttpResponse doLogout(String refreshToken, String clientSecret) throws IOException {
|
||||||
CloseableHttpClient client = new DefaultHttpClient();
|
CloseableHttpClient client = new DefaultHttpClient();
|
||||||
try {
|
try {
|
||||||
|
@ -400,6 +414,11 @@ public class OAuthClient {
|
||||||
return b.build(realm).toString();
|
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() {
|
public String getServiceAccountUrl() {
|
||||||
return getResourceOwnerPasswordCredentialGrantUrl();
|
return getResourceOwnerPasswordCredentialGrantUrl();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -137,6 +137,21 @@ public class LoginTest {
|
||||||
.assertEvent();
|
.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
|
@Test
|
||||||
public void loginInvalidPasswordDisabledUser() {
|
public void loginInvalidPasswordDisabledUser() {
|
||||||
keycloakRule.configure(new KeycloakRule.KeycloakSetup() {
|
keycloakRule.configure(new KeycloakRule.KeycloakSetup() {
|
||||||
|
@ -214,6 +229,20 @@ public class LoginTest {
|
||||||
.assertEvent();
|
.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
|
@Test
|
||||||
public void loginSuccess() {
|
public void loginSuccess() {
|
||||||
loginPage.open();
|
loginPage.open();
|
||||||
|
|
|
@ -121,6 +121,25 @@ public class LoginTotpTest {
|
||||||
.assertEvent();
|
.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
|
@Test
|
||||||
public void loginWithTotpSuccess() throws Exception {
|
public void loginWithTotpSuccess() throws Exception {
|
||||||
loginPage.open();
|
loginPage.open();
|
||||||
|
|
|
@ -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<String, String> 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<String, String>();
|
|
||||||
formData.add("username", "test");
|
|
||||||
formData.add(CredentialRepresentation.PASSWORD, "password");
|
|
||||||
|
|
||||||
otp = new TimeBasedOTP();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
13
testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AppPage.java
Normal file → Executable file
13
testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AppPage.java
Normal file → Executable file
|
@ -22,14 +22,20 @@
|
||||||
package org.keycloak.testsuite.pages;
|
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.WebElement;
|
||||||
import org.openqa.selenium.support.FindBy;
|
import org.openqa.selenium.support.FindBy;
|
||||||
|
|
||||||
|
import javax.ws.rs.core.UriBuilder;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
*/
|
*/
|
||||||
public class AppPage extends AbstractPage {
|
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";
|
public static final String baseUrl = "http://localhost:8081/app";
|
||||||
|
|
||||||
@FindBy(id = "account")
|
@FindBy(id = "account")
|
||||||
|
@ -57,4 +63,11 @@ public class AppPage extends AbstractPage {
|
||||||
AUTH_RESPONSE, LOGOUT_REQUEST, APP_REQUEST
|
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);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
13
testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginPage.java
Normal file → Executable file
13
testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginPage.java
Normal file → Executable file
|
@ -91,6 +91,19 @@ public class LoginPage extends AbstractPage {
|
||||||
submitButton.click();
|
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() {
|
public String getUsername() {
|
||||||
return usernameInput.getAttribute("value");
|
return usernameInput.getAttribute("value");
|
||||||
}
|
}
|
||||||
|
|
3
testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginTotpPage.java
Normal file → Executable file
3
testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginTotpPage.java
Normal file → Executable file
|
@ -43,7 +43,8 @@ public class LoginTotpPage extends AbstractPage {
|
||||||
private WebElement loginErrorMessage;
|
private WebElement loginErrorMessage;
|
||||||
|
|
||||||
public void login(String totp) {
|
public void login(String totp) {
|
||||||
totpInput.sendKeys(totp);
|
totpInput.clear();
|
||||||
|
if (totp != null) totpInput.sendKeys(totp);
|
||||||
|
|
||||||
submitButton.click();
|
submitButton.click();
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue