keycloak-uncached
Changes
adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KeycloakInstalled.java 81(+19 -62)
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/DefaultAuthenticationFlow.java b/services/src/main/java/org/keycloak/authentication/DefaultAuthenticationFlow.java
index 21b5a60..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,7 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
         }
         // todo create a provider for handling lack of display support
         if (OAuth2Constants.DISPLAY_CONSOLE.equalsIgnoreCase(display)) {
-            processor.getAuthenticationSession().removeClientNote(OAuth2Constants.DISPLAY);
+            processor.getAuthenticationSession().removeAuthNote(OAuth2Constants.DISPLAY);
             throw new AuthenticationFlowException(AuthenticationFlowError.DISPLAY_NOT_SUPPORTED,
                     ConsoleDisplayMode.browserContinue(processor.getSession(), processor.getRefreshUrl(true).toString()));
 
                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 d6512d4..87df917 100755
--- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
@@ -967,7 +967,7 @@ public class AuthenticationManager {
     }
 
     public static RequiredActionProvider createRequiredAction(RequiredActionContextResult context) {
-        String display = context.getAuthenticationSession().getClientNote(OAuth2Constants.DISPLAY);
+        String display = context.getAuthenticationSession().getAuthNote(OAuth2Constants.DISPLAY);
         if (display == null) return context.getFactory().create(context.getSession());
 
 
@@ -977,7 +977,7 @@ public class AuthenticationManager {
         }
         // todo create a provider for handling lack of display support
         if (OAuth2Constants.DISPLAY_CONSOLE.equalsIgnoreCase(display)) {
-            context.getAuthenticationSession().removeClientNote(OAuth2Constants.DISPLAY);
+            context.getAuthenticationSession().removeAuthNote(OAuth2Constants.DISPLAY);
             throw new AuthenticationFlowException(AuthenticationFlowError.DISPLAY_NOT_SUPPORTED, ConsoleDisplayMode.browserContinue(context.getSession(), context.getUriInfo().getRequestUri().toString()));
 
         } else {
                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/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 e58e9fb..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
@@ -107,11 +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.addRedirectUri("http://localhost:*");
-            kcinit.setPublicClient(false);
+            kcinit.setPublicClient(true);
 
             ClientModel app = realm.addClient(APP);
             app.setSecret("password");
@@ -272,13 +270,10 @@ public class KcinitTest extends AbstractTestRealmKeycloakTest {
 
             String current = driver.getCurrentUrl();
 
-
-            Pattern codePattern = Pattern.compile("code=([^&]+)");
-            Matcher m = codePattern.matcher(current);
-            Assert.assertTrue(m.find());
             exe.waitForStderr("Login successful");
             exe.waitCompletion();
             Assert.assertEquals(0, exe.exitCode());
+            Assert.assertTrue(driver.getPageSource().contains("Login Successful"));
         } finally {
 
             testingClient.server().run(session -> {
@@ -325,6 +320,7 @@ public class KcinitTest extends AbstractTestRealmKeycloakTest {
         exe.waitForStderr("Login successful");
         exe.waitCompletion();
         Assert.assertEquals(0, exe.exitCode());
+        Assert.assertTrue(driver.getPageSource().contains("Login Successful"));
     }
 
 
@@ -356,16 +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
+        @Test
     public void testBasic() throws Exception {
         testInstall();
         // login
@@ -390,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());
@@ -403,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");
@@ -418,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 65f23c5..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
@@ -190,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}.