form action refactor

This commit is contained in:
Bill Burke 2015-07-01 14:07:02 -04:00
parent a1c612f833
commit 39aa09ca36
30 changed files with 554 additions and 504 deletions

View file

@ -13,6 +13,7 @@ kerberosNotConfigured=Kerberos Not Configured
kerberosNotConfiguredTitle=Kerberos Not Configured kerberosNotConfiguredTitle=Kerberos Not Configured
bypassKerberos=Your browser is not set up for Kerberos login. Please click continue to login in through other means bypassKerberos=Your browser is not set up for Kerberos login. Please click continue to login in through other means
kerberosNotSetUp=Kerberos is not set up. You cannot login. kerberosNotSetUp=Kerberos is not set up. You cannot login.
recaptchaFailed=Recaptcha Failed
registerWithTitle=Registrierung bei {0} registerWithTitle=Registrierung bei {0}
registerWithTitleHtml=Registrierung bei <strong>{0}</strong> registerWithTitleHtml=Registrierung bei <strong>{0}</strong>

View file

@ -30,6 +30,7 @@ codeSuccessTitle=Success code
codeErrorTitle=Error code\: {0} codeErrorTitle=Error code\: {0}
termsTitle=Terms and Conditions termsTitle=Terms and Conditions
termsTitleHtml=Terms and Conditions termsTitleHtml=Terms and Conditions
recaptchaFailed=Recaptcha Failed
noAccount=New user? noAccount=New user?
username=Username username=Username

View file

@ -13,6 +13,7 @@ bypassKerberos=Your browser is not set up for Kerberos login. Please click cont
kerberosNotSetUp=Kerberos is not set up. You cannot login. kerberosNotSetUp=Kerberos is not set up. You cannot login.
kerberosNotConfigured=Kerberos Not Configured kerberosNotConfigured=Kerberos Not Configured
kerberosNotConfiguredTitle=Kerberos Not Configured kerberosNotConfiguredTitle=Kerberos Not Configured
recaptchaFailed=Recaptcha Failed
registerWithTitle=Registrati come {0} registerWithTitle=Registrati come {0}
registerWithTitleHtml=Registrati come <strong>{0}</strong> registerWithTitleHtml=Registrati come <strong>{0}</strong>

View file

@ -13,6 +13,7 @@ bypassKerberos=Your browser is not set up for Kerberos login. Please click cont
kerberosNotSetUp=Kerberos is not set up. You cannot login. kerberosNotSetUp=Kerberos is not set up. You cannot login.
kerberosNotConfigured=Kerberos Not Configured kerberosNotConfigured=Kerberos Not Configured
kerberosNotConfiguredTitle=Kerberos Not Configured kerberosNotConfiguredTitle=Kerberos Not Configured
recaptchaFailed=Recaptcha Failed
registerWithTitle=Registre-se com {0} registerWithTitle=Registre-se com {0}
registerWithTitleHtml=Registre-se com <strong>{0}</strong> registerWithTitleHtml=Registre-se com <strong>{0}</strong>

View file

@ -107,7 +107,13 @@
<input type="text" class="${properties.kcInputClass!}" id="user.attributes.country" name="user.attributes.country"/> <input type="text" class="${properties.kcInputClass!}" id="user.attributes.country" name="user.attributes.country"/>
</div> </div>
</div> </div>
<#if recaptchaRequired??>
<div class="form-group">
<div class="${properties.kcInputWrapperClass!}">
<div class="g-recaptcha" data-sitekey="${recaptchaSiteKey}"></div>
</div>
</div>
</#if>
<div class="${properties.kcFormGroupClass!}"> <div class="${properties.kcFormGroupClass!}">
<div id="kc-form-options" class="${properties.kcFormOptionsClass!}"> <div id="kc-form-options" class="${properties.kcFormOptionsClass!}">

View file

@ -21,6 +21,11 @@
<script src="${url.resourcesPath}/${script}" type="text/javascript"></script> <script src="${url.resourcesPath}/${script}" type="text/javascript"></script>
</#list> </#list>
</#if> </#if>
<#if scripts??>
<#list scripts as script>
<script src="${script}" type="text/javascript"></script>
</#list>
</#if>
</head> </head>
<body class="${properties.kcBodyClass!}"> <body class="${properties.kcBodyClass!}">

View file

