Merge pull request #1559 from patriot1burke/master

totp and profile refactor
This commit is contained in:
Bill Burke 2015-08-21 20:59:51 -04:00
commit d36376ffcd
8 changed files with 123 additions and 137 deletions

View file

@ -5,7 +5,7 @@
<#elseif section = "header"> <#elseif section = "header">
${msg("loginTotpTitle")} ${msg("loginTotpTitle")}
<#elseif section = "form"> <#elseif section = "form">
<form action="${url.loginUpdateTotpUrl}" class="${properties.kcFormClass!}" id="kc-totp-settings-form" method="post"> <form action="${url.loginAction}" class="${properties.kcFormClass!}" id="kc-totp-settings-form" method="post">
<div class="${properties.kcFormGroupClass!}"> <div class="${properties.kcFormGroupClass!}">
<div class="${properties.kcLabelWrapperClass!}"> <div class="${properties.kcLabelWrapperClass!}">
<label for="otp" class="${properties.kcLabelClass!}">${msg("loginTotpOneTime")}</label> <label for="otp" class="${properties.kcLabelClass!}">${msg("loginTotpOneTime")}</label>

View file

@ -5,7 +5,7 @@
<#elseif section = "header"> <#elseif section = "header">
${msg("loginProfileTitle")} ${msg("loginProfileTitle")}
<#elseif section = "form"> <#elseif section = "form">
<form id="kc-update-profile-form" class="${properties.kcFormClass!}" action="${url.loginUpdateProfileUrl}" method="post"> <form id="kc-update-profile-form" class="${properties.kcFormClass!}" action="${url.loginAction}" method="post">
<div class="${properties.kcFormGroupClass!} ${messagesPerField.printIfExists('email',properties.kcFormGroupErrorClass!)}"> <div class="${properties.kcFormGroupClass!} ${messagesPerField.printIfExists('email',properties.kcFormGroupErrorClass!)}">
<div class="${properties.kcLabelWrapperClass!}"> <div class="${properties.kcLabelWrapperClass!}">
<label for="email" class="${properties.kcLabelClass!}">${msg("email")}</label> <label for="email" class="${properties.kcLabelClass!}">${msg("email")}</label>

View file

@ -58,7 +58,8 @@ public class UpdatePassword implements RequiredActionProvider, RequiredActionFac
@Override @Override
public void requiredActionChallenge(RequiredActionContext context) { public void requiredActionChallenge(RequiredActionContext context) {
Response challenge = context.form().createForm("login-update-password.ftl"); Response challenge = context.form()
.createResponse(UserModel.RequiredAction.UPDATE_PASSWORD);
context.challenge(challenge); context.challenge(challenge);
} }
@ -73,13 +74,13 @@ public class UpdatePassword implements RequiredActionProvider, RequiredActionFac
if (Validation.isBlank(passwordNew)) { if (Validation.isBlank(passwordNew)) {
Response challenge = context.form() Response challenge = context.form()
.setError(Messages.MISSING_PASSWORD) .setError(Messages.MISSING_PASSWORD)
.createForm("login-update-password.ftl"); .createResponse(UserModel.RequiredAction.UPDATE_PASSWORD);
context.challenge(challenge); context.challenge(challenge);
return; return;
} else if (!passwordNew.equals(passwordConfirm)) { } else if (!passwordNew.equals(passwordConfirm)) {
Response challenge = context.form() Response challenge = context.form()
.setError(Messages.NOTMATCH_PASSWORD) .setError(Messages.NOTMATCH_PASSWORD)
.createForm("login-update-password.ftl"); .createResponse(UserModel.RequiredAction.UPDATE_PASSWORD);
context.challenge(challenge); context.challenge(challenge);
return; return;
} }
@ -90,13 +91,13 @@ public class UpdatePassword implements RequiredActionProvider, RequiredActionFac
} catch (ModelException me) { } catch (ModelException me) {
Response challenge = context.form() Response challenge = context.form()
.setError(me.getMessage(), me.getParameters()) .setError(me.getMessage(), me.getParameters())
.createForm("login-update-password.ftl"); .createResponse(UserModel.RequiredAction.UPDATE_PASSWORD);
context.challenge(challenge); context.challenge(challenge);
return; return;
} catch (Exception ape) { } catch (Exception ape) {
Response challenge = context.form() Response challenge = context.form()
.setError(ape.getMessage()) .setError(ape.getMessage())
.createForm("login-update-password.ftl"); .createResponse(UserModel.RequiredAction.UPDATE_PASSWORD);
context.challenge(challenge); context.challenge(challenge);
return; return;
} }

