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
+8740
-1324
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:
|
||||
@@ -180,4 +180,4 @@ Debug the Application
|
||||
If you want to debug the source code or look at the Javadocs of any library in the project, run either of the following commands to pull them into your local repository. The IDE should then detect them.
|
||||
|
||||
mvn dependency:sources
|
||||
mvn dependency:resolve -Dclassifier=javadoc
|
||||
mvn dependency:resolve -Dclassifier=javadoc
|
||||
@@ -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:
|
||||
@@ -179,4 +179,4 @@ Debug the Application
|
||||
If you want to debug the source code or look at the Javadocs of any library in the project, run either of the following commands to pull them into your local repository. The IDE should then detect them.
|
||||
|
||||
mvn dependency:sources
|
||||
mvn dependency:resolve -Dclassifier=javadoc
|
||||
mvn dependency:resolve -Dclassifier=javadoc
|
||||
@@ -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));
|
||||
@@ -153,39 +146,8 @@ public class DefaultJpaConnectionProviderFactory implements JpaConnectionProvide
|
||||
if (driverDialect != null) {
|
||||
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;
|
||||
}
|
||||
|
||||
+36
-120
@@ -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;
|
||||
@@ -67,43 +64,10 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
|
||||
/**
|
||||
* Prefix used to store additional HTTP GET params from original client request into {@link ClientSessionModel} note to be available later in Authenticators, RequiredActions etc. Prefix is used to
|
||||
* 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,14 +252,24 @@ 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();
|
||||
@@ -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