KEYCLOAK-8460 Request Object Signature Verification Other Than RS256 (#5603)

* KEYCLOAK-8460 Request Object Signature Verification Other Than RS256

also support client signed signature verification by refactored token
verification mechanism

* KEYCLOAK-8460 Request Object Signature Verification Other Than RS256

incorporate feedbacks and refactor client public key loading mechanism

* KEYCLOAK-8460 Request Object Signature Verification Other Than RS256

unsigned request object not allowed

* KEYCLOAK-8460 Request Object Signature Verification Other Than RS256

revert to re-support "none"
This commit is contained in:
Takashi Norimatsu 2018-11-19 22:28:32 +09:00 committed by Stian Thorgersen
parent 461dae20de
commit 0793234c19
38 changed files with 822 additions and 110 deletions

View file

@ -24,6 +24,8 @@ import com.fasterxml.jackson.annotation.JsonProperty;
*/
public class ECPublicJWK extends JWK {
public static final String EC = "EC";
public static final String CRV = "crv";
public static final String X = "x";
public static final String Y = "y";

View file

@ -128,7 +128,7 @@ public class JWKParser {
}
public boolean isKeyTypeSupported(String keyType) {
return RSAPublicJWK.RSA.equals(keyType);
return (RSAPublicJWK.RSA.equals(keyType) || ECPublicJWK.EC.equals(keyType));
}
}

View file

@ -17,6 +17,8 @@
package org.keycloak.util;
import org.keycloak.crypto.KeyUse;
import org.keycloak.crypto.KeyWrapper;
import org.keycloak.jose.jwk.JSONWebKeySet;
import org.keycloak.jose.jwk.JWK;
import org.keycloak.jose.jwk.JWKParser;
@ -43,6 +45,34 @@ public class JWKSUtils {
return result;
}
public static Map<String, KeyWrapper> getKeyWrappersForUse(JSONWebKeySet keySet, JWK.Use requestedUse) {
Map<String, KeyWrapper> result = new HashMap<>();
for (JWK jwk : keySet.getKeys()) {
JWKParser parser = JWKParser.create(jwk);
if (jwk.getPublicKeyUse().equals(requestedUse.asString()) && parser.isKeyTypeSupported(jwk.getKeyType())) {
KeyWrapper keyWrapper = new KeyWrapper();
keyWrapper.setKid(jwk.getKeyId());
keyWrapper.setAlgorithm(jwk.getAlgorithm());
keyWrapper.setType(jwk.getKeyType());
keyWrapper.setUse(getKeyUse(jwk.getPublicKeyUse()));
keyWrapper.setVerifyKey(parser.toPublicKey());
result.put(keyWrapper.getKid(), keyWrapper);
}
}
return result;
}
private static KeyUse getKeyUse(String keyUse) {
switch (keyUse) {
case "sig" :
return KeyUse.SIG;
case "enc" :
return KeyUse.ENC;
default :
return null;
}
}
public static JWK getKeyForUse(JSONWebKeySet keySet, JWK.Use requestedUse) {
for (JWK jwk : keySet.getKeys()) {
JWKParser parser = JWKParser.create(jwk);

View file

@ -17,7 +17,6 @@
package org.keycloak.keys.infinispan;
import java.security.PublicKey;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
@ -30,6 +29,7 @@ import org.infinispan.Cache;
import org.jboss.logging.Logger;
import org.keycloak.cluster.ClusterProvider;
import org.keycloak.common.util.Time;
import org.keycloak.crypto.KeyWrapper;
import org.keycloak.keys.PublicKeyLoader;
import org.keycloak.keys.PublicKeyStorageProvider;
import org.keycloak.models.KeycloakSession;
@ -127,11 +127,11 @@ public class InfinispanPublicKeyStorageProvider implements PublicKeyStorageProvi
@Override
public PublicKey getPublicKey(String modelKey, String kid, PublicKeyLoader loader) {
public KeyWrapper getPublicKey(String modelKey, String kid, PublicKeyLoader loader) {
// Check if key is in cache
PublicKeysEntry entry = keys.get(modelKey);
if (entry != null) {
PublicKey publicKey = getPublicKey(entry.getCurrentKeys(), kid);
KeyWrapper publicKey = getPublicKey(entry.getCurrentKeys(), kid);
if (publicKey != null) {
return publicKey;
}
@ -157,7 +157,7 @@ public class InfinispanPublicKeyStorageProvider implements PublicKeyStorageProvi
entry = task.get();
// Computation finished. Let's see if key is available
PublicKey publicKey = getPublicKey(entry.getCurrentKeys(), kid);
KeyWrapper publicKey = getPublicKey(entry.getCurrentKeys(), kid);
if (publicKey != null) {
return publicKey;
}
@ -182,7 +182,7 @@ public class InfinispanPublicKeyStorageProvider implements PublicKeyStorageProvi
return null;
}
private PublicKey getPublicKey(Map<String, PublicKey> publicKeys, String kid) {
private KeyWrapper getPublicKey(Map<String, KeyWrapper> publicKeys, String kid) {
// Backwards compatibility
if (kid == null && !publicKeys.isEmpty()) {
return publicKeys.values().iterator().next();
@ -218,7 +218,7 @@ public class InfinispanPublicKeyStorageProvider implements PublicKeyStorageProvi
// Check again if we are allowed to send request. There is a chance other task was already finished and removed from tasksInProgress in the meantime.
if (currentTime > lastRequestTime + minTimeBetweenRequests) {
Map<String, PublicKey> publicKeys = delegate.loadKeys();
Map<String, KeyWrapper> publicKeys = delegate.loadKeys();
if (log.isDebugEnabled()) {
log.debugf("Public keys retrieved successfully for model %s. New kids: %s", modelKey, publicKeys.keySet().toString());

View file

@ -18,9 +18,10 @@
package org.keycloak.keys.infinispan;
import java.io.Serializable;
import java.security.PublicKey;
import java.util.Map;
import org.keycloak.crypto.KeyWrapper;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
@ -28,9 +29,9 @@ public class PublicKeysEntry implements Serializable {
private final int lastRequestTime;
private final Map<String, PublicKey> currentKeys;
private final Map<String, KeyWrapper> currentKeys;
public PublicKeysEntry(int lastRequestTime, Map<String, PublicKey> currentKeys) {
public PublicKeysEntry(int lastRequestTime, Map<String, KeyWrapper> currentKeys) {
this.lastRequestTime = lastRequestTime;
this.currentKeys = currentKeys;
}
@ -39,7 +40,7 @@ public class PublicKeysEntry implements Serializable {
return lastRequestTime;
}
public Map<String, PublicKey> getCurrentKeys() {
public Map<String, KeyWrapper> getCurrentKeys() {
return currentKeys;
}
}

View file

@ -17,7 +17,6 @@
package org.keycloak.keys.infinispan;
import java.security.PublicKey;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
@ -39,6 +38,7 @@ import org.junit.Before;
import org.junit.Test;
import org.keycloak.common.util.Time;
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
import org.keycloak.crypto.KeyWrapper;
import org.keycloak.keys.PublicKeyLoader;
/**
@ -144,7 +144,7 @@ public class InfinispanKeyStorageProviderTest {
}
@Override
public Map<String, PublicKey> loadKeys() throws Exception {
public Map<String, KeyWrapper> loadKeys() throws Exception {
counters.putIfAbsent(modelKey, new AtomicInteger(0));
AtomicInteger currentCounter = counters.get(modelKey);

View file

@ -0,0 +1,31 @@
/*
* 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.crypto;
import org.keycloak.common.VerificationException;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.models.ClientModel;
import org.keycloak.provider.Provider;
public interface ClientSignatureVerifierProvider extends Provider {
SignatureVerifierContext verifier(ClientModel client, JWSInput input) throws VerificationException;
@Override
default void close() {
}
}

View file

@ -0,0 +1,38 @@
/*
* 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.crypto;
import org.keycloak.Config;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.provider.ProviderFactory;
public interface ClientSignatureVerifierProviderFactory extends ProviderFactory<ClientSignatureVerifierProvider> {
@Override
default void init(Config.Scope config) {
}
@Override
default void postInit(KeycloakSessionFactory factory) {
}
@Override
default void close() {
}
}

View file

@ -0,0 +1,44 @@
/*
* 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.crypto;
import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.provider.Spi;
public class ClientSignatureVerifierSpi implements Spi {
@Override
public boolean isInternal() {
return true;
}
@Override
public String getName() {
return "clientSignature";
}
@Override
public Class<? extends Provider> getProviderClass() {
return ClientSignatureVerifierProvider.class;
}
@Override
public Class<? extends ProviderFactory> getProviderFactoryClass() {
return ClientSignatureVerifierProviderFactory.class;
}
}

View file

@ -17,14 +17,15 @@
package org.keycloak.keys;
import java.security.PublicKey;
import java.util.Map;
import org.keycloak.crypto.KeyWrapper;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public interface PublicKeyLoader {
Map<String, PublicKey> loadKeys() throws Exception;
Map<String, KeyWrapper> loadKeys() throws Exception;
}

View file

@ -17,8 +17,7 @@
package org.keycloak.keys;
import java.security.PublicKey;
import org.keycloak.crypto.KeyWrapper;
import org.keycloak.provider.Provider;
/**
@ -35,7 +34,7 @@ public interface PublicKeyStorageProvider extends Provider {
* @param loader
* @return
*/
PublicKey getPublicKey(String modelKey, String kid, PublicKeyLoader loader);
KeyWrapper getPublicKey(String modelKey, String kid, PublicKeyLoader loader);
/**
* Clears all the cached public keys, so they need to be loaded again

View file

@ -72,4 +72,5 @@ org.keycloak.credential.CredentialSpi
org.keycloak.keys.PublicKeyStorageSpi
org.keycloak.keys.KeySpi
org.keycloak.storage.client.ClientStorageProviderSpi
org.keycloak.crypto.SignatureSpi
org.keycloak.crypto.SignatureSpi
org.keycloak.crypto.ClientSignatureVerifierSpi

View file

@ -41,4 +41,6 @@ public interface TokenManager {
String signatureAlgorithm(TokenCategory category);
<T> T decodeClientJWT(String token, ClientModel client, Class<T> clazz);
}

View file

@ -0,0 +1,38 @@
/*
* 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.crypto;
import org.keycloak.common.VerificationException;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
public class AsymmetricClientSignatureVerifierProvider implements ClientSignatureVerifierProvider {
private final KeycloakSession session;
private final String algorithm;
public AsymmetricClientSignatureVerifierProvider(KeycloakSession session, String algorithm) {
this.session = session;
this.algorithm = algorithm;
}
@Override
public SignatureVerifierContext verifier(ClientModel client, JWSInput input) throws VerificationException {
return new ClientAsymmetricSignatureVerifierContext(session, client, input);
}
}

View file

@ -0,0 +1,38 @@
/*
* 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.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;
public class ClientAsymmetricSignatureVerifierContext extends AsymmetricSignatureVerifierContext {
public ClientAsymmetricSignatureVerifierContext(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");
}
return key;
}
}

View file

@ -0,0 +1,35 @@
/*
* 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.crypto;
import org.keycloak.models.KeycloakSession;
public class ES256ClientSignatureVerifierProviderFactory implements ClientSignatureVerifierProviderFactory {
public static final String ID = Algorithm.ES256;
@Override
public String getId() {
return ID;
}
@Override
public ClientSignatureVerifierProvider create(KeycloakSession session) {
return new AsymmetricClientSignatureVerifierProvider(session, Algorithm.ES256);
}
}

View file

@ -0,0 +1,35 @@
/*
* 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.crypto;
import org.keycloak.models.KeycloakSession;
public class ES384ClientSignatureVerifierProviderFactory implements ClientSignatureVerifierProviderFactory {
public static final String ID = Algorithm.ES384;
@Override
public String getId() {
return ID;
}
@Override
public ClientSignatureVerifierProvider create(KeycloakSession session) {
return new AsymmetricClientSignatureVerifierProvider(session, Algorithm.ES384);
}
}

View file

@ -0,0 +1,35 @@
/*
* 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.crypto;
import org.keycloak.models.KeycloakSession;
public class ES512ClientSignatureVerifierProviderFactory implements ClientSignatureVerifierProviderFactory {
public static final String ID = Algorithm.ES512;
@Override
public String getId() {
return ID;
}
@Override
public ClientSignatureVerifierProvider create(KeycloakSession session) {
return new AsymmetricClientSignatureVerifierProvider(session, Algorithm.ES512);
}
}

View file

@ -0,0 +1,35 @@
/*
* 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.crypto;
import org.keycloak.models.KeycloakSession;
public class RS256ClientSignatureVerifierProviderFactory implements ClientSignatureVerifierProviderFactory {
public static final String ID = Algorithm.RS256;
@Override
public String getId() {
return ID;
}
@Override
public ClientSignatureVerifierProvider create(KeycloakSession session) {
return new AsymmetricClientSignatureVerifierProvider(session, Algorithm.RS256);
}
}

View file

@ -0,0 +1,35 @@
/*
* 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.crypto;
import org.keycloak.models.KeycloakSession;
public class RS384ClientSignatureVerifierProviderFactory implements ClientSignatureVerifierProviderFactory {
public static final String ID = Algorithm.RS384;
@Override
public String getId() {
return ID;
}
@Override
public ClientSignatureVerifierProvider create(KeycloakSession session) {
return new AsymmetricClientSignatureVerifierProvider(session, Algorithm.RS384);
}
}

View file

@ -0,0 +1,35 @@
/*
* 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.crypto;
import org.keycloak.models.KeycloakSession;
public class RS512ClientSignatureVerifierProviderFactory implements ClientSignatureVerifierProviderFactory {
public static final String ID = Algorithm.RS512;
@Override
public String getId() {
return ID;
}
@Override
public ClientSignatureVerifierProvider create(KeycloakSession session) {
return new AsymmetricClientSignatureVerifierProvider(session, Algorithm.RS512);
}
}

View file

@ -20,6 +20,7 @@ import org.jboss.logging.Logger;
import org.keycloak.Token;
import org.keycloak.TokenCategory;
import org.keycloak.crypto.Algorithm;
import org.keycloak.crypto.ClientSignatureVerifierProvider;
import org.keycloak.crypto.KeyUse;
import org.keycloak.crypto.SignatureProvider;
import org.keycloak.crypto.SignatureSignerContext;
@ -83,6 +84,29 @@ public class DefaultTokenManager implements TokenManager {
}
}
@Override
public <T> T decodeClientJWT(String token, ClientModel client, Class<T> clazz) {
if (token == null) {
return null;
}
try {
JWSInput jws = new JWSInput(token);
String signatureAlgorithm = jws.getHeader().getAlgorithm().name();
ClientSignatureVerifierProvider signatureProvider = session.getProvider(ClientSignatureVerifierProvider.class, signatureAlgorithm);
if (signatureProvider == null) {
return null;
}
boolean valid = signatureProvider.verifier(client, jws).verify(jws.getEncodedSignatureInput().getBytes("UTF-8"), jws.getSignature());
return valid ? jws.readJsonContent(clazz) : null;
} catch (Exception e) {
logger.debug("Failed to decode token", e);
return null;
}
}
@Override
public String signatureAlgorithm(TokenCategory category) {
switch (category) {

View file

@ -20,6 +20,10 @@ package org.keycloak.keys.loader;
import org.jboss.logging.Logger;
import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator;
import org.keycloak.common.util.KeyUtils;
import org.keycloak.crypto.Algorithm;
import org.keycloak.crypto.KeyType;
import org.keycloak.crypto.KeyUse;
import org.keycloak.crypto.KeyWrapper;
import org.keycloak.jose.jwk.JSONWebKeySet;
import org.keycloak.jose.jwk.JWK;
import org.keycloak.keys.PublicKeyLoader;
@ -56,21 +60,18 @@ public class ClientPublicKeyLoader implements PublicKeyLoader {
@Override
public Map<String, PublicKey> loadKeys() throws Exception {
public Map<String, KeyWrapper> loadKeys() throws Exception {
OIDCAdvancedConfigWrapper config = OIDCAdvancedConfigWrapper.fromClientModel(client);
if (config.isUseJwksUrl()) {
String jwksUrl = config.getJwksUrl();
jwksUrl = ResolveRelative.resolveRelativeUri(session.getContext().getUri().getRequestUri(), client.getRootUrl(), jwksUrl);
JSONWebKeySet jwks = JWKSHttpUtils.sendJwksRequest(session, jwksUrl);
return JWKSUtils.getKeysForUse(jwks, JWK.Use.SIG);
return JWKSUtils.getKeyWrappersForUse(jwks, JWK.Use.SIG);
} else {
try {
CertificateRepresentation certInfo = CertificateInfoHelper.getCertificateFromClient(client, JWTClientAuthenticator.ATTR_PREFIX);
PublicKey publicKey = getSignatureValidationKey(certInfo);
// Check if we have kid in DB, generate otherwise
String kid = certInfo.getKid() != null ? certInfo.getKid() : KeyUtils.createKeyId(publicKey);
return Collections.singletonMap(kid, publicKey);
KeyWrapper publicKey = getSignatureValidationKey(certInfo);
return Collections.singletonMap(publicKey.getKid(), publicKey);
} catch (ModelException me) {
logger.warnf(me, "Unable to retrieve publicKey for verify signature of client '%s' . Error details: %s", client.getClientId(), me.getMessage());
return Collections.emptyMap();
@ -79,7 +80,8 @@ public class ClientPublicKeyLoader implements PublicKeyLoader {
}
}
private static PublicKey getSignatureValidationKey(CertificateRepresentation certInfo) throws ModelException {
private static KeyWrapper getSignatureValidationKey(CertificateRepresentation certInfo) throws ModelException {
KeyWrapper keyWrapper = new KeyWrapper();
String encodedCertificate = certInfo.getCertificate();
String encodedPublicKey = certInfo.getPublicKey();
@ -91,12 +93,25 @@ public class ClientPublicKeyLoader implements PublicKeyLoader {
throw new ModelException("Client has both publicKey and certificate configured");
}
keyWrapper.setAlgorithm(Algorithm.RS256);
keyWrapper.setType(KeyType.RSA);
keyWrapper.setUse(KeyUse.SIG);
String kid = null;
if (encodedCertificate != null) {
X509Certificate clientCert = KeycloakModelUtils.getCertificate(encodedCertificate);
return clientCert.getPublicKey();
// Check if we have kid in DB, generate otherwise
kid = certInfo.getKid() != null ? certInfo.getKid() : KeyUtils.createKeyId(clientCert.getPublicKey());
keyWrapper.setKid(kid);
keyWrapper.setVerifyKey(clientCert.getPublicKey());
keyWrapper.setCertificate(clientCert);
} else {
return KeycloakModelUtils.getPublicKey(encodedPublicKey);
PublicKey publicKey = KeycloakModelUtils.getPublicKey(encodedPublicKey);
// Check if we have kid in DB, generate otherwise
kid = certInfo.getKid() != null ? certInfo.getKid() : KeyUtils.createKeyId(publicKey);
keyWrapper.setKid(kid);
keyWrapper.setVerifyKey(publicKey);
}
return keyWrapper;
}

View file

@ -17,9 +17,12 @@
package org.keycloak.keys.loader;
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.KeyWrapper;
import org.keycloak.keys.PublicKeyLoader;
import java.security.PublicKey;
import java.util.Collections;
import java.util.Map;
@ -38,15 +41,20 @@ public class HardcodedPublicKeyLoader implements PublicKeyLoader {
}
@Override
public Map<String, PublicKey> loadKeys() throws Exception {
public Map<String, KeyWrapper> loadKeys() throws Exception {
return Collections.unmodifiableMap(Collections.singletonMap(kid, getSavedPublicKey()));
}
protected PublicKey getSavedPublicKey() {
protected KeyWrapper getSavedPublicKey() {
KeyWrapper keyWrapper = null;
if (pem != null && ! pem.trim().equals("")) {
return PemUtils.decodePublicKey(pem);
} else {
return null;
keyWrapper = new KeyWrapper();
keyWrapper.setKid(kid);
keyWrapper.setType(KeyType.RSA);
keyWrapper.setAlgorithm(Algorithm.RS256);
keyWrapper.setUse(KeyUse.SIG);
keyWrapper.setVerifyKey(PemUtils.decodePublicKey(pem));
}
return keyWrapper;
}
}

View file

@ -21,6 +21,10 @@ import org.jboss.logging.Logger;
import org.keycloak.broker.oidc.OIDCIdentityProviderConfig;
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.KeyWrapper;
import org.keycloak.jose.jwk.JSONWebKeySet;
import org.keycloak.jose.jwk.JWK;
import org.keycloak.keys.PublicKeyLoader;
@ -48,23 +52,18 @@ public class OIDCIdentityProviderPublicKeyLoader implements PublicKeyLoader {
}
@Override
public Map<String, PublicKey> loadKeys() throws Exception {
public Map<String, KeyWrapper> loadKeys() throws Exception {
if (config.isUseJwksUrl()) {
String jwksUrl = config.getJwksUrl();
JSONWebKeySet jwks = JWKSHttpUtils.sendJwksRequest(session, jwksUrl);
return JWKSUtils.getKeysForUse(jwks, JWK.Use.SIG);
return JWKSUtils.getKeyWrappersForUse(jwks, JWK.Use.SIG);
} else {
try {
PublicKey publicKey = getSavedPublicKey();
KeyWrapper publicKey = getSavedPublicKey();
if (publicKey == null) {
return Collections.emptyMap();
}
String presetKeyId = config.getPublicKeySignatureVerifierKeyId();
String kid = (presetKeyId == null || presetKeyId.trim().isEmpty())
? KeyUtils.createKeyId(publicKey)
: presetKeyId;
return Collections.singletonMap(kid, publicKey);
return Collections.singletonMap(publicKey.getKid(), publicKey);
} catch (Exception e) {
logger.warnf(e, "Unable to retrieve publicKey for verify signature of identityProvider '%s' . Error details: %s", config.getAlias(), e.getMessage());
return Collections.emptyMap();
@ -72,12 +71,23 @@ public class OIDCIdentityProviderPublicKeyLoader implements PublicKeyLoader {
}
}
protected PublicKey getSavedPublicKey() throws Exception {
protected KeyWrapper getSavedPublicKey() throws Exception {
KeyWrapper keyWrapper = null;
if (config.getPublicKeySignatureVerifier() != null && !config.getPublicKeySignatureVerifier().trim().equals("")) {
return PemUtils.decodePublicKey(config.getPublicKeySignatureVerifier());
PublicKey publicKey = PemUtils.decodePublicKey(config.getPublicKeySignatureVerifier());
keyWrapper = new KeyWrapper();
String presetKeyId = config.getPublicKeySignatureVerifierKeyId();
String kid = (presetKeyId == null || presetKeyId.trim().isEmpty())
? KeyUtils.createKeyId(publicKey)
: presetKeyId;
keyWrapper.setKid(kid);
keyWrapper.setType(KeyType.RSA);
keyWrapper.setAlgorithm(Algorithm.RS256);
keyWrapper.setUse(KeyUse.SIG);
keyWrapper.setVerifyKey(publicKey);
} else {
logger.warnf("No public key saved on identityProvider %s", config.getAlias());
return null;
}
return keyWrapper;
}
}

View file

@ -19,6 +19,7 @@ package org.keycloak.keys.loader;
import org.jboss.logging.Logger;
import org.keycloak.broker.oidc.OIDCIdentityProviderConfig;
import org.keycloak.crypto.KeyWrapper;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.keys.PublicKeyLoader;
import org.keycloak.keys.PublicKeyStorageProvider;
@ -37,16 +38,22 @@ public class PublicKeyStorageManager {
private static final Logger logger = Logger.getLogger(PublicKeyStorageManager.class);
public static PublicKey getClientPublicKey(KeycloakSession session, ClientModel client, JWSInput input) {
KeyWrapper keyWrapper = getClientPublicKeyWrapper(session, client, input);
PublicKey publicKey = null;
if (keyWrapper != null) {
publicKey = (PublicKey)keyWrapper.getVerifyKey();
}
return publicKey;
}
public static KeyWrapper getClientPublicKeyWrapper(KeycloakSession session, ClientModel client, JWSInput input) {
String kid = input.getHeader().getKeyId();
PublicKeyStorageProvider keyStorage = session.getProvider(PublicKeyStorageProvider.class);
String modelKey = PublicKeyStorageUtils.getClientModelCacheKey(client.getRealm().getId(), client.getId());
ClientPublicKeyLoader loader = new ClientPublicKeyLoader(session, client);
return keyStorage.getPublicKey(modelKey, kid, loader);
}
public static PublicKey getIdentityProviderPublicKey(KeycloakSession session, RealmModel realm, OIDCIdentityProviderConfig idpConfig, JWSInput input) {
boolean keyIdSetInConfiguration = idpConfig.getPublicKeySignatureVerifierKeyId() != null
&& ! idpConfig.getPublicKeySignatureVerifierKeyId().trim().isEmpty();
@ -73,6 +80,6 @@ public class PublicKeyStorageManager {
: kid, pem);
}
return keyStorage.getPublicKey(modelKey, kid, loader);
return (PublicKey)keyStorage.getPublicKey(modelKey, kid, loader).getVerifyKey();
}
}

View file

@ -20,6 +20,7 @@ package org.keycloak.protocol.oidc;
import org.keycloak.OAuth2Constants;
import org.keycloak.authentication.ClientAuthenticator;
import org.keycloak.authentication.ClientAuthenticatorFactory;
import org.keycloak.crypto.ClientSignatureVerifierProvider;
import org.keycloak.crypto.SignatureProvider;
import org.keycloak.jose.jws.Algorithm;
import org.keycloak.models.ClientScopeModel;
@ -46,8 +47,6 @@ import java.util.List;
*/
public class OIDCWellKnownProvider implements WellKnownProvider {
public static final List<String> DEFAULT_REQUEST_OBJECT_SIGNING_ALG_VALUES_SUPPORTED = list(Algorithm.none.toString(), Algorithm.RS256.toString());
public static final List<String> DEFAULT_GRANT_TYPES_SUPPORTED = list(OAuth2Constants.AUTHORIZATION_CODE, OAuth2Constants.IMPLICIT, OAuth2Constants.REFRESH_TOKEN, OAuth2Constants.PASSWORD, OAuth2Constants.CLIENT_CREDENTIALS);
public static final List<String> DEFAULT_RESPONSE_TYPES_SUPPORTED = list(OAuth2Constants.CODE, OIDCResponseType.NONE, OIDCResponseType.ID_TOKEN, OIDCResponseType.TOKEN, "id_token token", "code id_token", "code token", "code id_token token");
@ -92,7 +91,7 @@ public class OIDCWellKnownProvider implements WellKnownProvider {
config.setIdTokenSigningAlgValuesSupported(getSupportedSigningAlgorithms(false));
config.setUserInfoSigningAlgValuesSupported(getSupportedSigningAlgorithms(true));
config.setRequestObjectSigningAlgValuesSupported(DEFAULT_REQUEST_OBJECT_SIGNING_ALG_VALUES_SUPPORTED);
config.setRequestObjectSigningAlgValuesSupported(getSupportedClientSigningAlgorithms(true));
config.setResponseTypesSupported(DEFAULT_RESPONSE_TYPES_SUPPORTED);
config.setSubjectTypesSupported(DEFAULT_SUBJECT_TYPES_SUPPORTED);
config.setResponseModesSupported(DEFAULT_RESPONSE_MODES_SUPPORTED);
@ -163,4 +162,14 @@ public class OIDCWellKnownProvider implements WellKnownProvider {
return result;
}
private List<String> getSupportedClientSigningAlgorithms(boolean includeNone) {
List<String> result = new LinkedList<>();
for (ProviderFactory s : session.getKeycloakSessionFactory().getProviderFactories(ClientSignatureVerifierProvider.class)) {
result.add(s.getId());
}
if (includeNone) {
result.add("none");
}
return result;
}
}

View file

@ -17,16 +17,15 @@
package org.keycloak.protocol.oidc.endpoints.request;
import com.fasterxml.jackson.databind.JsonNode;
import java.security.PublicKey;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
import org.keycloak.crypto.SignatureProvider;
import org.keycloak.crypto.SignatureVerifierContext;
import org.keycloak.jose.jws.Algorithm;
import org.keycloak.jose.jws.JWSHeader;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.crypto.RSAProvider;
import org.keycloak.keys.loader.PublicKeyStorageManager;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
@ -44,29 +43,24 @@ class AuthzEndpointRequestObjectParser extends AuthzEndpointRequestParser {
public AuthzEndpointRequestObjectParser(KeycloakSession session, String requestObject, ClientModel client) throws Exception {
JWSInput input = new JWSInput(requestObject);
JWSHeader header = input.getHeader();
Algorithm headerAlgorithm = header.getAlgorithm();
Algorithm requestedSignatureAlgorithm = OIDCAdvancedConfigWrapper.fromClientModel(client).getRequestObjectSignatureAlg();
if (requestedSignatureAlgorithm != null && requestedSignatureAlgorithm != header.getAlgorithm()) {
if (headerAlgorithm == null) {
throw new RuntimeException("Request object signed algorithm not specified");
}
if (requestedSignatureAlgorithm != null && requestedSignatureAlgorithm != headerAlgorithm) {
throw new RuntimeException("Request object signed with different algorithm than client requested algorithm");
}
if (header.getAlgorithm() == Algorithm.none) {
this.requestParams = JsonSerialization.readValue(input.getContent(), JsonNode.class);
} else if (header.getAlgorithm() == Algorithm.RS256) {
PublicKey clientPublicKey = PublicKeyStorageManager.getClientPublicKey(session, client, input);
if (clientPublicKey == null) {
throw new RuntimeException("Client public key not found");
}
boolean verified = RSAProvider.verify(input, clientPublicKey);
if (!verified) {
throw new RuntimeException("Failed to verify signature on 'request' object");
}
this.requestParams = JsonSerialization.readValue(input.getContent(), JsonNode.class);
} else {
throw new RuntimeException("Unsupported JWA algorithm used for signed request");
this.requestParams = session.tokens().decodeClientJWT(requestObject, client, JsonNode.class);
if (this.requestParams == null) {
throw new RuntimeException("Failed to verify signature on 'request' object");
}
}
}

View file

@ -0,0 +1,6 @@
org.keycloak.crypto.RS256ClientSignatureVerifierProviderFactory
org.keycloak.crypto.RS384ClientSignatureVerifierProviderFactory
org.keycloak.crypto.RS512ClientSignatureVerifierProviderFactory
org.keycloak.crypto.ES256ClientSignatureVerifierProviderFactory
org.keycloak.crypto.ES384ClientSignatureVerifierProviderFactory
org.keycloak.crypto.ES512ClientSignatureVerifierProviderFactory

View file

@ -18,6 +18,8 @@
package org.keycloak.testsuite.rest;
import org.keycloak.Config.Scope;
import org.keycloak.crypto.Algorithm;
import org.keycloak.crypto.KeyType;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.representations.adapters.action.LogoutAction;
@ -70,6 +72,8 @@ public class TestApplicationResourceProviderFactory implements RealmResourceProv
private KeyPair signingKeyPair;
private String oidcRequest;
private List<String> sectorIdentifierRedirectUris;
private String signingKeyType = KeyType.RSA;
private String signingKeyAlgorithm = Algorithm.RS256;
public KeyPair getSigningKeyPair() {
return signingKeyPair;
@ -94,5 +98,21 @@ public class TestApplicationResourceProviderFactory implements RealmResourceProv
public void setSectorIdentifierRedirectUris(List<String> sectorIdentifierRedirectUris) {
this.sectorIdentifierRedirectUris = sectorIdentifierRedirectUris;
}
public String getSigningKeyType() {
return signingKeyType;
}
public void setSigningKeyType(String signingKeyType) {
this.signingKeyType = signingKeyType;
}
public String getSigningKeyAlgorithm() {
return signingKeyAlgorithm;
}
public void setSigningKeyAlgorithm(String signingKeyAlgorithm) {
this.signingKeyAlgorithm = signingKeyAlgorithm;
}
}
}

View file

@ -22,6 +22,10 @@ import org.jboss.resteasy.spi.BadRequestException;
import org.keycloak.OAuth2Constants;
import org.keycloak.common.util.KeyUtils;
import org.keycloak.common.util.PemUtils;
import org.keycloak.crypto.AsymmetricSignatureSignerContext;
import org.keycloak.crypto.KeyType;
import org.keycloak.crypto.KeyWrapper;
import org.keycloak.crypto.SignatureSignerContext;
import org.keycloak.jose.jwk.JSONWebKeySet;
import org.keycloak.jose.jwk.JWK;
import org.keycloak.jose.jwk.JWKBuilder;
@ -36,10 +40,13 @@ import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import java.security.InvalidAlgorithmParameterException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.SecureRandom;
import java.security.spec.ECGenParameterSpec;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@ -63,17 +70,52 @@ public class TestingOIDCEndpointsApplicationResource {
@Produces(MediaType.APPLICATION_JSON)
@Path("/generate-keys")
@NoCache
public Map<String, String> generateKeys() {
public Map<String, String> generateKeys(@QueryParam("jwaAlgorithm") String jwaAlgorithm) {
try {
KeyPair keyPair = KeyUtils.generateRsaKeyPair(2048);
KeyPair keyPair = null;
if (jwaAlgorithm == null) jwaAlgorithm = org.keycloak.crypto.Algorithm.RS256;
String keyType = null;
switch (jwaAlgorithm) {
case org.keycloak.crypto.Algorithm.RS256:
case org.keycloak.crypto.Algorithm.RS384:
case org.keycloak.crypto.Algorithm.RS512:
keyType = KeyType.RSA;
keyPair = KeyUtils.generateRsaKeyPair(2048);
break;
case org.keycloak.crypto.Algorithm.ES256:
keyType = KeyType.EC;
keyPair = generateEcdsaKey("secp256r1");
break;
case org.keycloak.crypto.Algorithm.ES384:
keyType = KeyType.EC;
keyPair = generateEcdsaKey("secp384r1");
break;
case org.keycloak.crypto.Algorithm.ES512:
keyType = KeyType.EC;
keyPair = generateEcdsaKey("secp521r1");
break;
default :
throw new RuntimeException("Unsupported signature algorithm");
}
clientData.setSigningKeyPair(keyPair);
clientData.setSigningKeyType(keyType);
clientData.setSigningKeyAlgorithm(jwaAlgorithm);
} catch (Exception e) {
throw new BadRequestException("Error generating signing keypair", e);
}
return getKeysAsPem();
}
private KeyPair generateEcdsaKey(String ecDomainParamName) throws NoSuchAlgorithmException, InvalidAlgorithmParameterException {
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("EC");
SecureRandom randomGen = SecureRandom.getInstance("SHA1PRNG");
ECGenParameterSpec ecSpec = new ECGenParameterSpec(ecDomainParamName);
keyGen.initialize(ecSpec, randomGen);
KeyPair keyPair = keyGen.generateKeyPair();
return keyPair;
}
@GET
@Produces(MediaType.APPLICATION_JSON)
@ -95,11 +137,18 @@ public class TestingOIDCEndpointsApplicationResource {
@NoCache
public JSONWebKeySet getJwks() {
JSONWebKeySet keySet = new JSONWebKeySet();
KeyPair signingKeyPair = clientData.getSigningKeyPair();
String signingKeyAlgorithm = clientData.getSigningKeyAlgorithm();
String signingKeyType = clientData.getSigningKeyType();
if (clientData.getSigningKeyPair() == null) {
if (signingKeyPair == null || !isSupportedSigningAlgorithm(signingKeyAlgorithm)) {
keySet.setKeys(new JWK[] {});
} else if (KeyType.RSA.equals(signingKeyType)) {
keySet.setKeys(new JWK[] { JWKBuilder.create().algorithm(signingKeyAlgorithm).rsa(signingKeyPair.getPublic()) });
} else if (KeyType.EC.equals(signingKeyType)) {
keySet.setKeys(new JWK[] { JWKBuilder.create().algorithm(signingKeyAlgorithm).ec(signingKeyPair.getPublic()) });
} else {
keySet.setKeys(new JWK[] { JWKBuilder.create().rs256(clientData.getSigningKeyPair().getPublic()) });
keySet.setKeys(new JWK[] {});
}
return keySet;
@ -113,6 +162,7 @@ public class TestingOIDCEndpointsApplicationResource {
public void setOIDCRequest(@QueryParam("realmName") String realmName, @QueryParam("clientId") String clientId,
@QueryParam("redirectUri") String redirectUri, @QueryParam("maxAge") String maxAge,
@QueryParam("jwaAlgorithm") String jwaAlgorithm) {
Map<String, Object> oidcRequest = new HashMap<>();
oidcRequest.put(OIDCLoginProtocol.CLIENT_ID_PARAM, clientId);
oidcRequest.put(OIDCLoginProtocol.RESPONSE_TYPE_PARAM, OAuth2Constants.CODE);
@ -121,21 +171,38 @@ public class TestingOIDCEndpointsApplicationResource {
oidcRequest.put(OIDCLoginProtocol.MAX_AGE_PARAM, Integer.parseInt(maxAge));
}
Algorithm alg = Enum.valueOf(Algorithm.class, jwaAlgorithm);
if (alg == Algorithm.none) {
clientData.setOidcRequest(new JWSBuilder().jsonContent(oidcRequest).none());
} else if (alg == Algorithm.RS256) {
if (clientData.getSigningKeyPair() == null) {
throw new BadRequestException("Requested RS256, but signing key not set");
}
if (!isSupportedSigningAlgorithm(jwaAlgorithm)) throw new BadRequestException("Unknown argument: " + jwaAlgorithm);
if ("none".equals(jwaAlgorithm)) {
clientData.setOidcRequest(new JWSBuilder().jsonContent(oidcRequest).none());
} else if (clientData.getSigningKeyPair() == null) {
throw new BadRequestException("signing key not set");
} else {
PrivateKey privateKey = clientData.getSigningKeyPair().getPrivate();
String kid = KeyUtils.createKeyId(clientData.getSigningKeyPair().getPublic());
clientData.setOidcRequest(new JWSBuilder().kid(kid).jsonContent(oidcRequest).rsa256(privateKey));
} else {
throw new BadRequestException("Unknown argument: " + jwaAlgorithm);
KeyWrapper keyWrapper = new KeyWrapper();
keyWrapper.setAlgorithm(clientData.getSigningKeyAlgorithm());
keyWrapper.setKid(kid);
keyWrapper.setSignKey(privateKey);
SignatureSignerContext signer = new AsymmetricSignatureSignerContext(keyWrapper);
clientData.setOidcRequest(new JWSBuilder().kid(kid).jsonContent(oidcRequest).sign(signer));
}
}
private boolean isSupportedSigningAlgorithm(String signingAlgorithm) {
boolean ret = false;
switch (signingAlgorithm) {
case "none":
case org.keycloak.crypto.Algorithm.RS256:
case org.keycloak.crypto.Algorithm.RS384:
case org.keycloak.crypto.Algorithm.RS512:
case org.keycloak.crypto.Algorithm.ES256:
case org.keycloak.crypto.Algorithm.ES384:
case org.keycloak.crypto.Algorithm.ES512:
ret = true;
}
return ret;
}
@GET

View file

@ -35,7 +35,7 @@ public interface TestOIDCEndpointsApplicationResource {
@GET
@Produces(MediaType.APPLICATION_JSON)
@Path("/generate-keys")
Map<String, String> generateKeys();
Map<String, String> generateKeys(@QueryParam("jwaAlgorithm") String jwaAlgorithm);
@GET
@Produces(MediaType.APPLICATION_JSON)

View file

@ -207,17 +207,30 @@ public class OIDCClientRegistrationTest extends AbstractClientRegistrationTest {
@Test
public void testSignaturesRequired() throws Exception {
OIDCClientRepresentation clientRep = createRep();
clientRep.setUserinfoSignedResponseAlg(Algorithm.RS256.toString());
clientRep.setRequestObjectSigningAlg(Algorithm.RS256.toString());
clientRep.setUserinfoSignedResponseAlg(Algorithm.ES256.toString());
clientRep.setRequestObjectSigningAlg(Algorithm.ES256.toString());
OIDCClientRepresentation response = reg.oidc().create(clientRep);
Assert.assertEquals(Algorithm.RS256.toString(), response.getUserinfoSignedResponseAlg());
Assert.assertEquals(Algorithm.RS256.toString(), response.getRequestObjectSigningAlg());
Assert.assertEquals(Algorithm.ES256.toString(), response.getUserinfoSignedResponseAlg());
Assert.assertEquals(Algorithm.ES256.toString(), response.getRequestObjectSigningAlg());
Assert.assertNotNull(response.getClientSecret());
// Test Keycloak representation
ClientRepresentation kcClient = getClient(response.getClientId());
OIDCAdvancedConfigWrapper config = OIDCAdvancedConfigWrapper.fromClientRepresentation(kcClient);
Assert.assertEquals(config.getUserInfoSignedResponseAlg(), Algorithm.ES256);
Assert.assertEquals(config.getRequestObjectSignatureAlg(), Algorithm.ES256);
// update (ES256 to RS256)
clientRep.setUserinfoSignedResponseAlg(Algorithm.RS256.toString());
clientRep.setRequestObjectSigningAlg(Algorithm.RS256.toString());
response = reg.oidc().create(clientRep);
Assert.assertEquals(Algorithm.RS256.toString(), response.getUserinfoSignedResponseAlg());
Assert.assertEquals(Algorithm.RS256.toString(), response.getRequestObjectSigningAlg());
// keycloak representation
kcClient = getClient(response.getClientId());
config = OIDCAdvancedConfigWrapper.fromClientRepresentation(kcClient);
Assert.assertEquals(config.getUserInfoSignedResponseAlg(), Algorithm.RS256);
Assert.assertEquals(config.getRequestObjectSignatureAlg(), Algorithm.RS256);
}

View file

@ -28,7 +28,6 @@ import java.util.Map;
import javax.ws.rs.core.UriBuilder;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
@ -49,7 +48,6 @@ import org.keycloak.jose.jwk.JWK;
import org.keycloak.jose.jwk.JWKBuilder;
import org.keycloak.jose.jws.JWSBuilder;
import org.keycloak.keys.PublicKeyStorageUtils;
import org.keycloak.keys.loader.PublicKeyStorageManager;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
@ -106,7 +104,7 @@ public class OIDCJwksClientRegistrationTest extends AbstractClientRegistrationTe
// Generate keys for client
TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints();
Map<String, String> generatedKeys = oidcClientEndpointsResource.generateKeys();
Map<String, String> generatedKeys = oidcClientEndpointsResource.generateKeys("RS256");
JSONWebKeySet keySet = oidcClientEndpointsResource.getJwks();
clientRep.setJwks(keySet);
@ -131,7 +129,7 @@ public class OIDCJwksClientRegistrationTest extends AbstractClientRegistrationTe
// Generate keys for client
TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints();
Map<String, String> generatedKeys = oidcClientEndpointsResource.generateKeys();
Map<String, String> generatedKeys = oidcClientEndpointsResource.generateKeys("RS256");
JSONWebKeySet keySet = oidcClientEndpointsResource.getJwks();
clientRep.setJwks(keySet);
@ -163,7 +161,7 @@ public class OIDCJwksClientRegistrationTest extends AbstractClientRegistrationTe
// Generate keys for client
TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints();
oidcClientEndpointsResource.generateKeys();
oidcClientEndpointsResource.generateKeys("RS256");
JSONWebKeySet keySet = oidcClientEndpointsResource.getJwks();
@ -250,7 +248,7 @@ public class OIDCJwksClientRegistrationTest extends AbstractClientRegistrationTe
// Generate keys for client
TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints();
Map<String, String> generatedKeys = oidcClientEndpointsResource.generateKeys();
Map<String, String> generatedKeys = oidcClientEndpointsResource.generateKeys("RS256");
clientRep.setJwksUri(TestApplicationResourceUrls.clientJwksUri());
@ -273,7 +271,7 @@ public class OIDCJwksClientRegistrationTest extends AbstractClientRegistrationTe
// Generate keys for client
TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints();
Map<String, String> generatedKeys = oidcClientEndpointsResource.generateKeys();
Map<String, String> generatedKeys = oidcClientEndpointsResource.generateKeys("RS256");
clientRep.setJwksUri(TestApplicationResourceUrls.clientJwksUri());
@ -287,7 +285,7 @@ public class OIDCJwksClientRegistrationTest extends AbstractClientRegistrationTe
assertAuthenticateClientSuccess(generatedKeys, response, KEEP_GENERATED_KID);
// Add new key to the jwks
Map<String, String> generatedKeys2 = oidcClientEndpointsResource.generateKeys();
Map<String, String> generatedKeys2 = oidcClientEndpointsResource.generateKeys("RS256");
// Error should happen. KeyStorageProvider won't yet download new keys because of timeout
assertAuthenticateClientError(generatedKeys2, response, KEEP_GENERATED_KID);

View file

@ -796,7 +796,7 @@ public class OIDCAdvancedRequestParamsTest extends AbstractTestRealmKeycloakTest
assertEquals("Invalid Request", errorPage.getError());
// Generate keypair for client
String clientPublicKeyPem = oidcClientEndpointsResource.generateKeys().get(TestingOIDCEndpointsApplicationResource.PUBLIC_KEY);
String clientPublicKeyPem = oidcClientEndpointsResource.generateKeys(null).get(TestingOIDCEndpointsApplicationResource.PUBLIC_KEY);
// Verify signed request_uri will fail due to failed signature validation
oidcClientEndpointsResource.setOIDCRequest("test", "test-app", validRedirectUri, "10", Algorithm.RS256.toString());
@ -826,6 +826,117 @@ public class OIDCAdvancedRequestParamsTest extends AbstractTestRealmKeycloakTest
clientResource.update(clientRep);
}
private void requestUriParamSignedIn(Algorithm expectedAlgorithm, Algorithm actualAlgorithm) throws Exception {
ClientResource clientResource = null;
ClientRepresentation clientRep = null;
try {
oauth.stateParamHardcoded("mystate3");
String validRedirectUri = oauth.getRedirectUri();
TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints();
// Set required signature for request_uri
clientResource = ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app");
clientRep = clientResource.toRepresentation();
OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setRequestObjectSignatureAlg(expectedAlgorithm);
clientResource.update(clientRep);
// generate and register client keypair
if (Algorithm.none != actualAlgorithm) oidcClientEndpointsResource.generateKeys(actualAlgorithm.name());
// register request object
oidcClientEndpointsResource.setOIDCRequest("test", "test-app", validRedirectUri, "10", actualAlgorithm.name());
// use and set jwks_url
clientResource = ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app");
clientRep = clientResource.toRepresentation();
OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setUseJwksUrl(true);
String jwksUrl = TestApplicationResourceUrls.clientJwksUri();
OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setJwksUrl(jwksUrl);
clientResource.update(clientRep);
// set time offset, so that new keys are downloaded
setTimeOffset(20);
oauth.requestUri(TestApplicationResourceUrls.clientRequestUri());
if (expectedAlgorithm == null || expectedAlgorithm == actualAlgorithm) {
// Check signed request_uri will pass
OAuthClient.AuthorizationEndpointResponse response = oauth.doLogin("test-user@localhost", "password");
Assert.assertNotNull(response.getCode());
Assert.assertEquals("mystate3", response.getState());
assertTrue(appPage.isCurrent());
} else {
// Verify signed request_uri will fail due to failed signature validation
oauth.openLoginForm();
Assert.assertTrue(errorPage.isCurrent());
assertEquals("Invalid Request", errorPage.getError());
}
} finally {
// Revert requiring signature for client
OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setRequestObjectSignatureAlg(null);
// Revert jwks_url settings
OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setUseJwksUrl(false);
OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setJwksUrl(null);
clientResource.update(clientRep);
}
}
@Test
public void requestUriParamSignedExpectedES256ActualRS256() throws Exception {
// will fail
requestUriParamSignedIn(Algorithm.ES256, Algorithm.RS256);
}
@Test
public void requestUriParamSignedExpectedNoneActualES256() throws Exception {
// will fail
requestUriParamSignedIn(Algorithm.none, Algorithm.ES256);
}
@Test
public void requestUriParamSignedExpectedNoneActualNone() throws Exception {
// will success
requestUriParamSignedIn(Algorithm.none, Algorithm.none);
}
@Test
public void requestUriParamSignedExpectedES256ActualES256() throws Exception {
// will success
requestUriParamSignedIn(Algorithm.ES256, Algorithm.ES256);
}
@Test
public void requestUriParamSignedExpectedES384ActualES384() throws Exception {
// will success
requestUriParamSignedIn(Algorithm.ES384, Algorithm.ES384);
}
@Test
public void requestUriParamSignedExpectedES512ActualES512() throws Exception {
// will success
requestUriParamSignedIn(Algorithm.ES512, Algorithm.ES512);
}
@Test
public void requestUriParamSignedExpectedRS384ActualRS384() throws Exception {
// will success
requestUriParamSignedIn(Algorithm.RS384, Algorithm.RS384);
}
@Test
public void requestUriParamSignedExpectedRS512ActualRS512() throws Exception {
// will success
requestUriParamSignedIn(Algorithm.RS512, Algorithm.RS512);
}
@Test
public void requestUriParamSignedExpectedAnyActualES256() throws Exception {
// Algorithm is null if 'any'
// will success
requestUriParamSignedIn(null, Algorithm.ES256);
}
// LOGIN_HINT
@Test

View file

@ -97,7 +97,6 @@ public class OIDCWellKnownProviderTest extends AbstractKeycloakTest {
oauth.clientId("test-app");
}
@Test
public void testDiscovery() {
Client client = ClientBuilder.newClient();
@ -127,7 +126,7 @@ public class OIDCWellKnownProviderTest extends AbstractKeycloakTest {
Assert.assertNames(oidcConfig.getSubjectTypesSupported(), "pairwise", "public");
Assert.assertNames(oidcConfig.getIdTokenSigningAlgValuesSupported(), Algorithm.RS256, Algorithm.RS384, Algorithm.RS512, Algorithm.ES256, Algorithm.ES384, Algorithm.ES512, Algorithm.HS256, Algorithm.HS384, Algorithm.HS512);
Assert.assertNames(oidcConfig.getUserInfoSigningAlgValuesSupported(), "none", Algorithm.RS256, Algorithm.RS384, Algorithm.RS512, Algorithm.ES256, Algorithm.ES384, Algorithm.ES512, Algorithm.HS256, Algorithm.HS384, Algorithm.HS512);
Assert.assertNames(oidcConfig.getRequestObjectSigningAlgValuesSupported(), "none", Algorithm.RS256);
Assert.assertNames(oidcConfig.getRequestObjectSigningAlgValuesSupported(), "none", Algorithm.RS256, Algorithm.RS384, Algorithm.RS512, Algorithm.ES256, Algorithm.ES384, Algorithm.ES512);
// Client authentication
Assert.assertNames(oidcConfig.getTokenEndpointAuthMethodsSupported(), "client_secret_basic", "client_secret_post", "private_key_jwt", "client_secret_jwt");

View file

@ -959,12 +959,6 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, flows, $ro
{name: "INCLUSIVE_WITH_COMMENTS", value: "http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments"}
];
$scope.requestObjectSignatureAlgorithms = [
"any",
"none",
"RS256"
];
$scope.requestObjectRequiredOptions = [
"not required",
"request or request_uri",

View file

@ -438,8 +438,10 @@
<div>
<select class="form-control" id="requestObjectSignatureAlg"
ng-change="changeRequestObjectSignatureAlg()"
ng-model="requestObjectSignatureAlg"
ng-options="sig for sig in requestObjectSignatureAlgorithms">
ng-model="requestObjectSignatureAlg">
<option value="any">any</option>
<option value="none">none</option>
<option ng-repeat="provider in serverInfo.listProviderIds('clientSignature')" value="{{provider}}">{{provider}}</option>
</select>
</div>
</div>