This is an automated email from the ASF dual-hosted git repository.
more pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/knox.git
The following commit(s) were added to refs/heads/master by this push:
new c1e8a3c KNOX-2575 - Add `kid` and `jku` claims to JWT tokens issues
by Knox (#436)
c1e8a3c is described below
commit c1e8a3c98a50c09d199949c5a7e95ba7a1a9c9e6
Author: Sandeep Moré <[email protected]>
AuthorDate: Tue Apr 20 15:29:32 2021 -0400
KNOX-2575 - Add `kid` and `jku` claims to JWT tokens issues by Knox (#436)
---
.../token/impl/DefaultTokenAuthorityService.java | 24 ++++++++-
.../token/impl/TokenAuthorityServiceMessages.java | 28 ++++++++++
.../impl/DefaultTokenAuthorityServiceTest.java | 59 ++++++++++++++++++++++
.../token/impl/DefaultTokenStateServiceTest.java | 5 +-
.../service/knoxsso/WebSSOResourceTest.java | 4 +-
.../gateway/service/knoxtoken/JWKSResource.java | 8 +--
.../gateway/service/knoxtoken/TokenResource.java | 30 +++++++----
.../service/knoxtoken/JWKSResourceTest.java | 4 +-
.../knoxtoken/TokenServiceResourceTest.java | 38 +++++++++++++-
.../services/security/token/JWTokenAttributes.java | 7 ++-
.../security/token/JWTokenAttributesBuilder.java | 8 ++-
.../services/security/token/TokenUtils.java | 23 ++++++++-
.../security/token/impl/JWTProviderMessages.java | 3 ++
.../services/security/token/impl/JWTToken.java | 13 ++++-
.../services/security/token/impl/JWTTokenTest.java | 40 +++++++++++----
15 files changed, 261 insertions(+), 33 deletions(-)
diff --git
a/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/DefaultTokenAuthorityService.java
b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/DefaultTokenAuthorityService.java
index 76e65ab..1e1e225 100644
---
a/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/DefaultTokenAuthorityService.java
+++
b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/DefaultTokenAuthorityService.java
@@ -33,10 +33,12 @@ import java.text.ParseException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Map;
+import java.util.Optional;
import java.util.Set;
import org.apache.knox.gateway.GatewayResources;
import org.apache.knox.gateway.config.GatewayConfig;
+import org.apache.knox.gateway.i18n.messages.MessagesFactory;
import org.apache.knox.gateway.i18n.resources.ResourcesFactory;
import org.apache.knox.gateway.services.Service;
import org.apache.knox.gateway.services.ServiceLifecycleException;
@@ -73,6 +75,7 @@ import com.nimbusds.jwt.proc.JWTClaimsSetVerifier;
public class DefaultTokenAuthorityService implements JWTokenAuthority, Service
{
private static final GatewayResources RESOURCES =
ResourcesFactory.get(GatewayResources.class);
+ private static final TokenAuthorityServiceMessages LOG =
MessagesFactory.get(TokenAuthorityServiceMessages.class);
// Only standard RSA and HMAC signature algorithms are accepted
// https://tools.ietf.org/html/rfc7518
@@ -86,6 +89,8 @@ public class DefaultTokenAuthorityService implements
JWTokenAuthority, Service {
private byte[] cachedSigningHmacSecret;
private RSAPrivateKey signingKey;
+ private Optional<String> cachedSigningKeyID = Optional.empty();
+
public void setKeystoreService(KeystoreService ks) {
this.keystoreService = ks;
}
@@ -96,7 +101,7 @@ public class DefaultTokenAuthorityService implements
JWTokenAuthority, Service {
@Override
public JWT issueToken(JWTokenAttributes jwtAttributes) throws
TokenServiceException {
- String[] claimArray = new String[4];
+ String[] claimArray = new String[6];
claimArray[0] = "KNOXSSO";
claimArray[1] = jwtAttributes.getPrincipal().getName();
claimArray[2] = null;
@@ -106,8 +111,14 @@ public class DefaultTokenAuthorityService implements
JWTokenAuthority, Service {
else {
claimArray[3] = String.valueOf(jwtAttributes.getExpires());
}
-
final String algorithm = jwtAttributes.getAlgorithm();
+ if(SUPPORTED_HMAC_SIG_ALGS.contains(algorithm)) {
+ claimArray[4] = null;
+ claimArray[5] = null;
+ } else {
+ claimArray[4] = cachedSigningKeyID.isPresent() ?
cachedSigningKeyID.get() : null;
+ claimArray[5] = jwtAttributes.getJku();
+ }
final JWT token = SUPPORTED_PKI_SIG_ALGS.contains(algorithm) ||
SUPPORTED_HMAC_SIG_ALGS.contains(algorithm) ? new JWTToken(algorithm,
claimArray, jwtAttributes.getAudiences(), jwtAttributes.isManaged()) : null;
if (token != null) {
if (SUPPORTED_HMAC_SIG_ALGS.contains(algorithm)) {
@@ -289,8 +300,13 @@ public class DefaultTokenAuthorityService implements
JWTokenAuthority, Service {
else if (! (publicKey instanceof RSAPublicKey)) {
throw new
ServiceLifecycleException(RESOURCES.publicSigningKeyWrongType(signingKeyAlias));
}
+ cachedSigningKeyID = Optional.of(TokenUtils.getThumbprint((RSAPublicKey)
publicKey, "SHA-256"));
} catch (KeyStoreException e) {
throw new
ServiceLifecycleException(RESOURCES.publicSigningKeyNotFound(signingKeyAlias),
e);
+ } catch (final JOSEException e) {
+ /* in case there is an error getting KID log and move one */
+ LOG.errorGettingKid(e.toString());
+ cachedSigningKeyID = Optional.empty();
}
// Ensure that the private signing keys is available
@@ -311,4 +327,8 @@ public class DefaultTokenAuthorityService implements
JWTokenAuthority, Service {
@Override
public void stop() throws ServiceLifecycleException {
}
+
+ protected Optional<String> getCachedSigningKeyID() {
+ return cachedSigningKeyID;
+ }
}
diff --git
a/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/TokenAuthorityServiceMessages.java
b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/TokenAuthorityServiceMessages.java
new file mode 100644
index 0000000..f93c952
--- /dev/null
+++
b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/TokenAuthorityServiceMessages.java
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.knox.gateway.services.token.impl;
+
+import org.apache.knox.gateway.i18n.messages.Message;
+import org.apache.knox.gateway.i18n.messages.MessageLevel;
+import org.apache.knox.gateway.i18n.messages.Messages;
+
+@Messages(logger = "org.apache.knox.gateway.services.token.state")
+public interface TokenAuthorityServiceMessages {
+ @Message(level = MessageLevel.ERROR, text = "There was an error getting kid,
cause: {0}")
+ void errorGettingKid(String message);
+}
diff --git
a/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/DefaultTokenAuthorityServiceTest.java
b/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/DefaultTokenAuthorityServiceTest.java
index b3a62ed..3845a07 100644
---
a/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/DefaultTokenAuthorityServiceTest.java
+++
b/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/DefaultTokenAuthorityServiceTest.java
@@ -22,6 +22,7 @@ import java.security.Principal;
import java.security.interfaces.RSAPublicKey;
import java.util.Collections;
import java.util.HashMap;
+import java.util.Optional;
import org.apache.knox.gateway.config.GatewayConfig;
import org.apache.knox.gateway.services.ServiceLifecycleException;
@@ -337,6 +338,7 @@ public class DefaultTokenAuthorityServiceTest {
ta.setAliasService(as);
ta.setKeystoreService(ks);
ta.init(config, new HashMap<>());
+ ta.start();
final JWTokenAttributes jwtAttributes = new
JWTokenAttributesBuilder().setPrincipal(principal).setAudiences(Collections.emptyList()).setAlgorithm("RS256").setExpires(-1)
.setSigningKeystoreName(customSigningKeyName).setSigningKeystoreAlias(customSigningKeyAlias).setSigningKeystorePassphrase(customSigningKeyPassphrase.toCharArray()).build();
@@ -584,4 +586,61 @@ public class DefaultTokenAuthorityServiceTest {
EasyMock.verify(config, ms, as);
}
+
+ /**
+ * Test getSigningCertKid() function
+ * @throws Exception
+ */
+ @Test
+ public void testGetSigningCertKid() throws Exception {
+ Principal principal = EasyMock.createNiceMock(Principal.class);
+ EasyMock.expect(principal.getName()).andReturn("[email protected]");
+
+ GatewayConfig config = EasyMock.createNiceMock(GatewayConfig.class);
+ String basedir = System.getProperty("basedir");
+ if (basedir == null) {
+ basedir = new File(".").getCanonicalPath();
+ }
+
+ EasyMock.expect(config.getGatewaySecurityDir()).andReturn(basedir +
"/target/test-classes").anyTimes();
+ EasyMock.expect(config.getGatewayKeystoreDir()).andReturn(basedir +
"/target/test-classes/keystores").anyTimes();
+
EasyMock.expect(config.getSigningKeystoreName()).andReturn("server-keystore.jks").anyTimes();
+ EasyMock.expect(config.getSigningKeystorePath()).andReturn(basedir +
"/target/test-classes/keystores/server-keystore.jks").anyTimes();
+
EasyMock.expect(config.getSigningKeystorePasswordAlias()).andReturn(GatewayConfig.DEFAULT_SIGNING_KEYSTORE_PASSWORD_ALIAS).anyTimes();
+
EasyMock.expect(config.getSigningKeyPassphraseAlias()).andReturn(GatewayConfig.DEFAULT_SIGNING_KEY_PASSPHRASE_ALIAS).anyTimes();
+
EasyMock.expect(config.getSigningKeystoreType()).andReturn("jks").anyTimes();
+
EasyMock.expect(config.getSigningKeyAlias()).andReturn("server").anyTimes();
+
EasyMock.expect(config.getCredentialStoreType()).andReturn(GatewayConfig.DEFAULT_CREDENTIAL_STORE_TYPE).anyTimes();
+
EasyMock.expect(config.getCredentialStoreAlgorithm()).andReturn(GatewayConfig.DEFAULT_CREDENTIAL_STORE_ALG).anyTimes();
+
+ MasterService ms = EasyMock.createNiceMock(MasterService.class);
+ EasyMock.expect(ms.getMasterSecret()).andReturn("horton".toCharArray());
+
+ AliasService as = EasyMock.createNiceMock(AliasService.class);
+
EasyMock.expect(as.getSigningKeyPassphrase()).andReturn("horton".toCharArray()).anyTimes();
+
+ EasyMock.replay(principal, config, ms, as);
+
+ DefaultKeystoreService ks = new DefaultKeystoreService();
+ ks.setMasterService(ms);
+
+ ks.init(config, new HashMap<>());
+
+ DefaultTokenAuthorityService ta = new DefaultTokenAuthorityService();
+
+ /* negative test */
+ /* expectation that that the exception is eaten up in case where there was
an exception getting kid */
+ Optional<String> opt = ta.getCachedSigningKeyID();
+ assertFalse(opt.isPresent());
+
+ /* now test for cases where we expect to get kid */
+ ta.setAliasService(as);
+ ta.setKeystoreService(ks);
+
+ ta.init(config, new HashMap<>());
+ ta.start();
+
+ opt = ta.getCachedSigningKeyID();
+ assertTrue("Missing expected KID value", opt.isPresent());
+ }
}
diff --git
a/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/DefaultTokenStateServiceTest.java
b/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/DefaultTokenStateServiceTest.java
index cff5506..f11aa81 100644
---
a/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/DefaultTokenStateServiceTest.java
+++
b/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/DefaultTokenStateServiceTest.java
@@ -360,14 +360,15 @@ public class DefaultTokenStateServiceTest {
/* create a test JWT token */
protected JWT getJWTToken(final long expiry) {
- String[] claims = new String[4];
+ String[] claims = new String[6];
claims[0] = "KNOXSSO";
claims[1] = "[email protected]";
claims[2] = "https://login.example.com";
if(expiry > 0) {
claims[3] = Long.toString(expiry);
}
-
+ claims[4] = "E0LDZulQ0XE_otJ5aoQtQu-RnXv8hU-M9U4dD7vDioA";
+ claims[5] = null;
JWT token = new JWTToken("RS256", claims);
// Sign the token
JWSSigner signer = new RSASSASigner(privateKey);
diff --git
a/gateway-service-knoxsso/src/test/java/org/apache/knox/gateway/service/knoxsso/WebSSOResourceTest.java
b/gateway-service-knoxsso/src/test/java/org/apache/knox/gateway/service/knoxsso/WebSSOResourceTest.java
index b6671ec..6e8ba26 100644
---
a/gateway-service-knoxsso/src/test/java/org/apache/knox/gateway/service/knoxsso/WebSSOResourceTest.java
+++
b/gateway-service-knoxsso/src/test/java/org/apache/knox/gateway/service/knoxsso/WebSSOResourceTest.java
@@ -768,7 +768,7 @@ public class WebSSOResourceTest {
@Override
public JWT issueToken(JWTokenAttributes jwtAttributes)
throws TokenServiceException {
- String[] claimArray = new String[4];
+ String[] claimArray = new String[6];
claimArray[0] = "KNOXSSO";
claimArray[1] = jwtAttributes.getPrincipal().getName();
claimArray[2] = null;
@@ -777,6 +777,8 @@ public class WebSSOResourceTest {
} else {
claimArray[3] = String.valueOf(jwtAttributes.getExpires());
}
+ claimArray[4] = "E0LDZulQ0XE_otJ5aoQtQu-RnXv8hU-M9U4dD7vDioA";
+ claimArray[5] = null;
JWT token = new JWTToken(jwtAttributes.getAlgorithm(), claimArray,
jwtAttributes.getAudiences());
try {
diff --git
a/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/JWKSResource.java
b/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/JWKSResource.java
index ed2ae61..0477dd4 100644
---
a/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/JWKSResource.java
+++
b/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/JWKSResource.java
@@ -27,6 +27,7 @@ import org.apache.knox.gateway.services.GatewayServices;
import org.apache.knox.gateway.services.ServiceType;
import org.apache.knox.gateway.services.security.KeystoreService;
import org.apache.knox.gateway.services.security.KeystoreServiceException;
+import org.apache.knox.gateway.services.security.token.TokenUtils;
import javax.annotation.PostConstruct;
import javax.inject.Singleton;
@@ -47,9 +48,9 @@ import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
@Singleton
@Path(JWKSResource.RESOURCE_PATH)
public class JWKSResource {
-
+ public static final String JWKS_PATH = "/jwks.json";
static final String RESOURCE_PATH = "knoxtoken/api/v1";
- static final String JWKS_PATH = "/jwks.json";
+
@Context
HttpServletRequest request;
@Context
@@ -80,10 +81,11 @@ public class JWKSResource {
.entity(new JWKSet().toJSONObject().toString()).build();
}
+ final String kid = TokenUtils.getThumbprint(rsa, "SHA-256");
final RSAKey.Builder builder = new RSAKey.Builder(rsa)
.keyUse(KeyUse.SIGNATURE)
.algorithm(new JWSAlgorithm(rsa.getAlgorithm()))
- .keyIDFromThumbprint();
+ .keyID(kid);
jwks = new JWKSet(builder.build());
diff --git
a/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/TokenResource.java
b/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/TokenResource.java
index a5e852a..6ed3c08 100644
---
a/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/TokenResource.java
+++
b/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/TokenResource.java
@@ -89,7 +89,8 @@ public class TokenResource {
private static final String TOKEN_EXP_RENEWAL_MAX_LIFETIME =
"knox.token.exp.max-lifetime";
private static final String TOKEN_RENEWER_WHITELIST =
"knox.token.renewer.whitelist";
private static final long TOKEN_TTL_DEFAULT = 30000L;
- static final String RESOURCE_PATH = "knoxtoken/api/v1/token";
+ static final String TOKEN_API_PATH = "knoxtoken/api/v1";
+ static final String RESOURCE_PATH = TOKEN_API_PATH + "/token";
static final String RENEW_PATH = "/renew";
static final String REVOKE_PATH = "/revoke";
private static final String TARGET_ENDPOINT_PULIC_CERT_PEM =
"knox.token.target.endpoint.cert.pem";
@@ -393,7 +394,6 @@ public class TokenResource {
try {
Certificate cert = ks.getCertificateForGateway();
byte[] bytes = cert.getEncoded();
- //Base64 encoder = new Base64(76, "\n".getBytes("ASCII"));
endpointPublicCert = Base64.encodeBase64String(bytes);
} catch (KeyStoreException | KeystoreServiceException |
CertificateEncodingException e) {
// assuming that certs will be properly provisioned across all
clients
@@ -402,19 +402,31 @@ public class TokenResource {
}
}
+ String jku = null;
+ /* remove .../token and replace it with ..../jwks.json */
+ final int idx = request.getRequestURL().lastIndexOf("/");
+ if(idx > 1) {
+ jku = request.getRequestURL().substring(0, idx) + JWKSResource.JWKS_PATH;
+ }
+
try {
final boolean managedToken = tokenStateService != null;
JWT token;
JWTokenAttributes jwtAttributes;
- if (targetAudiences.isEmpty()) {
- jwtAttributes = new
JWTokenAttributesBuilder().setPrincipal(p).setAlgorithm(signatureAlgorithm).setExpires(expires).setManaged(managedToken).build();
- token = ts.issueToken(jwtAttributes);
- } else {
- jwtAttributes = new
JWTokenAttributesBuilder().setPrincipal(p).setAudiences(targetAudiences).setAlgorithm(signatureAlgorithm).setExpires(expires)
- .setManaged(managedToken).build();
- token = ts.issueToken(jwtAttributes);
+ final JWTokenAttributesBuilder jwtAttributesBuilder = new
JWTokenAttributesBuilder();
+ jwtAttributesBuilder
+ .setPrincipal(p)
+ .setAlgorithm(signatureAlgorithm)
+ .setExpires(expires)
+ .setManaged(managedToken)
+ .setJku(jku);
+ if (!targetAudiences.isEmpty()) {
+ jwtAttributesBuilder.setAudiences(targetAudiences);
}
+ jwtAttributes = jwtAttributesBuilder.build();
+ token = ts.issueToken(jwtAttributes);
+
if (token != null) {
String accessToken = token.toString();
String tokenId = TokenUtils.getTokenId(token);
diff --git
a/gateway-service-knoxtoken/src/test/java/org/apache/knox/gateway/service/knoxtoken/JWKSResourceTest.java
b/gateway-service-knoxtoken/src/test/java/org/apache/knox/gateway/service/knoxtoken/JWKSResourceTest.java
index 31bf363..09100e2 100644
---
a/gateway-service-knoxtoken/src/test/java/org/apache/knox/gateway/service/knoxtoken/JWKSResourceTest.java
+++
b/gateway-service-knoxtoken/src/test/java/org/apache/knox/gateway/service/knoxtoken/JWKSResourceTest.java
@@ -127,11 +127,13 @@ public class JWKSResourceTest {
}
private JWT getTestToken(final String algorithm) {
- String[] claimArray = new String[4];
+ String[] claimArray = new String[6];
claimArray[0] = "KNOXSSO";
claimArray[1] = "[email protected]";
claimArray[2] = null;
claimArray[3] = null;
+ claimArray[4] = "E0LDZulQ0XE_otJ5aoQtQu-RnXv8hU-M9U4dD7vDioA";
+ claimArray[5] = null;
final JWT token = new JWTToken(algorithm, claimArray,
Collections.singletonList("aud"), false);
diff --git
a/gateway-service-knoxtoken/src/test/java/org/apache/knox/gateway/service/knoxtoken/TokenServiceResourceTest.java
b/gateway-service-knoxtoken/src/test/java/org/apache/knox/gateway/service/knoxtoken/TokenServiceResourceTest.java
index 4959ce9..0c4de88 100644
---
a/gateway-service-knoxtoken/src/test/java/org/apache/knox/gateway/service/knoxtoken/TokenServiceResourceTest.java
+++
b/gateway-service-knoxtoken/src/test/java/org/apache/knox/gateway/service/knoxtoken/TokenServiceResourceTest.java
@@ -84,6 +84,10 @@ public class TokenServiceResourceTest {
private static RSAPublicKey publicKey;
private static RSAPrivateKey privateKey;
+ private static String TOKEN_API_PATH =
"https://gateway-host:8443/gateway/sandbox/knoxtoken/api/v1";
+ private static String TOKEN_PATH = "/token";
+ private static String JKWS_PATH = "/jwks.json";
+
private ServletContext context;
private HttpServletRequest request;
private JWTokenAuthority authority;
@@ -124,6 +128,7 @@ public class TokenServiceResourceTest {
Principal principal = EasyMock.createNiceMock(Principal.class);
EasyMock.expect(principal.getName()).andReturn("alice").anyTimes();
EasyMock.expect(request.getUserPrincipal()).andReturn(principal).anyTimes();
+ EasyMock.expect(request.getRequestURL()).andReturn(new
StringBuffer(TOKEN_API_PATH+TOKEN_PATH)).anyTimes();
GatewayServices services = EasyMock.createNiceMock(GatewayServices.class);
EasyMock.expect(context.getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE)).andReturn(services).anyTimes();
@@ -759,6 +764,35 @@ public class TokenServiceResourceTest {
validateSuccessfulRevocationResponse(renewalResponse);
}
+ @Test
+ public void testKidJkuClaims() throws Exception {
+ final Map<String, String> contextExpectations = new HashMap<>();
+ contextExpectations.put("knox.token.ttl", "60000");
+ configureCommonExpectations(contextExpectations);
+
+ TokenResource tr = new TokenResource();
+ tr.request = request;
+ tr.context = context;
+ tr.init();
+
+ // Issue a token
+ Response retResponse = tr.doGet();
+
+ assertEquals(200, retResponse.getStatus());
+
+ // Parse the response
+ final String retString = retResponse.getEntity().toString();
+ final String accessToken = getTagValue(retString, "access_token");
+ assertNotNull(accessToken);
+
+ // Verify the token
+ final JWT parsedToken = new JWTToken(accessToken);
+ assertEquals("alice", parsedToken.getSubject());
+ assertTrue(authority.verifyToken(parsedToken));
+
+ assertNotNull(parsedToken.getClaim("kid"));
+ assertEquals(TOKEN_API_PATH+JKWS_PATH, parsedToken.getClaim("jku"));
+ }
/**
*
@@ -1170,7 +1204,7 @@ public class TokenServiceResourceTest {
@Override
public JWT issueToken(JWTokenAttributes jwtAttributes) {
- String[] claimArray = new String[4];
+ String[] claimArray = new String[6];
claimArray[0] = "KNOXSSO";
claimArray[1] = jwtAttributes.getPrincipal().getName();
claimArray[2] = null;
@@ -1179,6 +1213,8 @@ public class TokenServiceResourceTest {
} else {
claimArray[3] = String.valueOf(jwtAttributes.getExpires());
}
+ claimArray[4] = "E0LDZulQ0XE_otJ5aoQtQu-RnXv8hU-M9U4dD7vDioA";
+ claimArray[5] = jwtAttributes.getJku();
JWT token = new JWTToken(jwtAttributes.getAlgorithm(), claimArray,
jwtAttributes.getAudiences());
JWSSigner signer = new RSASSASigner(privateKey);
diff --git
a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/JWTokenAttributes.java
b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/JWTokenAttributes.java
index b6db56c..0d5fb90 100644
---
a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/JWTokenAttributes.java
+++
b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/JWTokenAttributes.java
@@ -30,9 +30,10 @@ public class JWTokenAttributes {
private final String signingKeystoreAlias;
private final char[] signingKeystorePassphrase;
private final boolean managed;
+ private final String jku;
JWTokenAttributes(Principal principal, List<String> audiences, String
algorithm, long expires, String signingKeystoreName, String
signingKeystoreAlias,
- char[] signingKeystorePassphrase, boolean managed) {
+ char[] signingKeystorePassphrase, boolean managed, String jku) {
super();
this.principal = principal;
this.audiences = audiences;
@@ -42,6 +43,7 @@ public class JWTokenAttributes {
this.signingKeystoreAlias = signingKeystoreAlias;
this.signingKeystorePassphrase = signingKeystorePassphrase;
this.managed = managed;
+ this.jku = jku;
}
public Principal getPrincipal() {
@@ -76,4 +78,7 @@ public class JWTokenAttributes {
return managed;
}
+ public String getJku() {
+ return jku;
+ }
}
diff --git
a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/JWTokenAttributesBuilder.java
b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/JWTokenAttributesBuilder.java
index c4028e4..0c053f0 100644
---
a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/JWTokenAttributesBuilder.java
+++
b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/JWTokenAttributesBuilder.java
@@ -33,6 +33,7 @@ public class JWTokenAttributesBuilder {
private String signingKeystoreAlias;
private char[] signingKeystorePassphrase;
private boolean managed;
+ private String jku;
public JWTokenAttributesBuilder setPrincipal(Subject subject) {
return setPrincipal((Principal) subject.getPrincipals().toArray()[0]);
@@ -82,9 +83,14 @@ public class JWTokenAttributesBuilder {
return this;
}
+ public JWTokenAttributesBuilder setJku(String jku) {
+ this.jku = jku;
+ return this;
+ }
+
public JWTokenAttributes build() {
return new JWTokenAttributes(principal, (audiences == null ?
Collections.emptyList() : audiences), algorithm, expires, signingKeystoreName,
signingKeystoreAlias,
- signingKeystorePassphrase, managed);
+ signingKeystorePassphrase, managed, jku);
}
}
diff --git
a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenUtils.java
b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenUtils.java
index f4bf82d..7471150 100644
---
a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenUtils.java
+++
b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenUtils.java
@@ -16,6 +16,10 @@
*/
package org.apache.knox.gateway.services.security.token;
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.jwk.KeyType;
+import com.nimbusds.jose.jwk.ThumbprintUtils;
+import com.nimbusds.jose.util.Base64URL;
import org.apache.commons.lang3.StringUtils;
import org.apache.knox.gateway.config.GatewayConfig;
import org.apache.knox.gateway.services.security.AliasService;
@@ -25,7 +29,8 @@ import
org.apache.knox.gateway.services.security.token.impl.JWTToken;
import javax.servlet.FilterConfig;
import javax.servlet.ServletContext;
-
+import java.security.interfaces.RSAPublicKey;
+import java.util.LinkedHashMap;
public class TokenUtils {
public static final String SIGNING_HMAC_SECRET_ALIAS =
"gateway.signing.hmac.secret";
@@ -87,6 +92,22 @@ public class TokenUtils {
}
/**
+ * Utility method to calculate public key thumbprint
+ * @param publicKey
+ * @param hashAlgorithm
+ * @return
+ * @throws JOSEException
+ */
+ public static String getThumbprint(final RSAPublicKey publicKey, final
String hashAlgorithm)
+ throws JOSEException {
+ LinkedHashMap<String,String> params = new LinkedHashMap<>();
+ params.put("e",
Base64URL.encode(publicKey.getPublicExponent()).toString());
+ params.put("kty", KeyType.RSA.getValue());
+ params.put("n", Base64URL.encode(publicKey.getModulus()).toString());
+ return ThumbprintUtils.compute(hashAlgorithm, params).toString();
+ }
+
+ /**
* @return true, if the HMAC secret is configured via the alias service for
the gateway AND signing keystore name is not set ; false
* otherwise
*/
diff --git
a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/impl/JWTProviderMessages.java
b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/impl/JWTProviderMessages.java
index 2d8e589..e213f04 100644
---
a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/impl/JWTProviderMessages.java
+++
b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/impl/JWTProviderMessages.java
@@ -55,4 +55,7 @@ public interface JWTProviderMessages {
@Message( level = MessageLevel.ERROR, text = "Unable to verify JWT token:
{0}" )
void unableToVerifyToken(JOSEException e);
+
+ @Message( level = MessageLevel.ERROR, text = "Missing claims, expected 6
found : {0}" )
+ void missingClaims(int length);
}
diff --git
a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/impl/JWTToken.java
b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/impl/JWTToken.java
index 1b6d936..ceee909 100644
---
a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/impl/JWTToken.java
+++
b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/impl/JWTToken.java
@@ -39,6 +39,8 @@ public class JWTToken implements JWT {
public static final String KNOX_ID_CLAIM = "knox.id";
public static final String MANAGED_TOKEN_CLAIM = "managed.token";
+ public static final String KNOX_KID_CLAIM = "kid";
+ public static final String KNOX_JKU_CLAIM = "jku";
SignedJWT jwt;
@@ -66,6 +68,10 @@ public class JWTToken implements JWT {
public JWTToken(String alg, String[] claimsArray, List<String> audiences,
boolean managed) {
JWSHeader header = new JWSHeader(new JWSAlgorithm(alg));
+ if(claimsArray == null || claimsArray.length < 6) {
+ log.missingClaims(claimsArray.length);
+ }
+
if (claimsArray[2] != null) {
if (audiences == null) {
audiences = new ArrayList<>();
@@ -80,12 +86,17 @@ public class JWTToken implements JWT {
if(claimsArray[3] != null) {
builder = builder.expirationTime(new
Date(Long.parseLong(claimsArray[3])));
}
+ if(claimsArray[4] != null) {
+ builder.claim(KNOX_KID_CLAIM, claimsArray[4]);
+ }
+ if(claimsArray[5] != null) {
+ builder.claim(KNOX_JKU_CLAIM, claimsArray[5]);
+ }
// Add a private UUID claim for uniqueness
builder.claim(KNOX_ID_CLAIM, String.valueOf(UUID.randomUUID()));
builder.claim(MANAGED_TOKEN_CLAIM, String.valueOf(managed));
-
claims = builder.build();
jwt = new SignedJWT(header, claims);
diff --git
a/gateway-spi/src/test/java/org/apache/knox/gateway/services/security/token/impl/JWTTokenTest.java
b/gateway-spi/src/test/java/org/apache/knox/gateway/services/security/token/impl/JWTTokenTest.java
index 44e3d28..9cb239d 100644
---
a/gateway-spi/src/test/java/org/apache/knox/gateway/services/security/token/impl/JWTTokenTest.java
+++
b/gateway-spi/src/test/java/org/apache/knox/gateway/services/security/token/impl/JWTTokenTest.java
@@ -69,11 +69,13 @@ public class JWTTokenTest {
@Test
public void testTokenCreation() throws Exception {
- String[] claims = new String[4];
+ String[] claims = new String[6];
claims[0] = "KNOXSSO";
claims[1] = "[email protected]";
claims[2] = "https://login.example.com";
claims[3] = Long.toString( ( System.currentTimeMillis()/1000 ) + 300);
+ claims[4] = "E0LDZulQ0XE_otJ5aoQtQu-RnXv8hU-M9U4dD7vDioA";
+ claims[5] = null;
JWT token = new JWTToken("RS256", claims);
assertEquals("KNOXSSO", token.getIssuer());
@@ -83,11 +85,13 @@ public class JWTTokenTest {
@Test
public void testPrivateUUIDClaim() throws Exception {
- String[] claims = new String[4];
+ String[] claims = new String[6];
claims[0] = "KNOXSSO";
claims[1] = "[email protected]";
claims[2] = "https://login.example.com";
claims[3] = Long.toString( ( System.currentTimeMillis()/1000 ) + 300);
+ claims[4] = "E0LDZulQ0XE_otJ5aoQtQu-RnXv8hU-M9U4dD7vDioA";
+ claims[5] = null;
JWT token = new JWTToken("RS256", claims);
assertEquals("KNOXSSO", token.getIssuer());
@@ -102,11 +106,13 @@ public class JWTTokenTest {
@Test
public void testTokenCreationWithAudienceListSingle() throws Exception {
- String[] claims = new String[4];
+ String[] claims = new String[6];
claims[0] = "KNOXSSO";
claims[1] = "[email protected]";
claims[2] = null;
claims[3] = Long.toString( ( System.currentTimeMillis()/1000 ) + 300);
+ claims[4] = "E0LDZulQ0XE_otJ5aoQtQu-RnXv8hU-M9U4dD7vDioA";
+ claims[5] = null;
List<String> audiences = new ArrayList<>();
audiences.add("https://login.example.com");
@@ -120,11 +126,13 @@ public class JWTTokenTest {
@Test
public void testTokenCreationWithAudienceListMultiple() throws Exception {
- String[] claims = new String[4];
+ String[] claims = new String[6];
claims[0] = "KNOXSSO";
claims[1] = "[email protected]";
claims[2] = null;
claims[3] = Long.toString( ( System.currentTimeMillis()/1000 ) + 300);
+ claims[4] = "E0LDZulQ0XE_otJ5aoQtQu-RnXv8hU-M9U4dD7vDioA";
+ claims[5] = null;
List<String> audiences = new ArrayList<>();
audiences.add("https://login.example.com");
audiences.add("KNOXSSO");
@@ -139,11 +147,13 @@ public class JWTTokenTest {
@Test
public void testTokenCreationWithAudienceListCombined() throws Exception {
- String[] claims = new String[4];
+ String[] claims = new String[6];
claims[0] = "KNOXSSO";
claims[1] = "[email protected]";
claims[2] = "LJM";
claims[3] = Long.toString( ( System.currentTimeMillis()/1000 ) + 300);
+ claims[4] = "E0LDZulQ0XE_otJ5aoQtQu-RnXv8hU-M9U4dD7vDioA";
+ claims[5] = null;
ArrayList<String> audiences = new ArrayList<>();
audiences.add("https://login.example.com");
audiences.add("KNOXSSO");
@@ -158,11 +168,13 @@ public class JWTTokenTest {
@Test
public void testTokenCreationWithNullAudienceList() throws Exception {
- String[] claims = new String[4];
+ String[] claims = new String[6];
claims[0] = "KNOXSSO";
claims[1] = "[email protected]";
claims[2] = null;
claims[3] = Long.toString( ( System.currentTimeMillis()/1000 ) + 300);
+ claims[4] = "E0LDZulQ0XE_otJ5aoQtQu-RnXv8hU-M9U4dD7vDioA";
+ claims[5] = null;
List<String> audiences = null;
JWT token = new JWTToken("RS256", claims, audiences);
@@ -175,11 +187,13 @@ public class JWTTokenTest {
@Test
public void testTokenCreationRS512() throws Exception {
- String[] claims = new String[4];
+ String[] claims = new String[6];
claims[0] = "KNOXSSO";
claims[1] = "[email protected]";
claims[2] = "https://login.example.com";
claims[3] = Long.toString( ( System.currentTimeMillis()/1000 ) + 300);
+ claims[4] = "E0LDZulQ0XE_otJ5aoQtQu-RnXv8hU-M9U4dD7vDioA";
+ claims[5] = null;
JWTToken token = new JWTToken(JWSAlgorithm.RS512.getName(), claims);
assertEquals("KNOXSSO", token.getIssuer());
@@ -190,11 +204,13 @@ public class JWTTokenTest {
@Test
public void testTokenSignature() throws Exception {
- String[] claims = new String[4];
+ String[] claims = new String[6];
claims[0] = "KNOXSSO";
claims[1] = "[email protected]";
claims[2] = "https://login.example.com";
claims[3] = Long.toString( ( System.currentTimeMillis()/1000 ) + 300);
+ claims[4] = "E0LDZulQ0XE_otJ5aoQtQu-RnXv8hU-M9U4dD7vDioA";
+ claims[5] = null;
JWT token = new JWTToken("RS256", claims);
assertEquals("KNOXSSO", token.getIssuer());
@@ -213,11 +229,13 @@ public class JWTTokenTest {
@Test
public void testTokenSignatureRS512() throws Exception {
- String[] claims = new String[4];
+ String[] claims = new String[6];
claims[0] = "KNOXSSO";
claims[1] = "[email protected]";
claims[2] = "https://login.example.com";
claims[3] = Long.toString( ( System.currentTimeMillis()/1000 ) + 300);
+ claims[4] = "E0LDZulQ0XE_otJ5aoQtQu-RnXv8hU-M9U4dD7vDioA";
+ claims[5] = null;
JWT token = new JWTToken(JWSAlgorithm.RS512.getName(), claims);
assertEquals("KNOXSSO", token.getIssuer());
@@ -237,11 +255,13 @@ public class JWTTokenTest {
@Test
public void testTokenExpiry() throws Exception {
- String[] claims = new String[4];
+ String[] claims = new String[6];
claims[0] = "KNOXSSO";
claims[1] = "[email protected]";
claims[2] = "https://login.example.com";
claims[3] = Long.toString( ( System.currentTimeMillis()/1000 ) + 300);
+ claims[4] = "E0LDZulQ0XE_otJ5aoQtQu-RnXv8hU-M9U4dD7vDioA";
+ claims[5] = null;
JWT token = new JWTToken("RS256", claims);
assertNotNull(token.getExpires());