keycloak-uncached
Changes
adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KeycloakInstalled.java 81(+19 -62)
services/src/main/java/org/keycloak/authentication/authenticators/console/ConsoleOTPFormAuthenticator.java 6(+3 -3)
services/src/main/java/org/keycloak/authentication/authenticators/console/ConsoleUsernamePasswordAuthenticator.java 7(+2 -5)
services/src/main/java/org/keycloak/authentication/requiredactions/ConsoleTermsAndConditions.java 8(+2 -6)
services/src/main/java/org/keycloak/authentication/requiredactions/ConsoleUpdatePassword.java 9(+2 -7)
services/src/main/java/org/keycloak/authentication/requiredactions/ConsoleUpdateTotp.java 15(+3 -12)
services/src/main/java/org/keycloak/authentication/requiredactions/ConsoleVerifyEmail.java 17(+3 -14)
services/src/main/java/org/keycloak/services/resources/admin/AuthenticationManagementResource.java 23(+14 -9)
testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/actions/DummyRequiredActionFactory.java 23(+22 -1)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/exec/AbstractExec.java 2(+1 -1)
Details
diff --git a/adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KeycloakInstalled.java b/adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KeycloakInstalled.java
index 4f311c2..5011667 100644
--- a/adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KeycloakInstalled.java
+++ b/adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KeycloakInstalled.java
@@ -98,70 +98,12 @@ public class KeycloakInstalled {
         this.deployment = deployment;
     }
 
