From 636b197c4cfd729e5540728b26a399ae1045e749 Mon Sep 17 00:00:00 2001 From: Stian Thorgersen Date: Wed, 18 Sep 2013 18:10:47 +0100 Subject: [PATCH] KEYCLOAK-32 Registration verification email --- .../idm/RealmRepresentation.java | 9 ++ .../resources/forms/verify-email.xhtml | 6 + .../keycloak/services/email/EmailSender.java | 60 +++++++++ .../managers/AuthenticationManager.java | 5 - .../services/managers/RealmManager.java | 4 + .../keycloak/services/models/RealmModel.java | 4 + .../keycloak/services/models/UserModel.java | 4 + .../models/picketlink/RealmAdapter.java | 11 ++ .../models/picketlink/UserAdapter.java | 13 ++ .../models/picketlink/mappings/RealmData.java | 9 ++ .../picketlink/mappings/RealmEntity.java | 10 ++ .../services/resources/AccountService.java | 76 +++++++---- .../services/resources/SaasService.java | 18 +-- .../services/resources/SocialResource.java | 3 +- .../services/resources/TokenService.java | 44 +++++-- .../services/resources/flows/Flows.java | 4 +- .../services/resources/flows/FormFlows.java | 13 +- .../services/resources/flows/OAuthFlows.java | 3 +- .../services/resources/flows/Pages.java | 2 + .../services/resources/flows/Urls.java | 2 +- .../managers/AuthenticationManagerTest.java | 5 +- .../java/org/keycloak/test/ImportTest.java | 2 + services/src/test/resources/testrealm.json | 1 + testsuite/pom.xml | 6 + .../RequiredActionEmailVerificationTest.java | 123 ++++++++++++++++++ .../testsuite/SystemPropertiesSetter.java | 55 ++++++++ .../src/test/resources/testrealm-email.json | 78 +++++++++++ .../web-properties-email-verfication.xml | 26 ++++ 28 files changed, 540 insertions(+), 56 deletions(-) create mode 100644 forms/src/main/resources/META-INF/resources/forms/verify-email.xhtml create mode 100644 testsuite/src/test/java/org/keycloak/testsuite/RequiredActionEmailVerificationTest.java create mode 100644 testsuite/src/test/java/org/keycloak/testsuite/SystemPropertiesSetter.java create mode 100755 testsuite/src/test/resources/testrealm-email.json create mode 100755 testsuite/src/test/resources/web-properties-email-verfication.xml diff --git a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java index e83248659b..d709fe257c 100755 --- a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java @@ -19,6 +19,7 @@ public class RealmRepresentation { protected boolean sslNotRequired; protected boolean cookieLoginAllowed; protected boolean registrationAllowed; + protected boolean verifyEmail; protected boolean social; protected boolean automaticRegistrationAfterSocialLogin; protected String privateKey; @@ -239,6 +240,14 @@ public class RealmRepresentation { this.registrationAllowed = registrationAllowed; } + public boolean isVerifyEmail() { + return verifyEmail; + } + + public void setVerifyEmail(boolean verifyEmail) { + this.verifyEmail = verifyEmail; + } + public boolean isSocial() { return social; } diff --git a/forms/src/main/resources/META-INF/resources/forms/verify-email.xhtml b/forms/src/main/resources/META-INF/resources/forms/verify-email.xhtml new file mode 100644 index 0000000000..a23e6c47cb --- /dev/null +++ b/forms/src/main/resources/META-INF/resources/forms/verify-email.xhtml @@ -0,0 +1,6 @@ + + + +Please verify your email address + + \ No newline at end of file diff --git a/services/src/main/java/org/keycloak/services/email/EmailSender.java b/services/src/main/java/org/keycloak/services/email/EmailSender.java index da91a851cc..8b86fcf665 100644 --- a/services/src/main/java/org/keycloak/services/email/EmailSender.java +++ b/services/src/main/java/org/keycloak/services/email/EmailSender.java @@ -1,6 +1,31 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2012, Red Hat, Inc., and individual contributors + * as indicated by the @author tags. See the copyright.txt file in the + * distribution for a full listing of individual contributors. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ package org.keycloak.services.email; +import java.net.URI; import java.util.Map.Entry; +import java.util.concurrent.TimeUnit; +import java.util.Date; +import java.util.List; import java.util.Properties; import javax.mail.Message; @@ -10,9 +35,23 @@ import javax.mail.Transport; import javax.mail.internet.AddressException; import javax.mail.internet.InternetAddress; import javax.mail.internet.MimeMessage; +import javax.ws.rs.core.UriBuilder; +import javax.ws.rs.core.UriInfo; +import org.jboss.resteasy.logging.Logger; +import org.keycloak.services.managers.AccessCodeEntry; +import org.keycloak.services.models.RealmModel; +import org.keycloak.services.models.UserModel; +import org.keycloak.services.resources.AccountService; +import org.keycloak.services.resources.flows.Urls; + +/** + * @author Stian Thorgersen + */ public class EmailSender { + private static final Logger log = Logger.getLogger(EmailSender.class); + private Properties properties; public EmailSender() { @@ -41,4 +80,25 @@ public class EmailSender { transport.sendMessage(msg, new InternetAddress[] { new InternetAddress(address) }); } + public void sendEmailVerification(UserModel user, RealmModel realm, String code, UriInfo uriInfo) { + UriBuilder builder = Urls.accountBase(uriInfo.getBaseUri()).path(AccountService.class, "processEmailVerification"); + for (Entry> e : uriInfo.getQueryParameters().entrySet()) { + builder.queryParam(e.getKey(), e.getValue().toArray()); + } + builder.queryParam("code", code); + + URI uri = builder.build(realm.getId()); + + StringBuilder sb = new StringBuilder(); + sb.append(uri.toString()); + sb.append("\n"); + sb.append("Expires in " + TimeUnit.SECONDS.toMinutes(realm.getAccessCodeLifespanUserAction())); + + try { + send(user.getEmail(), "Verify email", sb.toString()); + } catch (Exception e1) { + log.warn("Failed to send email verification"); + } + } + } 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 b17dfccb04..2874ef19ca 100755 --- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java +++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java @@ -225,11 +225,6 @@ public class AuthenticationManager { types.add(credential.getType()); } - if (types.contains(CredentialRepresentation.TOTP) && !user.isTotp()) { - user.addRequiredAction(RequiredAction.CONFIGURE_TOTP); - user.setStatus(Status.ACTIONS_REQUIRED); - } - if (types.contains(CredentialRepresentation.PASSWORD)) { String password = formData.getFirst(CredentialRepresentation.PASSWORD); if (password == null) { diff --git a/services/src/main/java/org/keycloak/services/managers/RealmManager.java b/services/src/main/java/org/keycloak/services/managers/RealmManager.java index 9a2d09877a..f6f836f5d7 100755 --- a/services/src/main/java/org/keycloak/services/managers/RealmManager.java +++ b/services/src/main/java/org/keycloak/services/managers/RealmManager.java @@ -75,6 +75,7 @@ public class RealmManager { realm.setSocial(rep.isSocial()); realm.setCookieLoginAllowed(rep.isCookieLoginAllowed()); realm.setRegistrationAllowed(rep.isRegistrationAllowed()); + realm.setVerifyEmail(rep.isVerifyEmail()); realm.setAutomaticRegistrationAfterSocialLogin(rep.isAutomaticRegistrationAfterSocialLogin()); realm.setSslNotRequired((rep.isSslNotRequired())); realm.setAccessCodeLifespan(rep.getAccessCodeLifespan()); @@ -113,6 +114,7 @@ public class RealmManager { newRealm.setSslNotRequired(rep.isSslNotRequired()); newRealm.setCookieLoginAllowed(rep.isCookieLoginAllowed()); newRealm.setRegistrationAllowed(rep.isRegistrationAllowed()); + newRealm.setVerifyEmail(rep.isVerifyEmail()); newRealm.setAutomaticRegistrationAfterSocialLogin(rep.isAutomaticRegistrationAfterSocialLogin()); if (rep.getPrivateKey() == null || rep.getPublicKey() == null) { generateRealmKeys(newRealm); @@ -269,6 +271,8 @@ public class RealmManager { rep.setSslNotRequired(realm.isSslNotRequired()); rep.setCookieLoginAllowed(realm.isCookieLoginAllowed()); rep.setPublicKey(realm.getPublicKeyPem()); + rep.setRegistrationAllowed(realm.isRegistrationAllowed()); + rep.setVerifyEmail(realm.isVerifyEmail()); rep.setTokenLifespan(realm.getTokenLifespan()); rep.setAccessCodeLifespan(realm.getAccessCodeLifespan()); rep.setAccessCodeLifespanUserAction(realm.getAccessCodeLifespanUserAction()); diff --git a/services/src/main/java/org/keycloak/services/models/RealmModel.java b/services/src/main/java/org/keycloak/services/models/RealmModel.java index 39a7fe500b..9eaf1f5a4d 100755 --- a/services/src/main/java/org/keycloak/services/models/RealmModel.java +++ b/services/src/main/java/org/keycloak/services/models/RealmModel.java @@ -35,6 +35,10 @@ public interface RealmModel { void setRegistrationAllowed(boolean registrationAllowed); + boolean isVerifyEmail(); + + void setVerifyEmail(boolean verifyEmail); + int getTokenLifespan(); void setTokenLifespan(int tokenLifespan); diff --git a/services/src/main/java/org/keycloak/services/models/UserModel.java b/services/src/main/java/org/keycloak/services/models/UserModel.java index 75850fa79c..e0813b74d0 100755 --- a/services/src/main/java/org/keycloak/services/models/UserModel.java +++ b/services/src/main/java/org/keycloak/services/models/UserModel.java @@ -44,6 +44,10 @@ public interface UserModel { void setEmail(String email); + boolean isEmailVerified(); + + void setEmailVerified(boolean verified); + void setTotp(boolean totp); public static enum Status { diff --git a/services/src/main/java/org/keycloak/services/models/picketlink/RealmAdapter.java b/services/src/main/java/org/keycloak/services/models/picketlink/RealmAdapter.java index 184df8a68f..718efe4fe7 100755 --- a/services/src/main/java/org/keycloak/services/models/picketlink/RealmAdapter.java +++ b/services/src/main/java/org/keycloak/services/models/picketlink/RealmAdapter.java @@ -166,6 +166,17 @@ public class RealmAdapter implements RealmModel { updateRealm(); } + @Override + public boolean isVerifyEmail() { + return realm.isVerifyEmail(); + } + + @Override + public void setVerifyEmail(boolean verifyEmail) { + realm.setVerifyEmail(verifyEmail); + updateRealm(); + } + @Override public int getTokenLifespan() { return realm.getTokenLifespan(); diff --git a/services/src/main/java/org/keycloak/services/models/picketlink/UserAdapter.java b/services/src/main/java/org/keycloak/services/models/picketlink/UserAdapter.java index 7fd2af0b4d..6ff4a969d9 100755 --- a/services/src/main/java/org/keycloak/services/models/picketlink/UserAdapter.java +++ b/services/src/main/java/org/keycloak/services/models/picketlink/UserAdapter.java @@ -17,6 +17,7 @@ import org.picketlink.idm.model.sample.User; * @version $Revision: 1 $ */ public class UserAdapter implements UserModel { + private static final String EMAIL_VERIFIED_ATTR = "emailVerified"; private static final String KEYCLOAK_TOTP_ATTR = "totpEnabled"; private static final String REQUIRED_ACTIONS_ATTR = "requiredActions"; private static final String STATUS_ATTR = "status"; @@ -96,6 +97,18 @@ public class UserAdapter implements UserModel { idm.update(user); } + @Override + public boolean isEmailVerified() { + Attribute a = user.getAttribute(EMAIL_VERIFIED_ATTR); + return a != null ? a.getValue() : false; + } + + @Override + public void setEmailVerified(boolean verified) { + user.setAttribute(new Attribute(EMAIL_VERIFIED_ATTR, verified)); + idm.update(user); + } + @Override public void setAttribute(String name, String value) { user.setAttribute(new Attribute(name, value)); diff --git a/services/src/main/java/org/keycloak/services/models/picketlink/mappings/RealmData.java b/services/src/main/java/org/keycloak/services/models/picketlink/mappings/RealmData.java index 8699de7a2e..0e58cefaa9 100755 --- a/services/src/main/java/org/keycloak/services/models/picketlink/mappings/RealmData.java +++ b/services/src/main/java/org/keycloak/services/models/picketlink/mappings/RealmData.java @@ -13,6 +13,7 @@ public class RealmData extends AbstractPartition { private boolean sslNotRequired; private boolean cookieLoginAllowed; private boolean registrationAllowed; + private boolean verifyEmail; private boolean social; private boolean automaticRegistrationAfterSocialLogin; private int tokenLifespan; @@ -92,6 +93,14 @@ public class RealmData extends AbstractPartition { this.registrationAllowed = registrationAllowed; } + public boolean isVerifyEmail() { + return verifyEmail; + } + + public void setVerifyEmail(boolean verifyEmail) { + this.verifyEmail = verifyEmail; + } + @AttributeProperty public int getTokenLifespan() { return tokenLifespan; diff --git a/services/src/main/java/org/keycloak/services/models/picketlink/mappings/RealmEntity.java b/services/src/main/java/org/keycloak/services/models/picketlink/mappings/RealmEntity.java index de6bcf6616..9ddcd35e70 100755 --- a/services/src/main/java/org/keycloak/services/models/picketlink/mappings/RealmEntity.java +++ b/services/src/main/java/org/keycloak/services/models/picketlink/mappings/RealmEntity.java @@ -35,6 +35,8 @@ public class RealmEntity implements Serializable { @AttributeValue private boolean registrationAllowed; @AttributeValue + private boolean verifyEmail; + @AttributeValue private boolean social; @AttributeValue private boolean automaticRegistrationAfterSocialLogin; @@ -102,6 +104,14 @@ public class RealmEntity implements Serializable { this.registrationAllowed = registrationAllowed; } + public boolean isVerifyEmail() { + return verifyEmail; + } + + public void setVerifyEmail(boolean verifyEmail) { + this.verifyEmail = verifyEmail; + } + public boolean isSocial() { return social; } diff --git a/services/src/main/java/org/keycloak/services/resources/AccountService.java b/services/src/main/java/org/keycloak/services/resources/AccountService.java index cd142c5305..c69c39560f 100755 --- a/services/src/main/java/org/keycloak/services/resources/AccountService.java +++ b/services/src/main/java/org/keycloak/services/resources/AccountService.java @@ -21,6 +21,20 @@ */ package org.keycloak.services.resources; +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Context; +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 javax.ws.rs.core.Response.Status; +import javax.ws.rs.core.UriInfo; +import javax.ws.rs.ext.Providers; + import org.jboss.resteasy.jose.jws.JWSInput; import org.jboss.resteasy.jose.jws.crypto.RSAProvider; import org.jboss.resteasy.spi.HttpRequest; @@ -38,14 +52,6 @@ import org.keycloak.services.resources.flows.FormFlows; import org.keycloak.services.validation.Validation; import org.picketlink.idm.credential.util.TimeBasedOTP; -import javax.ws.rs.Consumes; -import javax.ws.rs.GET; -import javax.ws.rs.POST; -import javax.ws.rs.Path; -import javax.ws.rs.core.*; -import javax.ws.rs.core.Response.Status; -import javax.ws.rs.ext.Providers; - /** * @author Stian Thorgersen */ @@ -79,7 +85,7 @@ public class AccountService { public Response accessPage() { UserModel user = getUserFromAuthManager(); if (user != null) { - return Flows.forms(realm, request).setUser(user).forwardToAccess(); + return Flows.forms(realm, request, uriInfo).setUser(user).forwardToAccess(); } else { return Response.status(Status.FORBIDDEN).build(); } @@ -99,7 +105,7 @@ public class AccountService { if (response != null) { return response; } else { - return Flows.forms(realm, request).setUser(user).forwardToAccount(); + return Flows.forms(realm, request, uriInfo).setUser(user).forwardToAccount(); } } else { return Response.status(Status.FORBIDDEN).build(); @@ -108,17 +114,19 @@ public class AccountService { private UserModel getUser(RequiredAction action) { if (uriInfo.getQueryParameters().containsKey(FormFlows.CODE)) { - AccessCodeEntry accessCodeEntry = getAccessCodeEntry(); + AccessCodeEntry accessCodeEntry = getAccessCodeEntry(uriInfo.getQueryParameters().getFirst(FormFlows.CODE)); if (accessCodeEntry == null) { return null; } - String loginName = accessCodeEntry.getUser().getLoginName(); UserModel user = realm.getUser(loginName); if (!user.getRequiredActions().contains(action)) { return null; } + if (!accessCodeEntry.getUser().getRequiredActions().contains(action)) { + return null; + } return user; } else { return getUserFromAuthManager(); @@ -129,9 +137,7 @@ public class AccountService { return authManager.authenticateIdentityCookie(realm, uriInfo, headers); } - private AccessCodeEntry getAccessCodeEntry() { - String code = uriInfo.getQueryParameters().getFirst(FormFlows.CODE); - + private AccessCodeEntry getAccessCodeEntry(String code) { JWSInput input = new JWSInput(code, providers); boolean verifiedCode = false; try { @@ -163,7 +169,7 @@ public class AccountService { public Response processTotpUpdate(final MultivaluedMap formData) { UserModel user = getUser(RequiredAction.CONFIGURE_TOTP); if (user != null) { - FormFlows forms = Flows.forms(realm, request); + FormFlows forms = Flows.forms(realm, request, uriInfo); String totp = formData.getFirst("totp"); String totpSecret = formData.getFirst("totpSecret"); @@ -193,7 +199,28 @@ public class AccountService { if (response != null) { return response; } else { - return Flows.forms(realm, request).setUser(user).forwardToTotp(); + return Flows.forms(realm, request, uriInfo).setUser(user).forwardToTotp(); + } + } else { + return Response.status(Status.FORBIDDEN).build(); + } + } + + @Path("email-verify") + @GET + public Response processEmailVerification(@QueryParam("code") String code) { + AccessCodeEntry accessCodeEntry = getAccessCodeEntry(code); + String loginName = accessCodeEntry.getUser().getLoginName(); + UserModel user = realm.getUser(loginName); + if (user != null) { + user.setEmailVerified(true); + user.removeRequiredAction(UserModel.RequiredAction.VERIFY_EMAIL); + + Response response = redirectOauth(); + if (response != null) { + return response; + } else { + return Flows.forms(realm, request, uriInfo).setUser(user).forwardToVerifyEmail(); } } else { return Response.status(Status.FORBIDDEN).build(); @@ -203,7 +230,7 @@ public class AccountService { private Response redirectOauth() { String redirect = uriInfo.getQueryParameters().getFirst("redirect_uri"); if (redirect != null) { - AccessCodeEntry accessCode = getAccessCodeEntry(); + AccessCodeEntry accessCode = getAccessCodeEntry(uriInfo.getQueryParameters().getFirst(FormFlows.CODE)); String state = uriInfo.getQueryParameters().getFirst("state"); return Flows.oauth(realm, request, uriInfo, authManager, tokenManager).redirectAccessCode(accessCode, state, redirect); @@ -218,7 +245,7 @@ public class AccountService { public Response processPasswordUpdate(final MultivaluedMap formData) { UserModel user = getUser(RequiredAction.RESET_PASSWORD); if (user != null) { - FormFlows forms = Flows.forms(realm, request).setUser(user); + FormFlows forms = Flows.forms(realm, request, uriInfo).setUser(user); String password = formData.getFirst("password"); String passwordNew = formData.getFirst("password-new"); @@ -250,7 +277,7 @@ public class AccountService { if (response != null) { return response; } else { - return Flows.forms(realm, request).setUser(user).forwardToPassword(); + return Flows.forms(realm, request, uriInfo).setUser(user).forwardToPassword(); } } else { return Response.status(Status.FORBIDDEN).build(); @@ -262,7 +289,7 @@ public class AccountService { public Response accountPage() { UserModel user = getUserFromAuthManager(); if (user != null) { - return Flows.forms(realm, request).setUser(user).forwardToAccount(); + return Flows.forms(realm, request, uriInfo).setUser(user).forwardToAccount(); } else { return Response.status(Status.FORBIDDEN).build(); } @@ -273,7 +300,7 @@ public class AccountService { public Response socialPage() { UserModel user = getUserFromAuthManager(); if (user != null) { - return Flows.forms(realm, request).setUser(user).forwardToSocial(); + return Flows.forms(realm, request, uriInfo).setUser(user).forwardToSocial(); } else { return Response.status(Status.FORBIDDEN).build(); } @@ -284,7 +311,7 @@ public class AccountService { public Response totpPage() { UserModel user = getUserFromAuthManager(); if (user != null) { - return Flows.forms(realm, request).setUser(user).forwardToTotp(); + return Flows.forms(realm, request, uriInfo).setUser(user).forwardToTotp(); } else { return Response.status(Status.FORBIDDEN).build(); } @@ -295,9 +322,10 @@ public class AccountService { public Response passwordPage() { UserModel user = getUserFromAuthManager(); if (user != null) { - return Flows.forms(realm, request).setUser(user).forwardToPassword(); + return Flows.forms(realm, request, uriInfo).setUser(user).forwardToPassword(); } else { return Response.status(Status.FORBIDDEN).build(); } } + } diff --git a/services/src/main/java/org/keycloak/services/resources/SaasService.java b/services/src/main/java/org/keycloak/services/resources/SaasService.java index 2ded492717..12156c3994 100755 --- a/services/src/main/java/org/keycloak/services/resources/SaasService.java +++ b/services/src/main/java/org/keycloak/services/resources/SaasService.java @@ -170,7 +170,7 @@ public class SaasService { RealmModel realm = realmManager.defaultRealm(); authManager.expireSaasIdentityCookie(uriInfo); - Flows.forms(realm, request).forwardToLogin(); + Flows.forms(realm, request, uriInfo).forwardToLogin(); } @Path("registrations") @@ -181,7 +181,7 @@ public class SaasService { RealmModel realm = realmManager.defaultRealm(); authManager.expireSaasIdentityCookie(uriInfo); - Flows.forms(realm, request).forwardToRegistration(); + Flows.forms(realm, request, uriInfo).forwardToRegistration(); } @Path("logout") @@ -192,7 +192,7 @@ public class SaasService { RealmModel realm = realmManager.defaultRealm(); authManager.expireSaasIdentityCookie(uriInfo); - Flows.forms(realm, request).forwardToLogin(); + Flows.forms(realm, request, uriInfo).forwardToLogin(); } @Path("logout-cookie") @@ -226,11 +226,13 @@ public class SaasService { NewCookie cookie = authManager.createSaasIdentityCookie(realm, user, uriInfo); return Response.status(302).cookie(cookie).location(contextRoot(uriInfo).path(adminPath).build()).build(); case ACCOUNT_DISABLED: - return Flows.forms(realm, request).setError(Messages.ACCOUNT_DISABLED).setFormData(formData).forwardToLogin(); + return Flows.forms(realm, request, uriInfo).setError(Messages.ACCOUNT_DISABLED).setFormData(formData) + .forwardToLogin(); case ACTIONS_REQUIRED: - return Flows.forms(realm, request).forwardToAction(user.getRequiredActions().get(0)); + return Flows.forms(realm, request, uriInfo).forwardToAction(user.getRequiredActions().get(0)); default: - return Flows.forms(realm, request).setError(Messages.INVALID_USER).setFormData(formData).forwardToLogin(); + return Flows.forms(realm, request, uriInfo).setError(Messages.INVALID_USER).setFormData(formData) + .forwardToLogin(); } } @@ -262,7 +264,7 @@ public class SaasService { String error = Validation.validateRegistrationForm(formData, requiredCredentialTypes); if (error != null) { - return Flows.forms(defaultRealm, request).setError(error).setFormData(formData) + return Flows.forms(defaultRealm, request, uriInfo).setError(error).setFormData(formData) .forwardToRegistration(); } @@ -304,7 +306,7 @@ public class SaasService { UserModel user = registerMe(defaultRealm, newUser); if (user == null) { - return Flows.forms(defaultRealm, request).setError(Messages.USERNAME_EXISTS) + return Flows.forms(defaultRealm, request, uriInfo).setError(Messages.USERNAME_EXISTS) .setFormData(formData).forwardToRegistration(); } diff --git a/services/src/main/java/org/keycloak/services/resources/SocialResource.java b/services/src/main/java/org/keycloak/services/resources/SocialResource.java index b260262093..18e3da6452 100755 --- a/services/src/main/java/org/keycloak/services/resources/SocialResource.java +++ b/services/src/main/java/org/keycloak/services/resources/SocialResource.java @@ -201,7 +201,8 @@ public class SocialResource { cookiePath, null, "Added social cookie", NewCookie.DEFAULT_MAX_AGE, secureOnly); response.addNewCookie(newCookie); - return Flows.forms(realm, request).setFormData(formData).setSocialRegistration(true).forwardToRegistration(); + return Flows.forms(realm, request, uriInfo).setFormData(formData).setSocialRegistration(true) + .forwardToRegistration(); } } diff --git a/services/src/main/java/org/keycloak/services/resources/TokenService.java b/services/src/main/java/org/keycloak/services/resources/TokenService.java index 2e95b0fd82..2df6a7ca6d 100755 --- a/services/src/main/java/org/keycloak/services/resources/TokenService.java +++ b/services/src/main/java/org/keycloak/services/resources/TokenService.java @@ -11,7 +11,6 @@ import org.jboss.resteasy.spi.HttpResponse; import org.keycloak.representations.AccessTokenResponse; import org.keycloak.representations.SkeletonKeyToken; import org.keycloak.representations.idm.CredentialRepresentation; -import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.services.managers.AccessCodeEntry; import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationManager.AuthenticationStatus; @@ -20,13 +19,13 @@ import org.keycloak.services.managers.ResourceAdminManager; import org.keycloak.services.managers.TokenManager; import org.keycloak.services.messages.Messages; import org.keycloak.services.models.*; +import org.keycloak.services.models.UserModel.RequiredAction; +import org.keycloak.services.models.UserModel.Status; import org.keycloak.services.resources.flows.Flows; import org.keycloak.services.resources.flows.OAuthFlows; import org.keycloak.services.validation.Validation; -import org.picketlink.idm.credential.util.TimeBasedOTP; import javax.ws.rs.Consumes; -import javax.ws.rs.ForbiddenException; import javax.ws.rs.GET; import javax.ws.rs.NotAuthorizedException; import javax.ws.rs.POST; @@ -37,7 +36,6 @@ import javax.ws.rs.core.Context; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedMap; -import javax.ws.rs.core.NewCookie; import javax.ws.rs.core.Response; import javax.ws.rs.core.SecurityContext; import javax.ws.rs.core.UriBuilder; @@ -45,7 +43,6 @@ import javax.ws.rs.core.UriInfo; import javax.ws.rs.ext.Providers; import java.security.PrivateKey; -import java.util.Arrays; import java.util.HashMap; import java.util.LinkedList; import java.util.List; @@ -197,6 +194,9 @@ public class TokenService { String username = formData.getFirst("username"); UserModel user = realm.getUser(username); + isTotpConfigurationRequired(user); + isEmailVerificationRequired(user); + AuthenticationStatus status = authManager.authenticateForm(realm, user, formData); switch (status) { @@ -204,11 +204,31 @@ public class TokenService { case ACTIONS_REQUIRED: return oauth.processAccessCode(scopeParam, state, redirect, client, user); case ACCOUNT_DISABLED: - return Flows.forms(realm, request).setError(Messages.ACCOUNT_DISABLED).setFormData(formData).forwardToLogin(); + return Flows.forms(realm, request, uriInfo).setError(Messages.ACCOUNT_DISABLED).setFormData(formData) + .forwardToLogin(); case MISSING_TOTP: - return Flows.forms(realm, request).setFormData(formData).forwardToLoginTotp(); + return Flows.forms(realm, request, uriInfo).setFormData(formData).forwardToLoginTotp(); default: - return Flows.forms(realm, request).setError(Messages.INVALID_USER).setFormData(formData).forwardToLogin(); + return Flows.forms(realm, request, uriInfo).setError(Messages.INVALID_USER).setFormData(formData) + .forwardToLogin(); + } + } + + private void isTotpConfigurationRequired(UserModel user) { + for (RequiredCredentialModel c : realm.getRequiredCredentials()) { + if (c.getType().equals(CredentialRepresentation.TOTP) && !user.isTotp()) { + user.addRequiredAction(RequiredAction.CONFIGURE_TOTP); + user.setStatus(Status.ACTIONS_REQUIRED); + logger.info("User is required to configure totp"); + } + } + } + + private void isEmailVerificationRequired(UserModel user) { + if (realm.isVerifyEmail() && !user.isEmailVerified()) { + user.addRequiredAction(RequiredAction.VERIFY_EMAIL); + user.setStatus(Status.ACTIONS_REQUIRED); + logger.info("User is required to verify email"); } } @@ -256,7 +276,7 @@ public class TokenService { String error = Validation.validateRegistrationForm(formData, requiredCredentialTypes); if (error != null) { - return Flows.forms(realm, request).setError(error).setFormData(formData) + return Flows.forms(realm, request, uriInfo).setError(error).setFormData(formData) .setSocialRegistration(isSocialRegistration).forwardToRegistration(); } @@ -264,7 +284,7 @@ public class TokenService { UserModel user = realm.getUser(username); if (user != null) { - return Flows.forms(realm, request).setError(Messages.USERNAME_EXISTS).setFormData(formData) + return Flows.forms(realm, request, uriInfo).setError(Messages.USERNAME_EXISTS).setFormData(formData) .setSocialRegistration(isSocialRegistration).forwardToRegistration(); } @@ -473,7 +493,7 @@ public class TokenService { return oauth.processAccessCode(scopeParam, state, redirect, client, user); } - return Flows.forms(realm, request).forwardToLogin(); + return Flows.forms(realm, request, uriInfo).forwardToLogin(); } @Path("registrations") @@ -501,7 +521,7 @@ public class TokenService { authManager.expireIdentityCookie(realm, uriInfo); - return Flows.forms(realm, request).forwardToRegistration(); + return Flows.forms(realm, request, uriInfo).forwardToRegistration(); } @Path("logout") diff --git a/services/src/main/java/org/keycloak/services/resources/flows/Flows.java b/services/src/main/java/org/keycloak/services/resources/flows/Flows.java index e7ad5894b7..a466f8490a 100755 --- a/services/src/main/java/org/keycloak/services/resources/flows/Flows.java +++ b/services/src/main/java/org/keycloak/services/resources/flows/Flows.java @@ -40,8 +40,8 @@ public class Flows { return new PageFlows(request); } - public static FormFlows forms(RealmModel realm, HttpRequest request) { - return new FormFlows(realm, request); + public static FormFlows forms(RealmModel realm, HttpRequest request, UriInfo uriInfo) { + return new FormFlows(realm, request, uriInfo); } public static OAuthFlows oauth(RealmModel realm, HttpRequest request, UriInfo uriInfo, AuthenticationManager authManager, diff --git a/services/src/main/java/org/keycloak/services/resources/flows/FormFlows.java b/services/src/main/java/org/keycloak/services/resources/flows/FormFlows.java index 662b0a6a47..7b08e5c390 100755 --- a/services/src/main/java/org/keycloak/services/resources/flows/FormFlows.java +++ b/services/src/main/java/org/keycloak/services/resources/flows/FormFlows.java @@ -22,6 +22,7 @@ package org.keycloak.services.resources.flows; import org.jboss.resteasy.spi.HttpRequest; +import org.keycloak.services.email.EmailSender; import org.keycloak.services.models.RealmModel; import org.keycloak.services.models.UserModel; import org.keycloak.services.models.UserModel.RequiredAction; @@ -29,6 +30,7 @@ import org.picketlink.idm.model.sample.Realm; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; /** * @author Stian Thorgersen @@ -52,10 +54,12 @@ public class FormFlows { private boolean socialRegistration; private String code; + private UriInfo uriInfo; - FormFlows(RealmModel realm, HttpRequest request) { + FormFlows(RealmModel realm, HttpRequest request, UriInfo uriInfo) { this.realm = realm; this.request = request; + this.uriInfo = uriInfo; } public Response forwardToAction(RequiredAction action) { @@ -66,6 +70,8 @@ public class FormFlows { return forwardToAccount(); case RESET_PASSWORD: return forwardToPassword(); + case VERIFY_EMAIL: + return forwardToVerifyEmail(); default: return null; // TODO } @@ -128,6 +134,11 @@ public class FormFlows { return forwardToForm(Pages.TOTP); } + public Response forwardToVerifyEmail() { + new EmailSender().sendEmailVerification(userModel, realm, code, uriInfo); + return forwardToForm(Pages.VERIFY_EMAIL); + } + public FormFlows setCode(String code) { this.code = code; return this; diff --git a/services/src/main/java/org/keycloak/services/resources/flows/OAuthFlows.java b/services/src/main/java/org/keycloak/services/resources/flows/OAuthFlows.java index 3339d01223..66503e2f56 100755 --- a/services/src/main/java/org/keycloak/services/resources/flows/OAuthFlows.java +++ b/services/src/main/java/org/keycloak/services/resources/flows/OAuthFlows.java @@ -95,7 +95,8 @@ public class OAuthFlows { if (user.getRequiredActions() != null) { accessCode.setExpiration(System.currentTimeMillis() / 1000 + realm.getAccessCodeLifespanUserAction()); - return Flows.forms(realm, request).setCode(accessCode.getCode()).setUser(user).forwardToAction(user.getRequiredActions().get(0)); + return Flows.forms(realm, request, uriInfo).setCode(accessCode.getCode()).setUser(user) + .forwardToAction(user.getRequiredActions().get(0)); } if (redirect != null) { diff --git a/services/src/main/java/org/keycloak/services/resources/flows/Pages.java b/services/src/main/java/org/keycloak/services/resources/flows/Pages.java index 806ed63592..a1e770ed51 100644 --- a/services/src/main/java/org/keycloak/services/resources/flows/Pages.java +++ b/services/src/main/java/org/keycloak/services/resources/flows/Pages.java @@ -46,4 +46,6 @@ public class Pages { public final static String TOTP = "/forms/totp.xhtml"; + public final static String VERIFY_EMAIL = "/forms/verify-email.xhtml"; + } diff --git a/services/src/main/java/org/keycloak/services/resources/flows/Urls.java b/services/src/main/java/org/keycloak/services/resources/flows/Urls.java index 62b9e561ff..b797ff41c6 100755 --- a/services/src/main/java/org/keycloak/services/resources/flows/Urls.java +++ b/services/src/main/java/org/keycloak/services/resources/flows/Urls.java @@ -35,7 +35,7 @@ public class Urls { return accountBase(baseUri).path(AccountService.class, "accessPage").build(realmId); } - private static UriBuilder accountBase(URI baseUri) { + public static UriBuilder accountBase(URI baseUri) { return realmBase(baseUri).path(RealmsResource.class, "getAccountService"); } diff --git a/services/src/test/java/org/keycloak/services/managers/AuthenticationManagerTest.java b/services/src/test/java/org/keycloak/services/managers/AuthenticationManagerTest.java index e332e3dc04..ad3149cc76 100644 --- a/services/src/test/java/org/keycloak/services/managers/AuthenticationManagerTest.java +++ b/services/src/test/java/org/keycloak/services/managers/AuthenticationManagerTest.java @@ -16,6 +16,7 @@ import org.keycloak.services.models.KeycloakSessionFactory; import org.keycloak.services.models.RealmModel; import org.keycloak.services.models.UserCredentialModel; import org.keycloak.services.models.UserModel; +import org.keycloak.services.models.UserModel.RequiredAction; import org.keycloak.services.models.UserModel.Status; import org.keycloak.services.resources.KeycloakApplication; import org.picketlink.idm.credential.util.TimeBasedOTP; @@ -62,8 +63,10 @@ public class AuthenticationManagerTest { } @Test - public void authFormMissingTotp() { + public void authFormRequiredAction() { realm.addRequiredCredential(CredentialRepresentation.TOTP); + user.addRequiredAction(RequiredAction.CONFIGURE_TOTP); + user.setStatus(Status.ACTIONS_REQUIRED); AuthenticationStatus status = am.authenticateForm(realm, user, formData); Assert.assertEquals(AuthenticationStatus.ACTIONS_REQUIRED, status); diff --git a/services/src/test/java/org/keycloak/test/ImportTest.java b/services/src/test/java/org/keycloak/test/ImportTest.java index 38eb5e8c9c..1a5f0a00b7 100755 --- a/services/src/test/java/org/keycloak/test/ImportTest.java +++ b/services/src/test/java/org/keycloak/test/ImportTest.java @@ -72,6 +72,8 @@ public class ImportTest { manager.importRealm(rep, realm); realm.addRealmAdmin(admin); + Assert.assertTrue(realm.isVerifyEmail()); + Assert.assertFalse(realm.isAutomaticRegistrationAfterSocialLogin()); List creds = realm.getRequiredCredentials(); Assert.assertEquals(1, creds.size()); diff --git a/services/src/test/resources/testrealm.json b/services/src/test/resources/testrealm.json index 9caf6bba54..2549914d65 100755 --- a/services/src/test/resources/testrealm.json +++ b/services/src/test/resources/testrealm.json @@ -8,6 +8,7 @@ "requiredApplicationCredentials": [ "password" ], "requiredOAuthClientCredentials": [ "password" ], "defaultRoles": [ "foo", "bar" ], + "verifyEmail" : "true", "users": [ { "username": "wburke", diff --git a/testsuite/pom.xml b/testsuite/pom.xml index 946ec3e909..d2bb611cc1 100755 --- a/testsuite/pom.xml +++ b/testsuite/pom.xml @@ -136,6 +136,12 @@ shrinkwrap-resolver-impl-maven test + + + com.icegreen + greenmail + test + diff --git a/testsuite/src/test/java/org/keycloak/testsuite/RequiredActionEmailVerificationTest.java b/testsuite/src/test/java/org/keycloak/testsuite/RequiredActionEmailVerificationTest.java new file mode 100644 index 0000000000..afc308e061 --- /dev/null +++ b/testsuite/src/test/java/org/keycloak/testsuite/RequiredActionEmailVerificationTest.java @@ -0,0 +1,123 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2012, Red Hat, Inc., and individual contributors + * as indicated by the @author tags. See the copyright.txt file in the + * distribution for a full listing of individual contributors. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.keycloak.testsuite; + +import java.io.IOException; + +import javax.mail.MessagingException; +import javax.mail.internet.MimeMessage; + +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.drone.api.annotation.Drone; +import org.jboss.arquillian.graphene.page.Page; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.keycloak.testsuite.pages.AppPage; +import org.keycloak.testsuite.pages.LoginPage; +import org.keycloak.testsuite.pages.RegisterPage; +import org.openqa.selenium.WebDriver; + +import com.icegreen.greenmail.util.GreenMail; +import com.icegreen.greenmail.util.ServerSetup; + +/** + * @author Stian Thorgersen + */ +@RunWith(Arquillian.class) +public class RequiredActionEmailVerificationTest { + + @Deployment(name = "app", testable = false, order = 3) + public static WebArchive appDeployment() { + return Deployments.appDeployment(); + } + + @Deployment(name = "auth-server", testable = false, order = 2) + public static WebArchive deployment() { + return Deployments.deployment().addAsResource("testrealm-email.json", "META-INF/testrealm.json"); + } + + @Deployment(name = "properties", testable = false, order = 1) + public static WebArchive propertiesDeployment() { + return ShrinkWrap.create(WebArchive.class, "properties.war").addClass(SystemPropertiesSetter.class) + .addAsWebInfResource("web-properties-email-verfication.xml", "web.xml"); + } + + @Page + protected AppPage appPage; + + @Drone + protected WebDriver browser; + + @Page + protected LoginPage loginPage; + + @Page + protected RegisterPage registerPage; + + private GreenMail greenMail; + + @Before + public void before() { + ServerSetup setup = new ServerSetup(3025, "localhost", "smtp"); + + greenMail = new GreenMail(setup); + greenMail.start(); + } + + @After + public void after() { + appPage.open(); + if (appPage.isCurrent()) { + appPage.logout(); + } + if (greenMail != null) { + greenMail.stop(); + } + } + + @Test + public void verifyEmail() throws IOException, MessagingException { + appPage.open(); + + loginPage.register(); + registerPage.register("name", "email", "verifyEmail", "password", "password"); + + Assert.assertTrue(browser.getPageSource().contains("Please verify your email address")); + + MimeMessage message = greenMail.getReceivedMessages()[0]; + + String body = (String) message.getContent(); + String verificationUrl = body.split("\n")[0]; + + browser.navigate().to(verificationUrl.trim()); + + Assert.assertTrue(appPage.isCurrent()); + Assert.assertEquals("verifyEmail", appPage.getUser()); + } + +} diff --git a/testsuite/src/test/java/org/keycloak/testsuite/SystemPropertiesSetter.java b/testsuite/src/test/java/org/keycloak/testsuite/SystemPropertiesSetter.java new file mode 100644 index 0000000000..d9e68c9266 --- /dev/null +++ b/testsuite/src/test/java/org/keycloak/testsuite/SystemPropertiesSetter.java @@ -0,0 +1,55 @@ +package org.keycloak.testsuite; + +import java.io.IOException; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; + +import javax.servlet.Servlet; +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; + +public class SystemPropertiesSetter implements Servlet { + + @Override + public void destroy() { + for (Entry e : original.entrySet()) { + System.setProperty(e.getKey(), e.getValue()); + System.out.println("RESET " + e.getKey() + "=" + e.getValue()); + } + } + + @Override + public ServletConfig getServletConfig() { + return null; + } + + @Override + public String getServletInfo() { + return null; + } + + private Map original = new HashMap(); + + @Override + public void init(ServletConfig arg0) throws ServletException { + Enumeration n = arg0.getInitParameterNames(); + while (n.hasMoreElements()) { + String k = n.nextElement(); + String v = arg0.getInitParameter(k); + + original.put(k, v); + + System.setProperty(k, v); + System.out.println("SET " + k + "=" + v); + } + } + + @Override + public void service(ServletRequest arg0, ServletResponse arg1) throws ServletException, IOException { + } + +} diff --git a/testsuite/src/test/resources/testrealm-email.json b/testsuite/src/test/resources/testrealm-email.json new file mode 100755 index 0000000000..afc48e0cbb --- /dev/null +++ b/testsuite/src/test/resources/testrealm-email.json @@ -0,0 +1,78 @@ +{ + "realm": "demo", + "enabled": true, + "tokenLifespan": 300, + "accessCodeLifespan": 10, + "accessCodeLifespanUserAction": 600, + "sslNotRequired": true, + "cookieLoginAllowed": true, + "registrationAllowed": true, + "verifyEmail": true, + "privateKey": "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=", + "publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", + "requiredCredentials": [ "password" ], + "requiredApplicationCredentials": [ "password" ], + "requiredOAuthClientCredentials": [ "password" ], + "defaultRoles": [ "user" ], + "users" : [ + { + "username" : "bburke@redhat.com", + "status": "ENABLED", + "attributes" : { + "email" : "bburke@redhat.com" + }, + "credentials" : [ + { "type" : "password", + "value" : "password" } + ] + }, + { + "username" : "third-party", + "status": "ENABLED", + "credentials" : [ + { "type" : "password", + "value" : "password" } + ] + } + ], + "roles": [ + { + "name": "user", + "description": "Have User privileges" + }, + { + "name": "admin", + "description": "Have Administrator privileges" + } + ], + "roleMappings": [ + { + "username": "bburke@redhat.com", + "roles": ["user"] + }, + { + "username": "third-party", + "roles": ["KEYCLOAK_IDENTITY_REQUESTER"] + } + ], + "scopeMappings": [ + { + "username": "third-party", + "roles": ["user"] + } + ], + "applications": [ + { + "name": "customer-portal", + "enabled": true, + "adminUrl": "http://localhost:8080/app/j_admin_request", + "useRealmMappings": true, + "credentials": [ + { + "type": "password", + "value": "password" + } + ] + } + ] +} \ No newline at end of file diff --git a/testsuite/src/test/resources/web-properties-email-verfication.xml b/testsuite/src/test/resources/web-properties-email-verfication.xml new file mode 100755 index 0000000000..10896305d0 --- /dev/null +++ b/testsuite/src/test/resources/web-properties-email-verfication.xml @@ -0,0 +1,26 @@ + + + + properties + + + SystemPropertiesSetter + org.keycloak.testsuite.SystemPropertiesSetter + + keycloak.mail.smtp.from + auto@keycloak.org + + + keycloak.mail.smtp.host + localhost + + + keycloak.mail.smtp.port + 3025 + + 1 + + +