From dcfa4aca8c0ee470743126989e20a9e9dec9313f Mon Sep 17 00:00:00 2001 From: Stian Thorgersen Date: Tue, 29 Aug 2017 20:12:09 +0200 Subject: [PATCH] KEYCLOAK-943 Started account rest service. Profile and sessions completed. (#4439) --- .../account/ClientRepresentation.java | 25 ++ .../account/SessionRepresentation.java | 64 ++++ .../account/UserRepresentation.java | 97 +++++ .../model/AccountFederatedIdentityBean.java | 4 +- .../main/java/org/keycloak/services/Urls.java | 24 +- .../services/managers/AppAuthManager.java | 4 + .../org/keycloak/services/managers/Auth.java | 13 + .../managers/AuthenticationManager.java | 13 + .../keycloak/services/messages/Messages.java | 2 + .../AbstractSecuredLocalService.java | 26 -- .../org/keycloak/services/resources/Cors.java | 22 +- .../resources/IdentityBrokerService.java | 4 +- .../resources/PublicRealmResource.java | 3 +- .../services/resources/RealmsResource.java | 16 +- .../AccountFormService.java} | 290 +++++--------- .../resources/account/AccountLoader.java | 83 ++++ .../resources/account/AccountRestService.java | 270 +++++++++++++ .../services/resources/account/Constants.java | 47 +++ .../account/CorsPreflightService.java | 33 ++ .../services/resources/account/Errors.java | 29 ++ .../resources/admin/UserResource.java | 4 +- .../resources/admin/UsersResource.java | 1 - .../testsuite/pages/AccountPasswordPage.java | 4 +- .../testsuite/pages/AccountTotpPage.java | 4 +- .../keycloak/testsuite/util/TokenUtil.java | 85 ++++ .../testsuite/util/WebDriverLogDumper.java | 26 ++ ...tTest.java => AccountFormServiceTest.java} | 17 +- .../account/AccountRestServiceCorsTest.java | 180 +++++++++ .../account/AccountRestServiceTest.java | 198 ++++++++++ .../testsuite/account/ProfileTest.java | 362 ------------------ .../account/custom/CustomThemeTest.java | 4 +- .../testsuite/admin/UserTotpTest.java | 4 +- .../testsuite/oauth/OAuthGrantTest.java | 1 - .../testsuite/oauth/OfflineTokenTest.java | 4 +- .../{account => ssl}/TrustStoreEmailTest.java | 2 +- .../base/src/test/resources/testrealm.json | 7 + .../testsuite/pages/AccountPasswordPage.java | 4 +- .../testsuite/pages/AccountTotpPage.java | 4 +- .../account/messages/messages_en.properties | 1 + 39 files changed, 1331 insertions(+), 650 deletions(-) create mode 100644 core/src/main/java/org/keycloak/representations/account/ClientRepresentation.java create mode 100644 core/src/main/java/org/keycloak/representations/account/SessionRepresentation.java create mode 100755 core/src/main/java/org/keycloak/representations/account/UserRepresentation.java rename services/src/main/java/org/keycloak/services/resources/{AccountService.java => account/AccountFormService.java} (79%) create mode 100644 services/src/main/java/org/keycloak/services/resources/account/AccountLoader.java create mode 100755 services/src/main/java/org/keycloak/services/resources/account/AccountRestService.java create mode 100644 services/src/main/java/org/keycloak/services/resources/account/Constants.java create mode 100644 services/src/main/java/org/keycloak/services/resources/account/CorsPreflightService.java create mode 100644 services/src/main/java/org/keycloak/services/resources/account/Errors.java create mode 100644 testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/TokenUtil.java create mode 100644 testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/WebDriverLogDumper.java rename testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/{AccountTest.java => AccountFormServiceTest.java} (99%) create mode 100755 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountRestServiceCorsTest.java create mode 100755 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountRestServiceTest.java delete mode 100755 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/ProfileTest.java rename testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/{account => ssl}/TrustStoreEmailTest.java (99%) diff --git a/core/src/main/java/org/keycloak/representations/account/ClientRepresentation.java b/core/src/main/java/org/keycloak/representations/account/ClientRepresentation.java new file mode 100644 index 0000000000..c2964268a8 --- /dev/null +++ b/core/src/main/java/org/keycloak/representations/account/ClientRepresentation.java @@ -0,0 +1,25 @@ +package org.keycloak.representations.account; + +/** + * Created by st on 29/03/17. + */ +public class ClientRepresentation { + private String clientId; + private String clientName; + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public String getClientName() { + return clientName; + } + + public void setClientName(String clientName) { + this.clientName = clientName; + } +} diff --git a/core/src/main/java/org/keycloak/representations/account/SessionRepresentation.java b/core/src/main/java/org/keycloak/representations/account/SessionRepresentation.java new file mode 100644 index 0000000000..8e4d96fbd2 --- /dev/null +++ b/core/src/main/java/org/keycloak/representations/account/SessionRepresentation.java @@ -0,0 +1,64 @@ +package org.keycloak.representations.account; + +import java.util.List; + +/** + * Created by st on 29/03/17. + */ +public class SessionRepresentation { + + private String id; + private String ipAddress; + private int started; + private int lastAccess; + private int expires; + private List clients; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getIpAddress() { + return ipAddress; + } + + public void setIpAddress(String ipAddress) { + this.ipAddress = ipAddress; + } + + public int getStarted() { + return started; + } + + public void setStarted(int started) { + this.started = started; + } + + public int getLastAccess() { + return lastAccess; + } + + public void setLastAccess(int lastAccess) { + this.lastAccess = lastAccess; + } + + public int getExpires() { + return expires; + } + + public void setExpires(int expires) { + this.expires = expires; + } + + public List getClients() { + return clients; + } + + public void setClients(List clients) { + this.clients = clients; + } +} diff --git a/core/src/main/java/org/keycloak/representations/account/UserRepresentation.java b/core/src/main/java/org/keycloak/representations/account/UserRepresentation.java new file mode 100755 index 0000000000..f457b180ed --- /dev/null +++ b/core/src/main/java/org/keycloak/representations/account/UserRepresentation.java @@ -0,0 +1,97 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.representations.account; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import org.keycloak.json.StringListMapDeserializer; + +import java.util.List; +import java.util.Map; + +/** + * @author Stian Thorgersen + */ +public class UserRepresentation { + + private String id; + private String username; + private String firstName; + private String lastName; + private String email; + private boolean emailVerified; + + @JsonDeserialize(using = StringListMapDeserializer.class) + private Map> attributes; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public boolean isEmailVerified() { + return emailVerified; + } + + public void setEmailVerified(boolean emailVerified) { + this.emailVerified = emailVerified; + } + + public Map> getAttributes() { + return attributes; + } + + public void setAttributes(Map> attributes) { + this.attributes = attributes; + } + +} diff --git a/services/src/main/java/org/keycloak/forms/account/freemarker/model/AccountFederatedIdentityBean.java b/services/src/main/java/org/keycloak/forms/account/freemarker/model/AccountFederatedIdentityBean.java index 96f4257558..54dd7bff9a 100755 --- a/services/src/main/java/org/keycloak/forms/account/freemarker/model/AccountFederatedIdentityBean.java +++ b/services/src/main/java/org/keycloak/forms/account/freemarker/model/AccountFederatedIdentityBean.java @@ -23,7 +23,7 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.utils.KeycloakModelUtils; -import org.keycloak.services.resources.AccountService; +import org.keycloak.services.resources.account.AccountFormService; import org.keycloak.services.Urls; import javax.ws.rs.core.UriBuilder; @@ -80,7 +80,7 @@ public class AccountFederatedIdentityBean { this.identities = new LinkedList(orderedSet); // Removing last social provider is not possible if you don't have other possibility to authenticate - this.removeLinkPossible = availableIdentities > 1 || user.getFederationLink() != null || AccountService.isPasswordSet(session, realm, user); + this.removeLinkPossible = availableIdentities > 1 || user.getFederationLink() != null || AccountFormService.isPasswordSet(session, realm, user); } private FederatedIdentityModel getIdentity(Set identities, String providerId) { diff --git a/services/src/main/java/org/keycloak/services/Urls.java b/services/src/main/java/org/keycloak/services/Urls.java index 51f505e71e..cb023fa2f3 100755 --- a/services/src/main/java/org/keycloak/services/Urls.java +++ b/services/src/main/java/org/keycloak/services/Urls.java @@ -21,7 +21,7 @@ import org.keycloak.common.Version; import org.keycloak.models.Constants; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.OIDCLoginProtocolService; -import org.keycloak.services.resources.AccountService; +import org.keycloak.services.resources.account.AccountFormService; import org.keycloak.services.resources.IdentityBrokerService; import org.keycloak.services.resources.LoginActionsService; import org.keycloak.services.resources.RealmsResource; @@ -41,7 +41,7 @@ public class Urls { } public static URI accountApplicationsPage(URI baseUri, String realmName) { - return accountBase(baseUri).path(AccountService.class, "applicationsPage").build(realmName); + return accountBase(baseUri).path(AccountFormService.class, "applicationsPage").build(realmName); } public static UriBuilder accountBase(URI baseUri) { @@ -53,19 +53,19 @@ public class Urls { } public static UriBuilder accountPageBuilder(URI baseUri) { - return accountBase(baseUri).path(AccountService.class, "accountPage"); + return accountBase(baseUri).path(AccountFormService.class, "accountPage"); } public static URI accountPasswordPage(URI baseUri, String realmName) { - return accountBase(baseUri).path(AccountService.class, "passwordPage").build(realmName); + return accountBase(baseUri).path(AccountFormService.class, "passwordPage").build(realmName); } public static URI accountFederatedIdentityPage(URI baseUri, String realmName) { - return accountBase(baseUri).path(AccountService.class, "federatedIdentityPage").build(realmName); + return accountBase(baseUri).path(AccountFormService.class, "federatedIdentityPage").build(realmName); } public static URI accountFederatedIdentityUpdate(URI baseUri, String realmName) { - return accountBase(baseUri).path(AccountService.class, "processFederatedIdentityUpdate").build(realmName); + return accountBase(baseUri).path(AccountFormService.class, "processFederatedIdentityUpdate").build(realmName); } public static URI identityProviderAuthnResponse(URI baseUri, String providerId, String realmName) { @@ -123,31 +123,31 @@ public class Urls { } public static URI accountTotpPage(URI baseUri, String realmName) { - return accountBase(baseUri).path(AccountService.class, "totpPage").build(realmName); + return accountBase(baseUri).path(AccountFormService.class, "totpPage").build(realmName); } public static URI accountTotpRemove(URI baseUri, String realmName, String stateChecker) { - return accountBase(baseUri).path(AccountService.class, "processTotpRemove") + return accountBase(baseUri).path(AccountFormService.class, "processTotpRemove") .queryParam("stateChecker", stateChecker) .build(realmName); } public static URI accountLogPage(URI baseUri, String realmName) { - return accountBase(baseUri).path(AccountService.class, "logPage").build(realmName); + return accountBase(baseUri).path(AccountFormService.class, "logPage").build(realmName); } public static URI accountSessionsPage(URI baseUri, String realmName) { - return accountBase(baseUri).path(AccountService.class, "sessionsPage").build(realmName); + return accountBase(baseUri).path(AccountFormService.class, "sessionsPage").build(realmName); } public static URI accountSessionsLogoutPage(URI baseUri, String realmName, String stateChecker) { - return accountBase(baseUri).path(AccountService.class, "processSessionsLogout") + return accountBase(baseUri).path(AccountFormService.class, "processSessionsLogout") .queryParam("stateChecker", stateChecker) .build(realmName); } public static URI accountRevokeClientPage(URI baseUri, String realmName) { - return accountBase(baseUri).path(AccountService.class, "processRevokeGrant") + return accountBase(baseUri).path(AccountFormService.class, "processRevokeGrant") .build(realmName); } diff --git a/services/src/main/java/org/keycloak/services/managers/AppAuthManager.java b/services/src/main/java/org/keycloak/services/managers/AppAuthManager.java index 50972f4931..bebcb2de49 100755 --- a/services/src/main/java/org/keycloak/services/managers/AppAuthManager.java +++ b/services/src/main/java/org/keycloak/services/managers/AppAuthManager.java @@ -58,6 +58,10 @@ public class AppAuthManager extends AuthenticationManager { return authenticateBearerToken(session, realm, ctx.getUri(), ctx.getConnection(), ctx.getRequestHeaders()); } + public AuthResult authenticateBearerToken(KeycloakSession session) { + return authenticateBearerToken(session, session.getContext().getRealm(), session.getContext().getUri(), session.getContext().getConnection(), session.getContext().getRequestHeaders()); + } + public AuthResult authenticateBearerToken(KeycloakSession session, RealmModel realm, UriInfo uriInfo, ClientConnection connection, HttpHeaders headers) { String tokenString = extractAuthorizationHeaderToken(headers); if (tokenString == null) return null; diff --git a/services/src/main/java/org/keycloak/services/managers/Auth.java b/services/src/main/java/org/keycloak/services/managers/Auth.java index 8b6086e04b..7c97c8f83c 100755 --- a/services/src/main/java/org/keycloak/services/managers/Auth.java +++ b/services/src/main/java/org/keycloak/services/managers/Auth.java @@ -23,6 +23,7 @@ import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; import org.keycloak.representations.AccessToken; +import org.keycloak.services.ForbiddenException; /** * @author Stian Thorgersen @@ -79,6 +80,18 @@ public class Auth { this.clientSession = clientSession; } + public void require(String role) { + if (!hasClientRole(client, role)) { + throw new ForbiddenException(); + } + } + + public void requireOneOf(String... roles) { + if (!hasOneOfAppRole(client, roles)) { + throw new ForbiddenException(); + } + } + public boolean hasRealmRole(String role) { if (cookie) { return user.hasRole(realm.getRole(role)); 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 02351204aa..fa9fec6313 100755 --- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java +++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java @@ -136,6 +136,19 @@ public class AuthenticationManager { } + public static void backchannelLogout(KeycloakSession session, UserSessionModel userSession, boolean logoutBroker) { + backchannelLogout( + session, + session.getContext().getRealm(), + userSession, + session.getContext().getUri(), + session.getContext().getConnection(), + session.getContext().getRequestHeaders(), + logoutBroker + ); + } + + /** * Do not logout broker * 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 180694ada4..35359f480c 100755 --- a/services/src/main/java/org/keycloak/services/messages/Messages.java +++ b/services/src/main/java/org/keycloak/services/messages/Messages.java @@ -176,6 +176,8 @@ public class Messages { public static final String READ_ONLY_USER = "readOnlyUserMessage"; + public static final String READ_ONLY_USERNAME = "readOnlyUsernameMessage"; + public static final String READ_ONLY_PASSWORD = "readOnlyPasswordMessage"; public static final String SUCCESS_TOTP_REMOVED = "successTotpRemovedMessage"; diff --git a/services/src/main/java/org/keycloak/services/resources/AbstractSecuredLocalService.java b/services/src/main/java/org/keycloak/services/resources/AbstractSecuredLocalService.java index cc8abfb685..7b85795509 100755 --- a/services/src/main/java/org/keycloak/services/resources/AbstractSecuredLocalService.java +++ b/services/src/main/java/org/keycloak/services/resources/AbstractSecuredLocalService.java @@ -203,32 +203,6 @@ public abstract class AbstractSecuredLocalService { return oauth.redirect(uriInfo, accountUri.toString()); } - protected Response authenticateBrowser() { - AppAuthManager authManager = new AppAuthManager(); - AuthenticationManager.AuthResult authResult = authManager.authenticateIdentityCookie(session, realm); - if (authResult != null) { - auth = new Auth(realm, authResult.getToken(), authResult.getUser(), client, authResult.getSession(), true); - } else { - return login(null); - } - // don't allow cors requests - // This is to prevent CSRF attacks. - String requestOrigin = UriUtils.getOrigin(uriInfo.getBaseUri()); - String origin = headers.getRequestHeaders().getFirst("Origin"); - if (origin != null && !requestOrigin.equals(origin)) { - throw new ForbiddenException(); - } - - if (!request.getHttpMethod().equals("GET")) { - String referrer = headers.getRequestHeaders().getFirst("Referer"); - if (referrer != null && !requestOrigin.equals(UriUtils.getOrigin(referrer))) { - throw new ForbiddenException(); - } - } - updateCsrfChecks(); - return null; - } - static class OAuthRedirect extends AbstractOAuthClient { /** diff --git a/services/src/main/java/org/keycloak/services/resources/Cors.java b/services/src/main/java/org/keycloak/services/resources/Cors.java index c9bfa030fe..b647c75eda 100755 --- a/services/src/main/java/org/keycloak/services/resources/Cors.java +++ b/services/src/main/java/org/keycloak/services/resources/Cors.java @@ -108,28 +108,32 @@ public class Cors { public Cors allowedOrigins(String... allowedOrigins) { if (allowedOrigins != null && allowedOrigins.length > 0) { - this.allowedOrigins = new HashSet(Arrays.asList(allowedOrigins)); + this.allowedOrigins = new HashSet<>(Arrays.asList(allowedOrigins)); } return this; } public Cors allowedMethods(String... allowedMethods) { - this.allowedMethods = new HashSet(Arrays.asList(allowedMethods)); + this.allowedMethods = new HashSet<>(Arrays.asList(allowedMethods)); return this; } public Cors exposedHeaders(String... exposedHeaders) { - this.exposedHeaders = new HashSet(Arrays.asList(exposedHeaders)); + this.exposedHeaders = new HashSet<>(Arrays.asList(exposedHeaders)); return this; } public Response build() { String origin = request.getHttpHeaders().getRequestHeaders().getFirst(ORIGIN_HEADER); if (origin == null) { + logger.trace("No origin header ignoring"); return builder.build(); } if (!preflight && (allowedOrigins == null || (!allowedOrigins.contains(origin) && !allowedOrigins.contains(ACCESS_CONTROL_ALLOW_ORIGIN_WILDCARD)))) { + if (logger.isDebugEnabled()) { + logger.debugv("Invalid CORS request: origin {0} not in allowed origins {1}", origin, Arrays.toString(allowedOrigins.toArray())); + } return builder.build(); } @@ -165,23 +169,25 @@ public class Cors { builder.header(ACCESS_CONTROL_MAX_AGE, DEFAULT_MAX_AGE); } + logger.debug("Added CORS headers to response"); + return builder.build(); } public void build(HttpResponse response) { String origin = request.getHttpHeaders().getRequestHeaders().getFirst(ORIGIN_HEADER); if (origin == null) { - logger.debug("No origin returning"); + logger.trace("No origin header ignoring"); return; } if (!preflight && (allowedOrigins == null || (!allowedOrigins.contains(origin) && !allowedOrigins.contains(ACCESS_CONTROL_ALLOW_ORIGIN_WILDCARD)))) { - logger.debug("!preflight and no origin"); + if (logger.isDebugEnabled()) { + logger.debugv("Invalid CORS request: origin {0} not in allowed origins {1}", origin, Arrays.toString(allowedOrigins.toArray())); + } return; } - logger.debug("build CORS headers and return"); - if (allowedOrigins.contains(ACCESS_CONTROL_ALLOW_ORIGIN_WILDCARD)) { response.getOutputHeaders().add(ACCESS_CONTROL_ALLOW_ORIGIN, ACCESS_CONTROL_ALLOW_ORIGIN_WILDCARD); } else { @@ -213,6 +219,8 @@ public class Cors { if (preflight) { response.getOutputHeaders().add(ACCESS_CONTROL_MAX_AGE, DEFAULT_MAX_AGE); } + + logger.debug("Added CORS headers to response"); } } diff --git a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java index 7961163231..2ef481e2d1 100755 --- a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java +++ b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java @@ -37,7 +37,6 @@ import org.keycloak.common.ClientConnection; import org.keycloak.common.util.Base64Url; import org.keycloak.common.util.ObjectUtil; import org.keycloak.common.util.Time; -import org.keycloak.common.util.UriUtils; import org.keycloak.events.Details; import org.keycloak.events.Errors; import org.keycloak.events.EventBuilder; @@ -77,6 +76,7 @@ import org.keycloak.services.managers.AuthenticationSessionManager; import org.keycloak.services.managers.BruteForceProtector; import org.keycloak.services.managers.ClientSessionCode; import org.keycloak.services.messages.Messages; +import org.keycloak.services.resources.account.AccountFormService; import org.keycloak.services.util.BrowserHistoryHelper; import org.keycloak.services.util.CacheControlUtil; import org.keycloak.services.validation.Validation; @@ -1082,7 +1082,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal FormMessage errorMessage = new FormMessage(message, parameters); try { String serializedError = JsonSerialization.writeValueAsString(errorMessage); - authSession.setAuthNote(AccountService.ACCOUNT_MGMT_FORWARDED_ERROR_NOTE, serializedError); + authSession.setAuthNote(AccountFormService.ACCOUNT_MGMT_FORWARDED_ERROR_NOTE, serializedError); } catch (IOException ioe) { throw new RuntimeException(ioe); } diff --git a/services/src/main/java/org/keycloak/services/resources/PublicRealmResource.java b/services/src/main/java/org/keycloak/services/resources/PublicRealmResource.java index 7526139c3a..2890229220 100755 --- a/services/src/main/java/org/keycloak/services/resources/PublicRealmResource.java +++ b/services/src/main/java/org/keycloak/services/resources/PublicRealmResource.java @@ -25,6 +25,7 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.protocol.oidc.OIDCLoginProtocolService; import org.keycloak.representations.idm.PublishedRealmRepresentation; +import org.keycloak.services.resources.account.AccountFormService; import org.keycloak.services.resources.admin.AdminRoot; import javax.ws.rs.GET; @@ -91,7 +92,7 @@ public class PublicRealmResource { PublishedRealmRepresentation rep = new PublishedRealmRepresentation(); rep.setRealm(realm.getName()); rep.setTokenServiceUrl(OIDCLoginProtocolService.tokenServiceBaseUrl(uriInfo).build(realm.getName()).toString()); - rep.setAccountServiceUrl(AccountService.accountServiceBaseUrl(uriInfo).build(realm.getName()).toString()); + rep.setAccountServiceUrl(AccountFormService.accountServiceBaseUrl(uriInfo).build(realm.getName()).toString()); rep.setAdminApiUrl(uriInfo.getBaseUriBuilder().path(AdminRoot.class).build().toString()); rep.setPublicKeyPem(PemUtils.encodeKey(session.keys().getActiveRsaKey(realm).getPublicKey())); rep.setNotBefore(realm.getNotBefore()); 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 bc3f8dc19a..18a6fd9930 100755 --- a/services/src/main/java/org/keycloak/services/resources/RealmsResource.java +++ b/services/src/main/java/org/keycloak/services/resources/RealmsResource.java @@ -26,7 +26,6 @@ import org.keycloak.common.Profile; import org.keycloak.common.util.KeycloakUriBuilder; import org.keycloak.events.EventBuilder; import org.keycloak.models.ClientModel; -import org.keycloak.models.Constants; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.protocol.LoginProtocol; @@ -34,6 +33,7 @@ import org.keycloak.protocol.LoginProtocolFactory; import org.keycloak.services.clientregistration.ClientRegistrationService; import org.keycloak.services.managers.RealmManager; import org.keycloak.services.resource.RealmResourceProvider; +import org.keycloak.services.resources.account.AccountLoader; import org.keycloak.services.util.CacheControlUtil; import org.keycloak.services.util.ResolveRelative; import org.keycloak.utils.ProfileHelper; @@ -206,20 +206,10 @@ public class RealmsResource { } @Path("{realm}/account") - public AccountService getAccountService(final @PathParam("realm") String name) { + public Object getAccountService(final @PathParam("realm") String name) { RealmModel realm = init(name); - - ClientModel client = realm.getClientByClientId(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID); - if (client == null || !client.isEnabled()) { - logger.debug("account management not enabled"); - throw new NotFoundException("account management not enabled"); - } - EventBuilder event = new EventBuilder(realm, session, clientConnection); - AccountService accountService = new AccountService(realm, client, event); - ResteasyProviderFactory.getInstance().injectProperties(accountService); - accountService.init(); - return accountService; + return AccountLoader.getAccountService(session, event); } @Path("{realm}") diff --git a/services/src/main/java/org/keycloak/services/resources/AccountService.java b/services/src/main/java/org/keycloak/services/resources/account/AccountFormService.java similarity index 79% rename from services/src/main/java/org/keycloak/services/resources/AccountService.java rename to services/src/main/java/org/keycloak/services/resources/account/AccountFormService.java index 9b26b18c07..e5da4ac3ac 100755 --- a/services/src/main/java/org/keycloak/services/resources/AccountService.java +++ b/services/src/main/java/org/keycloak/services/resources/account/AccountFormService.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.keycloak.services.resources; +package org.keycloak.services.resources.account; import org.jboss.logging.Logger; import org.keycloak.common.util.Base64Url; @@ -44,9 +44,7 @@ import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; import org.keycloak.models.utils.CredentialValidation; import org.keycloak.models.utils.FormMessage; -import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.protocol.oidc.utils.RedirectUtils; -import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.services.ForbiddenException; import org.keycloak.services.ServicesLogger; import org.keycloak.services.Urls; @@ -55,14 +53,17 @@ import org.keycloak.services.managers.Auth; import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.UserSessionManager; import org.keycloak.services.messages.Messages; +import org.keycloak.services.resources.AbstractSecuredLocalService; +import org.keycloak.services.resources.AttributeFormDataProcessor; +import org.keycloak.services.resources.RealmsResource; import org.keycloak.services.util.ResolveRelative; import org.keycloak.services.validation.Validation; import org.keycloak.sessions.AuthenticationSessionModel; import org.keycloak.storage.ReadOnlyException; import org.keycloak.util.JsonSerialization; -import org.keycloak.utils.MediaType; import javax.ws.rs.*; +import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriBuilder; @@ -83,13 +84,13 @@ import java.util.UUID; /** * @author Stian Thorgersen */ -public class AccountService extends AbstractSecuredLocalService { +public class AccountFormService extends AbstractSecuredLocalService { - private static final Logger logger = Logger.getLogger(AccountService.class); + private static final Logger logger = Logger.getLogger(AccountFormService.class); private static Set VALID_PATHS = new HashSet(); static { - for (Method m : AccountService.class.getMethods()) { + for (Method m : AccountFormService.class.getMethods()) { Path p = m.getAnnotation(Path.class); if (p != null) { VALID_PATHS.add(p.value()); @@ -97,20 +98,6 @@ public class AccountService extends AbstractSecuredLocalService { } } - private static final EventType[] LOG_EVENTS = {EventType.LOGIN, EventType.LOGOUT, EventType.REGISTER, EventType.REMOVE_FEDERATED_IDENTITY, EventType.REMOVE_TOTP, EventType.SEND_RESET_PASSWORD, - EventType.SEND_VERIFY_EMAIL, EventType.FEDERATED_IDENTITY_LINK, EventType.UPDATE_EMAIL, EventType.UPDATE_PASSWORD, EventType.UPDATE_PROFILE, EventType.UPDATE_TOTP, EventType.VERIFY_EMAIL}; - - private static final Set LOG_DETAILS = new HashSet(); - static { - LOG_DETAILS.add(Details.UPDATED_EMAIL); - LOG_DETAILS.add(Details.EMAIL); - LOG_DETAILS.add(Details.PREVIOUS_EMAIL); - LOG_DETAILS.add(Details.USERNAME); - LOG_DETAILS.add(Details.REMEMBER_ME); - LOG_DETAILS.add(Details.REGISTER_METHOD); - LOG_DETAILS.add(Details.AUTH_METHOD); - } - // Used when some other context (ie. IdentityBrokerService) wants to forward error to account management and display it here public static final String ACCOUNT_MGMT_FORWARDED_ERROR_NOTE = "ACCOUNT_MGMT_FORWARDED_ERROR"; @@ -119,7 +106,7 @@ public class AccountService extends AbstractSecuredLocalService { private AccountProvider account; private EventStoreProvider eventStore; - public AccountService(RealmModel realm, ClientModel client, EventBuilder event) { + public AccountFormService(RealmModel realm, ClientModel client, EventBuilder event) { super(realm, client); this.event = event; this.authManager = new AppAuthManager(); @@ -130,33 +117,24 @@ public class AccountService extends AbstractSecuredLocalService { account = session.getProvider(AccountProvider.class).setRealm(realm).setUriInfo(uriInfo).setHttpHeaders(headers); - AuthenticationManager.AuthResult authResult = authManager.authenticateBearerToken(session, realm, uriInfo, clientConnection, headers); + AuthenticationManager.AuthResult authResult = authManager.authenticateIdentityCookie(session, realm); if (authResult != null) { - auth = new Auth(realm, authResult.getToken(), authResult.getUser(), client, authResult.getSession(), false); - } else { - authResult = authManager.authenticateIdentityCookie(session, realm); - if (authResult != null) { - auth = new Auth(realm, authResult.getToken(), authResult.getUser(), client, authResult.getSession(), true); - updateCsrfChecks(); - account.setStateChecker(stateChecker); - } + auth = new Auth(realm, authResult.getToken(), authResult.getUser(), client, authResult.getSession(), true); + updateCsrfChecks(); + account.setStateChecker(stateChecker); } String requestOrigin = UriUtils.getOrigin(uriInfo.getBaseUri()); - // don't allow cors requests unless they were authenticated by an access token - // This is to prevent CSRF attacks. - if (auth != null && auth.isCookieAuthenticated()) { - String origin = headers.getRequestHeaders().getFirst("Origin"); - if (origin != null && !requestOrigin.equals(origin)) { - throw new ForbiddenException(); - } + String origin = headers.getRequestHeaders().getFirst("Origin"); + if (origin != null && !requestOrigin.equals(origin)) { + throw new ForbiddenException(); + } - if (!request.getHttpMethod().equals("GET")) { - String referrer = headers.getRequestHeaders().getFirst("Referer"); - if (referrer != null && !requestOrigin.equals(UriUtils.getOrigin(referrer))) { - throw new ForbiddenException(); - } + if (!request.getHttpMethod().equals("GET")) { + String referrer = headers.getRequestHeaders().getFirst("Referer"); + if (referrer != null && !requestOrigin.equals(UriUtils.getOrigin(referrer))) { + throw new ForbiddenException(); } } @@ -171,13 +149,9 @@ public class AccountService extends AbstractSecuredLocalService { } account.setUser(auth.getUser()); - } - boolean eventsEnabled = eventStore != null && realm.isEventsEnabled(); - - // todo find out from federation if password is updatable - account.setFeatures(realm.isIdentityFederationEnabled(), eventsEnabled, true); + account.setFeatures(realm.isIdentityFederationEnabled(), eventStore != null && realm.isEventsEnabled(), true); } public static UriBuilder accountServiceBaseUrl(UriInfo uriInfo) { @@ -186,21 +160,17 @@ public class AccountService extends AbstractSecuredLocalService { } public static UriBuilder accountServiceApplicationPage(UriInfo uriInfo) { - return accountServiceBaseUrl(uriInfo).path(AccountService.class, "applicationsPage"); - } - - public static UriBuilder accountServiceBaseUrl(UriBuilder base) { - return base.path(RealmsResource.class).path(RealmsResource.class, "getAccountService"); + return accountServiceBaseUrl(uriInfo).path(AccountFormService.class, "applicationsPage"); } protected Set getValidPaths() { - return AccountService.VALID_PATHS; + return AccountFormService.VALID_PATHS; } private Response forwardToPage(String path, AccountPages page) { if (auth != null) { try { - require(AccountRoles.MANAGE_ACCOUNT); + auth.require(AccountRoles.MANAGE_ACCOUNT); } catch (ForbiddenException e) { return session.getProvider(LoginFormsProvider.class).setError(Messages.NO_ACCESS).createErrorPage(); } @@ -228,24 +198,13 @@ public class AccountService extends AbstractSecuredLocalService { } } - protected void setReferrerOnPage() { + private void setReferrerOnPage() { String[] referrer = getReferrer(); if (referrer != null) { account.setReferrer(referrer); } } - /** - * CORS preflight - * - * @return - */ - @Path("/") - @OPTIONS - public Response accountPreflight() { - return Cors.add(request, Response.ok()).auth().preflight().build(); - } - /** * Get account information. * @@ -253,28 +212,13 @@ public class AccountService extends AbstractSecuredLocalService { */ @Path("/") @GET + @Produces(MediaType.TEXT_HTML) public Response accountPage() { - if (session.getContext().getRequestHeaders().getAcceptableMediaTypes().contains(MediaType.APPLICATION_JSON_TYPE)) { - requireOneOf(AccountRoles.MANAGE_ACCOUNT, AccountRoles.VIEW_PROFILE); - - UserRepresentation rep = ModelToRepresentation.toRepresentation(session, realm, auth.getUser()); - if (rep.getAttributes() != null) { - Iterator itr = rep.getAttributes().keySet().iterator(); - while (itr.hasNext()) { - if (itr.next().startsWith("keycloak.")) { - itr.remove(); - } - } - } - - return Cors.add(request, Response.ok(rep).type(MediaType.APPLICATION_JSON_TYPE)).auth().allowedOrigins(auth.getToken()).build(); - } else { - return forwardToPage(null, AccountPages.ACCOUNT); - } + return forwardToPage(null, AccountPages.ACCOUNT); } public static UriBuilder totpUrl(UriBuilder base) { - return RealmsResource.accountUrl(base).path(AccountService.class, "totpPage"); + return RealmsResource.accountUrl(base).path(AccountFormService.class, "totpPage"); } @Path("totp") @GET @@ -283,7 +227,7 @@ public class AccountService extends AbstractSecuredLocalService { } public static UriBuilder passwordUrl(UriBuilder base) { - return RealmsResource.accountUrl(base).path(AccountService.class, "passwordPage"); + return RealmsResource.accountUrl(base).path(AccountFormService.class, "passwordPage"); } @Path("password") @GET @@ -305,12 +249,12 @@ public class AccountService extends AbstractSecuredLocalService { @GET public Response logPage() { if (auth != null) { - List events = eventStore.createQuery().type(LOG_EVENTS).user(auth.getUser().getId()).maxResults(30).getResultList(); + List events = eventStore.createQuery().type(Constants.EXPOSED_LOG_EVENTS).user(auth.getUser().getId()).maxResults(30).getResultList(); for (Event e : events) { if (e.getDetails() != null) { Iterator> itr = e.getDetails().entrySet().iterator(); while (itr.hasNext()) { - if (!LOG_DETAILS.contains(itr.next().getKey())) { + if (!Constants.EXPOSED_LOG_DETAILS.contains(itr.next().getKey())) { itr.remove(); } } @@ -356,7 +300,7 @@ public class AccountService extends AbstractSecuredLocalService { return login(null); } - require(AccountRoles.MANAGE_ACCOUNT); + auth.require(AccountRoles.MANAGE_ACCOUNT); String action = formData.getFirst("submitAction"); if (action != null && action.equals("Cancel")) { @@ -377,8 +321,8 @@ public class AccountService extends AbstractSecuredLocalService { } try { - updateUsername(formData.getFirst("username"), user); - updateEmail(formData.getFirst("email"), user); + updateUsername(formData.getFirst("username"), user, session); + updateEmail(formData.getFirst("email"), user, session, event); user.setFirstName(formData.getFirst("firstName")); user.setLastName(formData.getFirst("lastName")); @@ -398,82 +342,6 @@ public class AccountService extends AbstractSecuredLocalService { } } - @Path("/") - @POST - @Consumes(MediaType.APPLICATION_JSON) - @Produces(MediaType.APPLICATION_JSON) - public Response processAccountUpdateJson(UserRepresentation userRep) { - require(AccountRoles.MANAGE_ACCOUNT); - if (auth.isCookieAuthenticated()) { - throw new ForbiddenException(); - } - - UserModel user = auth.getUser(); - - event.event(EventType.UPDATE_PROFILE).client(auth.getClient()).user(auth.getUser()); - - updateUsername(userRep.getUsername(), user); - updateEmail(userRep.getEmail(), user); - - user.setFirstName(userRep.getFirstName()); - user.setLastName(userRep.getLastName()); - - if (userRep.getAttributes() != null) { - for (String k : user.getAttributes().keySet()) { - if (!userRep.getAttributes().containsKey(k)) { - user.removeAttribute(k); - } - } - - for (Map.Entry> e : userRep.getAttributes().entrySet()) { - user.setAttribute(e.getKey(), e.getValue()); - } - } - - event.success(); - - return Cors.add(request, Response.ok()).build(); - } - - private void updateUsername(String username, UserModel user) { - if (realm.isEditUsernameAllowed() && username != null) { - UserModel existing = session.users().getUserByUsername(username, realm); - if (existing != null && !existing.getId().equals(user.getId())) { - throw new ModelDuplicateException(Messages.USERNAME_EXISTS); - } - - user.setUsername(username); - } - } - - private void updateEmail(String email, UserModel user) { - String oldEmail = user.getEmail(); - boolean emailChanged = oldEmail != null ? !oldEmail.equals(email) : email != null; - if (emailChanged && !realm.isDuplicateEmailsAllowed()) { - UserModel existing = session.users().getUserByEmail(email, realm); - if (existing != null && !existing.getId().equals(user.getId())) { - throw new ModelDuplicateException(Messages.EMAIL_EXISTS); - } - } - - user.setEmail(email); - - if (emailChanged) { - user.setEmailVerified(false); - event.clone().event(EventType.UPDATE_EMAIL).detail(Details.PREVIOUS_EMAIL, oldEmail).detail(Details.UPDATED_EMAIL, email).success(); - } - - if (realm.isRegistrationEmailAsUsername()) { - if (!realm.isDuplicateEmailsAllowed()) { - UserModel existing = session.users().getUserByEmail(email, realm); - if (existing != null && !existing.getId().equals(user.getId())) { - throw new ModelDuplicateException(Messages.USERNAME_EXISTS); - } - } - user.setUsername(email); - } - } - @Path("totp-remove") @GET public Response processTotpRemove(@QueryParam("stateChecker") String stateChecker) { @@ -481,7 +349,7 @@ public class AccountService extends AbstractSecuredLocalService { return login("totp"); } - require(AccountRoles.MANAGE_ACCOUNT); + auth.require(AccountRoles.MANAGE_ACCOUNT); csrfCheck(stateChecker); @@ -502,7 +370,7 @@ public class AccountService extends AbstractSecuredLocalService { return login("sessions"); } - require(AccountRoles.MANAGE_ACCOUNT); + auth.require(AccountRoles.MANAGE_ACCOUNT); csrfCheck(stateChecker); UserModel user = auth.getUser(); @@ -516,7 +384,7 @@ public class AccountService extends AbstractSecuredLocalService { AuthenticationManager.backchannelLogout(session, realm, userSession, uriInfo, clientConnection, headers, true); } - UriBuilder builder = Urls.accountBase(uriInfo.getBaseUri()).path(AccountService.class, "sessionsPage"); + UriBuilder builder = Urls.accountBase(uriInfo.getBaseUri()).path(AccountFormService.class, "sessionsPage"); String referrer = uriInfo.getQueryParameters().getFirst("referrer"); if (referrer != null) { builder.queryParam("referrer", referrer); @@ -534,7 +402,7 @@ public class AccountService extends AbstractSecuredLocalService { return login("applications"); } - require(AccountRoles.MANAGE_ACCOUNT); + auth.require(AccountRoles.MANAGE_ACCOUNT); csrfCheck(formData); String clientId = formData.getFirst("clientId"); @@ -557,7 +425,7 @@ public class AccountService extends AbstractSecuredLocalService { event.event(EventType.REVOKE_GRANT).client(auth.getClient()).user(auth.getUser()).detail(Details.REVOKED_CLIENT, client.getClientId()).success(); setReferrerOnPage(); - UriBuilder builder = Urls.accountBase(uriInfo.getBaseUri()).path(AccountService.class, "applicationsPage"); + UriBuilder builder = Urls.accountBase(uriInfo.getBaseUri()).path(AccountFormService.class, "applicationsPage"); String referrer = uriInfo.getQueryParameters().getFirst("referrer"); if (referrer != null) { builder.queryParam("referrer", referrer); @@ -586,7 +454,7 @@ public class AccountService extends AbstractSecuredLocalService { return login("totp"); } - require(AccountRoles.MANAGE_ACCOUNT); + auth.require(AccountRoles.MANAGE_ACCOUNT); String action = formData.getFirst("submitAction"); if (action != null && action.equals("Cancel")) { @@ -646,7 +514,7 @@ public class AccountService extends AbstractSecuredLocalService { return login("password"); } - require(AccountRoles.MANAGE_ACCOUNT); + auth.require(AccountRoles.MANAGE_ACCOUNT); csrfCheck(formData); UserModel user = auth.getUser(); @@ -729,7 +597,7 @@ public class AccountService extends AbstractSecuredLocalService { return login("identity"); } - require(AccountRoles.MANAGE_ACCOUNT); + auth.require(AccountRoles.MANAGE_ACCOUNT); csrfCheck(stateChecker); UserModel user = auth.getUser(); @@ -816,7 +684,7 @@ public class AccountService extends AbstractSecuredLocalService { } public static UriBuilder loginRedirectUrl(UriBuilder base) { - return RealmsResource.accountUrl(base).path(AccountService.class, "loginRedirect"); + return RealmsResource.accountUrl(base).path(AccountFormService.class, "loginRedirect"); } @Override @@ -865,27 +733,7 @@ public class AccountService extends AbstractSecuredLocalService { return null; } - public void require(String role) { - if (auth == null) { - throw new ForbiddenException(); - } - - if (!auth.hasClientRole(client, role)) { - throw new ForbiddenException(); - } - } - - public void requireOneOf(String... roles) { - if (auth == null) { - throw new ForbiddenException(); - } - - if (!auth.hasOneOfAppRole(client, roles)) { - throw new ForbiddenException(); - } - } - - public enum AccountSocialAction { + private enum AccountSocialAction { ADD, REMOVE; @@ -899,4 +747,52 @@ public class AccountService extends AbstractSecuredLocalService { } } } + + + private void updateUsername(String username, UserModel user, KeycloakSession session) { + RealmModel realm = session.getContext().getRealm(); + boolean usernameChanged = username == null || !user.getUsername().equals(username); + if (realm.isEditUsernameAllowed()) { + if (usernameChanged) { + UserModel existing = session.users().getUserByUsername(username, realm); + if (existing != null && !existing.getId().equals(user.getId())) { + throw new ModelDuplicateException(Messages.USERNAME_EXISTS); + } + + user.setUsername(username); + } + } else if (usernameChanged) { + + } + } + + private void updateEmail(String email, UserModel user, KeycloakSession session, EventBuilder event) { + RealmModel realm = session.getContext().getRealm(); + String oldEmail = user.getEmail(); + boolean emailChanged = oldEmail != null ? !oldEmail.equals(email) : email != null; + if (emailChanged && !realm.isDuplicateEmailsAllowed()) { + UserModel existing = session.users().getUserByEmail(email, realm); + if (existing != null && !existing.getId().equals(user.getId())) { + throw new ModelDuplicateException(Messages.EMAIL_EXISTS); + } + } + + user.setEmail(email); + + if (emailChanged) { + user.setEmailVerified(false); + event.clone().event(EventType.UPDATE_EMAIL).detail(Details.PREVIOUS_EMAIL, oldEmail).detail(Details.UPDATED_EMAIL, email).success(); + } + + if (realm.isRegistrationEmailAsUsername()) { + if (!realm.isDuplicateEmailsAllowed()) { + UserModel existing = session.users().getUserByEmail(email, realm); + if (existing != null && !existing.getId().equals(user.getId())) { + throw new ModelDuplicateException(Messages.USERNAME_EXISTS); + } + } + user.setUsername(email); + } + } + } diff --git a/services/src/main/java/org/keycloak/services/resources/account/AccountLoader.java b/services/src/main/java/org/keycloak/services/resources/account/AccountLoader.java new file mode 100644 index 0000000000..11c3161ef0 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/resources/account/AccountLoader.java @@ -0,0 +1,83 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.services.resources.account; + +import org.jboss.logging.Logger; +import org.jboss.resteasy.spi.HttpRequest; +import org.jboss.resteasy.spi.ResteasyProviderFactory; +import org.keycloak.events.EventBuilder; +import org.keycloak.models.ClientModel; +import org.keycloak.models.Constants; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.services.managers.AppAuthManager; +import org.keycloak.services.managers.Auth; +import org.keycloak.services.managers.AuthenticationManager; + +import javax.ws.rs.HttpMethod; +import javax.ws.rs.NotAuthorizedException; +import javax.ws.rs.NotFoundException; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; +import java.util.List; + +/** + * @author Stian Thorgersen + */ +public class AccountLoader { + + private static final Logger logger = Logger.getLogger(AccountLoader.class); + + private AccountLoader() { + } + + public static Object getAccountService(KeycloakSession session, EventBuilder event) { + RealmModel realm = session.getContext().getRealm(); + + ClientModel client = realm.getClientByClientId(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID); + if (client == null || !client.isEnabled()) { + logger.debug("account management not enabled"); + throw new NotFoundException("account management not enabled"); + } + + HttpRequest request = session.getContext().getContextObject(HttpRequest.class); + HttpHeaders headers = session.getContext().getRequestHeaders(); + MediaType content = headers.getMediaType(); + List accepts = headers.getAcceptableMediaTypes(); + + if (request.getHttpMethod().equals(HttpMethod.OPTIONS)) { + return new CorsPreflightService(request); + } else if ((accepts.contains(MediaType.APPLICATION_JSON_TYPE) || MediaType.APPLICATION_JSON_TYPE.equals(content)) && !request.getUri().getPath().endsWith("keycloak.json")) { + AuthenticationManager.AuthResult authResult = new AppAuthManager().authenticateBearerToken(session); + if (authResult == null) { + throw new NotAuthorizedException("Bearer token required"); + } + + Auth auth = new Auth(session.getContext().getRealm(), authResult.getToken(), authResult.getUser(), client, authResult.getSession(), false); + AccountRestService accountRestService = new AccountRestService(session, auth, client, event); + ResteasyProviderFactory.getInstance().injectProperties(accountRestService); + accountRestService.init(); + return accountRestService; + } else { + AccountFormService accountFormService = new AccountFormService(realm, client, event); + ResteasyProviderFactory.getInstance().injectProperties(accountFormService); + accountFormService.init(); + return accountFormService; + } + } + +} diff --git a/services/src/main/java/org/keycloak/services/resources/account/AccountRestService.java b/services/src/main/java/org/keycloak/services/resources/account/AccountRestService.java new file mode 100755 index 0000000000..e327519378 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/resources/account/AccountRestService.java @@ -0,0 +1,270 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.services.resources.account; + +import org.jboss.resteasy.annotations.cache.NoCache; +import org.jboss.resteasy.spi.HttpRequest; +import org.keycloak.common.ClientConnection; +import org.keycloak.events.Details; +import org.keycloak.events.EventBuilder; +import org.keycloak.events.EventStoreProvider; +import org.keycloak.events.EventType; +import org.keycloak.models.AccountRoles; +import org.keycloak.models.ClientModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.UserSessionModel; +import org.keycloak.representations.account.ClientRepresentation; +import org.keycloak.representations.account.SessionRepresentation; +import org.keycloak.representations.account.UserRepresentation; +import org.keycloak.services.ErrorResponse; +import org.keycloak.services.managers.Auth; +import org.keycloak.services.managers.AuthenticationManager; +import org.keycloak.services.resources.Cors; +import org.keycloak.storage.ReadOnlyException; + +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.OPTIONS; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +/** + * @author Stian Thorgersen + */ +public class AccountRestService { + + @Context + private HttpRequest request; + @Context + protected UriInfo uriInfo; + @Context + protected HttpHeaders headers; + @Context + protected ClientConnection clientConnection; + + private final KeycloakSession session; + private final ClientModel client; + private final EventBuilder event; + private EventStoreProvider eventStore; + private Auth auth; + + private final RealmModel realm; + private final UserModel user; + + public AccountRestService(KeycloakSession session, Auth auth, ClientModel client, EventBuilder event) { + this.session = session; + this.auth = auth; + this.realm = auth.getRealm(); + this.user = auth.getUser(); + this.client = client; + this.event = event; + } + + public void init() { + eventStore = session.getProvider(EventStoreProvider.class); + } + + /** + * CORS preflight + * + * @return + */ + @Path("/") + @OPTIONS + @NoCache + public Response preflight() { + return Cors.add(request, Response.ok()).auth().preflight().build(); + } + + /** + * Get account information. + * + * @return + */ + @Path("/") + @GET + @Produces(MediaType.APPLICATION_JSON) + @NoCache + public Response account() { + auth.requireOneOf(AccountRoles.MANAGE_ACCOUNT, AccountRoles.VIEW_PROFILE); + + UserModel user = auth.getUser(); + + UserRepresentation rep = new UserRepresentation(); + rep.setUsername(user.getUsername()); + rep.setFirstName(user.getFirstName()); + rep.setLastName(user.getLastName()); + rep.setEmail(user.getEmail()); + rep.setEmailVerified(user.isEmailVerified()); + rep.setAttributes(user.getAttributes()); + + return Cors.add(request, Response.ok(rep)).auth().allowedOrigins(auth.getToken()).build(); + } + + @Path("/") + @POST + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @NoCache + public Response updateAccount(UserRepresentation userRep) { + auth.require(AccountRoles.MANAGE_ACCOUNT); + + event.event(EventType.UPDATE_PROFILE).client(auth.getClient()).user(user); + + try { + RealmModel realm = session.getContext().getRealm(); + + boolean usernameChanged = userRep.getUsername() != null && !userRep.getUsername().equals(user.getUsername()); + if (realm.isEditUsernameAllowed()) { + if (usernameChanged) { + UserModel existing = session.users().getUserByUsername(userRep.getUsername(), realm); + if (existing != null) { + return ErrorResponse.exists(Errors.USERNAME_EXISTS); + } + + user.setUsername(userRep.getUsername()); + } + } else if (usernameChanged) { + return ErrorResponse.error(Errors.READ_ONLY_USERNAME, Response.Status.BAD_REQUEST); + } + + boolean emailChanged = userRep.getEmail() != null && !userRep.getEmail().equals(user.getEmail()); + if (emailChanged && !realm.isDuplicateEmailsAllowed()) { + UserModel existing = session.users().getUserByEmail(userRep.getEmail(), realm); + if (existing != null) { + return ErrorResponse.exists(Errors.EMAIL_EXISTS); + } + } + + if (realm.isRegistrationEmailAsUsername() && !realm.isDuplicateEmailsAllowed()) { + UserModel existing = session.users().getUserByUsername(userRep.getEmail(), realm); + if (existing != null) { + return ErrorResponse.exists(Errors.USERNAME_EXISTS); + } + } + + if (emailChanged) { + String oldEmail = user.getEmail(); + user.setEmail(userRep.getEmail()); + user.setEmailVerified(false); + event.clone().event(EventType.UPDATE_EMAIL).detail(Details.PREVIOUS_EMAIL, oldEmail).detail(Details.UPDATED_EMAIL, userRep.getEmail()).success(); + + if (realm.isRegistrationEmailAsUsername()) { + user.setUsername(userRep.getEmail()); + } + } + + user.setFirstName(userRep.getFirstName()); + user.setLastName(userRep.getLastName()); + + if (userRep.getAttributes() != null) { + for (String k : user.getAttributes().keySet()) { + if (!userRep.getAttributes().containsKey(k)) { + user.removeAttribute(k); + } + } + + for (Map.Entry> e : userRep.getAttributes().entrySet()) { + user.setAttribute(e.getKey(), e.getValue()); + } + } + + event.success(); + + return Cors.add(request, Response.ok()).auth().allowedOrigins(auth.getToken()).build(); + } catch (ReadOnlyException e) { + return ErrorResponse.error(Errors.READ_ONLY_USER, Response.Status.BAD_REQUEST); + } + } + + /** + * Get session information. + * + * @return + */ + @Path("/sessions") + @GET + @Produces(MediaType.APPLICATION_JSON) + @NoCache + public Response sessions() { + List reps = new LinkedList<>(); + + List sessions = session.sessions().getUserSessions(realm, user); + for (UserSessionModel s : sessions) { + SessionRepresentation rep = new SessionRepresentation(); + rep.setId(s.getId()); + rep.setIpAddress(s.getIpAddress()); + rep.setStarted(s.getStarted()); + rep.setLastAccess(s.getLastSessionRefresh()); + rep.setExpires(s.getStarted() + realm.getSsoSessionMaxLifespan()); + rep.setClients(new LinkedList()); + + for (String clientUUID : s.getAuthenticatedClientSessions().keySet()) { + ClientModel client = realm.getClientById(clientUUID); + ClientRepresentation clientRep = new ClientRepresentation(); + clientRep.setClientId(client.getClientId()); + clientRep.setClientName(client.getName()); + rep.getClients().add(clientRep); + } + + reps.add(rep); + } + + return Cors.add(request, Response.ok(reps)).auth().allowedOrigins(auth.getToken()).build(); + } + + /** + * Remove sessions + * + * @param removeCurrent remove current session (default is false) + * @return + */ + @Path("/sessions") + @DELETE + @Produces(MediaType.APPLICATION_JSON) + @NoCache + public Response sessionsLogout(@QueryParam("current") boolean removeCurrent) { + UserSessionModel userSession = auth.getSession(); + + List userSessions = session.sessions().getUserSessions(realm, user); + for (UserSessionModel s : userSessions) { + if (removeCurrent || !s.getId().equals(userSession.getId())) { + AuthenticationManager.backchannelLogout(session, s, true); + } + } + + return Cors.add(request, Response.ok()).auth().allowedOrigins(auth.getToken()).build(); + } + + // TODO Federated identities + // TODO Applications + // TODO Logs + +} diff --git a/services/src/main/java/org/keycloak/services/resources/account/Constants.java b/services/src/main/java/org/keycloak/services/resources/account/Constants.java new file mode 100644 index 0000000000..95b76de399 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/resources/account/Constants.java @@ -0,0 +1,47 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.services.resources.account; + +import org.keycloak.events.Details; +import org.keycloak.events.EventType; + +import java.util.HashSet; +import java.util.Set; + +/** + * @author Stian Thorgersen + */ +public class Constants { + + public static final EventType[] EXPOSED_LOG_EVENTS = { + EventType.LOGIN, EventType.LOGOUT, EventType.REGISTER, EventType.REMOVE_FEDERATED_IDENTITY, EventType.REMOVE_TOTP, EventType.SEND_RESET_PASSWORD, + EventType.SEND_VERIFY_EMAIL, EventType.FEDERATED_IDENTITY_LINK, EventType.UPDATE_EMAIL, EventType.UPDATE_PASSWORD, EventType.UPDATE_PROFILE, EventType.UPDATE_TOTP, EventType.VERIFY_EMAIL + }; + + public static final Set EXPOSED_LOG_DETAILS = new HashSet<>(); + + static { + EXPOSED_LOG_DETAILS.add(Details.UPDATED_EMAIL); + EXPOSED_LOG_DETAILS.add(Details.EMAIL); + EXPOSED_LOG_DETAILS.add(Details.PREVIOUS_EMAIL); + EXPOSED_LOG_DETAILS.add(Details.USERNAME); + EXPOSED_LOG_DETAILS.add(Details.REMEMBER_ME); + EXPOSED_LOG_DETAILS.add(Details.REGISTER_METHOD); + EXPOSED_LOG_DETAILS.add(Details.AUTH_METHOD); + } + +} diff --git a/services/src/main/java/org/keycloak/services/resources/account/CorsPreflightService.java b/services/src/main/java/org/keycloak/services/resources/account/CorsPreflightService.java new file mode 100644 index 0000000000..f9c0fa6ec2 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/resources/account/CorsPreflightService.java @@ -0,0 +1,33 @@ +package org.keycloak.services.resources.account; + +import org.jboss.resteasy.spi.HttpRequest; +import org.keycloak.services.resources.Cors; + +import javax.ws.rs.OPTIONS; +import javax.ws.rs.Path; +import javax.ws.rs.core.Response; + +/** + * Created by st on 21/03/17. + */ +public class CorsPreflightService { + + private HttpRequest request; + + public CorsPreflightService(HttpRequest request) { + this.request = request; + } + + /** + * CORS preflight + * + * @return + */ + @Path("/") + @OPTIONS + public Response preflight() { + Cors cors = Cors.add(request, Response.ok()).auth().allowedMethods("GET", "POST", "HEAD", "OPTIONS").preflight(); + return cors.build(); + } + +} diff --git a/services/src/main/java/org/keycloak/services/resources/account/Errors.java b/services/src/main/java/org/keycloak/services/resources/account/Errors.java new file mode 100644 index 0000000000..6cb55bd0f6 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/resources/account/Errors.java @@ -0,0 +1,29 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.services.resources.account; + +/** + * @author Stian Thorgersen + */ +public class Errors { + + public static final String USERNAME_EXISTS = "username_exists"; + public static final String EMAIL_EXISTS = "email_exists"; + public static final String READ_ONLY_USER = "user_read_only"; + public static final String READ_ONLY_USERNAME = "username_read_only"; + +} diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java index 21943ccd79..5af5beb277 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java @@ -68,8 +68,8 @@ import org.keycloak.services.ServicesLogger; import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.BruteForceProtector; import org.keycloak.services.managers.UserSessionManager; -import org.keycloak.services.resources.AccountService; import org.keycloak.services.resources.LoginActionsService; +import org.keycloak.services.resources.account.AccountFormService; import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; import org.keycloak.services.validation.Validation; import org.keycloak.storage.ReadOnlyException; @@ -282,7 +282,7 @@ public class UserResource { String sessionId = KeycloakModelUtils.generateId(); UserSessionModel userSession = session.sessions().createUserSession(sessionId, realm, user, user.getUsername(), clientConnection.getRemoteAddr(), "impersonate", false, null, null); AuthenticationManager.createLoginCookie(session, realm, userSession.getUser(), userSession, uriInfo, clientConnection); - URI redirect = AccountService.accountServiceApplicationPage(uriInfo).build(realm.getName()); + URI redirect = AccountFormService.accountServiceApplicationPage(uriInfo).build(realm.getName()); Map result = new HashMap<>(); result.put("sameRealm", sameRealm); result.put("redirect", redirect.toString()); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java index c7b9945fc2..578885d292 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java @@ -68,7 +68,6 @@ import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.BruteForceProtector; import org.keycloak.services.*; import org.keycloak.services.managers.*; -import org.keycloak.services.resources.AccountService; import org.keycloak.services.resources.LoginActionsService; import org.keycloak.services.validation.Validation; import org.keycloak.storage.ReadOnlyException; diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/AccountPasswordPage.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/AccountPasswordPage.java index 2c98c55e40..d62806f0b4 100755 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/AccountPasswordPage.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/AccountPasswordPage.java @@ -16,7 +16,7 @@ */ package org.keycloak.testsuite.pages; -import org.keycloak.services.resources.AccountService; +import org.keycloak.services.resources.account.AccountFormService; import org.openqa.selenium.WebElement; import org.openqa.selenium.support.FindBy; @@ -69,6 +69,6 @@ public class AccountPasswordPage extends AbstractAccountPage { } public String getPath() { - return AccountService.passwordUrl(UriBuilder.fromUri(getAuthServerRoot())).build(this.realmName).toString(); + return AccountFormService.passwordUrl(UriBuilder.fromUri(getAuthServerRoot())).build(this.realmName).toString(); } } diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/AccountTotpPage.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/AccountTotpPage.java index 1029e1010a..6527476e02 100755 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/AccountTotpPage.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/AccountTotpPage.java @@ -16,7 +16,7 @@ */ package org.keycloak.testsuite.pages; -import org.keycloak.services.resources.AccountService; +import org.keycloak.services.resources.account.AccountFormService; import org.openqa.selenium.WebElement; import org.openqa.selenium.support.FindBy; @@ -40,7 +40,7 @@ public class AccountTotpPage extends AbstractAccountPage { private WebElement removeLink; private String getPath() { - return AccountService.totpUrl(UriBuilder.fromUri(getAuthServerRoot())).build("test").toString(); + return AccountFormService.totpUrl(UriBuilder.fromUri(getAuthServerRoot())).build("test").toString(); } public void configure(String totp) { diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/TokenUtil.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/TokenUtil.java new file mode 100644 index 0000000000..a75f5cd7cd --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/TokenUtil.java @@ -0,0 +1,85 @@ +package org.keycloak.testsuite.util; + +import org.junit.rules.TestRule; +import org.junit.runners.model.Statement; +import org.keycloak.common.util.Time; + +import static org.junit.Assert.fail; + +/** + * Created by st on 22/03/17. + */ +public class TokenUtil implements TestRule { + + private final String username; + private final String password; + private OAuthClient oauth; + + private String refreshToken; + private String token; + private int expires; + + public TokenUtil() { + this("test-user@localhost", "password"); + } + + public TokenUtil(String username, String password) { + this.username = username; + this.password = password; + this.oauth = new OAuthClient(); + this.oauth.init(null, null); + this.oauth.clientId("direct-grant"); + } + + @Override + public Statement apply(final Statement base, org.junit.runner.Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + base.evaluate(); + } + }; + } + + public String getToken() { + if (refreshToken == null) { + load(); + } else if (expires < Time.currentTime()) { + refresh(); + } + return token; + } + + private void load() { + try { + OAuthClient.AccessTokenResponse accessTokenResponse = oauth.doGrantAccessTokenRequest("password", username, password); + if (accessTokenResponse.getStatusCode() != 200) { + fail("Failed to get token: " + accessTokenResponse.getErrorDescription()); + } + + this.refreshToken = accessTokenResponse.getRefreshToken(); + this.token = accessTokenResponse.getAccessToken(); + + expires = Time.currentTime() + accessTokenResponse.getExpiresIn() - 20; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private void refresh() { + try { + OAuthClient.AccessTokenResponse accessTokenResponse = oauth.doRefreshTokenRequest(refreshToken, "password"); + if (accessTokenResponse.getStatusCode() != 200) { + fail("Failed to get token: " + accessTokenResponse.getErrorDescription()); + } + + this.refreshToken = accessTokenResponse.getRefreshToken(); + this.token = accessTokenResponse.getAccessToken(); + + expires = Time.currentTime() + accessTokenResponse.getExpiresIn() - 20; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + +} diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/WebDriverLogDumper.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/WebDriverLogDumper.java new file mode 100644 index 0000000000..f56a119fe3 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/WebDriverLogDumper.java @@ -0,0 +1,26 @@ +package org.keycloak.testsuite.util; + +import org.jboss.logging.Logger; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.logging.LogEntries; +import org.openqa.selenium.logging.LogEntry; + +/** + * Created by st on 21/03/17. + */ +public class WebDriverLogDumper { + + public static String dumpBrowserLogs(WebDriver driver) { + try { + StringBuilder sb = new StringBuilder(); + LogEntries logEntries = driver.manage().logs().get("browser"); + for (LogEntry e : logEntries.getAll()) { + sb.append("\n\t" + e.getMessage()); + } + return sb.toString(); + } catch (UnsupportedOperationException e) { + return "Browser doesn't support fetching logs"; + } + } + +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountFormServiceTest.java similarity index 99% rename from testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountTest.java rename to testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountFormServiceTest.java index c69f489000..2c6b772b71 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountFormServiceTest.java @@ -18,12 +18,10 @@ package org.keycloak.testsuite.account; import org.jboss.arquillian.drone.api.annotation.Drone; import org.jboss.arquillian.graphene.page.Page; -import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Rule; import org.junit.Test; - import org.keycloak.admin.client.resource.RealmResource; import org.keycloak.events.Details; import org.keycloak.events.Errors; @@ -37,10 +35,10 @@ import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.EventRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.UserRepresentation; -import org.keycloak.services.resources.AccountService; import org.keycloak.services.resources.RealmsResource; -import org.keycloak.testsuite.AssertEvents; +import org.keycloak.services.resources.account.AccountFormService; import org.keycloak.testsuite.AbstractTestRealmKeycloakTest; +import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.drone.Different; import org.keycloak.testsuite.pages.AccountApplicationsPage; @@ -59,7 +57,6 @@ import org.keycloak.testsuite.util.IdentityProviderBuilder; import org.keycloak.testsuite.util.OAuthClient; import org.keycloak.testsuite.util.RealmBuilder; import org.keycloak.testsuite.util.UserBuilder; - import org.openqa.selenium.By; import org.openqa.selenium.WebDriver; @@ -68,13 +65,15 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; -import static org.hamcrest.Matchers.*; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.hasItems; /** * @author Stian Thorgersen * @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc. */ -public class AccountTest extends AbstractTestRealmKeycloakTest { +public class AccountFormServiceTest extends AbstractTestRealmKeycloakTest { @Override public void configureTestRealm(RealmRepresentation testRealm) { @@ -121,7 +120,7 @@ public class AccountTest extends AbstractTestRealmKeycloakTest { private static final UriBuilder BASE = UriBuilder.fromUri("http://localhost:8180/auth"); private static final String ACCOUNT_URL = RealmsResource.accountUrl(BASE.clone()).build("test").toString(); - public static String ACCOUNT_REDIRECT = AccountService.loginRedirectUrl(BASE.clone()).build("test").toString(); + public static String ACCOUNT_REDIRECT = AccountFormService.loginRedirectUrl(BASE.clone()).build("test").toString(); // Create second session @Drone @@ -904,7 +903,7 @@ public class AccountTest extends AbstractTestRealmKeycloakTest { Assert.assertTrue(applicationsPage.isCurrent()); Map apps = applicationsPage.getApplications(); - Assert.assertThat(apps.keySet(), containsInAnyOrder("root-url-client", "Account", "test-app", "test-app-scope", "third-party", "test-app-authz", "My Named Test App", "Test App Named - ${client_account}")); + Assert.assertThat(apps.keySet(), containsInAnyOrder("root-url-client", "Account", "test-app", "test-app-scope", "third-party", "test-app-authz", "My Named Test App", "Test App Named - ${client_account}", "direct-grant")); AccountApplicationsPage.AppEntry accountEntry = apps.get("Account"); Assert.assertEquals(3, accountEntry.getRolesAvailable().size()); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountRestServiceCorsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountRestServiceCorsTest.java new file mode 100755 index 0000000000..3526386743 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountRestServiceCorsTest.java @@ -0,0 +1,180 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.testsuite.account; + +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.junit.After; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.representations.idm.UserRepresentation; +import org.keycloak.testsuite.AbstractTestRealmKeycloakTest; +import org.keycloak.testsuite.AssertEvents; +import org.keycloak.testsuite.util.OAuthClient; +import org.keycloak.testsuite.util.TokenUtil; +import org.keycloak.testsuite.util.WebDriverLogDumper; +import org.keycloak.util.JsonSerialization; +import org.openqa.selenium.JavascriptExecutor; + +import java.io.IOException; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * @author Stian Thorgersen + */ +public class AccountRestServiceCorsTest extends AbstractTestRealmKeycloakTest { + + private static final String VALID_CORS_URL = "http://localtest.me:8180/auth"; + private static final String INVALID_CORS_URL = "http://invalid.localtest.me:8180/auth"; + + @Rule + public TokenUtil tokenUtil = new TokenUtil(); + + private CloseableHttpClient client; + private JavascriptExecutor executor; + + @Before + public void before() { + client = HttpClientBuilder.create().build(); + oauth.clientId("direct-grant"); + executor = (JavascriptExecutor) driver; + } + + @After + public void after() { + try { + client.close(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public void configureTestRealm(RealmRepresentation testRealm) { + } + + @Rule + public AssertEvents events = new AssertEvents(this); + + @Test + public void testGetProfile() throws IOException, InterruptedException { + driver.navigate().to(VALID_CORS_URL); + + doJsGet(executor, getAccountUrl(), tokenUtil.getToken(), true); + } + + @Test + public void testGetProfileInvalidOrigin() throws IOException, InterruptedException { + driver.navigate().to(INVALID_CORS_URL); + + doJsGet(executor, getAccountUrl(), tokenUtil.getToken(), false); + } + + @Test + public void testUpdateProfile() throws IOException { + driver.navigate().to(VALID_CORS_URL); + + doJsPost(executor, getAccountUrl(), tokenUtil.getToken(), "{ \"firstName\" : \"Bob\" }", true); + } + + @Test + public void testUpdateProfileInvalidOrigin() throws IOException { + driver.navigate().to(INVALID_CORS_URL); + + doJsPost(executor, getAccountUrl(), tokenUtil.getToken(), "{ \"firstName\" : \"Bob\" }", false); + } + + private String getAccountUrl() { + return suiteContext.getAuthServerInfo().getContextRoot().toString() + "/auth/realms/test/account"; + } + + private Result doJsGet(JavascriptExecutor executor, String url, String token, boolean expectAllowed) { + String js = "var r = new XMLHttpRequest();" + + "var r = new XMLHttpRequest();" + + "r.open('GET', '" + url + "', false);" + + "r.setRequestHeader('Accept','application/json');" + + "r.setRequestHeader('Authorization','bearer " + token + "');" + + "r.send();" + + "return r.status + ':::' + r.responseText"; + return doXhr(executor, js, expectAllowed); + } + + private Result doJsPost(JavascriptExecutor executor, String url, String token, String data, boolean expectAllowed) { + String js = "var r = new XMLHttpRequest();" + + "var r = new XMLHttpRequest();" + + "r.open('POST', '" + url + "', false);" + + "r.setRequestHeader('Accept','application/json');" + + "r.setRequestHeader('Content-Type','application/json');" + + "r.setRequestHeader('Authorization','bearer " + token + "');" + + "r.send('" + data + "');" + + "return r.status + ':::' + r.responseText"; + return doXhr(executor, js, expectAllowed); + } + + private Result doXhr(JavascriptExecutor executor, String js, boolean expectAllowed) { + Result result = null; + Throwable error = null; + try { + String response = (String) executor.executeScript(js); + String r[] = response.split(":::"); + result = new Result(Integer.parseInt(r[0]), r.length == 2 ? r[1] : null); + } catch (Throwable t ) { + error = t; + } + + if (result == null || result.getStatus() != 200 || error != null) { + if (expectAllowed) { + throw new AssertionError("Cors request failed: " + WebDriverLogDumper.dumpBrowserLogs(driver)); + } else { + return result; + } + } else { + if (!expectAllowed) { + throw new AssertionError("Expected CORS request to be rejected, but was successful"); + } else { + return result; + } + } + } + + private static class Result { + int status; + + String result; + + public Result(int status, String result) { + this.status = status; + this.result = result; + } + + public int getStatus() { + return status; + } + + public String getResult() { + return result; + } + } + +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountRestServiceTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountRestServiceTest.java new file mode 100755 index 0000000000..e4f4ec1a79 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountRestServiceTest.java @@ -0,0 +1,198 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.testsuite.account; + +import com.fasterxml.jackson.core.type.TypeReference; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.keycloak.broker.provider.util.SimpleHttp; +import org.keycloak.representations.account.SessionRepresentation; +import org.keycloak.representations.account.UserRepresentation; +import org.keycloak.representations.idm.ErrorRepresentation; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.testsuite.AbstractTestRealmKeycloakTest; +import org.keycloak.testsuite.AssertEvents; +import org.keycloak.testsuite.util.TokenUtil; +import org.keycloak.testsuite.util.UserBuilder; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * @author Stian Thorgersen + */ +public class AccountRestServiceTest extends AbstractTestRealmKeycloakTest { + + @Rule + public TokenUtil tokenUtil = new TokenUtil(); + + @Rule + public AssertEvents events = new AssertEvents(this); + + private CloseableHttpClient client; + + @Before + public void before() { + client = HttpClientBuilder.create().build(); + } + + @After + public void after() { + try { + client.close(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public void configureTestRealm(RealmRepresentation testRealm) { + testRealm.getUsers().add(UserBuilder.create().username("no-account-access").password("password").build()); + testRealm.getUsers().add(UserBuilder.create().username("view-account-access").role("account", "view-profile").password("password").build()); + } + + @Test + public void testGetProfile() throws IOException { + UserRepresentation user = SimpleHttp.doGet(getAccountUrl(null), client).auth(tokenUtil.getToken()).asJson(UserRepresentation.class); + assertEquals("Tom", user.getFirstName()); + assertEquals("Brady", user.getLastName()); + assertEquals("test-user@localhost", user.getEmail()); + assertFalse(user.isEmailVerified()); + assertTrue(user.getAttributes().isEmpty()); + } + + @Test + public void testUpdateProfile() throws IOException { + UserRepresentation user = SimpleHttp.doGet(getAccountUrl(null), client).auth(tokenUtil.getToken()).asJson(UserRepresentation.class); + user.setFirstName("Homer"); + user.setLastName("Simpsons"); + user.getAttributes().put("attr1", Collections.singletonList("val1")); + user.getAttributes().put("attr2", Collections.singletonList("val2")); + + user = updateAndGet(user); + + assertEquals("Homer", user.getFirstName()); + assertEquals("Simpsons", user.getLastName()); + assertEquals(2, user.getAttributes().size()); + assertEquals(1, user.getAttributes().get("attr1").size()); + assertEquals("val1", user.getAttributes().get("attr1").get(0)); + assertEquals(1, user.getAttributes().get("attr2").size()); + assertEquals("val2", user.getAttributes().get("attr2").get(0)); + + // Update attributes + user.getAttributes().remove("attr1"); + user.getAttributes().get("attr2").add("val3"); + + user = updateAndGet(user); + + assertEquals(1, user.getAttributes().size()); + assertEquals(2, user.getAttributes().get("attr2").size()); + assertEquals("val2", user.getAttributes().get("attr2").get(0)); + assertEquals("val3", user.getAttributes().get("attr2").get(1)); + + // Update email + user.setEmail("bobby@localhost"); + user = updateAndGet(user); + assertEquals("bobby@localhost", user.getEmail()); + + user.setEmail("john-doh@localhost"); + updateError(user, 409, "email_exists"); + + user.setEmail("test-user@localhost"); + user = updateAndGet(user); + assertEquals("test-user@localhost", user.getEmail()); + + // Update username + user.setUsername("updatedUsername"); + user = updateAndGet(user); + assertEquals("updatedusername", user.getUsername()); + + user.setUsername("john-doh@localhost"); + updateError(user, 409, "username_exists"); + + user.setUsername("test-user@localhost"); + user = updateAndGet(user); + assertEquals("test-user@localhost", user.getUsername()); + + RealmRepresentation realmRep = adminClient.realm("test").toRepresentation(); + realmRep.setEditUsernameAllowed(false); + adminClient.realm("test").update(realmRep); + + user.setUsername("updatedUsername2"); + updateError(user, 400, "username_read_only"); + } + + private UserRepresentation updateAndGet(UserRepresentation user) throws IOException { + int status = SimpleHttp.doPost(getAccountUrl(null), client).auth(tokenUtil.getToken()).json(user).asStatus(); + assertEquals(200, status); + return SimpleHttp.doGet(getAccountUrl(null), client).auth(tokenUtil.getToken()).asJson(UserRepresentation.class); + } + + + private void updateError(UserRepresentation user, int expectedStatus, String expectedMessage) throws IOException { + SimpleHttp.Response response = SimpleHttp.doPost(getAccountUrl(null), client).auth(tokenUtil.getToken()).json(user).asResponse(); + assertEquals(expectedStatus, response.getStatus()); + assertEquals(expectedMessage, response.asJson(ErrorRepresentation.class).getErrorMessage()); + } + + @Test + public void testProfilePermissions() throws IOException { + TokenUtil noaccessToken = new TokenUtil("no-account-access", "password"); + TokenUtil viewToken = new TokenUtil("view-account-access", "password"); + + // Read with no access + assertEquals(403, SimpleHttp.doGet(getAccountUrl(null), client).header("Accept", "application/json").auth(noaccessToken.getToken()).asStatus()); + + // Update with no access + assertEquals(403, SimpleHttp.doPost(getAccountUrl(null), client).auth(noaccessToken.getToken()).json(new UserRepresentation()).asStatus()); + + // Update with read only + assertEquals(403, SimpleHttp.doPost(getAccountUrl(null), client).auth(viewToken.getToken()).json(new UserRepresentation()).asStatus()); + } + + @Test + public void testUpdateProfilePermissions() throws IOException { + TokenUtil noaccessToken = new TokenUtil("no-account-access", "password"); + int status = SimpleHttp.doGet(getAccountUrl(null), client).header("Accept", "application/json").auth(noaccessToken.getToken()).asStatus(); + assertEquals(403, status); + + TokenUtil viewToken = new TokenUtil("view-account-access", "password"); + status = SimpleHttp.doGet(getAccountUrl(null), client).header("Accept", "application/json").auth(viewToken.getToken()).asStatus(); + assertEquals(200, status); + } + + @Test + public void testGetSessions() throws IOException { + List sessions = SimpleHttp.doGet(getAccountUrl("sessions"), client).auth(tokenUtil.getToken()).asJson(new TypeReference>() {}); + + assertEquals(1, sessions.size()); + } + + private String getAccountUrl(String resource) { + return suiteContext.getAuthServerInfo().getContextRoot().toString() + "/auth/realms/test/account" + (resource != null ? "/" + resource : ""); + } + +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/ProfileTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/ProfileTest.java deleted file mode 100755 index 95274c6b89..0000000000 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/ProfileTest.java +++ /dev/null @@ -1,362 +0,0 @@ -/* - * Copyright 2016 Red Hat, Inc. and/or its affiliates - * and other contributors as indicated by the @author tags. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.keycloak.testsuite.account; - -import org.apache.commons.io.IOUtils; -import org.apache.http.HttpHeaders; -import org.apache.http.HttpResponse; -import org.apache.http.client.HttpClient; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.entity.StringEntity; -import org.apache.http.impl.client.DefaultHttpClient; -import org.jboss.arquillian.drone.api.annotation.Default; -import org.jboss.arquillian.graphene.context.GrapheneContext; -import org.jboss.arquillian.graphene.page.Page; -import org.junit.Before; -import org.junit.Test; -import org.keycloak.OAuth2Constants; -import org.keycloak.admin.client.resource.ClientResource; -import org.keycloak.admin.client.resource.RealmResource; -import org.keycloak.admin.client.resource.RoleMappingResource; -import org.keycloak.admin.client.resource.RoleScopeResource; -import org.keycloak.models.AccountRoles; -import org.keycloak.representations.idm.RealmRepresentation; -import org.keycloak.representations.idm.RoleRepresentation; -import org.keycloak.representations.idm.UserRepresentation; -import org.keycloak.services.resources.RealmsResource; -import org.keycloak.testsuite.AbstractTestRealmKeycloakTest; -import org.keycloak.testsuite.admin.ApiUtil; -import org.keycloak.testsuite.client.resources.TestApplicationResource; -import org.keycloak.testsuite.pages.AccountApplicationsPage; -import org.keycloak.testsuite.pages.AccountUpdateProfilePage; -import org.keycloak.testsuite.pages.LoginPage; -import org.keycloak.testsuite.pages.OAuthGrantPage; -import org.keycloak.testsuite.runonserver.SerializationUtil; -import org.keycloak.testsuite.util.ClientBuilder; -import org.keycloak.testsuite.util.OAuthClient; -import org.keycloak.testsuite.util.RealmBuilder; -import org.keycloak.testsuite.util.RealmRepUtil; -import org.keycloak.testsuite.util.UserBuilder; -import org.keycloak.testsuite.util.WaitUtils; -import org.keycloak.util.JsonSerialization; -import org.openqa.selenium.By; -import org.openqa.selenium.Capabilities; -import org.openqa.selenium.JavascriptExecutor; -import org.openqa.selenium.Platform; -import org.openqa.selenium.WebDriver; -import org.openqa.selenium.WebDriverException; -import org.openqa.selenium.htmlunit.HtmlUnitDriver; -import org.openqa.selenium.remote.DesiredCapabilities; -import twitter4j.JSONArray; -import twitter4j.JSONObject; - -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.UriBuilder; -import java.io.IOException; -import java.net.URI; -import java.util.Collections; -import java.util.List; -import java.util.Map; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; - -/** - * @author Stian Thorgersen - * @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc. - */ -public class ProfileTest extends AbstractTestRealmKeycloakTest { - - @Override - public void configureTestRealm(RealmRepresentation testRealm) { - UserRepresentation user = RealmRepUtil.findUser(testRealm, "test-user@localhost"); - user.setFirstName("First"); - user.setLastName("Last"); - user.singleAttribute("key1", "value1"); - user.singleAttribute("key2", "value2"); - - UserRepresentation user2 = UserBuilder.create() - .enabled(true) - .username("test-user-no-access@localhost") - .password("password") - .build(); - RealmBuilder.edit(testRealm) - .accessTokenLifespan(1000) - .user(user2); - - ClientBuilder.edit(RealmRepUtil.findClientByClientId(testRealm, "test-app")) - .addWebOrigin("http://localtest.me:8180"); - } - - private RoleRepresentation findViewProfileRole(ClientResource accountApp) { - RoleMappingResource scopeMappings = accountApp.getScopeMappings(); - RoleScopeResource clientLevelMappings = scopeMappings.clientLevel(accountApp.toRepresentation().getId()); - List accountRoleList = clientLevelMappings.listEffective(); - - for (RoleRepresentation role : accountRoleList) { - if (role.getName().equals(AccountRoles.VIEW_PROFILE)) return role; - } - - return null; - } - - @Before - public void addScopeMappings() { - String accountClientId = org.keycloak.models.Constants.ACCOUNT_MANAGEMENT_CLIENT_ID; - ClientResource accountApp = ApiUtil.findClientByClientId(testRealm(), accountClientId); - RoleRepresentation role = findViewProfileRole(accountApp); - - String accountAppId = accountApp.toRepresentation().getId(); - ClientResource app = ApiUtil.findClientByClientId(testRealm(), "test-app"); - app.getScopeMappings().clientLevel(accountAppId).add(Collections.singletonList(role)); - - ClientResource thirdParty = ApiUtil.findClientByClientId(testRealm(), "third-party"); - thirdParty.getScopeMappings().clientLevel(accountAppId).add(Collections.singletonList(role)); - } - - @Page - protected AccountUpdateProfilePage profilePage; - - @Page - protected AccountApplicationsPage accountApplicationsPage; - - @Page - protected LoginPage loginPage; - - @Page - protected OAuthGrantPage grantPage; - - @Test - public void getProfile() throws Exception { - oauth.doLogin("test-user@localhost", "password"); - - String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); - String token = oauth.doAccessTokenRequest(code, "password").getAccessToken(); - - HttpResponse response = doGetProfile(token, null); - assertEquals(200, response.getStatusLine().getStatusCode()); - UserRepresentation profile = JsonSerialization.readValue(IOUtils.toString(response.getEntity().getContent()), UserRepresentation.class); - - assertEquals("test-user@localhost", profile.getUsername()); - assertEquals("test-user@localhost", profile.getEmail()); - assertEquals("First", profile.getFirstName()); - assertEquals("Last", profile.getLastName()); - - Map> attributes = profile.getAttributes(); - List attrValue = attributes.get("key1"); - assertEquals(1, attrValue.size()); - assertEquals("value1", attrValue.get(0)); - attrValue = attributes.get("key2"); - assertEquals(1, attrValue.size()); - assertEquals("value2", attrValue.get(0)); - } - - @Test - public void updateProfile() throws Exception { - oauth.doLogin("test-user@localhost", "password"); - - String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); - String token = oauth.doAccessTokenRequest(code, "password").getAccessToken(); - - UserRepresentation user = new UserRepresentation(); - user.setUsername("test-user@localhost"); - user.setFirstName("NewFirst"); - user.setLastName("NewLast"); - user.setEmail("NewEmail@localhost"); - - HttpResponse response = doUpdateProfile(token, null, JsonSerialization.writeValueAsString(user)); - assertEquals(200, response.getStatusLine().getStatusCode()); - - response = doGetProfile(token, null); - - UserRepresentation profile = JsonSerialization.readValue(IOUtils.toString(response.getEntity().getContent()), UserRepresentation.class); - - assertEquals("test-user@localhost", profile.getUsername()); - assertEquals("newemail@localhost", profile.getEmail()); - assertEquals("NewFirst", profile.getFirstName()); - assertEquals("NewLast", profile.getLastName()); - - // Revert - user.setFirstName("First"); - user.setLastName("Last"); - user.setEmail("test-user@localhost"); - doUpdateProfile(token, null, JsonSerialization.writeValueAsString(user)); - assertEquals(200, response.getStatusLine().getStatusCode()); - } - - @Test - public void getProfileCors() throws Exception { - oauth.doLogin("test-user@localhost", "password"); - - String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); - String token = oauth.doAccessTokenRequest(code, "password").getAccessToken(); - - driver.navigate().to("http://localtest.me:8180/auth/realms/test/account"); - - String[] response = doGetProfileJs("http://localtest.me:8180/auth", token); - assertEquals("200", response[0]); - } - - - // WARN: If it's failing for phantomJS, make sure to enable CORS by using: - // -Dphantomjs.cli.args="--ignore-ssl-errors=true --web-security=true" - @Test - public void getProfileCorsInvalidOrigin() throws Exception { - oauth.doLogin("test-user@localhost", "password"); - - String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); - String token = oauth.doAccessTokenRequest(code, "password").getAccessToken(); - - String[] response = null; - try { - response = doGetProfileJs("http://invalid.localtest.me:8180/auth", token); - } catch (WebDriverException ex) { - // Expected - } - - // Some webDrivers throw exception (htmlUnit) , some just doesn't return anything. - if (response != null && response.length > 0 && response[0].equals("200")) { - fail("Not expected to retrieve response. Make sure CORS are enabled for your browser!"); - } - } - - @Test - public void getProfileCookieAuth() throws Exception { - profilePage.open(); - loginPage.login("test-user@localhost", "password"); - - String[] response = doGetProfileJs(OAuthClient.AUTH_SERVER_ROOT, null); - assertEquals("200", response[0]); - - JSONObject profile = new JSONObject(response[1]); - assertEquals("test-user@localhost", profile.getString("username")); - } - - @Test - public void getProfileNoAuth() throws Exception { - HttpResponse response = doGetProfile(null, null); - assertEquals(403, response.getStatusLine().getStatusCode()); - } - - @Test - public void getProfileNoAccess() throws Exception { - oauth.doLogin("test-user-no-access@localhost", "password"); - - String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); - String token = oauth.doAccessTokenRequest(code, "password").getAccessToken(); - - HttpResponse response = doGetProfile(token, null); - assertEquals(403, response.getStatusLine().getStatusCode()); - } - - @Test - public void getProfileOAuthClient() throws Exception { - oauth.clientId("third-party"); - oauth.doLoginGrant("test-user@localhost", "password"); - - grantPage.accept(); - - String token = oauth.doAccessTokenRequest(oauth.getCurrentQuery().get(OAuth2Constants.CODE), "password").getAccessToken(); - HttpResponse response = doGetProfile(token, null); - - assertEquals(200, response.getStatusLine().getStatusCode()); - JSONObject profile = new JSONObject(IOUtils.toString(response.getEntity().getContent())); - - assertEquals("test-user@localhost", profile.getString("username")); - - accountApplicationsPage.open(); - accountApplicationsPage.revokeGrant("third-party"); - } - - @Test - public void getProfileOAuthClientNoScope() throws Exception { - oauth.clientId("third-party"); - oauth.doLoginGrant("test-user@localhost", "password"); - - String token = oauth.doAccessTokenRequest(oauth.getCurrentQuery().get(OAuth2Constants.CODE), "password").getAccessToken(); - HttpResponse response = doGetProfile(token, null); - - assertEquals(403, response.getStatusLine().getStatusCode()); - } - - private URI getAccountURI() { - return RealmsResource.accountUrl(UriBuilder.fromUri(oauth.AUTH_SERVER_ROOT)).build(oauth.getRealm()); - } - - private HttpResponse doGetProfile(String token, String origin) throws IOException { - HttpClient client = new DefaultHttpClient(); - HttpGet get = new HttpGet(UriBuilder.fromUri(getAccountURI()).build()); - if (token != null) { - get.setHeader(HttpHeaders.AUTHORIZATION, "bearer " + token); - } - if (origin != null) { - get.setHeader("Origin", origin); - } - get.setHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON); - return client.execute(get); - } - - private HttpResponse doUpdateProfile(String token, String origin, String value) throws IOException { - HttpClient client = new DefaultHttpClient(); - HttpPost post = new HttpPost(UriBuilder.fromUri(getAccountURI()).build()); - if (token != null) { - post.setHeader(HttpHeaders.AUTHORIZATION, "bearer " + token); - } - if (origin != null) { - post.setHeader("Origin", origin); - } - post.setHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON); - post.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON); - post.setEntity(new StringEntity(value)); - return client.execute(post); - } - - private String[] doGetProfileJs(String authServerRoot, String token) { - UriBuilder uriBuilder = UriBuilder.fromUri(authServerRoot) - .path(TestApplicationResource.class) - .path(TestApplicationResource.class, "getAccountProfile") - .queryParam("account-uri", getAccountURI().toString()); - - if (token != null) { - uriBuilder.queryParam("token", token); - - // Remove Keycloak cookies. Some browsers send cookies even in preflight requests - driver.navigate().to(OAuthClient.AUTH_SERVER_ROOT + "/realms/test/account"); - driver.manage().deleteAllCookies(); - } - - String accountProfileUri = uriBuilder.build().toString(); - log.info("Retrieve profile with URI: " + accountProfileUri); - - driver.navigate().to(accountProfileUri); - WaitUtils.waitUntilElement(By.id("innerOutput")); - String response = driver.findElement(By.id("innerOutput")).getText(); - return response.split("///"); - } - - private WebDriver getHtmlUnitDriver() { - DesiredCapabilities cap = new DesiredCapabilities(); - cap.setPlatform(Platform.ANY); - cap.setJavascriptEnabled(true); - cap.setVersion("chrome"); - cap.setBrowserName("htmlunit"); - HtmlUnitDriver driver = new HtmlUnitDriver(cap); - return driver; - } -} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/custom/CustomThemeTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/custom/CustomThemeTest.java index 81942fee97..d9e30b3383 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/custom/CustomThemeTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/custom/CustomThemeTest.java @@ -27,7 +27,7 @@ import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.AbstractTestRealmKeycloakTest; -import org.keycloak.testsuite.account.AccountTest; +import org.keycloak.testsuite.account.AccountFormServiceTest; import org.keycloak.testsuite.pages.AccountUpdateProfilePage; import org.keycloak.testsuite.pages.LoginPage; import org.keycloak.testsuite.util.RealmBuilder; @@ -68,7 +68,7 @@ public class CustomThemeTest extends AbstractTestRealmKeycloakTest { profilePage.open(); loginPage.login("test-user@localhost", "password"); - events.expectLogin().client("account").detail(Details.REDIRECT_URI, AccountTest.ACCOUNT_REDIRECT).assertEvent(); + events.expectLogin().client("account").detail(Details.REDIRECT_URI, AccountFormServiceTest.ACCOUNT_REDIRECT).assertEvent(); Assert.assertEquals("test-user@localhost", profilePage.getEmail()); Assert.assertEquals("", profilePage.getAttribute("street")); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTotpTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTotpTest.java index 1e349d9e55..f2448e1df5 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTotpTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTotpTest.java @@ -28,7 +28,7 @@ import org.keycloak.models.utils.TimeBasedOTP; import org.keycloak.representations.idm.AdminEventRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.UserRepresentation; -import org.keycloak.services.resources.AccountService; +import org.keycloak.services.resources.account.AccountFormService; import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.AbstractTestRealmKeycloakTest; import org.keycloak.testsuite.pages.AccountTotpPage; @@ -45,7 +45,7 @@ import java.util.List; public class UserTotpTest extends AbstractTestRealmKeycloakTest { private static final UriBuilder BASE = UriBuilder.fromUri("http://localhost:8180/auth"); - public static String ACCOUNT_REDIRECT = AccountService.loginRedirectUrl(BASE.clone()).build("test").toString(); + public static String ACCOUNT_REDIRECT = AccountFormService.loginRedirectUrl(BASE.clone()).build("test").toString(); @Rule public AssertEvents events = new AssertEvents(this); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthGrantTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthGrantTest.java index 926ff69c6f..d0e05174de 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthGrantTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthGrantTest.java @@ -38,7 +38,6 @@ import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RoleRepresentation; import org.keycloak.testsuite.AbstractKeycloakTest; import org.keycloak.testsuite.AssertEvents; -import org.keycloak.testsuite.account.AccountTest; import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.pages.AccountApplicationsPage; import org.keycloak.testsuite.pages.AppPage; diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java index c0ca7f119b..6254b51da9 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java @@ -43,7 +43,7 @@ import org.keycloak.representations.idm.RoleRepresentation; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.testsuite.AbstractKeycloakTest; import org.keycloak.testsuite.AssertEvents; -import org.keycloak.testsuite.account.AccountTest; +import org.keycloak.testsuite.account.AccountFormServiceTest; import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.arquillian.AuthServerTestEnricher; import org.keycloak.testsuite.auth.page.AuthRealm; @@ -530,7 +530,7 @@ public class OfflineTokenTest extends AbstractKeycloakTest { // Go to account mgmt applications page applicationsPage.open(); loginPage.login("test-user@localhost", "password"); - events.expectLogin().client("account").detail(Details.REDIRECT_URI, AccountTest.ACCOUNT_REDIRECT + "?path=applications").assertEvent(); + events.expectLogin().client("account").detail(Details.REDIRECT_URI, AccountFormServiceTest.ACCOUNT_REDIRECT + "?path=applications").assertEvent(); Assert.assertTrue(applicationsPage.isCurrent()); Map apps = applicationsPage.getApplications(); Assert.assertTrue(apps.containsKey("offline-client-2")); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/TrustStoreEmailTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/ssl/TrustStoreEmailTest.java similarity index 99% rename from testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/TrustStoreEmailTest.java rename to testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/ssl/TrustStoreEmailTest.java index 7fb9692961..f4d3af7745 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/TrustStoreEmailTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/ssl/TrustStoreEmailTest.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.keycloak.testsuite.account; +package org.keycloak.testsuite.ssl; import org.jboss.arquillian.graphene.page.Page; import org.junit.After; diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/testrealm.json b/testsuite/integration-arquillian/tests/base/src/test/resources/testrealm.json index 2ce6b3916e..99cd578d8d 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/testrealm.json +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/testrealm.json @@ -358,6 +358,13 @@ ], "adminUrl": "http://localhost:8180/varnamedapp/base/admin", "secret": "password" + }, + { + "clientId": "direct-grant", + "enabled": true, + "directAccessGrantsEnabled": true, + "secret": "password", + "webOrigins": [ "http://localtest.me:8180" ] } ], "roles" : { 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 1e1da65f8f..c334074149 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountPasswordPage.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountPasswordPage.java @@ -16,7 +16,7 @@ */ package org.keycloak.testsuite.pages; -import org.keycloak.services.resources.AccountService; +import org.keycloak.services.resources.account.AccountFormService; import org.keycloak.testsuite.Constants; import org.openqa.selenium.WebElement; import org.openqa.selenium.support.FindBy; @@ -70,6 +70,6 @@ public class AccountPasswordPage extends AbstractAccountPage { } public String getPath() { - return AccountService.passwordUrl(UriBuilder.fromUri(Constants.AUTH_SERVER_ROOT)).build(this.realmName).toString(); + return AccountFormService.passwordUrl(UriBuilder.fromUri(Constants.AUTH_SERVER_ROOT)).build(this.realmName).toString(); } } 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 a715c564af..8d1dfacb41 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountTotpPage.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountTotpPage.java @@ -16,7 +16,7 @@ */ package org.keycloak.testsuite.pages; -import org.keycloak.services.resources.AccountService; +import org.keycloak.services.resources.account.AccountFormService; import org.keycloak.testsuite.Constants; import org.openqa.selenium.WebElement; import org.openqa.selenium.support.FindBy; @@ -28,7 +28,7 @@ import javax.ws.rs.core.UriBuilder; */ public class AccountTotpPage extends AbstractAccountPage { - private static String PATH = AccountService.totpUrl(UriBuilder.fromUri(Constants.AUTH_SERVER_ROOT)).build("test").toString(); + private static String PATH = AccountFormService.totpUrl(UriBuilder.fromUri(Constants.AUTH_SERVER_ROOT)).build("test").toString(); @FindBy(id = "totpSecret") private WebElement totpSecret; diff --git a/themes/src/main/resources/theme/base/account/messages/messages_en.properties b/themes/src/main/resources/theme/base/account/messages/messages_en.properties index 906c525ef2..c5ce32a39b 100755 --- a/themes/src/main/resources/theme/base/account/messages/messages_en.properties +++ b/themes/src/main/resources/theme/base/account/messages/messages_en.properties @@ -119,6 +119,7 @@ usernameExistsMessage=Username already exists. emailExistsMessage=Email already exists. readOnlyUserMessage=You can''t update your account as it is read only. +readOnlyUsernameMessage=You can''t update your username as it is read only. readOnlyPasswordMessage=You can''t update your password as your account is read only. successTotpMessage=Mobile authenticator configured.