You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@knox.apache.org by mo...@apache.org on 2021/04/08 01:32:08 UTC

[knox] branch master updated: KNOX-2570 - Add support for JWKS endpoint (#429)

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 b523cec  KNOX-2570 - Add support for JWKS endpoint (#429)
b523cec is described below

commit b523cec5986a9c9b6700e34a3363178e58bfa3c8
Author: Sandeep Moré <mo...@gmail.com>
AuthorDate: Wed Apr 7 21:32:02 2021 -0400

    KNOX-2570 - Add support for JWKS endpoint (#429)
    
    KNOX-2570 - Add support for JWKS endpoint
---
 gateway-service-knoxtoken/pom.xml                  |  11 +-
 .../gateway/service/knoxtoken/JWKSResource.java    | 116 +++++++++++++++++
 .../service/knoxtoken/JWKSResourceTest.java        | 143 +++++++++++++++++++++
 3 files changed, 269 insertions(+), 1 deletion(-)

diff --git a/gateway-service-knoxtoken/pom.xml b/gateway-service-knoxtoken/pom.xml
index 6a2bece..f74929d 100644
--- a/gateway-service-knoxtoken/pom.xml
+++ b/gateway-service-knoxtoken/pom.xml
@@ -80,7 +80,11 @@
             <groupId>org.apache.commons</groupId>
             <artifactId>commons-lang3</artifactId>
         </dependency>
-
+        <dependency>
+            <groupId>net.minidev</groupId>
+            <artifactId>json-smart</artifactId>
+            <scope>compile</scope>
+        </dependency>
         <dependency>
             <groupId>org.apache.knox</groupId>
             <artifactId>gateway-test-utils</artifactId>
@@ -96,5 +100,10 @@
             <artifactId>jackson-core</artifactId>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>org.apache.knox</groupId>
+            <artifactId>gateway-server</artifactId>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 </project>
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
new file mode 100644
index 0000000..ed2ae61
--- /dev/null
+++ b/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/JWKSResource.java
@@ -0,0 +1,116 @@
+/*
+ * 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.service.knoxtoken;
+
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.jwk.JWKSet;
+import com.nimbusds.jose.jwk.KeyUse;
+import com.nimbusds.jose.jwk.RSAKey;
+import org.apache.knox.gateway.config.GatewayConfig;
+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 javax.annotation.PostConstruct;
+import javax.inject.Singleton;
+import javax.servlet.ServletContext;
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.Response;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.cert.Certificate;
+import java.security.interfaces.RSAPublicKey;
+
+import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
+
+@Singleton
+@Path(JWKSResource.RESOURCE_PATH)
+public class JWKSResource {
+
+  static final String RESOURCE_PATH = "knoxtoken/api/v1";
+  static final String JWKS_PATH = "/jwks.json";
+  @Context
+  HttpServletRequest request;
+  @Context
+  ServletContext context;
+  private KeystoreService keystoreService;
+
+  @PostConstruct
+  public void init() {
+    final GatewayServices services = (GatewayServices) context
+        .getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE);
+    keystoreService = services.getService(ServiceType.KEYSTORE_SERVICE);
+  }
+
+  @GET
+  @Path(JWKS_PATH)
+  @Produces({ APPLICATION_JSON })
+  public Response getJwksResponse() {
+    return getJwks(null);
+  }
+
+  private Response getJwks(final String keystore) {
+    JWKSet jwks;
+    try {
+      final RSAPublicKey rsa = getPublicKey(keystore);
+      /* no public cert found, return empty set */
+      if(rsa == null) {
+        return Response.ok()
+            .entity(new JWKSet().toJSONObject().toString()).build();
+      }
+
+      final RSAKey.Builder builder = new RSAKey.Builder(rsa)
+          .keyUse(KeyUse.SIGNATURE)
+          .algorithm(new JWSAlgorithm(rsa.getAlgorithm()))
+          .keyIDFromThumbprint();
+
+      jwks = new JWKSet(builder.build());
+
+    } catch (KeyStoreException | JOSEException e) {
+      return Response.status(500)
+          .entity("{\n  \"error\": \"" + e.toString() + "\"\n}\n").build();
+    } catch (KeystoreServiceException e) {
+      return Response.status(500).entity(
+          "{\n  \"error\": \"" + "keystore " + keystore + " could not be found."
+              + "\"\n}\n").build();
+    }
+    return Response.ok()
+        .entity(jwks.toJSONObject().toString()).build();
+  }
+
+  protected RSAPublicKey getPublicKey(final String keystore)
+      throws KeystoreServiceException, KeyStoreException {
+    final KeyStore ks = keystoreService.getSigningKeystore(keystore);
+    final Certificate cert = ks.getCertificate(getSigningKeyAlias());
+    return (cert != null) ? (RSAPublicKey) cert.getPublicKey() : null;
+  }
+
+  private String getSigningKeyAlias() {
+    final GatewayConfig config = (GatewayConfig) request.getServletContext()
+        .getAttribute(GatewayConfig.GATEWAY_CONFIG_ATTRIBUTE);
+    final String alias = config.getSigningKeyAlias();
+    return (alias == null) ? GatewayConfig.DEFAULT_SIGNING_KEY_ALIAS : alias;
+  }
+
+}
\ No newline at end of file
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
new file mode 100644
index 0000000..31bf363
--- /dev/null
+++ b/gateway-service-knoxtoken/src/test/java/org/apache/knox/gateway/service/knoxtoken/JWKSResourceTest.java
@@ -0,0 +1,143 @@
+/*
+ * 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.service.knoxtoken;
+
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JWSSigner;
+import com.nimbusds.jose.JWSVerifier;
+import com.nimbusds.jose.crypto.RSASSASigner;
+import com.nimbusds.jose.crypto.RSASSAVerifier;
+import com.nimbusds.jose.jwk.JWK;
+import com.nimbusds.jose.jwk.JWKSet;
+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.impl.JWT;
+import org.apache.knox.gateway.services.security.token.impl.JWTToken;
+import org.easymock.EasyMock;
+import org.junit.Assert;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import javax.servlet.ServletContext;
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.core.Response;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.KeyStoreException;
+import java.security.PublicKey;
+import java.security.interfaces.RSAPrivateKey;
+import java.security.interfaces.RSAPublicKey;
+import java.text.ParseException;
+import java.util.Collections;
+
+/**
+ * Unit tests for JWKS Resource
+ */
+public class JWKSResourceTest {
+
+  private static RSAPublicKey publicKey;
+  private static RSAPrivateKey privateKey;
+  private ServletContext context;
+  private HttpServletRequest request;
+  private GatewayServices services;
+  private JWKSResource jwksResource;
+
+  @BeforeClass
+  public static void setUpBeforeClass() throws Exception {
+    KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
+    kpg.initialize(2048);
+    KeyPair KPair = kpg.generateKeyPair();
+
+    publicKey = (RSAPublicKey) KPair.getPublic();
+    privateKey = (RSAPrivateKey) KPair.getPrivate();
+  }
+
+  private void init() throws KeystoreServiceException, KeyStoreException {
+    final KeystoreService ks = EasyMock.createNiceMock(KeystoreService.class);
+    services = EasyMock.createNiceMock(GatewayServices.class);
+    context = EasyMock.createNiceMock(ServletContext.class);
+    request = EasyMock.createNiceMock(HttpServletRequest.class);
+
+    jwksResource = EasyMock.partialMockBuilder(JWKSResource.class)
+        .addMockedMethod("getPublicKey", String.class).createMock();
+
+    EasyMock.expect(services.getService(ServiceType.KEYSTORE_SERVICE))
+        .andReturn(ks).anyTimes();
+    EasyMock.expect(
+        context.getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE))
+        .andReturn(services).anyTimes();
+    EasyMock.expect(jwksResource.getPublicKey(null)).andReturn(publicKey)
+        .anyTimes();
+    EasyMock.replay(jwksResource, context, request, services, ks);
+  }
+
+  @Test
+  public void testJWKSrequest()
+      throws KeystoreServiceException, KeyStoreException {
+    init();
+    Response retResponse = jwksResource.getJwksResponse();
+    Assert.assertEquals(Response.Status.OK.getStatusCode(),
+        retResponse.getStatus());
+  }
+
+  /**
+   * End to End test that verifies the token acquired from JWKS endpoint.
+   *
+   * @throws KeystoreServiceException
+   * @throws KeyStoreException
+   * @throws ParseException
+   * @throws JOSEException
+   */
+  @Test
+  public void testE2E()
+      throws KeystoreServiceException, KeyStoreException, ParseException,
+      JOSEException {
+    init();
+    /* get a signed JWT token */
+    final JWT testToken = getTestToken("RS256");
+    /* get JWKS keyset */
+    final Response retResponse = jwksResource.getJwksResponse();
+
+    /* following lines just verifies the token */
+    final JWKSet jwks = JWKSet.parse(retResponse.getEntity().toString());
+    Assert.assertTrue("No keys found", jwks.getKeys().size() > 0);
+    final JWK jwk = jwks.getKeys().get(0);
+    Assert.assertNotNull("No private key found", jwk.toRSAKey().toPublicKey());
+    final PublicKey pk = jwk.toRSAKey().toPublicKey();
+    final JWSVerifier verifier = new RSASSAVerifier((RSAPublicKey) pk);
+    Assert.assertTrue("Cannot verify the token, wrong certificate",
+        testToken.verify(verifier));
+  }
+
+  private JWT getTestToken(final String algorithm) {
+    String[] claimArray = new String[4];
+    claimArray[0] = "KNOXSSO";
+    claimArray[1] = "joe@example.com";
+    claimArray[2] = null;
+    claimArray[3] = null;
+
+    final JWT token = new JWTToken(algorithm, claimArray,
+        Collections.singletonList("aud"), false);
+    final JWSSigner signer = new RSASSASigner(privateKey, true);
+    token.sign(signer);
+    return token;
+  }
+
+}
\ No newline at end of file