From 89ca52e96009450c796cb721cffa22c633cbdf3f Mon Sep 17 00:00:00 2001 From: vrockai Date: Fri, 18 Oct 2013 14:40:05 +0200 Subject: [PATCH 01/11] KEYCLOAK-108 add warning alerts to req action forms --- .../{ErrorBean.java => MessageBean.java} | 16 +++--- .../org/keycloak/service/FormServiceImpl.java | 41 +++++++-------- .../theme/default/css/login-register.css | 3 -- .../resources/forms/theme/default/error.ftl | 2 +- .../forms/theme/default/login-config-totp.ftl | 5 -- .../theme/default/login-reset-password.ftl | 9 ---- .../theme/default/login-verify-email.ftl | 5 -- .../theme/default/template-login-action.ftl | 30 +++++++---- .../forms/theme/default/template-login.ftl | 4 +- .../org/keycloak/forms/messages.properties | 7 +++ .../org/keycloak/services/FormService.java | 24 ++++----- .../keycloak/services/messages/Messages.java | 10 ++++ .../services/resources/AccountService.java | 4 +- .../resources/RequiredActionsService.java | 13 +++-- .../services/resources/flows/FormFlows.java | 34 +++++++++---- .../services/validation/Validation.java | 16 ++++++ .../RequiredActionUpdateProfileTest.java | 50 ++++++++++++++++++- .../testsuite/forms/ResetPasswordTest.java | 4 +- 18 files changed, 180 insertions(+), 97 deletions(-) rename forms/src/main/java/org/keycloak/forms/{ErrorBean.java => MessageBean.java} (78%) diff --git a/forms/src/main/java/org/keycloak/forms/ErrorBean.java b/forms/src/main/java/org/keycloak/forms/MessageBean.java similarity index 78% rename from forms/src/main/java/org/keycloak/forms/ErrorBean.java rename to forms/src/main/java/org/keycloak/forms/MessageBean.java index d6c18b6a61..c1b16a457c 100644 --- a/forms/src/main/java/org/keycloak/forms/ErrorBean.java +++ b/forms/src/main/java/org/keycloak/forms/MessageBean.java @@ -26,18 +26,18 @@ import org.keycloak.services.resources.flows.FormFlows; /** * @author Stian Thorgersen */ -public class ErrorBean { +public class MessageBean { private String summary; - private FormFlows.ErrorType type; + private FormFlows.MessageType type; // Message is considered ERROR by default - public ErrorBean(String summary) { - this(summary, FormFlows.ErrorType.ERROR); + public MessageBean(String summary) { + this(summary, FormFlows.MessageType.ERROR); } - public ErrorBean(String summary, FormFlows.ErrorType type) { + public MessageBean(String summary, FormFlows.MessageType type) { this.summary = summary; this.type = type; } @@ -47,15 +47,15 @@ public class ErrorBean { } public boolean isSuccess(){ - return FormFlows.ErrorType.SUCCESS.equals(this.type); + return FormFlows.MessageType.SUCCESS.equals(this.type); } public boolean isWarning(){ - return FormFlows.ErrorType.WARNING.equals(this.type); + return FormFlows.MessageType.WARNING.equals(this.type); } public boolean isError(){ - return FormFlows.ErrorType.ERROR.equals(this.type); + return FormFlows.MessageType.ERROR.equals(this.type); } } \ No newline at end of file diff --git a/forms/src/main/java/org/keycloak/service/FormServiceImpl.java b/forms/src/main/java/org/keycloak/service/FormServiceImpl.java index 9489448ebe..64624e471e 100644 --- a/forms/src/main/java/org/keycloak/service/FormServiceImpl.java +++ b/forms/src/main/java/org/keycloak/service/FormServiceImpl.java @@ -32,7 +32,7 @@ import freemarker.template.Configuration; import freemarker.template.Template; import freemarker.template.TemplateException; import org.jboss.resteasy.logging.Logger; -import org.keycloak.forms.ErrorBean; +import org.keycloak.forms.MessageBean; import org.keycloak.forms.LoginBean; import org.keycloak.forms.OAuthGrantBean; import org.keycloak.forms.RealmBean; @@ -69,8 +69,7 @@ public class FormServiceImpl implements FormService { commandMap.put(Pages.TOTP, new CommandTotp()); commandMap.put(Pages.LOGIN_CONFIG_TOTP, new CommandTotp()); commandMap.put(Pages.LOGIN_TOTP, new CommandLoginTotp()); - commandMap.put(Pages.LOGIN_VERIFY_EMAIL, new CommandLoginTotp()); - commandMap.put(Pages.ERROR, new CommandError()); + commandMap.put(Pages.LOGIN_VERIFY_EMAIL, new CommandVerifyEmail()); commandMap.put(Pages.OAUTH_GRANT, new CommandOAuthGrant()); } @@ -82,8 +81,8 @@ public class FormServiceImpl implements FormService { Map attributes = new HashMap(); - if (dataBean.getError() != null){ - attributes.put("message", new ErrorBean(dataBean.getError(), dataBean.getErrorType())); + if (dataBean.getMessage() != null){ + attributes.put("message", new MessageBean(dataBean.getMessage(), dataBean.getMessageType())); } RealmBean realm = new RealmBean(dataBean.getRealm()); @@ -161,9 +160,6 @@ public class FormServiceImpl implements FormService { private class CommandLoginTotp implements Command { public void exec(Map attributes, FormServiceDataBean dataBean) { - if (dataBean.getError() != null){ - attributes.put("error", new ErrorBean(dataBean.getError())); - } RealmBean realm = new RealmBean(dataBean.getRealm()); @@ -206,10 +202,6 @@ public class FormServiceImpl implements FormService { private class CommandLogin implements Command { public void exec(Map attributes, FormServiceDataBean dataBean) { - if (dataBean.getError() != null){ - attributes.put("error", new ErrorBean(dataBean.getError())); - } - RealmBean realm = new RealmBean(dataBean.getRealm()); attributes.put("realm", realm); @@ -230,9 +222,6 @@ public class FormServiceImpl implements FormService { private class CommandRegister implements Command { public void exec(Map attributes, FormServiceDataBean dataBean) { - if (dataBean.getError() != null){ - attributes.put("error", new ErrorBean(dataBean.getError())); - } RealmBean realm = new RealmBean(dataBean.getRealm()); @@ -252,14 +241,6 @@ public class FormServiceImpl implements FormService { } } - private class CommandError implements Command { - public void exec(Map attributes, FormServiceDataBean dataBean) { - if (dataBean.getError() != null){ - attributes.put("error", new ErrorBean(dataBean.getError())); - } - } - } - private class CommandOAuthGrant implements Command { public void exec(Map attributes, FormServiceDataBean dataBean) { @@ -274,6 +255,20 @@ public class FormServiceImpl implements FormService { } } + private class CommandVerifyEmail implements Command { + public void exec(Map attributes, FormServiceDataBean dataBean) { + + RealmBean realm = new RealmBean(dataBean.getRealm()); + + attributes.put("realm", realm); + + UrlBean url = new UrlBean(realm, dataBean.getBaseURI()); + url.setSocialRegistration(dataBean.getSocialRegistration()); + + attributes.put("url", url); + } + } + private interface Command { public void exec(Map attributes, FormServiceDataBean dataBean); } diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/css/login-register.css b/forms/src/main/resources/META-INF/resources/forms/theme/default/css/login-register.css index 8eacd72561..eca69a22c3 100644 --- a/forms/src/main/resources/META-INF/resources/forms/theme/default/css/login-register.css +++ b/forms/src/main/resources/META-INF/resources/forms/theme/default/css/login-register.css @@ -291,9 +291,6 @@ a.zocial:before { .rcue-login-register.reset .background-area .section.app-form { width: 43.2em; } -.rcue-login-register.reset .feedback { - left: 35.7em; -} .rcue-login-register.oauth .form-actions { margin-bottom: 0; diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/error.ftl b/forms/src/main/resources/META-INF/resources/forms/theme/default/error.ftl index 659fce1e1a..fd3fc1ebee 100755 --- a/forms/src/main/resources/META-INF/resources/forms/theme/default/error.ftl +++ b/forms/src/main/resources/META-INF/resources/forms/theme/default/error.ftl @@ -12,7 +12,7 @@ <#elseif section = "form">

Something happened and we could not process your request.

-

${error.summary}

+

${message.summary}

<#elseif section = "info" > diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/login-config-totp.ftl b/forms/src/main/resources/META-INF/resources/forms/theme/default/login-config-totp.ftl index 2b1c1c5c9a..97ff4adf47 100755 --- a/forms/src/main/resources/META-INF/resources/forms/theme/default/login-config-totp.ftl +++ b/forms/src/main/resources/META-INF/resources/forms/theme/default/login-config-totp.ftl @@ -8,11 +8,6 @@ Google Authenticator Setup - <#elseif section = "feedback"> - - <#elseif section = "form">
diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/login-reset-password.ftl b/forms/src/main/resources/META-INF/resources/forms/theme/default/login-reset-password.ftl index ad80199ed5..af308ac13c 100755 --- a/forms/src/main/resources/META-INF/resources/forms/theme/default/login-reset-password.ftl +++ b/forms/src/main/resources/META-INF/resources/forms/theme/default/login-reset-password.ftl @@ -11,15 +11,6 @@ <#elseif section = "form">
- <#if message?has_content> - <#if message.success> - - - <#if message.error> - - - -

${rb.getString('emailInstruction')}

diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/login-verify-email.ftl b/forms/src/main/resources/META-INF/resources/forms/theme/default/login-verify-email.ftl index cff75f1086..0832666004 100755 --- a/forms/src/main/resources/META-INF/resources/forms/theme/default/login-verify-email.ftl +++ b/forms/src/main/resources/META-INF/resources/forms/theme/default/login-verify-email.ftl @@ -8,11 +8,6 @@ Email verification - <#elseif section = "feedback"> - - <#elseif section = "form">
diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/template-login-action.ftl b/forms/src/main/resources/META-INF/resources/forms/theme/default/template-login-action.ftl index c2920cf57f..303aeebd68 100644 --- a/forms/src/main/resources/META-INF/resources/forms/theme/default/template-login-action.ftl +++ b/forms/src/main/resources/META-INF/resources/forms/theme/default/template-login-action.ftl @@ -17,7 +17,11 @@ <#if (template.themeConfig.logo)?has_content>

@@ -33,18 +37,26 @@
+ <#if !isErrorPage && message?has_content> + <#if message.error> + + <#elseif message.success> + + + +

Application login area

<#nested "form">
- <#if !isErrorPage && error?has_content> - - -

Info area

<#nested "info"> diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/template-login.ftl b/forms/src/main/resources/META-INF/resources/forms/theme/default/template-login.ftl index de023e1bfb..d64a3c694e 100644 --- a/forms/src/main/resources/META-INF/resources/forms/theme/default/template-login.ftl +++ b/forms/src/main/resources/META-INF/resources/forms/theme/default/template-login.ftl @@ -31,10 +31,10 @@

Application login area

- <#if error?has_content> + <#if message?has_content && message.error> diff --git a/forms/src/main/resources/org/keycloak/forms/messages.properties b/forms/src/main/resources/org/keycloak/forms/messages.properties index 03943dc0e1..8fb4f12000 100644 --- a/forms/src/main/resources/org/keycloak/forms/messages.properties +++ b/forms/src/main/resources/org/keycloak/forms/messages.properties @@ -31,6 +31,7 @@ missingLastName=Please specify last name missingEmail=Please specify email missingUsername=Please specify username missingPassword=Please specify password +notMatchPassword=Passwords don't match missingTotp=Please specify authenticator code invalidPasswordExisting=Invalid existing password @@ -43,6 +44,12 @@ successTotpRemoved=Google authenticator removed. usernameExists=Username already exists error=A system error has occured, contact admin +actionWarningHeader=Your account is not enabled. +actionTotpWarning=You need to set up the Google Authenticator to activate your account. +actionProfileWarning=You need to update your user profile to activate your account. +actionPasswordWarning=You need to change your password to activate your account. +actionEmailWarning=You need to verify your email address to activate your account. +actionFollow=Please follow the steps below. successHeader=Success! errorHeader=Error! diff --git a/services/src/main/java/org/keycloak/services/FormService.java b/services/src/main/java/org/keycloak/services/FormService.java index 6e5813855b..70e4c0fc03 100755 --- a/services/src/main/java/org/keycloak/services/FormService.java +++ b/services/src/main/java/org/keycloak/services/FormService.java @@ -44,9 +44,9 @@ public interface FormService { private RealmModel realm; private UserModel userModel; - private String error; + private String message; - private FormFlows.ErrorType errorType; + private FormFlows.MessageType messageType; private MultivaluedMap formData; private URI baseURI; @@ -81,11 +81,11 @@ public interface FormService { private String contextPath; - public FormServiceDataBean(RealmModel realm, UserModel userModel, MultivaluedMap formData, String error){ + public FormServiceDataBean(RealmModel realm, UserModel userModel, MultivaluedMap formData, String message){ this.realm = realm; this.userModel = userModel; this.formData = formData; - this.error = error; + this.message = message; } public URI getBaseURI() { @@ -96,12 +96,12 @@ public interface FormService { this.baseURI = baseURI; } - public String getError() { - return error; + public String getMessage() { + return message; } - public void setError(String error) { - this.error = error; + public void setMessage(String message) { + this.message = message; } public MultivaluedMap getFormData() { @@ -128,12 +128,12 @@ public interface FormService { this.userModel = userModel; } - public FormFlows.ErrorType getErrorType() { - return errorType; + public FormFlows.MessageType getMessageType() { + return messageType; } - public void setErrorType(FormFlows.ErrorType errorType) { - this.errorType = errorType; + public void setMessageType(FormFlows.MessageType messageType) { + this.messageType = messageType; } /* OAuth Part */ diff --git a/services/src/main/java/org/keycloak/services/messages/Messages.java b/services/src/main/java/org/keycloak/services/messages/Messages.java index 3b7a760d0e..2c659fbd57 100644 --- a/services/src/main/java/org/keycloak/services/messages/Messages.java +++ b/services/src/main/java/org/keycloak/services/messages/Messages.java @@ -44,6 +44,8 @@ public class Messages { public static final String MISSING_PASSWORD = "missingPassword"; + public static final String NOTMATCH_PASSWORD = "notMatchPassword"; + public static final String MISSING_USERNAME = "missingUsername"; public static final String MISSING_TOTP = "missingTotp"; @@ -52,6 +54,14 @@ public class Messages { public static final String USERNAME_EXISTS = "usernameExists"; + public static final String ACTION_WARN_TOTP = "actionTotpWarning"; + + public static final String ACTION_WARN_PROFILE = "actionProfileWarning"; + + public static final String ACTION_WARN_PASSWD = "actionPasswordWarning"; + + public static final String ACTION_WARN_EMAIL = "actionEmailWarning"; + public static final String ERROR = "error"; } 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 7f54e5232a..7205d6f73c 100755 --- a/services/src/main/java/org/keycloak/services/resources/AccountService.java +++ b/services/src/main/java/org/keycloak/services/resources/AccountService.java @@ -115,7 +115,7 @@ public class AccountService { public Response processTotpRemove() { UserModel user = getUserFromAuthManager(); user.setTotp(false); - return Flows.forms(realm, request, uriInfo).setError("successTotpRemoved").setErrorType(FormFlows.ErrorType.SUCCESS) + return Flows.forms(realm, request, uriInfo).setError("successTotpRemoved").setErrorType(FormFlows.MessageType.SUCCESS) .setUser(user).forwardToTotp(); } @@ -152,7 +152,7 @@ public class AccountService { user.setTotp(true); - return Flows.forms(realm, request, uriInfo).setError("successTotp").setErrorType(FormFlows.ErrorType.SUCCESS) + return Flows.forms(realm, request, uriInfo).setError("successTotp").setErrorType(FormFlows.MessageType.SUCCESS) .setUser(user).forwardToTotp(); } diff --git a/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java b/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java index 1517b799c1..67a241e4e6 100755 --- a/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java +++ b/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java @@ -88,6 +88,12 @@ public class RequiredActionsService { } UserModel user = getUser(accessCode); + + String error = Validation.validateUpdateProfileForm(formData); + if (error != null) { + return Flows.forms(realm, request, uriInfo).setError(error).forwardToAction(RequiredAction.UPDATE_PROFILE); + } + user.setFirstName(formData.getFirst("firstName")); user.setLastName(formData.getFirst("lastName")); user.setEmail(formData.getFirst("email")); @@ -146,15 +152,14 @@ public class RequiredActionsService { UserModel user = getUser(accessCode); - String password = formData.getFirst("password"); String passwordNew = formData.getFirst("password-new"); String passwordConfirm = formData.getFirst("password-confirm"); FormFlows forms = Flows.forms(realm, request, uriInfo).setUser(user); if (Validation.isEmpty(passwordNew)) { - forms.setError(Messages.MISSING_PASSWORD).forwardToAction(RequiredAction.UPDATE_PASSWORD); + return forms.setError(Messages.MISSING_PASSWORD).forwardToAction(RequiredAction.UPDATE_PASSWORD); } else if (!passwordNew.equals(passwordConfirm)) { - forms.setError(Messages.MISSING_PASSWORD).forwardToAction(RequiredAction.UPDATE_PASSWORD); + return forms.setError(Messages.NOTMATCH_PASSWORD).forwardToAction(RequiredAction.UPDATE_PASSWORD); } UserCredentialModel credentials = new UserCredentialModel(); @@ -257,7 +262,7 @@ public class RequiredActionsService { new EmailSender().sendPasswordReset(user, realm, accessCode, uriInfo); - return Flows.forms(realm, request, uriInfo).setError("emailSent").setErrorType(FormFlows.ErrorType.SUCCESS) + return Flows.forms(realm, request, uriInfo).setError("emailSent").setErrorType(FormFlows.MessageType.SUCCESS) .forwardToPasswordReset(); } 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 c4fe4a6d25..2aebb4e84b 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 @@ -34,6 +34,7 @@ import org.keycloak.services.managers.AccessCodeEntry; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserModel.RequiredAction; +import org.keycloak.services.messages.Messages; import org.picketlink.idm.model.sample.Realm; import javax.imageio.spi.ServiceRegistry; @@ -58,8 +59,8 @@ public class FormFlows { // TODO refactor/rename "error" to "message" everywhere where it makes sense private String error; - public static enum ErrorType {SUCCESS, WARNING, ERROR}; - private ErrorType errorType; + public static enum MessageType {SUCCESS, WARNING, ERROR}; + private MessageType messageType = MessageType.ERROR; private MultivaluedMap formData; @@ -79,16 +80,17 @@ public class FormFlows { } public Response forwardToAction(RequiredAction action) { + switch (action) { case CONFIGURE_TOTP: - return forwardToForm(Pages.LOGIN_CONFIG_TOTP); + return forwardToActionForm(Pages.LOGIN_CONFIG_TOTP, Messages.ACTION_WARN_TOTP); case UPDATE_PROFILE: - return forwardToForm(Pages.LOGIN_UPDATE_PROFILE); + return forwardToActionForm(Pages.LOGIN_UPDATE_PROFILE, Messages.ACTION_WARN_PROFILE); case UPDATE_PASSWORD: - return forwardToForm(Pages.LOGIN_UPDATE_PASSWORD); + return forwardToActionForm(Pages.LOGIN_UPDATE_PASSWORD, Messages.ACTION_WARN_PASSWD); case VERIFY_EMAIL: new EmailSender().sendEmailVerification(userModel, realm, accessCode, uriInfo); - return forwardToForm(Pages.LOGIN_VERIFY_EMAIL); + return forwardToActionForm(Pages.LOGIN_VERIFY_EMAIL, Messages.ACTION_WARN_EMAIL); default: return Response.serverError().build(); } @@ -103,7 +105,6 @@ public class FormFlows { } private Response forwardToForm(String template, FormService.FormServiceDataBean formDataBean) { - formDataBean.setErrorType(errorType == null ? ErrorType.ERROR : errorType); // Getting URI needed by form processing service ResteasyUriInfo uriInfo = request.getUri(); @@ -143,8 +144,21 @@ public class FormFlows { private Response forwardToForm(String template) { FormService.FormServiceDataBean formDataBean = new FormService.FormServiceDataBean(realm, userModel, formData, error); - return forwardToForm(template, formDataBean); + formDataBean.setMessageType(messageType); + return forwardToForm(template, formDataBean); + } + + private Response forwardToActionForm(String template, String warningSummary) { + + // If no other message is set, notify user about required action in the warning window + // so it's clear that this is a req. action form not a login form + if (error == null){ + messageType = MessageType.WARNING; + error = warningSummary; + } + + return forwardToForm(template); } public Response forwardToLogin() { @@ -202,8 +216,8 @@ public class FormFlows { return this; } - public FormFlows setErrorType(ErrorType errorType) { - this.errorType = errorType; + public FormFlows setErrorType(MessageType errorType) { + this.messageType = errorType; return this; } diff --git a/services/src/main/java/org/keycloak/services/validation/Validation.java b/services/src/main/java/org/keycloak/services/validation/Validation.java index 5a71f0d9f7..c652849f99 100755 --- a/services/src/main/java/org/keycloak/services/validation/Validation.java +++ b/services/src/main/java/org/keycloak/services/validation/Validation.java @@ -38,6 +38,22 @@ public class Validation { return null; } + public static String validateUpdateProfileForm(MultivaluedMap formData) { + if (isEmpty(formData.getFirst("firstName"))) { + return Messages.MISSING_FIRST_NAME; + } + + if (isEmpty(formData.getFirst("lastName"))) { + return Messages.MISSING_LAST_NAME; + } + + if (isEmpty(formData.getFirst("email"))) { + return Messages.MISSING_EMAIL; + } + + return null; + } + public static boolean isEmpty(String s) { return s == null || s.length() == 0; } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileTest.java index 08a2bfa3e7..c567e99104 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileTest.java @@ -44,8 +44,8 @@ import org.openqa.selenium.WebDriver; */ public class RequiredActionUpdateProfileTest { - @ClassRule - public static KeycloakRule keycloakRule = new KeycloakRule(new KeycloakSetup() { + @Rule + public KeycloakRule keycloakRule = new KeycloakRule(new KeycloakSetup() { @Override public void config(RealmManager manager, RealmModel defaultRealm, RealmModel appRealm) { @@ -83,4 +83,50 @@ public class RequiredActionUpdateProfileTest { Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); } + @Test + public void updateProfileMissingFirstName() { + loginPage.open(); + + loginPage.login("test-user@localhost", "password"); + + updateProfilePage.assertCurrent(); + + updateProfilePage.update("", "New last", "new@email.com"); + + updateProfilePage.assertCurrent(); + + Assert.assertEquals("Please specify first name", updateProfilePage.getError()); + } + + @Test + public void updateProfileMissingLastName() { + loginPage.open(); + + loginPage.login("test-user@localhost", "password"); + + updateProfilePage.assertCurrent(); + + updateProfilePage.update("New first", "", "new@email.com"); + + updateProfilePage.assertCurrent(); + + Assert.assertEquals("Please specify last name", updateProfilePage.getError()); + } + + @Test + public void updateProfileMissingEmail() { + loginPage.open(); + + loginPage.login("test-user@localhost", "password"); + + updateProfilePage.assertCurrent(); + + updateProfilePage.update("New first", "New last", ""); + + updateProfilePage.assertCurrent(); + + Assert.assertEquals("Please specify email", updateProfilePage.getError()); + } + + } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java index 9b5ca0f1b9..396f3d9411 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java @@ -123,7 +123,7 @@ public class ResetPasswordTest { resetPasswordPage.assertCurrent(); Assert.assertNotEquals("Success!", resetPasswordPage.getMessage()); - Assert.assertEquals("Error!", resetPasswordPage.getMessage()); + Assert.assertEquals("Invalid username or email.", resetPasswordPage.getMessage()); } @Test @@ -138,7 +138,7 @@ public class ResetPasswordTest { resetPasswordPage.assertCurrent(); Assert.assertNotEquals("Success!", resetPasswordPage.getMessage()); - Assert.assertEquals("Error!", resetPasswordPage.getMessage()); + Assert.assertEquals("Invalid username or email.", resetPasswordPage.getMessage()); } } From 1c90e16629b07e468e44a98b661a4c0429dcac0f Mon Sep 17 00:00:00 2001 From: Stian Thorgersen Date: Thu, 17 Oct 2013 15:00:46 +0100 Subject: [PATCH 02/11] Added redirect uris to application --- .../keycloak/test/ApplicationModelTest.java | 4 +++ .../java/org/keycloak/test/UserModelTest.java | 25 ++++++++++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/services/src/test/java/org/keycloak/test/ApplicationModelTest.java b/services/src/test/java/org/keycloak/test/ApplicationModelTest.java index 446607938a..cc7068b1f4 100644 --- a/services/src/test/java/org/keycloak/test/ApplicationModelTest.java +++ b/services/src/test/java/org/keycloak/test/ApplicationModelTest.java @@ -49,6 +49,9 @@ public class ApplicationModelTest extends AbstractKeycloakServerTest { application.getApplicationUser().addRedirectUri("redirect-1"); application.getApplicationUser().addRedirectUri("redirect-2"); + application.getApplicationUser().addWebOrigin("origin-1"); + application.getApplicationUser().addWebOrigin("origin-2"); + application.updateApplication(); } @@ -85,6 +88,7 @@ public class ApplicationModelTest extends AbstractKeycloakServerTest { UserModel euser = expected.getApplicationUser(); Assert.assertTrue(euser.getRedirectUris().containsAll(auser.getRedirectUris())); + Assert.assertTrue(euser.getWebOrigins().containsAll(auser.getWebOrigins())); } public static void assertEquals(List expected, List actual) { diff --git a/services/src/test/java/org/keycloak/test/UserModelTest.java b/services/src/test/java/org/keycloak/test/UserModelTest.java index 9922596cfb..90295112e2 100644 --- a/services/src/test/java/org/keycloak/test/UserModelTest.java +++ b/services/src/test/java/org/keycloak/test/UserModelTest.java @@ -53,10 +53,33 @@ public class UserModelTest extends AbstractKeycloakServerTest { user.addRequiredAction(RequiredAction.CONFIGURE_TOTP); user.addRequiredAction(RequiredAction.UPDATE_PASSWORD); + user.addWebOrigin("origin-1"); + user.addWebOrigin("origin-2"); + UserModel persisted = manager.getRealm(realm.getId()).getUser("user"); assertEquals(user, persisted); } + + @Test + public void webOriginSetTest() { + RealmModel realm = manager.createRealm("original"); + UserModel user = realm.addUser("user"); + + Assert.assertTrue(user.getWebOrigins().isEmpty()); + + user.addWebOrigin("origin-1"); + Assert.assertEquals(1, user.getWebOrigins().size()); + + user.addWebOrigin("origin-2"); + Assert.assertEquals(2, user.getWebOrigins().size()); + + user.removeWebOrigin("origin-2"); + Assert.assertEquals(1, user.getWebOrigins().size()); + + user.removeWebOrigin("origin-1"); + Assert.assertTrue(user.getWebOrigins().isEmpty()); + } @Test public void testUserRequiredActions() throws Exception { @@ -102,7 +125,7 @@ public class UserModelTest extends AbstractKeycloakServerTest { Assert.assertEquals(expected.getLastName(), actual.getLastName()); Assert.assertArrayEquals(expected.getRedirectUris().toArray(), actual.getRedirectUris().toArray()); Assert.assertArrayEquals(expected.getRequiredActions().toArray(), actual.getRequiredActions().toArray()); - + Assert.assertArrayEquals(expected.getWebOrigins().toArray(), actual.getWebOrigins().toArray()); } public static void assertEquals(List expected, List actual) { From 34fe0a751c428f36dffff4b2f054dfd1b6b50632 Mon Sep 17 00:00:00 2001 From: Stian Thorgersen Date: Thu, 10 Oct 2013 17:55:03 +0100 Subject: [PATCH 03/11] Added cors support to TokenService.accessCodeToToken --- .../idm/ApplicationRepresentation.java | 9 + examples/js-google/index.html | 24 ++ examples/js-google/keycloak.js | 139 +++++++++++ examples/js-google/keycloak.js.orig | 222 ++++++++++++++++++ examples/js-google/kinvey.html | 13 + examples/js-google/testrealm.json | 60 +++++ examples/js/keycloak.js | 186 +++++++-------- examples/js/testrealm.json | 1 + .../java/org/keycloak/models/UserModel.java | 8 + .../models/picketlink/UserAdapter.java | 21 ++ .../services/managers/ApplicationManager.java | 15 ++ .../org/keycloak/services/resources/Cors.java | 43 ++++ .../services/resources/TokenService.java | 3 +- 13 files changed, 639 insertions(+), 105 deletions(-) create mode 100644 examples/js-google/index.html create mode 100644 examples/js-google/keycloak.js create mode 100644 examples/js-google/keycloak.js.orig create mode 100644 examples/js-google/kinvey.html create mode 100755 examples/js-google/testrealm.json create mode 100644 services/src/main/java/org/keycloak/services/resources/Cors.java diff --git a/core/src/main/java/org/keycloak/representations/idm/ApplicationRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/ApplicationRepresentation.java index 7b7fa9d48a..878092e886 100755 --- a/core/src/main/java/org/keycloak/representations/idm/ApplicationRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/ApplicationRepresentation.java @@ -21,6 +21,7 @@ public class ApplicationRepresentation { protected List roleMappings; protected List scopeMappings; protected List redirectUris; + protected List webOrigins; public String getSelf() { return self; @@ -155,4 +156,12 @@ public class ApplicationRepresentation { public void setRedirectUris(List redirectUris) { this.redirectUris = redirectUris; } + + public List getWebOrigins() { + return webOrigins; + } + + public void setWebOrigins(List webOrigins) { + this.webOrigins = webOrigins; + } } diff --git a/examples/js-google/index.html b/examples/js-google/index.html new file mode 100644 index 0000000000..7a7212060d --- /dev/null +++ b/examples/js-google/index.html @@ -0,0 +1,24 @@ + + + + + + + + + + diff --git a/examples/js-google/keycloak.js b/examples/js-google/keycloak.js new file mode 100644 index 0000000000..172557dbad --- /dev/null +++ b/examples/js-google/keycloak.js @@ -0,0 +1,139 @@ +window.keycloak = (function () { + var kc = {}; + var config = { + clientId: null, + clientSecret: null + }; + + kc.init = function (c) { + for (var prop in config) { + if (c[prop]) { + config[prop] = c[prop]; + } + + if (!config[prop]) { + throw new Error(prop + ' not defined'); + } + } + + loadToken(); + + if (kc.token) { + kc.user = kc.tokenInfo.user_id; + kc.authenticated = true; + } else { + kc.authenticated = false; + kc.user = null; + } + } + + kc.login = function () { + var clientId = encodeURIComponent(config.clientId); + var redirectUri = encodeURIComponent(window.location.href); + var state = encodeURIComponent(createUUID()); + var scope = encodeURIComponent('https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/plus.login'); + var url = 'https://accounts.google.com/o/oauth2/auth?response_type=token&client_id=' + clientId + '&redirect_uri=' + redirectUri + + '&state=' + state + '&scope=' + scope; + + sessionStorage.state = state; + + window.location.href = url; + } + + function parseToken(token) { + return JSON.parse(atob(token.split('.')[1])); + } + + kc.profile = function(header) { + var url = 'https://www.googleapis.com/oauth2/v1/userinfo' + + if (!header) { + url = url + '?access_token=' + kc.token; + } + + var http = new XMLHttpRequest(); + http.open('GET', url, false); + if (header) { + http.setRequestHeader('Authorization', 'Bearer ' + kc.token); + } + + http.send(); + if (http.status == 200) { + return JSON.parse(http.responseText); + } + } + + kc.contacts = function(header) { + var url = 'https://www.googleapis.com/plus/v1/people/me'; + + if (!header) { + url = url + '?access_token=' + kc.token; + } + + var http = new XMLHttpRequest(); + http.open('GET', url, false); + if (header) { + http.setRequestHeader('Authorization', 'Bearer ' + kc.token); + } + + http.send(); + if (http.status == 200) { + return http.responseText; + } + } + + return kc; + + function loadToken() { + var params = {} + var queryString = location.hash.substring(1) + var regex = /([^&=]+)=([^&]*)/g, m; + while (m = regex.exec(queryString)) { + params[decodeURIComponent(m[1])] = decodeURIComponent(m[2]); + } + + var token = params['access_token']; + var state = params['state']; + + if (token && state === sessionStorage.state) { + window.history.replaceState({}, document.title, location.protocol + "//" + location.host + location.pathname); + + kc.token = token; + + var url = 'https://www.googleapis.com/oauth2/v1/tokeninfo?access_token=' + token; + + var http = new XMLHttpRequest(); + http.open('GET', url, false); + + http.send(); + if (http.status == 200) { + kc.tokenInfo = JSON.parse(http.responseText); + } + } + return undefined; + } + + function getQueryParam(name) { + console.debug(window.location.hash); + var params = window.location.hash.substring(1).split('&'); + for (var i = 0; i < params.length; i++) { + var p = params[i].split('='); + if (decodeURIComponent(p[0]) == name) { + return p[1]; + } + } + } + + function createUUID() { + var s = []; + var hexDigits = '0123456789abcdef'; + for (var i = 0; i < 36; i++) { + s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1); + } + s[14] = '4'; + s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1); + s[8] = s[13] = s[18] = s[23] = '-'; + var uuid = s.join(''); + return uuid; + } +})(); diff --git a/examples/js-google/keycloak.js.orig b/examples/js-google/keycloak.js.orig new file mode 100644 index 0000000000..439d2af5f6 --- /dev/null +++ b/examples/js-google/keycloak.js.orig @@ -0,0 +1,222 @@ +<<<<<<< Updated upstream +window.keycloak = (function() { + var kc = {}; + var config = null; + + kc.init = function(c) { + config = c; + + var token = getTokenFromCode(); + if (token) { + var t = parseToken(token); + kc.user = t.prn; + kc.authenticated = true; + } else { + kc.authenticated = false; + } + } + + kc.login = function() { + var clientId = encodeURIComponent(config.clientId); + var redirectUri = encodeURIComponent(window.location.href); + var state = encodeURIComponent(createUUID()); + var realm = encodeURIComponent(config.realm); + var url = config.baseUrl + '/rest/realms/' + realm + '/tokens/login?response_type=code&client_id=' + clientId + '&redirect_uri=' + redirectUri + + '&state=' + state; + window.location.href = url; + } + + return kc; + + function parseToken(token) { + return JSON.parse(atob(token.split('.')[1])); + } + + function getTokenFromCode() { + var code = getQueryParam('code'); + if (code) { + window.history.replaceState({}, document.title, location.protocol + "//" + location.host + location.pathname); + + var clientId = encodeURIComponent(config.clientId); + var clientSecret = encodeURIComponent(config.clientSecret); + var realm = encodeURIComponent(config.realm); + + var params = 'code=' + code + '&client_id=' + config.clientId + '&password=' + config.clientSecret; + var url = config.baseUrl + '/rest/realms/' + realm + '/tokens/access/codes' + + var http = new XMLHttpRequest(); + http.open('POST', url, false); + http.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); + + http.send(params); + if (http.status == 200) { + return JSON.parse(http.responseText)['access_token']; + } + } + return undefined; + } + + function getQueryParam(name) { + var params = window.location.search.substring(1).split('&'); + for ( var i = 0; i < params.length; i++) { + var p = params[i].split('='); + if (decodeURIComponent(p[0]) == name) { + return p[1]; + } + } + } + + function createUUID() { + var s = []; + var hexDigits = '0123456789abcdef'; + for ( var i = 0; i < 36; i++) { + s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1); + } + s[14] = '4'; + s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1); + s[8] = s[13] = s[18] = s[23] = '-'; + var uuid = s.join(''); + return uuid; + } +======= +window.keycloak = (function () { + var kc = {}; + var config = { + baseUrl : null, + clientId : null, + clientSecret: null, + realm: null + }; + + kc.init = function (c) { + for (var prop in config) { + if (c[prop]) { + config[prop] = c[prop]; + } + + if (!config[prop]) { + throw new Error(prop + 'not defined'); + } + } + + var token = getTokenFromCode(); + if (token) { + var t = parseToken(token); + kc.user = t.prn; + kc.authenticated = true; + } else { + kc.authenticated = false; + } + } + + kc.login = function () { + var clientId = encodeURIComponent(config.clientId); + var redirectUri = encodeURIComponent(window.location.href); + var realm = encodeURIComponent(config.realm); + var state = encodeURIComponent(createUUID()); + var url = config.baseUrl + '/rest/realms/' + realm + '/tokens/login?response_type=code&client_id=' + clientId + '&redirect_uri=' + redirectUri + + '&state=' + state; + + sessionStorage.state = state; + + window.location.href = url; + } + + return kc; + + function parseToken(token) { + var t = base64Decode(token.split('.')[1]); + return JSON.parse(t); + } + + function getTokenFromCode() { + var code = getQueryParam('code'); + var state = getQueryParam('state'); + + if (code) { + if (state && state === sessionStorage.state) { + window.history.replaceState({}, document.title, location.protocol + "//" + location.host + location.pathname); + + var clientId = encodeURIComponent(config.clientId); + var clientSecret = encodeURIComponent(config.clientSecret); + var realm = encodeURIComponent(config.realm); + + var params = 'code=' + code + '&client_id=' + clientId + '&password=' + clientSecret; + var url = config.baseUrl + '/rest/realms/' + realm + '/tokens/access/codes' + + var http = new XMLHttpRequest(); + http.open('POST', url, false); + http.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); + + http.send(params); + if (http.status == 200) { + return JSON.parse(http.responseText)['access_token']; + } + } + } + return undefined; + } + + function getQueryParam(name) { + var params = window.location.search.substring(1).split('&'); + for (var i = 0; i < params.length; i++) { + var p = params[i].split('='); + if (decodeURIComponent(p[0]) == name) { + return p[1]; + } + } + } + + function createUUID() { + var s = []; + var hexDigits = '0123456789abcdef'; + for (var i = 0; i < 36; i++) { + s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1); + } + s[14] = '4'; + s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1); + s[8] = s[13] = s[18] = s[23] = '-'; + var uuid = s.join(''); + return uuid; + } + + function base64Decode(data) { + var b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; + var o1, o2, o3, h1, h2, h3, h4, bits, i = 0, + ac = 0, + dec = "", + tmp_arr = []; + + if (!data) { + return data; + } + + data += ''; + + do { + h1 = b64.indexOf(data.charAt(i++)); + h2 = b64.indexOf(data.charAt(i++)); + h3 = b64.indexOf(data.charAt(i++)); + h4 = b64.indexOf(data.charAt(i++)); + + bits = h1 << 18 | h2 << 12 | h3 << 6 | h4; + + o1 = bits >> 16 & 0xff; + o2 = bits >> 8 & 0xff; + o3 = bits & 0xff; + + if (h3 == 64) { + tmp_arr[ac++] = String.fromCharCode(o1); + } else if (h4 == 64) { + tmp_arr[ac++] = String.fromCharCode(o1, o2); + } else { + tmp_arr[ac++] = String.fromCharCode(o1, o2, o3); + } + } while (i < data.length); + + dec = tmp_arr.join(''); + + return dec; + } +>>>>>>> Stashed changes +})(); \ No newline at end of file diff --git a/examples/js-google/kinvey.html b/examples/js-google/kinvey.html new file mode 100644 index 0000000000..9e1324c3d4 --- /dev/null +++ b/examples/js-google/kinvey.html @@ -0,0 +1,13 @@ + + + + + + + + + + + diff --git a/examples/js-google/testrealm.json b/examples/js-google/testrealm.json new file mode 100755 index 0000000000..2468f48035 --- /dev/null +++ b/examples/js-google/testrealm.json @@ -0,0 +1,60 @@ +{ + "id": "test", + "realm": "test", + "enabled": true, + "tokenLifespan": 300, + "accessCodeLifespan": 10, + "accessCodeLifespanUserAction": 600, + "sslNotRequired": true, + "cookieLoginAllowed": true, + "registrationAllowed": true, + "resetPasswordAllowed": 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" : "test-user@localhost", + "enabled": true, + "email" : "test-user@localhost", + "credentials" : [ + { "type" : "password", + "value" : "password" } + ] + } + ], + "roles": [ + { + "name": "user", + "description": "Have User privileges" + }, + { + "name": "admin", + "description": "Have Administrator privileges" + } + ], + "roleMappings": [ + { + "username": "test-user@localhost", + "roles": ["user"] + } + ], + "applications": [ + { + "name": "test-app", + "enabled": true, + "adminUrl": "http://localhost:8081/app/logout", + "useRealmMappings": true, + "webOrigins": [ "http://localhost", "http://localhost:8000", "http://localhost:8080" ], + "credentials": [ + { + "type": "password", + "value": "password" + } + ] + } + ] +} diff --git a/examples/js/keycloak.js b/examples/js/keycloak.js index dfb6ebb263..480996d343 100644 --- a/examples/js/keycloak.js +++ b/examples/js/keycloak.js @@ -1,120 +1,98 @@ -window.keycloak = (function() { - var kc = {}; - var config = null; +window.keycloak = (function () { + var kc = {}; + var config = { + baseUrl: null, + clientId: null, + clientSecret: null, + realm: null + }; - kc.init = function(c) { - config = c; + kc.init = function (c) { + for (var prop in config) { + if (c[prop]) { + config[prop] = c[prop]; + } - var token = getTokenFromCode(); - if (token) { - var t = parseToken(token); - kc.user = t.prn; - kc.authenticated = true; - } else { - kc.authenticated = false; - } - } + if (!config[prop]) { + throw new Error(prop + 'not defined'); + } + } - kc.login = function() { - var clientId = encodeURIComponent(config.clientId); - var redirectUri = encodeURIComponent(window.location.href); - var state = encodeURIComponent(createUUID()); - var realm = encodeURIComponent(config.realm); - var url = config.baseUrl + '/rest/realms/' + realm + '/tokens/login?response_type=code&client_id=' + clientId + '&redirect_uri=' + redirectUri - + '&state=' + state; - window.location.href = url; - } + var token = getTokenFromCode(); + if (token) { + var t = parseToken(token); + kc.user = t.prn; + kc.authenticated = true; + kc.token = token; + } else { + kc.authenticated = false; + } + } - return kc; + kc.login = function () { + var clientId = encodeURIComponent(config.clientId); + var redirectUri = encodeURIComponent(window.location.href); + var state = encodeURIComponent(createUUID()); + var realm = encodeURIComponent(config.realm); + var url = config.baseUrl + '/rest/realms/' + realm + '/tokens/login?response_type=code&client_id=' + clientId + '&redirect_uri=' + redirectUri + + '&state=' + state; - function parseToken(token) { - var t = base64Decode(token.split('.')[1]); - return JSON.parse(t); - } + sessionStorage.state = state; - function getTokenFromCode() { - var code = getQueryParam('code'); - if (code) { - window.history.replaceState({}, document.title, location.protocol + "//" + location.host + location.pathname); - - var clientId = encodeURIComponent(config.clientId); - var clientSecret = encodeURIComponent(config.clientSecret); - var realm = encodeURIComponent(config.realm); + window.location.href = url; + } - var params = 'code=' + code + '&client_id=' + config.clientId + '&password=' + config.clientSecret; - var url = config.baseUrl + '/rest/realms/' + realm + '/tokens/access/codes' + return kc; - var http = new XMLHttpRequest(); - http.open('POST', url, false); - http.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); + function parseToken(token) { + return JSON.parse(atob(token.split('.')[1])); + } - http.send(params); - if (http.status == 200) { - return JSON.parse(http.responseText)['access_token']; - } - } - return undefined; - } + function getTokenFromCode() { + var code = getQueryParam('code'); + var state = getQueryParam('state'); + if (code && state === sessionStorage.state) { + window.history.replaceState({}, document.title, location.protocol + "//" + location.host + location.pathname); - function getQueryParam(name) { - var params = window.location.search.substring(1).split('&'); - for ( var i = 0; i < params.length; i++) { - var p = params[i].split('='); - if (decodeURIComponent(p[0]) == name) { - return p[1]; - } - } - } + var clientId = encodeURIComponent(config.clientId); + var clientSecret = encodeURIComponent(config.clientSecret); + var realm = encodeURIComponent(config.realm); - function createUUID() { - var s = []; - var hexDigits = '0123456789abcdef'; - for ( var i = 0; i < 36; i++) { - s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1); - } - s[14] = '4'; - s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1); - s[8] = s[13] = s[18] = s[23] = '-'; - var uuid = s.join(''); - return uuid; - } + var params = 'code=' + code + '&client_id=' + clientId + '&password=' + clientSecret; + var url = config.baseUrl + '/rest/realms/' + realm + '/tokens/access/codes' - function base64Decode (data) { - var b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; - var o1, o2, o3, h1, h2, h3, h4, bits, i = 0, - ac = 0, - dec = "", - tmp_arr = []; + var http = new XMLHttpRequest(); + http.open('POST', url, false); + http.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); - if (!data) { - return data; - } + http.send(params); + if (http.status == 200) { + return JSON.parse(http.responseText)['access_token']; + } + } + return undefined; + } - data += ''; + function getQueryParam(name) { + var params = window.location.search.substring(1).split('&'); + for (var i = 0; i < params.length; i++) { + var p = params[i].split('='); + if (decodeURIComponent(p[0]) == name) { + return p[1]; + } + } + } - do { - h1 = b64.indexOf(data.charAt(i++)); - h2 = b64.indexOf(data.charAt(i++)); - h3 = b64.indexOf(data.charAt(i++)); - h4 = b64.indexOf(data.charAt(i++)); - - bits = h1 << 18 | h2 << 12 | h3 << 6 | h4; - - o1 = bits >> 16 & 0xff; - o2 = bits >> 8 & 0xff; - o3 = bits & 0xff; - - if (h3 == 64) { - tmp_arr[ac++] = String.fromCharCode(o1); - } else if (h4 == 64) { - tmp_arr[ac++] = String.fromCharCode(o1, o2); - } else { - tmp_arr[ac++] = String.fromCharCode(o1, o2, o3); - } - } while (i < data.length); - - dec = tmp_arr.join(''); - - return dec; - } + function createUUID() { + var s = []; + var hexDigits = '0123456789abcdef'; + for (var i = 0; i < 36; i++) { + s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1); + } + s[14] = '4'; + s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1); + s[8] = s[13] = s[18] = s[23] = '-'; + var uuid = s.join(''); + return uuid; + } })(); \ No newline at end of file diff --git a/examples/js/testrealm.json b/examples/js/testrealm.json index 824fe14153..2468f48035 100755 --- a/examples/js/testrealm.json +++ b/examples/js/testrealm.json @@ -48,6 +48,7 @@ "enabled": true, "adminUrl": "http://localhost:8081/app/logout", "useRealmMappings": true, + "webOrigins": [ "http://localhost", "http://localhost:8000", "http://localhost:8080" ], "credentials": [ { "type": "password", diff --git a/model/api/src/main/java/org/keycloak/models/UserModel.java b/model/api/src/main/java/org/keycloak/models/UserModel.java index 8598ae765d..7e331857c0 100755 --- a/model/api/src/main/java/org/keycloak/models/UserModel.java +++ b/model/api/src/main/java/org/keycloak/models/UserModel.java @@ -35,6 +35,14 @@ public interface UserModel { void removeRequiredAction(RequiredAction action); + Set getWebOrigins(); + + void setWebOrigins(Set webOrigins); + + void addWebOrigin(String webOrigin); + + void removeWebOrigin(String webOrigin); + Set getRedirectUris(); void setRedirectUris(Set redirectUris); diff --git a/model/picketlink/src/main/java/org/keycloak/models/picketlink/UserAdapter.java b/model/picketlink/src/main/java/org/keycloak/models/picketlink/UserAdapter.java index 98894ced7e..e44c92ac17 100755 --- a/model/picketlink/src/main/java/org/keycloak/models/picketlink/UserAdapter.java +++ b/model/picketlink/src/main/java/org/keycloak/models/picketlink/UserAdapter.java @@ -22,6 +22,7 @@ public class UserAdapter implements UserModel { private static final String REQUIRED_ACTIONS_ATTR = "requiredActions"; private static final String REDIRECT_URIS = "redirectUris"; + private static final String WEB_ORIGINS = "webOrigins"; protected User user; protected IdentityManager idm; @@ -161,6 +162,26 @@ public class UserAdapter implements UserModel { removeFromAttributeSet(REDIRECT_URIS, redirectUri); } + @Override + public Set getWebOrigins() { + return getAttributeSet(WEB_ORIGINS); + } + + @Override + public void setWebOrigins(Set webOrigins) { + setAttributeSet(WEB_ORIGINS, webOrigins); + } + + @Override + public void addWebOrigin(String webOrigin) { + addToAttributeSet(WEB_ORIGINS, webOrigin); + } + + @Override + public void removeWebOrigin(String webOrigin) { + removeFromAttributeSet(WEB_ORIGINS, webOrigin); + } + @Override public boolean isTotp() { Attribute a = user.getAttribute(KEYCLOAK_TOTP_ATTR); diff --git a/services/src/main/java/org/keycloak/services/managers/ApplicationManager.java b/services/src/main/java/org/keycloak/services/managers/ApplicationManager.java index 93c29599d4..9b89cf4836 100755 --- a/services/src/main/java/org/keycloak/services/managers/ApplicationManager.java +++ b/services/src/main/java/org/keycloak/services/managers/ApplicationManager.java @@ -42,6 +42,11 @@ public class ApplicationManager { resourceUser.addRedirectUri(redirectUri); } } + if (resourceRep.getWebOrigins() != null) { + for (String webOrigin : resourceRep.getWebOrigins()) { + resourceUser.addWebOrigin(webOrigin); + } + } realm.grantRole(resourceUser, loginRole); @@ -97,6 +102,11 @@ public class ApplicationManager { if (redirectUris != null) { resource.getApplicationUser().setRedirectUris(new HashSet(redirectUris)); } + + List webOrigins = rep.getWebOrigins(); + if (webOrigins != null) { + resource.getApplicationUser().setWebOrigins(new HashSet(webOrigins)); + } } public ApplicationRepresentation toRepresentation(ApplicationModel applicationModel) { @@ -113,6 +123,11 @@ public class ApplicationManager { rep.setRedirectUris(new LinkedList(redirectUris)); } + Set webOrigins = applicationModel.getApplicationUser().getWebOrigins(); + if (webOrigins != null) { + rep.setWebOrigins(new LinkedList(webOrigins)); + } + return rep; } diff --git a/services/src/main/java/org/keycloak/services/resources/Cors.java b/services/src/main/java/org/keycloak/services/resources/Cors.java new file mode 100644 index 0000000000..8c28d6fa20 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/resources/Cors.java @@ -0,0 +1,43 @@ +package org.keycloak.services.resources; + +import java.util.Set; + +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.ResponseBuilder; + +import org.jboss.resteasy.spi.HttpRequest; + +/** + * @author Stian Thorgersen + */ +public class Cors { + + private HttpRequest request; + private ResponseBuilder response; + private Set allowedOrigins; + + public Cors(HttpRequest request, ResponseBuilder response) { + this.request = request; + this.response = response; + } + + public static Cors add(HttpRequest request, ResponseBuilder response) { + return new Cors(request, response); + } + + public Cors allowedOrigins(Set allowedOrigins) { + this.allowedOrigins = allowedOrigins; + return this; + } + + public Response build() { + String origin = request.getHttpHeaders().getHeaderString("Origin"); + if (origin == null || allowedOrigins == null || (!allowedOrigins.contains(origin))) { + return response.build(); + } + + response.header("Access-Control-Allow-Origin", origin); + return response.build(); + } + +} 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 d72e80d9d4..7296d8e040 100755 --- a/services/src/main/java/org/keycloak/services/resources/TokenService.java +++ b/services/src/main/java/org/keycloak/services/resources/TokenService.java @@ -415,7 +415,8 @@ public class TokenService { } logger.info("accessRequest SUCCESS"); AccessTokenResponse res = accessTokenResponse(realm.getPrivateKey(), accessCode.getToken()); - return Response.ok(res).build(); + + return Cors.add(request, Response.ok(res)).allowedOrigins(client.getWebOrigins()).build(); } protected AccessTokenResponse accessTokenResponse(PrivateKey privateKey, SkeletonKeyToken token) { From 7d6f88f617ff51c030529d33a1164835d3c8c7cb Mon Sep 17 00:00:00 2001 From: ammendonca Date: Fri, 18 Oct 2013 16:30:45 +0100 Subject: [PATCH 04/11] KEYCLOAK-109: Add support for managing Web Origins and Redirect URIs. --- .../META-INF/resources/admin-ui/css/forms.css | 15 +++++++++ .../admin/js/controllers/applications.js | 14 ++++++++ .../admin/partials/application-detail.html | 32 ++++++++++++++++++- 3 files changed, 60 insertions(+), 1 deletion(-) diff --git a/admin-ui-styles/src/main/resources/META-INF/resources/admin-ui/css/forms.css b/admin-ui-styles/src/main/resources/META-INF/resources/admin-ui/css/forms.css index 92216b7702..5c03ee5a7e 100644 --- a/admin-ui-styles/src/main/resources/META-INF/resources/admin-ui/css/forms.css +++ b/admin-ui-styles/src/main/resources/META-INF/resources/admin-ui/css/forms.css @@ -42,6 +42,13 @@ input[type="password"].error:focus, input[type="email"].error:focus { box-shadow: 0 0 5px #ba1212; } +.input-below { + clear: both; + display: inline-block; + margin-left: 10.9090909090909em; + margin-top: 0.45454545454545em; + padding-left: 3.63636363636364em; +} input[type="button"], button, a.button { @@ -776,3 +783,11 @@ input[type="email"].tiny { .breadcrumb > li + li:before { content: "» "; } + +.item-deletable:hover .btn-delete { + display: inline-block; +} + +.btn-delete { + display: none; +} \ No newline at end of file diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/js/controllers/applications.js b/admin-ui/src/main/resources/META-INF/resources/admin/js/controllers/applications.js index 8c6f1783ab..343a462768 100755 --- a/admin-ui/src/main/resources/META-INF/resources/admin/js/controllers/applications.js +++ b/admin-ui/src/main/resources/META-INF/resources/admin/js/controllers/applications.js @@ -196,6 +196,20 @@ module.controller('ApplicationDetailCtrl', function($scope, realm, application, } }, true); + $scope.deleteWebOrigin = function(index) { + $scope.application.webOrigins.splice(index, 1); + } + $scope.addWebOrigin = function() { + $scope.application.webOrigins.push($scope.newWebOrigin); + $scope.newWebOrigin = ""; + } + $scope.deleteRedirectUri = function(index) { + $scope.application.redirectUris.splice(index, 1); + } + $scope.addRedirectUri = function() { + $scope.application.redirectUris.push($scope.newRedirectUri); + $scope.newRedirectUri = ""; + } $scope.save = function() { if ($scope.applicationForm.$valid) { diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/partials/application-detail.html b/admin-ui/src/main/resources/META-INF/resources/admin/partials/application-detail.html index bc23ca3bf5..8bca6060d2 100755 --- a/admin-ui/src/main/resources/META-INF/resources/admin/partials/application-detail.html +++ b/admin-ui/src/main/resources/META-INF/resources/admin/partials/application-detail.html @@ -50,7 +50,7 @@
- +
+
+ +
+
+ + +
+ + +
+
+
+ +
+
+ + +
+ + +
+
+
+ +
+ + +
+
diff --git a/core/src/main/java/org/keycloak/jaxrs/JaxrsOAuthClient.java b/core/src/main/java/org/keycloak/jaxrs/JaxrsOAuthClient.java index 71b58d7971..0fde58744a 100755 --- a/core/src/main/java/org/keycloak/jaxrs/JaxrsOAuthClient.java +++ b/core/src/main/java/org/keycloak/jaxrs/JaxrsOAuthClient.java @@ -22,7 +22,14 @@ import java.net.URI; public class JaxrsOAuthClient extends AbstractOAuthClient { protected static final Logger logger = Logger.getLogger(JaxrsOAuthClient.class); public Response redirect(UriInfo uriInfo, String redirectUri) { + return redirect(uriInfo, redirectUri, null); + } + + public Response redirect(UriInfo uriInfo, String redirectUri, String path) { String state = getStateCode(); + if (path != null) { + state += "#" + path; + } URI url = UriBuilder.fromUri(authUrl) .queryParam("client_id", clientId) @@ -58,7 +65,7 @@ public class JaxrsOAuthClient extends AbstractOAuthClient { return uriInfo.getQueryParameters().getFirst("code"); } - public void checkStateCookie(UriInfo uriInfo, HttpHeaders headers) { + public String checkStateCookie(UriInfo uriInfo, HttpHeaders headers) { Cookie stateCookie = headers.getCookies().get(stateCookieName); if (stateCookie == null) throw new BadRequestException("state cookie not set"); String state = uriInfo.getQueryParameters().getFirst("state"); @@ -66,5 +73,10 @@ public class JaxrsOAuthClient extends AbstractOAuthClient { if (!state.equals(stateCookie.getValue())) { throw new BadRequestException("state parameter invalid"); } + if (state.indexOf('#') != -1) { + return state.substring(state.indexOf('#') + 1); + } else { + return null; + } } } 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 14d7d501af..45d47d8beb 100755 --- a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java @@ -16,6 +16,7 @@ public class RealmRepresentation { protected Integer accessCodeLifespan; protected Integer accessCodeLifespanUserAction; protected Boolean enabled; + protected Boolean accountManagement; protected Boolean sslNotRequired; protected Boolean cookieLoginAllowed; protected Boolean registrationAllowed; @@ -101,6 +102,14 @@ public class RealmRepresentation { this.enabled = enabled; } + public Boolean isAccountManagement() { + return accountManagement; + } + + public void setAccountManagement(Boolean accountManagement) { + this.accountManagement = accountManagement; + } + public Boolean isSslNotRequired() { return sslNotRequired; } diff --git a/examples/js/testrealm.json b/examples/js/testrealm.json index 2468f48035..38225c618c 100755 --- a/examples/js/testrealm.json +++ b/examples/js/testrealm.json @@ -8,6 +8,7 @@ "sslNotRequired": true, "cookieLoginAllowed": true, "registrationAllowed": true, + "accountManagement": true, "resetPasswordAllowed": 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", diff --git a/forms/src/main/java/org/keycloak/forms/UrlBean.java b/forms/src/main/java/org/keycloak/forms/UrlBean.java index f527d38907..8d25c3d969 100755 --- a/forms/src/main/java/org/keycloak/forms/UrlBean.java +++ b/forms/src/main/java/org/keycloak/forms/UrlBean.java @@ -124,6 +124,10 @@ public class UrlBean { return Urls.accountTotpRemove(baseURI, realm.getId()).toString(); } + public String getLogoutUrl() { + return Urls.accountLogout(baseURI, realm.getId()).toString(); + } + public String getLoginPasswordResetUrl() { return Urls.loginPasswordReset(baseURI, realm.getId()).toString(); } diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/template-main.ftl b/forms/src/main/resources/META-INF/resources/forms/theme/default/template-main.ftl index 18ab4d5b60..19a95ee7fe 100644 --- a/forms/src/main/resources/META-INF/resources/forms/theme/default/template-main.ftl +++ b/forms/src/main/resources/META-INF/resources/forms/theme/default/template-main.ftl @@ -54,6 +54,9 @@ Icon: user ${user.firstName!''} ${user.lastName!''} +
  • + Logout +
  • diff --git a/model/api/src/main/java/org/keycloak/models/Constants.java b/model/api/src/main/java/org/keycloak/models/Constants.java index 52db21f024..b0af34c6ce 100755 --- a/model/api/src/main/java/org/keycloak/models/Constants.java +++ b/model/api/src/main/java/org/keycloak/models/Constants.java @@ -11,4 +11,6 @@ public interface Constants { String APPLICATION_ROLE = "KEYCLOAK_APPLICATION"; String IDENTITY_REQUESTER_ROLE = "KEYCLOAK_IDENTITY_REQUESTER"; String WILDCARD_ROLE = "*"; + + String ACCOUNT_MANAGEMENT_APPLICATION = "Account Management"; } 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 925076abc1..1797bc01a9 100755 --- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java +++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java @@ -13,6 +13,7 @@ import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.models.RealmModel; import org.keycloak.models.RequiredCredentialModel; import org.keycloak.models.UserModel; +import org.keycloak.services.resources.AccountService; import org.keycloak.services.resources.RealmsResource; import org.keycloak.services.resources.SaasService; @@ -61,6 +62,11 @@ public class AuthenticationManager { return createLoginCookie(realm, user, cookieName, cookiePath); } + public NewCookie createAccountIdentityCookie(RealmModel realm, UserModel user, URI uri) { + String cookieName = AccountService.ACCOUNT_IDENTITY_COOKIE; + String cookiePath = uri.getPath(); + return createLoginCookie(realm, user, cookieName, cookiePath); + } protected NewCookie createLoginCookie(RealmModel realm, UserModel user, String cookieName, String cookiePath) { SkeletonKeyToken identityToken = createIdentityToken(realm, user.getLoginName()); @@ -99,6 +105,11 @@ public class AuthenticationManager { expireCookie(SaasService.SAAS_IDENTITY_COOKIE, cookiePath); } + public void expireAccountIdentityCookie(URI uri) { + String cookiePath = uri.getPath(); + expireCookie(AccountService.ACCOUNT_IDENTITY_COOKIE, cookiePath); + } + public void expireCookie(String cookieName, String path) { HttpResponse response = ResteasyProviderFactory.getContextData(HttpResponse.class); if (response == null) { @@ -120,6 +131,11 @@ public class AuthenticationManager { return authenticateIdentityCookie(realm, uriInfo, headers, cookieName); } + public UserModel authenticateAccountIdentityCookie(RealmModel realm, UriInfo uriInfo, HttpHeaders headers) { + String cookieName = AccountService.ACCOUNT_IDENTITY_COOKIE; + return authenticateIdentityCookie(realm, uriInfo, headers, cookieName); + } + public UserModel authenticateSaasIdentity(RealmModel realm, UriInfo uriInfo, HttpHeaders headers) { UserModel user = authenticateSaasIdentityCookie(realm, uriInfo, headers); if (user != null) return user; 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 af6d4db93a..bae0cc2709 100755 --- a/services/src/main/java/org/keycloak/services/managers/RealmManager.java +++ b/services/src/main/java/org/keycloak/services/managers/RealmManager.java @@ -90,6 +90,36 @@ public class RealmManager { if (rep.getDefaultRoles() != null) { realm.updateDefaultRoles(rep.getDefaultRoles()); } + + if (rep.isAccountManagement()) { + enableAccountManagement(realm); + } else { + disableAccountManagement(realm); + } + } + + private void enableAccountManagement(RealmModel realm) { + ApplicationModel application = realm.getApplicationById(Constants.ACCOUNT_MANAGEMENT_APPLICATION); + if (application == null) { + application = realm.addApplication(Constants.ACCOUNT_MANAGEMENT_APPLICATION); + + UserCredentialModel password = new UserCredentialModel(); + password.setType(UserCredentialModel.PASSWORD); + password.setValue(UUID.randomUUID().toString()); // just a random password as we'll never access it + + realm.updateCredential(application.getApplicationUser(), password); + + RoleModel applicationRole = realm.getRole(Constants.APPLICATION_ROLE); + realm.grantRole(application.getApplicationUser(), applicationRole); + } + application.setEnabled(true); + } + + private void disableAccountManagement(RealmModel realm) { + ApplicationModel application = realm.getApplicationNameMap().get(Constants.ACCOUNT_MANAGEMENT_APPLICATION); + if (application != null) { + application.setEnabled(false); // TODO Should we delete the application instead? + } } public RealmModel importRealm(RealmRepresentation rep, UserModel realmCreator) { @@ -214,6 +244,10 @@ public class RealmManager { } } } + + if (rep.isAccountManagement() != null && rep.isAccountManagement()) { + enableAccountManagement(newRealm); + } } public void createRole(RealmModel newRealm, RoleRepresentation roleRep) { @@ -370,6 +404,9 @@ public class RealmManager { rep.setAccessCodeLifespan(realm.getAccessCodeLifespan()); rep.setAccessCodeLifespanUserAction(realm.getAccessCodeLifespanUserAction()); + ApplicationModel accountManagementApplication = realm.getApplicationNameMap().get(Constants.ACCOUNT_MANAGEMENT_APPLICATION); + rep.setAccountManagement(accountManagementApplication != null && accountManagementApplication.isEnabled()); + List defaultRoles = realm.getDefaultRoles(); if (defaultRoles.size() > 0) { String[] d = new String[defaultRoles.size()]; diff --git a/services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java b/services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java index c3de829e81..96d0c10c2e 100755 --- a/services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java +++ b/services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java @@ -37,16 +37,20 @@ public class ResourceAdminManager { } protected boolean logoutResource(RealmModel realm, ApplicationModel resource, String user, ResteasyClient client) { - LogoutAction adminAction = new LogoutAction(TokenIdGenerator.generateId(), System.currentTimeMillis() / 1000 + 30, resource.getName(), user); - String token = new TokenManager().encodeToken(realm, adminAction); - Form form = new Form(); - form.param("token", token); String managementUrl = resource.getManagementUrl(); - logger.info("logout user: " + user + " resource: " + resource.getName() + " url" + managementUrl); - Response response = client.target(managementUrl).queryParam("action", "logout").request().post(Entity.form(form)); - boolean success = response.getStatus() == 204; - response.close(); - return success; + if (managementUrl != null) { + LogoutAction adminAction = new LogoutAction(TokenIdGenerator.generateId(), System.currentTimeMillis() / 1000 + 30, resource.getName(), user); + String token = new TokenManager().encodeToken(realm, adminAction); + Form form = new Form(); + form.param("token", token); + logger.info("logout user: " + user + " resource: " + resource.getName() + " url" + managementUrl); + Response response = client.target(managementUrl).queryParam("action", "logout").request().post(Entity.form(form)); + boolean success = response.getStatus() == 204; + response.close(); + return success; + } else { + return false; + } } } 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 7205d6f73c..2a543238a6 100755 --- a/services/src/main/java/org/keycloak/services/resources/AccountService.java +++ b/services/src/main/java/org/keycloak/services/resources/AccountService.java @@ -21,37 +21,35 @@ */ package org.keycloak.services.resources; +import java.net.URI; import java.util.HashSet; import java.util.Set; -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.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.*; +import javax.ws.rs.core.*; import javax.ws.rs.core.Response.Status; -import javax.ws.rs.core.UriInfo; import javax.ws.rs.ext.Providers; +import org.jboss.resteasy.annotations.cache.NoCache; import org.jboss.resteasy.jose.jws.JWSInput; import org.jboss.resteasy.jose.jws.crypto.RSAProvider; +import org.jboss.resteasy.logging.Logger; import org.jboss.resteasy.spi.HttpRequest; +import org.keycloak.AbstractOAuthClient; +import org.keycloak.jaxrs.JaxrsOAuthClient; +import org.keycloak.models.*; import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.services.email.EmailSender; import org.keycloak.services.managers.AccessCodeEntry; import org.keycloak.services.managers.AuthenticationManager; +import org.keycloak.services.managers.RealmManager; import org.keycloak.services.managers.TokenManager; import org.keycloak.services.messages.Messages; -import org.keycloak.models.RealmModel; -import org.keycloak.models.UserCredentialModel; -import org.keycloak.models.UserModel; import org.keycloak.models.UserModel.RequiredAction; import org.keycloak.services.resources.flows.Flows; import org.keycloak.services.resources.flows.FormFlows; +import org.keycloak.services.resources.flows.Pages; +import org.keycloak.services.resources.flows.Urls; import org.keycloak.services.validation.Validation; import org.picketlink.idm.credential.util.TimeBasedOTP; @@ -60,6 +58,10 @@ import org.picketlink.idm.credential.util.TimeBasedOTP; */ public class AccountService { + private static final Logger logger = Logger.getLogger(AccountService.class); + + public static final String ACCOUNT_IDENTITY_COOKIE = "KEYCLOAK_ACCOUNT_IDENTITY"; + private RealmModel realm; @Context @@ -72,37 +74,64 @@ public class AccountService { private UriInfo uriInfo; @Context - protected Providers providers; + private Providers providers; - protected AuthenticationManager authManager = new AuthenticationManager(); + private AuthenticationManager authManager = new AuthenticationManager(); + + private ApplicationModel application; private TokenManager tokenManager; - public AccountService(RealmModel realm, TokenManager tokenManager) { + public AccountService(RealmModel realm, ApplicationModel application, TokenManager tokenManager) { this.realm = realm; + this.application = application; this.tokenManager = tokenManager; } + private Response forwardToPage(String path, String template) { + UserModel user = getUser(false); + if (user != null) { + return Flows.forms(realm, request, uriInfo).setUser(user).forwardToForm(template); + } else { + return login(path); + } + } + + @Path("") + @GET + public Response accountPage() { + return forwardToPage(null, Pages.ACCOUNT); + } + + @Path("social") + @GET + public Response socialPage() { + return forwardToPage("social", Pages.SOCIAL); + } + + @Path("totp") + @GET + public Response totpPage() { + return forwardToPage("totp", Pages.TOTP); + } + + @Path("password") + @GET + public Response passwordPage() { + return forwardToPage("password", Pages.PASSWORD); + } + @Path("access") @GET public Response accessPage() { - UserModel user = getUserFromAuthManager(); - if (user != null) { - return Flows.forms(realm, request, uriInfo).setUser(user).forwardToAccess(); - } else { - return Response.status(Status.FORBIDDEN).build(); - } + return forwardToPage("access", Pages.ACCESS); } @Path("") @POST @Consumes(MediaType.APPLICATION_FORM_URLENCODED) public Response processAccountUpdate(final MultivaluedMap formData) { - UserModel user = getUserFromAuthManager(); - if (user == null) { - return Response.status(Status.FORBIDDEN).build(); - } - + UserModel user = getUser(true); user.setFirstName(formData.getFirst("firstName")); user.setLastName(formData.getFirst("lastName")); user.setEmail(formData.getFirst("email")); @@ -113,7 +142,7 @@ public class AccountService { @Path("totp-remove") @GET public Response processTotpRemove() { - UserModel user = getUserFromAuthManager(); + UserModel user = getUser(true); user.setTotp(false); return Flows.forms(realm, request, uriInfo).setError("successTotpRemoved").setErrorType(FormFlows.MessageType.SUCCESS) .setUser(user).forwardToTotp(); @@ -123,31 +152,21 @@ public class AccountService { @POST @Consumes(MediaType.APPLICATION_FORM_URLENCODED) public Response processTotpUpdate(final MultivaluedMap formData) { - UserModel user = getUserFromAuthManager(); - if (user == null) { - return Response.status(Status.FORBIDDEN).build(); - } - - FormFlows forms = Flows.forms(realm, request, uriInfo); + UserModel user = getUser(true); String totp = formData.getFirst("totp"); String totpSecret = formData.getFirst("totpSecret"); - String error = null; - + FormFlows forms = Flows.forms(realm, request, uriInfo).setUser(user); if (Validation.isEmpty(totp)) { - error = Messages.MISSING_TOTP; + return forms.setError(Messages.MISSING_TOTP).forwardToTotp(); } else if (!new TimeBasedOTP().validate(totp, totpSecret.getBytes())) { - error = Messages.INVALID_TOTP; - } - - if (error != null) { - return forms.setError(error).setUser(user).forwardToTotp(); + return forms.setError(Messages.INVALID_TOTP).forwardToTotp(); } UserCredentialModel credentials = new UserCredentialModel(); credentials.setType(CredentialRepresentation.TOTP); - credentials.setValue(formData.getFirst("totpSecret")); + credentials.setValue(totpSecret); realm.updateCredential(user, credentials); user.setTotp(true); @@ -160,10 +179,7 @@ public class AccountService { @POST @Consumes(MediaType.APPLICATION_FORM_URLENCODED) public Response processPasswordUpdate(final MultivaluedMap formData) { - UserModel user = getUserFromAuthManager(); - if (user == null) { - return Response.status(Status.FORBIDDEN).build(); - } + UserModel user = getUser(true); FormFlows forms = Flows.forms(realm, request, uriInfo).setUser(user); @@ -172,18 +188,17 @@ public class AccountService { String passwordConfirm = formData.getFirst("password-confirm"); if (Validation.isEmpty(passwordNew)) { - forms.setError(Messages.MISSING_PASSWORD).forwardToPassword(); + return forms.setError(Messages.MISSING_PASSWORD).forwardToPassword(); } else if (!passwordNew.equals(passwordConfirm)) { - forms.setError(Messages.INVALID_PASSWORD_CONFIRM).forwardToPassword(); + return forms.setError(Messages.INVALID_PASSWORD_CONFIRM).forwardToPassword(); } if (Validation.isEmpty(password)) { - forms.setError(Messages.MISSING_PASSWORD).forwardToPassword(); + return forms.setError(Messages.MISSING_PASSWORD).forwardToPassword(); } else if (!realm.validatePassword(user, password)) { - forms.setError(Messages.INVALID_PASSWORD_EXISTING).forwardToPassword(); + return forms.setError(Messages.INVALID_PASSWORD_EXISTING).forwardToPassword(); } - UserCredentialModel credentials = new UserCredentialModel(); credentials.setType(CredentialRepresentation.PASSWORD); credentials.setValue(passwordNew); @@ -193,50 +208,112 @@ public class AccountService { return Flows.forms(realm, request, uriInfo).setUser(user).forwardToPassword(); } - @Path("") + @Path("login-redirect") @GET - public Response accountPage() { - UserModel user = getUserFromAuthManager(); - if (user != null) { - return Flows.forms(realm, request, uriInfo).setUser(user).forwardToAccount(); - } else { - return Response.status(Status.FORBIDDEN).build(); + public Response loginRedirect(@QueryParam("code") String code, + @QueryParam("state") String state, + @QueryParam("error") String error, + @Context HttpHeaders headers) { + try { + if (error != null) { + logger.debug("error from oauth"); + throw new ForbiddenException("error"); + } + if (!realm.isEnabled()) { + logger.debug("realm not enabled"); + throw new ForbiddenException(); + } + UserModel client = application.getApplicationUser(); + if (!client.isEnabled() || !application.isEnabled()) { + logger.debug("account management app not enabled"); + throw new ForbiddenException(); + } + if (code == null) { + logger.debug("code not specified"); + throw new BadRequestException(); + } + if (state == null) { + logger.debug("state not specified"); + throw new BadRequestException(); + } + String path = new JaxrsOAuthClient().checkStateCookie(uriInfo, headers); + + JWSInput input = new JWSInput(code, providers); + boolean verifiedCode = false; + try { + verifiedCode = RSAProvider.verify(input, realm.getPublicKey()); + } catch (Exception ignored) { + logger.debug("Failed to verify signature", ignored); + } + if (!verifiedCode) { + logger.debug("unverified access code"); + throw new BadRequestException(); + } + String key = input.readContent(String.class); + AccessCodeEntry accessCode = tokenManager.pullAccessCode(key); + if (accessCode == null) { + logger.debug("bad access code"); + throw new BadRequestException(); + } + if (accessCode.isExpired()) { + logger.debug("access code expired"); + throw new BadRequestException(); + } + if (!accessCode.getToken().isActive()) { + logger.debug("access token expired"); + throw new BadRequestException(); + } + if (!accessCode.getRealm().getId().equals(realm.getId())) { + logger.debug("bad realm"); + throw new BadRequestException(); + + } + if (!client.getLoginName().equals(accessCode.getClient().getLoginName())) { + logger.debug("bad client"); + throw new BadRequestException(); + } + + UriBuilder redirectBuilder = Urls.accountBase(uriInfo.getBaseUri()); + if (path != null) { + redirectBuilder.path(path); + } + URI redirectUri = redirectBuilder.build(realm.getId()); + + NewCookie cookie = authManager.createAccountIdentityCookie(realm, accessCode.getUser(), Urls.accountBase(uriInfo.getBaseUri()).build(realm.getId())); + return Response.status(302).cookie(cookie).location(redirectUri).build(); + } finally { + authManager.expireCookie(AbstractOAuthClient.OAUTH_TOKEN_REQUEST_STATE, uriInfo.getAbsolutePath().getPath()); } } - @Path("social") + @Path("logout") @GET - public Response socialPage() { - UserModel user = getUserFromAuthManager(); - if (user != null) { - return Flows.forms(realm, request, uriInfo).setUser(user).forwardToSocial(); - } else { - return Response.status(Status.FORBIDDEN).build(); - } + public Response logout() { + // TODO Should use single-sign out via TokenService + URI baseUri = Urls.accountBase(uriInfo.getBaseUri()).build(realm.getId()); + authManager.expireIdentityCookie(realm, uriInfo); + authManager.expireAccountIdentityCookie(baseUri); + return Response.status(302).location(baseUri).build(); } - @Path("totp") - @GET - public Response totpPage() { - UserModel user = getUserFromAuthManager(); - if (user != null) { - return Flows.forms(realm, request, uriInfo).setUser(user).forwardToTotp(); - } else { - return Response.status(Status.FORBIDDEN).build(); - } + private Response login(String path) { + JaxrsOAuthClient oauth = new JaxrsOAuthClient(); + String authUrl = Urls.realmLoginPage(uriInfo.getBaseUri(), realm.getId()).toString(); + oauth.setAuthUrl(authUrl); + + oauth.setClientId(Constants.ACCOUNT_MANAGEMENT_APPLICATION); + + URI accountUri = Urls.accountPageBuilder(uriInfo.getBaseUri()).path(AccountService.class, "loginRedirect").build(realm.getId()); + + oauth.setStateCookiePath(accountUri.getPath()); + return oauth.redirect(uriInfo, accountUri.toString(), path); } - @Path("password") - @GET - public Response passwordPage() { - UserModel user = getUserFromAuthManager(); - if (user == null) { - return Response.status(Status.FORBIDDEN).build(); + private UserModel getUser(boolean required) { + UserModel user = authManager.authenticateAccountIdentityCookie(realm, uriInfo, headers); + if (user == null && required) { + throw new ForbiddenException(); } - return Flows.forms(realm, request, uriInfo).setUser(user).forwardToPassword(); - } - - private UserModel getUserFromAuthManager() { - return authManager.authenticateIdentityCookie(realm, uriInfo, headers); + return user; } } diff --git a/services/src/main/java/org/keycloak/services/resources/RealmsResource.java b/services/src/main/java/org/keycloak/services/resources/RealmsResource.java index 9914622862..54fd5a5b33 100755 --- a/services/src/main/java/org/keycloak/services/resources/RealmsResource.java +++ b/services/src/main/java/org/keycloak/services/resources/RealmsResource.java @@ -1,6 +1,8 @@ package org.keycloak.services.resources; import org.jboss.resteasy.logging.Logger; +import org.keycloak.models.ApplicationModel; +import org.keycloak.models.Constants; import org.keycloak.services.managers.RealmManager; import org.keycloak.services.managers.TokenManager; import org.keycloak.models.KeycloakSession; @@ -66,7 +68,14 @@ public class RealmsResource { logger.debug("realm not found"); throw new NotFoundException(); } - AccountService accountService = new AccountService(realm, tokenManager); + + ApplicationModel application = realm.getApplicationNameMap().get(Constants.ACCOUNT_MANAGEMENT_APPLICATION); + if (application == null || !application.isEnabled()) { + logger.debug("account management not enabled"); + throw new NotFoundException(); + } + + AccountService accountService = new AccountService(realm, application, tokenManager); resourceContext.initResource(accountService); return accountService; } diff --git a/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java b/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java index 67a241e4e6..eeed370453 100755 --- a/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java +++ b/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java @@ -127,7 +127,7 @@ public class RequiredActionsService { UserCredentialModel credentials = new UserCredentialModel(); credentials.setType(CredentialRepresentation.TOTP); - credentials.setValue(formData.getFirst("totpSecret")); + credentials.setValue(totpSecret); realm.updateCredential(user, credentials); user.setTotp(true); 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 2aebb4e84b..fe924fbfe0 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 @@ -141,7 +141,7 @@ public class FormFlows { return Response.status(200).entity("form provider not found").build(); } - private Response forwardToForm(String template) { + public Response forwardToForm(String template) { FormService.FormServiceDataBean formDataBean = new FormService.FormServiceDataBean(realm, userModel, formData, error); formDataBean.setMessageType(messageType); 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 200b746ea3..b9f457aa40 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,12 +35,16 @@ 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"); } public static URI accountPage(URI baseUri, String realmId) { - return accountBase(baseUri).path(AccountService.class, "accountPage").build(realmId); + return accountPageBuilder(baseUri).build(realmId); + } + + public static UriBuilder accountPageBuilder(URI baseUri) { + return accountBase(baseUri).path(AccountService.class, "accountPage"); } public static URI accountPasswordPage(URI baseUri, String realmId) { @@ -59,6 +63,10 @@ public class Urls { return accountBase(baseUri).path(AccountService.class, "processTotpRemove").build(realmId); } + public static URI accountLogout(URI baseUri, String realmId) { + return accountBase(baseUri).path(AccountService.class, "logout").build(realmId); + } + public static URI loginActionUpdatePassword(URI baseUri, String realmId) { return requiredActionsBase(baseUri).path(RequiredActionsService.class, "updatePassword").build(realmId); } diff --git a/services/src/test/java/org/keycloak/test/ApplicationModelTest.java b/services/src/test/java/org/keycloak/test/ApplicationModelTest.java index cc7068b1f4..57c3ba5004 100644 --- a/services/src/test/java/org/keycloak/test/ApplicationModelTest.java +++ b/services/src/test/java/org/keycloak/test/ApplicationModelTest.java @@ -1,23 +1,20 @@ package org.keycloak.test; -import java.util.Iterator; -import java.util.List; - import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; -import org.keycloak.models.ApplicationModel; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.KeycloakSessionFactory; -import org.keycloak.models.RealmModel; -import org.keycloak.models.RoleModel; -import org.keycloak.models.UserModel; +import org.keycloak.models.*; import org.keycloak.representations.idm.ApplicationRepresentation; import org.keycloak.services.managers.ApplicationManager; import org.keycloak.services.managers.RealmManager; import org.keycloak.services.resources.KeycloakApplication; +import java.util.Iterator; +import java.util.List; + +import static org.junit.Assert.assertNotNull; + /** * @author Stian Thorgersen */ diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/AccountTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/AccountTest.java index 3d62d55ed5..4f8e75ada3 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/AccountTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/AccountTest.java @@ -21,11 +21,7 @@ */ package org.keycloak.testsuite.forms; -import org.junit.After; -import org.junit.Assert; -import org.junit.ClassRule; -import org.junit.Rule; -import org.junit.Test; +import org.junit.*; import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.services.managers.RealmManager; import org.keycloak.models.RealmModel; @@ -97,13 +93,12 @@ public class AccountTest { @Test public void changePassword() { - loginPage.open(); + changePasswordPage.open(); loginPage.login("test-user@localhost", "password"); - changePasswordPage.open(); changePasswordPage.changePassword("password", "new-password", "new-password"); - oauth.openLogout(); + changePasswordPage.logout(); loginPage.open(); loginPage.login("test-user@localhost", "password"); @@ -118,10 +113,8 @@ public class AccountTest { @Test public void changeProfile() { - loginPage.open(); - loginPage.login("test-user@localhost", "password"); - profilePage.open(); + loginPage.login("test-user@localhost", "password"); Assert.assertEquals("", profilePage.getFirstName()); Assert.assertEquals("", profilePage.getLastName()); @@ -136,10 +129,8 @@ public class AccountTest { @Test public void setupTotp() { - loginPage.open(); - loginPage.login("test-user@localhost", "password"); - totpPage.open(); + loginPage.login("test-user@localhost", "password"); Assert.assertTrue(totpPage.isCurrent()); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AbstractAccountPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AbstractAccountPage.java new file mode 100644 index 0000000000..9860b14078 --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AbstractAccountPage.java @@ -0,0 +1,42 @@ +/* + * 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.pages; + +import org.junit.Assert; +import org.keycloak.testsuite.rule.WebResource; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.FindBy; + +/** + * @author Stian Thorgersen + */ +public abstract class AbstractAccountPage extends AbstractPage { + + @FindBy(linkText = "Logout") + private WebElement logoutLink; + + public void logout() { + logoutLink.click(); + } + +} diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/Page.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AbstractPage.java similarity index 97% rename from testsuite/integration/src/test/java/org/keycloak/testsuite/pages/Page.java rename to testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AbstractPage.java index d38e2acfc1..1183ece12a 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/Page.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AbstractPage.java @@ -28,7 +28,7 @@ import org.openqa.selenium.WebDriver; /** * @author Stian Thorgersen */ -public abstract class Page { +public abstract class AbstractPage { @WebResource protected WebDriver driver; diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountPasswordPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountPasswordPage.java index b53e88ff9a..0a94649c73 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountPasswordPage.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountPasswordPage.java @@ -28,7 +28,7 @@ import org.openqa.selenium.support.FindBy; /** * @author Stian Thorgersen */ -public class AccountPasswordPage extends Page { +public class AccountPasswordPage extends AbstractAccountPage { private static String PATH = Constants.AUTH_SERVER_ROOT + "/rest/realms/test/account/password"; diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountTotpPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountTotpPage.java index 11cc502362..171e6efa4f 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountTotpPage.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountTotpPage.java @@ -28,7 +28,7 @@ import org.openqa.selenium.support.FindBy; /** * @author Stian Thorgersen */ -public class AccountTotpPage extends Page { +public class AccountTotpPage extends AbstractAccountPage { private static String PATH = Constants.AUTH_SERVER_ROOT + "/rest/realms/test/account/totp"; diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountUpdateProfilePage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountUpdateProfilePage.java index c890f2ff72..7c99e1bc53 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountUpdateProfilePage.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountUpdateProfilePage.java @@ -28,7 +28,7 @@ import org.openqa.selenium.support.FindBy; /** * @author Stian Thorgersen */ -public class AccountUpdateProfilePage extends Page { +public class AccountUpdateProfilePage extends AbstractAccountPage { private static String PATH = Constants.AUTH_SERVER_ROOT + "/rest/realms/test/account"; diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AppPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AppPage.java index 4270640740..3faa197818 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AppPage.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AppPage.java @@ -25,7 +25,7 @@ package org.keycloak.testsuite.pages; /** * @author Stian Thorgersen */ -public class AppPage extends Page { +public class AppPage extends AbstractPage { private String baseUrl = "http://localhost:8081/app"; diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/ErrorPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/ErrorPage.java index bfe3201984..cc06da5d3e 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/ErrorPage.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/ErrorPage.java @@ -23,14 +23,13 @@ package org.keycloak.testsuite.pages; import org.keycloak.testsuite.OAuthClient; import org.keycloak.testsuite.rule.WebResource; -import org.openqa.selenium.By; import org.openqa.selenium.WebElement; import org.openqa.selenium.support.FindBy; /** * @author Stian Thorgersen */ -public class ErrorPage extends Page { +public class ErrorPage extends AbstractPage { @WebResource protected OAuthClient oauth; diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginConfigTotpPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginConfigTotpPage.java index f2e23003cc..5b1613ad0c 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginConfigTotpPage.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginConfigTotpPage.java @@ -27,7 +27,7 @@ import org.openqa.selenium.support.FindBy; /** * @author Stian Thorgersen */ -public class LoginConfigTotpPage extends Page { +public class LoginConfigTotpPage extends AbstractPage { @FindBy(id = "totpSecret") private WebElement totpSecret; diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginPage.java index d099690763..28e87cf8cb 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginPage.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginPage.java @@ -30,7 +30,7 @@ import org.openqa.selenium.support.FindBy; /** * @author Stian Thorgersen */ -public class LoginPage extends Page { +public class LoginPage extends AbstractPage { @WebResource protected OAuthClient oauth; diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginPasswordResetPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginPasswordResetPage.java index 45fc30a512..fc62c3ca19 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginPasswordResetPage.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginPasswordResetPage.java @@ -27,7 +27,7 @@ import org.openqa.selenium.support.FindBy; /** * @author Stian Thorgersen */ -public class LoginPasswordResetPage extends Page { +public class LoginPasswordResetPage extends AbstractPage { @FindBy(id = "username") private WebElement usernameInput; diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginPasswordUpdatePage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginPasswordUpdatePage.java index 100b891c1f..be5df8c0fb 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginPasswordUpdatePage.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginPasswordUpdatePage.java @@ -27,7 +27,7 @@ import org.openqa.selenium.support.FindBy; /** * @author Stian Thorgersen */ -public class LoginPasswordUpdatePage extends Page { +public class LoginPasswordUpdatePage extends AbstractPage { @FindBy(id = "password-new") private WebElement newPasswordInput; diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginTotpPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginTotpPage.java index 67b3801c5f..2a7ed578ca 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginTotpPage.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginTotpPage.java @@ -28,7 +28,7 @@ import org.openqa.selenium.support.FindBy; /** * @author Stian Thorgersen */ -public class LoginTotpPage extends Page { +public class LoginTotpPage extends AbstractPage { @FindBy(id = "totp") private WebElement totpInput; diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginUpdateProfilePage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginUpdateProfilePage.java index 9a4b45465c..d25959d6e1 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginUpdateProfilePage.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginUpdateProfilePage.java @@ -27,7 +27,7 @@ import org.openqa.selenium.support.FindBy; /** * @author Stian Thorgersen */ -public class LoginUpdateProfilePage extends Page { +public class LoginUpdateProfilePage extends AbstractPage { @FindBy(id = "firstName") private WebElement firstNameInput; diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/OAuthGrantPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/OAuthGrantPage.java index 8749a918ce..7fdbe068f1 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/OAuthGrantPage.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/OAuthGrantPage.java @@ -27,7 +27,7 @@ import org.openqa.selenium.support.FindBy; /** * @author Stian Thorgersen */ -public class OAuthGrantPage extends Page { +public class OAuthGrantPage extends AbstractPage { @FindBy(css = "input[name=\"accept\"]") private WebElement acceptButton; diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/RegisterPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/RegisterPage.java index 7ff4d7f97e..292a462aa4 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/RegisterPage.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/RegisterPage.java @@ -27,7 +27,7 @@ import org.openqa.selenium.support.FindBy; /** * @author Stian Thorgersen */ -public class RegisterPage extends Page { +public class RegisterPage extends AbstractPage { @FindBy(id = "firstName") private WebElement firstNameInput; diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/VerifyEmailPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/VerifyEmailPage.java index 5e8c24c65b..cfcfbb4fdb 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/VerifyEmailPage.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/VerifyEmailPage.java @@ -29,7 +29,7 @@ import org.openqa.selenium.support.FindBy; /** * @author Viliam Rockai */ -public class VerifyEmailPage extends Page { +public class VerifyEmailPage extends AbstractPage { @WebResource protected OAuthClient oauth; diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/WebRule.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/WebRule.java index d9f789d578..88301c5417 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/WebRule.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/WebRule.java @@ -25,7 +25,7 @@ import java.lang.reflect.Field; import org.junit.rules.ExternalResource; import org.keycloak.testsuite.OAuthClient; -import org.keycloak.testsuite.pages.Page; +import org.keycloak.testsuite.pages.AbstractPage; import org.openqa.selenium.WebDriver; import org.openqa.selenium.chrome.ChromeDriver; import org.openqa.selenium.firefox.FirefoxDriver; @@ -78,7 +78,7 @@ public class WebRule extends ExternalResource { Class type = f.getType(); if (type.equals(WebDriver.class)) { set(f, o, driver); - } else if (Page.class.isAssignableFrom(type)) { + } else if (AbstractPage.class.isAssignableFrom(type)) { set(f, o, getPage(f.getType())); } else if (type.equals(OAuthClient.class)) { set(f, o, oauth); diff --git a/testsuite/integration/src/test/resources/testrealm.json b/testsuite/integration/src/test/resources/testrealm.json index 941c5db836..161f11e9c6 100755 --- a/testsuite/integration/src/test/resources/testrealm.json +++ b/testsuite/integration/src/test/resources/testrealm.json @@ -8,6 +8,7 @@ "sslNotRequired": true, "cookieLoginAllowed": true, "registrationAllowed": true, + "accountManagement": true, "resetPasswordAllowed": 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", From 1971fa03264f21d375bfa2598e27816b8501229a Mon Sep 17 00:00:00 2001 From: Stian Thorgersen Date: Mon, 21 Oct 2013 09:57:02 +0100 Subject: [PATCH 08/11] Added log4j to dev KeycloakServer --- pom.xml | 5 +++++ testsuite/integration/pom.xml | 4 ++++ testsuite/integration/src/main/resources/log4j.properties | 5 +++++ 3 files changed, 14 insertions(+) create mode 100644 testsuite/integration/src/main/resources/log4j.properties diff --git a/pom.xml b/pom.xml index 61503c50f2..da29152c6a 100755 --- a/pom.xml +++ b/pom.xml @@ -192,6 +192,11 @@ jboss-logging ${jboss.logging.version} + + log4j + log4j + 1.2.17 + junit junit diff --git a/testsuite/integration/pom.xml b/testsuite/integration/pom.xml index 3614016c9b..0e121c7026 100644 --- a/testsuite/integration/pom.xml +++ b/testsuite/integration/pom.xml @@ -101,6 +101,10 @@ org.picketlink picketlink-config + + log4j + log4j + org.jboss.resteasy resteasy-jaxrs diff --git a/testsuite/integration/src/main/resources/log4j.properties b/testsuite/integration/src/main/resources/log4j.properties new file mode 100644 index 0000000000..b3c1c5cea6 --- /dev/null +++ b/testsuite/integration/src/main/resources/log4j.properties @@ -0,0 +1,5 @@ +log4j.rootLogger=debug, stdout + +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d{HH:mm:ss,SSS} %-5p [%c] %m%n \ No newline at end of file From c28f30915bcb13fb2ddd0ce8becb6d1645231cde Mon Sep 17 00:00:00 2001 From: Stian Thorgersen Date: Mon, 21 Oct 2013 10:24:04 +0100 Subject: [PATCH 09/11] Added test for registration on first social login --- .../testsuite/pages/RegisterPage.java | 16 ++++++++++ .../testsuite/social/SocialLoginTest.java | 32 +++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/RegisterPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/RegisterPage.java index 292a462aa4..c1b52c54be 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/RegisterPage.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/RegisterPage.java @@ -85,6 +85,22 @@ public class RegisterPage extends AbstractPage { return loginErrorMessage != null ? loginErrorMessage.getText() : null; } + public String getFirstName() { + return firstNameInput.getAttribute("value"); + } + + public String getLastName() { + return lastNameInput.getAttribute("value"); + } + + public String getEmail() { + return emailInput.getAttribute("value"); + } + + public String getUsername() { + return usernameInput.getAttribute("value"); + } + public boolean isCurrent() { return driver.getTitle().equals("Register with test"); } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/social/SocialLoginTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/social/SocialLoginTest.java index f4cc01ce89..8936763fe2 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/social/SocialLoginTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/social/SocialLoginTest.java @@ -35,6 +35,7 @@ import org.keycloak.testsuite.OAuthClient.AccessTokenResponse; import org.keycloak.testsuite.pages.AppPage; import org.keycloak.testsuite.pages.AppPage.RequestType; import org.keycloak.testsuite.pages.LoginPage; +import org.keycloak.testsuite.pages.RegisterPage; import org.keycloak.testsuite.rule.KeycloakRule; import org.keycloak.testsuite.rule.KeycloakRule.KeycloakSetup; import org.keycloak.testsuite.rule.WebResource; @@ -68,6 +69,9 @@ public class SocialLoginTest { @WebResource protected LoginPage loginPage; + @WebResource + protected RegisterPage registerPage; + @WebResource protected OAuthClient oauth; @@ -97,4 +101,32 @@ public class SocialLoginTest { Assert.assertTrue(token.getRealmAccess().isUserInRole("user")); } + @Test + public void registerRequired() { + keycloakRule.configure(new KeycloakSetup() { + @Override + public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { + appRealm.setAutomaticRegistrationAfterSocialLogin(false); + } + }); + + loginPage.open(); + + loginPage.clickSocial("dummy"); + + driver.findElement(By.id("username")).sendKeys("dummy-user"); + driver.findElement(By.id("submit")).click(); + + registerPage.isCurrent(); + + Assert.assertEquals("", registerPage.getFirstName()); + Assert.assertEquals("", registerPage.getLastName()); + Assert.assertEquals("dummy-user@dummy-social", registerPage.getEmail()); + Assert.assertEquals("dummy-user", registerPage.getUsername()); + + registerPage.register("Dummy", "User", "dummy-user@dummy-social", "dummy-user", "password", "password"); + + Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); + } + } From 0c4df883fc19716fb4982368af48ef99c78c1613 Mon Sep 17 00:00:00 2001 From: Stian Thorgersen Date: Mon, 21 Oct 2013 10:49:36 +0100 Subject: [PATCH 10/11] Fixed SocialLoginTest --- .../testsuite/social/SocialLoginTest.java | 31 ++++++++++++------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/social/SocialLoginTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/social/SocialLoginTest.java index 8936763fe2..056af5c30f 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/social/SocialLoginTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/social/SocialLoginTest.java @@ -110,23 +110,32 @@ public class SocialLoginTest { } }); - loginPage.open(); + try { + loginPage.open(); - loginPage.clickSocial("dummy"); + loginPage.clickSocial("dummy"); - driver.findElement(By.id("username")).sendKeys("dummy-user"); - driver.findElement(By.id("submit")).click(); + driver.findElement(By.id("username")).sendKeys("dummy-user-reg"); + driver.findElement(By.id("submit")).click(); - registerPage.isCurrent(); + registerPage.isCurrent(); - Assert.assertEquals("", registerPage.getFirstName()); - Assert.assertEquals("", registerPage.getLastName()); - Assert.assertEquals("dummy-user@dummy-social", registerPage.getEmail()); - Assert.assertEquals("dummy-user", registerPage.getUsername()); + Assert.assertEquals("", registerPage.getFirstName()); + Assert.assertEquals("", registerPage.getLastName()); + Assert.assertEquals("dummy-user-reg@dummy-social", registerPage.getEmail()); + Assert.assertEquals("dummy-user-reg", registerPage.getUsername()); - registerPage.register("Dummy", "User", "dummy-user@dummy-social", "dummy-user", "password", "password"); + registerPage.register("Dummy", "User", "dummy-user-reg@dummy-social", "dummy-user-reg", "password", "password"); - Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); + Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); + } finally { + keycloakRule.configure(new KeycloakSetup() { + @Override + public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { + appRealm.setAutomaticRegistrationAfterSocialLogin(true); + } + }); + } } } From 115c0bdeca9c08964a4d22083fd15badd4020c78 Mon Sep 17 00:00:00 2001 From: Stian Thorgersen Date: Mon, 21 Oct 2013 11:50:29 +0100 Subject: [PATCH 11/11] Converted QR servlet into JAX-RS resource --- .../java/org/keycloak/forms/QRServlet.java | 70 ------------------- .../java/org/keycloak/forms/TotpBean.java | 2 +- services/pom.xml | 4 ++ .../resources/KeycloakApplication.java | 1 + .../services/resources/QRCodeResource.java | 52 ++++++++++++++ 5 files changed, 58 insertions(+), 71 deletions(-) delete mode 100644 forms/src/main/java/org/keycloak/forms/QRServlet.java create mode 100644 services/src/main/java/org/keycloak/services/resources/QRCodeResource.java diff --git a/forms/src/main/java/org/keycloak/forms/QRServlet.java b/forms/src/main/java/org/keycloak/forms/QRServlet.java deleted file mode 100644 index f1fddf635b..0000000000 --- a/forms/src/main/java/org/keycloak/forms/QRServlet.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * 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.forms; - -import java.io.IOException; - -import javax.servlet.ServletException; -import javax.servlet.annotation.WebServlet; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.jboss.resteasy.logging.Logger; - -import com.google.zxing.BarcodeFormat; -import com.google.zxing.client.j2se.MatrixToImageWriter; -import com.google.zxing.common.BitMatrix; -import com.google.zxing.qrcode.QRCodeWriter; - -/** - * @author Stian Thorgersen - */ -@WebServlet(urlPatterns = "/forms/qrcode") -public class QRServlet extends HttpServlet { - - private static final long serialVersionUID = 1L; - - private static final Logger log = Logger.getLogger(QRServlet.class); - - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { - String[] size = req.getParameter("size").split("x"); - int width = Integer.parseInt(size[0]); - int height = Integer.parseInt(size[1]); - - String contents = req.getParameter("contents"); - - try { - QRCodeWriter writer = new QRCodeWriter(); - - BitMatrix bitMatrix = writer.encode(contents, BarcodeFormat.QR_CODE, width, height); - - MatrixToImageWriter.writeToStream(bitMatrix, "png", resp.getOutputStream()); - resp.setContentType("image/png"); - } catch (Exception e) { - log.warn("Failed to generate qr code", e); - resp.sendError(500); - } - } - -} diff --git a/forms/src/main/java/org/keycloak/forms/TotpBean.java b/forms/src/main/java/org/keycloak/forms/TotpBean.java index 9919ff4d23..a283c2e45e 100644 --- a/forms/src/main/java/org/keycloak/forms/TotpBean.java +++ b/forms/src/main/java/org/keycloak/forms/TotpBean.java @@ -78,7 +78,7 @@ public class TotpBean { public String getTotpSecretQrCodeUrl() throws UnsupportedEncodingException { String contents = URLEncoder.encode("otpauth://totp/keycloak?secret=" + totpSecretEncoded, "utf-8"); - return contextUrl + "/forms/qrcode" + "?size=246x246&contents=" + contents; + return contextUrl + "/rest/qrcode" + "?size=246x246&contents=" + contents; } public UserBean getUser() { diff --git a/services/pom.xml b/services/pom.xml index 187d7be695..52d2ab85e4 100755 --- a/services/pom.xml +++ b/services/pom.xml @@ -165,6 +165,10 @@ de.flapdoodle.embed.mongo provided + + com.google.zxing + javase + junit junit diff --git a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java index 7095eab7b4..fba3aa9e83 100755 --- a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java +++ b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java @@ -50,6 +50,7 @@ public class KeycloakApplication extends Application { singletons.add(new SaasService(tokenManager)); singletons.add(new SocialResource(tokenManager, new SocialRequestManager())); classes.add(SkeletonKeyContextResolver.class); + classes.add(QRCodeResource.class); } protected KeycloakSessionFactory createSessionFactory() { diff --git a/services/src/main/java/org/keycloak/services/resources/QRCodeResource.java b/services/src/main/java/org/keycloak/services/resources/QRCodeResource.java new file mode 100644 index 0000000000..9d021117eb --- /dev/null +++ b/services/src/main/java/org/keycloak/services/resources/QRCodeResource.java @@ -0,0 +1,52 @@ +package org.keycloak.services.resources; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.WriterException; +import com.google.zxing.client.j2se.MatrixToImageWriter; +import com.google.zxing.common.BitMatrix; +import com.google.zxing.qrcode.QRCodeWriter; + +import javax.servlet.ServletException; +import javax.ws.rs.*; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.StreamingOutput; +import java.io.IOException; +import java.io.OutputStream; + +/** + * @author Stian Thorgersen + */ +@Path("/qrcode") +public class QRCodeResource { + + @GET + @Produces("image/png") + public Response createQrCode(@QueryParam("contents") String contents, @QueryParam("size") String size) throws ServletException, IOException, WriterException { + int width = 256; + int height = 256; + + if (size != null) { + String[] s = size.split("x"); + width = Integer.parseInt(s[0]); + height = Integer.parseInt(s[1]); + } + + if (contents == null) { + return Response.status(Response.Status.BAD_REQUEST).build(); + } + + QRCodeWriter writer = new QRCodeWriter(); + final BitMatrix bitMatrix = writer.encode(contents, BarcodeFormat.QR_CODE, width, height); + + StreamingOutput stream = new StreamingOutput() { + @Override + public void write(OutputStream os) throws IOException, + WebApplicationException { + MatrixToImageWriter.writeToStream(bitMatrix, "png", os); + } + }; + + return Response.ok(stream).build(); + } + +}