@ -63,6 +63,8 @@ public interface LoginFormsProvider extends Provider {
*/ */
public LoginFormsProvider setErrors(List<FormMessage> messages); public LoginFormsProvider setErrors(List<FormMessage> messages);
LoginFormsProvider addError(FormMessage errorMessage);
public LoginFormsProvider setSuccess(String message, Object ... parameters); public LoginFormsProvider setSuccess(String message, Object ... parameters);
public LoginFormsProvider setUser(UserModel user); public LoginFormsProvider setUser(UserModel user);

View file

@ -3,7 +3,6 @@ package org.keycloak.login.freemarker;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.jboss.resteasy.specimpl.MultivaluedMapImpl; import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
import org.keycloak.OAuth2Constants; import org.keycloak.OAuth2Constants;
import org.keycloak.authentication.AuthenticationProcessor;
import org.keycloak.email.EmailException; import org.keycloak.email.EmailException;
import org.keycloak.email.EmailProvider; import org.keycloak.email.EmailProvider;
import org.keycloak.freemarker.BrowserSecurityHeaderSetup; import org.keycloak.freemarker.BrowserSecurityHeaderSetup;
@ -39,8 +38,8 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel; import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.utils.FormMessage; import org.keycloak.models.utils.FormMessage;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.Urls; import org.keycloak.services.Urls;
import org.keycloak.services.messages.Messages;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.MultivaluedMap;
@ -52,6 +51,7 @@ import java.net.URI;
import java.text.MessageFormat; import java.text.MessageFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
@ -445,11 +445,27 @@ import java.util.concurrent.TimeUnit;
@Override @Override
public LoginFormsProvider setErrors(List<FormMessage> messages) { public LoginFormsProvider setErrors(List<FormMessage> messages) {
if (messages == null) return this;
this.messageType = MessageType.ERROR; this.messageType = MessageType.ERROR;
this.messages = new ArrayList<>(messages); this.messages = new ArrayList<>(messages);
return this; return this;
} }
@Override
public LoginFormsProvider addError(FormMessage errorMessage) {
if (this.messageType != MessageType.ERROR) {
this.messageType = null;
this.messages = null;
}
if (messages == null) {
this.messageType = MessageType.ERROR;
this.messages = new LinkedList<>();
}
this.messages.add(errorMessage);
return this;
}
@Override @Override
public FreeMarkerLoginFormsProvider setSuccess(String message, Object... parameters) { public FreeMarkerLoginFormsProvider setSuccess(String message, Object... parameters) {
setMessage(MessageType.SUCCESS, message, parameters); setMessage(MessageType.SUCCESS, message, parameters);

View file

@ -34,7 +34,9 @@ public class DefaultAuthenticationFlows {
registrationFormFlow.setProviderId("form-flow"); registrationFormFlow.setProviderId("form-flow");
registrationFormFlow = realm.addAuthenticationFlow(registrationFormFlow); registrationFormFlow = realm.addAuthenticationFlow(registrationFormFlow);
AuthenticationExecutionModel execution = new AuthenticationExecutionModel(); AuthenticationExecutionModel execution;
execution = new AuthenticationExecutionModel();
execution.setParentFlow(registrationFlow.getId()); execution.setParentFlow(registrationFlow.getId());
execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED); execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
execution.setAuthenticator("registration-page-form"); execution.setAuthenticator("registration-page-form");
@ -47,7 +49,7 @@ public class DefaultAuthenticationFlows {
execution = new AuthenticationExecutionModel(); execution = new AuthenticationExecutionModel();
execution.setParentFlow(registrationFormFlow.getId()); execution.setParentFlow(registrationFormFlow.getId());
execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED); execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
execution.setAuthenticator("username-validation-action"); execution.setAuthenticator("registration-user-creation");
execution.setPriority(20); execution.setPriority(20);
execution.setUserSetupAllowed(false); execution.setUserSetupAllowed(false);
execution.setAutheticatorFlow(false); execution.setAutheticatorFlow(false);
@ -56,16 +58,7 @@ public class DefaultAuthenticationFlows {
execution = new AuthenticationExecutionModel(); execution = new AuthenticationExecutionModel();
execution.setParentFlow(registrationFormFlow.getId()); execution.setParentFlow(registrationFormFlow.getId());
execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED); execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
execution.setAuthenticator("profile-validation-action"); execution.setAuthenticator("registration-profile-action");
execution.setPriority(30);
execution.setUserSetupAllowed(false);
execution.setAutheticatorFlow(false);
realm.addAuthenticatorExecution(execution);
execution = new AuthenticationExecutionModel();
execution.setParentFlow(registrationFormFlow.getId());
execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
execution.setAuthenticator("password-validation-action");
execution.setPriority(40); execution.setPriority(40);
execution.setUserSetupAllowed(false); execution.setUserSetupAllowed(false);
execution.setAutheticatorFlow(false); execution.setAutheticatorFlow(false);
@ -74,12 +67,22 @@ public class DefaultAuthenticationFlows {
execution = new AuthenticationExecutionModel(); execution = new AuthenticationExecutionModel();
execution.setParentFlow(registrationFormFlow.getId()); execution.setParentFlow(registrationFormFlow.getId());
execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED); execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
execution.setAuthenticator("registration-user-creation"); execution.setAuthenticator("registration-password-action");
execution.setPriority(50); execution.setPriority(50);
execution.setUserSetupAllowed(false); execution.setUserSetupAllowed(false);
execution.setAutheticatorFlow(false); execution.setAutheticatorFlow(false);
realm.addAuthenticatorExecution(execution); realm.addAuthenticatorExecution(execution);
execution = new AuthenticationExecutionModel();
execution.setParentFlow(registrationFormFlow.getId());
execution.setRequirement(AuthenticationExecutionModel.Requirement.DISABLED);
execution.setAuthenticator("registration-recaptcha-action");
execution.setPriority(60);
execution.setUserSetupAllowed(false);
execution.setAutheticatorFlow(false);
realm.addAuthenticatorExecution(execution);
} }

View file

@ -227,7 +227,8 @@ public class ClientSessionAdapter implements ClientSessionModel {
@Override @Override
public void setAuthenticatedUser(UserModel user) { public void setAuthenticatedUser(UserModel user) {
entity.setAuthUserId(user.getId()); if (user == null) entity.setAuthUserId(null);
else entity.setAuthUserId(user.getId());
update(); update();
} }

View file

@ -306,6 +306,7 @@ public class ClientSessionAdapter implements ClientSessionModel {
@Override @Override
public void setAuthenticatedUser(UserModel user) { public void setAuthenticatedUser(UserModel user) {
entity.setUserId(user.getId()); if (user == null) entity.setUserId(null);
else entity.setUserId(user.getId());
} }
} }

View file

@ -193,7 +193,8 @@ public class ClientSessionAdapter implements ClientSessionModel {
@Override @Override
public void setAuthenticatedUser(UserModel user) { public void setAuthenticatedUser(UserModel user) {
entity.setAuthUserId(user.getId()); if (user == null) entity.setAuthUserId(null);
else entity.setAuthUserId(user.getId());
} }
} }

View file

@ -210,7 +210,8 @@ public class ClientSessionAdapter extends AbstractMongoAdapter<MongoClientSessio
@Override @Override
public void setAuthenticatedUser(UserModel user) { public void setAuthenticatedUser(UserModel user) {
entity.setAuthUserId(user.getId()); if (user == null) entity.setAuthUserId(null);
else entity.setAuthUserId(user.getId());
updateMongoEntity(); updateMongoEntity();
} }

View file

