diff --git a/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java index db12aced40..a15d053604 100755 --- a/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java @@ -13,7 +13,7 @@ public class UserRepresentation { protected String self; // link protected String username; - protected String status; + protected boolean enabled; protected String firstName; protected String lastName; protected String email; @@ -61,6 +61,14 @@ public class UserRepresentation { this.username = username; } + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + public Map getAttributes() { return attributes; } @@ -92,14 +100,6 @@ public class UserRepresentation { return this; } - public String getStatus() { - return status; - } - - public void setStatus(String status) { - this.status = status; - } - public List getRequiredActions() { return requiredActions; } diff --git a/examples/as7-eap-demo/server/src/main/resources/META-INF/testrealm.json b/examples/as7-eap-demo/server/src/main/resources/META-INF/testrealm.json index 177b45fdca..284a4be550 100755 --- a/examples/as7-eap-demo/server/src/main/resources/META-INF/testrealm.json +++ b/examples/as7-eap-demo/server/src/main/resources/META-INF/testrealm.json @@ -18,7 +18,7 @@ "users" : [ { "username" : "bburke@redhat.com", - "status": "ENABLED", + "enabled": true, "attributes" : { "email" : "bburke@redhat.com" }, @@ -29,7 +29,7 @@ }, { "username" : "third-party", - "status": "ENABLED", + "enabled": true, "credentials" : [ { "type" : "password", "value" : "password" } diff --git a/services/src/main/java/org/keycloak/services/managers/AccessCodeEntry.java b/services/src/main/java/org/keycloak/services/managers/AccessCodeEntry.java index b735cd34eb..ce804973a6 100755 --- a/services/src/main/java/org/keycloak/services/managers/AccessCodeEntry.java +++ b/services/src/main/java/org/keycloak/services/managers/AccessCodeEntry.java @@ -3,11 +3,13 @@ package org.keycloak.services.managers; import org.keycloak.representations.SkeletonKeyToken; import org.keycloak.services.models.RoleModel; import org.keycloak.services.models.UserModel; +import org.keycloak.services.models.UserModel.RequiredAction; import javax.ws.rs.core.MultivaluedHashMap; import javax.ws.rs.core.MultivaluedMap; import java.util.ArrayList; import java.util.List; +import java.util.Set; import java.util.UUID; /** @@ -23,6 +25,7 @@ public class AccessCodeEntry { protected long expiration; protected SkeletonKeyToken token; protected UserModel user; + protected Set requiredActions; protected UserModel client; protected List realmRolesRequested = new ArrayList(); MultivaluedMap resourceRolesRequested = new MultivaluedHashMap(); @@ -75,6 +78,14 @@ public class AccessCodeEntry { this.user = user; } + public Set getRequiredActions() { + return requiredActions; + } + + public void setRequiredActions(Set requiredActions) { + this.requiredActions = requiredActions; + } + public List getRealmRolesRequested() { return realmRolesRequested; } diff --git a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java index 2874ef19ca..a0c10e98d5 100755 --- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java +++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java @@ -12,8 +12,6 @@ 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; @@ -205,7 +203,7 @@ public class AuthenticationManager { return AuthenticationStatus.INVALID_USER; } - if (!user.isEnabled() && user.getStatus() == Status.DISABLED) { + if (!user.isEnabled()) { logger.info("Account is disabled, contact admin."); return AuthenticationStatus.ACCOUNT_DISABLED; } @@ -249,7 +247,7 @@ public class AuthenticationManager { } } - if (user.getStatus() == Status.ACTIONS_REQUIRED) { + if (!user.getRequiredActions().isEmpty()) { return AuthenticationStatus.ACTIONS_REQUIRED; } else { return AuthenticationStatus.SUCCESS; diff --git a/services/src/main/java/org/keycloak/services/managers/RealmManager.java b/services/src/main/java/org/keycloak/services/managers/RealmManager.java index 438ac68938..f5848438d1 100755 --- a/services/src/main/java/org/keycloak/services/managers/RealmManager.java +++ b/services/src/main/java/org/keycloak/services/managers/RealmManager.java @@ -210,7 +210,7 @@ public class RealmManager { public UserModel createUser(RealmModel newRealm, UserRepresentation userRep) { UserModel user = newRealm.addUser(userRep.getUsername()); - user.setStatus(UserModel.Status.valueOf(userRep.getStatus())); + user.setEnabled(userRep.isEnabled()); user.setEmail(userRep.getEmail()); if (userRep.getAttributes() != null) { for (Map.Entry entry : userRep.getAttributes().entrySet()) { diff --git a/services/src/main/java/org/keycloak/services/managers/UserManager.java b/services/src/main/java/org/keycloak/services/managers/UserManager.java index b486cfea98..49cd813e7c 100755 --- a/services/src/main/java/org/keycloak/services/managers/UserManager.java +++ b/services/src/main/java/org/keycloak/services/managers/UserManager.java @@ -19,7 +19,7 @@ public class UserManager { rep.setEmail(user.getEmail()); rep.setLastName(user.getLastName()); rep.setFirstName(user.getFirstName()); - rep.setStatus(user.getStatus().name()); + rep.setEnabled(user.isEnabled()); rep.setUsername(user.getLoginName()); for (Map.Entry 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.setStatus(UserModel.Status.valueOf(userRep.getStatus())); + user.setEnabled(userRep.isEnabled()); 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.setStatus(UserModel.Status.valueOf(userRep.getStatus())); + user.setEnabled(userRep.isEnabled()); user.setEmail(userRep.getEmail()); user.setFirstName(userRep.getFirstName()); user.setLastName(userRep.getLastName()); diff --git a/services/src/main/java/org/keycloak/services/models/UserModel.java b/services/src/main/java/org/keycloak/services/models/UserModel.java index bab26e0831..9bd370a59b 100755 --- a/services/src/main/java/org/keycloak/services/models/UserModel.java +++ b/services/src/main/java/org/keycloak/services/models/UserModel.java @@ -1,7 +1,7 @@ package org.keycloak.services.models; -import java.util.List; import java.util.Map; +import java.util.Set; /** * @author Bill Burke @@ -19,9 +19,7 @@ public interface UserModel { boolean isTotp(); - Status getStatus(); - - void setStatus(Status status); + void setEnabled(boolean enabled); void setAttribute(String name, String value); @@ -31,7 +29,7 @@ public interface UserModel { Map getAttributes(); - List getRequiredActions(); + Set getRequiredActions(); void addRequiredAction(RequiredAction action); @@ -55,10 +53,6 @@ public interface UserModel { void setTotp(boolean totp); - public static enum Status { - ENABLED, DISABLED, ACTIONS_REQUIRED - } - public static enum RequiredAction { VERIFY_EMAIL, UPDATE_PROFILE, CONFIGURE_TOTP, RESET_PASSWORD } diff --git a/services/src/main/java/org/keycloak/services/models/picketlink/UserAdapter.java b/services/src/main/java/org/keycloak/services/models/picketlink/UserAdapter.java index 6ff4a969d9..169a4e2c5e 100755 --- a/services/src/main/java/org/keycloak/services/models/picketlink/UserAdapter.java +++ b/services/src/main/java/org/keycloak/services/models/picketlink/UserAdapter.java @@ -3,8 +3,9 @@ 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.HashSet; import java.util.Map; +import java.util.Set; import org.keycloak.services.models.UserModel; import org.keycloak.services.models.utils.ArrayUtils; @@ -20,7 +21,6 @@ public class UserAdapter implements UserModel { private static final String EMAIL_VERIFIED_ATTR = "emailVerified"; 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; @@ -44,23 +44,9 @@ public class UserAdapter implements UserModel { return user.isEnabled(); } - public UserModel.Status getStatus() { - Attribute a = user.getAttribute(STATUS_ATTR); - if (a != null) { - return a.getValue(); - } else { - return user.isEnabled() ? UserModel.Status.ENABLED : UserModel.Status.DISABLED; - } - } - @Override - public void setStatus(UserModel.Status status) { - user.setAttribute(new Attribute(STATUS_ATTR, status)); - if (status == UserModel.Status.DISABLED) { - user.setEnabled(false); - } else { - user.setEnabled(true); - } + public void setEnabled(boolean enabled) { + user.setEnabled(enabled); idm.update(user); } @@ -131,7 +117,7 @@ public class UserAdapter implements UserModel { @Override public Map getAttributes() { Map attributes = new HashMap(); - for (Attribute attribute : user.getAttributes()) { + for (Attribute attribute : user.getAttributes()) { if (attribute.getValue() != null) attributes.put(attribute.getName(), attribute.getValue().toString()); } return attributes; @@ -152,12 +138,16 @@ public class UserAdapter implements UserModel { } @Override - public List getRequiredActions() { + public Set getRequiredActions() { RequiredAction[] actions = getRequiredActionsArray(); if (actions == null) { - return null; + return Collections.emptySet(); } else { - return Collections.unmodifiableList(Arrays.asList(actions)); + Set s = new HashSet(); + for (RequiredAction a : actions) { + s.add(a); + } + return Collections.unmodifiableSet(s); } } @@ -167,7 +157,9 @@ public class UserAdapter implements UserModel { if (actions == null) { actions = new RequiredAction[] { action }; } else { - actions = ArrayUtils.add(actions, action); + if (Arrays.binarySearch(actions, action) < 0) { + actions = ArrayUtils.add(actions, action); + } } Attribute a = new Attribute(REQUIRED_ACTIONS_ATTR, actions); diff --git a/services/src/main/java/org/keycloak/services/resources/AccountService.java b/services/src/main/java/org/keycloak/services/resources/AccountService.java index 3213fe3628..da8aac6e64 100755 --- a/services/src/main/java/org/keycloak/services/resources/AccountService.java +++ b/services/src/main/java/org/keycloak/services/resources/AccountService.java @@ -21,6 +21,9 @@ */ package org.keycloak.services.resources; +import java.util.HashSet; +import java.util.Set; + import javax.ws.rs.Consumes; import javax.ws.rs.GET; import javax.ws.rs.POST; @@ -98,49 +101,44 @@ public class AccountService { @POST @Consumes(MediaType.APPLICATION_FORM_URLENCODED) public Response processAccountUpdate(final MultivaluedMap formData) { - UserModel user = getUser(RequiredAction.UPDATE_PROFILE); - if (user != null) { - user.setFirstName(formData.getFirst("firstName")); - user.setLastName(formData.getFirst("lastName")); - user.setEmail(formData.getFirst("email")); - - Response response = redirectOauth(); - if (response != null) { - return response; - } else { - return Flows.forms(realm, request, uriInfo).setUser(user).forwardToAccount(); - } - } else { + AccessCodeEntry accessCodeEntry = getAccessCodeEntry(RequiredAction.UPDATE_PROFILE); + UserModel user = accessCodeEntry != null ? getUserFromAccessCode(accessCodeEntry) : getUserFromAuthManager(); + if (user == null) { return Response.status(Status.FORBIDDEN).build(); } + + user.setFirstName(formData.getFirst("firstName")); + user.setLastName(formData.getFirst("lastName")); + user.setEmail(formData.getFirst("email")); + + user.removeRequiredAction(UserModel.RequiredAction.UPDATE_PROFILE); + if (accessCodeEntry != null) { + accessCodeEntry.getRequiredActions().remove(UserModel.RequiredAction.UPDATE_PROFILE); + } + + Response response = redirectOauth(accessCodeEntry); + if (response != null) { + return response; + } else { + return Flows.forms(realm, request, uriInfo).setUser(user).forwardToAccount(); + } } - private UserModel getUser(RequiredAction action) { - if (uriInfo.getQueryParameters().containsKey(FormFlows.CODE)) { - AccessCodeEntry accessCodeEntry = getAccessCodeEntry(uriInfo.getQueryParameters().getFirst(FormFlows.CODE)); - if (accessCodeEntry == null) { - return null; - } - - String loginName = accessCodeEntry.getUser().getLoginName(); - UserModel user = realm.getUser(loginName); - if (!user.getRequiredActions().contains(action)) { - return null; - } - if (!accessCodeEntry.getUser().getRequiredActions().contains(action)) { - return null; - } - return user; - } else { - return getUserFromAuthManager(); - } + private UserModel getUserFromAccessCode(AccessCodeEntry accessCodeEntry) { + String loginName = accessCodeEntry.getUser().getLoginName(); + return realm.getUser(loginName); } private UserModel getUserFromAuthManager() { return authManager.authenticateIdentityCookie(realm, uriInfo, headers); } - private AccessCodeEntry getAccessCodeEntry(String code) { + private AccessCodeEntry getAccessCodeEntry(RequiredAction requiredAction) { + String code = uriInfo.getQueryParameters().getFirst(FormFlows.CODE); + if (code == null) { + return null; + } + JWSInput input = new JWSInput(code, providers); boolean verifiedCode = false; try { @@ -163,6 +161,11 @@ public class AccountService { return null; } + if (accessCodeEntry.getRequiredActions() == null + || !accessCodeEntry.getRequiredActions().contains(requiredAction)) { + return null; + } + return accessCodeEntry; } @@ -170,72 +173,80 @@ public class AccountService { @POST @Consumes(MediaType.APPLICATION_FORM_URLENCODED) public Response processTotpUpdate(final MultivaluedMap formData) { - UserModel user = getUser(RequiredAction.CONFIGURE_TOTP); - if (user != null) { - FormFlows forms = Flows.forms(realm, request, uriInfo); - - String totp = formData.getFirst("totp"); - String totpSecret = formData.getFirst("totpSecret"); - - String error = null; - - if (Validation.isEmpty(totp)) { - error = Messages.MISSING_TOTP; - } else if (!new TimeBasedOTP().validate(totp, totpSecret.getBytes())) { - error = Messages.INVALID_TOTP; - } - - if (error != null) { - return forms.setError(error).setUser(user).forwardToTotp(); - } - - UserCredentialModel credentials = new UserCredentialModel(); - credentials.setType(CredentialRepresentation.TOTP); - credentials.setValue(formData.getFirst("totpSecret")); - realm.updateCredential(user, credentials); - - user.removeRequiredAction(UserModel.RequiredAction.CONFIGURE_TOTP); - - user.setTotp(true); - - Response response = redirectOauth(); - if (response != null) { - return response; - } else { - return Flows.forms(realm, request, uriInfo).setUser(user).forwardToTotp(); - } - } else { + AccessCodeEntry accessCodeEntry = getAccessCodeEntry(RequiredAction.CONFIGURE_TOTP); + UserModel user = accessCodeEntry != null ? getUserFromAccessCode(accessCodeEntry) : getUserFromAuthManager(); + if (user == null) { return Response.status(Status.FORBIDDEN).build(); } + + FormFlows forms = Flows.forms(realm, request, uriInfo); + + String totp = formData.getFirst("totp"); + String totpSecret = formData.getFirst("totpSecret"); + + String error = null; + + if (Validation.isEmpty(totp)) { + error = Messages.MISSING_TOTP; + } else if (!new TimeBasedOTP().validate(totp, totpSecret.getBytes())) { + error = Messages.INVALID_TOTP; + } + + if (error != null) { + return forms.setError(error).setUser(user).forwardToTotp(); + } + + UserCredentialModel credentials = new UserCredentialModel(); + credentials.setType(CredentialRepresentation.TOTP); + credentials.setValue(formData.getFirst("totpSecret")); + realm.updateCredential(user, credentials); + + user.removeRequiredAction(UserModel.RequiredAction.CONFIGURE_TOTP); + if (accessCodeEntry != null) { + accessCodeEntry.getRequiredActions().remove(UserModel.RequiredAction.CONFIGURE_TOTP); + } + + user.setTotp(true); + + Response response = redirectOauth(accessCodeEntry); + if (response != null) { + return response; + } else { + return Flows.forms(realm, request, uriInfo).setUser(user).forwardToTotp(); + } } @Path("email-verify") @GET - public Response processEmailVerification(@QueryParam("code") String code) { - AccessCodeEntry accessCodeEntry = getAccessCodeEntry(code); - String loginName = accessCodeEntry.getUser().getLoginName(); - UserModel user = realm.getUser(loginName); - if (user != null) { - user.setEmailVerified(true); - user.removeRequiredAction(UserModel.RequiredAction.VERIFY_EMAIL); - - Response response = redirectOauth(); - if (response != null) { - return response; - } else { - return Flows.forms(realm, request, uriInfo).setUser(user).forwardToVerifyEmail(); - } - } else { + public Response processEmailVerification() { + AccessCodeEntry accessCodeEntry = getAccessCodeEntry(RequiredAction.VERIFY_EMAIL); + UserModel user = accessCodeEntry != null ? getUserFromAccessCode(accessCodeEntry) : null; + if (user == null) { return Response.status(Status.FORBIDDEN).build(); } + + user.setEmailVerified(true); + user.removeRequiredAction(UserModel.RequiredAction.VERIFY_EMAIL); + if (accessCodeEntry != null) { + accessCodeEntry.getRequiredActions().remove(UserModel.RequiredAction.VERIFY_EMAIL); + } + + Response response = redirectOauth(accessCodeEntry); + if (response != null) { + return response; + } else { + return Flows.forms(realm, request, uriInfo).setUser(user).forwardToVerifyEmail(); + } } - private Response redirectOauth() { + private Response redirectOauth(AccessCodeEntry accessCodeEntry) { + if (accessCodeEntry == null) { + return null; + } String redirect = uriInfo.getQueryParameters().getFirst("redirect_uri"); if (redirect != null) { - AccessCodeEntry accessCode = getAccessCodeEntry(uriInfo.getQueryParameters().getFirst(FormFlows.CODE)); String state = uriInfo.getQueryParameters().getFirst("state"); - return Flows.oauth(realm, request, uriInfo, authManager, tokenManager).redirectAccessCode(accessCode, state, + return Flows.oauth(realm, request, uriInfo, authManager, tokenManager).redirectAccessCode(accessCodeEntry, state, redirect); } else { return null; @@ -246,50 +257,53 @@ public class AccountService { @POST @Consumes(MediaType.APPLICATION_FORM_URLENCODED) public Response processPasswordUpdate(final MultivaluedMap formData) { - UserModel user = getUser(RequiredAction.RESET_PASSWORD); - if (user != null) { - FormFlows forms = Flows.forms(realm, request, uriInfo).setUser(user); - - String password = formData.getFirst("password"); - String passwordNew = formData.getFirst("password-new"); - String passwordConfirm = formData.getFirst("password-confirm"); - - String error = null; - - if (Validation.isEmpty(passwordNew)) { - error = Messages.MISSING_PASSWORD; - } else if (!passwordNew.equals(passwordConfirm)) { - error = Messages.INVALID_PASSWORD_CONFIRM; - } - - if (user.getRequiredActions() == null || !user.getRequiredActions().contains(RequiredAction.RESET_PASSWORD)) { - if (Validation.isEmpty(password)) { - error = Messages.MISSING_PASSWORD; - } else if (!realm.validatePassword(user, password)) { - error = Messages.INVALID_PASSWORD_EXISTING; - } - } - - if (error != null) { - return forms.setError(error).forwardToPassword(); - } - - UserCredentialModel credentials = new UserCredentialModel(); - credentials.setType(CredentialRepresentation.PASSWORD); - credentials.setValue(passwordNew); - - realm.updateCredential(user, credentials); - - user.removeRequiredAction(RequiredAction.RESET_PASSWORD); - user.setStatus(UserModel.Status.ENABLED); - - authManager.expireIdentityCookie(realm, uriInfo); - new ResourceAdminManager().singleLogOut(realm, user.getLoginName()); - - return Flows.forms(realm, request, uriInfo).forwardToLogin(); - } else { + AccessCodeEntry accessCodeEntry = getAccessCodeEntry(RequiredAction.RESET_PASSWORD); + UserModel user = accessCodeEntry != null ? getUserFromAccessCode(accessCodeEntry) : getUserFromAuthManager(); + if (user == null) { return Response.status(Status.FORBIDDEN).build(); } + + FormFlows forms = Flows.forms(realm, request, uriInfo).setUser(user); + + String password = formData.getFirst("password"); + String passwordNew = formData.getFirst("password-new"); + String passwordConfirm = formData.getFirst("password-confirm"); + + String error = null; + + if (Validation.isEmpty(passwordNew)) { + error = Messages.MISSING_PASSWORD; + } else if (!passwordNew.equals(passwordConfirm)) { + error = Messages.INVALID_PASSWORD_CONFIRM; + } + + if (accessCodeEntry == null) { + if (Validation.isEmpty(password)) { + error = Messages.MISSING_PASSWORD; + } else if (!realm.validatePassword(user, password)) { + error = Messages.INVALID_PASSWORD_EXISTING; + } + } + + if (error != null) { + return forms.setError(error).forwardToPassword(); + } + + UserCredentialModel credentials = new UserCredentialModel(); + credentials.setType(CredentialRepresentation.PASSWORD); + credentials.setValue(passwordNew); + + realm.updateCredential(user, credentials); + + user.removeRequiredAction(RequiredAction.RESET_PASSWORD); + if (accessCodeEntry != null) { + accessCodeEntry.getRequiredActions().remove(UserModel.RequiredAction.RESET_PASSWORD); + } + + authManager.expireIdentityCookie(realm, uriInfo); + new ResourceAdminManager().singleLogOut(realm, user.getLoginName()); + + return Flows.forms(realm, request, uriInfo).forwardToLogin(); } @Path("") @@ -328,7 +342,14 @@ public class AccountService { @Path("password") @GET public Response passwordPage() { - UserModel user = getUser(RequiredAction.RESET_PASSWORD); + UserModel user = getUserFromAuthManager(); + + // TODO Remove when we have a separate login-reset-password page + if (user == null) { + AccessCodeEntry accessCodeEntry = getAccessCodeEntry(RequiredAction.RESET_PASSWORD); + user = accessCodeEntry != null ? getUserFromAccessCode(accessCodeEntry) : null; + } + if (user != null) { return Flows.forms(realm, request, uriInfo).setUser(user).forwardToPassword(); } else { @@ -361,10 +382,12 @@ public class AccountService { // String username = formData.getFirst("username"); UserModel user = realm.getUser(username); - user.addRequiredAction(RequiredAction.RESET_PASSWORD); - user.setStatus(UserModel.Status.ACTIONS_REQUIRED); + + Set requiredActions = new HashSet(user.getRequiredActions()); + requiredActions.add(RequiredAction.RESET_PASSWORD); AccessCodeEntry accessCode = tokenManager.createAccessCode(scopeParam, state, redirect, realm, client, user); + accessCode.setRequiredActions(requiredActions); accessCode.setExpiration(System.currentTimeMillis() / 1000 + realm.getAccessCodeLifespanUserAction()); if (user.getEmail() == null) { diff --git a/services/src/main/java/org/keycloak/services/resources/SaasService.java b/services/src/main/java/org/keycloak/services/resources/SaasService.java index 12156c3994..4a64674bd5 100755 --- a/services/src/main/java/org/keycloak/services/resources/SaasService.java +++ b/services/src/main/java/org/keycloak/services/resources/SaasService.java @@ -229,7 +229,7 @@ public class SaasService { return Flows.forms(realm, request, uriInfo).setError(Messages.ACCOUNT_DISABLED).setFormData(formData) .forwardToLogin(); case ACTIONS_REQUIRED: - return Flows.forms(realm, request, uriInfo).forwardToAction(user.getRequiredActions().get(0)); + return Flows.forms(realm, request, uriInfo).forwardToAction(user.getRequiredActions().iterator().next()); default: return Flows.forms(realm, request, uriInfo).setError(Messages.INVALID_USER).setFormData(formData) .forwardToLogin(); diff --git a/services/src/main/java/org/keycloak/services/resources/TokenService.java b/services/src/main/java/org/keycloak/services/resources/TokenService.java index 2df6a7ca6d..44b1d925ac 100755 --- a/services/src/main/java/org/keycloak/services/resources/TokenService.java +++ b/services/src/main/java/org/keycloak/services/resources/TokenService.java @@ -20,7 +20,6 @@ import org.keycloak.services.managers.TokenManager; import org.keycloak.services.messages.Messages; import org.keycloak.services.models.*; import org.keycloak.services.models.UserModel.RequiredAction; -import org.keycloak.services.models.UserModel.Status; import org.keycloak.services.resources.flows.Flows; import org.keycloak.services.resources.flows.OAuthFlows; import org.keycloak.services.validation.Validation; @@ -218,7 +217,6 @@ public class TokenService { for (RequiredCredentialModel c : realm.getRequiredCredentials()) { if (c.getType().equals(CredentialRepresentation.TOTP) && !user.isTotp()) { user.addRequiredAction(RequiredAction.CONFIGURE_TOTP); - user.setStatus(Status.ACTIONS_REQUIRED); logger.info("User is required to configure totp"); } } @@ -227,7 +225,6 @@ public class TokenService { private void isEmailVerificationRequired(UserModel user) { if (realm.isVerifyEmail() && !user.isEmailVerified()) { user.addRequiredAction(RequiredAction.VERIFY_EMAIL); - user.setStatus(Status.ACTIONS_REQUIRED); logger.info("User is required to verify email"); } } @@ -417,6 +414,12 @@ public class TokenService { return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(res) .build(); } + if (accessCode.getRequiredActions() != null && !accessCode.getRequiredActions().isEmpty()) { + Map res = new HashMap(); + res.put("error", "invalid_grant"); + res.put("error_description", "Actions required"); + return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(res).build(); + } if (!client.getLoginName().equals(accessCode.getClient().getLoginName())) { Map res = new HashMap(); res.put("error", "invalid_grant"); diff --git a/services/src/main/java/org/keycloak/services/resources/flows/OAuthFlows.java b/services/src/main/java/org/keycloak/services/resources/flows/OAuthFlows.java index a71e8ac86f..acd86cb2e0 100755 --- a/services/src/main/java/org/keycloak/services/resources/flows/OAuthFlows.java +++ b/services/src/main/java/org/keycloak/services/resources/flows/OAuthFlows.java @@ -21,6 +21,10 @@ */ package org.keycloak.services.resources.flows; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + import org.jboss.resteasy.logging.Logger; import org.jboss.resteasy.spi.HttpRequest; import org.keycloak.services.managers.AccessCodeEntry; @@ -30,6 +34,7 @@ import org.keycloak.services.managers.TokenManager; import org.keycloak.services.models.RealmModel; import org.keycloak.services.models.RoleModel; import org.keycloak.services.models.UserModel; +import org.keycloak.services.models.UserModel.RequiredAction; import org.keycloak.services.resources.TokenService; import javax.ws.rs.core.Response; @@ -88,18 +93,21 @@ public class OAuthFlows { log.info("processAccessCode: go to oauth page?: " + (!isResource && (accessCode.getRealmRolesRequested().size() > 0 || accessCode.getResourceRolesRequested() .size() > 0))); + + Set requiredActions = user.getRequiredActions(); + if (!requiredActions.isEmpty()) { + accessCode.setRequiredActions(new HashSet(requiredActions)); + accessCode.setExpiration(System.currentTimeMillis() / 1000 + realm.getAccessCodeLifespanUserAction()); + return Flows.forms(realm, request, uriInfo).setCode(accessCode.getCode()).setUser(user) + .forwardToAction(user.getRequiredActions().iterator().next()); + } + if (!isResource && (accessCode.getRealmRolesRequested().size() > 0 || accessCode.getResourceRolesRequested().size() > 0)) { accessCode.setExpiration(System.currentTimeMillis() / 1000 + realm.getAccessCodeLifespanUserAction()); return oauthGrantPage(accessCode, client); } - if (user.getRequiredActions() != null) { - accessCode.setExpiration(System.currentTimeMillis() / 1000 + realm.getAccessCodeLifespanUserAction()); - return Flows.forms(realm, request, uriInfo).setCode(accessCode.getCode()).setUser(user) - .forwardToAction(user.getRequiredActions().get(0)); - } - if (redirect != null) { return redirectAccessCode(accessCode, state, redirect); } else { diff --git a/services/src/test/java/org/keycloak/services/managers/AuthenticationManagerTest.java b/services/src/test/java/org/keycloak/services/managers/AuthenticationManagerTest.java index ad3149cc76..76870e0c5d 100644 --- a/services/src/test/java/org/keycloak/services/managers/AuthenticationManagerTest.java +++ b/services/src/test/java/org/keycloak/services/managers/AuthenticationManagerTest.java @@ -17,7 +17,6 @@ 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.models.UserModel.Status; import org.keycloak.services.resources.KeycloakApplication; import org.picketlink.idm.credential.util.TimeBasedOTP; @@ -66,7 +65,6 @@ public class AuthenticationManagerTest { public void authFormRequiredAction() { realm.addRequiredCredential(CredentialRepresentation.TOTP); user.addRequiredAction(RequiredAction.CONFIGURE_TOTP); - user.setStatus(Status.ACTIONS_REQUIRED); AuthenticationStatus status = am.authenticateForm(realm, user, formData); Assert.assertEquals(AuthenticationStatus.ACTIONS_REQUIRED, status); @@ -74,20 +72,12 @@ public class AuthenticationManagerTest { @Test public void authFormUserDisabled() { - user.setStatus(Status.DISABLED); + user.setEnabled(false); 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); diff --git a/services/src/test/java/org/keycloak/test/AdapterTest.java b/services/src/test/java/org/keycloak/test/AdapterTest.java index 015caac7fd..581c1e8ae0 100755 --- a/services/src/test/java/org/keycloak/test/AdapterTest.java +++ b/services/src/test/java/org/keycloak/test/AdapterTest.java @@ -15,11 +15,13 @@ import org.keycloak.services.models.RequiredCredentialModel; import org.keycloak.services.models.RoleModel; import org.keycloak.services.models.UserModel; import org.keycloak.services.models.UserCredentialModel; +import org.keycloak.services.models.UserModel.RequiredAction; import org.keycloak.services.resources.KeycloakApplication; import java.util.Arrays; import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.StringTokenizer; @@ -264,54 +266,42 @@ 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()); + Assert.assertTrue(user.getRequiredActions().isEmpty()); user.addRequiredAction(UserModel.RequiredAction.CONFIGURE_TOTP); user = realmModel.getUser("bburke"); - Assert.assertEquals(Arrays.asList(UserModel.RequiredAction.CONFIGURE_TOTP), user.getRequiredActions()); + Assert.assertEquals(1, user.getRequiredActions().size()); + Assert.assertTrue(user.getRequiredActions().contains(RequiredAction.CONFIGURE_TOTP)); + + user.addRequiredAction(UserModel.RequiredAction.CONFIGURE_TOTP); + user = realmModel.getUser("bburke"); + + Assert.assertEquals(1, user.getRequiredActions().size()); + Assert.assertTrue(user.getRequiredActions().contains(RequiredAction.CONFIGURE_TOTP)); user.addRequiredAction(UserModel.RequiredAction.VERIFY_EMAIL); user = realmModel.getUser("bburke"); - Assert.assertEquals(Arrays.asList(UserModel.RequiredAction.CONFIGURE_TOTP, UserModel.RequiredAction.VERIFY_EMAIL), - user.getRequiredActions()); + Assert.assertEquals(2, user.getRequiredActions().size()); + Assert.assertTrue(user.getRequiredActions().contains(RequiredAction.CONFIGURE_TOTP)); + Assert.assertTrue(user.getRequiredActions().contains(RequiredAction.VERIFY_EMAIL)); user.removeRequiredAction(UserModel.RequiredAction.CONFIGURE_TOTP); user = realmModel.getUser("bburke"); - Assert.assertEquals(Arrays.asList(UserModel.RequiredAction.VERIFY_EMAIL), user.getRequiredActions()); + Assert.assertEquals(1, user.getRequiredActions().size()); + Assert.assertTrue(user.getRequiredActions().contains(RequiredAction.VERIFY_EMAIL)); user.removeRequiredAction(UserModel.RequiredAction.VERIFY_EMAIL); user = realmModel.getUser("bburke"); - Assert.assertNull(user.getRequiredActions()); + Assert.assertTrue(user.getRequiredActions().isEmpty()); } } diff --git a/services/src/test/resources/testrealm-demo.json b/services/src/test/resources/testrealm-demo.json index 8ef37fe1b0..92d23ea773 100755 --- a/services/src/test/resources/testrealm-demo.json +++ b/services/src/test/resources/testrealm-demo.json @@ -15,7 +15,7 @@ "users" : [ { "username" : "bburke@redhat.com", - "status": "ENABLED", + "enabled": true, "attributes" : { "email" : "bburke@redhat.com" }, @@ -26,7 +26,7 @@ }, { "username" : "third-party", - "status": "ENABLED", + "enabled": true, "credentials" : [ { "type" : "Password", "value" : "password" } diff --git a/services/src/test/resources/testrealm.json b/services/src/test/resources/testrealm.json index 2549914d65..cfe52159d4 100755 --- a/services/src/test/resources/testrealm.json +++ b/services/src/test/resources/testrealm.json @@ -12,7 +12,7 @@ "users": [ { "username": "wburke", - "status": "ENABLED", + "enabled": true, "attributes": { "email": "bburke@redhat.com" }, @@ -25,7 +25,7 @@ }, { "username": "loginclient", - "status": "ENABLED", + "enabled": true, "credentials": [ { "type": "password", @@ -35,7 +35,7 @@ }, { "username": "admin", - "status": "ENABLED", + "enabled": true, "credentials": [ { "type": "password", @@ -45,7 +45,7 @@ }, { "username": "oauthclient", - "status": "ENABLED", + "enabled": true, "credentials": [ { "type": "password", @@ -55,7 +55,7 @@ }, { "username": "mySocialUser", - "status": "ENABLED" + "enabled": true } ], "roleMappings": [ diff --git a/testsuite/src/test/java/org/keycloak/testsuite/RegisterTest.java b/testsuite/src/test/java/org/keycloak/testsuite/RegisterTest.java index 26683e117b..1417262d36 100644 --- a/testsuite/src/test/java/org/keycloak/testsuite/RegisterTest.java +++ b/testsuite/src/test/java/org/keycloak/testsuite/RegisterTest.java @@ -39,7 +39,7 @@ public class RegisterTest extends AbstractDroneTest { Assert.assertTrue(registerPage.isCurrent()); - registerPage.register("name", "email", "username", null, null); + registerPage.register("name", "email", "registerExistingUser", null, null); Assert.assertTrue(registerPage.isCurrent()); Assert.assertEquals("Please specify password", registerPage.getError()); @@ -52,7 +52,7 @@ public class RegisterTest extends AbstractDroneTest { Assert.assertTrue(registerPage.isCurrent()); - registerPage.register("name", "email", "bburke@redhat.com", "password", "invalid"); + registerPage.register("name", "email", "registerUserInvalidPasswordConfirm", "password", "invalid"); Assert.assertTrue(registerPage.isCurrent()); Assert.assertEquals("Password confirmation doesn't match", registerPage.getError()); @@ -65,7 +65,7 @@ public class RegisterTest extends AbstractDroneTest { Assert.assertTrue(registerPage.isCurrent()); - registerPage.register("name", "email", "username", null, null); + registerPage.register("name", "email", "registerUserMissingPassword", null, null); Assert.assertTrue(registerPage.isCurrent()); Assert.assertEquals("Please specify password", registerPage.getError()); @@ -91,7 +91,7 @@ public class RegisterTest extends AbstractDroneTest { Assert.assertTrue(registerPage.isCurrent()); - registerPage.register("name", "email", "username", "password", "password"); + registerPage.register("name", "email", "registerUserSuccess", "password", "password"); Assert.assertTrue(appPage.isCurrent()); } diff --git a/testsuite/src/test/java/org/keycloak/testsuite/ResetPasswordTest.java b/testsuite/src/test/java/org/keycloak/testsuite/ResetPasswordTest.java index e90a1d9c42..22b0ab70fd 100644 --- a/testsuite/src/test/java/org/keycloak/testsuite/ResetPasswordTest.java +++ b/testsuite/src/test/java/org/keycloak/testsuite/ResetPasswordTest.java @@ -91,4 +91,24 @@ public class ResetPasswordTest extends AbstractDroneTest { Assert.assertEquals("bburke@redhat.com", appPage.getUser()); } + @Test + public void tempPassword() { + appPage.open(); + + Assert.assertTrue(loginPage.isCurrent()); + + loginPage.login("reset@pass.com", "temp-password"); + + Assert.assertTrue(changePasswordPage.isCurrent()); + + changePasswordPage.changePassword("new-password", "new-password"); + + Assert.assertTrue(loginPage.isCurrent()); + + loginPage.login("reset@pass.com", "new-password"); + + Assert.assertTrue(appPage.isCurrent()); + Assert.assertEquals("reset@pass.com", appPage.getUser()); + } + } diff --git a/testsuite/src/test/resources/testrealm-email.json b/testsuite/src/test/resources/testrealm-email.json index 49340fdcca..79e1a94f25 100755 --- a/testsuite/src/test/resources/testrealm-email.json +++ b/testsuite/src/test/resources/testrealm-email.json @@ -17,7 +17,7 @@ "users" : [ { "username" : "bburke@redhat.com", - "status": "ENABLED", + "enabled": true, "email" : "bburke@redhat.com", "credentials" : [ { "type" : "password", @@ -26,7 +26,7 @@ }, { "username" : "third-party", - "status": "ENABLED", + "enabled": true, "credentials" : [ { "type" : "password", "value" : "password" } diff --git a/testsuite/src/test/resources/testrealm-totp.json b/testsuite/src/test/resources/testrealm-totp.json index 014339fcdd..d73f805e02 100755 --- a/testsuite/src/test/resources/testrealm-totp.json +++ b/testsuite/src/test/resources/testrealm-totp.json @@ -16,7 +16,7 @@ "users" : [ { "username" : "bburke@redhat.com", - "status": "ENABLED", + "enabled": true, "email" : "bburke@redhat.com", "credentials" : [ { "type" : "password", @@ -25,7 +25,7 @@ }, { "username" : "third-party", - "status": "ENABLED", + "enabled": true, "credentials" : [ { "type" : "password", "value" : "password" } diff --git a/testsuite/src/test/resources/testrealm.json b/testsuite/src/test/resources/testrealm.json index 3161c1f285..0ea28100f2 100755 --- a/testsuite/src/test/resources/testrealm.json +++ b/testsuite/src/test/resources/testrealm.json @@ -17,16 +17,26 @@ "users" : [ { "username" : "bburke@redhat.com", - "status": "ENABLED", + "enabled": true, "email" : "bburke@redhat.com", "credentials" : [ { "type" : "password", "value" : "password" } ] }, + { + "username" : "reset@pass.com", + "enabled": true, + "requiredActions" : [ "RESET_PASSWORD" ], + "email" : "reset@pass.com", + "credentials" : [ + { "type" : "password", + "value" : "temp-password" } + ] + }, { "username" : "third-party", - "status": "ENABLED", + "enabled": true, "credentials" : [ { "type" : "password", "value" : "password" } @@ -48,6 +58,10 @@ "username": "bburke@redhat.com", "roles": ["user"] }, + { + "username": "reset@pass.com", + "roles": ["user"] + }, { "username": "third-party", "roles": ["KEYCLOAK_IDENTITY_REQUESTER"]