keycloak-memoizeit

Merge pull request #2951 from mposolda/KEYCLOAK-2474 KEYCLOAK-2474

6/21/2016 10:31:12 AM

Changes

Details

diff --git a/examples/providers/domain-extension/invoke-authenticated.sh b/examples/providers/domain-extension/invoke-authenticated.sh
new file mode 100755
index 0000000..19b56b1
--- /dev/null
+++ b/examples/providers/domain-extension/invoke-authenticated.sh
@@ -0,0 +1,19 @@
+#!/bin/bash
+
+export DIRECT_GRANT_RESPONSE=$(curl -i --request POST http://localhost:8080/auth/realms/master/protocol/openid-connect/token --header "Accept: application/json" --header "Content-Type: application/x-www-form-urlencoded" --data "grant_type=password&username=admin&password=admin&client_id=admin-cli")
+
+echo -e "\n\nSENT RESOURCE-OWNER-PASSWORD-CREDENTIALS-REQUEST. OUTPUT IS:\n\n";
+echo $DIRECT_GRANT_RESPONSE;
+
+export ACCESS_TOKEN=$(echo $DIRECT_GRANT_RESPONSE | grep "access_token" | sed 's/.*\"access_token\":\"\([^\"]*\)\".*/\1/g');
+echo -e "\n\nACCESS TOKEN IS \"$ACCESS_TOKEN\"";
+
+echo -e "\n\nSENDING UN-AUTHENTICATED REQUEST. THIS SHOULD FAIL WITH 401: ";
+curl -i --request POST http://localhost:8080/auth/realms/master/example/companies-auth --data "{ \"name\": \"auth foo company\" }" --header "Content-type: application/json"
+
+echo -e "\n\nSENDING AUTHENTICATED REQUEST. THIS SHOULD SUCCESSFULY CREATE COMPANY AND SUCCESS WITH 201: ";
+curl -i --request POST http://localhost:8080/auth/realms/master/example/companies-auth --data "{ \"name\": \"auth foo company\" }" --header "Content-type: application/json" --header "Authorization: Bearer $ACCESS_TOKEN";
+
+echo -e "\n\nSEARCH COMPANIES: ";
+curl -i --request GET http://localhost:8080/auth/realms/master/example/companies-auth --header "Accept: application/json" --header "Authorization: Bearer $ACCESS_TOKEN";
+
diff --git a/examples/providers/domain-extension/pom.xml b/examples/providers/domain-extension/pom.xml
new file mode 100755
index 0000000..8d8e496
--- /dev/null
+++ b/examples/providers/domain-extension/pom.xml
@@ -0,0 +1,64 @@
+<!--
+  ~ 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.
+  -->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+    <parent>
+        <artifactId>keycloak-examples-providers-parent</artifactId>
+        <groupId>org.keycloak</groupId>
+        <version>2.0.0.CR1-SNAPSHOT</version>
+    </parent>
+
+    <name>Domain Extension Example</name>
+    <description/>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>keycloak-examples-providers-domain-extension</artifactId>
+    <packaging>jar</packaging>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-core</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-services</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-server-spi</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-model-jpa</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.jboss.spec.javax.ws.rs</groupId>
+            <artifactId>jboss-jaxrs-api_2.0_spec</artifactId>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <finalName>domain-extension-example</finalName>
+    </build>
+</project>
+
diff --git a/examples/providers/domain-extension/README.md b/examples/providers/domain-extension/README.md
new file mode 100644
index 0000000..e1aa2cd
--- /dev/null
+++ b/examples/providers/domain-extension/README.md
@@ -0,0 +1,44 @@
+Example Domain Extension 
+========================
+
+To run, deploy as a module by running:
+
+    $KEYCLOAK_HOME/bin/jboss-cli.sh --command="module add --name=org.keycloak.examples.domain-extension-example --resources=target/domain-extension-example.jar --dependencies=org.keycloak.keycloak-core,org.keycloak.keycloak-services,org.keycloak.keycloak-model-jpa,org.keycloak.keycloak-server-spi,javax.ws.rs.api,javax.persistence.api,org.hibernate,org.javassist"
+
+
+Then registering the provider by editing keycloak-server.json and adding the module to the providers field:
+
+    "providers": [
+        ....
+        "module:org.keycloak.examples.domain-extension-example"
+    ],
+
+Then start (or restart) the server.
+
+Testing
+-------
+First you can create some example companies with these CURL requests.
+
+````
+curl -i --request POST http://localhost:8080/auth/realms/master/example/companies --data "{ \"name\": \"foo company\" }" --header "Content-type: application/json"
+curl -i --request POST http://localhost:8080/auth/realms/master/example/companies --data "{ \"name\": \"bar company\" }" --header "Content-type: application/json"
+````
+
+Then you can lookup all companies 
+
+````
+curl -i --request GET http://localhost:8080/auth/realms/master/example/companies --header "Accept: application/json"
+````
+
+If you create realm `foo` in Keycloak admin console and then replace the realm name in the URI (for example like `http://localhost:8080/auth/realms/foo/example/companies` ) you will see
+that companies are scoped per-realm. So you will see different companies for realm `master` and for realm `foo` .
+
+
+Testing with authenticated access
+---------------------------------
+Example contains the endpoint, which is accessible just for authenticated users. REST request must be authenticated with bearer access token
+of authenticated user and the user must be in realm role `admin` in order to access the resource. You can run bash script from the current directory:
+````
+./invoke-authenticated.sh
+````
+The script assumes user `admin` with password `admin` exists in realm `master`. Also it assumes that you have `curl` installed.
\ No newline at end of file
diff --git a/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/CompanyRepresentation.java b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/CompanyRepresentation.java
new file mode 100644
index 0000000..2b76d57
--- /dev/null
+++ b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/CompanyRepresentation.java
@@ -0,0 +1,33 @@
+package org.keycloak.examples.domainextension;
+
+import org.keycloak.examples.domainextension.jpa.Company;
+
+public class CompanyRepresentation {
+
+    private String id;
+    private String name;
+
+    public CompanyRepresentation() {
+    }
+
+    public CompanyRepresentation(Company company) {
+        id = company.getId();
+        name = company.getName();
+    }
+    
+    public String getId() {
+		return id;
+	}
+    
+    public String getName() {
+		return name;
+	}
+
+    public void setId(String id) {
+        this.id = id;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+}
diff --git a/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/jpa/Company.java b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/jpa/Company.java
new file mode 100644
index 0000000..bca8e1d
--- /dev/null
+++ b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/jpa/Company.java
@@ -0,0 +1,66 @@
+/*
+ * 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.examples.domainextension.jpa;
+
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.Id;
+import javax.persistence.NamedQueries;
+import javax.persistence.NamedQuery;
+import javax.persistence.Table;
+
+@Entity
+@Table(name = "EXAMPLE_COMPANY")
+@NamedQueries({ @NamedQuery(name = "findByRealm", query = "from Company where realmId = :realmId") })
+public class Company {
+
+    @Id
+    @Column(name = "ID")
+    private String id;
+
+    @Column(name = "NAME", nullable = false)
+    private String name;
+
+    @Column(name = "REALM_ID", nullable = false)
+    private String realmId;
+
+    public String getId() {
+		return id;
+	}
+    
+    public String getRealmId() {
+        return realmId;
+    }
+    
+    public String getName() {
+		return name;
+	}
+
+    public void setId(String id) {
+        this.id = id;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public void setRealmId(String realmId) {
+        this.realmId = realmId;
+    }
+}
diff --git a/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/jpa/ExampleJpaEntityProvider.java b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/jpa/ExampleJpaEntityProvider.java
new file mode 100644
index 0000000..b6529fd
--- /dev/null
+++ b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/jpa/ExampleJpaEntityProvider.java
@@ -0,0 +1,50 @@
+/*
+ * 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.examples.domainextension.jpa;
+
+import java.util.Collections;
+import java.util.List;
+
+import org.keycloak.connections.jpa.entityprovider.JpaEntityProvider;
+
+/**
+ * @author <a href="mailto:erik.mulder@docdatapayments.com">Erik Mulder</a>
+ * 
+ * Example JpaEntityProvider.
+ */
+public class ExampleJpaEntityProvider implements JpaEntityProvider {
+
+    @Override
+    public List<Class<?>> getEntities() {
+        return Collections.<Class<?>>singletonList(Company.class);
+    }
+
+    @Override
+    public String getChangelogLocation() {
+    	return "META-INF/example-changelog.xml";
+    }
+    
+    @Override
+    public void close() {
+    }
+
+    @Override
+    public String getFactoryId() {
+        return ExampleJpaEntityProviderFactory.ID;
+    }
+}
diff --git a/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/jpa/ExampleJpaEntityProviderFactory.java b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/jpa/ExampleJpaEntityProviderFactory.java
new file mode 100644
index 0000000..2c919f4
--- /dev/null
+++ b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/jpa/ExampleJpaEntityProviderFactory.java
@@ -0,0 +1,57 @@
+/*
+ * 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.examples.domainextension.jpa;
+
+import org.keycloak.Config.Scope;
+import org.keycloak.connections.jpa.entityprovider.JpaEntityProvider;
+import org.keycloak.connections.jpa.entityprovider.JpaEntityProviderFactory;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+
+/**
+ * @author <a href="mailto:erik.mulder@docdatapayments.com">Erik Mulder</a>
+ * 
+ * Example JpaEntityProviderFactory.
+ */
+public class ExampleJpaEntityProviderFactory implements JpaEntityProviderFactory {
+
+	protected static final String ID = "example-entity-provider";
+	
+    @Override
+    public JpaEntityProvider create(KeycloakSession session) {
+        return new ExampleJpaEntityProvider();
+    }
+
+    @Override
+    public String getId() {
+        return ID;
+    }
+
+    @Override
+    public void init(Scope config) {
+    }
+
+    @Override
+    public void postInit(KeycloakSessionFactory factory) {
+    }
+
+    @Override
+    public void close() {
+    }
+
+}
diff --git a/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/rest/CompanyResource.java b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/rest/CompanyResource.java
new file mode 100644
index 0000000..ba98978
--- /dev/null
+++ b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/rest/CompanyResource.java
@@ -0,0 +1,52 @@
+package org.keycloak.examples.domainextension.rest;
+
+import java.util.List;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+import org.jboss.resteasy.annotations.cache.NoCache;
+import org.keycloak.examples.domainextension.CompanyRepresentation;
+import org.keycloak.examples.domainextension.spi.ExampleService;
+import org.keycloak.models.KeycloakSession;
+
+public class CompanyResource {
+
+	private final KeycloakSession session;
+	
+	public CompanyResource(KeycloakSession session) {
+		this.session = session;
+	}
+
+    @GET
+    @Path("")
+    @NoCache
+    @Produces(MediaType.APPLICATION_JSON)
+    public List<CompanyRepresentation> getCompanies() {
+        return session.getProvider(ExampleService.class).listCompanies();
+    }
+
+    @POST
+    @Path("")
+    @NoCache
+    @Consumes(MediaType.APPLICATION_JSON)
+    public Response createCompany(CompanyRepresentation rep) {
+        session.getProvider(ExampleService.class).addCompany(rep);
+        return Response.created(session.getContext().getUri().getAbsolutePathBuilder().path(rep.getId()).build()).build();
+    }
+
+    @GET
+    @NoCache
+    @Path("{id}")
+    @Produces(MediaType.APPLICATION_JSON)
+    public CompanyRepresentation getCompany(@PathParam("id") final String id) {
+        return session.getProvider(ExampleService.class).findCompany(id);
+    }
+
+}
\ No newline at end of file
diff --git a/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/rest/ExampleRealmResourceProvider.java b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/rest/ExampleRealmResourceProvider.java
new file mode 100644
index 0000000..0882d22
--- /dev/null
+++ b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/rest/ExampleRealmResourceProvider.java
@@ -0,0 +1,40 @@
+/*
+ * 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.examples.domainextension.rest;
+
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.services.resource.RealmResourceProvider;
+
+public class ExampleRealmResourceProvider implements RealmResourceProvider {
+
+    private KeycloakSession session;
+
+    public ExampleRealmResourceProvider(KeycloakSession session) {
+        this.session = session;
+    }
+
+    @Override
+    public Object getResource() {
+        return new ExampleRestResource(session);
+    }
+
+    @Override
+    public void close() {
+    }
+
+}
diff --git a/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/rest/ExampleRealmResourceProviderFactory.java b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/rest/ExampleRealmResourceProviderFactory.java
new file mode 100644
index 0000000..33c2ca2
--- /dev/null
+++ b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/rest/ExampleRealmResourceProviderFactory.java
@@ -0,0 +1,52 @@
+/*
+ * 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.examples.domainextension.rest;
+
+import org.keycloak.Config.Scope;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.services.resource.RealmResourceProvider;
+import org.keycloak.services.resource.RealmResourceProviderFactory;
+
+public class ExampleRealmResourceProviderFactory implements RealmResourceProviderFactory {
+
+    public static final String ID = "example";
+
+    @Override
+    public String getId() {
+        return ID;
+    }
+
+    @Override
+    public RealmResourceProvider create(KeycloakSession session) {
+        return new ExampleRealmResourceProvider(session);
+    }
+
+    @Override
+    public void init(Scope config) {
+    }
+
+    @Override
+    public void postInit(KeycloakSessionFactory factory) {
+    }
+
+    @Override
+    public void close() {
+    }
+
+}
diff --git a/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/rest/ExampleRestResource.java b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/rest/ExampleRestResource.java
new file mode 100644
index 0000000..db774cf
--- /dev/null
+++ b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/rest/ExampleRestResource.java
@@ -0,0 +1,43 @@
+package org.keycloak.examples.domainextension.rest;
+
+import javax.ws.rs.ForbiddenException;
+import javax.ws.rs.NotAuthorizedException;
+import javax.ws.rs.Path;
+
+
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.services.managers.AppAuthManager;
+import org.keycloak.services.managers.AuthenticationManager;
+
+public class ExampleRestResource {
+
+	private final KeycloakSession session;
+    private final AuthenticationManager.AuthResult auth;
+	
+	public ExampleRestResource(KeycloakSession session) {
+		this.session = session;
+        this.auth = new AppAuthManager().authenticateBearerToken(session, session.getContext().getRealm());
+	}
+	
+    @Path("companies")
+    public CompanyResource getCompanyResource() {
+        return new CompanyResource(session);
+    }
+
+    // Same like "companies" endpoint, but REST endpoint is authenticated with Bearer token and user must be in realm role "admin"
+    // Just for illustration purposes
+    @Path("companies-auth")
+    public CompanyResource getCompanyResourceAuthenticated() {
+        checkRealmAdmin();
+        return new CompanyResource(session);
+    }
+
+    private void checkRealmAdmin() {
+        if (auth == null) {
+            throw new NotAuthorizedException("Bearer");
+        } else if (auth.getToken().getRealmAccess() == null || !auth.getToken().getRealmAccess().isUserInRole("admin")) {
+            throw new ForbiddenException("Does not have realm admin role");
+        }
+    }
+
+}
diff --git a/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/spi/ExampleService.java b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/spi/ExampleService.java
new file mode 100644
index 0000000..7f41327
--- /dev/null
+++ b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/spi/ExampleService.java
@@ -0,0 +1,33 @@
+/*
+ * 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.examples.domainextension.spi;
+
+import java.util.List;
+
+import org.keycloak.examples.domainextension.CompanyRepresentation;
+import org.keycloak.provider.Provider;
+
+public interface ExampleService extends Provider {
+
+    List<CompanyRepresentation> listCompanies();
+
+    CompanyRepresentation findCompany(String id);
+
+    CompanyRepresentation addCompany(CompanyRepresentation company);
+
+}
diff --git a/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/spi/ExampleServiceProviderFactory.java b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/spi/ExampleServiceProviderFactory.java
new file mode 100644
index 0000000..2c6a122
--- /dev/null
+++ b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/spi/ExampleServiceProviderFactory.java
@@ -0,0 +1,24 @@
+/*
+ * 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.examples.domainextension.spi;
+
+import org.keycloak.provider.ProviderFactory;
+
+public interface ExampleServiceProviderFactory extends ProviderFactory<ExampleService> {
+
+}
diff --git a/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/spi/ExampleSpi.java b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/spi/ExampleSpi.java
new file mode 100644
index 0000000..811ec92
--- /dev/null
+++ b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/spi/ExampleSpi.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.examples.domainextension.spi;
+
+import org.keycloak.provider.Provider;
+import org.keycloak.provider.ProviderFactory;
+import org.keycloak.provider.Spi;
+
+public class ExampleSpi implements Spi {
+
+    @Override
+    public boolean isInternal() {
+        return false;
+    }
+
+    @Override
+    public String getName() {
+        return "example";
+    }
+
+    @Override
+    public Class<? extends Provider> getProviderClass() {
+        return ExampleService.class;
+    }
+
+    @Override
+    @SuppressWarnings("rawtypes")
+    public Class<? extends ProviderFactory> getProviderFactoryClass() {
+        return ExampleServiceProviderFactory.class;
+    }
+
+}
diff --git a/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/spi/impl/ExampleServiceImpl.java b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/spi/impl/ExampleServiceImpl.java
new file mode 100644
index 0000000..49cc228
--- /dev/null
+++ b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/spi/impl/ExampleServiceImpl.java
@@ -0,0 +1,88 @@
+/*
+ * 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.examples.domainextension.spi.impl;
+
+import java.util.LinkedList;
+import java.util.List;
+
+import javax.persistence.EntityManager;
+
+import org.keycloak.connections.jpa.JpaConnectionProvider;
+import org.keycloak.examples.domainextension.jpa.Company;
+import org.keycloak.examples.domainextension.CompanyRepresentation;
+import org.keycloak.examples.domainextension.spi.ExampleService;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.utils.KeycloakModelUtils;
+
+public class ExampleServiceImpl implements ExampleService {
+
+    private final KeycloakSession session;
+
+    public ExampleServiceImpl(KeycloakSession session) {
+        this.session = session;
+        if (getRealm() == null) {
+            throw new IllegalStateException("The service cannot accept a session without a realm in it's context.");
+        }
+    }
+
+    private EntityManager getEntityManager() {
+        return session.getProvider(JpaConnectionProvider.class).getEntityManager();
+    }
+
+    protected RealmModel getRealm() {
+        return session.getContext().getRealm();
+    }
+    
+    @Override
+    public List<CompanyRepresentation> listCompanies() {
+    	List<Company> companyEntities = getEntityManager().createNamedQuery("findByRealm", Company.class)
+                .setParameter("realmId", getRealm().getId())
+                .getResultList();
+
+        List<CompanyRepresentation> result = new LinkedList<>();
+        for (Company entity : companyEntities) {
+            result.add(new CompanyRepresentation(entity));
+        }
+        return result;
+    }
+    
+    @Override
+    public CompanyRepresentation findCompany(String id) {
+    	Company entity = getEntityManager().find(Company.class, id);
+        return entity==null ? null : new CompanyRepresentation(entity);
+    }
+    
+    @Override
+    public CompanyRepresentation addCompany(CompanyRepresentation company) {
+        Company entity = new Company();
+        String id = company.getId()==null ?  KeycloakModelUtils.generateId() : company.getId();
+        entity.setId(id);
+        entity.setName(company.getName());
+        entity.setRealmId(getRealm().getId());
+        getEntityManager().persist(entity);
+
+        company.setId(id);
+        return company;
+    }
+
+    public void close() {
+        // Nothing to do.
+    }
+
+}
diff --git a/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/spi/impl/ExampleServiceProviderFactoryImpl.java b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/spi/impl/ExampleServiceProviderFactoryImpl.java
new file mode 100644
index 0000000..e4e2ddf
--- /dev/null
+++ b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/spi/impl/ExampleServiceProviderFactoryImpl.java
@@ -0,0 +1,53 @@
+/*
+ * 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.examples.domainextension.spi.impl;
+
+import org.keycloak.Config.Scope;
+import org.keycloak.examples.domainextension.spi.ExampleService;
+import org.keycloak.examples.domainextension.spi.ExampleServiceProviderFactory;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+
+public class ExampleServiceProviderFactoryImpl implements ExampleServiceProviderFactory {
+
+    @Override
+    public ExampleService create(KeycloakSession session) {
+        return new ExampleServiceImpl(session);
+    }
+
+    @Override
+    public void init(Scope config) {
+
+    }
+
+    @Override
+    public void postInit(KeycloakSessionFactory factory) {
+
+    }
+
+    @Override
+    public void close() {
+
+    }
+
+    @Override
+    public String getId() {
+        return "exampleServiceImpl";
+    }
+
+}
diff --git a/examples/providers/domain-extension/src/main/resources/META-INF/example-changelog.xml b/examples/providers/domain-extension/src/main/resources/META-INF/example-changelog.xml
new file mode 100644
index 0000000..5edd719
--- /dev/null
+++ b/examples/providers/domain-extension/src/main/resources/META-INF/example-changelog.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
+    <changeSet author="erik.mulder@docdatapayments.com" id="example-1.0">
+
+        <createTable tableName="EXAMPLE_COMPANY">
+            <column name="ID" type="VARCHAR(36)">
+                <constraints nullable="false"/>
+            </column>
+            <column name="NAME" type="VARCHAR(255)">
+                <constraints nullable="false"/>
+            </column>
+            <column name="REALM_ID" type="VARCHAR(36)">
+                <constraints nullable="false"/>
+            </column>
+        </createTable>
+
+        <addPrimaryKey
+            constraintName="PK_COMPANY"
+            tableName="EXAMPLE_COMPANY"
+            columnNames="ID"
+        />
+
+    </changeSet>
+    
+</databaseChangeLog>
diff --git a/examples/providers/domain-extension/src/main/resources/META-INF/services/org.keycloak.connections.jpa.entityprovider.JpaEntityProviderFactory b/examples/providers/domain-extension/src/main/resources/META-INF/services/org.keycloak.connections.jpa.entityprovider.JpaEntityProviderFactory
new file mode 100644
index 0000000..c7b88db
--- /dev/null
+++ b/examples/providers/domain-extension/src/main/resources/META-INF/services/org.keycloak.connections.jpa.entityprovider.JpaEntityProviderFactory
@@ -0,0 +1,18 @@
+#
+# 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.
+#
+
+org.keycloak.examples.domainextension.jpa.ExampleJpaEntityProviderFactory
diff --git a/examples/providers/domain-extension/src/main/resources/META-INF/services/org.keycloak.examples.domainextension.spi.ExampleServiceProviderFactory b/examples/providers/domain-extension/src/main/resources/META-INF/services/org.keycloak.examples.domainextension.spi.ExampleServiceProviderFactory
new file mode 100644
index 0000000..57f9f89
--- /dev/null
+++ b/examples/providers/domain-extension/src/main/resources/META-INF/services/org.keycloak.examples.domainextension.spi.ExampleServiceProviderFactory
@@ -0,0 +1,18 @@
+#
+# 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.
+#
+
+org.keycloak.examples.domainextension.spi.impl.ExampleServiceProviderFactoryImpl
diff --git a/examples/providers/domain-extension/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/examples/providers/domain-extension/src/main/resources/META-INF/services/org.keycloak.provider.Spi
new file mode 100644
index 0000000..e013bbd
--- /dev/null
+++ b/examples/providers/domain-extension/src/main/resources/META-INF/services/org.keycloak.provider.Spi
@@ -0,0 +1,18 @@
+#
+# 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.
+#
+
+org.keycloak.examples.domainextension.spi.ExampleSpi
diff --git a/examples/providers/domain-extension/src/main/resources/META-INF/services/org.keycloak.services.resource.RealmResourceProviderFactory b/examples/providers/domain-extension/src/main/resources/META-INF/services/org.keycloak.services.resource.RealmResourceProviderFactory
new file mode 100644
index 0000000..ea81617
--- /dev/null
+++ b/examples/providers/domain-extension/src/main/resources/META-INF/services/org.keycloak.services.resource.RealmResourceProviderFactory
@@ -0,0 +1,18 @@
+#
+# 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.
+#
+
+org.keycloak.examples.domainextension.rest.ExampleRealmResourceProviderFactory
\ No newline at end of file
diff --git a/examples/providers/pom.xml b/examples/providers/pom.xml
index 9557807..7153c45 100755
--- a/examples/providers/pom.xml
+++ b/examples/providers/pom.xml
@@ -36,5 +36,6 @@
         <module>federation-provider</module>
         <module>authenticator</module>
         <module>rest</module>
+        <module>domain-extension</module>
     </modules>
 </project>
diff --git a/model/jpa/pom.xml b/model/jpa/pom.xml
index 47035c5..e1c8742 100755
--- a/model/jpa/pom.xml
+++ b/model/jpa/pom.xml
@@ -102,6 +102,11 @@
             <artifactId>jackson-core</artifactId>
             <scope>provided</scope>
         </dependency>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
     <build>
         <plugins>
diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java
index 9c93214..b5621ef 100755
--- a/model/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java
+++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java
@@ -20,25 +20,17 @@ package org.keycloak.connections.jpa;
 import java.sql.Connection;
 import java.sql.DatabaseMetaData;
 import java.sql.DriverManager;
-import java.sql.ResultSet;
 import java.sql.SQLException;
 import java.util.HashMap;
 import java.util.LinkedHashMap;
-import java.util.List;
 import java.util.Map;
 
 import javax.naming.InitialContext;
 import javax.persistence.EntityManager;
 import javax.persistence.EntityManagerFactory;
-import javax.persistence.Persistence;
-import javax.persistence.spi.PersistenceUnitTransactionType;
 import javax.sql.DataSource;
 
-import org.hibernate.boot.registry.classloading.internal.ClassLoaderServiceImpl;
 import org.hibernate.ejb.AvailableSettings;
-import org.hibernate.jpa.boot.internal.ParsedPersistenceXmlDescriptor;
-import org.hibernate.jpa.boot.internal.PersistenceXmlParser;
-import org.hibernate.jpa.boot.spi.Bootstrap;
 import org.jboss.logging.Logger;
 import org.keycloak.Config;
 import org.keycloak.connections.jpa.updater.JpaUpdaterProvider;
@@ -182,7 +174,7 @@ public class DefaultJpaConnectionProviderFactory implements JpaConnectionProvide
                         }
 
 	                    logger.trace("Creating EntityManagerFactory");
-	                    emf = JpaUtils.createEntityManagerFactory(unitName, properties, getClass().getClassLoader());
+	                    emf = JpaUtils.createEntityManagerFactory(session, unitName, properties, getClass().getClassLoader());
 	                    logger.trace("EntityManagerFactory created");
 
                         if (globalStatsInterval != -1) {
diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/entityprovider/JpaEntityProvider.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/entityprovider/JpaEntityProvider.java
new file mode 100644
index 0000000..567080e
--- /dev/null
+++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/entityprovider/JpaEntityProvider.java
@@ -0,0 +1,53 @@
+/*
+ * 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.connections.jpa.entityprovider;
+
+import java.util.List;
+
+import org.keycloak.provider.Provider;
+
+/**
+ * @author <a href="mailto:erik.mulder@docdatapayments.com">Erik Mulder</a>
+ * 
+ * A JPA Entity Provider can supply extra JPA entities that the Keycloak system should include in it's entity manager. The
+ * entities should be provided as a list of Class objects.
+ */
+public interface JpaEntityProvider extends Provider {
+
+    /**
+     * Return the entities that should be added to the entity manager.
+     * 
+     * @return list of class objects
+     */
+	List<Class<?>> getEntities();
+	
+	/**
+	 * Return the location of the Liquibase changelog that facilitates the extra JPA entities.
+	 * This should be a location that can be found on the same classpath as the entity classes.
+	 * 
+	 * @return a changelog location or null if not needed
+	 */
+	String getChangelogLocation();
+
+	/**
+	 * Return the ID of provider factory, which created this provider. Might be used to "compute" the table name of liquibase changelog table.
+	 * @return ID of provider factory
+	 */
+	String getFactoryId();
+
+}
diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/entityprovider/JpaEntityProviderFactory.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/entityprovider/JpaEntityProviderFactory.java
new file mode 100644
index 0000000..8c08bf9
--- /dev/null
+++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/entityprovider/JpaEntityProviderFactory.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.connections.jpa.entityprovider;
+
+import org.keycloak.provider.ProviderFactory;
+
+/**
+ * @author <a href="mailto:erik.mulder@docdatapayments.com">Erik Mulder</a>
+ * 
+ * Extended interface for a provider factory for JpaEntityProvider's.
+ */
+public interface JpaEntityProviderFactory extends ProviderFactory<JpaEntityProvider> {
+
+}
diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/entityprovider/JpaEntitySpi.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/entityprovider/JpaEntitySpi.java
new file mode 100644
index 0000000..d89389f
--- /dev/null
+++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/entityprovider/JpaEntitySpi.java
@@ -0,0 +1,51 @@
+/*
+ * 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.connections.jpa.entityprovider;
+
+import org.keycloak.provider.Provider;
+import org.keycloak.provider.ProviderFactory;
+import org.keycloak.provider.Spi;
+
+/**
+ * @author <a href="mailto:erik.mulder@docdatapayments.com">Erik Mulder</a>
+ * 
+ * Spi that allows for adding extra JPA entity's to the Keycloak entity manager.
+ */
+public class JpaEntitySpi implements Spi {
+
+	@Override
+	public boolean isInternal() {
+		return false;
+	}
+
+	@Override
+	public String getName() {
+		return "jpa-entity-provider";
+	}
+
+	@Override
+	public Class<? extends Provider> getProviderClass() {
+		return JpaEntityProvider.class;
+	}
+
+	@Override
+	public Class<? extends ProviderFactory> getProviderFactoryClass() {
+		return JpaEntityProviderFactory.class;
+	}
+	
+}
diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/entityprovider/ProxyClassLoader.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/entityprovider/ProxyClassLoader.java
new file mode 100644
index 0000000..e9b82a3
--- /dev/null
+++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/entityprovider/ProxyClassLoader.java
@@ -0,0 +1,87 @@
+/*
+ * 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.connections.jpa.entityprovider;
+
+import java.net.URL;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * @author <a href="mailto:erik.mulder@docdatapayments.com">Erik Mulder</a>
+ * 
+ * Classloader implementation to facilitate loading classes and resources from a collection of other classloaders.
+ * Effectively it forms a proxy to one or more other classloaders.
+ * 
+ * The way it works:
+ * - Get all (unique) classloaders from all provided classes
+ * - For each class or resource that is 'requested':
+ *   - First try all provided classloaders and if we have a match, return that
+ *   - If no match was found: proceed with 'normal' classloading in 'current classpath' scope
+ * 
+ * In this particular context: only loadClass and getResource overrides are needed, since those
+ * are the methods that a classloading and resource loading process will need.
+ */
+public class ProxyClassLoader extends ClassLoader {
+
+    private Set<ClassLoader> classloaders;
+
+    public ProxyClassLoader(Collection<Class<?>> classes, ClassLoader parentClassLoader) {
+    	super(parentClassLoader);
+    	init(classes);
+    }
+    
+    public ProxyClassLoader(Collection<Class<?>> classes) {
+    	init(classes);
+    }
+
+    private void init(Collection<Class<?>> classes) {
+        classloaders = new HashSet<>();
+        for (Class<?> clazz : classes) {
+            classloaders.add(clazz.getClassLoader());
+        }
+    }
+    
+    @Override
+    public Class<?> loadClass(String name) throws ClassNotFoundException {
+        for (ClassLoader classloader : classloaders) {
+            try {
+                return classloader.loadClass(name);
+            } catch (ClassNotFoundException e) {
+                // This particular class loader did not find the class. It's expected behavior that
+                // this can happen, so we'll just ignore the exception and let the next one try.
+            }
+        }
+        // We did not find the class in the proxy class loaders, so proceed with 'normal' behavior.
+        return super.loadClass(name);
+    }
+
+    @Override
+    public URL getResource(String name) {
+        for (ClassLoader classloader : classloaders) {
+            URL resource = classloader.getResource(name);
+            if (resource != null) {
+                return resource;
+            }
+            // Resource == null means not found, so let the next one try.
+        }
+        // We could not get the resource from the proxy class loaders, so proceed with 'normal' behavior.
+        return super.getResource(name);
+    }
+
+}
diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/conn/DefaultLiquibaseConnectionProvider.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/conn/DefaultLiquibaseConnectionProvider.java
index b9018f8..5a58702 100644
--- a/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/conn/DefaultLiquibaseConnectionProvider.java
+++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/conn/DefaultLiquibaseConnectionProvider.java
@@ -19,6 +19,16 @@ package org.keycloak.connections.jpa.updater.liquibase.conn;
 
 import java.sql.Connection;
 
+import org.jboss.logging.Logger;
+import org.keycloak.Config;
+import org.keycloak.connections.jpa.updater.liquibase.LiquibaseJpaUpdaterProvider;
+import org.keycloak.connections.jpa.updater.liquibase.PostgresPlusDatabase;
+import org.keycloak.connections.jpa.updater.liquibase.lock.CustomInsertLockRecordGenerator;
+import org.keycloak.connections.jpa.updater.liquibase.lock.CustomLockDatabaseChangeLogGenerator;
+import org.keycloak.connections.jpa.updater.liquibase.lock.DummyLockService;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+
 import liquibase.Liquibase;
 import liquibase.changelog.ChangeSet;
 import liquibase.changelog.DatabaseChangeLog;
@@ -27,23 +37,12 @@ import liquibase.database.DatabaseFactory;
 import liquibase.database.core.DB2Database;
 import liquibase.database.jvm.JdbcConnection;
 import liquibase.exception.LiquibaseException;
-import liquibase.lockservice.LockService;
-import liquibase.lockservice.LockServiceFactory;
 import liquibase.logging.LogFactory;
 import liquibase.logging.LogLevel;
 import liquibase.resource.ClassLoaderResourceAccessor;
+import liquibase.resource.ResourceAccessor;
 import liquibase.servicelocator.ServiceLocator;
 import liquibase.sqlgenerator.SqlGeneratorFactory;
-import org.jboss.logging.Logger;
-import org.keycloak.Config;
-import org.keycloak.connections.jpa.updater.liquibase.LiquibaseJpaUpdaterProvider;
-import org.keycloak.connections.jpa.updater.liquibase.PostgresPlusDatabase;
-import org.keycloak.connections.jpa.updater.liquibase.lock.CustomInsertLockRecordGenerator;
-import org.keycloak.connections.jpa.updater.liquibase.lock.CustomLockDatabaseChangeLogGenerator;
-import org.keycloak.connections.jpa.updater.liquibase.lock.CustomLockService;
-import org.keycloak.connections.jpa.updater.liquibase.lock.DummyLockService;
-import org.keycloak.models.KeycloakSession;
-import org.keycloak.models.KeycloakSessionFactory;
 
 /**
  * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@@ -53,7 +52,7 @@ public class DefaultLiquibaseConnectionProvider implements LiquibaseConnectionPr
     private static final Logger logger = Logger.getLogger(DefaultLiquibaseConnectionProvider.class);
 
     private volatile boolean initialized = false;
-
+    
     @Override
     public LiquibaseConnectionProvider create(KeycloakSession session) {
         if (!initialized) {
@@ -132,9 +131,26 @@ public class DefaultLiquibaseConnectionProvider implements LiquibaseConnectionPr
         }
 
         String changelog = (database instanceof DB2Database) ? LiquibaseJpaUpdaterProvider.DB2_CHANGELOG :  LiquibaseJpaUpdaterProvider.CHANGELOG;
-        logger.debugf("Using changelog file: %s", changelog);
+        ResourceAccessor resourceAccessor = new ClassLoaderResourceAccessor(getClass().getClassLoader());
+
+        logger.debugf("Using changelog file %s and changelogTableName %s", changelog, database.getDatabaseChangeLogTableName());
+        
+        return new Liquibase(changelog, resourceAccessor, database);
+    }
+
+    @Override
+    public Liquibase getLiquibaseForCustomUpdate(Connection connection, String defaultSchema, String changelogLocation, ClassLoader classloader, String changelogTableName) throws LiquibaseException {
+        Database database = DatabaseFactory.getInstance().findCorrectDatabaseImplementation(new JdbcConnection(connection));
+        if (defaultSchema != null) {
+            database.setDefaultSchemaName(defaultSchema);
+        }
+
+        ResourceAccessor resourceAccessor = new ClassLoaderResourceAccessor(classloader);
+        database.setDatabaseChangeLogTableName(changelogTableName);
+
+        logger.debugf("Using changelog file %s and changelogTableName %s", changelogLocation, database.getDatabaseChangeLogTableName());
 
-        return new Liquibase(changelog, new ClassLoaderResourceAccessor(getClass().getClassLoader()), database);
+        return new Liquibase(changelogLocation, resourceAccessor, database);
     }
 
     private static class LogWrapper extends LogFactory {
diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/conn/LiquibaseConnectionProvider.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/conn/LiquibaseConnectionProvider.java
index 5aa81cc..215bd1d 100644
--- a/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/conn/LiquibaseConnectionProvider.java
+++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/conn/LiquibaseConnectionProvider.java
@@ -30,4 +30,6 @@ public interface LiquibaseConnectionProvider extends Provider {
 
     Liquibase getLiquibase(Connection connection, String defaultSchema) throws LiquibaseException;
 
+    Liquibase getLiquibaseForCustomUpdate(Connection connection, String defaultSchema, String changelogLocation, ClassLoader classloader, String changelogTableName) throws LiquibaseException;
+
 }
diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/LiquibaseJpaUpdaterProvider.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/LiquibaseJpaUpdaterProvider.java
index 8e9d253..2610174 100755
--- a/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/LiquibaseJpaUpdaterProvider.java
+++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/LiquibaseJpaUpdaterProvider.java
@@ -23,12 +23,17 @@ import liquibase.changelog.ChangeSet;
 import liquibase.changelog.RanChangeSet;
 import liquibase.exception.LiquibaseException;
 import org.jboss.logging.Logger;
+import org.keycloak.common.util.reflections.Reflections;
+import org.keycloak.connections.jpa.entityprovider.JpaEntityProvider;
 import org.keycloak.connections.jpa.updater.JpaUpdaterProvider;
 import org.keycloak.connections.jpa.updater.liquibase.conn.LiquibaseConnectionProvider;
+import org.keycloak.connections.jpa.util.JpaUtils;
 import org.keycloak.models.KeycloakSession;
 
+import java.lang.reflect.Method;
 import java.sql.Connection;
 import java.util.List;
+import java.util.Set;
 
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -54,25 +59,20 @@ public class LiquibaseJpaUpdaterProvider implements JpaUpdaterProvider {
         ThreadLocalSessionContext.setCurrentSession(session);
 
         try {
-            Liquibase liquibase = getLiquibase(connection, defaultSchema);
-
-            List<ChangeSet> changeSets = liquibase.listUnrunChangeSets((Contexts) null);
-            if (!changeSets.isEmpty()) {
-                if (changeSets.get(0).getId().equals(FIRST_VERSION)) {
-                    logger.info("Initializing database schema");
-                } else {
-                    if (logger.isDebugEnabled()) {
-                        List<RanChangeSet> ranChangeSets = liquibase.getDatabase().getRanChangeSetList();
-                        logger.debugv("Updating database from {0} to {1}", ranChangeSets.get(ranChangeSets.size() - 1).getId(), changeSets.get(changeSets.size() - 1).getId());
-                    } else {
-                        logger.infov("Updating database");
-                    }
+            // Run update with keycloak master changelog first
+            Liquibase liquibase = getLiquibaseForKeycloakUpdate(connection, defaultSchema);
+            updateChangeSet(liquibase, liquibase.getChangeLogFile());
+
+            // Run update for each custom JpaEntityProvider
+            Set<JpaEntityProvider> jpaProviders = session.getAllProviders(JpaEntityProvider.class);
+            for (JpaEntityProvider jpaProvider : jpaProviders) {
+                String customChangelog = jpaProvider.getChangelogLocation();
+                if (customChangelog != null) {
+                    String factoryId = jpaProvider.getFactoryId();
+                    String changelogTableName = JpaUtils.getCustomChangelogTableName(factoryId);
+                    liquibase = getLiquibaseForCustomProviderUpdate(connection, defaultSchema, customChangelog, jpaProvider.getClass().getClassLoader(), changelogTableName);
+                    updateChangeSet(liquibase, liquibase.getChangeLogFile());
                 }
-
-                liquibase.update((Contexts) null);
-                logger.debug("Completed database update");
-            } else {
-                logger.debug("Database is up to date");
             }
         } catch (Exception e) {
             throw new RuntimeException("Failed to update database", e);
@@ -81,21 +81,50 @@ public class LiquibaseJpaUpdaterProvider implements JpaUpdaterProvider {
         }
     }
 
+    protected void updateChangeSet(Liquibase liquibase, String changelog) throws LiquibaseException {
+        List<ChangeSet> changeSets = liquibase.listUnrunChangeSets((Contexts) null);
+        if (!changeSets.isEmpty()) {
+            List<RanChangeSet> ranChangeSets = liquibase.getDatabase().getRanChangeSetList();
+            if (ranChangeSets.isEmpty()) {
+                logger.infov("Initializing database schema. Using changelog {0}", changelog);
+            } else {
+                if (logger.isDebugEnabled()) {
+                    logger.debugv("Updating database from {0} to {1}. Using changelog {2}", ranChangeSets.get(ranChangeSets.size() - 1).getId(), changeSets.get(changeSets.size() - 1).getId(), changelog);
+                } else {
+                    logger.infov("Updating database. Using changelog {0}", changelog);
+                }
+            }
+
+            liquibase.update((Contexts) null);
+            logger.debugv("Completed database update for changelog {0}", changelog);
+        } else {
+            logger.debugv("Database is up to date for changelog {0}", changelog);
+
+            // Needs to restart liquibase services to clear changeLogHistory.
+            Method resetServices = Reflections.findDeclaredMethod(Liquibase.class, "resetServices");
+            Reflections.invokeMethod(true, resetServices, liquibase);
+        }
+    }
+
     @Override
     public void validate(Connection connection, String defaultSchema) {
         logger.debug("Validating if database is updated");
 
         try {
-            Liquibase liquibase = getLiquibase(connection, defaultSchema);
-
-            List<ChangeSet> changeSets = liquibase.listUnrunChangeSets((Contexts) null);
-            if (!changeSets.isEmpty()) {
-                List<RanChangeSet> ranChangeSets = liquibase.getDatabase().getRanChangeSetList();
-                String errorMessage = String.format("Failed to validate database schema. Schema needs updating database from %s to %s. Please change databaseSchema to 'update' or use other database",
-                        ranChangeSets.get(ranChangeSets.size() - 1).getId(), changeSets.get(changeSets.size() - 1).getId());
-                throw new RuntimeException(errorMessage);
-            } else {
-                logger.debug("Validation passed. Database is up-to-date");
+            // Validate with keycloak master changelog first
+            Liquibase liquibase = getLiquibaseForKeycloakUpdate(connection, defaultSchema);
+            validateChangeSet(liquibase, liquibase.getChangeLogFile());
+
+            // Validate each custom JpaEntityProvider
+            Set<JpaEntityProvider> jpaProviders = session.getAllProviders(JpaEntityProvider.class);
+            for (JpaEntityProvider jpaProvider : jpaProviders) {
+                String customChangelog = jpaProvider.getChangelogLocation();
+                if (customChangelog != null) {
+                    String factoryId = jpaProvider.getFactoryId();
+                    String changelogTableName = JpaUtils.getCustomChangelogTableName(factoryId);
+                    liquibase = getLiquibaseForCustomProviderUpdate(connection, defaultSchema, customChangelog, jpaProvider.getClass().getClassLoader(), changelogTableName);
+                    validateChangeSet(liquibase, liquibase.getChangeLogFile());
+                }
             }
 
         } catch (LiquibaseException e) {
@@ -103,11 +132,28 @@ public class LiquibaseJpaUpdaterProvider implements JpaUpdaterProvider {
         }
     }
 
-    private Liquibase getLiquibase(Connection connection, String defaultSchema) throws LiquibaseException {
+    protected void validateChangeSet(Liquibase liquibase, String changelog) throws LiquibaseException {
+        List<ChangeSet> changeSets = liquibase.listUnrunChangeSets((Contexts) null);
+        if (!changeSets.isEmpty()) {
+            List<RanChangeSet> ranChangeSets = liquibase.getDatabase().getRanChangeSetList();
+            String errorMessage = String.format("Failed to validate database schema. Schema needs updating database from %s to %s. Please change databaseSchema to 'update' or use other database. Used changelog was %s",
+                    ranChangeSets.get(ranChangeSets.size() - 1).getId(), changeSets.get(changeSets.size() - 1).getId(), changelog);
+            throw new RuntimeException(errorMessage);
+        } else {
+            logger.debugf("Validation passed. Database is up-to-date for changelog %s", changelog);
+        }
+    }
+
+    private Liquibase getLiquibaseForKeycloakUpdate(Connection connection, String defaultSchema) throws LiquibaseException {
         LiquibaseConnectionProvider liquibaseProvider = session.getProvider(LiquibaseConnectionProvider.class);
         return liquibaseProvider.getLiquibase(connection, defaultSchema);
     }
 
+    private Liquibase getLiquibaseForCustomProviderUpdate(Connection connection, String defaultSchema, String changelogLocation, ClassLoader classloader, String changelogTableName) throws LiquibaseException {
+        LiquibaseConnectionProvider liquibaseProvider = session.getProvider(LiquibaseConnectionProvider.class);
+        return liquibaseProvider.getLiquibaseForCustomUpdate(connection, defaultSchema, changelogLocation, classloader, changelogTableName);
+    }
+
     @Override
     public void close() {
     }
diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/util/JpaUtils.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/util/JpaUtils.java
index de07fd5..5ac7d2f 100644
--- a/model/jpa/src/main/java/org/keycloak/connections/jpa/util/JpaUtils.java
+++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/util/JpaUtils.java
@@ -21,12 +21,18 @@ import org.hibernate.boot.registry.classloading.internal.ClassLoaderServiceImpl;
 import org.hibernate.jpa.boot.internal.ParsedPersistenceXmlDescriptor;
 import org.hibernate.jpa.boot.internal.PersistenceXmlParser;
 import org.hibernate.jpa.boot.spi.Bootstrap;
+import org.keycloak.connections.jpa.entityprovider.JpaEntityProvider;
+import org.keycloak.connections.jpa.entityprovider.ProxyClassLoader;
+import org.keycloak.models.KeycloakSession;
 
 import javax.persistence.EntityManager;
 import javax.persistence.EntityManagerFactory;
 import javax.persistence.spi.PersistenceUnitTransactionType;
+
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 /**
  * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@@ -40,14 +46,52 @@ public class JpaUtils {
         return (schema==null) ? tableName : schema + "." + tableName;
     }
 
-    public static EntityManagerFactory createEntityManagerFactory(String unitName, Map<String, Object> properties, ClassLoader classLoader) {
+    public static EntityManagerFactory createEntityManagerFactory(KeycloakSession session, String unitName, Map<String, Object> properties, ClassLoader classLoader) {
         PersistenceXmlParser parser = new PersistenceXmlParser(new ClassLoaderServiceImpl(classLoader), PersistenceUnitTransactionType.RESOURCE_LOCAL);
         List<ParsedPersistenceXmlDescriptor> persistenceUnits = parser.doResolve(properties);
         for (ParsedPersistenceXmlDescriptor persistenceUnit : persistenceUnits) {
             if (persistenceUnit.getName().equals(unitName)) {
-                return Bootstrap.getEntityManagerFactoryBuilder(persistenceUnit, properties, classLoader).build();
+                List<Class<?>> providedEntities = getProvidedEntities(session);
+                for (Class<?> entityClass : providedEntities) {
+                    // Add all extra entity classes to the persistence unit.
+                    persistenceUnit.addClasses(entityClass.getName());
+                }
+                // Now build the entity manager factory, supplying a proxy classloader, so Hibernate will be able
+                // to find and load the extra provided entities. Set the provided classloader as parent classloader.
+                return Bootstrap.getEntityManagerFactoryBuilder(persistenceUnit, properties,
+                        new ProxyClassLoader(providedEntities, classLoader)).build();
             }
         }
         throw new RuntimeException("Persistence unit '" + unitName + "' not found");
     }
+
+    /**
+     * Get a list of all provided entities by looping over all configured entity providers.
+     * 
+     * @param session the keycloak session
+     * @return a list of all provided entities (can be an empty list)
+     */
+    public static List<Class<?>> getProvidedEntities(KeycloakSession session) {
+        List<Class<?>> providedEntityClasses = new ArrayList<>();
+        // Get all configured entity providers.
+        Set<JpaEntityProvider> entityProviders = session.getAllProviders(JpaEntityProvider.class);
+        // For every provider, add all entity classes to the list.
+        for (JpaEntityProvider entityProvider : entityProviders) {
+            providedEntityClasses.addAll(entityProvider.getEntities());
+        }
+        return providedEntityClasses;
+    }
+
+    /**
+     * Get the name of custom table for liquibase updates for give ID of JpaEntityProvider
+     * @param jpaEntityProviderFactoryId
+     * @return table name
+     */
+    public static String getCustomChangelogTableName(String jpaEntityProviderFactoryId) {
+        String upperCased = jpaEntityProviderFactoryId.toUpperCase();
+        upperCased = upperCased.replaceAll("-", "_");
+        upperCased = upperCased.replaceAll("[^A-Z_]", "");
+        return "DATABASECHANGELOG_" + upperCased.substring(0, Math.min(10, upperCased.length()));
+    }
+
 }
diff --git a/model/jpa/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/model/jpa/src/main/resources/META-INF/services/org.keycloak.provider.Spi
index 94c6512..5aba7ba 100644
--- a/model/jpa/src/main/resources/META-INF/services/org.keycloak.provider.Spi
+++ b/model/jpa/src/main/resources/META-INF/services/org.keycloak.provider.Spi
@@ -17,4 +17,5 @@
 
 org.keycloak.connections.jpa.JpaConnectionSpi
 org.keycloak.connections.jpa.updater.JpaUpdaterSpi
-org.keycloak.connections.jpa.updater.liquibase.conn.LiquibaseConnectionSpi
\ No newline at end of file
+org.keycloak.connections.jpa.updater.liquibase.conn.LiquibaseConnectionSpi
+org.keycloak.connections.jpa.entityprovider.JpaEntitySpi
diff --git a/model/jpa/src/test/java/org/keycloak/connections/jpa/util/JpaUtilsTest.java b/model/jpa/src/test/java/org/keycloak/connections/jpa/util/JpaUtilsTest.java
new file mode 100644
index 0000000..838f1c0
--- /dev/null
+++ b/model/jpa/src/test/java/org/keycloak/connections/jpa/util/JpaUtilsTest.java
@@ -0,0 +1,36 @@
+/*
+ * 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.connections.jpa.util;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class JpaUtilsTest {
+
+    @Test
+    public void testConvertTableName() {
+        Assert.assertEquals("DATABASECHANGELOG_FOO", JpaUtils.getCustomChangelogTableName("foo"));
+        Assert.assertEquals("DATABASECHANGELOG_FOOBAR", JpaUtils.getCustomChangelogTableName("foo123bar"));
+        Assert.assertEquals("DATABASECHANGELOG_FOO_BAR", JpaUtils.getCustomChangelogTableName("foo_bar568"));
+        Assert.assertEquals("DATABASECHANGELOG_FOO_BAR_C", JpaUtils.getCustomChangelogTableName("foo-bar-c568"));
+        Assert.assertEquals("DATABASECHANGELOG_EXAMPLE_EN", JpaUtils.getCustomChangelogTableName("example-entity-provider"));
+    }
+}
diff --git a/server-spi/src/main/java/org/keycloak/provider/ProviderLoader.java b/server-spi/src/main/java/org/keycloak/provider/ProviderLoader.java
index 76c2950..2d7a07a 100644
--- a/server-spi/src/main/java/org/keycloak/provider/ProviderLoader.java
+++ b/server-spi/src/main/java/org/keycloak/provider/ProviderLoader.java
@@ -24,6 +24,19 @@ import java.util.List;
  */
 public interface ProviderLoader {
 
+    /**
+     * Load the SPI definitions themselves.
+     *
+     * @return a list of Spi definition objects
+     */
+    List<Spi> loadSpis();
+
+    /**
+     * Load all provider factories of a specific SPI.
+     *
+     * @param spi the Spi definition
+     * @return a list of provider factories
+     */
     List<ProviderFactory> load(Spi spi);
 
 }
diff --git a/services/src/main/java/org/keycloak/provider/DefaultProviderLoader.java b/services/src/main/java/org/keycloak/provider/DefaultProviderLoader.java
index 26aa19d..5f4b19d 100644
--- a/services/src/main/java/org/keycloak/provider/DefaultProviderLoader.java
+++ b/services/src/main/java/org/keycloak/provider/DefaultProviderLoader.java
@@ -33,6 +33,15 @@ public class DefaultProviderLoader implements ProviderLoader {
     }
 
     @Override
+    public List<Spi> loadSpis() {
+        LinkedList<Spi> list = new LinkedList<>();
+        for (Spi spi : ServiceLoader.load(Spi.class, classLoader)) {
+            list.add(spi);
+        }
+        return list;
+    }
+
+    @Override
     public List<ProviderFactory> load(Spi spi) {
         LinkedList<ProviderFactory> list = new LinkedList<ProviderFactory>();
         for (ProviderFactory f : ServiceLoader.load(spi.getProviderFactoryClass(), classLoader)) {
diff --git a/services/src/main/java/org/keycloak/provider/ProviderManager.java b/services/src/main/java/org/keycloak/provider/ProviderManager.java
index 997c68a..e906df9 100644
--- a/services/src/main/java/org/keycloak/provider/ProviderManager.java
+++ b/services/src/main/java/org/keycloak/provider/ProviderManager.java
@@ -65,6 +65,20 @@ public class ProviderManager {
         }
     }
 
+    public synchronized List<Spi> loadSpis() {
+        // Use a map to prevent duplicates, since the loaders may have overlapping classpaths.
+        Map<String, Spi> spiMap = new HashMap<>();
+        for (ProviderLoader loader : loaders) {
+            List<Spi> spis = loader.loadSpis();
+            if (spis != null) {
+                for (Spi spi : spis) {
+                    spiMap.put(spi.getName(), spi);
+                }
+            }
+        }
+        return new LinkedList<>(spiMap.values());
+    }
+
     public synchronized List<ProviderFactory> load(Spi spi) {
         List<ProviderFactory> factories = cache.get(spi.getName());
         if (factories == null) {
diff --git a/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java b/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java
index 90b495c..172de6e 100755
--- a/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java
+++ b/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java
@@ -70,8 +70,9 @@ public class DefaultKeycloakSessionFactory implements KeycloakSessionFactory {
 
         ProviderManager pm = new ProviderManager(getClass().getClassLoader(), Config.scope().getArray("providers"));
 
-        ServiceLoader<Spi> load = ServiceLoader.load(Spi.class, getClass().getClassLoader());
-        loadSPIs(pm, load);
+        // Load the SPI classes through the provider manager, so both Keycloak internal SPI's and
+        // the ones defined in deployed modules will be found.
+        loadSPIs(pm, pm.loadSpis());
         for ( Map<String, ProviderFactory> factories : factoriesMap.values()) {
             for (ProviderFactory factory : factories.values()) {
                 factory.postInit(this);
@@ -79,8 +80,8 @@ public class DefaultKeycloakSessionFactory implements KeycloakSessionFactory {
         }
     }
 
-    protected void loadSPIs(ProviderManager pm, ServiceLoader<Spi> load) {
-        for (Spi spi : load) {
+    protected void loadSPIs(ProviderManager pm, List<Spi> spiList) {
+        for (Spi spi : spiList) {
             spis.add(spi);
 
             Map<String, ProviderFactory> factories = new HashMap<String, ProviderFactory>();
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
index b26d95a..1eee46b 100644
--- a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
@@ -496,9 +496,9 @@ public class RealmAdminResource {
     @NoCache
     @Produces(MediaType.APPLICATION_JSON)
     public List<EventRepresentation> getEvents(@QueryParam("type") List<String> types, @QueryParam("client") String client,
-            @QueryParam("user") String user, @QueryParam("dateFrom") String dateFrom, @QueryParam("dateTo") String dateTo,
-            @QueryParam("ipAddress") String ipAddress, @QueryParam("first") Integer firstResult,
-            @QueryParam("max") Integer maxResults) {
+                                               @QueryParam("user") String user, @QueryParam("dateFrom") String dateFrom, @QueryParam("dateTo") String dateTo,
+                                               @QueryParam("ipAddress") String ipAddress, @QueryParam("first") Integer firstResult,
+                                               @QueryParam("max") Integer maxResults) {
         auth.init(RealmAuth.Resource.EVENTS).requireView();
 
         EventStoreProvider eventStore = session.getProvider(EventStoreProvider.class);
@@ -585,10 +585,10 @@ public class RealmAdminResource {
     @NoCache
     @Produces(MediaType.APPLICATION_JSON)
     public List<AdminEventRepresentation> getEvents(@QueryParam("operationTypes") List<String> operationTypes, @QueryParam("authRealm") String authRealm, @QueryParam("authClient") String authClient,
-            @QueryParam("authUser") String authUser, @QueryParam("authIpAddress") String authIpAddress,
-            @QueryParam("resourcePath") String resourcePath, @QueryParam("dateFrom") String dateFrom,
-            @QueryParam("dateTo") String dateTo, @QueryParam("first") Integer firstResult,
-            @QueryParam("max") Integer maxResults) {
+                                                    @QueryParam("authUser") String authUser, @QueryParam("authIpAddress") String authIpAddress,
+                                                    @QueryParam("resourcePath") String resourcePath, @QueryParam("dateFrom") String dateFrom,
+                                                    @QueryParam("dateTo") String dateTo, @QueryParam("first") Integer firstResult,
+                                                    @QueryParam("max") Integer maxResults) {
         auth.init(RealmAuth.Resource.EVENTS).requireView();
 
         EventStoreProvider eventStore = session.getProvider(EventStoreProvider.class);
diff --git a/services/src/main/java/org/keycloak/services/resources/RealmsResource.java b/services/src/main/java/org/keycloak/services/resources/RealmsResource.java
index 4161763..53c9aaf 100755
--- a/services/src/main/java/org/keycloak/services/resources/RealmsResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/RealmsResource.java
@@ -101,7 +101,7 @@ public class RealmsResource {
 
     @Path("{realm}/protocol/{protocol}")
     public Object getProtocol(final @PathParam("realm") String name,
-                                            final @PathParam("protocol") String protocol) {
+                              final @PathParam("protocol") String protocol) {
         RealmModel realm = init(name);
 
         LoginProtocolFactory factory = (LoginProtocolFactory)session.getKeycloakSessionFactory().getProviderFactory(LoginProtocol.class, protocol);
@@ -239,7 +239,7 @@ public class RealmsResource {
     @Path("{realm}/.well-known/{provider}")
     @Produces(MediaType.APPLICATION_JSON)
     public Response getWellKnown(final @PathParam("realm") String name,
-                              final @PathParam("provider") String providerName) {
+                                 final @PathParam("provider") String providerName) {
         init(name);
 
         WellKnownProvider wellKnown = session.getProvider(WellKnownProvider.class, providerName);
diff --git a/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi
index 50bb346..55b31a0 100755
--- a/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi
+++ b/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi
@@ -18,3 +18,4 @@
 org.keycloak.exportimport.ClientDescriptionConverterSpi
 org.keycloak.wellknown.WellKnownSpi
 org.keycloak.services.clientregistration.ClientRegistrationSpi
+
diff --git a/testsuite/integration/pom.xml b/testsuite/integration/pom.xml
index d394e2a..c1d0412 100755
--- a/testsuite/integration/pom.xml
+++ b/testsuite/integration/pom.xml
@@ -140,6 +140,10 @@
         </dependency>
         <dependency>
             <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-authz-client</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
             <artifactId>keycloak-saml-servlet-filter-adapter</artifactId>
         </dependency>
         <dependency>
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/CompanyRepresentation.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/CompanyRepresentation.java
new file mode 100644
index 0000000..117b928
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/CompanyRepresentation.java
@@ -0,0 +1,51 @@
+/*
+ * 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.domainextension;
+
+
+import org.keycloak.testsuite.domainextension.jpa.Company;
+
+public class CompanyRepresentation {
+
+    private String id;
+    private String name;
+
+    public CompanyRepresentation() {
+    }
+
+    public CompanyRepresentation(Company company) {
+        id = company.getId();
+        name = company.getName();
+    }
+    
+    public String getId() {
+		return id;
+	}
+    
+    public String getName() {
+		return name;
+	}
+
+    public void setId(String id) {
+        this.id = id;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+}
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/jpa/Company.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/jpa/Company.java
new file mode 100644
index 0000000..d5f95ae
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/jpa/Company.java
@@ -0,0 +1,66 @@
+/*
+ * 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.domainextension.jpa;
+
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.Id;
+import javax.persistence.NamedQueries;
+import javax.persistence.NamedQuery;
+import javax.persistence.Table;
+
+@Entity
+@Table(name = "EXAMPLE_COMPANY")
+@NamedQueries({ @NamedQuery(name = "findByRealm", query = "from Company where realmId = :realmId") })
+public class Company {
+
+    @Id
+    @Column(name = "ID")
+    private String id;
+
+    @Column(name = "NAME", nullable = false)
+    private String name;
+
+    @Column(name = "REALM_ID", nullable = false)
+    private String realmId;
+
+    public String getId() {
+		return id;
+	}
+    
+    public String getRealmId() {
+        return realmId;
+    }
+    
+    public String getName() {
+		return name;
+	}
+
+    public void setId(String id) {
+        this.id = id;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public void setRealmId(String realmId) {
+        this.realmId = realmId;
+    }
+}
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/jpa/ExampleJpaEntityProvider.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/jpa/ExampleJpaEntityProvider.java
new file mode 100644
index 0000000..2f87632
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/jpa/ExampleJpaEntityProvider.java
@@ -0,0 +1,50 @@
+/*
+ * 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.domainextension.jpa;
+
+import java.util.Collections;
+import java.util.List;
+
+import org.keycloak.connections.jpa.entityprovider.JpaEntityProvider;
+
+/**
+ * @author <a href="mailto:erik.mulder@docdatapayments.com">Erik Mulder</a>
+ * 
+ * Example JpaEntityProvider.
+ */
+public class ExampleJpaEntityProvider implements JpaEntityProvider {
+
+    @Override
+    public List<Class<?>> getEntities() {
+        return Collections.<Class<?>>singletonList(Company.class);
+    }
+
+    @Override
+    public String getChangelogLocation() {
+    	return "META-INF/example-changelog.xml";
+    }
+    
+    @Override
+    public void close() {
+    }
+
+    @Override
+    public String getFactoryId() {
+        return ExampleJpaEntityProviderFactory.ID;
+    }
+}
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/jpa/ExampleJpaEntityProviderFactory.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/jpa/ExampleJpaEntityProviderFactory.java
new file mode 100644
index 0000000..83dc0a7
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/jpa/ExampleJpaEntityProviderFactory.java
@@ -0,0 +1,57 @@
+/*
+ * 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.domainextension.jpa;
+
+import org.keycloak.Config.Scope;
+import org.keycloak.connections.jpa.entityprovider.JpaEntityProvider;
+import org.keycloak.connections.jpa.entityprovider.JpaEntityProviderFactory;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+
+/**
+ * @author <a href="mailto:erik.mulder@docdatapayments.com">Erik Mulder</a>
+ * 
+ * Example JpaEntityProviderFactory.
+ */
+public class ExampleJpaEntityProviderFactory implements JpaEntityProviderFactory {
+
+	protected static final String ID = "example-entity-provider";
+	
+    @Override
+    public JpaEntityProvider create(KeycloakSession session) {
+        return new ExampleJpaEntityProvider();
+    }
+
+    @Override
+    public String getId() {
+        return ID;
+    }
+
+    @Override
+    public void init(Scope config) {
+    }
+
+    @Override
+    public void postInit(KeycloakSessionFactory factory) {
+    }
+
+    @Override
+    public void close() {
+    }
+
+}
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/rest/CompanyResource.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/rest/CompanyResource.java
new file mode 100644
index 0000000..197cbce
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/rest/CompanyResource.java
@@ -0,0 +1,77 @@
+/*
+ * 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.domainextension.rest;
+
+import java.util.List;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+import org.jboss.resteasy.annotations.cache.NoCache;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.testsuite.domainextension.CompanyRepresentation;
+import org.keycloak.testsuite.domainextension.spi.ExampleService;
+
+public class CompanyResource {
+
+	private final KeycloakSession session;
+	
+	public CompanyResource(KeycloakSession session) {
+		this.session = session;
+	}
+
+    @GET
+    @Path("")
+    @NoCache
+    @Produces(MediaType.APPLICATION_JSON)
+    public List<CompanyRepresentation> getCompanies() {
+        return session.getProvider(ExampleService.class).listCompanies();
+    }
+
+    @DELETE
+    @Path("")
+    @NoCache
+    public void deleteAllCompanies() {
+        session.getProvider(ExampleService.class).deleteAllCompanies();
+    }
+
+    @POST
+    @Path("")
+    @NoCache
+    @Consumes(MediaType.APPLICATION_JSON)
+    public Response createCompany(CompanyRepresentation rep) {
+        session.getProvider(ExampleService.class).addCompany(rep);
+        return Response.created(session.getContext().getUri().getAbsolutePathBuilder().path(rep.getId()).build()).build();
+    }
+
+    @GET
+    @NoCache
+    @Path("{id}")
+    @Produces(MediaType.APPLICATION_JSON)
+    public CompanyRepresentation getCompany(@PathParam("id") final String id) {
+        return session.getProvider(ExampleService.class).findCompany(id);
+    }
+
+}
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/rest/ExampleRealmResourceProvider.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/rest/ExampleRealmResourceProvider.java
new file mode 100644
index 0000000..ff9ad02
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/rest/ExampleRealmResourceProvider.java
@@ -0,0 +1,40 @@
+/*
+ * 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.domainextension.rest;
+
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.services.resource.RealmResourceProvider;
+
+public class ExampleRealmResourceProvider implements RealmResourceProvider {
+
+    private KeycloakSession session;
+
+    public ExampleRealmResourceProvider(KeycloakSession session) {
+        this.session = session;
+    }
+
+    @Override
+    public Object getResource() {
+        return new ExampleRestResource(session);
+    }
+
+    @Override
+    public void close() {
+    }
+
+}
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/rest/ExampleRealmResourceProviderFactory.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/rest/ExampleRealmResourceProviderFactory.java
new file mode 100644
index 0000000..ae016b6
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/rest/ExampleRealmResourceProviderFactory.java
@@ -0,0 +1,52 @@
+/*
+ * 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.domainextension.rest;
+
+import org.keycloak.Config.Scope;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.services.resource.RealmResourceProvider;
+import org.keycloak.services.resource.RealmResourceProviderFactory;
+
+public class ExampleRealmResourceProviderFactory implements RealmResourceProviderFactory {
+
+    public static final String ID = "example";
+
+    @Override
+    public String getId() {
+        return ID;
+    }
+
+    @Override
+    public RealmResourceProvider create(KeycloakSession session) {
+        return new ExampleRealmResourceProvider(session);
+    }
+
+    @Override
+    public void init(Scope config) {
+    }
+
+    @Override
+    public void postInit(KeycloakSessionFactory factory) {
+    }
+
+    @Override
+    public void close() {
+    }
+
+}
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/rest/ExampleRestResource.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/rest/ExampleRestResource.java
new file mode 100644
index 0000000..629acc8
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/rest/ExampleRestResource.java
@@ -0,0 +1,59 @@
+/*
+ * 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.domainextension.rest;
+
+import javax.ws.rs.ForbiddenException;
+import javax.ws.rs.NotAuthorizedException;
+import javax.ws.rs.Path;
+
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.services.managers.AppAuthManager;
+import org.keycloak.services.managers.AuthenticationManager;
+
+public class ExampleRestResource {
+
+	private final KeycloakSession session;
+    private final AuthenticationManager.AuthResult auth;
+	
+	public ExampleRestResource(KeycloakSession session) {
+		this.session = session;
+        this.auth = new AppAuthManager().authenticateBearerToken(session, session.getContext().getRealm());
+	}
+	
+    @Path("companies")
+    public CompanyResource getCompanyResource() {
+        return new CompanyResource(session);
+    }
+
+    // Same like "companies" endpoint, but REST endpoint is authenticated with Bearer token and user must be in realm role "admin"
+    // Just for illustration purposes
+    @Path("companies-auth")
+    public CompanyResource getCompanyResourceAuthenticated() {
+        checkRealmAdmin();
+        return new CompanyResource(session);
+    }
+
+    private void checkRealmAdmin() {
+        if (auth == null) {
+            throw new NotAuthorizedException("Bearer");
+        } else if (auth.getToken().getRealmAccess() == null || !auth.getToken().getRealmAccess().isUserInRole("admin")) {
+            throw new ForbiddenException("Does not have realm admin role");
+        }
+    }
+
+}
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/spi/ExampleService.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/spi/ExampleService.java
new file mode 100644
index 0000000..dd47d96
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/spi/ExampleService.java
@@ -0,0 +1,35 @@
+/*
+ * 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.domainextension.spi;
+
+import java.util.List;
+
+import org.keycloak.provider.Provider;
+import org.keycloak.testsuite.domainextension.CompanyRepresentation;
+
+public interface ExampleService extends Provider {
+
+    List<CompanyRepresentation> listCompanies();
+
+    CompanyRepresentation findCompany(String id);
+
+    CompanyRepresentation addCompany(CompanyRepresentation company);
+
+    void deleteAllCompanies();
+
+}
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/spi/ExampleServiceProviderFactory.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/spi/ExampleServiceProviderFactory.java
new file mode 100644
index 0000000..ee802b0
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/spi/ExampleServiceProviderFactory.java
@@ -0,0 +1,24 @@
+/*
+ * 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.domainextension.spi;
+
+import org.keycloak.provider.ProviderFactory;
+
+public interface ExampleServiceProviderFactory extends ProviderFactory<ExampleService> {
+
+}
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/spi/ExampleSpi.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/spi/ExampleSpi.java
new file mode 100644
index 0000000..d85f043
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/spi/ExampleSpi.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.domainextension.spi;
+
+import org.keycloak.provider.Provider;
+import org.keycloak.provider.ProviderFactory;
+import org.keycloak.provider.Spi;
+
+public class ExampleSpi implements Spi {
+
+    @Override
+    public boolean isInternal() {
+        return false;
+    }
+
+    @Override
+    public String getName() {
+        return "example";
+    }
+
+    @Override
+    public Class<? extends Provider> getProviderClass() {
+        return ExampleService.class;
+    }
+
+    @Override
+    @SuppressWarnings("rawtypes")
+    public Class<? extends ProviderFactory> getProviderFactoryClass() {
+        return ExampleServiceProviderFactory.class;
+    }
+
+}
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/spi/impl/ExampleServiceImpl.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/spi/impl/ExampleServiceImpl.java
new file mode 100644
index 0000000..213f7e5
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/spi/impl/ExampleServiceImpl.java
@@ -0,0 +1,100 @@
+/*
+ * 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.domainextension.spi.impl;
+
+import java.util.LinkedList;
+import java.util.List;
+
+import javax.persistence.EntityManager;
+
+import org.keycloak.connections.jpa.JpaConnectionProvider;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.utils.KeycloakModelUtils;
+import org.keycloak.testsuite.domainextension.CompanyRepresentation;
+import org.keycloak.testsuite.domainextension.jpa.Company;
+import org.keycloak.testsuite.domainextension.spi.ExampleService;
+
+public class ExampleServiceImpl implements ExampleService {
+
+    private final KeycloakSession session;
+
+    public ExampleServiceImpl(KeycloakSession session) {
+        this.session = session;
+        if (getRealm() == null) {
+            throw new IllegalStateException("The service cannot accept a session without a realm in it's context.");
+        }
+    }
+
+    private EntityManager getEntityManager() {
+        return session.getProvider(JpaConnectionProvider.class).getEntityManager();
+    }
+
+    protected RealmModel getRealm() {
+        return session.getContext().getRealm();
+    }
+    
+    @Override
+    public List<CompanyRepresentation> listCompanies() {
+    	List<Company> companyEntities = getEntityManager().createNamedQuery("findByRealm", Company.class)
+                .setParameter("realmId", getRealm().getId())
+                .getResultList();
+
+        List<CompanyRepresentation> result = new LinkedList<>();
+        for (Company entity : companyEntities) {
+            result.add(new CompanyRepresentation(entity));
+        }
+        return result;
+    }
+    
+    @Override
+    public CompanyRepresentation findCompany(String id) {
+    	Company entity = getEntityManager().find(Company.class, id);
+        return entity==null ? null : new CompanyRepresentation(entity);
+    }
+    
+    @Override
+    public CompanyRepresentation addCompany(CompanyRepresentation company) {
+        Company entity = new Company();
+        String id = company.getId()==null ?  KeycloakModelUtils.generateId() : company.getId();
+        entity.setId(id);
+        entity.setName(company.getName());
+        entity.setRealmId(getRealm().getId());
+        getEntityManager().persist(entity);
+
+        company.setId(id);
+        return company;
+    }
+
+    @Override
+    public void deleteAllCompanies() {
+        EntityManager em = getEntityManager();
+        List<Company> companyEntities = em.createNamedQuery("findByRealm", Company.class)
+                .setParameter("realmId", getRealm().getId())
+                .getResultList();
+
+        for (Company entity : companyEntities) {
+            em.remove(entity);
+        }
+    }
+
+    public void close() {
+        // Nothing to do.
+    }
+
+}
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/spi/impl/ExampleServiceProviderFactoryImpl.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/spi/impl/ExampleServiceProviderFactoryImpl.java
new file mode 100644
index 0000000..c772827
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/spi/impl/ExampleServiceProviderFactoryImpl.java
@@ -0,0 +1,53 @@
+/*
+ * 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.domainextension.spi.impl;
+
+import org.keycloak.Config.Scope;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.testsuite.domainextension.spi.ExampleService;
+import org.keycloak.testsuite.domainextension.spi.ExampleServiceProviderFactory;
+
+public class ExampleServiceProviderFactoryImpl implements ExampleServiceProviderFactory {
+
+    @Override
+    public ExampleService create(KeycloakSession session) {
+        return new ExampleServiceImpl(session);
+    }
+
+    @Override
+    public void init(Scope config) {
+
+    }
+
+    @Override
+    public void postInit(KeycloakSessionFactory factory) {
+
+    }
+
+    @Override
+    public void close() {
+
+    }
+
+    @Override
+    public String getId() {
+        return "exampleServiceImpl";
+    }
+
+}
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/example-changelog.xml b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/example-changelog.xml
new file mode 100644
index 0000000..5edd719
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/example-changelog.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
+    <changeSet author="erik.mulder@docdatapayments.com" id="example-1.0">
+
+        <createTable tableName="EXAMPLE_COMPANY">
+            <column name="ID" type="VARCHAR(36)">
+                <constraints nullable="false"/>
+            </column>
+            <column name="NAME" type="VARCHAR(255)">
+                <constraints nullable="false"/>
+            </column>
+            <column name="REALM_ID" type="VARCHAR(36)">
+                <constraints nullable="false"/>
+            </column>
+        </createTable>
+
+        <addPrimaryKey
+            constraintName="PK_COMPANY"
+            tableName="EXAMPLE_COMPANY"
+            columnNames="ID"
+        />
+
+    </changeSet>
+    
+</databaseChangeLog>
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.connections.jpa.entityprovider.JpaEntityProviderFactory b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.connections.jpa.entityprovider.JpaEntityProviderFactory
new file mode 100644
index 0000000..67464bd
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.connections.jpa.entityprovider.JpaEntityProviderFactory
@@ -0,0 +1,18 @@
+#
+# 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.
+#
+
+org.keycloak.testsuite.domainextension.jpa.ExampleJpaEntityProviderFactory
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.provider.Spi
new file mode 100644
index 0000000..1c31c00
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.provider.Spi
@@ -0,0 +1,18 @@
+#
+# 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.
+#
+
+org.keycloak.testsuite.domainextension.spi.ExampleSpi
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.services.resource.RealmResourceProviderFactory b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.services.resource.RealmResourceProviderFactory
index 8823682..1958ff0 100644
--- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.services.resource.RealmResourceProviderFactory
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.services.resource.RealmResourceProviderFactory
@@ -16,4 +16,5 @@
 #
 
 org.keycloak.testsuite.rest.TestingResourceProviderFactory
-org.keycloak.testsuite.rest.TestApplicationResourceProviderFactory
\ No newline at end of file
+org.keycloak.testsuite.rest.TestApplicationResourceProviderFactory
+org.keycloak.testsuite.domainextension.rest.ExampleRealmResourceProviderFactory
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.testsuite.domainextension.spi.ExampleServiceProviderFactory b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.testsuite.domainextension.spi.ExampleServiceProviderFactory
new file mode 100644
index 0000000..d58236f
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.testsuite.domainextension.spi.ExampleServiceProviderFactory
@@ -0,0 +1,18 @@
+#
+# 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.
+#
+
+org.keycloak.testsuite.domainextension.spi.impl.ExampleServiceProviderFactoryImpl
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/org/keycloak/testsuite/integration-arquillian-testsuite-providers/main/module.xml b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/org/keycloak/testsuite/integration-arquillian-testsuite-providers/main/module.xml
index a5d327f..28e4789 100644
--- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/org/keycloak/testsuite/integration-arquillian-testsuite-providers/main/module.xml
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/org/keycloak/testsuite/integration-arquillian-testsuite-providers/main/module.xml
@@ -30,8 +30,12 @@
         <module name="org.keycloak.keycloak-server-spi"/>
         <module name="org.keycloak.keycloak-services"/>
         <module name="org.keycloak.keycloak-model-infinispan"/>
+        <module name="org.keycloak.keycloak-model-jpa"/>
         <module name="org.infinispan"/>
         <module name="org.jboss.logging"/>
-	<module name="org.jboss.resteasy.resteasy-jaxrs"/>
+        <module name="org.jboss.resteasy.resteasy-jaxrs"/>
+        <module name="javax.persistence.api"/>
+        <module name="org.hibernate"/>
+        <module name="org.javassist"/>
     </dependencies>
 </module>
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/KeycloakTestingClient.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/KeycloakTestingClient.java
index 46f7b23..dedc08d 100755
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/KeycloakTestingClient.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/KeycloakTestingClient.java
@@ -21,6 +21,7 @@ import org.jboss.resteasy.client.jaxrs.ResteasyClient;
 import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder;
 import org.jboss.resteasy.client.jaxrs.ResteasyWebTarget;
 import org.keycloak.testsuite.client.resources.TestApplicationResource;
+import org.keycloak.testsuite.client.resources.TestExampleCompanyResource;
 import org.keycloak.testsuite.client.resources.TestingResource;
 
 /**
@@ -45,6 +46,8 @@ public class KeycloakTestingClient {
 
     public TestApplicationResource testApp() { return target.proxy(TestApplicationResource.class); }
 
+    public TestExampleCompanyResource testExampleCompany() { return target.proxy(TestExampleCompanyResource.class); }
+
     public void close() {
         client.close();
     }
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestExampleCompanyResource.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestExampleCompanyResource.java
new file mode 100644
index 0000000..00d6e28
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestExampleCompanyResource.java
@@ -0,0 +1,56 @@
+/*
+ * 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.client.resources;
+
+import java.util.List;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+import org.keycloak.testsuite.domainextension.CompanyRepresentation;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+@Path("/realms/{realmName}/example/companies")
+@Consumes(MediaType.APPLICATION_JSON)
+public interface TestExampleCompanyResource {
+
+    @GET
+    @Produces(MediaType.APPLICATION_JSON)
+    List<CompanyRepresentation> getCompanies(@PathParam("realmName") String realmName);
+
+    @GET
+    @Path("/{companyId}")
+    @Produces(MediaType.APPLICATION_JSON)
+    CompanyRepresentation getCompany(@PathParam("realmName") String realmName, @PathParam("companyId") String companyId);
+
+    @POST
+    @Consumes(MediaType.APPLICATION_JSON)
+    Response createCompany(@PathParam("realmName") String realmName, CompanyRepresentation rep);
+
+    @DELETE
+    void deleteAllCompanies(@PathParam("realmName") String realmName);
+}
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/domainextension/CustomExtensionTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/domainextension/CustomExtensionTest.java
new file mode 100644
index 0000000..36b5702
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/domainextension/CustomExtensionTest.java
@@ -0,0 +1,79 @@
+/*
+ * 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.domainextension;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.junit.Test;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.testsuite.AbstractKeycloakTest;
+import org.keycloak.testsuite.Assert;
+import org.keycloak.testsuite.client.resources.TestExampleCompanyResource;
+import org.keycloak.testsuite.util.RealmBuilder;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class CustomExtensionTest extends AbstractKeycloakTest {
+
+    @Override
+    public void addTestRealms(List<RealmRepresentation> testRealms) {
+        RealmRepresentation foo = RealmBuilder.create().name("foo").build();
+        testRealms.add(foo);
+    }
+
+    @Test
+    public void testDomainExtension() throws Exception {
+        companyResource().createCompany("foo", buildCompany("foo-company"));
+        companyResource().createCompany("foo", buildCompany("bar-company"));
+        companyResource().createCompany("master", buildCompany("master-company"));
+
+        List<CompanyRepresentation> fooCompanies = companyResource().getCompanies("foo");
+        List<CompanyRepresentation> masterCompanies = companyResource().getCompanies("master");
+
+        assertCompanyNames(fooCompanies, "foo-company", "bar-company");
+        assertCompanyNames(masterCompanies, "master-company");
+
+        companyResource().deleteAllCompanies("foo");
+        companyResource().deleteAllCompanies("master");
+    }
+
+    private TestExampleCompanyResource companyResource() {
+        return testingClient.testExampleCompany();
+    }
+
+    private CompanyRepresentation buildCompany(String companyName) {
+        CompanyRepresentation rep = new CompanyRepresentation();
+        rep.setName(companyName);
+        return rep;
+    }
+
+    private void assertCompanyNames(List<CompanyRepresentation> companies, String... expectedNames) {
+        Set<String> names = new HashSet<>();
+        for (CompanyRepresentation comp : companies) {
+            names.add(comp.getName());
+        }
+
+        Assert.assertEquals(expectedNames.length, names.size());
+        for (String expectedName : expectedNames) {
+            Assert.assertTrue(names.contains(expectedName));
+        }
+    }
+}