@ -45,7 +45,6 @@ public class AuthenticationProcessor {
protected EventBuilder event; protected EventBuilder event;
protected HttpRequest request; protected HttpRequest request;
protected String flowId; protected String flowId;
protected String action;
/** /**
* This could be an error message forwarded from brokering when the broker failed authentication * This could be an error message forwarded from brokering when the broker failed authentication
* and we want to continue authentication locally. forwardedErrorMessage can then be displayed by * and we want to continue authentication locally. forwardedErrorMessage can then be displayed by
@ -151,16 +150,39 @@ public class AuthenticationProcessor {
return this; return this;
} }
public AuthenticationProcessor setAction(String action) {
this.action = action;
return this;
}
public AuthenticationProcessor setForwardedErrorMessage(String forwardedErrorMessage) { public AuthenticationProcessor setForwardedErrorMessage(String forwardedErrorMessage) {
this.forwardedErrorMessage = forwardedErrorMessage; this.forwardedErrorMessage = forwardedErrorMessage;
return this; return this;
} }
public String generateCode() {
ClientSessionCode accessCode = new ClientSessionCode(getRealm(), getClientSession());
clientSession.setTimestamp(Time.currentTime());
return accessCode.getCode();
}
public EventBuilder newEvent() {
this.event = new EventBuilder(realm, session, connection);
return this.event;
}
public EventBuilder getEvent() {
return event;
}
public HttpRequest getRequest() {
return request;
}
public void setAutheticatedUser(UserModel user) {
UserModel previousUser = clientSession.getAuthenticatedUser();
if (previousUser != null && !user.getId().equals(previousUser.getId()))
throw new AuthException(Error.USER_CONFLICT);
validateUser(user);
getClientSession().setAuthenticatedUser(user);
}
private class Result implements AuthenticatorContext { private class Result implements AuthenticatorContext {
AuthenticatorConfigModel authenticatorConfig; AuthenticatorConfigModel authenticatorConfig;
AuthenticationExecutionModel execution; AuthenticationExecutionModel execution;
@ -178,8 +200,7 @@ public class AuthenticationProcessor {
@Override @Override
public EventBuilder newEvent() { public EventBuilder newEvent() {
AuthenticationProcessor.this.event = new EventBuilder(realm, session, connection); return AuthenticationProcessor.this.newEvent();
return AuthenticationProcessor.this.event;
} }
@Override @Override
@ -213,11 +234,6 @@ public class AuthenticationProcessor {
return authenticatorConfig; return authenticatorConfig;
} }
@Override
public String getAction() {
return AuthenticationProcessor.this.action;
}
@Override @Override
public Authenticator getAuthenticator() { public Authenticator getAuthenticator() {
return authenticator; return authenticator;
@ -288,11 +304,7 @@ public class AuthenticationProcessor {
@Override @Override
public void setUser(UserModel user) { public void setUser(UserModel user) {
UserModel previousUser = getUser(); setAutheticatedUser(user);
if (previousUser != null && !user.getId().equals(previousUser.getId()))
throw new AuthException(Error.USER_CONFLICT);
validateUser(user);
getClientSession().setAuthenticatedUser(user);
} }
@Override @Override
@ -347,11 +359,10 @@ public class AuthenticationProcessor {
@Override @Override
public String generateAccessCode() { public String generateAccessCode() {
ClientSessionCode accessCode = new ClientSessionCode(getRealm(), getClientSession()); return generateCode();
clientSession.setTimestamp(Time.currentTime());
return accessCode.getCode();
} }
@Override @Override
public Response getChallenge() { public Response getChallenge() {
return challenge; return challenge;

View file

@ -27,16 +27,6 @@ public interface AuthenticatorContext {
void setExecution(AuthenticationExecutionModel execution); void setExecution(AuthenticationExecutionModel execution);
AuthenticatorConfigModel getAuthenticatorConfig();
String getAction();
Authenticator getAuthenticator();
void setAuthenticator(Authenticator authenticator);
AuthenticationProcessor.Status getStatus();
UserModel getUser(); UserModel getUser();
void setUser(UserModel user); void setUser(UserModel user);
@ -55,17 +45,6 @@ public interface AuthenticatorContext {
HttpRequest getHttpRequest(); HttpRequest getHttpRequest();
BruteForceProtector getProtector(); BruteForceProtector getProtector();
AuthenticationExecutionModel.Requirement getCategoryRequirementFromCurrentFlow(String authenticatorCategory);
void success();
void failure(AuthenticationProcessor.Error error);
void failure(AuthenticationProcessor.Error error, Response response);
void challenge(Response challenge);
void forceChallenge(Response challenge);
void failureChallenge(AuthenticationProcessor.Error error, Response challenge);
void attempted();
/** /**
* This could be an error message forwarded from brokering when the broker failed authentication * This could be an error message forwarded from brokering when the broker failed authentication
@ -81,6 +60,27 @@ public interface AuthenticatorContext {
*/ */
String generateAccessCode(); String generateAccessCode();
AuthenticatorConfigModel getAuthenticatorConfig();
Authenticator getAuthenticator();
void setAuthenticator(Authenticator authenticator);
AuthenticationProcessor.Status getStatus();
AuthenticationExecutionModel.Requirement getCategoryRequirementFromCurrentFlow(String authenticatorCategory);
void success();
void failure(AuthenticationProcessor.Error error);
void failure(AuthenticationProcessor.Error error, Response response);
void challenge(Response challenge);
void forceChallenge(Response challenge);
void failureChallenge(AuthenticationProcessor.Error error, Response challenge);
void attempted();
Response getChallenge(); Response getChallenge();
AuthenticationProcessor.Error getError(); AuthenticationProcessor.Error getError();

View file

@ -1,5 +1,6 @@
package org.keycloak.authentication; package org.keycloak.authentication;
import org.keycloak.login.LoginFormsProvider;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
@ -10,7 +11,8 @@ import org.keycloak.provider.Provider;
* @version $Revision: 1 $ * @version $Revision: 1 $
*/ */
public interface FormAction extends Provider { public interface FormAction extends Provider {
void authenticate(FormActionContext context); void validate(ValidationContext context);
void success(FormContext context);
boolean requiresUser(); boolean requiresUser();
boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user); boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user);
@ -21,4 +23,6 @@ public interface FormAction extends Provider {
*/ */
void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user); void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user);
void buildPage(FormContext context, LoginFormsProvider form);
} }

View file

@ -1,12 +0,0 @@
package org.keycloak.authentication;
import org.keycloak.models.AuthenticationExecutionModel;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public interface FormActionContext extends AuthenticatorContext {
FormAuthenticator getFormAuthenticator();
AuthenticationExecutionModel getFormExecution();
}

View file

