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