<html>
<head>
<meta content="text/html; charset=windows-1252"
http-equiv="Content-Type">
</head>
<body bgcolor="#FFFFFF" text="#000000">
<div class="moz-cite-prefix">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<br>
<meta http-equiv="content-type" content="text/html;
charset=windows-1252">
<meta http-equiv="content-type" content="text/html;
charset=windows-1252">
<br>
On 08/07/16 10:35, Thomas Darimont wrote:<br>
</div>
<blockquote
cite="mid:CAK-7U1hA1OBzH9Bj3gufT8ztOmr8vZ-JKOsAzKV=H3_gjdLeRA@mail.gmail.com"
type="cite">
<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 moz-do-not-send="true"
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 moz-do-not-send="true"
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'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 moz-do-not-send="true"
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 "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 class="moz-txt-link-rfc2396E" href="http://$KC_SERVER/$KC_CONTEXT/realms/$KC_REALM/protocol/openid-connect/token">"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 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 = "login_stats_action";</div>
<div><span class="" style="white-space:pre">        </span>private
static final String RECORD_LOGIN_STATISTICS_ACTION = "Record
Login Statistics Action";</div>
<div><br>
</div>
<div><span class="" style="white-space:pre">        </span>private
static final String LOGIN_LOGIN_COUNT = "login.login-count";</div>
<div><span class="" style="white-space:pre">        </span>private
static final String LOGIN_FAILED_LOGIN_COUNT =
"login.failed-login-count";</div>
<div><span class="" style="white-space:pre">        </span>private
static final String LOGIN_FAILED_LOGIN_DATE =
"login.failed-login-date";</div>
<div><br>
</div>
<div><span class="" style="white-space:pre">        </span>private
static final String LOGIN_FIRST_LOGIN_DATE =
"login.first-login-date";</div>
<div><span class="" style="white-space:pre">        </span>private
static final String LOGIN_RECENT_LOGIN_DATE =
"login.recent-login-date";</div>
<div><br>
</div>
<div><span class="" style="white-space:pre">        </span>private
static final String ONE = "1";</div>
<div><span class="" style="white-space:pre">        </span>private
static final String ZERO = "0";</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,
"Couldn't record first login <{0}>", 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,
"Couldn't record recent login <{0}>", 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,
"Couldn't record login count <{0}>", 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,
"Couldn't record failed login <{0}>", 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<String>
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<String>
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("Creating
IdM Keycloak extension <{0}>", 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>
<br>
<fieldset class="mimeAttachmentHeader"></fieldset>
<br>
<pre wrap="">_______________________________________________
keycloak-dev mailing list
<a class="moz-txt-link-abbreviated" href="mailto:keycloak-dev@lists.jboss.org">keycloak-dev@lists.jboss.org</a>
<a class="moz-txt-link-freetext" href="https://lists.jboss.org/mailman/listinfo/keycloak-dev">https://lists.jboss.org/mailman/listinfo/keycloak-dev</a></pre>
</blockquote>
<br>
</body>
</html>