This commit is contained in:
Bill Burke 2018-03-29 17:14:36 -04:00
parent 5d74b776d6
commit f4a5e49b63
15 changed files with 129 additions and 100 deletions

View file

@ -35,7 +35,7 @@ import javax.ws.rs.core.Response;
* should abort with an error message. * 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. * 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(); .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 * Build challenge response for required actions
@ -57,8 +78,8 @@ public class TextChallenge {
* @param context * @param context
* @return * @return
*/ */
public static TextChallenge challenge(RequiredActionContext context) { public static ConsoleDisplayMode challenge(RequiredActionContext context) {
return new TextChallenge(context); return new ConsoleDisplayMode(context);
} }
@ -68,8 +89,8 @@ public class TextChallenge {
* @param context * @param context
* @return * @return
*/ */
public static TextChallenge challenge(AuthenticationFlowContext context) { public static ConsoleDisplayMode challenge(AuthenticationFlowContext context) {
return new TextChallenge(context); return new ConsoleDisplayMode(context);
} }
/** /**
@ -79,7 +100,7 @@ public class TextChallenge {
* @return * @return
*/ */
public static HeaderBuilder header(RequiredActionContext context) { public static HeaderBuilder header(RequiredActionContext context) {
return new TextChallenge(context).header(); return new ConsoleDisplayMode(context).header();
} }
@ -90,14 +111,14 @@ public class TextChallenge {
* @return * @return
*/ */
public static HeaderBuilder header(AuthenticationFlowContext context) { 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; this.requiredActionContext = requiredActionContext;
} }
TextChallenge(AuthenticationFlowContext flowContext) { ConsoleDisplayMode(AuthenticationFlowContext flowContext) {
this.flowContext = flowContext; this.flowContext = flowContext;
} }
@ -278,20 +299,20 @@ public class TextChallenge {
return HeaderBuilder.this.build(); return HeaderBuilder.this.build();
} }
public TextChallenge challenge() { public ConsoleDisplayMode challenge() {
return TextChallenge.this; return ConsoleDisplayMode.this;
} }
public LoginFormsProvider form() { public LoginFormsProvider form() {
return TextChallenge.this.form(); return ConsoleDisplayMode.this.form();
} }
public Response message(String msg, String... params) { public Response message(String msg, String... params) {
return TextChallenge.this.message(msg, params); return ConsoleDisplayMode.this.message(msg, params);
} }
public Response text(String text) { public Response text(String text) {
return TextChallenge.this.text(text); return ConsoleDisplayMode.this.text(text);
} }

View file

@ -254,6 +254,19 @@ public class AuthenticationProcessor {
getAuthenticationSession().setAuthenticatedUser(null); 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 { public class Result implements AuthenticationFlowContext, ClientAuthenticationFlowContext {
AuthenticatorConfigModel authenticatorConfig; AuthenticatorConfigModel authenticatorConfig;
AuthenticationExecutionModel execution; AuthenticationExecutionModel execution;
@ -546,15 +559,7 @@ public class AuthenticationProcessor {
@Override @Override
public URI getRefreshUrl(boolean authSessionIdParam) { public URI getRefreshUrl(boolean authSessionIdParam) {
UriBuilder uriBuilder = LoginActionsService.loginActionsBaseUrl(getUriInfo()) return AuthenticationProcessor.this.getRefreshUrl(authSessionIdParam);
.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());
} }
@Override @Override

View file

@ -70,7 +70,9 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
} }
// todo create a provider for handling lack of display support // todo create a provider for handling lack of display support
if (OAuth2Constants.DISPLAY_CONSOLE.equalsIgnoreCase(display)) { 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 { } else {
return factory.create(processor.getSession()); return factory.create(processor.getSession());

View file

@ -65,6 +65,10 @@ public class RequiredActionContextResult implements RequiredActionContext {
this.factory = factory; this.factory = factory;
} }
public RequiredActionFactory getFactory() {
return factory;
}
@Override @Override
public EventBuilder getEvent() { public EventBuilder getEvent() {
return eventBuilder; return eventBuilder;
@ -170,7 +174,6 @@ public class RequiredActionContextResult implements RequiredActionContext {
uri = UriBuilder.fromUri(uri).queryParam(LoginActionsService.AUTH_SESSION_ID, getAuthenticationSession().getParentSession().getId()).build(); uri = UriBuilder.fromUri(uri).queryParam(LoginActionsService.AUTH_SESSION_ID, getAuthenticationSession().getParentSession().getId()).build();
} }
return uri; return uri;
} }
@Override @Override

View file

@ -19,7 +19,7 @@ package org.keycloak.authentication.authenticators.console;
import org.keycloak.authentication.AuthenticationFlowContext; import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.Authenticator; 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.authentication.authenticators.browser.OTPFormAuthenticator;
import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.CredentialRepresentation;
@ -37,8 +37,8 @@ public class ConsoleOTPFormAuthenticator extends OTPFormAuthenticator implements
return context.getActionUrl(context.generateAccessCode(), true); return context.getActionUrl(context.generateAccessCode(), true);
} }
protected TextChallenge challenge(AuthenticationFlowContext context) { protected ConsoleDisplayMode challenge(AuthenticationFlowContext context) {
return TextChallenge.challenge(context) return ConsoleDisplayMode.challenge(context)
.header() .header()
.param(CredentialRepresentation.TOTP) .param(CredentialRepresentation.TOTP)
.label("console-otp") .label("console-otp")

View file

@ -24,11 +24,8 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.services.messages.Messages; 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.MultivaluedMap;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import java.net.URI;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -43,8 +40,8 @@ public class ConsoleUsernamePasswordAuthenticator extends AbstractUsernameFormAu
return false; return false;
} }
protected TextChallenge challenge(AuthenticationFlowContext context) { protected ConsoleDisplayMode challenge(AuthenticationFlowContext context) {
return TextChallenge.challenge(context) return ConsoleDisplayMode.challenge(context)
.header() .header()
.param("username") .param("username")
.label("console-username") .label("console-username")

View file

@ -17,14 +17,10 @@
package org.keycloak.authentication.requiredactions; package org.keycloak.authentication.requiredactions;
import org.keycloak.Config;
import org.keycloak.authentication.RequiredActionContext; import org.keycloak.authentication.RequiredActionContext;
import org.keycloak.authentication.RequiredActionFactory;
import org.keycloak.authentication.RequiredActionProvider; import org.keycloak.authentication.RequiredActionProvider;
import org.keycloak.authentication.TextChallenge; import org.keycloak.authentication.ConsoleDisplayMode;
import org.keycloak.common.util.Time; import org.keycloak.common.util.Time;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import java.util.Arrays; import java.util.Arrays;
@ -45,7 +41,7 @@ public class ConsoleTermsAndConditions implements RequiredActionProvider {
@Override @Override
public void requiredActionChallenge(RequiredActionContext context) { public void requiredActionChallenge(RequiredActionContext context) {
Response challenge = TextChallenge.challenge(context) Response challenge = ConsoleDisplayMode.challenge(context)
.header() .header()
.param("accept") .param("accept")
.label("console-accept-terms") .label("console-accept-terms")

View file

@ -18,7 +18,6 @@
package org.keycloak.authentication.requiredactions; package org.keycloak.authentication.requiredactions;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.authentication.*; import org.keycloak.authentication.*;
import org.keycloak.events.Details; import org.keycloak.events.Details;
import org.keycloak.events.Errors; import org.keycloak.events.Errors;
@ -28,11 +27,7 @@ import org.keycloak.models.*;
import org.keycloak.services.messages.Messages; import org.keycloak.services.messages.Messages;
import org.keycloak.services.validation.Validation; 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.MultivaluedMap;
import javax.ws.rs.core.Response;
import java.net.URI;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -45,8 +40,8 @@ public class ConsoleUpdatePassword extends UpdatePassword implements RequiredAct
public static final String PASSWORD_NEW = "password-new"; public static final String PASSWORD_NEW = "password-new";
public static final String PASSWORD_CONFIRM = "password-confirm"; public static final String PASSWORD_CONFIRM = "password-confirm";
protected TextChallenge challenge(RequiredActionContext context) { protected ConsoleDisplayMode challenge(RequiredActionContext context) {
return TextChallenge.challenge(context) return ConsoleDisplayMode.challenge(context)
.header() .header()
.param(PASSWORD_NEW) .param(PASSWORD_NEW)
.label("console-new-password") .label("console-new-password")

View file

@ -17,28 +17,19 @@
package org.keycloak.authentication.requiredactions; package org.keycloak.authentication.requiredactions;
import org.keycloak.Config;
import org.keycloak.authentication.RequiredActionContext; import org.keycloak.authentication.RequiredActionContext;
import org.keycloak.authentication.RequiredActionFactory;
import org.keycloak.authentication.RequiredActionProvider; import org.keycloak.authentication.RequiredActionProvider;
import org.keycloak.authentication.TextChallenge; import org.keycloak.authentication.ConsoleDisplayMode;
import org.keycloak.events.EventBuilder; import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType; import org.keycloak.events.EventType;
import org.keycloak.forms.login.LoginFormsProvider;
import org.keycloak.forms.login.freemarker.model.TotpBean; 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.UserCredentialModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.CredentialValidation; import org.keycloak.models.utils.CredentialValidation;
import org.keycloak.services.messages.Messages; import org.keycloak.services.messages.Messages;
import org.keycloak.services.validation.Validation; 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.MultivaluedMap;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import java.net.URI;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -61,8 +52,8 @@ public class ConsoleUpdateTotp implements RequiredActionProvider {
context.challenge(challenge); context.challenge(challenge);
} }
protected TextChallenge challenge(RequiredActionContext context) { protected ConsoleDisplayMode challenge(RequiredActionContext context) {
return TextChallenge.challenge(context) return ConsoleDisplayMode.challenge(context)
.header() .header()
.param("totp") .param("totp")
.label("console-otp") .label("console-otp")

View file

@ -18,35 +18,24 @@
package org.keycloak.authentication.requiredactions; package org.keycloak.authentication.requiredactions;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.authentication.RequiredActionContext; import org.keycloak.authentication.RequiredActionContext;
import org.keycloak.authentication.RequiredActionFactory;
import org.keycloak.authentication.RequiredActionProvider; import org.keycloak.authentication.RequiredActionProvider;
import org.keycloak.authentication.TextChallenge; import org.keycloak.authentication.ConsoleDisplayMode;
import org.keycloak.authentication.actiontoken.verifyemail.VerifyEmailActionToken;
import org.keycloak.common.util.RandomString; import org.keycloak.common.util.RandomString;
import org.keycloak.common.util.Time;
import org.keycloak.email.EmailException; import org.keycloak.email.EmailException;
import org.keycloak.email.EmailTemplateProvider; import org.keycloak.email.EmailTemplateProvider;
import org.keycloak.events.Details; import org.keycloak.events.Details;
import org.keycloak.events.Errors; import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder; import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType; import org.keycloak.events.EventType;
import org.keycloak.forms.login.LoginFormsProvider;
import org.keycloak.models.*; import org.keycloak.models.*;
import org.keycloak.services.Urls;
import org.keycloak.services.messages.Messages; import org.keycloak.services.messages.Messages;
import org.keycloak.services.validation.Validation; import org.keycloak.services.validation.Validation;
import org.keycloak.sessions.AuthenticationSessionCompoundId;
import org.keycloak.sessions.AuthenticationSessionModel; import org.keycloak.sessions.AuthenticationSessionModel;
import javax.ws.rs.core.*; import javax.ws.rs.core.*;
import java.net.URI;
import java.text.MessageFormat;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -114,8 +103,8 @@ public class ConsoleVerifyEmail implements RequiredActionProvider {
} }
public static String EMAIL_CODE="email_code"; public static String EMAIL_CODE="email_code";
protected TextChallenge challenge(RequiredActionContext context) { protected ConsoleDisplayMode challenge(RequiredActionContext context) {
return TextChallenge.challenge(context) return ConsoleDisplayMode.challenge(context)
.header() .header()
.param(EMAIL_CODE) .param(EMAIL_CODE)
.label("console-email-code") .label("console-email-code")

View file

@ -966,21 +966,22 @@ public class AuthenticationManager {
authSession.setProtocolMappers(requestedProtocolMappers); authSession.setProtocolMappers(requestedProtocolMappers);
} }
public static RequiredActionProvider createRequiredAction(KeycloakSession session, RequiredActionFactory factory, AuthenticationSessionModel authSession) { public static RequiredActionProvider createRequiredAction(RequiredActionContextResult context) {
String display = authSession.getClientNote(OAuth2Constants.DISPLAY); String display = context.getAuthenticationSession().getClientNote(OAuth2Constants.DISPLAY);
if (display == null) return factory.create(session); if (display == null) return context.getFactory().create(context.getSession());
if (factory instanceof DisplayTypeRequiredActionFactory) { if (context.getFactory() instanceof DisplayTypeRequiredActionFactory) {
RequiredActionProvider provider = ((DisplayTypeRequiredActionFactory)factory).createDisplay(session, display); RequiredActionProvider provider = ((DisplayTypeRequiredActionFactory)context.getFactory()).createDisplay(context.getSession(), display);
if (provider != null) return provider; if (provider != null) return provider;
} }
// todo create a provider for handling lack of display support // todo create a provider for handling lack of display support
if (OAuth2Constants.DISPLAY_CONSOLE.equalsIgnoreCase(display)) { 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 { } else {
return factory.create(session); return context.getFactory().create(context.getSession());
} }
} }
@ -1002,16 +1003,16 @@ public class AuthenticationManager {
if (factory == null) { 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?"); 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; RequiredActionProvider actionProvider = null;
try { try {
actionProvider = createRequiredAction(session, factory, authSession); actionProvider = createRequiredAction(context);
} catch (AuthenticationFlowException e) { } catch (AuthenticationFlowException e) {
if (e.getResponse() != null) { if (e.getResponse() != null) {
return e.getResponse(); return e.getResponse();
} }
throw e; throw e;
} }
RequiredActionContextResult context = new RequiredActionContextResult(authSession, realm, event, session, request, user, factory);
actionProvider.requiredActionChallenge(context); actionProvider.requiredActionChallenge(context);
if (context.getStatus() == RequiredActionContext.Status.FAILURE) { if (context.getStatus() == RequiredActionContext.Status.FAILURE) {

View file

@ -929,9 +929,15 @@ public class LoginActionsService {
event.error(Errors.INVALID_CODE); event.error(Errors.INVALID_CODE);
throw new WebApplicationException(ErrorPage.error(session, authSession, Response.Status.BAD_REQUEST, Messages.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; RequiredActionProvider provider = null;
try { try {
provider = AuthenticationManager.createRequiredAction(session, factory, authSession); provider = AuthenticationManager.createRequiredAction(context);
} catch (AuthenticationFlowException e) { } catch (AuthenticationFlowException e) {
if (e.getResponse() != null) { if (e.getResponse() != null) {
return e.getResponse(); return e.getResponse();
@ -939,12 +945,6 @@ public class LoginActionsService {
throw new WebApplicationException(ErrorPage.error(session, authSession, Response.Status.BAD_REQUEST, Messages.DISPLAY_UNSUPPORTED)); 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; Response response;
provider.processAction(context); provider.processAction(context);

View file

@ -306,14 +306,7 @@ public class AuthenticationManagementResource {
logger.debug("flow not found: " + flowAlias); logger.debug("flow not found: " + flowAlias);
return Response.status(NOT_FOUND).build(); return Response.status(NOT_FOUND).build();
} }
AuthenticationFlowModel copy = new AuthenticationFlowModel(); AuthenticationFlowModel copy = copyFlow(realm, flow, newName);
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);
data.put("id", copy.getId()); data.put("id", copy.getId());
adminEvent.operation(OperationType.CREATE).resourcePath(uriInfo).representation(data).success(); 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())) { for (AuthenticationExecutionModel execution : realm.getAuthenticationExecutions(from.getId())) {
if (execution.isAuthenticatorFlow()) { if (execution.isAuthenticatorFlow()) {
AuthenticationFlowModel subFlow = realm.getAuthenticationFlowById(execution.getFlowId()); AuthenticationFlowModel subFlow = realm.getAuthenticationFlowById(execution.getFlowId());
@ -334,7 +339,7 @@ public class AuthenticationManagementResource {
copy.setTopLevel(false); copy.setTopLevel(false);
copy = realm.addAuthenticationFlow(copy); copy = realm.addAuthenticationFlow(copy);
execution.setFlowId(copy.getId()); execution.setFlowId(copy.getId());
copy(newName, subFlow, copy); copy(realm, newName, subFlow, copy);
} }
execution.setId(null); execution.setId(null);
execution.setParentFlow(to.getId()); execution.setParentFlow(to.getId());

View file

@ -31,14 +31,18 @@ import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.model.ResourceServer; import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.credential.CredentialModel; import org.keycloak.credential.CredentialModel;
import org.keycloak.models.*; import org.keycloak.models.*;
import org.keycloak.models.utils.DefaultAuthenticationFlows;
import org.keycloak.models.utils.TimeBasedOTP; import org.keycloak.models.utils.TimeBasedOTP;
import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.RequiredActionProviderRepresentation; import org.keycloak.representations.idm.RequiredActionProviderRepresentation;
import org.keycloak.representations.idm.authorization.ClientPolicyRepresentation; 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.AdminPermissionManagement;
import org.keycloak.services.resources.admin.permissions.AdminPermissions; import org.keycloak.services.resources.admin.permissions.AdminPermissions;
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest; import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
import org.keycloak.testsuite.AssertEvents; 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.forms.PassThroughAuthenticator;
import org.keycloak.testsuite.pages.AppPage; import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.ErrorPage; import org.keycloak.testsuite.pages.ErrorPage;
@ -98,6 +102,7 @@ public class KcinitTest extends AbstractTestRealmKeycloakTest {
kcinit.setSecret("password"); kcinit.setSecret("password");
kcinit.setEnabled(true); kcinit.setEnabled(true);
kcinit.addRedirectUri("urn:ietf:wg:oauth:2.0:oob"); kcinit.addRedirectUri("urn:ietf:wg:oauth:2.0:oob");
kcinit.addRedirectUri("http://localhost:*");
kcinit.setPublicClient(false); kcinit.setPublicClient(false);
ClientModel app = realm.addClient(APP); ClientModel app = realm.addClient(APP);
@ -154,13 +159,25 @@ public class KcinitTest extends AbstractTestRealmKeycloakTest {
execution.setParentFlow(browser.getId()); execution.setParentFlow(browser.getId());
execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED); execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
execution.setPriority(20); 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); realm.addAuthenticatorExecution(execution);
}); });
} }
//@Test @Test
public void testDemo() throws Exception { public void testDemo() throws Exception {
testingClient.server().run(session -> { testingClient.server().run(session -> {
RealmModel realm = session.realms().getRealmByName("test"); RealmModel realm = session.realms().getRealmByName("test");
@ -193,7 +210,9 @@ public class KcinitTest extends AbstractTestRealmKeycloakTest {
}); });
Thread.sleep(100000000);
/*
testInstall(); testInstall();
// login // login
//System.out.println("login...."); //System.out.println("login....");
@ -204,6 +223,7 @@ public class KcinitTest extends AbstractTestRealmKeycloakTest {
Assert.assertEquals(1, exe.exitCode()); Assert.assertEquals(1, exe.exitCode());
Assert.assertTrue(exe.stderrString().contains("Browser required to login")); Assert.assertTrue(exe.stderrString().contains("Browser required to login"));
//Assert.assertEquals("stderr first line", "Browser required to login", exe.stderrLines().get(1)); //Assert.assertEquals("stderr first line", "Browser required to login", exe.stderrLines().get(1));
*/
testingClient.server().run(session -> { testingClient.server().run(session -> {

View file

@ -36,6 +36,10 @@ codeSuccessTitle=Success code
codeErrorTitle=Error code\: {0} codeErrorTitle=Error code\: {0}
displayUnsupported=Requested display type unsupported displayUnsupported=Requested display type unsupported
browserRequired=Browser required to login 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 termsTitle=Terms and Conditions
termsText=<p>Terms and conditions to be defined</p> termsText=<p>Terms and conditions to be defined</p>