Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Bill Burke 2016-09-06 08:56:17 -04:00
commit 15d31a202f
308 changed files with 12610 additions and 5269 deletions

View file

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

View file

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

View file

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

View file

@ -160,15 +160,25 @@
if (loginIframe.enable) {
setupCheckLoginIframe().success(function() {
checkLoginIframe().success(function () {
kc.onAuthSuccess && kc.onAuthSuccess();
initPromise.setSuccess();
}).error(function () {
kc.onAuthError && kc.onAuthError();
if (initOptions.onLoad) {
onLoad();
}
});
});
} else {
initPromise.setSuccess();
kc.updateToken(-1).success(function() {
kc.onAuthSuccess && kc.onAuthSuccess();
initPromise.setSuccess();
}).error(function() {
kc.onAuthError && kc.onAuthError();
if (initOptions.onLoad) {
onLoad();
}
});
}
} else if (initOptions.onLoad) {
onLoad();
@ -368,7 +378,7 @@
minValidity = minValidity || 5;
var exec = function() {
if (!kc.isTokenExpired(minValidity)) {
if (minValidity >= 0 && !kc.isTokenExpired(minValidity)) {
promise.setSuccess(false);
} else {
var params = 'grant_type=refresh_token&' + 'refresh_token=' + kc.refreshToken;
@ -380,6 +390,7 @@
var req = new XMLHttpRequest();
req.open('POST', url, true);
req.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
req.withCredentials = true;
if (kc.clientId && kc.clientSecret) {
req.setRequestHeader('Authorization', 'Basic ' + btoa(kc.clientId + ':' + kc.clientSecret));
@ -1055,7 +1066,7 @@
if (!(this instanceof CookieStorage)) {
return new CookieStorage();
}
var cs = this;
cs.get = function(state) {

View file

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

View file

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

View file

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

View file

@ -16,31 +16,21 @@
-->
<assembly>
<id>auth-server-jboss-kc14</id>
<id>fuse-adapter-dist</id>
<formats>
<format>zip</format>
<format>tar.gz</format>
</formats>
<includeBaseDirectory>false</includeBaseDirectory>
<fileSets>
<fileSet>
<directory>${keycloak.server.home}</directory>
<outputDirectory>keycloak-1.3.1.Final</outputDirectory>
<excludes>
<exclude>**/*.sh</exclude>
</excludes>
</fileSet>
<fileSet>
<directory>${keycloak.server.home}</directory>
<outputDirectory>keycloak-1.3.1.Final</outputDirectory>
<directory>${project.build.directory}/system</directory>
<includes>
<include>**/*.sh</include>
<include>*/**</include>
</includes>
<fileMode>0755</fileMode>
<outputDirectory>system</outputDirectory>
</fileSet>
</fileSets>
</assembly>
</assembly>

View file

@ -0,0 +1,173 @@
<!--
~ Copyright 2016 Red Hat, Inc. and/or its affiliates
~ and other contributors as indicated by the @author tags.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
<version>2.2.0-SNAPSHOT</version>
<relativePath>../../../pom.xml</relativePath>
</parent>
<artifactId>keycloak-fuse-adapter-dist</artifactId>
<packaging>pom</packaging>
<name>Keycloak Fuse Adapter Distro</name>
<description/>
<dependencies>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-osgi-features</artifactId>
<version>${project.version}</version>
<type>xml</type>
<classifier>features</classifier>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-osgi-thirdparty</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-jaxb-annotations</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.jaxrs</groupId>
<artifactId>jackson-jaxrs-base</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.jaxrs</groupId>
<artifactId>jackson-jaxrs-json-provider</artifactId>
</dependency>
<dependency>
<groupId>org.jboss.logging</groupId>
<artifactId>jboss-logging</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-common</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-core</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-authz-client</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-adapter-spi</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-adapter-core</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-osgi-adapter</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-jetty-adapter-spi</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-jetty-core</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-jetty81-adapter</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-osgi-jaas</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-jetty92-adapter</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>copy-dependencies</id>
<phase>prepare-package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/system</outputDirectory>
<overWriteReleases>false</overWriteReleases>
<overWriteSnapshots>false</overWriteSnapshots>
<overWriteIfNewer>true</overWriteIfNewer>
<useRepositoryLayout>true</useRepositoryLayout>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<executions>
<execution>
<id>assemble</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
<configuration>
<descriptors>
<descriptor>assembly.xml</descriptor>
</descriptors>
<outputDirectory>target</outputDirectory>
<workDirectory>target/assembly/work</workDirectory>
<appendAssemblyId>false</appendAssemblyId>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View file

@ -32,6 +32,7 @@
<modules>
<module>as7-eap6-adapter</module>
<module>fuse-adapter-zip</module>
<module>jetty81-adapter-zip</module>
<module>jetty91-adapter-zip</module>
<module>jetty92-adapter-zip</module>

View file

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

View file

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

View file

@ -262,6 +262,17 @@
<artifactId>keycloak-wildfly-adapter-dist</artifactId>
<type>tar.gz</type>
</artifactItem>
<artifactItem>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-fuse-adapter-dist</artifactId>
<type>zip</type>
</artifactItem>
<artifactItem>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-fuse-adapter-dist</artifactId>
<type>tar.gz</type>
</artifactItem>
</artifactItems>
<outputDirectory>target/${project.version}/adapters/keycloak-oidc</outputDirectory>
</configuration>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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="[&quot;EVENT1&quot;,&quot;EVENT2&quot;]"/>
</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
`&quot;`.

View file

@ -103,6 +103,11 @@ public class CachedPolicyStore implements PolicyStore {
return getDelegate().findByResourceServer(resourceServerId).stream().map(policy -> findById(policy.getId())).collect(Collectors.toList());
}
@Override
public List<Policy> findByResourceServer(Map<String, String[]> attributes, String resourceServerId, int firstResult, int maxResult) {
return getDelegate().findByResourceServer(attributes, resourceServerId, firstResult, maxResult);
}
@Override
public List<Policy> findByResource(String resourceId) {
List<Policy> cache = new ArrayList<>();

View file

@ -32,6 +32,7 @@ import org.keycloak.models.authorization.infinispan.entities.CachedResource;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.stream.Collectors;
@ -126,6 +127,11 @@ public class CachedResourceStore implements ResourceStore {
return getDelegate().findByResourceServer(resourceServerId).stream().map(resource -> findById(resource.getId())).collect(Collectors.toList());
}
@Override
public List<Resource> findByResourceServer(Map<String, String[]> attributes, String resourceServerId, int firstResult, int maxResult) {
return getDelegate().findByResourceServer(attributes, resourceServerId, firstResult, maxResult);
}
@Override
public List<Resource> findByScope(String... id) {
return getDelegate().findByScope(id).stream().map(resource -> findById(resource.getId())).collect(Collectors.toList());

View file

@ -30,6 +30,7 @@ import org.keycloak.models.authorization.infinispan.entities.CachedScope;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
/**
@ -114,6 +115,11 @@ public class CachedScopeStore implements ScopeStore {
return getDelegate().findByResourceServer(id);
}
@Override
public List<Scope> findByResourceServer(Map<String, String[]> attributes, String resourceServerId, int firstResult, int maxResult) {
return getDelegate().findByResourceServer(attributes, resourceServerId, firstResult, maxResult);
}
private String getCacheKeyForScope(String id) {
return SCOPE_ID_CACHE_PREFIX + id;
}

View file

@ -27,11 +27,15 @@ import org.keycloak.models.utils.KeycloakModelUtils;
import javax.persistence.EntityManager;
import javax.persistence.NoResultException;
import javax.persistence.Query;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
@ -100,6 +104,43 @@ public class JPAPolicyStore implements PolicyStore {
return query.getResultList();
}
@Override
public List<Policy> findByResourceServer(Map<String, String[]> attributes, String resourceServerId, int firstResult, int maxResult) {
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<PolicyEntity> querybuilder = builder.createQuery(PolicyEntity.class);
Root<PolicyEntity> root = querybuilder.from(PolicyEntity.class);
List<Predicate> predicates = new ArrayList();
predicates.add(builder.equal(root.get("resourceServer").get("id"), resourceServerId));
attributes.forEach((name, value) -> {
if ("permission".equals(name)) {
if (Boolean.valueOf(value[0])) {
predicates.add(root.get("type").in("resource", "scope"));
} else {
predicates.add(builder.not(root.get("type").in("resource", "scope")));
}
} else if ("id".equals(name)) {
predicates.add(root.get(name).in(value));
} else {
predicates.add(builder.like(builder.lower(root.get(name)), "%" + value[0].toLowerCase() + "%"));
}
});
querybuilder.where(predicates.toArray(new Predicate[predicates.size()])).orderBy(builder.asc(root.get("name")));
Query query = entityManager.createQuery(querybuilder);
if (firstResult != -1) {
query.setFirstResult(firstResult);
}
if (maxResult != -1) {
query.setMaxResults(maxResult);
}
return query.getResultList();
}
@Override
public List<Policy> findByResource(final String resourceId) {
Query query = getEntityManager().createQuery("select p from PolicyEntity p inner join p.resources r where r.id = :resourceId");

View file

@ -26,8 +26,14 @@ import org.keycloak.models.utils.KeycloakModelUtils;
import javax.persistence.EntityManager;
import javax.persistence.Query;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
@ -96,6 +102,37 @@ public class JPAResourceStore implements ResourceStore {
return query.getResultList();
}
@Override
public List findByResourceServer(Map<String, String[]> attributes, String resourceServerId, int firstResult, int maxResult) {
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<ResourceEntity> querybuilder = builder.createQuery(ResourceEntity.class);
Root<ResourceEntity> root = querybuilder.from(ResourceEntity.class);
List<Predicate> predicates = new ArrayList();
predicates.add(builder.equal(root.get("resourceServer").get("id"), resourceServerId));
attributes.forEach((name, value) -> {
if ("scope".equals(name)) {
predicates.add(root.join("scopes").get("id").in(value));
} else {
predicates.add(builder.like(builder.lower(root.get(name)), "%" + value[0].toLowerCase() + "%"));
}
});
querybuilder.where(predicates.toArray(new Predicate[predicates.size()])).orderBy(builder.asc(root.get("name")));
Query query = entityManager.createQuery(querybuilder);
if (firstResult != -1) {
query.setFirstResult(firstResult);
}
if (maxResult != -1) {
query.setMaxResults(maxResult);
}
return query.getResultList();
}
@Override
public List<Resource> findByScope(String... id) {
Query query = entityManager.createQuery("select r from ResourceEntity r inner join r.scopes s where s.id in (:scopeIds)");

View file

@ -27,7 +27,13 @@ import org.keycloak.models.utils.KeycloakModelUtils;
import javax.persistence.EntityManager;
import javax.persistence.NoResultException;
import javax.persistence.Query;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
@ -85,4 +91,31 @@ public class JPAScopeStore implements ScopeStore {
return query.getResultList();
}
@Override
public List<Scope> findByResourceServer(Map<String, String[]> attributes, String resourceServerId, int firstResult, int maxResult) {
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<ScopeEntity> querybuilder = builder.createQuery(ScopeEntity.class);
Root<ScopeEntity> root = querybuilder.from(ScopeEntity.class);
List<Predicate> predicates = new ArrayList();
predicates.add(builder.equal(root.get("resourceServer").get("id"), resourceServerId));
attributes.forEach((name, value) -> {
predicates.add(builder.like(builder.lower(root.get(name)), "%" + value[0].toLowerCase() + "%"));
});
querybuilder.where(predicates.toArray(new Predicate[predicates.size()])).orderBy(builder.asc(root.get("name")));
Query query = entityManager.createQuery(querybuilder);
if (firstResult != -1) {
query.setFirstResult(firstResult);
}
if (maxResult != -1) {
query.setMaxResults(maxResult);
}
return query.getResultList();
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -32,6 +32,8 @@ import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext;
import org.keycloak.models.utils.KeycloakModelUtils;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import static java.util.stream.Collectors.toList;
@ -101,6 +103,31 @@ public class MongoPolicyStore implements PolicyStore {
.collect(toList());
}
@Override
public List<Policy> findByResourceServer(Map<String, String[]> attributes, String resourceServerId, int firstResult, int maxResult) {
QueryBuilder queryBuilder = new QueryBuilder()
.and("resourceServerId").is(resourceServerId);
attributes.forEach((name, value) -> {
if ("permission".equals(name)) {
if (Boolean.valueOf(value[0])) {
queryBuilder.and("type").in(new String[] {"resource", "scope"});
} else {
queryBuilder.and("type").notIn(new String[] {"resource", "scope"});
}
} else if ("id".equals(name)) {
queryBuilder.and("_id").in(value);
} else {
queryBuilder.and(name).regex(Pattern.compile(".*" + value[0] + ".*", Pattern.CASE_INSENSITIVE));
}
});
DBObject sort = new BasicDBObject("name", 1);
return getMongoStore().loadEntities(PolicyEntity.class, queryBuilder.get(), sort, firstResult, maxResult, invocationContext).stream()
.map(policy -> findById(policy.getId())).collect(toList());
}
@Override
public List<Policy> findByResource(String resourceId) {
DBObject query = new QueryBuilder()

View file

@ -18,6 +18,7 @@
package org.keycloak.authorization.mongo.store;
import com.mongodb.BasicDBObject;
import com.mongodb.DBObject;
import com.mongodb.QueryBuilder;
import org.keycloak.authorization.AuthorizationProvider;
@ -26,12 +27,13 @@ import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.authorization.mongo.adapter.ResourceAdapter;
import org.keycloak.authorization.mongo.entities.ResourceEntity;
import org.keycloak.authorization.store.ResourceStore;
import org.keycloak.connections.mongo.api.MongoIdentifiableEntity;
import org.keycloak.connections.mongo.api.MongoStore;
import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext;
import org.keycloak.models.utils.KeycloakModelUtils;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import static java.util.stream.Collectors.toList;
@ -98,10 +100,29 @@ public class MongoResourceStore implements ResourceStore {
.map(scope -> findById(scope.getId())).collect(toList());
}
@Override
public List<Resource> findByResourceServer(Map<String, String[]> attributes, String resourceServerId, int firstResult, int maxResult) {
QueryBuilder queryBuilder = new QueryBuilder()
.and("resourceServerId").is(resourceServerId);
attributes.forEach((name, value) -> {
if ("scope".equals(name)) {
queryBuilder.and("scopes").in(value);
} else {
queryBuilder.and(name).regex(Pattern.compile(".*" + value[0] + ".*", Pattern.CASE_INSENSITIVE));
}
});
DBObject sort = new BasicDBObject("name", 1);
return getMongoStore().loadEntities(ResourceEntity.class, queryBuilder.get(), sort, firstResult, maxResult, invocationContext).stream()
.map(scope -> findById(scope.getId())).collect(toList());
}
@Override
public List<Resource> findByScope(String... id) {
DBObject query = new QueryBuilder()
.and("scopes.id").in(id)
.and("scopes").in(id)
.get();
return getMongoStore().loadEntities(ResourceEntity.class, query, getInvocationContext()).stream()

View file

@ -18,6 +18,7 @@
package org.keycloak.authorization.mongo.store;
import com.mongodb.BasicDBObject;
import com.mongodb.DBObject;
import com.mongodb.QueryBuilder;
import org.keycloak.authorization.AuthorizationProvider;
@ -31,6 +32,8 @@ import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext;
import org.keycloak.models.utils.KeycloakModelUtils;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import static java.util.stream.Collectors.toList;
@ -98,6 +101,21 @@ public class MongoScopeStore implements ScopeStore {
.collect(toList());
}
@Override
public List<Scope> findByResourceServer(Map<String, String[]> attributes, String resourceServerId, int firstResult, int maxResult) {
QueryBuilder queryBuilder = new QueryBuilder()
.and("resourceServerId").is(resourceServerId);
attributes.forEach((name, value) -> {
queryBuilder.and(name).regex(Pattern.compile(".*" + value[0] + ".*", Pattern.CASE_INSENSITIVE));
});
DBObject sort = new BasicDBObject("name", 1);
return getMongoStore().loadEntities(ScopeEntity.class, queryBuilder.get(), sort, firstResult, maxResult, invocationContext).stream()
.map(scope -> findById(scope.getId())).collect(toList());
}
private MongoStoreInvocationContext getInvocationContext() {
return this.invocationContext;
}

View file

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

View file

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

View file

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

View file

@ -19,11 +19,10 @@ package org.keycloak.authorization.store;
import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.model.Resource;
import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.authorization.model.Scope;
import java.util.List;
import java.util.Map;
/**
* A {@link PolicyStore} is responsible to manage the persistence of {@link Policy} instances.
@ -75,6 +74,15 @@ public interface PolicyStore {
*/
List<Policy> findByResourceServer(String resourceServerId);
/**
* Returns a list of {@link Policy} associated with a {@link ResourceServer} with the given <code>resourceServerId</code>.
*
* @param attributes a map holding the attributes that will be used as a filter
* @param resourceServerId the identifier of a resource server
* @return a list of policies that belong to the given resource server
*/
List<Policy> findByResourceServer(Map<String, String[]> attributes, String resourceServerId, int firstResult, int maxResult);
/**
* Returns a list of {@link Policy} associated with a {@link org.keycloak.authorization.core.model.Resource} with the given <code>resourceId</code>.
*

View file

@ -19,10 +19,9 @@ package org.keycloak.authorization.store;
import org.keycloak.authorization.model.Resource;
import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.authorization.model.Scope;
import java.util.List;
import java.util.Set;
import java.util.Map;
/**
* A {@link ResourceStore} is responsible to manage the persistence of {@link Resource} instances.
@ -72,6 +71,15 @@ public interface ResourceStore {
*/
List<Resource> findByResourceServer(String resourceServerId);
/**
* Finds all {@link Resource} instances associated with a given resource server.
*
* @param attributes a map holding the attributes that will be used as a filter
* @param resourceServerId the identifier of the resource server
* @return a list with all resources associated with the given resource server
*/
List<Resource> findByResourceServer(Map<String, String[]> attributes, String resourceServerId, int firstResult, int maxResult);
/**
* Finds all {@link Resource} associated with a given scope.
*

View file

@ -22,6 +22,7 @@ import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.authorization.model.Scope;
import java.util.List;
import java.util.Map;
/**
* A {@link ScopeStore} is responsible to manage the persistence of {@link Scope} instances.
@ -75,4 +76,14 @@ public interface ScopeStore {
* @return a list of scopes that belong to the given resource server
*/
List<Scope> findByResourceServer(String id);
}
/**
* Returns a list of {@link Scope} associated with a {@link ResourceServer} with the given <code>resourceServerId</code>.
*
* @param attributes a map holding the attributes that will be used as a filter
* @param resourceServerId the identifier of a resource server
*
* @return a list of scopes that belong to the given resource server
*/
List<Scope> findByResourceServer(Map<String, String[]> attributes, String resourceServerId, int firstResult, int maxResult);
}

View file

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

View file

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

View file

@ -58,4 +58,7 @@ public interface Details {
String CLIENT_AUTH_METHOD = "client_auth_method";
String SIGNATURE_REQUIRED = "signature_required";
String SIGNATURE_ALGORITHM = "signature_algorithm";
}

View file

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

View file

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

View file

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

View file

@ -17,6 +17,10 @@
package org.keycloak.protocol;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperContainerModel;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel;
import org.keycloak.provider.ConfiguredProvider;
import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderFactory;
@ -30,4 +34,16 @@ public interface ProtocolMapper extends Provider, ProviderFactory<ProtocolMapper
String getDisplayCategory();
String getDisplayType();
/**
* Called when instance of mapperModel is created/updated for this protocolMapper through admin endpoint
*
* @param session
* @param realm
* @param client client or clientTemplate
* @param mapperModel
* @throws ProtocolMapperConfigException if configuration provided in mapperModel is not valid
*/
default void validateConfig(KeycloakSession session, RealmModel realm, ProtocolMapperContainerModel client, ProtocolMapperModel mapperModel) throws ProtocolMapperConfigException {
};
}

View file

@ -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.protocol;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class ProtocolMapperConfigException extends Exception {
private Object[] parameters;
public ProtocolMapperConfigException(String message) {
super(message);
}
public ProtocolMapperConfigException(String message, Throwable cause) {
super(message, cause);
}
public ProtocolMapperConfigException(String message, Object ... parameters) {
super(message);
this.parameters = parameters;
}
public Object[] getParameters() {
return parameters;
}
public void setParameters(Object[] parameters) {
this.parameters = parameters;
}
}

View file

@ -1,387 +0,0 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.changeset;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.models.UserCredentialValueModel;
import org.keycloak.models.entities.AbstractIdentifiableEntity;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class UserData {
private String id;
private boolean idChanged;
private String username;
private boolean usernameChanged;
private Long createdTimestamp;
private boolean createdTimestampChanged;
private String firstName;
private boolean firstNameChanged;
private String lastName;
private boolean lastNameChanged;
private String email;
private boolean emailChanged;
private boolean emailVerified;
private boolean emailVerifiedChanged;
private boolean totp;
private boolean totpChanged;
private boolean enabled;
private boolean enabledChanged;
private Set<String> roleIds = new HashSet<>();
private boolean rolesChanged;
private Set<String> groupIds = new HashSet<>();
private boolean groupsChanged;
private MultivaluedHashMap<String, String> attributes = new MultivaluedHashMap<>();
private boolean attributesChanged;
private Set<String> requiredActions = new HashSet<>();
private boolean requiredActionsChanged;
private List<UserCredentialValueModel> credentials = new LinkedList<>();
private boolean credentialsChanged;
public void rememberState() {
original = new UserData();
original.id = id;
original.username = username;
original.createdTimestamp = createdTimestamp;
original.firstName = firstName;
original.lastName = lastName;
original.email = email;
original.emailVerified = emailVerified;
original.totp = totp;
original.enabled = enabled;
original.attributes.putAll(attributes);
original.requiredActions.addAll(requiredActions);
original.credentials.addAll(credentials);
}
private UserData original = null;
public void clearChangeFlags() {
original = null;
idChanged = false;
usernameChanged = false;
createdTimestampChanged = false;
firstNameChanged = false;
lastNameChanged = false;
emailChanged = false;
emailVerifiedChanged = false;
totpChanged = false;
enabledChanged = false;
rolesChanged = false;
groupsChanged = false;
attributesChanged = false;
requiredActionsChanged = false;
credentialsChanged = false;
}
public boolean isChanged() {
return !idChanged
&& !usernameChanged
&& !createdTimestampChanged
&& !firstNameChanged
&& !lastNameChanged
&& !emailChanged
&& !emailVerifiedChanged
&& !totpChanged
&& !enabledChanged
&& !rolesChanged
&& !groupsChanged
&& !attributesChanged
&& !requiredActionsChanged
&& !credentialsChanged;
}
public boolean isIdChanged() {
return idChanged;
}
public boolean isUsernameChanged() {
return usernameChanged;
}
public boolean isCreatedTimestampChanged() {
return createdTimestampChanged;
}
public boolean isFirstNameChanged() {
return firstNameChanged;
}
public boolean isLastNameChanged() {
return lastNameChanged;
}
public boolean isEmailChanged() {
return emailChanged;
}
public boolean isEmailVerifiedChanged() {
return emailVerifiedChanged;
}
public boolean isTotpChanged() {
return totpChanged;
}
public boolean isEnabledChanged() {
return enabledChanged;
}
public boolean isRolesChanged() {
return rolesChanged;
}
public boolean isGroupsChanged() {
return groupsChanged;
}
public boolean isAttributesChanged() {
return attributesChanged;
}
public boolean isRequiredActionsChanged() {
return requiredActionsChanged;
}
public boolean isCredentialsChanged() {
return credentialsChanged;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
idChanged = true;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
usernameChanged = true;
}
public Long getCreatedTimestamp() {
return createdTimestamp;
}
public void setCreatedTimestamp(Long timestamp) {
this.createdTimestamp = timestamp;
createdTimestampChanged = true;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
firstNameChanged = true;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
lastNameChanged = true;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
emailChanged = true;
}
public boolean isEmailVerified() {
return emailVerified;
}
public void setEmailVerified(boolean emailVerified) {
this.emailVerified = emailVerified;
emailVerifiedChanged = true;
}
public boolean isTotp() {
return totp;
}
public void setTotp(boolean totp) {
this.totp = totp;
totpChanged = true;
}
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
enabledChanged = true;
}
public Set<String> getRoleMappings() {
return Collections.unmodifiableSet(roleIds);
}
public void grantRole(String roleId) {
if (roleIds.contains(roleId)) return;
roleIds.add(roleId);
rolesChanged = true;
}
public void deleteRoleMapping(String roleId) {
if (!roleIds.contains(roleId)) return;
roleIds.remove(roleId);
rolesChanged = true;
}
public MultivaluedHashMap<String, String> getAttributes() {
return attributes;
}
public void setSingleAttribute(String name, String value) {
attributes.putSingle(name, value);
attributesChanged = true;
}
public void setAttribute(String name, List<String> values) {
attributes.put(name, values);
attributesChanged = true;
}
public void removeAttribute(String name) {
attributes.remove(name);
attributesChanged = true;
}
public Set<String> getRequiredActions() {
return Collections.unmodifiableSet(requiredActions);
}
public void addRequiredAction(String action) {
if (requiredActions.contains(action)) return;
requiredActions.add(action);
requiredActionsChanged = true;
}
public void removeRequiredAction(String action) {
if (!requiredActions.contains(action)) return;
requiredActions.remove(action);
requiredActionsChanged = true;
}
public List<UserCredentialValueModel> getCredentials() {
return Collections.unmodifiableList(credentials);
}
public void removeCredentialType(String type) {
Iterator<UserCredentialValueModel> it = credentials.iterator();
while (it.hasNext()) {
if (it.next().getType().equals(type)) {
it.remove();
credentialsChanged = true;
}
}
}
public void removeCredentialDevice(String type, String device) {
Iterator<UserCredentialValueModel> it = credentials.iterator();
while (it.hasNext()) {
UserCredentialValueModel next = it.next();
if (next.getType().equals(type) && next.getDevice().equals(device)) {
it.remove();
credentialsChanged = true;
}
}
}
public void setCredential(UserCredentialValueModel cred) {
removeCredentialType(cred.getType());
addCredential(cred);
}
public void addCredential(UserCredentialValueModel cred) {
credentials.add(cred);
credentialsChanged = true;
}
public Set<String> getGroupIds() {
return Collections.unmodifiableSet(groupIds);
}
public void joinGroup(String groupId) {
if (groupIds.contains(groupId)) return;
groupIds.add(groupId);
groupsChanged = true;
}
public void leaveGroup(String groupId) {
if (!groupIds.contains(groupId)) return;
groupIds.remove(groupId);
groupsChanged = true;
}
@Override
public boolean equals(Object o) {
if (o == this) return true;
if (this.id == null) return false;
if (o == null || getClass() != o.getClass()) return false;
AbstractIdentifiableEntity that = (AbstractIdentifiableEntity) o;
if (!getId().equals(that.getId())) return false;
return true;
}
@Override
public int hashCode() {
return id!=null ? id.hashCode() : super.hashCode();
}
@Override
public String toString() {
return String.format("%s [ id=%s ]", getClass().getSimpleName(), getId());
}
}

View file

@ -1,340 +0,0 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.changeset;
import org.keycloak.common.util.Time;
import org.keycloak.hash.PasswordHashManager;
import org.keycloak.models.ClientModel;
import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.OTPPolicy;
import org.keycloak.models.PasswordPolicy;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleContainerModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserCredentialValueModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
*
*
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class UserDataAdapter implements UserModel {
protected UserData userData;
protected RealmModel realm;
protected KeycloakSession session;
protected Set<String> managedCredentialTypes;
protected List<UserCredentialModel> updatedManagedCredentials = new LinkedList<>();
public UserDataAdapter(KeycloakSession session, RealmModel realm, UserData userData) {
this.session = session;
this.realm = realm;
this.userData = userData;
this.userData.rememberState();
}
@Override
public String getId() {
return userData.getId();
}
@Override
public String getUsername() {
return userData.getUsername();
}
@Override
public void setUsername(String username) {
userData.setUsername(username);
}
@Override
public Long getCreatedTimestamp() {
return userData.getCreatedTimestamp();
}
@Override
public void setCreatedTimestamp(Long timestamp) {
userData.setCreatedTimestamp(timestamp);
}
@Override
public boolean isEnabled() {
return userData.isEnabled();
}
@Override
public boolean isOtpEnabled() {
return userData.isTotp();
}
@Override
public void setEnabled(boolean enabled) {
userData.setEnabled(enabled);
}
@Override
public void setSingleAttribute(String name, String value) {
userData.setSingleAttribute(name, value);
}
@Override
public void setAttribute(String name, List<String> values) {
userData.setAttribute(name, values);
}
@Override
public void removeAttribute(String name) {
userData.removeAttribute(name);
}
@Override
public String getFirstAttribute(String name) {
return userData.getAttributes().getFirst(name);
}
@Override
public List<String> getAttribute(String name) {
return userData.getAttributes().get(name);
}
@Override
public Map<String, List<String>> getAttributes() {
return userData.getAttributes();
}
@Override
public Set<String> getRequiredActions() {
return userData.getRequiredActions();
}
@Override
public void addRequiredAction(String action) {
userData.addRequiredAction(action);
}
@Override
public void removeRequiredAction(String action) {
userData.removeRequiredAction(action);
}
@Override
public void addRequiredAction(RequiredAction action) {
userData.addRequiredAction(action.name());
}
@Override
public void removeRequiredAction(RequiredAction action) {
userData.removeRequiredAction(action.name());
}
@Override
public String getFirstName() {
return userData.getFirstName();
}
@Override
public void setFirstName(String firstName) {
userData.setFirstName(firstName);
}
@Override
public String getLastName() {
return userData.getLastName();
}
@Override
public void setLastName(String lastName) {
userData.setLastName(lastName);
}
@Override
public String getEmail() {
return userData.getEmail();
}
@Override
public void setEmail(String email) {
userData.setEmail(email);
}
@Override
public boolean isEmailVerified() {
return userData.isEmailVerified();
}
@Override
public void setEmailVerified(boolean verified) {
userData.setEmailVerified(verified);
}
@Override
public void setOtpEnabled(boolean totp) {
userData.setTotp(totp);
}
@Override
public void updateCredential(UserCredentialModel cred) {
}
@Override
public List<UserCredentialValueModel> getCredentialsDirectly() {
return null;
}
@Override
public void updateCredentialDirectly(UserCredentialValueModel cred) {
}
@Override
public Set<GroupModel> getGroups() {
Set<String> groups = userData.getGroupIds();
Set<GroupModel> set = new HashSet<>();
for (String id : groups) {
GroupModel group = realm.getGroupById(id);
if (group != null) set.add(group);
}
return set;
}
@Override
public void joinGroup(GroupModel group) {
userData.joinGroup(group.getId());
}
@Override
public void leaveGroup(GroupModel group) {
userData.leaveGroup(group.getId());
}
@Override
public boolean isMemberOf(GroupModel group) {
Set<GroupModel> roles = getGroups();
return KeycloakModelUtils.isMember(roles, group);
}
@Override
public String getFederationLink() {
return null;
}
@Override
public void setFederationLink(String link) {
}
@Override
public String getServiceAccountClientLink() {
return null;
}
@Override
public void setServiceAccountClientLink(String clientInternalId) {
}
@Override
public Set<RoleModel> getRealmRoleMappings() {
Set<RoleModel> roleMappings = getRoleMappings();
Set<RoleModel> realmRoles = new HashSet<RoleModel>();
for (RoleModel role : roleMappings) {
RoleContainerModel container = role.getContainer();
if (container instanceof RealmModel) {
realmRoles.add(role);
}
}
return realmRoles;
}
@Override
public Set<RoleModel> getClientRoleMappings(ClientModel app) {
Set<RoleModel> roleMappings = getRoleMappings();
Set<RoleModel> roles = new HashSet<RoleModel>();
for (RoleModel role : roleMappings) {
RoleContainerModel container = role.getContainer();
if (container instanceof ClientModel) {
ClientModel appModel = (ClientModel)container;
if (appModel.getId().equals(app.getId())) {
roles.add(role);
}
}
}
return roles;
}
@Override
public boolean hasRole(RoleModel role) {
Set<RoleModel> roles = getRoleMappings();
return KeycloakModelUtils.hasRole(roles, role);
}
@Override
public void grantRole(RoleModel role) {
userData.grantRole(role.getId());
}
@Override
public Set<RoleModel> getRoleMappings() {
Set<String> roles = userData.getRoleMappings();
Set<RoleModel> set = new HashSet<>();
for (String id : roles) {
RoleModel role = realm.getRoleById(id);
if (role != null) set.add(role);
}
return set;
}
@Override
public void deleteRoleMapping(RoleModel role) {
userData.deleteRoleMapping(role.getId());
}
}

View file

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

View file

@ -132,6 +132,9 @@ public class IdpEmailVerificationAuthenticator extends AbstractIdpAuthenticator
clientSession.setNote(IS_DIFFERENT_BROWSER, "true");
}
// User successfully confirmed linking by email verification. His email was defacto verified
existingUser.setEmailVerified(true);
context.setUser(existingUser);
context.success();
} else {

View file

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

View file

@ -26,8 +26,10 @@ import org.keycloak.authorization.policy.provider.PolicyProviderAdminService;
import org.keycloak.authorization.policy.provider.PolicyProviderFactory;
import org.keycloak.authorization.store.PolicyStore;
import org.keycloak.authorization.store.StoreFactory;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.representations.idm.authorization.PolicyProviderRepresentation;
import org.keycloak.representations.idm.authorization.PolicyRepresentation;
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
import org.keycloak.services.resources.admin.RealmAuth;
import javax.ws.rs.Consumes;
@ -41,6 +43,12 @@ import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import static org.keycloak.models.utils.ModelToRepresentation.toRepresentation;
@ -188,11 +196,54 @@ public class PolicyService {
@GET
@Produces("application/json")
@NoCache
public Response findAll() {
public Response findAll(@QueryParam("name") String name,
@QueryParam("type") String type,
@QueryParam("resource") String resource,
@QueryParam("permission") Boolean permission,
@QueryParam("first") Integer firstResult,
@QueryParam("max") Integer maxResult) {
this.auth.requireView();
Map<String, String[]> search = new HashMap<>();
if (name != null && !"".equals(name.trim())) {
search.put("name", new String[] {name});
}
if (type != null && !"".equals(type.trim())) {
search.put("type", new String[] {type});
}
StoreFactory storeFactory = authorization.getStoreFactory();
if (resource != null && !"".equals(resource.trim())) {
List<Policy> policies = new ArrayList<>();
HashMap<String, String[]> resourceSearch = new HashMap<>();
resourceSearch.put("name", new String[] {resource});
storeFactory.getResourceStore().findByResourceServer(resourceSearch, resourceServer.getId(), -1, -1).forEach(resource1 -> {
ResourceRepresentation resourceRepresentation = ModelToRepresentation.toRepresentation(resource1, resourceServer, authorization);
resourceRepresentation.getPolicies().forEach(policyRepresentation -> {
Policy associated = storeFactory.getPolicyStore().findById(policyRepresentation.getId());
policies.add(associated);
findAssociatedPolicies(associated, policies);
});
});
if (policies.isEmpty()) {
return Response.ok(Collections.emptyList()).build();
}
search.put("id", policies.stream().map(Policy::getId).toArray(String[]::new));
}
if (permission != null) {
search.put("permission", new String[] {permission.toString()});
}
return Response.ok(
storeFactory.getPolicyStore().findByResourceServer(resourceServer.getId()).stream()
storeFactory.getPolicyStore().findByResourceServer(search, resourceServer.getId(), firstResult != null ? firstResult : -1, maxResult != null ? maxResult : -1).stream()
.map(policy -> toRepresentation(policy, authorization))
.collect(Collectors.toList()))
.build();
@ -244,4 +295,11 @@ public class PolicyService {
return null;
}
private void findAssociatedPolicies(Policy policy, List<Policy> policies) {
policy.getAssociatedPolicies().forEach(associated -> {
policies.add(associated);
findAssociatedPolicies(associated, policies);
});
}
}

View file

@ -22,9 +22,13 @@ import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.model.Resource;
import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.authorization.model.Scope;
import org.keycloak.authorization.store.PolicyStore;
import org.keycloak.authorization.store.ResourceStore;
import org.keycloak.authorization.store.StoreFactory;
import org.keycloak.models.ClientModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
import org.keycloak.services.ErrorResponse;
import org.keycloak.services.resources.admin.RealmAuth;
@ -40,7 +44,10 @@ import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static org.keycloak.models.utils.ModelToRepresentation.toRepresentation;
@ -170,12 +177,63 @@ public class ResourceSetService {
@GET
@NoCache
@Produces("application/json")
public Response findAll() {
public Response findAll(@QueryParam("name") String name,
@QueryParam("uri") String uri,
@QueryParam("owner") String owner,
@QueryParam("type") String type,
@QueryParam("scope") String scope,
@QueryParam("first") Integer firstResult,
@QueryParam("max") Integer maxResult) {
requireView();
StoreFactory storeFactory = authorization.getStoreFactory();
Map<String, String[]> search = new HashMap<>();
if (name != null && !"".equals(name.trim())) {
search.put("name", new String[] {name});
}
if (uri != null && !"".equals(uri.trim())) {
search.put("uri", new String[] {uri});
}
if (owner != null && !"".equals(owner.trim())) {
RealmModel realm = authorization.getKeycloakSession().getContext().getRealm();
ClientModel clientModel = realm.getClientByClientId(owner);
if (clientModel != null) {
owner = clientModel.getId();
} else {
UserModel user = authorization.getKeycloakSession().users().getUserByUsername(owner, realm);
if (user != null) {
owner = user.getId();
}
}
search.put("owner", new String[] {owner});
}
if (type != null && !"".equals(type.trim())) {
search.put("type", new String[] {type});
}
if (scope != null && !"".equals(scope.trim())) {
HashMap<String, String[]> scopeFilter = new HashMap<>();
scopeFilter.put("name", new String[] {scope});
List<Scope> scopes = authorization.getStoreFactory().getScopeStore().findByResourceServer(scopeFilter, resourceServer.getId(), -1, -1);
if (scopes.isEmpty()) {
return Response.ok(Collections.emptyList()).build();
}
search.put("scope", scopes.stream().map(Scope::getId).toArray(String[]::new));
}
return Response.ok(
storeFactory.getResourceStore().findByResourceServer(this.resourceServer.getId()).stream()
storeFactory.getResourceStore().findByResourceServer(search, this.resourceServer.getId(), firstResult != null ? firstResult : -1, maxResult != null ? maxResult : -1).stream()
.map(resource -> toRepresentation(resource, this.resourceServer, authorization))
.collect(Collectors.toList()))
.build();

View file

@ -41,7 +41,9 @@ import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static org.keycloak.models.utils.ModelToRepresentation.toRepresentation;
@ -158,10 +160,19 @@ public class ScopeService {
@GET
@Produces("application/json")
public Response findAll() {
public Response findAll(@QueryParam("name") String name,
@QueryParam("first") Integer firstResult,
@QueryParam("max") Integer maxResult) {
this.auth.requireView();
Map<String, String[]> search = new HashMap<>();
if (name != null && !"".equals(name.trim())) {
search.put("name", new String[] {name});
}
return Response.ok(
this.authorization.getStoreFactory().getScopeStore().findByResourceServer(this.resourceServer.getId()).stream()
this.authorization.getStoreFactory().getScopeStore().findByResourceServer(search, this.resourceServer.getId(), firstResult != null ? firstResult : -1, maxResult != null ? maxResult : -1).stream()
.map(scope -> toRepresentation(scope, this.authorization))
.collect(Collectors.toList()))
.build();

View file

@ -109,7 +109,7 @@ public class ResourceService {
}
private Set<String> findAll() {
Response response = this.resourceManager.findAll();
Response response = this.resourceManager.findAll(null, null, null, null, null, -1, -1);
List<ResourceRepresentation> resources = (List<ResourceRepresentation>) response.getEntity();
return resources.stream().map(ResourceRepresentation::getId).collect(Collectors.toSet());
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -19,10 +19,12 @@ package org.keycloak.protocol.oidc.mappers;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperContainerModel;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.ProtocolMapperConfigException;
import org.keycloak.protocol.ProtocolMapperUtils;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.representations.AccessToken;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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;
@ -107,7 +107,7 @@ public class ClientResource {
@Path("protocol-mappers")
public ProtocolMappersResource getProtocolMappers() {
ProtocolMappersResource mappers = new ProtocolMappersResource(client, auth, adminEvent);
ProtocolMappersResource mappers = new ProtocolMappersResource(realm, client, auth, adminEvent);
ResteasyProviderFactory.getInstance().injectProperties(mappers);
return mappers;
}
@ -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();

View file

@ -81,7 +81,7 @@ public class ClientTemplateResource {
@Path("protocol-mappers")
public ProtocolMappersResource getProtocolMappers() {
ProtocolMappersResource mappers = new ProtocolMappersResource(template, auth, adminEvent);
ProtocolMappersResource mappers = new ProtocolMappersResource(realm, template, auth, adminEvent);
ResteasyProviderFactory.getInstance().injectProperties(mappers);
return mappers;
}

View file

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

View file

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

View file

@ -20,14 +20,22 @@ import org.jboss.resteasy.annotations.cache.NoCache;
import org.jboss.resteasy.spi.NotFoundException;
import org.keycloak.events.admin.OperationType;
import org.keycloak.events.admin.ResourceType;
import org.keycloak.mappers.FederationConfigValidationException;
import org.keycloak.mappers.UserFederationMapper;
import org.keycloak.mappers.UserFederationMapperFactory;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.ProtocolMapperContainerModel;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserFederationMapperModel;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.models.utils.RepresentationToModel;
import org.keycloak.protocol.ProtocolMapper;
import org.keycloak.protocol.ProtocolMapperConfigException;
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
import org.keycloak.services.ErrorResponse;
import org.keycloak.services.ErrorResponseException;
import org.keycloak.services.ServicesLogger;
import org.keycloak.services.resources.admin.RealmAuth.Resource;
@ -44,8 +52,10 @@ import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import java.text.MessageFormat;
import java.util.LinkedList;
import java.util.List;
import java.util.Properties;
/**
* Base resource for managing users
@ -56,6 +66,8 @@ import java.util.List;
public class ProtocolMappersResource {
protected static final ServicesLogger logger = ServicesLogger.ROOT_LOGGER;
protected RealmModel realm;
protected ProtocolMapperContainerModel client;
protected RealmAuth auth;
@ -68,7 +80,8 @@ public class ProtocolMappersResource {
@Context
protected KeycloakSession session;
public ProtocolMappersResource(ProtocolMapperContainerModel client, RealmAuth auth, AdminEventBuilder adminEvent) {
public ProtocolMappersResource(RealmModel realm, ProtocolMapperContainerModel client, RealmAuth auth, AdminEventBuilder adminEvent) {
this.realm = realm;
this.auth = auth;
this.client = client;
this.adminEvent = adminEvent.resource(ResourceType.PROTOCOL_MAPPER);
@ -119,6 +132,7 @@ public class ProtocolMappersResource {
ProtocolMapperModel model = null;
try {
model = RepresentationToModel.toModel(rep);
validateModel(model);
model = client.addProtocolMapper(model);
adminEvent.operation(OperationType.CREATE).resourcePath(uriInfo, model.getId()).representation(rep).success();
@ -146,6 +160,7 @@ public class ProtocolMappersResource {
ProtocolMapperModel model = null;
for (ProtocolMapperRepresentation rep : reps) {
model = RepresentationToModel.toModel(rep);
validateModel(model);
model = client.addProtocolMapper(model);
}
adminEvent.operation(OperationType.CREATE).resourcePath(uriInfo).representation(reps).success();
@ -216,6 +231,9 @@ public class ProtocolMappersResource {
ProtocolMapperModel model = client.getProtocolMapperById(id);
if (model == null) throw new NotFoundException("Model not found");
model = RepresentationToModel.toModel(rep);
validateModel(model);
client.updateProtocolMapper(model);
adminEvent.operation(OperationType.UPDATE).resourcePath(uriInfo).representation(rep).success();
}
@ -242,4 +260,18 @@ public class ProtocolMappersResource {
}
private void validateModel(ProtocolMapperModel model) {
try {
ProtocolMapper mapper = (ProtocolMapper)session.getKeycloakSessionFactory().getProviderFactory(ProtocolMapper.class, model.getProtocolMapper());
if (mapper != null) {
mapper.validateConfig(session, realm, client, model);
}
} catch (ProtocolMapperConfigException ex) {
logger.error(ex.getMessage());
Properties messages = AdminRoot.getMessages(session, realm, auth.getAuth().getToken().getLocale());
throw new ErrorResponseException(ex.getMessage(), MessageFormat.format(messages.getProperty(ex.getMessage(), ex.getMessage()), ex.getParameters()),
Response.Status.BAD_REQUEST);
}
}
}

View file

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

View file

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

Some files were not shown because too many files have changed in this diff Show more