Improvements to required user actions. Including adding support for required actions to AccessCodeEntry. Added test for temporary password.

This commit is contained in:
Stian Thorgersen 2013-09-21 12:21:28 +01:00
parent 4422d9609b
commit 72315bb9d7
21 changed files with 290 additions and 247 deletions

View file

@ -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<String, String> 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<String> getRequiredActions() {
return requiredActions;
}

View file

@ -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" }

View file

@ -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<RequiredAction> requiredActions;
protected UserModel client;
protected List<RoleModel> realmRolesRequested = new ArrayList<RoleModel>();
MultivaluedMap<String, RoleModel> resourceRolesRequested = new MultivaluedHashMap<String, RoleModel>();
@ -75,6 +78,14 @@ public class AccessCodeEntry {
this.user = user;
}
public Set<RequiredAction> getRequiredActions() {
return requiredActions;
}
public void setRequiredActions(Set<RequiredAction> requiredActions) {
this.requiredActions = requiredActions;
}
public List<RoleModel> getRealmRolesRequested() {
return realmRolesRequested;
}

View file

@ -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;

View file

@ -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<String, String> entry : userRep.getAttributes().entrySet()) {

View file

@ -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<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.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());

View file

@ -1,7 +1,7 @@
package org.keycloak.services.models;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -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<String, String> getAttributes();
List<RequiredAction> getRequiredActions();
Set<RequiredAction> 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
}

View file

@ -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<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 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);
}
public void setEnabled(boolean enabled) {
user.setEnabled(enabled);
idm.update(user);
}
@ -131,7 +117,7 @@ public class UserAdapter implements UserModel {
@Override
public Map<String, String> getAttributes() {
Map<String, String> attributes = new HashMap<String, String>();
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<RequiredAction> getRequiredActions() {
public Set<RequiredAction> getRequiredActions() {
RequiredAction[] actions = getRequiredActionsArray();
if (actions == null) {
return null;
return Collections.emptySet();
} else {
return Collections.unmodifiableList(Arrays.asList(actions));
Set<RequiredAction> s = new HashSet<RequiredAction>();
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<RequiredAction[]> a = new Attribute<RequiredAction[]>(REQUIRED_ACTIONS_ATTR, actions);

View file

@ -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<String, String> 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<String, String> 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<String, String> 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<RequiredAction> requiredActions = new HashSet<RequiredAction>(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) {

View file

@ -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();

View file

@ -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<String, String> res = new HashMap<String, String>();
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<String, String> res = new HashMap<String, String>();
res.put("error", "invalid_grant");

View file

@ -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<RequiredAction> requiredActions = user.getRequiredActions();
if (!requiredActions.isEmpty()) {
accessCode.setRequiredActions(new HashSet<UserModel.RequiredAction>(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 {

View file

@ -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);

View file

@ -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());
}
}

View file

@ -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" }

View file

@ -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": [

View file

@ -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());
}

View file

@ -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());
}
}

View file

@ -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" }

View file

@ -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" }

View file

@ -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"]