KEYCLOAK-15332 Missing CORS headers in some endpoints in Account REST API

This commit is contained in:
vmuzikar 2020-09-09 12:09:08 +02:00 committed by Bruno Oliveira da Silva
parent 540516c6a9
commit bb7ce62cd5
8 changed files with 99 additions and 84 deletions

View file

@ -10,8 +10,12 @@ import org.keycloak.credential.CredentialProvider;
import org.keycloak.credential.CredentialTypeMetadata; import org.keycloak.credential.CredentialTypeMetadata;
import org.keycloak.credential.CredentialTypeMetadataContext; import org.keycloak.credential.CredentialTypeMetadataContext;
import org.keycloak.credential.UserCredentialStoreManager; import org.keycloak.credential.UserCredentialStoreManager;
import org.keycloak.events.EventBuilder; import org.keycloak.models.AccountRoles;
import org.keycloak.models.*; import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.AuthenticationFlowModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.services.ErrorResponse; import org.keycloak.services.ErrorResponse;

View file

@ -18,6 +18,7 @@ package org.keycloak.services.resources.account;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.jboss.resteasy.spi.HttpRequest; import org.jboss.resteasy.spi.HttpRequest;
import org.jboss.resteasy.spi.HttpResponse;
import org.jboss.resteasy.spi.ResteasyProviderFactory; import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.keycloak.common.enums.AccountRestApiVersion; import org.keycloak.common.enums.AccountRestApiVersion;
import org.keycloak.events.EventBuilder; import org.keycloak.events.EventBuilder;
@ -28,6 +29,7 @@ import org.keycloak.models.RealmModel;
import org.keycloak.services.managers.AppAuthManager; import org.keycloak.services.managers.AppAuthManager;
import org.keycloak.services.managers.Auth; import org.keycloak.services.managers.Auth;
import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.resources.Cors;
import org.keycloak.theme.Theme; import org.keycloak.theme.Theme;
import javax.ws.rs.HttpMethod; import javax.ws.rs.HttpMethod;
@ -37,6 +39,7 @@ import javax.ws.rs.NotFoundException;
import javax.ws.rs.Path; import javax.ws.rs.Path;
import javax.ws.rs.PathParam; import javax.ws.rs.PathParam;
import javax.ws.rs.Produces; import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.UriInfo; import javax.ws.rs.core.UriInfo;
@ -51,6 +54,11 @@ public class AccountLoader {
private KeycloakSession session; private KeycloakSession session;
private EventBuilder event; private EventBuilder event;
@Context
private HttpRequest request;
@Context
private HttpResponse response;
private static final Logger logger = Logger.getLogger(AccountLoader.class); private static final Logger logger = Logger.getLogger(AccountLoader.class);
public AccountLoader(KeycloakSession session, EventBuilder event) { public AccountLoader(KeycloakSession session, EventBuilder event) {
@ -94,6 +102,9 @@ public class AccountLoader {
@Path("{version : v\\d[0-9a-zA-Z_\\-]*}") @Path("{version : v\\d[0-9a-zA-Z_\\-]*}")
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
public Object getVersionedAccountRestService(final @PathParam("version") String version) { public Object getVersionedAccountRestService(final @PathParam("version") String version) {
if (request.getHttpMethod().equals(HttpMethod.OPTIONS)) {
return new CorsPreflightService(request);
}
return getAccountRestService(getAccountManagementClient(session.getContext().getRealm()), version); return getAccountRestService(getAccountManagementClient(session.getContext().getRealm()), version);
} }
@ -121,6 +132,9 @@ public class AccountLoader {
if (authResult == null) { if (authResult == null) {
throw new NotAuthorizedException("Bearer token required"); throw new NotAuthorizedException("Bearer token required");
} }
Auth auth = new Auth(session.getContext().getRealm(), authResult.getToken(), authResult.getUser(), client, authResult.getSession(), false);
Cors.add(request).allowedOrigins(auth.getToken()).allowedMethods("GET", "PUT", "POST", "DELETE").auth().build(response);
if (authResult.getUser().getServiceAccountClientLink() != null) { if (authResult.getUser().getServiceAccountClientLink() != null) {
throw new NotAuthorizedException("Service accounts are not allowed to access this service"); throw new NotAuthorizedException("Service accounts are not allowed to access this service");
@ -137,7 +151,6 @@ public class AccountLoader {
} }
} }
Auth auth = new Auth(session.getContext().getRealm(), authResult.getToken(), authResult.getUser(), client, authResult.getSession(), false);
AccountRestService accountRestService = new AccountRestService(session, auth, client, event, version); AccountRestService accountRestService = new AccountRestService(session, auth, client, event, version);
ResteasyProviderFactory.getInstance().injectProperties(accountRestService); ResteasyProviderFactory.getInstance().injectProperties(accountRestService);
accountRestService.init(); accountRestService.init();

View file

@ -19,8 +19,8 @@ package org.keycloak.services.resources.account;
import org.jboss.resteasy.annotations.cache.NoCache; import org.jboss.resteasy.annotations.cache.NoCache;
import org.jboss.resteasy.spi.HttpRequest; import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.common.ClientConnection; import org.keycloak.common.ClientConnection;
import org.keycloak.common.enums.AccountRestApiVersion;
import org.keycloak.common.Profile; import org.keycloak.common.Profile;
import org.keycloak.common.enums.AccountRestApiVersion;
import org.keycloak.common.util.StringPropertyReplacer; import org.keycloak.common.util.StringPropertyReplacer;
import org.keycloak.events.EventBuilder; import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventStoreProvider; import org.keycloak.events.EventStoreProvider;
@ -42,7 +42,6 @@ import org.keycloak.services.ErrorResponse;
import org.keycloak.services.managers.Auth; import org.keycloak.services.managers.Auth;
import org.keycloak.services.managers.UserSessionManager; import org.keycloak.services.managers.UserSessionManager;
import org.keycloak.services.messages.Messages; import org.keycloak.services.messages.Messages;
import org.keycloak.services.resources.Cors;
import org.keycloak.services.resources.account.resources.ResourcesService; import org.keycloak.services.resources.account.resources.ResourcesService;
import org.keycloak.services.util.ResolveRelative; import org.keycloak.services.util.ResolveRelative;
import org.keycloak.storage.ReadOnlyException; import org.keycloak.storage.ReadOnlyException;
@ -50,16 +49,15 @@ import org.keycloak.theme.Theme;
import org.keycloak.userprofile.LegacyUserProfileProviderFactory; import org.keycloak.userprofile.LegacyUserProfileProviderFactory;
import org.keycloak.userprofile.UserProfile; import org.keycloak.userprofile.UserProfile;
import org.keycloak.userprofile.UserProfileProvider; import org.keycloak.userprofile.UserProfileProvider;
import org.keycloak.userprofile.utils.UserUpdateHelper;
import org.keycloak.userprofile.profile.representations.AccountUserRepresentationUserProfile;
import org.keycloak.userprofile.profile.DefaultUserProfileContext; import org.keycloak.userprofile.profile.DefaultUserProfileContext;
import org.keycloak.userprofile.profile.representations.AccountUserRepresentationUserProfile;
import org.keycloak.userprofile.utils.UserUpdateHelper;
import org.keycloak.userprofile.validation.UserProfileValidationResult; import org.keycloak.userprofile.validation.UserProfileValidationResult;
import javax.ws.rs.Consumes; import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE; import javax.ws.rs.DELETE;
import javax.ws.rs.GET; import javax.ws.rs.GET;
import javax.ws.rs.NotFoundException; import javax.ws.rs.NotFoundException;
import javax.ws.rs.OPTIONS;
import javax.ws.rs.POST; import javax.ws.rs.POST;
import javax.ws.rs.PUT; import javax.ws.rs.PUT;
import javax.ws.rs.Path; import javax.ws.rs.Path;
@ -71,7 +69,6 @@ import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import java.io.IOException; import java.io.IOException;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedList; import java.util.LinkedList;
@ -120,18 +117,6 @@ public class AccountRestService {
eventStore = session.getProvider(EventStoreProvider.class); 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. * Get account information.
* *
@ -141,7 +126,7 @@ public class AccountRestService {
@GET @GET
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
@NoCache @NoCache
public Response account() { public UserRepresentation account() {
auth.requireOneOf(AccountRoles.MANAGE_ACCOUNT, AccountRoles.VIEW_PROFILE); auth.requireOneOf(AccountRoles.MANAGE_ACCOUNT, AccountRoles.VIEW_PROFILE);
UserModel user = auth.getUser(); UserModel user = auth.getUser();
@ -161,7 +146,7 @@ public class AccountRestService {
copiedAttributes.remove(UserModel.USERNAME); copiedAttributes.remove(UserModel.USERNAME);
rep.setAttributes(copiedAttributes); rep.setAttributes(copiedAttributes);
return Cors.add(request, Response.ok(rep)).auth().allowedOrigins(auth.getToken()).build(); return rep;
} }
@Path("/") @Path("/")
@ -189,7 +174,7 @@ public class AccountRestService {
UserUpdateHelper.updateAccount(realm, user, updatedUser); UserUpdateHelper.updateAccount(realm, user, updatedUser);
event.success(); event.success();
return Cors.add(request, Response.noContent()).auth().allowedOrigins(auth.getToken()).build(); return Response.noContent().build();
} catch (ReadOnlyException e) { } catch (ReadOnlyException e) {
return ErrorResponse.error(Messages.READ_ONLY_USER, Response.Status.BAD_REQUEST); return ErrorResponse.error(Messages.READ_ONLY_USER, Response.Status.BAD_REQUEST);
} }
@ -270,15 +255,15 @@ public class AccountRestService {
ClientModel client = realm.getClientByClientId(clientId); ClientModel client = realm.getClientByClientId(clientId);
if (client == null) { if (client == null) {
return Cors.add(request, Response.status(Response.Status.NOT_FOUND).entity("No client with clientId: " + clientId + " found.")).build(); return ErrorResponse.error("No client with clientId: " + clientId + " found.", Response.Status.NOT_FOUND);
} }
UserConsentModel consent = session.users().getConsentByClient(realm, user.getId(), client.getId()); UserConsentModel consent = session.users().getConsentByClient(realm, user.getId(), client.getId());
if (consent == null) { if (consent == null) {
return Cors.add(request, Response.noContent()).build(); return Response.noContent().build();
} }
return Cors.add(request, Response.ok(modelToRepresentation(consent))).build(); return Response.ok(modelToRepresentation(consent)).build();
} }
/** /**
@ -299,14 +284,14 @@ public class AccountRestService {
event.event(EventType.REVOKE_GRANT_ERROR); event.event(EventType.REVOKE_GRANT_ERROR);
String msg = String.format("No client with clientId: %s found.", clientId); String msg = String.format("No client with clientId: %s found.", clientId);
event.error(msg); event.error(msg);
return Cors.add(request, Response.status(Response.Status.NOT_FOUND).entity(msg)).build(); return ErrorResponse.error(msg, Response.Status.NOT_FOUND);
} }
session.users().revokeConsentForClient(realm, user.getId(), client.getId()); session.users().revokeConsentForClient(realm, user.getId(), client.getId());
new UserSessionManager(session).revokeOfflineToken(user, client); new UserSessionManager(session).revokeOfflineToken(user, client);
event.success(); event.success();
return Cors.add(request, Response.noContent()).build(); return Response.noContent().build();
} }
/** /**
@ -359,7 +344,7 @@ public class AccountRestService {
event.event(EventType.GRANT_CONSENT_ERROR); event.event(EventType.GRANT_CONSENT_ERROR);
String msg = String.format("No client with clientId: %s found.", clientId); String msg = String.format("No client with clientId: %s found.", clientId);
event.error(msg); event.error(msg);
return Cors.add(request, Response.status(Response.Status.NOT_FOUND).entity(msg)).build(); return ErrorResponse.error(msg, Response.Status.NOT_FOUND);
} }
try { try {
@ -371,9 +356,9 @@ public class AccountRestService {
} }
event.success(); event.success();
grantedConsent = session.users().getConsentByClient(realm, user.getId(), client.getId()); grantedConsent = session.users().getConsentByClient(realm, user.getId(), client.getId());
return Cors.add(request, Response.ok(modelToRepresentation(grantedConsent))).build(); return Response.ok(modelToRepresentation(grantedConsent)).build();
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
return Cors.add(request, Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage())).build(); return ErrorResponse.error(e.getMessage(), Response.Status.BAD_REQUEST);
} }
} }
@ -416,7 +401,7 @@ public class AccountRestService {
@GET @GET
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
@NoCache @NoCache
public Response applications(@QueryParam("name") String name) { public List<ClientRepresentation> applications(@QueryParam("name") String name) {
checkAccountApiEnabled(); checkAccountApiEnabled();
auth.requireOneOf(AccountRoles.MANAGE_ACCOUNT, AccountRoles.VIEW_APPLICATIONS); auth.requireOneOf(AccountRoles.MANAGE_ACCOUNT, AccountRoles.VIEW_APPLICATIONS);
@ -461,7 +446,7 @@ public class AccountRestService {
} }
} }
return Cors.add(request, Response.ok(apps)).auth().allowedOrigins(auth.getToken()).build(); return apps;
} }
private boolean matches(ClientModel client, String name) { private boolean matches(ClientModel client, String name) {

View file

@ -24,6 +24,7 @@ import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam; import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
@ -44,7 +45,6 @@ import org.keycloak.representations.account.DeviceRepresentation;
import org.keycloak.representations.account.SessionRepresentation; import org.keycloak.representations.account.SessionRepresentation;
import org.keycloak.services.managers.Auth; import org.keycloak.services.managers.Auth;
import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.resources.Cors;
/** /**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a> * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
@ -73,9 +73,8 @@ public class SessionResource {
@GET @GET
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
@NoCache @NoCache
public Response toRepresentation() { public List<SessionRepresentation> toRepresentation() {
return Cors.add(request, Response.ok(session.sessions().getUserSessions(realm, user).stream() return session.sessions().getUserSessions(realm, user).stream().map(this::toRepresentation).collect(Collectors.toList());
.map(this::toRepresentation).collect(Collectors.toList()))).auth().allowedOrigins(auth.getToken()).build();
} }
/** /**
@ -87,7 +86,7 @@ public class SessionResource {
@GET @GET
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
@NoCache @NoCache
public Response devices() { public Collection<DeviceRepresentation> devices() {
Map<String, DeviceRepresentation> reps = new HashMap<>(); Map<String, DeviceRepresentation> reps = new HashMap<>();
List<UserSessionModel> sessions = session.sessions().getUserSessions(realm, user); List<UserSessionModel> sessions = session.sessions().getUserSessions(realm, user);
@ -117,7 +116,7 @@ public class SessionResource {
rep.addSession(createSessionRepresentation(s, device)); rep.addSession(createSessionRepresentation(s, device));
} }
return Cors.add(request, Response.ok(reps.values())).auth().allowedOrigins(auth.getToken()).build(); return reps.values();
} }
/** /**
@ -139,7 +138,7 @@ public class SessionResource {
} }
} }
return Cors.add(request, Response.noContent()).auth().allowedOrigins(auth.getToken()).build(); return Response.noContent().build();
} }
/** /**
@ -158,7 +157,7 @@ public class SessionResource {
if (userSession != null && userSession.getUser().equals(user)) { if (userSession != null && userSession.getUser().equals(user)) {
AuthenticationManager.backchannelLogout(session, userSession, true); AuthenticationManager.backchannelLogout(session, userSession, true);
} }
return Cors.add(request, Response.noContent()).auth().allowedOrigins(auth.getToken()).build(); return Response.noContent().build();
} }
private SessionRepresentation createSessionRepresentation(UserSessionModel s, DeviceRepresentation device) { private SessionRepresentation createSessionRepresentation(UserSessionModel s, DeviceRepresentation device) {

View file

@ -16,7 +16,6 @@
*/ */
package org.keycloak.services.resources.account.resources; package org.keycloak.services.resources.account.resources;
import javax.ws.rs.core.Response;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
@ -29,7 +28,6 @@ import java.util.stream.Collectors;
import org.jboss.resteasy.spi.HttpRequest; import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.authorization.AuthorizationProvider; import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.model.PermissionTicket; import org.keycloak.authorization.model.PermissionTicket;
import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.authorization.store.PermissionTicketStore; import org.keycloak.authorization.store.PermissionTicketStore;
import org.keycloak.authorization.store.ResourceStore; import org.keycloak.authorization.store.ResourceStore;
import org.keycloak.authorization.store.ScopeStore; import org.keycloak.authorization.store.ScopeStore;
@ -42,7 +40,6 @@ import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.representations.idm.authorization.ResourceRepresentation; import org.keycloak.representations.idm.authorization.ResourceRepresentation;
import org.keycloak.representations.idm.authorization.ScopeRepresentation; import org.keycloak.representations.idm.authorization.ScopeRepresentation;
import org.keycloak.services.managers.Auth; import org.keycloak.services.managers.Auth;
import org.keycloak.services.resources.Cors;
/** /**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a> * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
@ -69,10 +66,6 @@ public abstract class AbstractResourceService {
uriInfo = session.getContext().getUri(); uriInfo = session.getContext().getUri();
} }
protected Response cors(Response.ResponseBuilder response) {
return Cors.add(request, response).auth().allowedOrigins(auth.getToken()).build();
}
public static class Resource extends ResourceRepresentation { public static class Resource extends ResourceRepresentation {
private Client client; private Client client;

View file

@ -66,8 +66,8 @@ public class ResourceService extends AbstractResourceService {
*/ */
@GET @GET
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
public Response getResource() { public Resource getResource() {
return cors(Response.ok(new Resource(resource, provider))); return new Resource(resource, provider);
} }
/** /**
@ -78,7 +78,7 @@ public class ResourceService extends AbstractResourceService {
@GET @GET
@Path("permissions") @Path("permissions")
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
public Response toPermissions() { public Collection<Permission> toPermissions() {
Map<String, String> filters = new HashMap<>(); Map<String, String> filters = new HashMap<>();
filters.put(PermissionTicket.OWNER, user.getId()); filters.put(PermissionTicket.OWNER, user.getId());
@ -92,7 +92,7 @@ public class ResourceService extends AbstractResourceService {
permissions = resources.iterator().next().getPermissions(); permissions = resources.iterator().next().getPermissions();
} }
return cors(Response.ok(permissions)); return permissions;
} }
@GET @GET
@ -101,9 +101,9 @@ public class ResourceService extends AbstractResourceService {
public Response user(@QueryParam("value") String value) { public Response user(@QueryParam("value") String value) {
try { try {
final UserModel user = getUser(value); final UserModel user = getUser(value);
return cors(Response.ok(toRepresentation(provider.getKeycloakSession(), provider.getRealm(), user))); return Response.ok(toRepresentation(provider.getKeycloakSession(), provider.getRealm(), user)).build();
} catch (NotFoundException e) { } catch (NotFoundException e) {
return cors(Response.noContent()); return Response.noContent().build();
} }
} }
@ -172,7 +172,7 @@ public class ResourceService extends AbstractResourceService {
} }
} }
return cors(Response.noContent()); return Response.noContent().build();
} }
/** /**
@ -183,7 +183,7 @@ public class ResourceService extends AbstractResourceService {
@GET @GET
@Path("permissions/requests") @Path("permissions/requests")
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
public Response getPermissionRequests() { public Collection<Permission> getPermissionRequests() {
Map<String, String> filters = new HashMap<>(); Map<String, String> filters = new HashMap<>();
filters.put(PermissionTicket.OWNER, user.getId()); filters.put(PermissionTicket.OWNER, user.getId());
@ -196,7 +196,7 @@ public class ResourceService extends AbstractResourceService {
requests.computeIfAbsent(ticket.getRequester(), requester -> new Permission(ticket, provider)).addScope(ticket.getScope().getName()); requests.computeIfAbsent(ticket.getRequester(), requester -> new Permission(ticket, provider)).addScope(ticket.getScope().getName());
} }
return cors(Response.ok(requests.values())); return requests.values();
} }
private void grantPermission(UserModel user, String scopeId) { private void grantPermission(UserModel user, String scopeId) {

View file

@ -203,10 +203,10 @@ public class ResourcesService extends AbstractResourceService {
result = result.subList(0, size - 1); result = result.subList(0, size - 1);
} }
return cors(Response.ok().entity(result).links(createPageLinks(first, max, size))); return Response.ok().entity(result).links(createPageLinks(first, max, size)).build();
} }
return cors(Response.ok().entity(query.apply(-1, -1).collect(Collectors.toList()))); return Response.ok().entity(query.apply(-1, -1).collect(Collectors.toList())).build();
} }
private Link[] createPageLinks(Integer first, Integer max, int resultSize) { private Link[] createPageLinks(Integer first, Integer max, int resultSize) {

View file

@ -22,6 +22,7 @@ import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.keycloak.common.enums.AccountRestApiVersion;
import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest; import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.AssertEvents;
@ -34,6 +35,10 @@ import java.io.IOException;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude; import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer; import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer;
import static org.hamcrest.CoreMatchers.containsString;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/ */
@ -67,6 +72,7 @@ public class AccountRestServiceCorsTest extends AbstractTestRealmKeycloakTest {
@Override @Override
public void configureTestRealm(RealmRepresentation testRealm) { public void configureTestRealm(RealmRepresentation testRealm) {
testRealm.setEditUsernameAllowed(false);
} }
@Rule @Rule
@ -76,69 +82,84 @@ public class AccountRestServiceCorsTest extends AbstractTestRealmKeycloakTest {
public void testGetProfile() throws IOException, InterruptedException { public void testGetProfile() throws IOException, InterruptedException {
driver.navigate().to(VALID_CORS_URL); driver.navigate().to(VALID_CORS_URL);
doJsGet(executor, getAccountUrl(), tokenUtil.getToken(), true); doXhr(executor, getAccountUrl(), tokenUtil.getToken(), null, true);
} }
@Test @Test
public void testGetProfileInvalidOrigin() throws IOException, InterruptedException { public void testGetProfileInvalidOrigin() throws IOException, InterruptedException {
driver.navigate().to(INVALID_CORS_URL); driver.navigate().to(INVALID_CORS_URL);
doJsGet(executor, getAccountUrl(), tokenUtil.getToken(), false); doXhr(executor, getAccountUrl(), tokenUtil.getToken(), null, false);
} }
@Test @Test
public void testUpdateProfile() throws IOException { public void testUpdateProfile() throws IOException {
driver.navigate().to(VALID_CORS_URL); driver.navigate().to(VALID_CORS_URL);
doJsPost(executor, getAccountUrl(), tokenUtil.getToken(), "{ \"firstName\" : \"Bob\" }", true); doXhr(executor, getAccountUrl(), tokenUtil.getToken(), "{ \"firstName\" : \"Bob\" }", true);
} }
@Test @Test
public void testUpdateProfileInvalidOrigin() throws IOException { public void testUpdateProfileInvalidOrigin() throws IOException {
driver.navigate().to(INVALID_CORS_URL); driver.navigate().to(INVALID_CORS_URL);
doJsPost(executor, getAccountUrl(), tokenUtil.getToken(), "{ \"firstName\" : \"Bob\" }", false); doXhr(executor, getAccountUrl(), tokenUtil.getToken(), "{ \"firstName\" : \"Bob\" }", false);
}
@Test
public void testErrorResponse() {
driver.navigate().to(VALID_CORS_URL);
Result result = doXhr(executor, getAccountUrl(), tokenUtil.getToken(), "{ \"username\" : \"vmuzikar\" }", true);
assertEquals(400, result.getStatus());
assertThat(result.getResult(), containsString("readOnlyUsernameMessage"));
}
@Test
public void testErrorResponseInvalidOrigin() {
driver.navigate().to(INVALID_CORS_URL);
doXhr(executor, getAccountUrl(), tokenUtil.getToken(), "{ \"username\" : \"vmuzikar\" }", false);
}
@Test
public void testGetVersionedApi() {
driver.navigate().to(VALID_CORS_URL);
doXhr(executor, getAccountUrl() + "/" + AccountRestApiVersion.DEFAULT.getStrVersion(), tokenUtil.getToken(), null, true);
}
@Test
public void testGetVersionedApiInvalidOrigin() {
driver.navigate().to(INVALID_CORS_URL);
doXhr(executor, getAccountUrl() + "/" + AccountRestApiVersion.DEFAULT.getStrVersion(), tokenUtil.getToken(), null, false);
} }
private String getAccountUrl() { private String getAccountUrl() {
return suiteContext.getAuthServerInfo().getContextRoot().toString() + "/auth/realms/test/account"; return suiteContext.getAuthServerInfo().getContextRoot().toString() + "/auth/realms/test/account";
} }
private Result doJsGet(JavascriptExecutor executor, String url, String token, boolean expectAllowed) { private Result doXhr(JavascriptExecutor executor, String url, String token, String postData, boolean expectAllowed) {
String js = "var r = new XMLHttpRequest();" + String js = "var r = new XMLHttpRequest();" +
"var r = new XMLHttpRequest();" + "r.open('" + (postData == null ? "GET" : "POST") + "', '" + url + "', false);" +
"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('Accept','application/json');" +
"r.setRequestHeader('Content-Type','application/json');" + "r.setRequestHeader('Content-Type','application/json');" +
"r.setRequestHeader('Authorization','bearer " + token + "');" + "r.setRequestHeader('Authorization','bearer " + token + "');" +
"r.send('" + data + "');" + "r.send(" + (postData == null ? "" : "'" + postData + "'") + ");" +
"return r.status + ':::' + r.responseText"; "return r.status + ':::' + r.responseText";
return doXhr(executor, js, expectAllowed);
}
private Result doXhr(JavascriptExecutor executor, String js, boolean expectAllowed) {
Result result = null; Result result = null;
Throwable error = null; Throwable error = null;
try { try {
String response = (String) executor.executeScript(js); String response = (String) executor.executeScript(js);
String r[] = response.split(":::"); String[] r = response.split(":::");
result = new Result(Integer.parseInt(r[0]), r.length == 2 ? r[1] : null); result = new Result(Integer.parseInt(r[0]), r.length == 2 ? r[1] : null);
} catch (Throwable t ) { } catch (Throwable t ) {
error = t; error = t;
} }
if (result == null || (result.getStatus() != 200 && result.getStatus() != 204) || error != null) { if (error != null) {
if (expectAllowed) { if (expectAllowed) {
throw new AssertionError("Cors request failed: " + WebDriverLogDumper.dumpBrowserLogs(driver)); throw new AssertionError("Cors request failed: " + WebDriverLogDumper.dumpBrowserLogs(driver));
} else { } else {