View file

@ -5,12 +5,25 @@ import org.keycloak.Config;
import org.keycloak.authentication.RequiredActionContext; import org.keycloak.authentication.RequiredActionContext;
import org.keycloak.authentication.RequiredActionFactory; import org.keycloak.authentication.RequiredActionFactory;
import org.keycloak.authentication.RequiredActionProvider; import org.keycloak.authentication.RequiredActionProvider;
import org.keycloak.events.Details;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.login.LoginFormsProvider; import org.keycloak.login.LoginFormsProvider;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.FormMessage;
import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.resources.AttributeFormDataProcessor;
import org.keycloak.services.validation.Validation;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import java.util.List;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -24,16 +37,63 @@ public class UpdateProfile implements RequiredActionProvider, RequiredActionFact
@Override @Override
public void requiredActionChallenge(RequiredActionContext context) { public void requiredActionChallenge(RequiredActionContext context) {
LoginFormsProvider loginFormsProvider = context.getSession().getProvider(LoginFormsProvider.class) Response challenge = context.form()
.setClientSessionCode(context.generateAccessCode(UserModel.RequiredAction.UPDATE_PROFILE.name())) .createResponse(UserModel.RequiredAction.UPDATE_PROFILE);
.setUser(context.getUser());
Response challenge = loginFormsProvider.createResponse(UserModel.RequiredAction.UPDATE_PROFILE);
context.challenge(challenge); context.challenge(challenge);
} }
@Override @Override
public void processAction(RequiredActionContext context) { public void processAction(RequiredActionContext context) {
context.failure(); EventBuilder event = context.getEvent();
event.event(EventType.UPDATE_PROFILE);
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
UserModel user = context.getUser();
KeycloakSession session = context.getSession();
RealmModel realm = context.getRealm();
List<FormMessage> errors = Validation.validateUpdateProfileForm(formData);
if (errors != null && !errors.isEmpty()) {
Response challenge = context.form()
.setErrors(errors)
.setFormData(formData)
.createResponse(UserModel.RequiredAction.UPDATE_PROFILE);
context.challenge(challenge);
return;
}
user.setFirstName(formData.getFirst("firstName"));
user.setLastName(formData.getFirst("lastName"));
String email = formData.getFirst("email");
String oldEmail = user.getEmail();
boolean emailChanged = oldEmail != null ? !oldEmail.equals(email) : email != null;
if (emailChanged) {
UserModel userByEmail = session.users().getUserByEmail(email, realm);
// check for duplicated email
if (userByEmail != null && !userByEmail.getId().equals(user.getId())) {
Response challenge = context.form()
.setError(Messages.EMAIL_EXISTS)
.setFormData(formData)
.createResponse(UserModel.RequiredAction.UPDATE_PROFILE);
context.challenge(challenge);
return;
}
user.setEmail(email);
user.setEmailVerified(false);
}
AttributeFormDataProcessor.process(formData, realm, user);
if (emailChanged) {
event.clone().event(EventType.UPDATE_EMAIL).detail(Details.PREVIOUS_EMAIL, oldEmail).detail(Details.UPDATED_EMAIL, email).success();
}
context.success();
} }

View file

