Merge remote-tracking branch 'upstream/master'
This commit is contained in:
commit
15d31a202f
308 changed files with 12610 additions and 5269 deletions
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -22,8 +22,11 @@ import org.keycloak.authorization.policy.evaluation.Evaluation;
|
|||
import org.keycloak.authorization.policy.provider.PolicyProvider;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
|
||||
import static com.sun.corba.se.spi.activation.IIOP_CLEAR_TEXT.value;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||
*/
|
||||
|
@ -45,9 +48,7 @@ public class TimePolicyProvider implements PolicyProvider {
|
|||
public void evaluate(Evaluation evaluation) {
|
||||
try {
|
||||
String notBefore = this.policy.getConfig().get("nbf");
|
||||
|
||||
if (notBefore != null) {
|
||||
|
||||
if (this.currentDate.before(this.dateFormat.parse(format(notBefore)))) {
|
||||
evaluation.deny();
|
||||
return;
|
||||
|
@ -55,7 +56,6 @@ public class TimePolicyProvider implements PolicyProvider {
|
|||
}
|
||||
|
||||
String notOnOrAfter = this.policy.getConfig().get("noa");
|
||||
|
||||
if (notOnOrAfter != null) {
|
||||
if (this.currentDate.after(this.dateFormat.parse(format(notOnOrAfter)))) {
|
||||
evaluation.deny();
|
||||
|
@ -63,12 +63,48 @@ public class TimePolicyProvider implements PolicyProvider {
|
|||
}
|
||||
}
|
||||
|
||||
if (isInvalid(Calendar.DAY_OF_MONTH, "dayMonth")
|
||||
|| isInvalid(Calendar.MONTH, "month")
|
||||
|| isInvalid(Calendar.YEAR, "year")
|
||||
|| isInvalid(Calendar.HOUR_OF_DAY, "hour")
|
||||
|| isInvalid(Calendar.MINUTE, "minute")) {
|
||||
evaluation.deny();
|
||||
return;
|
||||
}
|
||||
|
||||
evaluation.grant();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Could not evaluate time-based policy [" + this.policy.getName() + "].", e);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isInvalid(int timeConstant, String configName) {
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
|
||||
calendar.setTime(this.currentDate);
|
||||
|
||||
int dateField = calendar.get(timeConstant);
|
||||
|
||||
if (Calendar.MONTH == timeConstant) {
|
||||
dateField++;
|
||||
}
|
||||
|
||||
String start = this.policy.getConfig().get(configName);
|
||||
if (start != null) {
|
||||
String end = this.policy.getConfig().get(configName + "End");
|
||||
if (end != null) {
|
||||
if (dateField < Integer.parseInt(start) || dateField > Integer.parseInt(end)) {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
if (dateField != Integer.parseInt(start)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static String format(String notBefore) {
|
||||
String trimmed = notBefore.trim();
|
||||
|
||||
|
|
|
@ -16,12 +16,31 @@
|
|||
*/
|
||||
package org.keycloak.representations;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAnyGetter;
|
||||
import com.fasterxml.jackson.annotation.JsonAnySetter;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import org.keycloak.json.StringOrArrayDeserializer;
|
||||
import org.keycloak.json.StringOrArraySerializer;
|
||||
|
||||
/**
|
||||
* @author pedroigor
|
||||
*/
|
||||
public class UserInfo {
|
||||
|
||||
// Should be in signed UserInfo response
|
||||
@JsonProperty("iss")
|
||||
protected String issuer;
|
||||
@JsonProperty("aud")
|
||||
@JsonSerialize(using = StringOrArraySerializer.class)
|
||||
@JsonDeserialize(using = StringOrArrayDeserializer.class)
|
||||
protected String[] audience;
|
||||
|
||||
@JsonProperty("sub")
|
||||
protected String sub;
|
||||
|
||||
|
@ -85,6 +104,34 @@ public class UserInfo {
|
|||
@JsonProperty("claims_locales")
|
||||
protected String claimsLocales;
|
||||
|
||||
protected Map<String, Object> otherClaims = new HashMap<>();
|
||||
|
||||
public String getIssuer() {
|
||||
return issuer;
|
||||
}
|
||||
|
||||
public void setIssuer(String issuer) {
|
||||
this.issuer = issuer;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public String[] getAudience() {
|
||||
return audience;
|
||||
}
|
||||
|
||||
public boolean hasAudience(String audience) {
|
||||
for (String a : this.audience) {
|
||||
if (a.equals(audience)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void setAudience(String... audience) {
|
||||
this.audience = audience;
|
||||
}
|
||||
|
||||
public String getSubject() {
|
||||
return this.sub;
|
||||
}
|
||||
|
@ -260,4 +307,19 @@ public class UserInfo {
|
|||
public void setClaimsLocales(String claimsLocales) {
|
||||
this.claimsLocales = claimsLocales;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a map of any other claims and data that might be in the UserInfo. Could be custom claims set up by the auth server
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@JsonAnyGetter
|
||||
public Map<String, Object> getOtherClaims() {
|
||||
return otherClaims;
|
||||
}
|
||||
|
||||
@JsonAnySetter
|
||||
public void setOtherClaims(String name, Object value) {
|
||||
otherClaims.put(name, value);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
173
distribution/adapters/fuse-adapter-zip/pom.xml
Normal file
173
distribution/adapters/fuse-adapter-zip/pom.xml
Normal 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>
|
|
@ -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>
|
||||
|
|
|
@ -202,6 +202,31 @@
|
|||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-dependency-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>unpack</id>
|
||||
<phase>compile</phase>
|
||||
<goals>
|
||||
<goal>unpack</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<artifactItems>
|
||||
<artifactItem>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-wildfly-server-subsystem</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<type>jar</type>
|
||||
<includes>default-config/*.xml</includes>
|
||||
</artifactItem>
|
||||
</artifactItems>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
<!--
|
||||
~ Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
~ and other contributors as indicated by the @author tags.
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
~ Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
~ and other contributors as indicated by the @author tags.
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
|
||||
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
|
||||
xmlns:xalan="http://xml.apache.org/xalan"
|
||||
|
@ -42,7 +42,7 @@
|
|||
<xsl:template match="//ds:datasources">
|
||||
<xsl:copy>
|
||||
<xsl:apply-templates select="node()[name(.)='datasource']"/>
|
||||
<datasource jndi-name="java:jboss/datasources/KeycloakDS" pool-name="KeycloakDS" use-java-context="true">
|
||||
<datasource jndi-name="java:jboss/datasources/KeycloakDS" jta="false" pool-name="KeycloakDS" use-java-context="true">
|
||||
<connection-url>jdbc:h2:${jboss.server.data.dir}/keycloak;AUTO_SERVER=TRUE</connection-url>
|
||||
<driver>h2</driver>
|
||||
<security>
|
||||
|
@ -57,9 +57,7 @@
|
|||
<xsl:template match="//j:profile">
|
||||
<xsl:copy>
|
||||
<xsl:apply-templates select="node()|@*"/>
|
||||
<subsystem xmlns="urn:jboss:domain:keycloak-server:1.1">
|
||||
<web-context>auth</web-context>
|
||||
</subsystem>
|
||||
<xsl:copy-of select="document('../../../target/dependency/default-config/keycloak-server-default-config.xml')"/>
|
||||
<subsystem xmlns="urn:jboss:domain:keycloak:1.1"/>
|
||||
<subsystem xmlns="urn:jboss:domain:keycloak-saml:1.1"/>
|
||||
</xsl:copy>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -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"/>
|
||||
|
|
|
@ -28,6 +28,8 @@
|
|||
</resources>
|
||||
|
||||
<dependencies>
|
||||
<module name="com.fasterxml.jackson.core.jackson-core"/>
|
||||
<module name="com.fasterxml.jackson.core.jackson-databind"/>
|
||||
<module name="javax.api"/>
|
||||
<module name="org.jboss.staxmapper"/>
|
||||
<module name="org.jboss.as.controller"/>
|
||||
|
|
|
@ -84,10 +84,6 @@
|
|||
</fileSets>
|
||||
|
||||
<files>
|
||||
<file>
|
||||
<source>${project.build.directory}/unpacked/keycloak-${project.version}/standalone/configuration/keycloak-server.json</source>
|
||||
<outputDirectory>standalone/configuration</outputDirectory>
|
||||
</file>
|
||||
<file>
|
||||
<source>${project.build.directory}/unpacked/keycloak-${project.version}/bin/add-user-keycloak.sh</source>
|
||||
<outputDirectory>bin</outputDirectory>
|
||||
|
@ -98,6 +94,11 @@
|
|||
<outputDirectory>bin</outputDirectory>
|
||||
<destName>add-user-keycloak.bat</destName>
|
||||
</file>
|
||||
<file>
|
||||
<source>${project.build.directory}/cli/default-keycloak-subsys-config.cli</source>
|
||||
<outputDirectory>bin</outputDirectory>
|
||||
<destName>default-keycloak-subsys-config.cli</destName>
|
||||
</file>
|
||||
</files>
|
||||
|
||||
</assembly>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
embed-server --server-config=standalone-ha.xml
|
||||
/subsystem=datasources/data-source=KeycloakDS/:add(connection-url="jdbc:h2:${jboss.server.data.dir}/keycloak;AUTO_SERVER=TRUE",driver-name=h2,jndi-name=java:jboss/datasources/KeycloakDS,password=sa,user-name=sa,use-java-context=true)
|
||||
/subsystem=datasources/data-source=KeycloakDS/:add(connection-url="jdbc:h2:${jboss.server.data.dir}/keycloak;AUTO_SERVER=TRUE",jta=false,driver-name=h2,jndi-name=java:jboss/datasources/KeycloakDS,password=sa,user-name=sa,use-java-context=true)
|
||||
/subsystem=infinispan/cache-container=keycloak:add(jndi-name="infinispan/Keycloak")
|
||||
/subsystem=infinispan/cache-container=keycloak/transport=TRANSPORT:add(lock-timeout=60000)
|
||||
/subsystem=infinispan/cache-container=keycloak/invalidation-cache=realms:add(mode="SYNC")
|
||||
|
@ -11,4 +11,4 @@ embed-server --server-config=standalone-ha.xml
|
|||
/subsystem=infinispan/cache-container=keycloak/distributed-cache=authorization:add(mode="SYNC",owners="1")
|
||||
/subsystem=infinispan/cache-container=keycloak/replicated-cache=work:add(mode="SYNC")
|
||||
/extension=org.keycloak.keycloak-server-subsystem/:add(module=org.keycloak.keycloak-server-subsystem)
|
||||
/subsystem=keycloak-server:add(web-context=auth)
|
||||
run-batch --file=default-keycloak-subsys-config.cli
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
embed-server --server-config=standalone.xml
|
||||
/subsystem=datasources/data-source=KeycloakDS/:add(connection-url="jdbc:h2:${jboss.server.data.dir}/keycloak;AUTO_SERVER=TRUE",driver-name=h2,jndi-name=java:jboss/datasources/KeycloakDS,password=sa,user-name=sa,use-java-context=true)
|
||||
/subsystem=datasources/data-source=KeycloakDS/:add(connection-url="jdbc:h2:${jboss.server.data.dir}/keycloak;AUTO_SERVER=TRUE",jta=false,driver-name=h2,jndi-name=java:jboss/datasources/KeycloakDS,password=sa,user-name=sa,use-java-context=true)
|
||||
/subsystem=infinispan/cache-container=keycloak:add(jndi-name="infinispan/Keycloak")
|
||||
/subsystem=infinispan/cache-container=keycloak/local-cache=realms:add()
|
||||
/subsystem=infinispan/cache-container=keycloak/local-cache=users:add()
|
||||
|
@ -11,4 +11,4 @@ embed-server --server-config=standalone.xml
|
|||
/subsystem=infinispan/cache-container=keycloak/local-cache=authorization:add()
|
||||
/subsystem=infinispan/cache-container=keycloak/local-cache=authorization/eviction=EVICTION:add(max-entries=100,strategy=LRU)
|
||||
/extension=org.keycloak.keycloak-server-subsystem/:add(module=org.keycloak.keycloak-server-subsystem)
|
||||
/subsystem=keycloak-server:add(web-context=auth)
|
||||
run-batch --file=default-keycloak-subsys-config.cli
|
|
@ -45,6 +45,25 @@
|
|||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-dependency-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>unpack</id>
|
||||
<phase>compile</phase>
|
||||
<goals>
|
||||
<goal>unpack</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<artifactItems>
|
||||
<artifactItem>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-wildfly-server-subsystem</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<type>jar</type>
|
||||
<includes>cli/*.cli</includes>
|
||||
<outputDirectory>${project.build.directory}</outputDirectory>
|
||||
</artifactItem>
|
||||
</artifactItems>
|
||||
</configuration>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>unpack-server-dist</id>
|
||||
<phase>prepare-package</phase>
|
||||
|
|
|
@ -39,6 +39,12 @@
|
|||
<version>1.0.0.Final</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>javax.enterprise</groupId>
|
||||
<artifactId>cdi-api</artifactId>
|
||||
<version>1.0-SP4</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-authz-client</artifactId>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.keycloak.example.photoz.util;
|
||||
|
||||
import javax.enterprise.inject.Instance;
|
||||
import javax.inject.Inject;
|
||||
import javax.interceptor.AroundInvoke;
|
||||
import javax.interceptor.Interceptor;
|
||||
import javax.interceptor.InvocationContext;
|
||||
import javax.persistence.EntityManager;
|
||||
import javax.persistence.EntityTransaction;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||
*/
|
||||
@Interceptor
|
||||
@Transaction
|
||||
public class TransactionInterceptor {
|
||||
|
||||
@Inject
|
||||
private Instance<EntityManager> entityManager;
|
||||
|
||||
@AroundInvoke
|
||||
public Object aroundInvoke(InvocationContext context) {
|
||||
EntityManager entityManager = this.entityManager.get();
|
||||
EntityTransaction transaction = entityManager.getTransaction();
|
||||
|
||||
try {
|
||||
transaction.begin();
|
||||
Object proceed = context.proceed();
|
||||
transaction.commit();
|
||||
return proceed;
|
||||
} catch (Exception cause) {
|
||||
if (transaction != null && transaction.isActive()) {
|
||||
transaction.rollback();
|
||||
}
|
||||
throw new RuntimeException(cause);
|
||||
} finally {
|
||||
entityManager.close();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,5 +3,7 @@
|
|||
xsi:schemaLocation="
|
||||
http://java.sun.com/xml/ns/javaee
|
||||
http://java.sun.com/xml/ns/javaee/beans_1_0.xsd">
|
||||
|
||||
<interceptors>
|
||||
<class>org.keycloak.example.photoz.util.TransactionInterceptor</class>
|
||||
</interceptors>
|
||||
</beans>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
<deployment>
|
||||
<dependencies>
|
||||
<module name="org.keycloak.keycloak-authz-client" services="import"/>
|
||||
<module name="com.h2database.h2" services="import"/>
|
||||
</dependencies>
|
||||
</deployment>
|
||||
</jboss-deployment-structure>
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
<datasources xmlns="http://www.jboss.org/ironjacamar/schema"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.jboss.org/ironjacamar/schema http://docs.jboss.org/ironjacamar/schema/datasources_1_0.xsd">
|
||||
<datasource jndi-name="java:jboss/datasources/PhotozDS" pool-name="PhotozDS" enabled="true" use-java-context="true">
|
||||
<connection-url>jdbc:h2:${jboss.server.data.dir}/kc-authz-photo;AUTO_SERVER=TRUE</connection-url>
|
||||
<driver>h2</driver>
|
||||
<security>
|
||||
<user-name>sa</user-name>
|
||||
<password>sa</password>
|
||||
</security>
|
||||
</datasource>
|
||||
</datasources>
|
|
@ -3,7 +3,7 @@
|
|||
What is it?
|
||||
-----------
|
||||
|
||||
This example demonstrates how to use Social Ientity Providers with Keycloak to authenticate users. In this case,
|
||||
This example demonstrates how to use Social Identity Providers with Keycloak to authenticate users. In this case,
|
||||
users are authenticated with Google using Keycloak Identity Broker capabilities using the oAuth 2 protocol.
|
||||
|
||||
From this example, you'll learn how to:
|
||||
|
@ -180,4 +180,4 @@ Debug the Application
|
|||
If you want to debug the source code or look at the Javadocs of any library in the project, run either of the following commands to pull them into your local repository. The IDE should then detect them.
|
||||
|
||||
mvn dependency:sources
|
||||
mvn dependency:resolve -Dclassifier=javadoc
|
||||
mvn dependency:resolve -Dclassifier=javadoc
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
What is it?
|
||||
-----------
|
||||
|
||||
This example demonstrates how to use Social Ientity Providers with Keycloak to authenticate users. In this case,
|
||||
This example demonstrates how to use Social Identity Providers with Keycloak to authenticate users. In this case,
|
||||
users are authenticated with Twitter using Keycloak Identity Broker capabilities using the oAuth 2 protocol.
|
||||
|
||||
From this example, you'll learn how to:
|
||||
|
@ -179,4 +179,4 @@ Debug the Application
|
|||
If you want to debug the source code or look at the Javadocs of any library in the project, run either of the following commands to pull them into your local repository. The IDE should then detect them.
|
||||
|
||||
mvn dependency:sources
|
||||
mvn dependency:resolve -Dclassifier=javadoc
|
||||
mvn dependency:resolve -Dclassifier=javadoc
|
||||
|
|
65
misc/UpdatingServerConfig.md
Normal file
65
misc/UpdatingServerConfig.md
Normal 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="["EVENT1","EVENT2"]"/>
|
||||
</properties>
|
||||
</provider>
|
||||
</spi>
|
||||
````
|
||||
We see that the value begins and ends with square brackets. That means that
|
||||
the value will be passed to the provider as a list. In this example,
|
||||
the system will pass the
|
||||
provider a list with two element values `EVENT1` and `EVENT2`. To add
|
||||
more values to the list, just separate each list element with a comma. Unfortunately,
|
||||
you do need to escape the quotes surrounding each list element with
|
||||
`"`.
|
|
@ -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<>();
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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)");
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
|
@ -30,6 +30,9 @@ import org.keycloak.connections.jpa.updater.liquibase.conn.LiquibaseConnectionPr
|
|||
import org.keycloak.connections.jpa.util.JpaUtils;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.sql.Connection;
|
||||
import java.util.List;
|
||||
|
@ -53,6 +56,15 @@ public class LiquibaseJpaUpdaterProvider implements JpaUpdaterProvider {
|
|||
|
||||
@Override
|
||||
public void update(Connection connection, String defaultSchema) {
|
||||
update(connection, null, defaultSchema);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void export(Connection connection, String defaultSchema, File file) {
|
||||
update(connection, file, defaultSchema);
|
||||
}
|
||||
|
||||
private void update(Connection connection, File file, String defaultSchema) {
|
||||
logger.debug("Starting database update");
|
||||
|
||||
// Need ThreadLocal as liquibase doesn't seem to have API to inject custom objects into tasks
|
||||
|
@ -61,7 +73,7 @@ public class LiquibaseJpaUpdaterProvider implements JpaUpdaterProvider {
|
|||
try {
|
||||
// Run update with keycloak master changelog first
|
||||
Liquibase liquibase = getLiquibaseForKeycloakUpdate(connection, defaultSchema);
|
||||
updateChangeSet(liquibase, liquibase.getChangeLogFile());
|
||||
updateChangeSet(liquibase, liquibase.getChangeLogFile(), file);
|
||||
|
||||
// Run update for each custom JpaEntityProvider
|
||||
Set<JpaEntityProvider> jpaProviders = session.getAllProviders(JpaEntityProvider.class);
|
||||
|
@ -71,7 +83,7 @@ public class LiquibaseJpaUpdaterProvider implements JpaUpdaterProvider {
|
|||
String factoryId = jpaProvider.getFactoryId();
|
||||
String changelogTableName = JpaUtils.getCustomChangelogTableName(factoryId);
|
||||
liquibase = getLiquibaseForCustomProviderUpdate(connection, defaultSchema, customChangelog, jpaProvider.getClass().getClassLoader(), changelogTableName);
|
||||
updateChangeSet(liquibase, liquibase.getChangeLogFile());
|
||||
updateChangeSet(liquibase, liquibase.getChangeLogFile(), file);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
|
@ -81,7 +93,8 @@ public class LiquibaseJpaUpdaterProvider implements JpaUpdaterProvider {
|
|||
}
|
||||
}
|
||||
|
||||
protected void updateChangeSet(Liquibase liquibase, String changelog) throws LiquibaseException {
|
||||
|
||||
protected void updateChangeSet(Liquibase liquibase, String changelog, File exportFile) throws LiquibaseException, IOException {
|
||||
List<ChangeSet> changeSets = liquibase.listUnrunChangeSets((Contexts) null);
|
||||
if (!changeSets.isEmpty()) {
|
||||
List<RanChangeSet> ranChangeSets = liquibase.getDatabase().getRanChangeSetList();
|
||||
|
@ -95,7 +108,12 @@ public class LiquibaseJpaUpdaterProvider implements JpaUpdaterProvider {
|
|||
}
|
||||
}
|
||||
|
||||
liquibase.update((Contexts) null);
|
||||
if (exportFile != null) {
|
||||
liquibase.update((Contexts) null, new FileWriter(exportFile));
|
||||
} else {
|
||||
liquibase.update((Contexts) null);
|
||||
}
|
||||
|
||||
logger.debugv("Completed database update for changelog {0}", changelog);
|
||||
} else {
|
||||
logger.debugv("Database is up to date for changelog {0}", changelog);
|
||||
|
@ -107,13 +125,18 @@ public class LiquibaseJpaUpdaterProvider implements JpaUpdaterProvider {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void validate(Connection connection, String defaultSchema) {
|
||||
public Status validate(Connection connection, String defaultSchema) {
|
||||
logger.debug("Validating if database is updated");
|
||||
ThreadLocalSessionContext.setCurrentSession(session);
|
||||
|
||||
try {
|
||||
// Validate with keycloak master changelog first
|
||||
Liquibase liquibase = getLiquibaseForKeycloakUpdate(connection, defaultSchema);
|
||||
validateChangeSet(liquibase, liquibase.getChangeLogFile());
|
||||
|
||||
Status status = validateChangeSet(liquibase, liquibase.getChangeLogFile());
|
||||
if (status != Status.VALID) {
|
||||
return status;
|
||||
}
|
||||
|
||||
// Validate each custom JpaEntityProvider
|
||||
Set<JpaEntityProvider> jpaProviders = session.getAllProviders(JpaEntityProvider.class);
|
||||
|
@ -123,24 +146,30 @@ public class LiquibaseJpaUpdaterProvider implements JpaUpdaterProvider {
|
|||
String factoryId = jpaProvider.getFactoryId();
|
||||
String changelogTableName = JpaUtils.getCustomChangelogTableName(factoryId);
|
||||
liquibase = getLiquibaseForCustomProviderUpdate(connection, defaultSchema, customChangelog, jpaProvider.getClass().getClassLoader(), changelogTableName);
|
||||
validateChangeSet(liquibase, liquibase.getChangeLogFile());
|
||||
if (validateChangeSet(liquibase, liquibase.getChangeLogFile()) != Status.VALID) {
|
||||
return Status.OUTDATED;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (LiquibaseException e) {
|
||||
throw new RuntimeException("Failed to validate database", e);
|
||||
}
|
||||
|
||||
return Status.VALID;
|
||||
}
|
||||
|
||||
protected void validateChangeSet(Liquibase liquibase, String changelog) throws LiquibaseException {
|
||||
protected Status validateChangeSet(Liquibase liquibase, String changelog) throws LiquibaseException {
|
||||
List<ChangeSet> changeSets = liquibase.listUnrunChangeSets((Contexts) null);
|
||||
if (!changeSets.isEmpty()) {
|
||||
List<RanChangeSet> ranChangeSets = liquibase.getDatabase().getRanChangeSetList();
|
||||
String errorMessage = String.format("Failed to validate database schema. Schema needs updating database from %s to %s. Please change databaseSchema to 'update' or use other database. Used changelog was %s",
|
||||
ranChangeSets.get(ranChangeSets.size() - 1).getId(), changeSets.get(changeSets.size() - 1).getId(), changelog);
|
||||
throw new RuntimeException(errorMessage);
|
||||
if (changeSets.size() == liquibase.getDatabaseChangeLog().getChangeSets().size()) {
|
||||
return Status.EMPTY;
|
||||
} else {
|
||||
logger.debugf("Validation failed. Database is not up-to-date for changelog %s", changelog);
|
||||
return Status.OUTDATED;
|
||||
}
|
||||
} else {
|
||||
logger.debugf("Validation passed. Database is up-to-date for changelog %s", changelog);
|
||||
return Status.VALID;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -498,6 +498,18 @@ public class ClientAdapter implements ClientModel, JpaModel<ClientEntity> {
|
|||
@Override
|
||||
public void updateClient() {
|
||||
em.flush();
|
||||
session.getKeycloakSessionFactory().publish(new RealmModel.ClientUpdatedEvent() {
|
||||
|
||||
@Override
|
||||
public ClientModel getUpdatedClient() {
|
||||
return ClientAdapter.this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeycloakSession getKeycloakSession() {
|
||||
return session;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -17,8 +17,6 @@
|
|||
|
||||
package org.keycloak.models.mongo.keycloak.adapters;
|
||||
|
||||
import com.mongodb.DBObject;
|
||||
import com.mongodb.QueryBuilder;
|
||||
import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.ClientTemplateModel;
|
||||
|
@ -65,6 +63,19 @@ public class ClientAdapter extends AbstractMongoAdapter<MongoClientEntity> imple
|
|||
@Override
|
||||
public void updateClient() {
|
||||
updateMongoEntity();
|
||||
|
||||
session.getKeycloakSessionFactory().publish(new RealmModel.ClientUpdatedEvent() {
|
||||
|
||||
@Override
|
||||
public ClientModel getUpdatedClient() {
|
||||
return ClientAdapter.this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeycloakSession getKeycloakSession() {
|
||||
return session;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak;
|
||||
|
||||
/**
|
||||
* Non-recoverable error thrown during server startup
|
||||
*
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
public class ServerStartupError extends Error {
|
||||
|
||||
private final boolean fillStackTrace;
|
||||
|
||||
public ServerStartupError(String message) {
|
||||
super(message);
|
||||
fillStackTrace = true;
|
||||
}
|
||||
|
||||
public ServerStartupError(String message, boolean fillStackTrace) {
|
||||
super(message);
|
||||
this.fillStackTrace = fillStackTrace;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized Throwable fillInStackTrace() {
|
||||
if (fillStackTrace) {
|
||||
return super.fillInStackTrace();
|
||||
} else {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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>.
|
||||
*
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -50,7 +50,7 @@ public abstract class AbstractIdentityProviderFactory<T extends IdentityProvider
|
|||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> parseConfig(InputStream inputStream) {
|
||||
public Map<String, String> parseConfig(KeycloakSession session, InputStream inputStream) {
|
||||
return new HashMap<String, String>();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
package org.keycloak.broker.provider;
|
||||
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
@ -47,8 +48,9 @@ public interface IdentityProviderFactory<T extends IdentityProvider> extends Pro
|
|||
* <p>Creates an {@link IdentityProvider} based on the configuration from
|
||||
* <code>inputStream</code>.</p>
|
||||
*
|
||||
* @param session
|
||||
* @param inputStream The input stream from where configuration will be loaded from..
|
||||
* @return
|
||||
*/
|
||||
Map<String, String> parseConfig(InputStream inputStream);
|
||||
Map<String, String> parseConfig(KeycloakSession session, InputStream inputStream);
|
||||
}
|
|
@ -58,4 +58,7 @@ public interface Details {
|
|||
|
||||
String CLIENT_AUTH_METHOD = "client_auth_method";
|
||||
|
||||
String SIGNATURE_REQUIRED = "signature_required";
|
||||
String SIGNATURE_ALGORITHM = "signature_algorithm";
|
||||
|
||||
}
|
||||
|
|
|
@ -49,8 +49,8 @@ public class MigrationUtils {
|
|||
|
||||
public static void updateOTPRequiredAction(RequiredActionProviderModel otpAction) {
|
||||
if (otpAction == null) return;
|
||||
if (!otpAction.getProviderId().equals(UserModel.RequiredAction.CONFIGURE_TOTP.name())) return;
|
||||
if (!otpAction.getName().equals("Configure Totp")) return;
|
||||
if (!UserModel.RequiredAction.CONFIGURE_TOTP.name().equals(otpAction.getProviderId())) return;
|
||||
if (!"Configure Totp".equals(otpAction.getName())) return;
|
||||
|
||||
otpAction.setName("Configure OTP");
|
||||
}
|
||||
|
|
|
@ -56,6 +56,12 @@ public interface RealmModel extends RoleContainerModel {
|
|||
ClientModel getCreatedClient();
|
||||
}
|
||||
|
||||
// Called also during client creation after client is fully initialized (including all attributes etc)
|
||||
interface ClientUpdatedEvent extends ProviderEvent {
|
||||
ClientModel getUpdatedClient();
|
||||
KeycloakSession getKeycloakSession();
|
||||
}
|
||||
|
||||
interface ClientRemovedEvent extends ProviderEvent {
|
||||
ClientModel getClient();
|
||||
KeycloakSession getKeycloakSession();
|
||||
|
|
|
@ -943,7 +943,6 @@ public class RepresentationToModel {
|
|||
} else {
|
||||
client.setNodeReRegistrationTimeout(-1);
|
||||
}
|
||||
client.updateClient();
|
||||
|
||||
if (resourceRep.getNotBefore() != null) {
|
||||
client.setNotBefore(resourceRep.getNotBefore());
|
||||
|
@ -1043,6 +1042,8 @@ public class RepresentationToModel {
|
|||
if (resourceRep.isUseTemplateMappers() != null) client.setUseTemplateMappers(resourceRep.isUseTemplateMappers());
|
||||
else client.setUseTemplateMappers(resourceRep.getClientTemplate() != null);
|
||||
|
||||
client.updateClient();
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
|
@ -1066,7 +1067,6 @@ public class RepresentationToModel {
|
|||
if (rep.isSurrogateAuthRequired() != null) resource.setSurrogateAuthRequired(rep.isSurrogateAuthRequired());
|
||||
if (rep.getNodeReRegistrationTimeout() != null) resource.setNodeReRegistrationTimeout(rep.getNodeReRegistrationTimeout());
|
||||
if (rep.getClientAuthenticatorType() != null) resource.setClientAuthenticatorType(rep.getClientAuthenticatorType());
|
||||
resource.updateClient();
|
||||
|
||||
if (rep.getProtocol() != null) resource.setProtocol(rep.getProtocol());
|
||||
if (rep.getAttributes() != null) {
|
||||
|
@ -1122,7 +1122,7 @@ public class RepresentationToModel {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
resource.updateClient();
|
||||
}
|
||||
|
||||
// CLIENT TEMPLATES
|
||||
|
|
|
@ -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 {
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ package org.keycloak.broker.oidc;
|
|||
|
||||
import org.keycloak.broker.provider.AbstractIdentityProviderFactory;
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.Map;
|
||||
|
@ -45,8 +46,8 @@ public class KeycloakOIDCIdentityProviderFactory extends AbstractIdentityProvide
|
|||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> parseConfig(InputStream inputStream) {
|
||||
return OIDCIdentityProviderFactory.parseOIDCConfig(inputStream);
|
||||
public Map<String, String> parseConfig(KeycloakSession session, InputStream inputStream) {
|
||||
return OIDCIdentityProviderFactory.parseOIDCConfig(session, inputStream);
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@ package org.keycloak.broker.oidc;
|
|||
import org.keycloak.broker.provider.AbstractIdentityProviderFactory;
|
||||
import org.keycloak.jose.jwk.JWK;
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.jose.jwk.JSONWebKeySet;
|
||||
import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation;
|
||||
|
@ -56,11 +57,11 @@ public class OIDCIdentityProviderFactory extends AbstractIdentityProviderFactory
|
|||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> parseConfig(InputStream inputStream) {
|
||||
return parseOIDCConfig(inputStream);
|
||||
public Map<String, String> parseConfig(KeycloakSession session, InputStream inputStream) {
|
||||
return parseOIDCConfig(session, inputStream);
|
||||
}
|
||||
|
||||
protected static Map<String, String> parseOIDCConfig(InputStream inputStream) {
|
||||
protected static Map<String, String> parseOIDCConfig(KeycloakSession session, InputStream inputStream) {
|
||||
OIDCConfigurationRepresentation rep;
|
||||
try {
|
||||
rep = JsonSerialization.readValue(inputStream, OIDCConfigurationRepresentation.class);
|
||||
|
@ -74,14 +75,14 @@ public class OIDCIdentityProviderFactory extends AbstractIdentityProviderFactory
|
|||
config.setTokenUrl(rep.getTokenEndpoint());
|
||||
config.setUserInfoUrl(rep.getUserinfoEndpoint());
|
||||
if (rep.getJwksUri() != null) {
|
||||
sendJwksRequest(rep, config);
|
||||
sendJwksRequest(session, rep, config);
|
||||
}
|
||||
return config.getConfig();
|
||||
}
|
||||
|
||||
protected static void sendJwksRequest(OIDCConfigurationRepresentation rep, OIDCIdentityProviderConfig config) {
|
||||
protected static void sendJwksRequest(KeycloakSession session, OIDCConfigurationRepresentation rep, OIDCIdentityProviderConfig config) {
|
||||
try {
|
||||
JSONWebKeySet keySet = JWKSUtils.sendJwksRequest(rep.getJwksUri());
|
||||
JSONWebKeySet keySet = JWKSUtils.sendJwksRequest(session, rep.getJwksUri());
|
||||
PublicKey key = JWKSUtils.getKeyForUse(keySet, JWK.Use.SIG);
|
||||
if (key == null) {
|
||||
logger.supportedJwkNotFound(JWK.Use.SIG.asString());
|
||||
|
|
|
@ -24,6 +24,7 @@ import org.keycloak.dom.saml.v2.metadata.IDPSSODescriptorType;
|
|||
import org.keycloak.dom.saml.v2.metadata.KeyDescriptorType;
|
||||
import org.keycloak.dom.saml.v2.metadata.KeyTypes;
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
|
||||
import org.keycloak.saml.common.exceptions.ParsingException;
|
||||
import org.keycloak.saml.common.util.DocumentUtil;
|
||||
|
@ -54,7 +55,7 @@ public class SAMLIdentityProviderFactory extends AbstractIdentityProviderFactory
|
|||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> parseConfig(InputStream inputStream) {
|
||||
public Map<String, String> parseConfig(KeycloakSession session, InputStream inputStream) {
|
||||
try {
|
||||
Object parsedObject = new SAMLParser().parse(inputStream);
|
||||
EntityDescriptorType entityType;
|
||||
|
|
|
@ -0,0 +1,106 @@
|
|||
/*
|
||||
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.protocol.oidc;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
import org.keycloak.jose.jws.Algorithm;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class OIDCAdvancedConfigWrapper {
|
||||
|
||||
private static final String USER_INFO_RESPONSE_SIGNATURE_ALG = "user.info.response.signature.alg";
|
||||
|
||||
private static final String REQUEST_OBJECT_SIGNATURE_ALG = "request.object.signature.alg";
|
||||
|
||||
private final ClientModel clientModel;
|
||||
private final ClientRepresentation clientRep;
|
||||
|
||||
private OIDCAdvancedConfigWrapper(ClientModel client, ClientRepresentation clientRep) {
|
||||
this.clientModel = client;
|
||||
this.clientRep = clientRep;
|
||||
}
|
||||
|
||||
|
||||
public static OIDCAdvancedConfigWrapper fromClientModel(ClientModel client) {
|
||||
return new OIDCAdvancedConfigWrapper(client, null);
|
||||
}
|
||||
|
||||
public static OIDCAdvancedConfigWrapper fromClientRepresentation(ClientRepresentation clientRep) {
|
||||
return new OIDCAdvancedConfigWrapper(null, clientRep);
|
||||
}
|
||||
|
||||
|
||||
public Algorithm getUserInfoSignedResponseAlg() {
|
||||
String alg = getAttribute(USER_INFO_RESPONSE_SIGNATURE_ALG);
|
||||
return alg==null ? null : Enum.valueOf(Algorithm.class, alg);
|
||||
}
|
||||
|
||||
public void setUserInfoSignedResponseAlg(Algorithm alg) {
|
||||
String algStr = alg==null ? null : alg.toString();
|
||||
setAttribute(USER_INFO_RESPONSE_SIGNATURE_ALG, algStr);
|
||||
}
|
||||
|
||||
public boolean isUserInfoSignatureRequired() {
|
||||
return getUserInfoSignedResponseAlg() != null;
|
||||
}
|
||||
|
||||
public Algorithm getRequestObjectSignatureAlg() {
|
||||
String alg = getAttribute(REQUEST_OBJECT_SIGNATURE_ALG);
|
||||
return alg==null ? null : Enum.valueOf(Algorithm.class, alg);
|
||||
}
|
||||
|
||||
public void setRequestObjectSignatureAlg(Algorithm alg) {
|
||||
String algStr = alg==null ? null : alg.toString();
|
||||
setAttribute(REQUEST_OBJECT_SIGNATURE_ALG, algStr);
|
||||
}
|
||||
|
||||
|
||||
private String getAttribute(String attrKey) {
|
||||
if (clientModel != null) {
|
||||
return clientModel.getAttribute(attrKey);
|
||||
} else {
|
||||
return clientRep.getAttributes()==null ? null : clientRep.getAttributes().get(attrKey);
|
||||
}
|
||||
}
|
||||
|
||||
private void setAttribute(String attrKey, String attrValue) {
|
||||
if (clientModel != null) {
|
||||
if (attrValue != null) {
|
||||
clientModel.setAttribute(attrKey, attrValue);
|
||||
} else {
|
||||
clientModel.removeAttribute(attrKey);
|
||||
}
|
||||
} else {
|
||||
if (attrValue != null) {
|
||||
if (clientRep.getAttributes() == null) {
|
||||
clientRep.setAttributes(new HashMap<>());
|
||||
}
|
||||
clientRep.getAttributes().put(attrKey, attrValue);
|
||||
} else {
|
||||
if (clientRep.getAttributes() != null) {
|
||||
clientRep.getAttributes().put(attrKey, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -66,6 +66,7 @@ public class OIDCLoginProtocol implements LoginProtocol {
|
|||
public static final String REQUEST_PARAM = "request";
|
||||
public static final String REQUEST_URI_PARAM = "request_uri";
|
||||
public static final String UI_LOCALES_PARAM = OAuth2Constants.UI_LOCALES_PARAM;
|
||||
public static final String CLAIMS_PARAM = "claims";
|
||||
|
||||
public static final String LOGOUT_REDIRECT_URI = "OIDC_LOGOUT_REDIRECT_URI";
|
||||
public static final String ISSUER = "iss";
|
||||
|
|
|
@ -48,6 +48,10 @@ public class OIDCWellKnownProvider implements WellKnownProvider {
|
|||
|
||||
public static final List<String> DEFAULT_ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED = list(Algorithm.RS256.toString());
|
||||
|
||||
public static final List<String> DEFAULT_USER_INFO_SIGNING_ALG_VALUES_SUPPORTED = list(Algorithm.RS256.toString());
|
||||
|
||||
public static final List<String> DEFAULT_REQUEST_OBJECT_SIGNING_ALG_VALUES_SUPPORTED = list(Algorithm.none.toString(), Algorithm.RS256.toString());
|
||||
|
||||
public static final List<String> DEFAULT_GRANT_TYPES_SUPPORTED = list(OAuth2Constants.AUTHORIZATION_CODE, OAuth2Constants.IMPLICIT, OAuth2Constants.REFRESH_TOKEN, OAuth2Constants.PASSWORD, OAuth2Constants.CLIENT_CREDENTIALS);
|
||||
|
||||
public static final List<String> DEFAULT_RESPONSE_TYPES_SUPPORTED = list(OAuth2Constants.CODE, OIDCResponseType.NONE, OIDCResponseType.ID_TOKEN, OIDCResponseType.TOKEN, "id_token token", "code id_token", "code token", "code id_token token");
|
||||
|
@ -90,6 +94,8 @@ public class OIDCWellKnownProvider implements WellKnownProvider {
|
|||
config.setRegistrationEndpoint(RealmsResource.clientRegistrationUrl(uriInfo).path(ClientRegistrationService.class, "provider").build(realm.getName(), OIDCClientRegistrationProviderFactory.ID).toString());
|
||||
|
||||
config.setIdTokenSigningAlgValuesSupported(DEFAULT_ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED);
|
||||
config.setUserInfoSigningAlgValuesSupported(DEFAULT_USER_INFO_SIGNING_ALG_VALUES_SUPPORTED);
|
||||
config.setRequestObjectSigningAlgValuesSupported(DEFAULT_REQUEST_OBJECT_SIGNING_ALG_VALUES_SUPPORTED);
|
||||
config.setResponseTypesSupported(DEFAULT_RESPONSE_TYPES_SUPPORTED);
|
||||
config.setSubjectTypesSupported(DEFAULT_SUBJECT_TYPES_SUPPORTED);
|
||||
config.setResponseModesSupported(DEFAULT_RESPONSE_MODES_SUPPORTED);
|
||||
|
@ -104,8 +110,8 @@ public class OIDCWellKnownProvider implements WellKnownProvider {
|
|||
|
||||
config.setScopesSupported(SCOPES_SUPPORTED);
|
||||
|
||||
config.setRequestParameterSupported(false);
|
||||
config.setRequestUriParameterSupported(false);
|
||||
config.setRequestParameterSupported(true);
|
||||
config.setRequestUriParameterSupported(true);
|
||||
|
||||
return config;
|
||||
}
|
||||
|
|
|
@ -17,11 +17,6 @@
|
|||
|
||||
package org.keycloak.protocol.oidc.endpoints;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.core.MultivaluedMap;
|
||||
import javax.ws.rs.core.Response;
|
||||
|
@ -42,6 +37,8 @@ import org.keycloak.models.IdentityProviderModel;
|
|||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.protocol.AuthorizationEndpointBase;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.protocol.oidc.endpoints.request.AuthorizationEndpointRequest;
|
||||
import org.keycloak.protocol.oidc.endpoints.request.AuthorizationEndpointRequestParserProcessor;
|
||||
import org.keycloak.protocol.oidc.utils.OIDCRedirectUriBuilder;
|
||||
import org.keycloak.protocol.oidc.utils.OIDCResponseMode;
|
||||
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
|
||||
|
@ -67,43 +64,10 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
|
|||
/**
|
||||
* Prefix used to store additional HTTP GET params from original client request into {@link ClientSessionModel} note to be available later in Authenticators, RequiredActions etc. Prefix is used to
|
||||
* prevent collisions with internally used notes.
|
||||
*
|
||||
*
|
||||
* @see ClientSessionModel#getNote(String)
|
||||
* @see #KNOWN_REQ_PARAMS
|
||||
* @see #additionalReqParams
|
||||
*/
|
||||
public static final String CLIENT_SESSION_NOTE_ADDITIONAL_REQ_PARAMS_PREFIX = "client_request_param_";
|
||||
/**
|
||||
* Max number of additional req params copied into client session note to prevent DoS attacks
|
||||
*
|
||||
* @see #additionalReqParams
|
||||
*/
|
||||
public static final int ADDITIONAL_REQ_PARAMS_MAX_MUMBER = 5;
|
||||
/**
|
||||
* Max size of additional req param value copied into client session note to prevent DoS attacks - params with longer value are ignored
|
||||
*
|
||||
* @see #additionalReqParams
|
||||
*/
|
||||
public static final int ADDITIONAL_REQ_PARAMS_MAX_SIZE = 200;
|
||||
|
||||
/** Set of known protocol GET params not to be stored into {@link #additionalReqParams} */
|
||||
private static final Set<String> KNOWN_REQ_PARAMS = new HashSet<>();
|
||||
static {
|
||||
KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.CLIENT_ID_PARAM);
|
||||
KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.RESPONSE_TYPE_PARAM);
|
||||
KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.RESPONSE_MODE_PARAM);
|
||||
KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.REDIRECT_URI_PARAM);
|
||||
KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.STATE_PARAM);
|
||||
KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.SCOPE_PARAM);
|
||||
KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.LOGIN_HINT_PARAM);
|
||||
KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.PROMPT_PARAM);
|
||||
KNOWN_REQ_PARAMS.add(AdapterConstants.KC_IDP_HINT);
|
||||
KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.NONCE_PARAM);
|
||||
KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.MAX_AGE_PARAM);
|
||||
KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.UI_LOCALES_PARAM);
|
||||
KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.REQUEST_PARAM);
|
||||
KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.REQUEST_URI_PARAM);
|
||||
}
|
||||
|
||||
private enum Action {
|
||||
REGISTER, CODE, FORGOT_CREDENTIALS
|
||||
|
@ -116,19 +80,8 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
|
|||
private OIDCResponseType parsedResponseType;
|
||||
private OIDCResponseMode parsedResponseMode;
|
||||
|
||||
private String clientId;
|
||||
private AuthorizationEndpointRequest request;
|
||||
private String redirectUri;
|
||||
private String redirectUriParam;
|
||||
private String responseType;
|
||||
private String responseMode;
|
||||
private String state;
|
||||
private String scope;
|
||||
private String loginHint;
|
||||
private String prompt;
|
||||
private String nonce;
|
||||
private String maxAge;
|
||||
private String idpHint;
|
||||
protected Map<String, String> additionalReqParams = new HashMap<>();
|
||||
|
||||
public AuthorizationEndpoint(RealmModel realm, EventBuilder event) {
|
||||
super(realm, event);
|
||||
|
@ -139,34 +92,25 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
|
|||
public Response build() {
|
||||
MultivaluedMap<String, String> params = uriInfo.getQueryParameters();
|
||||
|
||||
clientId = params.getFirst(OIDCLoginProtocol.CLIENT_ID_PARAM);
|
||||
responseType = params.getFirst(OIDCLoginProtocol.RESPONSE_TYPE_PARAM);
|
||||
responseMode = params.getFirst(OIDCLoginProtocol.RESPONSE_MODE_PARAM);
|
||||
redirectUriParam = params.getFirst(OIDCLoginProtocol.REDIRECT_URI_PARAM);
|
||||
state = params.getFirst(OIDCLoginProtocol.STATE_PARAM);
|
||||
scope = params.getFirst(OIDCLoginProtocol.SCOPE_PARAM);
|
||||
loginHint = params.getFirst(OIDCLoginProtocol.LOGIN_HINT_PARAM);
|
||||
prompt = params.getFirst(OIDCLoginProtocol.PROMPT_PARAM);
|
||||
idpHint = params.getFirst(AdapterConstants.KC_IDP_HINT);
|
||||
nonce = params.getFirst(OIDCLoginProtocol.NONCE_PARAM);
|
||||
maxAge = params.getFirst(OIDCLoginProtocol.MAX_AGE_PARAM);
|
||||
|
||||
extractAdditionalReqParams(params);
|
||||
String clientId = params.getFirst(OIDCLoginProtocol.CLIENT_ID_PARAM);
|
||||
|
||||
checkSsl();
|
||||
checkRealm();
|
||||
checkClient();
|
||||
checkClient(clientId);
|
||||
|
||||
request = AuthorizationEndpointRequestParserProcessor.parseRequest(event, session, client, params);
|
||||
|
||||
checkRedirectUri();
|
||||
Response errorResponse = checkResponseType();
|
||||
if (errorResponse != null) {
|
||||
return errorResponse;
|
||||
}
|
||||
|
||||
if (!TokenUtil.isOIDCRequest(scope)) {
|
||||
if (!TokenUtil.isOIDCRequest(request.getScope())) {
|
||||
logger.oidcScopeMissing();
|
||||
}
|
||||
|
||||
errorResponse = checkOIDCParams(params);
|
||||
errorResponse = checkOIDCParams();
|
||||
if (errorResponse != null) {
|
||||
return errorResponse;
|
||||
}
|
||||
|
@ -186,27 +130,6 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
|
|||
throw new RuntimeException("Unknown action " + action);
|
||||
}
|
||||
|
||||
protected void extractAdditionalReqParams(MultivaluedMap<String, String> params) {
|
||||
for (String paramName : params.keySet()) {
|
||||
if (!KNOWN_REQ_PARAMS.contains(paramName)) {
|
||||
String value = params.getFirst(paramName);
|
||||
if (value != null && value.trim().isEmpty()) {
|
||||
value = null;
|
||||
}
|
||||
if (value != null && value.length() <= ADDITIONAL_REQ_PARAMS_MAX_SIZE) {
|
||||
if (additionalReqParams.size() >= ADDITIONAL_REQ_PARAMS_MAX_MUMBER) {
|
||||
logger.debug("Maximal number of additional OIDC params (" + ADDITIONAL_REQ_PARAMS_MAX_MUMBER + ") exceeded, ignoring rest of them!");
|
||||
break;
|
||||
}
|
||||
additionalReqParams.put(paramName, value);
|
||||
} else {
|
||||
logger.debug("OIDC Additional param " + paramName + " ignored because value is empty or longer than " + ADDITIONAL_REQ_PARAMS_MAX_SIZE);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public AuthorizationEndpoint register() {
|
||||
event.event(EventType.REGISTER);
|
||||
action = Action.REGISTER;
|
||||
|
@ -243,7 +166,7 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
|
|||
}
|
||||
}
|
||||
|
||||
private void checkClient() {
|
||||
private void checkClient(String clientId) {
|
||||
if (clientId == null) {
|
||||
event.error(Errors.INVALID_REQUEST);
|
||||
throw new ErrorPageException(session, Messages.MISSING_PARAMETER, OIDCLoginProtocol.CLIENT_ID_PARAM);
|
||||
|
@ -271,6 +194,8 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
|
|||
}
|
||||
|
||||
private Response checkResponseType() {
|
||||
String responseType = request.getResponseType();
|
||||
|
||||
if (responseType == null) {
|
||||
logger.missingParameter(OAuth2Constants.RESPONSE_TYPE);
|
||||
event.error(Errors.INVALID_REQUEST);
|
||||
|
@ -292,7 +217,7 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
|
|||
|
||||
OIDCResponseMode parsedResponseMode = null;
|
||||
try {
|
||||
parsedResponseMode = OIDCResponseMode.parse(responseMode, parsedResponseType);
|
||||
parsedResponseMode = OIDCResponseMode.parse(request.getResponseMode(), parsedResponseType);
|
||||
} catch (IllegalArgumentException iae) {
|
||||
logger.invalidParameter(OIDCLoginProtocol.RESPONSE_MODE_PARAM);
|
||||
event.error(Errors.INVALID_REQUEST);
|
||||
|
@ -325,20 +250,8 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
|
|||
return null;
|
||||
}
|
||||
|
||||
private Response checkOIDCParams(MultivaluedMap<String, String> params) {
|
||||
if (params.getFirst(OIDCLoginProtocol.REQUEST_PARAM) != null) {
|
||||
logger.unsupportedParameter(OIDCLoginProtocol.REQUEST_PARAM);
|
||||
event.error(Errors.INVALID_REQUEST);
|
||||
return redirectErrorToClient(parsedResponseMode, OAuthErrorException.REQUEST_NOT_SUPPORTED, null);
|
||||
}
|
||||
|
||||
if (params.getFirst(OIDCLoginProtocol.REQUEST_URI_PARAM) != null) {
|
||||
logger.unsupportedParameter(OIDCLoginProtocol.REQUEST_URI_PARAM);
|
||||
event.error(Errors.INVALID_REQUEST);
|
||||
return redirectErrorToClient(parsedResponseMode, OAuthErrorException.REQUEST_URI_NOT_SUPPORTED, null);
|
||||
}
|
||||
|
||||
if (parsedResponseType.isImplicitOrHybridFlow() && nonce == null) {
|
||||
private Response checkOIDCParams() {
|
||||
if (parsedResponseType.isImplicitOrHybridFlow() && request.getNonce() == null) {
|
||||
logger.missingParameter(OIDCLoginProtocol.NONCE_PARAM);
|
||||
event.error(Errors.INVALID_REQUEST);
|
||||
return redirectErrorToClient(parsedResponseMode, OAuthErrorException.INVALID_REQUEST, "Missing parameter: nonce");
|
||||
|
@ -355,14 +268,16 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
|
|||
errorResponseBuilder.addParam(OAuth2Constants.ERROR_DESCRIPTION, errorDescription);
|
||||
}
|
||||
|
||||
if (state != null) {
|
||||
errorResponseBuilder.addParam(OAuth2Constants.STATE, state);
|
||||
if (request.getState() != null) {
|
||||
errorResponseBuilder.addParam(OAuth2Constants.STATE, request.getState());
|
||||
}
|
||||
|
||||
return errorResponseBuilder.build();
|
||||
}
|
||||
|
||||
private void checkRedirectUri() {
|
||||
String redirectUriParam = request.getRedirectUriParam();
|
||||
|
||||
event.detail(Details.REDIRECT_URI, redirectUriParam);
|
||||
|
||||
redirectUri = RedirectUtils.verifyRedirectUri(uriInfo, redirectUriParam, realm, client);
|
||||
|
@ -377,27 +292,28 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
|
|||
clientSession.setAuthMethod(OIDCLoginProtocol.LOGIN_PROTOCOL);
|
||||
clientSession.setRedirectUri(redirectUri);
|
||||
clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE.name());
|
||||
clientSession.setNote(OIDCLoginProtocol.RESPONSE_TYPE_PARAM, responseType);
|
||||
clientSession.setNote(OIDCLoginProtocol.REDIRECT_URI_PARAM, redirectUriParam);
|
||||
clientSession.setNote(OIDCLoginProtocol.RESPONSE_TYPE_PARAM, request.getResponseType());
|
||||
clientSession.setNote(OIDCLoginProtocol.REDIRECT_URI_PARAM, request.getRedirectUriParam());
|
||||
clientSession.setNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName()));
|
||||
|
||||
if (state != null) clientSession.setNote(OIDCLoginProtocol.STATE_PARAM, state);
|
||||
if (nonce != null) clientSession.setNote(OIDCLoginProtocol.NONCE_PARAM, nonce);
|
||||
if (maxAge != null) clientSession.setNote(OIDCLoginProtocol.MAX_AGE_PARAM, maxAge);
|
||||
if (scope != null) clientSession.setNote(OIDCLoginProtocol.SCOPE_PARAM, scope);
|
||||
if (loginHint != null) clientSession.setNote(OIDCLoginProtocol.LOGIN_HINT_PARAM, loginHint);
|
||||
if (prompt != null) clientSession.setNote(OIDCLoginProtocol.PROMPT_PARAM, prompt);
|
||||
if (idpHint != null) clientSession.setNote(AdapterConstants.KC_IDP_HINT, idpHint);
|
||||
if (responseMode != null) clientSession.setNote(OIDCLoginProtocol.RESPONSE_MODE_PARAM, responseMode);
|
||||
if (request.getState() != null) clientSession.setNote(OIDCLoginProtocol.STATE_PARAM, request.getState());
|
||||
if (request.getNonce() != null) clientSession.setNote(OIDCLoginProtocol.NONCE_PARAM, request.getNonce());
|
||||
if (request.getMaxAge() != null) clientSession.setNote(OIDCLoginProtocol.MAX_AGE_PARAM, String.valueOf(request.getMaxAge()));
|
||||
if (request.getScope() != null) clientSession.setNote(OIDCLoginProtocol.SCOPE_PARAM, request.getScope());
|
||||
if (request.getLoginHint() != null) clientSession.setNote(OIDCLoginProtocol.LOGIN_HINT_PARAM, request.getLoginHint());
|
||||
if (request.getPrompt() != null) clientSession.setNote(OIDCLoginProtocol.PROMPT_PARAM, request.getPrompt());
|
||||
if (request.getIdpHint() != null) clientSession.setNote(AdapterConstants.KC_IDP_HINT, request.getIdpHint());
|
||||
if (request.getResponseMode() != null) clientSession.setNote(OIDCLoginProtocol.RESPONSE_MODE_PARAM, request.getResponseMode());
|
||||
|
||||
if (additionalReqParams != null) {
|
||||
for (String paramName : additionalReqParams.keySet()) {
|
||||
clientSession.setNote(CLIENT_SESSION_NOTE_ADDITIONAL_REQ_PARAMS_PREFIX + paramName, additionalReqParams.get(paramName));
|
||||
if (request.getAdditionalReqParams() != null) {
|
||||
for (String paramName : request.getAdditionalReqParams().keySet()) {
|
||||
clientSession.setNote(CLIENT_SESSION_NOTE_ADDITIONAL_REQ_PARAMS_PREFIX + paramName, request.getAdditionalReqParams().get(paramName));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Response buildAuthorizationCodeAuthorizationResponse() {
|
||||
String idpHint = request.getIdpHint();
|
||||
|
||||
if (idpHint != null && !"".equals(idpHint)) {
|
||||
IdentityProviderModel identityProviderModel = realm.getIdentityProviderByAlias(idpHint);
|
||||
|
@ -413,7 +329,7 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
|
|||
this.event.event(EventType.LOGIN);
|
||||
clientSession.setNote(Details.AUTH_TYPE, CODE_AUTH_TYPE);
|
||||
|
||||
return handleBrowserAuthenticationRequest(clientSession, new OIDCLoginProtocol(session, realm, uriInfo, headers, event), TokenUtil.hasPrompt(prompt, OIDCLoginProtocol.PROMPT_VALUE_NONE), false);
|
||||
return handleBrowserAuthenticationRequest(clientSession, new OIDCLoginProtocol(session, realm, uriInfo, headers, event), TokenUtil.hasPrompt(request.getPrompt(), OIDCLoginProtocol.PROMPT_VALUE_NONE), false);
|
||||
}
|
||||
|
||||
private Response buildRegister() {
|
||||
|
|
|
@ -19,6 +19,7 @@ package org.keycloak.protocol.oidc.endpoints;
|
|||
import org.jboss.resteasy.annotations.cache.NoCache;
|
||||
import org.jboss.resteasy.spi.HttpRequest;
|
||||
import org.jboss.resteasy.spi.HttpResponse;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.common.ClientConnection;
|
||||
import org.keycloak.OAuthErrorException;
|
||||
import org.keycloak.RSATokenVerifier;
|
||||
|
@ -27,12 +28,15 @@ import org.keycloak.events.Details;
|
|||
import org.keycloak.events.Errors;
|
||||
import org.keycloak.events.EventBuilder;
|
||||
import org.keycloak.events.EventType;
|
||||
import org.keycloak.jose.jws.Algorithm;
|
||||
import org.keycloak.jose.jws.JWSBuilder;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.ClientSessionModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
|
||||
import org.keycloak.protocol.oidc.TokenManager;
|
||||
import org.keycloak.representations.AccessToken;
|
||||
import org.keycloak.services.ErrorResponseException;
|
||||
|
@ -40,17 +44,18 @@ import org.keycloak.services.managers.AppAuthManager;
|
|||
import org.keycloak.services.managers.AuthenticationManager;
|
||||
import org.keycloak.services.resources.Cors;
|
||||
import org.keycloak.services.Urls;
|
||||
import org.keycloak.utils.MediaType;
|
||||
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.OPTIONS;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.HttpHeaders;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
|
||||
import java.security.PrivateKey;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
|
@ -86,7 +91,6 @@ public class UserInfoEndpoint {
|
|||
|
||||
@Path("/")
|
||||
@OPTIONS
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Response issueUserInfoPreflight() {
|
||||
return Cors.add(this.request, Response.ok()).auth().preflight().build();
|
||||
}
|
||||
|
@ -94,7 +98,6 @@ public class UserInfoEndpoint {
|
|||
@Path("/")
|
||||
@GET
|
||||
@NoCache
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Response issueUserInfoGet(@Context final HttpHeaders headers) {
|
||||
String accessToken = this.appAuthManager.extractAuthorizationHeaderToken(headers);
|
||||
return issueUserInfo(accessToken);
|
||||
|
@ -103,7 +106,6 @@ public class UserInfoEndpoint {
|
|||
@Path("/")
|
||||
@POST
|
||||
@NoCache
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Response issueUserInfoPost() {
|
||||
// Try header first
|
||||
HttpHeaders headers = request.getHttpHeaders();
|
||||
|
@ -176,12 +178,39 @@ public class UserInfoEndpoint {
|
|||
AccessToken userInfo = new AccessToken();
|
||||
tokenManager.transformUserInfoAccessToken(session, userInfo, realm, clientModel, userModel, userSession, clientSession);
|
||||
|
||||
event.success();
|
||||
|
||||
Map<String, Object> claims = new HashMap<String, Object>();
|
||||
claims.putAll(userInfo.getOtherClaims());
|
||||
claims.put("sub", userModel.getId());
|
||||
return Cors.add(request, Response.ok(claims)).auth().allowedOrigins(token).build();
|
||||
|
||||
Response.ResponseBuilder responseBuilder;
|
||||
OIDCAdvancedConfigWrapper cfg = OIDCAdvancedConfigWrapper.fromClientModel(clientModel);
|
||||
|
||||
if (cfg.isUserInfoSignatureRequired()) {
|
||||
String issuerUrl = Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName());
|
||||
String audience = clientModel.getClientId();
|
||||
claims.put("iss", issuerUrl);
|
||||
claims.put("aud", audience);
|
||||
|
||||
Algorithm signatureAlg = cfg.getUserInfoSignedResponseAlg();
|
||||
PrivateKey privateKey = realm.getPrivateKey();
|
||||
|
||||
String signedUserInfo = new JWSBuilder()
|
||||
.jsonContent(claims)
|
||||
.sign(signatureAlg, privateKey);
|
||||
|
||||
responseBuilder = Response.ok(signedUserInfo).header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JWT);
|
||||
|
||||
event.detail(Details.SIGNATURE_REQUIRED, "true");
|
||||
event.detail(Details.SIGNATURE_ALGORITHM, cfg.getUserInfoSignedResponseAlg().toString());
|
||||
} else {
|
||||
responseBuilder = Response.ok(claims).header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON);
|
||||
|
||||
event.detail(Details.SIGNATURE_REQUIRED, "false");
|
||||
}
|
||||
|
||||
event.success();
|
||||
|
||||
return Cors.add(request, responseBuilder).auth().allowedOrigins(token).build();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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> {
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -64,6 +64,12 @@ public class OIDCConfigurationRepresentation {
|
|||
@JsonProperty("id_token_signing_alg_values_supported")
|
||||
private List<String> idTokenSigningAlgValuesSupported;
|
||||
|
||||
@JsonProperty("userinfo_signing_alg_values_supported")
|
||||
private List<String> userInfoSigningAlgValuesSupported;
|
||||
|
||||
@JsonProperty("request_object_signing_alg_values_supported")
|
||||
private List<String> requestObjectSigningAlgValuesSupported;
|
||||
|
||||
@JsonProperty("response_modes_supported")
|
||||
private List<String> responseModesSupported;
|
||||
|
||||
|
@ -184,6 +190,22 @@ public class OIDCConfigurationRepresentation {
|
|||
this.idTokenSigningAlgValuesSupported = idTokenSigningAlgValuesSupported;
|
||||
}
|
||||
|
||||
public List<String> getUserInfoSigningAlgValuesSupported() {
|
||||
return userInfoSigningAlgValuesSupported;
|
||||
}
|
||||
|
||||
public void setUserInfoSigningAlgValuesSupported(List<String> userInfoSigningAlgValuesSupported) {
|
||||
this.userInfoSigningAlgValuesSupported = userInfoSigningAlgValuesSupported;
|
||||
}
|
||||
|
||||
public List<String> getRequestObjectSigningAlgValuesSupported() {
|
||||
return requestObjectSigningAlgValuesSupported;
|
||||
}
|
||||
|
||||
public void setRequestObjectSigningAlgValuesSupported(List<String> requestObjectSigningAlgValuesSupported) {
|
||||
this.requestObjectSigningAlgValuesSupported = requestObjectSigningAlgValuesSupported;
|
||||
}
|
||||
|
||||
public List<String> getResponseModesSupported() {
|
||||
return responseModesSupported;
|
||||
}
|
||||
|
|
|
@ -18,12 +18,15 @@
|
|||
package org.keycloak.protocol.oidc.utils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.PublicKey;
|
||||
|
||||
import org.keycloak.broker.provider.util.SimpleHttp;
|
||||
import org.keycloak.common.util.StreamUtil;
|
||||
import org.keycloak.connections.httpclient.HttpClientProvider;
|
||||
import org.keycloak.jose.jwk.JSONWebKeySet;
|
||||
import org.keycloak.jose.jwk.JWK;
|
||||
import org.keycloak.jose.jwk.JWKParser;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
||||
/**
|
||||
|
@ -31,8 +34,9 @@ import org.keycloak.util.JsonSerialization;
|
|||
*/
|
||||
public class JWKSUtils {
|
||||
|
||||
public static JSONWebKeySet sendJwksRequest(String jwksURI) throws IOException {
|
||||
String keySetString = SimpleHttp.doGet(jwksURI).asString();
|
||||
public static JSONWebKeySet sendJwksRequest(KeycloakSession session, String jwksURI) throws IOException {
|
||||
InputStream is = session.getProvider(HttpClientProvider.class).get(jwksURI);
|
||||
String keySetString = StreamUtil.readString(is);
|
||||
return JsonSerialization.readValue(keySetString, JSONWebKeySet.class);
|
||||
}
|
||||
|
||||
|
|
|
@ -77,7 +77,7 @@ public class RedirectUtils {
|
|||
} else {
|
||||
redirectUri = lowerCaseHostname(redirectUri);
|
||||
|
||||
String r = redirectUri.indexOf('?') != -1 ? redirectUri.substring(0, redirectUri.indexOf('?')) : redirectUri;
|
||||
String r = redirectUri;
|
||||
Set<String> resolveValidRedirects = resolveValidRedirects(uriInfo, rootUrl, validRedirects);
|
||||
|
||||
boolean valid = matchesRedirects(resolveValidRedirects, r);
|
||||
|
@ -134,15 +134,17 @@ public class RedirectUtils {
|
|||
|
||||
private static boolean matchesRedirects(Set<String> validRedirects, String redirect) {
|
||||
for (String validRedirect : validRedirects) {
|
||||
if (validRedirect.endsWith("*")) {
|
||||
if (validRedirect.endsWith("*") && !validRedirect.contains("?")) {
|
||||
// strip off the query component - we don't check them when wildcards are effective
|
||||
String r = redirect.contains("?") ? redirect.substring(0, redirect.indexOf("?")) : redirect;
|
||||
// strip off *
|
||||
int length = validRedirect.length() - 1;
|
||||
validRedirect = validRedirect.substring(0, length);
|
||||
if (redirect.startsWith(validRedirect)) return true;
|
||||
if (r.startsWith(validRedirect)) return true;
|
||||
// strip off trailing '/'
|
||||
if (length - 1 > 0 && validRedirect.charAt(length - 1) == '/') length--;
|
||||
validRedirect = validRedirect.substring(0, length);
|
||||
if (validRedirect.equals(redirect)) return true;
|
||||
if (validRedirect.equals(r)) return true;
|
||||
} else if (validRedirect.equals(redirect)) return true;
|
||||
}
|
||||
return false;
|
||||
|
|
|
@ -430,4 +430,8 @@ public interface ServicesLogger extends BasicLogger {
|
|||
@Message(id=96, value="Not found JWK of supported keyType under jwks_uri for usage: %s")
|
||||
void supportedJwkNotFound(String usage);
|
||||
|
||||
@LogMessage(level = WARN)
|
||||
@Message(id=97, value="Invalid request")
|
||||
void invalidRequest(@Cause Throwable t);
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -24,8 +24,10 @@ import org.keycloak.authentication.authenticators.client.ClientIdAndSecretAuthen
|
|||
import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator;
|
||||
import org.keycloak.jose.jwk.JSONWebKeySet;
|
||||
import org.keycloak.jose.jwk.JWK;
|
||||
import org.keycloak.jose.jws.Algorithm;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil;
|
||||
import org.keycloak.protocol.oidc.utils.JWKSUtils;
|
||||
|
@ -87,14 +89,12 @@ public class DescriptionConverter {
|
|||
}
|
||||
client.setClientAuthenticatorType(clientAuthFactory.getId());
|
||||
|
||||
// Externalize to ClientAuthenticator itself?
|
||||
if (authMethod != null && authMethod.equals(OIDCLoginProtocol.PRIVATE_KEY_JWT)) {
|
||||
|
||||
PublicKey publicKey = retrievePublicKey(clientOIDC);
|
||||
if (publicKey == null) {
|
||||
throw new ClientRegistrationException("Didn't find key of supported keyType for use " + JWK.Use.SIG.asString());
|
||||
}
|
||||
PublicKey publicKey = retrievePublicKey(session, clientOIDC);
|
||||
if (authMethod != null && authMethod.equals(OIDCLoginProtocol.PRIVATE_KEY_JWT) && publicKey == null) {
|
||||
throw new ClientRegistrationException("Didn't find key of supported keyType for use " + JWK.Use.SIG.asString());
|
||||
}
|
||||
|
||||
if (publicKey != null) {
|
||||
String publicKeyPem = KeycloakModelUtils.getPemFromKey(publicKey);
|
||||
|
||||
CertificateRepresentation rep = new CertificateRepresentation();
|
||||
|
@ -102,13 +102,24 @@ public class DescriptionConverter {
|
|||
CertificateInfoHelper.updateClientRepresentationCertificateInfo(client, rep, JWTClientAuthenticator.ATTR_PREFIX);
|
||||
}
|
||||
|
||||
OIDCAdvancedConfigWrapper configWrapper = OIDCAdvancedConfigWrapper.fromClientRepresentation(client);
|
||||
if (clientOIDC.getUserinfoSignedResponseAlg() != null) {
|
||||
Algorithm algorithm = Enum.valueOf(Algorithm.class, clientOIDC.getUserinfoSignedResponseAlg());
|
||||
configWrapper.setUserInfoSignedResponseAlg(algorithm);
|
||||
}
|
||||
|
||||
if (clientOIDC.getRequestObjectSigningAlg() != null) {
|
||||
Algorithm algorithm = Enum.valueOf(Algorithm.class, clientOIDC.getRequestObjectSigningAlg());
|
||||
configWrapper.setRequestObjectSignatureAlg(algorithm);
|
||||
}
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
|
||||
private static PublicKey retrievePublicKey(OIDCClientRepresentation clientOIDC) {
|
||||
private static PublicKey retrievePublicKey(KeycloakSession session, OIDCClientRepresentation clientOIDC) {
|
||||
if (clientOIDC.getJwksUri() == null && clientOIDC.getJwks() == null) {
|
||||
throw new ClientRegistrationException("Requested client authentication method '%s' but jwks_uri nor jwks were available in config");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (clientOIDC.getJwksUri() != null && clientOIDC.getJwks() != null) {
|
||||
|
@ -120,7 +131,7 @@ public class DescriptionConverter {
|
|||
keySet = clientOIDC.getJwks();
|
||||
} else {
|
||||
try {
|
||||
keySet = JWKSUtils.sendJwksRequest(clientOIDC.getJwksUri());
|
||||
keySet = JWKSUtils.sendJwksRequest(session, clientOIDC.getJwksUri());
|
||||
} catch (IOException ioe) {
|
||||
throw new ClientRegistrationException("Failed to send JWKS request to specified jwks_uri", ioe);
|
||||
}
|
||||
|
@ -152,6 +163,15 @@ public class DescriptionConverter {
|
|||
response.setRegistrationClientUri(uri.toString());
|
||||
response.setResponseTypes(getOIDCResponseTypes(client));
|
||||
response.setGrantTypes(getOIDCGrantTypes(client));
|
||||
|
||||
OIDCAdvancedConfigWrapper config = OIDCAdvancedConfigWrapper.fromClientRepresentation(client);
|
||||
if (config.isUserInfoSignatureRequired()) {
|
||||
response.setUserinfoSignedResponseAlg(config.getUserInfoSignedResponseAlg().toString());
|
||||
}
|
||||
if (config.getRequestObjectSignatureAlg() != null) {
|
||||
response.setRequestObjectSigningAlg(config.getRequestObjectSignatureAlg().toString());
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
|
|
|
@ -56,15 +56,22 @@ import java.io.*;
|
|||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.util.*;
|
||||
import org.jboss.dmr.ModelNode;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class KeycloakApplication extends Application {
|
||||
// This param name is defined again in Keycloak Server Subsystem class
|
||||
// org.keycloak.subsystem.server.extension.KeycloakServerDeploymentProcessor. We have this value in
|
||||
// two places to avoid dependency between Keycloak Subsystem and Keycloak Services module.
|
||||
public static final String KEYCLOAK_CONFIG_PARAM_NAME = "org.keycloak.server-subsystem.Config";
|
||||
|
||||
private static final ServicesLogger logger = ServicesLogger.ROOT_LOGGER;
|
||||
|
||||
protected boolean embedded = false;
|
||||
|
||||
protected Set<Object> singletons = new HashSet<Object>();
|
||||
protected Set<Class<?>> classes = new HashSet<Class<?>>();
|
||||
|
||||
|
@ -72,69 +79,79 @@ public class KeycloakApplication extends Application {
|
|||
protected String contextPath;
|
||||
|
||||
public KeycloakApplication(@Context ServletContext context, @Context Dispatcher dispatcher) {
|
||||
loadConfig();
|
||||
|
||||
this.contextPath = context.getContextPath();
|
||||
this.sessionFactory = createSessionFactory();
|
||||
|
||||
dispatcher.getDefaultContextObjects().put(KeycloakApplication.class, this);
|
||||
ResteasyProviderFactory.pushContext(KeycloakApplication.class, this); // for injection
|
||||
context.setAttribute(KeycloakSessionFactory.class.getName(), this.sessionFactory);
|
||||
|
||||
singletons.add(new ServerVersionResource());
|
||||
singletons.add(new RobotsResource());
|
||||
singletons.add(new RealmsResource());
|
||||
singletons.add(new AdminRoot());
|
||||
classes.add(ThemeResource.class);
|
||||
classes.add(JsResource.class);
|
||||
|
||||
classes.add(KeycloakTransactionCommitter.class);
|
||||
|
||||
singletons.add(new ObjectMapperResolver(Boolean.parseBoolean(System.getProperty("keycloak.jsonPrettyPrint", "false"))));
|
||||
|
||||
ExportImportManager[] exportImportManager = new ExportImportManager[1];
|
||||
|
||||
KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
|
||||
|
||||
@Override
|
||||
public void run(KeycloakSession lockSession) {
|
||||
DBLockManager dbLockManager = new DBLockManager(lockSession);
|
||||
dbLockManager.checkForcedUnlock();
|
||||
DBLockProvider dbLock = dbLockManager.getDBLock();
|
||||
dbLock.waitForLock();
|
||||
try {
|
||||
exportImportManager[0] = migrateAndBootstrap();
|
||||
} finally {
|
||||
dbLock.releaseLock();
|
||||
}
|
||||
try {
|
||||
if ("true".equals(context.getInitParameter("keycloak.embedded"))) {
|
||||
embedded = true;
|
||||
}
|
||||
|
||||
});
|
||||
loadConfig(context);
|
||||
|
||||
this.contextPath = context.getContextPath();
|
||||
this.sessionFactory = createSessionFactory();
|
||||
|
||||
dispatcher.getDefaultContextObjects().put(KeycloakApplication.class, this);
|
||||
ResteasyProviderFactory.pushContext(KeycloakApplication.class, this); // for injection
|
||||
context.setAttribute(KeycloakSessionFactory.class.getName(), this.sessionFactory);
|
||||
|
||||
singletons.add(new ServerVersionResource());
|
||||
singletons.add(new RobotsResource());
|
||||
singletons.add(new RealmsResource());
|
||||
singletons.add(new AdminRoot());
|
||||
classes.add(ThemeResource.class);
|
||||
classes.add(JsResource.class);
|
||||
|
||||
classes.add(KeycloakTransactionCommitter.class);
|
||||
|
||||
singletons.add(new ObjectMapperResolver(Boolean.parseBoolean(System.getProperty("keycloak.jsonPrettyPrint", "false"))));
|
||||
|
||||
ExportImportManager[] exportImportManager = new ExportImportManager[1];
|
||||
|
||||
KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
|
||||
|
||||
@Override
|
||||
public void run(KeycloakSession lockSession) {
|
||||
DBLockManager dbLockManager = new DBLockManager(lockSession);
|
||||
dbLockManager.checkForcedUnlock();
|
||||
DBLockProvider dbLock = dbLockManager.getDBLock();
|
||||
dbLock.waitForLock();
|
||||
try {
|
||||
exportImportManager[0] = migrateAndBootstrap();
|
||||
} finally {
|
||||
dbLock.releaseLock();
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
if (exportImportManager[0].isRunExport()) {
|
||||
exportImportManager[0].runExport();
|
||||
if (exportImportManager[0].isRunExport()) {
|
||||
exportImportManager[0].runExport();
|
||||
}
|
||||
|
||||
boolean bootstrapAdminUser = false;
|
||||
KeycloakSession session = sessionFactory.create();
|
||||
try {
|
||||
session.getTransactionManager().begin();
|
||||
bootstrapAdminUser = new ApplianceBootstrap(session).isNoMasterUser();
|
||||
|
||||
session.getTransactionManager().commit();
|
||||
} finally {
|
||||
session.close();
|
||||
}
|
||||
|
||||
sessionFactory.publish(new PostMigrationEvent());
|
||||
|
||||
singletons.add(new WelcomeResource(bootstrapAdminUser));
|
||||
|
||||
setupScheduledTasks(sessionFactory);
|
||||
} catch (Throwable t) {
|
||||
if (!embedded) {
|
||||
exit(1);
|
||||
}
|
||||
throw t;
|
||||
}
|
||||
|
||||
boolean bootstrapAdminUser = false;
|
||||
KeycloakSession session = sessionFactory.create();
|
||||
try {
|
||||
session.getTransactionManager().begin();
|
||||
bootstrapAdminUser = new ApplianceBootstrap(session).isNoMasterUser();
|
||||
|
||||
session.getTransactionManager().commit();
|
||||
} finally {
|
||||
session.close();
|
||||
}
|
||||
|
||||
sessionFactory.publish(new PostMigrationEvent());
|
||||
|
||||
singletons.add(new WelcomeResource(bootstrapAdminUser));
|
||||
|
||||
setupScheduledTasks(sessionFactory);
|
||||
}
|
||||
|
||||
|
||||
// Migrate model, bootstrap master realm, import realms and create admin user. This is done with acquired dbLock
|
||||
protected ExportImportManager migrateAndBootstrap() {
|
||||
ExportImportManager exportImportManager;
|
||||
|
@ -185,7 +202,6 @@ public class KeycloakApplication extends Application {
|
|||
session.getTransactionManager().commit();
|
||||
} catch (Exception e) {
|
||||
session.getTransactionManager().rollback();
|
||||
logger.migrationFailure(e);
|
||||
throw e;
|
||||
} finally {
|
||||
session.close();
|
||||
|
@ -206,12 +222,18 @@ public class KeycloakApplication extends Application {
|
|||
return uriInfo.getBaseUriBuilder().replacePath(getContextPath()).build();
|
||||
}
|
||||
|
||||
public static void loadConfig() {
|
||||
public static void loadConfig(ServletContext context) {
|
||||
try {
|
||||
JsonNode node = null;
|
||||
|
||||
String dmrConfig = loadDmrConfig(context);
|
||||
if (dmrConfig != null) {
|
||||
node = new ObjectMapper().readTree(dmrConfig);
|
||||
logger.loadingFrom("standalone.xml or domain.xml");
|
||||
}
|
||||
|
||||
String configDir = System.getProperty("jboss.server.config.dir");
|
||||
if (configDir != null) {
|
||||
if (node == null && configDir != null) {
|
||||
File f = new File(configDir + File.separator + "keycloak-server.json");
|
||||
if (f.isFile()) {
|
||||
logger.loadingFrom(f.getAbsolutePath());
|
||||
|
@ -230,14 +252,24 @@ public class KeycloakApplication extends Application {
|
|||
if (node != null) {
|
||||
Properties properties = new SystemEnvProperties();
|
||||
Config.init(new JsonConfigProvider(node, properties));
|
||||
return;
|
||||
} else {
|
||||
throw new RuntimeException("Config 'keycloak-server.json' not found");
|
||||
throw new RuntimeException("Keycloak config not found.");
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Failed to load config", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static String loadDmrConfig(ServletContext context) {
|
||||
String dmrConfig = context.getInitParameter(KEYCLOAK_CONFIG_PARAM_NAME);
|
||||
if (dmrConfig == null) return null;
|
||||
|
||||
ModelNode dmrConfigNode = ModelNode.fromString(dmrConfig);
|
||||
if (dmrConfigNode.asPropertyList().isEmpty()) return null;
|
||||
|
||||
// note that we need to resolve expressions BEFORE we convert to JSON
|
||||
return dmrConfigNode.resolve().toJSONString(true);
|
||||
}
|
||||
|
||||
public static KeycloakSessionFactory createSessionFactory() {
|
||||
DefaultKeycloakSessionFactory factory = new DefaultKeycloakSessionFactory();
|
||||
|
@ -386,4 +418,13 @@ public class KeycloakApplication extends Application {
|
|||
}
|
||||
}
|
||||
|
||||
private void exit(int status) {
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
System.exit(status);
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -66,6 +66,10 @@ import java.util.Map;
|
|||
*/
|
||||
public class ClientAttributeCertificateResource {
|
||||
|
||||
public static final String CERTIFICATE_PEM = "Certificate PEM";
|
||||
public static final String PUBLIC_KEY_PEM = "Public Key PEM";
|
||||
public static final String JSON_WEB_KEY_SET = "JSON Web Key Set";
|
||||
|
||||
protected RealmModel realm;
|
||||
private RealmAuth auth;
|
||||
protected ClientModel client;
|
||||
|
@ -195,12 +199,23 @@ public class ClientAttributeCertificateResource {
|
|||
Map<String, List<InputPart>> uploadForm = input.getFormDataMap();
|
||||
String keystoreFormat = uploadForm.get("keystoreFormat").get(0).getBodyAsString();
|
||||
List<InputPart> inputParts = uploadForm.get("file");
|
||||
if (keystoreFormat.equals("Certificate PEM")) {
|
||||
if (keystoreFormat.equals(CERTIFICATE_PEM)) {
|
||||
String pem = StreamUtil.readString(inputParts.get(0).getBody(InputStream.class, null));
|
||||
|
||||
// Validate format
|
||||
KeycloakModelUtils.getCertificate(pem);
|
||||
|
||||
info.setCertificate(pem);
|
||||
return info;
|
||||
} else if (keystoreFormat.equals(PUBLIC_KEY_PEM)) {
|
||||
String pem = StreamUtil.readString(inputParts.get(0).getBody(InputStream.class, null));
|
||||
|
||||
} else if (keystoreFormat.equals("JSON Web Key Set (JWK)")) {
|
||||
// Validate format
|
||||
KeycloakModelUtils.getPublicKey(pem);
|
||||
|
||||
info.setPublicKey(pem);
|
||||
return info;
|
||||
} else if (keystoreFormat.equals(JSON_WEB_KEY_SET)) {
|
||||
InputStream stream = inputParts.get(0).getBody(InputStream.class, null);
|
||||
JSONWebKeySet keySet = JsonSerialization.readValue(stream, JSONWebKeySet.class);
|
||||
PublicKey publicKey = JWKSUtils.getKeyForUse(keySet, JWK.Use.SIG);
|
||||
|
|
|
@ -40,6 +40,7 @@ import org.keycloak.representations.idm.ClientRepresentation;
|
|||
import org.keycloak.representations.idm.CredentialRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.representations.idm.UserSessionRepresentation;
|
||||
import org.keycloak.services.ErrorResponseException;
|
||||
import org.keycloak.services.ServicesLogger;
|
||||
import org.keycloak.services.clientregistration.ClientRegistrationTokenUtils;
|
||||
import org.keycloak.services.managers.ClientManager;
|
||||
|
@ -48,6 +49,8 @@ import org.keycloak.services.managers.ResourceAdminManager;
|
|||
import org.keycloak.services.resources.KeycloakApplication;
|
||||
import org.keycloak.services.ErrorResponse;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.services.validation.ClientValidator;
|
||||
import org.keycloak.services.validation.ValidationMessages;
|
||||
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.DELETE;
|
||||
|
@ -63,10 +66,7 @@ import javax.ws.rs.core.MediaType;
|
|||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
|
||||
import static java.lang.Boolean.TRUE;
|
||||
|
||||
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -17,9 +17,18 @@
|
|||
|
||||
package org.keycloak.services.util;
|
||||
|
||||
import java.security.PublicKey;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.HashMap;
|
||||
|
||||
import javax.ws.rs.core.Response;
|
||||
|
||||
import org.keycloak.authentication.AuthenticationFlowError;
|
||||
import org.keycloak.authentication.ClientAuthenticationFlowContext;
|
||||
import org.keycloak.authentication.authenticators.client.ClientAuthUtil;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.ModelException;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.representations.idm.CertificateRepresentation;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
|
||||
|
@ -34,6 +43,8 @@ public class CertificateInfoHelper {
|
|||
public static final String PUBLIC_KEY = "public.key";
|
||||
|
||||
|
||||
// CLIENT MODEL METHODS
|
||||
|
||||
public static CertificateRepresentation getCertificateFromClient(ClientModel client, String attributePrefix) {
|
||||
String privateKeyAttribute = attributePrefix + "." + PRIVATE_KEY;
|
||||
String certificateAttribute = attributePrefix + "." + X509CERTIFICATE;
|
||||
|
@ -75,6 +86,32 @@ public class CertificateInfoHelper {
|
|||
}
|
||||
|
||||
|
||||
public static PublicKey getSignatureValidationKey(ClientModel client, String attributePrefix) throws ModelException {
|
||||
CertificateRepresentation certInfo = getCertificateFromClient(client, attributePrefix);
|
||||
|
||||
String encodedCertificate = certInfo.getCertificate();
|
||||
String encodedPublicKey = certInfo.getPublicKey();
|
||||
|
||||
if (encodedCertificate == null && encodedPublicKey == null) {
|
||||
throw new ModelException("Client doesn't have certificate or publicKey configured");
|
||||
}
|
||||
|
||||
if (encodedCertificate != null && encodedPublicKey != null) {
|
||||
throw new ModelException("Client has both publicKey and certificate configured");
|
||||
}
|
||||
|
||||
// TODO: Caching of publicKeys / certificates, so it doesn't need to be always computed from pem. For performance reasons...
|
||||
if (encodedCertificate != null) {
|
||||
X509Certificate clientCert = KeycloakModelUtils.getCertificate(encodedCertificate);
|
||||
return clientCert.getPublicKey();
|
||||
} else {
|
||||
return KeycloakModelUtils.getPublicKey(encodedPublicKey);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// CLIENT REPRESENTATION METHODS
|
||||
|
||||
public static void updateClientRepresentationCertificateInfo(ClientRepresentation client, CertificateRepresentation rep, String attributePrefix) {
|
||||
String privateKeyAttribute = attributePrefix + "." + PRIVATE_KEY;
|
||||
String certificateAttribute = attributePrefix + "." + X509CERTIFICATE;
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
*
|
||||
* * Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
* * and other contributors as indicated by the @author tags.
|
||||
* *
|
||||
* * Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* * you may not use this file except in compliance with the License.
|
||||
* * You may obtain a copy of the License at
|
||||
* *
|
||||
* * http://www.apache.org/licenses/LICENSE-2.0
|
||||
* *
|
||||
* * Unless required by applicable law or agreed to in writing, software
|
||||
* * distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* * See the License for the specific language governing permissions and
|
||||
* * limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.keycloak.services.validation;
|
||||
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
|
||||
/**
|
||||
* @author Vaclav Muzikar <vmuzikar@redhat.com>
|
||||
*/
|
||||
public class ClientValidator {
|
||||
/**
|
||||
* Checks if the Client's Redirect URIs doesn't contain any URI fragments (like http://example.org/auth#fragment)
|
||||
*
|
||||
* @see <a href="https://issues.jboss.org/browse/KEYCLOAK-3421">KEYCLOAK-3421</a>
|
||||
* @param client
|
||||
* @param messages
|
||||
* @return true if Redirect URIs doesn't contain any URI with fragments
|
||||
*/
|
||||
public static boolean validate(ClientRepresentation client, ValidationMessages messages) {
|
||||
boolean isValid = true;
|
||||
|
||||
if (client.getRedirectUris() != null) {
|
||||
long urisWithFragmentCount = client.getRedirectUris().stream().filter(p -> p.contains("#")).count();
|
||||
if (urisWithFragmentCount > 0) {
|
||||
messages.add("redirectUris", "Redirect URIs must not contain an URI fragment", "clientRedirectURIsFragmentError");
|
||||
isValid = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (client.getRootUrl() != null && client.getRootUrl().contains("#")) {
|
||||
messages.add("rootUrl", "Root URL must not contain an URL fragment", "clientRootURLFragmentError");
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
return isValid;
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue