KEYCLOAK-943 Started account rest service. Profile and sessions completed. (#4439)

This commit is contained in:
Stian Thorgersen 2017-08-29 20:12:09 +02:00 committed by GitHub
parent 463661b051
commit dcfa4aca8c
39 changed files with 1331 additions and 650 deletions

View file

@ -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;
}
}

View file

@ -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<ClientRepresentation> 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<ClientRepresentation> getClients() {
return clients;
}
public void setClients(List<ClientRepresentation> clients) {
this.clients = clients;
}
}

View file

@ -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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
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<String, List<String>> 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<String, List<String>> getAttributes() {
return attributes;
}
public void setAttributes(Map<String, List<String>> attributes) {
this.attributes = attributes;
}
}

View file

@ -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<FederatedIdentityEntry>(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<FederatedIdentityModel> identities, String providerId) {

View file

@ -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);
}

View file

@ -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;

View file

@ -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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -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));

View file

@ -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
*

View file

@ -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";

View file

@ -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 {
/**

View file

@ -108,28 +108,32 @@ public class Cors {
public Cors allowedOrigins(String... allowedOrigins) {
if (allowedOrigins != null && allowedOrigins.length > 0) {
this.allowedOrigins = new HashSet<String>(Arrays.asList(allowedOrigins));
this.allowedOrigins = new HashSet<>(Arrays.asList(allowedOrigins));
}
return this;
}
public Cors allowedMethods(String... allowedMethods) {
this.allowedMethods = new HashSet<String>(Arrays.asList(allowedMethods));
this.allowedMethods = new HashSet<>(Arrays.asList(allowedMethods));
return this;
}
public Cors exposedHeaders(String... exposedHeaders) {
this.exposedHeaders = new HashSet<String>(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");
}
}

View file

@ -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);
}

View file

@ -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());

View file

@ -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}")

View file

@ -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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
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<String> VALID_PATHS = new HashSet<String>();
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<String> LOG_DETAILS = new HashSet<String>();
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,23 +117,15 @@ 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);
if (authResult != null) {
auth = new Auth(realm, authResult.getToken(), authResult.getUser(), client, authResult.getSession(), false);
} else {
authResult = authManager.authenticateIdentityCookie(session, realm);
AuthenticationManager.AuthResult authResult = authManager.authenticateIdentityCookie(session, realm);
if (authResult != null) {
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();
@ -158,7 +137,6 @@ public class AccountService extends AbstractSecuredLocalService {
throw new ForbiddenException();
}
}
}
if (authResult != null) {
UserSessionModel userSession = authResult.getSession();
@ -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<String> 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<String> 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);
}
}
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<Event> events = eventStore.createQuery().type(LOG_EVENTS).user(auth.getUser().getId()).maxResults(30).getResultList();
List<Event> events = eventStore.createQuery().type(Constants.EXPOSED_LOG_EVENTS).user(auth.getUser().getId()).maxResults(30).getResultList();
for (Event e : events) {
if (e.getDetails() != null) {
Iterator<Map.Entry<String, String>> 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<String, List<String>> 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);
}
}
}

View file

@ -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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
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<MediaType> 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;
}
}
}

View file

@ -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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
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<String, List<String>> 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<SessionRepresentation> reps = new LinkedList<>();
List<UserSessionModel> 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<UserSessionModel> 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
}

View file

@ -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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
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<String> 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);
}
}

View file

@ -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();
}
}

View file

@ -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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
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";
}

View file

@ -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<String, Object> result = new HashMap<>();
result.put("sameRealm", sameRealm);
result.put("redirect", redirect.toString());

View file

@ -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;

View file

@ -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();
}
}

View file

@ -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) {

View file

@ -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);
}
}
}

View file

@ -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";
}
}
}

View file

@ -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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
* @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<String, AccountApplicationsPage.AppEntry> 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());

View file

@ -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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
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;
}
}
}

View file

@ -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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
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<SessionRepresentation> sessions = SimpleHttp.doGet(getAccountUrl("sessions"), client).auth(tokenUtil.getToken()).asJson(new TypeReference<List<SessionRepresentation>>() {});
assertEquals(1, sessions.size());
}
private String getAccountUrl(String resource) {
return suiteContext.getAuthServerInfo().getContextRoot().toString() + "/auth/realms/test/account" + (resource != null ? "/" + resource : "");
}
}

View file

@ -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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
* @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<RoleRepresentation> 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<String, List<String>> attributes = profile.getAttributes();
List<String> 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;
}
}

View file

@ -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"));

View file

@ -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);

View file

@ -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;

View file

@ -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<String, AccountApplicationsPage.AppEntry> apps = applicationsPage.getApplications();
Assert.assertTrue(apps.containsKey("offline-client-2"));

View file

@ -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;

View file

@ -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" : {

View file

@ -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();
}
}

View file

@ -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;

View file

@ -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.