@ -5,13 +5,23 @@ import org.keycloak.Config;
import org.keycloak.authentication.RequiredActionContext; import org.keycloak.authentication.RequiredActionContext;
import org.keycloak.authentication.RequiredActionFactory; import org.keycloak.authentication.RequiredActionFactory;
import org.keycloak.authentication.RequiredActionProvider; import org.keycloak.authentication.RequiredActionProvider;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.login.LoginFormsProvider; import org.keycloak.login.LoginFormsProvider;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RequiredCredentialModel; import org.keycloak.models.RequiredCredentialModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.CredentialValidation;
import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.validation.Validation;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
/** /**
@ -26,16 +36,47 @@ public class UpdateTotp implements RequiredActionProvider, RequiredActionFactory
@Override @Override
public void requiredActionChallenge(RequiredActionContext context) { public void requiredActionChallenge(RequiredActionContext context) {
LoginFormsProvider loginFormsProvider = context.getSession().getProvider(LoginFormsProvider.class) Response challenge = context.form()
.setClientSessionCode(context.generateAccessCode(UserModel.RequiredAction.CONFIGURE_TOTP.name())) .createResponse(UserModel.RequiredAction.CONFIGURE_TOTP);
.setUser(context.getUser());
Response challenge = loginFormsProvider.createResponse(UserModel.RequiredAction.CONFIGURE_TOTP);
context.challenge(challenge); context.challenge(challenge);
} }
@Override @Override
public void processAction(RequiredActionContext context) { public void processAction(RequiredActionContext context) {
context.failure(); EventBuilder event = context.getEvent();
event.event(EventType.UPDATE_TOTP);
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
String totp = formData.getFirst("totp");
String totpSecret = formData.getFirst("totpSecret");
if (Validation.isBlank(totp)) {
Response challenge = context.form()
.setError(Messages.MISSING_TOTP)
.createResponse(UserModel.RequiredAction.CONFIGURE_TOTP);
context.challenge(challenge);
return;
} else if (!CredentialValidation.validOTP(context.getRealm(), totp, totpSecret)) {
Response challenge = context.form()
.setError(Messages.INVALID_TOTP)
.createResponse(UserModel.RequiredAction.CONFIGURE_TOTP);
context.challenge(challenge);
return;
}
UserCredentialModel credentials = new UserCredentialModel();
credentials.setType(context.getRealm().getOTPPolicy().getType());
credentials.setValue(totpSecret);
context.getSession().users().updateCredential(context.getRealm(), context.getUser(), credentials);
// if type is HOTP, to update counter we execute validation based on supplied token
UserCredentialModel cred = new UserCredentialModel();
cred.setType(context.getRealm().getOTPPolicy().getType());
cred.setValue(totp);
context.getSession().users().validCredentials(context.getRealm(), context.getUser(), cred);
context.getUser().setOtpEnabled(true);
context.success();
} }

View file

@ -512,122 +512,6 @@ public class LoginActionsService {
return authManager.redirectAfterSuccessfulFlow(session, realm, userSession, clientSession, request, uriInfo, clientConnection); return authManager.redirectAfterSuccessfulFlow(session, realm, userSession, clientSession, request, uriInfo, clientConnection);
} }
@Path("profile")
@POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public Response updateProfile(@QueryParam("code") String code,
final MultivaluedMap<String, String> formData) {
event.event(EventType.UPDATE_PROFILE);
Checks checks = new Checks();
if (!checks.verifyCode(code, ClientSessionModel.Action.UPDATE_PROFILE.name())) {
return checks.response;
}
ClientSessionCode accessCode = checks.clientCode;
ClientSessionModel clientSession = accessCode.getClientSession();
UserSessionModel userSession = clientSession.getUserSession();
UserModel user = userSession.getUser();
initEvent(clientSession);
List<FormMessage> errors = Validation.validateUpdateProfileForm(formData);
if (errors != null && !errors.isEmpty()) {
return session.getProvider(LoginFormsProvider.class)
.setClientSessionCode(accessCode.getCode())
.setUser(user)
.setErrors(errors)
.setFormData(formData)
.createResponse(RequiredAction.UPDATE_PROFILE);
}
user.setFirstName(formData.getFirst("firstName"));
user.setLastName(formData.getFirst("lastName"));
String email = formData.getFirst("email");
String oldEmail = user.getEmail();
boolean emailChanged = oldEmail != null ? !oldEmail.equals(email) : email != null;
if (emailChanged) {
UserModel userByEmail = session.users().getUserByEmail(email, realm);
// check for duplicated email
if (userByEmail != null && !userByEmail.getId().equals(user.getId())) {
return session.getProvider(LoginFormsProvider.class)
.setUser(user)
.setError(Messages.EMAIL_EXISTS)
.setClientSessionCode(accessCode.getCode())
.setFormData(formData)
.createResponse(RequiredAction.UPDATE_PROFILE);
}
user.setEmail(email);
user.setEmailVerified(false);
}
AttributeFormDataProcessor.process(formData, realm, user);
user.removeRequiredAction(RequiredAction.UPDATE_PROFILE);
event.clone().event(EventType.UPDATE_PROFILE).success();
if (emailChanged) {
event.clone().event(EventType.UPDATE_EMAIL).detail(Details.PREVIOUS_EMAIL, oldEmail).detail(Details.UPDATED_EMAIL, email).success();
}
return AuthenticationManager.nextActionAfterAuthentication(session, userSession, clientSession, clientConnection, request, uriInfo, event);
}
@Path("totp")
@POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public Response updateTotp(@QueryParam("code") String code,
final MultivaluedMap<String, String> formData) {
event.event(EventType.UPDATE_TOTP);
Checks checks = new Checks();
if (!checks.verifyCode(code, ClientSessionModel.Action.CONFIGURE_TOTP.name())) {
return checks.response;
}
ClientSessionCode accessCode = checks.clientCode;
ClientSessionModel clientSession = accessCode.getClientSession();
UserSessionModel userSession = clientSession.getUserSession();
UserModel user = userSession.getUser();
initEvent(clientSession);
String totp = formData.getFirst("totp");
String totpSecret = formData.getFirst("totpSecret");
LoginFormsProvider loginForms = session.getProvider(LoginFormsProvider.class).setUser(user);
if (Validation.isBlank(totp)) {
return loginForms.setError(Messages.MISSING_TOTP)
.setClientSessionCode(accessCode.getCode())
.createResponse(RequiredAction.CONFIGURE_TOTP);
} else if (!CredentialValidation.validOTP(realm, totp, totpSecret)) {
return loginForms.setError(Messages.INVALID_TOTP)
.setClientSessionCode(accessCode.getCode())
.createResponse(RequiredAction.CONFIGURE_TOTP);
}
UserCredentialModel credentials = new UserCredentialModel();
credentials.setType(realm.getOTPPolicy().getType());
credentials.setValue(totpSecret);
session.users().updateCredential(realm, user, credentials);
// if type is HOTP, to update counter we execute validation based on supplied token
UserCredentialModel cred = new UserCredentialModel();
cred.setType(realm.getOTPPolicy().getType());
cred.setValue(totp);
session.users().validCredentials(realm, user, cred);
user.setOtpEnabled(true);
user.removeRequiredAction(RequiredAction.CONFIGURE_TOTP);
event.clone().event(EventType.UPDATE_TOTP).success();
return AuthenticationManager.nextActionAfterAuthentication(session, userSession, clientSession, clientConnection, request, uriInfo, event);
}
@Path("email-verification") @Path("email-verification")
@GET @GET
public Response emailVerification(@QueryParam("code") String code, @QueryParam("key") String key) { public Response emailVerification(@QueryParam("code") String code, @QueryParam("key") String key) {

View file

@ -123,12 +123,12 @@ public class RequiredActionMultipleActionsTest {
public String updateProfile(String sessionId) { public String updateProfile(String sessionId) {
updateProfilePage.update("New first", "New last", "new@email.com"); updateProfilePage.update("New first", "New last", "new@email.com");
AssertEvents.ExpectedEvent expectedEvent = events.expectRequiredAction(EventType.UPDATE_PROFILE); AssertEvents.ExpectedEvent expectedEvent = events.expectRequiredAction(EventType.UPDATE_EMAIL).detail(Details.PREVIOUS_EMAIL, "test-user@localhost").detail(Details.UPDATED_EMAIL, "new@email.com");
if (sessionId != null) { if (sessionId != null) {
expectedEvent.session(sessionId); expectedEvent.session(sessionId);
} }
sessionId = expectedEvent.assertEvent().getSessionId(); sessionId = expectedEvent.assertEvent().getSessionId();
events.expectRequiredAction(EventType.UPDATE_EMAIL).session(sessionId).detail(Details.PREVIOUS_EMAIL, "test-user@localhost").detail(Details.UPDATED_EMAIL, "new@email.com").assertEvent(); events.expectRequiredAction(EventType.UPDATE_PROFILE).session(sessionId).assertEvent();
return sessionId; return sessionId;
} }

View file

@ -89,8 +89,8 @@ public class RequiredActionUpdateProfileTest {
updateProfilePage.update("New first", "New last", "new@email.com"); updateProfilePage.update("New first", "New last", "new@email.com");
String sessionId = events.expectRequiredAction(EventType.UPDATE_PROFILE).assertEvent().getSessionId(); String sessionId = events.expectRequiredAction(EventType.UPDATE_EMAIL).detail(Details.PREVIOUS_EMAIL, "test-user@localhost").detail(Details.UPDATED_EMAIL, "new@email.com").assertEvent().getSessionId();
events.expectRequiredAction(EventType.UPDATE_EMAIL).session(sessionId).detail(Details.PREVIOUS_EMAIL, "test-user@localhost").detail(Details.UPDATED_EMAIL, "new@email.com").assertEvent(); events.expectRequiredAction(EventType.UPDATE_PROFILE).session(sessionId).assertEvent();
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());