<div dir="ltr">A required action is aimed at an action a user has to take, not as a way to listen for events. So you&#39;d end up with a null-op action just so that you can log an event.</div><div class="gmail_extra"><br><div class="gmail_quote">On 11 July 2016 at 10:28, Thomas Darimont <span dir="ltr">&lt;<a href="mailto:thomas.darimont@googlemail.com" target="_blank">thomas.darimont@googlemail.com</a>&gt;</span> wrote:<br><blockquote class="gmail_quote" style="margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex"><div dir="ltr">Hi,<div><br></div><div>an EventListener would work as well - but in this case RequiredAction was IMHO simpler than a custom EventListener (less ceremony).</div><div>Besides the configuration gotcha discussed above, do you see any advantage of using an EventListener here instead of an RequiredAction?<br></div><div><br></div><div>Cheers,</div><div>Thomas</div></div><div class="HOEnZb"><div class="h5"><div class="gmail_extra"><br><div class="gmail_quote">2016-07-11 8:08 GMT+02:00 Stian Thorgersen <span dir="ltr">&lt;<a href="mailto:sthorger@redhat.com" target="_blank">sthorger@redhat.com</a>&gt;</span>:<br><blockquote class="gmail_quote" style="margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex"><div dir="ltr">Would it not be better to use an event listener for this?</div><div><div><div class="gmail_extra"><br><div class="gmail_quote">On 8 July 2016 at 14:13, Thomas Darimont <span dir="ltr">&lt;<a href="mailto:thomas.darimont@googlemail.com" target="_blank">thomas.darimont@googlemail.com</a>&gt;</span> wrote:<br><blockquote class="gmail_quote" style="margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex"><div dir="ltr">Thanks Marek, that makes sense now. <div><br><div>Removing the &quot;default&quot; checkbox on my provider did the trick - yes I want to execute this action for every user regardless on login in the sense of an filter / interceptor.</div><div><br></div><div>Btw. this use case I implemented is IMHO quite common - would be cool if keycloak would ship with something like that out-of-the-box ;-)</div><div><div><br></div><div>Cheers,</div><div>Thomas</div></div></div></div><div><div><div class="gmail_extra"><br><div class="gmail_quote">2016-07-08 13:39 GMT+02:00 Marek Posolda <span dir="ltr">&lt;<a href="mailto:mposolda@redhat.com" target="_blank">mposolda@redhat.com</a>&gt;</span>:<br><blockquote class="gmail_quote" style="margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex">
  
    
  
  <div bgcolor="#FFFFFF" text="#000000">
    <div>There was already some quite a long
      discussion about similar stuff. It was about consents though, not
      about required actions. However IMO both consents and
      requiredActions are quite similar thing and it&#39;s by design that
      user is not able to login with directGrant if he has either
      consent or requiredAction on himself.<br>
      <br>
      However when I look at your particular use-case, it seems that you
      are using LoginStatsRecordingRequiredActionProvider as an
      interceptor, which doesn&#39;t need any real requiredAction on user,
      but you just want to ensure that &quot;evaluateTriggers&quot; is called
      after each user login. Is it correct? <br>
      <br>
      Then you can just check your requiredAction provider as &quot;enabled&quot;,
      but NOT as &quot;default&quot; . Note that &quot;evaluateTriggers&quot; will be always
      invoked for every user after his login even if user doesn&#39;t have
      the particular action on him. The purpose of &quot;evaluateTriggers&quot; is
      actually to check, if requiredAction should be added to the user
      if some specific conditions occurs. For example see <span style="background-color:#e4e4ff">VerifyEmail.</span><span style="background-color:#e4e4ff">evaluateTriggers</span><span style="background-color:#e4e4ff"></span> , which adds the
      VERIFY_EMAIL requiredAction to user if he doesn&#39;t yet have
      verified email.<br>
      <br>
      Marek<div><div><br>
      
      
      <br>
      On 08/07/16 10:35, Thomas Darimont wrote:<br>
    </div></div></div>
    <blockquote type="cite"><div><div>
      <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" target="_blank">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" target="_blank">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" target="_blank">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>       