@ -2,7 +2,9 @@ package org.keycloak.authentication;
import org.jboss.resteasy.spi.HttpRequest; import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.ClientConnection; import org.keycloak.ClientConnection;
import org.keycloak.OAuth2Constants;
import org.keycloak.events.EventBuilder; import org.keycloak.events.EventBuilder;
import org.keycloak.login.LoginFormsProvider;
import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.AuthenticatorConfigModel; import org.keycloak.models.AuthenticatorConfigModel;
import org.keycloak.models.ClientSessionModel; import org.keycloak.models.ClientSessionModel;
@ -10,10 +12,14 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.FormMessage;
import org.keycloak.services.managers.BruteForceProtector; import org.keycloak.services.managers.BruteForceProtector;
import org.keycloak.services.resources.LoginActionsService;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo; import javax.ws.rs.core.UriInfo;
import java.net.URI;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
@ -37,183 +43,99 @@ public class FormAuthenticationFlow implements AuthenticationFlow {
formAuthenticator = processor.getSession().getProvider(FormAuthenticator.class, execution.getAuthenticator()); formAuthenticator = processor.getSession().getProvider(FormAuthenticator.class, execution.getAuthenticator());
} }
private class FormContext implements FormActionContext { private class FormContextImpl implements FormContext {
protected AuthenticatorContext delegate; AuthenticationExecutionModel executionModel;
AuthenticatorConfigModel authenticatorConfig;
private FormContext(AuthenticatorContext delegate) { private FormContextImpl(AuthenticationExecutionModel executionModel) {
this.delegate = delegate; this.executionModel = executionModel;
} }
@Override @Override
public EventBuilder newEvent() { public EventBuilder newEvent() {
return delegate.newEvent(); return processor.newEvent();
}
@Override
public FormAuthenticator getFormAuthenticator() {
return formAuthenticator;
}
@Override
public AuthenticationExecutionModel getFormExecution() {
return formExecution;
} }
@Override @Override
public EventBuilder getEvent() { public EventBuilder getEvent() {
return delegate.getEvent(); return processor.getEvent();
} }
@Override @Override
public AuthenticationExecutionModel getExecution() { public AuthenticationExecutionModel getExecution() {
return delegate.getExecution(); return executionModel;
}
@Override
public void setExecution(AuthenticationExecutionModel execution) {
delegate.setExecution(execution);
} }
@Override @Override
public AuthenticatorConfigModel getAuthenticatorConfig() { public AuthenticatorConfigModel getAuthenticatorConfig() {
return delegate.getAuthenticatorConfig(); if (executionModel.getAuthenticatorConfig() == null) return null;
} if (authenticatorConfig != null) return authenticatorConfig;
authenticatorConfig = getRealm().getAuthenticatorConfigById(executionModel.getAuthenticatorConfig());
@Override return authenticatorConfig;
public String getAction() {
return delegate.getAction();
}
@Override
public Authenticator getAuthenticator() {
return delegate.getAuthenticator();
}
@Override
public void setAuthenticator(Authenticator authenticator) {
delegate.setAuthenticator(authenticator);
}
@Override
public AuthenticationProcessor.Status getStatus() {
return delegate.getStatus();
} }
@Override @Override
public UserModel getUser() { public UserModel getUser() {
return delegate.getUser(); return getClientSession().getAuthenticatedUser();
} }
@Override @Override
public void setUser(UserModel user) { public void setUser(UserModel user) {
delegate.setUser(user); processor.setAutheticatedUser(user);
} }
@Override @Override
public RealmModel getRealm() { public RealmModel getRealm() {
return delegate.getRealm(); return processor.getRealm();
} }
@Override @Override
public ClientSessionModel getClientSession() { public ClientSessionModel getClientSession() {
return delegate.getClientSession(); return processor.getClientSession();
}
@Override
public void attachUserSession(UserSessionModel userSession) {
delegate.attachUserSession(userSession);
} }
@Override @Override
public ClientConnection getConnection() { public ClientConnection getConnection() {
return delegate.getConnection(); return processor.getConnection();
} }
@Override @Override
public UriInfo getUriInfo() { public UriInfo getUriInfo() {
return delegate.getUriInfo(); return processor.getUriInfo();
} }
@Override @Override
public KeycloakSession getSession() { public KeycloakSession getSession() {
return delegate.getSession(); return processor.getSession();
} }
@Override @Override
public HttpRequest getHttpRequest() { public HttpRequest getHttpRequest() {
return delegate.getHttpRequest(); return processor.getRequest();
} }
}
private class ValidationContextImpl extends FormContextImpl implements ValidationContext {
FormAction action;
private ValidationContextImpl(AuthenticationExecutionModel executionModel, FormAction action) {
super(executionModel);
this.action = action;
}
boolean success;
List<FormMessage> errors = null;
MultivaluedMap<String, String> formData = null;
@Override @Override
public BruteForceProtector getProtector() { public void validationError(MultivaluedMap<String, String> formData, List<FormMessage> errors) {
return delegate.getProtector(); this.errors = errors;
} this.formData = formData;
@Override
public AuthenticationExecutionModel.Requirement getCategoryRequirementFromCurrentFlow(String authenticatorCategory) {
for (AuthenticationExecutionModel formActionExecution : formActionExecutions) {
FormActionFactory factory = (FormActionFactory) getSession().getKeycloakSessionFactory().getProviderFactory(FormAction.class, formActionExecution.getAuthenticator());
if (factory != null && authenticatorCategory.equals(factory.getReferenceCategory())) {
return formActionExecution.getRequirement();
}
}
return null;
} }
@Override @Override
public void success() { public void success() {
delegate.success(); success = true;
}
@Override
public void failure(AuthenticationProcessor.Error error) {
delegate.failure(error);
}
@Override
public void failure(AuthenticationProcessor.Error error, Response response) {
delegate.failure(error, response);
}
@Override
public void challenge(Response challenge) {
delegate.challenge(challenge);
}
@Override
public void forceChallenge(Response challenge) {
delegate.forceChallenge(challenge);
}
@Override
public void failureChallenge(AuthenticationProcessor.Error error, Response challenge) {
delegate.failureChallenge(error, challenge);
}
@Override
public void attempted() {
delegate.attempted();
}
@Override
public String getForwardedErrorMessage() {
return delegate.getForwardedErrorMessage();
}
@Override
public String generateAccessCode() {
return delegate.generateAccessCode();
}
@Override
public Response getChallenge() {
return delegate.getChallenge();
}
@Override
public AuthenticationProcessor.Error getError() {
return delegate.getError();
} }
} }
@ -224,7 +146,12 @@ public class FormAuthenticationFlow implements AuthenticationFlow {
} }
Map<String, ClientSessionModel.ExecutionStatus> executionStatus = new HashMap<>(); Map<String, ClientSessionModel.ExecutionStatus> executionStatus = new HashMap<>();
List<FormAction> requiredActions = new LinkedList<>(); List<FormAction> requiredActions = new LinkedList<>();
List<ValidationContextImpl> successes = new LinkedList<>();
for (AuthenticationExecutionModel formActionExecution : formActionExecutions) { for (AuthenticationExecutionModel formActionExecution : formActionExecutions) {
if (!formActionExecution.isEnabled()) {
executionStatus.put(formActionExecution.getId(), ClientSessionModel.ExecutionStatus.SKIPPED);
continue;
}
FormAction action = processor.getSession().getProvider(FormAction.class, formActionExecution.getAuthenticator()); FormAction action = processor.getSession().getProvider(FormAction.class, formActionExecution.getAuthenticator());
UserModel authUser = processor.getClientSession().getAuthenticatedUser(); UserModel authUser = processor.getClientSession().getAuthenticatedUser();
@ -251,12 +178,20 @@ public class FormAuthenticationFlow implements AuthenticationFlow {
} }
} }
AuthenticatorContext delegate = processor.createAuthenticatorContext(formActionExecution, null, formActionExecutions); ValidationContextImpl result = new ValidationContextImpl(formActionExecution, action);
FormActionContext result = new FormContext(delegate); action.validate(result);
action.authenticate(result); if (result.success) {
Response challenge = processResult(executionStatus, result, formActionExecution);
if (challenge != null) return challenge;
executionStatus.put(formActionExecution.getId(), ClientSessionModel.ExecutionStatus.SUCCESS); executionStatus.put(formActionExecution.getId(), ClientSessionModel.ExecutionStatus.SUCCESS);
successes.add(result);
} else {
processor.logFailure();
executionStatus.put(formActionExecution.getId(), ClientSessionModel.ExecutionStatus.CHALLENGED);
return renderForm(result.formData, result.errors);
}
}
for (ValidationContextImpl context : successes) {
context.action.success(context);
} }
// set status and required actions only if form is fully successful // set status and required actions only if form is fully successful
for (Map.Entry<String, ClientSessionModel.ExecutionStatus> entry : executionStatus.entrySet()) { for (Map.Entry<String, ClientSessionModel.ExecutionStatus> entry : executionStatus.entrySet()) {
@ -270,63 +205,36 @@ public class FormAuthenticationFlow implements AuthenticationFlow {
} }
public URI getActionUrl(String executionId, String code) {
return LoginActionsService.registrationFormProcessor(processor.getUriInfo())
.queryParam(OAuth2Constants.CODE, code)
.queryParam("execution", executionId)
.build(processor.getRealm().getName());
}
@Override @Override
public Response processFlow() { public Response processFlow() {
AuthenticatorContext delegate = processor.createAuthenticatorContext(formExecution, null, formActionExecutions); return renderForm(null, null);
FormActionContext result = new FormContext(delegate);
formAuthenticator.authenticate(result);
Map<String, ClientSessionModel.ExecutionStatus> executionStatus = new HashMap<>();
Response response = processResult(executionStatus, result, formExecution);
for (Map.Entry<String, ClientSessionModel.ExecutionStatus> entry : executionStatus.entrySet()) {
processor.getClientSession().setExecutionStatus(entry.getKey(), entry.getValue());
}
return response;
} }
public Response renderForm(MultivaluedMap<String, String> formData, List<FormMessage> errors) {
public Response processResult(Map<String, ClientSessionModel.ExecutionStatus> executionStatus, AuthenticatorContext result, AuthenticationExecutionModel execution) { String executionId = formExecution.getId();
AuthenticationProcessor.Status status = result.getStatus(); processor.getClientSession().setNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION, executionId);
if (status == AuthenticationProcessor.Status.SUCCESS) { String code = processor.generateCode();
executionStatus.put(execution.getId(), ClientSessionModel.ExecutionStatus.SUCCESS); URI actionUrl = getActionUrl(executionId, code);
return null; LoginFormsProvider form = processor.getSession().getProvider(LoginFormsProvider.class)
} else if (status == AuthenticationProcessor.Status.FAILED) { .setActionUri(actionUrl)
AuthenticationProcessor.logger.debugv("authenticator FAILED: {0}", execution.getAuthenticator()); .setClientSessionCode(code)
processor.logFailure(); .setFormData(formData)
executionStatus.put(execution.getId(), ClientSessionModel.ExecutionStatus.FAILED); .setErrors(errors);
if (result.getChallenge() != null) { for (AuthenticationExecutionModel formActionExecution : formActionExecutions) {
return sendChallenge(result); if (!formActionExecution.isEnabled()) continue;
FormAction action = processor.getSession().getProvider(FormAction.class, formActionExecution.getAuthenticator());
FormContext result = new FormContextImpl(formActionExecution);
action.buildPage(result, form);
} }
throw new AuthenticationProcessor.AuthException(result.getError()); FormContext context = new FormContextImpl(formExecution);
} else if (status == AuthenticationProcessor.Status.FORCE_CHALLENGE) { return formAuthenticator.render(context, form);
executionStatus.put(execution.getId(), ClientSessionModel.ExecutionStatus.CHALLENGED);
return sendChallenge(result);
} else if (status == AuthenticationProcessor.Status.CHALLENGE) {
processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.CHALLENGED);
return sendChallenge(result);
} else if (status == AuthenticationProcessor.Status.FAILURE_CHALLENGE) {
AuthenticationProcessor.logger.debugv("authenticator FAILURE_CHALLENGE: {0}", execution.getAuthenticator());
processor.logFailure();
executionStatus.put(execution.getId(), ClientSessionModel.ExecutionStatus.CHALLENGED);
return sendChallenge(result);
} else if (status == AuthenticationProcessor.Status.ATTEMPTED) {
AuthenticationProcessor.logger.debugv("authenticator ATTEMPTED: {0}", execution.getAuthenticator());
if (execution.getRequirement() == AuthenticationExecutionModel.Requirement.REQUIRED) {
throw new AuthenticationProcessor.AuthException(AuthenticationProcessor.Error.INVALID_CREDENTIALS);
} }
executionStatus.put(execution.getId(), ClientSessionModel.ExecutionStatus.ATTEMPTED);
return null;
} else {
AuthenticationProcessor.logger.debugv("authenticator INTERNAL_ERROR: {0}", execution.getAuthenticator());
AuthenticationProcessor.logger.error("Unknown result status");
throw new AuthenticationProcessor.AuthException(AuthenticationProcessor.Error.INTERNAL_ERROR);
}
}
public Response sendChallenge(AuthenticatorContext result) {
processor.getClientSession().setNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION, formExecution.getId());
return result.getChallenge();
}
} }

