knox-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From m...@apache.org
Subject [knox] branch master updated: KNOX-2575 - Add `kid` and `jku` claims to JWT tokens issues by Knox (#436)
Date Tue, 20 Apr 2021 19:29:41 GMT
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é <moresandeep@gmail.com>
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("john.doe@example.com");
+
+    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] = "john.doe@example.com";
     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] = "joe@example.com";
     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] = "john.doe@example.com";
     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] = "john.doe@example.com";
     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] = "john.doe@example.com";
     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] = "john.doe@example.com";
     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] = "john.doe@example.com";
     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] = "john.doe@example.com";
     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] = "john.doe@example.com";
     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] = "john.doe@example.com";
     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] = "john.doe@example.com";
     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] = "john.doe@example.com";
     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());

Mime
View raw message