terms and conditions

This commit is contained in:
Bill Burke 2015-06-11 14:39:08 -04:00
parent 3dd282e11b
commit 3f62cd9271
26 changed files with 685 additions and 210 deletions

View file

@ -195,7 +195,7 @@ module.controller('UserListCtrl', function($scope, realm, User) {
module.controller('UserDetailCtrl', function($scope, realm, user, User, UserFederationInstances, $location, Dialog, Notifications) { module.controller('UserDetailCtrl', function($scope, realm, user, User, UserFederationInstances, RequiredActions, $location, Dialog, Notifications) {
$scope.realm = realm; $scope.realm = realm;
$scope.create = !user.id; $scope.create = !user.id;
$scope.editUsername = $scope.create || $scope.realm.editUsernameAllowed; $scope.editUsername = $scope.create || $scope.realm.editUsernameAllowed;
@ -219,14 +219,29 @@ module.controller('UserDetailCtrl', function($scope, realm, user, User, UserFede
} }
$scope.changed = false; // $scope.create; $scope.changed = false; // $scope.create;
if (user.requiredActions) {
for (var i = 0; i < user.requiredActions.length; i++) {
console.log("user require action: " + user.requiredActions[i]);
}
}
// ID - Name map for required actions. IDs are enum names. // ID - Name map for required actions. IDs are enum names.
$scope.userReqActionList = [ RequiredActions.query({id: realm.realm}, function(data) {
$scope.userReqActionList = [];
for (var i = 0; i < data.length; i++) {
console.log("listed required action: " + data[i].text);
item = { id: data[i].id, text: data[i].text };
$scope.userReqActionList.push(item);
}
});
/*[
{id: "VERIFY_EMAIL", text: "Verify Email"}, {id: "VERIFY_EMAIL", text: "Verify Email"},
{id: "UPDATE_PROFILE", text: "Update Profile"}, {id: "UPDATE_PROFILE", text: "Update Profile"},
{id: "CONFIGURE_TOTP", text: "Configure Totp"}, {id: "CONFIGURE_TOTP", text: "Configure Totp"},
{id: "UPDATE_PASSWORD", text: "Update Password"} {id: "UPDATE_PASSWORD", text: "Update Password"}
]; ];
*/
$scope.$watch('user', function() { $scope.$watch('user', function() {
if (!angular.equals($scope.user, user)) { if (!angular.equals($scope.user, user)) {

View file

@ -186,6 +186,12 @@ module.factory('RealmAdminEvents', function($resource) {
}); });
}); });
module.factory('RequiredActions', function($resource) {
return $resource(authUrl + '/admin/realms/:id/required-actions', {
id : '@realm'
});
});
module.factory('RealmLDAPConnectionTester', function($resource) { module.factory('RealmLDAPConnectionTester', function($resource) {
return $resource(authUrl + '/admin/realms/:realm/testLDAPConnection'); return $resource(authUrl + '/admin/realms/:realm/testLDAPConnection');
}); });

View file

@ -4,6 +4,8 @@ doCancel=Abbrechen
doSubmit=Absenden doSubmit=Absenden
doYes=Ja doYes=Ja
doNo=Nein doNo=Nein
doAccept=Accept
doDecline=Decline
doForgotPassword=Passwort vergessen? doForgotPassword=Passwort vergessen?
doClickHere=hier klicken doClickHere=hier klicken

View file

@ -4,6 +4,8 @@ doCancel=Cancel
doSubmit=Submit doSubmit=Submit
doYes=Yes doYes=Yes
doNo=No doNo=No
doAccept=Accept
doDecline=Decline
doForgotPassword=Forgot Password? doForgotPassword=Forgot Password?
doClickHere=Click here doClickHere=Click here
@ -22,6 +24,8 @@ emailForgotTitle=Forgot Your Password?
updatePasswordTitle=Update password updatePasswordTitle=Update password
codeSuccessTitle=Success code codeSuccessTitle=Success code
codeErrorTitle=Error code\: {0} codeErrorTitle=Error code\: {0}
termsTitle=Terms and Conditions
termsTitleHtml=Terms and Conditions
noAccount=New user? noAccount=New user?
username=Username username=Username

View file

@ -4,6 +4,8 @@ doCancel=Annulla
doSubmit=Invia doSubmit=Invia
doYes=Si doYes=Si
doNo=No doNo=No
doAccept=Accept
doDecline=Decline
doForgotPassword=Password Dimenticata? doForgotPassword=Password Dimenticata?
doClickHere=Clicca qui doClickHere=Clicca qui

View file

@ -3,6 +3,8 @@ doRegister=Cadastre-se
doCancel=Cancelar doCancel=Cancelar
doSubmit=Ok doSubmit=Ok
doYes=Sim doYes=Sim
doAccept=Accept
doDecline=Decline
doNo=N\u00E3o doNo=N\u00E3o
doForgotPassword=Esqueceu sua senha? doForgotPassword=Esqueceu sua senha?
doClickHere=Clique aqui doClickHere=Clique aqui

View file

@ -0,0 +1,29 @@
<#import "template.ftl" as layout>
<@layout.registrationLayout displayMessage=false; section>
<#if section = "title">
${msg("termsTitle")}
<#elseif section = "header">
${msg("termsTitleHtml")}
<#elseif section = "form">
<div id="kc-info-message">
<textarea class="${properties.kcTextareaClass!}" rows="20" cols="120">
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
</textarea>
<form class="form-actions" action="${requiredActionUrl("terms_and_conditions", "")}" method="POST">
<div class="${properties.kcFormGroupClass!}">
<div id="kc-form-buttons" class="${properties.kcFormButtonsClass!}">
<div class="${properties.kcFormButtonsWrapperClass!}">
<input class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonLargeClass!}" name="accept" id="kc-login" type="submit" value="${msg("doAccept")}"/>
<input class="${properties.kcButtonClass!} ${properties.kcButtonDefaultClass!} ${properties.kcButtonLargeClass!}" name="cancel" id="kc-cancel" type="submit" value="${msg("doDecline")}"/>
</div>
</div>
</div>
</form>
</div>
</#if>
</@layout.registrationLayout>

View file

@ -2,6 +2,7 @@ package org.keycloak.login;
import java.net.URI; import java.net.URI;
import java.util.List; import java.util.List;
import java.util.Map;
import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.MultivaluedMap;
@ -24,6 +25,8 @@ public interface LoginFormsProvider extends Provider {
public Response createResponse(UserModel.RequiredAction action); public Response createResponse(UserModel.RequiredAction action);
Response createForm(String form, Map<String, Object> attributes);
public Response createLogin(); public Response createLogin();
public Response createPasswordReset(); public Response createPasswordReset();

View file

@ -27,6 +27,7 @@ import org.keycloak.login.freemarker.model.OAuthGrantBean;
import org.keycloak.login.freemarker.model.ProfileBean; import org.keycloak.login.freemarker.model.ProfileBean;
import org.keycloak.login.freemarker.model.RealmBean; import org.keycloak.login.freemarker.model.RealmBean;
import org.keycloak.login.freemarker.model.RegisterBean; import org.keycloak.login.freemarker.model.RegisterBean;
import org.keycloak.login.freemarker.model.RequiredActionUrlFormatterMethod;
import org.keycloak.login.freemarker.model.TotpBean; import org.keycloak.login.freemarker.model.TotpBean;
import org.keycloak.login.freemarker.model.UrlBean; import org.keycloak.login.freemarker.model.UrlBean;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
@ -205,6 +206,7 @@ import java.util.concurrent.TimeUnit;
uriBuilder.replaceQuery(null); uriBuilder.replaceQuery(null);
} }
URI baseUri = uriBuilder.build(); URI baseUri = uriBuilder.build();
attributes.put("requiredActionUrl", new RequiredActionUrlFormatterMethod(realm, baseUri));
if (realm != null) { if (realm != null) {
attributes.put("realm", new RealmBean(realm)); attributes.put("realm", new RealmBean(realm));
@ -272,6 +274,98 @@ import java.util.concurrent.TimeUnit;
} }
} }
@Override
public Response createForm(String form, Map<String, Object> attributes) {
RealmModel realm = session.getContext().getRealm();
ClientModel client = session.getContext().getClient();
UriInfo uriInfo = session.getContext().getUri();
MultivaluedMap<String, String> queryParameterMap = queryParams != null ? queryParams : new MultivaluedMapImpl<String, String>();
String requestURI = uriInfo.getBaseUri().getPath();
UriBuilder uriBuilder = UriBuilder.fromUri(requestURI);
for (String k : queryParameterMap.keySet()) {
Object[] objects = queryParameterMap.get(k).toArray();
if (objects.length == 1 && objects[0] == null) continue; //
uriBuilder.replaceQueryParam(k, objects);
}
if (accessCode != null) {
uriBuilder.replaceQueryParam(OAuth2Constants.CODE, accessCode);
}
URI baseUri = uriBuilder.build();
ThemeProvider themeProvider = session.getProvider(ThemeProvider.class, "extending");
Theme theme;
try {
theme = themeProvider.getTheme(realm.getLoginTheme(), Theme.Type.LOGIN);
} catch (IOException e) {
logger.error("Failed to create theme", e);
return Response.serverError().build();
}
try {
attributes.put("properties", theme.getProperties());
} catch (IOException e) {
logger.warn("Failed to load properties", e);
}
Properties messagesBundle;
Locale locale = LocaleHelper.getLocale(realm, user, uriInfo, session.getContext().getRequestHeaders());
try {
messagesBundle = theme.getMessages(locale);
attributes.put("msg", new MessageFormatterMethod(locale, messagesBundle));
} catch (IOException e) {
logger.warn("Failed to load messages", e);
messagesBundle = new Properties();
}
MessagesPerFieldBean messagesPerField = new MessagesPerFieldBean();
if (messages != null) {
MessageBean wholeMessage = new MessageBean(null, messageType);
for (FormMessage message : this.messages) {
String formattedMessageText = formatMessage(message, messagesBundle, locale);
if (formattedMessageText != null) {
wholeMessage.appendSummaryLine(formattedMessageText);
messagesPerField.addMessage(message.getField(), formattedMessageText, messageType);
}
}
attributes.put("message", wholeMessage);
}
attributes.put("messagesPerField", messagesPerField);
if (status == null) {
status = Response.Status.OK;
}
if (realm != null) {
attributes.put("realm", new RealmBean(realm));
attributes.put("social", new IdentityProviderBean(realm, baseUri, uriInfo));
attributes.put("url", new UrlBean(realm, theme, baseUri, this.actionUri));
attributes.put("requiredActionUrl", new RequiredActionUrlFormatterMethod(realm, baseUri));
if (realm.isInternationalizationEnabled()) {
UriBuilder b = UriBuilder.fromUri(baseUri).path(uriInfo.getPath());
attributes.put("locale", new LocaleBean(realm, locale, b, messagesBundle));
}
}
try {
String result = freeMarker.processTemplate(attributes, form, theme);
Response.ResponseBuilder builder = Response.status(status).type(MediaType.TEXT_HTML).entity(result);
BrowserSecurityHeaderSetup.headers(builder, realm);
for (Map.Entry<String, String> entry : httpResponseHeaders.entrySet()) {
builder.header(entry.getKey(), entry.getValue());
}
LocaleHelper.updateLocaleCookie(builder, locale, realm, uriInfo, Urls.localeCookiePath(baseUri, realm.getName()));
return builder.build();
} catch (FreeMarkerException e) {
logger.error("Failed to process template", e);
return Response.serverError().build();
}
}
public Response createLogin() { public Response createLogin() {
return createResponse(LoginFormsPages.LOGIN); return createResponse(LoginFormsPages.LOGIN);
} }

View file

@ -0,0 +1,32 @@
package org.keycloak.login.freemarker.model;
import freemarker.template.TemplateMethodModelEx;
import freemarker.template.TemplateModelException;
import org.keycloak.models.RealmModel;
import org.keycloak.services.Urls;
import java.net.URI;
import java.text.MessageFormat;
import java.util.List;
import java.util.Locale;
import java.util.Properties;
/**
*/
public class RequiredActionUrlFormatterMethod implements TemplateMethodModelEx {
private final String realm;
private final URI baseUri;
public RequiredActionUrlFormatterMethod(RealmModel realm, URI baseUri) {
this.realm = realm.getName();
this.baseUri = baseUri;
}
@Override
public Object exec(List list) throws TemplateModelException {
String action = list.get(0).toString();
String relativePath = list.get(1).toString();
String url = Urls.requiredActionBase(baseUri).path(relativePath).build(realm, action).toString();
return url;
}
}

View file

@ -59,7 +59,8 @@ public class ModelToRepresentation {
rep.setFederationLink(user.getFederationLink()); rep.setFederationLink(user.getFederationLink());
List<String> reqActions = new ArrayList<String>(); List<String> reqActions = new ArrayList<String>();
for (String ra : user.getRequiredActions()){ Set<String> requiredActions = user.getRequiredActions();
for (String ra : requiredActions){
reqActions.add(ra); reqActions.add(ra);
} }

View file

@ -7,4 +7,5 @@ import org.keycloak.provider.ProviderFactory;
* @version $Revision: 1 $ * @version $Revision: 1 $
*/ */
public interface RequiredActionFactory extends ProviderFactory<RequiredActionProvider> { public interface RequiredActionFactory extends ProviderFactory<RequiredActionProvider> {
String getDisplayText();
} }

View file

@ -0,0 +1,115 @@
package org.keycloak.authentication.actions;
import org.keycloak.Config;
import org.keycloak.Version;
import org.keycloak.authentication.RequiredActionContext;
import org.keycloak.authentication.RequiredActionFactory;
import org.keycloak.authentication.RequiredActionProvider;
import org.keycloak.events.Errors;
import org.keycloak.freemarker.BrowserSecurityHeaderSetup;
import org.keycloak.freemarker.FreeMarkerException;
import org.keycloak.freemarker.FreeMarkerUtil;
import org.keycloak.freemarker.Theme;
import org.keycloak.freemarker.ThemeProvider;
import org.keycloak.login.LoginFormsProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.protocol.LoginProtocol;
import org.keycloak.services.Urls;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.ClientSessionCode;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.Map;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class TermsAndConditions implements RequiredActionProvider, RequiredActionFactory {
public static final String PROVIDER_ID = "terms_and_conditions";
public static class Resource {
public Resource(RequiredActionContext context) {
this.context = context;
}
protected RequiredActionContext context;
@POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public Response agree(final MultivaluedMap<String, String> formData) throws URISyntaxException, IOException, FreeMarkerException {
if (formData.containsKey("cancel")) {
LoginProtocol protocol = context.getSession().getProvider(LoginProtocol.class, context.getClientSession().getAuthMethod());
protocol.setRealm(context.getRealm())
.setHttpHeaders(context.getHttpRequest().getHttpHeaders())
.setUriInfo(context.getUriInfo());
context.getEvent().error(Errors.REJECTED_BY_USER);
return protocol.consentDenied(context.getClientSession());
}
context.getUser().removeRequiredAction(PROVIDER_ID);
return AuthenticationManager.nextActionAfterAuthentication(context.getSession(), context.getUserSession(), context.getClientSession(), context.getConnection(), context.getHttpRequest(), context.getUriInfo(), context.getEvent());
}
}
@Override
public RequiredActionProvider create(KeycloakSession session) {
return this;
}
@Override
public void init(Config.Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public String getId() {
return PROVIDER_ID;
}
@Override
public void evaluateTriggers(RequiredActionContext context) {
}
@Override
public Response invokeRequiredAction(RequiredActionContext context) {
return context.getSession().getProvider(LoginFormsProvider.class)
.setClientSessionCode(new ClientSessionCode(context.getRealm(), context.getClientSession()).getCode())
.createForm("terms.ftl", new HashMap<String, Object>());
}
@Override
public Object jaxrsService(RequiredActionContext context) {
return new Resource(context);
}
@Override
public String getDisplayText() {
return "Terms and Conditions";
}
@Override
public void close() {
}
}

View file

@ -86,6 +86,12 @@ public class UpdatePassword implements RequiredActionProvider, RequiredActionFac
} }
@Override
public String getDisplayText() {
return "Update Password";
}
@Override @Override
public String getId() { public String getId() {
return UserModel.RequiredAction.UPDATE_PASSWORD.name(); return UserModel.RequiredAction.UPDATE_PASSWORD.name();

View file

@ -68,6 +68,12 @@ public class UpdateProfile implements RequiredActionProvider, RequiredActionFact
} }
@Override
public String getDisplayText() {
return "Update Profile";
}
@Override @Override
public String getId() { public String getId() {
return UserModel.RequiredAction.UPDATE_PROFILE.name(); return UserModel.RequiredAction.UPDATE_PROFILE.name();

View file

@ -76,6 +76,12 @@ public class UpdateTotp implements RequiredActionProvider, RequiredActionFactory
} }
@Override
public String getDisplayText() {
return "Configure Totp";
}
@Override @Override
public String getId() { public String getId() {
return UserModel.RequiredAction.CONFIGURE_TOTP.name(); return UserModel.RequiredAction.CONFIGURE_TOTP.name();

View file

@ -96,6 +96,12 @@ public class VerifyEmail implements RequiredActionProvider, RequiredActionFactor
} }
@Override
public String getDisplayText() {
return "Verify Email";
}
@Override @Override
public String getId() { public String getId() {
return UserModel.RequiredAction.VERIFY_EMAIL.name(); return UserModel.RequiredAction.VERIFY_EMAIL.name();

View file

@ -128,15 +128,20 @@ public class Urls {
} }
public static URI loginActionUpdatePassword(URI baseUri, String realmId) { public static URI loginActionUpdatePassword(URI baseUri, String realmId) {
return requiredActionsBase(baseUri).path(LoginActionsService.class, "updatePassword").build(realmId); return loginActionsBase(baseUri).path(LoginActionsService.class, "updatePassword").build(realmId);
} }
public static URI loginActionUpdateTotp(URI baseUri, String realmId) { public static URI loginActionUpdateTotp(URI baseUri, String realmId) {
return requiredActionsBase(baseUri).path(LoginActionsService.class, "updateTotp").build(realmId); return loginActionsBase(baseUri).path(LoginActionsService.class, "updateTotp").build(realmId);
} }
public static UriBuilder requiredActionBase(URI baseUri) {
return loginActionsBase(baseUri).path(LoginActionsService.class, "requiredAction");
}
public static URI loginActionUpdateProfile(URI baseUri, String realmId) { public static URI loginActionUpdateProfile(URI baseUri, String realmId) {
return requiredActionsBase(baseUri).path(LoginActionsService.class, "updateProfile").build(realmId); return loginActionsBase(baseUri).path(LoginActionsService.class, "updateProfile").build(realmId);
} }
public static URI loginActionEmailVerification(URI baseUri, String realmId) { public static URI loginActionEmailVerification(URI baseUri, String realmId) {
@ -144,7 +149,7 @@ public class Urls {
} }
public static UriBuilder loginActionEmailVerificationBuilder(URI baseUri) { public static UriBuilder loginActionEmailVerificationBuilder(URI baseUri) {
return requiredActionsBase(baseUri).path(LoginActionsService.class, "emailVerification"); return loginActionsBase(baseUri).path(LoginActionsService.class, "emailVerification");
} }
public static URI loginPasswordReset(URI baseUri, String realmId) { public static URI loginPasswordReset(URI baseUri, String realmId) {
@ -152,7 +157,7 @@ public class Urls {
} }
public static UriBuilder loginPasswordResetBuilder(URI baseUri) { public static UriBuilder loginPasswordResetBuilder(URI baseUri) {
return requiredActionsBase(baseUri).path(LoginActionsService.class, "passwordReset"); return loginActionsBase(baseUri).path(LoginActionsService.class, "passwordReset");
} }
public static URI loginUsernameReminder(URI baseUri, String realmId) { public static URI loginUsernameReminder(URI baseUri, String realmId) {
@ -160,7 +165,7 @@ public class Urls {
} }
public static UriBuilder loginUsernameReminderBuilder(URI baseUri) { public static UriBuilder loginUsernameReminderBuilder(URI baseUri) {
return requiredActionsBase(baseUri).path(LoginActionsService.class, "usernameReminder"); return loginActionsBase(baseUri).path(LoginActionsService.class, "usernameReminder");
} }
public static String realmIssuer(URI baseUri, String realmId) { public static String realmIssuer(URI baseUri, String realmId) {
@ -172,11 +177,11 @@ public class Urls {
} }
public static URI realmLoginAction(URI baseUri, String realmId) { public static URI realmLoginAction(URI baseUri, String realmId) {
return requiredActionsBase(baseUri).path(LoginActionsService.class, "processLogin").build(realmId); return loginActionsBase(baseUri).path(LoginActionsService.class, "processLogin").build(realmId);
} }
public static URI realmLoginPage(URI baseUri, String realmId) { public static URI realmLoginPage(URI baseUri, String realmId) {
return requiredActionsBase(baseUri).path(LoginActionsService.class, "loginPage").build(realmId); return loginActionsBase(baseUri).path(LoginActionsService.class, "loginPage").build(realmId);
} }
private static UriBuilder realmLogout(URI baseUri) { private static UriBuilder realmLogout(URI baseUri) {
@ -184,11 +189,11 @@ public class Urls {
} }
public static URI realmRegisterAction(URI baseUri, String realmId) { public static URI realmRegisterAction(URI baseUri, String realmId) {
return requiredActionsBase(baseUri).path(LoginActionsService.class, "processRegister").build(realmId); return loginActionsBase(baseUri).path(LoginActionsService.class, "processRegister").build(realmId);
} }
public static URI realmRegisterPage(URI baseUri, String realmId) { public static URI realmRegisterPage(URI baseUri, String realmId) {
return requiredActionsBase(baseUri).path(LoginActionsService.class, "registerPage").build(realmId); return loginActionsBase(baseUri).path(LoginActionsService.class, "registerPage").build(realmId);
} }
public static URI realmInstalledAppUrnCallback(URI baseUri, String realmId) { public static URI realmInstalledAppUrnCallback(URI baseUri, String realmId) {
@ -196,7 +201,7 @@ public class Urls {
} }
public static URI realmOauthAction(URI baseUri, String realmId) { public static URI realmOauthAction(URI baseUri, String realmId) {
return requiredActionsBase(baseUri).path(LoginActionsService.class, "processConsent").build(realmId); return loginActionsBase(baseUri).path(LoginActionsService.class, "processConsent").build(realmId);
} }
public static String localeCookiePath(URI baseUri, String realmName){ public static String localeCookiePath(URI baseUri, String realmName){
@ -207,7 +212,7 @@ public class Urls {
return themeBase(baseUri).path(Version.RESOURCES_VERSION).build(); return themeBase(baseUri).path(Version.RESOURCES_VERSION).build();
} }
private static UriBuilder requiredActionsBase(URI baseUri) { private static UriBuilder loginActionsBase(URI baseUri) {
return realmBase(baseUri).path(RealmsResource.class, "getLoginActionsService"); return realmBase(baseUri).path(RealmsResource.class, "getLoginActionsService");
} }

View file

@ -25,6 +25,8 @@ import org.jboss.logging.Logger;
import org.jboss.resteasy.spi.HttpRequest; import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.ClientConnection; import org.keycloak.ClientConnection;
import org.keycloak.authentication.AuthenticationProcessor; import org.keycloak.authentication.AuthenticationProcessor;
import org.keycloak.authentication.RequiredActionContext;
import org.keycloak.authentication.RequiredActionProvider;
import org.keycloak.email.EmailException; import org.keycloak.email.EmailException;
import org.keycloak.email.EmailProvider; import org.keycloak.email.EmailProvider;
import org.keycloak.events.Details; import org.keycloak.events.Details;
@ -67,7 +69,9 @@ import javax.ws.rs.Consumes;
import javax.ws.rs.GET; import javax.ws.rs.GET;
import javax.ws.rs.POST; import javax.ws.rs.POST;
import javax.ws.rs.Path; import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.QueryParam; import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context; import javax.ws.rs.core.Context;
import javax.ws.rs.core.Cookie; import javax.ws.rs.core.Cookie;
import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.HttpHeaders;
@ -303,12 +307,8 @@ public class LoginActionsService {
event.detail(Details.CODE_ID, clientSession.getId()); event.detail(Details.CODE_ID, clientSession.getId());
if (!clientCode.isValid(ClientSessionModel.Action.AUTHENTICATE.name()) || clientSession.getUserSession() != null) { if (!clientCode.isValid(ClientSessionModel.Action.AUTHENTICATE.name()) || clientSession.getUserSession() != null) {
clientCode.setAction(ClientSessionModel.Action.AUTHENTICATE.name());
event.client(clientSession.getClient()).error(Errors.EXPIRED_CODE); event.client(clientSession.getClient()).error(Errors.EXPIRED_CODE);
return session.getProvider(LoginFormsProvider.class) return ErrorPage.error(session, Messages.EXPIRED_CODE);
.setError(Messages.EXPIRED_CODE)
.setClientSessionCode(clientCode.getCode())
.createLogin();
} }
ClientModel client = clientSession.getClient(); ClientModel client = clientSession.getClient();
@ -685,11 +685,11 @@ public class LoginActionsService {
} }
event.session(userSession); event.session(userSession);
if (formData.containsKey("cancel")) {
LoginProtocol protocol = session.getProvider(LoginProtocol.class, clientSession.getAuthMethod()); LoginProtocol protocol = session.getProvider(LoginProtocol.class, clientSession.getAuthMethod());
protocol.setRealm(realm) protocol.setRealm(realm)
.setHttpHeaders(headers) .setHttpHeaders(headers)
.setUriInfo(uriInfo); .setUriInfo(uriInfo);
if (formData.containsKey("cancel")) {
event.error(Errors.REJECTED_BY_USER); event.error(Errors.REJECTED_BY_USER);
return protocol.consentDenied(clientSession); return protocol.consentDenied(clientSession);
} }
@ -1075,4 +1075,111 @@ public class LoginActionsService {
} }
} }
} }
@Path("required-actions/{action}")
public Object requiredAction(@QueryParam("code") String code,
@PathParam("action") String action) {
event.event(EventType.LOGIN);
if (!checkSsl()) {
event.error(Errors.SSL_REQUIRED);
throw new WebApplicationException(ErrorPage.error(session, Messages.HTTPS_REQUIRED));
}
if (!realm.isEnabled()) {
event.error(Errors.REALM_DISABLED);
return ErrorPage.error(session, Messages.REALM_NOT_ENABLED);
}
ClientSessionCode clientCode = ClientSessionCode.parse(code, session, realm);
if (clientCode == null) {
event.error(Errors.INVALID_CODE);
throw new WebApplicationException(ErrorPage.error(session, Messages.INVALID_CODE));
}
final ClientSessionModel clientSession = clientCode.getClientSession();
event.detail(Details.CODE_ID, clientSession.getId());
/*
if (!clientCode.isValid(ClientSessionModel.Action.AUTHENTICATE.name()) || clientSession.getUserSession() != null) {
event.client(clientSession.getClient()).error(Errors.EXPIRED_CODE);
throw new WebApplicationException(ErrorPage.error(session, Messages.EXPIRED_CODE));
}
*/
ClientModel client = clientSession.getClient();
if (client == null) {
event.error(Errors.CLIENT_NOT_FOUND);
throw new WebApplicationException( ErrorPage.error(session, Messages.UNKNOWN_LOGIN_REQUESTER));
}
session.getContext().setClient(client);
if (!client.isEnabled()) {
event.error(Errors.CLIENT_NOT_FOUND);
throw new WebApplicationException( ErrorPage.error(session, Messages.LOGIN_REQUESTER_NOT_ENABLED));
}
if (action == null) {
logger.error("required action was null");
event.error(Errors.INVALID_CODE);
throw new WebApplicationException(ErrorPage.error(session, Messages.INVALID_CODE));
}
RequiredActionProvider provider = session.getProvider(RequiredActionProvider.class, action);
if (provider == null) {
logger.error("required action provider was null");
event.error(Errors.INVALID_CODE);
throw new WebApplicationException(ErrorPage.error(session, Messages.INVALID_CODE));
}
RequiredActionContext context = new RequiredActionContext() {
@Override
public EventBuilder getEvent() {
return event;
}
@Override
public UserModel getUser() {
return getUserSession().getUser();
}
@Override
public RealmModel getRealm() {
return realm;
}
@Override
public ClientSessionModel getClientSession() {
return clientSession;
}
@Override
public UserSessionModel getUserSession() {
return clientSession.getUserSession();
}
@Override
public ClientConnection getConnection() {
return clientConnection;
}
@Override
public UriInfo getUriInfo() {
return uriInfo;
}
@Override
public KeycloakSession getSession() {
return session;
}
@Override
public HttpRequest getHttpRequest() {
return request;
}
};
return provider.jaxrsService(context);
}
} }

View file

@ -4,6 +4,7 @@ import org.jboss.logging.Logger;
import org.jboss.resteasy.spi.NotFoundException; import org.jboss.resteasy.spi.NotFoundException;
import org.jboss.resteasy.spi.ResteasyProviderFactory; import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.keycloak.ClientConnection; import org.keycloak.ClientConnection;
import org.keycloak.authentication.RequiredActionProvider;
import org.keycloak.events.EventBuilder; import org.keycloak.events.EventBuilder;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.Constants; import org.keycloak.models.Constants;

View file

@ -5,6 +5,8 @@ import org.jboss.resteasy.annotations.cache.NoCache;
import org.jboss.resteasy.spi.NotFoundException; import org.jboss.resteasy.spi.NotFoundException;
import org.jboss.resteasy.spi.ResteasyProviderFactory; import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.keycloak.ClientConnection; import org.keycloak.ClientConnection;
import org.keycloak.authentication.RequiredActionFactory;
import org.keycloak.authentication.RequiredActionProvider;
import org.keycloak.events.Event; import org.keycloak.events.Event;
import org.keycloak.events.EventQuery; import org.keycloak.events.EventQuery;
import org.keycloak.events.EventStoreProvider; import org.keycloak.events.EventStoreProvider;
@ -24,6 +26,7 @@ import org.keycloak.models.cache.CacheUserProvider;
import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.models.utils.RepresentationToModel; import org.keycloak.models.utils.RepresentationToModel;
import org.keycloak.protocol.oidc.TokenManager; import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.representations.adapters.action.GlobalRequestResult; import org.keycloak.representations.adapters.action.GlobalRequestResult;
import org.keycloak.representations.idm.RealmEventsConfigRepresentation; import org.keycloak.representations.idm.RealmEventsConfigRepresentation;
import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RealmRepresentation;
@ -552,4 +555,19 @@ public class RealmAdminResource {
public IdentityProvidersResource getIdentityProviderResource() { public IdentityProvidersResource getIdentityProviderResource() {
return new IdentityProvidersResource(realm, session, this.auth, adminEvent); return new IdentityProvidersResource(realm, session, this.auth, adminEvent);
} }
@Path("required-actions")
@GET
@Produces(MediaType.APPLICATION_JSON)
public List<Map<String, String>> getRequiredActions() {
List<Map<String, String>> list = new LinkedList<>();
for (ProviderFactory factory : session.getKeycloakSessionFactory().getProviderFactories(RequiredActionProvider.class)) {
RequiredActionFactory actionFactory = (RequiredActionFactory)factory;
Map<String, String> data = new HashMap<>();
data.put("id", actionFactory.getId());
data.put("text", actionFactory.getDisplayText());
list.add(data);
}
return list;
}
} }

View file

@ -5,6 +5,8 @@ import org.jboss.resteasy.annotations.cache.NoCache;
import org.jboss.resteasy.spi.BadRequestException; import org.jboss.resteasy.spi.BadRequestException;
import org.jboss.resteasy.spi.NotFoundException; import org.jboss.resteasy.spi.NotFoundException;
import org.keycloak.ClientConnection; import org.keycloak.ClientConnection;
import org.keycloak.authentication.RequiredActionFactory;
import org.keycloak.authentication.RequiredActionProvider;
import org.keycloak.email.EmailException; import org.keycloak.email.EmailException;
import org.keycloak.email.EmailProvider; import org.keycloak.email.EmailProvider;
import org.keycloak.events.admin.OperationType; import org.keycloak.events.admin.OperationType;
@ -27,6 +29,7 @@ import org.keycloak.models.utils.RepresentationToModel;
import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.TokenManager; import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.protocol.oidc.utils.RedirectUtils; import org.keycloak.protocol.oidc.utils.RedirectUtils;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.representations.idm.ClientMappingsRepresentation; import org.keycloak.representations.idm.ClientMappingsRepresentation;
import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.FederatedIdentityRepresentation; import org.keycloak.representations.idm.FederatedIdentityRepresentation;
@ -200,11 +203,15 @@ public class UsersResource {
List<String> reqActions = rep.getRequiredActions(); List<String> reqActions = rep.getRequiredActions();
if (reqActions != null) { if (reqActions != null) {
for (UserModel.RequiredAction ra : UserModel.RequiredAction.values()) { Set<String> allActions = new HashSet<>();
if (reqActions.contains(ra.name())) { for (ProviderFactory factory : session.getKeycloakSessionFactory().getProviderFactories(RequiredActionProvider.class)) {
user.addRequiredAction(ra); allActions.add(factory.getId());
}
for (String action : allActions) {
if (reqActions.contains(action)) {
user.addRequiredAction(action);
} else { } else {
user.removeRequiredAction(ra); user.removeRequiredAction(action);
} }
} }
} }

View file

@ -2,3 +2,4 @@ org.keycloak.authentication.actions.UpdatePassword
org.keycloak.authentication.actions.UpdateProfile org.keycloak.authentication.actions.UpdateProfile
org.keycloak.authentication.actions.UpdateTotp org.keycloak.authentication.actions.UpdateTotp
org.keycloak.authentication.actions.VerifyEmail org.keycloak.authentication.actions.VerifyEmail
org.keycloak.authentication.actions.TermsAndConditions

View file

@ -40,6 +40,7 @@ import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.OAuthClient; import org.keycloak.testsuite.OAuthClient;
import org.keycloak.testsuite.pages.AppPage; import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.AppPage.RequestType; import org.keycloak.testsuite.pages.AppPage.RequestType;
import org.keycloak.testsuite.pages.ErrorPage;
import org.keycloak.testsuite.pages.LoginPage; import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.pages.LoginPasswordUpdatePage; import org.keycloak.testsuite.pages.LoginPasswordUpdatePage;
import org.keycloak.testsuite.rule.KeycloakRule; import org.keycloak.testsuite.rule.KeycloakRule;
@ -99,6 +100,9 @@ public class LoginTest {
@WebResource @WebResource
protected LoginPage loginPage; protected LoginPage loginPage;
@WebResource
protected ErrorPage errorPage;
@WebResource @WebResource
protected LoginPasswordUpdatePage updatePasswordPage; protected LoginPasswordUpdatePage updatePasswordPage;
@ -424,8 +428,10 @@ public class LoginTest {
Time.setOffset(5000); Time.setOffset(5000);
loginPage.login("login@test.com", "password"); loginPage.login("login@test.com", "password");
loginPage.assertCurrent(); //loginPage.assertCurrent();
Assert.assertEquals("Login timeout. Please login again.", loginPage.getError()); errorPage.assertCurrent();
//Assert.assertEquals("Login timeout. Please login again.", loginPage.getError());
events.expectLogin().user((String) null).session((String) null).error("expired_code").clearDetails().detail(Details.CODE_ID, AssertEvents.isCodeId()).assertEvent(); events.expectLogin().user((String) null).session((String) null).error("expired_code").clearDetails().detail(Details.CODE_ID, AssertEvents.isCodeId()).assertEvent();