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 6b3afd32e6..92216b7702 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 @@ -344,6 +344,7 @@ fieldset.border-top { border-style: solid; border-color: #e9e8e8; padding-top: 2em; + clear: both; } .form-group { display: block; diff --git a/admin-ui-styles/src/main/resources/META-INF/resources/admin-ui/css/forms.less b/admin-ui-styles/src/main/resources/META-INF/resources/admin-ui/css/forms.less index 962e7b1d42..e25d6a0048 100644 --- a/admin-ui-styles/src/main/resources/META-INF/resources/admin-ui/css/forms.less +++ b/admin-ui-styles/src/main/resources/META-INF/resources/admin-ui/css/forms.less @@ -400,6 +400,7 @@ fieldset.border-top { border-style: solid; border-color: #e9e8e8; padding-top: 2em; + clear: both; } .form-group { diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/partials/application-list.html b/admin-ui/src/main/resources/META-INF/resources/admin/partials/application-list.html index 174f65d544..bf79f8bccd 100755 --- a/admin-ui/src/main/resources/META-INF/resources/admin/partials/application-list.html +++ b/admin-ui/src/main/resources/META-INF/resources/admin/partials/application-list.html @@ -11,10 +11,9 @@

Applications

- - + diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/partials/application-scope-mappings.html b/admin-ui/src/main/resources/META-INF/resources/admin/partials/application-scope-mappings.html index a3884d5eb5..cfa29f7f44 100755 --- a/admin-ui/src/main/resources/META-INF/resources/admin/partials/application-scope-mappings.html +++ b/admin-ui/src/main/resources/META-INF/resources/admin/partials/application-scope-mappings.html @@ -52,7 +52,7 @@
-
diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/partials/realm-credentials.html b/admin-ui/src/main/resources/META-INF/resources/admin/partials/realm-credentials.html index fa1a42c338..5748a86e3d 100755 --- a/admin-ui/src/main/resources/META-INF/resources/admin/partials/realm-credentials.html +++ b/admin-ui/src/main/resources/META-INF/resources/admin/partials/realm-credentials.html @@ -12,36 +12,38 @@
-

Realm: {{realm.realm}}

-

+ +

{{realm.realm}} Credentials

-
- Required Credentials -
- +
+
+
- +
-
- +
+
- +
-
- +
+
- +
diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/partials/realm-detail.html b/admin-ui/src/main/resources/META-INF/resources/admin/partials/realm-detail.html index b740e8cec8..f3d97f8863 100755 --- a/admin-ui/src/main/resources/META-INF/resources/admin/partials/realm-detail.html +++ b/admin-ui/src/main/resources/META-INF/resources/admin/partials/realm-detail.html @@ -32,7 +32,7 @@
- +
Login Options
- +
- +
- +
- +
-

Realm: {{realm.realm}}

-

+ +

{{realm.realm}} Token Settings

Token Settings @@ -48,7 +52,6 @@
diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/partials/role-detail.html b/admin-ui/src/main/resources/META-INF/resources/admin/partials/role-detail.html index 0aeae23552..1494078947 100755 --- a/admin-ui/src/main/resources/META-INF/resources/admin/partials/role-detail.html +++ b/admin-ui/src/main/resources/META-INF/resources/admin/partials/role-detail.html @@ -4,20 +4,33 @@
-

New Realm Role

-

Realm Role {{role.name}}

+ +

{{realm.realm}} {{role.name}}

+ +

Add Realm Role

* Required fields

-
- Details +
- * +
Description
- + +
@@ -43,7 +56,6 @@
diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/partials/role-list.html b/admin-ui/src/main/resources/META-INF/resources/admin/partials/role-list.html index f2d0a3e5f6..6f8fedb377 100755 --- a/admin-ui/src/main/resources/META-INF/resources/admin/partials/role-list.html +++ b/admin-ui/src/main/resources/META-INF/resources/admin/partials/role-list.html @@ -18,14 +18,16 @@
  • Roles
  • {{realm.realm}} Roles

    +
    Table of realm applications No configured applications...
    @@ -23,6 +22,10 @@ Icon: search
    +
    + Add + +
    - @@ -58,7 +60,7 @@ - + diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/partials/role-mappings.html b/admin-ui/src/main/resources/META-INF/resources/admin/partials/role-mappings.html index 9f58a7ee74..665c89a48a 100755 --- a/admin-ui/src/main/resources/META-INF/resources/admin/partials/role-mappings.html +++ b/admin-ui/src/main/resources/META-INF/resources/admin/partials/role-mappings.html @@ -19,8 +19,8 @@

    {{user.username}}'s Role Mappings

    All fields required

    - Realm Roles -
    +
    + Realm Roles
    @@ -58,11 +58,11 @@
    -
    +
    - - @@ -73,11 +73,11 @@
    - -
    diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/partials/user-detail.html b/admin-ui/src/main/resources/META-INF/resources/admin/partials/user-detail.html index a62c47405d..792abfb9b2 100755 --- a/admin-ui/src/main/resources/META-INF/resources/admin/partials/user-detail.html +++ b/admin-ui/src/main/resources/META-INF/resources/admin/partials/user-detail.html @@ -3,31 +3,38 @@
    -
    -

    New User

    -

    User {{user.username}}

    -

    * Required fields

    + +

    Add User

    +

    * Required fields

    + + +

    {{user.username}}'s Attributes

    + -
    - Attributes +
    - * +
    @@ -69,7 +76,6 @@
    diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/partials/user-list.html b/admin-ui/src/main/resources/META-INF/resources/admin/partials/user-list.html index d87de2ec2a..9ea14e7c39 100755 --- a/admin-ui/src/main/resources/META-INF/resources/admin/partials/user-list.html +++ b/admin-ui/src/main/resources/META-INF/resources/admin/partials/user-list.html @@ -2,37 +2,44 @@
    -
    +
    -

    Query Users

    + +

    {{realm.realm}} Users

    No configured realm roles...
    {{role.name}} {{role.description}}
    - + - - - - - - - - - - - + + + + + + + + + + + diff --git a/forms/src/main/java/org/keycloak/forms/OAuthGrantBean.java b/forms/src/main/java/org/keycloak/forms/OAuthGrantBean.java new file mode 100644 index 0000000000..ce7bf04da8 --- /dev/null +++ b/forms/src/main/java/org/keycloak/forms/OAuthGrantBean.java @@ -0,0 +1,88 @@ +/* + * 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.util.ArrayList; +import java.util.List; + +import javax.ws.rs.core.MultivaluedMap; + +import org.keycloak.models.RoleModel; +import org.keycloak.models.UserModel; + +/** + * @author Viliam Rockai + */ +public class OAuthGrantBean { + + private MultivaluedMap resourceRolesRequested; + private List realmRolesRequested; + private UserModel client; + private String oAuthCode; + private String action; + + public String getAction() { + return action; + } + + public void setAction(String action) { + this.action = action; + } + + public List getResourceNames(){ + return new ArrayList(resourceRolesRequested.keySet()); + } + + public MultivaluedMap getResourceRolesRequested() { + return resourceRolesRequested; + } + + public void setResourceRolesRequested(MultivaluedMap resourceRolesRequested) { + this.resourceRolesRequested = resourceRolesRequested; + } + + public List getRealmRolesRequested() { + return realmRolesRequested; + } + + public void setRealmRolesRequested(List realmRolesRequested) { + this.realmRolesRequested = realmRolesRequested; + } + + public UserModel getClient() { + return client; + } + + public void setClient(UserModel client) { + this.client = client; + } + + // lowercase "o" needed for FM template to access the property + public String getoAuthCode() { + return oAuthCode; + } + + public void setoAuthCode(String oAuthCode) { + this.oAuthCode = oAuthCode; + } + +} diff --git a/forms/src/main/java/org/keycloak/forms/UrlBean.java b/forms/src/main/java/org/keycloak/forms/UrlBean.java index cb82aaa6ba..0fb83c0c34 100644 --- a/forms/src/main/java/org/keycloak/forms/UrlBean.java +++ b/forms/src/main/java/org/keycloak/forms/UrlBean.java @@ -108,8 +108,16 @@ public class UrlBean { } } - public String getPasswordResetUrl() { - return Urls.accountPasswordReset(baseURI, realm.getId()).toString(); + public String getLoginUpdatePasswordUrl() { + return Urls.loginActionUpdatePassword(baseURI, realm.getId()).toString(); + } + + public String getLoginUpdateTotpUrl() { + return Urls.loginActionUpdateTotp(baseURI, realm.getId()).toString(); + } + + public String getLoginUpdateProfileUrl() { + return Urls.loginActionUpdateProfile(baseURI, realm.getId()).toString(); } public String getSocialUrl() { @@ -124,8 +132,12 @@ public class UrlBean { return Urls.accountTotpRemove(baseURI, realm.getId()).toString(); } - public String getEmailVerificationUrl() { - return Urls.accountEmailVerification(baseURI, realm.getId()).toString(); + public String getLoginPasswordResetUrl() { + return Urls.loginPasswordReset(baseURI, realm.getId()).toString(); + } + + public String getLoginEmailVerificationUrl() { + return Urls.loginActionEmailVerification(baseURI, realm.getId()).toString(); } } diff --git a/forms/src/main/java/org/keycloak/service/FormServiceImpl.java b/forms/src/main/java/org/keycloak/service/FormServiceImpl.java index 27c2df1bd8..9489448ebe 100644 --- a/forms/src/main/java/org/keycloak/service/FormServiceImpl.java +++ b/forms/src/main/java/org/keycloak/service/FormServiceImpl.java @@ -34,6 +34,7 @@ import freemarker.template.TemplateException; import org.jboss.resteasy.logging.Logger; import org.keycloak.forms.ErrorBean; import org.keycloak.forms.LoginBean; +import org.keycloak.forms.OAuthGrantBean; import org.keycloak.forms.RealmBean; import org.keycloak.forms.RegisterBean; import org.keycloak.forms.SocialBean; @@ -42,7 +43,6 @@ import org.keycloak.forms.TotpBean; import org.keycloak.forms.UrlBean; import org.keycloak.forms.UserBean; import org.keycloak.services.FormService; -import org.keycloak.services.resources.flows.FormFlows; import org.keycloak.services.resources.flows.Pages; /** @@ -71,6 +71,7 @@ public class FormServiceImpl implements FormService { commandMap.put(Pages.LOGIN_TOTP, new CommandLoginTotp()); commandMap.put(Pages.LOGIN_VERIFY_EMAIL, new CommandLoginTotp()); commandMap.put(Pages.ERROR, new CommandError()); + commandMap.put(Pages.OAUTH_GRANT, new CommandOAuthGrant()); } public String getId(){ @@ -259,6 +260,20 @@ public class FormServiceImpl implements FormService { } } + private class CommandOAuthGrant implements Command { + public void exec(Map attributes, FormServiceDataBean dataBean) { + + OAuthGrantBean oauth = new OAuthGrantBean(); + oauth.setAction(dataBean.getOAuthAction()); + oauth.setResourceRolesRequested(dataBean.getOAuthResourceRolesRequested()); + oauth.setClient(dataBean.getOAuthClient()); + oauth.setoAuthCode(dataBean.getOAuthCode()); + oauth.setRealmRolesRequested(dataBean.getOAuthRealmRolesRequested()); + + attributes.put("oauth", oauth); + } + } + private interface Command { public void exec(Map attributes, FormServiceDataBean dataBean); } diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/css/forms.css b/forms/src/main/resources/META-INF/resources/forms/theme/default/css/forms.css index f3fadb5a8c..0d6bfbf985 100644 --- a/forms/src/main/resources/META-INF/resources/forms/theme/default/css/forms.css +++ b/forms/src/main/resources/META-INF/resources/forms/theme/default/css/forms.css @@ -102,6 +102,16 @@ a.button.disabled { font-weight: normal; letter-spacing: 0.06363636363636em; } + +input[type="button"].btn-secondary, +button.btn-secondary, +input[type="submit"].btn-secondary, +a.button.btn-secondary { + background-color: #EEEEEE; + border-color: #BBBBBB; + color: #4D5258; +} + input[type="button"].disabled:hover, input[type="submit"].disabled:hover, button.disabled:hover, 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 9b3cca5326..2b1c1c5c9a 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 @@ -27,7 +27,7 @@
  • 3Enter the one-time-password provided by Google Authenticator below and click Submit to finish the setup.

    - +
    diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/login-oauth-grant.ftl b/forms/src/main/resources/META-INF/resources/forms/theme/default/login-oauth-grant.ftl index c115777669..edc7ef76a2 100755 --- a/forms/src/main/resources/META-INF/resources/forms/theme/default/login-oauth-grant.ftl +++ b/forms/src/main/resources/META-INF/resources/forms/theme/default/login-oauth-grant.ftl @@ -11,20 +11,32 @@ <#elseif section = "form">
    -

    This application requests access to:

    +

    ${oauth.client.loginName} requests access to:

      -
    • - View basic information about your account -
    • -
    • - View your email address -
    • + <#list oauth.realmRolesRequested as role> +
    • + ${role.description} +
    • +
    + + <#list oauth.resourceRolesRequested?keys as resourceRole> +

    ${resourceRole} requests access to:

    +
      + <#list oauth.resourceRolesRequested[resourceRole] as role> +
    • + ${role.description} +
    • + +
    + +

    Keycloak Central Login and Google will use this information in accordance with their respective terms of service and privacy policies.

    -
    - - -
    + + + + +
    <#elseif section = "info" > 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 60ae59a4cf..ad80199ed5 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 @@ -21,7 +21,7 @@

    ${rb.getString('emailInstruction')}

    -
    +
    diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/login-update-password.ftl b/forms/src/main/resources/META-INF/resources/forms/theme/default/login-update-password.ftl index a9b1ed24fd..4db19119b4 100755 --- a/forms/src/main/resources/META-INF/resources/forms/theme/default/login-update-password.ftl +++ b/forms/src/main/resources/META-INF/resources/forms/theme/default/login-update-password.ftl @@ -11,7 +11,7 @@ <#elseif section = "form">
    - +
    diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/login-update-profile.ftl b/forms/src/main/resources/META-INF/resources/forms/theme/default/login-update-profile.ftl index fb9a8e47ba..2683d2f29d 100755 --- a/forms/src/main/resources/META-INF/resources/forms/theme/default/login-update-profile.ftl +++ b/forms/src/main/resources/META-INF/resources/forms/theme/default/login-update-profile.ftl @@ -15,7 +15,7 @@ <#elseif section = "form">
    - + 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 d92c349235..cff75f1086 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 @@ -20,7 +20,7 @@ Your account is not enabled. An email with instructions to verify your email address has been sent to you.

    Haven't received a verification code in your email? - Click here to re-send the email. + Click here to re-send the email.

    diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/login.ftl b/forms/src/main/resources/META-INF/resources/forms/theme/default/login.ftl index 863118e86b..7e63046d97 100755 --- a/forms/src/main/resources/META-INF/resources/forms/theme/default/login.ftl +++ b/forms/src/main/resources/META-INF/resources/forms/theme/default/login.ftl @@ -24,7 +24,7 @@
    -

    Forgot Password?

    +

    Forgot Password?

    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 7cf9f71441..18ab4d5b60 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 @@ -66,7 +66,7 @@
  • Account
  • Password
  • Authenticator
  • -
  • Social Accounts
  • + <#--
  • Social Accounts
  • -->
  • Authorized Access
  • diff --git a/services/src/main/java/org/keycloak/services/FormService.java b/services/src/main/java/org/keycloak/services/FormService.java index a70e4573bf..6e5813855b 100755 --- a/services/src/main/java/org/keycloak/services/FormService.java +++ b/services/src/main/java/org/keycloak/services/FormService.java @@ -22,10 +22,12 @@ package org.keycloak.services; import java.net.URI; +import java.util.List; import javax.ws.rs.core.MultivaluedMap; import org.keycloak.models.RealmModel; +import org.keycloak.models.RoleModel; import org.keycloak.models.UserModel; import org.keycloak.services.resources.flows.FormFlows; @@ -133,5 +135,53 @@ public interface FormService { public void setErrorType(FormFlows.ErrorType errorType) { this.errorType = errorType; } + + /* OAuth Part */ + private MultivaluedMap oAuthResourceRolesRequested; + private List oAuthRealmRolesRequested; + private UserModel oAuthClient; + private String oAuthCode; + private String oAuthAction; + + public String getOAuthAction() { + return oAuthAction; + } + + public void setOAuthAction(String action) { + this.oAuthAction = action; + } + + public MultivaluedMap getOAuthResourceRolesRequested() { + return oAuthResourceRolesRequested; + } + + public void setOAuthResourceRolesRequested(MultivaluedMap resourceRolesRequested) { + this.oAuthResourceRolesRequested = resourceRolesRequested; + } + + public List getOAuthRealmRolesRequested() { + return oAuthRealmRolesRequested; + } + + public void setOAuthRealmRolesRequested(List realmRolesRequested) { + this.oAuthRealmRolesRequested = realmRolesRequested; + } + + public UserModel getOAuthClient() { + return oAuthClient; + } + + public void setOAuthClient(UserModel client) { + this.oAuthClient = client; + } + + public String getOAuthCode() { + return oAuthCode; + } + + public void setOAuthCode(String oAuthCode) { + this.oAuthCode = oAuthCode; + } + } } diff --git a/services/src/main/java/org/keycloak/services/email/EmailSender.java b/services/src/main/java/org/keycloak/services/email/EmailSender.java index 93f59f9f74..6fd09075bf 100755 --- a/services/src/main/java/org/keycloak/services/email/EmailSender.java +++ b/services/src/main/java/org/keycloak/services/email/EmailSender.java @@ -79,7 +79,7 @@ public class EmailSender { } public void sendEmailVerification(UserModel user, RealmModel realm, AccessCodeEntry accessCode, UriInfo uriInfo) { - UriBuilder builder = Urls.accountBase(uriInfo.getBaseUri()).path(AccountService.class, "emailVerification"); + UriBuilder builder = Urls.loginActionEmailVerificationBuilder(uriInfo.getBaseUri()); builder.queryParam("key", accessCode.getId()); URI uri = builder.build(realm.getId()); @@ -103,7 +103,7 @@ public class EmailSender { } public void sendPasswordReset(UserModel user, RealmModel realm, AccessCodeEntry accessCode, UriInfo uriInfo) { - UriBuilder builder = Urls.accountBase(uriInfo.getBaseUri()).path(AccountService.class, "passwordPage"); + UriBuilder builder = Urls.loginPasswordResetBuilder(uriInfo.getBaseUri()); builder.queryParam("key", accessCode.getId()); URI uri = builder.build(realm.getId()); 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 2874971439..7f54e5232a 100755 --- a/services/src/main/java/org/keycloak/services/resources/AccountService.java +++ b/services/src/main/java/org/keycloak/services/resources/AccountService.java @@ -98,8 +98,7 @@ public class AccountService { @POST @Consumes(MediaType.APPLICATION_FORM_URLENCODED) public Response processAccountUpdate(final MultivaluedMap formData) { - AccessCodeEntry accessCodeEntry = getAccessCodeEntry(RequiredAction.UPDATE_PROFILE); - UserModel user = accessCodeEntry != null ? getUserFromAccessCode(accessCodeEntry) : getUserFromAuthManager(); + UserModel user = getUserFromAuthManager(); if (user == null) { return Response.status(Status.FORBIDDEN).build(); } @@ -108,60 +107,7 @@ public class AccountService { user.setLastName(formData.getFirst("lastName")); user.setEmail(formData.getFirst("email")); - user.removeRequiredAction(UserModel.RequiredAction.UPDATE_PROFILE); - if (accessCodeEntry != null) { - accessCodeEntry.getRequiredActions().remove(UserModel.RequiredAction.UPDATE_PROFILE); - } - - if (accessCodeEntry != null) { - return redirectOauth(user, accessCodeEntry); - } else { - return Flows.forms(realm, request, uriInfo).setUser(user).forwardToAccount(); - } - } - - private UserModel getUserFromAccessCode(AccessCodeEntry accessCodeEntry) { - String loginName = accessCodeEntry.getUser().getLoginName(); - return realm.getUser(loginName); - } - - private UserModel getUserFromAuthManager() { - return authManager.authenticateIdentityCookie(realm, uriInfo, headers); - } - - private AccessCodeEntry getAccessCodeEntry(RequiredAction requiredAction) { - String code = uriInfo.getQueryParameters().getFirst(FormFlows.CODE); - if (code == null) { - return null; - } - - JWSInput input = new JWSInput(code, providers); - boolean verifiedCode = false; - try { - verifiedCode = RSAProvider.verify(input, realm.getPublicKey()); - } catch (Exception ignored) { - return null; - } - - if (!verifiedCode) { - return null; - } - - String key = input.readContent(String.class); - AccessCodeEntry accessCodeEntry = tokenManager.getAccessCode(key); - if (accessCodeEntry == null) { - return null; - } - - if (accessCodeEntry.isExpired()) { - return null; - } - - if (accessCodeEntry.getRequiredActions() == null || !accessCodeEntry.getRequiredActions().contains(requiredAction)) { - return null; - } - - return accessCodeEntry; + return Flows.forms(realm, request, uriInfo).setUser(user).forwardToAccount(); } @Path("totp-remove") @@ -177,8 +123,7 @@ public class AccountService { @POST @Consumes(MediaType.APPLICATION_FORM_URLENCODED) public Response processTotpUpdate(final MultivaluedMap formData) { - AccessCodeEntry accessCodeEntry = getAccessCodeEntry(RequiredAction.CONFIGURE_TOTP); - UserModel user = accessCodeEntry != null ? getUserFromAccessCode(accessCodeEntry) : getUserFromAuthManager(); + UserModel user = getUserFromAuthManager(); if (user == null) { return Response.status(Status.FORBIDDEN).build(); } @@ -205,150 +150,39 @@ public class AccountService { credentials.setValue(formData.getFirst("totpSecret")); realm.updateCredential(user, credentials); - user.removeRequiredAction(UserModel.RequiredAction.CONFIGURE_TOTP); - if (accessCodeEntry != null) { - accessCodeEntry.getRequiredActions().remove(UserModel.RequiredAction.CONFIGURE_TOTP); - } - user.setTotp(true); - if (accessCodeEntry != null) { - return redirectOauth(user, accessCodeEntry); - } else { - return Flows.forms(realm, request, uriInfo).setError("successTotp").setErrorType(FormFlows.ErrorType.SUCCESS) - .setUser(user).forwardToTotp(); - } - } - - @Path("password-reset") - @GET - public Response passwordReset() { - return Flows.forms(realm, request, uriInfo).forwardToPasswordReset(); - } - - @Path("password-reset") - @POST - @Consumes(MediaType.APPLICATION_FORM_URLENCODED) - public Response sendPasswordReset(final MultivaluedMap formData) { - String username = formData.getFirst("username"); - String email = formData.getFirst("email"); - - String scopeParam = uriInfo.getQueryParameters().getFirst("scope"); - String state = uriInfo.getQueryParameters().getFirst("state"); - String redirect = uriInfo.getQueryParameters().getFirst("redirect_uri"); - String clientId = uriInfo.getQueryParameters().getFirst("client_id"); - - UserModel client = realm.getUser(clientId); - if (client == null) { - return Flows.oauth(realm, request, uriInfo, authManager, tokenManager).forwardToSecurityFailure( - "Unknown login requester."); - } - if (!client.isEnabled()) { - return Flows.oauth(realm, request, uriInfo, authManager, tokenManager).forwardToSecurityFailure( - "Login requester not enabled."); - } - - UserModel user = realm.getUser(username); - if (user == null || !email.equals(user.getEmail())) { - return Flows.forms(realm, request, uriInfo).setError("emailError").forwardToPasswordReset(); - } - - Set requiredActions = new HashSet(user.getRequiredActions()); - requiredActions.add(RequiredAction.UPDATE_PASSWORD); - - AccessCodeEntry accessCode = tokenManager.createAccessCode(scopeParam, state, redirect, realm, client, user); - accessCode.setRequiredActions(requiredActions); - accessCode.setExpiration(System.currentTimeMillis() / 1000 + realm.getAccessCodeLifespanUserAction()); - - new EmailSender().sendPasswordReset(user, realm, accessCode, uriInfo); - - return Flows.forms(realm, request, uriInfo).setError("emailSent").setErrorType(FormFlows.ErrorType.SUCCESS) - .forwardToPasswordReset(); - } - - @Path("email-verification") - @GET - public Response emailVerification() { - if (uriInfo.getQueryParameters().containsKey("key")) { - AccessCodeEntry accessCode = tokenManager.getAccessCode(uriInfo.getQueryParameters().getFirst("key")); - if (accessCode == null || accessCode.isExpired() - || !accessCode.getRequiredActions().contains(RequiredAction.VERIFY_EMAIL)) { - return Response.status(Status.FORBIDDEN).build(); - } - - String loginName = accessCode.getUser().getLoginName(); - UserModel user = realm.getUser(loginName); - user.setEmailVerified(true); - user.removeRequiredAction(RequiredAction.VERIFY_EMAIL); - - accessCode.getRequiredActions().remove(RequiredAction.VERIFY_EMAIL); - - return redirectOauth(user, accessCode); - } else { - AccessCodeEntry accessCode = getAccessCodeEntry(RequiredAction.VERIFY_EMAIL); - UserModel user = accessCode != null ? getUserFromAccessCode(accessCode) : null; - if (user == null) { - return Response.status(Status.FORBIDDEN).build(); - } - - return Flows.forms(realm, request, uriInfo).setAccessCode(accessCode).setUser(user) - .forwardToAction(RequiredAction.VERIFY_EMAIL); - } - } - - private Response redirectOauth(UserModel user, AccessCodeEntry accessCode) { - if (accessCode == null) { - return null; - } - - Set requiredActions = user.getRequiredActions(); - if (!requiredActions.isEmpty()) { - return Flows.forms(realm, request, uriInfo).setAccessCode(accessCode).setUser(user) - .forwardToAction(requiredActions.iterator().next()); - } else { - accessCode.setExpiration((System.currentTimeMillis() / 1000) + realm.getAccessCodeLifespan()); - return Flows.oauth(realm, request, uriInfo, authManager, tokenManager).redirectAccessCode(accessCode, - accessCode.getState(), accessCode.getRedirectUri()); - } + return Flows.forms(realm, request, uriInfo).setError("successTotp").setErrorType(FormFlows.ErrorType.SUCCESS) + .setUser(user).forwardToTotp(); } @Path("password") @POST @Consumes(MediaType.APPLICATION_FORM_URLENCODED) public Response processPasswordUpdate(final MultivaluedMap formData) { - AccessCodeEntry accessCode = getAccessCodeEntry(RequiredAction.UPDATE_PASSWORD); - UserModel user = accessCode != null ? getUserFromAccessCode(accessCode) : getUserFromAuthManager(); + UserModel user = getUserFromAuthManager(); if (user == null) { return Response.status(Status.FORBIDDEN).build(); } - boolean loginAction = accessCode != null; - FormFlows forms = Flows.forms(realm, request, uriInfo).setUser(user); String password = formData.getFirst("password"); String passwordNew = formData.getFirst("password-new"); String passwordConfirm = formData.getFirst("password-confirm"); - String error = null; - if (Validation.isEmpty(passwordNew)) { - error = Messages.MISSING_PASSWORD; + forms.setError(Messages.MISSING_PASSWORD).forwardToPassword(); } else if (!passwordNew.equals(passwordConfirm)) { - error = Messages.INVALID_PASSWORD_CONFIRM; + forms.setError(Messages.INVALID_PASSWORD_CONFIRM).forwardToPassword(); } - if (!loginAction) { - if (Validation.isEmpty(password)) { - error = Messages.MISSING_PASSWORD; - } else if (!realm.validatePassword(user, password)) { - error = Messages.INVALID_PASSWORD_EXISTING; - } + if (Validation.isEmpty(password)) { + forms.setError(Messages.MISSING_PASSWORD).forwardToPassword(); + } else if (!realm.validatePassword(user, password)) { + forms.setError(Messages.INVALID_PASSWORD_EXISTING).forwardToPassword(); } - if (error != null) { - return forms.setError(error).forwardToPassword(); - } UserCredentialModel credentials = new UserCredentialModel(); credentials.setType(CredentialRepresentation.PASSWORD); @@ -356,16 +190,7 @@ public class AccountService { realm.updateCredential(user, credentials); - user.removeRequiredAction(RequiredAction.UPDATE_PASSWORD); - if (accessCode != null) { - accessCode.getRequiredActions().remove(UserModel.RequiredAction.UPDATE_PASSWORD); - } - - if (accessCode != null) { - return redirectOauth(user, accessCode); - } else { - return Flows.forms(realm, request, uriInfo).setUser(user).forwardToPassword(); - } + return Flows.forms(realm, request, uriInfo).setUser(user).forwardToPassword(); } @Path("") @@ -404,21 +229,14 @@ public class AccountService { @Path("password") @GET public Response passwordPage() { - if (uriInfo.getQueryParameters().containsKey("key")) { - AccessCodeEntry accessCode = tokenManager.getAccessCode(uriInfo.getQueryParameters().getFirst("key")); - if (accessCode == null || accessCode.isExpired() - || !accessCode.getRequiredActions().contains(RequiredAction.UPDATE_PASSWORD)) { - return Response.status(Status.FORBIDDEN).build(); - } - - return Flows.forms(realm, request, uriInfo).setAccessCode(accessCode) - .forwardToAction(RequiredAction.UPDATE_PASSWORD); - } else { - UserModel user = getUserFromAuthManager(); - if (user == null) { - return Response.status(Status.FORBIDDEN).build(); - } - return Flows.forms(realm, request, uriInfo).setUser(user).forwardToPassword(); + UserModel user = getUserFromAuthManager(); + if (user == null) { + return Response.status(Status.FORBIDDEN).build(); } + return Flows.forms(realm, request, uriInfo).setUser(user).forwardToPassword(); + } + + private UserModel getUserFromAuthManager() { + return authManager.authenticateIdentityCookie(realm, uriInfo, headers); } } diff --git a/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java b/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java new file mode 100755 index 0000000000..214a814a8a --- /dev/null +++ b/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java @@ -0,0 +1,316 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2012, Red Hat, Inc., and individual contributors + * as indicated by the @author tags. See the copyright.txt file in the + * distribution for a full listing of individual contributors. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.keycloak.services.resources; + +import org.jboss.resteasy.jose.jws.JWSInput; +import org.jboss.resteasy.jose.jws.crypto.RSAProvider; +import org.jboss.resteasy.spi.HttpRequest; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserCredentialModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.UserModel.RequiredAction; +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.TokenManager; +import org.keycloak.services.messages.Messages; +import org.keycloak.services.resources.flows.Flows; +import org.keycloak.services.resources.flows.FormFlows; +import org.keycloak.services.validation.Validation; +import org.picketlink.idm.credential.util.TimeBasedOTP; + +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.core.*; +import javax.ws.rs.ext.Providers; +import java.util.HashSet; +import java.util.Set; + +/** + * @author Stian Thorgersen + */ +public class RequiredActionsService { + + private RealmModel realm; + + @Context + private HttpRequest request; + + @Context + protected HttpHeaders headers; + + @Context + private UriInfo uriInfo; + + @Context + protected Providers providers; + + protected AuthenticationManager authManager = new AuthenticationManager(); + + private TokenManager tokenManager; + + public RequiredActionsService(RealmModel realm, TokenManager tokenManager) { + this.realm = realm; + this.tokenManager = tokenManager; + } + + @Path("profile") + @POST + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + public Response updateProfile(final MultivaluedMap formData) { + AccessCodeEntry accessCode = getAccessCodeEntry(RequiredAction.UPDATE_PROFILE); + if (accessCode == null) { + return forwardToErrorPage(); + } + + UserModel user = getUser(accessCode); + user.setFirstName(formData.getFirst("firstName")); + user.setLastName(formData.getFirst("lastName")); + user.setEmail(formData.getFirst("email")); + + user.removeRequiredAction(RequiredAction.UPDATE_PROFILE); + accessCode.getRequiredActions().remove(RequiredAction.UPDATE_PROFILE); + + return redirectOauth(user, accessCode); + } + + @Path("totp") + @POST + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + public Response updateTotp(final MultivaluedMap formData) { + AccessCodeEntry accessCode = getAccessCodeEntry(RequiredAction.CONFIGURE_TOTP); + if (accessCode == null) { + return forwardToErrorPage(); + } + + UserModel user = getUser(accessCode); + + String totp = formData.getFirst("totp"); + String totpSecret = formData.getFirst("totpSecret"); + + FormFlows forms = Flows.forms(realm, request, uriInfo).setUser(user); + if (Validation.isEmpty(totp)) { + return forms.setError(Messages.MISSING_TOTP).forwardToAction(RequiredAction.CONFIGURE_TOTP); + } else if (!new TimeBasedOTP().validate(totp, totpSecret.getBytes())) { + return forms.setError(Messages.INVALID_TOTP).forwardToAction(RequiredAction.CONFIGURE_TOTP); + } + + UserCredentialModel credentials = new UserCredentialModel(); + credentials.setType(CredentialRepresentation.TOTP); + credentials.setValue(formData.getFirst("totpSecret")); + realm.updateCredential(user, credentials); + + user.setTotp(true); + + user.removeRequiredAction(RequiredAction.CONFIGURE_TOTP); + accessCode.getRequiredActions().remove(RequiredAction.CONFIGURE_TOTP); + + return redirectOauth(user, accessCode); + } + + @Path("password") + @POST + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + public Response updatePassword(final MultivaluedMap formData) { + AccessCodeEntry accessCode = getAccessCodeEntry(RequiredAction.UPDATE_PASSWORD); + if (accessCode == null) { + return forwardToErrorPage(); + } + + 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); + } else if (!passwordNew.equals(passwordConfirm)) { + forms.setError(Messages.MISSING_PASSWORD).forwardToAction(RequiredAction.UPDATE_PASSWORD); + } + + UserCredentialModel credentials = new UserCredentialModel(); + credentials.setType(CredentialRepresentation.PASSWORD); + credentials.setValue(passwordNew); + + realm.updateCredential(user, credentials); + + user.removeRequiredAction(RequiredAction.UPDATE_PASSWORD); + if (accessCode != null) { + accessCode.getRequiredActions().remove(RequiredAction.UPDATE_PASSWORD); + } + + if (accessCode != null) { + return redirectOauth(user, accessCode); + } else { + return Flows.forms(realm, request, uriInfo).setUser(user).forwardToPassword(); + } + } + + + @Path("email-verification") + @GET + public Response emailVerification() { + if (uriInfo.getQueryParameters().containsKey("key")) { + AccessCodeEntry accessCode = tokenManager.getAccessCode(uriInfo.getQueryParameters().getFirst("key")); + if (accessCode == null || accessCode.isExpired() + || !accessCode.getRequiredActions().contains(RequiredAction.VERIFY_EMAIL)) { + return forwardToErrorPage(); + } + + UserModel user = getUser(accessCode); + user.setEmailVerified(true); + + user.removeRequiredAction(RequiredAction.VERIFY_EMAIL); + accessCode.getRequiredActions().remove(RequiredAction.VERIFY_EMAIL); + + return redirectOauth(user, accessCode); + } else { + AccessCodeEntry accessCode = getAccessCodeEntry(RequiredAction.VERIFY_EMAIL); + if (accessCode == null) { + return forwardToErrorPage(); + } + + return Flows.forms(realm, request, uriInfo).setAccessCode(accessCode).setUser(accessCode.getUser()) + .forwardToAction(RequiredAction.VERIFY_EMAIL); + } + } + + @Path("password-reset") + @GET + public Response passwordReset() { + if (uriInfo.getQueryParameters().containsKey("key")) { + AccessCodeEntry accessCode = tokenManager.getAccessCode(uriInfo.getQueryParameters().getFirst("key")); + if (accessCode == null || accessCode.isExpired() + || !accessCode.getRequiredActions().contains(RequiredAction.UPDATE_PASSWORD)) { + return forwardToErrorPage(); + } + return Flows.forms(realm, request, uriInfo).setAccessCode(accessCode).forwardToAction(RequiredAction.UPDATE_PASSWORD); + } else { + return Flows.forms(realm, request, uriInfo).forwardToPasswordReset(); + } + } + + @Path("password-reset") + @POST + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + public Response sendPasswordReset(final MultivaluedMap formData) { + String username = formData.getFirst("username"); + String email = formData.getFirst("email"); + + String scopeParam = uriInfo.getQueryParameters().getFirst("scope"); + String state = uriInfo.getQueryParameters().getFirst("state"); + String redirect = uriInfo.getQueryParameters().getFirst("redirect_uri"); + String clientId = uriInfo.getQueryParameters().getFirst("client_id"); + + UserModel client = realm.getUser(clientId); + if (client == null) { + return Flows.oauth(realm, request, uriInfo, authManager, tokenManager).forwardToSecurityFailure( + "Unknown login requester."); + } + if (!client.isEnabled()) { + return Flows.oauth(realm, request, uriInfo, authManager, tokenManager).forwardToSecurityFailure( + "Login requester not enabled."); + } + + UserModel user = realm.getUser(username); + if (user == null || !email.equals(user.getEmail())) { + return Flows.forms(realm, request, uriInfo).setError("emailError").forwardToPasswordReset(); + } + + Set requiredActions = new HashSet(user.getRequiredActions()); + requiredActions.add(RequiredAction.UPDATE_PASSWORD); + + AccessCodeEntry accessCode = tokenManager.createAccessCode(scopeParam, state, redirect, realm, client, user); + accessCode.setRequiredActions(requiredActions); + accessCode.setExpiration(System.currentTimeMillis() / 1000 + realm.getAccessCodeLifespanUserAction()); + + new EmailSender().sendPasswordReset(user, realm, accessCode, uriInfo); + + return Flows.forms(realm, request, uriInfo).setError("emailSent").setErrorType(FormFlows.ErrorType.SUCCESS) + .forwardToPasswordReset(); + } + + private AccessCodeEntry getAccessCodeEntry(RequiredAction requiredAction) { + String code = uriInfo.getQueryParameters().getFirst(FormFlows.CODE); + if (code == null) { + return null; + } + + JWSInput input = new JWSInput(code, providers); + boolean verifiedCode = false; + try { + verifiedCode = RSAProvider.verify(input, realm.getPublicKey()); + } catch (Exception ignored) { + return null; + } + + if (!verifiedCode) { + return null; + } + + String key = input.readContent(String.class); + AccessCodeEntry accessCodeEntry = tokenManager.getAccessCode(key); + if (accessCodeEntry == null) { + return null; + } + + if (accessCodeEntry.isExpired()) { + return null; + } + + if (accessCodeEntry.getRequiredActions() == null || !accessCodeEntry.getRequiredActions().contains(requiredAction)) { + return null; + } + + return accessCodeEntry; + } + + private UserModel getUser(AccessCodeEntry accessCode) { + return realm.getUser(accessCode.getUser().getLoginName()); + } + + private Response redirectOauth(UserModel user, AccessCodeEntry accessCode) { + if (accessCode == null) { + return null; + } + + Set requiredActions = user.getRequiredActions(); + if (!requiredActions.isEmpty()) { + return Flows.forms(realm, request, uriInfo).setAccessCode(accessCode).setUser(user) + .forwardToAction(requiredActions.iterator().next()); + } else { + accessCode.setExpiration((System.currentTimeMillis() / 1000) + realm.getAccessCodeLifespan()); + return Flows.oauth(realm, request, uriInfo, authManager, tokenManager).redirectAccessCode(accessCode, + accessCode.getState(), accessCode.getRedirectUri()); + } + } + + private Response forwardToErrorPage() { + return Flows.forms(realm, request, uriInfo).forwardToErrorPage(); + } + +} 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 89742bf752..d72e80d9d4 100755 --- a/services/src/main/java/org/keycloak/services/resources/TokenService.java +++ b/services/src/main/java/org/keycloak/services/resources/TokenService.java @@ -30,6 +30,7 @@ import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; +import javax.ws.rs.container.ResourceContext; import javax.ws.rs.core.Context; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; @@ -75,6 +76,8 @@ public class TokenService { @Context protected KeycloakTransaction transaction; + @Context + protected ResourceContext resourceContext; private ResourceAdminManager resourceAdminManager = new ResourceAdminManager(); @@ -217,6 +220,13 @@ public class TokenService { } } + @Path("auth/request/login-actions") + public RequiredActionsService getRequiredActionsService() { + RequiredActionsService service = new RequiredActionsService(realm, tokenManager); + resourceContext.initResource(service); + return service; + } + private void isTotpConfigurationRequired(UserModel user) { for (RequiredCredentialModel c : realm.getRequiredCredentials()) { if (c.getType().equals(CredentialRepresentation.TOTP) && !user.isTotp()) { 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 133b062354..c4fe4a6d25 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 @@ -23,9 +23,11 @@ package org.keycloak.services.resources.flows; import java.net.URI; import java.util.Iterator; +import java.util.List; import org.jboss.resteasy.spi.HttpRequest; import org.jboss.resteasy.spi.ResteasyUriInfo; +import org.keycloak.models.RoleModel; import org.keycloak.services.FormService; import org.keycloak.services.email.EmailSender; import org.keycloak.services.managers.AccessCodeEntry; @@ -100,9 +102,7 @@ public class FormFlows { return forwardToForm(Pages.ACCOUNT); } - private Response forwardToForm(String template) { - - FormService.FormServiceDataBean formDataBean = new FormService.FormServiceDataBean(realm, userModel, formData, error); + private Response forwardToForm(String template, FormService.FormServiceDataBean formDataBean) { formDataBean.setErrorType(errorType == null ? ErrorType.ERROR : errorType); // Getting URI needed by form processing service @@ -140,6 +140,13 @@ public class FormFlows { return Response.status(200).entity("form provider not found").build(); } + private Response forwardToForm(String template) { + + FormService.FormServiceDataBean formDataBean = new FormService.FormServiceDataBean(realm, userModel, formData, error); + return forwardToForm(template, formDataBean); + + } + public Response forwardToLogin() { return forwardToForm(Pages.LOGIN); } @@ -172,6 +179,19 @@ public class FormFlows { return forwardToForm(Pages.ERROR); } + public Response forwardToOAuthGrant(){ + + FormService.FormServiceDataBean formDataBean = new FormService.FormServiceDataBean(realm, userModel, formData, error); + + formDataBean.setOAuthRealmRolesRequested((List) request.getAttribute("realmRolesRequested")); + formDataBean.setOAuthResourceRolesRequested((MultivaluedMap) request.getAttribute("resourceRolesRequested")); + formDataBean.setOAuthClient((UserModel)request.getAttribute("client")); + formDataBean.setOAuthCode((String)request.getAttribute("code")); + formDataBean.setOAuthAction((String)request.getAttribute("action")); + + return forwardToForm(Pages.OAUTH_GRANT, formDataBean); + } + public FormFlows setAccessCode(AccessCodeEntry accessCode) { this.accessCode = accessCode; return this; diff --git a/services/src/main/java/org/keycloak/services/resources/flows/OAuthFlows.java b/services/src/main/java/org/keycloak/services/resources/flows/OAuthFlows.java index 06c0a06b07..5d01a39cf1 100755 --- a/services/src/main/java/org/keycloak/services/resources/flows/OAuthFlows.java +++ b/services/src/main/java/org/keycloak/services/resources/flows/OAuthFlows.java @@ -121,8 +121,7 @@ public class OAuthFlows { request.setAttribute("action", TokenService.processOAuthUrl(uriInfo).build(realm.getId()).toString()); request.setAttribute("code", accessCode.getCode()); - request.forward(Pages.OAUTH_GRANT); - return null; + return Flows.forms(realm, request, uriInfo).setAccessCode(accessCode).forwardToOAuthGrant(); } public Response forwardToSecurityFailure(String message) { diff --git a/services/src/main/java/org/keycloak/services/resources/flows/Pages.java b/services/src/main/java/org/keycloak/services/resources/flows/Pages.java index 110da3ad25..695ecbb7af 100644 --- a/services/src/main/java/org/keycloak/services/resources/flows/Pages.java +++ b/services/src/main/java/org/keycloak/services/resources/flows/Pages.java @@ -38,7 +38,7 @@ public class Pages { public final static String LOGIN_VERIFY_EMAIL = "/forms/login-verify-email.ftl"; - public final static String OAUTH_GRANT = "/saas/oauthGrantForm.jsp"; + public final static String OAUTH_GRANT = "/forms/login-oauth-grant.ftl"; public final static String PASSWORD = "/forms/password.ftl"; 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 b6a86a2cd4..200b746ea3 100755 --- a/services/src/main/java/org/keycloak/services/resources/flows/Urls.java +++ b/services/src/main/java/org/keycloak/services/resources/flows/Urls.java @@ -35,7 +35,7 @@ public class Urls { return accountBase(baseUri).path(AccountService.class, "accessPage").build(realmId); } - public static UriBuilder accountBase(URI baseUri) { + private static UriBuilder accountBase(URI baseUri) { return realmBase(baseUri).path(RealmsResource.class, "getAccountService"); } @@ -59,12 +59,32 @@ public class Urls { return accountBase(baseUri).path(AccountService.class, "processTotpRemove").build(realmId); } - public static URI accountEmailVerification(URI baseUri, String realmId) { - return accountBase(baseUri).path(AccountService.class, "emailVerification").build(realmId); + public static URI loginActionUpdatePassword(URI baseUri, String realmId) { + return requiredActionsBase(baseUri).path(RequiredActionsService.class, "updatePassword").build(realmId); } - public static URI accountPasswordReset(URI baseUri, String realmId) { - return accountBase(baseUri).path(AccountService.class, "passwordReset").build(realmId); + public static URI loginActionUpdateTotp(URI baseUri, String realmId) { + return requiredActionsBase(baseUri).path(RequiredActionsService.class, "updateTotp").build(realmId); + } + + public static URI loginActionUpdateProfile(URI baseUri, String realmId) { + return requiredActionsBase(baseUri).path(RequiredActionsService.class, "updateProfile").build(realmId); + } + + public static URI loginActionEmailVerification(URI baseUri, String realmId) { + return loginActionEmailVerificationBuilder(baseUri).build(realmId); + } + + public static UriBuilder loginActionEmailVerificationBuilder(URI baseUri) { + return requiredActionsBase(baseUri).path(RequiredActionsService.class, "emailVerification"); + } + + public static URI loginPasswordReset(URI baseUri, String realmId) { + return loginPasswordResetBuilder(baseUri).build(realmId); + } + + public static UriBuilder loginPasswordResetBuilder(URI baseUri) { + return requiredActionsBase(baseUri).path(RequiredActionsService.class, "passwordReset"); } private static UriBuilder realmBase(URI baseUri) { @@ -120,11 +140,15 @@ public class Urls { .build(realmId); } - private static UriBuilder tokenBase(URI baseUri) { - return realmBase(baseUri).path(RealmsResource.class, "getTokenService"); - } - public static URI socialRegisterAction(URI baseUri, String realmId) { return socialBase(baseUri).path(SocialResource.class, "socialRegistration").build(realmId); } + + private static UriBuilder requiredActionsBase(URI baseUri) { + return tokenBase(baseUri).path(TokenService.class, "getRequiredActionsService"); + } + + private static UriBuilder tokenBase(URI baseUri) { + return realmBase(baseUri).path(RealmsResource.class, "getTokenService"); + } } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthGrantServlet.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthGrantServlet.java new file mode 100644 index 0000000000..c0ae537816 --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthGrantServlet.java @@ -0,0 +1,112 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2012, Red Hat, Inc., and individual contributors + * as indicated by the @author tags. See the copyright.txt file in the + * distribution for a full listing of individual contributors. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.keycloak.testsuite; + +import java.io.IOException; +import java.io.PrintWriter; +import java.security.PublicKey; +import java.util.Map; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.core.UriBuilder; + +import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder; +import org.keycloak.PemUtils; +import org.keycloak.RSATokenVerifier; +import org.keycloak.representations.SkeletonKeyToken; +import org.keycloak.servlet.ServletOAuthClient; + +/** + * @author Viliam Rockai + */ +public class OAuthGrantServlet extends HttpServlet { + + public static ServletOAuthClient client; + + private static String baseUrl = Constants.AUTH_SERVER_ROOT + "/rest"; + private static String realm = "test"; + + private static String realmKeyPem = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvg" + + "cwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/" + + "p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB"; + + public void init() { + client = new ServletOAuthClient(); + client.setClientId("third-party"); + client.setPassword("password"); + client.setAuthUrl(UriBuilder.fromUri(baseUrl + "/realms/" + realm + "/tokens/login").build().toString()); + client.setCodeUrl(UriBuilder.fromUri(baseUrl + "/realms/" + realm + "/tokens/access/codes").build().toString()); + client.setClient(new ResteasyClientBuilder().build()); + client.start(); + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException{ + + PrintWriter pw = resp.getWriter(); + + // Error "access_denied" happens after clicking on cancel when asked for granting permission + if (req.getParameterValues("error") != null){ + pw.print("Access rights not granted."); + + // Code is sent as a parameter in case that access was granted + } else if(req.getParameterValues("code") != null) { + String token = client.getBearerToken(req); + + pw.print("Access rights granted.
    Token:"+token+"
    "); + + // Check whether the token itself is relevant + try { + PublicKey realmKey = PemUtils.decodePublicKey(realmKeyPem); + SkeletonKeyToken skeletonToken = RSATokenVerifier.verifyToken(token, realmKey, realm); + + // Writing app/role information on a page in format which is easy to assert in a test. + pw.print("Role:"); + for(String role: skeletonToken.getRealmAccess().getRoles()){ + pw.print(role); + } + pw.print(".
    "); + + for(Map.Entry entry: skeletonToken.getResourceAccess().entrySet()){ + pw.print("App:"+entry.getKey()+";"); + for(String role: entry.getValue().getRoles()){ + pw.print(role); + } + } + pw.print(".
    "); + } catch (Exception e){ + } + + pw.print(""); + + // If no code was sent or error happened, it's 1st visit to servlet and we need to ask for permissions + } else { + client.redirectRelative("", req, resp); + } + + pw.flush(); + } + +} diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OAuthGrantTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OAuthGrantTest.java new file mode 100644 index 0000000000..0513f6707d --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OAuthGrantTest.java @@ -0,0 +1,124 @@ +/* + * 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.oauth; + +import java.io.IOException; + +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.keycloak.testsuite.OAuthClient; +import org.keycloak.testsuite.OAuthGrantServlet; +import org.keycloak.testsuite.pages.LoginPage; +import org.keycloak.testsuite.pages.OAuthGrantPage; +import org.keycloak.testsuite.rule.KeycloakRule; +import org.keycloak.testsuite.rule.WebResource; +import org.keycloak.testsuite.rule.WebRule; +import org.openqa.selenium.WebDriver; + +/** + * @author Viliam Rockai + */ +public class OAuthGrantTest { + + @ClassRule + public static KeycloakRule keycloakRule = new KeycloakRule(); + + @BeforeClass + public static void before() { + keycloakRule.deployServlet("grant", "/grant", OAuthGrantServlet.class); + } + + @Rule + public WebRule webRule = new WebRule(this); + + @WebResource + protected WebDriver driver; + + @WebResource + protected OAuthClient oauth; + + @WebResource + protected LoginPage loginPage; + + @WebResource + protected OAuthGrantPage grantPage; + + private static String GRANT_APP_URL = "http://localhost:8081/grant/"; + + private static String ACCESS_GRANTED = "Access rights granted."; + private static String ACCESS_NOT_GRANTED = "Access rights not granted."; + + private static String ROLE_USER = "Have User privileges"; + private static String ROLE_CUSTOMER = "Have Customer User privileges"; + + private static String GRANT_ROLE = "user"; + private static String GRANT_APP = "test-app"; + private static String GRANT_APP_ROLE = "customer-user"; + + @Test + public void oauthGrantAcceptTest() throws IOException { + + driver.navigate().to(GRANT_APP_URL); + + Assert.assertFalse(driver.getPageSource().contains(ACCESS_GRANTED)); + Assert.assertFalse(driver.getPageSource().contains(ACCESS_NOT_GRANTED)); + + loginPage.isCurrent(); + loginPage.login("test-user@localhost", "password"); + + grantPage.assertCurrent(); + Assert.assertTrue(driver.getPageSource().contains(ROLE_USER)); + Assert.assertTrue(driver.getPageSource().contains(ROLE_CUSTOMER)); + + grantPage.accept(); + + Assert.assertTrue(driver.getPageSource().contains(ACCESS_GRANTED)); + Assert.assertFalse(driver.getPageSource().contains(ACCESS_NOT_GRANTED)); + + Assert.assertTrue(driver.getPageSource().contains("Role:"+ GRANT_ROLE +".")); + Assert.assertTrue(driver.getPageSource().contains("App:"+ GRANT_APP +";"+ GRANT_APP_ROLE +".")); + } + + @Test + public void oauthGrantCancelTest() throws IOException { + + driver.navigate().to(GRANT_APP_URL); + + Assert.assertFalse(driver.getPageSource().contains(ACCESS_GRANTED)); + Assert.assertFalse(driver.getPageSource().contains(ACCESS_NOT_GRANTED)); + + loginPage.isCurrent(); + loginPage.login("test-user@localhost", "password"); + + grantPage.assertCurrent(); + Assert.assertTrue(driver.getPageSource().contains(ROLE_USER)); + Assert.assertTrue(driver.getPageSource().contains(ROLE_CUSTOMER)); + + grantPage.cancel(); + + Assert.assertFalse(driver.getPageSource().contains(ACCESS_GRANTED)); + Assert.assertTrue(driver.getPageSource().contains(ACCESS_NOT_GRANTED)); + } +} 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 new file mode 100644 index 0000000000..8749a918ce --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/OAuthGrantPage.java @@ -0,0 +1,55 @@ +/* + * 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.openqa.selenium.WebElement; +import org.openqa.selenium.support.FindBy; + +/** + * @author Stian Thorgersen + */ +public class OAuthGrantPage extends Page { + + @FindBy(css = "input[name=\"accept\"]") + private WebElement acceptButton; + @FindBy(css = "input[name=\"cancel\"]") + private WebElement cancelButton; + + + public void accept(){ + acceptButton.click(); + } + + public void cancel(){ + cancelButton.click(); + } + + @Override + public boolean isCurrent() { + return driver.getTitle().equals("OAuth Grant"); + } + + @Override + void open() { + } + +} diff --git a/testsuite/integration/src/test/resources/testrealm.json b/testsuite/integration/src/test/resources/testrealm.json index f48b969936..941c5db836 100755 --- a/testsuite/integration/src/test/resources/testrealm.json +++ b/testsuite/integration/src/test/resources/testrealm.json @@ -24,6 +24,14 @@ { "type" : "password", "value" : "password" } ] + }, + { + "username" : "third-party", + "enabled": true, + "credentials" : [ + { "type" : "password", + "value" : "password" } + ] } ], "roles": [ @@ -40,6 +48,16 @@ { "username": "test-user@localhost", "roles": ["user"] + }, + { + "username": "third-party", + "roles": ["KEYCLOAK_IDENTITY_REQUESTER"] + } + ], + "scopeMappings": [ + { + "username": "third-party", + "roles": ["user"] } ], "applications": [ @@ -53,6 +71,24 @@ "type": "password", "value": "password" } + ], + "roles": [ + { + "name": "customer-user", + "description": "Have Customer User privileges" + } + ], + "roleMappings": [ + { + "username": "test-user@localhost", + "roles": ["customer-user"] + } + ], + "scopeMappings": [ + { + "username": "third-party", + "roles": ["customer-user"] + } ] } ]
    Table of realm users
    -
    - - -
    -
    UsernameLast NameFirst NameEmail
    +
    + + +
    +
    + Add User + +
    +
    UsernameLast NameFirst NameEmail