<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="gmail_extra"><br><div class="gmail_quote">2016-07-11 8:08 GMT+02:00 Stian Thorgersen <span dir="ltr"><<a href="mailto:sthorger@redhat.com" target="_blank">sthorger@redhat.com</a>></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 class="HOEnZb"><div class="h5"><div class="gmail_extra"><br><div class="gmail_quote">On 8 July 2016 at 14:13, Thomas Darimont <span dir="ltr"><<a href="mailto:thomas.darimont@googlemail.com" target="_blank">thomas.darimont@googlemail.com</a>></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 "default" 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"><<a href="mailto:mposolda@redhat.com" target="_blank">mposolda@redhat.com</a>></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'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't need any real requiredAction on user,
but you just want to ensure that "evaluateTriggers" is called
after each user login. Is it correct? <br>
<br>
Then you can just check your requiredAction provider as "enabled",
but NOT as "default" . Note that "evaluateTriggers" will be always
invoked for every user after his login even if user doesn't have
the particular action on him. The purpose of "evaluateTriggers" 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'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 &&
user.getRequiredActions().size() > 0) {</div>
<div> event.error(Errors.RESOLVE_REQUIRED_ACTIONS);</div>
<div> throw new ErrorResponseException("invalid_grant",
"Account is not fully set up", 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'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 "Content-Type:
application/x-www-form-urlencoded" \</div>
<div> -d "grant_type=password" \</div>
<div> -d "username=$KC_USERNAME" \</div>
<div> -d "password=$KC_PASSWORD" \</div>
<div> -d "client_id=$KC_CLIENT" \</div>
<div> -d "client_secret=$KC_CLIENT_SECRET" \</div>
<div>
<a href="http://$KC_SERVER/$KC_CONTEXT/realms/$KC_REALM/protocol/openid-connect/token" target="_blank">"http://$KC_SERVER/$KC_CONTEXT/realms/$KC_REALM/protocol/openid-connect/token"</a></div>
<div><br>
</div>
<div>Gives:</div>
<div>< HTTP/1.1 400 Bad Request</div>
<div>< Connection: keep-alive</div>
<div>< X-Powered-By: Undertow/1</div>
<div>< Server: WildFly/10</div>
<div>< Content-Type: application/json</div>
<div>< Content-Length: 75</div>
<div>< Date: Fri, 08 Jul 2016 08:24:59 GMT</div>
<div>{"error_description":"Account is not fully set
up","error":"invalid_grant"}</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> * <p></div>
<div> * Collects the following:</div>
<div> * <ul></div>
<div> * <li>Date / Time of first login</li></div>
<div> * <li>Date / Time of recent login</li></div>
<div> * <li>Count of logins since first login</li></div>
<div> * <li>Count of failed logins since the configured
<code>Failure Reset Time</code> unter
<code>Security Defenses</code></li></div>
<div> * </ul></div>
<div> * <p></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 = "login_stats_action";</div>
<div><span style="white-space:pre-wrap">        </span>private
static final String RECORD_LOGIN_STATISTICS_ACTION = "Record
Login Statistics Action";</div>
<div><br>
</div>
<div><span style="white-space:pre-wrap">        </span>private
static final String LOGIN_LOGIN_COUNT = "login.login-count";</div>
<div><span style="white-space:pre-wrap">        </span>private
static final String LOGIN_FAILED_LOGIN_COUNT =
"login.failed-login-count";</div>
<div><span style="white-space:pre-wrap">        </span>private
static final String LOGIN_FAILED_LOGIN_DATE =
"login.failed-login-date";</div>
<div><br>
</div>
<div><span style="white-space:pre-wrap">        </span>private
static final String LOGIN_FIRST_LOGIN_DATE =
"login.first-login-date";</div>
<div><span style="white-space:pre-wrap">        </span>private
static final String LOGIN_RECENT_LOGIN_DATE =
"login.recent-login-date";</div>
<div><br>
</div>
<div><span style="white-space:pre-wrap">        </span>private
static final String ONE = "1";</div>
<div><span style="white-space:pre-wrap">        </span>private
static final String ZERO = "0";</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,
"Couldn't record first login <{0}>", 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,
"Couldn't record recent login <{0}>", 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,
"Couldn't record login count <{0}>", 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,
"Couldn't record failed login <{0}>", 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<String>
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<String>
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("Creating
IdM Keycloak extension <{0}>", 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>