-    private static HttpResponseWriter defaultLoginWriter = new HttpResponseWriter() {
-        @Override
-        public void success(PrintWriter pw, KeycloakInstalled ki) {
-            pw.println("HTTP/1.1 200 OK");
-            pw.println("Content-Type: text/html");
-            pw.println();
-            pw.println("<html><h1>Login completed.</h1><div>");
-            pw.println("This browser will remain logged in until you close it, logout, or the session expires.");
-            pw.println("</div></html>");
-            pw.flush();
-
-        }
-
-        @Override
-        public void failure(PrintWriter pw, KeycloakInstalled ki) {
-            pw.println("HTTP/1.1 200 OK");
-            pw.println("Content-Type: text/html");
-            pw.println();
-            pw.println("<html><h1>Login attempt failed.</h1><div>");
-            pw.println("</div></html>");
-            pw.flush();
-
-        }
-    };
-    private static HttpResponseWriter defaultLogoutWriter = new HttpResponseWriter() {
-        @Override
-        public void success(PrintWriter pw, KeycloakInstalled ki) {
-            pw.println("HTTP/1.1 200 OK");
-            pw.println("Content-Type: text/html");
-            pw.println();
-            pw.println("<html><h1>Logout completed.</h1><div>");
-            pw.println("You may close this browser tab.");
-            pw.println("</div></html>");
-            pw.flush();
-
-        }
-
-        @Override
-        public void failure(PrintWriter pw, KeycloakInstalled ki) {
-            pw.println("HTTP/1.1 200 OK");
-            pw.println("Content-Type: text/html");
-            pw.println();
-            pw.println("<html><h1>Logout failed.</h1><div>");
-            pw.println("You may close this browser tab.");
-            pw.println("</div></html>");
-            pw.flush();
-
-        }
-    };
-
     public HttpResponseWriter getLoginResponseWriter() {
-        if (loginResponseWriter == null) {
-            return defaultLoginWriter;
-        } else {
-            return loginResponseWriter;
-        }
+        return null;
     }
 
     public HttpResponseWriter getLogoutResponseWriter() {
-        if (logoutResponseWriter == null) {
-            return defaultLogoutWriter;
-        } else {
-            return logoutResponseWriter;
-        }
+        return null;
     }
 
     public void setLoginResponseWriter(HttpResponseWriter loginResponseWriter) {
@@ -709,11 +651,26 @@ public class KeycloakInstalled {
 
                 OutputStreamWriter out = new OutputStreamWriter(socket.getOutputStream());
                 PrintWriter pw = new PrintWriter(out);
+                if (writer != null) {
+                    System.err.println("Using a writer is deprecated.  Please remove its usage.  This is now handled by endpoint on server");
+                }
 
                 if (error == null) {
-                    writer.success(pw, KeycloakInstalled.this);
+                     if (writer != null) {
+                         writer.success(pw, KeycloakInstalled.this);
+                     } else {
+                         pw.println("HTTP/1.1 302 Found");
+                         pw.println("Location: " + deployment.getTokenUrl().replace("/token", "/delegated"));
+
+                     }
                 } else {
-                    writer.failure(pw, KeycloakInstalled.this);
+                    if (writer != null) {
+                        writer.failure(pw, KeycloakInstalled.this);
+                    } else {
+                        pw.println("HTTP/1.1 302 Found");
+                        pw.println("Location: " + deployment.getTokenUrl().replace("/token", "/delegated?error=true"));
+
+                    }
                 }
                 pw.flush();
                 socket.close();
                diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java
index db96f11..502ac6d 100755
--- a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java
+++ b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java
@@ -254,6 +254,19 @@ public class AuthenticationProcessor {
         getAuthenticationSession().setAuthenticatedUser(null);
     }
 
+    public URI getRefreshUrl(boolean authSessionIdParam) {
+        UriBuilder uriBuilder = LoginActionsService.loginActionsBaseUrl(getUriInfo())
+                .path(AuthenticationProcessor.this.flowPath)
+                .queryParam(Constants.CLIENT_ID, getAuthenticationSession().getClient().getClientId())
+                .queryParam(Constants.TAB_ID, getAuthenticationSession().getTabId());
+        if (authSessionIdParam) {
+            uriBuilder.queryParam(LoginActionsService.AUTH_SESSION_ID, getAuthenticationSession().getParentSession().getId());
+        }
+        return uriBuilder
+                .build(getRealm().getName());
+    }
+
+
     public class Result implements AuthenticationFlowContext, ClientAuthenticationFlowContext {
         AuthenticatorConfigModel authenticatorConfig;
         AuthenticationExecutionModel execution;
@@ -546,15 +559,7 @@ public class AuthenticationProcessor {
 
         @Override
         public URI getRefreshUrl(boolean authSessionIdParam) {
-            UriBuilder uriBuilder = LoginActionsService.loginActionsBaseUrl(getUriInfo())
-                    .path(AuthenticationProcessor.this.flowPath)
-                    .queryParam(Constants.CLIENT_ID, getAuthenticationSession().getClient().getClientId())
-                    .queryParam(Constants.TAB_ID, getAuthenticationSession().getTabId());
-            if (authSessionIdParam) {
-                uriBuilder.queryParam(LoginActionsService.AUTH_SESSION_ID, getAuthenticationSession().getParentSession().getId());
-            }
-            return uriBuilder
-                    .build(getRealm().getName());
+            return AuthenticationProcessor.this.getRefreshUrl(authSessionIdParam);
         }
 
         @Override
                diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/console/ConsoleOTPFormAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/console/ConsoleOTPFormAuthenticator.java
index fff2c80..0335b17 100755
--- a/services/src/main/java/org/keycloak/authentication/authenticators/console/ConsoleOTPFormAuthenticator.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/console/ConsoleOTPFormAuthenticator.java
@@ -19,7 +19,7 @@ package org.keycloak.authentication.authenticators.console;
 
 import org.keycloak.authentication.AuthenticationFlowContext;
 import org.keycloak.authentication.Authenticator;
-import org.keycloak.authentication.TextChallenge;
+import org.keycloak.authentication.ConsoleDisplayMode;
 import org.keycloak.authentication.authenticators.browser.OTPFormAuthenticator;
 import org.keycloak.representations.idm.CredentialRepresentation;
 
@@ -37,8 +37,8 @@ public class ConsoleOTPFormAuthenticator extends OTPFormAuthenticator implements
         return context.getActionUrl(context.generateAccessCode(), true);
     }
 
-    protected TextChallenge challenge(AuthenticationFlowContext context) {
-        return TextChallenge.challenge(context)
+    protected ConsoleDisplayMode challenge(AuthenticationFlowContext context) {
+        return ConsoleDisplayMode.challenge(context)
                 .header()
                 .param(CredentialRepresentation.TOTP)
                 .label("console-otp")
                diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/console/ConsoleUsernamePasswordAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/console/ConsoleUsernamePasswordAuthenticator.java
index 4595df5..720e4e5 100755
--- a/services/src/main/java/org/keycloak/authentication/authenticators/console/ConsoleUsernamePasswordAuthenticator.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/console/ConsoleUsernamePasswordAuthenticator.java
@@ -24,11 +24,8 @@ import org.keycloak.models.RealmModel;
 import org.keycloak.models.UserModel;
 import org.keycloak.services.messages.Messages;
 
-import javax.ws.rs.core.HttpHeaders;
-import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.MultivaluedMap;
 import javax.ws.rs.core.Response;
-import java.net.URI;
 
 /**
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -43,8 +40,8 @@ public class ConsoleUsernamePasswordAuthenticator extends AbstractUsernameFormAu
         return false;
     }
 
-    protected TextChallenge challenge(AuthenticationFlowContext context) {
-        return TextChallenge.challenge(context)
+    protected ConsoleDisplayMode challenge(AuthenticationFlowContext context) {
+        return ConsoleDisplayMode.challenge(context)
                 .header()
                 .param("username")
                 .label("console-username")
                diff --git a/services/src/main/java/org/keycloak/authentication/DefaultAuthenticationFlow.java b/services/src/main/java/org/keycloak/authentication/DefaultAuthenticationFlow.java
index ac0c5e1..3c4c2e6 100755
--- a/services/src/main/java/org/keycloak/authentication/DefaultAuthenticationFlow.java
+++ b/services/src/main/java/org/keycloak/authentication/DefaultAuthenticationFlow.java
@@ -60,7 +60,7 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
     }
 
     protected Authenticator createAuthenticator(AuthenticatorFactory factory) {
-        String display = processor.getAuthenticationSession().getClientNote(OAuth2Constants.DISPLAY);
+        String display = processor.getAuthenticationSession().getAuthNote(OAuth2Constants.DISPLAY);
         if (display == null) return factory.create(processor.getSession());
 
 
@@ -70,7 +70,9 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
         }
         // todo create a provider for handling lack of display support
         if (OAuth2Constants.DISPLAY_CONSOLE.equalsIgnoreCase(display)) {
-            throw new AuthenticationFlowException(AuthenticationFlowError.DISPLAY_NOT_SUPPORTED, TextChallenge.browserRequired(processor.getSession()));
+            processor.getAuthenticationSession().removeAuthNote(OAuth2Constants.DISPLAY);
+            throw new AuthenticationFlowException(AuthenticationFlowError.DISPLAY_NOT_SUPPORTED,
+                    ConsoleDisplayMode.browserContinue(processor.getSession(), processor.getRefreshUrl(true).toString()));
 
         } else {
             return factory.create(processor.getSession());
                diff --git a/services/src/main/java/org/keycloak/authentication/RequiredActionContextResult.java b/services/src/main/java/org/keycloak/authentication/RequiredActionContextResult.java
index 38b9c2f..f793234 100755
--- a/services/src/main/java/org/keycloak/authentication/RequiredActionContextResult.java
+++ b/services/src/main/java/org/keycloak/authentication/RequiredActionContextResult.java
@@ -65,6 +65,10 @@ public class RequiredActionContextResult implements RequiredActionContext {
         this.factory = factory;
     }
 
+    public RequiredActionFactory getFactory() {
+        return factory;
+    }
+
     @Override
     public EventBuilder getEvent() {
         return eventBuilder;
@@ -170,7 +174,6 @@ public class RequiredActionContextResult implements RequiredActionContext {
             uri = UriBuilder.fromUri(uri).queryParam(LoginActionsService.AUTH_SESSION_ID, getAuthenticationSession().getParentSession().getId()).build();
         }
         return uri;
-
     }
 
     @Override
                diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/ConsoleTermsAndConditions.java b/services/src/main/java/org/keycloak/authentication/requiredactions/ConsoleTermsAndConditions.java
index 24c6938..1db910d 100755
--- a/services/src/main/java/org/keycloak/authentication/requiredactions/ConsoleTermsAndConditions.java
+++ b/services/src/main/java/org/keycloak/authentication/requiredactions/ConsoleTermsAndConditions.java
@@ -17,14 +17,10 @@
 
 package org.keycloak.authentication.requiredactions;
 
-import org.keycloak.Config;
 import org.keycloak.authentication.RequiredActionContext;
-import org.keycloak.authentication.RequiredActionFactory;
 import org.keycloak.authentication.RequiredActionProvider;
-import org.keycloak.authentication.TextChallenge;
+import org.keycloak.authentication.ConsoleDisplayMode;
 import org.keycloak.common.util.Time;
-import org.keycloak.models.KeycloakSession;
-import org.keycloak.models.KeycloakSessionFactory;
 
 import javax.ws.rs.core.Response;
 import java.util.Arrays;
@@ -45,7 +41,7 @@ public class ConsoleTermsAndConditions implements RequiredActionProvider {
 
     @Override
     public void requiredActionChallenge(RequiredActionContext context) {
-        Response challenge = TextChallenge.challenge(context)
+        Response challenge = ConsoleDisplayMode.challenge(context)
                 .header()
                 .param("accept")
                 .label("console-accept-terms")
                diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/ConsoleUpdatePassword.java b/services/src/main/java/org/keycloak/authentication/requiredactions/ConsoleUpdatePassword.java
index d499ead..461007a 100755
--- a/services/src/main/java/org/keycloak/authentication/requiredactions/ConsoleUpdatePassword.java
+++ b/services/src/main/java/org/keycloak/authentication/requiredactions/ConsoleUpdatePassword.java
@@ -18,7 +18,6 @@
 package org.keycloak.authentication.requiredactions;
 
 import org.jboss.logging.Logger;
-import org.keycloak.Config;
 import org.keycloak.authentication.*;
 import org.keycloak.events.Details;
 import org.keycloak.events.Errors;
@@ -28,11 +27,7 @@ import org.keycloak.models.*;
 import org.keycloak.services.messages.Messages;
 import org.keycloak.services.validation.Validation;
 
-import javax.ws.rs.core.HttpHeaders;
-import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.MultivaluedMap;
-import javax.ws.rs.core.Response;
-import java.net.URI;
 
 /**
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -45,8 +40,8 @@ public class ConsoleUpdatePassword extends UpdatePassword implements RequiredAct
     public static final String PASSWORD_NEW = "password-new";
     public static final String PASSWORD_CONFIRM = "password-confirm";
 
-     protected TextChallenge challenge(RequiredActionContext context) {
-        return TextChallenge.challenge(context)
+     protected ConsoleDisplayMode challenge(RequiredActionContext context) {
+        return ConsoleDisplayMode.challenge(context)
                 .header()
                 .param(PASSWORD_NEW)
                 .label("console-new-password")
                diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/ConsoleUpdateTotp.java b/services/src/main/java/org/keycloak/authentication/requiredactions/ConsoleUpdateTotp.java
index 89ef89b..32751b9 100644
--- a/services/src/main/java/org/keycloak/authentication/requiredactions/ConsoleUpdateTotp.java
+++ b/services/src/main/java/org/keycloak/authentication/requiredactions/ConsoleUpdateTotp.java
@@ -17,28 +17,19 @@
 
 package org.keycloak.authentication.requiredactions;
 
-import org.keycloak.Config;
 import org.keycloak.authentication.RequiredActionContext;
-import org.keycloak.authentication.RequiredActionFactory;
 import org.keycloak.authentication.RequiredActionProvider;
-import org.keycloak.authentication.TextChallenge;
+import org.keycloak.authentication.ConsoleDisplayMode;
 import org.keycloak.events.EventBuilder;
 import org.keycloak.events.EventType;
-import org.keycloak.forms.login.LoginFormsProvider;
 import org.keycloak.forms.login.freemarker.model.TotpBean;
-import org.keycloak.models.KeycloakSession;
-import org.keycloak.models.KeycloakSessionFactory;
 import org.keycloak.models.UserCredentialModel;
-import org.keycloak.models.UserModel;
 import org.keycloak.models.utils.CredentialValidation;
 import org.keycloak.services.messages.Messages;
 import org.keycloak.services.validation.Validation;
 
-import javax.ws.rs.core.HttpHeaders;
-import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.MultivaluedMap;
 import javax.ws.rs.core.Response;
-import java.net.URI;
 
 /**
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -61,8 +52,8 @@ public class ConsoleUpdateTotp implements RequiredActionProvider {
         context.challenge(challenge);
     }
 
-    protected TextChallenge challenge(RequiredActionContext context) {
-        return TextChallenge.challenge(context)
+    protected ConsoleDisplayMode challenge(RequiredActionContext context) {
+        return ConsoleDisplayMode.challenge(context)
                 .header()
                 .param("totp")
                 .label("console-otp")
                diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/ConsoleVerifyEmail.java b/services/src/main/java/org/keycloak/authentication/requiredactions/ConsoleVerifyEmail.java
index e136ceb..e3c6ec5 100755
--- a/services/src/main/java/org/keycloak/authentication/requiredactions/ConsoleVerifyEmail.java
+++ b/services/src/main/java/org/keycloak/authentication/requiredactions/ConsoleVerifyEmail.java
@@ -18,35 +18,24 @@
 package org.keycloak.authentication.requiredactions;
 
 import org.jboss.logging.Logger;
-import org.keycloak.Config;
 import org.keycloak.authentication.RequiredActionContext;
-import org.keycloak.authentication.RequiredActionFactory;
 import org.keycloak.authentication.RequiredActionProvider;
-import org.keycloak.authentication.TextChallenge;
-import org.keycloak.authentication.actiontoken.verifyemail.VerifyEmailActionToken;
+import org.keycloak.authentication.ConsoleDisplayMode;
 import org.keycloak.common.util.RandomString;
-import org.keycloak.common.util.Time;
 import org.keycloak.email.EmailException;
 import org.keycloak.email.EmailTemplateProvider;
 import org.keycloak.events.Details;
 import org.keycloak.events.Errors;
 import org.keycloak.events.EventBuilder;
 import org.keycloak.events.EventType;
-import org.keycloak.forms.login.LoginFormsProvider;
 import org.keycloak.models.*;
-import org.keycloak.services.Urls;
 import org.keycloak.services.messages.Messages;
 import org.keycloak.services.validation.Validation;
-import org.keycloak.sessions.AuthenticationSessionCompoundId;
 import org.keycloak.sessions.AuthenticationSessionModel;
 
 import javax.ws.rs.core.*;
-import java.net.URI;
-import java.text.MessageFormat;
 import java.util.HashMap;
 import java.util.Map;
-import java.util.Objects;
-import java.util.concurrent.TimeUnit;
 
 /**
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -114,8 +103,8 @@ public class ConsoleVerifyEmail implements RequiredActionProvider {
     }
 
     public static String EMAIL_CODE="email_code";
-    protected TextChallenge challenge(RequiredActionContext context) {
-        return TextChallenge.challenge(context)
+    protected ConsoleDisplayMode challenge(RequiredActionContext context) {
+        return ConsoleDisplayMode.challenge(context)
                 .header()
                 .param(EMAIL_CODE)
                 .label("console-email-code")
                diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java
index 65c66e2..666cf3e 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java
@@ -371,7 +371,7 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
         if (request.getResponseMode() != null) authenticationSession.setClientNote(OIDCLoginProtocol.RESPONSE_MODE_PARAM, request.getResponseMode());
         if (request.getClaims()!= null) authenticationSession.setClientNote(OIDCLoginProtocol.CLAIMS_PARAM, request.getClaims());
         if (request.getAcr() != null) authenticationSession.setClientNote(OIDCLoginProtocol.ACR_PARAM, request.getAcr());
-        if (request.getDisplay() != null) authenticationSession.setClientNote(OAuth2Constants.DISPLAY, request.getDisplay());
+        if (request.getDisplay() != null) authenticationSession.setAuthNote(OAuth2Constants.DISPLAY, request.getDisplay());
 
         // https://tools.ietf.org/html/rfc7636#section-4
         if (request.getCodeChallenge() != null) authenticationSession.setClientNote(OIDCLoginProtocol.CODE_CHALLENGE_PARAM, request.getCodeChallenge());
                diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java
index 56c0022..148d840 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java
@@ -71,7 +71,6 @@ public class OIDCLoginProtocol implements LoginProtocol {
     public static final String MAX_AGE_PARAM = OAuth2Constants.MAX_AGE;
     public static final String PROMPT_PARAM = OAuth2Constants.PROMPT;
     public static final String LOGIN_HINT_PARAM = "login_hint";
-    public static final String DISPLAY_PARAM = "display";
     public static final String REQUEST_PARAM = "request";
     public static final String REQUEST_URI_PARAM = "request_uri";
     public static final String UI_LOCALES_PARAM = OAuth2Constants.UI_LOCALES_PARAM;
                diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolService.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolService.java
index 160013f..6fa6705 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolService.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolService.java
@@ -20,6 +20,7 @@ package org.keycloak.protocol.oidc;
 import org.jboss.resteasy.annotations.cache.NoCache;
 import org.jboss.resteasy.spi.HttpRequest;
 import org.jboss.resteasy.spi.ResteasyProviderFactory;
+import org.keycloak.common.ClientConnection;
 import org.keycloak.events.EventBuilder;
 import org.keycloak.forms.login.LoginFormsProvider;
 import org.keycloak.jose.jwk.JSONWebKeySet;
@@ -27,6 +28,7 @@ import org.keycloak.jose.jwk.JWK;
 import org.keycloak.jose.jwk.JWKBuilder;
 import org.keycloak.keys.KeyMetadata;
 import org.keycloak.keys.RsaKeyMetadata;
+import org.keycloak.models.Constants;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.RealmModel;
 import org.keycloak.protocol.oidc.endpoints.AuthorizationEndpoint;
@@ -34,6 +36,8 @@ import org.keycloak.protocol.oidc.endpoints.LoginStatusIframeEndpoint;
 import org.keycloak.protocol.oidc.endpoints.LogoutEndpoint;
 import org.keycloak.protocol.oidc.endpoints.TokenEndpoint;
 import org.keycloak.protocol.oidc.endpoints.UserInfoEndpoint;
+import org.keycloak.services.managers.AuthenticationManager;
+import org.keycloak.services.messages.Messages;
 import org.keycloak.services.resources.Cors;
 import org.keycloak.services.resources.RealmsResource;
 import org.keycloak.services.util.CacheControlUtil;
@@ -75,6 +79,9 @@ public class OIDCLoginProtocolService {
     @Context
     private HttpRequest request;
 
+    @Context
+    private ClientConnection clientConnection;
+
     public OIDCLoginProtocolService(RealmModel realm, EventBuilder event) {
         this.realm = realm;
         this.tokenManager = new TokenManager();
@@ -228,4 +235,31 @@ public class OIDCLoginProtocolService {
         }
     }
 
+    /**
+     * For KeycloakInstalled and kcinit login where command line login is delegated to a browser.
+     * This clears login cookies and outputs login success or failure messages.
+     *
+     * @param error
+     * @return
+     */
+    @GET
+    @Path("delegated")
+    public Response kcinitBrowserLoginComplete(@QueryParam("error") boolean error) {
+        AuthenticationManager.expireIdentityCookie(realm, uriInfo, clientConnection);
+        AuthenticationManager.expireRememberMeCookie(realm, uriInfo, clientConnection);
+        if (error) {
+            LoginFormsProvider forms = session.getProvider(LoginFormsProvider.class);
+            return forms
+                    .setAttribute("messageHeader", forms.getMessage(Messages.DELEGATION_FAILED_HEADER))
+                    .setAttribute(Constants.SKIP_LINK, true).setError(Messages.DELEGATION_FAILED).createInfoPage();
+
+        } else {
+            LoginFormsProvider forms = session.getProvider(LoginFormsProvider.class);
+            return forms
+                    .setAttribute("messageHeader", forms.getMessage(Messages.DELEGATION_COMPLETE_HEADER))
+                    .setAttribute(Constants.SKIP_LINK, true)
+                    .setSuccess(Messages.DELEGATION_COMPLETE).createInfoPage();
+        }
+    }
+
 }
                diff --git a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
index edb9b51..87df917 100755
--- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
@@ -966,21 +966,22 @@ public class AuthenticationManager {
         authSession.setProtocolMappers(requestedProtocolMappers);
     }
 
-    public static RequiredActionProvider createRequiredAction(KeycloakSession session, RequiredActionFactory factory, AuthenticationSessionModel authSession) {
-        String display = authSession.getClientNote(OAuth2Constants.DISPLAY);
-        if (display == null) return factory.create(session);
+    public static RequiredActionProvider createRequiredAction(RequiredActionContextResult context) {
+        String display = context.getAuthenticationSession().getAuthNote(OAuth2Constants.DISPLAY);
+        if (display == null) return context.getFactory().create(context.getSession());
 
 
-        if (factory instanceof DisplayTypeRequiredActionFactory) {
-            RequiredActionProvider provider = ((DisplayTypeRequiredActionFactory)factory).createDisplay(session, display);
+        if (context.getFactory() instanceof DisplayTypeRequiredActionFactory) {
+            RequiredActionProvider provider = ((DisplayTypeRequiredActionFactory)context.getFactory()).createDisplay(context.getSession(), display);
             if (provider != null) return provider;
         }
         // todo create a provider for handling lack of display support
         if (OAuth2Constants.DISPLAY_CONSOLE.equalsIgnoreCase(display)) {
-            throw new AuthenticationFlowException(AuthenticationFlowError.DISPLAY_NOT_SUPPORTED, TextChallenge.browserRequired(session));
+            context.getAuthenticationSession().removeAuthNote(OAuth2Constants.DISPLAY);
+            throw new AuthenticationFlowException(AuthenticationFlowError.DISPLAY_NOT_SUPPORTED, ConsoleDisplayMode.browserContinue(context.getSession(), context.getUriInfo().getRequestUri().toString()));
 
         } else {
-            return factory.create(session);
+            return context.getFactory().create(context.getSession());
         }
     }
 
@@ -1002,16 +1003,16 @@ public class AuthenticationManager {
             if (factory == null) {
                 throw new RuntimeException("Unable to find factory for Required Action: " + model.getProviderId() + " did you forget to declare it in a META-INF/services file?");
             }
+            RequiredActionContextResult context = new RequiredActionContextResult(authSession, realm, event, session, request, user, factory);
             RequiredActionProvider actionProvider = null;
             try {
-                actionProvider = createRequiredAction(session, factory, authSession);
+                actionProvider = createRequiredAction(context);
             } catch (AuthenticationFlowException e) {
                 if (e.getResponse() != null) {
                     return e.getResponse();
                 }
                 throw e;
             }
-            RequiredActionContextResult context = new RequiredActionContextResult(authSession, realm, event, session, request, user, factory);
             actionProvider.requiredActionChallenge(context);
 
             if (context.getStatus() == RequiredActionContext.Status.FAILURE) {
                diff --git a/services/src/main/java/org/keycloak/services/messages/Messages.java b/services/src/main/java/org/keycloak/services/messages/Messages.java
index 425a889..5a825cc 100755
--- a/services/src/main/java/org/keycloak/services/messages/Messages.java
+++ b/services/src/main/java/org/keycloak/services/messages/Messages.java
@@ -225,4 +225,9 @@ public class Messages {
 
     public static final String INTERNAL_SERVER_ERROR = "internalServerError";
 
+    public static final String DELEGATION_COMPLETE = "delegationCompleteMessage";
+    public static final String DELEGATION_COMPLETE_HEADER = "delegationCompleteHeader";
+    public static final String DELEGATION_FAILED = "delegationFailedMessage";
+    public static final String DELEGATION_FAILED_HEADER = "delegationFailedHeader";
+
 }
                diff --git a/services/src/main/java/org/keycloak/services/resources/admin/AuthenticationManagementResource.java b/services/src/main/java/org/keycloak/services/resources/admin/AuthenticationManagementResource.java
index 09892cb..f689f6e 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/AuthenticationManagementResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/AuthenticationManagementResource.java
@@ -306,6 +306,16 @@ public class AuthenticationManagementResource {
             logger.debug("flow not found: " + flowAlias);
             return Response.status(NOT_FOUND).build();
         }
+        AuthenticationFlowModel copy = copyFlow(realm, flow, newName);
+
+        data.put("id", copy.getId());
+        adminEvent.operation(OperationType.CREATE).resourcePath(uriInfo).representation(data).success();
+
+        return Response.status(Response.Status.CREATED).build();
+
+    }
+
+    public static AuthenticationFlowModel copyFlow(RealmModel realm, AuthenticationFlowModel flow, String newName) {
         AuthenticationFlowModel copy = new AuthenticationFlowModel();
         copy.setAlias(newName);
         copy.setDescription(flow.getDescription());
@@ -313,16 +323,11 @@ public class AuthenticationManagementResource {
         copy.setBuiltIn(false);
         copy.setTopLevel(flow.isTopLevel());
         copy = realm.addAuthenticationFlow(copy);
-        copy(newName, flow, copy);
-
-        data.put("id", copy.getId());
-        adminEvent.operation(OperationType.CREATE).resourcePath(uriInfo).representation(data).success();
-
-        return Response.status(Response.Status.CREATED).build();
-
+        copy(realm, newName, flow, copy);
+        return copy;
     }
 
-    protected void copy(String newName, AuthenticationFlowModel from, AuthenticationFlowModel to) {
+    public static void copy(RealmModel realm, String newName, AuthenticationFlowModel from, AuthenticationFlowModel to) {
         for (AuthenticationExecutionModel execution : realm.getAuthenticationExecutions(from.getId())) {
             if (execution.isAuthenticatorFlow()) {
                 AuthenticationFlowModel subFlow = realm.getAuthenticationFlowById(execution.getFlowId());
@@ -334,7 +339,7 @@ public class AuthenticationManagementResource {
                 copy.setTopLevel(false);
                 copy = realm.addAuthenticationFlow(copy);
                 execution.setFlowId(copy.getId());
-                copy(newName, subFlow, copy);
+                copy(realm, newName, subFlow, copy);
             }
             execution.setId(null);
             execution.setParentFlow(to.getId());
                diff --git a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
index ef9525a..f517c7d 100755
--- a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
+++ b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
@@ -929,9 +929,15 @@ public class LoginActionsService {
             event.error(Errors.INVALID_CODE);
             throw new WebApplicationException(ErrorPage.error(session, authSession, Response.Status.BAD_REQUEST, Messages.INVALID_CODE));
         }
+        RequiredActionContextResult context = new RequiredActionContextResult(authSession, realm, event, session, request, authSession.getAuthenticatedUser(), factory) {
+            @Override
+            public void ignore() {
+                throw new RuntimeException("Cannot call ignore within processAction()");
+            }
+        };
         RequiredActionProvider provider = null;
         try {
-            provider = AuthenticationManager.createRequiredAction(session, factory, authSession);
+            provider = AuthenticationManager.createRequiredAction(context);
         }  catch (AuthenticationFlowException e) {
             if (e.getResponse() != null) {
                 return e.getResponse();
@@ -939,12 +945,6 @@ public class LoginActionsService {
             throw new WebApplicationException(ErrorPage.error(session, authSession, Response.Status.BAD_REQUEST, Messages.DISPLAY_UNSUPPORTED));
         }
 
-        RequiredActionContextResult context = new RequiredActionContextResult(authSession, realm, event, session, request, authSession.getAuthenticatedUser(), factory) {
-            @Override
-            public void ignore() {
-                throw new RuntimeException("Cannot call ignore within processAction()");
-            }
-        };
 
         Response response;
         provider.processAction(context);
                diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/actions/DummyRequiredActionFactory.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/actions/DummyRequiredActionFactory.java
index fb2f74d..600d6f7 100755
--- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/actions/DummyRequiredActionFactory.java
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/actions/DummyRequiredActionFactory.java
@@ -18,6 +18,7 @@
 package org.keycloak.testsuite.actions;
 
 import org.keycloak.Config;
+import org.keycloak.authentication.RequiredActionContext;
 import org.keycloak.authentication.RequiredActionFactory;
 import org.keycloak.authentication.RequiredActionProvider;
 import org.keycloak.models.KeycloakSession;
@@ -38,7 +39,27 @@ public class DummyRequiredActionFactory implements RequiredActionFactory {
 
     @Override
     public RequiredActionProvider create(KeycloakSession session) {
-        return null;
+        return new RequiredActionProvider() {
+            @Override
+            public void evaluateTriggers(RequiredActionContext context) {
+
+            }
+
+            @Override
+            public void requiredActionChallenge(RequiredActionContext context) {
+                context.success();
+            }
+
+            @Override
+            public void processAction(RequiredActionContext context) {
+
+            }
+
+            @Override
+            public void close() {
+
+            }
+        };
     }
 
     @Override
                diff --git a/testsuite/integration-arquillian/tests/base/pom.xml b/testsuite/integration-arquillian/tests/base/pom.xml
index 1e5d755..754c6da 100644
--- a/testsuite/integration-arquillian/tests/base/pom.xml
+++ b/testsuite/integration-arquillian/tests/base/pom.xml
@@ -264,7 +264,7 @@
                                 <package>github.com/keycloak/kcinit</package>
                             </packages>
                             <goPath>${project.build.directory}/gopath</goPath>
-                            <tag>0.3</tag>
+                            <tag>0.4</tag>
                         </configuration>
                     </execution>
                   </executions>
                diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/exec/AbstractExec.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/exec/AbstractExec.java
index ddfe91d..e7cd34d 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/exec/AbstractExec.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/exec/AbstractExec.java
@@ -239,7 +239,7 @@ public abstract class AbstractExec {
             }
         }
 
-        throw new RuntimeException("Timed while waiting for content to appear in stdout");
+        throw new RuntimeException("Timed while waiting for content to appear in stderr");
     }
 
     public void sendToStdin(String s) {
                diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/KcinitTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/KcinitTest.java
index f23a63c..2d5ca5b 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/KcinitTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/KcinitTest.java
@@ -25,20 +25,26 @@ import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.keycloak.admin.client.resource.UserResource;
+import org.keycloak.authentication.RequiredActionProvider;
 import org.keycloak.authentication.authenticators.console.ConsoleUsernamePasswordAuthenticatorFactory;
 import org.keycloak.authentication.requiredactions.TermsAndConditions;
 import org.keycloak.authorization.model.Policy;
 import org.keycloak.authorization.model.ResourceServer;
 import org.keycloak.credential.CredentialModel;
 import org.keycloak.models.*;
+import org.keycloak.models.utils.DefaultAuthenticationFlows;
 import org.keycloak.models.utils.TimeBasedOTP;
 import org.keycloak.representations.idm.RealmRepresentation;
 import org.keycloak.representations.idm.RequiredActionProviderRepresentation;
 import org.keycloak.representations.idm.authorization.ClientPolicyRepresentation;
+import org.keycloak.services.resources.admin.AuthenticationManagementResource;
 import org.keycloak.services.resources.admin.permissions.AdminPermissionManagement;
 import org.keycloak.services.resources.admin.permissions.AdminPermissions;
 import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
 import org.keycloak.testsuite.AssertEvents;
+import org.keycloak.testsuite.actions.DummyRequiredActionFactory;
+import org.keycloak.testsuite.authentication.PushButtonAuthenticator;
+import org.keycloak.testsuite.authentication.PushButtonAuthenticatorFactory;
 import org.keycloak.testsuite.forms.PassThroughAuthenticator;
 import org.keycloak.testsuite.pages.AppPage;
 import org.keycloak.testsuite.pages.ErrorPage;
@@ -49,8 +55,11 @@ import org.keycloak.testsuite.util.MailUtils;
 import org.keycloak.testsuite.util.OAuthClient;
 import org.keycloak.util.JsonSerialization;
 import org.keycloak.utils.TotpUtils;
+import org.openqa.selenium.By;
 
 import javax.mail.internet.MimeMessage;
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.ClientBuilder;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.regex.Matcher;
@@ -69,6 +78,9 @@ public class KcinitTest extends AbstractTestRealmKeycloakTest {
     @Rule
     public AssertEvents events = new AssertEvents(this);
 
+    @Page
+    protected LoginPage loginPage;
+
     @Override
     public void configureTestRealm(RealmRepresentation testRealm) {
     }
@@ -95,10 +107,9 @@ public class KcinitTest extends AbstractTestRealmKeycloakTest {
             }
 
             ClientModel kcinit = realm.addClient(KCINIT_CLIENT);
-            kcinit.setSecret("password");
             kcinit.setEnabled(true);
-            kcinit.addRedirectUri("urn:ietf:wg:oauth:2.0:oob");
-            kcinit.setPublicClient(false);
+            kcinit.addRedirectUri("http://localhost:*");
+            kcinit.setPublicClient(true);
 
             ClientModel app = realm.addClient(APP);
             app.setSecret("password");
@@ -154,9 +165,29 @@ public class KcinitTest extends AbstractTestRealmKeycloakTest {
             execution.setParentFlow(browser.getId());
             execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
             execution.setPriority(20);
-            execution.setAuthenticator(PassThroughAuthenticator.PROVIDER_ID);
+            execution.setAuthenticator(PushButtonAuthenticatorFactory.PROVIDER_ID);
+            realm.addAuthenticatorExecution(execution);
+
+            AuthenticationFlowModel browserBuiltin = realm.getFlowByAlias(DefaultAuthenticationFlows.BROWSER_FLOW);
+            AuthenticationFlowModel copy = AuthenticationManagementResource.copyFlow(realm, browserBuiltin, "copy-browser");
+            copy.setTopLevel(false);
+            realm.updateAuthenticationFlow(copy);
+            execution = new AuthenticationExecutionModel();
+            execution.setParentFlow(browser.getId());
+            execution.setRequirement(AuthenticationExecutionModel.Requirement.ALTERNATIVE);
+            execution.setFlowId(copy.getId());
+            execution.setPriority(30);
+            execution.setAuthenticatorFlow(true);
             realm.addAuthenticatorExecution(execution);
 
+            RequiredActionProviderModel action = new RequiredActionProviderModel();
+            action.setAlias("dummy");
+            action.setEnabled(true);
+            action.setProviderId(DummyRequiredActionFactory.PROVIDER_ID);
+            action.setName("dummy");
+            action = realm.addRequiredActionProvider(action);
+
+
         });
     }
 
@@ -183,8 +214,8 @@ public class KcinitTest extends AbstractTestRealmKeycloakTest {
     }
 
     @Test
-    public void testBrowserRequired() throws Exception {
-        // that that a browser require challenge is sent back if authentication flow doesn't support console display mode
+    public void testBrowserContinueAuthenticator() throws Exception {
+        // test that we can continue in the middle of a console login that doesn't support console display mode
         testingClient.server().run(session -> {
             RealmModel realm = session.realms().getRealmByName("test");
             ClientModel kcinit = realm.getClientByClientId(KCINIT_CLIENT);
@@ -193,29 +224,107 @@ public class KcinitTest extends AbstractTestRealmKeycloakTest {
 
 
         });
+        //Thread.sleep(100000000);
+
+        try {
+
+            testInstall();
+
+            KcinitExec exe = KcinitExec.newBuilder()
+                    .argsLine("login -f --fake-browser") // --fake-browser is a hidden command so that this test can execute
+                    .executeAsync();
+            exe.waitForStderr("Open browser and continue login? [y/n]");
+            exe.sendLine("y");
+            exe.waitForStdout("http://");
+
+            // the --fake-browser skips launching a browser and outputs url to stdout
+            String redirect = exe.stdoutString().trim();
+
+            //System.out.println("********************************");
+            //System.out.println("Redirect: " + redirect);
+
+            //redirect.replace("Browser required to complete login", "");
+
+            driver.navigate().to(redirect.trim());
+
+            Assert.assertEquals("PushTheButton", driver.getTitle());
+
+            // Push the button. I am redirected to username+password form
+            driver.findElement(By.name("submit1")).click();
+            //System.out.println("-----");
+            //System.out.println(driver.getPageSource());
+
+            //System.out.println(driver.getTitle());
+
+
 
+            loginPage.assertCurrent();
+
+            // Fill username+password. I am successfully authenticated
+            try {
+                oauth.fillLoginForm("wburke", "password");
+            } catch (Throwable e) {
+                e.printStackTrace();
+            }
+
+
+            String current = driver.getCurrentUrl();
+
+            exe.waitForStderr("Login successful");
+            exe.waitCompletion();
+            Assert.assertEquals(0, exe.exitCode());
+            Assert.assertTrue(driver.getPageSource().contains("Login Successful"));
+        } finally {
+
+            testingClient.server().run(session -> {
+                RealmModel realm = session.realms().getRealmByName("test");
+                ClientModel kcinit = realm.getClientByClientId(KCINIT_CLIENT);
+                kcinit.removeAuthenticationFlowBindingOverride(AuthenticationFlowBindings.BROWSER_BINDING);
+
+
+            });
+        }
+    }
+
+    @Test
+    public void testBrowserContinueRequiredAction() throws Exception {
+        testingClient.server().run(session -> {
+            RealmModel realm = session.realms().getRealmByName("test");
+            UserModel user = session.users().getUserByUsername("wburke", realm);
+            user.addRequiredAction("dummy");
+        });
         testInstall();
         // login
         //System.out.println("login....");
         KcinitExec exe = KcinitExec.newBuilder()
-                .argsLine("login")
+                .argsLine("login -f --fake-browser")
                 .executeAsync();
-        exe.waitCompletion();
-        Assert.assertEquals(1, exe.exitCode());
-        Assert.assertTrue(exe.stderrString().contains("Browser required to login"));
-        //Assert.assertEquals("stderr first line", "Browser required to login", exe.stderrLines().get(1));
+        //System.out.println(exe.stderrString());
+        exe.waitForStderr("Username:");
+        exe.sendLine("wburke");
+        //System.out.println(exe.stderrString());
+        exe.waitForStderr("Password:");
+        exe.sendLine("password");
 
+        exe.waitForStderr("Open browser and continue login? [y/n]");
+        exe.sendLine("y");
+        exe.waitForStdout("http://");
 
-        testingClient.server().run(session -> {
-            RealmModel realm = session.realms().getRealmByName("test");
-            ClientModel kcinit = realm.getClientByClientId(KCINIT_CLIENT);
-            kcinit.removeAuthenticationFlowBindingOverride(AuthenticationFlowBindings.BROWSER_BINDING);
+        // the --fake-browser skips launching a browser and outputs url to stdout
+        String redirect = exe.stdoutString().trim();
 
+        driver.navigate().to(redirect.trim());
 
-        });
+
+        //System.out.println(exe.stderrString());
+        exe.waitForStderr("Login successful");
+        exe.waitCompletion();
+        Assert.assertEquals(0, exe.exitCode());
+        Assert.assertTrue(driver.getPageSource().contains("Login Successful"));
     }
 
 
+
     @Test
     public void testBadCommand() throws Exception {
         KcinitExec exe = KcinitExec.execute("covfefe");
@@ -243,14 +352,36 @@ public class KcinitTest extends AbstractTestRealmKeycloakTest {
         exe.waitForStderr("client id [kcinit]:");
         exe.sendLine("");
         //System.out.println(exe.stderrString());
-        exe.waitForStderr("Client secret [none]:");
-        exe.sendLine("password");
+        exe.waitForStderr("secret [none]:");
+        exe.sendLine("");
         //System.out.println(exe.stderrString());
         exe.waitCompletion();
         Assert.assertEquals(0, exe.exitCode());
     }
 
     @Test
+    public void testOffline() throws Exception {
+        testInstall();
+        // login
+        //System.out.println("login....");
+        KcinitExec exe = KcinitExec.newBuilder()
+                .argsLine("login --offline")
+                .executeAsync();
+        //System.out.println(exe.stderrString());
+        exe.waitForStderr("Username:");
+        exe.sendLine("wburke");
+        //System.out.println(exe.stderrString());
+        exe.waitForStderr("Password:");
+        exe.sendLine("password");
+        //System.out.println(exe.stderrString());
+        exe.waitForStderr("Offline tokens not allowed for the user or client");
+        exe.waitCompletion();
+        Assert.assertEquals(1, exe.exitCode());
+    }
+
+
+
+        @Test
     public void testBasic() throws Exception {
         testInstall();
         // login
@@ -275,12 +406,6 @@ public class KcinitTest extends AbstractTestRealmKeycloakTest {
         Assert.assertEquals(1, exe.stdoutLines().size());
         String token = exe.stdoutLines().get(0).trim();
         //System.out.println("token: " + token);
-        String introspect = oauth.introspectAccessTokenWithClientCredential("kcinit", "password", token);
-        Map json = JsonSerialization.readValue(introspect, Map.class);
-        Assert.assertTrue(json.containsKey("active"));
-        Assert.assertTrue((Boolean)json.get("active"));
-        //System.out.println("introspect");
-        //System.out.println(introspect);
 
         exe = KcinitExec.execute("token app");
         Assert.assertEquals(0, exe.exitCode());
@@ -288,10 +413,6 @@ public class KcinitTest extends AbstractTestRealmKeycloakTest {
         String appToken = exe.stdoutLines().get(0).trim();
         Assert.assertFalse(appToken.equals(token));
         //System.out.println("token: " + token);
-        introspect = oauth.introspectAccessTokenWithClientCredential("kcinit", "password", appToken);
-        json = JsonSerialization.readValue(introspect, Map.class);
-        Assert.assertTrue(json.containsKey("active"));
-        Assert.assertTrue((Boolean)json.get("active"));
 
 
         exe = KcinitExec.execute("token badapp");
@@ -303,10 +424,6 @@ public class KcinitTest extends AbstractTestRealmKeycloakTest {
         exe = KcinitExec.execute("logout");
         Assert.assertEquals(0, exe.exitCode());
 
-        introspect = oauth.introspectAccessTokenWithClientCredential("kcinit", "password", token);
-        json = JsonSerialization.readValue(introspect, Map.class);
-        Assert.assertTrue(json.containsKey("active"));
-        Assert.assertFalse((Boolean)json.get("active"));
 
 
 
                diff --git a/themes/src/main/resources/theme/base/login/info.ftl b/themes/src/main/resources/theme/base/login/info.ftl
index ab8c567..8eff9c3 100755
--- a/themes/src/main/resources/theme/base/login/info.ftl
+++ b/themes/src/main/resources/theme/base/login/info.ftl
@@ -1,7 +1,11 @@
 <#import "template.ftl" as layout>
 <@layout.registrationLayout displayMessage=false; section>
     <#if section = "header">
+        <#if messageHeader??>
+        ${messageHeader}
+        <#else>
         ${message.summary}
+        </#if>
     <#elseif section = "form">
     <div id="kc-info-message">
         <p class="instruction">${message.summary}<#if requiredActions??><#list requiredActions>: <b><#items as reqActionItem>${msg("requiredAction.${reqActionItem}")}<#sep>, </#items></b></#list><#else></#if></p>
                diff --git a/themes/src/main/resources/theme/base/login/messages/messages_en.properties b/themes/src/main/resources/theme/base/login/messages/messages_en.properties
index 5f4c6a2..253a20b 100755
--- a/themes/src/main/resources/theme/base/login/messages/messages_en.properties
+++ b/themes/src/main/resources/theme/base/login/messages/messages_en.properties
@@ -36,6 +36,10 @@ codeSuccessTitle=Success code
 codeErrorTitle=Error code\: {0}
 displayUnsupported=Requested display type unsupported
 browserRequired=Browser required to login
+browserContinue=Browser required to complete login
+browserContinuePrompt=Open browser and continue login? [y/n]:
+browserContinueAnswer=y
+
 
 termsTitle=Terms and Conditions
 termsText=<p>Terms and conditions to be defined</p>
@@ -186,6 +190,11 @@ emailSendErrorMessage=Failed to send email, please try again later.
 accountUpdatedMessage=Your account has been updated.
 accountPasswordUpdatedMessage=Your password has been updated.
 
+delegationCompleteHeader=Login Successful
+delegationCompleteMessage=You may close this browser window and go back to your console application.
+delegationFailedHeader=Login Failed
+delegationFailedMessage=You may close this browser window and go back to your console application and try logging in again.
+
 noAccessMessage=No access
 
 invalidPasswordMinLengthMessage=Invalid password: minimum length {0}.