Supporting EdDSA
closes #15714 Signed-off-by: Takashi Norimatsu <takashi.norimatsu.ws@hitachi.com> Co-authored-by: Muhammad Zakwan Bin Mohd Zahid <muhammadzakwan.mohdzahid.fg@hitachi.com> Co-authored-by: rmartinc <rmartinc@redhat.com>
This commit is contained in:
parent
3b3eef2560
commit
b99f45ed3d
67 changed files with 2728 additions and 1130 deletions
2
adapters/oidc/spring-boot2/.gitignore
vendored
Normal file
2
adapters/oidc/spring-boot2/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
/.apt_generated/
|
||||||
|
/.apt_generated_tests/
|
2
adapters/saml/wildfly/wildfly-jakarta-subsystem/.gitignore
vendored
Normal file
2
adapters/saml/wildfly/wildfly-jakarta-subsystem/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
/.apt_generated/
|
||||||
|
/.apt_generated_tests/
|
2
adapters/saml/wildfly/wildfly-subsystem/.gitignore
vendored
Normal file
2
adapters/saml/wildfly/wildfly-subsystem/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
/.apt_generated/
|
||||||
|
/.apt_generated_tests/
|
45
core/pom.xml
45
core/pom.xml
|
@ -69,6 +69,48 @@
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
|
<profiles>
|
||||||
|
<profile>
|
||||||
|
<id>jdk-16</id>
|
||||||
|
<activation>
|
||||||
|
<jdk>[16,)</jdk>
|
||||||
|
</activation>
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>compile-java16</id>
|
||||||
|
<phase>compile</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>compile</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<release>16</release>
|
||||||
|
<compileSourceRoots>
|
||||||
|
<compileSourceRoot>${project.basedir}/src/main/java16</compileSourceRoot>
|
||||||
|
</compileSourceRoots>
|
||||||
|
<multiReleaseOutput>true</multiReleaseOutput>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.felix</groupId>
|
||||||
|
<artifactId>maven-bundle-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<instructions>
|
||||||
|
<_fixupmessages>"Classes found in the wrong directory";is:=warning</_fixupmessages>
|
||||||
|
</instructions>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</profile>
|
||||||
|
</profiles>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
<resources>
|
<resources>
|
||||||
<resource>
|
<resource>
|
||||||
|
@ -83,6 +125,9 @@
|
||||||
<configuration>
|
<configuration>
|
||||||
<archive>
|
<archive>
|
||||||
<manifestFile>${project.build.outputDirectory}/META-INF/MANIFEST.MF</manifestFile>
|
<manifestFile>${project.build.outputDirectory}/META-INF/MANIFEST.MF</manifestFile>
|
||||||
|
<manifestEntries>
|
||||||
|
<Multi-Release>true</Multi-Release>
|
||||||
|
</manifestEntries>
|
||||||
</archive>
|
</archive>
|
||||||
</configuration>
|
</configuration>
|
||||||
<executions>
|
<executions>
|
||||||
|
|
|
@ -27,13 +27,21 @@ public interface Algorithm {
|
||||||
String RS256 = "RS256";
|
String RS256 = "RS256";
|
||||||
String RS384 = "RS384";
|
String RS384 = "RS384";
|
||||||
String RS512 = "RS512";
|
String RS512 = "RS512";
|
||||||
String ES256 = "ES256";
|
|
||||||
String ES384 = "ES384";
|
|
||||||
String ES512 = "ES512";
|
|
||||||
String PS256 = "PS256";
|
String PS256 = "PS256";
|
||||||
String PS384 = "PS384";
|
String PS384 = "PS384";
|
||||||
String PS512 = "PS512";
|
String PS512 = "PS512";
|
||||||
|
|
||||||
|
/* ECDSA signing algorithms */
|
||||||
|
String ES256 = "ES256";
|
||||||
|
String ES384 = "ES384";
|
||||||
|
String ES512 = "ES512";
|
||||||
|
|
||||||
|
/* EdDSA signing algorithms */
|
||||||
|
String EdDSA = "EdDSA";
|
||||||
|
/* EdDSA Curve */
|
||||||
|
String Ed25519 = "Ed25519";
|
||||||
|
String Ed448 = "Ed448";
|
||||||
|
|
||||||
/* RSA Encryption Algorithms */
|
/* RSA Encryption Algorithms */
|
||||||
String RSA1_5 = CryptoConstants.RSA1_5;
|
String RSA1_5 = CryptoConstants.RSA1_5;
|
||||||
String RSA_OAEP = CryptoConstants.RSA_OAEP;
|
String RSA_OAEP = CryptoConstants.RSA_OAEP;
|
||||||
|
|
|
@ -39,13 +39,13 @@ public class AsymmetricSignatureSignerContext implements SignatureSignerContext
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getHashAlgorithm() {
|
public String getHashAlgorithm() {
|
||||||
return JavaAlgorithm.getJavaAlgorithmForHash(key.getAlgorithmOrDefault());
|
return JavaAlgorithm.getJavaAlgorithmForHash(key.getAlgorithmOrDefault(), key.getCurve());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public byte[] sign(byte[] data) throws SignatureException {
|
public byte[] sign(byte[] data) throws SignatureException {
|
||||||
try {
|
try {
|
||||||
Signature signature = Signature.getInstance(JavaAlgorithm.getJavaAlgorithm(key.getAlgorithmOrDefault()));
|
Signature signature = Signature.getInstance(JavaAlgorithm.getJavaAlgorithm(key.getAlgorithmOrDefault(), key.getCurve()));
|
||||||
signature.initSign((PrivateKey) key.getPrivateKey());
|
signature.initSign((PrivateKey) key.getPrivateKey());
|
||||||
signature.update(data);
|
signature.update(data);
|
||||||
return signature.sign();
|
return signature.sign();
|
||||||
|
|
|
@ -42,7 +42,7 @@ public class AsymmetricSignatureVerifierContext implements SignatureVerifierCont
|
||||||
@Override
|
@Override
|
||||||
public boolean verify(byte[] data, byte[] signature) throws VerificationException {
|
public boolean verify(byte[] data, byte[] signature) throws VerificationException {
|
||||||
try {
|
try {
|
||||||
Signature verifier = Signature.getInstance(JavaAlgorithm.getJavaAlgorithm(key.getAlgorithmOrDefault()));
|
Signature verifier = Signature.getInstance(JavaAlgorithm.getJavaAlgorithm(key.getAlgorithmOrDefault(), key.getCurve()));
|
||||||
verifier.initVerify((PublicKey) key.getPublicKey());
|
verifier.initVerify((PublicKey) key.getPublicKey());
|
||||||
verifier.update(data);
|
verifier.update(data);
|
||||||
return verifier.verify(signature);
|
return verifier.verify(signature);
|
||||||
|
|
|
@ -30,13 +30,20 @@ public class JavaAlgorithm {
|
||||||
public static final String PS256 = "SHA256withRSAandMGF1";
|
public static final String PS256 = "SHA256withRSAandMGF1";
|
||||||
public static final String PS384 = "SHA384withRSAandMGF1";
|
public static final String PS384 = "SHA384withRSAandMGF1";
|
||||||
public static final String PS512 = "SHA512withRSAandMGF1";
|
public static final String PS512 = "SHA512withRSAandMGF1";
|
||||||
|
public static final String Ed25519 = "Ed25519";
|
||||||
|
public static final String Ed448 = "Ed448";
|
||||||
public static final String AES = "AES";
|
public static final String AES = "AES";
|
||||||
|
|
||||||
public static final String SHA256 = "SHA-256";
|
public static final String SHA256 = "SHA-256";
|
||||||
public static final String SHA384 = "SHA-384";
|
public static final String SHA384 = "SHA-384";
|
||||||
public static final String SHA512 = "SHA-512";
|
public static final String SHA512 = "SHA-512";
|
||||||
|
public static final String SHAKE256 = "SHAKE-256";
|
||||||
|
|
||||||
public static String getJavaAlgorithm(String algorithm) {
|
public static String getJavaAlgorithm(String algorithm) {
|
||||||
|
return getJavaAlgorithm(algorithm, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getJavaAlgorithm(String algorithm, String curve) {
|
||||||
switch (algorithm) {
|
switch (algorithm) {
|
||||||
case Algorithm.RS256:
|
case Algorithm.RS256:
|
||||||
return RS256;
|
return RS256;
|
||||||
|
@ -62,6 +69,11 @@ public class JavaAlgorithm {
|
||||||
return PS384;
|
return PS384;
|
||||||
case Algorithm.PS512:
|
case Algorithm.PS512:
|
||||||
return PS512;
|
return PS512;
|
||||||
|
case Algorithm.EdDSA:
|
||||||
|
if (curve != null) {
|
||||||
|
return curve;
|
||||||
|
}
|
||||||
|
return Ed25519;
|
||||||
case Algorithm.AES:
|
case Algorithm.AES:
|
||||||
return AES;
|
return AES;
|
||||||
default:
|
default:
|
||||||
|
@ -69,8 +81,11 @@ public class JavaAlgorithm {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static String getJavaAlgorithmForHash(String algorithm) {
|
public static String getJavaAlgorithmForHash(String algorithm) {
|
||||||
|
return getJavaAlgorithmForHash(algorithm, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getJavaAlgorithmForHash(String algorithm, String curve) {
|
||||||
switch (algorithm) {
|
switch (algorithm) {
|
||||||
case Algorithm.RS256:
|
case Algorithm.RS256:
|
||||||
return SHA256;
|
return SHA256;
|
||||||
|
@ -96,6 +111,18 @@ public class JavaAlgorithm {
|
||||||
return SHA384;
|
return SHA384;
|
||||||
case Algorithm.PS512:
|
case Algorithm.PS512:
|
||||||
return SHA512;
|
return SHA512;
|
||||||
|
case Algorithm.EdDSA:
|
||||||
|
if (curve != null) {
|
||||||
|
switch (curve) {
|
||||||
|
case Algorithm.Ed25519:
|
||||||
|
return SHA512;
|
||||||
|
case Algorithm.Ed448:
|
||||||
|
return SHAKE256;
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException("Unknown curve for EdDSA " + curve);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return SHA512;
|
||||||
case Algorithm.AES:
|
case Algorithm.AES:
|
||||||
return AES;
|
return AES;
|
||||||
default:
|
default:
|
||||||
|
@ -111,6 +138,10 @@ public class JavaAlgorithm {
|
||||||
return getJavaAlgorithm(algorithm).contains("ECDSA");
|
return getJavaAlgorithm(algorithm).contains("ECDSA");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean isEddsaJavaAlgorithm(String algorithm) {
|
||||||
|
return getJavaAlgorithm(algorithm).contains("Ed");
|
||||||
|
}
|
||||||
|
|
||||||
public static boolean isHMACJavaAlgorithm(String algorithm) {
|
public static boolean isHMACJavaAlgorithm(String algorithm) {
|
||||||
return getJavaAlgorithm(algorithm).contains("HMAC");
|
return getJavaAlgorithm(algorithm).contains("HMAC");
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,5 +21,6 @@ public interface KeyType {
|
||||||
String EC = "EC";
|
String EC = "EC";
|
||||||
String RSA = "RSA";
|
String RSA = "RSA";
|
||||||
String OCT = "OCT";
|
String OCT = "OCT";
|
||||||
|
String OKP = "OKP";
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,6 +49,7 @@ public class KeyWrapper {
|
||||||
private X509Certificate certificate;
|
private X509Certificate certificate;
|
||||||
private List<X509Certificate> certificateChain;
|
private List<X509Certificate> certificateChain;
|
||||||
private boolean isDefaultClientCertificate;
|
private boolean isDefaultClientCertificate;
|
||||||
|
private String curve;
|
||||||
|
|
||||||
public String getProviderId() {
|
public String getProviderId() {
|
||||||
return providerId;
|
return providerId;
|
||||||
|
@ -176,6 +177,14 @@ public class KeyWrapper {
|
||||||
this.isDefaultClientCertificate = isDefaultClientCertificate;
|
this.isDefaultClientCertificate = isDefaultClientCertificate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setCurve(String curve) {
|
||||||
|
this.curve = curve;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCurve() {
|
||||||
|
return curve;
|
||||||
|
}
|
||||||
|
|
||||||
public KeyWrapper cloneKey() {
|
public KeyWrapper cloneKey() {
|
||||||
KeyWrapper key = new KeyWrapper();
|
KeyWrapper key = new KeyWrapper();
|
||||||
key.providerId = this.providerId;
|
key.providerId = this.providerId;
|
||||||
|
@ -189,6 +198,7 @@ public class KeyWrapper {
|
||||||
key.publicKey = this.publicKey;
|
key.publicKey = this.publicKey;
|
||||||
key.privateKey = this.privateKey;
|
key.privateKey = this.privateKey;
|
||||||
key.certificate = this.certificate;
|
key.certificate = this.certificate;
|
||||||
|
key.curve = this.curve;
|
||||||
if (this.certificateChain != null) {
|
if (this.certificateChain != null) {
|
||||||
key.certificateChain = new ArrayList<>(this.certificateChain);
|
key.certificateChain = new ArrayList<>(this.certificateChain);
|
||||||
}
|
}
|
||||||
|
|
138
core/src/main/java/org/keycloak/jose/jwk/AbstractJWKBuilder.java
Normal file
138
core/src/main/java/org/keycloak/jose/jwk/AbstractJWKBuilder.java
Normal file
|
@ -0,0 +1,138 @@
|
||||||
|
/*
|
||||||
|
* 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.jose.jwk;
|
||||||
|
|
||||||
|
import static org.keycloak.jose.jwk.JWKUtil.toIntegerBytes;
|
||||||
|
|
||||||
|
import java.security.Key;
|
||||||
|
import java.security.PublicKey;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.security.interfaces.ECPublicKey;
|
||||||
|
import java.security.interfaces.RSAPublicKey;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.keycloak.common.util.Base64Url;
|
||||||
|
import org.keycloak.common.util.KeyUtils;
|
||||||
|
import org.keycloak.common.util.PemUtils;
|
||||||
|
import org.keycloak.crypto.Algorithm;
|
||||||
|
import org.keycloak.crypto.KeyType;
|
||||||
|
import org.keycloak.crypto.KeyUse;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
|
*/
|
||||||
|
public abstract class AbstractJWKBuilder {
|
||||||
|
|
||||||
|
public static final KeyUse DEFAULT_PUBLIC_KEY_USE = KeyUse.SIG;
|
||||||
|
|
||||||
|
protected String kid;
|
||||||
|
|
||||||
|
protected String algorithm;
|
||||||
|
|
||||||
|
public JWK rs256(PublicKey key) {
|
||||||
|
this.algorithm = Algorithm.RS256;
|
||||||
|
return rsa(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public JWK rsa(Key key) {
|
||||||
|
return rsa(key, null, KeyUse.SIG);
|
||||||
|
}
|
||||||
|
|
||||||
|
public JWK rsa(Key key, X509Certificate certificate) {
|
||||||
|
return rsa(key, Collections.singletonList(certificate), KeyUse.SIG);
|
||||||
|
}
|
||||||
|
|
||||||
|
public JWK rsa(Key key, List<X509Certificate> certificates) {
|
||||||
|
return rsa(key, certificates, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public JWK rsa(Key key, List<X509Certificate> certificates, KeyUse keyUse) {
|
||||||
|
RSAPublicKey rsaKey = (RSAPublicKey) key;
|
||||||
|
|
||||||
|
RSAPublicJWK k = new RSAPublicJWK();
|
||||||
|
|
||||||
|
String kid = this.kid != null ? this.kid : KeyUtils.createKeyId(key);
|
||||||
|
k.setKeyId(kid);
|
||||||
|
k.setKeyType(KeyType.RSA);
|
||||||
|
k.setAlgorithm(algorithm);
|
||||||
|
k.setPublicKeyUse(keyUse == null ? KeyUse.SIG.getSpecName() : keyUse.getSpecName());
|
||||||
|
k.setModulus(Base64Url.encode(toIntegerBytes(rsaKey.getModulus())));
|
||||||
|
k.setPublicExponent(Base64Url.encode(toIntegerBytes(rsaKey.getPublicExponent())));
|
||||||
|
|
||||||
|
if (certificates != null && !certificates.isEmpty()) {
|
||||||
|
String[] certificateChain = new String[certificates.size()];
|
||||||
|
for (int i = 0; i < certificates.size(); i++) {
|
||||||
|
certificateChain[i] = PemUtils.encodeCertificate(certificates.get(i));
|
||||||
|
}
|
||||||
|
k.setX509CertificateChain(certificateChain);
|
||||||
|
}
|
||||||
|
|
||||||
|
return k;
|
||||||
|
}
|
||||||
|
|
||||||
|
public JWK rsa(Key key, KeyUse keyUse) {
|
||||||
|
JWK k = rsa(key);
|
||||||
|
String keyUseString = keyUse == null ? DEFAULT_PUBLIC_KEY_USE.getSpecName() : keyUse.getSpecName();
|
||||||
|
if (KeyUse.ENC == keyUse) keyUseString = "enc";
|
||||||
|
k.setPublicKeyUse(keyUseString);
|
||||||
|
return k;
|
||||||
|
}
|
||||||
|
|
||||||
|
public JWK ec(Key key) {
|
||||||
|
return ec(key, DEFAULT_PUBLIC_KEY_USE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public JWK ec(Key key, KeyUse keyUse) {
|
||||||
|
ECPublicKey ecKey = (ECPublicKey) key;
|
||||||
|
|
||||||
|
ECPublicJWK k = new ECPublicJWK();
|
||||||
|
|
||||||
|
String kid = this.kid != null ? this.kid : KeyUtils.createKeyId(key);
|
||||||
|
int fieldSize = ecKey.getParams().getCurve().getField().getFieldSize();
|
||||||
|
|
||||||
|
k.setKeyId(kid);
|
||||||
|
k.setKeyType(KeyType.EC);
|
||||||
|
k.setAlgorithm(algorithm);
|
||||||
|
k.setPublicKeyUse(keyUse == null ? DEFAULT_PUBLIC_KEY_USE.getSpecName() : keyUse.getSpecName());
|
||||||
|
k.setCrv("P-" + fieldSize);
|
||||||
|
k.setX(Base64Url.encode(toIntegerBytes(ecKey.getW().getAffineX(), fieldSize)));
|
||||||
|
k.setY(Base64Url.encode(toIntegerBytes(ecKey.getW().getAffineY(), fieldSize)));
|
||||||
|
|
||||||
|
return k;
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract JWK okp(Key key);
|
||||||
|
|
||||||
|
public abstract JWK okp(Key key, KeyUse keyUse);
|
||||||
|
|
||||||
|
public static byte[] reverseBytes(byte[] array) {
|
||||||
|
if (array == null || array.length == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
int length = array.length;
|
||||||
|
byte[] reversedArray = new byte[length];
|
||||||
|
|
||||||
|
for (int i = 0; i < length; i++) {
|
||||||
|
reversedArray[length - 1 - i] = array[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
return reversedArray;
|
||||||
|
}
|
||||||
|
}
|
118
core/src/main/java/org/keycloak/jose/jwk/AbstractJWKParser.java
Normal file
118
core/src/main/java/org/keycloak/jose/jwk/AbstractJWKParser.java
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
/*
|
||||||
|
* 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.jose.jwk;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.security.KeyFactory;
|
||||||
|
import java.security.PublicKey;
|
||||||
|
import java.security.spec.ECParameterSpec;
|
||||||
|
import java.security.spec.ECPoint;
|
||||||
|
import java.security.spec.ECPublicKeySpec;
|
||||||
|
import java.security.spec.RSAPublicKeySpec;
|
||||||
|
|
||||||
|
import org.keycloak.common.crypto.CryptoIntegration;
|
||||||
|
import org.keycloak.common.util.Base64Url;
|
||||||
|
import org.keycloak.crypto.KeyType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
|
*/
|
||||||
|
public abstract class AbstractJWKParser {
|
||||||
|
|
||||||
|
protected JWK jwk;
|
||||||
|
|
||||||
|
public JWK getJwk() {
|
||||||
|
return jwk;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PublicKey toPublicKey() {
|
||||||
|
String keyType = jwk.getKeyType();
|
||||||
|
if (keyType.equals(KeyType.RSA)) {
|
||||||
|
return createRSAPublicKey();
|
||||||
|
} else if (keyType.equals(KeyType.EC)) {
|
||||||
|
return createECPublicKey();
|
||||||
|
|
||||||
|
} else {
|
||||||
|
throw new RuntimeException("Unsupported keyType " + keyType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected PublicKey createECPublicKey() {
|
||||||
|
/* Check if jwk.getOtherClaims return an empty map */
|
||||||
|
if (jwk.getOtherClaims().size() == 0) {
|
||||||
|
throw new RuntimeException("JWK Otherclaims map is empty.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Try retrieving the necessary fields */
|
||||||
|
String crv = (String) jwk.getOtherClaims().get(ECPublicJWK.CRV);
|
||||||
|
String xStr = (String) jwk.getOtherClaims().get(ECPublicJWK.X);
|
||||||
|
String yStr = (String) jwk.getOtherClaims().get(ECPublicJWK.Y);
|
||||||
|
|
||||||
|
/* Check if the retrieving of necessary fields success */
|
||||||
|
if (crv == null || xStr == null || yStr == null) {
|
||||||
|
throw new RuntimeException("Fail to retrieve ECPublicJWK.CRV, ECPublicJWK.X or ECPublicJWK.Y field.");
|
||||||
|
}
|
||||||
|
|
||||||
|
BigInteger x = new BigInteger(1, Base64Url.decode(xStr));
|
||||||
|
BigInteger y = new BigInteger(1, Base64Url.decode(yStr));
|
||||||
|
|
||||||
|
String name;
|
||||||
|
switch (crv) {
|
||||||
|
case "P-256" :
|
||||||
|
name = "secp256r1";
|
||||||
|
break;
|
||||||
|
case "P-384" :
|
||||||
|
name = "secp384r1";
|
||||||
|
break;
|
||||||
|
case "P-521" :
|
||||||
|
name = "secp521r1";
|
||||||
|
break;
|
||||||
|
default :
|
||||||
|
throw new RuntimeException("Unsupported curve");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
ECPoint point = new ECPoint(x, y);
|
||||||
|
ECParameterSpec params = CryptoIntegration.getProvider().createECParams(name);
|
||||||
|
ECPublicKeySpec pubKeySpec = new ECPublicKeySpec(point, params);
|
||||||
|
|
||||||
|
KeyFactory kf = CryptoIntegration.getProvider().getKeyFactory("ECDSA");
|
||||||
|
return kf.generatePublic(pubKeySpec);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected PublicKey createRSAPublicKey() {
|
||||||
|
BigInteger modulus = new BigInteger(1, Base64Url.decode(jwk.getOtherClaims().get(RSAPublicJWK.MODULUS).toString()));
|
||||||
|
BigInteger publicExponent = new BigInteger(1, Base64Url.decode(jwk.getOtherClaims().get(RSAPublicJWK.PUBLIC_EXPONENT).toString()));
|
||||||
|
|
||||||
|
try {
|
||||||
|
KeyFactory kf = KeyFactory.getInstance("RSA");
|
||||||
|
return kf.generatePublic(new RSAPublicKeySpec(modulus, publicExponent));
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isKeyTypeSupported(String keyType) {
|
||||||
|
return (RSAPublicJWK.RSA.equals(keyType) || ECPublicJWK.EC.equals(keyType));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -17,36 +17,14 @@
|
||||||
|
|
||||||
package org.keycloak.jose.jwk;
|
package org.keycloak.jose.jwk;
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
import org.keycloak.common.util.Base64Url;
|
|
||||||
import org.keycloak.common.util.KeyUtils;
|
|
||||||
import org.keycloak.common.util.PemUtils;
|
|
||||||
import org.keycloak.crypto.Algorithm;
|
|
||||||
import org.keycloak.crypto.KeyType;
|
|
||||||
import org.keycloak.crypto.KeyUse;
|
import org.keycloak.crypto.KeyUse;
|
||||||
|
|
||||||
import java.security.Key;
|
import java.security.Key;
|
||||||
import java.security.PublicKey;
|
|
||||||
import java.security.cert.X509Certificate;
|
|
||||||
import java.security.interfaces.ECPublicKey;
|
|
||||||
import java.security.interfaces.RSAPublicKey;
|
|
||||||
|
|
||||||
import static org.keycloak.jose.jwk.JWKUtil.toIntegerBytes;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
*/
|
*/
|
||||||
public class JWKBuilder {
|
public class JWKBuilder extends AbstractJWKBuilder {
|
||||||
|
|
||||||
public static final KeyUse DEFAULT_PUBLIC_KEY_USE = KeyUse.SIG;
|
|
||||||
|
|
||||||
private String kid;
|
|
||||||
|
|
||||||
private String algorithm;
|
|
||||||
|
|
||||||
private JWKBuilder() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static JWKBuilder create() {
|
public static JWKBuilder create() {
|
||||||
return new JWKBuilder();
|
return new JWKBuilder();
|
||||||
|
@ -62,75 +40,15 @@ public class JWKBuilder {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public JWK rs256(PublicKey key) {
|
@Override
|
||||||
algorithm(Algorithm.RS256);
|
public JWK okp(Key key) {
|
||||||
return rsa(key);
|
// not supported if jdk vesion < 17
|
||||||
|
throw new UnsupportedOperationException("EdDSA algorithms not supported in this JDK version");
|
||||||
}
|
}
|
||||||
|
|
||||||
public JWK rsa(Key key) {
|
@Override
|
||||||
return rsa(key, null, KeyUse.SIG);
|
public JWK okp(Key key, KeyUse keyUse) {
|
||||||
}
|
// not supported if jdk version < 17
|
||||||
|
throw new UnsupportedOperationException("EdDSA algorithms not supported in this JDK version");
|
||||||
public JWK rsa(Key key, X509Certificate certificate) {
|
|
||||||
return rsa(key, Collections.singletonList(certificate), KeyUse.SIG);
|
|
||||||
}
|
|
||||||
|
|
||||||
public JWK rsa(Key key, List<X509Certificate> certificates) {
|
|
||||||
return rsa(key, certificates, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public JWK rsa(Key key, List<X509Certificate> certificates, KeyUse keyUse) {
|
|
||||||
RSAPublicKey rsaKey = (RSAPublicKey) key;
|
|
||||||
|
|
||||||
RSAPublicJWK k = new RSAPublicJWK();
|
|
||||||
|
|
||||||
String kid = this.kid != null ? this.kid : KeyUtils.createKeyId(key);
|
|
||||||
k.setKeyId(kid);
|
|
||||||
k.setKeyType(KeyType.RSA);
|
|
||||||
k.setAlgorithm(algorithm);
|
|
||||||
k.setPublicKeyUse(keyUse == null ? KeyUse.SIG.getSpecName() : keyUse.getSpecName());
|
|
||||||
k.setModulus(Base64Url.encode(toIntegerBytes(rsaKey.getModulus())));
|
|
||||||
k.setPublicExponent(Base64Url.encode(toIntegerBytes(rsaKey.getPublicExponent())));
|
|
||||||
|
|
||||||
if (certificates != null && !certificates.isEmpty()) {
|
|
||||||
String[] certificateChain = new String[certificates.size()];
|
|
||||||
for (int i = 0; i < certificates.size(); i++) {
|
|
||||||
certificateChain[i] = PemUtils.encodeCertificate(certificates.get(i));
|
|
||||||
}
|
|
||||||
k.setX509CertificateChain(certificateChain);
|
|
||||||
}
|
|
||||||
|
|
||||||
return k;
|
|
||||||
}
|
|
||||||
|
|
||||||
public JWK rsa(Key key, KeyUse keyUse) {
|
|
||||||
JWK k = rsa(key);
|
|
||||||
String keyUseString = keyUse == null ? DEFAULT_PUBLIC_KEY_USE.getSpecName() : keyUse.getSpecName();
|
|
||||||
if (KeyUse.ENC == keyUse) keyUseString = "enc";
|
|
||||||
k.setPublicKeyUse(keyUseString);
|
|
||||||
return k;
|
|
||||||
}
|
|
||||||
|
|
||||||
public JWK ec(Key key) {
|
|
||||||
return ec(key, DEFAULT_PUBLIC_KEY_USE);
|
|
||||||
}
|
|
||||||
|
|
||||||
public JWK ec(Key key, KeyUse keyUse) {
|
|
||||||
ECPublicKey ecKey = (ECPublicKey) key;
|
|
||||||
|
|
||||||
ECPublicJWK k = new ECPublicJWK();
|
|
||||||
|
|
||||||
String kid = this.kid != null ? this.kid : KeyUtils.createKeyId(key);
|
|
||||||
int fieldSize = ecKey.getParams().getCurve().getField().getFieldSize();
|
|
||||||
|
|
||||||
k.setKeyId(kid);
|
|
||||||
k.setKeyType(KeyType.EC);
|
|
||||||
k.setAlgorithm(algorithm);
|
|
||||||
k.setPublicKeyUse(keyUse == null ? DEFAULT_PUBLIC_KEY_USE.getSpecName() : keyUse.getSpecName());
|
|
||||||
k.setCrv("P-" + fieldSize);
|
|
||||||
k.setX(Base64Url.encode(toIntegerBytes(ecKey.getW().getAffineX(), fieldSize)));
|
|
||||||
k.setY(Base64Url.encode(toIntegerBytes(ecKey.getW().getAffineY(), fieldSize)));
|
|
||||||
|
|
||||||
return k;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,28 +17,14 @@
|
||||||
|
|
||||||
package org.keycloak.jose.jwk;
|
package org.keycloak.jose.jwk;
|
||||||
|
|
||||||
|
|
||||||
import org.keycloak.common.crypto.CryptoIntegration;
|
|
||||||
import org.keycloak.common.util.Base64Url;
|
|
||||||
import org.keycloak.crypto.KeyType;
|
|
||||||
import org.keycloak.util.JsonSerialization;
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
|
||||||
import java.math.BigInteger;
|
|
||||||
import java.security.KeyFactory;
|
|
||||||
import java.security.PublicKey;
|
|
||||||
import java.security.spec.ECParameterSpec;
|
|
||||||
import java.security.spec.ECPoint;
|
|
||||||
import java.security.spec.ECPublicKeySpec;
|
|
||||||
import java.security.spec.RSAPublicKeySpec;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
*/
|
*/
|
||||||
public class JWKParser {
|
public class JWKParser extends AbstractJWKParser {
|
||||||
|
|
||||||
private JWK jwk;
|
protected JWKParser() {
|
||||||
|
|
||||||
private JWKParser() {
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public JWKParser(JWK jwk) {
|
public JWKParser(JWK jwk) {
|
||||||
|
@ -62,83 +48,4 @@ public class JWKParser {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public JWK getJwk() {
|
|
||||||
return jwk;
|
|
||||||
}
|
|
||||||
|
|
||||||
public PublicKey toPublicKey() {
|
|
||||||
String keyType = jwk.getKeyType();
|
|
||||||
if (keyType.equals(KeyType.RSA)) {
|
|
||||||
return createRSAPublicKey();
|
|
||||||
} else if (keyType.equals(KeyType.EC)) {
|
|
||||||
return createECPublicKey();
|
|
||||||
|
|
||||||
} else {
|
|
||||||
throw new RuntimeException("Unsupported keyType " + keyType);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private PublicKey createECPublicKey() {
|
|
||||||
/* Check if jwk.getOtherClaims return an empty map */
|
|
||||||
if (jwk.getOtherClaims().size() == 0) {
|
|
||||||
throw new RuntimeException("JWK Otherclaims map is empty.");
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Try retrieving the necessary fields */
|
|
||||||
String crv = (String) jwk.getOtherClaims().get(ECPublicJWK.CRV);
|
|
||||||
String xStr = (String) jwk.getOtherClaims().get(ECPublicJWK.X);
|
|
||||||
String yStr = (String) jwk.getOtherClaims().get(ECPublicJWK.Y);
|
|
||||||
|
|
||||||
/* Check if the retrieving of necessary fields success */
|
|
||||||
if (crv == null || xStr == null || yStr == null) {
|
|
||||||
throw new RuntimeException("Fail to retrieve ECPublicJWK.CRV, ECPublicJWK.X or ECPublicJWK.Y field.");
|
|
||||||
}
|
|
||||||
|
|
||||||
BigInteger x = new BigInteger(1, Base64Url.decode(xStr));
|
|
||||||
BigInteger y = new BigInteger(1, Base64Url.decode(yStr));
|
|
||||||
|
|
||||||
String name;
|
|
||||||
switch (crv) {
|
|
||||||
case "P-256" :
|
|
||||||
name = "secp256r1";
|
|
||||||
break;
|
|
||||||
case "P-384" :
|
|
||||||
name = "secp384r1";
|
|
||||||
break;
|
|
||||||
case "P-521" :
|
|
||||||
name = "secp521r1";
|
|
||||||
break;
|
|
||||||
default :
|
|
||||||
throw new RuntimeException("Unsupported curve");
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
|
|
||||||
ECPoint point = new ECPoint(x, y);
|
|
||||||
ECParameterSpec params = CryptoIntegration.getProvider().createECParams(name);
|
|
||||||
ECPublicKeySpec pubKeySpec = new ECPublicKeySpec(point, params);
|
|
||||||
|
|
||||||
KeyFactory kf = CryptoIntegration.getProvider().getKeyFactory("ECDSA");
|
|
||||||
return kf.generatePublic(pubKeySpec);
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private PublicKey createRSAPublicKey() {
|
|
||||||
BigInteger modulus = new BigInteger(1, Base64Url.decode(jwk.getOtherClaims().get(RSAPublicJWK.MODULUS).toString()));
|
|
||||||
BigInteger publicExponent = new BigInteger(1, Base64Url.decode(jwk.getOtherClaims().get(RSAPublicJWK.PUBLIC_EXPONENT).toString()));
|
|
||||||
|
|
||||||
try {
|
|
||||||
KeyFactory kf = KeyFactory.getInstance("RSA");
|
|
||||||
return kf.generatePublic(new RSAPublicKeySpec(modulus, publicExponent));
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isKeyTypeSupported(String keyType) {
|
|
||||||
return (RSAPublicJWK.RSA.equals(keyType) || ECPublicJWK.EC.equals(keyType));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
56
core/src/main/java/org/keycloak/jose/jwk/OKPPublicJWK.java
Normal file
56
core/src/main/java/org/keycloak/jose/jwk/OKPPublicJWK.java
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023 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.jose.jwk;
|
||||||
|
|
||||||
|
import org.keycloak.crypto.KeyType;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
|
||||||
|
*/
|
||||||
|
public class OKPPublicJWK extends JWK {
|
||||||
|
|
||||||
|
public static final String OKP = KeyType.OKP;
|
||||||
|
|
||||||
|
public static final String CRV = "crv";
|
||||||
|
public static final String X = "x";
|
||||||
|
|
||||||
|
@JsonProperty(CRV)
|
||||||
|
private String crv;
|
||||||
|
|
||||||
|
@JsonProperty(X)
|
||||||
|
private String x;
|
||||||
|
|
||||||
|
public String getCrv() {
|
||||||
|
return crv;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCrv(String crv) {
|
||||||
|
this.crv = crv;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getX() {
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setX(String x) {
|
||||||
|
this.x = x;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -39,7 +39,10 @@ public enum Algorithm {
|
||||||
PS512(AlgorithmType.RSA, null),
|
PS512(AlgorithmType.RSA, null),
|
||||||
ES256(AlgorithmType.ECDSA, null),
|
ES256(AlgorithmType.ECDSA, null),
|
||||||
ES384(AlgorithmType.ECDSA, null),
|
ES384(AlgorithmType.ECDSA, null),
|
||||||
ES512(AlgorithmType.ECDSA, null)
|
ES512(AlgorithmType.ECDSA, null),
|
||||||
|
EdDSA(AlgorithmType.EDDSA, null),
|
||||||
|
Ed25519(AlgorithmType.EDDSA, null),
|
||||||
|
Ed448(AlgorithmType.EDDSA, null)
|
||||||
;
|
;
|
||||||
|
|
||||||
private AlgorithmType type;
|
private AlgorithmType type;
|
||||||
|
|
|
@ -26,6 +26,7 @@ public enum AlgorithmType {
|
||||||
RSA,
|
RSA,
|
||||||
HMAC,
|
HMAC,
|
||||||
AES,
|
AES,
|
||||||
ECDSA
|
ECDSA,
|
||||||
|
EDDSA
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
package org.keycloak.jose.jws;
|
package org.keycloak.jose.jws;
|
||||||
|
|
||||||
import org.keycloak.common.util.Base64Url;
|
import org.keycloak.common.util.Base64Url;
|
||||||
|
import org.keycloak.crypto.JavaAlgorithm;
|
||||||
import org.keycloak.crypto.SignatureSignerContext;
|
import org.keycloak.crypto.SignatureSignerContext;
|
||||||
import org.keycloak.jose.jws.crypto.HMACProvider;
|
import org.keycloak.jose.jws.crypto.HMACProvider;
|
||||||
import org.keycloak.jose.jws.crypto.RSAProvider;
|
import org.keycloak.jose.jws.crypto.RSAProvider;
|
||||||
|
@ -76,7 +77,13 @@ public class JWSBuilder {
|
||||||
|
|
||||||
protected String encodeHeader(String sigAlgName) {
|
protected String encodeHeader(String sigAlgName) {
|
||||||
StringBuilder builder = new StringBuilder("{");
|
StringBuilder builder = new StringBuilder("{");
|
||||||
|
|
||||||
|
if (org.keycloak.crypto.Algorithm.Ed25519.equals(sigAlgName) || org.keycloak.crypto.Algorithm.Ed448.equals(sigAlgName)) {
|
||||||
|
builder.append("\"alg\":\"").append(org.keycloak.crypto.Algorithm.EdDSA).append("\"");
|
||||||
|
builder.append(",\"crv\":\"").append(sigAlgName).append("\"");
|
||||||
|
} else {
|
||||||
builder.append("\"alg\":\"").append(sigAlgName).append("\"");
|
builder.append("\"alg\":\"").append(sigAlgName).append("\"");
|
||||||
|
}
|
||||||
|
|
||||||
if (type != null) builder.append(",\"typ\" : \"").append(type).append("\"");
|
if (type != null) builder.append(",\"typ\" : \"").append(type).append("\"");
|
||||||
if (kid != null) builder.append(",\"kid\" : \"").append(kid).append("\"");
|
if (kid != null) builder.append(",\"kid\" : \"").append(kid).append("\"");
|
||||||
|
|
|
@ -22,6 +22,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
import org.keycloak.jose.JOSEHeader;
|
import org.keycloak.jose.JOSEHeader;
|
||||||
import org.keycloak.jose.jwk.JWK;
|
import org.keycloak.jose.jwk.JWK;
|
||||||
|
|
||||||
|
|
|
@ -27,6 +27,7 @@ import org.keycloak.jose.jwk.ECPublicJWK;
|
||||||
import org.keycloak.jose.jwk.JSONWebKeySet;
|
import org.keycloak.jose.jwk.JSONWebKeySet;
|
||||||
import org.keycloak.jose.jwk.JWK;
|
import org.keycloak.jose.jwk.JWK;
|
||||||
import org.keycloak.jose.jwk.JWKParser;
|
import org.keycloak.jose.jwk.JWKParser;
|
||||||
|
import org.keycloak.jose.jwk.OKPPublicJWK;
|
||||||
import org.keycloak.jose.jwk.RSAPublicJWK;
|
import org.keycloak.jose.jwk.RSAPublicJWK;
|
||||||
import org.keycloak.jose.jws.crypto.HashUtils;
|
import org.keycloak.jose.jws.crypto.HashUtils;
|
||||||
|
|
||||||
|
@ -125,6 +126,9 @@ public class JWKSUtils {
|
||||||
if (jwk.getAlgorithm() != null) {
|
if (jwk.getAlgorithm() != null) {
|
||||||
keyWrapper.setAlgorithm(jwk.getAlgorithm());
|
keyWrapper.setAlgorithm(jwk.getAlgorithm());
|
||||||
}
|
}
|
||||||
|
if (jwk.getOtherClaims().get(OKPPublicJWK.CRV) != null) {
|
||||||
|
keyWrapper.setCurve((String) jwk.getOtherClaims().get(OKPPublicJWK.CRV));
|
||||||
|
}
|
||||||
keyWrapper.setType(jwk.getKeyType());
|
keyWrapper.setType(jwk.getKeyType());
|
||||||
keyWrapper.setUse(getKeyUse(jwk.getPublicKeyUse()));
|
keyWrapper.setUse(getKeyUse(jwk.getPublicKeyUse()));
|
||||||
keyWrapper.setPublicKey(parser.toPublicKey());
|
keyWrapper.setPublicKey(parser.toPublicKey());
|
||||||
|
|
108
core/src/main/java16/org/keycloak/jose/jwk/JWKBuilder.java
Normal file
108
core/src/main/java16/org/keycloak/jose/jwk/JWKBuilder.java
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023 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.jose.jwk;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.security.Key;
|
||||||
|
import java.security.interfaces.EdECPublicKey;
|
||||||
|
import java.security.spec.EdECPoint;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import org.keycloak.common.util.Base64Url;
|
||||||
|
import org.keycloak.common.util.KeyUtils;
|
||||||
|
import org.keycloak.crypto.Algorithm;
|
||||||
|
import org.keycloak.crypto.KeyType;
|
||||||
|
import org.keycloak.crypto.KeyUse;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
|
||||||
|
*/
|
||||||
|
public class JWKBuilder extends AbstractJWKBuilder {
|
||||||
|
|
||||||
|
private JWKBuilder() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static JWKBuilder create() {
|
||||||
|
return new JWKBuilder();
|
||||||
|
}
|
||||||
|
|
||||||
|
public JWKBuilder kid(String kid) {
|
||||||
|
this.kid = kid;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public JWKBuilder algorithm(String algorithm) {
|
||||||
|
this.algorithm = algorithm;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JWK okp(Key key) {
|
||||||
|
return okp(key, DEFAULT_PUBLIC_KEY_USE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JWK okp(Key key, KeyUse keyUse) {
|
||||||
|
EdECPublicKey eddsaPublicKey = (EdECPublicKey) key;
|
||||||
|
|
||||||
|
OKPPublicJWK k = new OKPPublicJWK();
|
||||||
|
|
||||||
|
String kid = this.kid != null ? this.kid : KeyUtils.createKeyId(key);
|
||||||
|
|
||||||
|
k.setKeyId(kid);
|
||||||
|
k.setKeyType(KeyType.OKP);
|
||||||
|
k.setAlgorithm(algorithm);
|
||||||
|
k.setPublicKeyUse(keyUse == null ? DEFAULT_PUBLIC_KEY_USE.getSpecName() : keyUse.getSpecName());
|
||||||
|
k.setCrv(eddsaPublicKey.getParams().getName());
|
||||||
|
|
||||||
|
Optional<String> x = edPublicKeyInJwkRepresentation(eddsaPublicKey);
|
||||||
|
k.setX(x.orElse(""));
|
||||||
|
|
||||||
|
return k;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<String> edPublicKeyInJwkRepresentation(EdECPublicKey eddsaPublicKey) {
|
||||||
|
EdECPoint edEcPoint = eddsaPublicKey.getPoint();
|
||||||
|
BigInteger yCoordinate = edEcPoint.getY();
|
||||||
|
|
||||||
|
// JWK representation "x" of a public key
|
||||||
|
int bytesLength = 0;
|
||||||
|
if (Algorithm.Ed25519.equals(eddsaPublicKey.getParams().getName())) {
|
||||||
|
bytesLength = 32;
|
||||||
|
} else if (Algorithm.Ed448.equals(eddsaPublicKey.getParams().getName())) {
|
||||||
|
bytesLength = 57;
|
||||||
|
} else {
|
||||||
|
return Optional.ofNullable(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// consider the case where yCoordinate.toByteArray() is less than bytesLength due to relatively small value of y-coordinate.
|
||||||
|
byte[] yCoordinateLittleEndianBytes = new byte[bytesLength];
|
||||||
|
|
||||||
|
// convert big endian representation of BigInteger to little endian representation of JWK representation (RFC 8032,8027)
|
||||||
|
yCoordinateLittleEndianBytes = Arrays.copyOf(reverseBytes(yCoordinate.toByteArray()), bytesLength);
|
||||||
|
|
||||||
|
// set a parity of x-coordinate to the most significant bit of the last octet (RFC 8032, 8037)
|
||||||
|
if (edEcPoint.isXOdd()) {
|
||||||
|
yCoordinateLittleEndianBytes[yCoordinateLittleEndianBytes.length - 1] |= -128; // 0b10000000
|
||||||
|
}
|
||||||
|
|
||||||
|
return Optional.ofNullable(Base64Url.encode(yCoordinateLittleEndianBytes));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
124
core/src/main/java16/org/keycloak/jose/jwk/JWKParser.java
Normal file
124
core/src/main/java16/org/keycloak/jose/jwk/JWKParser.java
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023 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.jose.jwk;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.security.KeyFactory;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.PublicKey;
|
||||||
|
import java.security.spec.EdECPoint;
|
||||||
|
import java.security.spec.EdECPublicKeySpec;
|
||||||
|
import java.security.spec.InvalidKeySpecException;
|
||||||
|
import java.security.spec.NamedParameterSpec;
|
||||||
|
|
||||||
|
import org.keycloak.common.util.Base64Url;
|
||||||
|
import org.keycloak.crypto.Algorithm;
|
||||||
|
import org.keycloak.crypto.KeyType;
|
||||||
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
|
||||||
|
*/
|
||||||
|
public class JWKParser extends AbstractJWKParser {
|
||||||
|
|
||||||
|
private JWKParser() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static JWKParser create() {
|
||||||
|
return new JWKParser();
|
||||||
|
}
|
||||||
|
|
||||||
|
public JWKParser(JWK jwk) {
|
||||||
|
this.jwk = jwk;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static JWKParser create(JWK jwk) {
|
||||||
|
return new JWKParser(jwk);
|
||||||
|
}
|
||||||
|
|
||||||
|
public JWKParser parse(String jwk) {
|
||||||
|
try {
|
||||||
|
this.jwk = JsonSerialization.mapper.readValue(jwk, JWK.class);
|
||||||
|
return this;
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PublicKey toPublicKey() {
|
||||||
|
String keyType = jwk.getKeyType();
|
||||||
|
if (keyType.equals(KeyType.RSA)) {
|
||||||
|
return createRSAPublicKey();
|
||||||
|
} else if (keyType.equals(KeyType.EC)) {
|
||||||
|
return createECPublicKey();
|
||||||
|
} else if (keyType.equals(KeyType.OKP)) {
|
||||||
|
return createOKPPublicKey();
|
||||||
|
} else {
|
||||||
|
throw new RuntimeException("Unsupported keyType " + keyType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private PublicKey createOKPPublicKey() {
|
||||||
|
String x = (String) jwk.getOtherClaims().get(OKPPublicJWK.X);
|
||||||
|
String crv = (String) jwk.getOtherClaims().get(OKPPublicJWK.CRV);
|
||||||
|
// JWK representation "x" of a public key
|
||||||
|
int bytesLength = 0;
|
||||||
|
if (Algorithm.Ed25519.equals(crv)) {
|
||||||
|
bytesLength = 32;
|
||||||
|
} else if (Algorithm.Ed448.equals(crv)) {
|
||||||
|
bytesLength = 57;
|
||||||
|
} else {
|
||||||
|
throw new RuntimeException("Invalid JWK representation of OKP type algorithm");
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] decodedX = Base64Url.decode(x);
|
||||||
|
if (decodedX.length != bytesLength) {
|
||||||
|
throw new RuntimeException("Invalid JWK representation of OKP type public key");
|
||||||
|
}
|
||||||
|
|
||||||
|
// x-coordinate's parity check shown by MSB(bit) of MSB(byte) of decoded "x": 1 is odd, 0 is even
|
||||||
|
boolean isOddX = false;
|
||||||
|
if ((decodedX[decodedX.length - 1] & -128) != 0) { // 0b10000000
|
||||||
|
isOddX = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// MSB(bit) of MSB(byte) showing x-coodinate's parity is set to 0
|
||||||
|
decodedX[decodedX.length - 1] &= 127; // 0b01111111
|
||||||
|
|
||||||
|
// both x and y-coordinate in twisted Edwards curve are always 0 or natural number
|
||||||
|
BigInteger y = new BigInteger(1, JWKBuilder.reverseBytes(decodedX));
|
||||||
|
NamedParameterSpec spec = new NamedParameterSpec(crv);
|
||||||
|
EdECPoint ep = new EdECPoint(isOddX, y);
|
||||||
|
EdECPublicKeySpec keySpec = new EdECPublicKeySpec(spec, ep);
|
||||||
|
|
||||||
|
PublicKey publicKey = null;
|
||||||
|
try {
|
||||||
|
publicKey = KeyFactory.getInstance(crv).generatePublic(keySpec);
|
||||||
|
} catch (InvalidKeySpecException | NoSuchAlgorithmException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
return publicKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isKeyTypeSupported(String keyType) {
|
||||||
|
return (RSAPublicJWK.RSA.equals(keyType) || ECPublicJWK.EC.equals(keyType) || OKPPublicJWK.OKP.equals(keyType));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -756,6 +756,12 @@
|
||||||
"ES256":{
|
"ES256":{
|
||||||
"order":0
|
"order":0
|
||||||
},
|
},
|
||||||
|
"Ed25519":{
|
||||||
|
"order":0
|
||||||
|
},
|
||||||
|
"Ed448":{
|
||||||
|
"order":0
|
||||||
|
},
|
||||||
"RS256":{
|
"RS256":{
|
||||||
"order":0
|
"order":0
|
||||||
},
|
},
|
||||||
|
@ -1517,6 +1523,12 @@
|
||||||
"ES256":{
|
"ES256":{
|
||||||
"order":0
|
"order":0
|
||||||
},
|
},
|
||||||
|
"Ed25519":{
|
||||||
|
"order":0
|
||||||
|
},
|
||||||
|
"Ed448":{
|
||||||
|
"order":0
|
||||||
|
},
|
||||||
"RS256":{
|
"RS256":{
|
||||||
"order":0
|
"order":0
|
||||||
},
|
},
|
||||||
|
|
|
@ -527,6 +527,8 @@
|
||||||
"HS256": { "order": 0 },
|
"HS256": { "order": 0 },
|
||||||
"HS512": { "order": 0 },
|
"HS512": { "order": 0 },
|
||||||
"ES256": { "order": 0 },
|
"ES256": { "order": 0 },
|
||||||
|
"Ed25519": { "order": 0 },
|
||||||
|
"Ed448": { "order": 0 },
|
||||||
"RS256": { "order": 0 },
|
"RS256": { "order": 0 },
|
||||||
"HS384": { "order": 0 },
|
"HS384": { "order": 0 },
|
||||||
"ES512": { "order": 0 },
|
"ES512": { "order": 0 },
|
||||||
|
@ -927,6 +929,8 @@
|
||||||
"HS256": { "order": 0 },
|
"HS256": { "order": 0 },
|
||||||
"HS512": { "order": 0 },
|
"HS512": { "order": 0 },
|
||||||
"ES256": { "order": 0 },
|
"ES256": { "order": 0 },
|
||||||
|
"Ed25519": { "order": 0 },
|
||||||
|
"Ed448": { "order": 0 },
|
||||||
"RS256": { "order": 0 },
|
"RS256": { "order": 0 },
|
||||||
"HS384": { "order": 0 },
|
"HS384": { "order": 0 },
|
||||||
"ES512": { "order": 0 },
|
"ES512": { "order": 0 },
|
||||||
|
|
|
@ -235,6 +235,19 @@ export const KeysListTab = ({ realmComponents }: KeysListTabProps) => {
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
} else if (type === "OKP") {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
togglePublicKeyDialog();
|
||||||
|
setPublicKey(publicKey!);
|
||||||
|
}}
|
||||||
|
variant="secondary"
|
||||||
|
id="kc-public-key"
|
||||||
|
>
|
||||||
|
{t("publicKeys").slice(0, -1)}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
} else return "";
|
} else return "";
|
||||||
},
|
},
|
||||||
cellFormatters: [],
|
cellFormatters: [],
|
||||||
|
|
2
pom.xml
2
pom.xml
|
@ -189,7 +189,7 @@
|
||||||
<jboss.as.plugin.version>7.5.Final</jboss.as.plugin.version>
|
<jboss.as.plugin.version>7.5.Final</jboss.as.plugin.version>
|
||||||
<jmeter.plugin.version>1.9.0</jmeter.plugin.version>
|
<jmeter.plugin.version>1.9.0</jmeter.plugin.version>
|
||||||
<jmeter.analysis.plugin.version>1.0.4</jmeter.analysis.plugin.version>
|
<jmeter.analysis.plugin.version>1.0.4</jmeter.analysis.plugin.version>
|
||||||
<osgi.bundle.plugin.version>2.4.0</osgi.bundle.plugin.version>
|
<osgi.bundle.plugin.version>5.1.8</osgi.bundle.plugin.version>
|
||||||
<wildfly.plugin.version>2.0.1.Final</wildfly.plugin.version>
|
<wildfly.plugin.version>2.0.1.Final</wildfly.plugin.version>
|
||||||
<nexus.staging.plugin.version>1.6.13</nexus.staging.plugin.version>
|
<nexus.staging.plugin.version>1.6.13</nexus.staging.plugin.version>
|
||||||
<frontend.plugin.version>1.14.2</frontend.plugin.version>
|
<frontend.plugin.version>1.14.2</frontend.plugin.version>
|
||||||
|
|
|
@ -30,6 +30,14 @@
|
||||||
<name>Keycloak REST Services</name>
|
<name>Keycloak REST Services</name>
|
||||||
<description />
|
<description />
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<version.swagger.doclet>1.1.2</version.swagger.doclet>
|
||||||
|
<maven.compiler.plugin.version>3.8.1</maven.compiler.plugin.version>
|
||||||
|
<maven.compiler.release>17</maven.compiler.release>
|
||||||
|
<maven.compiler.source>17</maven.compiler.source>
|
||||||
|
<maven.compiler.target>17</maven.compiler.target>
|
||||||
|
</properties>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
|
|
|
@ -585,12 +585,13 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
|
||||||
logger.debugf("Failed to verify token, key not found for algorithm %s", jws.getHeader().getRawAlgorithm());
|
logger.debugf("Failed to verify token, key not found for algorithm %s", jws.getHeader().getRawAlgorithm());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
String algorithm = jws.getHeader().getRawAlgorithm();
|
||||||
if (key.getAlgorithm() == null) {
|
if (key.getAlgorithm() == null) {
|
||||||
key.setAlgorithm(jws.getHeader().getRawAlgorithm());
|
key.setAlgorithm(algorithm);
|
||||||
}
|
}
|
||||||
SignatureProvider signatureProvider = session.getProvider(SignatureProvider.class, jws.getHeader().getRawAlgorithm());
|
SignatureProvider signatureProvider = session.getProvider(SignatureProvider.class, algorithm);
|
||||||
if (signatureProvider == null) {
|
if (signatureProvider == null) {
|
||||||
logger.debugf("Failed to verify token, signature provider not found for algorithm %s", jws.getHeader().getRawAlgorithm());
|
logger.debugf("Failed to verify token, signature provider not found for algorithm %s", algorithm);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,20 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023 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.crypto;
|
package org.keycloak.crypto;
|
||||||
|
|
||||||
import org.keycloak.common.VerificationException;
|
import org.keycloak.common.VerificationException;
|
||||||
|
@ -6,6 +23,9 @@ import org.keycloak.keys.loader.PublicKeyStorageManager;
|
||||||
import org.keycloak.models.ClientModel;
|
import org.keycloak.models.ClientModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
|
||||||
|
*/
|
||||||
public class ClientECDSASignatureVerifierContext extends AsymmetricSignatureVerifierContext {
|
public class ClientECDSASignatureVerifierContext extends AsymmetricSignatureVerifierContext {
|
||||||
public ClientECDSASignatureVerifierContext(KeycloakSession session, ClientModel client, JWSInput input) throws VerificationException {
|
public ClientECDSASignatureVerifierContext(KeycloakSession session, ClientModel client, JWSInput input) throws VerificationException {
|
||||||
super(getKey(session, client, input));
|
super(getKey(session, client, input));
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023 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.crypto;
|
||||||
|
|
||||||
|
import org.keycloak.common.VerificationException;
|
||||||
|
import org.keycloak.jose.jws.JWSInput;
|
||||||
|
import org.keycloak.keys.loader.PublicKeyStorageManager;
|
||||||
|
import org.keycloak.models.ClientModel;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
|
||||||
|
*/
|
||||||
|
public class ClientEdDSASignatureVerifierContext extends AsymmetricSignatureVerifierContext {
|
||||||
|
public ClientEdDSASignatureVerifierContext(KeycloakSession session, ClientModel client, JWSInput input) throws VerificationException {
|
||||||
|
super(getKey(session, client, input));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static KeyWrapper getKey(KeycloakSession session, ClientModel client, JWSInput input) throws VerificationException {
|
||||||
|
KeyWrapper key = PublicKeyStorageManager.getClientPublicKeyWrapper(session, client, input);
|
||||||
|
if (key == null) {
|
||||||
|
throw new VerificationException("Key not found");
|
||||||
|
}
|
||||||
|
if (!KeyType.OKP.equals(key.getType())) {
|
||||||
|
throw new VerificationException("Key Type is not OKP: " + key.getType());
|
||||||
|
}
|
||||||
|
if (key.getCurve() == null) {
|
||||||
|
throw new VerificationException("EdDSA key should have curve defined");
|
||||||
|
}
|
||||||
|
if (key.getAlgorithm() == null) {
|
||||||
|
// defaults to the algorithm set to the JWS
|
||||||
|
// validations should be performed prior to verifying signature in case there are restrictions on the algorithms
|
||||||
|
// that can used for signing
|
||||||
|
key.setAlgorithm(input.getHeader().getRawAlgorithm());
|
||||||
|
}
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023 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.crypto;
|
||||||
|
|
||||||
|
import org.keycloak.common.VerificationException;
|
||||||
|
import org.keycloak.jose.jws.JWSInput;
|
||||||
|
import org.keycloak.models.ClientModel;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
|
||||||
|
*/
|
||||||
|
public class EdDSAClientSignatureVerifierProvider implements ClientSignatureVerifierProvider {
|
||||||
|
private final KeycloakSession session;
|
||||||
|
private final String algorithm;
|
||||||
|
|
||||||
|
public EdDSAClientSignatureVerifierProvider(KeycloakSession session, String algorithm) {
|
||||||
|
this.session = session;
|
||||||
|
this.algorithm = algorithm;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SignatureVerifierContext verifier(ClientModel client, JWSInput input) throws VerificationException {
|
||||||
|
return new ClientEdDSASignatureVerifierContext(session, client, input);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getAlgorithm() {
|
||||||
|
return algorithm;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isAsymmetricAlgorithm() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023 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.crypto;
|
||||||
|
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
|
||||||
|
*/
|
||||||
|
public class EdDSAClientSignatureVerifierProviderFactory implements ClientSignatureVerifierProviderFactory {
|
||||||
|
|
||||||
|
public static final String ID = Algorithm.EdDSA;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId() {
|
||||||
|
return ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ClientSignatureVerifierProvider create(KeycloakSession session) {
|
||||||
|
return new EdDSAClientSignatureVerifierProvider(session, Algorithm.EdDSA);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023 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.crypto;
|
||||||
|
|
||||||
|
import org.keycloak.common.VerificationException;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
|
||||||
|
*/
|
||||||
|
public class EdDSASignatureProvider implements SignatureProvider {
|
||||||
|
|
||||||
|
private final KeycloakSession session;
|
||||||
|
|
||||||
|
public EdDSASignatureProvider(KeycloakSession session) {
|
||||||
|
this.session = session;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SignatureSignerContext signer() throws SignatureException {
|
||||||
|
return new ServerEdDSASignatureSignerContext(session, Algorithm.EdDSA);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SignatureSignerContext signer(KeyWrapper key) throws SignatureException {
|
||||||
|
SignatureProvider.checkKeyForSignature(key, Algorithm.EdDSA, KeyType.OKP);
|
||||||
|
return new ServerEdDSASignatureSignerContext(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SignatureVerifierContext verifier(String kid) throws VerificationException {
|
||||||
|
return new ServerEdDSASignatureVerifierContext(session, kid, Algorithm.EdDSA);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SignatureVerifierContext verifier(KeyWrapper key) throws VerificationException {
|
||||||
|
SignatureProvider.checkKeyForVerification(key, Algorithm.EdDSA, KeyType.OKP);
|
||||||
|
return new ServerEdDSASignatureVerifierContext(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isAsymmetricAlgorithm() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023 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.crypto;
|
||||||
|
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
|
||||||
|
*/
|
||||||
|
public class EdDSASignatureProviderFactory implements SignatureProviderFactory {
|
||||||
|
|
||||||
|
public static final String ID = Algorithm.EdDSA;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId() {
|
||||||
|
return ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SignatureProvider create(KeycloakSession session) {
|
||||||
|
return new EdDSASignatureProvider(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023 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.crypto;
|
||||||
|
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
|
||||||
|
*/
|
||||||
|
public class ServerEdDSASignatureSignerContext extends AsymmetricSignatureSignerContext {
|
||||||
|
|
||||||
|
public ServerEdDSASignatureSignerContext(KeycloakSession session, String algorithm) throws SignatureException {
|
||||||
|
super(ServerAsymmetricSignatureSignerContext.getKey(session, algorithm));
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServerEdDSASignatureSignerContext(KeyWrapper key) {
|
||||||
|
super(key);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023 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.crypto;
|
||||||
|
|
||||||
|
import org.keycloak.common.VerificationException;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
|
||||||
|
*/
|
||||||
|
public class ServerEdDSASignatureVerifierContext extends AsymmetricSignatureVerifierContext {
|
||||||
|
public ServerEdDSASignatureVerifierContext(KeycloakSession session, String kid, String algorithm) throws VerificationException {
|
||||||
|
super(ServerAsymmetricSignatureVerifierContext.getKey(session, kid, algorithm));
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServerEdDSASignatureVerifierContext(KeyWrapper key) {
|
||||||
|
super(key);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,78 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023 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.keys;
|
||||||
|
|
||||||
|
import org.keycloak.common.util.KeyUtils;
|
||||||
|
import org.keycloak.component.ComponentModel;
|
||||||
|
import org.keycloak.crypto.Algorithm;
|
||||||
|
import org.keycloak.crypto.KeyStatus;
|
||||||
|
import org.keycloak.crypto.KeyType;
|
||||||
|
import org.keycloak.crypto.KeyUse;
|
||||||
|
import org.keycloak.crypto.KeyWrapper;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
|
||||||
|
import java.security.KeyPair;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
|
||||||
|
*/
|
||||||
|
public abstract class AbstractEddsaKeyProvider implements KeyProvider {
|
||||||
|
|
||||||
|
private final KeyStatus status;
|
||||||
|
|
||||||
|
private final ComponentModel model;
|
||||||
|
|
||||||
|
private final KeyWrapper key;
|
||||||
|
|
||||||
|
public AbstractEddsaKeyProvider(RealmModel realm, ComponentModel model) {
|
||||||
|
this.model = model;
|
||||||
|
this.status = KeyStatus.from(model.get(Attributes.ACTIVE_KEY, true), model.get(Attributes.ENABLED_KEY, true));
|
||||||
|
|
||||||
|
if (model.hasNote(KeyWrapper.class.getName())) {
|
||||||
|
key = model.getNote(KeyWrapper.class.getName());
|
||||||
|
} else {
|
||||||
|
key = loadKey(realm, model);
|
||||||
|
model.setNote(KeyWrapper.class.getName(), key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract KeyWrapper loadKey(RealmModel realm, ComponentModel model);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Stream<KeyWrapper> getKeysStream() {
|
||||||
|
return Stream.of(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected KeyWrapper createKeyWrapper(KeyPair keyPair, String curveName) {
|
||||||
|
KeyWrapper key = new KeyWrapper();
|
||||||
|
|
||||||
|
key.setProviderId(model.getId());
|
||||||
|
key.setProviderPriority(model.get("priority", 0l));
|
||||||
|
|
||||||
|
key.setKid(KeyUtils.createKeyId(keyPair.getPublic()));
|
||||||
|
key.setUse(KeyUse.SIG);
|
||||||
|
key.setType(KeyType.OKP);
|
||||||
|
key.setAlgorithm(Algorithm.EdDSA);
|
||||||
|
key.setCurve(curveName);
|
||||||
|
key.setStatus(status);
|
||||||
|
key.setPrivateKey(keyPair.getPrivate());
|
||||||
|
key.setPublicKey(keyPair.getPublic());
|
||||||
|
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,71 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023 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.keys;
|
||||||
|
|
||||||
|
import org.keycloak.component.ComponentModel;
|
||||||
|
import org.keycloak.component.ComponentValidationException;
|
||||||
|
import org.keycloak.crypto.Algorithm;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.provider.ConfigurationValidationHelper;
|
||||||
|
import org.keycloak.provider.ProviderConfigProperty;
|
||||||
|
import org.keycloak.provider.ProviderConfigurationBuilder;
|
||||||
|
|
||||||
|
import java.security.KeyPair;
|
||||||
|
import java.security.KeyPairGenerator;
|
||||||
|
|
||||||
|
import static org.keycloak.provider.ProviderConfigProperty.LIST_TYPE;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
|
||||||
|
*/
|
||||||
|
public abstract class AbstractEddsaKeyProviderFactory implements KeyProviderFactory {
|
||||||
|
|
||||||
|
protected static final String EDDSA_PRIVATE_KEY_KEY = "eddsaPrivateKey";
|
||||||
|
protected static final String EDDSA_PUBLIC_KEY_KEY = "eddsaPublicKey";
|
||||||
|
protected static final String EDDSA_ELLIPTIC_CURVE_KEY = "eddsaEllipticCurveKey";
|
||||||
|
protected static final String DEFAULT_EDDSA_ELLIPTIC_CURVE = Algorithm.Ed25519;
|
||||||
|
|
||||||
|
protected static ProviderConfigProperty EDDSA_ELLIPTIC_CURVE_PROPERTY = new ProviderConfigProperty(EDDSA_ELLIPTIC_CURVE_KEY,
|
||||||
|
"Elliptic Curve", "Elliptic Curve used in EdDSA", LIST_TYPE,
|
||||||
|
String.valueOf(DEFAULT_EDDSA_ELLIPTIC_CURVE), Algorithm.Ed25519, Algorithm.Ed448);
|
||||||
|
|
||||||
|
public final static ProviderConfigurationBuilder configurationBuilder() {
|
||||||
|
return ProviderConfigurationBuilder.create()
|
||||||
|
.property(Attributes.PRIORITY_PROPERTY)
|
||||||
|
.property(Attributes.ENABLED_PROPERTY)
|
||||||
|
.property(Attributes.ACTIVE_PROPERTY);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel model) throws ComponentValidationException {
|
||||||
|
ConfigurationValidationHelper.check(model)
|
||||||
|
.checkLong(Attributes.PRIORITY_PROPERTY, false)
|
||||||
|
.checkBoolean(Attributes.ENABLED_PROPERTY, false)
|
||||||
|
.checkBoolean(Attributes.ACTIVE_PROPERTY, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static KeyPair generateEddsaKeyPair(String curveName) {
|
||||||
|
try {
|
||||||
|
KeyPairGenerator keyGen = KeyPairGenerator.getInstance(curveName);
|
||||||
|
return keyGen.generateKeyPair();
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023 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.keys;
|
||||||
|
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
import org.keycloak.common.util.Base64;
|
||||||
|
import org.keycloak.component.ComponentModel;
|
||||||
|
import org.keycloak.crypto.KeyWrapper;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
|
||||||
|
import java.security.KeyFactory;
|
||||||
|
import java.security.KeyPair;
|
||||||
|
import java.security.PrivateKey;
|
||||||
|
import java.security.PublicKey;
|
||||||
|
import java.security.spec.PKCS8EncodedKeySpec;
|
||||||
|
import java.security.spec.X509EncodedKeySpec;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
|
||||||
|
*/
|
||||||
|
public class GeneratedEddsaKeyProvider extends AbstractEddsaKeyProvider {
|
||||||
|
private static final Logger logger = Logger.getLogger(GeneratedEddsaKeyProvider.class);
|
||||||
|
|
||||||
|
public GeneratedEddsaKeyProvider(RealmModel realm, ComponentModel model) {
|
||||||
|
super(realm, model);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected KeyWrapper loadKey(RealmModel realm, ComponentModel model) {
|
||||||
|
String privateEddsaKeyBase64Encoded = model.getConfig().getFirst(GeneratedEddsaKeyProviderFactory.EDDSA_PRIVATE_KEY_KEY);
|
||||||
|
String publicEddsaKeyBase64Encoded = model.getConfig().getFirst(GeneratedEddsaKeyProviderFactory.EDDSA_PUBLIC_KEY_KEY);
|
||||||
|
String curveName = model.getConfig().getFirst(GeneratedEddsaKeyProviderFactory.EDDSA_ELLIPTIC_CURVE_KEY);
|
||||||
|
|
||||||
|
try {
|
||||||
|
PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(Base64.decode(privateEddsaKeyBase64Encoded));
|
||||||
|
KeyFactory kf = KeyFactory.getInstance("EdDSA");
|
||||||
|
PrivateKey decodedPrivateKey = kf.generatePrivate(privateKeySpec);
|
||||||
|
|
||||||
|
X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(Base64.decode(publicEddsaKeyBase64Encoded));
|
||||||
|
PublicKey decodedPublicKey = kf.generatePublic(publicKeySpec);
|
||||||
|
|
||||||
|
KeyPair keyPair = new KeyPair(decodedPublicKey, decodedPrivateKey);
|
||||||
|
|
||||||
|
return createKeyWrapper(keyPair, curveName);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warnf("Exception at decodeEddsaPublicKey. %s", e.toString());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,141 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023 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.keys;
|
||||||
|
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
import org.keycloak.common.util.Base64;
|
||||||
|
import org.keycloak.common.util.MultivaluedHashMap;
|
||||||
|
import org.keycloak.component.ComponentModel;
|
||||||
|
import org.keycloak.component.ComponentValidationException;
|
||||||
|
import org.keycloak.crypto.Algorithm;
|
||||||
|
import org.keycloak.crypto.KeyUse;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.provider.ConfigurationValidationHelper;
|
||||||
|
import org.keycloak.provider.ProviderConfigProperty;
|
||||||
|
|
||||||
|
import java.security.KeyFactory;
|
||||||
|
import java.security.KeyPair;
|
||||||
|
import java.security.interfaces.EdECPublicKey;
|
||||||
|
import java.security.spec.X509EncodedKeySpec;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
|
||||||
|
*/
|
||||||
|
public class GeneratedEddsaKeyProviderFactory extends AbstractEddsaKeyProviderFactory {
|
||||||
|
|
||||||
|
private static final Logger logger = Logger.getLogger(GeneratedEddsaKeyProviderFactory.class);
|
||||||
|
|
||||||
|
public static final String ID = "eddsa-generated";
|
||||||
|
|
||||||
|
private static final String HELP_TEXT = "Generates EdDSA keys";
|
||||||
|
|
||||||
|
public static final String DEFAULT_EDDSA_ELLIPTIC_CURVE = Algorithm.Ed25519;
|
||||||
|
|
||||||
|
private static final List<ProviderConfigProperty> CONFIG_PROPERTIES = AbstractEddsaKeyProviderFactory.configurationBuilder()
|
||||||
|
.property(EDDSA_ELLIPTIC_CURVE_PROPERTY)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public KeyProvider create(KeycloakSession session, ComponentModel model) {
|
||||||
|
return new GeneratedEddsaKeyProvider(session.getContext().getRealm(), model);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean createFallbackKeys(KeycloakSession session, KeyUse keyUse, String algorithm) {
|
||||||
|
if (keyUse.equals(KeyUse.SIG) && algorithm.equals(Algorithm.EdDSA)) {
|
||||||
|
RealmModel realm = session.getContext().getRealm();
|
||||||
|
|
||||||
|
ComponentModel generated = new ComponentModel();
|
||||||
|
generated.setName("fallback-" + algorithm);
|
||||||
|
generated.setParentId(realm.getId());
|
||||||
|
generated.setProviderId(ID);
|
||||||
|
generated.setProviderType(KeyProvider.class.getName());
|
||||||
|
|
||||||
|
MultivaluedHashMap<String, String> config = new MultivaluedHashMap<>();
|
||||||
|
config.putSingle(Attributes.PRIORITY_KEY, "-100");
|
||||||
|
config.putSingle(EDDSA_ELLIPTIC_CURVE_KEY, DEFAULT_EDDSA_ELLIPTIC_CURVE);
|
||||||
|
generated.setConfig(config);
|
||||||
|
|
||||||
|
realm.addComponentModel(generated);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getHelpText() {
|
||||||
|
return HELP_TEXT;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<ProviderConfigProperty> getConfigProperties() {
|
||||||
|
return CONFIG_PROPERTIES;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId() {
|
||||||
|
return ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel model) throws ComponentValidationException {
|
||||||
|
super.validateConfiguration(session, realm, model);
|
||||||
|
|
||||||
|
ConfigurationValidationHelper.check(model).checkList(EDDSA_ELLIPTIC_CURVE_PROPERTY, false);
|
||||||
|
|
||||||
|
String curveName = model.get(EDDSA_ELLIPTIC_CURVE_KEY);
|
||||||
|
if (curveName == null) curveName = DEFAULT_EDDSA_ELLIPTIC_CURVE;
|
||||||
|
|
||||||
|
if (!(model.contains(EDDSA_PRIVATE_KEY_KEY) && model.contains(EDDSA_PUBLIC_KEY_KEY))) {
|
||||||
|
generateKeys(model, curveName);
|
||||||
|
logger.debugv("Generated keys for {0}", realm.getName());
|
||||||
|
} else {
|
||||||
|
String currentEdEc = getCurveFromPublicKey(model.getConfig().getFirst(GeneratedEddsaKeyProviderFactory.EDDSA_PUBLIC_KEY_KEY));
|
||||||
|
if (!curveName.equals(currentEdEc)) {
|
||||||
|
generateKeys(model, curveName);
|
||||||
|
logger.debugv("Twisted Edwards Curve changed, generating new keys for {0}", realm.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void generateKeys(ComponentModel model, String curveName) {
|
||||||
|
KeyPair keyPair;
|
||||||
|
try {
|
||||||
|
keyPair = generateEddsaKeyPair(curveName);
|
||||||
|
model.put(EDDSA_PRIVATE_KEY_KEY, Base64.encodeBytes(keyPair.getPrivate().getEncoded()));
|
||||||
|
model.put(EDDSA_PUBLIC_KEY_KEY, Base64.encodeBytes(keyPair.getPublic().getEncoded()));
|
||||||
|
model.put(EDDSA_ELLIPTIC_CURVE_KEY, curveName);
|
||||||
|
} catch (Throwable t) {
|
||||||
|
throw new ComponentValidationException("Failed to generate EdDSA keys", t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getCurveFromPublicKey(String publicEddsaKeyBase64Encoded) {
|
||||||
|
try {
|
||||||
|
KeyFactory kf = KeyFactory.getInstance("EdDSA");
|
||||||
|
X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(Base64.decode(publicEddsaKeyBase64Encoded));
|
||||||
|
EdECPublicKey edEcKey = (EdECPublicKey) kf.generatePublic(publicKeySpec);
|
||||||
|
return edEcKey.getParams().getName();
|
||||||
|
} catch (Throwable t) {
|
||||||
|
throw new ComponentValidationException("Failed to get Twisted Edwards Curve from its public key", t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -48,6 +48,9 @@ public class HardcodedPublicKeyLoader implements PublicKeyLoader {
|
||||||
} else if (JavaAlgorithm.isECJavaAlgorithm(algorithm)) {
|
} else if (JavaAlgorithm.isECJavaAlgorithm(algorithm)) {
|
||||||
keyWrapper.setType(KeyType.EC);
|
keyWrapper.setType(KeyType.EC);
|
||||||
keyWrapper.setPublicKey(PemUtils.decodePublicKey(encodedKey, KeyType.EC));
|
keyWrapper.setPublicKey(PemUtils.decodePublicKey(encodedKey, KeyType.EC));
|
||||||
|
} else if (JavaAlgorithm.isEddsaJavaAlgorithm(algorithm)) {
|
||||||
|
keyWrapper.setType(KeyType.OKP);
|
||||||
|
keyWrapper.setPublicKey(PemUtils.decodePublicKey(encodedKey, KeyType.OKP));
|
||||||
} else if (JavaAlgorithm.isHMACJavaAlgorithm(algorithm)) {
|
} else if (JavaAlgorithm.isHMACJavaAlgorithm(algorithm)) {
|
||||||
keyWrapper.setType(KeyType.OCT);
|
keyWrapper.setType(KeyType.OCT);
|
||||||
keyWrapper.setSecretKey(KeyUtils.loadSecretKey(Base64Url.decode(encodedKey), algorithm));
|
keyWrapper.setSecretKey(KeyUtils.loadSecretKey(Base64Url.decode(encodedKey), algorithm));
|
||||||
|
|
|
@ -19,6 +19,7 @@ package org.keycloak.keys.loader;
|
||||||
|
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.broker.oidc.OIDCIdentityProviderConfig;
|
import org.keycloak.broker.oidc.OIDCIdentityProviderConfig;
|
||||||
|
import org.keycloak.crypto.Algorithm;
|
||||||
import org.keycloak.crypto.KeyWrapper;
|
import org.keycloak.crypto.KeyWrapper;
|
||||||
import org.keycloak.jose.jwk.JWK;
|
import org.keycloak.jose.jwk.JWK;
|
||||||
import org.keycloak.jose.jws.JWSInput;
|
import org.keycloak.jose.jws.JWSInput;
|
||||||
|
|
|
@ -220,6 +220,8 @@ public class OIDCLoginProtocolService {
|
||||||
return b.rsa(k.getPublicKey(), certificates, k.getUse());
|
return b.rsa(k.getPublicKey(), certificates, k.getUse());
|
||||||
} else if (k.getType().equals(KeyType.EC)) {
|
} else if (k.getType().equals(KeyType.EC)) {
|
||||||
return b.ec(k.getPublicKey(), k.getUse());
|
return b.ec(k.getPublicKey(), k.getUse());
|
||||||
|
} else if (k.getType().equals(KeyType.OKP)) {
|
||||||
|
return b.okp(k.getPublicKey(), k.getUse());
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
})
|
})
|
||||||
|
|
|
@ -21,7 +21,6 @@ import java.util.HashSet;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.function.BiConsumer;
|
import java.util.function.BiConsumer;
|
||||||
|
|
||||||
import org.keycloak.OAuth2Constants;
|
|
||||||
import org.keycloak.jose.JOSEHeader;
|
import org.keycloak.jose.JOSEHeader;
|
||||||
import org.keycloak.jose.JOSE;
|
import org.keycloak.jose.JOSE;
|
||||||
import org.keycloak.jose.jwe.JWE;
|
import org.keycloak.jose.jwe.JWE;
|
||||||
|
|
|
@ -10,3 +10,4 @@ org.keycloak.crypto.PS512ClientSignatureVerifierProviderFactory
|
||||||
org.keycloak.crypto.HS256ClientSignatureVerifierProviderFactory
|
org.keycloak.crypto.HS256ClientSignatureVerifierProviderFactory
|
||||||
org.keycloak.crypto.HS384ClientSignatureVerifierProviderFactory
|
org.keycloak.crypto.HS384ClientSignatureVerifierProviderFactory
|
||||||
org.keycloak.crypto.HS512ClientSignatureVerifierProviderFactory
|
org.keycloak.crypto.HS512ClientSignatureVerifierProviderFactory
|
||||||
|
org.keycloak.crypto.EdDSAClientSignatureVerifierProviderFactory
|
||||||
|
|
|
@ -10,3 +10,4 @@ org.keycloak.crypto.ES512SignatureProviderFactory
|
||||||
org.keycloak.crypto.PS256SignatureProviderFactory
|
org.keycloak.crypto.PS256SignatureProviderFactory
|
||||||
org.keycloak.crypto.PS384SignatureProviderFactory
|
org.keycloak.crypto.PS384SignatureProviderFactory
|
||||||
org.keycloak.crypto.PS512SignatureProviderFactory
|
org.keycloak.crypto.PS512SignatureProviderFactory
|
||||||
|
org.keycloak.crypto.EdDSASignatureProviderFactory
|
||||||
|
|
|
@ -23,3 +23,4 @@ org.keycloak.keys.ImportedRsaKeyProviderFactory
|
||||||
org.keycloak.keys.GeneratedEcdsaKeyProviderFactory
|
org.keycloak.keys.GeneratedEcdsaKeyProviderFactory
|
||||||
org.keycloak.keys.GeneratedRsaEncKeyProviderFactory
|
org.keycloak.keys.GeneratedRsaEncKeyProviderFactory
|
||||||
org.keycloak.keys.ImportedRsaEncKeyProviderFactory
|
org.keycloak.keys.ImportedRsaEncKeyProviderFactory
|
||||||
|
org.keycloak.keys.GeneratedEddsaKeyProviderFactory
|
127
services/src/test/java/org/keycloak/jose/jwk/ServerJWKTest.java
Normal file
127
services/src/test/java/org/keycloak/jose/jwk/ServerJWKTest.java
Normal file
|
@ -0,0 +1,127 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023 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.jose.jwk;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertArrayEquals;
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.KeyPair;
|
||||||
|
import java.security.KeyPairGenerator;
|
||||||
|
import java.security.PrivateKey;
|
||||||
|
import java.security.PublicKey;
|
||||||
|
import java.security.Signature;
|
||||||
|
|
||||||
|
import org.junit.ClassRule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.keycloak.common.util.KeyUtils;
|
||||||
|
import org.keycloak.crypto.Algorithm;
|
||||||
|
import org.keycloak.crypto.JavaAlgorithm;
|
||||||
|
import org.keycloak.rule.CryptoInitRule;
|
||||||
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is not tested in keycloak-core. The subclasses should be created in the crypto modules to make sure it is tested with corresponding modules (bouncycastle VS bouncycastle-fips)
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
|
||||||
|
*/
|
||||||
|
public abstract class ServerJWKTest {
|
||||||
|
|
||||||
|
@ClassRule
|
||||||
|
public static CryptoInitRule cryptoInitRule = new CryptoInitRule();
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void publicEd25519() throws Exception {
|
||||||
|
KeyPairGenerator keyGen = KeyPairGenerator.getInstance(Algorithm.Ed25519);
|
||||||
|
KeyPair keyPair = keyGen.generateKeyPair();
|
||||||
|
|
||||||
|
PublicKey publicKey = keyPair.getPublic();
|
||||||
|
JWK jwk = JWKBuilder.create().kid(KeyUtils.createKeyId(keyPair.getPublic())).algorithm(Algorithm.EdDSA).okp(publicKey);
|
||||||
|
|
||||||
|
assertEquals("OKP", jwk.getKeyType());
|
||||||
|
assertEquals("EdDSA", jwk.getAlgorithm());
|
||||||
|
assertEquals("sig", jwk.getPublicKeyUse());
|
||||||
|
|
||||||
|
assertTrue(jwk instanceof OKPPublicJWK);
|
||||||
|
|
||||||
|
OKPPublicJWK okpJwk = (OKPPublicJWK) jwk;
|
||||||
|
|
||||||
|
assertEquals("Ed25519", okpJwk.getCrv());
|
||||||
|
assertNotNull(okpJwk.getX());
|
||||||
|
|
||||||
|
String jwkJson = JsonSerialization.writeValueAsString(jwk);
|
||||||
|
|
||||||
|
JWKParser parser = JWKParser.create().parse(jwkJson);
|
||||||
|
PublicKey publicKeyFromJwk = parser.toPublicKey();
|
||||||
|
|
||||||
|
assertArrayEquals(publicKey.getEncoded(), publicKeyFromJwk.getEncoded());
|
||||||
|
|
||||||
|
byte[] data = "Some test string".getBytes(StandardCharsets.UTF_8);
|
||||||
|
byte[] sign = sign(data, JavaAlgorithm.Ed25519, keyPair.getPrivate());
|
||||||
|
verify(data, sign, JavaAlgorithm.Ed25519, publicKeyFromJwk);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void publicEd448() throws Exception {
|
||||||
|
KeyPairGenerator keyGen = KeyPairGenerator.getInstance(Algorithm.Ed448);
|
||||||
|
KeyPair keyPair = keyGen.generateKeyPair();
|
||||||
|
|
||||||
|
PublicKey publicKey = keyPair.getPublic();
|
||||||
|
JWK jwk = JWKBuilder.create().kid(KeyUtils.createKeyId(keyPair.getPublic())).algorithm(Algorithm.EdDSA).okp(publicKey);
|
||||||
|
|
||||||
|
assertEquals("OKP", jwk.getKeyType());
|
||||||
|
assertEquals("EdDSA", jwk.getAlgorithm());
|
||||||
|
assertEquals("sig", jwk.getPublicKeyUse());
|
||||||
|
|
||||||
|
assertTrue(jwk instanceof OKPPublicJWK);
|
||||||
|
|
||||||
|
OKPPublicJWK okpJwk = (OKPPublicJWK) jwk;
|
||||||
|
|
||||||
|
assertEquals("Ed448", okpJwk.getCrv());
|
||||||
|
assertNotNull(okpJwk.getX());
|
||||||
|
|
||||||
|
String jwkJson = JsonSerialization.writeValueAsString(jwk);
|
||||||
|
|
||||||
|
JWKParser parser = JWKParser.create().parse(jwkJson);
|
||||||
|
PublicKey publicKeyFromJwk = parser.toPublicKey();
|
||||||
|
|
||||||
|
assertArrayEquals(publicKey.getEncoded(), publicKeyFromJwk.getEncoded());
|
||||||
|
|
||||||
|
byte[] data = "Some test string".getBytes(StandardCharsets.UTF_8);
|
||||||
|
byte[] sign = sign(data, JavaAlgorithm.Ed448, keyPair.getPrivate());
|
||||||
|
verify(data, sign, JavaAlgorithm.Ed448, publicKeyFromJwk);
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] sign(byte[] data, String javaAlgorithm, PrivateKey key) throws Exception {
|
||||||
|
Signature signature = Signature.getInstance(javaAlgorithm);
|
||||||
|
signature.initSign(key);
|
||||||
|
signature.update(data);
|
||||||
|
return signature.sign();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean verify(byte[] data, byte[] signature, String javaAlgorithm, PublicKey key) throws Exception {
|
||||||
|
Signature verifier = Signature.getInstance(javaAlgorithm);
|
||||||
|
verifier.initVerify(key);
|
||||||
|
verifier.update(data);
|
||||||
|
return verifier.verify(signature);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -33,6 +33,10 @@
|
||||||
<properties>
|
<properties>
|
||||||
<js-adapter.version>${project.version}</js-adapter.version>
|
<js-adapter.version>${project.version}</js-adapter.version>
|
||||||
<js-adapter.file.path>${project.basedir}/target/classes/javascript</js-adapter.file.path>
|
<js-adapter.file.path>${project.basedir}/target/classes/javascript</js-adapter.file.path>
|
||||||
|
<maven.compiler.plugin.version>3.8.1</maven.compiler.plugin.version>
|
||||||
|
<maven.compiler.release>17</maven.compiler.release>
|
||||||
|
<maven.compiler.source>17</maven.compiler.source>
|
||||||
|
<maven.compiler.target>17</maven.compiler.target>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
|
|
|
@ -126,6 +126,7 @@ public class TestApplicationResourceProviderFactory implements RealmResourceProv
|
||||||
private String keyType = KeyType.RSA;
|
private String keyType = KeyType.RSA;
|
||||||
private String keyAlgorithm;
|
private String keyAlgorithm;
|
||||||
private KeyUse keyUse = KeyUse.SIG;
|
private KeyUse keyUse = KeyUse.SIG;
|
||||||
|
private String curve;
|
||||||
|
|
||||||
// Kid will be randomly generated (based on the key hash) if not provided here
|
// Kid will be randomly generated (based on the key hash) if not provided here
|
||||||
private String kid;
|
private String kid;
|
||||||
|
@ -193,5 +194,13 @@ public class TestApplicationResourceProviderFactory implements RealmResourceProv
|
||||||
public void setKid(String kid) {
|
public void setKid(String kid) {
|
||||||
this.kid = kid;
|
this.kid = kid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getCurve() {
|
||||||
|
return curve;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCurve(String curve) {
|
||||||
|
this.curve = curve;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,6 +39,7 @@ import org.keycloak.crypto.KeyUse;
|
||||||
import org.keycloak.crypto.KeyWrapper;
|
import org.keycloak.crypto.KeyWrapper;
|
||||||
import org.keycloak.crypto.MacSignatureSignerContext;
|
import org.keycloak.crypto.MacSignatureSignerContext;
|
||||||
import org.keycloak.crypto.ServerECDSASignatureSignerContext;
|
import org.keycloak.crypto.ServerECDSASignatureSignerContext;
|
||||||
|
import org.keycloak.crypto.ServerEdDSASignatureSignerContext;
|
||||||
import org.keycloak.crypto.SignatureSignerContext;
|
import org.keycloak.crypto.SignatureSignerContext;
|
||||||
import org.keycloak.jose.jwe.JWEConstants;
|
import org.keycloak.jose.jwe.JWEConstants;
|
||||||
import org.keycloak.jose.jwk.JSONWebKeySet;
|
import org.keycloak.jose.jwk.JSONWebKeySet;
|
||||||
|
@ -88,9 +89,6 @@ import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
|
||||||
import java.util.concurrent.BlockingQueue;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
import java.util.concurrent.ConcurrentMap;
|
import java.util.concurrent.ConcurrentMap;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
@ -121,6 +119,7 @@ public class TestingOIDCEndpointsApplicationResource {
|
||||||
@Path("/generate-keys")
|
@Path("/generate-keys")
|
||||||
@NoCache
|
@NoCache
|
||||||
public Map<String, String> generateKeys(@QueryParam("jwaAlgorithm") String jwaAlgorithm,
|
public Map<String, String> generateKeys(@QueryParam("jwaAlgorithm") String jwaAlgorithm,
|
||||||
|
@QueryParam("crv") String curve,
|
||||||
@QueryParam("advertiseJWKAlgorithm") Boolean advertiseJWKAlgorithm,
|
@QueryParam("advertiseJWKAlgorithm") Boolean advertiseJWKAlgorithm,
|
||||||
@QueryParam("keepExistingKeys") Boolean keepExistingKeys,
|
@QueryParam("keepExistingKeys") Boolean keepExistingKeys,
|
||||||
@QueryParam("kid") String kid) {
|
@QueryParam("kid") String kid) {
|
||||||
|
@ -152,6 +151,13 @@ public class TestingOIDCEndpointsApplicationResource {
|
||||||
keyType = KeyType.EC;
|
keyType = KeyType.EC;
|
||||||
keyPair = generateEcdsaKey("secp521r1");
|
keyPair = generateEcdsaKey("secp521r1");
|
||||||
break;
|
break;
|
||||||
|
case Algorithm.EdDSA:
|
||||||
|
if (curve == null) {
|
||||||
|
curve = Algorithm.Ed25519;
|
||||||
|
}
|
||||||
|
keyType = KeyType.OKP;
|
||||||
|
keyPair = generateEddsaKey(curve);
|
||||||
|
break;
|
||||||
case JWEConstants.RSA1_5:
|
case JWEConstants.RSA1_5:
|
||||||
case JWEConstants.RSA_OAEP:
|
case JWEConstants.RSA_OAEP:
|
||||||
case JWEConstants.RSA_OAEP_256:
|
case JWEConstants.RSA_OAEP_256:
|
||||||
|
@ -168,6 +174,7 @@ public class TestingOIDCEndpointsApplicationResource {
|
||||||
keyData.setKid(kid); // Can be null. It will be generated in that case
|
keyData.setKid(kid); // Can be null. It will be generated in that case
|
||||||
keyData.setKeyPair(keyPair);
|
keyData.setKeyPair(keyPair);
|
||||||
keyData.setKeyType(keyType);
|
keyData.setKeyType(keyType);
|
||||||
|
keyData.setCurve(curve);
|
||||||
if (advertiseJWKAlgorithm == null || Boolean.TRUE.equals(advertiseJWKAlgorithm)) {
|
if (advertiseJWKAlgorithm == null || Boolean.TRUE.equals(advertiseJWKAlgorithm)) {
|
||||||
keyData.setKeyAlgorithm(jwaAlgorithm);
|
keyData.setKeyAlgorithm(jwaAlgorithm);
|
||||||
} else {
|
} else {
|
||||||
|
@ -190,6 +197,12 @@ public class TestingOIDCEndpointsApplicationResource {
|
||||||
return keyPair;
|
return keyPair;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private KeyPair generateEddsaKey(String curveName) throws NoSuchAlgorithmException, InvalidAlgorithmParameterException {
|
||||||
|
KeyPairGenerator keyGen = KeyPairGenerator.getInstance(curveName);
|
||||||
|
KeyPair keyPair = keyGen.generateKeyPair();
|
||||||
|
return keyPair;
|
||||||
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
@Path("/get-keys-as-pem")
|
@Path("/get-keys-as-pem")
|
||||||
|
@ -238,6 +251,8 @@ public class TestingOIDCEndpointsApplicationResource {
|
||||||
return builder.rsa(keyPair.getPublic(), keyUse);
|
return builder.rsa(keyPair.getPublic(), keyUse);
|
||||||
} else if (KeyType.EC.equals(keyType)) {
|
} else if (KeyType.EC.equals(keyType)) {
|
||||||
return builder.ec(keyPair.getPublic());
|
return builder.ec(keyPair.getPublic());
|
||||||
|
} else if (KeyType.OKP.equals(keyType)) {
|
||||||
|
return builder.okp(keyPair.getPublic());
|
||||||
} else {
|
} else {
|
||||||
throw new IllegalArgumentException("Unknown keyType: " + keyType);
|
throw new IllegalArgumentException("Unknown keyType: " + keyType);
|
||||||
}
|
}
|
||||||
|
@ -326,6 +341,10 @@ public class TestingOIDCEndpointsApplicationResource {
|
||||||
case Algorithm.ES512:
|
case Algorithm.ES512:
|
||||||
signer = new ServerECDSASignatureSignerContext(keyWrapper);
|
signer = new ServerECDSASignatureSignerContext(keyWrapper);
|
||||||
break;
|
break;
|
||||||
|
case Algorithm.EdDSA:
|
||||||
|
keyWrapper.setCurve(keyData.getCurve());
|
||||||
|
signer = new ServerEdDSASignatureSignerContext(keyWrapper);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
signer = new AsymmetricSignatureSignerContext(keyWrapper);
|
signer = new AsymmetricSignatureSignerContext(keyWrapper);
|
||||||
}
|
}
|
||||||
|
@ -374,6 +393,7 @@ public class TestingOIDCEndpointsApplicationResource {
|
||||||
case Algorithm.ES256:
|
case Algorithm.ES256:
|
||||||
case Algorithm.ES384:
|
case Algorithm.ES384:
|
||||||
case Algorithm.ES512:
|
case Algorithm.ES512:
|
||||||
|
case Algorithm.EdDSA:
|
||||||
case Algorithm.HS256:
|
case Algorithm.HS256:
|
||||||
case Algorithm.HS384:
|
case Algorithm.HS384:
|
||||||
case Algorithm.HS512:
|
case Algorithm.HS512:
|
||||||
|
|
|
@ -46,10 +46,16 @@ public interface TestOIDCEndpointsApplicationResource {
|
||||||
@Path("/generate-keys")
|
@Path("/generate-keys")
|
||||||
Map<String, String> generateKeys(@QueryParam("jwaAlgorithm") String jwaAlgorithm);
|
Map<String, String> generateKeys(@QueryParam("jwaAlgorithm") String jwaAlgorithm);
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
@Path("/generate-keys")
|
||||||
|
Map<String, String> generateKeys(@QueryParam("jwaAlgorithm") String jwaAlgorithm, @QueryParam("crv") String curve);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate single private/public keyPair
|
* Generate single private/public keyPair
|
||||||
*
|
*
|
||||||
* @param jwaAlgorithm
|
* @param jwaAlgorithm
|
||||||
|
* @param curve The crv for EdDSA
|
||||||
* @param advertiseJWKAlgorithm whether algorithm should be adwertised in JWKS or not (Once the keys are returned by JWKS)
|
* @param advertiseJWKAlgorithm whether algorithm should be adwertised in JWKS or not (Once the keys are returned by JWKS)
|
||||||
* @param keepExistingKeys Should be existing keys kept replaced with newly generated keyPair. If it is not kept, then resulting JWK will contain single key. It is false by default.
|
* @param keepExistingKeys Should be existing keys kept replaced with newly generated keyPair. If it is not kept, then resulting JWK will contain single key. It is false by default.
|
||||||
* The value 'true' is useful if we want to test with multiple client keys (For example mulitple keys set in the JWKS and test if correct key is picked)
|
* The value 'true' is useful if we want to test with multiple client keys (For example mulitple keys set in the JWKS and test if correct key is picked)
|
||||||
|
@ -60,6 +66,7 @@ public interface TestOIDCEndpointsApplicationResource {
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
@Path("/generate-keys")
|
@Path("/generate-keys")
|
||||||
Map<String, String> generateKeys(@QueryParam("jwaAlgorithm") String jwaAlgorithm,
|
Map<String, String> generateKeys(@QueryParam("jwaAlgorithm") String jwaAlgorithm,
|
||||||
|
@QueryParam("crv") String curve,
|
||||||
@QueryParam("advertiseJWKAlgorithm") Boolean advertiseJWKAlgorithm,
|
@QueryParam("advertiseJWKAlgorithm") Boolean advertiseJWKAlgorithm,
|
||||||
@QueryParam("keepExistingKeys") Boolean keepExistingKeys,
|
@QueryParam("keepExistingKeys") Boolean keepExistingKeys,
|
||||||
@QueryParam("kid") String kid);
|
@QueryParam("kid") String kid);
|
||||||
|
|
|
@ -53,6 +53,7 @@ import org.keycloak.jose.jwk.JSONWebKeySet;
|
||||||
import org.keycloak.jose.jwk.JWK;
|
import org.keycloak.jose.jwk.JWK;
|
||||||
import org.keycloak.jose.jwk.JWKParser;
|
import org.keycloak.jose.jwk.JWKParser;
|
||||||
import org.keycloak.jose.jws.JWSInput;
|
import org.keycloak.jose.jws.JWSInput;
|
||||||
|
import org.keycloak.jose.jwk.OKPPublicJWK;
|
||||||
import org.keycloak.models.Constants;
|
import org.keycloak.models.Constants;
|
||||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||||
|
@ -1376,10 +1377,15 @@ public class OAuthClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
public SignatureSignerContext createSigner(PrivateKey privateKey, String kid, String algorithm) {
|
public SignatureSignerContext createSigner(PrivateKey privateKey, String kid, String algorithm) {
|
||||||
|
return createSigner(privateKey, kid, algorithm, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SignatureSignerContext createSigner(PrivateKey privateKey, String kid, String algorithm, String curve) {
|
||||||
KeyWrapper keyWrapper = new KeyWrapper();
|
KeyWrapper keyWrapper = new KeyWrapper();
|
||||||
keyWrapper.setAlgorithm(algorithm);
|
keyWrapper.setAlgorithm(algorithm);
|
||||||
keyWrapper.setKid(kid);
|
keyWrapper.setKid(kid);
|
||||||
keyWrapper.setPrivateKey(privateKey);
|
keyWrapper.setPrivateKey(privateKey);
|
||||||
|
keyWrapper.setCurve(curve);
|
||||||
SignatureSignerContext signer;
|
SignatureSignerContext signer;
|
||||||
switch (algorithm) {
|
switch (algorithm) {
|
||||||
case Algorithm.ES256:
|
case Algorithm.ES256:
|
||||||
|
@ -2199,6 +2205,9 @@ public class OAuthClient {
|
||||||
KeyWrapper key = new KeyWrapper();
|
KeyWrapper key = new KeyWrapper();
|
||||||
key.setKid(k.getKeyId());
|
key.setKid(k.getKeyId());
|
||||||
key.setAlgorithm(k.getAlgorithm());
|
key.setAlgorithm(k.getAlgorithm());
|
||||||
|
if (k.getOtherClaims().get(OKPPublicJWK.CRV) != null) {
|
||||||
|
key.setCurve((String) k.getOtherClaims().get(OKPPublicJWK.CRV));
|
||||||
|
}
|
||||||
key.setPublicKey(publicKey);
|
key.setPublicKey(publicKey);
|
||||||
key.setUse(KeyUse.SIG);
|
key.setUse(KeyUse.SIG);
|
||||||
|
|
||||||
|
|
|
@ -118,6 +118,10 @@ public class TokenSignatureUtil {
|
||||||
case Algorithm.ES512:
|
case Algorithm.ES512:
|
||||||
registerKeyProvider(realm, "ecdsaEllipticCurveKey", convertAlgorithmToECDomainParamNistRep(jwaAlgorithmName), GeneratedEcdsaKeyProviderFactory.ID, adminClient, testContext);
|
registerKeyProvider(realm, "ecdsaEllipticCurveKey", convertAlgorithmToECDomainParamNistRep(jwaAlgorithmName), GeneratedEcdsaKeyProviderFactory.ID, adminClient, testContext);
|
||||||
break;
|
break;
|
||||||
|
case Algorithm.Ed25519:
|
||||||
|
case Algorithm.Ed448:
|
||||||
|
registerKeyProvider(realm, "eddsaEllipticCurveKey", jwaAlgorithmName, "eddsa-generated", adminClient, testContext);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -71,8 +71,9 @@ public class ServerInfoTest extends AbstractKeycloakTest {
|
||||||
Assert.assertNames(info.getCryptoInfo().getClientSignatureSymmetricAlgorithms(), Algorithm.HS256, Algorithm.HS384, Algorithm.HS512);
|
Assert.assertNames(info.getCryptoInfo().getClientSignatureSymmetricAlgorithms(), Algorithm.HS256, Algorithm.HS384, Algorithm.HS512);
|
||||||
Assert.assertNames(info.getCryptoInfo().getClientSignatureAsymmetricAlgorithms(),
|
Assert.assertNames(info.getCryptoInfo().getClientSignatureAsymmetricAlgorithms(),
|
||||||
Algorithm.ES256, Algorithm.ES384, Algorithm.ES512,
|
Algorithm.ES256, Algorithm.ES384, Algorithm.ES512,
|
||||||
Algorithm.PS256, Algorithm.PS384, Algorithm.PS512,
|
Algorithm.EdDSA, Algorithm.PS256, Algorithm.PS384,
|
||||||
Algorithm.RS256, Algorithm.RS384, Algorithm.RS512);
|
Algorithm.PS512, Algorithm.RS256, Algorithm.RS384,
|
||||||
|
Algorithm.RS512);
|
||||||
|
|
||||||
ComponentTypeRepresentation rsaGeneratedProviderInfo = info.getComponentTypes().get(KeyProvider.class.getName())
|
ComponentTypeRepresentation rsaGeneratedProviderInfo = info.getComponentTypes().get(KeyProvider.class.getName())
|
||||||
.stream()
|
.stream()
|
||||||
|
|
|
@ -1481,6 +1481,16 @@ public class CIBATest extends AbstractClientPoliciesTest {
|
||||||
testBackchannelAuthenticationFlowWithSignedAuthenticationRequest(true, Algorithm.ES256);
|
testBackchannelAuthenticationFlowWithSignedAuthenticationRequest(true, Algorithm.ES256);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testBackchannelAuthenticationFlowWithSignedAuthenticationRequestEd25519Param() throws Exception {
|
||||||
|
testBackchannelAuthenticationFlowWithSignedAuthenticationRequest(false, Algorithm.EdDSA, Algorithm.Ed25519);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testBackchannelAuthenticationFlowWithSignedAuthenticationRequestEd448UriParam() throws Exception {
|
||||||
|
testBackchannelAuthenticationFlowWithSignedAuthenticationRequest(true, Algorithm.EdDSA, Algorithm.Ed448);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testBackchannelAuthenticationFlowWithInvalidSignedAuthenticationRequestUriParam() throws Exception {
|
public void testBackchannelAuthenticationFlowWithInvalidSignedAuthenticationRequestUriParam() throws Exception {
|
||||||
testBackchannelAuthenticationFlowWithInvalidSignedAuthenticationRequest(true, "none", 400, "None signed algorithm is not allowed");
|
testBackchannelAuthenticationFlowWithInvalidSignedAuthenticationRequest(true, "none", 400, "None signed algorithm is not allowed");
|
||||||
|
@ -2455,7 +2465,7 @@ public class CIBATest extends AbstractClientPoliciesTest {
|
||||||
AuthorizationEndpointRequestObject sharedAuthenticationRequest = createValidSharedAuthenticationRequest();
|
AuthorizationEndpointRequestObject sharedAuthenticationRequest = createValidSharedAuthenticationRequest();
|
||||||
sharedAuthenticationRequest.setLoginHint(username);
|
sharedAuthenticationRequest.setLoginHint(username);
|
||||||
sharedAuthenticationRequest.setBindingMessage(bindingMessage);
|
sharedAuthenticationRequest.setBindingMessage(bindingMessage);
|
||||||
registerSharedAuthenticationRequest(sharedAuthenticationRequest, clientId, requestedSigAlg, sigAlg, useRequestUri, clientSecret);
|
registerSharedAuthenticationRequest(sharedAuthenticationRequest, clientId, requestedSigAlg, sigAlg, useRequestUri, clientSecret, null);
|
||||||
|
|
||||||
// user Backchannel Authentication Request
|
// user Backchannel Authentication Request
|
||||||
AuthenticationRequestAcknowledgement response = oauth.doBackchannelAuthenticationRequest(clientId, clientSecret, null, null, null);
|
AuthenticationRequestAcknowledgement response = oauth.doBackchannelAuthenticationRequest(clientId, clientSecret, null, null, null);
|
||||||
|
@ -2498,6 +2508,10 @@ public class CIBATest extends AbstractClientPoliciesTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void testBackchannelAuthenticationFlowWithSignedAuthenticationRequest(boolean useRequestUri, String sigAlg) throws Exception {
|
private void testBackchannelAuthenticationFlowWithSignedAuthenticationRequest(boolean useRequestUri, String sigAlg) throws Exception {
|
||||||
|
testBackchannelAuthenticationFlowWithSignedAuthenticationRequest(useRequestUri, sigAlg, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void testBackchannelAuthenticationFlowWithSignedAuthenticationRequest(boolean useRequestUri, String sigAlg, String curve) throws Exception {
|
||||||
ClientResource clientResource = null;
|
ClientResource clientResource = null;
|
||||||
ClientRepresentation clientRep = null;
|
ClientRepresentation clientRep = null;
|
||||||
try {
|
try {
|
||||||
|
@ -2512,7 +2526,7 @@ public class CIBATest extends AbstractClientPoliciesTest {
|
||||||
AuthorizationEndpointRequestObject sharedAuthenticationRequest = createValidSharedAuthenticationRequest();
|
AuthorizationEndpointRequestObject sharedAuthenticationRequest = createValidSharedAuthenticationRequest();
|
||||||
sharedAuthenticationRequest.setLoginHint(username);
|
sharedAuthenticationRequest.setLoginHint(username);
|
||||||
sharedAuthenticationRequest.setBindingMessage(bindingMessage);
|
sharedAuthenticationRequest.setBindingMessage(bindingMessage);
|
||||||
registerSharedAuthenticationRequest(sharedAuthenticationRequest, TEST_CLIENT_NAME, sigAlg, useRequestUri);
|
registerSharedAuthenticationRequest(sharedAuthenticationRequest, TEST_CLIENT_NAME, sigAlg, sigAlg, useRequestUri, null, curve);
|
||||||
|
|
||||||
// user Backchannel Authentication Request
|
// user Backchannel Authentication Request
|
||||||
AuthenticationRequestAcknowledgement response = doBackchannelAuthenticationRequest(TEST_CLIENT_NAME, TEST_CLIENT_PASSWORD, null, null);
|
AuthenticationRequestAcknowledgement response = doBackchannelAuthenticationRequest(TEST_CLIENT_NAME, TEST_CLIENT_PASSWORD, null, null);
|
||||||
|
@ -2567,7 +2581,7 @@ public class CIBATest extends AbstractClientPoliciesTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void registerSharedAuthenticationRequest(AuthorizationEndpointRequestObject requestObject, String clientId, String sigAlg, boolean isUseRequestUri, String clientSecret) throws URISyntaxException, IOException {
|
protected void registerSharedAuthenticationRequest(AuthorizationEndpointRequestObject requestObject, String clientId, String sigAlg, boolean isUseRequestUri, String clientSecret) throws URISyntaxException, IOException {
|
||||||
registerSharedAuthenticationRequest(requestObject, clientId, sigAlg, sigAlg, isUseRequestUri, clientSecret);
|
registerSharedAuthenticationRequest(requestObject, clientId, sigAlg, sigAlg, isUseRequestUri, clientSecret, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isSymmetricSigAlg(String sigAlg) {
|
private boolean isSymmetricSigAlg(String sigAlg) {
|
||||||
|
@ -2577,7 +2591,8 @@ public class CIBATest extends AbstractClientPoliciesTest {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void registerSharedAuthenticationRequest(AuthorizationEndpointRequestObject requestObject, String clientId, String requestedSigAlg, String sigAlg, boolean isUseRequestUri, String clientSecret) throws URISyntaxException, IOException {
|
protected void registerSharedAuthenticationRequest(AuthorizationEndpointRequestObject requestObject, String clientId,
|
||||||
|
String requestedSigAlg, String sigAlg, boolean isUseRequestUri, String clientSecret, String curve) throws URISyntaxException, IOException {
|
||||||
TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints();
|
TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints();
|
||||||
|
|
||||||
// Set required signature for request_uri
|
// Set required signature for request_uri
|
||||||
|
@ -2603,7 +2618,7 @@ public class CIBATest extends AbstractClientPoliciesTest {
|
||||||
oidcClientEndpointsResource.registerOIDCRequestSymmetricSig(encodedRequestObject, sigAlg, clientSecret);
|
oidcClientEndpointsResource.registerOIDCRequestSymmetricSig(encodedRequestObject, sigAlg, clientSecret);
|
||||||
} else {
|
} else {
|
||||||
// generate and register client keypair
|
// generate and register client keypair
|
||||||
if (!"none".equals(sigAlg)) oidcClientEndpointsResource.generateKeys(sigAlg);
|
if (!"none".equals(sigAlg)) oidcClientEndpointsResource.generateKeys(sigAlg, curve);
|
||||||
|
|
||||||
oidcClientEndpointsResource.registerOIDCRequest(encodedRequestObject, sigAlg);
|
oidcClientEndpointsResource.registerOIDCRequest(encodedRequestObject, sigAlg);
|
||||||
}
|
}
|
||||||
|
|
|
@ -474,6 +474,10 @@ public abstract class AbstractClientPoliciesTest extends AbstractKeycloakTest {
|
||||||
case Algorithm.ES512:
|
case Algorithm.ES512:
|
||||||
keyAlg = KeyType.EC;
|
keyAlg = KeyType.EC;
|
||||||
break;
|
break;
|
||||||
|
case Algorithm.Ed25519:
|
||||||
|
case Algorithm.Ed448:
|
||||||
|
keyAlg = KeyType.OKP;
|
||||||
|
break;
|
||||||
default :
|
default :
|
||||||
throw new RuntimeException("Unsupported signature algorithm");
|
throw new RuntimeException("Unsupported signature algorithm");
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,966 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.testsuite.oauth;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertNotEquals;
|
||||||
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.security.KeyFactory;
|
||||||
|
import java.security.KeyPair;
|
||||||
|
import java.security.KeyStore;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.NoSuchProviderException;
|
||||||
|
import java.security.PrivateKey;
|
||||||
|
import java.security.PublicKey;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.security.spec.InvalidKeySpecException;
|
||||||
|
import java.security.spec.PKCS8EncodedKeySpec;
|
||||||
|
import java.security.spec.X509EncodedKeySpec;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import jakarta.ws.rs.core.Response;
|
||||||
|
|
||||||
|
import org.apache.http.HttpEntity;
|
||||||
|
import org.apache.http.HttpHeaders;
|
||||||
|
import org.apache.http.HttpResponse;
|
||||||
|
import org.apache.http.NameValuePair;
|
||||||
|
import org.apache.http.client.HttpClient;
|
||||||
|
import org.apache.http.client.entity.UrlEncodedFormEntity;
|
||||||
|
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||||
|
import org.apache.http.client.methods.HttpPost;
|
||||||
|
import org.apache.http.entity.mime.MultipartEntityBuilder;
|
||||||
|
import org.apache.http.entity.mime.content.FileBody;
|
||||||
|
import org.apache.http.impl.client.CloseableHttpClient;
|
||||||
|
import org.apache.http.impl.client.DefaultHttpClient;
|
||||||
|
import org.apache.http.impl.client.HttpClients;
|
||||||
|
import org.apache.http.message.BasicNameValuePair;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.ClassRule;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.rules.TemporaryFolder;
|
||||||
|
import org.keycloak.OAuth2Constants;
|
||||||
|
import org.keycloak.OAuthErrorException;
|
||||||
|
import org.keycloak.adapters.AdapterUtils;
|
||||||
|
import org.keycloak.admin.client.resource.ClientAttributeCertificateResource;
|
||||||
|
import org.keycloak.admin.client.resource.ClientResource;
|
||||||
|
import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator;
|
||||||
|
import org.keycloak.common.constants.ServiceAccountConstants;
|
||||||
|
import org.keycloak.common.crypto.CryptoIntegration;
|
||||||
|
import org.keycloak.common.util.Base64;
|
||||||
|
import org.keycloak.common.util.Base64Url;
|
||||||
|
import org.keycloak.common.util.KeyUtils;
|
||||||
|
import org.keycloak.common.util.KeycloakUriBuilder;
|
||||||
|
import org.keycloak.common.util.KeystoreUtil;
|
||||||
|
import org.keycloak.common.util.PemUtils;
|
||||||
|
import org.keycloak.common.util.Time;
|
||||||
|
import org.keycloak.common.util.UriUtils;
|
||||||
|
import org.keycloak.common.util.KeystoreUtil.KeystoreFormat;
|
||||||
|
import org.keycloak.constants.ServiceUrlConstants;
|
||||||
|
import org.keycloak.crypto.Algorithm;
|
||||||
|
import org.keycloak.crypto.ECDSAAlgorithm;
|
||||||
|
import org.keycloak.crypto.ECDSASignatureProvider;
|
||||||
|
import org.keycloak.crypto.KeyType;
|
||||||
|
import org.keycloak.crypto.SignatureSignerContext;
|
||||||
|
import org.keycloak.events.Details;
|
||||||
|
import org.keycloak.events.EventType;
|
||||||
|
import org.keycloak.jose.jwk.JSONWebKeySet;
|
||||||
|
import org.keycloak.jose.jws.JWSBuilder;
|
||||||
|
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||||
|
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
|
||||||
|
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
|
||||||
|
import org.keycloak.protocol.oidc.client.authentication.JWTClientCredentialsProvider;
|
||||||
|
import org.keycloak.representations.AccessToken;
|
||||||
|
import org.keycloak.representations.JsonWebToken;
|
||||||
|
import org.keycloak.representations.KeyStoreConfig;
|
||||||
|
import org.keycloak.representations.RefreshToken;
|
||||||
|
import org.keycloak.representations.idm.ClientRepresentation;
|
||||||
|
import org.keycloak.representations.idm.EventRepresentation;
|
||||||
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
|
import org.keycloak.representations.idm.UserRepresentation;
|
||||||
|
import org.keycloak.services.util.CertificateInfoHelper;
|
||||||
|
import org.keycloak.testsuite.AbstractKeycloakTest;
|
||||||
|
import org.keycloak.testsuite.Assert;
|
||||||
|
import org.keycloak.testsuite.AssertEvents;
|
||||||
|
import org.keycloak.testsuite.admin.ApiUtil;
|
||||||
|
import org.keycloak.testsuite.auth.page.AuthRealm;
|
||||||
|
import org.keycloak.testsuite.client.resources.TestApplicationResourceUrls;
|
||||||
|
import org.keycloak.testsuite.client.resources.TestOIDCEndpointsApplicationResource;
|
||||||
|
import org.keycloak.testsuite.rest.resource.TestingOIDCEndpointsApplicationResource;
|
||||||
|
import org.keycloak.testsuite.util.ClientBuilder;
|
||||||
|
import org.keycloak.testsuite.util.ClientManager;
|
||||||
|
import org.keycloak.testsuite.util.KeystoreUtils;
|
||||||
|
import org.keycloak.testsuite.util.OAuthClient;
|
||||||
|
import org.keycloak.testsuite.util.RealmBuilder;
|
||||||
|
import org.keycloak.testsuite.util.UserBuilder;
|
||||||
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
|
||||||
|
public abstract class AbstractClientAuthSignedJWTTest extends AbstractKeycloakTest {
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public AssertEvents events = new AssertEvents(this);
|
||||||
|
|
||||||
|
@ClassRule
|
||||||
|
public static TemporaryFolder folder = new TemporaryFolder();
|
||||||
|
|
||||||
|
protected static KeystoreUtils.KeystoreInfo generatedKeystoreClient1;
|
||||||
|
protected static KeyPair keyPairClient1;
|
||||||
|
|
||||||
|
@BeforeClass
|
||||||
|
public static void generateClient1KeyPair() throws Exception {
|
||||||
|
generatedKeystoreClient1 = KeystoreUtils.generateKeystore(folder, KeystoreFormat.JKS, "clientkey", "storepass", "keypass");
|
||||||
|
PublicKey publicKey = PemUtils.decodePublicKey(generatedKeystoreClient1.getCertificateInfo().getPublicKey());
|
||||||
|
PrivateKey privateKey = PemUtils.decodePrivateKey(generatedKeystoreClient1.getCertificateInfo().getPrivateKey());
|
||||||
|
keyPairClient1 = new KeyPair(publicKey, privateKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static String client1SAUserId;
|
||||||
|
|
||||||
|
protected static RealmRepresentation testRealm;
|
||||||
|
protected static ClientRepresentation app1, app2, app3;
|
||||||
|
protected static UserRepresentation defaultUser, serviceAccountUser;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void beforeAbstractKeycloakTest() throws Exception {
|
||||||
|
super.beforeAbstractKeycloakTest();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addTestRealms(List<RealmRepresentation> testRealms) {
|
||||||
|
RealmBuilder realmBuilder = RealmBuilder.create().name("test")
|
||||||
|
.testEventListener();
|
||||||
|
|
||||||
|
app1 = ClientBuilder.create()
|
||||||
|
.id(KeycloakModelUtils.generateId())
|
||||||
|
.clientId("client1")
|
||||||
|
.attribute(JWTClientAuthenticator.CERTIFICATE_ATTR, generatedKeystoreClient1.getCertificateInfo().getCertificate())
|
||||||
|
.attribute(OIDCConfigAttributes.USE_REFRESH_TOKEN_FOR_CLIENT_CREDENTIALS_GRANT, "true")
|
||||||
|
.authenticatorType(JWTClientAuthenticator.PROVIDER_ID)
|
||||||
|
.serviceAccountsEnabled(true)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
realmBuilder.client(app1);
|
||||||
|
|
||||||
|
app2 = ClientBuilder.create()
|
||||||
|
.id(KeycloakModelUtils.generateId())
|
||||||
|
.clientId("client2")
|
||||||
|
.directAccessGrants()
|
||||||
|
.serviceAccountsEnabled(true)
|
||||||
|
.redirectUris(OAuthClient.APP_ROOT + "/auth")
|
||||||
|
.attribute(JWTClientAuthenticator.CERTIFICATE_ATTR, "MIICnTCCAYUCBgFPPQDGxTANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdjbGllbnQxMB4XDTE1MDgxNzE4NTAwNVoXDTI1MDgxNzE4NTE0NVowEjEQMA4GA1UEAwwHY2xpZW50MTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMMw3PaBffWxgS2PYSDDBp6As+cNvv9kt2C4f/RDAGmvSIHPFev9kuQiKs3Oaws3ZsV4JG3qHEuYgnh9W4vfe3DwNwtD1bjL5FYBhPBFTw0lAQECYxaBHnkjHwUKp957FqdSPPICm3LjmTcEdlH+9dpp9xHCMbbiNiWDzWI1xSxC8Fs2d0hwz1sd+Q4QeTBPIBWcPM+ICZtNG5MN+ORfayu4X+Me5d0tXG2fQO//rAevk1i5IFjKZuOjTwyKB5SJIY4b8QTeg0g/50IU7Ht00Pxw6CK02dHS+FvXHasZlD3ckomqCDjStTBWdhJo5dST0CbOqalkkpLlCCbGA1yEQRsCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAUIMeJ+EAo8eNpCG/nXImacjrKakbFnZYBGD/gqeTGaZynkX+jgBSructTHR83zSH+yELEhsAy+3BfK4EEihp+PEcRnK2fASVkHste8AQ7rlzC+HGGirlwrVhWCdizNUCGK80DE537IZ7nmZw6LFG9P5/Q2MvCsOCYjRUvMkukq6TdXBXR9tETwZ+0gpSfsOxjj0ZF7ftTRUSzx4rFfcbM9fRNdVizdOuKGc8HJPA5lLOxV6CyaYIvi3y5RlQI1OHeS34lE4w9CNPRFa/vdxXvN7ClyzA0HMFNWxBN7pC/Ht/FbhSvaAagJBHg+vCrcY5C26Oli7lAglf/zZrwUPs0w==")
|
||||||
|
.authenticatorType(JWTClientAuthenticator.PROVIDER_ID)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
realmBuilder.client(app2);
|
||||||
|
|
||||||
|
defaultUser = UserBuilder.create()
|
||||||
|
.id(KeycloakModelUtils.generateId())
|
||||||
|
//.serviceAccountId(app1.getClientId())
|
||||||
|
.username("test-user@localhost")
|
||||||
|
.password("password")
|
||||||
|
.build();
|
||||||
|
realmBuilder.user(defaultUser);
|
||||||
|
|
||||||
|
client1SAUserId = KeycloakModelUtils.generateId();
|
||||||
|
|
||||||
|
serviceAccountUser = UserBuilder.create()
|
||||||
|
.id(client1SAUserId)
|
||||||
|
.username(ServiceAccountConstants.SERVICE_ACCOUNT_USER_PREFIX + app1.getClientId())
|
||||||
|
.serviceAccountId(app1.getClientId())
|
||||||
|
.build();
|
||||||
|
realmBuilder.user(serviceAccountUser);
|
||||||
|
|
||||||
|
testRealm = realmBuilder.build();
|
||||||
|
testRealms.add(testRealm);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void recreateApp3() {
|
||||||
|
app3 = ClientBuilder.create()
|
||||||
|
.id(KeycloakModelUtils.generateId())
|
||||||
|
.clientId("client3")
|
||||||
|
.directAccessGrants()
|
||||||
|
.authenticatorType(JWTClientAuthenticator.PROVIDER_ID)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Response resp = adminClient.realm("test").clients().create(app3);
|
||||||
|
getCleanup().addClientUuid(ApiUtil.getCreatedId(resp));
|
||||||
|
resp.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testCodeToTokenRequestSuccess(String algorithm) throws Exception {
|
||||||
|
oauth.clientId("client2");
|
||||||
|
oauth.doLogin("test-user@localhost", "password");
|
||||||
|
EventRepresentation loginEvent = events.expectLogin()
|
||||||
|
.client("client2")
|
||||||
|
.assertEvent();
|
||||||
|
|
||||||
|
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
|
||||||
|
OAuthClient.AccessTokenResponse response = doAccessTokenRequest(code, getClient2SignedJWT(algorithm));
|
||||||
|
|
||||||
|
assertEquals(200, response.getStatusCode());
|
||||||
|
oauth.verifyToken(response.getAccessToken());
|
||||||
|
oauth.parseRefreshToken(response.getRefreshToken());
|
||||||
|
events.expectCodeToToken(loginEvent.getDetails().get(Details.CODE_ID), loginEvent.getSessionId())
|
||||||
|
.client("client2")
|
||||||
|
.detail(Details.CLIENT_AUTH_METHOD, JWTClientAuthenticator.PROVIDER_ID)
|
||||||
|
.assertEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testCodeToTokenRequestSuccessForceAlgInClient(String algorithm) throws Exception {
|
||||||
|
ClientManager.realm(adminClient.realm("test")).clientId("client2")
|
||||||
|
.updateAttribute(OIDCConfigAttributes.TOKEN_ENDPOINT_AUTH_SIGNING_ALG, algorithm);
|
||||||
|
try {
|
||||||
|
testCodeToTokenRequestSuccess(algorithm);
|
||||||
|
} finally {
|
||||||
|
ClientManager.realm(adminClient.realm("test")).clientId("client2")
|
||||||
|
.updateAttribute(OIDCConfigAttributes.TOKEN_ENDPOINT_AUTH_SIGNING_ALG, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void testECDSASignatureLength(String clientSignedToken, String alg) {
|
||||||
|
String encodedSignature = clientSignedToken.split("\\.",3)[2];
|
||||||
|
byte[] signature = Base64Url.decode(encodedSignature);
|
||||||
|
assertEquals(ECDSAAlgorithm.getSignatureLength(alg), signature.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String getClientSignedToken(String alg) throws Exception {
|
||||||
|
ClientRepresentation clientRepresentation = app2;
|
||||||
|
ClientResource clientResource = getClient(testRealm.getRealm(), clientRepresentation.getId());
|
||||||
|
clientRepresentation = clientResource.toRepresentation();
|
||||||
|
String clientSignedToken;
|
||||||
|
try {
|
||||||
|
// setup Jwks
|
||||||
|
KeyPair keyPair = setupJwksUrl(alg, clientRepresentation, clientResource);
|
||||||
|
PublicKey publicKey = keyPair.getPublic();
|
||||||
|
PrivateKey privateKey = keyPair.getPrivate();
|
||||||
|
|
||||||
|
// test
|
||||||
|
oauth.clientId("client2");
|
||||||
|
oauth.doLogin("test-user@localhost", "password");
|
||||||
|
|
||||||
|
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
|
||||||
|
clientSignedToken = createSignedRequestToken("client2", getRealmInfoUrl(), privateKey, publicKey, alg);
|
||||||
|
OAuthClient.AccessTokenResponse response = doAccessTokenRequest(code, clientSignedToken);
|
||||||
|
|
||||||
|
assertEquals(200, response.getStatusCode());
|
||||||
|
oauth.verifyToken(response.getAccessToken());
|
||||||
|
oauth.idTokenHint(response.getIdToken()).openLogout();
|
||||||
|
return clientSignedToken;
|
||||||
|
} finally {
|
||||||
|
// Revert jwks_url settings
|
||||||
|
revertJwksUriSettings(clientRepresentation, clientResource);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void testCodeToTokenRequestSuccess(String algorithm, boolean useJwksUri) throws Exception {
|
||||||
|
testCodeToTokenRequestSuccess(algorithm, null, useJwksUri);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void testCodeToTokenRequestSuccess(String algorithm, String curve, boolean useJwksUri) throws Exception {
|
||||||
|
ClientRepresentation clientRepresentation = app2;
|
||||||
|
ClientResource clientResource = getClient(testRealm.getRealm(), clientRepresentation.getId());
|
||||||
|
clientRepresentation = clientResource.toRepresentation();
|
||||||
|
try {
|
||||||
|
// setup Jwks
|
||||||
|
KeyPair keyPair;
|
||||||
|
if (useJwksUri) {
|
||||||
|
keyPair = setupJwksUrl(algorithm, curve, true, false, null, clientRepresentation, clientResource);
|
||||||
|
} else {
|
||||||
|
keyPair = setupJwks(algorithm, curve, clientRepresentation, clientResource);
|
||||||
|
}
|
||||||
|
PublicKey publicKey = keyPair.getPublic();
|
||||||
|
PrivateKey privateKey = keyPair.getPrivate();
|
||||||
|
|
||||||
|
// test
|
||||||
|
oauth.clientId("client2");
|
||||||
|
oauth.doLogin("test-user@localhost", "password");
|
||||||
|
EventRepresentation loginEvent = events.expectLogin()
|
||||||
|
.client("client2")
|
||||||
|
.assertEvent();
|
||||||
|
|
||||||
|
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
|
||||||
|
OAuthClient.AccessTokenResponse response = doAccessTokenRequest(code,
|
||||||
|
createSignedRequestToken("client2", getRealmInfoUrl(), privateKey, publicKey, algorithm, curve));
|
||||||
|
|
||||||
|
assertEquals(200, response.getStatusCode());
|
||||||
|
oauth.verifyToken(response.getAccessToken());
|
||||||
|
oauth.parseRefreshToken(response.getRefreshToken());
|
||||||
|
events.expectCodeToToken(loginEvent.getDetails().get(Details.CODE_ID), loginEvent.getSessionId())
|
||||||
|
.client("client2")
|
||||||
|
.detail(Details.CLIENT_AUTH_METHOD, JWTClientAuthenticator.PROVIDER_ID)
|
||||||
|
.assertEvent();
|
||||||
|
} finally {
|
||||||
|
// Revert jwks settings
|
||||||
|
if (useJwksUri) {
|
||||||
|
revertJwksUriSettings(clientRepresentation, clientResource);
|
||||||
|
} else {
|
||||||
|
revertJwksSettings(clientRepresentation, clientResource);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void testDirectGrantRequestSuccess(String algorithm) throws Exception {
|
||||||
|
ClientRepresentation clientRepresentation = app2;
|
||||||
|
ClientResource clientResource = getClient(testRealm.getRealm(), clientRepresentation.getId());
|
||||||
|
clientRepresentation = clientResource.toRepresentation();
|
||||||
|
try {
|
||||||
|
// setup Jwks
|
||||||
|
KeyPair keyPair = setupJwksUrl(algorithm, clientRepresentation, clientResource);
|
||||||
|
PublicKey publicKey = keyPair.getPublic();
|
||||||
|
PrivateKey privateKey = keyPair.getPrivate();
|
||||||
|
|
||||||
|
// test
|
||||||
|
oauth.clientId("client2");
|
||||||
|
OAuthClient.AccessTokenResponse response = doGrantAccessTokenRequest("test-user@localhost", "password", createSignedRequestToken("client2", getRealmInfoUrl(), privateKey, publicKey, algorithm));
|
||||||
|
|
||||||
|
assertEquals(200, response.getStatusCode());
|
||||||
|
AccessToken accessToken = oauth.verifyToken(response.getAccessToken());
|
||||||
|
RefreshToken refreshToken = oauth.parseRefreshToken(response.getRefreshToken());
|
||||||
|
|
||||||
|
events.expectLogin()
|
||||||
|
.client("client2")
|
||||||
|
.session(accessToken.getSessionState())
|
||||||
|
.detail(Details.GRANT_TYPE, OAuth2Constants.PASSWORD)
|
||||||
|
.detail(Details.TOKEN_ID, accessToken.getId())
|
||||||
|
.detail(Details.REFRESH_TOKEN_ID, refreshToken.getId())
|
||||||
|
.detail(Details.USERNAME, "test-user@localhost")
|
||||||
|
.detail(Details.CLIENT_AUTH_METHOD, JWTClientAuthenticator.PROVIDER_ID)
|
||||||
|
.removeDetail(Details.CODE_ID)
|
||||||
|
.removeDetail(Details.REDIRECT_URI)
|
||||||
|
.removeDetail(Details.CONSENT)
|
||||||
|
.assertEvent();
|
||||||
|
} finally {
|
||||||
|
// Revert jwks_url settings
|
||||||
|
revertJwksUriSettings(clientRepresentation, clientResource);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void testClientWithGeneratedKeys(String format) throws Exception {
|
||||||
|
ClientRepresentation client = app3;
|
||||||
|
UserRepresentation user = defaultUser;
|
||||||
|
final String keyAlias = "somekey";
|
||||||
|
final String keyPassword = "pwd1";
|
||||||
|
final String storePassword = "pwd2";
|
||||||
|
|
||||||
|
|
||||||
|
// Generate new keystore (which is intended for sending to the user and store in a client app)
|
||||||
|
// with public/private keys; in KC, store the certificate itself
|
||||||
|
|
||||||
|
KeyStoreConfig keyStoreConfig = new KeyStoreConfig();
|
||||||
|
keyStoreConfig.setFormat(format);
|
||||||
|
keyStoreConfig.setKeyPassword(keyPassword);
|
||||||
|
keyStoreConfig.setStorePassword(storePassword);
|
||||||
|
keyStoreConfig.setKeyAlias(keyAlias);
|
||||||
|
|
||||||
|
client = getClient(testRealm.getRealm(), client.getId()).toRepresentation();
|
||||||
|
final String certOld = client.getAttributes().get(JWTClientAuthenticator.CERTIFICATE_ATTR);
|
||||||
|
|
||||||
|
// Generate the keystore and save the new certificate in client (in KC)
|
||||||
|
byte[] keyStoreBytes = getClientAttributeCertificateResource(testRealm.getRealm(), client.getId())
|
||||||
|
.generateAndGetKeystore(keyStoreConfig);
|
||||||
|
|
||||||
|
ByteArrayInputStream keyStoreIs = new ByteArrayInputStream(keyStoreBytes);
|
||||||
|
KeyStore keyStore = getKeystore(keyStoreIs, storePassword, format);
|
||||||
|
keyStoreIs.close();
|
||||||
|
|
||||||
|
client = getClient(testRealm.getRealm(), client.getId()).toRepresentation();
|
||||||
|
X509Certificate x509Cert = (X509Certificate) keyStore.getCertificate(keyAlias);
|
||||||
|
|
||||||
|
assertCertificate(client, certOld,
|
||||||
|
KeycloakModelUtils.getPemFromCertificate(x509Cert));
|
||||||
|
|
||||||
|
|
||||||
|
// Try to login with the new keys
|
||||||
|
|
||||||
|
oauth.clientId(client.getClientId());
|
||||||
|
PrivateKey privateKey = (PrivateKey) keyStore.getKey(keyAlias, keyPassword.toCharArray());
|
||||||
|
KeyPair keyPair = new KeyPair(x509Cert.getPublicKey(), privateKey);
|
||||||
|
|
||||||
|
OAuthClient.AccessTokenResponse response = doGrantAccessTokenRequest(user.getUsername(),
|
||||||
|
user.getCredentials().get(0).getValue(),
|
||||||
|
getClientSignedJWT(keyPair, client.getClientId()));
|
||||||
|
|
||||||
|
assertEquals(200, response.getStatusCode());
|
||||||
|
|
||||||
|
AccessToken accessToken = oauth.verifyToken(response.getAccessToken());
|
||||||
|
RefreshToken refreshToken = oauth.parseRefreshToken(response.getRefreshToken());
|
||||||
|
|
||||||
|
events.expectLogin()
|
||||||
|
.client(client.getClientId())
|
||||||
|
.session(accessToken.getSessionState())
|
||||||
|
.detail(Details.GRANT_TYPE, OAuth2Constants.PASSWORD)
|
||||||
|
.detail(Details.TOKEN_ID, accessToken.getId())
|
||||||
|
.detail(Details.REFRESH_TOKEN_ID, refreshToken.getId())
|
||||||
|
.detail(Details.USERNAME, user.getUsername())
|
||||||
|
.detail(Details.CLIENT_AUTH_METHOD, JWTClientAuthenticator.PROVIDER_ID)
|
||||||
|
.removeDetail(Details.CODE_ID)
|
||||||
|
.removeDetail(Details.REDIRECT_URI)
|
||||||
|
.removeDetail(Details.CONSENT)
|
||||||
|
.assertEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// We need to test this as a genuine REST API HTTP request
|
||||||
|
// since there's no easy and direct way to call ClientAttributeCertificateResource.uploadJksCertificate
|
||||||
|
// (and especially to create MultipartFormDataInput)
|
||||||
|
protected void testUploadKeystore(String keystoreFormat, String filePath, String keyAlias, String storePassword) throws Exception {
|
||||||
|
ClientRepresentation client = getClient(testRealm.getRealm(), app3.getId()).toRepresentation();
|
||||||
|
final String certOld = client.getAttributes().get(JWTClientAuthenticator.CERTIFICATE_ATTR);
|
||||||
|
|
||||||
|
// Load the keystore file
|
||||||
|
URL fileUrl = (getClass().getClassLoader().getResource(filePath));
|
||||||
|
File keystoreFile = fileUrl != null ? new File(fileUrl.getFile()) : new File(filePath);
|
||||||
|
if (!keystoreFile.exists()) {
|
||||||
|
throw new IOException("File not found: " + keystoreFile.getAbsolutePath());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get admin access token, no matter it's master realm's admin
|
||||||
|
OAuthClient.AccessTokenResponse accessTokenResponse = oauth.doGrantAccessTokenRequest(
|
||||||
|
AuthRealm.MASTER, AuthRealm.ADMIN, AuthRealm.ADMIN, null, "admin-cli", null);
|
||||||
|
assertEquals(200, accessTokenResponse.getStatusCode());
|
||||||
|
|
||||||
|
final String url = suiteContext.getAuthServerInfo().getContextRoot()
|
||||||
|
+ "/auth/admin/realms/" + testRealm.getRealm()
|
||||||
|
+ "/clients/" + client.getId() + "/certificates/jwt.credential/upload-certificate";
|
||||||
|
|
||||||
|
// Prepare the HTTP request
|
||||||
|
FileBody fileBody = new FileBody(keystoreFile);
|
||||||
|
HttpEntity entity = MultipartEntityBuilder.create()
|
||||||
|
.addPart("file", fileBody)
|
||||||
|
.addTextBody("keystoreFormat", keystoreFormat)
|
||||||
|
.addTextBody("keyAlias", keyAlias)
|
||||||
|
.addTextBody("storePassword", storePassword)
|
||||||
|
.addTextBody("keyPassword", "undefined")
|
||||||
|
.build();
|
||||||
|
HttpPost httpRequest = new HttpPost(url);
|
||||||
|
httpRequest.setHeader(HttpHeaders.AUTHORIZATION, "Bearer " + accessTokenResponse.getAccessToken());
|
||||||
|
httpRequest.setEntity(entity);
|
||||||
|
|
||||||
|
// Send the request
|
||||||
|
HttpClient httpClient = HttpClients.createDefault();
|
||||||
|
HttpResponse httpResponse = httpClient.execute(httpRequest);
|
||||||
|
assertEquals(200, httpResponse.getStatusLine().getStatusCode());
|
||||||
|
|
||||||
|
client = getClient(testRealm.getRealm(), client.getId()).toRepresentation();
|
||||||
|
|
||||||
|
// Assert the uploaded certificate
|
||||||
|
if (keystoreFormat.equals(org.keycloak.services.resources.admin.ClientAttributeCertificateResource.PUBLIC_KEY_PEM)) {
|
||||||
|
String pem = new String(Files.readAllBytes(keystoreFile.toPath()));
|
||||||
|
final String publicKeyNew = client.getAttributes().get(JWTClientAuthenticator.ATTR_PREFIX + "." + CertificateInfoHelper.PUBLIC_KEY);
|
||||||
|
assertEquals("Certificates don't match", pem, publicKeyNew);
|
||||||
|
} else if (keystoreFormat.equals(org.keycloak.services.resources.admin.ClientAttributeCertificateResource.JSON_WEB_KEY_SET)) {
|
||||||
|
final String publicKeyNew = client.getAttributes().get(JWTClientAuthenticator.ATTR_PREFIX + "." + CertificateInfoHelper.PUBLIC_KEY);
|
||||||
|
// Just assert it's valid public key
|
||||||
|
PublicKey pk = KeycloakModelUtils.getPublicKey(publicKeyNew);
|
||||||
|
Assert.assertNotNull(pk);
|
||||||
|
} else if (keystoreFormat.equals(org.keycloak.services.resources.admin.ClientAttributeCertificateResource.CERTIFICATE_PEM)) {
|
||||||
|
String pem = new String(Files.readAllBytes(keystoreFile.toPath()));
|
||||||
|
assertCertificate(client, certOld, pem);
|
||||||
|
} else {
|
||||||
|
InputStream keystoreIs = new FileInputStream(keystoreFile);
|
||||||
|
KeyStore keyStore = getKeystore(keystoreIs, storePassword, keystoreFormat);
|
||||||
|
keystoreIs.close();
|
||||||
|
String pem = KeycloakModelUtils.getPemFromCertificate((X509Certificate) keyStore.getCertificate(keyAlias));
|
||||||
|
assertCertificate(client, certOld, pem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void testEndpointAsAudience(String endpointUrl) throws Exception {
|
||||||
|
ClientRepresentation clientRepresentation = app2;
|
||||||
|
ClientResource clientResource = getClient(testRealm.getRealm(), clientRepresentation.getId());
|
||||||
|
clientRepresentation = clientResource.toRepresentation();
|
||||||
|
try {
|
||||||
|
KeyPair keyPair = setupJwksUrl(Algorithm.PS256, clientRepresentation, clientResource);
|
||||||
|
PublicKey publicKey = keyPair.getPublic();
|
||||||
|
PrivateKey privateKey = keyPair.getPrivate();
|
||||||
|
JsonWebToken assertion = createRequestToken(app2.getClientId(), getRealmInfoUrl());
|
||||||
|
|
||||||
|
assertion.audience(endpointUrl);
|
||||||
|
|
||||||
|
List<NameValuePair> parameters = new LinkedList<NameValuePair>();
|
||||||
|
parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS));
|
||||||
|
parameters
|
||||||
|
.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT));
|
||||||
|
parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION,
|
||||||
|
createSignedRequestToken(privateKey, publicKey, Algorithm.PS256, null, assertion, null)));
|
||||||
|
|
||||||
|
try (CloseableHttpResponse resp = sendRequest(oauth.getServiceAccountUrl(), parameters)) {
|
||||||
|
OAuthClient.AccessTokenResponse response = new OAuthClient.AccessTokenResponse(resp);
|
||||||
|
assertNotNull(response.getAccessToken());
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
revertJwksUriSettings(clientRepresentation, clientResource);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected OAuthClient.AccessTokenResponse testMissingClaim(String... claims) throws Exception {
|
||||||
|
return testMissingClaim(0, claims);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected OAuthClient.AccessTokenResponse testMissingClaim(int tokenTimeOffset, String... claims) throws Exception {
|
||||||
|
CustomJWTClientCredentialsProvider jwtProvider = new CustomJWTClientCredentialsProvider();
|
||||||
|
jwtProvider.setupKeyPair(keyPairClient1);
|
||||||
|
jwtProvider.setTokenTimeout(10);
|
||||||
|
|
||||||
|
for (String claim : claims) {
|
||||||
|
jwtProvider.enableClaim(claim, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
Time.setOffset(tokenTimeOffset);
|
||||||
|
String jwt;
|
||||||
|
try {
|
||||||
|
jwt = jwtProvider.createSignedRequestToken(app1.getClientId(), getRealmInfoUrl());
|
||||||
|
} finally {
|
||||||
|
Time.setOffset(0);
|
||||||
|
}
|
||||||
|
return doClientCredentialsGrantRequest(jwt);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void assertError(OAuthClient.AccessTokenResponse response, String clientId, String responseError, String eventError) {
|
||||||
|
assertEquals(400, response.getStatusCode());
|
||||||
|
assertMessageError(response,clientId,responseError,eventError);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void assertError(OAuthClient.AccessTokenResponse response, int erroCode, String clientId, String responseError, String eventError) {
|
||||||
|
assertEquals(erroCode, response.getStatusCode());
|
||||||
|
assertMessageError(response, clientId, responseError, eventError);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void assertMessageError(OAuthClient.AccessTokenResponse response, String clientId, String responseError, String eventError) {
|
||||||
|
assertEquals(responseError, response.getError());
|
||||||
|
|
||||||
|
events.expectClientLogin()
|
||||||
|
.client(clientId)
|
||||||
|
.session((String) null)
|
||||||
|
.clearDetails()
|
||||||
|
.error(eventError)
|
||||||
|
.user((String) null)
|
||||||
|
.assertEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void assertSuccess(OAuthClient.AccessTokenResponse response, String clientId, String userId, String userName) {
|
||||||
|
assertEquals(200, response.getStatusCode());
|
||||||
|
|
||||||
|
AccessToken accessToken = oauth.verifyToken(response.getAccessToken());
|
||||||
|
RefreshToken refreshToken = oauth.parseRefreshToken(response.getRefreshToken());
|
||||||
|
|
||||||
|
events.expectClientLogin()
|
||||||
|
.client(clientId)
|
||||||
|
.user(userId)
|
||||||
|
.session(accessToken.getSessionState())
|
||||||
|
.detail(Details.TOKEN_ID, accessToken.getId())
|
||||||
|
.detail(Details.REFRESH_TOKEN_ID, refreshToken.getId())
|
||||||
|
.detail(Details.USERNAME, userName)
|
||||||
|
.detail(Details.CLIENT_AUTH_METHOD, JWTClientAuthenticator.PROVIDER_ID)
|
||||||
|
.assertEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static void assertCertificate(ClientRepresentation client, String certOld, String pem) {
|
||||||
|
pem = PemUtils.removeBeginEnd(pem);
|
||||||
|
final String certNew = client.getAttributes().get(JWTClientAuthenticator.CERTIFICATE_ATTR);
|
||||||
|
assertNotEquals("The old and new certificates shouldn't match", certOld, certNew);
|
||||||
|
assertEquals("Certificates don't match", pem, certNew);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void testCodeToTokenRequestFailure(String algorithm, String error, String description) throws Exception {
|
||||||
|
ClientRepresentation clientRepresentation = app2;
|
||||||
|
ClientResource clientResource = getClient(testRealm.getRealm(), clientRepresentation.getId());
|
||||||
|
clientRepresentation = clientResource.toRepresentation();
|
||||||
|
try {
|
||||||
|
// setup Jwks
|
||||||
|
KeyPair keyPair = setupJwksUrl(algorithm, clientRepresentation, clientResource);
|
||||||
|
PublicKey publicKey = keyPair.getPublic();
|
||||||
|
PrivateKey privateKey = keyPair.getPrivate();
|
||||||
|
|
||||||
|
// test
|
||||||
|
oauth.clientId("client2");
|
||||||
|
oauth.doLogin("test-user@localhost", "password");
|
||||||
|
EventRepresentation loginEvent = events.expectLogin()
|
||||||
|
.client("client2")
|
||||||
|
.assertEvent();
|
||||||
|
|
||||||
|
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
|
||||||
|
OAuthClient.AccessTokenResponse response = doAccessTokenRequest(code, getClient2SignedJWT());
|
||||||
|
|
||||||
|
assertEquals(400, response.getStatusCode());
|
||||||
|
assertEquals(error, response.getError());
|
||||||
|
|
||||||
|
events.expect(EventType.CODE_TO_TOKEN_ERROR)
|
||||||
|
.client("client2")
|
||||||
|
.session((String) null)
|
||||||
|
.clearDetails()
|
||||||
|
.error(description)
|
||||||
|
.user((String) null)
|
||||||
|
.assertEvent();
|
||||||
|
} finally {
|
||||||
|
// Revert jwks_url settings
|
||||||
|
revertJwksUriSettings(clientRepresentation, clientResource);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void testDirectGrantRequestFailure(String algorithm) throws Exception {
|
||||||
|
ClientRepresentation clientRepresentation = app2;
|
||||||
|
ClientResource clientResource = getClient(testRealm.getRealm(), clientRepresentation.getId());
|
||||||
|
clientRepresentation = clientResource.toRepresentation();
|
||||||
|
try {
|
||||||
|
// setup Jwks
|
||||||
|
setupJwksUrl(algorithm, clientRepresentation, clientResource);
|
||||||
|
|
||||||
|
// test
|
||||||
|
oauth.clientId("client2");
|
||||||
|
OAuthClient.AccessTokenResponse response = doGrantAccessTokenRequest("test-user@localhost", "password", getClient2SignedJWT());
|
||||||
|
|
||||||
|
assertEquals(400, response.getStatusCode());
|
||||||
|
assertEquals(OAuthErrorException.INVALID_CLIENT, response.getError());
|
||||||
|
|
||||||
|
events.expect(EventType.LOGIN_ERROR)
|
||||||
|
.client("client2")
|
||||||
|
.session((String) null)
|
||||||
|
.clearDetails()
|
||||||
|
.error("client_credentials_setup_required")
|
||||||
|
.user((String) null)
|
||||||
|
.assertEvent();
|
||||||
|
} finally {
|
||||||
|
// Revert jwks_url settings
|
||||||
|
revertJwksUriSettings(clientRepresentation, clientResource);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HELPER METHODS
|
||||||
|
|
||||||
|
protected OAuthClient.AccessTokenResponse doAccessTokenRequest(String code, String signedJwt) throws Exception {
|
||||||
|
List<NameValuePair> parameters = new LinkedList<>();
|
||||||
|
parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.AUTHORIZATION_CODE));
|
||||||
|
parameters.add(new BasicNameValuePair(OAuth2Constants.CODE, code));
|
||||||
|
parameters.add(new BasicNameValuePair(OAuth2Constants.REDIRECT_URI, oauth.getRedirectUri()));
|
||||||
|
parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT));
|
||||||
|
parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION, signedJwt));
|
||||||
|
|
||||||
|
CloseableHttpResponse response = sendRequest(oauth.getAccessTokenUrl(), parameters);
|
||||||
|
return new OAuthClient.AccessTokenResponse(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected OAuthClient.AccessTokenResponse doRefreshTokenRequest(String refreshToken, String signedJwt) throws Exception {
|
||||||
|
List<NameValuePair> parameters = new LinkedList<NameValuePair>();
|
||||||
|
parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.REFRESH_TOKEN));
|
||||||
|
parameters.add(new BasicNameValuePair(OAuth2Constants.REFRESH_TOKEN, refreshToken));
|
||||||
|
parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT));
|
||||||
|
parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION, signedJwt));
|
||||||
|
|
||||||
|
CloseableHttpResponse response = sendRequest(oauth.getRefreshTokenUrl(), parameters);
|
||||||
|
return new OAuthClient.AccessTokenResponse(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected HttpResponse doLogout(String refreshToken, String signedJwt) throws Exception {
|
||||||
|
List<NameValuePair> parameters = new LinkedList<NameValuePair>();
|
||||||
|
parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.REFRESH_TOKEN));
|
||||||
|
parameters.add(new BasicNameValuePair(OAuth2Constants.REFRESH_TOKEN, refreshToken));
|
||||||
|
parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT));
|
||||||
|
parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION, signedJwt));
|
||||||
|
|
||||||
|
return sendRequest(oauth.getLogoutUrl().build(), parameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected OAuthClient.AccessTokenResponse doClientCredentialsGrantRequest(String signedJwt) throws Exception {
|
||||||
|
List<NameValuePair> parameters = new LinkedList<NameValuePair>();
|
||||||
|
parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS));
|
||||||
|
parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT));
|
||||||
|
parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION, signedJwt));
|
||||||
|
|
||||||
|
CloseableHttpResponse response = sendRequest(oauth.getServiceAccountUrl(), parameters);
|
||||||
|
return new OAuthClient.AccessTokenResponse(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected OAuthClient.AccessTokenResponse doGrantAccessTokenRequest(String username, String password, String signedJwt) throws Exception {
|
||||||
|
List<NameValuePair> parameters = new LinkedList<NameValuePair>();
|
||||||
|
parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.PASSWORD));
|
||||||
|
parameters.add(new BasicNameValuePair("username", username));
|
||||||
|
parameters.add(new BasicNameValuePair("password", password));
|
||||||
|
parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT));
|
||||||
|
parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION, signedJwt));
|
||||||
|
|
||||||
|
CloseableHttpResponse response = sendRequest(oauth.getResourceOwnerPasswordCredentialGrantUrl(), parameters);
|
||||||
|
return new OAuthClient.AccessTokenResponse(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected CloseableHttpResponse sendRequest(String requestUrl, List<NameValuePair> parameters) throws Exception {
|
||||||
|
CloseableHttpClient client = new DefaultHttpClient();
|
||||||
|
try {
|
||||||
|
HttpPost post = new HttpPost(requestUrl);
|
||||||
|
UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(parameters, "UTF-8");
|
||||||
|
post.setEntity(formEntity);
|
||||||
|
return client.execute(post);
|
||||||
|
} finally {
|
||||||
|
oauth.closeClient(client);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String getClient2SignedJWT(String algorithm) {
|
||||||
|
return getClientSignedJWT(getClient2KeyPair(), "client2", algorithm);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String getClient1SignedJWT() throws Exception {
|
||||||
|
return getClientSignedJWT(keyPairClient1, "client1", Algorithm.RS256);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String getClient2SignedJWT() {
|
||||||
|
return getClientSignedJWT(getClient2KeyPair(), "client2", Algorithm.RS256);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected KeyPair getClient2KeyPair() {
|
||||||
|
return KeystoreUtil.loadKeyPairFromKeystore("classpath:client-auth-test/keystore-client2.jks",
|
||||||
|
"storepass", "keypass", "clientkey", KeystoreUtil.KeystoreFormat.JKS);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String getClientSignedJWT(KeyPair keyPair, String clientId) {
|
||||||
|
return getClientSignedJWT(keyPair, clientId, Algorithm.RS256);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getClientSignedJWT(KeyPair keyPair, String clientId, String algorithm) {
|
||||||
|
JWTClientCredentialsProvider jwtProvider = new JWTClientCredentialsProvider();
|
||||||
|
jwtProvider.setupKeyPair(keyPair, algorithm);
|
||||||
|
jwtProvider.setTokenTimeout(10);
|
||||||
|
return jwtProvider.createSignedRequestToken(clientId, getRealmInfoUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String getRealmInfoUrl() {
|
||||||
|
String authServerBaseUrl = UriUtils.getOrigin(oauth.getRedirectUri()) + "/auth";
|
||||||
|
return KeycloakUriBuilder.fromUri(authServerBaseUrl).path(ServiceUrlConstants.REALM_INFO_PATH).build("test").toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected ClientAttributeCertificateResource getClientAttributeCertificateResource(String realm, String clientId) {
|
||||||
|
return getClient(realm, clientId).getCertficateResource("jwt.credential");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected ClientResource getClient(String realm, String clientId) {
|
||||||
|
return realmsResouce().realm(realm).clients().get(clientId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom JWTClientCredentialsProvider with support for missing JWT claims
|
||||||
|
*/
|
||||||
|
protected class CustomJWTClientCredentialsProvider extends JWTClientCredentialsProvider {
|
||||||
|
private Map<String, Boolean> enabledClaims = new HashMap<>();
|
||||||
|
|
||||||
|
public CustomJWTClientCredentialsProvider() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
final String[] claims = {"id", "issuer", "subject", "audience", "expiration", "notBefore", "issuedAt"};
|
||||||
|
for (String claim : claims) {
|
||||||
|
enabledClaims.put(claim, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void enableClaim(String claim, boolean value) {
|
||||||
|
if (!enabledClaims.containsKey(claim)) {
|
||||||
|
throw new IllegalArgumentException("Claim \"" + claim + "\" doesn't exist");
|
||||||
|
}
|
||||||
|
enabledClaims.put(claim, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isClaimEnabled(String claim) {
|
||||||
|
Boolean value = enabledClaims.get(claim);
|
||||||
|
if (value == null) {
|
||||||
|
throw new IllegalArgumentException("Claim \"" + claim + "\" doesn't exist");
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<String> getClaims() {
|
||||||
|
return enabledClaims.keySet();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected JsonWebToken createRequestToken(String clientId, String realmInfoUrl) {
|
||||||
|
JsonWebToken reqToken = new JsonWebToken();
|
||||||
|
if (isClaimEnabled("id")) reqToken.id(AdapterUtils.generateId());
|
||||||
|
if (isClaimEnabled("issuer")) reqToken.issuer(clientId);
|
||||||
|
if (isClaimEnabled("subject")) reqToken.subject(clientId);
|
||||||
|
if (isClaimEnabled("audience")) reqToken.audience(realmInfoUrl);
|
||||||
|
|
||||||
|
int now = Time.currentTime();
|
||||||
|
if (isClaimEnabled("issuedAt")) reqToken.issuedAt(now);
|
||||||
|
if (isClaimEnabled("expiration")) reqToken.expiration(now + getTokenTimeout());
|
||||||
|
if (isClaimEnabled("notBefore")) reqToken.notBefore(now);
|
||||||
|
|
||||||
|
return reqToken;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static KeyStore getKeystore(InputStream is, String storePassword, String format) throws Exception {
|
||||||
|
KeyStore keyStore = CryptoIntegration.getProvider().getKeyStore(KeystoreFormat.valueOf(format));
|
||||||
|
keyStore.load(is, storePassword.toCharArray());
|
||||||
|
return keyStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected KeyPair setupJwksUrl(String algorithm, ClientRepresentation clientRepresentation, ClientResource clientResource) throws Exception {
|
||||||
|
return setupJwksUrl(algorithm, null, true, false, null, clientRepresentation, clientResource);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected KeyPair setupJwksUrl(String algorithm, boolean advertiseJWKAlgorithm, boolean keepExistingKeys, String kid,
|
||||||
|
ClientRepresentation clientRepresentation, ClientResource clientResource) throws Exception {
|
||||||
|
return setupJwksUrl(algorithm, null, advertiseJWKAlgorithm, keepExistingKeys, kid, clientRepresentation, clientResource);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected KeyPair setupJwksUrl(String algorithm, String curve, boolean advertiseJWKAlgorithm, boolean keepExistingKeys, String kid,
|
||||||
|
ClientRepresentation clientRepresentation, ClientResource clientResource) throws Exception {
|
||||||
|
// generate and register client keypair
|
||||||
|
TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints();
|
||||||
|
oidcClientEndpointsResource.generateKeys(algorithm, curve, advertiseJWKAlgorithm, keepExistingKeys, kid);
|
||||||
|
Map<String, String> generatedKeys = oidcClientEndpointsResource.getKeysAsBase64();
|
||||||
|
KeyPair keyPair = getKeyPairFromGeneratedBase64(generatedKeys, algorithm, curve);
|
||||||
|
|
||||||
|
// use and set jwks_url
|
||||||
|
OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRepresentation).setUseJwksUrl(true);
|
||||||
|
String jwksUrl = TestApplicationResourceUrls.clientJwksUri();
|
||||||
|
OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRepresentation).setJwksUrl(jwksUrl);
|
||||||
|
clientResource.update(clientRepresentation);
|
||||||
|
|
||||||
|
// set time offset, so that new keys are downloaded
|
||||||
|
setTimeOffset(20);
|
||||||
|
|
||||||
|
return keyPair;
|
||||||
|
}
|
||||||
|
|
||||||
|
private KeyPair setupJwks(String algorithm, String curve, ClientRepresentation clientRepresentation, ClientResource clientResource)
|
||||||
|
throws Exception {
|
||||||
|
// generate and register client keypair
|
||||||
|
TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints();
|
||||||
|
oidcClientEndpointsResource.generateKeys(algorithm, curve);
|
||||||
|
Map<String, String> generatedKeys = oidcClientEndpointsResource.getKeysAsBase64();
|
||||||
|
KeyPair keyPair = getKeyPairFromGeneratedBase64(generatedKeys, algorithm, curve);
|
||||||
|
|
||||||
|
// use and set JWKS
|
||||||
|
OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRepresentation).setUseJwksString(true);
|
||||||
|
JSONWebKeySet keySet = oidcClientEndpointsResource.getJwks();
|
||||||
|
OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRepresentation)
|
||||||
|
.setJwksString(JsonSerialization.writeValueAsString(keySet));
|
||||||
|
clientResource.update(clientRepresentation);
|
||||||
|
|
||||||
|
// set time offset, so that new keys are downloaded
|
||||||
|
setTimeOffset(20);
|
||||||
|
|
||||||
|
return keyPair;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void revertJwksUriSettings(ClientRepresentation clientRepresentation, ClientResource clientResource) {
|
||||||
|
OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRepresentation).setUseJwksUrl(false);
|
||||||
|
OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRepresentation).setJwksUrl(null);
|
||||||
|
clientResource.update(clientRepresentation);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void revertJwksSettings(ClientRepresentation clientRepresentation, ClientResource clientResource) {
|
||||||
|
OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRepresentation).setUseJwksString(false);
|
||||||
|
OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRepresentation).setJwksString(null);
|
||||||
|
clientResource.update(clientRepresentation);
|
||||||
|
}
|
||||||
|
|
||||||
|
private KeyPair getKeyPairFromGeneratedBase64(Map<String, String> generatedKeys, String algorithm, String curve) throws Exception {
|
||||||
|
// It seems that PemUtils.decodePrivateKey, decodePublicKey can only treat RSA type keys, not EC type keys. Therefore, these are not used.
|
||||||
|
String privateKeyBase64 = generatedKeys.get(TestingOIDCEndpointsApplicationResource.PRIVATE_KEY);
|
||||||
|
String publicKeyBase64 = generatedKeys.get(TestingOIDCEndpointsApplicationResource.PUBLIC_KEY);
|
||||||
|
PrivateKey privateKey = decodePrivateKey(Base64.decode(privateKeyBase64), algorithm, curve);
|
||||||
|
PublicKey publicKey = decodePublicKey(Base64.decode(publicKeyBase64), algorithm, curve);
|
||||||
|
return new KeyPair(publicKey, privateKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PrivateKey decodePrivateKey(byte[] der, String algorithm, String curve) throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchProviderException {
|
||||||
|
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(der);
|
||||||
|
String keyAlg = getKeyAlgorithmFromJwaAlgorithm(algorithm, curve);
|
||||||
|
KeyFactory kf = CryptoIntegration.getProvider().getKeyFactory(keyAlg);
|
||||||
|
return kf.generatePrivate(spec);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PublicKey decodePublicKey(byte[] der, String algorithm, String curve) throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchProviderException {
|
||||||
|
X509EncodedKeySpec spec = new X509EncodedKeySpec(der);
|
||||||
|
String keyAlg = getKeyAlgorithmFromJwaAlgorithm(algorithm, curve);
|
||||||
|
KeyFactory kf = CryptoIntegration.getProvider().getKeyFactory(keyAlg);
|
||||||
|
return kf.generatePublic(spec);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String createSignedRequestToken(String clientId, String realmInfoUrl, PrivateKey privateKey, PublicKey publicKey, String algorithm) {
|
||||||
|
return createSignedRequestToken(privateKey, publicKey, algorithm, null, createRequestToken(clientId, realmInfoUrl), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String createSignedRequestToken(String clientId, String realmInfoUrl, PrivateKey privateKey, PublicKey publicKey, String algorithm, String curve) {
|
||||||
|
return createSignedRequestToken(privateKey, publicKey, algorithm, null, createRequestToken(clientId, realmInfoUrl), curve);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String createSignledRequestToken(PrivateKey privateKey, PublicKey publicKey, String algorithm, String kid, JsonWebToken jwt) {
|
||||||
|
return createSignedRequestToken(privateKey, publicKey, algorithm, kid, jwt, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String createSignedRequestToken(PrivateKey privateKey, PublicKey publicKey, String algorithm, String kid, JsonWebToken jwt, String curve) {
|
||||||
|
if (kid == null) {
|
||||||
|
kid = KeyUtils.createKeyId(publicKey);
|
||||||
|
}
|
||||||
|
SignatureSignerContext signer = oauth.createSigner(privateKey, kid, algorithm, curve);
|
||||||
|
String ret = new JWSBuilder().kid(kid).jsonContent(jwt).sign(signer);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected JsonWebToken createRequestToken(String clientId, String realmInfoUrl) {
|
||||||
|
JsonWebToken reqToken = new JsonWebToken();
|
||||||
|
reqToken.id(AdapterUtils.generateId());
|
||||||
|
reqToken.issuer(clientId);
|
||||||
|
reqToken.subject(clientId);
|
||||||
|
reqToken.audience(realmInfoUrl);
|
||||||
|
|
||||||
|
int now = Time.currentTime();
|
||||||
|
reqToken.issuedAt(now);
|
||||||
|
reqToken.expiration(now + 10);
|
||||||
|
reqToken.notBefore(now);
|
||||||
|
|
||||||
|
return reqToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String getKeyAlgorithmFromJwaAlgorithm(String jwaAlgorithm, String curve) {
|
||||||
|
String keyAlg = null;
|
||||||
|
switch (jwaAlgorithm) {
|
||||||
|
case Algorithm.RS256:
|
||||||
|
case Algorithm.RS384:
|
||||||
|
case Algorithm.RS512:
|
||||||
|
case Algorithm.PS256:
|
||||||
|
case Algorithm.PS384:
|
||||||
|
case Algorithm.PS512:
|
||||||
|
keyAlg = KeyType.RSA;
|
||||||
|
break;
|
||||||
|
case Algorithm.ES256:
|
||||||
|
case Algorithm.ES384:
|
||||||
|
case Algorithm.ES512:
|
||||||
|
keyAlg = KeyType.EC;
|
||||||
|
break;
|
||||||
|
default :
|
||||||
|
throw new RuntimeException("Unsupported signature algorithm");
|
||||||
|
}
|
||||||
|
return keyAlg;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1310,6 +1310,16 @@ public class AccessTokenTest extends AbstractKeycloakTest {
|
||||||
conductAccessTokenRequest(Algorithm.HS256, Algorithm.ES512, Algorithm.RS256);
|
conductAccessTokenRequest(Algorithm.HS256, Algorithm.ES512, Algorithm.RS256);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void accessTokenRequest_ClientEdDSA_RealmES256() throws Exception {
|
||||||
|
conductAccessTokenRequest(Algorithm.HS256, Algorithm.EdDSA, Algorithm.ES256);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void accessTokenRequest_ClientEdDSA_RealmEdDSA() throws Exception {
|
||||||
|
conductAccessTokenRequest(Algorithm.HS256, Algorithm.EdDSA, Algorithm.EdDSA);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void validateECDSASignatures() {
|
public void validateECDSASignatures() {
|
||||||
validateTokenECDSASignature(Algorithm.ES256);
|
validateTokenECDSASignature(Algorithm.ES256);
|
||||||
|
@ -1354,7 +1364,6 @@ public class AccessTokenTest extends AbstractKeycloakTest {
|
||||||
TokenSignatureUtil.changeRealmTokenSignatureProvider(adminClient, Algorithm.RS256);
|
TokenSignatureUtil.changeRealmTokenSignatureProvider(adminClient, Algorithm.RS256);
|
||||||
TokenSignatureUtil.changeClientAccessTokenSignatureProvider(ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app"), Algorithm.RS256);
|
TokenSignatureUtil.changeClientAccessTokenSignatureProvider(ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app"), Algorithm.RS256);
|
||||||
}
|
}
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void tokenRequest(String expectedRefreshAlg, String expectedAccessAlg, String expectedIdTokenAlg) throws Exception {
|
private void tokenRequest(String expectedRefreshAlg, String expectedAccessAlg, String expectedIdTokenAlg) throws Exception {
|
||||||
|
@ -1373,17 +1382,17 @@ public class AccessTokenTest extends AbstractKeycloakTest {
|
||||||
assertEquals("Bearer", response.getTokenType());
|
assertEquals("Bearer", response.getTokenType());
|
||||||
|
|
||||||
JWSHeader header = new JWSInput(response.getAccessToken()).getHeader();
|
JWSHeader header = new JWSInput(response.getAccessToken()).getHeader();
|
||||||
assertEquals(expectedAccessAlg, header.getAlgorithm().name());
|
verifySignatureAlgorithm(header, expectedAccessAlg);
|
||||||
assertEquals("JWT", header.getType());
|
assertEquals("JWT", header.getType());
|
||||||
assertNull(header.getContentType());
|
assertNull(header.getContentType());
|
||||||
|
|
||||||
header = new JWSInput(response.getIdToken()).getHeader();
|
header = new JWSInput(response.getIdToken()).getHeader();
|
||||||
assertEquals(expectedIdTokenAlg, header.getAlgorithm().name());
|
verifySignatureAlgorithm(header, expectedIdTokenAlg);
|
||||||
assertEquals("JWT", header.getType());
|
assertEquals("JWT", header.getType());
|
||||||
assertNull(header.getContentType());
|
assertNull(header.getContentType());
|
||||||
|
|
||||||
header = new JWSInput(response.getRefreshToken()).getHeader();
|
header = new JWSInput(response.getRefreshToken()).getHeader();
|
||||||
assertEquals(expectedRefreshAlg, header.getAlgorithm().name());
|
verifySignatureAlgorithm(header, expectedRefreshAlg);
|
||||||
assertEquals("JWT", header.getType());
|
assertEquals("JWT", header.getType());
|
||||||
assertNull(header.getContentType());
|
assertNull(header.getContentType());
|
||||||
|
|
||||||
|
@ -1401,6 +1410,10 @@ public class AccessTokenTest extends AbstractKeycloakTest {
|
||||||
assertEquals(sessionId, token.getSessionState());
|
assertEquals(sessionId, token.getSessionState());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void verifySignatureAlgorithm(JWSHeader header, String expectedAlgorithm) {
|
||||||
|
assertEquals(expectedAlgorithm, header.getAlgorithm().name());
|
||||||
|
}
|
||||||
|
|
||||||
// KEYCLOAK-16009
|
// KEYCLOAK-16009
|
||||||
@Test
|
@Test
|
||||||
public void tokenRequestParamsMoreThanOnce() throws Exception {
|
public void tokenRequestParamsMoreThanOnce() throws Exception {
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.testsuite.oauth;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.keycloak.crypto.Algorithm;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
|
||||||
|
*/
|
||||||
|
public class ClientAuthEdDSASignedJWTTest extends AbstractClientAuthSignedJWTTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCodeToTokenRequestSuccessEd448usingJwksUri() throws Exception {
|
||||||
|
testCodeToTokenRequestSuccess(Algorithm.EdDSA, Algorithm.Ed448, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCodeToTokenRequestSuccessEd25519usingJwks() throws Exception {
|
||||||
|
testCodeToTokenRequestSuccess(Algorithm.EdDSA, Algorithm.Ed25519, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String getKeyAlgorithmFromJwaAlgorithm(String jwaAlgorithm, String curve) {
|
||||||
|
if (!Algorithm.EdDSA.equals(jwaAlgorithm)) {
|
||||||
|
throw new RuntimeException("Unsupported signature algorithm: " + jwaAlgorithm);
|
||||||
|
}
|
||||||
|
switch (curve) {
|
||||||
|
case Algorithm.Ed25519:
|
||||||
|
return Algorithm.Ed25519;
|
||||||
|
case Algorithm.Ed448:
|
||||||
|
return Algorithm.Ed448;
|
||||||
|
default :
|
||||||
|
throw new RuntimeException("Unsupported signature curve " + curve);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load diff
|
@ -101,7 +101,7 @@ public class AuthorizationTokenEncryptionTest extends AbstractTestRealmKeycloakT
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testAuthorizationEncryptionAlgRSA1_5EncA192GCM() {
|
public void testAuthorizationEncryptionAlgRSA1_5EncA192GCM() {
|
||||||
testAuthorizationTokenSignatureAndEncryption(Algorithm.RS512, JWEConstants.RSA1_5, JWEConstants.A192GCM);
|
testAuthorizationTokenSignatureAndEncryption(Algorithm.EdDSA, JWEConstants.RSA1_5, JWEConstants.A192GCM);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -123,7 +123,7 @@ public class AuthorizationTokenEncryptionTest extends AbstractTestRealmKeycloakT
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testAuthorizationEncryptionAlgRSA_OAEPEncA256CBC_HS512() {
|
public void testAuthorizationEncryptionAlgRSA_OAEPEncA256CBC_HS512() {
|
||||||
testAuthorizationTokenSignatureAndEncryption(Algorithm.PS512, JWEConstants.RSA_OAEP, JWEConstants.A256CBC_HS512);
|
testAuthorizationTokenSignatureAndEncryption(Algorithm.EdDSA, JWEConstants.RSA_OAEP, JWEConstants.A256CBC_HS512);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -997,6 +997,10 @@ public class OIDCAdvancedRequestParamsTest extends AbstractTestRealmKeycloakTest
|
||||||
}
|
}
|
||||||
|
|
||||||
private void requestUriParamSignedIn(String expectedAlgorithm, String actualAlgorithm) {
|
private void requestUriParamSignedIn(String expectedAlgorithm, String actualAlgorithm) {
|
||||||
|
requestUriParamSignedIn(expectedAlgorithm, actualAlgorithm, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void requestUriParamSignedIn(String expectedAlgorithm, String actualAlgorithm, String curve) {
|
||||||
ClientResource clientResource = null;
|
ClientResource clientResource = null;
|
||||||
ClientRepresentation clientRep = null;
|
ClientRepresentation clientRep = null;
|
||||||
try {
|
try {
|
||||||
|
@ -1010,7 +1014,7 @@ public class OIDCAdvancedRequestParamsTest extends AbstractTestRealmKeycloakTest
|
||||||
clientResource.update(clientRep);
|
clientResource.update(clientRep);
|
||||||
|
|
||||||
// generate and register client keypair
|
// generate and register client keypair
|
||||||
if ("none" != actualAlgorithm) oidcClientEndpointsResource.generateKeys(actualAlgorithm);
|
if (!"none".equals(actualAlgorithm)) oidcClientEndpointsResource.generateKeys(actualAlgorithm, curve);
|
||||||
|
|
||||||
// register request object
|
// register request object
|
||||||
oidcClientEndpointsResource.setOIDCRequest("test", "test-app", validRedirectUri, "10", "mystate3", actualAlgorithm);
|
oidcClientEndpointsResource.setOIDCRequest("test", "test-app", validRedirectUri, "10", "mystate3", actualAlgorithm);
|
||||||
|
@ -1119,7 +1123,19 @@ public class OIDCAdvancedRequestParamsTest extends AbstractTestRealmKeycloakTest
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void requestUriParamSignedExpectedAnyActualES256() {
|
public void requestUriParamSignedExpectedEd25519ActualEd25519() throws Exception {
|
||||||
|
// will success
|
||||||
|
requestUriParamSignedIn(Algorithm.EdDSA, Algorithm.EdDSA, Algorithm.Ed25519);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void requestUriParamSignedExpectedES256ActualEd448() throws Exception {
|
||||||
|
// will fail
|
||||||
|
requestUriParamSignedIn(Algorithm.ES256, Algorithm.EdDSA, Algorithm.Ed448);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void requestUriParamSignedExpectedAnyActualES256() throws Exception {
|
||||||
// Algorithm is null if 'any'
|
// Algorithm is null if 'any'
|
||||||
// will success
|
// will success
|
||||||
requestUriParamSignedIn(null, Algorithm.ES256);
|
requestUriParamSignedIn(null, Algorithm.ES256);
|
||||||
|
|
|
@ -142,10 +142,10 @@ public class OIDCWellKnownProviderTest extends AbstractKeycloakTest {
|
||||||
Assert.assertNames(oidcConfig.getSubjectTypesSupported(), "pairwise", "public");
|
Assert.assertNames(oidcConfig.getSubjectTypesSupported(), "pairwise", "public");
|
||||||
|
|
||||||
// Signature algorithms
|
// Signature algorithms
|
||||||
Assert.assertNames(oidcConfig.getIdTokenSigningAlgValuesSupported(), Algorithm.PS256, Algorithm.PS384, Algorithm.PS512, Algorithm.RS256, Algorithm.RS384, Algorithm.RS512, Algorithm.ES256, Algorithm.ES384, Algorithm.ES512, Algorithm.HS256, Algorithm.HS384, Algorithm.HS512);
|
Assert.assertNames(oidcConfig.getIdTokenSigningAlgValuesSupported(), Algorithm.PS256, Algorithm.PS384, Algorithm.PS512, Algorithm.RS256, Algorithm.RS384, Algorithm.RS512, Algorithm.ES256, Algorithm.ES384, Algorithm.ES512, Algorithm.HS256, Algorithm.HS384, Algorithm.HS512, Algorithm.EdDSA);
|
||||||
Assert.assertNames(oidcConfig.getUserInfoSigningAlgValuesSupported(), "none", Algorithm.PS256, Algorithm.PS384, Algorithm.PS512, Algorithm.RS256, Algorithm.RS384, Algorithm.RS512, Algorithm.ES256, Algorithm.ES384, Algorithm.ES512, Algorithm.HS256, Algorithm.HS384, Algorithm.HS512);
|
Assert.assertNames(oidcConfig.getUserInfoSigningAlgValuesSupported(), "none", Algorithm.PS256, Algorithm.PS384, Algorithm.PS512, Algorithm.RS256, Algorithm.RS384, Algorithm.RS512, Algorithm.ES256, Algorithm.ES384, Algorithm.ES512, Algorithm.HS256, Algorithm.HS384, Algorithm.HS512, Algorithm.EdDSA);
|
||||||
Assert.assertNames(oidcConfig.getRequestObjectSigningAlgValuesSupported(), "none", Algorithm.PS256, Algorithm.PS384, Algorithm.PS512, Algorithm.RS256, Algorithm.RS384, Algorithm.RS512, Algorithm.ES256, Algorithm.ES384, Algorithm.ES512, Algorithm.HS256, Algorithm.HS384, Algorithm.HS512);
|
Assert.assertNames(oidcConfig.getRequestObjectSigningAlgValuesSupported(), "none", Algorithm.PS256, Algorithm.PS384, Algorithm.PS512, Algorithm.RS256, Algorithm.RS384, Algorithm.RS512, Algorithm.ES256, Algorithm.ES384, Algorithm.ES512, Algorithm.HS256, Algorithm.HS384, Algorithm.HS512, Algorithm.EdDSA);
|
||||||
Assert.assertNames(oidcConfig.getAuthorizationSigningAlgValuesSupported(), Algorithm.PS256, Algorithm.PS384, Algorithm.PS512, Algorithm.RS256, Algorithm.RS384, Algorithm.RS512, Algorithm.ES256, Algorithm.ES384, Algorithm.ES512, Algorithm.HS256, Algorithm.HS384, Algorithm.HS512);
|
Assert.assertNames(oidcConfig.getAuthorizationSigningAlgValuesSupported(), Algorithm.PS256, Algorithm.PS384, Algorithm.PS512, Algorithm.RS256, Algorithm.RS384, Algorithm.RS512, Algorithm.ES256, Algorithm.ES384, Algorithm.ES512, Algorithm.HS256, Algorithm.HS384, Algorithm.HS512, Algorithm.EdDSA);
|
||||||
|
|
||||||
// request object encryption algorithms
|
// request object encryption algorithms
|
||||||
Assert.assertNames(oidcConfig.getRequestObjectEncryptionAlgValuesSupported(), JWEConstants.RSA_OAEP, JWEConstants.RSA_OAEP_256, JWEConstants.RSA1_5);
|
Assert.assertNames(oidcConfig.getRequestObjectEncryptionAlgValuesSupported(), JWEConstants.RSA_OAEP, JWEConstants.RSA_OAEP_256, JWEConstants.RSA1_5);
|
||||||
|
@ -161,12 +161,12 @@ public class OIDCWellKnownProviderTest extends AbstractKeycloakTest {
|
||||||
|
|
||||||
// Client authentication
|
// Client authentication
|
||||||
Assert.assertNames(oidcConfig.getTokenEndpointAuthMethodsSupported(), "client_secret_basic", "client_secret_post", "private_key_jwt", "client_secret_jwt", "tls_client_auth");
|
Assert.assertNames(oidcConfig.getTokenEndpointAuthMethodsSupported(), "client_secret_basic", "client_secret_post", "private_key_jwt", "client_secret_jwt", "tls_client_auth");
|
||||||
Assert.assertNames(oidcConfig.getTokenEndpointAuthSigningAlgValuesSupported(), Algorithm.PS256, Algorithm.PS384, Algorithm.PS512, Algorithm.RS256, Algorithm.RS384, Algorithm.RS512, Algorithm.ES256, Algorithm.ES384, Algorithm.ES512, Algorithm.HS256, Algorithm.HS384, Algorithm.HS512);
|
Assert.assertNames(oidcConfig.getTokenEndpointAuthSigningAlgValuesSupported(), Algorithm.PS256, Algorithm.PS384, Algorithm.PS512, Algorithm.RS256, Algorithm.RS384, Algorithm.RS512, Algorithm.ES256, Algorithm.ES384, Algorithm.ES512, Algorithm.HS256, Algorithm.HS384, Algorithm.HS512, Algorithm.EdDSA);
|
||||||
// NOTE: Those are overriden in "oidc-well-known-config-override.json" and they are tested in testDefaultProviderCustomizations
|
// NOTE: Those are overriden in "oidc-well-known-config-override.json" and they are tested in testDefaultProviderCustomizations
|
||||||
//Assert.assertNames(oidcConfig.getIntrospectionEndpointAuthMethodsSupported(), "private_key_jwt", "client_secret_jwt", "tls_client_auth", "custom_nonexisting_authenticator");
|
//Assert.assertNames(oidcConfig.getIntrospectionEndpointAuthMethodsSupported(), "private_key_jwt", "client_secret_jwt", "tls_client_auth", "custom_nonexisting_authenticator");
|
||||||
Assert.assertNames(oidcConfig.getIntrospectionEndpointAuthSigningAlgValuesSupported(), Algorithm.PS256,
|
Assert.assertNames(oidcConfig.getIntrospectionEndpointAuthSigningAlgValuesSupported(), Algorithm.PS256,
|
||||||
Algorithm.PS384, Algorithm.PS512, Algorithm.RS256, Algorithm.RS384, Algorithm.RS512, Algorithm.ES256,
|
Algorithm.PS384, Algorithm.PS512, Algorithm.RS256, Algorithm.RS384, Algorithm.RS512, Algorithm.ES256,
|
||||||
Algorithm.ES384, Algorithm.ES512, Algorithm.HS256, Algorithm.HS384, Algorithm.HS512);
|
Algorithm.ES384, Algorithm.ES512, Algorithm.HS256, Algorithm.HS384, Algorithm.HS512, Algorithm.EdDSA);
|
||||||
|
|
||||||
// Claims
|
// Claims
|
||||||
assertContains(oidcConfig.getClaimsSupported(), IDToken.NAME, IDToken.EMAIL, IDToken.PREFERRED_USERNAME, IDToken.FAMILY_NAME, IDToken.ACR);
|
assertContains(oidcConfig.getClaimsSupported(), IDToken.NAME, IDToken.EMAIL, IDToken.PREFERRED_USERNAME, IDToken.FAMILY_NAME, IDToken.ACR);
|
||||||
|
@ -196,7 +196,7 @@ public class OIDCWellKnownProviderTest extends AbstractKeycloakTest {
|
||||||
assertEquals(oidcConfig.getBackchannelAuthenticationEndpoint(), oauth.getBackchannelAuthenticationUrl());
|
assertEquals(oidcConfig.getBackchannelAuthenticationEndpoint(), oauth.getBackchannelAuthenticationUrl());
|
||||||
assertContains(oidcConfig.getGrantTypesSupported(), OAuth2Constants.CIBA_GRANT_TYPE);
|
assertContains(oidcConfig.getGrantTypesSupported(), OAuth2Constants.CIBA_GRANT_TYPE);
|
||||||
Assert.assertNames(oidcConfig.getBackchannelTokenDeliveryModesSupported(), "poll", "ping");
|
Assert.assertNames(oidcConfig.getBackchannelTokenDeliveryModesSupported(), "poll", "ping");
|
||||||
Assert.assertNames(oidcConfig.getBackchannelAuthenticationRequestSigningAlgValuesSupported(), Algorithm.PS256, Algorithm.PS384, Algorithm.PS512, Algorithm.RS256, Algorithm.RS384, Algorithm.RS512, Algorithm.ES256, Algorithm.ES384, Algorithm.ES512);
|
Assert.assertNames(oidcConfig.getBackchannelAuthenticationRequestSigningAlgValuesSupported(), Algorithm.PS256, Algorithm.PS384, Algorithm.PS512, Algorithm.RS256, Algorithm.RS384, Algorithm.RS512, Algorithm.ES256, Algorithm.ES384, Algorithm.ES512, Algorithm.EdDSA);
|
||||||
|
|
||||||
Assert.assertTrue(oidcConfig.getBackchannelLogoutSupported());
|
Assert.assertTrue(oidcConfig.getBackchannelLogoutSupported());
|
||||||
Assert.assertTrue(oidcConfig.getBackchannelLogoutSessionSupported());
|
Assert.assertTrue(oidcConfig.getBackchannelLogoutSessionSupported());
|
||||||
|
@ -207,7 +207,7 @@ public class OIDCWellKnownProviderTest extends AbstractKeycloakTest {
|
||||||
"client_secret_post", "private_key_jwt", "client_secret_jwt", "tls_client_auth");
|
"client_secret_post", "private_key_jwt", "client_secret_jwt", "tls_client_auth");
|
||||||
Assert.assertNames(oidcConfig.getRevocationEndpointAuthSigningAlgValuesSupported(), Algorithm.PS256,
|
Assert.assertNames(oidcConfig.getRevocationEndpointAuthSigningAlgValuesSupported(), Algorithm.PS256,
|
||||||
Algorithm.PS384, Algorithm.PS512, Algorithm.RS256, Algorithm.RS384, Algorithm.RS512, Algorithm.ES256,
|
Algorithm.PS384, Algorithm.PS512, Algorithm.RS256, Algorithm.RS384, Algorithm.RS512, Algorithm.ES256,
|
||||||
Algorithm.ES384, Algorithm.ES512, Algorithm.HS256, Algorithm.HS384, Algorithm.HS512);
|
Algorithm.ES384, Algorithm.ES512, Algorithm.HS256, Algorithm.HS384, Algorithm.HS512, Algorithm.EdDSA);
|
||||||
|
|
||||||
assertEquals(oidcConfig.getDeviceAuthorizationEndpoint(), oauth.getDeviceAuthorizationUrl());
|
assertEquals(oidcConfig.getDeviceAuthorizationEndpoint(), oauth.getDeviceAuthorizationUrl());
|
||||||
|
|
||||||
|
|
|
@ -327,13 +327,27 @@ public class UserInfoTest extends AbstractKeycloakTest {
|
||||||
testUserInfoSignatureAndEncryption(null, JWEConstants.RSA1_5, null);
|
testUserInfoSignatureAndEncryption(null, JWEConstants.RSA1_5, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccessEncryptedResponseSigAlgEd25519AlgRSA_OAEPEncA256GCM() throws Exception {
|
||||||
|
testUserInfoSignatureAndEncryption(Algorithm.EdDSA, Algorithm.Ed25519, JWEConstants.RSA_OAEP, JWEConstants.A256GCM);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccessEncryptedResponseSigAlgEd448AlgRSA_OAEP256EncA256CBC_HS512() throws Exception {
|
||||||
|
testUserInfoSignatureAndEncryption(Algorithm.EdDSA, Algorithm.Ed448, JWEConstants.RSA_OAEP_256, JWEConstants.A256CBC_HS512);
|
||||||
|
}
|
||||||
|
|
||||||
private void testUserInfoSignatureAndEncryption(String sigAlgorithm, String algAlgorithm, String encAlgorithm) {
|
private void testUserInfoSignatureAndEncryption(String sigAlgorithm, String algAlgorithm, String encAlgorithm) {
|
||||||
|
testUserInfoSignatureAndEncryption(sigAlgorithm, null, algAlgorithm, encAlgorithm);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void testUserInfoSignatureAndEncryption(String sigAlgorithm, String curve, String algAlgorithm, String encAlgorithm) {
|
||||||
ClientResource clientResource = null;
|
ClientResource clientResource = null;
|
||||||
ClientRepresentation clientRep = null;
|
ClientRepresentation clientRep = null;
|
||||||
try {
|
try {
|
||||||
// generate and register encryption key onto client
|
// generate and register encryption key onto client
|
||||||
TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints();
|
TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints();
|
||||||
oidcClientEndpointsResource.generateKeys(algAlgorithm);
|
oidcClientEndpointsResource.generateKeys(algAlgorithm, curve);
|
||||||
|
|
||||||
clientResource = ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app");
|
clientResource = ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app");
|
||||||
clientRep = clientResource.toRepresentation();
|
clientRep = clientResource.toRepresentation();
|
||||||
|
|
|
@ -233,13 +233,13 @@ public abstract class AbstractOIDCResponseTypeTest extends AbstractTestRealmKeyc
|
||||||
String accessToken = authzResponse.getAccessToken();
|
String accessToken = authzResponse.getAccessToken();
|
||||||
if (idToken != null) {
|
if (idToken != null) {
|
||||||
header = new JWSInput(idToken).getHeader();
|
header = new JWSInput(idToken).getHeader();
|
||||||
assertEquals(expectedIdTokenAlg, header.getAlgorithm().name());
|
verifySignatureAlgorithm(header, expectedIdTokenAlg);
|
||||||
assertEquals("JWT", header.getType());
|
assertEquals("JWT", header.getType());
|
||||||
assertNull(header.getContentType());
|
assertNull(header.getContentType());
|
||||||
}
|
}
|
||||||
if (accessToken != null) {
|
if (accessToken != null) {
|
||||||
header = new JWSInput(accessToken).getHeader();
|
header = new JWSInput(accessToken).getHeader();
|
||||||
assertEquals(expectedAccessAlg, header.getAlgorithm().name());
|
verifySignatureAlgorithm(header, expectedAccessAlg);
|
||||||
assertEquals("JWT", header.getType());
|
assertEquals("JWT", header.getType());
|
||||||
assertNull(header.getContentType());
|
assertNull(header.getContentType());
|
||||||
}
|
}
|
||||||
|
@ -252,6 +252,10 @@ public abstract class AbstractOIDCResponseTypeTest extends AbstractTestRealmKeyc
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void verifySignatureAlgorithm(JWSHeader header, String expectedAlgorithm) {
|
||||||
|
assertEquals(expectedAlgorithm, header.getAlgorithm().name());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void oidcFlow_RealmRS256_ClientRS384() throws Exception {
|
public void oidcFlow_RealmRS256_ClientRS384() throws Exception {
|
||||||
oidcFlowRequest(Algorithm.RS256, Algorithm.RS384);
|
oidcFlowRequest(Algorithm.RS256, Algorithm.RS384);
|
||||||
|
@ -272,6 +276,16 @@ public abstract class AbstractOIDCResponseTypeTest extends AbstractTestRealmKeyc
|
||||||
oidcFlowRequest(Algorithm.PS256, Algorithm.ES256);
|
oidcFlowRequest(Algorithm.PS256, Algorithm.ES256);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void oidcFlow_RealmEdDSA_ClientES256() throws Exception {
|
||||||
|
oidcFlowRequest(Algorithm.EdDSA, Algorithm.ES256);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void oidcFlow_RealmPS256_ClientEdDSA() throws Exception {
|
||||||
|
oidcFlowRequest(Algorithm.PS256, Algorithm.EdDSA);
|
||||||
|
}
|
||||||
|
|
||||||
private void oidcFlowRequest(String expectedAccessAlg, String expectedIdTokenAlg) throws Exception {
|
private void oidcFlowRequest(String expectedAccessAlg, String expectedIdTokenAlg) throws Exception {
|
||||||
try {
|
try {
|
||||||
setIdTokenSignatureAlgorithm(expectedIdTokenAlg);
|
setIdTokenSignatureAlgorithm(expectedIdTokenAlg);
|
||||||
|
|
|
@ -19,6 +19,7 @@ package org.keycloak.testsuite.oidc.flows;
|
||||||
|
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
import org.keycloak.crypto.KeyWrapper;
|
||||||
import org.keycloak.events.Details;
|
import org.keycloak.events.Details;
|
||||||
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
|
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
|
||||||
import org.keycloak.representations.IDToken;
|
import org.keycloak.representations.IDToken;
|
||||||
|
|
Loading…
Reference in a new issue