<div dir="ltr"><div>Hello Group,</div><div><br></div><div>I just found out that as long as a user has a required_action set up one cannot authenticate via grant_type password throught the TokenEndpoint </div><div>because of this line in TokenEndpoint.buildResourceOwnerPasswordCredentialsGrant</div><div><br></div><div>if (user.getRequiredActions() != null &amp;&amp; user.getRequiredActions().size() &gt; 0) {</div><div>   event.error(Errors.RESOLVE_REQUIRED_ACTIONS);</div><div>   throw new ErrorResponseException(&quot;invalid_grant&quot;, &quot;Account is not fully set up&quot;, Response.Status.BAD_REQUEST);</div><div>}</div><div><br></div><div>current master:</div><div><a href="https://github.com/keycloak/keycloak/blob/bd2887aa77184d82e795e4200eb55a3d9b11e4d4/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java#L388">https://github.com/keycloak/keycloak/blob/bd2887aa77184d82e795e4200eb55a3d9b11e4d4/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java#L388</a></div><div><br></div><div>1.9.x</div><div><a href="https://github.com/keycloak/keycloak/blob/e7822431fded5948a5e248766e6ffbf86d476cf8/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java#L388">https://github.com/keycloak/keycloak/blob/e7822431fded5948a5e248766e6ffbf86d476cf8/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java#L388</a></div><div><br></div><div>I think that this is too restrictive. Imagine a default required action that has no user interaction but records some user login statistics like </div><div>last login date, count logins, login failures etc.</div><div>In this case it would be better to check if the configured required_action requires user-interaction (e.g. via a new method boolean isInteractive())</div><div>or communicate that required actions should not be used for actions that don&#39;t require user interactions - an authenticator is then probably the better choice.</div><div><br></div><div>Cheers,</div><div>Thomas</div><div><br></div><div>I tested this with a required action like the LoginStatsRecordingRequiredActionProvider listed below.</div><div>Now setup a client with the name test-client and a user with the name tester.</div><div><br></div><div>With that deployed to keycloak as a default action one can use the following curl command</div><div>to see the problem.</div><div><br></div><div>KC_REALM=test-realm</div><div>KC_USERNAME=tester</div><div>KC_PASSWORD=test</div><div>KC_CLIENT=test-client</div><div>KC_CLIENT_SECRET=e9ca8fad-d399-4455-5290-817102e7f9ff</div><div>KC_SERVER=<a href="http://192.168.99.1:8080">192.168.99.1:8080</a></div><div>KC_CONTEXT=auth</div><div><br></div><div>curl -k -v --noproxy 192.168.99.1 \</div><div>        -H &quot;Content-Type: application/x-www-form-urlencoded&quot; \</div><div>        -d &quot;grant_type=password&quot; \</div><div>        -d &quot;username=$KC_USERNAME&quot; \</div><div>        -d &quot;password=$KC_PASSWORD&quot; \</div><div>        -d &quot;client_id=$KC_CLIENT&quot; \</div><div>        -d &quot;client_secret=$KC_CLIENT_SECRET&quot; \</div><div>        &quot;http://$KC_SERVER/$KC_CONTEXT/realms/$KC_REALM/protocol/openid-connect/token&quot;</div><div><br></div><div>Gives:</div><div>&lt; HTTP/1.1 400 Bad Request</div><div>&lt; Connection: keep-alive</div><div>&lt; X-Powered-By: Undertow/1</div><div>&lt; Server: WildFly/10</div><div>&lt; Content-Type: application/json</div><div>&lt; Content-Length: 75</div><div>&lt; Date: Fri, 08 Jul 2016 08:24:59 GMT</div><div>{&quot;error_description&quot;:&quot;Account is not fully set up&quot;,&quot;error&quot;:&quot;invalid_grant&quot;}</div><div><br></div><div><br></div><div>----</div><div><br></div><div><br></div><div>package com.acme.keycloak.ext.authentication;</div><div><br></div><div>import static java.util.Arrays.asList;</div><div><br></div><div>import java.time.Instant;</div><div>import java.time.LocalDateTime;</div><div>import java.util.Arrays;</div><div>import java.util.List;</div><div>import java.util.TimeZone;</div><div><br></div><div>import org.jboss.logging.Logger;</div><div>import org.keycloak.Config.Scope;</div><div>import org.keycloak.authentication.RequiredActionContext;</div><div>import org.keycloak.authentication.RequiredActionFactory;</div><div>import org.keycloak.authentication.RequiredActionProvider;</div><div>import org.keycloak.models.KeycloakSession;</div><div>import org.keycloak.models.KeycloakSessionFactory;</div><div>import org.keycloak.models.UserLoginFailureModel;</div><div>import org.keycloak.models.UserModel;</div><div><br></div><div>/**</div><div> * Collects information about User Login behaviour.</div><div> * &lt;p&gt;</div><div> * Collects the following:</div><div> * &lt;ul&gt;</div><div> * &lt;li&gt;Date / Time of first login&lt;/li&gt;</div><div> * &lt;li&gt;Date / Time of recent login&lt;/li&gt;</div><div> * &lt;li&gt;Count of logins since first login&lt;/li&gt;</div><div> * &lt;li&gt;Count of failed logins since the configured &lt;code&gt;Failure Reset Time&lt;/code&gt; unter &lt;code&gt;Security Defenses&lt;/code&gt;&lt;/li&gt;</div><div> * &lt;/ul&gt;</div><div> * &lt;p&gt;</div><div> * This {@link RequiredActionProvider} is Stateless and can be reused.</div><div> * </div><div> * @author tdarimont</div><div> */</div><div>public class LoginStatsRecordingRequiredActionProvider implements RequiredActionProvider, RequiredActionFactory {</div><div><br></div><div><span class="" style="white-space:pre">        </span>private static final Logger LOG = Logger.getLogger(LoginStatsRecordingRequiredActionProvider.class);</div><div><br></div><div><span class="" style="white-space:pre">        </span>private static final String PROVIDER_ID = &quot;login_stats_action&quot;;</div><div><span class="" style="white-space:pre">        </span>private static final String RECORD_LOGIN_STATISTICS_ACTION = &quot;Record Login Statistics Action&quot;;</div><div><br></div><div><span class="" style="white-space:pre">        </span>private static final String LOGIN_LOGIN_COUNT = &quot;login.login-count&quot;;</div><div><span class="" style="white-space:pre">        </span>private static final String LOGIN_FAILED_LOGIN_COUNT = &quot;login.failed-login-count&quot;;</div><div><span class="" style="white-space:pre">        </span>private static final String LOGIN_FAILED_LOGIN_DATE = &quot;login.failed-login-date&quot;;</div><div><br></div><div><span class="" style="white-space:pre">        </span>private static final String LOGIN_FIRST_LOGIN_DATE = &quot;login.first-login-date&quot;;</div><div><span class="" style="white-space:pre">        </span>private static final String LOGIN_RECENT_LOGIN_DATE = &quot;login.recent-login-date&quot;;</div><div><br></div><div><span class="" style="white-space:pre">        </span>private static final String ONE = &quot;1&quot;;</div><div><span class="" style="white-space:pre">        </span>private static final String ZERO = &quot;0&quot;;</div><div><br></div><div><span class="" style="white-space:pre">        </span>private static final LoginStatsRecordingRequiredActionProvider INSTANCE = new LoginStatsRecordingRequiredActionProvider();</div><div><br></div><div><span class="" style="white-space:pre">        </span>@Override</div><div><span class="" style="white-space:pre">        </span>public void close() {</div><div><span class="" style="white-space:pre">                </span>// NOOP</div><div><span class="" style="white-space:pre">        </span>}</div><div><br></div><div><span class="" style="white-space:pre">        </span>@Override</div><div><span class="" style="white-space:pre">        </span>public void evaluateTriggers(RequiredActionContext context) {</div><div><br></div><div><span class="" style="white-space:pre">                </span>UserModel user = context.getUser();</div><div><br></div><div><span class="" style="white-space:pre">                </span>try {</div><div><span class="" style="white-space:pre">                        </span>recordFirstLogin(user);</div><div><span class="" style="white-space:pre">                </span>} catch (Exception ex) {</div><div><span class="" style="white-space:pre">                        </span>LOG.warnv(ex, &quot;Couldn&#39;t record first login &lt;{0}&gt;&quot;, this);</div><div><span class="" style="white-space:pre">                </span>}</div><div><br></div><div><span class="" style="white-space:pre">                </span>try {</div><div><span class="" style="white-space:pre">                        </span>recordRecentLogin(user);</div><div><span class="" style="white-space:pre">                </span>} catch (Exception ex) {</div><div><span class="" style="white-space:pre">                        </span>LOG.warnv(ex, &quot;Couldn&#39;t record recent login &lt;{0}&gt;&quot;, this);</div><div><span class="" style="white-space:pre">                </span>}</div><div><br></div><div><span class="" style="white-space:pre">                </span>try {</div><div><span class="" style="white-space:pre">                        </span>recordLoginCount(user);</div><div><span class="" style="white-space:pre">                </span>} catch (Exception ex) {</div><div><span class="" style="white-space:pre">                        </span>LOG.warnv(ex, &quot;Couldn&#39;t record login count &lt;{0}&gt;&quot;, this);</div><div><span class="" style="white-space:pre">                </span>}</div><div><br></div><div><span class="" style="white-space:pre">                </span>try {</div><div><span class="" style="white-space:pre">                        </span>recordFailedLogin(user, context);</div><div><span class="" style="white-space:pre">                </span>} catch (Exception ex) {</div><div><span class="" style="white-space:pre">                        </span>LOG.warnv(ex, &quot;Couldn&#39;t record failed login &lt;{0}&gt;&quot;, this);</div><div><span class="" style="white-space:pre">                </span>}</div><div><span class="" style="white-space:pre">        </span>}</div><div><br></div><div><span class="" style="white-space:pre">        </span>private void recordFailedLogin(UserModel user, RequiredActionContext context) {</div><div><br></div><div><span class="" style="white-space:pre">                </span>UserLoginFailureModel loginFailures = context.getSession().sessions()</div><div><span class="" style="white-space:pre">                                </span>.getUserLoginFailure(context.getRealm(), user.getUsername());</div><div><br></div><div><span class="" style="white-space:pre">                </span>if (loginFailures != null) {</div><div><span class="" style="white-space:pre">                        </span>user.setAttribute(LOGIN_FAILED_LOGIN_COUNT, Arrays.asList(String.valueOf(loginFailures.getNumFailures())));</div><div><span class="" style="white-space:pre">                        </span>user.setAttribute(LOGIN_FAILED_LOGIN_DATE,</div><div><span class="" style="white-space:pre">                                        </span>Arrays.asList(getLocalDateTimeFromTimestamp(loginFailures.getLastFailure()).toString()));</div><div><span class="" style="white-space:pre">                </span>} else {</div><div><span class="" style="white-space:pre">                        </span>user.setAttribute(LOGIN_FAILED_LOGIN_COUNT, Arrays.asList(ZERO));</div><div><span class="" style="white-space:pre">                </span>}</div><div><span class="" style="white-space:pre">        </span>}</div><div><br></div><div><span class="" style="white-space:pre">        </span>private void recordLoginCount(UserModel user) {</div><div><br></div><div><span class="" style="white-space:pre">                </span>List&lt;String&gt; list = user.getAttribute(LOGIN_LOGIN_COUNT);</div><div><br></div><div><span class="" style="white-space:pre">                </span>if (list == null || list.isEmpty()) {</div><div><span class="" style="white-space:pre">                        </span>list = asList(ONE);</div><div><span class="" style="white-space:pre">                </span>} else {</div><div><span class="" style="white-space:pre">                        </span>list = asList(String.valueOf(Long.parseLong(list.get(0)) + 1));</div><div><span class="" style="white-space:pre">                </span>}</div><div><br></div><div><span class="" style="white-space:pre">                </span>user.setAttribute(LOGIN_LOGIN_COUNT, list);</div><div><span class="" style="white-space:pre">        </span>}</div><div><br></div><div><span class="" style="white-space:pre">        </span>private void recordRecentLogin(UserModel user) {</div><div><span class="" style="white-space:pre">                </span>user.setAttribute(LOGIN_RECENT_LOGIN_DATE,</div><div><span class="" style="white-space:pre">                                </span>asList(getLocalDateTimeFromTimestamp(System.currentTimeMillis()).toString()));</div><div><span class="" style="white-space:pre">        </span>}</div><div><br></div><div><span class="" style="white-space:pre">        </span>private void recordFirstLogin(UserModel user) {</div><div><br></div><div><span class="" style="white-space:pre">                </span>List&lt;String&gt; list = user.getAttribute(LOGIN_FIRST_LOGIN_DATE);</div><div><br></div><div><span class="" style="white-space:pre">                </span>if (list == null || list.isEmpty()) {</div><div><span class="" style="white-space:pre">                        </span>user.setAttribute(LOGIN_FIRST_LOGIN_DATE,</div><div><span class="" style="white-space:pre">                                        </span>asList(getLocalDateTimeFromTimestamp(System.currentTimeMillis()).toString()));</div><div><span class="" style="white-space:pre">                </span>}</div><div><span class="" style="white-space:pre">        </span>}</div><div><br></div><div><span class="" style="white-space:pre">        </span>@Override</div><div><span class="" style="white-space:pre">        </span>public void requiredActionChallenge(RequiredActionContext context) {</div><div><span class="" style="white-space:pre">                </span>// NOOP</div><div><span class="" style="white-space:pre">        </span>}</div><div><br></div><div><span class="" style="white-space:pre">        </span>@Override</div><div><span class="" style="white-space:pre">        </span>public void processAction(RequiredActionContext context) {</div><div><span class="" style="white-space:pre">                </span>context.success();</div><div><span class="" style="white-space:pre">        </span>}</div><div><br></div><div><span class="" style="white-space:pre">        </span>@Override</div><div><span class="" style="white-space:pre">        </span>public RequiredActionProvider create(KeycloakSession session) {</div><div><span class="" style="white-space:pre">                </span>return INSTANCE;</div><div><span class="" style="white-space:pre">        </span>}</div><div><br></div><div><span class="" style="white-space:pre">        </span>@Override</div><div><span class="" style="white-space:pre">        </span>public void init(Scope config) {</div><div><span class="" style="white-space:pre">                </span>LOG.infov(&quot;Creating IdM Keycloak extension &lt;{0}&gt;&quot;, this);</div><div><span class="" style="white-space:pre">                </span>// NOOP</div><div><span class="" style="white-space:pre">        </span>}</div><div><br></div><div><span class="" style="white-space:pre">        </span>@Override</div><div><span class="" style="white-space:pre">        </span>public void postInit(KeycloakSessionFactory factory) {</div><div><span class="" style="white-space:pre">                </span>// NOOP</div><div><span class="" style="white-space:pre">        </span>}</div><div><br></div><div><span class="" style="white-space:pre">        </span>@Override</div><div><span class="" style="white-space:pre">        </span>public String getId() {</div><div><span class="" style="white-space:pre">                </span>return PROVIDER_ID;</div><div><span class="" style="white-space:pre">        </span>}</div><div><br></div><div><span class="" style="white-space:pre">        </span>@Override</div><div><span class="" style="white-space:pre">        </span>public String getDisplayText() {</div><div><span class="" style="white-space:pre">                </span>return RECORD_LOGIN_STATISTICS_ACTION;</div><div><span class="" style="white-space:pre">        </span>}</div><div><br></div><div><span class="" style="white-space:pre">        </span>private LocalDateTime getLocalDateTimeFromTimestamp(long timestampMillis) {</div><div><span class="" style="white-space:pre">                </span>return LocalDateTime.ofInstant(Instant.ofEpochSecond(timestampMillis / 1000), TimeZone.getDefault().toZoneId());</div><div><span class="" style="white-space:pre">        </span>}</div><div>}</div><div><br></div></div>