Merge branch 'master-upstream' into feature-lithuanian-locale
# Conflicts: # themes/src/main/resources/theme/base/account/messages/messages_en.properties # themes/src/main/resources/theme/base/account/theme.properties # themes/src/main/resources/theme/base/admin/theme.properties # themes/src/main/resources/theme/base/email/theme.properties # themes/src/main/resources/theme/base/login/theme.properties
This commit is contained in:
commit
2e9f14dfd8
214 files changed
+8731
-1315
No files matched your search
@@ -65,6 +65,7 @@ Contributing
|
||||
* [Testsuite](misc/Testsuite.md) - Details about testsuite, but also how to quickly run Keycloak during development and a few test tools (OTP generation, LDAP server, Mail server)
|
||||
* [Database Testing](misc/DatabaseTesting.md) - How to do testing of Keycloak on different databases
|
||||
* [Updating Database](misc/UpdatingDatabaseSchema.md) - How to change the Keycloak database
|
||||
* [Changing the Default keycloak-subsystem Configuration](misc/UpdatingServerConfig.md) - How to update the default keycloak-subsystem config
|
||||
* [Developer Mailing List](https://lists.jboss.org/mailman/listinfo/keycloak-dev) - Mailing list to discuss development of Keycloak
|
||||
|
||||
|
||||
|
||||
+2
-1
@@ -32,6 +32,7 @@ import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathCon
|
||||
import org.keycloak.representations.idm.authorization.Permission;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
@@ -55,7 +56,7 @@ public abstract class AbstractPolicyEnforcer {
|
||||
this.enforcerConfig = policyEnforcer.getEnforcerConfig();
|
||||
this.authzClient = policyEnforcer.getClient();
|
||||
this.pathMatcher = new PathMatcher();
|
||||
this.paths = policyEnforcer.getPaths();
|
||||
this.paths = new ArrayList<>(policyEnforcer.getPaths());
|
||||
}
|
||||
|
||||
public AuthorizationContext authorize(OIDCHttpFacade httpFacade) {
|
||||
|
||||
+22
-4
@@ -33,8 +33,12 @@ import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathCon
|
||||
import org.keycloak.representations.idm.authorization.Permission;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
@@ -95,8 +99,8 @@ public class PolicyEnforcer {
|
||||
return authzClient;
|
||||
}
|
||||
|
||||
List<PathConfig> getPaths() {
|
||||
return paths;
|
||||
public List<PathConfig> getPaths() {
|
||||
return Collections.unmodifiableList(paths);
|
||||
}
|
||||
|
||||
KeycloakDeployment getDeployment() {
|
||||
@@ -154,13 +158,27 @@ public class PolicyEnforcer {
|
||||
|
||||
pathConfig.setId(registrationResponse.getId());
|
||||
} else {
|
||||
throw new RuntimeException("Could not find matching resource on server with uri [" + path + "] or name [" + resourceName + ". Make sure you have created a resource on the server that matches with the path configuration.");
|
||||
throw new RuntimeException("Could not find matching resource on server with uri [" + path + "] or name [" + resourceName + "]. Make sure you have created a resource on the server that matches with the path configuration.");
|
||||
}
|
||||
} else {
|
||||
pathConfig.setId(search.iterator().next());
|
||||
}
|
||||
|
||||
paths.add(pathConfig);
|
||||
PathConfig existingPath = null;
|
||||
|
||||
for (PathConfig current : paths) {
|
||||
if (current.getId().equals(pathConfig.getId()) && current.getPath().equals(pathConfig.getPath())) {
|
||||
existingPath = current;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (existingPath == null) {
|
||||
paths.add(pathConfig);
|
||||
} else {
|
||||
existingPath.getMethods().addAll(pathConfig.getMethods());
|
||||
existingPath.getScopes().addAll(pathConfig.getScopes());
|
||||
}
|
||||
}
|
||||
|
||||
return paths;
|
||||
|
||||
+3
-5
@@ -37,12 +37,10 @@ public class TimePolicyAdminResource implements PolicyProviderAdminService {
|
||||
String nbf = policy.getConfig().get("nbf");
|
||||
String noa = policy.getConfig().get("noa");
|
||||
|
||||
if (nbf == null && noa == null) {
|
||||
throw new RuntimeException("You must provide NotBefore, NotOnOrAfter or both.");
|
||||
if (nbf != null && noa != null) {
|
||||
validateFormat(nbf);
|
||||
validateFormat(noa);
|
||||
}
|
||||
|
||||
validateFormat(nbf);
|
||||
validateFormat(noa);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+39
-3
@@ -22,8 +22,11 @@ import org.keycloak.authorization.policy.evaluation.Evaluation;
|
||||
import org.keycloak.authorization.policy.provider.PolicyProvider;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
|
||||
import static com.sun.corba.se.spi.activation.IIOP_CLEAR_TEXT.value;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||
*/
|
||||
@@ -45,9 +48,7 @@ public class TimePolicyProvider implements PolicyProvider {
|
||||
public void evaluate(Evaluation evaluation) {
|
||||
try {
|
||||
String notBefore = this.policy.getConfig().get("nbf");
|
||||
|
||||
if (notBefore != null) {
|
||||
|
||||
if (this.currentDate.before(this.dateFormat.parse(format(notBefore)))) {
|
||||
evaluation.deny();
|
||||
return;
|
||||
@@ -55,7 +56,6 @@ public class TimePolicyProvider implements PolicyProvider {
|
||||
}
|
||||
|
||||
String notOnOrAfter = this.policy.getConfig().get("noa");
|
||||
|
||||
if (notOnOrAfter != null) {
|
||||
if (this.currentDate.after(this.dateFormat.parse(format(notOnOrAfter)))) {
|
||||
evaluation.deny();
|
||||
@@ -63,12 +63,48 @@ public class TimePolicyProvider implements PolicyProvider {
|
||||
}
|
||||
}
|
||||
|
||||
if (isInvalid(Calendar.DAY_OF_MONTH, "dayMonth")
|
||||
|| isInvalid(Calendar.MONTH, "month")
|
||||
|| isInvalid(Calendar.YEAR, "year")
|
||||
|| isInvalid(Calendar.HOUR_OF_DAY, "hour")
|
||||
|| isInvalid(Calendar.MINUTE, "minute")) {
|
||||
evaluation.deny();
|
||||
return;
|
||||
}
|
||||
|
||||
evaluation.grant();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Could not evaluate time-based policy [" + this.policy.getName() + "].", e);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isInvalid(int timeConstant, String configName) {
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
|
||||
calendar.setTime(this.currentDate);
|
||||
|
||||
int dateField = calendar.get(timeConstant);
|
||||
|
||||
if (Calendar.MONTH == timeConstant) {
|
||||
dateField++;
|
||||
}
|
||||
|
||||
String start = this.policy.getConfig().get(configName);
|
||||
if (start != null) {
|
||||
String end = this.policy.getConfig().get(configName + "End");
|
||||
if (end != null) {
|
||||
if (dateField < Integer.parseInt(start) || dateField > Integer.parseInt(end)) {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
if (dateField != Integer.parseInt(start)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static String format(String notBefore) {
|
||||
String trimmed = notBefore.trim();
|
||||
|
||||
|
||||
@@ -16,12 +16,31 @@
|
||||
*/
|
||||
package org.keycloak.representations;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAnyGetter;
|
||||
import com.fasterxml.jackson.annotation.JsonAnySetter;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import org.keycloak.json.StringOrArrayDeserializer;
|
||||
import org.keycloak.json.StringOrArraySerializer;
|
||||
|
||||
/**
|
||||
* @author pedroigor
|
||||
*/
|
||||
public class UserInfo {
|
||||
|
||||
// Should be in signed UserInfo response
|
||||
@JsonProperty("iss")
|
||||
protected String issuer;
|
||||
@JsonProperty("aud")
|
||||
@JsonSerialize(using = StringOrArraySerializer.class)
|
||||
@JsonDeserialize(using = StringOrArrayDeserializer.class)
|
||||
protected String[] audience;
|
||||
|
||||
@JsonProperty("sub")
|
||||
protected String sub;
|
||||
|
||||
@@ -85,6 +104,34 @@ public class UserInfo {
|
||||
@JsonProperty("claims_locales")
|
||||
protected String claimsLocales;
|
||||
|
||||
protected Map<String, Object> otherClaims = new HashMap<>();
|
||||
|
||||
public String getIssuer() {
|
||||
return issuer;
|
||||
}
|
||||
|
||||
public void setIssuer(String issuer) {
|
||||
this.issuer = issuer;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public String[] getAudience() {
|
||||
return audience;
|
||||
}
|
||||
|
||||
public boolean hasAudience(String audience) {
|
||||
for (String a : this.audience) {
|
||||
if (a.equals(audience)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void setAudience(String... audience) {
|
||||
this.audience = audience;
|
||||
}
|
||||
|
||||
public String getSubject() {
|
||||
return this.sub;
|
||||
}
|
||||
@@ -260,4 +307,19 @@ public class UserInfo {
|
||||
public void setClaimsLocales(String claimsLocales) {
|
||||
this.claimsLocales = claimsLocales;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a map of any other claims and data that might be in the UserInfo. Could be custom claims set up by the auth server
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@JsonAnyGetter
|
||||
public Map<String, Object> getOtherClaims() {
|
||||
return otherClaims;
|
||||
}
|
||||
|
||||
@JsonAnySetter
|
||||
public void setOtherClaims(String name, Object value) {
|
||||
otherClaims.put(name, value);
|
||||
}
|
||||
}
|
||||
@@ -202,6 +202,31 @@
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-dependency-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>unpack</id>
|
||||
<phase>compile</phase>
|
||||
<goals>
|
||||
<goal>unpack</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<artifactItems>
|
||||
<artifactItem>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-wildfly-server-subsystem</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<type>jar</type>
|
||||
<includes>default-config/*.xml</includes>
|
||||
</artifactItem>
|
||||
</artifactItems>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
~ 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.
|
||||
-->
|
||||
|
||||
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
|
||||
xmlns:xalan="http://xml.apache.org/xalan"
|
||||
@@ -42,7 +42,7 @@
|
||||
<xsl:template match="//ds:datasources">
|
||||
<xsl:copy>
|
||||
<xsl:apply-templates select="node()[name(.)='datasource']"/>
|
||||
<datasource jndi-name="java:jboss/datasources/KeycloakDS" pool-name="KeycloakDS" use-java-context="true">
|
||||
<datasource jndi-name="java:jboss/datasources/KeycloakDS" jta="false" pool-name="KeycloakDS" use-java-context="true">
|
||||
<connection-url>jdbc:h2:${jboss.server.data.dir}/keycloak;AUTO_SERVER=TRUE</connection-url>
|
||||
<driver>h2</driver>
|
||||
<security>
|
||||
@@ -57,9 +57,7 @@
|
||||
<xsl:template match="//j:profile">
|
||||
<xsl:copy>
|
||||
<xsl:apply-templates select="node()|@*"/>
|
||||
<subsystem xmlns="urn:jboss:domain:keycloak-server:1.1">
|
||||
<web-context>auth</web-context>
|
||||
</subsystem>
|
||||
<xsl:copy-of select="document('../../../target/dependency/default-config/keycloak-server-default-config.xml')"/>
|
||||
<subsystem xmlns="urn:jboss:domain:keycloak:1.1"/>
|
||||
<subsystem xmlns="urn:jboss:domain:keycloak-saml:1.1"/>
|
||||
</xsl:copy>
|
||||
|
||||
@@ -59,15 +59,4 @@
|
||||
</includes>
|
||||
</fileSet>
|
||||
</fileSets>
|
||||
|
||||
<files>
|
||||
<file>
|
||||
<source>src/main/resources/content/standalone/configuration/keycloak-server.json</source>
|
||||
<outputDirectory>content/domain/servers/server-one/configuration</outputDirectory>
|
||||
</file>
|
||||
<file>
|
||||
<source>src/main/resources/content/standalone/configuration/keycloak-server.json</source>
|
||||
<outputDirectory>content/domain/servers/server-two/configuration</outputDirectory>
|
||||
</file>
|
||||
</files>
|
||||
</assembly>
|
||||
-87
@@ -1,87 +0,0 @@
|
||||
{
|
||||
"providers": [
|
||||
"classpath:${jboss.home.dir}/providers/*"
|
||||
],
|
||||
|
||||
"admin": {
|
||||
"realm": "master"
|
||||
},
|
||||
|
||||
"eventsStore": {
|
||||
"provider": "jpa",
|
||||
"jpa": {
|
||||
"exclude-events": [ "REFRESH_TOKEN" ]
|
||||
}
|
||||
},
|
||||
|
||||
"realm": {
|
||||
"provider": "jpa"
|
||||
},
|
||||
|
||||
"user": {
|
||||
"provider": "jpa"
|
||||
},
|
||||
|
||||
"userCache": {
|
||||
"default" : {
|
||||
"enabled": true
|
||||
}
|
||||
},
|
||||
|
||||
"userSessionPersister": {
|
||||
"provider": "jpa"
|
||||
},
|
||||
|
||||
"authorizationPersister": {
|
||||
"provider": "jpa"
|
||||
},
|
||||
|
||||
"timer": {
|
||||
"provider": "basic"
|
||||
},
|
||||
|
||||
"theme": {
|
||||
"staticMaxAge": 2592000,
|
||||
"cacheTemplates": true,
|
||||
"cacheThemes": true,
|
||||
"folder": {
|
||||
"dir": "${jboss.home.dir}/themes"
|
||||
}
|
||||
},
|
||||
|
||||
"scheduled": {
|
||||
"interval": 900
|
||||
},
|
||||
|
||||
"connectionsHttpClient": {
|
||||
"default": {}
|
||||
},
|
||||
|
||||
"connectionsJpa": {
|
||||
"default": {
|
||||
"dataSource": "java:jboss/datasources/KeycloakDS",
|
||||
"databaseSchema": "update"
|
||||
}
|
||||
},
|
||||
|
||||
"realmCache": {
|
||||
"default" : {
|
||||
"enabled": true
|
||||
}
|
||||
},
|
||||
|
||||
"connectionsInfinispan": {
|
||||
"provider": "default",
|
||||
"default": {
|
||||
"cacheContainer" : "java:comp/env/infinispan/Keycloak"
|
||||
}
|
||||
},
|
||||
|
||||
"jta-lookup": {
|
||||
"provider": "${keycloak.jta.lookup.provider:jboss}",
|
||||
"jboss" : {
|
||||
"enabled": true
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
+1
@@ -55,6 +55,7 @@
|
||||
<module name="org.jboss.resteasy.resteasy-jaxrs"/>
|
||||
<module name="org.jboss.resteasy.resteasy-crypto"/>
|
||||
<module name="org.jboss.resteasy.resteasy-multipart-provider"/>
|
||||
<module name="org.jboss.dmr"/>
|
||||
<module name="javax.servlet.api"/>
|
||||
<module name="com.fasterxml.jackson.core.jackson-core"/>
|
||||
<module name="com.fasterxml.jackson.core.jackson-annotations"/>
|
||||
|
||||
+2
@@ -28,6 +28,8 @@
|
||||
</resources>
|
||||
|
||||
<dependencies>
|
||||
<module name="com.fasterxml.jackson.core.jackson-core"/>
|
||||
<module name="com.fasterxml.jackson.core.jackson-databind"/>
|
||||
<module name="javax.api"/>
|
||||
<module name="org.jboss.staxmapper"/>
|
||||
<module name="org.jboss.as.controller"/>
|
||||
|
||||
@@ -84,10 +84,6 @@
|
||||
</fileSets>
|
||||
|
||||
<files>
|
||||
<file>
|
||||
<source>${project.build.directory}/unpacked/keycloak-${project.version}/standalone/configuration/keycloak-server.json</source>
|
||||
<outputDirectory>standalone/configuration</outputDirectory>
|
||||
</file>
|
||||
<file>
|
||||
<source>${project.build.directory}/unpacked/keycloak-${project.version}/bin/add-user-keycloak.sh</source>
|
||||
<outputDirectory>bin</outputDirectory>
|
||||
@@ -98,6 +94,11 @@
|
||||
<outputDirectory>bin</outputDirectory>
|
||||
<destName>add-user-keycloak.bat</destName>
|
||||
</file>
|
||||
<file>
|
||||
<source>${project.build.directory}/cli/default-keycloak-subsys-config.cli</source>
|
||||
<outputDirectory>bin</outputDirectory>
|
||||
<destName>default-keycloak-subsys-config.cli</destName>
|
||||
</file>
|
||||
</files>
|
||||
|
||||
</assembly>
|
||||
@@ -1,5 +1,5 @@
|
||||
embed-server --server-config=standalone-ha.xml
|
||||
/subsystem=datasources/data-source=KeycloakDS/:add(connection-url="jdbc:h2:${jboss.server.data.dir}/keycloak;AUTO_SERVER=TRUE",driver-name=h2,jndi-name=java:jboss/datasources/KeycloakDS,password=sa,user-name=sa,use-java-context=true)
|
||||
/subsystem=datasources/data-source=KeycloakDS/:add(connection-url="jdbc:h2:${jboss.server.data.dir}/keycloak;AUTO_SERVER=TRUE",jta=false,driver-name=h2,jndi-name=java:jboss/datasources/KeycloakDS,password=sa,user-name=sa,use-java-context=true)
|
||||
/subsystem=infinispan/cache-container=keycloak:add(jndi-name="infinispan/Keycloak")
|
||||
/subsystem=infinispan/cache-container=keycloak/transport=TRANSPORT:add(lock-timeout=60000)
|
||||
/subsystem=infinispan/cache-container=keycloak/invalidation-cache=realms:add(mode="SYNC")
|
||||
@@ -11,4 +11,4 @@ embed-server --server-config=standalone-ha.xml
|
||||
/subsystem=infinispan/cache-container=keycloak/distributed-cache=authorization:add(mode="SYNC",owners="1")
|
||||
/subsystem=infinispan/cache-container=keycloak/replicated-cache=work:add(mode="SYNC")
|
||||
/extension=org.keycloak.keycloak-server-subsystem/:add(module=org.keycloak.keycloak-server-subsystem)
|
||||
/subsystem=keycloak-server:add(web-context=auth)
|
||||
run-batch --file=default-keycloak-subsys-config.cli
|
||||
@@ -1,5 +1,5 @@
|
||||
embed-server --server-config=standalone.xml
|
||||
/subsystem=datasources/data-source=KeycloakDS/:add(connection-url="jdbc:h2:${jboss.server.data.dir}/keycloak;AUTO_SERVER=TRUE",driver-name=h2,jndi-name=java:jboss/datasources/KeycloakDS,password=sa,user-name=sa,use-java-context=true)
|
||||
/subsystem=datasources/data-source=KeycloakDS/:add(connection-url="jdbc:h2:${jboss.server.data.dir}/keycloak;AUTO_SERVER=TRUE",jta=false,driver-name=h2,jndi-name=java:jboss/datasources/KeycloakDS,password=sa,user-name=sa,use-java-context=true)
|
||||
/subsystem=infinispan/cache-container=keycloak:add(jndi-name="infinispan/Keycloak")
|
||||
/subsystem=infinispan/cache-container=keycloak/local-cache=realms:add()
|
||||
/subsystem=infinispan/cache-container=keycloak/local-cache=users:add()
|
||||
@@ -11,4 +11,4 @@ embed-server --server-config=standalone.xml
|
||||
/subsystem=infinispan/cache-container=keycloak/local-cache=authorization:add()
|
||||
/subsystem=infinispan/cache-container=keycloak/local-cache=authorization/eviction=EVICTION:add(max-entries=100,strategy=LRU)
|
||||
/extension=org.keycloak.keycloak-server-subsystem/:add(module=org.keycloak.keycloak-server-subsystem)
|
||||
/subsystem=keycloak-server:add(web-context=auth)
|
||||
run-batch --file=default-keycloak-subsys-config.cli
|
||||
@@ -45,6 +45,25 @@
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-dependency-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>unpack</id>
|
||||
<phase>compile</phase>
|
||||
<goals>
|
||||
<goal>unpack</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<artifactItems>
|
||||
<artifactItem>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-wildfly-server-subsystem</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<type>jar</type>
|
||||
<includes>cli/*.cli</includes>
|
||||
<outputDirectory>${project.build.directory}</outputDirectory>
|
||||
</artifactItem>
|
||||
</artifactItems>
|
||||
</configuration>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>unpack-server-dist</id>
|
||||
<phase>prepare-package</phase>
|
||||
|
||||
@@ -39,6 +39,12 @@
|
||||
<version>1.0.0.Final</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>javax.enterprise</groupId>
|
||||
<artifactId>cdi-api</artifactId>
|
||||
<version>1.0-SP4</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-authz-client</artifactId>
|
||||
|
||||
+2
-4
@@ -19,9 +19,8 @@ package org.keycloak.example.photoz.admin;
|
||||
|
||||
import org.keycloak.example.photoz.entity.Album;
|
||||
|
||||
import javax.ejb.Stateless;
|
||||
import javax.inject.Inject;
|
||||
import javax.persistence.EntityManager;
|
||||
import javax.persistence.PersistenceContext;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.Produces;
|
||||
@@ -36,12 +35,11 @@ import java.util.List;
|
||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||
*/
|
||||
@Path("/admin/album")
|
||||
@Stateless
|
||||
public class AdminAlbumService {
|
||||
|
||||
public static final String SCOPE_ADMIN_ALBUM_MANAGE = "urn:photoz.com:scopes:album:admin:manage";
|
||||
|
||||
@PersistenceContext
|
||||
@Inject
|
||||
private EntityManager entityManager;
|
||||
|
||||
@Context
|
||||
|
||||
+4
-2
@@ -7,10 +7,12 @@ import org.keycloak.authorization.client.representation.ScopeRepresentation;
|
||||
import org.keycloak.authorization.client.resource.ProtectionResource;
|
||||
import org.keycloak.example.photoz.ErrorResponse;
|
||||
import org.keycloak.example.photoz.entity.Album;
|
||||
import org.keycloak.example.photoz.util.Transaction;
|
||||
import org.keycloak.representations.adapters.config.AdapterConfig;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
||||
import javax.ejb.Stateless;
|
||||
import javax.inject.Inject;
|
||||
import javax.persistence.EntityManager;
|
||||
import javax.persistence.PersistenceContext;
|
||||
import javax.persistence.Query;
|
||||
@@ -31,14 +33,14 @@ import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
@Path("/album")
|
||||
@Stateless
|
||||
@Transaction
|
||||
public class AlbumService {
|
||||
|
||||
public static final String SCOPE_ALBUM_VIEW = "urn:photoz.com:scopes:album:view";
|
||||
public static final String SCOPE_ALBUM_CREATE = "urn:photoz.com:scopes:album:create";
|
||||
public static final String SCOPE_ALBUM_DELETE = "urn:photoz.com:scopes:album:delete";
|
||||
|
||||
@PersistenceContext
|
||||
@Inject
|
||||
private EntityManager entityManager;
|
||||
|
||||
@Context
|
||||
|
||||
+2
-2
@@ -18,6 +18,7 @@
|
||||
package org.keycloak.example.photoz.album;
|
||||
|
||||
import javax.ejb.Stateless;
|
||||
import javax.inject.Inject;
|
||||
import javax.persistence.EntityManager;
|
||||
import javax.persistence.PersistenceContext;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
@@ -34,12 +35,11 @@ import java.util.List;
|
||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||
*/
|
||||
@Path("/profile")
|
||||
@Stateless
|
||||
public class ProfileService {
|
||||
|
||||
private static final String PROFILE_VIEW = "urn:photoz.com:scopes:profile:view";
|
||||
|
||||
@PersistenceContext
|
||||
@Inject
|
||||
private EntityManager entityManager;
|
||||
|
||||
@GET
|
||||
|
||||
+51
@@ -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.example.photoz.util;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import javax.annotation.PreDestroy;
|
||||
import javax.enterprise.context.ApplicationScoped;
|
||||
import javax.enterprise.context.RequestScoped;
|
||||
import javax.enterprise.inject.Produces;
|
||||
import javax.persistence.EntityManager;
|
||||
import javax.persistence.EntityManagerFactory;
|
||||
import javax.persistence.Persistence;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||
*/
|
||||
@ApplicationScoped
|
||||
public class Resources {
|
||||
|
||||
private EntityManagerFactory entityManagerFactory;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
entityManagerFactory = Persistence.createEntityManagerFactory("primary");
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void dispose() {
|
||||
entityManagerFactory.close();
|
||||
}
|
||||
|
||||
@RequestScoped
|
||||
@Produces
|
||||
public EntityManager createEntityManager() {
|
||||
return entityManagerFactory.createEntityManager();
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 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.example.photoz.util;
|
||||
|
||||
import javax.interceptor.InterceptorBinding;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import static java.lang.annotation.ElementType.METHOD;
|
||||
import static java.lang.annotation.ElementType.TYPE;
|
||||
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||
*/
|
||||
@InterceptorBinding
|
||||
@Target({ TYPE })
|
||||
@Retention(RUNTIME)
|
||||
public @interface Transaction {
|
||||
}
|
||||
+56
@@ -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.example.photoz.util;
|
||||
|
||||
import javax.enterprise.inject.Instance;
|
||||
import javax.inject.Inject;
|
||||
import javax.interceptor.AroundInvoke;
|
||||
import javax.interceptor.Interceptor;
|
||||
import javax.interceptor.InvocationContext;
|
||||
import javax.persistence.EntityManager;
|
||||
import javax.persistence.EntityTransaction;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||
*/
|
||||
@Interceptor
|
||||
@Transaction
|
||||
public class TransactionInterceptor {
|
||||
|
||||
@Inject
|
||||
private Instance<EntityManager> entityManager;
|
||||
|
||||
@AroundInvoke
|
||||
public Object aroundInvoke(InvocationContext context) {
|
||||
EntityManager entityManager = this.entityManager.get();
|
||||
EntityTransaction transaction = entityManager.getTransaction();
|
||||
|
||||
try {
|
||||
transaction.begin();
|
||||
Object proceed = context.proceed();
|
||||
transaction.commit();
|
||||
return proceed;
|
||||
} catch (Exception cause) {
|
||||
if (transaction != null && transaction.isActive()) {
|
||||
transaction.rollback();
|
||||
}
|
||||
throw new RuntimeException(cause);
|
||||
} finally {
|
||||
entityManager.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,5 +3,7 @@
|
||||
xsi:schemaLocation="
|
||||
http://java.sun.com/xml/ns/javaee
|
||||
http://java.sun.com/xml/ns/javaee/beans_1_0.xsd">
|
||||
|
||||
<interceptors>
|
||||
<class>org.keycloak.example.photoz.util.TransactionInterceptor</class>
|
||||
</interceptors>
|
||||
</beans>
|
||||
+7
-3
@@ -4,14 +4,18 @@
|
||||
xsi:schemaLocation="
|
||||
http://java.sun.com/xml/ns/persistence
|
||||
http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
|
||||
<persistence-unit name="primary">
|
||||
<non-jta-data-source>java:jboss/datasources/PhotozDS</non-jta-data-source>
|
||||
<persistence-unit name="primary" transaction-type="RESOURCE_LOCAL">
|
||||
<provider>org.hibernate.ejb.HibernatePersistence</provider>
|
||||
|
||||
<class>org.keycloak.example.photoz.entity.Album</class>
|
||||
<class>org.keycloak.example.photoz.entity.Photo</class>
|
||||
|
||||
<properties>
|
||||
<property name="hibernate.dialect" value="org.hibernate.dialect.MySQLDialect"/>
|
||||
<property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect" />
|
||||
<property name="hibernate.connection.driver_class" value="org.h2.Driver" />
|
||||
<property name="hibernate.connection.url" value="jdbc:h2:~/keycloak-photoz-example" />
|
||||
<property name="hibernate.connection.user" value="sa" />
|
||||
<property name="hibernate.flushMode" value="FLUSH_AUTO" />
|
||||
<property name="hibernate.hbm2ddl.auto" value="update" />
|
||||
<property name="hibernate.show_sql" value="false" />
|
||||
</properties>
|
||||
|
||||
+1
@@ -20,6 +20,7 @@
|
||||
<deployment>
|
||||
<dependencies>
|
||||
<module name="org.keycloak.keycloak-authz-client" services="import"/>
|
||||
<module name="com.h2database.h2" services="import"/>
|
||||
</dependencies>
|
||||
</deployment>
|
||||
</jboss-deployment-structure>
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
<datasources xmlns="http://www.jboss.org/ironjacamar/schema"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.jboss.org/ironjacamar/schema http://docs.jboss.org/ironjacamar/schema/datasources_1_0.xsd">
|
||||
<datasource jndi-name="java:jboss/datasources/PhotozDS" pool-name="PhotozDS" enabled="true" use-java-context="true">
|
||||
<connection-url>jdbc:h2:${jboss.server.data.dir}/kc-authz-photo;AUTO_SERVER=TRUE</connection-url>
|
||||
<driver>h2</driver>
|
||||
<security>
|
||||
<user-name>sa</user-name>
|
||||
<password>sa</password>
|
||||
</security>
|
||||
</datasource>
|
||||
</datasources>
|
||||
@@ -3,7 +3,7 @@
|
||||
What is it?
|
||||
-----------
|
||||
|
||||
This example demonstrates how to use Social Ientity Providers with Keycloak to authenticate users. In this case,
|
||||
This example demonstrates how to use Social Identity Providers with Keycloak to authenticate users. In this case,
|
||||
users are authenticated with Google using Keycloak Identity Broker capabilities using the oAuth 2 protocol.
|
||||
|
||||
From this example, you'll learn how to:
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
What is it?
|
||||
-----------
|
||||
|
||||
This example demonstrates how to use Social Ientity Providers with Keycloak to authenticate users. In this case,
|
||||
This example demonstrates how to use Social Identity Providers with Keycloak to authenticate users. In this case,
|
||||
users are authenticated with Twitter using Keycloak Identity Broker capabilities using the oAuth 2 protocol.
|
||||
|
||||
From this example, you'll learn how to:
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
# Changing the Default *keycloak-subsystem* Configuration
|
||||
|
||||
If you need to make a change to the default keycloak-subsystem
|
||||
configuration that is packaged with our distributions, you will need to edit this file:
|
||||
https://github.com/keycloak/keycloak/blob/master/wildfly/server-subsystem/src/main/config/default-server-subsys-config.properties
|
||||
|
||||
This file contains a single multi-line property containing the subsystem
|
||||
xml declaration. Maven filtering is used to read this property and
|
||||
inject it everywhere it needs to go. Editing this file will also take
|
||||
care of propagating it to the distributions like server-dist and demo-dist.
|
||||
|
||||
Also, you need to create CLI commands for each change by editing this file:
|
||||
https://github.com/keycloak/keycloak/blob/master/wildfly/server-subsystem/src/main/resources/cli/default-keycloak-subsys-config.cli
|
||||
|
||||
This CLI snippet is used in the scripts required by the overlay distribution.
|
||||
|
||||
## Updating an SPI
|
||||
The changes you will likely make are when you need to add a new SPI, change an existing SPI, or add/change a provider within an SPI.
|
||||
|
||||
All elements in an SPI declaration are optional, but a full SPI declaration
|
||||
looks like this:
|
||||
````xml
|
||||
<spi name="dblock">
|
||||
<default-provider>mongo</default-provider>
|
||||
<provider name="jpa" enabled="true">
|
||||
<properties>
|
||||
<property name="lockWaitTimeout" value="800"/>
|
||||
</properties>
|
||||
</provider>
|
||||
<provider name="mongo" enabled="true">
|
||||
<properties>
|
||||
<property name="lockRecheckTime" value="2"/>
|
||||
<property name="lockWaitTimeout" value="600"/>
|
||||
</properties>
|
||||
</provider>
|
||||
</spi>
|
||||
````
|
||||
Here we have two providers defined for the SPI `dblock`. The
|
||||
`default-provider` is listed as `mongo`. However it is up to the SPI to decide how it will
|
||||
treat this setting. Some SPIs allow more than one provider and some do not. So
|
||||
`default-provider` can help the SPI to choose.
|
||||
|
||||
Also notice that each provider defines its own set of configuration
|
||||
properties. The fact that both providers above have a property called
|
||||
`lockWaitTimeout` is just a coincidence.
|
||||
|
||||
## Values of type *List*
|
||||
The type of each property value is interpreted by the provider. However,
|
||||
there is one exception. Consider the `jpa` provider for the `eventStore` API:
|
||||
````xml
|
||||
<spi name="eventsStore">
|
||||
<provider name="jpa" enabled="true">
|
||||
<properties>
|
||||
<property name="exclude-events" value="["EVENT1","EVENT2"]"/>
|
||||
</properties>
|
||||
</provider>
|
||||
</spi>
|
||||
````
|
||||
We see that the value begins and ends with square brackets. That means that
|
||||
the value will be passed to the provider as a list. In this example,
|
||||
the system will pass the
|
||||
provider a list with two element values `EVENT1` and `EVENT2`. To add
|
||||
more values to the list, just separate each list element with a comma. Unfortunately,
|
||||
you do need to escape the quotes surrounding each list element with
|
||||
`"`.
|
||||
+100
-69
@@ -17,6 +17,7 @@
|
||||
|
||||
package org.keycloak.connections.jpa;
|
||||
|
||||
import java.io.File;
|
||||
import java.sql.Connection;
|
||||
import java.sql.DatabaseMetaData;
|
||||
import java.sql.DriverManager;
|
||||
@@ -42,6 +43,7 @@ import org.keycloak.models.dblock.DBLockProvider;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.provider.ServerInfoAwareProviderFactory;
|
||||
import org.keycloak.models.dblock.DBLockManager;
|
||||
import org.keycloak.ServerStartupError;
|
||||
import org.keycloak.timer.TimerProvider;
|
||||
|
||||
/**
|
||||
@@ -51,6 +53,10 @@ public class DefaultJpaConnectionProviderFactory implements JpaConnectionProvide
|
||||
|
||||
private static final Logger logger = Logger.getLogger(DefaultJpaConnectionProviderFactory.class);
|
||||
|
||||
enum MigrationStrategy {
|
||||
UPDATE, VALIDATE, MANUAL
|
||||
}
|
||||
|
||||
private volatile EntityManagerFactory emf;
|
||||
|
||||
private Config.Scope config;
|
||||
@@ -125,22 +131,9 @@ public class DefaultJpaConnectionProviderFactory implements JpaConnectionProvide
|
||||
properties.put(JpaUtils.HIBERNATE_DEFAULT_SCHEMA, schema);
|
||||
}
|
||||
|
||||
|
||||
String databaseSchema;
|
||||
String databaseSchemaConf = config.get("databaseSchema");
|
||||
if (databaseSchemaConf == null) {
|
||||
throw new RuntimeException("Property 'databaseSchema' needs to be specified in the configuration");
|
||||
}
|
||||
|
||||
if (databaseSchemaConf.equals("development-update")) {
|
||||
properties.put("hibernate.hbm2ddl.auto", "update");
|
||||
databaseSchema = null;
|
||||
} else if (databaseSchemaConf.equals("development-validate")) {
|
||||
properties.put("hibernate.hbm2ddl.auto", "validate");
|
||||
databaseSchema = null;
|
||||
} else {
|
||||
databaseSchema = databaseSchemaConf;
|
||||
}
|
||||
MigrationStrategy migrationStrategy = getMigrationStrategy();
|
||||
boolean initializeEmpty = config.getBoolean("initializeEmpty", true);
|
||||
File databaseUpdateFile = getDatabaseUpdateFile();
|
||||
|
||||
properties.put("hibernate.show_sql", config.getBoolean("showSql", false));
|
||||
properties.put("hibernate.format_sql", config.getBoolean("formatSql", true));
|
||||
@@ -154,38 +147,7 @@ public class DefaultJpaConnectionProviderFactory implements JpaConnectionProvide
|
||||
properties.put("hibernate.dialect", driverDialect);
|
||||
}
|
||||
|
||||
if (databaseSchema != null) {
|
||||
logger.trace("Updating database");
|
||||
|
||||
JpaUpdaterProvider updater = session.getProvider(JpaUpdaterProvider.class);
|
||||
if (updater == null) {
|
||||
throw new RuntimeException("Can't update database: JPA updater provider not found");
|
||||
}
|
||||
|
||||
// Check if having DBLock before trying to initialize hibernate
|
||||
DBLockProvider dbLock = new DBLockManager(session).getDBLock();
|
||||
if (dbLock.hasLock()) {
|
||||
updateOrValidateDB(databaseSchema, connection, updater, schema);
|
||||
} else {
|
||||
logger.trace("Don't have DBLock retrieved before upgrade. Needs to acquire lock first in separate transaction");
|
||||
|
||||
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), new KeycloakSessionTask() {
|
||||
|
||||
@Override
|
||||
public void run(KeycloakSession lockSession) {
|
||||
DBLockManager dbLockManager = new DBLockManager(lockSession);
|
||||
DBLockProvider dbLock2 = dbLockManager.getDBLock();
|
||||
dbLock2.waitForLock();
|
||||
try {
|
||||
updateOrValidateDB(databaseSchema, connection, updater, schema);
|
||||
} finally {
|
||||
dbLock2.releaseLock();
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
migration(migrationStrategy, initializeEmpty, schema, databaseUpdateFile, connection, session);
|
||||
|
||||
int globalStatsInterval = config.getInt("globalStatsInterval", -1);
|
||||
if (globalStatsInterval != -1) {
|
||||
@@ -199,18 +161,6 @@ public class DefaultJpaConnectionProviderFactory implements JpaConnectionProvide
|
||||
if (globalStatsInterval != -1) {
|
||||
startGlobalStats(session, globalStatsInterval);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
// Safe rollback
|
||||
if (connection != null) {
|
||||
try {
|
||||
connection.rollback();
|
||||
} catch (SQLException e2) {
|
||||
logger.warn("Can't rollback connection", e2);
|
||||
}
|
||||
}
|
||||
|
||||
throw e;
|
||||
} finally {
|
||||
// Close after creating EntityManagerFactory to prevent in-mem databases from closing
|
||||
if (connection != null) {
|
||||
@@ -226,6 +176,11 @@ public class DefaultJpaConnectionProviderFactory implements JpaConnectionProvide
|
||||
}
|
||||
}
|
||||
|
||||
private File getDatabaseUpdateFile() {
|
||||
String databaseUpdateFile = config.get("migrationExport", "keycloak-database-update.sql");
|
||||
return new File(databaseUpdateFile);
|
||||
}
|
||||
|
||||
protected void prepareOperationalInfo(Connection connection) {
|
||||
try {
|
||||
operationalInfo = new LinkedHashMap<>();
|
||||
@@ -282,20 +237,82 @@ public class DefaultJpaConnectionProviderFactory implements JpaConnectionProvide
|
||||
timer.scheduleTask(new HibernateStatsReporter(emf), globalStatsIntervalSecs * 1000, "ReportHibernateGlobalStats");
|
||||
}
|
||||
|
||||
public void migration(MigrationStrategy strategy, boolean initializeEmpty, String schema, File databaseUpdateFile, Connection connection, KeycloakSession session) {
|
||||
JpaUpdaterProvider updater = session.getProvider(JpaUpdaterProvider.class);
|
||||
|
||||
// Needs to be called with acquired DBLock
|
||||
protected void updateOrValidateDB(String databaseSchema, Connection connection, JpaUpdaterProvider updater, String schema) {
|
||||
if (databaseSchema.equals("update")) {
|
||||
updater.update(connection, schema);
|
||||
logger.trace("Database update completed");
|
||||
} else if (databaseSchema.equals("validate")) {
|
||||
updater.validate(connection, schema);
|
||||
logger.trace("Database validation completed");
|
||||
JpaUpdaterProvider.Status status = updater.validate(connection, schema);
|
||||
if (status == JpaUpdaterProvider.Status.VALID) {
|
||||
logger.debug("Database is up-to-date");
|
||||
} else if (status == JpaUpdaterProvider.Status.EMPTY) {
|
||||
if (initializeEmpty) {
|
||||
update(connection, schema, session, updater);
|
||||
} else {
|
||||
switch (strategy) {
|
||||
case UPDATE:
|
||||
update(connection, schema, session, updater);
|
||||
break;
|
||||
case MANUAL:
|
||||
export(connection, schema, databaseUpdateFile, session, updater);
|
||||
throw new ServerStartupError("Database not initialized, please initialize database with " + databaseUpdateFile.getAbsolutePath(), false);
|
||||
case VALIDATE:
|
||||
throw new ServerStartupError("Database not initialized, please enable database initialization", false);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new RuntimeException("Invalid value for databaseSchema: " + databaseSchema);
|
||||
switch (strategy) {
|
||||
case UPDATE:
|
||||
update(connection, schema, session, updater);
|
||||
break;
|
||||
case MANUAL:
|
||||
export(connection, schema, databaseUpdateFile, session, updater);
|
||||
throw new ServerStartupError("Database not up-to-date, please migrate database with " + databaseUpdateFile.getAbsolutePath(), false);
|
||||
case VALIDATE:
|
||||
throw new ServerStartupError("Database not up-to-date, please enable database migration", false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void update(Connection connection, String schema, KeycloakSession session, JpaUpdaterProvider updater) {
|
||||
DBLockProvider dbLock = new DBLockManager(session).getDBLock();
|
||||
if (dbLock.hasLock()) {
|
||||
updater.update(connection, schema);
|
||||
} else {
|
||||
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), new KeycloakSessionTask() {
|
||||
@Override
|
||||
public void run(KeycloakSession lockSession) {
|
||||
DBLockManager dbLockManager = new DBLockManager(lockSession);
|
||||
DBLockProvider dbLock2 = dbLockManager.getDBLock();
|
||||
dbLock2.waitForLock();
|
||||
try {
|
||||
updater.update(connection, schema);
|
||||
} finally {
|
||||
dbLock2.releaseLock();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected void export(Connection connection, String schema, File databaseUpdateFile, KeycloakSession session, JpaUpdaterProvider updater) {
|
||||
DBLockProvider dbLock = new DBLockManager(session).getDBLock();
|
||||
if (dbLock.hasLock()) {
|
||||
updater.export(connection, schema, databaseUpdateFile);
|
||||
} else {
|
||||
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), new KeycloakSessionTask() {
|
||||
@Override
|
||||
public void run(KeycloakSession lockSession) {
|
||||
DBLockManager dbLockManager = new DBLockManager(lockSession);
|
||||
DBLockProvider dbLock2 = dbLockManager.getDBLock();
|
||||
dbLock2.waitForLock();
|
||||
try {
|
||||
updater.export(connection, schema, databaseUpdateFile);
|
||||
} finally {
|
||||
dbLock2.releaseLock();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Connection getConnection() {
|
||||
@@ -323,4 +340,18 @@ public class DefaultJpaConnectionProviderFactory implements JpaConnectionProvide
|
||||
return operationalInfo;
|
||||
}
|
||||
|
||||
private MigrationStrategy getMigrationStrategy() {
|
||||
String migrationStrategy = config.get("migrationStrategy");
|
||||
if (migrationStrategy == null) {
|
||||
// Support 'databaseSchema' for backwards compatibility
|
||||
migrationStrategy = config.get("databaseSchema");
|
||||
}
|
||||
|
||||
if (migrationStrategy != null) {
|
||||
return MigrationStrategy.valueOf(migrationStrategy.toUpperCase());
|
||||
} else {
|
||||
return MigrationStrategy.UPDATE;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+8
-3
@@ -19,6 +19,7 @@ package org.keycloak.connections.jpa.updater;
|
||||
|
||||
import org.keycloak.provider.Provider;
|
||||
|
||||
import java.io.File;
|
||||
import java.sql.Connection;
|
||||
|
||||
/**
|
||||
@@ -26,10 +27,14 @@ import java.sql.Connection;
|
||||
*/
|
||||
public interface JpaUpdaterProvider extends Provider {
|
||||
|
||||
public String FIRST_VERSION = "1.0.0.Final";
|
||||
enum Status {
|
||||
VALID, EMPTY, OUTDATED
|
||||
}
|
||||
|
||||
public void update(Connection connection, String defaultSchema);
|
||||
void update(Connection connection, String defaultSchema);
|
||||
|
||||
public void validate(Connection connection, String defaultSchema);
|
||||
Status validate(Connection connection, String defaultSchema);
|
||||
|
||||
void export(Connection connection, String defaultSchema, File file);
|
||||
|
||||
}
|
||||
+42
-13
@@ -30,6 +30,9 @@ import org.keycloak.connections.jpa.updater.liquibase.conn.LiquibaseConnectionPr
|
||||
import org.keycloak.connections.jpa.util.JpaUtils;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.sql.Connection;
|
||||
import java.util.List;
|
||||
@@ -53,6 +56,15 @@ public class LiquibaseJpaUpdaterProvider implements JpaUpdaterProvider {
|
||||
|
||||
@Override
|
||||
public void update(Connection connection, String defaultSchema) {
|
||||
update(connection, null, defaultSchema);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void export(Connection connection, String defaultSchema, File file) {
|
||||
update(connection, file, defaultSchema);
|
||||
}
|
||||
|
||||
private void update(Connection connection, File file, String defaultSchema) {
|
||||
logger.debug("Starting database update");
|
||||
|
||||
// Need ThreadLocal as liquibase doesn't seem to have API to inject custom objects into tasks
|
||||
@@ -61,7 +73,7 @@ public class LiquibaseJpaUpdaterProvider implements JpaUpdaterProvider {
|
||||
try {
|
||||
// Run update with keycloak master changelog first
|
||||
Liquibase liquibase = getLiquibaseForKeycloakUpdate(connection, defaultSchema);
|
||||
updateChangeSet(liquibase, liquibase.getChangeLogFile());
|
||||
updateChangeSet(liquibase, liquibase.getChangeLogFile(), file);
|
||||
|
||||
// Run update for each custom JpaEntityProvider
|
||||
Set<JpaEntityProvider> jpaProviders = session.getAllProviders(JpaEntityProvider.class);
|
||||
@@ -71,7 +83,7 @@ public class LiquibaseJpaUpdaterProvider implements JpaUpdaterProvider {
|
||||
String factoryId = jpaProvider.getFactoryId();
|
||||
String changelogTableName = JpaUtils.getCustomChangelogTableName(factoryId);
|
||||
liquibase = getLiquibaseForCustomProviderUpdate(connection, defaultSchema, customChangelog, jpaProvider.getClass().getClassLoader(), changelogTableName);
|
||||
updateChangeSet(liquibase, liquibase.getChangeLogFile());
|
||||
updateChangeSet(liquibase, liquibase.getChangeLogFile(), file);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
@@ -81,7 +93,8 @@ public class LiquibaseJpaUpdaterProvider implements JpaUpdaterProvider {
|
||||
}
|
||||
}
|
||||
|
||||
protected void updateChangeSet(Liquibase liquibase, String changelog) throws LiquibaseException {
|
||||
|
||||
protected void updateChangeSet(Liquibase liquibase, String changelog, File exportFile) throws LiquibaseException, IOException {
|
||||
List<ChangeSet> changeSets = liquibase.listUnrunChangeSets((Contexts) null);
|
||||
if (!changeSets.isEmpty()) {
|
||||
List<RanChangeSet> ranChangeSets = liquibase.getDatabase().getRanChangeSetList();
|
||||
@@ -95,7 +108,12 @@ public class LiquibaseJpaUpdaterProvider implements JpaUpdaterProvider {
|
||||
}
|
||||
}
|
||||
|
||||
liquibase.update((Contexts) null);
|
||||
if (exportFile != null) {
|
||||
liquibase.update((Contexts) null, new FileWriter(exportFile));
|
||||
} else {
|
||||
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);
|
||||
@@ -107,13 +125,18 @@ public class LiquibaseJpaUpdaterProvider implements JpaUpdaterProvider {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate(Connection connection, String defaultSchema) {
|
||||
public Status validate(Connection connection, String defaultSchema) {
|
||||
logger.debug("Validating if database is updated");
|
||||
ThreadLocalSessionContext.setCurrentSession(session);
|
||||
|
||||
try {
|
||||
// Validate with keycloak master changelog first
|
||||
Liquibase liquibase = getLiquibaseForKeycloakUpdate(connection, defaultSchema);
|
||||
validateChangeSet(liquibase, liquibase.getChangeLogFile());
|
||||
|
||||
Status status = validateChangeSet(liquibase, liquibase.getChangeLogFile());
|
||||
if (status != Status.VALID) {
|
||||
return status;
|
||||
}
|
||||
|
||||
// Validate each custom JpaEntityProvider
|
||||
Set<JpaEntityProvider> jpaProviders = session.getAllProviders(JpaEntityProvider.class);
|
||||
@@ -123,24 +146,30 @@ public class LiquibaseJpaUpdaterProvider implements JpaUpdaterProvider {
|
||||
String factoryId = jpaProvider.getFactoryId();
|
||||
String changelogTableName = JpaUtils.getCustomChangelogTableName(factoryId);
|
||||
liquibase = getLiquibaseForCustomProviderUpdate(connection, defaultSchema, customChangelog, jpaProvider.getClass().getClassLoader(), changelogTableName);
|
||||
validateChangeSet(liquibase, liquibase.getChangeLogFile());
|
||||
if (validateChangeSet(liquibase, liquibase.getChangeLogFile()) != Status.VALID) {
|
||||
return Status.OUTDATED;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (LiquibaseException e) {
|
||||
throw new RuntimeException("Failed to validate database", e);
|
||||
}
|
||||
|
||||
return Status.VALID;
|
||||
}
|
||||
|
||||
protected void validateChangeSet(Liquibase liquibase, String changelog) throws LiquibaseException {
|
||||
protected Status 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);
|
||||
if (changeSets.size() == liquibase.getDatabaseChangeLog().getChangeSets().size()) {
|
||||
return Status.EMPTY;
|
||||
} else {
|
||||
logger.debugf("Validation failed. Database is not up-to-date for changelog %s", changelog);
|
||||
return Status.OUTDATED;
|
||||
}
|
||||
} else {
|
||||
logger.debugf("Validation passed. Database is up-to-date for changelog %s", changelog);
|
||||
return Status.VALID;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -498,6 +498,18 @@ public class ClientAdapter implements ClientModel, JpaModel<ClientEntity> {
|
||||
@Override
|
||||
public void updateClient() {
|
||||
em.flush();
|
||||
session.getKeycloakSessionFactory().publish(new RealmModel.ClientUpdatedEvent() {
|
||||
|
||||
@Override
|
||||
public ClientModel getUpdatedClient() {
|
||||
return ClientAdapter.this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeycloakSession getKeycloakSession() {
|
||||
return session;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+48
-41
@@ -51,6 +51,10 @@ import com.mongodb.ServerAddress;
|
||||
*/
|
||||
public class DefaultMongoConnectionFactoryProvider implements MongoConnectionProviderFactory, ServerInfoAwareProviderFactory {
|
||||
|
||||
enum MigrationStrategy {
|
||||
UPDATE, VALIDATE
|
||||
}
|
||||
|
||||
// TODO Make it dynamic
|
||||
private String[] entities = new String[]{
|
||||
"org.keycloak.models.mongo.keycloak.entities.MongoRealmEntity",
|
||||
@@ -165,46 +169,34 @@ public class DefaultMongoConnectionFactoryProvider implements MongoConnectionPro
|
||||
}
|
||||
|
||||
private void update(KeycloakSession session) {
|
||||
String databaseSchema = config.get("databaseSchema");
|
||||
MigrationStrategy strategy = getMigrationStrategy();
|
||||
|
||||
if (databaseSchema == null) {
|
||||
throw new RuntimeException("Property 'databaseSchema' needs to be specified in the configuration of mongo connections");
|
||||
MongoUpdaterProvider mongoUpdater = session.getProvider(MongoUpdaterProvider.class);
|
||||
if (mongoUpdater == null) {
|
||||
throw new RuntimeException("Can't update database: Mongo updater provider not found");
|
||||
}
|
||||
|
||||
DBLockProvider dbLock = new DBLockManager(session).getDBLock();
|
||||
if (dbLock.hasLock()) {
|
||||
updateOrValidateDB(strategy, session, mongoUpdater);
|
||||
} else {
|
||||
MongoUpdaterProvider mongoUpdater = session.getProvider(MongoUpdaterProvider.class);
|
||||
if (mongoUpdater == null) {
|
||||
throw new RuntimeException("Can't update database: Mongo updater provider not found");
|
||||
}
|
||||
logger.trace("Don't have DBLock retrieved before upgrade. Needs to acquire lock first in separate transaction");
|
||||
|
||||
DBLockProvider dbLock = new DBLockManager(session).getDBLock();
|
||||
if (dbLock.hasLock()) {
|
||||
updateOrValidateDB(databaseSchema, session, mongoUpdater);
|
||||
} else {
|
||||
logger.trace("Don't have DBLock retrieved before upgrade. Needs to acquire lock first in separate transaction");
|
||||
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), new KeycloakSessionTask() {
|
||||
|
||||
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), new KeycloakSessionTask() {
|
||||
|
||||
@Override
|
||||
public void run(KeycloakSession lockSession) {
|
||||
DBLockManager dbLockManager = new DBLockManager(lockSession);
|
||||
DBLockProvider dbLock2 = dbLockManager.getDBLock();
|
||||
dbLock2.waitForLock();
|
||||
try {
|
||||
updateOrValidateDB(databaseSchema, session, mongoUpdater);
|
||||
} finally {
|
||||
dbLock2.releaseLock();
|
||||
}
|
||||
@Override
|
||||
public void run(KeycloakSession lockSession) {
|
||||
DBLockManager dbLockManager = new DBLockManager(lockSession);
|
||||
DBLockProvider dbLock2 = dbLockManager.getDBLock();
|
||||
dbLock2.waitForLock();
|
||||
try {
|
||||
updateOrValidateDB(strategy, session, mongoUpdater);
|
||||
} finally {
|
||||
dbLock2.releaseLock();
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
if (databaseSchema.equals("update")) {
|
||||
mongoUpdater.update(session, db);
|
||||
} else if (databaseSchema.equals("validate")) {
|
||||
mongoUpdater.validate(session, db);
|
||||
} else {
|
||||
throw new RuntimeException("Invalid value for databaseSchema: " + databaseSchema);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,13 +209,14 @@ public class DefaultMongoConnectionFactoryProvider implements MongoConnectionPro
|
||||
return entityClasses;
|
||||
}
|
||||
|
||||
protected void updateOrValidateDB(String databaseSchema, KeycloakSession session, MongoUpdaterProvider mongoUpdater) {
|
||||
if (databaseSchema.equals("update")) {
|
||||
mongoUpdater.update(session, db);
|
||||
} else if (databaseSchema.equals("validate")) {
|
||||
mongoUpdater.validate(session, db);
|
||||
} else {
|
||||
throw new RuntimeException("Invalid value for databaseSchema: " + databaseSchema);
|
||||
protected void updateOrValidateDB(MigrationStrategy strategy, KeycloakSession session, MongoUpdaterProvider mongoUpdater) {
|
||||
switch (strategy) {
|
||||
case UPDATE:
|
||||
mongoUpdater.update(session, db);
|
||||
break;
|
||||
case VALIDATE:
|
||||
mongoUpdater.validate(session, db);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -345,4 +338,18 @@ public class DefaultMongoConnectionFactoryProvider implements MongoConnectionPro
|
||||
return operationalInfo;
|
||||
}
|
||||
|
||||
private MigrationStrategy getMigrationStrategy() {
|
||||
String migrationStrategy = config.get("migrationStrategy");
|
||||
if (migrationStrategy == null) {
|
||||
// Support 'databaseSchema' for backwards compatibility
|
||||
migrationStrategy = config.get("databaseSchema");
|
||||
}
|
||||
|
||||
if (migrationStrategy != null) {
|
||||
return MigrationStrategy.valueOf(migrationStrategy.toUpperCase());
|
||||
} else {
|
||||
return MigrationStrategy.UPDATE;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+13
-2
@@ -17,8 +17,6 @@
|
||||
|
||||
package org.keycloak.models.mongo.keycloak.adapters;
|
||||
|
||||
import com.mongodb.DBObject;
|
||||
import com.mongodb.QueryBuilder;
|
||||
import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.ClientTemplateModel;
|
||||
@@ -65,6 +63,19 @@ public class ClientAdapter extends AbstractMongoAdapter<MongoClientEntity> imple
|
||||
@Override
|
||||
public void updateClient() {
|
||||
updateMongoEntity();
|
||||
|
||||
session.getKeycloakSessionFactory().publish(new RealmModel.ClientUpdatedEvent() {
|
||||
|
||||
@Override
|
||||
public ClientModel getUpdatedClient() {
|
||||
return ClientAdapter.this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeycloakSession getKeycloakSession() {
|
||||
return session;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Non-recoverable error thrown during server startup
|
||||
*
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
public class ServerStartupError extends Error {
|
||||
|
||||
private final boolean fillStackTrace;
|
||||
|
||||
public ServerStartupError(String message) {
|
||||
super(message);
|
||||
fillStackTrace = true;
|
||||
}
|
||||
|
||||
public ServerStartupError(String message, boolean fillStackTrace) {
|
||||
super(message);
|
||||
this.fillStackTrace = fillStackTrace;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized Throwable fillInStackTrace() {
|
||||
if (fillStackTrace) {
|
||||
return super.fillInStackTrace();
|
||||
} else {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+1
-1
@@ -50,7 +50,7 @@ public abstract class AbstractIdentityProviderFactory<T extends IdentityProvider
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> parseConfig(InputStream inputStream) {
|
||||
public Map<String, String> parseConfig(KeycloakSession session, InputStream inputStream) {
|
||||
return new HashMap<String, String>();
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@
|
||||
package org.keycloak.broker.provider;
|
||||
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
|
||||
import java.io.InputStream;
|
||||
@@ -47,8 +48,9 @@ public interface IdentityProviderFactory<T extends IdentityProvider> extends Pro
|
||||
* <p>Creates an {@link IdentityProvider} based on the configuration from
|
||||
* <code>inputStream</code>.</p>
|
||||
*
|
||||
* @param session
|
||||
* @param inputStream The input stream from where configuration will be loaded from..
|
||||
* @return
|
||||
*/
|
||||
Map<String, String> parseConfig(InputStream inputStream);
|
||||
Map<String, String> parseConfig(KeycloakSession session, InputStream inputStream);
|
||||
}
|
||||
@@ -58,4 +58,7 @@ public interface Details {
|
||||
|
||||
String CLIENT_AUTH_METHOD = "client_auth_method";
|
||||
|
||||
String SIGNATURE_REQUIRED = "signature_required";
|
||||
String SIGNATURE_ALGORITHM = "signature_algorithm";
|
||||
|
||||
}
|
||||
@@ -49,8 +49,8 @@ public class MigrationUtils {
|
||||
|
||||
public static void updateOTPRequiredAction(RequiredActionProviderModel otpAction) {
|
||||
if (otpAction == null) return;
|
||||
if (!otpAction.getProviderId().equals(UserModel.RequiredAction.CONFIGURE_TOTP.name())) return;
|
||||
if (!otpAction.getName().equals("Configure Totp")) return;
|
||||
if (!UserModel.RequiredAction.CONFIGURE_TOTP.name().equals(otpAction.getProviderId())) return;
|
||||
if (!"Configure Totp".equals(otpAction.getName())) return;
|
||||
|
||||
otpAction.setName("Configure OTP");
|
||||
}
|
||||
|
||||
@@ -56,6 +56,12 @@ public interface RealmModel extends RoleContainerModel {
|
||||
ClientModel getCreatedClient();
|
||||
}
|
||||
|
||||
// Called also during client creation after client is fully initialized (including all attributes etc)
|
||||
interface ClientUpdatedEvent extends ProviderEvent {
|
||||
ClientModel getUpdatedClient();
|
||||
KeycloakSession getKeycloakSession();
|
||||
}
|
||||
|
||||
interface ClientRemovedEvent extends ProviderEvent {
|
||||
ClientModel getClient();
|
||||
KeycloakSession getKeycloakSession();
|
||||
|
||||
@@ -943,7 +943,6 @@ public class RepresentationToModel {
|
||||
} else {
|
||||
client.setNodeReRegistrationTimeout(-1);
|
||||
}
|
||||
client.updateClient();
|
||||
|
||||
if (resourceRep.getNotBefore() != null) {
|
||||
client.setNotBefore(resourceRep.getNotBefore());
|
||||
@@ -1043,6 +1042,8 @@ public class RepresentationToModel {
|
||||
if (resourceRep.isUseTemplateMappers() != null) client.setUseTemplateMappers(resourceRep.isUseTemplateMappers());
|
||||
else client.setUseTemplateMappers(resourceRep.getClientTemplate() != null);
|
||||
|
||||
client.updateClient();
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
@@ -1066,7 +1067,6 @@ public class RepresentationToModel {
|
||||
if (rep.isSurrogateAuthRequired() != null) resource.setSurrogateAuthRequired(rep.isSurrogateAuthRequired());
|
||||
if (rep.getNodeReRegistrationTimeout() != null) resource.setNodeReRegistrationTimeout(rep.getNodeReRegistrationTimeout());
|
||||
if (rep.getClientAuthenticatorType() != null) resource.setClientAuthenticatorType(rep.getClientAuthenticatorType());
|
||||
resource.updateClient();
|
||||
|
||||
if (rep.getProtocol() != null) resource.setProtocol(rep.getProtocol());
|
||||
if (rep.getAttributes() != null) {
|
||||
@@ -1122,7 +1122,7 @@ public class RepresentationToModel {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
resource.updateClient();
|
||||
}
|
||||
|
||||
// CLIENT TEMPLATES
|
||||
|
||||
@@ -72,6 +72,11 @@
|
||||
<artifactId>twitter4j-core</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.wildfly.core</groupId>
|
||||
<artifactId>wildfly-controller</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jboss.logging</groupId>
|
||||
<artifactId>jboss-logging</artifactId>
|
||||
|
||||
+12
-25
@@ -30,6 +30,7 @@ import java.util.Set;
|
||||
|
||||
import javax.ws.rs.core.MultivaluedMap;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.UriBuilder;
|
||||
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.authentication.AuthenticationFlowError;
|
||||
@@ -39,9 +40,11 @@ import org.keycloak.jose.jws.JWSInput;
|
||||
import org.keycloak.jose.jws.crypto.RSAProvider;
|
||||
import org.keycloak.models.AuthenticationExecutionModel;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.ModelException;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
import org.keycloak.representations.JsonWebToken;
|
||||
import org.keycloak.representations.idm.CertificateRepresentation;
|
||||
@@ -139,10 +142,11 @@ public class JWTClientAuthenticator extends AbstractClientAuthenticator {
|
||||
throw new RuntimeException("Signature on JWT token failed validation");
|
||||
}
|
||||
|
||||
// Validate other things
|
||||
String expectedAudience = Urls.realmIssuer(context.getUriInfo().getBaseUri(), realm.getName());
|
||||
if (!token.hasAudience(expectedAudience)) {
|
||||
throw new RuntimeException("Token audience doesn't match domain. Realm audience is '" + expectedAudience + "' but audience from token is '" + Arrays.asList(token.getAudience()).toString() + "'");
|
||||
// Allow both "issuer" or "token-endpoint" as audience
|
||||
String issuerUrl = Urls.realmIssuer(context.getUriInfo().getBaseUri(), realm.getName());
|
||||
String tokenUrl = OIDCLoginProtocolService.tokenUrl(context.getUriInfo().getBaseUriBuilder()).build(realm.getName()).toString();
|
||||
if (!token.hasAudience(issuerUrl) && !token.hasAudience(tokenUrl)) {
|
||||
throw new RuntimeException("Token audience doesn't match domain. Realm issuer is '" + issuerUrl + "' but audience from token is '" + Arrays.asList(token.getAudience()).toString() + "'");
|
||||
}
|
||||
|
||||
if (!token.isActive()) {
|
||||
@@ -163,30 +167,13 @@ public class JWTClientAuthenticator extends AbstractClientAuthenticator {
|
||||
}
|
||||
|
||||
protected PublicKey getSignatureValidationKey(ClientModel client, ClientAuthenticationFlowContext context) {
|
||||
CertificateRepresentation certInfo = CertificateInfoHelper.getCertificateFromClient(client, ATTR_PREFIX);
|
||||
|
||||
String encodedCertificate = certInfo.getCertificate();
|
||||
String encodedPublicKey = certInfo.getPublicKey();
|
||||
|
||||
if (encodedCertificate == null && encodedPublicKey == null) {
|
||||
Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "unauthorized_client", "Client '" + client.getClientId() + "' doesn't have certificate or publicKey configured");
|
||||
try {
|
||||
return CertificateInfoHelper.getSignatureValidationKey(client, ATTR_PREFIX);
|
||||
} catch (ModelException me) {
|
||||
Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "unauthorized_client", me.getMessage());
|
||||
context.failure(AuthenticationFlowError.CLIENT_CREDENTIALS_SETUP_REQUIRED, challengeResponse);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (encodedCertificate != null && encodedPublicKey != null) {
|
||||
Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "unauthorized_client", "Client '" + client.getClientId() + "' has both publicKey and certificate configured");
|
||||
context.failure(AuthenticationFlowError.CLIENT_CREDENTIALS_SETUP_REQUIRED, challengeResponse);
|
||||
return null;
|
||||
}
|
||||
|
||||
// TODO: Caching of publicKeys / certificates, so it doesn't need to be always computed from pem. For performance reasons...
|
||||
if (encodedCertificate != null) {
|
||||
X509Certificate clientCert = KeycloakModelUtils.getCertificate(encodedCertificate);
|
||||
return clientCert.getPublicKey();
|
||||
} else {
|
||||
return KeycloakModelUtils.getPublicKey(encodedPublicKey);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+3
-2
@@ -18,6 +18,7 @@ package org.keycloak.broker.oidc;
|
||||
|
||||
import org.keycloak.broker.provider.AbstractIdentityProviderFactory;
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.Map;
|
||||
@@ -45,8 +46,8 @@ public class KeycloakOIDCIdentityProviderFactory extends AbstractIdentityProvide
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> parseConfig(InputStream inputStream) {
|
||||
return OIDCIdentityProviderFactory.parseOIDCConfig(inputStream);
|
||||
public Map<String, String> parseConfig(KeycloakSession session, InputStream inputStream) {
|
||||
return OIDCIdentityProviderFactory.parseOIDCConfig(session, inputStream);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ package org.keycloak.broker.oidc;
|
||||
import org.keycloak.broker.provider.AbstractIdentityProviderFactory;
|
||||
import org.keycloak.jose.jwk.JWK;
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.jose.jwk.JSONWebKeySet;
|
||||
import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation;
|
||||
@@ -56,11 +57,11 @@ public class OIDCIdentityProviderFactory extends AbstractIdentityProviderFactory
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> parseConfig(InputStream inputStream) {
|
||||
return parseOIDCConfig(inputStream);
|
||||
public Map<String, String> parseConfig(KeycloakSession session, InputStream inputStream) {
|
||||
return parseOIDCConfig(session, inputStream);
|
||||
}
|
||||
|
||||
protected static Map<String, String> parseOIDCConfig(InputStream inputStream) {
|
||||
protected static Map<String, String> parseOIDCConfig(KeycloakSession session, InputStream inputStream) {
|
||||
OIDCConfigurationRepresentation rep;
|
||||
try {
|
||||
rep = JsonSerialization.readValue(inputStream, OIDCConfigurationRepresentation.class);
|
||||
@@ -74,14 +75,14 @@ public class OIDCIdentityProviderFactory extends AbstractIdentityProviderFactory
|
||||
config.setTokenUrl(rep.getTokenEndpoint());
|
||||
config.setUserInfoUrl(rep.getUserinfoEndpoint());
|
||||
if (rep.getJwksUri() != null) {
|
||||
sendJwksRequest(rep, config);
|
||||
sendJwksRequest(session, rep, config);
|
||||
}
|
||||
return config.getConfig();
|
||||
}
|
||||
|
||||
protected static void sendJwksRequest(OIDCConfigurationRepresentation rep, OIDCIdentityProviderConfig config) {
|
||||
protected static void sendJwksRequest(KeycloakSession session, OIDCConfigurationRepresentation rep, OIDCIdentityProviderConfig config) {
|
||||
try {
|
||||
JSONWebKeySet keySet = JWKSUtils.sendJwksRequest(rep.getJwksUri());
|
||||
JSONWebKeySet keySet = JWKSUtils.sendJwksRequest(session, rep.getJwksUri());
|
||||
PublicKey key = JWKSUtils.getKeyForUse(keySet, JWK.Use.SIG);
|
||||
if (key == null) {
|
||||
logger.supportedJwkNotFound(JWK.Use.SIG.asString());
|
||||
|
||||
@@ -24,6 +24,7 @@ import org.keycloak.dom.saml.v2.metadata.IDPSSODescriptorType;
|
||||
import org.keycloak.dom.saml.v2.metadata.KeyDescriptorType;
|
||||
import org.keycloak.dom.saml.v2.metadata.KeyTypes;
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
|
||||
import org.keycloak.saml.common.exceptions.ParsingException;
|
||||
import org.keycloak.saml.common.util.DocumentUtil;
|
||||
@@ -54,7 +55,7 @@ public class SAMLIdentityProviderFactory extends AbstractIdentityProviderFactory
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> parseConfig(InputStream inputStream) {
|
||||
public Map<String, String> parseConfig(KeycloakSession session, InputStream inputStream) {
|
||||
try {
|
||||
Object parsedObject = new SAMLParser().parse(inputStream);
|
||||
EntityDescriptorType entityType;
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* 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.protocol.oidc;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
import org.keycloak.jose.jws.Algorithm;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class OIDCAdvancedConfigWrapper {
|
||||
|
||||
private static final String USER_INFO_RESPONSE_SIGNATURE_ALG = "user.info.response.signature.alg";
|
||||
|
||||
private static final String REQUEST_OBJECT_SIGNATURE_ALG = "request.object.signature.alg";
|
||||
|
||||
private final ClientModel clientModel;
|
||||
private final ClientRepresentation clientRep;
|
||||
|
||||
private OIDCAdvancedConfigWrapper(ClientModel client, ClientRepresentation clientRep) {
|
||||
this.clientModel = client;
|
||||
this.clientRep = clientRep;
|
||||
}
|
||||
|
||||
|
||||
public static OIDCAdvancedConfigWrapper fromClientModel(ClientModel client) {
|
||||
return new OIDCAdvancedConfigWrapper(client, null);
|
||||
}
|
||||
|
||||
public static OIDCAdvancedConfigWrapper fromClientRepresentation(ClientRepresentation clientRep) {
|
||||
return new OIDCAdvancedConfigWrapper(null, clientRep);
|
||||
}
|
||||
|
||||
|
||||
public Algorithm getUserInfoSignedResponseAlg() {
|
||||
String alg = getAttribute(USER_INFO_RESPONSE_SIGNATURE_ALG);
|
||||
return alg==null ? null : Enum.valueOf(Algorithm.class, alg);
|
||||
}
|
||||
|
||||
public void setUserInfoSignedResponseAlg(Algorithm alg) {
|
||||
String algStr = alg==null ? null : alg.toString();
|
||||
setAttribute(USER_INFO_RESPONSE_SIGNATURE_ALG, algStr);
|
||||
}
|
||||
|
||||
public boolean isUserInfoSignatureRequired() {
|
||||
return getUserInfoSignedResponseAlg() != null;
|
||||
}
|
||||
|
||||
public Algorithm getRequestObjectSignatureAlg() {
|
||||
String alg = getAttribute(REQUEST_OBJECT_SIGNATURE_ALG);
|
||||
return alg==null ? null : Enum.valueOf(Algorithm.class, alg);
|
||||
}
|
||||
|
||||
public void setRequestObjectSignatureAlg(Algorithm alg) {
|
||||
String algStr = alg==null ? null : alg.toString();
|
||||
setAttribute(REQUEST_OBJECT_SIGNATURE_ALG, algStr);
|
||||
}
|
||||
|
||||
|
||||
private String getAttribute(String attrKey) {
|
||||
if (clientModel != null) {
|
||||
return clientModel.getAttribute(attrKey);
|
||||
} else {
|
||||
return clientRep.getAttributes()==null ? null : clientRep.getAttributes().get(attrKey);
|
||||
}
|
||||
}
|
||||
|
||||
private void setAttribute(String attrKey, String attrValue) {
|
||||
if (clientModel != null) {
|
||||
if (attrValue != null) {
|
||||
clientModel.setAttribute(attrKey, attrValue);
|
||||
} else {
|
||||
clientModel.removeAttribute(attrKey);
|
||||
}
|
||||
} else {
|
||||
if (attrValue != null) {
|
||||
if (clientRep.getAttributes() == null) {
|
||||
clientRep.setAttributes(new HashMap<>());
|
||||
}
|
||||
clientRep.getAttributes().put(attrKey, attrValue);
|
||||
} else {
|
||||
if (clientRep.getAttributes() != null) {
|
||||
clientRep.getAttributes().put(attrKey, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -66,6 +66,7 @@ public class OIDCLoginProtocol implements LoginProtocol {
|
||||
public static final String REQUEST_PARAM = "request";
|
||||
public static final String REQUEST_URI_PARAM = "request_uri";
|
||||
public static final String UI_LOCALES_PARAM = OAuth2Constants.UI_LOCALES_PARAM;
|
||||
public static final String CLAIMS_PARAM = "claims";
|
||||
|
||||
public static final String LOGOUT_REDIRECT_URI = "OIDC_LOGOUT_REDIRECT_URI";
|
||||
public static final String ISSUER = "iss";
|
||||
|
||||
@@ -48,6 +48,10 @@ public class OIDCWellKnownProvider implements WellKnownProvider {
|
||||
|
||||
public static final List<String> DEFAULT_ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED = list(Algorithm.RS256.toString());
|
||||
|
||||
public static final List<String> DEFAULT_USER_INFO_SIGNING_ALG_VALUES_SUPPORTED = list(Algorithm.RS256.toString());
|
||||
|
||||
public static final List<String> DEFAULT_REQUEST_OBJECT_SIGNING_ALG_VALUES_SUPPORTED = list(Algorithm.none.toString(), Algorithm.RS256.toString());
|
||||
|
||||
public static final List<String> DEFAULT_GRANT_TYPES_SUPPORTED = list(OAuth2Constants.AUTHORIZATION_CODE, OAuth2Constants.IMPLICIT, OAuth2Constants.REFRESH_TOKEN, OAuth2Constants.PASSWORD, OAuth2Constants.CLIENT_CREDENTIALS);
|
||||
|
||||
public static final List<String> DEFAULT_RESPONSE_TYPES_SUPPORTED = list(OAuth2Constants.CODE, OIDCResponseType.NONE, OIDCResponseType.ID_TOKEN, OIDCResponseType.TOKEN, "id_token token", "code id_token", "code token", "code id_token token");
|
||||
@@ -90,6 +94,8 @@ public class OIDCWellKnownProvider implements WellKnownProvider {
|
||||
config.setRegistrationEndpoint(RealmsResource.clientRegistrationUrl(uriInfo).path(ClientRegistrationService.class, "provider").build(realm.getName(), OIDCClientRegistrationProviderFactory.ID).toString());
|
||||
|
||||
config.setIdTokenSigningAlgValuesSupported(DEFAULT_ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED);
|
||||
config.setUserInfoSigningAlgValuesSupported(DEFAULT_USER_INFO_SIGNING_ALG_VALUES_SUPPORTED);
|
||||
config.setRequestObjectSigningAlgValuesSupported(DEFAULT_REQUEST_OBJECT_SIGNING_ALG_VALUES_SUPPORTED);
|
||||
config.setResponseTypesSupported(DEFAULT_RESPONSE_TYPES_SUPPORTED);
|
||||
config.setSubjectTypesSupported(DEFAULT_SUBJECT_TYPES_SUPPORTED);
|
||||
config.setResponseModesSupported(DEFAULT_RESPONSE_MODES_SUPPORTED);
|
||||
@@ -104,8 +110,8 @@ public class OIDCWellKnownProvider implements WellKnownProvider {
|
||||
|
||||
config.setScopesSupported(SCOPES_SUPPORTED);
|
||||
|
||||
config.setRequestParameterSupported(false);
|
||||
config.setRequestUriParameterSupported(false);
|
||||
config.setRequestParameterSupported(true);
|
||||
config.setRequestUriParameterSupported(true);
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
+35
-119
@@ -17,11 +17,6 @@
|
||||
|
||||
package org.keycloak.protocol.oidc.endpoints;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.core.MultivaluedMap;
|
||||
import javax.ws.rs.core.Response;
|
||||
@@ -42,6 +37,8 @@ import org.keycloak.models.IdentityProviderModel;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.protocol.AuthorizationEndpointBase;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.protocol.oidc.endpoints.request.AuthorizationEndpointRequest;
|
||||
import org.keycloak.protocol.oidc.endpoints.request.AuthorizationEndpointRequestParserProcessor;
|
||||
import org.keycloak.protocol.oidc.utils.OIDCRedirectUriBuilder;
|
||||
import org.keycloak.protocol.oidc.utils.OIDCResponseMode;
|
||||
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
|
||||
@@ -69,41 +66,8 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
|
||||
* prevent collisions with internally used notes.
|
||||
*
|
||||
* @see ClientSessionModel#getNote(String)
|
||||
* @see #KNOWN_REQ_PARAMS
|
||||
* @see #additionalReqParams
|
||||
*/
|
||||
public static final String CLIENT_SESSION_NOTE_ADDITIONAL_REQ_PARAMS_PREFIX = "client_request_param_";
|
||||
/**
|
||||
* Max number of additional req params copied into client session note to prevent DoS attacks
|
||||
*
|
||||
* @see #additionalReqParams
|
||||
*/
|
||||
public static final int ADDITIONAL_REQ_PARAMS_MAX_MUMBER = 5;
|
||||
/**
|
||||
* Max size of additional req param value copied into client session note to prevent DoS attacks - params with longer value are ignored
|
||||
*
|
||||
* @see #additionalReqParams
|
||||
*/
|
||||
public static final int ADDITIONAL_REQ_PARAMS_MAX_SIZE = 200;
|
||||
|
||||
/** Set of known protocol GET params not to be stored into {@link #additionalReqParams} */
|
||||
private static final Set<String> KNOWN_REQ_PARAMS = new HashSet<>();
|
||||
static {
|
||||
KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.CLIENT_ID_PARAM);
|
||||
KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.RESPONSE_TYPE_PARAM);
|
||||
KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.RESPONSE_MODE_PARAM);
|
||||
KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.REDIRECT_URI_PARAM);
|
||||
KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.STATE_PARAM);
|
||||
KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.SCOPE_PARAM);
|
||||
KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.LOGIN_HINT_PARAM);
|
||||
KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.PROMPT_PARAM);
|
||||
KNOWN_REQ_PARAMS.add(AdapterConstants.KC_IDP_HINT);
|
||||
KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.NONCE_PARAM);
|
||||
KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.MAX_AGE_PARAM);
|
||||
KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.UI_LOCALES_PARAM);
|
||||
KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.REQUEST_PARAM);
|
||||
KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.REQUEST_URI_PARAM);
|
||||
}
|
||||
|
||||
private enum Action {
|
||||
REGISTER, CODE, FORGOT_CREDENTIALS
|
||||
@@ -116,19 +80,8 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
|
||||
private OIDCResponseType parsedResponseType;
|
||||
private OIDCResponseMode parsedResponseMode;
|
||||
|
||||
private String clientId;
|
||||
private AuthorizationEndpointRequest request;
|
||||
private String redirectUri;
|
||||
private String redirectUriParam;
|
||||
private String responseType;
|
||||
private String responseMode;
|
||||
private String state;
|
||||
private String scope;
|
||||
private String loginHint;
|
||||
private String prompt;
|
||||
private String nonce;
|
||||
private String maxAge;
|
||||
private String idpHint;
|
||||
protected Map<String, String> additionalReqParams = new HashMap<>();
|
||||
|
||||
public AuthorizationEndpoint(RealmModel realm, EventBuilder event) {
|
||||
super(realm, event);
|
||||
@@ -139,34 +92,25 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
|
||||
public Response build() {
|
||||
MultivaluedMap<String, String> params = uriInfo.getQueryParameters();
|
||||
|
||||
clientId = params.getFirst(OIDCLoginProtocol.CLIENT_ID_PARAM);
|
||||
responseType = params.getFirst(OIDCLoginProtocol.RESPONSE_TYPE_PARAM);
|
||||
responseMode = params.getFirst(OIDCLoginProtocol.RESPONSE_MODE_PARAM);
|
||||
redirectUriParam = params.getFirst(OIDCLoginProtocol.REDIRECT_URI_PARAM);
|
||||
state = params.getFirst(OIDCLoginProtocol.STATE_PARAM);
|
||||
scope = params.getFirst(OIDCLoginProtocol.SCOPE_PARAM);
|
||||
loginHint = params.getFirst(OIDCLoginProtocol.LOGIN_HINT_PARAM);
|
||||
prompt = params.getFirst(OIDCLoginProtocol.PROMPT_PARAM);
|
||||
idpHint = params.getFirst(AdapterConstants.KC_IDP_HINT);
|
||||
nonce = params.getFirst(OIDCLoginProtocol.NONCE_PARAM);
|
||||
maxAge = params.getFirst(OIDCLoginProtocol.MAX_AGE_PARAM);
|
||||
|
||||
extractAdditionalReqParams(params);
|
||||
String clientId = params.getFirst(OIDCLoginProtocol.CLIENT_ID_PARAM);
|
||||
|
||||
checkSsl();
|
||||
checkRealm();
|
||||
checkClient();
|
||||
checkClient(clientId);
|
||||
|
||||
request = AuthorizationEndpointRequestParserProcessor.parseRequest(event, session, client, params);
|
||||
|
||||
checkRedirectUri();
|
||||
Response errorResponse = checkResponseType();
|
||||
if (errorResponse != null) {
|
||||
return errorResponse;
|
||||
}
|
||||
|
||||
if (!TokenUtil.isOIDCRequest(scope)) {
|
||||
if (!TokenUtil.isOIDCRequest(request.getScope())) {
|
||||
logger.oidcScopeMissing();
|
||||
}
|
||||
|
||||
errorResponse = checkOIDCParams(params);
|
||||
errorResponse = checkOIDCParams();
|
||||
if (errorResponse != null) {
|
||||
return errorResponse;
|
||||
}
|
||||
@@ -186,27 +130,6 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
|
||||
throw new RuntimeException("Unknown action " + action);
|
||||
}
|
||||
|
||||
protected void extractAdditionalReqParams(MultivaluedMap<String, String> params) {
|
||||
for (String paramName : params.keySet()) {
|
||||
if (!KNOWN_REQ_PARAMS.contains(paramName)) {
|
||||
String value = params.getFirst(paramName);
|
||||
if (value != null && value.trim().isEmpty()) {
|
||||
value = null;
|
||||
}
|
||||
if (value != null && value.length() <= ADDITIONAL_REQ_PARAMS_MAX_SIZE) {
|
||||
if (additionalReqParams.size() >= ADDITIONAL_REQ_PARAMS_MAX_MUMBER) {
|
||||
logger.debug("Maximal number of additional OIDC params (" + ADDITIONAL_REQ_PARAMS_MAX_MUMBER + ") exceeded, ignoring rest of them!");
|
||||
break;
|
||||
}
|
||||
additionalReqParams.put(paramName, value);
|
||||
} else {
|
||||
logger.debug("OIDC Additional param " + paramName + " ignored because value is empty or longer than " + ADDITIONAL_REQ_PARAMS_MAX_SIZE);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public AuthorizationEndpoint register() {
|
||||
event.event(EventType.REGISTER);
|
||||
action = Action.REGISTER;
|
||||
@@ -243,7 +166,7 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
|
||||
}
|
||||
}
|
||||
|
||||
private void checkClient() {
|
||||
private void checkClient(String clientId) {
|
||||
if (clientId == null) {
|
||||
event.error(Errors.INVALID_REQUEST);
|
||||
throw new ErrorPageException(session, Messages.MISSING_PARAMETER, OIDCLoginProtocol.CLIENT_ID_PARAM);
|
||||
@@ -271,6 +194,8 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
|
||||
}
|
||||
|
||||
private Response checkResponseType() {
|
||||
String responseType = request.getResponseType();
|
||||
|
||||
if (responseType == null) {
|
||||
logger.missingParameter(OAuth2Constants.RESPONSE_TYPE);
|
||||
event.error(Errors.INVALID_REQUEST);
|
||||
@@ -292,7 +217,7 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
|
||||
|
||||
OIDCResponseMode parsedResponseMode = null;
|
||||
try {
|
||||
parsedResponseMode = OIDCResponseMode.parse(responseMode, parsedResponseType);
|
||||
parsedResponseMode = OIDCResponseMode.parse(request.getResponseMode(), parsedResponseType);
|
||||
} catch (IllegalArgumentException iae) {
|
||||
logger.invalidParameter(OIDCLoginProtocol.RESPONSE_MODE_PARAM);
|
||||
event.error(Errors.INVALID_REQUEST);
|
||||
@@ -325,20 +250,8 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
|
||||
return null;
|
||||
}
|
||||
|
||||
private Response checkOIDCParams(MultivaluedMap<String, String> params) {
|
||||
if (params.getFirst(OIDCLoginProtocol.REQUEST_PARAM) != null) {
|
||||
logger.unsupportedParameter(OIDCLoginProtocol.REQUEST_PARAM);
|
||||
event.error(Errors.INVALID_REQUEST);
|
||||
return redirectErrorToClient(parsedResponseMode, OAuthErrorException.REQUEST_NOT_SUPPORTED, null);
|
||||
}
|
||||
|
||||
if (params.getFirst(OIDCLoginProtocol.REQUEST_URI_PARAM) != null) {
|
||||
logger.unsupportedParameter(OIDCLoginProtocol.REQUEST_URI_PARAM);
|
||||
event.error(Errors.INVALID_REQUEST);
|
||||
return redirectErrorToClient(parsedResponseMode, OAuthErrorException.REQUEST_URI_NOT_SUPPORTED, null);
|
||||
}
|
||||
|
||||
if (parsedResponseType.isImplicitOrHybridFlow() && nonce == null) {
|
||||
private Response checkOIDCParams() {
|
||||
if (parsedResponseType.isImplicitOrHybridFlow() && request.getNonce() == null) {
|
||||
logger.missingParameter(OIDCLoginProtocol.NONCE_PARAM);
|
||||
event.error(Errors.INVALID_REQUEST);
|
||||
return redirectErrorToClient(parsedResponseMode, OAuthErrorException.INVALID_REQUEST, "Missing parameter: nonce");
|
||||
@@ -355,14 +268,16 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
|
||||
errorResponseBuilder.addParam(OAuth2Constants.ERROR_DESCRIPTION, errorDescription);
|
||||
}
|
||||
|
||||
if (state != null) {
|
||||
errorResponseBuilder.addParam(OAuth2Constants.STATE, state);
|
||||
if (request.getState() != null) {
|
||||
errorResponseBuilder.addParam(OAuth2Constants.STATE, request.getState());
|
||||
}
|
||||
|
||||
return errorResponseBuilder.build();
|
||||
}
|
||||
|
||||
private void checkRedirectUri() {
|
||||
String redirectUriParam = request.getRedirectUriParam();
|
||||
|
||||
event.detail(Details.REDIRECT_URI, redirectUriParam);
|
||||
|
||||
redirectUri = RedirectUtils.verifyRedirectUri(uriInfo, redirectUriParam, realm, client);
|
||||
@@ -377,27 +292,28 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
|
||||
clientSession.setAuthMethod(OIDCLoginProtocol.LOGIN_PROTOCOL);
|
||||
clientSession.setRedirectUri(redirectUri);
|
||||
clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE.name());
|
||||
clientSession.setNote(OIDCLoginProtocol.RESPONSE_TYPE_PARAM, responseType);
|
||||
clientSession.setNote(OIDCLoginProtocol.REDIRECT_URI_PARAM, redirectUriParam);
|
||||
clientSession.setNote(OIDCLoginProtocol.RESPONSE_TYPE_PARAM, request.getResponseType());
|
||||
clientSession.setNote(OIDCLoginProtocol.REDIRECT_URI_PARAM, request.getRedirectUriParam());
|
||||
clientSession.setNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName()));
|
||||
|
||||
if (state != null) clientSession.setNote(OIDCLoginProtocol.STATE_PARAM, state);
|
||||
if (nonce != null) clientSession.setNote(OIDCLoginProtocol.NONCE_PARAM, nonce);
|
||||
if (maxAge != null) clientSession.setNote(OIDCLoginProtocol.MAX_AGE_PARAM, maxAge);
|
||||
if (scope != null) clientSession.setNote(OIDCLoginProtocol.SCOPE_PARAM, scope);
|
||||
if (loginHint != null) clientSession.setNote(OIDCLoginProtocol.LOGIN_HINT_PARAM, loginHint);
|
||||
if (prompt != null) clientSession.setNote(OIDCLoginProtocol.PROMPT_PARAM, prompt);
|
||||
if (idpHint != null) clientSession.setNote(AdapterConstants.KC_IDP_HINT, idpHint);
|
||||
if (responseMode != null) clientSession.setNote(OIDCLoginProtocol.RESPONSE_MODE_PARAM, responseMode);
|
||||
if (request.getState() != null) clientSession.setNote(OIDCLoginProtocol.STATE_PARAM, request.getState());
|
||||
if (request.getNonce() != null) clientSession.setNote(OIDCLoginProtocol.NONCE_PARAM, request.getNonce());
|
||||
if (request.getMaxAge() != null) clientSession.setNote(OIDCLoginProtocol.MAX_AGE_PARAM, String.valueOf(request.getMaxAge()));
|
||||
if (request.getScope() != null) clientSession.setNote(OIDCLoginProtocol.SCOPE_PARAM, request.getScope());
|
||||
if (request.getLoginHint() != null) clientSession.setNote(OIDCLoginProtocol.LOGIN_HINT_PARAM, request.getLoginHint());
|
||||
if (request.getPrompt() != null) clientSession.setNote(OIDCLoginProtocol.PROMPT_PARAM, request.getPrompt());
|
||||
if (request.getIdpHint() != null) clientSession.setNote(AdapterConstants.KC_IDP_HINT, request.getIdpHint());
|
||||
if (request.getResponseMode() != null) clientSession.setNote(OIDCLoginProtocol.RESPONSE_MODE_PARAM, request.getResponseMode());
|
||||
|
||||
if (additionalReqParams != null) {
|
||||
for (String paramName : additionalReqParams.keySet()) {
|
||||
clientSession.setNote(CLIENT_SESSION_NOTE_ADDITIONAL_REQ_PARAMS_PREFIX + paramName, additionalReqParams.get(paramName));
|
||||
if (request.getAdditionalReqParams() != null) {
|
||||
for (String paramName : request.getAdditionalReqParams().keySet()) {
|
||||
clientSession.setNote(CLIENT_SESSION_NOTE_ADDITIONAL_REQ_PARAMS_PREFIX + paramName, request.getAdditionalReqParams().get(paramName));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Response buildAuthorizationCodeAuthorizationResponse() {
|
||||
String idpHint = request.getIdpHint();
|
||||
|
||||
if (idpHint != null && !"".equals(idpHint)) {
|
||||
IdentityProviderModel identityProviderModel = realm.getIdentityProviderByAlias(idpHint);
|
||||
@@ -413,7 +329,7 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
|
||||
this.event.event(EventType.LOGIN);
|
||||
clientSession.setNote(Details.AUTH_TYPE, CODE_AUTH_TYPE);
|
||||
|
||||
return handleBrowserAuthenticationRequest(clientSession, new OIDCLoginProtocol(session, realm, uriInfo, headers, event), TokenUtil.hasPrompt(prompt, OIDCLoginProtocol.PROMPT_VALUE_NONE), false);
|
||||
return handleBrowserAuthenticationRequest(clientSession, new OIDCLoginProtocol(session, realm, uriInfo, headers, event), TokenUtil.hasPrompt(request.getPrompt(), OIDCLoginProtocol.PROMPT_VALUE_NONE), false);
|
||||
}
|
||||
|
||||
private Response buildRegister() {
|
||||
|
||||
@@ -19,6 +19,7 @@ package org.keycloak.protocol.oidc.endpoints;
|
||||
import org.jboss.resteasy.annotations.cache.NoCache;
|
||||
import org.jboss.resteasy.spi.HttpRequest;
|
||||
import org.jboss.resteasy.spi.HttpResponse;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.common.ClientConnection;
|
||||
import org.keycloak.OAuthErrorException;
|
||||
import org.keycloak.RSATokenVerifier;
|
||||
@@ -27,12 +28,15 @@ import org.keycloak.events.Details;
|
||||
import org.keycloak.events.Errors;
|
||||
import org.keycloak.events.EventBuilder;
|
||||
import org.keycloak.events.EventType;
|
||||
import org.keycloak.jose.jws.Algorithm;
|
||||
import org.keycloak.jose.jws.JWSBuilder;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.ClientSessionModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
|
||||
import org.keycloak.protocol.oidc.TokenManager;
|
||||
import org.keycloak.representations.AccessToken;
|
||||
import org.keycloak.services.ErrorResponseException;
|
||||
@@ -40,17 +44,18 @@ import org.keycloak.services.managers.AppAuthManager;
|
||||
import org.keycloak.services.managers.AuthenticationManager;
|
||||
import org.keycloak.services.resources.Cors;
|
||||
import org.keycloak.services.Urls;
|
||||
import org.keycloak.utils.MediaType;
|
||||
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.OPTIONS;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.HttpHeaders;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
|
||||
import java.security.PrivateKey;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -86,7 +91,6 @@ public class UserInfoEndpoint {
|
||||
|
||||
@Path("/")
|
||||
@OPTIONS
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Response issueUserInfoPreflight() {
|
||||
return Cors.add(this.request, Response.ok()).auth().preflight().build();
|
||||
}
|
||||
@@ -94,7 +98,6 @@ public class UserInfoEndpoint {
|
||||
@Path("/")
|
||||
@GET
|
||||
@NoCache
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Response issueUserInfoGet(@Context final HttpHeaders headers) {
|
||||
String accessToken = this.appAuthManager.extractAuthorizationHeaderToken(headers);
|
||||
return issueUserInfo(accessToken);
|
||||
@@ -103,7 +106,6 @@ public class UserInfoEndpoint {
|
||||
@Path("/")
|
||||
@POST
|
||||
@NoCache
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Response issueUserInfoPost() {
|
||||
// Try header first
|
||||
HttpHeaders headers = request.getHttpHeaders();
|
||||
@@ -176,12 +178,39 @@ public class UserInfoEndpoint {
|
||||
AccessToken userInfo = new AccessToken();
|
||||
tokenManager.transformUserInfoAccessToken(session, userInfo, realm, clientModel, userModel, userSession, clientSession);
|
||||
|
||||
event.success();
|
||||
|
||||
Map<String, Object> claims = new HashMap<String, Object>();
|
||||
claims.putAll(userInfo.getOtherClaims());
|
||||
claims.put("sub", userModel.getId());
|
||||
return Cors.add(request, Response.ok(claims)).auth().allowedOrigins(token).build();
|
||||
|
||||
Response.ResponseBuilder responseBuilder;
|
||||
OIDCAdvancedConfigWrapper cfg = OIDCAdvancedConfigWrapper.fromClientModel(clientModel);
|
||||
|
||||
if (cfg.isUserInfoSignatureRequired()) {
|
||||
String issuerUrl = Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName());
|
||||
String audience = clientModel.getClientId();
|
||||
claims.put("iss", issuerUrl);
|
||||
claims.put("aud", audience);
|
||||
|
||||
Algorithm signatureAlg = cfg.getUserInfoSignedResponseAlg();
|
||||
PrivateKey privateKey = realm.getPrivateKey();
|
||||
|
||||
String signedUserInfo = new JWSBuilder()
|
||||
.jsonContent(claims)
|
||||
.sign(signatureAlg, privateKey);
|
||||
|
||||
responseBuilder = Response.ok(signedUserInfo).header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JWT);
|
||||
|
||||
event.detail(Details.SIGNATURE_REQUIRED, "true");
|
||||
event.detail(Details.SIGNATURE_ALGORITHM, cfg.getUserInfoSignedResponseAlg().toString());
|
||||
} else {
|
||||
responseBuilder = Response.ok(claims).header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON);
|
||||
|
||||
event.detail(Details.SIGNATURE_REQUIRED, "false");
|
||||
}
|
||||
|
||||
event.success();
|
||||
|
||||
return Cors.add(request, responseBuilder).auth().allowedOrigins(token).build();
|
||||
}
|
||||
|
||||
}
|
||||
+88
@@ -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.protocol.oidc.endpoints.request;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class AuthorizationEndpointRequest {
|
||||
|
||||
String clientId;
|
||||
String redirectUriParam;
|
||||
String responseType;
|
||||
String responseMode;
|
||||
String state;
|
||||
String scope;
|
||||
String loginHint;
|
||||
String prompt;
|
||||
String nonce;
|
||||
Integer maxAge;
|
||||
String idpHint;
|
||||
Map<String, String> additionalReqParams = new HashMap<>();
|
||||
|
||||
public String getClientId() {
|
||||
return clientId;
|
||||
}
|
||||
|
||||
public String getRedirectUriParam() {
|
||||
return redirectUriParam;
|
||||
}
|
||||
|
||||
public String getResponseType() {
|
||||
return responseType;
|
||||
}
|
||||
|
||||
public String getResponseMode() {
|
||||
return responseMode;
|
||||
}
|
||||
|
||||
public String getState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
public String getScope() {
|
||||
return scope;
|
||||
}
|
||||
|
||||
public String getLoginHint() {
|
||||
return loginHint;
|
||||
}
|
||||
|
||||
public String getPrompt() {
|
||||
return prompt;
|
||||
}
|
||||
|
||||
public String getNonce() {
|
||||
return nonce;
|
||||
}
|
||||
|
||||
public Integer getMaxAge() {
|
||||
return maxAge;
|
||||
}
|
||||
|
||||
public String getIdpHint() {
|
||||
return idpHint;
|
||||
}
|
||||
|
||||
public Map<String, String> getAdditionalReqParams() {
|
||||
return additionalReqParams;
|
||||
}
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* 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.protocol.oidc.endpoints.request;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.ws.rs.core.MultivaluedMap;
|
||||
|
||||
import org.keycloak.common.util.StreamUtil;
|
||||
import org.keycloak.connections.httpclient.HttpClientProvider;
|
||||
import org.keycloak.events.Errors;
|
||||
import org.keycloak.events.EventBuilder;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.services.ErrorPageException;
|
||||
import org.keycloak.services.ServicesLogger;
|
||||
import org.keycloak.services.messages.Messages;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class AuthorizationEndpointRequestParserProcessor {
|
||||
|
||||
private static final ServicesLogger logger = ServicesLogger.ROOT_LOGGER;
|
||||
|
||||
public static AuthorizationEndpointRequest parseRequest(EventBuilder event, KeycloakSession session, ClientModel client, MultivaluedMap<String, String> requestParams) {
|
||||
try {
|
||||
AuthorizationEndpointRequest request = new AuthorizationEndpointRequest();
|
||||
|
||||
new AuthzEndpointQueryStringParser(requestParams).parseRequest(request);
|
||||
|
||||
String requestParam = requestParams.getFirst(OIDCLoginProtocol.REQUEST_PARAM);
|
||||
String requestUriParam = requestParams.getFirst(OIDCLoginProtocol.REQUEST_URI_PARAM);
|
||||
|
||||
if (requestParam != null && requestUriParam != null) {
|
||||
throw new RuntimeException("Illegal to use both 'request' and 'request_uri' parameters together");
|
||||
}
|
||||
|
||||
if (requestParam != null) {
|
||||
new AuthzEndpointRequestObjectParser(requestParam, client).parseRequest(request);
|
||||
} else if (requestUriParam != null) {
|
||||
InputStream is = session.getProvider(HttpClientProvider.class).get(requestUriParam);
|
||||
String retrievedRequest = StreamUtil.readString(is);
|
||||
|
||||
new AuthzEndpointRequestObjectParser(retrievedRequest, client).parseRequest(request);
|
||||
}
|
||||
|
||||
return request;
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.invalidRequest(e);
|
||||
event.error(Errors.INVALID_REQUEST);
|
||||
throw new ErrorPageException(session, Messages.INVALID_REQUEST);
|
||||
}
|
||||
}
|
||||
}
|
||||
+52
@@ -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.protocol.oidc.endpoints.request;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import javax.ws.rs.core.MultivaluedMap;
|
||||
|
||||
/**
|
||||
* Parse the parameters from request queryString
|
||||
*
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
class AuthzEndpointQueryStringParser extends AuthzEndpointRequestParser {
|
||||
|
||||
private final MultivaluedMap<String, String> requestParams;
|
||||
|
||||
public AuthzEndpointQueryStringParser(MultivaluedMap<String, String> requestParams) {
|
||||
this.requestParams = requestParams;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getParameter(String paramName) {
|
||||
return requestParams.getFirst(paramName);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Integer getIntParameter(String paramName) {
|
||||
String paramVal = requestParams.getFirst(paramName);
|
||||
return paramVal==null ? null : Integer.parseInt(paramVal);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Set<String> keySet() {
|
||||
return requestParams.keySet();
|
||||
}
|
||||
}
|
||||
+88
@@ -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.protocol.oidc.endpoints.request;
|
||||
|
||||
import java.security.PublicKey;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator;
|
||||
import org.keycloak.jose.jws.Algorithm;
|
||||
import org.keycloak.jose.jws.JWSHeader;
|
||||
import org.keycloak.jose.jws.JWSInput;
|
||||
import org.keycloak.jose.jws.crypto.RSAProvider;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
|
||||
import org.keycloak.services.util.CertificateInfoHelper;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
||||
/**
|
||||
* Parse the parameters from OIDC "request" object
|
||||
*
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
class AuthzEndpointRequestObjectParser extends AuthzEndpointRequestParser {
|
||||
|
||||
private final Map<String, Object> requestParams;
|
||||
|
||||
public AuthzEndpointRequestObjectParser(String requestObject, ClientModel client) throws Exception {
|
||||
JWSInput input = new JWSInput(requestObject);
|
||||
JWSHeader header = input.getHeader();
|
||||
|
||||
Algorithm requestedSignatureAlgorithm = OIDCAdvancedConfigWrapper.fromClientModel(client).getRequestObjectSignatureAlg();
|
||||
|
||||
if (requestedSignatureAlgorithm != null && requestedSignatureAlgorithm != header.getAlgorithm()) {
|
||||
throw new RuntimeException("Request object signed with different algorithm than client requested algorithm");
|
||||
}
|
||||
|
||||
if (header.getAlgorithm() == Algorithm.none) {
|
||||
this.requestParams = JsonSerialization.readValue(input.getContent(), TypedHashMap.class);
|
||||
} else if (header.getAlgorithm() == Algorithm.RS256) {
|
||||
PublicKey clientPublicKey = CertificateInfoHelper.getSignatureValidationKey(client, JWTClientAuthenticator.ATTR_PREFIX);
|
||||
boolean verified = RSAProvider.verify(input, clientPublicKey);
|
||||
if (!verified) {
|
||||
throw new RuntimeException("Failed to verify signature on 'request' object");
|
||||
}
|
||||
|
||||
this.requestParams = JsonSerialization.readValue(input.getContent(), TypedHashMap.class);
|
||||
} else {
|
||||
throw new RuntimeException("Unsupported JWA algorithm used for signed request");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getParameter(String paramName) {
|
||||
Object val = this.requestParams.get(paramName);
|
||||
return val==null ? null : val.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Integer getIntParameter(String paramName) {
|
||||
Object val = this.requestParams.get(paramName);
|
||||
return val==null ? null : Integer.parseInt(getParameter(paramName));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Set<String> keySet() {
|
||||
return requestParams.keySet();
|
||||
}
|
||||
|
||||
static class TypedHashMap extends HashMap<String, Object> {
|
||||
}
|
||||
}
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* 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.protocol.oidc.endpoints.request;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.keycloak.constants.AdapterConstants;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.services.ServicesLogger;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
abstract class AuthzEndpointRequestParser {
|
||||
|
||||
private static final ServicesLogger logger = ServicesLogger.ROOT_LOGGER;
|
||||
|
||||
/**
|
||||
* Max number of additional req params copied into client session note to prevent DoS attacks
|
||||
*
|
||||
*/
|
||||
public static final int ADDITIONAL_REQ_PARAMS_MAX_MUMBER = 5;
|
||||
|
||||
/**
|
||||
* Max size of additional req param value copied into client session note to prevent DoS attacks - params with longer value are ignored
|
||||
*
|
||||
*/
|
||||
public static final int ADDITIONAL_REQ_PARAMS_MAX_SIZE = 200;
|
||||
|
||||
/** Set of known protocol GET params not to be stored into additionalReqParams} */
|
||||
private static final Set<String> KNOWN_REQ_PARAMS = new HashSet<>();
|
||||
static {
|
||||
KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.CLIENT_ID_PARAM);
|
||||
KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.RESPONSE_TYPE_PARAM);
|
||||
KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.RESPONSE_MODE_PARAM);
|
||||
KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.REDIRECT_URI_PARAM);
|
||||
KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.STATE_PARAM);
|
||||
KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.SCOPE_PARAM);
|
||||
KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.LOGIN_HINT_PARAM);
|
||||
KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.PROMPT_PARAM);
|
||||
KNOWN_REQ_PARAMS.add(AdapterConstants.KC_IDP_HINT);
|
||||
KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.NONCE_PARAM);
|
||||
KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.MAX_AGE_PARAM);
|
||||
KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.UI_LOCALES_PARAM);
|
||||
KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.REQUEST_PARAM);
|
||||
KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.REQUEST_URI_PARAM);
|
||||
}
|
||||
|
||||
|
||||
public void parseRequest(AuthorizationEndpointRequest request) {
|
||||
String clientId = getParameter(OIDCLoginProtocol.CLIENT_ID_PARAM);
|
||||
|
||||
if (request.clientId != null && !request.clientId.equals(clientId)) {
|
||||
throw new IllegalArgumentException("The client_id parameter doesn't match the one from OIDC 'request' or 'request_uri'");
|
||||
}
|
||||
|
||||
request.clientId = clientId;
|
||||
request.responseType = replaceIfNotNull(request.responseType, getParameter(OIDCLoginProtocol.RESPONSE_TYPE_PARAM));
|
||||
request.responseMode = replaceIfNotNull(request.responseMode, getParameter(OIDCLoginProtocol.RESPONSE_MODE_PARAM));
|
||||
request.redirectUriParam = replaceIfNotNull(request.redirectUriParam, getParameter(OIDCLoginProtocol.REDIRECT_URI_PARAM));
|
||||
request.state = replaceIfNotNull(request.state, getParameter(OIDCLoginProtocol.STATE_PARAM));
|
||||
request.scope = replaceIfNotNull(request.scope, getParameter(OIDCLoginProtocol.SCOPE_PARAM));
|
||||
request.loginHint = replaceIfNotNull(request.loginHint, getParameter(OIDCLoginProtocol.LOGIN_HINT_PARAM));
|
||||
request.prompt = replaceIfNotNull(request.prompt, getParameter(OIDCLoginProtocol.PROMPT_PARAM));
|
||||
request.idpHint = replaceIfNotNull(request.idpHint, getParameter(AdapterConstants.KC_IDP_HINT));
|
||||
request.nonce = replaceIfNotNull(request.nonce, getParameter(OIDCLoginProtocol.NONCE_PARAM));
|
||||
request.maxAge = replaceIfNotNull(request.maxAge, getIntParameter(OIDCLoginProtocol.MAX_AGE_PARAM));
|
||||
|
||||
extractAdditionalReqParams(request.additionalReqParams);
|
||||
}
|
||||
|
||||
|
||||
protected void extractAdditionalReqParams(Map<String, String> additionalReqParams) {
|
||||
for (String paramName : keySet()) {
|
||||
if (!KNOWN_REQ_PARAMS.contains(paramName)) {
|
||||
String value = getParameter(paramName);
|
||||
if (value != null && value.trim().isEmpty()) {
|
||||
value = null;
|
||||
}
|
||||
if (value != null && value.length() <= ADDITIONAL_REQ_PARAMS_MAX_SIZE) {
|
||||
if (additionalReqParams.size() >= ADDITIONAL_REQ_PARAMS_MAX_MUMBER) {
|
||||
logger.debug("Maximal number of additional OIDC params (" + ADDITIONAL_REQ_PARAMS_MAX_MUMBER + ") exceeded, ignoring rest of them!");
|
||||
break;
|
||||
}
|
||||
additionalReqParams.put(paramName, value);
|
||||
} else {
|
||||
logger.debug("OIDC Additional param " + paramName + " ignored because value is empty or longer than " + ADDITIONAL_REQ_PARAMS_MAX_SIZE);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
protected <T> T replaceIfNotNull(T previousVal, T newVal) {
|
||||
return newVal==null ? previousVal : newVal;
|
||||
}
|
||||
|
||||
|
||||
protected abstract String getParameter(String paramName);
|
||||
|
||||
protected abstract Integer getIntParameter(String paramName);
|
||||
|
||||
protected abstract Set<String> keySet();
|
||||
|
||||
}
|
||||
+22
@@ -64,6 +64,12 @@ public class OIDCConfigurationRepresentation {
|
||||
@JsonProperty("id_token_signing_alg_values_supported")
|
||||
private List<String> idTokenSigningAlgValuesSupported;
|
||||
|
||||
@JsonProperty("userinfo_signing_alg_values_supported")
|
||||
private List<String> userInfoSigningAlgValuesSupported;
|
||||
|
||||
@JsonProperty("request_object_signing_alg_values_supported")
|
||||
private List<String> requestObjectSigningAlgValuesSupported;
|
||||
|
||||
@JsonProperty("response_modes_supported")
|
||||
private List<String> responseModesSupported;
|
||||
|
||||
@@ -184,6 +190,22 @@ public class OIDCConfigurationRepresentation {
|
||||
this.idTokenSigningAlgValuesSupported = idTokenSigningAlgValuesSupported;
|
||||
}
|
||||
|
||||
public List<String> getUserInfoSigningAlgValuesSupported() {
|
||||
return userInfoSigningAlgValuesSupported;
|
||||
}
|
||||
|
||||
public void setUserInfoSigningAlgValuesSupported(List<String> userInfoSigningAlgValuesSupported) {
|
||||
this.userInfoSigningAlgValuesSupported = userInfoSigningAlgValuesSupported;
|
||||
}
|
||||
|
||||
public List<String> getRequestObjectSigningAlgValuesSupported() {
|
||||
return requestObjectSigningAlgValuesSupported;
|
||||
}
|
||||
|
||||
public void setRequestObjectSigningAlgValuesSupported(List<String> requestObjectSigningAlgValuesSupported) {
|
||||
this.requestObjectSigningAlgValuesSupported = requestObjectSigningAlgValuesSupported;
|
||||
}
|
||||
|
||||
public List<String> getResponseModesSupported() {
|
||||
return responseModesSupported;
|
||||
}
|
||||
|
||||
@@ -18,12 +18,15 @@
|
||||
package org.keycloak.protocol.oidc.utils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.PublicKey;
|
||||
|
||||
import org.keycloak.broker.provider.util.SimpleHttp;
|
||||
import org.keycloak.common.util.StreamUtil;
|
||||
import org.keycloak.connections.httpclient.HttpClientProvider;
|
||||
import org.keycloak.jose.jwk.JSONWebKeySet;
|
||||
import org.keycloak.jose.jwk.JWK;
|
||||
import org.keycloak.jose.jwk.JWKParser;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
||||
/**
|
||||
@@ -31,8 +34,9 @@ import org.keycloak.util.JsonSerialization;
|
||||
*/
|
||||
public class JWKSUtils {
|
||||
|
||||
public static JSONWebKeySet sendJwksRequest(String jwksURI) throws IOException {
|
||||
String keySetString = SimpleHttp.doGet(jwksURI).asString();
|
||||
public static JSONWebKeySet sendJwksRequest(KeycloakSession session, String jwksURI) throws IOException {
|
||||
InputStream is = session.getProvider(HttpClientProvider.class).get(jwksURI);
|
||||
String keySetString = StreamUtil.readString(is);
|
||||
return JsonSerialization.readValue(keySetString, JSONWebKeySet.class);
|
||||
}
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ public class RedirectUtils {
|
||||
} else {
|
||||
redirectUri = lowerCaseHostname(redirectUri);
|
||||
|
||||
String r = redirectUri.indexOf('?') != -1 ? redirectUri.substring(0, redirectUri.indexOf('?')) : redirectUri;
|
||||
String r = redirectUri;
|
||||
Set<String> resolveValidRedirects = resolveValidRedirects(uriInfo, rootUrl, validRedirects);
|
||||
|
||||
boolean valid = matchesRedirects(resolveValidRedirects, r);
|
||||
@@ -134,15 +134,17 @@ public class RedirectUtils {
|
||||
|
||||
private static boolean matchesRedirects(Set<String> validRedirects, String redirect) {
|
||||
for (String validRedirect : validRedirects) {
|
||||
if (validRedirect.endsWith("*")) {
|
||||
if (validRedirect.endsWith("*") && !validRedirect.contains("?")) {
|
||||
// strip off the query component - we don't check them when wildcards are effective
|
||||
String r = redirect.contains("?") ? redirect.substring(0, redirect.indexOf("?")) : redirect;
|
||||
// strip off *
|
||||
int length = validRedirect.length() - 1;
|
||||
validRedirect = validRedirect.substring(0, length);
|
||||
if (redirect.startsWith(validRedirect)) return true;
|
||||
if (r.startsWith(validRedirect)) return true;
|
||||
// strip off trailing '/'
|
||||
if (length - 1 > 0 && validRedirect.charAt(length - 1) == '/') length--;
|
||||
validRedirect = validRedirect.substring(0, length);
|
||||
if (validRedirect.equals(redirect)) return true;
|
||||
if (validRedirect.equals(r)) return true;
|
||||
} else if (validRedirect.equals(redirect)) return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -430,4 +430,8 @@ public interface ServicesLogger extends BasicLogger {
|
||||
@Message(id=96, value="Not found JWK of supported keyType under jwks_uri for usage: %s")
|
||||
void supportedJwkNotFound(String usage);
|
||||
|
||||
@LogMessage(level = WARN)
|
||||
@Message(id=97, value="Invalid request")
|
||||
void invalidRequest(@Cause Throwable t);
|
||||
|
||||
}
|
||||
+24
@@ -29,8 +29,12 @@ import org.keycloak.models.utils.RepresentationToModel;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.services.ErrorResponseException;
|
||||
import org.keycloak.services.ForbiddenException;
|
||||
import org.keycloak.services.resources.admin.AdminRoot;
|
||||
import org.keycloak.services.validation.ClientValidator;
|
||||
import org.keycloak.services.validation.ValidationMessages;
|
||||
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.util.Properties;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
@@ -50,6 +54,16 @@ public abstract class AbstractClientRegistrationProvider implements ClientRegist
|
||||
|
||||
auth.requireCreate();
|
||||
|
||||
ValidationMessages validationMessages = new ValidationMessages();
|
||||
if (!ClientValidator.validate(client, validationMessages)) {
|
||||
String errorCode = validationMessages.fieldHasError("redirectUris") ? ErrorCodes.INVALID_REDIRECT_URI : ErrorCodes.INVALID_CLIENT_METADATA;
|
||||
throw new ErrorResponseException(
|
||||
errorCode,
|
||||
validationMessages.getStringMessages(),
|
||||
Response.Status.BAD_REQUEST
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
ClientModel clientModel = RepresentationToModel.createClient(session, session.getContext().getRealm(), client, true);
|
||||
|
||||
@@ -104,6 +118,16 @@ public abstract class AbstractClientRegistrationProvider implements ClientRegist
|
||||
throw new ErrorResponseException(ErrorCodes.INVALID_CLIENT_METADATA, "Client Identifier modified", Response.Status.BAD_REQUEST);
|
||||
}
|
||||
|
||||
ValidationMessages validationMessages = new ValidationMessages();
|
||||
if (!ClientValidator.validate(rep, validationMessages)) {
|
||||
String errorCode = validationMessages.fieldHasError("redirectUris") ? ErrorCodes.INVALID_REDIRECT_URI : ErrorCodes.INVALID_CLIENT_METADATA;
|
||||
throw new ErrorResponseException(
|
||||
errorCode,
|
||||
validationMessages.getStringMessages(),
|
||||
Response.Status.BAD_REQUEST
|
||||
);
|
||||
}
|
||||
|
||||
RepresentationToModel.updateClient(rep, client);
|
||||
rep = ModelToRepresentation.toRepresentation(client);
|
||||
|
||||
|
||||
+30
-10
@@ -24,8 +24,10 @@ import org.keycloak.authentication.authenticators.client.ClientIdAndSecretAuthen
|
||||
import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator;
|
||||
import org.keycloak.jose.jwk.JSONWebKeySet;
|
||||
import org.keycloak.jose.jwk.JWK;
|
||||
import org.keycloak.jose.jws.Algorithm;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil;
|
||||
import org.keycloak.protocol.oidc.utils.JWKSUtils;
|
||||
@@ -87,14 +89,12 @@ public class DescriptionConverter {
|
||||
}
|
||||
client.setClientAuthenticatorType(clientAuthFactory.getId());
|
||||
|
||||
// Externalize to ClientAuthenticator itself?
|
||||
if (authMethod != null && authMethod.equals(OIDCLoginProtocol.PRIVATE_KEY_JWT)) {
|
||||
|
||||
PublicKey publicKey = retrievePublicKey(clientOIDC);
|
||||
if (publicKey == null) {
|
||||
throw new ClientRegistrationException("Didn't find key of supported keyType for use " + JWK.Use.SIG.asString());
|
||||
}
|
||||
PublicKey publicKey = retrievePublicKey(session, clientOIDC);
|
||||
if (authMethod != null && authMethod.equals(OIDCLoginProtocol.PRIVATE_KEY_JWT) && publicKey == null) {
|
||||
throw new ClientRegistrationException("Didn't find key of supported keyType for use " + JWK.Use.SIG.asString());
|
||||
}
|
||||
|
||||
if (publicKey != null) {
|
||||
String publicKeyPem = KeycloakModelUtils.getPemFromKey(publicKey);
|
||||
|
||||
CertificateRepresentation rep = new CertificateRepresentation();
|
||||
@@ -102,13 +102,24 @@ public class DescriptionConverter {
|
||||
CertificateInfoHelper.updateClientRepresentationCertificateInfo(client, rep, JWTClientAuthenticator.ATTR_PREFIX);
|
||||
}
|
||||
|
||||
OIDCAdvancedConfigWrapper configWrapper = OIDCAdvancedConfigWrapper.fromClientRepresentation(client);
|
||||
if (clientOIDC.getUserinfoSignedResponseAlg() != null) {
|
||||
Algorithm algorithm = Enum.valueOf(Algorithm.class, clientOIDC.getUserinfoSignedResponseAlg());
|
||||
configWrapper.setUserInfoSignedResponseAlg(algorithm);
|
||||
}
|
||||
|
||||
if (clientOIDC.getRequestObjectSigningAlg() != null) {
|
||||
Algorithm algorithm = Enum.valueOf(Algorithm.class, clientOIDC.getRequestObjectSigningAlg());
|
||||
configWrapper.setRequestObjectSignatureAlg(algorithm);
|
||||
}
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
|
||||
private static PublicKey retrievePublicKey(OIDCClientRepresentation clientOIDC) {
|
||||
private static PublicKey retrievePublicKey(KeycloakSession session, OIDCClientRepresentation clientOIDC) {
|
||||
if (clientOIDC.getJwksUri() == null && clientOIDC.getJwks() == null) {
|
||||
throw new ClientRegistrationException("Requested client authentication method '%s' but jwks_uri nor jwks were available in config");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (clientOIDC.getJwksUri() != null && clientOIDC.getJwks() != null) {
|
||||
@@ -120,7 +131,7 @@ public class DescriptionConverter {
|
||||
keySet = clientOIDC.getJwks();
|
||||
} else {
|
||||
try {
|
||||
keySet = JWKSUtils.sendJwksRequest(clientOIDC.getJwksUri());
|
||||
keySet = JWKSUtils.sendJwksRequest(session, clientOIDC.getJwksUri());
|
||||
} catch (IOException ioe) {
|
||||
throw new ClientRegistrationException("Failed to send JWKS request to specified jwks_uri", ioe);
|
||||
}
|
||||
@@ -152,6 +163,15 @@ public class DescriptionConverter {
|
||||
response.setRegistrationClientUri(uri.toString());
|
||||
response.setResponseTypes(getOIDCResponseTypes(client));
|
||||
response.setGrantTypes(getOIDCGrantTypes(client));
|
||||
|
||||
OIDCAdvancedConfigWrapper config = OIDCAdvancedConfigWrapper.fromClientRepresentation(client);
|
||||
if (config.isUserInfoSignatureRequired()) {
|
||||
response.setUserinfoSignedResponseAlg(config.getUserInfoSignedResponseAlg().toString());
|
||||
}
|
||||
if (config.getRequestObjectSignatureAlg() != null) {
|
||||
response.setRequestObjectSigningAlg(config.getRequestObjectSignatureAlg().toString());
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
|
||||
@@ -56,15 +56,22 @@ import java.io.*;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.util.*;
|
||||
import org.jboss.dmr.ModelNode;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class KeycloakApplication extends Application {
|
||||
// This param name is defined again in Keycloak Server Subsystem class
|
||||
// org.keycloak.subsystem.server.extension.KeycloakServerDeploymentProcessor. We have this value in
|
||||
// two places to avoid dependency between Keycloak Subsystem and Keycloak Services module.
|
||||
public static final String KEYCLOAK_CONFIG_PARAM_NAME = "org.keycloak.server-subsystem.Config";
|
||||
|
||||
private static final ServicesLogger logger = ServicesLogger.ROOT_LOGGER;
|
||||
|
||||
protected boolean embedded = false;
|
||||
|
||||
protected Set<Object> singletons = new HashSet<Object>();
|
||||
protected Set<Class<?>> classes = new HashSet<Class<?>>();
|
||||
|
||||
@@ -72,69 +79,79 @@ public class KeycloakApplication extends Application {
|
||||
protected String contextPath;
|
||||
|
||||
public KeycloakApplication(@Context ServletContext context, @Context Dispatcher dispatcher) {
|
||||
loadConfig();
|
||||
|
||||
this.contextPath = context.getContextPath();
|
||||
this.sessionFactory = createSessionFactory();
|
||||
|
||||
dispatcher.getDefaultContextObjects().put(KeycloakApplication.class, this);
|
||||
ResteasyProviderFactory.pushContext(KeycloakApplication.class, this); // for injection
|
||||
context.setAttribute(KeycloakSessionFactory.class.getName(), this.sessionFactory);
|
||||
|
||||
singletons.add(new ServerVersionResource());
|
||||
singletons.add(new RobotsResource());
|
||||
singletons.add(new RealmsResource());
|
||||
singletons.add(new AdminRoot());
|
||||
classes.add(ThemeResource.class);
|
||||
classes.add(JsResource.class);
|
||||
|
||||
classes.add(KeycloakTransactionCommitter.class);
|
||||
|
||||
singletons.add(new ObjectMapperResolver(Boolean.parseBoolean(System.getProperty("keycloak.jsonPrettyPrint", "false"))));
|
||||
|
||||
ExportImportManager[] exportImportManager = new ExportImportManager[1];
|
||||
|
||||
KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
|
||||
|
||||
@Override
|
||||
public void run(KeycloakSession lockSession) {
|
||||
DBLockManager dbLockManager = new DBLockManager(lockSession);
|
||||
dbLockManager.checkForcedUnlock();
|
||||
DBLockProvider dbLock = dbLockManager.getDBLock();
|
||||
dbLock.waitForLock();
|
||||
try {
|
||||
exportImportManager[0] = migrateAndBootstrap();
|
||||
} finally {
|
||||
dbLock.releaseLock();
|
||||
}
|
||||
try {
|
||||
if ("true".equals(context.getInitParameter("keycloak.embedded"))) {
|
||||
embedded = true;
|
||||
}
|
||||
|
||||
});
|
||||
loadConfig(context);
|
||||
|
||||
this.contextPath = context.getContextPath();
|
||||
this.sessionFactory = createSessionFactory();
|
||||
|
||||
dispatcher.getDefaultContextObjects().put(KeycloakApplication.class, this);
|
||||
ResteasyProviderFactory.pushContext(KeycloakApplication.class, this); // for injection
|
||||
context.setAttribute(KeycloakSessionFactory.class.getName(), this.sessionFactory);
|
||||
|
||||
singletons.add(new ServerVersionResource());
|
||||
singletons.add(new RobotsResource());
|
||||
singletons.add(new RealmsResource());
|
||||
singletons.add(new AdminRoot());
|
||||
classes.add(ThemeResource.class);
|
||||
classes.add(JsResource.class);
|
||||
|
||||
classes.add(KeycloakTransactionCommitter.class);
|
||||
|
||||
singletons.add(new ObjectMapperResolver(Boolean.parseBoolean(System.getProperty("keycloak.jsonPrettyPrint", "false"))));
|
||||
|
||||
ExportImportManager[] exportImportManager = new ExportImportManager[1];
|
||||
|
||||
KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
|
||||
|
||||
@Override
|
||||
public void run(KeycloakSession lockSession) {
|
||||
DBLockManager dbLockManager = new DBLockManager(lockSession);
|
||||
dbLockManager.checkForcedUnlock();
|
||||
DBLockProvider dbLock = dbLockManager.getDBLock();
|
||||
dbLock.waitForLock();
|
||||
try {
|
||||
exportImportManager[0] = migrateAndBootstrap();
|
||||
} finally {
|
||||
dbLock.releaseLock();
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
if (exportImportManager[0].isRunExport()) {
|
||||
exportImportManager[0].runExport();
|
||||
if (exportImportManager[0].isRunExport()) {
|
||||
exportImportManager[0].runExport();
|
||||
}
|
||||
|
||||
boolean bootstrapAdminUser = false;
|
||||
KeycloakSession session = sessionFactory.create();
|
||||
try {
|
||||
session.getTransactionManager().begin();
|
||||
bootstrapAdminUser = new ApplianceBootstrap(session).isNoMasterUser();
|
||||
|
||||
session.getTransactionManager().commit();
|
||||
} finally {
|
||||
session.close();
|
||||
}
|
||||
|
||||
sessionFactory.publish(new PostMigrationEvent());
|
||||
|
||||
singletons.add(new WelcomeResource(bootstrapAdminUser));
|
||||
|
||||
setupScheduledTasks(sessionFactory);
|
||||
} catch (Throwable t) {
|
||||
if (!embedded) {
|
||||
exit(1);
|
||||
}
|
||||
throw t;
|
||||
}
|
||||
|
||||
boolean bootstrapAdminUser = false;
|
||||
KeycloakSession session = sessionFactory.create();
|
||||
try {
|
||||
session.getTransactionManager().begin();
|
||||
bootstrapAdminUser = new ApplianceBootstrap(session).isNoMasterUser();
|
||||
|
||||
session.getTransactionManager().commit();
|
||||
} finally {
|
||||
session.close();
|
||||
}
|
||||
|
||||
sessionFactory.publish(new PostMigrationEvent());
|
||||
|
||||
singletons.add(new WelcomeResource(bootstrapAdminUser));
|
||||
|
||||
setupScheduledTasks(sessionFactory);
|
||||
}
|
||||
|
||||
|
||||
// Migrate model, bootstrap master realm, import realms and create admin user. This is done with acquired dbLock
|
||||
protected ExportImportManager migrateAndBootstrap() {
|
||||
ExportImportManager exportImportManager;
|
||||
@@ -185,7 +202,6 @@ public class KeycloakApplication extends Application {
|
||||
session.getTransactionManager().commit();
|
||||
} catch (Exception e) {
|
||||
session.getTransactionManager().rollback();
|
||||
logger.migrationFailure(e);
|
||||
throw e;
|
||||
} finally {
|
||||
session.close();
|
||||
@@ -206,12 +222,18 @@ public class KeycloakApplication extends Application {
|
||||
return uriInfo.getBaseUriBuilder().replacePath(getContextPath()).build();
|
||||
}
|
||||
|
||||
public static void loadConfig() {
|
||||
public static void loadConfig(ServletContext context) {
|
||||
try {
|
||||
JsonNode node = null;
|
||||
|
||||
String dmrConfig = loadDmrConfig(context);
|
||||
if (dmrConfig != null) {
|
||||
node = new ObjectMapper().readTree(dmrConfig);
|
||||
logger.loadingFrom("standalone.xml or domain.xml");
|
||||
}
|
||||
|
||||
String configDir = System.getProperty("jboss.server.config.dir");
|
||||
if (configDir != null) {
|
||||
if (node == null && configDir != null) {
|
||||
File f = new File(configDir + File.separator + "keycloak-server.json");
|
||||
if (f.isFile()) {
|
||||
logger.loadingFrom(f.getAbsolutePath());
|
||||
@@ -230,15 +252,25 @@ public class KeycloakApplication extends Application {
|
||||
if (node != null) {
|
||||
Properties properties = new SystemEnvProperties();
|
||||
Config.init(new JsonConfigProvider(node, properties));
|
||||
return;
|
||||
} else {
|
||||
throw new RuntimeException("Config 'keycloak-server.json' not found");
|
||||
throw new RuntimeException("Keycloak config not found.");
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Failed to load config", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static String loadDmrConfig(ServletContext context) {
|
||||
String dmrConfig = context.getInitParameter(KEYCLOAK_CONFIG_PARAM_NAME);
|
||||
if (dmrConfig == null) return null;
|
||||
|
||||
ModelNode dmrConfigNode = ModelNode.fromString(dmrConfig);
|
||||
if (dmrConfigNode.asPropertyList().isEmpty()) return null;
|
||||
|
||||
// note that we need to resolve expressions BEFORE we convert to JSON
|
||||
return dmrConfigNode.resolve().toJSONString(true);
|
||||
}
|
||||
|
||||
public static KeycloakSessionFactory createSessionFactory() {
|
||||
DefaultKeycloakSessionFactory factory = new DefaultKeycloakSessionFactory();
|
||||
factory.init();
|
||||
@@ -386,4 +418,13 @@ public class KeycloakApplication extends Application {
|
||||
}
|
||||
}
|
||||
|
||||
private void exit(int status) {
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
System.exit(status);
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
|
||||
}
|
||||
+17
-2
@@ -66,6 +66,10 @@ import java.util.Map;
|
||||
*/
|
||||
public class ClientAttributeCertificateResource {
|
||||
|
||||
public static final String CERTIFICATE_PEM = "Certificate PEM";
|
||||
public static final String PUBLIC_KEY_PEM = "Public Key PEM";
|
||||
public static final String JSON_WEB_KEY_SET = "JSON Web Key Set";
|
||||
|
||||
protected RealmModel realm;
|
||||
private RealmAuth auth;
|
||||
protected ClientModel client;
|
||||
@@ -195,12 +199,23 @@ public class ClientAttributeCertificateResource {
|
||||
Map<String, List<InputPart>> uploadForm = input.getFormDataMap();
|
||||
String keystoreFormat = uploadForm.get("keystoreFormat").get(0).getBodyAsString();
|
||||
List<InputPart> inputParts = uploadForm.get("file");
|
||||
if (keystoreFormat.equals("Certificate PEM")) {
|
||||
if (keystoreFormat.equals(CERTIFICATE_PEM)) {
|
||||
String pem = StreamUtil.readString(inputParts.get(0).getBody(InputStream.class, null));
|
||||
|
||||
// Validate format
|
||||
KeycloakModelUtils.getCertificate(pem);
|
||||
|
||||
info.setCertificate(pem);
|
||||
return info;
|
||||
} else if (keystoreFormat.equals(PUBLIC_KEY_PEM)) {
|
||||
String pem = StreamUtil.readString(inputParts.get(0).getBody(InputStream.class, null));
|
||||
|
||||
} else if (keystoreFormat.equals("JSON Web Key Set (JWK)")) {
|
||||
// Validate format
|
||||
KeycloakModelUtils.getPublicKey(pem);
|
||||
|
||||
info.setPublicKey(pem);
|
||||
return info;
|
||||
} else if (keystoreFormat.equals(JSON_WEB_KEY_SET)) {
|
||||
InputStream stream = inputParts.get(0).getBody(InputStream.class, null);
|
||||
JSONWebKeySet keySet = JsonSerialization.readValue(stream, JSONWebKeySet.class);
|
||||
PublicKey publicKey = JWKSUtils.getKeyForUse(keySet, JWK.Use.SIG);
|
||||
|
||||
@@ -40,6 +40,7 @@ import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.representations.idm.CredentialRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.representations.idm.UserSessionRepresentation;
|
||||
import org.keycloak.services.ErrorResponseException;
|
||||
import org.keycloak.services.ServicesLogger;
|
||||
import org.keycloak.services.clientregistration.ClientRegistrationTokenUtils;
|
||||
import org.keycloak.services.managers.ClientManager;
|
||||
@@ -48,6 +49,8 @@ import org.keycloak.services.managers.ResourceAdminManager;
|
||||
import org.keycloak.services.resources.KeycloakApplication;
|
||||
import org.keycloak.services.ErrorResponse;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.services.validation.ClientValidator;
|
||||
import org.keycloak.services.validation.ValidationMessages;
|
||||
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.DELETE;
|
||||
@@ -63,10 +66,7 @@ import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
|
||||
import static java.lang.Boolean.TRUE;
|
||||
|
||||
@@ -126,6 +126,16 @@ public class ClientResource {
|
||||
throw new NotFoundException("Could not find client");
|
||||
}
|
||||
|
||||
ValidationMessages validationMessages = new ValidationMessages();
|
||||
if (!ClientValidator.validate(rep, validationMessages)) {
|
||||
Properties messages = AdminRoot.getMessages(session, realm, auth.getAuth().getToken().getLocale());
|
||||
throw new ErrorResponseException(
|
||||
validationMessages.getStringMessages(),
|
||||
validationMessages.getStringMessages(messages),
|
||||
Response.Status.BAD_REQUEST
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
updateClientFromRep(rep, client, session);
|
||||
adminEvent.operation(OperationType.UPDATE).resourcePath(uriInfo).representation(rep).success();
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
package org.keycloak.services.resources.admin;
|
||||
|
||||
import org.jboss.resteasy.annotations.cache.NoCache;
|
||||
import org.jboss.resteasy.spi.NotFoundException;
|
||||
import org.jboss.resteasy.spi.ResteasyProviderFactory;
|
||||
import org.keycloak.events.admin.OperationType;
|
||||
import org.keycloak.events.admin.ResourceType;
|
||||
@@ -28,16 +27,13 @@ import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.utils.ModelToRepresentation;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.services.ErrorResponse;
|
||||
import org.keycloak.services.ErrorResponseException;
|
||||
import org.keycloak.services.ServicesLogger;
|
||||
import org.keycloak.services.managers.ClientManager;
|
||||
import org.keycloak.services.validation.ClientValidator;
|
||||
import org.keycloak.services.validation.ValidationMessages;
|
||||
|
||||
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.QueryParam;
|
||||
import javax.ws.rs.*;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
@@ -45,6 +41,7 @@ import javax.ws.rs.core.UriInfo;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
|
||||
/**
|
||||
* Base resource class for managing a realm's clients.
|
||||
@@ -122,6 +119,16 @@ public class ClientsResource {
|
||||
public Response createClient(final @Context UriInfo uriInfo, final ClientRepresentation rep) {
|
||||
auth.requireManage();
|
||||
|
||||
ValidationMessages validationMessages = new ValidationMessages();
|
||||
if (!ClientValidator.validate(rep, validationMessages)) {
|
||||
Properties messages = AdminRoot.getMessages(session, realm, auth.getAuth().getToken().getLocale());
|
||||
throw new ErrorResponseException(
|
||||
validationMessages.getStringMessages(),
|
||||
validationMessages.getStringMessages(messages),
|
||||
Response.Status.BAD_REQUEST
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
ClientModel clientModel = ClientManager.createClient(session, realm, rep, true);
|
||||
|
||||
|
||||
+2
-2
@@ -116,7 +116,7 @@ public class IdentityProvidersResource {
|
||||
InputPart file = formDataMap.get("file").get(0);
|
||||
InputStream inputStream = file.getBody(InputStream.class, null);
|
||||
IdentityProviderFactory providerFactory = getProviderFactorytById(providerId);
|
||||
Map<String, String> config = providerFactory.parseConfig(inputStream);
|
||||
Map<String, String> config = providerFactory.parseConfig(session, inputStream);
|
||||
return config;
|
||||
}
|
||||
|
||||
@@ -143,7 +143,7 @@ public class IdentityProvidersResource {
|
||||
try {
|
||||
IdentityProviderFactory providerFactory = getProviderFactorytById(providerId);
|
||||
Map<String, String> config;
|
||||
config = providerFactory.parseConfig(inputStream);
|
||||
config = providerFactory.parseConfig(session, inputStream);
|
||||
return config;
|
||||
} finally {
|
||||
try {
|
||||
|
||||
@@ -17,9 +17,18 @@
|
||||
|
||||
package org.keycloak.services.util;
|
||||
|
||||
import java.security.PublicKey;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.HashMap;
|
||||
|
||||
import javax.ws.rs.core.Response;
|
||||
|
||||
import org.keycloak.authentication.AuthenticationFlowError;
|
||||
import org.keycloak.authentication.ClientAuthenticationFlowContext;
|
||||
import org.keycloak.authentication.authenticators.client.ClientAuthUtil;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.ModelException;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.representations.idm.CertificateRepresentation;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
|
||||
@@ -34,6 +43,8 @@ public class CertificateInfoHelper {
|
||||
public static final String PUBLIC_KEY = "public.key";
|
||||
|
||||
|
||||
// CLIENT MODEL METHODS
|
||||
|
||||
public static CertificateRepresentation getCertificateFromClient(ClientModel client, String attributePrefix) {
|
||||
String privateKeyAttribute = attributePrefix + "." + PRIVATE_KEY;
|
||||
String certificateAttribute = attributePrefix + "." + X509CERTIFICATE;
|
||||
@@ -75,6 +86,32 @@ public class CertificateInfoHelper {
|
||||
}
|
||||
|
||||
|
||||
public static PublicKey getSignatureValidationKey(ClientModel client, String attributePrefix) throws ModelException {
|
||||
CertificateRepresentation certInfo = getCertificateFromClient(client, attributePrefix);
|
||||
|
||||
String encodedCertificate = certInfo.getCertificate();
|
||||
String encodedPublicKey = certInfo.getPublicKey();
|
||||
|
||||
if (encodedCertificate == null && encodedPublicKey == null) {
|
||||
throw new ModelException("Client doesn't have certificate or publicKey configured");
|
||||
}
|
||||
|
||||
if (encodedCertificate != null && encodedPublicKey != null) {
|
||||
throw new ModelException("Client has both publicKey and certificate configured");
|
||||
}
|
||||
|
||||
// TODO: Caching of publicKeys / certificates, so it doesn't need to be always computed from pem. For performance reasons...
|
||||
if (encodedCertificate != null) {
|
||||
X509Certificate clientCert = KeycloakModelUtils.getCertificate(encodedCertificate);
|
||||
return clientCert.getPublicKey();
|
||||
} else {
|
||||
return KeycloakModelUtils.getPublicKey(encodedPublicKey);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// CLIENT REPRESENTATION METHODS
|
||||
|
||||
public static void updateClientRepresentationCertificateInfo(ClientRepresentation client, CertificateRepresentation rep, String attributePrefix) {
|
||||
String privateKeyAttribute = attributePrefix + "." + PRIVATE_KEY;
|
||||
String certificateAttribute = attributePrefix + "." + X509CERTIFICATE;
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
*
|
||||
* * Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
* * and other contributors as indicated by the @author tags.
|
||||
* *
|
||||
* * Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* * you may not use this file except in compliance with the License.
|
||||
* * You may obtain a copy of the License at
|
||||
* *
|
||||
* * http://www.apache.org/licenses/LICENSE-2.0
|
||||
* *
|
||||
* * Unless required by applicable law or agreed to in writing, software
|
||||
* * distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* * See the License for the specific language governing permissions and
|
||||
* * limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.keycloak.services.validation;
|
||||
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
|
||||
/**
|
||||
* @author Vaclav Muzikar <vmuzikar@redhat.com>
|
||||
*/
|
||||
public class ClientValidator {
|
||||
/**
|
||||
* Checks if the Client's Redirect URIs doesn't contain any URI fragments (like http://example.org/auth#fragment)
|
||||
*
|
||||
* @see <a href="https://issues.jboss.org/browse/KEYCLOAK-3421">KEYCLOAK-3421</a>
|
||||
* @param client
|
||||
* @param messages
|
||||
* @return true if Redirect URIs doesn't contain any URI with fragments
|
||||
*/
|
||||
public static boolean validate(ClientRepresentation client, ValidationMessages messages) {
|
||||
boolean isValid = true;
|
||||
|
||||
if (client.getRedirectUris() != null) {
|
||||
long urisWithFragmentCount = client.getRedirectUris().stream().filter(p -> p.contains("#")).count();
|
||||
if (urisWithFragmentCount > 0) {
|
||||
messages.add("redirectUris", "Redirect URIs must not contain an URI fragment", "clientRedirectURIsFragmentError");
|
||||
isValid = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (client.getRootUrl() != null && client.getRootUrl().contains("#")) {
|
||||
messages.add("rootUrl", "Root URL must not contain an URL fragment", "clientRootURLFragmentError");
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
return isValid;
|
||||
}
|
||||
}
|
||||
@@ -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.services.validation;
|
||||
|
||||
import java.text.MessageFormat;
|
||||
import java.util.Properties;
|
||||
|
||||
/**
|
||||
* @author Vaclav Muzikar <vmuzikar@redhat.com>
|
||||
*/
|
||||
public class ValidationMessage {
|
||||
private String fieldId;
|
||||
private String message;
|
||||
private String localizedMessageKey;
|
||||
private Object[] localizedMessageParameters;
|
||||
|
||||
public ValidationMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public ValidationMessage(String message, String localizedMessageKey, Object... localizedMessageParameters) {
|
||||
this.message = message;
|
||||
this.localizedMessageKey = localizedMessageKey;
|
||||
this.localizedMessageParameters = localizedMessageParameters;
|
||||
}
|
||||
|
||||
public String getFieldId() {
|
||||
return fieldId;
|
||||
}
|
||||
|
||||
public void setFieldId(String fieldId) {
|
||||
this.fieldId = fieldId;
|
||||
}
|
||||
|
||||
public String getLocalizedMessageKey() {
|
||||
return localizedMessageKey;
|
||||
}
|
||||
|
||||
public void setLocalizedMessageKey(String localizedMessageKey) {
|
||||
this.localizedMessageKey = localizedMessageKey;
|
||||
}
|
||||
|
||||
public Object[] getLocalizedMessageParameters() {
|
||||
return localizedMessageParameters;
|
||||
}
|
||||
|
||||
public void setLocalizedMessageParameters(Object[] localizedMessageParameters) {
|
||||
this.localizedMessageParameters = localizedMessageParameters;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public String getMessage(Properties localizedMessages) {
|
||||
if (getLocalizedMessageKey() != null) {
|
||||
return MessageFormat.format(localizedMessages.getProperty(getLocalizedMessageKey(), getMessage()), getLocalizedMessageParameters());
|
||||
}
|
||||
else {
|
||||
return getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
ValidationMessage message1 = (ValidationMessage) o;
|
||||
|
||||
if (getFieldId() != null ? !getFieldId().equals(message1.getFieldId()) : message1.getFieldId() != null)
|
||||
return false;
|
||||
return getMessage() != null ? getMessage().equals(message1.getMessage()) : message1.getMessage() == null;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = getFieldId() != null ? getFieldId().hashCode() : 0;
|
||||
result = 31 * result + (getMessage() != null ? getMessage().hashCode() : 0);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
*
|
||||
* * Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
* * and other contributors as indicated by the @author tags.
|
||||
* *
|
||||
* * Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* * you may not use this file except in compliance with the License.
|
||||
* * You may obtain a copy of the License at
|
||||
* *
|
||||
* * http://www.apache.org/licenses/LICENSE-2.0
|
||||
* *
|
||||
* * Unless required by applicable law or agreed to in writing, software
|
||||
* * distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* * See the License for the specific language governing permissions and
|
||||
* * limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.keycloak.services.validation;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
|
||||
/**
|
||||
* @author Vaclav Muzikar <vmuzikar@redhat.com>
|
||||
*/
|
||||
public class ValidationMessages {
|
||||
private Set<ValidationMessage> messages = new LinkedHashSet<>();
|
||||
|
||||
public ValidationMessages() {}
|
||||
|
||||
public ValidationMessages(String... messages) {
|
||||
for (String message : messages) {
|
||||
add(message);
|
||||
}
|
||||
}
|
||||
|
||||
public void add(String message) {
|
||||
messages.add(new ValidationMessage(message));
|
||||
}
|
||||
|
||||
public void add(String message, String localizedMessageKey) {
|
||||
messages.add(new ValidationMessage(message, localizedMessageKey));
|
||||
}
|
||||
|
||||
public void add(String fieldId, String message, String localizedMessageKey) {
|
||||
ValidationMessage validationMessage = new ValidationMessage(message, localizedMessageKey);
|
||||
validationMessage.setFieldId(fieldId);
|
||||
add(validationMessage);
|
||||
}
|
||||
|
||||
public void add(ValidationMessage message) {
|
||||
messages.add(message);
|
||||
}
|
||||
|
||||
public boolean fieldHasError(String fieldId) {
|
||||
for (ValidationMessage message : messages) {
|
||||
if (message.getFieldId().equals(fieldId)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public Set<ValidationMessage> getMessages() {
|
||||
return Collections.unmodifiableSet(messages);
|
||||
}
|
||||
|
||||
protected String getStringMessages(Function<? super ValidationMessage, ? extends String> function) {
|
||||
return messages.stream().map(function).collect(Collectors.joining("; "));
|
||||
}
|
||||
|
||||
public String getStringMessages() {
|
||||
return getStringMessages(ValidationMessage::getMessage);
|
||||
}
|
||||
|
||||
public String getStringMessages(Properties localizedMessages) {
|
||||
return getStringMessages(x -> x.getMessage(localizedMessages));
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@ import java.net.Socket;
|
||||
* <p>
|
||||
* This SSLSocketFactory can only use truststore configured by TruststoreProvider after the ProviderFactory was
|
||||
* initialized using standard Spi load / init mechanism. That will only happen if "truststore" provider is configured
|
||||
* in keycloak-server.json.
|
||||
* in standalone.xml or domain.xml.
|
||||
* <p>
|
||||
* If TruststoreProvider is not available this SSLSocketFactory will delegate all operations to javax.net.ssl.SSLSocketFactory.getDefault().
|
||||
*
|
||||
|
||||
@@ -31,4 +31,7 @@ public class MediaType {
|
||||
public static final String APPLICATION_FORM_URLENCODED = javax.ws.rs.core.MediaType.APPLICATION_FORM_URLENCODED;
|
||||
public static final javax.ws.rs.core.MediaType APPLICATION_FORM_URLENCODED_TYPE = javax.ws.rs.core.MediaType.APPLICATION_FORM_URLENCODED_TYPE;
|
||||
|
||||
public static final String APPLICATION_JWT = "application/jwt";
|
||||
public static final javax.ws.rs.core.MediaType APPLICATION_JWT_TYPE = new javax.ws.rs.core.MediaType("application", "jwt");
|
||||
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
<!--
|
||||
~ Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
~ and other contributors as indicated by the @author tags.
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
|
||||
<project name="inject-truststore" basedir="." default="inject-truststore">
|
||||
|
||||
<scriptdef name="inject-truststore" language="javascript" manager="bsf">
|
||||
<attribute name="path"/>
|
||||
<![CDATA[
|
||||
importClass(Packages.java.io.File);
|
||||
importClass(Packages.org.keycloak.util.JsonSerialization);
|
||||
|
||||
path = attributes.get("path");
|
||||
file = new File(path + "/standalone/configuration/keycloak-server.json");
|
||||
root = JsonSerialization.mapper.readTree(file);
|
||||
|
||||
jNode = JsonSerialization.mapper.createObjectNode();
|
||||
//jNode.put("file","${auth.server.home:" + path + "}/standalone/configuration/keycloak.truststore");
|
||||
jNode.put("file","${jboss.home.dir}/standalone/configuration/keycloak.truststore");
|
||||
jNode.put("password","secret");
|
||||
jNode.put("hostname-verification-policy","WILDCARD");
|
||||
jNode.put("disabled","false");
|
||||
|
||||
fileNode = JsonSerialization.mapper.createObjectNode();
|
||||
fileNode.set("file", jNode);
|
||||
truststoreNode = JsonSerialization.mapper.createObjectNode();
|
||||
truststoreNode.set("truststore", fileNode);
|
||||
|
||||
root.putAll(truststoreNode);
|
||||
|
||||
// save file
|
||||
JsonSerialization.prettyMapper.writeValue(file, root);
|
||||
]]>
|
||||
</scriptdef>
|
||||
|
||||
<target name="inject-truststore">
|
||||
<inject-truststore path="${auth.server.home}"/>
|
||||
</target>
|
||||
</project>
|
||||
@@ -1,42 +0,0 @@
|
||||
<!--
|
||||
~ Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
~ and other contributors as indicated by the @author tags.
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
|
||||
<project name="inject-provider" basedir="." default="inject-provider">
|
||||
|
||||
<scriptdef name="inject-provider" language="javascript" manager="bsf">
|
||||
<attribute name="path"/>
|
||||
<![CDATA[
|
||||
importClass(Packages.java.io.File);
|
||||
importClass(Packages.org.keycloak.util.JsonSerialization);
|
||||
|
||||
path = attributes.get("path");
|
||||
file = new File(path);
|
||||
root = JsonSerialization.mapper.readTree(file);
|
||||
|
||||
// inject provider
|
||||
providers = root.withArray("providers");
|
||||
providers.add("module:org.keycloak.testsuite.integration-arquillian-testsuite-providers");
|
||||
|
||||
// save file
|
||||
JsonSerialization.prettyMapper.writeValue(file, root);
|
||||
]]>
|
||||
</scriptdef>
|
||||
|
||||
<target name="inject-provider">
|
||||
<inject-provider path="${auth.server.home}/standalone/configuration/keycloak-server.json"/>
|
||||
</target>
|
||||
</project>
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
|
||||
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
|
||||
xmlns:xalan="http://xml.apache.org/xalan"
|
||||
version="2.0"
|
||||
exclude-result-prefixes="xalan">
|
||||
|
||||
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes" xalan:indent-amount="4" standalone="no"/>
|
||||
<xsl:strip-space elements="*"/>
|
||||
|
||||
<xsl:variable name="nsKS" select="'urn:jboss:domain:keycloak-server'"/>
|
||||
<xsl:variable name="truststoreDefinition">
|
||||
<spi name="truststore">
|
||||
<provider name="file" enabled="true">
|
||||
<properties>
|
||||
<property name="file" value="${{jboss.home.dir}}/standalone/configuration/keycloak.truststore"/>
|
||||
<property name="password" value="secret"/>
|
||||
<property name="hostname-verification-policy" value="WILDCARD"/>
|
||||
<property name="disabled" value="false"/>
|
||||
</properties>
|
||||
</provider>
|
||||
</spi>
|
||||
</xsl:variable>
|
||||
|
||||
<!--inject provider-->
|
||||
<xsl:template match="//*[local-name()='providers']/*[local-name()='provider']">
|
||||
<xsl:copy>
|
||||
<xsl:apply-templates select="@*|node()" />
|
||||
</xsl:copy>
|
||||
<provider>
|
||||
<xsl:text>module:org.keycloak.testsuite.integration-arquillian-testsuite-providers</xsl:text>
|
||||
</provider>
|
||||
</xsl:template>
|
||||
|
||||
<!--inject truststore-->
|
||||
<xsl:template match="//*[local-name()='subsystem' and starts-with(namespace-uri(), $nsKS)]">
|
||||
<xsl:copy>
|
||||
<xsl:apply-templates select="@*|node()" />
|
||||
<xsl:copy-of select="$truststoreDefinition"/>
|
||||
</xsl:copy>
|
||||
</xsl:template>
|
||||
|
||||
<!--copy everything else-->
|
||||
<xsl:template match="@*|node()">
|
||||
<xsl:copy>
|
||||
<xsl:apply-templates select="@*|node()" />
|
||||
</xsl:copy>
|
||||
</xsl:template>
|
||||
|
||||
</xsl:stylesheet>
|
||||
@@ -142,74 +142,6 @@
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-antrun-plugin</artifactId>
|
||||
<version>1.8</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>inject-into-keycloak-server-json</id>
|
||||
<phase>process-resources</phase>
|
||||
<goals>
|
||||
<goal>run</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<target>
|
||||
<ant antfile="../build.xml" inheritRefs="true">
|
||||
<target name="inject-provider"/>
|
||||
</ant>
|
||||
</target>
|
||||
</configuration>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>inject-truststore-into-keycloak-server-json</id>
|
||||
<phase>process-resources</phase>
|
||||
<goals>
|
||||
<goal>run</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<target>
|
||||
<ant antfile="../build-truststore.xml" inheritRefs="true">
|
||||
<target name="inject-truststore"/>
|
||||
</ant>
|
||||
</target>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>ant-contrib</groupId>
|
||||
<artifactId>ant-contrib</artifactId>
|
||||
<version>1.0b3</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>ant</groupId>
|
||||
<artifactId>ant</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.ant</groupId>
|
||||
<artifactId>ant-apache-bsf</artifactId>
|
||||
<version>1.9.3</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.bsf</groupId>
|
||||
<artifactId>bsf-api</artifactId>
|
||||
<version>3.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>rhino</groupId>
|
||||
<artifactId>js</artifactId>
|
||||
<version>1.7R2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-core</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-enforcer-plugin</artifactId>
|
||||
</plugin>
|
||||
@@ -247,6 +179,25 @@
|
||||
</transformationSets>
|
||||
</configuration>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>inject-provider-and-truststore</id>
|
||||
<phase>process-resources</phase>
|
||||
<goals>
|
||||
<goal>transform</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<transformationSets>
|
||||
<transformationSet>
|
||||
<dir>${auth.server.home}/standalone/configuration</dir>
|
||||
<includes>
|
||||
<include>standalone.xml</include>
|
||||
</includes>
|
||||
<stylesheet>${common.resources}/keycloak-server-subsystem.xsl</stylesheet>
|
||||
<outputDir>${auth.server.home}/standalone/configuration</outputDir>
|
||||
</transformationSet>
|
||||
</transformationSets>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
@@ -318,7 +269,6 @@
|
||||
</build>
|
||||
</profile>
|
||||
|
||||
|
||||
<!-- OPT-IN -->
|
||||
<profile>
|
||||
<id>server-overlay</id>
|
||||
|
||||
+10
-1
@@ -27,6 +27,8 @@ import org.keycloak.representations.adapters.action.PushNotBeforeAction;
|
||||
import org.keycloak.representations.adapters.action.TestAvailabilityAction;
|
||||
import org.keycloak.services.resource.RealmResourceProvider;
|
||||
import org.keycloak.services.resources.RealmsResource;
|
||||
import org.keycloak.testsuite.rest.resource.TestingExportImportResource;
|
||||
import org.keycloak.testsuite.rest.resource.TestingOIDCEndpointsApplicationResource;
|
||||
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.GET;
|
||||
@@ -53,14 +55,16 @@ public class TestApplicationResourceProvider implements RealmResourceProvider {
|
||||
private final BlockingQueue<LogoutAction> adminLogoutActions;
|
||||
private final BlockingQueue<PushNotBeforeAction> adminPushNotBeforeActions;
|
||||
private final BlockingQueue<TestAvailabilityAction> adminTestAvailabilityAction;
|
||||
private final TestApplicationResourceProviderFactory.OIDCClientData oidcClientData;
|
||||
|
||||
public TestApplicationResourceProvider(KeycloakSession session, BlockingQueue<LogoutAction> adminLogoutActions,
|
||||
BlockingQueue<PushNotBeforeAction> adminPushNotBeforeActions,
|
||||
BlockingQueue<TestAvailabilityAction> adminTestAvailabilityAction) {
|
||||
BlockingQueue<TestAvailabilityAction> adminTestAvailabilityAction, TestApplicationResourceProviderFactory.OIDCClientData oidcClientData) {
|
||||
this.session = session;
|
||||
this.adminLogoutActions = adminLogoutActions;
|
||||
this.adminPushNotBeforeActions = adminPushNotBeforeActions;
|
||||
this.adminTestAvailabilityAction = adminTestAvailabilityAction;
|
||||
this.oidcClientData = oidcClientData;
|
||||
}
|
||||
|
||||
@POST
|
||||
@@ -164,6 +168,11 @@ public class TestApplicationResourceProvider implements RealmResourceProvider {
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Path("/oidc-client-endpoints")
|
||||
public TestingOIDCEndpointsApplicationResource getTestingOIDCClientEndpoints() {
|
||||
return new TestingOIDCEndpointsApplicationResource(oidcClientData);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getResource() {
|
||||
return this;
|
||||
|
||||
+26
-1
@@ -28,6 +28,7 @@ import org.keycloak.representations.adapters.action.TestAvailabilityAction;
|
||||
import org.keycloak.services.resource.RealmResourceProvider;
|
||||
import org.keycloak.services.resource.RealmResourceProviderFactory;
|
||||
|
||||
import java.security.KeyPair;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.LinkedBlockingDeque;
|
||||
|
||||
@@ -40,9 +41,11 @@ public class TestApplicationResourceProviderFactory implements RealmResourceProv
|
||||
private BlockingQueue<PushNotBeforeAction> pushNotBeforeActions = new LinkedBlockingDeque<>();
|
||||
private BlockingQueue<TestAvailabilityAction> testAvailabilityActions = new LinkedBlockingDeque<>();
|
||||
|
||||
private final OIDCClientData oidcClientData = new OIDCClientData();
|
||||
|
||||
@Override
|
||||
public RealmResourceProvider create(KeycloakSession session) {
|
||||
return new TestApplicationResourceProvider(session, adminLogoutActions, pushNotBeforeActions, testAvailabilityActions);
|
||||
return new TestApplicationResourceProvider(session, adminLogoutActions, pushNotBeforeActions, testAvailabilityActions, oidcClientData);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -62,4 +65,26 @@ public class TestApplicationResourceProviderFactory implements RealmResourceProv
|
||||
return "app";
|
||||
}
|
||||
|
||||
|
||||
public static class OIDCClientData {
|
||||
|
||||
private KeyPair signingKeyPair;
|
||||
private String oidcRequest;
|
||||
|
||||
public KeyPair getSigningKeyPair() {
|
||||
return signingKeyPair;
|
||||
}
|
||||
|
||||
public void setSigningKeyPair(KeyPair signingKeyPair) {
|
||||
this.signingKeyPair = signingKeyPair;
|
||||
}
|
||||
|
||||
public String getOidcRequest() {
|
||||
return oidcRequest;
|
||||
}
|
||||
|
||||
public void setOidcRequest(String oidcRequest) {
|
||||
this.oidcRequest = oidcRequest;
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
-90
@@ -23,15 +23,20 @@ import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import org.infinispan.Cache;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
|
||||
import org.keycloak.events.Event;
|
||||
import org.keycloak.events.admin.AdminEvent;
|
||||
import org.keycloak.events.admin.ResourceType;
|
||||
import org.keycloak.jose.jws.Algorithm;
|
||||
import org.keycloak.jose.jws.JWSBuilder;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.models.utils.ModelToRepresentation;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.protocol.oidc.endpoints.request.AuthorizationEndpointRequest;
|
||||
import org.keycloak.representations.idm.AdminEventRepresentation;
|
||||
import org.keycloak.representations.idm.EventRepresentation;
|
||||
import org.keycloak.services.managers.ClientSessionCode;
|
||||
@@ -76,6 +81,7 @@ import org.keycloak.models.UserProvider;
|
||||
import org.keycloak.representations.idm.AuthDetailsRepresentation;
|
||||
import org.keycloak.representations.idm.AuthenticationFlowRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.testsuite.rest.resource.TestingExportImportResource;
|
||||
|
||||
import static org.keycloak.exportimport.ExportImportConfig.*;
|
||||
|
||||
@@ -564,22 +570,6 @@ public class TestingResourceProvider implements RealmResourceProvider {
|
||||
return result;
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/run-import")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Response runImport() {
|
||||
new ExportImportManager(session).runImport();
|
||||
return Response.ok().build();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/run-export")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Response runExport() {
|
||||
new ExportImportManager(session).runExport();
|
||||
return Response.ok().build();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/valid-credentials")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@@ -647,83 +637,14 @@ public class TestingResourceProvider implements RealmResourceProvider {
|
||||
return ModelToRepresentation.toRepresentation(user);
|
||||
}
|
||||
|
||||
@Path("/export-import")
|
||||
public TestingExportImportResource getExportImportResource() {
|
||||
return new TestingExportImportResource(session);
|
||||
}
|
||||
|
||||
private RealmModel getRealmByName(String realmName) {
|
||||
RealmProvider realmProvider = session.getProvider(RealmProvider.class);
|
||||
return realmProvider.getRealmByName(realmName);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/get-users-per-file")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Integer getUsersPerFile() {
|
||||
String usersPerFile = System.getProperty(USERS_PER_FILE, String.valueOf(DEFAULT_USERS_PER_FILE));
|
||||
return Integer.parseInt(usersPerFile.trim());
|
||||
}
|
||||
|
||||
@PUT
|
||||
@Path("/set-users-per-file")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
public void setUsersPerFile(@QueryParam("usersPerFile") Integer usersPerFile) {
|
||||
System.setProperty(USERS_PER_FILE, String.valueOf(usersPerFile));
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/get-dir")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public String getDir() {
|
||||
return System.getProperty(DIR);
|
||||
}
|
||||
|
||||
@PUT
|
||||
@Path("/set-dir")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public String setDir(@QueryParam("dir") String dir) {
|
||||
return System.setProperty(DIR, dir);
|
||||
}
|
||||
|
||||
@PUT
|
||||
@Path("/export-import-provider")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
public void setProvider(@QueryParam("exportImportProvider") String exportImportProvider) {
|
||||
System.setProperty(PROVIDER, exportImportProvider);
|
||||
}
|
||||
|
||||
@PUT
|
||||
@Path("/export-import-file")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
public void setFile(@QueryParam("file") String file) {
|
||||
System.setProperty(FILE, file);
|
||||
}
|
||||
|
||||
@PUT
|
||||
@Path("/export-import-action")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
public void setAction(@QueryParam("exportImportAction") String exportImportAction) {
|
||||
System.setProperty(ACTION, exportImportAction);
|
||||
}
|
||||
|
||||
@PUT
|
||||
@Path("/set-realm-name")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
public void setRealmName(@QueryParam("realmName") String realmName) {
|
||||
if (realmName != null && !realmName.isEmpty()) {
|
||||
System.setProperty(REALM_NAME, realmName);
|
||||
} else {
|
||||
System.getProperties().remove(REALM_NAME);
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/get-test-dir")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public String getExportImportTestDirectory() {
|
||||
System.setProperty("project.build.directory", "target");
|
||||
String absolutePath = new File(System.getProperty("project.build.directory", "target")).getAbsolutePath();
|
||||
return absolutePath;
|
||||
}
|
||||
|
||||
}
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
* 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.rest.resource;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.PUT;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
|
||||
import org.keycloak.exportimport.ExportImportManager;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
|
||||
import static org.keycloak.exportimport.ExportImportConfig.ACTION;
|
||||
import static org.keycloak.exportimport.ExportImportConfig.DEFAULT_USERS_PER_FILE;
|
||||
import static org.keycloak.exportimport.ExportImportConfig.DIR;
|
||||
import static org.keycloak.exportimport.ExportImportConfig.FILE;
|
||||
import static org.keycloak.exportimport.ExportImportConfig.PROVIDER;
|
||||
import static org.keycloak.exportimport.ExportImportConfig.REALM_NAME;
|
||||
import static org.keycloak.exportimport.ExportImportConfig.USERS_PER_FILE;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class TestingExportImportResource {
|
||||
|
||||
private final KeycloakSession session;
|
||||
|
||||
public TestingExportImportResource(KeycloakSession session) {
|
||||
this.session = session;
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/run-import")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Response runImport() {
|
||||
new ExportImportManager(session).runImport();
|
||||
return Response.ok().build();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/run-export")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Response runExport() {
|
||||
new ExportImportManager(session).runExport();
|
||||
return Response.ok().build();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/get-users-per-file")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Integer getUsersPerFile() {
|
||||
String usersPerFile = System.getProperty(USERS_PER_FILE, String.valueOf(DEFAULT_USERS_PER_FILE));
|
||||
return Integer.parseInt(usersPerFile.trim());
|
||||
}
|
||||
|
||||
@PUT
|
||||
@Path("/set-users-per-file")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
public void setUsersPerFile(@QueryParam("usersPerFile") Integer usersPerFile) {
|
||||
System.setProperty(USERS_PER_FILE, String.valueOf(usersPerFile));
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/get-dir")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public String getDir() {
|
||||
return System.getProperty(DIR);
|
||||
}
|
||||
|
||||
@PUT
|
||||
@Path("/set-dir")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public String setDir(@QueryParam("dir") String dir) {
|
||||
return System.setProperty(DIR, dir);
|
||||
}
|
||||
|
||||
@PUT
|
||||
@Path("/export-import-provider")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
public void setProvider(@QueryParam("exportImportProvider") String exportImportProvider) {
|
||||
System.setProperty(PROVIDER, exportImportProvider);
|
||||
}
|
||||
|
||||
@PUT
|
||||
@Path("/export-import-file")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
public void setFile(@QueryParam("file") String file) {
|
||||
System.setProperty(FILE, file);
|
||||
}
|
||||
|
||||
@PUT
|
||||
@Path("/export-import-action")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
public void setAction(@QueryParam("exportImportAction") String exportImportAction) {
|
||||
System.setProperty(ACTION, exportImportAction);
|
||||
}
|
||||
|
||||
@PUT
|
||||
@Path("/set-realm-name")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
public void setRealmName(@QueryParam("realmName") String realmName) {
|
||||
if (realmName != null && !realmName.isEmpty()) {
|
||||
System.setProperty(REALM_NAME, realmName);
|
||||
} else {
|
||||
System.getProperties().remove(REALM_NAME);
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/get-test-dir")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public String getExportImportTestDirectory() {
|
||||
System.setProperty("project.build.directory", "target");
|
||||
String absolutePath = new File(System.getProperty("project.build.directory", "target")).getAbsolutePath();
|
||||
return absolutePath;
|
||||
}
|
||||
}
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
* 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.rest.resource;
|
||||
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PrivateKey;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
|
||||
import org.jboss.resteasy.annotations.cache.NoCache;
|
||||
import org.jboss.resteasy.spi.BadRequestException;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.jose.jwk.JSONWebKeySet;
|
||||
import org.keycloak.jose.jwk.JWK;
|
||||
import org.keycloak.jose.jwk.JWKBuilder;
|
||||
import org.keycloak.jose.jws.Algorithm;
|
||||
import org.keycloak.jose.jws.JWSBuilder;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.testsuite.rest.TestApplicationResourceProviderFactory;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class TestingOIDCEndpointsApplicationResource {
|
||||
|
||||
public static final String PRIVATE_KEY = "privateKey";
|
||||
public static final String PUBLIC_KEY = "publicKey";
|
||||
|
||||
private final TestApplicationResourceProviderFactory.OIDCClientData clientData;
|
||||
|
||||
public TestingOIDCEndpointsApplicationResource(TestApplicationResourceProviderFactory.OIDCClientData oidcClientData) {
|
||||
this.clientData = oidcClientData;
|
||||
}
|
||||
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Path("/generate-keys")
|
||||
@NoCache
|
||||
public Map<String, String> generateKeys() {
|
||||
try {
|
||||
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
|
||||
generator.initialize(2048);
|
||||
clientData.setSigningKeyPair(generator.generateKeyPair());
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new BadRequestException("Error generating signing keypair", e);
|
||||
}
|
||||
|
||||
String privateKeyPem = KeycloakModelUtils.getPemFromKey(clientData.getSigningKeyPair().getPrivate());
|
||||
String publicKeyPem = KeycloakModelUtils.getPemFromKey(clientData.getSigningKeyPair().getPublic());
|
||||
|
||||
Map<String, String> res = new HashMap<>();
|
||||
res.put(PRIVATE_KEY, privateKeyPem);
|
||||
res.put(PUBLIC_KEY, publicKeyPem);
|
||||
return res;
|
||||
}
|
||||
|
||||
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Path("/get-jwks")
|
||||
@NoCache
|
||||
public JSONWebKeySet getJwks() {
|
||||
JSONWebKeySet keySet = new JSONWebKeySet();
|
||||
|
||||
if (clientData.getSigningKeyPair() == null) {
|
||||
keySet.setKeys(new JWK[] {});
|
||||
} else {
|
||||
keySet.setKeys(new JWK[] { JWKBuilder.create().rs256(clientData.getSigningKeyPair().getPublic()) });
|
||||
}
|
||||
|
||||
return keySet;
|
||||
}
|
||||
|
||||
|
||||
@GET
|
||||
@Path("/set-oidc-request")
|
||||
@Produces(org.keycloak.utils.MediaType.APPLICATION_JWT)
|
||||
@NoCache
|
||||
public void setOIDCRequest(@QueryParam("realmName") String realmName, @QueryParam("clientId") String clientId,
|
||||
@QueryParam("redirectUri") String redirectUri, @QueryParam("maxAge") String maxAge,
|
||||
@QueryParam("jwaAlgorithm") String jwaAlgorithm) {
|
||||
Map<String, Object> oidcRequest = new HashMap<>();
|
||||
oidcRequest.put(OIDCLoginProtocol.CLIENT_ID_PARAM, clientId);
|
||||
oidcRequest.put(OIDCLoginProtocol.RESPONSE_TYPE_PARAM, OAuth2Constants.CODE);
|
||||
oidcRequest.put(OIDCLoginProtocol.REDIRECT_URI_PARAM, redirectUri);
|
||||
if (maxAge != null) {
|
||||
oidcRequest.put(OIDCLoginProtocol.MAX_AGE_PARAM, Integer.parseInt(maxAge));
|
||||
}
|
||||
|
||||
Algorithm alg = Enum.valueOf(Algorithm.class, jwaAlgorithm);
|
||||
if (alg == Algorithm.none) {
|
||||
clientData.setOidcRequest(new JWSBuilder().jsonContent(oidcRequest).none());
|
||||
} else if (alg == Algorithm.RS256) {
|
||||
if (clientData.getSigningKeyPair() == null) {
|
||||
throw new BadRequestException("Requested RS256, but signing key not set");
|
||||
}
|
||||
|
||||
PrivateKey privateKey = clientData.getSigningKeyPair().getPrivate();
|
||||
clientData.setOidcRequest(new JWSBuilder().jsonContent(oidcRequest).rsa256(privateKey));
|
||||
} else {
|
||||
throw new BadRequestException("Unknown argument: " + jwaAlgorithm);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@GET
|
||||
@Path("/get-oidc-request")
|
||||
@Produces(org.keycloak.utils.MediaType.APPLICATION_JWT)
|
||||
@NoCache
|
||||
public String getOIDCRequest() {
|
||||
return clientData.getOidcRequest();
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,12 @@
|
||||
<version>1.0.0.Final</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>javax.enterprise</groupId>
|
||||
<artifactId>cdi-api</artifactId>
|
||||
<version>1.0-SP4</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-authz-client</artifactId>
|
||||
|
||||
+2
-4
@@ -19,9 +19,8 @@ package org.keycloak.example.photoz.admin;
|
||||
|
||||
import org.keycloak.example.photoz.entity.Album;
|
||||
|
||||
import javax.ejb.Stateless;
|
||||
import javax.inject.Inject;
|
||||
import javax.persistence.EntityManager;
|
||||
import javax.persistence.PersistenceContext;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.Produces;
|
||||
@@ -36,12 +35,11 @@ import java.util.List;
|
||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||
*/
|
||||
@Path("/admin/album")
|
||||
@Stateless
|
||||
public class AdminAlbumService {
|
||||
|
||||
public static final String SCOPE_ADMIN_ALBUM_MANAGE = "urn:photoz.com:scopes:album:admin:manage";
|
||||
|
||||
@PersistenceContext
|
||||
@Inject
|
||||
private EntityManager entityManager;
|
||||
|
||||
@Context
|
||||
|
||||
+4
-2
@@ -7,10 +7,12 @@ import org.keycloak.authorization.client.representation.ScopeRepresentation;
|
||||
import org.keycloak.authorization.client.resource.ProtectionResource;
|
||||
import org.keycloak.example.photoz.ErrorResponse;
|
||||
import org.keycloak.example.photoz.entity.Album;
|
||||
import org.keycloak.example.photoz.util.Transaction;
|
||||
import org.keycloak.representations.adapters.config.AdapterConfig;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
||||
import javax.ejb.Stateless;
|
||||
import javax.inject.Inject;
|
||||
import javax.persistence.EntityManager;
|
||||
import javax.persistence.PersistenceContext;
|
||||
import javax.persistence.Query;
|
||||
@@ -31,14 +33,14 @@ import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
@Path("/album")
|
||||
@Stateless
|
||||
@Transaction
|
||||
public class AlbumService {
|
||||
|
||||
public static final String SCOPE_ALBUM_VIEW = "urn:photoz.com:scopes:album:view";
|
||||
public static final String SCOPE_ALBUM_CREATE = "urn:photoz.com:scopes:album:create";
|
||||
public static final String SCOPE_ALBUM_DELETE = "urn:photoz.com:scopes:album:delete";
|
||||
|
||||
@PersistenceContext
|
||||
@Inject
|
||||
private EntityManager entityManager;
|
||||
|
||||
@Context
|
||||
|
||||
+2
-2
@@ -18,6 +18,7 @@
|
||||
package org.keycloak.example.photoz.album;
|
||||
|
||||
import javax.ejb.Stateless;
|
||||
import javax.inject.Inject;
|
||||
import javax.persistence.EntityManager;
|
||||
import javax.persistence.PersistenceContext;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
@@ -34,12 +35,11 @@ import java.util.List;
|
||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||
*/
|
||||
@Path("/profile")
|
||||
@Stateless
|
||||
public class ProfileService {
|
||||
|
||||
private static final String PROFILE_VIEW = "urn:photoz.com:scopes:profile:view";
|
||||
|
||||
@PersistenceContext
|
||||
@Inject
|
||||
private EntityManager entityManager;
|
||||
|
||||
@GET
|
||||
|
||||
+51
@@ -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.example.photoz.util;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import javax.annotation.PreDestroy;
|
||||
import javax.enterprise.context.ApplicationScoped;
|
||||
import javax.enterprise.context.RequestScoped;
|
||||
import javax.enterprise.inject.Produces;
|
||||
import javax.persistence.EntityManager;
|
||||
import javax.persistence.EntityManagerFactory;
|
||||
import javax.persistence.Persistence;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||
*/
|
||||
@ApplicationScoped
|
||||
public class Resources {
|
||||
|
||||
private EntityManagerFactory entityManagerFactory;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
entityManagerFactory = Persistence.createEntityManagerFactory("primary");
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void dispose() {
|
||||
entityManagerFactory.close();
|
||||
}
|
||||
|
||||
@RequestScoped
|
||||
@Produces
|
||||
public EntityManager createEntityManager() {
|
||||
return entityManagerFactory.createEntityManager();
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 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.example.photoz.util;
|
||||
|
||||
import javax.interceptor.InterceptorBinding;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import static java.lang.annotation.ElementType.METHOD;
|
||||
import static java.lang.annotation.ElementType.TYPE;
|
||||
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||
*/
|
||||
@InterceptorBinding
|
||||
@Target({ TYPE })
|
||||
@Retention(RUNTIME)
|
||||
public @interface Transaction {
|
||||
}
|
||||
+56
@@ -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.example.photoz.util;
|
||||
|
||||
import javax.enterprise.inject.Instance;
|
||||
import javax.inject.Inject;
|
||||
import javax.interceptor.AroundInvoke;
|
||||
import javax.interceptor.Interceptor;
|
||||
import javax.interceptor.InvocationContext;
|
||||
import javax.persistence.EntityManager;
|
||||
import javax.persistence.EntityTransaction;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||
*/
|
||||
@Interceptor
|
||||
@Transaction
|
||||
public class TransactionInterceptor {
|
||||
|
||||
@Inject
|
||||
private Instance<EntityManager> entityManager;
|
||||
|
||||
@AroundInvoke
|
||||
public Object aroundInvoke(InvocationContext context) {
|
||||
EntityManager entityManager = this.entityManager.get();
|
||||
EntityTransaction transaction = entityManager.getTransaction();
|
||||
|
||||
try {
|
||||
transaction.begin();
|
||||
Object proceed = context.proceed();
|
||||
transaction.commit();
|
||||
return proceed;
|
||||
} catch (Exception cause) {
|
||||
if (transaction != null && transaction.isActive()) {
|
||||
transaction.rollback();
|
||||
}
|
||||
throw new RuntimeException(cause);
|
||||
} finally {
|
||||
entityManager.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -3,5 +3,7 @@
|
||||
xsi:schemaLocation="
|
||||
http://java.sun.com/xml/ns/javaee
|
||||
http://java.sun.com/xml/ns/javaee/beans_1_0.xsd">
|
||||
|
||||
<interceptors>
|
||||
<class>org.keycloak.example.photoz.util.TransactionInterceptor</class>
|
||||
</interceptors>
|
||||
</beans>
|
||||
+7
-3
@@ -4,14 +4,18 @@
|
||||
xsi:schemaLocation="
|
||||
http://java.sun.com/xml/ns/persistence
|
||||
http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
|
||||
<persistence-unit name="primary">
|
||||
<non-jta-data-source>java:jboss/datasources/PhotozDS</non-jta-data-source>
|
||||
<persistence-unit name="primary" transaction-type="RESOURCE_LOCAL">
|
||||
<provider>org.hibernate.ejb.HibernatePersistence</provider>
|
||||
|
||||
<class>org.keycloak.example.photoz.entity.Album</class>
|
||||
<class>org.keycloak.example.photoz.entity.Photo</class>
|
||||
|
||||
<properties>
|
||||
<property name="hibernate.dialect" value="org.hibernate.dialect.MySQLDialect"/>
|
||||
<property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect" />
|
||||
<property name="hibernate.connection.driver_class" value="org.h2.Driver" />
|
||||
<property name="hibernate.connection.url" value="jdbc:h2:mem:test-keycloak-photoz-example" />
|
||||
<property name="hibernate.connection.user" value="sa" />
|
||||
<property name="hibernate.flushMode" value="FLUSH_AUTO" />
|
||||
<property name="hibernate.hbm2ddl.auto" value="update" />
|
||||
<property name="hibernate.show_sql" value="false" />
|
||||
</properties>
|
||||
|
||||
+1
@@ -20,6 +20,7 @@
|
||||
<deployment>
|
||||
<dependencies>
|
||||
<module name="org.keycloak.keycloak-authz-client" services="import"/>
|
||||
<module name="com.h2database.h2" services="import"/>
|
||||
</dependencies>
|
||||
</deployment>
|
||||
</jboss-deployment-structure>
|
||||
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
<datasources xmlns="http://www.jboss.org/ironjacamar/schema"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.jboss.org/ironjacamar/schema http://docs.jboss.org/ironjacamar/schema/datasources_1_0.xsd">
|
||||
<datasource jndi-name="java:jboss/datasources/PhotozDS" pool-name="PhotozDS" enabled="true" use-java-context="true">
|
||||
<connection-url>jdbc:h2:${jboss.server.data.dir}/kc-authz-photo;AUTO_SERVER=TRUE</connection-url>
|
||||
<driver>h2</driver>
|
||||
<security>
|
||||
<user-name>sa</user-name>
|
||||
<password>sa</password>
|
||||
</security>
|
||||
</datasource>
|
||||
</datasources>
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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.adapter.page;
|
||||
|
||||
import org.jboss.arquillian.container.test.api.OperateOnDeployment;
|
||||
import org.jboss.arquillian.test.api.ArquillianResource;
|
||||
|
||||
import java.net.URL;
|
||||
|
||||
/**
|
||||
* @author mhajas
|
||||
*/
|
||||
public class BadAssertionSalesPostSig extends SAMLServlet {
|
||||
public static final String DEPLOYMENT_NAME = "bad-assertion-sales-post-sig";
|
||||
|
||||
@ArquillianResource
|
||||
@OperateOnDeployment(DEPLOYMENT_NAME)
|
||||
private URL url;
|
||||
|
||||
@Override
|
||||
public URL getInjectedUrl() {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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.adapter.page;
|
||||
|
||||
import org.jboss.arquillian.container.test.api.OperateOnDeployment;
|
||||
import org.jboss.arquillian.test.api.ArquillianResource;
|
||||
|
||||
import java.net.URL;
|
||||
|
||||
/**
|
||||
* @author mhajas
|
||||
*/
|
||||
public class EmployeeServlet extends SAMLServlet {
|
||||
public static final String DEPLOYMENT_NAME = "employee";
|
||||
|
||||
@ArquillianResource
|
||||
@OperateOnDeployment(DEPLOYMENT_NAME)
|
||||
private URL url;
|
||||
|
||||
@Override
|
||||
public URL getInjectedUrl() {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -17,18 +17,18 @@
|
||||
|
||||
package org.keycloak.testsuite.adapter.page;
|
||||
|
||||
import java.net.URL;
|
||||
import org.jboss.arquillian.container.test.api.OperateOnDeployment;
|
||||
import org.jboss.arquillian.test.api.ArquillianResource;
|
||||
import org.keycloak.testsuite.page.AbstractPageWithInjectedUrl;
|
||||
import org.openqa.selenium.WebElement;
|
||||
import org.openqa.selenium.support.FindBy;
|
||||
|
||||
import java.net.URL;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author tkyjovsk
|
||||
*/
|
||||
public class InputPortal extends AbstractPageWithInjectedUrl {
|
||||
public class InputPortal extends SAMLServlet {
|
||||
|
||||
public static final String DEPLOYMENT_NAME = "input-portal";
|
||||
|
||||
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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.adapter.page;
|
||||
|
||||
import org.jboss.arquillian.container.test.api.OperateOnDeployment;
|
||||
import org.jboss.arquillian.test.api.ArquillianResource;
|
||||
|
||||
import java.net.URL;
|
||||
|
||||
/**
|
||||
* @author mhajas
|
||||
*/
|
||||
public class MissingAssertionSig extends SAMLServlet {
|
||||
public static final String DEPLOYMENT_NAME = "missing-assertion-sig";
|
||||
|
||||
@ArquillianResource
|
||||
@OperateOnDeployment(DEPLOYMENT_NAME)
|
||||
private URL url;
|
||||
|
||||
@Override
|
||||
public URL getInjectedUrl() {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -40,8 +40,8 @@ public abstract class SAMLServlet extends AbstractPageWithInjectedUrl {
|
||||
}
|
||||
}
|
||||
|
||||
public void checkRolesEndPoint() {
|
||||
driver.navigate().to(getUriBuilder().build().toASCIIString() + "/checkRoles");
|
||||
public void checkRolesEndPoint(boolean value) {
|
||||
driver.navigate().to(getUriBuilder().build().toASCIIString() + "/" + (value ? "" : "un") + "checkRoles");
|
||||
pause(300);
|
||||
}
|
||||
}
|
||||
Loaded 100 of 214 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user