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 4f311c223f..501166722b 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("
Login completed.
");
- pw.println("This browser will remain logged in until you close it, logout, or the session expires.");
- pw.println("
");
- 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("Login attempt failed.
");
- pw.println("
");
- 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("Logout completed.
");
- pw.println("You may close this browser tab.");
- pw.println("
");
- 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("Logout failed.
");
- pw.println("You may close this browser tab.");
- pw.println("
");
- 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 21b5a6091a..3c4c2e639e 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/OIDCLoginProtocol.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java
index 56c002280d..148d840ace 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 160013fefb..6fa6705c85 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/protocol/oidc/endpoints/AuthorizationEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java
index 65c66e28e5..666cf3e2e2 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/services/managers/AuthenticationManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
index d6512d4f1f..87df91759f 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 425a88927f..5a825ccc51 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 ddfe91dd73..e7cd34de86 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 e58e9fb6d9..2d5ca5b7fa 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
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 ab8c567ff6..8eff9c3622 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">
${message.summary}<#if requiredActions??><#list requiredActions>: <#items as reqActionItem>${msg("requiredAction.${reqActionItem}")}<#sep>, #items>#list><#else>#if>
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 65f23c5d4a..253a20bcd5 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}.