From f4a5e49b635d5e814d9fae6c6f392e28c4dad29d Mon Sep 17 00:00:00 2001 From: Bill Burke Date: Thu, 29 Mar 2018 17:14:36 -0400 Subject: [PATCH 1/4] initial --- ...Challenge.java => ConsoleDisplayMode.java} | 49 +++++++++++++------ .../AuthenticationProcessor.java | 23 +++++---- .../DefaultAuthenticationFlow.java | 4 +- .../RequiredActionContextResult.java | 5 +- .../console/ConsoleOTPFormAuthenticator.java | 6 +-- .../ConsoleUsernamePasswordAuthenticator.java | 7 +-- .../ConsoleTermsAndConditions.java | 8 +-- .../ConsoleUpdatePassword.java | 9 +--- .../requiredactions/ConsoleUpdateTotp.java | 15 ++---- .../requiredactions/ConsoleVerifyEmail.java | 17 ++----- .../managers/AuthenticationManager.java | 19 +++---- .../resources/LoginActionsService.java | 14 +++--- .../AuthenticationManagementResource.java | 25 ++++++---- .../keycloak/testsuite/cli/KcinitTest.java | 24 ++++++++- .../login/messages/messages_en.properties | 4 ++ 15 files changed, 129 insertions(+), 100 deletions(-) rename server-spi-private/src/main/java/org/keycloak/authentication/{TextChallenge.java => ConsoleDisplayMode.java} (82%) diff --git a/server-spi-private/src/main/java/org/keycloak/authentication/TextChallenge.java b/server-spi-private/src/main/java/org/keycloak/authentication/ConsoleDisplayMode.java similarity index 82% rename from server-spi-private/src/main/java/org/keycloak/authentication/TextChallenge.java rename to server-spi-private/src/main/java/org/keycloak/authentication/ConsoleDisplayMode.java index b1dc9a2d15..f0f31872c0 100644 --- a/server-spi-private/src/main/java/org/keycloak/authentication/TextChallenge.java +++ b/server-spi-private/src/main/java/org/keycloak/authentication/ConsoleDisplayMode.java @@ -35,7 +35,7 @@ import javax.ws.rs.core.Response; * should abort with an error message. * */ -public class TextChallenge { +public class ConsoleDisplayMode { /** * Browser is required to login. This will abort client from doing a console login. @@ -50,6 +50,27 @@ public class TextChallenge { .entity("\n" + session.getProvider(LoginFormsProvider.class).getMessage("browserRequired") + "\n").build(); } + /** + * Browser is required to continue login. This will prompt client on whether to continue with a browser or abort. + * + * @param session + * @param callback + * @return + */ + public static Response browserContinue(KeycloakSession session, String callback) { + String browserContinueMsg = session.getProvider(LoginFormsProvider.class).getMessage("browserContinue"); + String browserPrompt = session.getProvider(LoginFormsProvider.class).getMessage("browserContinuePrompt"); + String answer = session.getProvider(LoginFormsProvider.class).getMessage("browserContinueAnswer"); + + String header = "X-Text-Form-Challenge callback=\"" + callback + "\""; + header += " browserContinue=\"" + browserPrompt + "\" answer=\"" + answer + "\""; + return Response.status(Response.Status.UNAUTHORIZED) + .header("WWW-Authenticate", header) + .type(MediaType.TEXT_PLAIN) + .entity("\n" + browserContinueMsg + "\n").build(); + } + + /** * Build challenge response for required actions @@ -57,8 +78,8 @@ public class TextChallenge { * @param context * @return */ - public static TextChallenge challenge(RequiredActionContext context) { - return new TextChallenge(context); + public static ConsoleDisplayMode challenge(RequiredActionContext context) { + return new ConsoleDisplayMode(context); } @@ -68,8 +89,8 @@ public class TextChallenge { * @param context * @return */ - public static TextChallenge challenge(AuthenticationFlowContext context) { - return new TextChallenge(context); + public static ConsoleDisplayMode challenge(AuthenticationFlowContext context) { + return new ConsoleDisplayMode(context); } /** @@ -79,7 +100,7 @@ public class TextChallenge { * @return */ public static HeaderBuilder header(RequiredActionContext context) { - return new TextChallenge(context).header(); + return new ConsoleDisplayMode(context).header(); } @@ -90,14 +111,14 @@ public class TextChallenge { * @return */ public static HeaderBuilder header(AuthenticationFlowContext context) { - return new TextChallenge(context).header(); + return new ConsoleDisplayMode(context).header(); } - TextChallenge(RequiredActionContext requiredActionContext) { + ConsoleDisplayMode(RequiredActionContext requiredActionContext) { this.requiredActionContext = requiredActionContext; } - TextChallenge(AuthenticationFlowContext flowContext) { + ConsoleDisplayMode(AuthenticationFlowContext flowContext) { this.flowContext = flowContext; } @@ -278,20 +299,20 @@ public class TextChallenge { return HeaderBuilder.this.build(); } - public TextChallenge challenge() { - return TextChallenge.this; + public ConsoleDisplayMode challenge() { + return ConsoleDisplayMode.this; } public LoginFormsProvider form() { - return TextChallenge.this.form(); + return ConsoleDisplayMode.this.form(); } public Response message(String msg, String... params) { - return TextChallenge.this.message(msg, params); + return ConsoleDisplayMode.this.message(msg, params); } public Response text(String text) { - return TextChallenge.this.text(text); + return ConsoleDisplayMode.this.text(text); } diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java index db96f11e68..502ac6d9e2 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/DefaultAuthenticationFlow.java b/services/src/main/java/org/keycloak/authentication/DefaultAuthenticationFlow.java index ac0c5e1a4b..21b5a6091a 100755 --- a/services/src/main/java/org/keycloak/authentication/DefaultAuthenticationFlow.java +++ b/services/src/main/java/org/keycloak/authentication/DefaultAuthenticationFlow.java @@ -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().removeClientNote(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 38b9c2fe0e..f79323443a 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/authenticators/console/ConsoleOTPFormAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/console/ConsoleOTPFormAuthenticator.java index fff2c80e75..0335b17436 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 4595df58f1..720e4e59fd 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 Bill Burke @@ -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/requiredactions/ConsoleTermsAndConditions.java b/services/src/main/java/org/keycloak/authentication/requiredactions/ConsoleTermsAndConditions.java index 24c6938771..1db910dbe9 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 d499eadaaf..461007ab04 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 Bill Burke @@ -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 89ef89b6b1..32751b99e4 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 Bill Burke @@ -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 e136cebf67..e3c6ec5a30 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 Bill Burke @@ -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/services/managers/AuthenticationManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java index edb9b511bb..d6512d4f1f 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().getClientNote(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().removeClientNote(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/resources/LoginActionsService.java b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java index ef9525a96c..f517c7d162 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/services/src/main/java/org/keycloak/services/resources/admin/AuthenticationManagementResource.java b/services/src/main/java/org/keycloak/services/resources/admin/AuthenticationManagementResource.java index 09892cbf0f..f689f6e15c 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,14 +306,7 @@ public class AuthenticationManagementResource { logger.debug("flow not found: " + flowAlias); return Response.status(NOT_FOUND).build(); } - AuthenticationFlowModel copy = new AuthenticationFlowModel(); - copy.setAlias(newName); - copy.setDescription(flow.getDescription()); - copy.setProviderId(flow.getProviderId()); - copy.setBuiltIn(false); - copy.setTopLevel(flow.isTopLevel()); - copy = realm.addAuthenticationFlow(copy); - copy(newName, flow, copy); + AuthenticationFlowModel copy = copyFlow(realm, flow, newName); data.put("id", copy.getId()); adminEvent.operation(OperationType.CREATE).resourcePath(uriInfo).representation(data).success(); @@ -322,7 +315,19 @@ public class AuthenticationManagementResource { } - protected void copy(String newName, AuthenticationFlowModel from, AuthenticationFlowModel to) { + public static AuthenticationFlowModel copyFlow(RealmModel realm, AuthenticationFlowModel flow, String newName) { + AuthenticationFlowModel copy = new AuthenticationFlowModel(); + copy.setAlias(newName); + copy.setDescription(flow.getDescription()); + copy.setProviderId(flow.getProviderId()); + copy.setBuiltIn(false); + copy.setTopLevel(flow.isTopLevel()); + copy = realm.addAuthenticationFlow(copy); + copy(realm, newName, flow, copy); + return copy; + } + + 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/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 f23a63c4b2..0ff37f8ddf 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 @@ -31,14 +31,18 @@ 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.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; @@ -98,6 +102,7 @@ public class KcinitTest extends AbstractTestRealmKeycloakTest { kcinit.setSecret("password"); kcinit.setEnabled(true); kcinit.addRedirectUri("urn:ietf:wg:oauth:2.0:oob"); + kcinit.addRedirectUri("http://localhost:*"); kcinit.setPublicClient(false); ClientModel app = realm.addClient(APP); @@ -154,13 +159,25 @@ 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); }); } - //@Test + @Test public void testDemo() throws Exception { testingClient.server().run(session -> { RealmModel realm = session.realms().getRealmByName("test"); @@ -193,7 +210,9 @@ public class KcinitTest extends AbstractTestRealmKeycloakTest { }); + Thread.sleep(100000000); + /* testInstall(); // login //System.out.println("login...."); @@ -204,6 +223,7 @@ public class KcinitTest extends AbstractTestRealmKeycloakTest { 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)); + */ testingClient.server().run(session -> { 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 5f4c6a2f6d..65f23c5d4a 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=

Terms and conditions to be defined

From 06f32a47ecfa0aa0bca9b33ac454a6aedf16dffc Mon Sep 17 00:00:00 2001 From: Bill Burke Date: Fri, 30 Mar 2018 08:24:30 -0400 Subject: [PATCH 2/4] fake browser tests --- .../actions/DummyRequiredActionFactory.java | 23 ++- .../keycloak/testsuite/cli/KcinitTest.java | 133 +++++++++++++++--- 2 files changed, 136 insertions(+), 20 deletions(-) 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 fb2f74d418..600d6f7470 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/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 0ff37f8ddf..e58e9fb6d9 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,6 +25,7 @@ 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; @@ -41,6 +42,7 @@ import org.keycloak.services.resources.admin.permissions.AdminPermissionManageme 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; @@ -53,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; @@ -73,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) { } @@ -174,10 +182,18 @@ public class KcinitTest extends AbstractTestRealmKeycloakTest { 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); + + }); } - @Test + //@Test public void testDemo() throws Exception { testingClient.server().run(session -> { RealmModel realm = session.realms().getRealmByName("test"); @@ -200,8 +216,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); @@ -210,32 +226,109 @@ public class KcinitTest extends AbstractTestRealmKeycloakTest { }); - Thread.sleep(100000000); + //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(); + + + 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()); + } 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(); + //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://"); + + // 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(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)); - */ - - - testingClient.server().run(session -> { - RealmModel realm = session.realms().getRealmByName("test"); - ClientModel kcinit = realm.getClientByClientId(KCINIT_CLIENT); - kcinit.removeAuthenticationFlowBindingOverride(AuthenticationFlowBindings.BROWSER_BINDING); - - - }); + Assert.assertEquals(0, exe.exitCode()); } + @Test public void testBadCommand() throws Exception { KcinitExec exe = KcinitExec.execute("covfefe"); @@ -270,6 +363,8 @@ public class KcinitTest extends AbstractTestRealmKeycloakTest { Assert.assertEquals(0, exe.exitCode()); } + + @Test public void testBasic() throws Exception { testInstall(); From 4078e84fb6cf4bea647b015e029eafd6e68521f5 Mon Sep 17 00:00:00 2001 From: Bill Burke Date: Sat, 31 Mar 2018 10:16:44 -0400 Subject: [PATCH 3/4] server driven success page --- .../adapters/installed/KeycloakInstalled.java | 81 +++++-------------- .../DefaultAuthenticationFlow.java | 4 +- .../protocol/oidc/OIDCLoginProtocol.java | 1 - .../oidc/OIDCLoginProtocolService.java | 34 ++++++++ .../oidc/endpoints/AuthorizationEndpoint.java | 2 +- .../managers/AuthenticationManager.java | 4 +- .../keycloak/services/messages/Messages.java | 5 ++ .../testsuite/cli/exec/AbstractExec.java | 2 +- .../keycloak/testsuite/cli/KcinitTest.java | 52 ++++++------ .../main/resources/theme/base/login/info.ftl | 4 + .../login/messages/messages_en.properties | 5 ++ 11 files changed, 100 insertions(+), 94 deletions(-) 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}. From 04a72b96080428ad3c03850aa34b2eb4694f5ec9 Mon Sep 17 00:00:00 2001 From: Bill Burke Date: Sat, 31 Mar 2018 22:34:37 -0400 Subject: [PATCH 4/4] bump kcinit version tag --- testsuite/integration-arquillian/tests/base/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testsuite/integration-arquillian/tests/base/pom.xml b/testsuite/integration-arquillian/tests/base/pom.xml index 1e5d7552d4..754c6dad32 100644 --- a/testsuite/integration-arquillian/tests/base/pom.xml +++ b/testsuite/integration-arquillian/tests/base/pom.xml @@ -264,7 +264,7 @@ github.com/keycloak/kcinit ${project.build.directory}/gopath - 0.3 + 0.4