Added required actions to user. Completed flow for user registering when Realm requires TOTP
This commit is contained in:
parent
f48d061217
commit
6c124a2172
51 changed files with 1429 additions and 336 deletions
|
@ -14,6 +14,7 @@ public class RealmRepresentation {
|
|||
protected String realm;
|
||||
protected int tokenLifespan;
|
||||
protected int accessCodeLifespan;
|
||||
protected int accessCodeLifespanUserAction;
|
||||
protected boolean enabled;
|
||||
protected boolean sslNotRequired;
|
||||
protected boolean cookieLoginAllowed;
|
||||
|
@ -190,6 +191,14 @@ public class RealmRepresentation {
|
|||
this.accessCodeLifespan = accessCodeLifespan;
|
||||
}
|
||||
|
||||
public int getAccessCodeLifespanUserAction() {
|
||||
return accessCodeLifespanUserAction;
|
||||
}
|
||||
|
||||
public void setAccessCodeLifespanUserAction(int accessCodeLifespanUserAction) {
|
||||
this.accessCodeLifespanUserAction = accessCodeLifespanUserAction;
|
||||
}
|
||||
|
||||
public List<RoleRepresentation> getRoles() {
|
||||
return roles;
|
||||
}
|
||||
|
|
|
@ -13,12 +13,13 @@ public class UserRepresentation {
|
|||
|
||||
protected String self; // link
|
||||
protected String username;
|
||||
protected boolean enabled;
|
||||
protected String status;
|
||||
protected String firstName;
|
||||
protected String lastName;
|
||||
protected String email;
|
||||
protected Map<String, String> attributes;
|
||||
protected List<CredentialRepresentation> credentials;
|
||||
protected List<String> requiredActions;
|
||||
|
||||
public String getSelf() {
|
||||
return self;
|
||||
|
@ -91,11 +92,19 @@ public class UserRepresentation {
|
|||
return this;
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public List<String> getRequiredActions() {
|
||||
return requiredActions;
|
||||
}
|
||||
|
||||
public void setRequiredActions(List<String> requiredActions) {
|
||||
this.requiredActions = requiredActions;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
"enabled": true,
|
||||
"tokenLifespan": 300,
|
||||
"accessCodeLifespan": 10,
|
||||
"accessCodeLifespanUserAction": 600,
|
||||
"sslNotRequired": true,
|
||||
"cookieLoginAllowed": true,
|
||||
"registrationAllowed": true,
|
||||
|
@ -17,7 +18,7 @@
|
|||
"users" : [
|
||||
{
|
||||
"username" : "bburke@redhat.com",
|
||||
"enabled" : true,
|
||||
"status": "ENABLED",
|
||||
"attributes" : {
|
||||
"email" : "bburke@redhat.com"
|
||||
},
|
||||
|
@ -28,7 +29,7 @@
|
|||
},
|
||||
{
|
||||
"username" : "third-party",
|
||||
"enabled" : true,
|
||||
"status": "ENABLED",
|
||||
"credentials" : [
|
||||
{ "type" : "password",
|
||||
"value" : "password" }
|
||||
|
|
|
@ -69,7 +69,7 @@ public class TotpBean {
|
|||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return "ENABLED".equals(user.getUser().getAttribute("KEYCLOAK_TOTP"));
|
||||
return user.getUser().isTotp();
|
||||
}
|
||||
|
||||
public String getTotpSecret() {
|
||||
|
|
|
@ -31,6 +31,7 @@ import javax.faces.context.FacesContext;
|
|||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.ws.rs.core.UriBuilder;
|
||||
|
||||
import org.keycloak.services.resources.flows.FormFlows;
|
||||
import org.keycloak.services.resources.flows.Urls;
|
||||
|
||||
/**
|
||||
|
@ -56,6 +57,11 @@ public class UrlBean {
|
|||
|
||||
UriBuilder b = UriBuilder.fromUri(request.getRequestURI()).replaceQuery(request.getQueryString())
|
||||
.replacePath(request.getContextPath()).path("rest");
|
||||
|
||||
if (request.getAttribute(FormFlows.CODE) != null) {
|
||||
b.queryParam("code", request.getAttribute(FormFlows.CODE));
|
||||
}
|
||||
|
||||
baseURI = b.build();
|
||||
}
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
<ui:composition xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core" xmlns:ui="http://java.sun.com/jsf/facelets"
|
||||
xmlns:c="http://java.sun.com/jstl/core" template="template-login.xhtml">
|
||||
|
||||
<ui:define name="title">Log in to #{realm.name}</ui:define>
|
||||
<ui:define name="header">Log in to <strong>#{realm.name}</strong></ui:define>
|
||||
|
||||
<ui:define name="form">
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
<ui:composition xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core" xmlns:ui="http://java.sun.com/jsf/facelets"
|
||||
xmlns:c="http://java.sun.com/jstl/core" template="template-login.xhtml">
|
||||
|
||||
<ui:define name="title">Log in to #{realm.name}</ui:define>
|
||||
<ui:define name="header">Log in to <strong>#{realm.name}</strong></ui:define>
|
||||
|
||||
<ui:define name="form">
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
<ui:param name="bodyClass" value="register" />
|
||||
|
||||
<ui:define name="title">#{messages.registerWith} #{realm.name}</ui:define>
|
||||
<ui:define name="header">#{messages.registerWith} <strong>#{realm.name}</strong></ui:define>
|
||||
|
||||
<ui:define name="form">
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
<h:head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<title>#{messages.logInTo} #{realm.name}</title>
|
||||
<title><ui:insert name="title" /></title>
|
||||
<link href="#{template.themeConfig['styles']}" rel="stylesheet" />
|
||||
<style>
|
||||
body {
|
||||
|
@ -34,7 +34,7 @@ body {
|
|||
<h:panelGroup rendered="#{not empty error.summary}">
|
||||
<div class="feedback error bottom-left show">
|
||||
<p>
|
||||
<strong>#{messages[error.summary]}</strong>
|
||||
<strong id="loginError">#{messages[error.summary]}</strong>
|
||||
</p>
|
||||
</div>
|
||||
</h:panelGroup>
|
||||
|
|
|
@ -12,6 +12,8 @@ import org.keycloak.representations.idm.CredentialRepresentation;
|
|||
import org.keycloak.services.models.RealmModel;
|
||||
import org.keycloak.services.models.RequiredCredentialModel;
|
||||
import org.keycloak.services.models.UserModel;
|
||||
import org.keycloak.services.models.UserModel.RequiredAction;
|
||||
import org.keycloak.services.models.UserModel.Status;
|
||||
import org.keycloak.services.resources.RealmsResource;
|
||||
import org.keycloak.services.resources.SaasService;
|
||||
|
||||
|
@ -197,7 +199,17 @@ public class AuthenticationManager {
|
|||
}
|
||||
}
|
||||
|
||||
public boolean authenticateForm(RealmModel realm, UserModel user, MultivaluedMap<String, String> formData) {
|
||||
public AuthenticationStatus authenticateForm(RealmModel realm, UserModel user, MultivaluedMap<String, String> formData) {
|
||||
if (user == null) {
|
||||
logger.info("Not Authenticated! Incorrect user name");
|
||||
return AuthenticationStatus.INVALID_USER;
|
||||
}
|
||||
|
||||
if (!user.isEnabled() && user.getStatus() == Status.DISABLED) {
|
||||
logger.info("Account is disabled, contact admin.");
|
||||
return AuthenticationStatus.ACCOUNT_DISABLED;
|
||||
}
|
||||
|
||||
Set<String> types = new HashSet<String>();
|
||||
|
||||
List<RequiredCredentialModel> requiredCredentials = null;
|
||||
|
@ -207,37 +219,54 @@ public class AuthenticationManager {
|
|||
requiredCredentials = realm.getRequiredOAuthClientCredentials();
|
||||
} else {
|
||||
requiredCredentials = realm.getRequiredCredentials();
|
||||
|
||||
if (!types.contains(CredentialRepresentation.TOTP) && "ENABLED".equals(user.getAttribute("KEYCLOAK_TOTP"))) {
|
||||
types.add(CredentialRepresentation.TOTP);
|
||||
}
|
||||
}
|
||||
|
||||
for (RequiredCredentialModel credential : requiredCredentials) {
|
||||
types.add(credential.getType());
|
||||
}
|
||||
|
||||
if (types.contains(CredentialRepresentation.TOTP) && !user.isTotp()) {
|
||||
user.addRequiredAction(RequiredAction.CONFIGURE_TOTP);
|
||||
user.setStatus(Status.ACTIONS_REQUIRED);
|
||||
}
|
||||
|
||||
if (types.contains(CredentialRepresentation.PASSWORD)) {
|
||||
String password = formData.getFirst(CredentialRepresentation.PASSWORD);
|
||||
if (password == null) {
|
||||
logger.warn("Password not provided");
|
||||
return false;
|
||||
return AuthenticationStatus.MISSING_PASSWORD;
|
||||
}
|
||||
|
||||
if (types.contains(CredentialRepresentation.TOTP)) {
|
||||
if (user.isTotp()) {
|
||||
String token = formData.getFirst(CredentialRepresentation.TOTP);
|
||||
if (token == null) {
|
||||
logger.warn("TOTP token not provided");
|
||||
return false;
|
||||
return AuthenticationStatus.MISSING_TOTP;
|
||||
}
|
||||
logger.info("validating TOTP");
|
||||
return realm.validateTOTP(user, password, token);
|
||||
if (!realm.validateTOTP(user, password, token)) {
|
||||
return AuthenticationStatus.INVALID_CREDENTIALS;
|
||||
}
|
||||
} else {
|
||||
logger.info("validating password for user: " + user.getLoginName());
|
||||
return realm.validatePassword(user, password);
|
||||
if (!realm.validatePassword(user, password)) {
|
||||
return AuthenticationStatus.INVALID_CREDENTIALS;
|
||||
}
|
||||
}
|
||||
|
||||
if (user.getStatus() == Status.ACTIONS_REQUIRED) {
|
||||
return AuthenticationStatus.ACTIONS_REQUIRED;
|
||||
} else {
|
||||
return AuthenticationStatus.SUCCESS;
|
||||
}
|
||||
} else {
|
||||
logger.warn("Do not know how to authenticate user");
|
||||
return false;
|
||||
return AuthenticationStatus.FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
public enum AuthenticationStatus {
|
||||
SUCCESS, ACCOUNT_DISABLED, ACTIONS_REQUIRED, INVALID_USER, INVALID_CREDENTIALS, MISSING_PASSWORD, MISSING_TOTP, FAILED
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package org.keycloak.services.managers;
|
|||
import org.jboss.resteasy.logging.Logger;
|
||||
import org.keycloak.representations.idm.*;
|
||||
import org.keycloak.services.models.*;
|
||||
import org.keycloak.services.models.UserModel.RequiredAction;
|
||||
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
|
@ -77,6 +78,7 @@ public class RealmManager {
|
|||
realm.setAutomaticRegistrationAfterSocialLogin(rep.isAutomaticRegistrationAfterSocialLogin());
|
||||
realm.setSslNotRequired((rep.isSslNotRequired()));
|
||||
realm.setAccessCodeLifespan(rep.getAccessCodeLifespan());
|
||||
realm.setAccessCodeLifespanUserAction(rep.getAccessCodeLifespanUserAction());
|
||||
realm.setTokenLifespan(rep.getTokenLifespan());
|
||||
if (rep.getRequiredOAuthClientCredentials() != null) {
|
||||
realm.updateRequiredOAuthClientCredentials(rep.getRequiredOAuthClientCredentials());
|
||||
|
@ -107,6 +109,7 @@ public class RealmManager {
|
|||
newRealm.setSocial(rep.isSocial());
|
||||
newRealm.setTokenLifespan(rep.getTokenLifespan());
|
||||
newRealm.setAccessCodeLifespan(rep.getAccessCodeLifespan());
|
||||
newRealm.setAccessCodeLifespanUserAction(rep.getAccessCodeLifespanUserAction());
|
||||
newRealm.setSslNotRequired(rep.isSslNotRequired());
|
||||
newRealm.setCookieLoginAllowed(rep.isCookieLoginAllowed());
|
||||
newRealm.setRegistrationAllowed(rep.isRegistrationAllowed());
|
||||
|
@ -206,12 +209,17 @@ public class RealmManager {
|
|||
|
||||
public UserModel createUser(RealmModel newRealm, UserRepresentation userRep) {
|
||||
UserModel user = newRealm.addUser(userRep.getUsername());
|
||||
user.setEnabled(userRep.isEnabled());
|
||||
user.setStatus(UserModel.Status.valueOf(userRep.getStatus()));
|
||||
if (userRep.getAttributes() != null) {
|
||||
for (Map.Entry<String, String> entry : userRep.getAttributes().entrySet()) {
|
||||
user.setAttribute(entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
if (userRep.getRequiredActions() != null) {
|
||||
for (String requiredAction : userRep.getRequiredActions()) {
|
||||
user.addRequiredAction(RequiredAction.valueOf(requiredAction));
|
||||
}
|
||||
}
|
||||
if (userRep.getCredentials() != null) {
|
||||
for (CredentialRepresentation cred : userRep.getCredentials()) {
|
||||
UserCredentialModel credential = new UserCredentialModel();
|
||||
|
@ -263,6 +271,7 @@ public class RealmManager {
|
|||
rep.setPublicKey(realm.getPublicKeyPem());
|
||||
rep.setTokenLifespan(realm.getTokenLifespan());
|
||||
rep.setAccessCodeLifespan(realm.getAccessCodeLifespan());
|
||||
rep.setAccessCodeLifespanUserAction(realm.getAccessCodeLifespanUserAction());
|
||||
List<RequiredCredentialModel> requiredCredentialModels = realm.getRequiredCredentials();
|
||||
if (requiredCredentialModels.size() > 0) {
|
||||
rep.setRequiredCredentials(new HashSet<String>());
|
||||
|
|
|
@ -19,7 +19,7 @@ public class UserManager {
|
|||
rep.setEmail(user.getEmail());
|
||||
rep.setLastName(user.getLastName());
|
||||
rep.setFirstName(user.getFirstName());
|
||||
rep.setEnabled(user.isEnabled());
|
||||
rep.setStatus(user.getStatus().name());
|
||||
rep.setUsername(user.getLoginName());
|
||||
for (Map.Entry<String, String> entry : user.getAttributes().entrySet()) {
|
||||
rep.attribute(entry.getKey(), entry.getValue());
|
||||
|
@ -29,7 +29,7 @@ public class UserManager {
|
|||
|
||||
public UserModel createUser(RealmModel newRealm, UserRepresentation userRep) {
|
||||
UserModel user = newRealm.addUser(userRep.getUsername());
|
||||
user.setEnabled(userRep.isEnabled());
|
||||
user.setStatus(UserModel.Status.valueOf(userRep.getStatus()));
|
||||
user.setEmail(userRep.getEmail());
|
||||
user.setFirstName(userRep.getFirstName());
|
||||
user.setLastName(userRep.getLastName());
|
||||
|
@ -56,7 +56,7 @@ public class UserManager {
|
|||
* @param userRep
|
||||
*/
|
||||
public void updateUserAsAdmin(UserModel user, UserRepresentation userRep) {
|
||||
user.setEnabled(userRep.isEnabled());
|
||||
user.setStatus(UserModel.Status.valueOf(userRep.getStatus()));
|
||||
user.setEmail(userRep.getEmail());
|
||||
user.setFirstName(userRep.getFirstName());
|
||||
user.setLastName(userRep.getLastName());
|
||||
|
@ -65,6 +65,11 @@ public class UserManager {
|
|||
user.setAttribute(entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
if (userRep.getAttributes() != null) {
|
||||
for (Map.Entry<String, String> entry : userRep.getAttributes().entrySet()) {
|
||||
user.setAttribute(entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -83,6 +88,11 @@ public class UserManager {
|
|||
user.setAttribute(entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
if (userRep.getAttributes() != null) {
|
||||
for (Map.Entry<String, String> entry : userRep.getAttributes().entrySet()) {
|
||||
user.setAttribute(entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -43,6 +43,10 @@ public interface RealmModel {
|
|||
|
||||
void setAccessCodeLifespan(int accessCodeLifespan);
|
||||
|
||||
int getAccessCodeLifespanUserAction();
|
||||
|
||||
void setAccessCodeLifespanUserAction(int accessCodeLifespanUserAction);
|
||||
|
||||
String getPublicKeyPem();
|
||||
|
||||
void setPublicKeyPem(String publicKeyPem);
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package org.keycloak.services.models;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
|
@ -7,15 +8,15 @@ import java.util.Map;
|
|||
* @version $Revision: 1 $
|
||||
*/
|
||||
public interface UserModel {
|
||||
public static final String LAST_NAME = "lastName";
|
||||
public static final String FIRST_NAME = "firstName";
|
||||
public static final String EMAIL = "email";
|
||||
|
||||
String getLoginName();
|
||||
|
||||
boolean isEnabled();
|
||||
|
||||
void setEnabled(boolean enabled);
|
||||
boolean isTotp();
|
||||
|
||||
Status getStatus();
|
||||
|
||||
void setStatus(Status status);
|
||||
|
||||
void setAttribute(String name, String value);
|
||||
|
||||
|
@ -25,6 +26,12 @@ public interface UserModel {
|
|||
|
||||
Map<String, String> getAttributes();
|
||||
|
||||
List<RequiredAction> getRequiredActions();
|
||||
|
||||
void addRequiredAction(RequiredAction action);
|
||||
|
||||
void removeRequiredAction(RequiredAction action);
|
||||
|
||||
String getFirstName();
|
||||
|
||||
void setFirstName(String firstName);
|
||||
|
@ -36,4 +43,14 @@ public interface UserModel {
|
|||
String getEmail();
|
||||
|
||||
void setEmail(String email);
|
||||
}
|
||||
|
||||
void setTotp(boolean totp);
|
||||
|
||||
public static enum Status {
|
||||
ENABLED, DISABLED, ACTIONS_REQUIRED
|
||||
}
|
||||
|
||||
public static enum RequiredAction {
|
||||
VERIFY_EMAIL, UPDATE_PROFILE, CONFIGURE_TOTP, RESET_PASSWORD
|
||||
}
|
||||
}
|
|
@ -188,6 +188,17 @@ public class RealmAdapter implements RealmModel {
|
|||
updateRealm();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAccessCodeLifespanUserAction() {
|
||||
return realm.getAccessCodeLifespanUserAction();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAccessCodeLifespanUserAction(int accessCodeLifespanUserAction) {
|
||||
realm.setAccessCodeLifespanUserAction(accessCodeLifespanUserAction);
|
||||
updateRealm();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPublicKeyPem() {
|
||||
return realm.getPublicKeyPem();
|
||||
|
|
|
@ -1,18 +1,26 @@
|
|||
package org.keycloak.services.models.picketlink;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.keycloak.services.models.UserModel;
|
||||
import org.keycloak.services.models.utils.ArrayUtils;
|
||||
import org.picketlink.idm.IdentityManager;
|
||||
import org.picketlink.idm.model.Attribute;
|
||||
import org.picketlink.idm.model.sample.User;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class UserAdapter implements UserModel {
|
||||
private static final String KEYCLOAK_TOTP_ATTR = "totpEnabled";
|
||||
private static final String REQUIRED_ACTIONS_ATTR = "requiredActions";
|
||||
private static final String STATUS_ATTR = "status";
|
||||
|
||||
protected User user;
|
||||
protected IdentityManager idm;
|
||||
|
||||
|
@ -35,9 +43,23 @@ public class UserAdapter implements UserModel {
|
|||
return user.isEnabled();
|
||||
}
|
||||
|
||||
public UserModel.Status getStatus() {
|
||||
Attribute<UserModel.Status> a = user.getAttribute(STATUS_ATTR);
|
||||
if (a != null) {
|
||||
return a.getValue();
|
||||
} else {
|
||||
return user.isEnabled() ? UserModel.Status.ENABLED : UserModel.Status.DISABLED;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEnabled(boolean enabled) {
|
||||
user.setEnabled(enabled);
|
||||
public void setStatus(UserModel.Status status) {
|
||||
user.setAttribute(new Attribute<UserModel.Status>(STATUS_ATTR, status));
|
||||
if (status == UserModel.Status.DISABLED) {
|
||||
user.setEnabled(false);
|
||||
} else {
|
||||
user.setEnabled(true);
|
||||
}
|
||||
idm.update(user);
|
||||
}
|
||||
|
||||
|
@ -101,4 +123,75 @@ public class UserAdapter implements UserModel {
|
|||
}
|
||||
return attributes;
|
||||
}
|
||||
|
||||
private RequiredAction[] getRequiredActionsArray() {
|
||||
Attribute<?> a = user.getAttribute(REQUIRED_ACTIONS_ATTR);
|
||||
if (a == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Object o = a.getValue();
|
||||
if (o instanceof RequiredAction) {
|
||||
return new RequiredAction[] { (RequiredAction) o };
|
||||
} else {
|
||||
return (RequiredAction[]) o;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<RequiredAction> getRequiredActions() {
|
||||
RequiredAction[] actions = getRequiredActionsArray();
|
||||
if (actions == null) {
|
||||
return null;
|
||||
} else {
|
||||
return Collections.unmodifiableList(Arrays.asList(actions));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addRequiredAction(RequiredAction action) {
|
||||
RequiredAction[] actions = getRequiredActionsArray();
|
||||
if (actions == null) {
|
||||
actions = new RequiredAction[] { action };
|
||||
} else {
|
||||
actions = ArrayUtils.add(actions, action);
|
||||
}
|
||||
|
||||
Attribute<RequiredAction[]> a = new Attribute<RequiredAction[]>(REQUIRED_ACTIONS_ATTR, actions);
|
||||
|
||||
user.setAttribute(a);
|
||||
idm.update(user);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeRequiredAction(RequiredAction action) {
|
||||
RequiredAction[] actions = getRequiredActionsArray();
|
||||
if (actions != null) {
|
||||
if (Arrays.binarySearch(actions, action) >= 0) {
|
||||
actions = ArrayUtils.remove(actions, action);
|
||||
|
||||
if (actions.length == 0) {
|
||||
user.removeAttribute(REQUIRED_ACTIONS_ATTR);
|
||||
} else {
|
||||
Attribute<RequiredAction[]> a = new Attribute<RequiredAction[]>(REQUIRED_ACTIONS_ATTR, actions);
|
||||
user.setAttribute(a);
|
||||
}
|
||||
|
||||
idm.update(user);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTotp() {
|
||||
Attribute<Boolean> a = user.getAttribute(KEYCLOAK_TOTP_ATTR);
|
||||
return a != null ? a.getValue() : false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTotp(boolean totp) {
|
||||
user.setAttribute(new Attribute<Boolean>(KEYCLOAK_TOTP_ATTR, totp));
|
||||
idm.update(user);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ public class RealmData extends AbstractPartition {
|
|||
private boolean automaticRegistrationAfterSocialLogin;
|
||||
private int tokenLifespan;
|
||||
private int accessCodeLifespan;
|
||||
private int accessCodeLifespanUserAction;
|
||||
private String publicKeyPem;
|
||||
private String privateKeyPem;
|
||||
private String[] defaultRoles;
|
||||
|
@ -109,6 +110,15 @@ public class RealmData extends AbstractPartition {
|
|||
this.accessCodeLifespan = accessCodeLifespan;
|
||||
}
|
||||
|
||||
@AttributeProperty
|
||||
public int getAccessCodeLifespanUserAction() {
|
||||
return accessCodeLifespanUserAction;
|
||||
}
|
||||
|
||||
public void setAccessCodeLifespanUserAction(int accessCodeLifespanUserAction) {
|
||||
this.accessCodeLifespanUserAction = accessCodeLifespanUserAction;
|
||||
}
|
||||
|
||||
@AttributeProperty
|
||||
public String getPublicKeyPem() {
|
||||
return publicKeyPem;
|
||||
|
|
|
@ -43,6 +43,8 @@ public class RealmEntity implements Serializable {
|
|||
@AttributeValue
|
||||
private int accessCodeLifespan;
|
||||
@AttributeValue
|
||||
private int accessCodeLifespanUserAction;
|
||||
@AttributeValue
|
||||
@Column(length = 2048)
|
||||
private String publicKeyPem;
|
||||
@AttributeValue
|
||||
|
@ -132,6 +134,14 @@ public class RealmEntity implements Serializable {
|
|||
this.accessCodeLifespan = accessCodeLifespan;
|
||||
}
|
||||
|
||||
public int getAccessCodeLifespanUserAction() {
|
||||
return accessCodeLifespanUserAction;
|
||||
}
|
||||
|
||||
public void setAccessCodeLifespanUserAction(int accessCodeLifespanUserAction) {
|
||||
this.accessCodeLifespanUserAction = accessCodeLifespanUserAction;
|
||||
}
|
||||
|
||||
public String getPublicKeyPem() {
|
||||
return publicKeyPem;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
package org.keycloak.services.models.utils;
|
||||
|
||||
import java.lang.reflect.Array;
|
||||
import java.util.Arrays;
|
||||
|
||||
public class ArrayUtils {
|
||||
|
||||
public static <T> T[] add(T[] src, T o) {
|
||||
T[] dst = Arrays.copyOf(src, src.length + 1);
|
||||
dst[src.length] = o;
|
||||
return dst;
|
||||
}
|
||||
|
||||
public static <T> T[] remove(T[] src, T o) {
|
||||
int l = Arrays.binarySearch(src, o);
|
||||
if (l < 0) {
|
||||
return src;
|
||||
}
|
||||
|
||||
T[] dst = newInstance(o, src.length - 1);
|
||||
System.arraycopy(src, 0, dst, 0, l);
|
||||
System.arraycopy(src, l + 1, dst, l, dst.length - l);
|
||||
return dst;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <T extends Object> T[] newInstance(T type, int length) {
|
||||
return (T[]) Array.newInstance(type.getClass(), length);
|
||||
}
|
||||
|
||||
}
|
|
@ -21,13 +21,18 @@
|
|||
*/
|
||||
package org.keycloak.services.resources;
|
||||
|
||||
import org.jboss.resteasy.jose.jws.JWSInput;
|
||||
import org.jboss.resteasy.jose.jws.crypto.RSAProvider;
|
||||
import org.jboss.resteasy.spi.HttpRequest;
|
||||
import org.keycloak.representations.idm.CredentialRepresentation;
|
||||
import org.keycloak.services.managers.AccessCodeEntry;
|
||||
import org.keycloak.services.managers.AuthenticationManager;
|
||||
import org.keycloak.services.managers.TokenManager;
|
||||
import org.keycloak.services.messages.Messages;
|
||||
import org.keycloak.services.models.RealmModel;
|
||||
import org.keycloak.services.models.UserCredentialModel;
|
||||
import org.keycloak.services.models.UserModel;
|
||||
import org.keycloak.services.models.UserModel.RequiredAction;
|
||||
import org.keycloak.services.resources.flows.Flows;
|
||||
import org.keycloak.services.resources.flows.FormFlows;
|
||||
import org.keycloak.services.validation.Validation;
|
||||
|
@ -39,6 +44,7 @@ import javax.ws.rs.POST;
|
|||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.core.*;
|
||||
import javax.ws.rs.core.Response.Status;
|
||||
import javax.ws.rs.ext.Providers;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
|
@ -56,16 +62,22 @@ public class AccountService {
|
|||
@Context
|
||||
private UriInfo uriInfo;
|
||||
|
||||
@Context
|
||||
protected Providers providers;
|
||||
|
||||
protected AuthenticationManager authManager = new AuthenticationManager();
|
||||
|
||||
public AccountService(RealmModel realm) {
|
||||
private TokenManager tokenManager;
|
||||
|
||||
public AccountService(RealmModel realm, TokenManager tokenManager) {
|
||||
this.realm = realm;
|
||||
this.tokenManager = tokenManager;
|
||||
}
|
||||
|
||||
@Path("access")
|
||||
@GET
|
||||
public Response accessPage() {
|
||||
UserModel user = authManager.authenticateIdentityCookie(realm, uriInfo, headers);
|
||||
UserModel user = getUserFromAuthManager();
|
||||
if (user != null) {
|
||||
return Flows.forms(realm, request).setUser(user).forwardToAccess();
|
||||
} else {
|
||||
|
@ -77,23 +89,79 @@ public class AccountService {
|
|||
@POST
|
||||
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
public Response processAccountUpdate(final MultivaluedMap<String, String> formData) {
|
||||
UserModel user = authManager.authenticateIdentityCookie(realm, uriInfo, headers);
|
||||
UserModel user = getUser(RequiredAction.UPDATE_PROFILE);
|
||||
if (user != null) {
|
||||
user.setFirstName(formData.getFirst("firstName"));
|
||||
user.setLastName(formData.getFirst("lastName"));
|
||||
user.setEmail(formData.getFirst("email"));
|
||||
|
||||
return Flows.forms(realm, request).setUser(user).forwardToAccount();
|
||||
Response response = redirectOauth();
|
||||
if (response != null) {
|
||||
return response;
|
||||
} else {
|
||||
return Flows.forms(realm, request).setUser(user).forwardToAccount();
|
||||
}
|
||||
} else {
|
||||
return Response.status(Status.FORBIDDEN).build();
|
||||
}
|
||||
}
|
||||
|
||||
private UserModel getUser(RequiredAction action) {
|
||||
if (uriInfo.getQueryParameters().containsKey(FormFlows.CODE)) {
|
||||
AccessCodeEntry accessCodeEntry = getAccessCodeEntry();
|
||||
if (accessCodeEntry == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
String loginName = accessCodeEntry.getUser().getLoginName();
|
||||
UserModel user = realm.getUser(loginName);
|
||||
if (!user.getRequiredActions().contains(action)) {
|
||||
return null;
|
||||
}
|
||||
return user;
|
||||
} else {
|
||||
return getUserFromAuthManager();
|
||||
}
|
||||
}
|
||||
|
||||
private UserModel getUserFromAuthManager() {
|
||||
return authManager.authenticateIdentityCookie(realm, uriInfo, headers);
|
||||
}
|
||||
|
||||
private AccessCodeEntry getAccessCodeEntry() {
|
||||
String code = uriInfo.getQueryParameters().getFirst(FormFlows.CODE);
|
||||
|
||||
JWSInput input = new JWSInput(code, providers);
|
||||
boolean verifiedCode = false;
|
||||
try {
|
||||
verifiedCode = RSAProvider.verify(input, realm.getPublicKey());
|
||||
} catch (Exception ignored) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!verifiedCode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String key = input.readContent(String.class);
|
||||
AccessCodeEntry accessCodeEntry = tokenManager.getAccessCode(key);
|
||||
if (accessCodeEntry == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (accessCodeEntry.isExpired()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return accessCodeEntry;
|
||||
}
|
||||
|
||||
@Path("totp")
|
||||
@POST
|
||||
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
public Response processTotpUpdate(final MultivaluedMap<String, String> formData) {
|
||||
UserModel user = authManager.authenticateIdentityCookie(realm, uriInfo, headers);
|
||||
UserModel user = getUser(RequiredAction.CONFIGURE_TOTP);
|
||||
if (user != null) {
|
||||
FormFlows forms = Flows.forms(realm, request);
|
||||
|
||||
|
@ -109,7 +177,7 @@ public class AccountService {
|
|||
}
|
||||
|
||||
if (error != null) {
|
||||
return forms.setError(error).forwardToTotp();
|
||||
return forms.setError(error).setUser(user).forwardToTotp();
|
||||
}
|
||||
|
||||
UserCredentialModel credentials = new UserCredentialModel();
|
||||
|
@ -117,23 +185,38 @@ public class AccountService {
|
|||
credentials.setValue(formData.getFirst("totpSecret"));
|
||||
realm.updateCredential(user, credentials);
|
||||
|
||||
if (!user.isEnabled() && "REQUIRED".equals(user.getAttribute("KEYCLOAK_TOTP"))) {
|
||||
user.setEnabled(true);
|
||||
user.removeRequiredAction(UserModel.RequiredAction.CONFIGURE_TOTP);
|
||||
|
||||
user.setTotp(true);
|
||||
|
||||
Response response = redirectOauth();
|
||||
if (response != null) {
|
||||
return response;
|
||||
} else {
|
||||
return Flows.forms(realm, request).setUser(user).forwardToTotp();
|
||||
}
|
||||
|
||||
user.setAttribute("KEYCLOAK_TOTP", "ENABLED");
|
||||
|
||||
return Flows.forms(realm, request).setUser(user).forwardToTotp();
|
||||
} else {
|
||||
return Response.status(Status.FORBIDDEN).build();
|
||||
}
|
||||
}
|
||||
|
||||
private Response redirectOauth() {
|
||||
String redirect = uriInfo.getQueryParameters().getFirst("redirect_uri");
|
||||
if (redirect != null) {
|
||||
AccessCodeEntry accessCode = getAccessCodeEntry();
|
||||
String state = uriInfo.getQueryParameters().getFirst("state");
|
||||
return Flows.oauth(realm, request, uriInfo, authManager, tokenManager).redirectAccessCode(accessCode, state,
|
||||
redirect);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Path("password")
|
||||
@POST
|
||||
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
public Response processPasswordUpdate(final MultivaluedMap<String, String> formData) {
|
||||
UserModel user = authManager.authenticateIdentityCookie(realm, uriInfo, headers);
|
||||
UserModel user = getUser(RequiredAction.RESET_PASSWORD);
|
||||
if (user != null) {
|
||||
FormFlows forms = Flows.forms(realm, request).setUser(user);
|
||||
|
||||
|
@ -163,7 +246,12 @@ public class AccountService {
|
|||
|
||||
realm.updateCredential(user, credentials);
|
||||
|
||||
return Flows.forms(realm, request).setUser(user).forwardToPassword();
|
||||
Response response = redirectOauth();
|
||||
if (response != null) {
|
||||
return response;
|
||||
} else {
|
||||
return Flows.forms(realm, request).setUser(user).forwardToPassword();
|
||||
}
|
||||
} else {
|
||||
return Response.status(Status.FORBIDDEN).build();
|
||||
}
|
||||
|
@ -172,7 +260,7 @@ public class AccountService {
|
|||
@Path("")
|
||||
@GET
|
||||
public Response accountPage() {
|
||||
UserModel user = authManager.authenticateIdentityCookie(realm, uriInfo, headers);
|
||||
UserModel user = getUserFromAuthManager();
|
||||
if (user != null) {
|
||||
return Flows.forms(realm, request).setUser(user).forwardToAccount();
|
||||
} else {
|
||||
|
@ -183,7 +271,7 @@ public class AccountService {
|
|||
@Path("social")
|
||||
@GET
|
||||
public Response socialPage() {
|
||||
UserModel user = authManager.authenticateIdentityCookie(realm, uriInfo, headers);
|
||||
UserModel user = getUserFromAuthManager();
|
||||
if (user != null) {
|
||||
return Flows.forms(realm, request).setUser(user).forwardToSocial();
|
||||
} else {
|
||||
|
@ -194,7 +282,7 @@ public class AccountService {
|
|||
@Path("totp")
|
||||
@GET
|
||||
public Response totpPage() {
|
||||
UserModel user = authManager.authenticateIdentityCookie(realm, uriInfo, headers);
|
||||
UserModel user = getUserFromAuthManager();
|
||||
if (user != null) {
|
||||
return Flows.forms(realm, request).setUser(user).forwardToTotp();
|
||||
} else {
|
||||
|
@ -205,7 +293,7 @@ public class AccountService {
|
|||
@Path("password")
|
||||
@GET
|
||||
public Response passwordPage() {
|
||||
UserModel user = authManager.authenticateIdentityCookie(realm, uriInfo, headers);
|
||||
UserModel user = getUserFromAuthManager();
|
||||
if (user != null) {
|
||||
return Flows.forms(realm, request).setUser(user).forwardToPassword();
|
||||
} else {
|
||||
|
|
|
@ -66,7 +66,7 @@ public class RealmsResource {
|
|||
logger.debug("realm not found");
|
||||
throw new NotFoundException();
|
||||
}
|
||||
AccountService accountService = new AccountService(realm);
|
||||
AccountService accountService = new AccountService(realm, tokenManager);
|
||||
resourceContext.initResource(accountService);
|
||||
return accountService;
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import org.jboss.resteasy.spi.NotImplementedYetException;
|
|||
import org.keycloak.representations.idm.CredentialRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.services.managers.AuthenticationManager;
|
||||
import org.keycloak.services.managers.AuthenticationManager.AuthenticationStatus;
|
||||
import org.keycloak.services.managers.RealmManager;
|
||||
import org.keycloak.services.messages.Messages;
|
||||
import org.keycloak.services.models.*;
|
||||
|
@ -217,29 +218,20 @@ public class SaasService {
|
|||
}
|
||||
String username = formData.getFirst("username");
|
||||
UserModel user = realm.getUser(username);
|
||||
if (user == null) {
|
||||
logger.info("Not Authenticated! Incorrect user name");
|
||||
|
||||
return Flows.forms(realm, request).setError(Messages.INVALID_USER).setFormData(formData)
|
||||
.forwardToLogin();
|
||||
AuthenticationStatus status = authManager.authenticateForm(realm, user, formData);
|
||||
|
||||
switch (status) {
|
||||
case SUCCESS:
|
||||
NewCookie cookie = authManager.createSaasIdentityCookie(realm, user, uriInfo);
|
||||
return Response.status(302).cookie(cookie).location(contextRoot(uriInfo).path(adminPath).build()).build();
|
||||
case ACCOUNT_DISABLED:
|
||||
return Flows.forms(realm, request).setError(Messages.ACCOUNT_DISABLED).setFormData(formData).forwardToLogin();
|
||||
case ACTIONS_REQUIRED:
|
||||
return Flows.forms(realm, request).forwardToAction(user.getRequiredActions().get(0));
|
||||
default:
|
||||
return Flows.forms(realm, request).setError(Messages.INVALID_USER).setFormData(formData).forwardToLogin();
|
||||
}
|
||||
if (!user.isEnabled()) {
|
||||
logger.info("Account is disabled, contact admin.");
|
||||
|
||||
return Flows.forms(realm, request).setError(Messages.ACCOUNT_DISABLED)
|
||||
.setFormData(formData).forwardToLogin();
|
||||
}
|
||||
|
||||
boolean authenticated = authManager.authenticateForm(realm, user, formData);
|
||||
if (!authenticated) {
|
||||
logger.info("Not Authenticated! Invalid credentials");
|
||||
|
||||
return Flows.forms(realm, request).setError(Messages.INVALID_PASSWORD).setFormData(formData)
|
||||
.forwardToLogin();
|
||||
}
|
||||
|
||||
NewCookie cookie = authManager.createSaasIdentityCookie(realm, user, uriInfo);
|
||||
return Response.status(302).cookie(cookie).location(contextRoot(uriInfo).path(adminPath).build()).build();
|
||||
}
|
||||
|
||||
@Path("registrations")
|
||||
|
|
|
@ -14,6 +14,7 @@ import org.keycloak.representations.idm.CredentialRepresentation;
|
|||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.services.managers.AccessCodeEntry;
|
||||
import org.keycloak.services.managers.AuthenticationManager;
|
||||
import org.keycloak.services.managers.AuthenticationManager.AuthenticationStatus;
|
||||
import org.keycloak.services.managers.RealmManager;
|
||||
import org.keycloak.services.managers.ResourceAdminManager;
|
||||
import org.keycloak.services.managers.TokenManager;
|
||||
|
@ -44,6 +45,7 @@ import javax.ws.rs.core.UriInfo;
|
|||
import javax.ws.rs.ext.Providers;
|
||||
|
||||
import java.security.PrivateKey;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
@ -132,15 +134,12 @@ public class TokenService {
|
|||
throw new NotAuthorizedException("Disabled realm");
|
||||
}
|
||||
UserModel user = realm.getUser(username);
|
||||
if (user == null) {
|
||||
throw new NotAuthorizedException("No user");
|
||||
}
|
||||
if (!user.isEnabled()) {
|
||||
throw new NotAuthorizedException("Disabled user.");
|
||||
}
|
||||
if (!authManager.authenticateForm(realm, user, form)) {
|
||||
throw new NotAuthorizedException("FORM");
|
||||
|
||||
AuthenticationStatus status = authManager.authenticateForm(realm, user, form);
|
||||
if (status != AuthenticationStatus.SUCCESS) {
|
||||
throw new NotAuthorizedException(status);
|
||||
}
|
||||
|
||||
tokenManager = new TokenManager();
|
||||
SkeletonKeyToken token = authManager.createIdentityToken(realm, username);
|
||||
String encoded = tokenManager.encodeToken(realm, token);
|
||||
|
@ -167,7 +166,7 @@ public class TokenService {
|
|||
if (!user.isEnabled()) {
|
||||
throw new NotAuthorizedException("Disabled user.");
|
||||
}
|
||||
if (authManager.authenticateForm(realm, user, form)) {
|
||||
if (authManager.authenticateForm(realm, user, form) != AuthenticationStatus.SUCCESS) {
|
||||
throw new NotAuthorizedException("Auth failed");
|
||||
}
|
||||
SkeletonKeyToken token = tokenManager.createAccessToken(realm, user);
|
||||
|
@ -194,38 +193,23 @@ public class TokenService {
|
|||
if (!client.isEnabled()) {
|
||||
return oauth.forwardToSecurityFailure("Login requester not enabled.");
|
||||
}
|
||||
|
||||
String username = formData.getFirst("username");
|
||||
UserModel user = realm.getUser(username);
|
||||
if (user == null) {
|
||||
logger.error("Incorrect user name.");
|
||||
|
||||
return Flows.forms(realm, request).setError(Messages.INVALID_USER).setFormData(formData)
|
||||
.forwardToLogin();
|
||||
AuthenticationStatus status = authManager.authenticateForm(realm, user, formData);
|
||||
|
||||
switch (status) {
|
||||
case SUCCESS:
|
||||
case ACTIONS_REQUIRED:
|
||||
return oauth.processAccessCode(scopeParam, state, redirect, client, user);
|
||||
case ACCOUNT_DISABLED:
|
||||
return Flows.forms(realm, request).setError(Messages.ACCOUNT_DISABLED).setFormData(formData).forwardToLogin();
|
||||
case MISSING_TOTP:
|
||||
return Flows.forms(realm, request).setFormData(formData).forwardToLoginTotp();
|
||||
default:
|
||||
return Flows.forms(realm, request).setError(Messages.INVALID_USER).setFormData(formData).forwardToLogin();
|
||||
}
|
||||
|
||||
if (!user.isEnabled()) {
|
||||
return oauth.forwardToSecurityFailure("Your account is not enabled.");
|
||||
}
|
||||
|
||||
if ("ENABLED".equals(user.getAttribute("KEYCLOAK_TOTP")) && Validation.isEmpty(formData.getFirst("totp"))) {
|
||||
return Flows.forms(realm, request).setFormData(formData).forwardToLoginTotp();
|
||||
} else {
|
||||
for (RequiredCredentialModel c : realm.getRequiredCredentials()) {
|
||||
if (c.getType().equals(CredentialRepresentation.TOTP)) {
|
||||
return Flows.forms(realm, request).forwardToTotp();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
boolean authenticated = authManager.authenticateForm(realm, user, formData);
|
||||
if (!authenticated) {
|
||||
logger.error("Authentication failed");
|
||||
|
||||
return Flows.forms(realm, request).setError(Messages.INVALID_PASSWORD).setFormData(formData)
|
||||
.forwardToLogin();
|
||||
}
|
||||
|
||||
return oauth.processAccessCode(scopeParam, state, redirect, client, user);
|
||||
}
|
||||
|
||||
@Path("registrations")
|
||||
|
@ -369,8 +353,8 @@ public class TokenService {
|
|||
return Response.status(Response.Status.BAD_REQUEST).entity(error).type("application/json").build();
|
||||
}
|
||||
|
||||
boolean authenticated = authManager.authenticateForm(realm, client, formData);
|
||||
if (!authenticated) {
|
||||
AuthenticationStatus status = authManager.authenticateForm(realm, client, formData);
|
||||
if (status != AuthenticationStatus.SUCCESS) {
|
||||
Map<String, String> error = new HashMap<String, String>();
|
||||
error.put("error", "unauthorized_client");
|
||||
return Response.status(Response.Status.BAD_REQUEST).entity(error).type("application/json").build();
|
||||
|
|
|
@ -24,6 +24,7 @@ package org.keycloak.services.resources.flows;
|
|||
import org.jboss.resteasy.spi.HttpRequest;
|
||||
import org.keycloak.services.models.RealmModel;
|
||||
import org.keycloak.services.models.UserModel;
|
||||
import org.keycloak.services.models.UserModel.RequiredAction;
|
||||
import org.picketlink.idm.model.sample.Realm;
|
||||
|
||||
import javax.ws.rs.core.MultivaluedMap;
|
||||
|
@ -39,6 +40,7 @@ public class FormFlows {
|
|||
public static final String REALM = Realm.class.getName();
|
||||
public static final String USER = UserModel.class.getName();
|
||||
public static final String SOCIAL_REGISTRATION = "socialRegistration";
|
||||
public static final String CODE = "code";
|
||||
|
||||
private String error;
|
||||
private MultivaluedMap<String, String> formData;
|
||||
|
@ -49,12 +51,26 @@ public class FormFlows {
|
|||
private UserModel userModel;
|
||||
|
||||
private boolean socialRegistration;
|
||||
private String code;
|
||||
|
||||
FormFlows(RealmModel realm, HttpRequest request) {
|
||||
this.realm = realm;
|
||||
this.request = request;
|
||||
}
|
||||
|
||||
public Response forwardToAction(RequiredAction action) {
|
||||
switch (action) {
|
||||
case CONFIGURE_TOTP:
|
||||
return forwardToTotp();
|
||||
case UPDATE_PROFILE:
|
||||
return forwardToAccount();
|
||||
case RESET_PASSWORD:
|
||||
return forwardToPassword();
|
||||
default:
|
||||
return null; // TODO
|
||||
}
|
||||
}
|
||||
|
||||
public Response forwardToAccess() {
|
||||
return forwardToForm(Pages.ACCESS);
|
||||
}
|
||||
|
@ -78,6 +94,10 @@ public class FormFlows {
|
|||
request.setAttribute(USER, userModel);
|
||||
}
|
||||
|
||||
if (code != null) {
|
||||
request.setAttribute(CODE, code);
|
||||
}
|
||||
|
||||
request.setAttribute(SOCIAL_REGISTRATION, socialRegistration);
|
||||
|
||||
request.forward(form);
|
||||
|
@ -108,6 +128,11 @@ public class FormFlows {
|
|||
return forwardToForm(Pages.TOTP);
|
||||
}
|
||||
|
||||
public FormFlows setCode(String code) {
|
||||
this.code = code;
|
||||
return this;
|
||||
}
|
||||
|
||||
public FormFlows setError(String error) {
|
||||
this.error = error;
|
||||
return this;
|
||||
|
|
|
@ -93,7 +93,16 @@ public class OAuthFlows {
|
|||
return oauthGrantPage(accessCode, client);
|
||||
}
|
||||
|
||||
return redirectAccessCode(accessCode, state, redirect);
|
||||
if (user.getRequiredActions() != null) {
|
||||
accessCode.setExpiration(System.currentTimeMillis() / 1000 + realm.getAccessCodeLifespanUserAction());
|
||||
return Flows.forms(realm, request).setCode(accessCode.getCode()).setUser(user).forwardToAction(user.getRequiredActions().get(0));
|
||||
}
|
||||
|
||||
if (redirect != null) {
|
||||
return redirectAccessCode(accessCode, state, redirect);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public Response oauthGrantPage(AccessCodeEntry accessCode, UserModel client) {
|
||||
|
|
14
services/src/test/java/TotpUtil.java
Normal file
14
services/src/test/java/TotpUtil.java
Normal file
|
@ -0,0 +1,14 @@
|
|||
import org.picketlink.common.util.Base32;
|
||||
import org.picketlink.idm.credential.util.TimeBasedOTP;
|
||||
|
||||
public class TotpUtil {
|
||||
|
||||
public static void main(String[] args) {
|
||||
String google = "PJBX GURY NZIT C2JX I44T S3D2 JBKD G6SB";
|
||||
google = google.replace(" ", "");
|
||||
google = new String(Base32.decode(google));
|
||||
TimeBasedOTP otp = new TimeBasedOTP();
|
||||
System.out.println(otp.generate(google));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,175 @@
|
|||
package org.keycloak.services.managers;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import javax.ws.rs.core.MultivaluedHashMap;
|
||||
import javax.ws.rs.core.MultivaluedMap;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.representations.idm.CredentialRepresentation;
|
||||
import org.keycloak.services.managers.AuthenticationManager.AuthenticationStatus;
|
||||
import org.keycloak.services.models.KeycloakSession;
|
||||
import org.keycloak.services.models.KeycloakSessionFactory;
|
||||
import org.keycloak.services.models.RealmModel;
|
||||
import org.keycloak.services.models.UserCredentialModel;
|
||||
import org.keycloak.services.models.UserModel;
|
||||
import org.keycloak.services.models.UserModel.Status;
|
||||
import org.keycloak.services.resources.KeycloakApplication;
|
||||
import org.picketlink.idm.credential.util.TimeBasedOTP;
|
||||
|
||||
public class AuthenticationManagerTest {
|
||||
|
||||
private RealmManager adapter;
|
||||
private AuthenticationManager am;
|
||||
private KeycloakSessionFactory factory;
|
||||
private MultivaluedMap<String, String> formData;
|
||||
private KeycloakSession identitySession;
|
||||
private TimeBasedOTP otp;
|
||||
private RealmModel realm;
|
||||
private UserModel user;
|
||||
|
||||
@After
|
||||
public void after() throws Exception {
|
||||
identitySession.getTransaction().commit();
|
||||
identitySession.close();
|
||||
factory.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authForm() {
|
||||
AuthenticationStatus status = am.authenticateForm(realm, user, formData);
|
||||
Assert.assertEquals(AuthenticationStatus.SUCCESS, status);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authFormInvalidPassword() {
|
||||
formData.remove(CredentialRepresentation.PASSWORD);
|
||||
formData.add(CredentialRepresentation.PASSWORD, "invalid");
|
||||
|
||||
AuthenticationStatus status = am.authenticateForm(realm, user, formData);
|
||||
Assert.assertEquals(AuthenticationStatus.INVALID_CREDENTIALS, status);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authFormMissingPassword() {
|
||||
formData.remove(CredentialRepresentation.PASSWORD);
|
||||
|
||||
AuthenticationStatus status = am.authenticateForm(realm, user, formData);
|
||||
Assert.assertEquals(AuthenticationStatus.MISSING_PASSWORD, status);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authFormMissingTotp() {
|
||||
realm.addRequiredCredential(CredentialRepresentation.TOTP);
|
||||
|
||||
AuthenticationStatus status = am.authenticateForm(realm, user, formData);
|
||||
Assert.assertEquals(AuthenticationStatus.ACTIONS_REQUIRED, status);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authFormUserDisabled() {
|
||||
user.setStatus(Status.DISABLED);
|
||||
|
||||
AuthenticationStatus status = am.authenticateForm(realm, user, formData);
|
||||
Assert.assertEquals(AuthenticationStatus.ACCOUNT_DISABLED, status);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authFormUserRequiredActions() {
|
||||
user.setStatus(Status.ACTIONS_REQUIRED);
|
||||
|
||||
AuthenticationStatus status = am.authenticateForm(realm, user, formData);
|
||||
Assert.assertEquals(AuthenticationStatus.ACTIONS_REQUIRED, 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);
|
||||
|
||||
realm.updateCredential(user, credential);
|
||||
|
||||
user.setTotp(true);
|
||||
|
||||
String token = otp.generate(totpSecret);
|
||||
|
||||
formData.add(CredentialRepresentation.TOTP, token);
|
||||
|
||||
AuthenticationStatus status = am.authenticateForm(realm, user, formData);
|
||||
Assert.assertEquals(AuthenticationStatus.SUCCESS, status);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authFormWithTotpInvalidPassword() {
|
||||
authFormWithTotp();
|
||||
|
||||
formData.remove(CredentialRepresentation.PASSWORD);
|
||||
formData.add(CredentialRepresentation.PASSWORD, "invalid");
|
||||
|
||||
AuthenticationStatus status = am.authenticateForm(realm, user, formData);
|
||||
Assert.assertEquals(AuthenticationStatus.INVALID_CREDENTIALS, status);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authFormWithTotpInvalidTotp() {
|
||||
authFormWithTotp();
|
||||
|
||||
formData.remove(CredentialRepresentation.TOTP);
|
||||
formData.add(CredentialRepresentation.TOTP, "invalid");
|
||||
|
||||
AuthenticationStatus status = am.authenticateForm(realm, user, formData);
|
||||
Assert.assertEquals(AuthenticationStatus.INVALID_CREDENTIALS, status);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authFormWithTotpMissingTotp() {
|
||||
authFormWithTotp();
|
||||
|
||||
formData.remove(CredentialRepresentation.TOTP);
|
||||
|
||||
AuthenticationStatus status = am.authenticateForm(realm, user, formData);
|
||||
Assert.assertEquals(AuthenticationStatus.MISSING_TOTP, status);
|
||||
}
|
||||
|
||||
@Before
|
||||
public void before() throws Exception {
|
||||
factory = KeycloakApplication.buildSessionFactory();
|
||||
identitySession = factory.createSession();
|
||||
identitySession.getTransaction().begin();
|
||||
adapter = new RealmManager(identitySession);
|
||||
|
||||
realm = adapter.createRealm("Test");
|
||||
realm.setAccessCodeLifespan(100);
|
||||
realm.setCookieLoginAllowed(true);
|
||||
realm.setEnabled(true);
|
||||
realm.setName("Test");
|
||||
realm.setPrivateKeyPem("0234234");
|
||||
realm.setPublicKeyPem("0234234");
|
||||
realm.setTokenLifespan(1000);
|
||||
realm.addRequiredCredential(CredentialRepresentation.PASSWORD);
|
||||
|
||||
am = new AuthenticationManager();
|
||||
|
||||
user = realm.addUser("test");
|
||||
|
||||
UserCredentialModel credential = new UserCredentialModel();
|
||||
credential.setType(CredentialRepresentation.PASSWORD);
|
||||
credential.setValue("password");
|
||||
|
||||
realm.updateCredential(user, credential);
|
||||
|
||||
formData = new MultivaluedHashMap<String, String>();
|
||||
formData.add(CredentialRepresentation.PASSWORD, "password");
|
||||
|
||||
otp = new TimeBasedOTP();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
package org.keycloak.services.models.utils;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
public class ArrayUtilsTest {
|
||||
|
||||
@Test
|
||||
public void add() {
|
||||
String[] a = new String[] { "a" };
|
||||
a = ArrayUtils.add(a, "b");
|
||||
Assert.assertArrayEquals(new String[] { "a", "b" }, a);
|
||||
|
||||
a = ArrayUtils.add(a, "c");
|
||||
Assert.assertArrayEquals(new String[] { "a", "b", "c" }, a);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void remove() {
|
||||
String[] a = new String[] { "a", "b", "c", "d" };
|
||||
|
||||
a = ArrayUtils.remove(a, "b");
|
||||
Assert.assertArrayEquals(new String[] { "a", "c", "d" }, a);
|
||||
|
||||
a = ArrayUtils.remove(a, "d");
|
||||
Assert.assertArrayEquals(new String[] { "a", "c" }, a);
|
||||
|
||||
a = ArrayUtils.remove(a, "a");
|
||||
Assert.assertArrayEquals(new String[] { "c" }, a);
|
||||
|
||||
a = ArrayUtils.remove(a, "c");
|
||||
Assert.assertArrayEquals(new String[] {}, a);
|
||||
}
|
||||
|
||||
}
|
|
@ -18,6 +18,7 @@ import org.keycloak.services.models.UserCredentialModel;
|
|||
import org.keycloak.services.resources.KeycloakApplication;
|
||||
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
@ -69,6 +70,7 @@ public class AdapterTest {
|
|||
public void test1CreateRealm() throws Exception {
|
||||
realmModel = adapter.createRealm("JUGGLER");
|
||||
realmModel.setAccessCodeLifespan(100);
|
||||
realmModel.setAccessCodeLifespanUserAction(600);
|
||||
realmModel.setCookieLoginAllowed(true);
|
||||
realmModel.setEnabled(true);
|
||||
realmModel.setName("JUGGLER");
|
||||
|
@ -82,6 +84,7 @@ public class AdapterTest {
|
|||
realmModel = adapter.getRealm(realmModel.getId());
|
||||
Assert.assertNotNull(realmModel);
|
||||
Assert.assertEquals(realmModel.getAccessCodeLifespan(), 100);
|
||||
Assert.assertEquals(600, realmModel.getAccessCodeLifespanUserAction());
|
||||
Assert.assertEquals(realmModel.getTokenLifespan(), 1000);
|
||||
Assert.assertEquals(realmModel.isEnabled(), true);
|
||||
Assert.assertEquals(realmModel.getName(), "JUGGLER");
|
||||
|
@ -150,5 +153,54 @@ public class AdapterTest {
|
|||
Assert.assertEquals("user", role.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUserStatus() throws Exception {
|
||||
test1CreateRealm();
|
||||
|
||||
UserModel user = realmModel.addUser("bburke");
|
||||
Assert.assertTrue(user.isEnabled());
|
||||
Assert.assertEquals(UserModel.Status.ENABLED, user.getStatus());
|
||||
|
||||
user.setStatus(UserModel.Status.DISABLED);
|
||||
user = realmModel.getUser("bburke");
|
||||
|
||||
Assert.assertFalse(user.isEnabled());
|
||||
Assert.assertEquals(UserModel.Status.DISABLED, user.getStatus());
|
||||
|
||||
user.setStatus(UserModel.Status.ACTIONS_REQUIRED);
|
||||
user = realmModel.getUser("bburke");
|
||||
|
||||
Assert.assertTrue(user.isEnabled());
|
||||
Assert.assertEquals(UserModel.Status.ACTIONS_REQUIRED, user.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUserRequiredActions() throws Exception {
|
||||
test1CreateRealm();
|
||||
|
||||
UserModel user = realmModel.addUser("bburke");
|
||||
|
||||
Assert.assertNull(user.getRequiredActions());
|
||||
|
||||
user.addRequiredAction(UserModel.RequiredAction.CONFIGURE_TOTP);
|
||||
user = realmModel.getUser("bburke");
|
||||
|
||||
Assert.assertEquals(Arrays.asList(UserModel.RequiredAction.CONFIGURE_TOTP), user.getRequiredActions());
|
||||
|
||||
user.addRequiredAction(UserModel.RequiredAction.VERIFY_EMAIL);
|
||||
user = realmModel.getUser("bburke");
|
||||
|
||||
Assert.assertEquals(Arrays.asList(UserModel.RequiredAction.CONFIGURE_TOTP, UserModel.RequiredAction.VERIFY_EMAIL),
|
||||
user.getRequiredActions());
|
||||
|
||||
user.removeRequiredAction(UserModel.RequiredAction.CONFIGURE_TOTP);
|
||||
user = realmModel.getUser("bburke");
|
||||
|
||||
Assert.assertEquals(Arrays.asList(UserModel.RequiredAction.VERIFY_EMAIL), user.getRequiredActions());
|
||||
|
||||
user.removeRequiredAction(UserModel.RequiredAction.VERIFY_EMAIL);
|
||||
user = realmModel.getUser("bburke");
|
||||
|
||||
Assert.assertNull(user.getRequiredActions());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -56,6 +56,7 @@ public class ImportTest {
|
|||
defaultRealm.setEnabled(true);
|
||||
defaultRealm.setTokenLifespan(300);
|
||||
defaultRealm.setAccessCodeLifespan(60);
|
||||
defaultRealm.setAccessCodeLifespanUserAction(600);
|
||||
defaultRealm.setSslNotRequired(false);
|
||||
defaultRealm.setCookieLoginAllowed(true);
|
||||
defaultRealm.setRegistrationAllowed(true);
|
||||
|
@ -131,6 +132,7 @@ public class ImportTest {
|
|||
defaultRealm.setEnabled(true);
|
||||
defaultRealm.setTokenLifespan(300);
|
||||
defaultRealm.setAccessCodeLifespan(60);
|
||||
defaultRealm.setAccessCodeLifespanUserAction(600);
|
||||
defaultRealm.setSslNotRequired(false);
|
||||
defaultRealm.setCookieLoginAllowed(true);
|
||||
defaultRealm.setRegistrationAllowed(true);
|
||||
|
@ -147,6 +149,7 @@ public class ImportTest {
|
|||
realm.addRealmAdmin(admin);
|
||||
|
||||
Assert.assertTrue(realm.isAutomaticRegistrationAfterSocialLogin());
|
||||
Assert.assertEquals(600, realm.getAccessCodeLifespanUserAction());
|
||||
verifyRequiredCredentials(realm.getRequiredCredentials(), "password");
|
||||
verifyRequiredCredentials(realm.getRequiredApplicationCredentials(), "totp");
|
||||
verifyRequiredCredentials(realm.getRequiredOAuthClientCredentials(), "cert");
|
||||
|
|
|
@ -18,6 +18,7 @@ public class InstallationManager {
|
|||
defaultRealm.setEnabled(true);
|
||||
defaultRealm.setTokenLifespan(300);
|
||||
defaultRealm.setAccessCodeLifespan(60);
|
||||
defaultRealm.setAccessCodeLifespanUserAction(600);
|
||||
defaultRealm.setSslNotRequired(false);
|
||||
defaultRealm.setCookieLoginAllowed(true);
|
||||
defaultRealm.setRegistrationAllowed(true);
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
"enabled": true,
|
||||
"tokenLifespan": 300,
|
||||
"accessCodeLifespan": 10,
|
||||
"accessCodeLifespanUserAction": 600,
|
||||
"sslNotRequired": true,
|
||||
"cookieLoginAllowed": true,
|
||||
"automaticRegistrationAfterSocialLogin": true,
|
||||
|
@ -14,7 +15,7 @@
|
|||
"users" : [
|
||||
{
|
||||
"username" : "bburke@redhat.com",
|
||||
"enabled" : true,
|
||||
"status": "ENABLED",
|
||||
"attributes" : {
|
||||
"email" : "bburke@redhat.com"
|
||||
},
|
||||
|
@ -25,7 +26,7 @@
|
|||
},
|
||||
{
|
||||
"username" : "third-party",
|
||||
"enabled" : true,
|
||||
"status": "ENABLED",
|
||||
"credentials" : [
|
||||
{ "type" : "Password",
|
||||
"value" : "password" }
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
"enabled": true,
|
||||
"tokenLifespan": 6000,
|
||||
"accessCodeLifespan": 30,
|
||||
"accessCodeLifespanUserAction": 600,
|
||||
"requiredCredentials": [ "password" ],
|
||||
"requiredApplicationCredentials": [ "password" ],
|
||||
"requiredOAuthClientCredentials": [ "password" ],
|
||||
|
@ -10,7 +11,7 @@
|
|||
"users": [
|
||||
{
|
||||
"username": "wburke",
|
||||
"enabled": true,
|
||||
"status": "ENABLED",
|
||||
"attributes": {
|
||||
"email": "bburke@redhat.com"
|
||||
},
|
||||
|
@ -23,7 +24,7 @@
|
|||
},
|
||||
{
|
||||
"username": "loginclient",
|
||||
"enabled": true,
|
||||
"status": "ENABLED",
|
||||
"credentials": [
|
||||
{
|
||||
"type": "password",
|
||||
|
@ -33,7 +34,7 @@
|
|||
},
|
||||
{
|
||||
"username": "admin",
|
||||
"enabled": true,
|
||||
"status": "ENABLED",
|
||||
"credentials": [
|
||||
{
|
||||
"type": "password",
|
||||
|
@ -43,7 +44,7 @@
|
|||
},
|
||||
{
|
||||
"username": "oauthclient",
|
||||
"enabled": true,
|
||||
"status": "ENABLED",
|
||||
"credentials": [
|
||||
{
|
||||
"type": "password",
|
||||
|
@ -53,7 +54,7 @@
|
|||
},
|
||||
{
|
||||
"username": "mySocialUser",
|
||||
"enabled": true
|
||||
"status": "ENABLED"
|
||||
}
|
||||
],
|
||||
"roleMappings": [
|
||||
|
|
24
testsuite/README.md
Normal file
24
testsuite/README.md
Normal file
|
@ -0,0 +1,24 @@
|
|||
Executing testsuite
|
||||
===================
|
||||
|
||||
Currently the testsuite is not executed as part of a regular build. To run the testsuite first you need to do a "mvn clean install" on the project. Then you need to install JBoss AS 7.1.1.Final and upgrade Resteasy (see descriptions in examples/as7-eap-demo/README.md).
|
||||
|
||||
The tests can either be run in remote (JBoss AS already running) or managed (tests starts/stops JBoss AS).
|
||||
|
||||
To run tests in remote mode:
|
||||
|
||||
mvn clean install -Pjboss-remote
|
||||
|
||||
To run tests in managed mode:
|
||||
|
||||
export JBOSS_HOME=<path to JBoss AS 7.1.1.Final with upgraded Resteasy>
|
||||
mvn clean install -Pjboss-managed
|
||||
|
||||
When running tests in the testsuite from an IDE it is best to use the remote mode.
|
||||
|
||||
Browser
|
||||
-------
|
||||
|
||||
The testsuite uses Arquillian Drone and Graphene. By default it uses the headless PhantomJS webkit, but it is also possible to run it with other browsers. For example using Firefox or Chrome is good if you want to step-through a test to see what's actually going in.
|
||||
|
||||
To run the tests with Firefox add `-Dbrowser=firefox` or for Chrome add `-Dbrowser=chrome`
|
|
@ -120,40 +120,22 @@
|
|||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jboss.arquillian.extension</groupId>
|
||||
<artifactId>arquillian-drone-impl</artifactId>
|
||||
<artifactId>arquillian-drone-webdriver-depchain</artifactId>
|
||||
<type>pom</type>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jboss.arquillian.extension</groupId>
|
||||
<artifactId>arquillian-drone-selenium</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jboss.arquillian.extension</groupId>
|
||||
<artifactId>arquillian-drone-selenium-server</artifactId>
|
||||
<groupId>org.jboss.arquillian.graphene</groupId>
|
||||
<artifactId>graphene-webdriver</artifactId>
|
||||
<type>pom</type>
|
||||
<scope>test</scope>
|
||||
<version>2.0.0.Beta1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jboss.shrinkwrap.resolver</groupId>
|
||||
<artifactId>shrinkwrap-resolver-impl-maven</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.seleniumhq.selenium</groupId>
|
||||
<artifactId>selenium-java</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.seleniumhq.selenium</groupId>
|
||||
<artifactId>selenium-server</artifactId>
|
||||
<scope>test</scope>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.mortbay.jetty</groupId>
|
||||
<artifactId>servlet-api-2.5</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<build>
|
||||
<plugins>
|
||||
|
|
|
@ -21,20 +21,15 @@
|
|||
*/
|
||||
package org.keycloak.testsuite;
|
||||
|
||||
import java.io.File;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
|
||||
import org.jboss.arquillian.container.test.api.Deployment;
|
||||
import org.jboss.arquillian.drone.api.annotation.Drone;
|
||||
import org.jboss.shrinkwrap.api.ShrinkWrap;
|
||||
import org.jboss.arquillian.graphene.page.Page;
|
||||
import org.jboss.shrinkwrap.api.spec.WebArchive;
|
||||
import org.jboss.shrinkwrap.resolver.api.maven.Maven;
|
||||
import org.junit.After;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
|
||||
import com.thoughtworks.selenium.DefaultSelenium;
|
||||
import org.keycloak.testsuite.pages.AppPage;
|
||||
import org.keycloak.testsuite.pages.LoginPage;
|
||||
import org.keycloak.testsuite.pages.RegisterPage;
|
||||
import org.openqa.selenium.WebDriver;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
|
@ -43,112 +38,33 @@ public abstract class AbstractDroneTest {
|
|||
|
||||
@Deployment(name = "app", testable = false, order = 2)
|
||||
public static WebArchive appDeployment() {
|
||||
File[] libs = Maven.resolver().loadPomFromFile("pom.xml")
|
||||
.resolve("org.keycloak:keycloak-core", "org.keycloak:keycloak-as7-adapter").withoutTransitivity().asFile();
|
||||
|
||||
WebArchive archive = ShrinkWrap.create(WebArchive.class, "app.war").addClasses(TestApplication.class)
|
||||
.addAsLibraries(libs).addAsWebInfResource("jboss-deployment-structure.xml")
|
||||
.addAsWebInfResource("app-web.xml", "web.xml").addAsWebInfResource("app-jboss-web.xml", "jboss-web.xml")
|
||||
.addAsWebInfResource("app-resteasy-oauth.json", "resteasy-oauth.json").addAsWebResource("user.jsp");
|
||||
return archive;
|
||||
return Deployments.appDeployment();
|
||||
}
|
||||
|
||||
@Deployment(name = "auth-server", testable = false, order = 1)
|
||||
public static WebArchive deployment() {
|
||||
File[] libs = Maven.resolver().loadPomFromFile("pom.xml").importRuntimeDependencies().resolve().withTransitivity()
|
||||
.asFile();
|
||||
|
||||
WebArchive archive = ShrinkWrap.create(WebArchive.class, "auth-server.war").addClasses(TestApplication.class)
|
||||
.addAsLibraries(libs).addAsWebInfResource("jboss-deployment-structure.xml").addAsWebInfResource("web.xml")
|
||||
.addAsResource("persistence.xml", "META-INF/persistence.xml")
|
||||
.addAsResource("testrealm.json", "META-INF/testrealm.json");
|
||||
|
||||
return archive;
|
||||
return Deployments.deployment();
|
||||
}
|
||||
|
||||
URL appUrl;
|
||||
|
||||
URL authServerUrl;
|
||||
|
||||
String DEFAULT_WAIT = "10000";
|
||||
@Page
|
||||
protected AppPage appPage;
|
||||
|
||||
@Drone
|
||||
DefaultSelenium selenium;
|
||||
protected WebDriver browser;
|
||||
|
||||
@Page
|
||||
protected LoginPage loginPage;
|
||||
|
||||
@Page
|
||||
protected RegisterPage registerPage;
|
||||
|
||||
@After
|
||||
public void after() {
|
||||
logout();
|
||||
}
|
||||
|
||||
@Before
|
||||
public void before() throws MalformedURLException {
|
||||
authServerUrl = new URL("http://localhost:8080/auth-server");
|
||||
appUrl = new URL("http://localhost:8080/app/user.jsp");
|
||||
}
|
||||
|
||||
public void login(String username, String password) {
|
||||
login(username, password, null);
|
||||
}
|
||||
|
||||
public void login(String username, String password, String expectErrorMessage) {
|
||||
selenium.open(appUrl.toString());
|
||||
selenium.waitForPageToLoad(DEFAULT_WAIT);
|
||||
|
||||
Assert.assertEquals("Log in to demo", selenium.getTitle());
|
||||
|
||||
if (username != null) {
|
||||
selenium.type("id=username", username);
|
||||
}
|
||||
|
||||
if (password != null) {
|
||||
selenium.type("id=password", password);
|
||||
}
|
||||
|
||||
selenium.click("css=input[type=\"submit\"]");
|
||||
|
||||
selenium.waitForPageToLoad(DEFAULT_WAIT);
|
||||
|
||||
if (expectErrorMessage == null) {
|
||||
Assert.assertEquals(username, selenium.getText("id=user"));
|
||||
} else {
|
||||
Assert.assertTrue(selenium.isTextPresent(expectErrorMessage));
|
||||
}
|
||||
}
|
||||
|
||||
public void logout() {
|
||||
selenium.open(authServerUrl + "/rest/realms/demo/tokens/logout?redirect_uri=" + appUrl);
|
||||
selenium.waitForPageToLoad(DEFAULT_WAIT);
|
||||
|
||||
Assert.assertEquals("Log in to demo", selenium.getTitle());
|
||||
}
|
||||
|
||||
public void registerUser(String username, String password) {
|
||||
registerUser(username, password, null);
|
||||
}
|
||||
|
||||
public void registerUser(String username, String password, String expectErrorMessage) {
|
||||
selenium.open(appUrl.toString());
|
||||
selenium.waitForPageToLoad(DEFAULT_WAIT);
|
||||
|
||||
selenium.click("link=Register");
|
||||
selenium.waitForPageToLoad(DEFAULT_WAIT);
|
||||
selenium.type("id=name", "Test User");
|
||||
selenium.type("id=email", "test@user.com");
|
||||
if (username != null) {
|
||||
selenium.type("id=username", username);
|
||||
}
|
||||
if (password != null) {
|
||||
selenium.type("id=password", password);
|
||||
selenium.type("id=password-confirm", password);
|
||||
}
|
||||
selenium.click("css=input[type=\"submit\"]");
|
||||
selenium.waitForPageToLoad(DEFAULT_WAIT);
|
||||
|
||||
if (expectErrorMessage == null) {
|
||||
Assert.assertEquals(username, selenium.getText("id=user"));
|
||||
} else {
|
||||
Assert.assertTrue(selenium.isTextPresent(expectErrorMessage));
|
||||
appPage.open();
|
||||
if (appPage.isCurrent()) {
|
||||
appPage.logout();
|
||||
}
|
||||
browser.manage().deleteAllCookies();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -34,42 +34,42 @@ public class AccountTest extends AbstractDroneTest {
|
|||
|
||||
@Test
|
||||
public void changePassword() {
|
||||
registerUser("changePassword", "password");
|
||||
|
||||
selenium.open(authServerUrl + "/rest/realms/demo/account/password");
|
||||
selenium.waitForPageToLoad(DEFAULT_WAIT);
|
||||
|
||||
Assert.assertTrue(selenium.isTextPresent("Change Password"));
|
||||
|
||||
selenium.type("id=password", "password");
|
||||
selenium.type("id=password-new", "newpassword");
|
||||
selenium.type("id=password-confirm", "newpassword");
|
||||
selenium.click("css=input[type=\"submit\"]");
|
||||
selenium.waitForPageToLoad(DEFAULT_WAIT);
|
||||
|
||||
logout();
|
||||
|
||||
login("changePassword", "password", "Invalid username or password");
|
||||
login("changePassword", "newpassword");
|
||||
// registerUser("changePassword", "password");
|
||||
//
|
||||
// browser.open(authServerUrl + "/rest/realms/demo/account/password");
|
||||
// browser.waitForPageToLoad(DEFAULT_WAIT);
|
||||
//
|
||||
// Assert.assertTrue(browser.isTextPresent("Change Password"));
|
||||
//
|
||||
// browser.type("id=password", "password");
|
||||
// browser.type("id=password-new", "newpassword");
|
||||
// browser.type("id=password-confirm", "newpassword");
|
||||
// browser.click("css=input[type=\"submit\"]");
|
||||
// browser.waitForPageToLoad(DEFAULT_WAIT);
|
||||
//
|
||||
// logout();
|
||||
//
|
||||
// login("changePassword", "password", "Invalid username or password");
|
||||
// login("changePassword", "newpassword");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void changeProfile() {
|
||||
registerUser("changeProfile", "password");
|
||||
|
||||
selenium.open(authServerUrl + "/rest/realms/demo/account");
|
||||
selenium.waitForPageToLoad(DEFAULT_WAIT);
|
||||
|
||||
selenium.type("id=firstName", "Newfirst");
|
||||
selenium.type("id=lastName", "Newlast");
|
||||
selenium.type("id=email", "new@email.com");
|
||||
|
||||
selenium.click("css=input[type=\"submit\"]");
|
||||
selenium.waitForPageToLoad(DEFAULT_WAIT);
|
||||
|
||||
Assert.assertEquals("Newfirst", selenium.getValue("id=firstName"));
|
||||
Assert.assertEquals("Newlast", selenium.getValue("id=lastName"));
|
||||
Assert.assertEquals("new@email.com", selenium.getValue("id=email"));
|
||||
// registerUser("changeProfile", "password");
|
||||
//
|
||||
// browser.open(authServerUrl + "/rest/realms/demo/account");
|
||||
// browser.waitForPageToLoad(DEFAULT_WAIT);
|
||||
//
|
||||
// browser.type("id=firstName", "Newfirst");
|
||||
// browser.type("id=lastName", "Newlast");
|
||||
// browser.type("id=email", "new@email.com");
|
||||
//
|
||||
// browser.click("css=input[type=\"submit\"]");
|
||||
// browser.waitForPageToLoad(DEFAULT_WAIT);
|
||||
//
|
||||
// Assert.assertEquals("Newfirst", browser.getValue("id=firstName"));
|
||||
// Assert.assertEquals("Newlast", browser.getValue("id=lastName"));
|
||||
// Assert.assertEquals("new@email.com", browser.getValue("id=email"));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
package org.keycloak.testsuite;
|
||||
|
||||
public class Constants {
|
||||
|
||||
public static String APP_ROOT = "http://localhost:8080/app";
|
||||
|
||||
public static String AUTH_SERVER_ROOT = "http://localhost:8080/auth-server";
|
||||
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
package org.keycloak.testsuite;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import org.jboss.shrinkwrap.api.ShrinkWrap;
|
||||
import org.jboss.shrinkwrap.api.spec.WebArchive;
|
||||
import org.jboss.shrinkwrap.resolver.api.maven.Maven;
|
||||
|
||||
public class Deployments {
|
||||
public static WebArchive appDeployment() {
|
||||
File[] libs = Maven.resolver().loadPomFromFile("pom.xml")
|
||||
.resolve("org.keycloak:keycloak-core", "org.keycloak:keycloak-as7-adapter").withoutTransitivity().asFile();
|
||||
|
||||
WebArchive archive = ShrinkWrap.create(WebArchive.class, "app.war").addClasses(TestApplication.class)
|
||||
.addAsLibraries(libs).addAsWebInfResource("jboss-deployment-structure.xml")
|
||||
.addAsWebInfResource("app-web.xml", "web.xml").addAsWebInfResource("app-jboss-web.xml", "jboss-web.xml")
|
||||
.addAsWebInfResource("app-resteasy-oauth.json", "resteasy-oauth.json").addAsWebResource("user.jsp");
|
||||
return archive;
|
||||
}
|
||||
|
||||
public static WebArchive deployment() {
|
||||
File[] libs = Maven.resolver().loadPomFromFile("pom.xml").importRuntimeDependencies().resolve().withTransitivity()
|
||||
.asFile();
|
||||
|
||||
WebArchive archive = ShrinkWrap.create(WebArchive.class, "auth-server.war").addClasses(TestApplication.class)
|
||||
.addAsLibraries(libs).addAsWebInfResource("jboss-deployment-structure.xml").addAsWebInfResource("web.xml")
|
||||
.addAsResource("persistence.xml", "META-INF/persistence.xml")
|
||||
.addAsResource("testrealm.json", "META-INF/testrealm.json");
|
||||
|
||||
return archive;
|
||||
}
|
||||
}
|
|
@ -22,6 +22,7 @@
|
|||
package org.keycloak.testsuite;
|
||||
|
||||
import org.jboss.arquillian.junit.Arquillian;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
|
@ -33,17 +34,42 @@ public class LoginTest extends AbstractDroneTest {
|
|||
|
||||
@Test
|
||||
public void loginInvalidPassword() {
|
||||
login("invalid", "password", "Invalid username or password");
|
||||
appPage.open();
|
||||
|
||||
Assert.assertTrue(loginPage.isCurrent());
|
||||
loginPage.login("bburke@redhat.com", "invalid");
|
||||
|
||||
Assert.assertEquals("Invalid username or password", loginPage.getError());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loginInvalidUsername() {
|
||||
login("invalid", "password", "Invalid username or password");
|
||||
appPage.open();
|
||||
|
||||
Assert.assertTrue(loginPage.isCurrent());
|
||||
loginPage.login("invalid", "password");
|
||||
|
||||
Assert.assertEquals("Invalid username or password", loginPage.getError());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loginSuccess() {
|
||||
login("bburke@redhat.com", "password");
|
||||
appPage.open();
|
||||
|
||||
Assert.assertTrue(loginPage.isCurrent());
|
||||
loginPage.login("bburke@redhat.com", "password");
|
||||
|
||||
Assert.assertTrue(appPage.isCurrent());
|
||||
Assert.assertEquals("bburke@redhat.com", appPage.getUser());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void logout() {
|
||||
loginSuccess();
|
||||
appPage.logout();
|
||||
|
||||
appPage.open();
|
||||
Assert.assertTrue(loginPage.isCurrent());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
package org.keycloak.testsuite;
|
||||
|
||||
import org.jboss.arquillian.junit.Arquillian;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
|
@ -31,19 +32,67 @@ import org.junit.runner.RunWith;
|
|||
@RunWith(Arquillian.class)
|
||||
public class RegisterTest extends AbstractDroneTest {
|
||||
|
||||
@Test
|
||||
public void registerExistingUser() {
|
||||
appPage.open();
|
||||
loginPage.register();
|
||||
|
||||
Assert.assertTrue(registerPage.isCurrent());
|
||||
|
||||
registerPage.register("name", "email", "username", null, null);
|
||||
|
||||
Assert.assertTrue(registerPage.isCurrent());
|
||||
Assert.assertEquals("Please specify password", registerPage.getError());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void registerUserInvalidPasswordConfirm() {
|
||||
appPage.open();
|
||||
loginPage.register();
|
||||
|
||||
Assert.assertTrue(registerPage.isCurrent());
|
||||
|
||||
registerPage.register("name", "email", "bburke@redhat.com", "password", "invalid");
|
||||
|
||||
Assert.assertTrue(registerPage.isCurrent());
|
||||
Assert.assertEquals("Password confirmation doesn't match", registerPage.getError());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void registerUserMissingPassword() {
|
||||
registerUser("registerUserMissingPassword", null, "Please specify password");
|
||||
appPage.open();
|
||||
loginPage.register();
|
||||
|
||||
Assert.assertTrue(registerPage.isCurrent());
|
||||
|
||||
registerPage.register("name", "email", "username", null, null);
|
||||
|
||||
Assert.assertTrue(registerPage.isCurrent());
|
||||
Assert.assertEquals("Please specify password", registerPage.getError());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void registerUserMissingUsername() {
|
||||
registerUser(null, "password", "Please specify username");
|
||||
appPage.open();
|
||||
loginPage.register();
|
||||
|
||||
Assert.assertTrue(registerPage.isCurrent());
|
||||
|
||||
registerPage.register("name", "email", null, "password", "password");
|
||||
|
||||
Assert.assertTrue(registerPage.isCurrent());
|
||||
Assert.assertEquals("Please specify username", registerPage.getError());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void registerUserSuccess() {
|
||||
registerUser("registerUserSuccess", "password");
|
||||
appPage.open();
|
||||
loginPage.register();
|
||||
|
||||
Assert.assertTrue(registerPage.isCurrent());
|
||||
|
||||
registerPage.register("name", "email", "username", "password", "password");
|
||||
Assert.assertTrue(appPage.isCurrent());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
* 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;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
|
||||
import org.jboss.arquillian.container.test.api.Deployment;
|
||||
import org.jboss.arquillian.drone.api.annotation.Drone;
|
||||
import org.jboss.arquillian.graphene.page.Page;
|
||||
import org.jboss.arquillian.junit.Arquillian;
|
||||
import org.jboss.shrinkwrap.api.spec.WebArchive;
|
||||
import org.junit.After;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.keycloak.testsuite.pages.AppPage;
|
||||
import org.keycloak.testsuite.pages.LoginPage;
|
||||
import org.keycloak.testsuite.pages.RegisterPage;
|
||||
import org.keycloak.testsuite.pages.TotpPage;
|
||||
import org.openqa.selenium.WebDriver;
|
||||
import org.picketlink.idm.credential.util.TimeBasedOTP;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
@RunWith(Arquillian.class)
|
||||
public class RequiredActionTotpSetupTest {
|
||||
|
||||
@Deployment(name = "app", testable = false, order = 2)
|
||||
public static WebArchive appDeployment() {
|
||||
return Deployments.appDeployment();
|
||||
}
|
||||
|
||||
@Deployment(name = "auth-server", testable = false, order = 1)
|
||||
public static WebArchive deployment() {
|
||||
return Deployments.deployment().addAsResource("testrealm-totp.json", "META-INF/testrealm.json");
|
||||
}
|
||||
|
||||
@Page
|
||||
protected AppPage appPage;
|
||||
|
||||
@Drone
|
||||
protected WebDriver browser;
|
||||
|
||||
@Page
|
||||
protected TotpPage totpPage;
|
||||
|
||||
@Page
|
||||
protected LoginPage loginPage;
|
||||
|
||||
@Page
|
||||
protected RegisterPage registerPage;
|
||||
|
||||
protected TimeBasedOTP totp;
|
||||
|
||||
@Before
|
||||
public void before() throws MalformedURLException {
|
||||
totp = new TimeBasedOTP();
|
||||
}
|
||||
|
||||
@After
|
||||
public void after() {
|
||||
appPage.open();
|
||||
if (appPage.isCurrent()) {
|
||||
appPage.logout();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setupTotp() {
|
||||
appPage.open();
|
||||
|
||||
loginPage.register();
|
||||
registerPage.register("name", "email", "setupTotp", "password", "password");
|
||||
|
||||
Assert.assertTrue(totpPage.isCurrent());
|
||||
|
||||
totpPage.configure(totp.generate(totpPage.getTotpSecret()));
|
||||
|
||||
Assert.assertTrue(appPage.isCurrent());
|
||||
Assert.assertEquals("setupTotp", appPage.getUser());
|
||||
}
|
||||
|
||||
}
|
|
@ -23,11 +23,14 @@ package org.keycloak.testsuite;
|
|||
|
||||
import java.net.MalformedURLException;
|
||||
|
||||
import org.jboss.arquillian.graphene.page.Page;
|
||||
import org.jboss.arquillian.junit.Arquillian;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.keycloak.testsuite.pages.LoginTotpPage;
|
||||
import org.keycloak.testsuite.pages.TotpPage;
|
||||
import org.picketlink.idm.credential.util.TimeBasedOTP;
|
||||
|
||||
/**
|
||||
|
@ -36,80 +39,77 @@ import org.picketlink.idm.credential.util.TimeBasedOTP;
|
|||
@RunWith(Arquillian.class)
|
||||
public class TotpTest extends AbstractDroneTest {
|
||||
|
||||
@Page
|
||||
private LoginTotpPage loginTotpPage;
|
||||
|
||||
private TimeBasedOTP totp;
|
||||
private String totpSecret;
|
||||
|
||||
@Page
|
||||
protected TotpPage totpPage;
|
||||
|
||||
@Before
|
||||
public void before() throws MalformedURLException {
|
||||
super.before();
|
||||
|
||||
totp = new TimeBasedOTP();
|
||||
}
|
||||
|
||||
public void configureTotp() {
|
||||
selenium.open(authServerUrl + "/rest/realms/demo/account/totp");
|
||||
selenium.waitForPageToLoad("10000");
|
||||
|
||||
Assert.assertTrue(selenium.isTextPresent("To setup Google Authenticator"));
|
||||
|
||||
totpSecret = selenium.getValue("totpSecret");
|
||||
String code = totp.generate(totpSecret);
|
||||
|
||||
selenium.type("id=totp", code);
|
||||
selenium.click("css=input[type=\"submit\"]");
|
||||
selenium.waitForPageToLoad("30000");
|
||||
|
||||
Assert.assertTrue(selenium.isTextPresent("Google Authenticator enabled"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loginWithTotpFailure() {
|
||||
registerUser("loginWithTotpFailure", "password");
|
||||
configureTotp();
|
||||
logout();
|
||||
appPage.open();
|
||||
loginPage.register();
|
||||
registerPage.register("name", "email", "loginWithTotpFailure", "password", "password");
|
||||
totpPage.open();
|
||||
|
||||
selenium.type("id=username", "loginWithTotpFailure");
|
||||
selenium.type("id=password", "password");
|
||||
String totpSecret = totpPage.getTotpSecret();
|
||||
totpPage.configure(totp.generate(totpSecret));
|
||||
|
||||
selenium.click("css=input[type=\"submit\"]");
|
||||
selenium.waitForPageToLoad(DEFAULT_WAIT);
|
||||
appPage.open();
|
||||
appPage.logout();
|
||||
|
||||
Assert.assertEquals("Log in to demo", selenium.getTitle());
|
||||
loginPage.login("loginWithTotpSuccess", "password");
|
||||
|
||||
selenium.type("id=totp", "123456");
|
||||
Assert.assertFalse(appPage.isCurrent());
|
||||
|
||||
selenium.click("css=input[type=\"submit\"]");
|
||||
selenium.waitForPageToLoad(DEFAULT_WAIT);
|
||||
loginTotpPage.login("123456");
|
||||
|
||||
Assert.assertTrue(selenium.isTextPresent("Invalid username or password"));
|
||||
Assert.assertTrue(loginTotpPage.isCurrent());
|
||||
Assert.assertEquals("Invalid username or password", loginTotpPage.getError());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loginWithTotpSuccess() {
|
||||
registerUser("loginWithTotpSuccess", "password");
|
||||
configureTotp();
|
||||
logout();
|
||||
appPage.open();
|
||||
loginPage.register();
|
||||
registerPage.register("name", "email", "loginWithTotpSuccess", "password", "password");
|
||||
totpPage.open();
|
||||
|
||||
selenium.type("id=username", "loginWithTotpSuccess");
|
||||
selenium.type("id=password", "password");
|
||||
String totpSecret = totpPage.getTotpSecret();
|
||||
totpPage.configure(totp.generate(totpSecret));
|
||||
|
||||
selenium.click("css=input[type=\"submit\"]");
|
||||
selenium.waitForPageToLoad(DEFAULT_WAIT);
|
||||
appPage.open();
|
||||
appPage.logout();
|
||||
|
||||
Assert.assertEquals("Log in to demo", selenium.getTitle());
|
||||
loginPage.login("loginWithTotpSuccess", "password");
|
||||
|
||||
selenium.type("id=totp", totp.generate(totpSecret));
|
||||
Assert.assertFalse(appPage.isCurrent());
|
||||
|
||||
selenium.click("css=input[type=\"submit\"]");
|
||||
selenium.waitForPageToLoad(DEFAULT_WAIT);
|
||||
loginTotpPage.login(totp.generate(totpSecret));
|
||||
|
||||
Assert.assertEquals("loginWithTotpSuccess", selenium.getText("id=user"));
|
||||
Assert.assertTrue(appPage.isCurrent());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setupTotp() {
|
||||
registerUser("setupTotp", "password");
|
||||
configureTotp();
|
||||
appPage.open();
|
||||
loginPage.register();
|
||||
registerPage.register("name", "email", "setupTotp", "password", "password");
|
||||
|
||||
totpPage.open();
|
||||
|
||||
Assert.assertTrue(totpPage.isCurrent());
|
||||
|
||||
totpPage.configure(totp.generate(totpPage.getTotpSecret()));
|
||||
|
||||
Assert.assertTrue(browser.getPageSource().contains("Google Authenticator enabled"));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
package org.keycloak.testsuite.pages;
|
||||
|
||||
import org.jboss.arquillian.drone.api.annotation.Drone;
|
||||
import org.keycloak.testsuite.Constants;
|
||||
import org.openqa.selenium.WebDriver;
|
||||
import org.openqa.selenium.WebElement;
|
||||
import org.openqa.selenium.support.FindBy;
|
||||
|
||||
public class AppPage {
|
||||
|
||||
private static String PATH = Constants.APP_ROOT + "/user.jsp";
|
||||
|
||||
@Drone
|
||||
private WebDriver browser;
|
||||
|
||||
@FindBy(id = "logout")
|
||||
private WebElement logoutLink;
|
||||
|
||||
@FindBy(id = "user")
|
||||
private WebElement user;
|
||||
|
||||
public void open() {
|
||||
browser.navigate().to(PATH);
|
||||
}
|
||||
|
||||
public String getUser() {
|
||||
return user.getText();
|
||||
}
|
||||
|
||||
public boolean isCurrent() {
|
||||
return browser.getCurrentUrl().equals(PATH);
|
||||
}
|
||||
|
||||
public void logout() {
|
||||
logoutLink.click();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
package org.keycloak.testsuite.pages;
|
||||
|
||||
import org.jboss.arquillian.drone.api.annotation.Drone;
|
||||
import org.openqa.selenium.WebDriver;
|
||||
import org.openqa.selenium.WebElement;
|
||||
import org.openqa.selenium.support.FindBy;
|
||||
|
||||
public class LoginPage {
|
||||
|
||||
@Drone
|
||||
private WebDriver browser;
|
||||
|
||||
@FindBy(id = "username")
|
||||
private WebElement usernameInput;
|
||||
|
||||
@FindBy(id = "password")
|
||||
private WebElement passwordInput;
|
||||
|
||||
@FindBy(css = "input[type=\"submit\"]")
|
||||
private WebElement submitButton;
|
||||
|
||||
@FindBy(linkText = "Register")
|
||||
private WebElement registerLink;
|
||||
|
||||
@FindBy(id = "loginError")
|
||||
private WebElement loginErrorMessage;
|
||||
|
||||
public void login(String username, String password) {
|
||||
usernameInput.sendKeys(username);
|
||||
passwordInput.sendKeys(password);
|
||||
|
||||
submitButton.click();
|
||||
}
|
||||
|
||||
public String getError() {
|
||||
return loginErrorMessage != null ? loginErrorMessage.getText() : null;
|
||||
}
|
||||
|
||||
public boolean isCurrent() {
|
||||
return browser.getTitle().equals("Log in to demo");
|
||||
}
|
||||
|
||||
public void register() {
|
||||
registerLink.click();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
package org.keycloak.testsuite.pages;
|
||||
|
||||
import org.jboss.arquillian.drone.api.annotation.Drone;
|
||||
import org.openqa.selenium.WebDriver;
|
||||
import org.openqa.selenium.WebElement;
|
||||
import org.openqa.selenium.support.FindBy;
|
||||
|
||||
public class LoginTotpPage {
|
||||
|
||||
@Drone
|
||||
private WebDriver browser;
|
||||
|
||||
@FindBy(id = "totp")
|
||||
private WebElement totpInput;
|
||||
|
||||
@FindBy(css = "input[type=\"submit\"]")
|
||||
private WebElement submitButton;
|
||||
|
||||
@FindBy(linkText = "Register")
|
||||
private WebElement registerLink;
|
||||
|
||||
@FindBy(id = "loginError")
|
||||
private WebElement loginErrorMessage;
|
||||
|
||||
public void login(String totp) {
|
||||
totpInput.sendKeys(totp);
|
||||
|
||||
submitButton.click();
|
||||
}
|
||||
|
||||
public String getError() {
|
||||
return loginErrorMessage != null ? loginErrorMessage.getText() : null;
|
||||
}
|
||||
|
||||
public boolean isCurrent() {
|
||||
return browser.getTitle().equals("Log in to demo");
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
package org.keycloak.testsuite.pages;
|
||||
|
||||
import org.jboss.arquillian.drone.api.annotation.Drone;
|
||||
import org.openqa.selenium.WebDriver;
|
||||
import org.openqa.selenium.WebElement;
|
||||
import org.openqa.selenium.support.FindBy;
|
||||
|
||||
public class RegisterPage {
|
||||
|
||||
@Drone
|
||||
private WebDriver browser;
|
||||
|
||||
@FindBy(id = "name")
|
||||
private WebElement nameInput;
|
||||
|
||||
@FindBy(id = "email")
|
||||
private WebElement emailInput;
|
||||
|
||||
@FindBy(id = "username")
|
||||
private WebElement usernameInput;
|
||||
|
||||
@FindBy(id = "password")
|
||||
private WebElement passwordInput;
|
||||
|
||||
@FindBy(id = "password-confirm")
|
||||
private WebElement passwordConfirmInput;
|
||||
|
||||
@FindBy(css = "input[type=\"submit\"]")
|
||||
private WebElement submitButton;
|
||||
|
||||
@FindBy(id = "loginError")
|
||||
private WebElement loginErrorMessage;
|
||||
|
||||
public void register(String name, String email, String username, String password, String passwordConfirm) {
|
||||
if (name != null) {
|
||||
nameInput.sendKeys(name);
|
||||
}
|
||||
|
||||
if (email != null) {
|
||||
emailInput.sendKeys(email);
|
||||
}
|
||||
|
||||
if (username != null) {
|
||||
usernameInput.sendKeys(username);
|
||||
}
|
||||
|
||||
if (password != null) {
|
||||
passwordInput.sendKeys(password);
|
||||
}
|
||||
|
||||
if (passwordConfirm != null) {
|
||||
passwordConfirmInput.sendKeys(passwordConfirm);
|
||||
}
|
||||
|
||||
submitButton.click();
|
||||
}
|
||||
|
||||
public String getError() {
|
||||
return loginErrorMessage != null ? loginErrorMessage.getText() : null;
|
||||
}
|
||||
|
||||
public boolean isCurrent() {
|
||||
return browser.getTitle().equals("Register with demo");
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
package org.keycloak.testsuite.pages;
|
||||
|
||||
import org.jboss.arquillian.drone.api.annotation.Drone;
|
||||
import org.keycloak.testsuite.Constants;
|
||||
import org.openqa.selenium.WebDriver;
|
||||
import org.openqa.selenium.WebElement;
|
||||
import org.openqa.selenium.support.FindBy;
|
||||
|
||||
public class TotpPage {
|
||||
|
||||
private static String PATH = Constants.AUTH_SERVER_ROOT + "/rest/realms/demo/account/totp";
|
||||
|
||||
@Drone
|
||||
private WebDriver browser;
|
||||
|
||||
@FindBy(id = "totpSecret")
|
||||
private WebElement totpSecret;
|
||||
|
||||
@FindBy(id = "totp")
|
||||
private WebElement totpInput;
|
||||
|
||||
@FindBy(css = "input[type=\"submit\"]")
|
||||
private WebElement submitButton;
|
||||
|
||||
public void configure(String totp) {
|
||||
totpInput.sendKeys(totp);
|
||||
submitButton.click();
|
||||
}
|
||||
|
||||
public String getTotpSecret() {
|
||||
return totpSecret.getAttribute("value");
|
||||
}
|
||||
|
||||
public boolean isCurrent() {
|
||||
return browser.getPageSource().contains("Google Authenticator Setup");
|
||||
}
|
||||
|
||||
public void open() {
|
||||
browser.navigate().to(PATH);
|
||||
}
|
||||
|
||||
}
|
9
testsuite/src/test/resources/arquillian.xml
Normal file
9
testsuite/src/test/resources/arquillian.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<arquillian xmlns="http://jboss.org/schema/arquillian"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="
|
||||
http://jboss.org/schema/arquillian
|
||||
http://jboss.org/schema/arquillian/arquillian_1_0.xsd">
|
||||
<extension qualifier="webdriver">
|
||||
<property name="browser">${browser:phantomjs}</property>
|
||||
</extension>
|
||||
</arquillian>
|
77
testsuite/src/test/resources/testrealm-totp.json
Executable file
77
testsuite/src/test/resources/testrealm-totp.json
Executable file
|
@ -0,0 +1,77 @@
|
|||
{
|
||||
"realm": "demo",
|
||||
"enabled": true,
|
||||
"tokenLifespan": 300,
|
||||
"accessCodeLifespan": 10,
|
||||
"accessCodeLifespanUserAction": 600,
|
||||
"sslNotRequired": true,
|
||||
"cookieLoginAllowed": true,
|
||||
"registrationAllowed": true,
|
||||
"privateKey": "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=",
|
||||
"publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
|
||||
"requiredCredentials": [ "password", "totp" ],
|
||||
"requiredApplicationCredentials": [ "password" ],
|
||||
"requiredOAuthClientCredentials": [ "password" ],
|
||||
"defaultRoles": [ "user" ],
|
||||
"users" : [
|
||||
{
|
||||
"username" : "bburke@redhat.com",
|
||||
"status": "ENABLED",
|
||||
"attributes" : {
|
||||
"email" : "bburke@redhat.com"
|
||||
},
|
||||
"credentials" : [
|
||||
{ "type" : "password",
|
||||
"value" : "password" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"username" : "third-party",
|
||||
"status": "ENABLED",
|
||||
"credentials" : [
|
||||
{ "type" : "password",
|
||||
"value" : "password" }
|
||||
]
|
||||
}
|
||||
],
|
||||
"roles": [
|
||||
{
|
||||
"name": "user",
|
||||
"description": "Have User privileges"
|
||||
},
|
||||
{
|
||||
"name": "admin",
|
||||
"description": "Have Administrator privileges"
|
||||
}
|
||||
],
|
||||
"roleMappings": [
|
||||
{
|
||||
"username": "bburke@redhat.com",
|
||||
"roles": ["user"]
|
||||
},
|
||||
{
|
||||
"username": "third-party",
|
||||
"roles": ["KEYCLOAK_IDENTITY_REQUESTER"]
|
||||
}
|
||||
],
|
||||
"scopeMappings": [
|
||||
{
|
||||
"username": "third-party",
|
||||
"roles": ["user"]
|
||||
}
|
||||
],
|
||||
"applications": [
|
||||
{
|
||||
"name": "customer-portal",
|
||||
"enabled": true,
|
||||
"adminUrl": "http://localhost:8080/app/j_admin_request",
|
||||
"useRealmMappings": true,
|
||||
"credentials": [
|
||||
{
|
||||
"type": "password",
|
||||
"value": "password"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -3,6 +3,7 @@
|
|||
"enabled": true,
|
||||
"tokenLifespan": 300,
|
||||
"accessCodeLifespan": 10,
|
||||
"accessCodeLifespanUserAction": 600,
|
||||
"sslNotRequired": true,
|
||||
"cookieLoginAllowed": true,
|
||||
"registrationAllowed": true,
|
||||
|
@ -15,7 +16,7 @@
|
|||
"users" : [
|
||||
{
|
||||
"username" : "bburke@redhat.com",
|
||||
"enabled" : true,
|
||||
"status": "ENABLED",
|
||||
"attributes" : {
|
||||
"email" : "bburke@redhat.com"
|
||||
},
|
||||
|
@ -26,7 +27,7 @@
|
|||
},
|
||||
{
|
||||
"username" : "third-party",
|
||||
"enabled" : true,
|
||||
"status": "ENABLED",
|
||||
"credentials" : [
|
||||
{ "type" : "password",
|
||||
"value" : "password" }
|
||||
|
|
Loading…
Reference in a new issue