View file

@ -1,5 +1,6 @@
package org.keycloak.authentication; package org.keycloak.authentication;
import org.keycloak.login.LoginFormsProvider;
import org.keycloak.models.utils.FormMessage; import org.keycloak.models.utils.FormMessage;
import org.keycloak.provider.Provider; import org.keycloak.provider.Provider;
@ -12,6 +13,5 @@ import java.util.List;
* @version $Revision: 1 $ * @version $Revision: 1 $
*/ */
public interface FormAuthenticator extends Provider { public interface FormAuthenticator extends Provider {
void authenticate(AuthenticatorContext context); Response render(FormContext context, LoginFormsProvider form);
Response createChallenge(FormActionContext context, MultivaluedMap<String, String> formData, List<FormMessage> errorMessages);
} }

View file

@ -0,0 +1,33 @@
package org.keycloak.authentication;
import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.ClientConnection;
import org.keycloak.events.EventBuilder;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.AuthenticatorConfigModel;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import javax.ws.rs.core.UriInfo;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public interface FormContext {
EventBuilder getEvent();
EventBuilder newEvent();
AuthenticationExecutionModel getExecution();
UserModel getUser();
void setUser(UserModel user);
RealmModel getRealm();
ClientSessionModel getClientSession();
ClientConnection getConnection();
UriInfo getUriInfo();
KeycloakSession getSession();
HttpRequest getHttpRequest();
AuthenticatorConfigModel getAuthenticatorConfig();
}

View file

@ -0,0 +1,15 @@
package org.keycloak.authentication;
import org.keycloak.models.utils.FormMessage;
import javax.ws.rs.core.MultivaluedMap;
import java.util.List;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public interface ValidationContext extends FormContext {
void validationError(MultivaluedMap<String, String> formData, List<FormMessage> errors);
void success();
}

View file

