KEYCLOAK-9089 IllegalArgumentException when trying to use ES256 as OIDC access token signature
This commit is contained in:
parent
4150daa9cb
commit
061693a8c9
22 changed files with 472 additions and 61 deletions
|
@ -37,6 +37,11 @@ public class AsymmetricSignatureSignerContext implements SignatureSignerContext
|
||||||
return key.getAlgorithm();
|
return key.getAlgorithm();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getHashAlgorithm() {
|
||||||
|
return JavaAlgorithm.getJavaAlgorithmForHash(key.getAlgorithm());
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public byte[] sign(byte[] data) throws SignatureException {
|
public byte[] sign(byte[] data) throws SignatureException {
|
||||||
try {
|
try {
|
||||||
|
|
32
core/src/main/java/org/keycloak/crypto/HashException.java
Normal file
32
core/src/main/java/org/keycloak/crypto/HashException.java
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2017 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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public class HashException extends RuntimeException {
|
||||||
|
|
||||||
|
public HashException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public HashException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
|
@ -29,6 +29,10 @@ public class JavaAlgorithm {
|
||||||
public static final String ES512 = "SHA512withECDSA";
|
public static final String ES512 = "SHA512withECDSA";
|
||||||
public static final String AES = "AES";
|
public static final String AES = "AES";
|
||||||
|
|
||||||
|
public static final String SHA256 = "SHA-256";
|
||||||
|
public static final String SHA384 = "SHA-384";
|
||||||
|
public static final String SHA512 = "SHA-512";
|
||||||
|
|
||||||
public static String getJavaAlgorithm(String algorithm) {
|
public static String getJavaAlgorithm(String algorithm) {
|
||||||
switch (algorithm) {
|
switch (algorithm) {
|
||||||
case Algorithm.RS256:
|
case Algorithm.RS256:
|
||||||
|
@ -56,4 +60,32 @@ public class JavaAlgorithm {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static String getJavaAlgorithmForHash(String algorithm) {
|
||||||
|
switch (algorithm) {
|
||||||
|
case Algorithm.RS256:
|
||||||
|
return SHA256;
|
||||||
|
case Algorithm.RS384:
|
||||||
|
return SHA384;
|
||||||
|
case Algorithm.RS512:
|
||||||
|
return SHA512;
|
||||||
|
case Algorithm.HS256:
|
||||||
|
return SHA256;
|
||||||
|
case Algorithm.HS384:
|
||||||
|
return SHA384;
|
||||||
|
case Algorithm.HS512:
|
||||||
|
return SHA512;
|
||||||
|
case Algorithm.ES256:
|
||||||
|
return SHA256;
|
||||||
|
case Algorithm.ES384:
|
||||||
|
return SHA384;
|
||||||
|
case Algorithm.ES512:
|
||||||
|
return SHA512;
|
||||||
|
case Algorithm.AES:
|
||||||
|
return AES;
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException("Unknown algorithm " + algorithm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,6 +36,11 @@ public class MacSignatureSignerContext implements SignatureSignerContext {
|
||||||
return key.getAlgorithm();
|
return key.getAlgorithm();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getHashAlgorithm() {
|
||||||
|
return JavaAlgorithm.getJavaAlgorithmForHash(key.getAlgorithm());
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public byte[] sign(byte[] data) throws SignatureException {
|
public byte[] sign(byte[] data) throws SignatureException {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -22,6 +22,8 @@ public interface SignatureSignerContext {
|
||||||
|
|
||||||
String getAlgorithm();
|
String getAlgorithm();
|
||||||
|
|
||||||
|
String getHashAlgorithm();
|
||||||
|
|
||||||
byte[] sign(byte[] data) throws SignatureException;
|
byte[] sign(byte[] data) throws SignatureException;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,51 +18,48 @@
|
||||||
package org.keycloak.jose.jws.crypto;
|
package org.keycloak.jose.jws.crypto;
|
||||||
|
|
||||||
import org.keycloak.common.util.Base64Url;
|
import org.keycloak.common.util.Base64Url;
|
||||||
import org.keycloak.jose.jws.Algorithm;
|
import org.keycloak.crypto.HashException;
|
||||||
|
import org.keycloak.crypto.JavaAlgorithm;
|
||||||
|
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
*/
|
*/
|
||||||
public class HashProvider {
|
public class HashUtils {
|
||||||
|
|
||||||
|
// See "at_hash" and "c_hash" in OIDC specification
|
||||||
public static String oidcHash(String jwtAlgorithmName, String input) {
|
public static String oidcHash(String jwtAlgorithmName, String input) {
|
||||||
byte[] digest = digest(jwtAlgorithmName, input);
|
try {
|
||||||
|
byte[] inputBytes = input.getBytes("UTF-8");
|
||||||
|
String javaAlgName = JavaAlgorithm.getJavaAlgorithmForHash(jwtAlgorithmName);
|
||||||
|
byte[] hash = hash(javaAlgName, inputBytes);
|
||||||
|
|
||||||
int hashLength = digest.length / 2;
|
return encodeHashToOIDC(hash);
|
||||||
byte[] hashInput = Arrays.copyOf(digest, hashLength);
|
} catch (UnsupportedEncodingException e) {
|
||||||
|
throw new HashException("Error when creating token hash", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static byte[] hash(String javaAlgorithmName, byte[] inputBytes) {
|
||||||
|
try {
|
||||||
|
MessageDigest md = MessageDigest.getInstance(javaAlgorithmName);
|
||||||
|
md.update(inputBytes);
|
||||||
|
return md.digest();
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new HashException("Error when creating token hash", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static String encodeHashToOIDC(byte[] hash) {
|
||||||
|
int hashLength = hash.length / 2;
|
||||||
|
byte[] hashInput = Arrays.copyOf(hash, hashLength);
|
||||||
|
|
||||||
return Base64Url.encode(hashInput);
|
return Base64Url.encode(hashInput);
|
||||||
}
|
}
|
||||||
private static byte[] digest(String algorithm, String input) {
|
|
||||||
String digestAlg = getJavaDigestAlgorithm(algorithm);
|
|
||||||
|
|
||||||
try {
|
|
||||||
MessageDigest md = MessageDigest.getInstance(digestAlg);
|
|
||||||
md.update(input.getBytes("UTF-8"));
|
|
||||||
return md.digest();
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private static String getJavaDigestAlgorithm(String alg) {
|
|
||||||
switch (alg) {
|
|
||||||
case "RS256":
|
|
||||||
return "SHA-256";
|
|
||||||
case "RS384":
|
|
||||||
return "SHA-384";
|
|
||||||
case "RS512":
|
|
||||||
return "SHA-512";
|
|
||||||
default:
|
|
||||||
throw new IllegalArgumentException("Not an RSA Algorithm");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// See "at_hash" and "c_hash" in OIDC specification
|
|
||||||
public static String oidcHash(Algorithm jwtAlgorithm, String input) {
|
|
||||||
return oidcHash(jwtAlgorithm.name(), input);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -20,8 +20,8 @@ package org.keycloak;
|
||||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.keycloak.jose.jws.Algorithm;
|
import org.keycloak.crypto.Algorithm;
|
||||||
import org.keycloak.jose.jws.crypto.HashProvider;
|
import org.keycloak.jose.jws.crypto.HashUtils;
|
||||||
|
|
||||||
import java.security.Security;
|
import java.security.Security;
|
||||||
|
|
||||||
|
@ -37,13 +37,19 @@ public class AtHashTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testAtHash() throws Exception {
|
public void testAtHashRsa() throws Exception {
|
||||||
verifyHash("jHkWEdUXMU1BwAsC4vtUsZwnNvTIxEl0z9K3vx5KF0Y", "77QmUPtjPfzWtF2AnpK9RQ");
|
verifyHash(Algorithm.RS256,"jHkWEdUXMU1BwAsC4vtUsZwnNvTIxEl0z9K3vx5KF0Y", "77QmUPtjPfzWtF2AnpK9RQ");
|
||||||
verifyHash("ya29.eQETFbFOkAs8nWHcmYXKwEi0Zz46NfsrUU_KuQLOLTwWS40y6Fb99aVzEXC0U14m61lcPMIr1hEIBA", "aUAkJG-u6x4RTWuILWy-CA");
|
verifyHash(Algorithm.RS256,"ya29.eQETFbFOkAs8nWHcmYXKwEi0Zz46NfsrUU_KuQLOLTwWS40y6Fb99aVzEXC0U14m61lcPMIr1hEIBA", "aUAkJG-u6x4RTWuILWy-CA");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void verifyHash(String accessToken, String expectedAtHash) {
|
@Test
|
||||||
String atHash = HashProvider.oidcHash(Algorithm.RS256, accessToken);
|
public void testAtHashEs() throws Exception {
|
||||||
|
verifyHash(Algorithm.ES256,"jHkWEdUXMU1BwAsC4vtUsZwnNvTIxEl0z9K3vx5KF0Y", "77QmUPtjPfzWtF2AnpK9RQ");
|
||||||
|
verifyHash(Algorithm.ES256,"ya29.eQETFbFOkAs8nWHcmYXKwEi0Zz46NfsrUU_KuQLOLTwWS40y6Fb99aVzEXC0U14m61lcPMIr1hEIBA", "aUAkJG-u6x4RTWuILWy-CA");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void verifyHash(String jwtAlgorithm, String accessToken, String expectedAtHash) {
|
||||||
|
String atHash = HashUtils.oidcHash(jwtAlgorithm, accessToken);
|
||||||
Assert.assertEquals(expectedAtHash, atHash);
|
Assert.assertEquals(expectedAtHash, atHash);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2017 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 java.io.UnsupportedEncodingException;
|
||||||
|
|
||||||
|
import org.keycloak.provider.Provider;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public interface HashProvider extends Provider {
|
||||||
|
|
||||||
|
|
||||||
|
default byte[] hash(String input) throws HashException {
|
||||||
|
try {
|
||||||
|
byte[] inputBytes = input.getBytes("UTF-8");
|
||||||
|
return hash(inputBytes);
|
||||||
|
} catch (UnsupportedEncodingException e) {
|
||||||
|
throw new HashException("Unsupported encoding when trying to hash", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
byte[] hash(byte[] input) throws HashException;
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
default void close() {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2017 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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public interface HashProviderFactory extends ProviderFactory<HashProvider> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
default void init(Config.Scope config) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
default void postInit(KeycloakSessionFactory factory) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
default void close() {
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2017 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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public class HashSpi implements Spi {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isInternal() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "hash";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Class<? extends Provider> getProviderClass() {
|
||||||
|
return HashProvider.class;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Class<? extends ProviderFactory> getProviderFactoryClass() {
|
||||||
|
return HashProviderFactory.class;
|
||||||
|
}
|
||||||
|
}
|
|
@ -73,4 +73,5 @@ org.keycloak.keys.PublicKeyStorageSpi
|
||||||
org.keycloak.keys.KeySpi
|
org.keycloak.keys.KeySpi
|
||||||
org.keycloak.storage.client.ClientStorageProviderSpi
|
org.keycloak.storage.client.ClientStorageProviderSpi
|
||||||
org.keycloak.crypto.SignatureSpi
|
org.keycloak.crypto.SignatureSpi
|
||||||
org.keycloak.crypto.ClientSignatureVerifierSpi
|
org.keycloak.crypto.ClientSignatureVerifierSpi
|
||||||
|
org.keycloak.crypto.HashSpi
|
|
@ -0,0 +1,37 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2017 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.jose.jws.crypto.HashUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public class JavaAlgorithmHashProvider implements HashProvider {
|
||||||
|
|
||||||
|
private final String javaAlgorithm;
|
||||||
|
|
||||||
|
public JavaAlgorithmHashProvider(String javaAlgorithm) {
|
||||||
|
this.javaAlgorithm = javaAlgorithm;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] hash(byte[] input) throws HashException {
|
||||||
|
return HashUtils.hash(javaAlgorithm, input);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2017 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:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public class SHA256HashProviderFactory implements HashProviderFactory {
|
||||||
|
|
||||||
|
public static final String ID = JavaAlgorithm.SHA256;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId() {
|
||||||
|
return ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HashProvider create(KeycloakSession session) {
|
||||||
|
return new JavaAlgorithmHashProvider(ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2017 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:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public class SHA384HashProviderFactory implements HashProviderFactory {
|
||||||
|
|
||||||
|
public static final String ID = JavaAlgorithm.SHA384;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId() {
|
||||||
|
return ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HashProvider create(KeycloakSession session) {
|
||||||
|
return new JavaAlgorithmHashProvider(ID);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2017 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:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public class SHA512HashProviderFactory implements HashProviderFactory {
|
||||||
|
|
||||||
|
public static final String ID = JavaAlgorithm.SHA512;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId() {
|
||||||
|
return ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HashProvider create(KeycloakSession session) {
|
||||||
|
return new JavaAlgorithmHashProvider(ID);
|
||||||
|
}
|
||||||
|
}
|
|
@ -25,11 +25,13 @@ import org.keycloak.TokenCategory;
|
||||||
import org.keycloak.cluster.ClusterProvider;
|
import org.keycloak.cluster.ClusterProvider;
|
||||||
import org.keycloak.common.ClientConnection;
|
import org.keycloak.common.ClientConnection;
|
||||||
import org.keycloak.common.util.Time;
|
import org.keycloak.common.util.Time;
|
||||||
|
import org.keycloak.crypto.HashProvider;
|
||||||
|
import org.keycloak.crypto.SignatureProvider;
|
||||||
import org.keycloak.events.Details;
|
import org.keycloak.events.Details;
|
||||||
import org.keycloak.events.Errors;
|
import org.keycloak.events.Errors;
|
||||||
import org.keycloak.events.EventBuilder;
|
import org.keycloak.events.EventBuilder;
|
||||||
import org.keycloak.jose.jws.JWSInputException;
|
import org.keycloak.jose.jws.JWSInputException;
|
||||||
import org.keycloak.jose.jws.crypto.HashProvider;
|
import org.keycloak.jose.jws.crypto.HashUtils;
|
||||||
import org.keycloak.migration.migrators.MigrationUtils;
|
import org.keycloak.migration.migrators.MigrationUtils;
|
||||||
import org.keycloak.models.AuthenticatedClientSessionModel;
|
import org.keycloak.models.AuthenticatedClientSessionModel;
|
||||||
import org.keycloak.models.ClientModel;
|
import org.keycloak.models.ClientModel;
|
||||||
|
@ -763,14 +765,14 @@ public class TokenManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
public AccessTokenResponseBuilder generateCodeHash(String code) {
|
public AccessTokenResponseBuilder generateCodeHash(String code) {
|
||||||
codeHash = HashProvider.oidcHash(session.tokens().signatureAlgorithm(TokenCategory.ID), code);
|
codeHash = generateOIDCHash(code);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Financial API - Part 2: Read and Write API Security Profile
|
// Financial API - Part 2: Read and Write API Security Profile
|
||||||
// http://openid.net/specs/openid-financial-api-part-2.html#authorization-server
|
// http://openid.net/specs/openid-financial-api-part-2.html#authorization-server
|
||||||
public AccessTokenResponseBuilder generateStateHash(String state) {
|
public AccessTokenResponseBuilder generateStateHash(String state) {
|
||||||
stateHash = HashProvider.oidcHash(session.tokens().signatureAlgorithm(TokenCategory.ID), state);
|
stateHash = generateOIDCHash(state);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -801,7 +803,7 @@ public class TokenManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (generateAccessTokenHash) {
|
if (generateAccessTokenHash) {
|
||||||
String atHash = HashProvider.oidcHash(session.tokens().signatureAlgorithm(TokenCategory.ID), res.getToken());
|
String atHash = generateOIDCHash(res.getToken());
|
||||||
idToken.setAccessTokenHash(atHash);
|
idToken.setAccessTokenHash(atHash);
|
||||||
}
|
}
|
||||||
if (codeHash != null) {
|
if (codeHash != null) {
|
||||||
|
@ -838,6 +840,18 @@ public class TokenManager {
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private String generateOIDCHash(String input) {
|
||||||
|
String signatureAlgorithm = session.tokens().signatureAlgorithm(TokenCategory.ID);
|
||||||
|
SignatureProvider signatureProvider = session.getProvider(SignatureProvider.class, signatureAlgorithm);
|
||||||
|
String hashAlgorithm = signatureProvider.signer().getHashAlgorithm();
|
||||||
|
|
||||||
|
HashProvider hashProvider = session.getProvider(HashProvider.class, hashAlgorithm);
|
||||||
|
byte[] hash = hashProvider.hash(input);
|
||||||
|
|
||||||
|
return HashUtils.encodeHashToOIDC(hash);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class RefreshResult {
|
public class RefreshResult {
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
#
|
||||||
|
# Copyright 2017 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.
|
||||||
|
#
|
||||||
|
|
||||||
|
org.keycloak.crypto.SHA256HashProviderFactory
|
||||||
|
org.keycloak.crypto.SHA384HashProviderFactory
|
||||||
|
org.keycloak.crypto.SHA512HashProviderFactory
|
|
@ -37,13 +37,13 @@ import org.keycloak.OAuth2Constants;
|
||||||
import org.keycloak.OAuthErrorException;
|
import org.keycloak.OAuthErrorException;
|
||||||
import org.keycloak.admin.client.resource.ClientResource;
|
import org.keycloak.admin.client.resource.ClientResource;
|
||||||
import org.keycloak.common.util.KeystoreUtil;
|
import org.keycloak.common.util.KeystoreUtil;
|
||||||
|
import org.keycloak.crypto.Algorithm;
|
||||||
import org.keycloak.events.Details;
|
import org.keycloak.events.Details;
|
||||||
import org.keycloak.events.EventType;
|
import org.keycloak.events.EventType;
|
||||||
import org.keycloak.jose.jws.Algorithm;
|
|
||||||
import org.keycloak.jose.jws.JWSHeader;
|
import org.keycloak.jose.jws.JWSHeader;
|
||||||
import org.keycloak.jose.jws.JWSInput;
|
import org.keycloak.jose.jws.JWSInput;
|
||||||
import org.keycloak.jose.jws.JWSInputException;
|
import org.keycloak.jose.jws.JWSInputException;
|
||||||
import org.keycloak.jose.jws.crypto.HashProvider;
|
import org.keycloak.jose.jws.crypto.HashUtils;
|
||||||
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
|
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
|
||||||
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
|
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
|
||||||
import org.keycloak.representations.AccessToken;
|
import org.keycloak.representations.AccessToken;
|
||||||
|
@ -579,7 +579,7 @@ public class HoKTest extends AbstractTestRealmKeycloakTest {
|
||||||
// Validate "c_hash"
|
// Validate "c_hash"
|
||||||
Assert.assertNull(idToken.getAccessTokenHash());
|
Assert.assertNull(idToken.getAccessTokenHash());
|
||||||
Assert.assertNotNull(idToken.getCodeHash());
|
Assert.assertNotNull(idToken.getCodeHash());
|
||||||
Assert.assertEquals(idToken.getCodeHash(), HashProvider.oidcHash(Algorithm.RS256, authzResponse.getCode()));
|
Assert.assertEquals(idToken.getCodeHash(), HashUtils.oidcHash(Algorithm.RS256, authzResponse.getCode()));
|
||||||
|
|
||||||
// IDToken exchanged for the code
|
// IDToken exchanged for the code
|
||||||
IDToken idToken2 = sendTokenRequestAndGetIDToken(loginEvent);
|
IDToken idToken2 = sendTokenRequestAndGetIDToken(loginEvent);
|
||||||
|
|
|
@ -23,7 +23,6 @@ import org.junit.Test;
|
||||||
import org.keycloak.OAuthErrorException;
|
import org.keycloak.OAuthErrorException;
|
||||||
import org.keycloak.events.Details;
|
import org.keycloak.events.Details;
|
||||||
import org.keycloak.events.Errors;
|
import org.keycloak.events.Errors;
|
||||||
import org.keycloak.jose.jws.Algorithm;
|
|
||||||
import org.keycloak.jose.jws.JWSHeader;
|
import org.keycloak.jose.jws.JWSHeader;
|
||||||
import org.keycloak.jose.jws.JWSInput;
|
import org.keycloak.jose.jws.JWSInput;
|
||||||
import org.keycloak.representations.IDToken;
|
import org.keycloak.representations.IDToken;
|
||||||
|
@ -56,9 +55,6 @@ import static org.junit.Assert.assertNull;
|
||||||
*/
|
*/
|
||||||
public abstract class AbstractOIDCResponseTypeTest extends AbstractTestRealmKeycloakTest {
|
public abstract class AbstractOIDCResponseTypeTest extends AbstractTestRealmKeycloakTest {
|
||||||
|
|
||||||
// Harcoded for now
|
|
||||||
Algorithm jwsAlgorithm = Algorithm.RS256;
|
|
||||||
|
|
||||||
@Rule
|
@Rule
|
||||||
public AssertEvents events = new AssertEvents(this);
|
public AssertEvents events = new AssertEvents(this);
|
||||||
|
|
||||||
|
@ -263,6 +259,20 @@ public abstract class AbstractOIDCResponseTypeTest extends AbstractTestRealmKeyc
|
||||||
TokenSignatureUtil.changeClientIdTokenSignatureProvider(ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app"), "RS256");
|
TokenSignatureUtil.changeClientIdTokenSignatureProvider(ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app"), "RS256");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void oidcFlow_RealmES256_ClientES384_EffectiveES384() throws Exception {
|
||||||
|
try {
|
||||||
|
setSignatureAlgorithm("ES384");
|
||||||
|
TokenSignatureUtil.changeRealmTokenSignatureProvider(adminClient, "ES256");
|
||||||
|
TokenSignatureUtil.changeClientIdTokenSignatureProvider(ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app"), "ES384");
|
||||||
|
oidcFlow("ES256", "ES384");
|
||||||
|
} finally {
|
||||||
|
setSignatureAlgorithm("RS256");
|
||||||
|
TokenSignatureUtil.changeClientIdTokenSignatureProvider(ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app"), "RS256");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private String sigAlgName = "RS256";
|
private String sigAlgName = "RS256";
|
||||||
private void setSignatureAlgorithm(String sigAlgName) {
|
private void setSignatureAlgorithm(String sigAlgName) {
|
||||||
this.sigAlgName = sigAlgName;
|
this.sigAlgName = sigAlgName;
|
||||||
|
|
|
@ -20,7 +20,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.events.Details;
|
import org.keycloak.events.Details;
|
||||||
import org.keycloak.jose.jws.crypto.HashProvider;
|
import org.keycloak.jose.jws.crypto.HashUtils;
|
||||||
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
|
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
|
||||||
import org.keycloak.representations.IDToken;
|
import org.keycloak.representations.IDToken;
|
||||||
import org.keycloak.representations.idm.EventRepresentation;
|
import org.keycloak.representations.idm.EventRepresentation;
|
||||||
|
@ -64,14 +64,14 @@ public class OIDCHybridResponseTypeCodeIDTokenTest extends AbstractOIDCResponseT
|
||||||
Assert.assertNull(idToken.getAccessTokenHash());
|
Assert.assertNull(idToken.getAccessTokenHash());
|
||||||
Assert.assertNotNull(idToken.getCodeHash());
|
Assert.assertNotNull(idToken.getCodeHash());
|
||||||
|
|
||||||
Assert.assertEquals(idToken.getCodeHash(), HashProvider.oidcHash(getSignatureAlgorithm(), authzResponse.getCode()));
|
Assert.assertEquals(idToken.getCodeHash(), HashUtils.oidcHash(getSignatureAlgorithm(), authzResponse.getCode()));
|
||||||
|
|
||||||
// Financial API - Part 2: Read and Write API Security Profile
|
// Financial API - Part 2: Read and Write API Security Profile
|
||||||
// http://openid.net/specs/openid-financial-api-part-2.html#authorization-server
|
// http://openid.net/specs/openid-financial-api-part-2.html#authorization-server
|
||||||
// Validate "s_hash"
|
// Validate "s_hash"
|
||||||
Assert.assertNotNull(idToken.getStateHash());
|
Assert.assertNotNull(idToken.getStateHash());
|
||||||
|
|
||||||
Assert.assertEquals(idToken.getStateHash(), HashProvider.oidcHash(getSignatureAlgorithm(), authzResponse.getState()));
|
Assert.assertEquals(idToken.getStateHash(), HashUtils.oidcHash(getSignatureAlgorithm(), authzResponse.getState()));
|
||||||
|
|
||||||
// IDToken exchanged for the code
|
// IDToken exchanged for the code
|
||||||
IDToken idToken2 = sendTokenRequestAndGetIDToken(loginEvent);
|
IDToken idToken2 = sendTokenRequestAndGetIDToken(loginEvent);
|
||||||
|
|
|
@ -20,7 +20,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.events.Details;
|
import org.keycloak.events.Details;
|
||||||
import org.keycloak.jose.jws.crypto.HashProvider;
|
import org.keycloak.jose.jws.crypto.HashUtils;
|
||||||
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
|
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
|
||||||
import org.keycloak.representations.IDToken;
|
import org.keycloak.representations.IDToken;
|
||||||
import org.keycloak.representations.idm.EventRepresentation;
|
import org.keycloak.representations.idm.EventRepresentation;
|
||||||
|
@ -63,17 +63,17 @@ public class OIDCHybridResponseTypeCodeIDTokenTokenTest extends AbstractOIDCResp
|
||||||
// Validate "at_hash" and "c_hash"
|
// Validate "at_hash" and "c_hash"
|
||||||
Assert.assertNotNull(idToken.getAccessTokenHash());
|
Assert.assertNotNull(idToken.getAccessTokenHash());
|
||||||
|
|
||||||
Assert.assertEquals(idToken.getAccessTokenHash(), HashProvider.oidcHash(getSignatureAlgorithm(), authzResponse.getAccessToken()));
|
Assert.assertEquals(idToken.getAccessTokenHash(), HashUtils.oidcHash(getSignatureAlgorithm(), authzResponse.getAccessToken()));
|
||||||
Assert.assertNotNull(idToken.getCodeHash());
|
Assert.assertNotNull(idToken.getCodeHash());
|
||||||
|
|
||||||
Assert.assertEquals(idToken.getCodeHash(), HashProvider.oidcHash(getSignatureAlgorithm(), authzResponse.getCode()));
|
Assert.assertEquals(idToken.getCodeHash(), HashUtils.oidcHash(getSignatureAlgorithm(), authzResponse.getCode()));
|
||||||
|
|
||||||
// Financial API - Part 2: Read and Write API Security Profile
|
// Financial API - Part 2: Read and Write API Security Profile
|
||||||
// http://openid.net/specs/openid-financial-api-part-2.html#authorization-server
|
// http://openid.net/specs/openid-financial-api-part-2.html#authorization-server
|
||||||
// Validate "s_hash"
|
// Validate "s_hash"
|
||||||
Assert.assertNotNull(idToken.getStateHash());
|
Assert.assertNotNull(idToken.getStateHash());
|
||||||
|
|
||||||
Assert.assertEquals(idToken.getStateHash(), HashProvider.oidcHash(getSignatureAlgorithm(), authzResponse.getState()));
|
Assert.assertEquals(idToken.getStateHash(), HashUtils.oidcHash(getSignatureAlgorithm(), authzResponse.getState()));
|
||||||
|
|
||||||
// IDToken exchanged for the code
|
// IDToken exchanged for the code
|
||||||
IDToken idToken2 = sendTokenRequestAndGetIDToken(loginEvent);
|
IDToken idToken2 = sendTokenRequestAndGetIDToken(loginEvent);
|
||||||
|
|
|
@ -20,7 +20,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.events.Details;
|
import org.keycloak.events.Details;
|
||||||
import org.keycloak.jose.jws.crypto.HashProvider;
|
import org.keycloak.jose.jws.crypto.HashUtils;
|
||||||
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
|
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
|
||||||
import org.keycloak.representations.IDToken;
|
import org.keycloak.representations.IDToken;
|
||||||
import org.keycloak.representations.idm.EventRepresentation;
|
import org.keycloak.representations.idm.EventRepresentation;
|
||||||
|
@ -62,7 +62,7 @@ public class OIDCImplicitResponseTypeIDTokenTokenTest extends AbstractOIDCRespon
|
||||||
// Validate "at_hash"
|
// Validate "at_hash"
|
||||||
Assert.assertNotNull(idToken.getAccessTokenHash());
|
Assert.assertNotNull(idToken.getAccessTokenHash());
|
||||||
|
|
||||||
Assert.assertEquals(idToken.getAccessTokenHash(), HashProvider.oidcHash(getSignatureAlgorithm(), authzResponse.getAccessToken()));
|
Assert.assertEquals(idToken.getAccessTokenHash(), HashUtils.oidcHash(getSignatureAlgorithm(), authzResponse.getAccessToken()));
|
||||||
Assert.assertNull(idToken.getCodeHash());
|
Assert.assertNull(idToken.getCodeHash());
|
||||||
|
|
||||||
return Collections.singletonList(idToken);
|
return Collections.singletonList(idToken);
|
||||||
|
|
Loading…
Reference in a new issue