/* * Copyright 2016 Red Hat, Inc. and/or its affiliates * and other contributors as indicated by the @author tags. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.keycloak.authentication; import org.jboss.resteasy.spi.HttpRequest; import org.keycloak.OAuth2Constants; import org.keycloak.common.ClientConnection; import org.keycloak.events.EventBuilder; import org.keycloak.forms.login.LoginFormsProvider; import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.AuthenticatorConfigModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.utils.FormMessage; import org.keycloak.services.resources.LoginActionsService; import org.keycloak.sessions.AuthenticationSessionModel; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; import java.net.URI; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; /** * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @version $Revision: 1 $ */ public class FormAuthenticationFlow implements AuthenticationFlow { AuthenticationProcessor processor; AuthenticationExecutionModel formExecution; private final List<AuthenticationExecutionModel> formActionExecutions; private final FormAuthenticator formAuthenticator; public FormAuthenticationFlow(AuthenticationProcessor processor, AuthenticationExecutionModel execution) { this.processor = processor; this.formExecution = execution; formActionExecutions = processor.getRealm().getAuthenticationExecutions(execution.getFlowId()); formAuthenticator = processor.getSession().getProvider(FormAuthenticator.class, execution.getAuthenticator()); } private class FormContextImpl implements FormContext { AuthenticationExecutionModel executionModel; AuthenticatorConfigModel authenticatorConfig; private FormContextImpl(AuthenticationExecutionModel executionModel) { this.executionModel = executionModel; } @Override public EventBuilder newEvent() { return processor.newEvent(); } @Override public EventBuilder getEvent() { return processor.getEvent(); } @Override public AuthenticationExecutionModel getExecution() { return executionModel; } @Override public AuthenticatorConfigModel getAuthenticatorConfig() { if (executionModel.getAuthenticatorConfig() == null) return null; if (authenticatorConfig != null) return authenticatorConfig; authenticatorConfig = getRealm().getAuthenticatorConfigById(executionModel.getAuthenticatorConfig()); return authenticatorConfig; } @Override public UserModel getUser() { return getAuthenticationSession().getAuthenticatedUser(); } @Override public void setUser(UserModel user) { processor.setAutheticatedUser(user); } @Override public RealmModel getRealm() { return processor.getRealm(); } @Override public AuthenticationSessionModel getAuthenticationSession() { return processor.getAuthenticationSession(); } @Override public ClientConnection getConnection() { return processor.getConnection(); } @Override public UriInfo getUriInfo() { return processor.getUriInfo(); } @Override public KeycloakSession getSession() { return processor.getSession(); } @Override public HttpRequest getHttpRequest() { return processor.getRequest(); } } private class ValidationContextImpl extends FormContextImpl implements ValidationContext { FormAction action; String error; private ValidationContextImpl(AuthenticationExecutionModel executionModel, FormAction action) { super(executionModel); this.action = action; } boolean success; List<FormMessage> errors = null; MultivaluedMap<String, String> formData = null; @Override public void validationError(MultivaluedMap<String, String> formData, List<FormMessage> errors) { this.errors = errors; this.formData = formData; } public void error(String error) { this.error = error; } @Override public void success() { success = true; } } @Override public Response processAction(String actionExecution) { if (!actionExecution.equals(formExecution.getId())) { throw new AuthenticationFlowException("action is not current execution", AuthenticationFlowError.INTERNAL_ERROR); } Map<String, AuthenticationSessionModel.ExecutionStatus> executionStatus = new HashMap<>(); List<FormAction> requiredActions = new LinkedList<>(); List<ValidationContextImpl> successes = new LinkedList<>(); List<ValidationContextImpl> errors = new LinkedList<>(); for (AuthenticationExecutionModel formActionExecution : formActionExecutions) { if (!formActionExecution.isEnabled()) { executionStatus.put(formActionExecution.getId(), AuthenticationSessionModel.ExecutionStatus.SKIPPED); continue; } FormActionFactory factory = (FormActionFactory)processor.getSession().getKeycloakSessionFactory().getProviderFactory(FormAction.class, formActionExecution.getAuthenticator()); FormAction action = factory.create(processor.getSession()); UserModel authUser = processor.getAuthenticationSession().getAuthenticatedUser(); if (action.requiresUser() && authUser == null) { throw new AuthenticationFlowException("form action: " + formExecution.getAuthenticator() + " requires user", AuthenticationFlowError.UNKNOWN_USER); } boolean configuredFor = false; if (action.requiresUser() && authUser != null) { configuredFor = action.configuredFor(processor.getSession(), processor.getRealm(), authUser); if (!configuredFor) { if (formActionExecution.isRequired()) { if (factory.isUserSetupAllowed()) { AuthenticationProcessor.logger.debugv("authenticator SETUP_REQUIRED: {0}", formExecution.getAuthenticator()); executionStatus.put(formActionExecution.getId(), AuthenticationSessionModel.ExecutionStatus.SETUP_REQUIRED); requiredActions.add(action); continue; } else { throw new AuthenticationFlowException(AuthenticationFlowError.CREDENTIAL_SETUP_REQUIRED); } } else if (formActionExecution.isOptional()) { executionStatus.put(formActionExecution.getId(), AuthenticationSessionModel.ExecutionStatus.SKIPPED); continue; } } } ValidationContextImpl result = new ValidationContextImpl(formActionExecution, action); action.validate(result); if (result.success) { executionStatus.put(formActionExecution.getId(), AuthenticationSessionModel.ExecutionStatus.SUCCESS); successes.add(result); } else { executionStatus.put(formActionExecution.getId(), AuthenticationSessionModel.ExecutionStatus.CHALLENGED); errors.add(result); } } if (!errors.isEmpty()) { processor.logFailure(); List<FormMessage> messages = new LinkedList<>(); Set<String> fields = new HashSet<>(); for (ValidationContextImpl v : errors) { for (FormMessage m : v.errors) { if (!fields.contains(m.getField())) { fields.add(m.getField()); messages.add(m); } } } ValidationContextImpl first = errors.get(0); first.getEvent().error(first.error); return renderForm(first.formData, messages); } for (ValidationContextImpl context : successes) { context.action.success(context); } // set status and required actions only if form is fully successful for (Map.Entry<String, AuthenticationSessionModel.ExecutionStatus> entry : executionStatus.entrySet()) { processor.getAuthenticationSession().setExecutionStatus(entry.getKey(), entry.getValue()); } for (FormAction action : requiredActions) { action.setRequiredActions(processor.getSession(), processor.getRealm(), processor.getAuthenticationSession().getAuthenticatedUser()); } processor.getAuthenticationSession().setExecutionStatus(actionExecution, AuthenticationSessionModel.ExecutionStatus.SUCCESS); processor.getAuthenticationSession().removeAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION); return null; } public URI getActionUrl(String executionId, String code) { return LoginActionsService.registrationFormProcessor(processor.getUriInfo()) .queryParam(OAuth2Constants.CODE, code) .queryParam("execution", executionId) .build(processor.getRealm().getName()); } @Override public Response processFlow() { return renderForm(null, null); } public Response renderForm(MultivaluedMap<String, String> formData, List<FormMessage> errors) { String executionId = formExecution.getId(); processor.getAuthenticationSession().setAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION, executionId); String code = processor.generateCode(); URI actionUrl = getActionUrl(executionId, code); LoginFormsProvider form = processor.getSession().getProvider(LoginFormsProvider.class) .setActionUri(actionUrl) .setClientSessionCode(code) .setFormData(formData) .setErrors(errors); for (AuthenticationExecutionModel formActionExecution : formActionExecutions) { if (!formActionExecution.isEnabled()) continue; FormAction action = processor.getSession().getProvider(FormAction.class, formActionExecution.getAuthenticator()); FormContext result = new FormContextImpl(formActionExecution); action.buildPage(result, form); } FormContext context = new FormContextImpl(formExecution); return formAuthenticator.render(context, form); } }