@ -48,10 +48,6 @@ public class UsernamePasswordForm extends AbstractFormAuthenticator implements A
@Override @Override
public void authenticate(AuthenticatorContext context) { public void authenticate(AuthenticatorContext context) {
if (REGISTRATION_FORM_ACTION.equals(context.getAction()) && context.getUser() != null) {
context.success();
return;
}
MultivaluedMap<String, String> formData = new MultivaluedMapImpl<>(); MultivaluedMap<String, String> formData = new MultivaluedMapImpl<>();
String loginHint = context.getClientSession().getNote(OIDCLoginProtocol.LOGIN_HINT_PARAM); String loginHint = context.getClientSession().getNote(OIDCLoginProtocol.LOGIN_HINT_PARAM);

View file

@ -1,23 +1,15 @@
package org.keycloak.authentication.forms; package org.keycloak.authentication.forms;
import org.keycloak.Config; import org.keycloak.Config;
import org.keycloak.OAuth2Constants;
import org.keycloak.authentication.AuthenticatorContext;
import org.keycloak.authentication.FormActionContext;
import org.keycloak.authentication.FormAuthenticator; import org.keycloak.authentication.FormAuthenticator;
import org.keycloak.authentication.FormAuthenticatorFactory; import org.keycloak.authentication.FormAuthenticatorFactory;
import org.keycloak.authentication.FormContext;
import org.keycloak.login.LoginFormsProvider; import org.keycloak.login.LoginFormsProvider;
import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.utils.FormMessage;
import org.keycloak.services.resources.LoginActionsService;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import java.net.URI;
import java.util.List;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -35,38 +27,8 @@ public class RegistrationPage implements FormAuthenticator, FormAuthenticatorFac
public static final String PROVIDER_ID = "registration-page-form"; public static final String PROVIDER_ID = "registration-page-form";
@Override @Override
public void authenticate(AuthenticatorContext context) { public Response render(FormContext context, LoginFormsProvider form) {
LoginFormsProvider registrationPage = createForm(context, context.getExecution().getId()); return form.createRegistration();
context.challenge(registrationPage.createRegistration());
}
public URI getActionUrl(AuthenticatorContext context, String executionId, String code) {
return LoginActionsService.registrationFormProcessor(context.getUriInfo())
.queryParam(OAuth2Constants.CODE, code)
.queryParam(EXECUTION, executionId)
.build(context.getRealm().getName());
}
@Override
public Response createChallenge(FormActionContext context, MultivaluedMap<String, String> formData, List<FormMessage> errorMessages) {
LoginFormsProvider registrationPage = createForm(context, context.getFormExecution().getId());
if (formData != null) registrationPage.setFormData(formData);
if (errorMessages != null) {
registrationPage.setErrors(errorMessages);
}
return registrationPage.createRegistration();
}
public LoginFormsProvider createForm(AuthenticatorContext context, String executionId) {
AuthenticationExecutionModel.Requirement categoryRequirement = context.getCategoryRequirementFromCurrentFlow(UserCredentialModel.PASSWORD);
boolean passwordRequired = categoryRequirement != null && categoryRequirement != AuthenticationExecutionModel.Requirement.DISABLED;
String code = context.generateAccessCode();
URI actionUrl = getActionUrl(context, executionId, code);
return context.getSession().getProvider(LoginFormsProvider.class)
.setAttribute("passwordRequired", passwordRequired)
.setActionUri(actionUrl)
.setClientSessionCode(code);
} }
@Override @Override

View file

@ -1,13 +1,13 @@
package org.keycloak.authentication.forms; package org.keycloak.authentication.forms;
import org.keycloak.Config; import org.keycloak.Config;
import org.keycloak.authentication.AuthenticatorContext;
import org.keycloak.authentication.FormAction; import org.keycloak.authentication.FormAction;
import org.keycloak.authentication.FormActionContext;
import org.keycloak.authentication.FormActionFactory; import org.keycloak.authentication.FormActionFactory;
import org.keycloak.authentication.FormAuthenticator; import org.keycloak.authentication.FormContext;
import org.keycloak.authentication.ValidationContext;
import org.keycloak.events.Details; import org.keycloak.events.Details;
import org.keycloak.events.Errors; import org.keycloak.events.Errors;
import org.keycloak.login.LoginFormsProvider;
import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.KeycloakSessionFactory;
@ -16,11 +16,11 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.utils.FormMessage; import org.keycloak.models.utils.FormMessage;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.services.messages.Messages; import org.keycloak.services.messages.Messages;
import org.keycloak.services.validation.Validation; import org.keycloak.services.validation.Validation;
import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -28,11 +28,11 @@ import java.util.List;
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $ * @version $Revision: 1 $
*/ */
public class RegistrationPasswordValidation implements FormAction, FormActionFactory { public class RegistrationPassword implements FormAction, FormActionFactory {
public static final String PROVIDER_ID = "password-validation-action"; public static final String PROVIDER_ID = "registration-password-action";
@Override @Override
public void authenticate(FormActionContext context) { public void validate(ValidationContext context) {
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters(); MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
List<FormMessage> errors = new ArrayList<>(); List<FormMessage> errors = new ArrayList<>();
context.getEvent().detail(Details.REGISTER_METHOD, "form"); context.getEvent().detail(Details.REGISTER_METHOD, "form");
@ -51,14 +51,34 @@ public class RegistrationPasswordValidation implements FormAction, FormActionFac
context.getEvent().error(Errors.INVALID_REGISTRATION); context.getEvent().error(Errors.INVALID_REGISTRATION);
formData.remove(RegistrationPage.FIELD_PASSWORD); formData.remove(RegistrationPage.FIELD_PASSWORD);
formData.remove(RegistrationPage.FIELD_PASSWORD_CONFIRM); formData.remove(RegistrationPage.FIELD_PASSWORD_CONFIRM);
Response challenge = context.getFormAuthenticator().createChallenge(context, formData, errors); context.validationError(formData, errors);
context.challenge(challenge);
return; return;
} else { } else {
context.success(); context.success();
} }
} }
@Override
public void success(FormContext context) {
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
String password = formData.getFirst(RegistrationPage.FIELD_PASSWORD);
UserCredentialModel credentials = new UserCredentialModel();
credentials.setType(CredentialRepresentation.PASSWORD);
credentials.setValue(password);
UserModel user = context.getUser();
try {
context.getSession().users().updateCredential(context.getRealm(), user, UserCredentialModel.password(formData.getFirst("password")));
} catch (Exception me) {
user.addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD);
}
}
@Override
public void buildPage(FormContext context, LoginFormsProvider form) {
form.setAttribute("passwordRequired", true);
}
@Override @Override
public boolean requiresUser() { public boolean requiresUser() {
return false; return false;

View file

@ -1,17 +1,16 @@
package org.keycloak.authentication.forms; package org.keycloak.authentication.forms;
import org.keycloak.Config; import org.keycloak.Config;
import org.keycloak.authentication.AuthenticatorContext;
import org.keycloak.authentication.FormAction; import org.keycloak.authentication.FormAction;
import org.keycloak.authentication.FormActionContext;
import org.keycloak.authentication.FormActionFactory; import org.keycloak.authentication.FormActionFactory;
import org.keycloak.authentication.FormAuthenticator; import org.keycloak.authentication.FormContext;
import org.keycloak.authentication.ValidationContext;
import org.keycloak.events.Details; import org.keycloak.events.Details;
import org.keycloak.events.Errors; import org.keycloak.events.Errors;
import org.keycloak.login.LoginFormsProvider;
import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.PasswordPolicy;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.utils.FormMessage; import org.keycloak.models.utils.FormMessage;
@ -19,7 +18,6 @@ import org.keycloak.services.messages.Messages;
import org.keycloak.services.validation.Validation; import org.keycloak.services.validation.Validation;
import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -27,12 +25,12 @@ import java.util.List;
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $ * @version $Revision: 1 $
*/ */
public class RegistrationProfileValidation implements FormAction, FormActionFactory { public class RegistrationProfile implements FormAction, FormActionFactory {
public static final String PROVIDER_ID = "profile-validation-action"; public static final String PROVIDER_ID = "registration-profile-action";
@Override @Override
public void authenticate(FormActionContext context) { public void validate(ValidationContext context) {
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters(); MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
List<FormMessage> errors = new ArrayList<>(); List<FormMessage> errors = new ArrayList<>();
@ -65,8 +63,7 @@ public class RegistrationProfileValidation implements FormAction, FormActionFact
if (errors.size() > 0) { if (errors.size() > 0) {
context.getEvent().error(eventError); context.getEvent().error(eventError);
Response challenge = context.getFormAuthenticator().createChallenge(context, formData, errors); context.validationError(formData, errors);
context.challenge(challenge);
return; return;
} else { } else {
@ -74,6 +71,20 @@ public class RegistrationProfileValidation implements FormAction, FormActionFact
} }
} }
@Override
public void success(FormContext context) {
UserModel user = context.getUser();
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
user.setFirstName(formData.getFirst(RegistrationPage.FIELD_FIRST_NAME));
user.setLastName(formData.getFirst(RegistrationPage.FIELD_LAST_NAME));
user.setEmail(formData.getFirst(RegistrationPage.FIELD_EMAIL));
}
@Override
public void buildPage(FormContext context, LoginFormsProvider form) {
// complete
}
@Override @Override
public boolean requiresUser() { public boolean requiresUser() {
return false; return false;

View file

@ -0,0 +1,169 @@
package org.keycloak.authentication.forms;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.message.BasicNameValuePair;
import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.authentication.FormAction;
import org.keycloak.authentication.FormActionFactory;
import org.keycloak.authentication.FormContext;
import org.keycloak.authentication.ValidationContext;
import org.keycloak.connections.httpclient.HttpClientProvider;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.login.LoginFormsProvider;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.FormMessage;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.validation.Validation;
import org.keycloak.util.JsonSerialization;
import javax.ws.rs.core.MultivaluedMap;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class RegistrationRecaptcha implements FormAction, FormActionFactory {
public static final String G_RECAPTCHA_RESPONSE = "g-recaptcha-response";
public static final String RECAPTCHA_REFERENCE_CATEGORY = "recaptcha";
protected static Logger logger = Logger.getLogger(RegistrationRecaptcha.class);
public static final String PROVIDER_ID = "registration-recaptcha-action";
@Override
public String getDisplayType() {
return "Recaptcha";
}
@Override
public String getReferenceCategory() {
return RECAPTCHA_REFERENCE_CATEGORY;
}
@Override
public boolean isConfigurable() {
return true;
}
@Override
public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
return new AuthenticationExecutionModel.Requirement[0];
}
@Override
public void buildPage(FormContext context, LoginFormsProvider form) {
form.setAttribute("recaptchaRequired", true);
form.setAttribute("recaptchaSiteKey", "6LcFEAkTAAAAAOaY-5RJk3zIYw4AalNtqfac27Bn");
List<String> scripts = new LinkedList<>();
scripts.add("https://www.google.com/recaptcha/api.js");
form.setAttribute("scripts", scripts);
}
@Override
public void validate(ValidationContext context) {
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
List<FormMessage> errors = new ArrayList<>();
boolean success = false;
context.getEvent().detail(Details.REGISTER_METHOD, "form");
String captcha = formData.getFirst(G_RECAPTCHA_RESPONSE);
if (Validation.isBlank(captcha)) {
HttpClient httpClient = context.getSession().getProvider(HttpClientProvider.class).getHttpClient();
HttpPost post = new HttpPost("https://www.google.com/recaptcha/api/siteverify");
List<NameValuePair> formparams = new ArrayList<NameValuePair>();
formparams.add(new BasicNameValuePair("secret", "6LcFEAkTAAAAAM0SErEs9NlfhYpOTRj_vOVJSAMI"));
formparams.add(new BasicNameValuePair("response", captcha));
formparams.add(new BasicNameValuePair("remoteip", context.getConnection().getRemoteAddr()));
try {
UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8");
post.setEntity(form);
HttpResponse response = httpClient.execute(post);
InputStream content = response.getEntity().getContent();
try {
Map json = JsonSerialization.readValue(content, Map.class);
Object val = json.get("success");
success = Boolean.TRUE.equals(val);
} finally {
content.close();
}
} catch (Exception e) {
logger.error("Recaptcha failed", e);
}
}
if (success) {
context.success();
} else {
String usernameField = RegistrationPage.FIELD_USERNAME;
if (context.getRealm().isRegistrationEmailAsUsername()) {
usernameField = RegistrationPage.FIELD_EMAIL;
}
errors.add(new FormMessage(usernameField, Messages.RECAPTCHA_FAILED));
formData.remove(G_RECAPTCHA_RESPONSE);
context.getEvent().error(Errors.INVALID_REGISTRATION);
context.validationError(formData, errors);
return;
}
}
@Override
public void success(FormContext context) {
}
@Override
public boolean requiresUser() {
return false;
}
@Override
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
return true;
}
@Override
public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
}
@Override
public void close() {
}
@Override
public FormAction create(KeycloakSession session) {
return this;
}
@Override
public void init(Config.Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public String getId() {
return PROVIDER_ID;
}
}

View file

@ -1,27 +1,28 @@
package org.keycloak.authentication.forms; package org.keycloak.authentication.forms;
import org.keycloak.Config; import org.keycloak.Config;
import org.keycloak.authentication.AuthenticatorContext;
import org.keycloak.authentication.FormAction; import org.keycloak.authentication.FormAction;
import org.keycloak.authentication.FormActionContext;
import org.keycloak.authentication.FormActionFactory; import org.keycloak.authentication.FormActionFactory;
import org.keycloak.authentication.FormAuthenticator; import org.keycloak.authentication.FormContext;
import org.keycloak.authentication.ValidationContext;
import org.keycloak.events.Details; import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventType; import org.keycloak.events.EventType;
import org.keycloak.login.LoginFormsProvider; import org.keycloak.login.LoginFormsProvider;
import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.ModelException;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.utils.FormMessage;
import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.services.messages.Messages;
import org.keycloak.services.resources.AttributeFormDataProcessor; import org.keycloak.services.resources.AttributeFormDataProcessor;
import org.keycloak.services.validation.Validation; import org.keycloak.services.validation.Validation;
import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.MultivaluedMap;
import java.util.ArrayList;
import java.util.List;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -32,7 +33,67 @@ public class RegistrationUserCreation implements FormAction, FormActionFactory {
public static final String PROVIDER_ID = "registration-user-creation"; public static final String PROVIDER_ID = "registration-user-creation";
@Override @Override
public void authenticate(FormActionContext context) { public void validate(ValidationContext context) {
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
List<FormMessage> errors = new ArrayList<>();
context.getEvent().detail(Details.REGISTER_METHOD, "form");
String email = formData.getFirst(Validation.FIELD_EMAIL);
String username = formData.getFirst(RegistrationPage.FIELD_USERNAME);
context.getEvent().detail(Details.USERNAME, username);
context.getEvent().detail(Details.EMAIL, email);
String usernameField = RegistrationPage.FIELD_USERNAME;
if (context.getRealm().isRegistrationEmailAsUsername()) {
username = email;
context.getEvent().detail(Details.USERNAME, username);
usernameField = RegistrationPage.FIELD_EMAIL;
if (Validation.isBlank(email)) {
errors.add(new FormMessage(RegistrationPage.FIELD_EMAIL, Messages.MISSING_EMAIL));
} else if (!Validation.isEmailValid(email)) {
errors.add(new FormMessage(RegistrationPage.FIELD_EMAIL, Messages.INVALID_EMAIL));
formData.remove(Validation.FIELD_EMAIL);
}
if (errors.size() > 0) {
context.getEvent().error(Errors.INVALID_REGISTRATION);
context.validationError(formData, errors);
return;
}
if (email != null && context.getSession().users().getUserByEmail(email, context.getRealm()) != null) {
context.getEvent().error(Errors.USERNAME_IN_USE);
formData.remove(Validation.FIELD_EMAIL);
errors.add(new FormMessage(RegistrationPage.FIELD_EMAIL, Messages.USERNAME_EXISTS));
context.validationError(formData, errors);
return;
}
} else {
if (Validation.isBlank(username)) {
context.getEvent().error(Errors.INVALID_REGISTRATION);
errors.add(new FormMessage(RegistrationPage.FIELD_USERNAME, Messages.MISSING_USERNAME));
context.validationError(formData, errors);
return;
}
}
if (context.getSession().users().getUserByUsername(username, context.getRealm()) != null) {
context.getEvent().error(Errors.USERNAME_IN_USE);
errors.add(new FormMessage(usernameField, Messages.USERNAME_EXISTS));
formData.remove(Validation.FIELD_USERNAME);
formData.remove(Validation.FIELD_EMAIL);
context.validationError(formData, errors);
return;
}
context.success();
}
@Override
public void buildPage(FormContext context, LoginFormsProvider form) {
}
@Override
public void success(FormContext context) {
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters(); MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
String email = formData.getFirst(Validation.FIELD_EMAIL); String email = formData.getFirst(Validation.FIELD_EMAIL);
String username = formData.getFirst(RegistrationPage.FIELD_USERNAME); String username = formData.getFirst(RegistrationPage.FIELD_USERNAME);
@ -45,29 +106,12 @@ public class RegistrationUserCreation implements FormAction, FormActionFactory {
; ;
UserModel user = context.getSession().users().addUser(context.getRealm(), username); UserModel user = context.getSession().users().addUser(context.getRealm(), username);
user.setEnabled(true); user.setEnabled(true);
user.setFirstName(formData.getFirst("firstName"));
user.setLastName(formData.getFirst("lastName"));
user.setEmail(email); user.setEmail(email);
context.getClientSession().setNote(OIDCLoginProtocol.LOGIN_HINT_PARAM, username); context.getClientSession().setNote(OIDCLoginProtocol.LOGIN_HINT_PARAM, username);
AttributeFormDataProcessor.process(formData, context.getRealm(), user); AttributeFormDataProcessor.process(formData, context.getRealm(), user);
context.setUser(user); context.setUser(user);
AuthenticationExecutionModel.Requirement categoryRequirement = context.getCategoryRequirementFromCurrentFlow(UserCredentialModel.PASSWORD);
boolean passwordRequired = categoryRequirement != null && categoryRequirement != AuthenticationExecutionModel.Requirement.DISABLED;
if (passwordRequired) {
String password = formData.getFirst(RegistrationPage.FIELD_PASSWORD);
UserCredentialModel credentials = new UserCredentialModel();
credentials.setType(CredentialRepresentation.PASSWORD);
credentials.setValue(password);
try {
context.getSession().users().updateCredential(context.getRealm(), user, UserCredentialModel.password(formData.getFirst("password")));
} catch (Exception me) {
user.addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD);
}
}
context.getEvent().user(user); context.getEvent().user(user);
context.success();
context.getEvent().success(); context.getEvent().success();
context.newEvent().event(EventType.LOGIN); context.newEvent().event(EventType.LOGIN);
context.getEvent().client(context.getClientSession().getClient().getClientId()) context.getEvent().client(context.getClientSession().getClient().getClientId())

View file

@ -1,152 +0,0 @@
package org.keycloak.authentication.forms;
import org.keycloak.Config;
import org.keycloak.authentication.AuthenticatorContext;
import org.keycloak.authentication.FormAction;
import org.keycloak.authentication.FormActionContext;
import org.keycloak.authentication.FormActionFactory;
import org.keycloak.authentication.FormAuthenticator;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.FormMessage;
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 java.util.ArrayList;
import java.util.List;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class RegistrationUsernameValidation implements FormAction, FormActionFactory {
public static final String PROVIDER_ID = "username-validation-action";
@Override
public void authenticate(FormActionContext context) {
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
List<FormMessage> errors = new ArrayList<>();
context.getEvent().detail(Details.REGISTER_METHOD, "form");
String email = formData.getFirst(Validation.FIELD_EMAIL);
String username = formData.getFirst(RegistrationPage.FIELD_USERNAME);
context.getEvent().detail(Details.USERNAME, username);
context.getEvent().detail(Details.EMAIL, email);
String usernameField = RegistrationPage.FIELD_USERNAME;
if (context.getRealm().isRegistrationEmailAsUsername()) {
username = email;
context.getEvent().detail(Details.USERNAME, username);
usernameField = RegistrationPage.FIELD_EMAIL;
if (Validation.isBlank(email)) {
errors.add(new FormMessage(RegistrationPage.FIELD_EMAIL, Messages.MISSING_EMAIL));
} else if (!Validation.isEmailValid(email)) {
errors.add(new FormMessage(RegistrationPage.FIELD_EMAIL, Messages.INVALID_EMAIL));
formData.remove(Validation.FIELD_EMAIL);
}
if (errors.size() > 0) {
context.getEvent().error(Errors.INVALID_REGISTRATION);
Response challenge = context.getFormAuthenticator().createChallenge(context, formData, errors);
context.challenge(challenge);
return;
}
if (email != null && context.getSession().users().getUserByEmail(email, context.getRealm()) != null) {
context.getEvent().error(Errors.USERNAME_IN_USE);
formData.remove(Validation.FIELD_EMAIL);
errors.add(new FormMessage(RegistrationPage.FIELD_EMAIL, Messages.USERNAME_EXISTS));
Response challenge = context.getFormAuthenticator().createChallenge(context, formData, errors);
context.challenge(challenge);
return;
}
} else {
if (Validation.isBlank(username)) {
context.getEvent().error(Errors.INVALID_REGISTRATION);
errors.add(new FormMessage(RegistrationPage.FIELD_USERNAME, Messages.MISSING_USERNAME));
Response challenge = context.getFormAuthenticator().createChallenge(context, formData, errors);
context.challenge(challenge);
return;
}
}
if (context.getSession().users().getUserByUsername(username, context.getRealm()) != null) {
context.getEvent().error(Errors.USERNAME_IN_USE);
errors.add(new FormMessage(usernameField, Messages.USERNAME_EXISTS));
formData.remove(Validation.FIELD_USERNAME);
formData.remove(Validation.FIELD_EMAIL);
Response challenge = context.getFormAuthenticator().createChallenge(context, formData, errors);
context.challenge(challenge);
return;
}
context.success();
}
@Override
public boolean requiresUser() {
return false;
}
@Override
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
return true;
}
@Override
public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
}
@Override
public void close() {
}
@Override
public String getDisplayType() {
return "Username Validation";
}
@Override
public String getReferenceCategory() {
return null;
}
@Override
public boolean isConfigurable() {
return false;
}
@Override
public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
return new AuthenticationExecutionModel.Requirement[0];
}
@Override
public FormAction create(KeycloakSession session) {
return this;
}
@Override
public void init(Config.Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public String getId() {
return PROVIDER_ID;
}
}

View file

@ -57,6 +57,7 @@ public class Messages {
public static final String INVALID_TOTP = "invalidTotpMessage"; public static final String INVALID_TOTP = "invalidTotpMessage";
public static final String USERNAME_EXISTS = "usernameExistsMessage"; public static final String USERNAME_EXISTS = "usernameExistsMessage";
public static final String RECAPTCHA_FAILED = "recaptchaFailed";
public static final String EMAIL_EXISTS = "emailExistsMessage"; public static final String EMAIL_EXISTS = "emailExistsMessage";

View file

@ -1,4 +1,4 @@
org.keycloak.authentication.forms.RegistrationPasswordValidation org.keycloak.authentication.forms.RegistrationPassword
org.keycloak.authentication.forms.RegistrationProfileValidation org.keycloak.authentication.forms.RegistrationProfile
org.keycloak.authentication.forms.RegistrationUserCreation org.keycloak.authentication.forms.RegistrationUserCreation
org.keycloak.authentication.forms.RegistrationUsernameValidation org.keycloak.authentication.forms.RegistrationRecaptcha