KEYCLOAK-5007 Used single-use cache for tracke OAuth code. OAuth code changed to be encrypted and signed JWT
This commit is contained in:
1 parent
63673c4328
commit
3b6e1f4e93
69 files changed
+1574
-464
No files matched your search
@@ -111,7 +111,7 @@ public class JWE {
|
||||
}
|
||||
|
||||
|
||||
public String encodeJwe() {
|
||||
public String encodeJwe() throws JWEException {
|
||||
try {
|
||||
if (header == null) {
|
||||
throw new IllegalStateException("Header must be set");
|
||||
@@ -139,8 +139,8 @@ public class JWE {
|
||||
encryptionProvider.encodeJwe(this);
|
||||
|
||||
return getEncodedJweString();
|
||||
} catch (IOException | GeneralSecurityException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (Exception e) {
|
||||
throw new JWEException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,7 +157,7 @@ public class JWE {
|
||||
}
|
||||
|
||||
|
||||
public JWE verifyAndDecodeJwe(String jweStr) {
|
||||
public JWE verifyAndDecodeJwe(String jweStr) throws JWEException {
|
||||
try {
|
||||
String[] parts = jweStr.split("\\.");
|
||||
if (parts.length != 5) {
|
||||
@@ -189,8 +189,8 @@ public class JWE {
|
||||
encryptionProvider.verifyAndDecodeJwe(this);
|
||||
|
||||
return this;
|
||||
} catch (IOException | GeneralSecurityException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (Exception e) {
|
||||
throw new JWEException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* 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.jose.jwe;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class JWEException extends Exception {
|
||||
|
||||
public JWEException(String s) {
|
||||
super(s);
|
||||
}
|
||||
|
||||
public JWEException() {
|
||||
}
|
||||
|
||||
public JWEException(Throwable throwable) {
|
||||
super(throwable);
|
||||
}
|
||||
}
|
||||
@@ -34,18 +34,14 @@ import org.keycloak.jose.jwe.enc.JWEEncryptionProvider;
|
||||
public class AesKeyWrapAlgorithmProvider implements JWEAlgorithmProvider {
|
||||
|
||||
@Override
|
||||
public byte[] decodeCek(byte[] encodedCek, Key encryptionKey) throws IOException, GeneralSecurityException {
|
||||
try {
|
||||
Wrapper encrypter = new AESWrapEngine();
|
||||
encrypter.init(false, new KeyParameter(encryptionKey.getEncoded()));
|
||||
return encrypter.unwrap(encodedCek, 0, encodedCek.length);
|
||||
} catch (InvalidCipherTextException icte) {
|
||||
throw new IllegalStateException(icte);
|
||||
}
|
||||
public byte[] decodeCek(byte[] encodedCek, Key encryptionKey) throws Exception {
|
||||
Wrapper encrypter = new AESWrapEngine();
|
||||
encrypter.init(false, new KeyParameter(encryptionKey.getEncoded()));
|
||||
return encrypter.unwrap(encodedCek, 0, encodedCek.length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] encodeCek(JWEEncryptionProvider encryptionProvider, JWEKeyStorage keyStorage, Key encryptionKey) throws IOException, GeneralSecurityException {
|
||||
public byte[] encodeCek(JWEEncryptionProvider encryptionProvider, JWEKeyStorage keyStorage, Key encryptionKey) throws Exception {
|
||||
Wrapper encrypter = new AESWrapEngine();
|
||||
encrypter.init(true, new KeyParameter(encryptionKey.getEncoded()));
|
||||
byte[] cekBytes = keyStorage.getCekBytes();
|
||||
|
||||
@@ -30,12 +30,12 @@ import org.keycloak.jose.jwe.enc.JWEEncryptionProvider;
|
||||
public class DirectAlgorithmProvider implements JWEAlgorithmProvider {
|
||||
|
||||
@Override
|
||||
public byte[] decodeCek(byte[] encodedCek, Key encryptionKey) throws IOException, GeneralSecurityException {
|
||||
public byte[] decodeCek(byte[] encodedCek, Key encryptionKey) {
|
||||
return new byte[0];
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] encodeCek(JWEEncryptionProvider encryptionProvider, JWEKeyStorage keyStorage, Key encryptionKey) throws IOException, GeneralSecurityException {
|
||||
public byte[] encodeCek(JWEEncryptionProvider encryptionProvider, JWEKeyStorage keyStorage, Key encryptionKey) {
|
||||
return new byte[0];
|
||||
}
|
||||
}
|
||||
@@ -29,8 +29,8 @@ import org.keycloak.jose.jwe.enc.JWEEncryptionProvider;
|
||||
*/
|
||||
public interface JWEAlgorithmProvider {
|
||||
|
||||
byte[] decodeCek(byte[] encodedCek, Key encryptionKey) throws IOException, GeneralSecurityException;
|
||||
byte[] decodeCek(byte[] encodedCek, Key encryptionKey) throws Exception;
|
||||
|
||||
byte[] encodeCek(JWEEncryptionProvider encryptionProvider, JWEKeyStorage keyStorage, Key encryptionKey) throws IOException, GeneralSecurityException;
|
||||
byte[] encodeCek(JWEEncryptionProvider encryptionProvider, JWEKeyStorage keyStorage, Key encryptionKey) throws Exception;
|
||||
|
||||
}
|
||||
@@ -41,7 +41,7 @@ public interface JWEEncryptionProvider {
|
||||
* @throws IOException
|
||||
* @throws GeneralSecurityException
|
||||
*/
|
||||
void encodeJwe(JWE jwe) throws IOException, GeneralSecurityException;
|
||||
void encodeJwe(JWE jwe) throws Exception;
|
||||
|
||||
|
||||
/**
|
||||
@@ -51,7 +51,7 @@ public interface JWEEncryptionProvider {
|
||||
* @throws IOException
|
||||
* @throws GeneralSecurityException
|
||||
*/
|
||||
void verifyAndDecodeJwe(JWE jwe) throws IOException, GeneralSecurityException;
|
||||
void verifyAndDecodeJwe(JWE jwe) throws Exception;
|
||||
|
||||
|
||||
/**
|
||||
|
||||
@@ -25,6 +25,7 @@ public enum AlgorithmType {
|
||||
|
||||
RSA,
|
||||
HMAC,
|
||||
AES,
|
||||
ECDSA
|
||||
|
||||
}
|
||||
@@ -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.representations;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class CodeJWT extends JsonWebToken {
|
||||
|
||||
@JsonProperty("uss")
|
||||
protected String userSessionId;
|
||||
|
||||
public String getUserSessionId() {
|
||||
return userSessionId;
|
||||
}
|
||||
|
||||
public CodeJWT userSessionId(String userSessionId) {
|
||||
this.userSessionId = userSessionId;
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -18,11 +18,18 @@
|
||||
package org.keycloak.util;
|
||||
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.jose.jwe.JWE;
|
||||
import org.keycloak.jose.jwe.JWEConstants;
|
||||
import org.keycloak.jose.jwe.JWEException;
|
||||
import org.keycloak.jose.jwe.JWEHeader;
|
||||
import org.keycloak.jose.jwe.JWEKeyStorage;
|
||||
import org.keycloak.jose.jws.JWSInput;
|
||||
import org.keycloak.jose.jws.JWSInputException;
|
||||
import org.keycloak.representations.JsonWebToken;
|
||||
import org.keycloak.representations.RefreshToken;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.Key;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
@@ -115,4 +122,52 @@ public class TokenUtil {
|
||||
return token.getType().equals(TOKEN_TYPE_OFFLINE);
|
||||
}
|
||||
|
||||
|
||||
public static String jweDirectEncode(Key aesKey, Key hmacKey, JsonWebToken jwt) throws JWEException {
|
||||
int keyLength = aesKey.getEncoded().length;
|
||||
String encAlgorithm;
|
||||
switch (keyLength) {
|
||||
case 16: encAlgorithm = JWEConstants.A128CBC_HS256;
|
||||
break;
|
||||
case 24: encAlgorithm = JWEConstants.A192CBC_HS384;
|
||||
break;
|
||||
case 32: encAlgorithm = JWEConstants.A256CBC_HS512;
|
||||
break;
|
||||
default: throw new IllegalArgumentException("Bad size for Encryption key: " + aesKey + ". Valid sizes are 16, 24, 32.");
|
||||
}
|
||||
|
||||
try {
|
||||
byte[] contentBytes = JsonSerialization.writeValueAsBytes(jwt);
|
||||
|
||||
JWEHeader jweHeader = new JWEHeader(JWEConstants.DIR, encAlgorithm, null);
|
||||
JWE jwe = new JWE()
|
||||
.header(jweHeader)
|
||||
.content(contentBytes);
|
||||
|
||||
jwe.getKeyStorage()
|
||||
.setCEKKey(aesKey, JWEKeyStorage.KeyUse.ENCRYPTION)
|
||||
.setCEKKey(hmacKey, JWEKeyStorage.KeyUse.SIGNATURE);
|
||||
|
||||
return jwe.encodeJwe();
|
||||
} catch (IOException ioe) {
|
||||
throw new JWEException(ioe);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static <T extends JsonWebToken> T jweDirectVerifyAndDecode(Key aesKey, Key hmacKey, String jweStr, Class<T> expectedClass) throws JWEException {
|
||||
JWE jwe = new JWE();
|
||||
jwe.getKeyStorage()
|
||||
.setCEKKey(aesKey, JWEKeyStorage.KeyUse.ENCRYPTION)
|
||||
.setCEKKey(hmacKey, JWEKeyStorage.KeyUse.SIGNATURE);
|
||||
|
||||
jwe.verifyAndDecodeJwe(jweStr);
|
||||
|
||||
try {
|
||||
return JsonSerialization.readValue(jwe.getContent(), expectedClass);
|
||||
} catch (IOException ioe) {
|
||||
throw new JWEException(ioe);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import org.junit.Test;
|
||||
import org.keycloak.common.util.Base64Url;
|
||||
import org.keycloak.jose.jwe.JWE;
|
||||
import org.keycloak.jose.jwe.JWEConstants;
|
||||
import org.keycloak.jose.jwe.JWEException;
|
||||
import org.keycloak.jose.jwe.JWEHeader;
|
||||
import org.keycloak.jose.jwe.JWEKeyStorage;
|
||||
|
||||
@@ -36,7 +37,7 @@ import org.keycloak.jose.jwe.JWEKeyStorage;
|
||||
*/
|
||||
public class JWETest {
|
||||
|
||||
private static final String PAYLOAD = "Hello world! How are you? This is some quite a long text, which is much longer than just simple 'Hello World'";
|
||||
private static final String PAYLOAD = "Hello world! How are you man? I hope you are fine. This is some quite a long text, which is much longer than just simple 'Hello World'";
|
||||
|
||||
private static final byte[] HMAC_SHA256_KEY = new byte[] { 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 13, 14, 15, 16 };
|
||||
private static final byte[] AES_128_KEY = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 };
|
||||
@@ -49,43 +50,25 @@ public class JWETest {
|
||||
SecretKey aesKey = new SecretKeySpec(AES_128_KEY, "AES");
|
||||
SecretKey hmacKey = new SecretKeySpec(HMAC_SHA256_KEY, "HMACSHA2");
|
||||
|
||||
JWEHeader jweHeader = new JWEHeader(JWEConstants.DIR, JWEConstants.A128CBC_HS256, null);
|
||||
JWE jwe = new JWE()
|
||||
.header(jweHeader)
|
||||
.content(PAYLOAD.getBytes("UTF-8"));
|
||||
|
||||
jwe.getKeyStorage()
|
||||
.setCEKKey(aesKey, JWEKeyStorage.KeyUse.ENCRYPTION)
|
||||
.setCEKKey(hmacKey, JWEKeyStorage.KeyUse.SIGNATURE);
|
||||
|
||||
String encodedContent = jwe.encodeJwe();
|
||||
|
||||
System.out.println("Encoded content: " + encodedContent);
|
||||
System.out.println("Encoded content length: " + encodedContent.length());
|
||||
|
||||
jwe = new JWE();
|
||||
jwe.getKeyStorage()
|
||||
.setCEKKey(aesKey, JWEKeyStorage.KeyUse.ENCRYPTION)
|
||||
.setCEKKey(hmacKey, JWEKeyStorage.KeyUse.SIGNATURE);
|
||||
|
||||
jwe.verifyAndDecodeJwe(encodedContent);
|
||||
|
||||
String decodedContent = new String(jwe.getContent(), "UTF-8");
|
||||
|
||||
Assert.assertEquals(PAYLOAD, decodedContent);
|
||||
|
||||
testDirectEncryptAndDecrypt(aesKey, hmacKey, JWEConstants.A128CBC_HS256, PAYLOAD, true);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
// Works just on OpenJDK 8. Other JDKs (IBM, Oracle) have restrictions on maximum key size of AES to be 128
|
||||
// @Test
|
||||
public void testDirect_Aes256CbcHmacSha512() throws Exception {
|
||||
final SecretKey aesKey = new SecretKeySpec(AES_256_KEY, "AES");
|
||||
final SecretKey hmacKey = new SecretKeySpec(HMAC_SHA512_KEY, "HMACSHA2");
|
||||
|
||||
JWEHeader jweHeader = new JWEHeader(JWEConstants.DIR, JWEConstants.A256CBC_HS512, null);
|
||||
testDirectEncryptAndDecrypt(aesKey, hmacKey, JWEConstants.A256CBC_HS512, PAYLOAD, true);
|
||||
}
|
||||
|
||||
|
||||
private void testDirectEncryptAndDecrypt(Key aesKey, Key hmacKey, String encAlgorithm, String payload, boolean sysout) throws Exception {
|
||||
JWEHeader jweHeader = new JWEHeader(JWEConstants.DIR, encAlgorithm, null);
|
||||
JWE jwe = new JWE()
|
||||
.header(jweHeader)
|
||||
.content(PAYLOAD.getBytes("UTF-8"));
|
||||
.content(payload.getBytes("UTF-8"));
|
||||
|
||||
jwe.getKeyStorage()
|
||||
.setCEKKey(aesKey, JWEKeyStorage.KeyUse.ENCRYPTION)
|
||||
@@ -93,8 +76,10 @@ public class JWETest {
|
||||
|
||||
String encodedContent = jwe.encodeJwe();
|
||||
|
||||
System.out.println("Encoded content: " + encodedContent);
|
||||
System.out.println("Encoded content length: " + encodedContent.length());
|
||||
if (sysout) {
|
||||
System.out.println("Encoded content: " + encodedContent);
|
||||
System.out.println("Encoded content length: " + encodedContent.length());
|
||||
}
|
||||
|
||||
jwe = new JWE();
|
||||
jwe.getKeyStorage()
|
||||
@@ -105,8 +90,32 @@ public class JWETest {
|
||||
|
||||
String decodedContent = new String(jwe.getContent(), "UTF-8");
|
||||
|
||||
Assert.assertEquals(PAYLOAD, decodedContent);
|
||||
Assert.assertEquals(payload, decodedContent);
|
||||
}
|
||||
|
||||
|
||||
// @Test
|
||||
public void testPerfDirect() throws Exception {
|
||||
int iterations = 50000;
|
||||
|
||||
long start = System.currentTimeMillis();
|
||||
for (int i=0 ; i<iterations ; i++) {
|
||||
// took around 2950 ms with 50000 iterations
|
||||
SecretKey aesKey = new SecretKeySpec(AES_128_KEY, "AES");
|
||||
SecretKey hmacKey = new SecretKeySpec(HMAC_SHA256_KEY, "HMACSHA2");
|
||||
String encAlg = JWEConstants.A128CBC_HS256;
|
||||
|
||||
// Similar perf like AES128CBC_HS256
|
||||
//SecretKey aesKey = new SecretKeySpec(AES_256_KEY, "AES");
|
||||
//SecretKey hmacKey = new SecretKeySpec(HMAC_SHA512_KEY, "HMACSHA2");
|
||||
//String encAlg = JWEConstants.A256CBC_HS512;
|
||||
|
||||
String payload = PAYLOAD + i;
|
||||
testDirectEncryptAndDecrypt(aesKey, hmacKey, encAlg, payload, false);
|
||||
}
|
||||
|
||||
long took = System.currentTimeMillis() - start;
|
||||
System.out.println("Iterations: " + iterations + ", took: " + took);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -139,7 +148,7 @@ public class JWETest {
|
||||
|
||||
|
||||
@Test
|
||||
public void externalJweAes128CbcHmacSha256Test() throws UnsupportedEncodingException {
|
||||
public void externalJweAes128CbcHmacSha256Test() throws UnsupportedEncodingException, JWEException {
|
||||
String externalJwe = "eyJlbmMiOiJBMTI4Q0JDLUhTMjU2IiwiYWxnIjoiZGlyIn0..qysUrI1iVtiG4Z4jyr7XXg.apdNSQhR7WDMg6IHf5aLVI0gGp6JuOHYmIUtflns4WHmyxOOnh_GShLI6DWaK_SiywTV5gZvZYtl8H8Iv5fTfLkc4tiDDjbdtmsOP7tqyRxVh069gU5UvEAgmCXbIKALutgYXcYe2WM4E6BIHPTSt8jXdkktFcm7XHiD7mpakZyjXsG8p3XVkQJ72WbJI_t6.Ks6gHeko7BRTZ4CFs5ijRA";
|
||||
System.out.println("External encoded content length: " + externalJwe.length());
|
||||
|
||||
@@ -159,8 +168,9 @@ public class JWETest {
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void externalJweAes256CbcHmacSha512Test() throws UnsupportedEncodingException {
|
||||
// Works just on OpenJDK 8. Other JDKs (IBM, Oracle) have restrictions on maximum key size of AES to be 128
|
||||
// @Test
|
||||
public void externalJweAes256CbcHmacSha512Test() throws UnsupportedEncodingException, JWEException {
|
||||
String externalJwe = "eyJlbmMiOiJBMjU2Q0JDLUhTNTEyIiwiYWxnIjoiZGlyIn0..xUPndQ5U69CYaWMKr4nyeg.AzSzba6OdNsvTIoNpub8d2TmYnkY7W8Sd-1S33DjJwJsSaNcfvfXBq5bqXAGVAnLHrLZJKWoEYsmOrYHz3Nao-kpLtUpc4XZI8yiYUqkHTjmxZnfD02R6hz31a5KBCnDTtUEv23VSxm8yUyQKoUTpVHbJ3b2VQvycg2XFUXPsA6oaSSEpz-uwe1Vmun2hUBB.Qal4rMYn1RrXQ9AQ9ONUjUXvlS2ow8np-T8QWMBR0ns";
|
||||
System.out.println("External encoded content length: " + externalJwe.length());
|
||||
|
||||
@@ -181,7 +191,7 @@ public class JWETest {
|
||||
|
||||
|
||||
@Test
|
||||
public void externalJweAesKeyWrapTest() throws Exception {
|
||||
public void externalJweAes128KeyWrapTest() throws Exception {
|
||||
// See example "A.3" from JWE specification - https://tools.ietf.org/html/rfc7516#page-41
|
||||
String externalJwe = "eyJhbGciOiJBMTI4S1ciLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0.6KB707dM9YTIgHtLvtgWQ8mKwboJW3of9locizkDTHzBC2IlrT1oOQ.AxY8DCtDaGlsbGljb3RoZQ.KDlTtXchhZTGufMYmOYGS4HffxPSUrfmqCHXaI9wOGY.U0m_YmjN04DJvceFICbCVQ";
|
||||
|
||||
|
||||
Reference in new issue
Block a user