<a href="http://$KC_SERVER/$KC_CONTEXT/realms/$KC_REALM/protocol/openid-connect/token" target="_blank">&quot;http://$KC_SERVER/$KC_CONTEXT/realms/$KC_REALM/protocol/openid-connect/token&quot;</a></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 style="white-space:pre-wrap">        </span>private
          static final Logger LOG =
          Logger.getLogger(LoginStatsRecordingRequiredActionProvider.class);</div>
        <div><br>
        </div>
        <div><span style="white-space:pre-wrap">        </span>private
          static final String PROVIDER_ID = &quot;login_stats_action&quot;;</div>
        <div><span style="white-space:pre-wrap">        </span>private
          static final String RECORD_LOGIN_STATISTICS_ACTION = &quot;Record
          Login Statistics Action&quot;;</div>
        <div><br>
        </div>
        <div><span style="white-space:pre-wrap">        </span>private
          static final String LOGIN_LOGIN_COUNT = &quot;login.login-count&quot;;</div>
        <div><span style="white-space:pre-wrap">        </span>private
          static final String LOGIN_FAILED_LOGIN_COUNT =
          &quot;login.failed-login-count&quot;;</div>
        <div><span style="white-space:pre-wrap">        </span>private
          static final String LOGIN_FAILED_LOGIN_DATE =
          &quot;login.failed-login-date&quot;;</div>
        <div><br>
        </div>
        <div><span style="white-space:pre-wrap">        </span>private
          static final String LOGIN_FIRST_LOGIN_DATE =
          &quot;login.first-login-date&quot;;</div>
        <div><span style="white-space:pre-wrap">        </span>private
          static final String LOGIN_RECENT_LOGIN_DATE =
          &quot;login.recent-login-date&quot;;</div>
        <div><br>
        </div>
        <div><span style="white-space:pre-wrap">        </span>private
          static final String ONE = &quot;1&quot;;</div>
        <div><span style="white-space:pre-wrap">        </span>private
          static final String ZERO = &quot;0&quot;;</div>
        <div><br>
        </div>
        <div><span style="white-space:pre-wrap">        </span>private
          static final LoginStatsRecordingRequiredActionProvider
          INSTANCE = new LoginStatsRecordingRequiredActionProvider();</div>
        <div><br>
        </div>
        <div><span style="white-space:pre-wrap">        </span>@Override</div>
        <div><span style="white-space:pre-wrap">        </span>public void
          close() {</div>
        <div><span style="white-space:pre-wrap">                </span>// NOOP</div>
        <div><span style="white-space:pre-wrap">        </span>}</div>
        <div><br>
        </div>
        <div><span style="white-space:pre-wrap">        </span>@Override</div>
        <div><span style="white-space:pre-wrap">        </span>public void
          evaluateTriggers(RequiredActionContext context) {</div>
        <div><br>
        </div>
        <div><span style="white-space:pre-wrap">                </span>UserModel
          user = context.getUser();</div>
        <div><br>
        </div>
        <div><span style="white-space:pre-wrap">                </span>try {</div>
        <div><span style="white-space:pre-wrap">                        </span>recordFirstLogin(user);</div>
        <div><span style="white-space:pre-wrap">                </span>} catch
          (Exception ex) {</div>
        <div><span style="white-space:pre-wrap">                        </span>LOG.warnv(ex,
          &quot;Couldn&#39;t record first login &lt;{0}&gt;&quot;, this);</div>
        <div><span style="white-space:pre-wrap">                </span>}</div>
        <div><br>
        </div>
        <div><span style="white-space:pre-wrap">                </span>try {</div>
        <div><span style="white-space:pre-wrap">                        </span>recordRecentLogin(user);</div>
        <div><span style="white-space:pre-wrap">                </span>} catch
          (Exception ex) {</div>
        <div><span style="white-space:pre-wrap">                        </span>LOG.warnv(ex,
          &quot;Couldn&#39;t record recent login &lt;{0}&gt;&quot;, this);</div>
        <div><span style="white-space:pre-wrap">                </span>}</div>
        <div><br>
        </div>
        <div><span style="white-space:pre-wrap">                </span>try {</div>
        <div><span style="white-space:pre-wrap">                        </span>recordLoginCount(user);</div>
        <div><span style="white-space:pre-wrap">                </span>} catch
          (Exception ex) {</div>
        <div><span style="white-space:pre-wrap">                        </span>LOG.warnv(ex,
          &quot;Couldn&#39;t record login count &lt;{0}&gt;&quot;, this);</div>
        <div><span style="white-space:pre-wrap">                </span>}</div>
        <div><br>
        </div>
        <div><span style="white-space:pre-wrap">                </span>try {</div>
        <div><span style="white-space:pre-wrap">                        </span>recordFailedLogin(user,
          context);</div>
        <div><span style="white-space:pre-wrap">                </span>} catch
          (Exception ex) {</div>
        <div><span style="white-space:pre-wrap">                        </span>LOG.warnv(ex,
          &quot;Couldn&#39;t record failed login &lt;{0}&gt;&quot;, this);</div>
        <div><span style="white-space:pre-wrap">                </span>}</div>
        <div><span style="white-space:pre-wrap">        </span>}</div>
        <div><br>
        </div>
        <div><span style="white-space:pre-wrap">        </span>private void
          recordFailedLogin(UserModel user, RequiredActionContext
          context) {</div>
        <div><br>
        </div>
        <div><span style="white-space:pre-wrap">                </span>UserLoginFailureModel
          loginFailures = context.getSession().sessions()</div>
        <div><span style="white-space:pre-wrap">                                </span>.getUserLoginFailure(context.getRealm(),
          user.getUsername());</div>
        <div><br>
        </div>
        <div><span style="white-space:pre-wrap">                </span>if
          (loginFailures != null) {</div>
        <div><span style="white-space:pre-wrap">                        </span>user.setAttribute(LOGIN_FAILED_LOGIN_COUNT,
Arrays.asList(String.valueOf(loginFailures.getNumFailures())));</div>
        <div><span style="white-space:pre-wrap">                        </span>user.setAttribute(LOGIN_FAILED_LOGIN_DATE,</div>
        <div><span style="white-space:pre-wrap">                                        </span>Arrays.asList(getLocalDateTimeFromTimestamp(loginFailures.getLastFailure()).toString()));</div>
        <div><span style="white-space:pre-wrap">                </span>} else {</div>
        <div><span style="white-space:pre-wrap">                        </span>user.setAttribute(LOGIN_FAILED_LOGIN_COUNT,
          Arrays.asList(ZERO));</div>
        <div><span style="white-space:pre-wrap">                </span>}</div>
        <div><span style="white-space:pre-wrap">        </span>}</div>
        <div><br>
        </div>
        <div><span style="white-space:pre-wrap">        </span>private void
          recordLoginCount(UserModel user) {</div>
        <div><br>
        </div>
        <div><span style="white-space:pre-wrap">                </span>List&lt;String&gt;
          list = user.getAttribute(LOGIN_LOGIN_COUNT);</div>
        <div><br>
        </div>
        <div><span style="white-space:pre-wrap">                </span>if (list ==
          null || list.isEmpty()) {</div>
        <div><span style="white-space:pre-wrap">                        </span>list =
          asList(ONE);</div>
        <div><span style="white-space:pre-wrap">                </span>} else {</div>
        <div><span style="white-space:pre-wrap">                        </span>list =
          asList(String.valueOf(Long.parseLong(list.get(0)) + 1));</div>
        <div><span style="white-space:pre-wrap">                </span>}</div>
        <div><br>
        </div>
        <div><span style="white-space:pre-wrap">                </span>user.setAttribute(LOGIN_LOGIN_COUNT,
          list);</div>
        <div><span style="white-space:pre-wrap">        </span>}</div>
        <div><br>
        </div>
        <div><span style="white-space:pre-wrap">        </span>private void
          recordRecentLogin(UserModel user) {</div>
        <div><span style="white-space:pre-wrap">                </span>user.setAttribute(LOGIN_RECENT_LOGIN_DATE,</div>
        <div><span style="white-space:pre-wrap">                                </span>asList(getLocalDateTimeFromTimestamp(System.currentTimeMillis()).toString()));</div>
        <div><span style="white-space:pre-wrap">        </span>}</div>
        <div><br>
        </div>
        <div><span style="white-space:pre-wrap">        </span>private void
          recordFirstLogin(UserModel user) {</div>
        <div><br>
        </div>
        <div><span style="white-space:pre-wrap">                </span>List&lt;String&gt;
          list = user.getAttribute(LOGIN_FIRST_LOGIN_DATE);</div>
        <div><br>
        </div>
        <div><span style="white-space:pre-wrap">                </span>if (list ==
          null || list.isEmpty()) {</div>
        <div><span style="white-space:pre-wrap">                        </span>user.setAttribute(LOGIN_FIRST_LOGIN_DATE,</div>
        <div><span style="white-space:pre-wrap">                                        </span>asList(getLocalDateTimeFromTimestamp(System.currentTimeMillis()).toString()));</div>
        <div><span style="white-space:pre-wrap">                </span>}</div>
        <div><span style="white-space:pre-wrap">        </span>}</div>
        <div><br>
        </div>
        <div><span style="white-space:pre-wrap">        </span>@Override</div>
        <div><span style="white-space:pre-wrap">        </span>public void
          requiredActionChallenge(RequiredActionContext context) {</div>
        <div><span style="white-space:pre-wrap">                </span>// NOOP</div>
        <div><span style="white-space:pre-wrap">        </span>}</div>
        <div><br>
        </div>
        <div><span style="white-space:pre-wrap">        </span>@Override</div>
        <div><span style="white-space:pre-wrap">        </span>public void
          processAction(RequiredActionContext context) {</div>
        <div><span style="white-space:pre-wrap">                </span>context.success();</div>
        <div><span style="white-space:pre-wrap">        </span>}</div>
        <div><br>
        </div>
        <div><span style="white-space:pre-wrap">        </span>@Override</div>
        <div><span style="white-space:pre-wrap">        </span>public
          RequiredActionProvider create(KeycloakSession session) {</div>
        <div><span style="white-space:pre-wrap">                </span>return
          INSTANCE;</div>
        <div><span style="white-space:pre-wrap">        </span>}</div>
        <div><br>
        </div>
        <div><span style="white-space:pre-wrap">        </span>@Override</div>
        <div><span style="white-space:pre-wrap">        </span>public void
          init(Scope config) {</div>
        <div><span style="white-space:pre-wrap">                </span>LOG.infov(&quot;Creating
          IdM Keycloak extension &lt;{0}&gt;&quot;, this);</div>
        <div><span style="white-space:pre-wrap">                </span>// NOOP</div>
        <div><span style="white-space:pre-wrap">        </span>}</div>
        <div><br>
        </div>
        <div><span style="white-space:pre-wrap">        </span>@Override</div>
        <div><span style="white-space:pre-wrap">        </span>public void
          postInit(KeycloakSessionFactory factory) {</div>
        <div><span style="white-space:pre-wrap">                </span>// NOOP</div>
        <div><span style="white-space:pre-wrap">        </span>}</div>
        <div><br>
        </div>
        <div><span style="white-space:pre-wrap">        </span>@Override</div>
        <div><span style="white-space:pre-wrap">        </span>public
          String getId() {</div>
        <div><span style="white-space:pre-wrap">                </span>return
          PROVIDER_ID;</div>
        <div><span style="white-space:pre-wrap">        </span>}</div>
        <div><br>
        </div>
        <div><span style="white-space:pre-wrap">        </span>@Override</div>
        <div><span style="white-space:pre-wrap">        </span>public
          String getDisplayText() {</div>
        <div><span style="white-space:pre-wrap">                </span>return
          RECORD_LOGIN_STATISTICS_ACTION;</div>
        <div><span style="white-space:pre-wrap">        </span>}</div>
        <div><br>
        </div>
        <div><span style="white-space:pre-wrap">        </span>private
          LocalDateTime getLocalDateTimeFromTimestamp(long
          timestampMillis) {</div>
        <div><span style="white-space:pre-wrap">                </span>return
          LocalDateTime.ofInstant(Instant.ofEpochSecond(timestampMillis
          / 1000), TimeZone.getDefault().toZoneId());</div>
        <div><span style="white-space:pre-wrap">        </span>}</div>
        <div>}</div>
        <div><br>
        </div>
      </div>
      <br>
      <fieldset></fieldset>
      <br>
      </div></div><pre>_______________________________________________
keycloak-dev mailing list
<a href="mailto:keycloak-dev@lists.jboss.org" target="_blank">keycloak-dev@lists.jboss.org</a>
<a href="https://lists.jboss.org/mailman/listinfo/keycloak-dev" target="_blank">https://lists.jboss.org/mailman/listinfo/keycloak-dev</a></pre>
    </blockquote>
    <br>
  </div>

</blockquote></div><br></div>
</div></div><br>_______________________________________________<br>
keycloak-dev mailing list<br>
<a href="mailto:keycloak-dev@lists.jboss.org" target="_blank">keycloak-dev@lists.jboss.org</a><br>
<a href="https://lists.jboss.org/mailman/listinfo/keycloak-dev" rel="noreferrer" target="_blank">https://lists.jboss.org/mailman/listinfo/keycloak-dev</a><br></blockquote></div><br></div>
</div></div></blockquote></div><br></div>
</div></div></blockquote></div><br></div>