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} + <#elseif section = "form">

${message.summary}<#if requiredActions??><#list requiredActions>: <#items as reqActionItem>${msg("requiredAction.${reqActionItem}")}<#sep>, <#else>

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}.