[java-oidc-common] branch main updated: JCOMOIDC-6 Move common crypto/security code from the OIDC plugin

Henri Mikkonen henri.mikkonen at iki.fi
Fri Nov 27 17:23:53 UTC 2020


This is an automated email from the git hooks/post-receive script.

hjmikkon pushed a commit to branch main
in repository java-oidc-common.

View the commit online:
http://git.shibboleth.net/view/?p=java-oidc-common.git;a=commit;h=b2b5aeb887c6e77780a4a6d13b412c229f7b7c36

The following commit(s) were added to refs/heads/main by this push:
       new  b2b5aeb   JCOMOIDC-6 Move common crypto/security code from the OIDC plugin
b2b5aeb is described below

commit b2b5aeb887c6e77780a4a6d13b412c229f7b7c36
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Fri Nov 27 19:22:52 2020 +0200

    JCOMOIDC-6 Move common crypto/security code from the OIDC plugin
    
    https://issues.shibboleth.net/jira/browse/JCOMOIDC-6
    
    Initial import of remote JSON Web Key (JWK) cache and utilities from java-idp-oidc.
---
 oidc-common-crypto-api/pom.xml                     |  11 ++
 .../net/shibboleth/oidc/jwk/RemoteJwkSetCache.java | 180 +++++++++++++++++++++
 .../java/net/shibboleth/oidc/jwk/package-info.java |  19 +++
 .../oidc/jwk/support/RemoteJwkUtils.java           | 107 ++++++++++++
 .../shibboleth/oidc/jwk/support/package-info.java  |  19 +++
 .../shibboleth/oidc/jwk/RemoteJwkSetCacheTest.java | 139 ++++++++++++++++
 6 files changed, 475 insertions(+)

diff --git a/oidc-common-crypto-api/pom.xml b/oidc-common-crypto-api/pom.xml
index f63b6c0..f5d6779 100644
--- a/oidc-common-crypto-api/pom.xml
+++ b/oidc-common-crypto-api/pom.xml
@@ -23,10 +23,21 @@
             <groupId>org.opensaml</groupId>
             <artifactId>opensaml-saml-api</artifactId>
         </dependency>
+        <dependency>
+            <groupId>org.opensaml</groupId>
+            <artifactId>opensaml-storage-api</artifactId>
+        </dependency>
         <dependency>
             <groupId>com.nimbusds</groupId>
             <artifactId>oauth2-oidc-sdk</artifactId>
         </dependency>
+
+        <!-- Test Dependencies -->
+        <dependency>
+            <groupId>org.opensaml</groupId>
+            <artifactId>opensaml-storage-impl</artifactId>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 
 </project>
diff --git a/oidc-common-crypto-api/src/main/java/net/shibboleth/oidc/jwk/RemoteJwkSetCache.java b/oidc-common-crypto-api/src/main/java/net/shibboleth/oidc/jwk/RemoteJwkSetCache.java
new file mode 100644
index 0000000..2982a06
--- /dev/null
+++ b/oidc-common-crypto-api/src/main/java/net/shibboleth/oidc/jwk/RemoteJwkSetCache.java
@@ -0,0 +1,180 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID 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 net.shibboleth.oidc.jwk;
+
+import java.io.IOException;
+import java.net.URI;
+import java.time.Instant;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.apache.http.client.HttpClient;
+import org.opensaml.security.httpclient.HttpClientSecurityParameters;
+import org.opensaml.storage.StorageCapabilities;
+import org.opensaml.storage.StorageCapabilitiesEx;
+import org.opensaml.storage.StorageRecord;
+import org.opensaml.storage.StorageService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.nimbusds.jose.jwk.JWKSet;
+
+import net.shibboleth.oidc.jwk.support.RemoteJwkUtils;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+import net.shibboleth.utilities.java.support.component.AbstractIdentifiableInitializableComponent;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+
+/**
+ * Stores fetched remote key set values for a desired period of time.
+ */
+public class RemoteJwkSetCache extends AbstractIdentifiableInitializableComponent {
+
+    /** The context name in the {@link StorageService}. */
+    public static final String CONTEXT_NAME = "oidcRemoteJwkSetContents";
+
+    /** Logger. */
+    private final Logger log = LoggerFactory.getLogger(RemoteJwkSetCache.class);
+
+    /** Backing storage for the remote JWK set contents. */
+    private StorageService storage;
+
+    /** The {@link HttpClient} to use. */
+    @NonnullAfterInit private HttpClient httpClient;
+    
+    /** HTTP client security parameters. */
+    @Nullable private HttpClientSecurityParameters httpClientSecurityParameters;
+
+    /**
+     * Get the backing store for the remote JWK set contents.
+     * 
+     * @return the backing store.
+     */
+    @NonnullAfterInit
+    public StorageService getStorage() {
+        return storage;
+    }
+
+    /**
+     * Set the backing store for the remote JWK set contents.
+     * 
+     * @param storageService backing store to use
+     */
+    public void setStorage(@Nonnull final StorageService storageService) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+        storage = Constraint.isNotNull(storageService, "StorageService cannot be null");
+        final StorageCapabilities caps = storage.getCapabilities();
+        if (caps instanceof StorageCapabilitiesEx) {
+            Constraint.isTrue(((StorageCapabilitiesEx) caps).isServerSide(), "StorageService cannot be client-side");
+        }
+    }
+
+    /**
+     * Set the {@link HttpClient} to use.
+     * 
+     * @param client client to use
+     */
+    public void setHttpClient(@Nonnull final HttpClient client) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+
+        httpClient = Constraint.isNotNull(client, "HttpClient cannot be null");
+    }
+
+    /**
+     * Set the optional client security parameters.
+     * 
+     * @param params the new client security parameters
+     */
+    public void setHttpClientSecurityParameters(@Nullable final HttpClientSecurityParameters params) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+
+        httpClientSecurityParameters = params;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void doInitialize() throws ComponentInitializationException {
+        if (storage == null) {
+            throw new ComponentInitializationException("StorageService cannot be null");
+        }
+        if (httpClient == null) {
+            throw new ComponentInitializationException("HttpClient cannot be null");
+        }
+    }
+
+    /**
+     * Returns remote JWK set if found from the cache, otherwise fetches and stores it.
+     * 
+     * @param uri value to check
+     * @param expires time for disposal of value from cache
+     * 
+     * @return JWK set, null if not found from the cache and cannot be fetched.
+     */
+    public JWKSet fetch(@Nonnull final URI uri, @Nonnull final Instant expires) {
+        return fetch(CONTEXT_NAME, uri, expires);
+    }
+
+    /**
+     * Returns remote JWK set if found from the cache, otherwise fetches and stores it.
+     * 
+     * @param context a context label to subdivide the cache
+     * @param uri value to check
+     * @param expires time (in milliseconds since beginning of epoch) for disposal of value from cache
+     * 
+     * @return JWK set, null if not found from the cache and cannot be fetched.
+     */
+    @Nullable public JWKSet fetch(@Nonnull @NotEmpty final String context, @Nonnull final URI uri,
+            @Nonnull final Instant expires) {
+        final String key = uri.toString();
+
+        final StorageCapabilities caps = storage.getCapabilities();
+        if (context.length() > caps.getContextSize()) {
+            log.error("context {} too long for StorageService (limit {})", context, caps.getContextSize());
+            return null;
+        }
+
+        try {
+            final StorageRecord<?> entry = storage.read(context, key);
+            if (entry == null) {
+                log.debug("Value '{}' was not in the cache, fetching it", key);
+                final JWKSet remoteJwkSet = RemoteJwkUtils.fetchRemoteJwkSet("RemoteJwkSetCache", uri, httpClient, 
+                        httpClientSecurityParameters);
+                if (remoteJwkSet != null && remoteJwkSet.getKeys() != null && !remoteJwkSet.getKeys().isEmpty()) {
+                    storage.create(context, key, remoteJwkSet.toString(), expires.toEpochMilli());
+                    return remoteJwkSet;
+                } else {
+                    log.warn("Could not find any remote keys from {}", key);
+                }
+            } else {
+                final JWKSet cachedSet = JWKSet.parse(entry.getValue());
+                log.debug("Cached value found and will be returned, expires at {}", entry.getExpiration());
+                return cachedSet;
+            }
+        } catch (final IOException | java.text.ParseException e) {
+            log.error("Exception reading/writing to storage service", e);
+        }
+
+        return null;
+    }
+}
diff --git a/oidc-common-crypto-api/src/main/java/net/shibboleth/oidc/jwk/package-info.java b/oidc-common-crypto-api/src/main/java/net/shibboleth/oidc/jwk/package-info.java
new file mode 100644
index 0000000..0f35dc3
--- /dev/null
+++ b/oidc-common-crypto-api/src/main/java/net/shibboleth/oidc/jwk/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID 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.
+ */
+
+/** Classes related to JSON Web Key (JWK) handling. */
+package net.shibboleth.oidc.jwk;
\ No newline at end of file
diff --git a/oidc-common-crypto-api/src/main/java/net/shibboleth/oidc/jwk/support/RemoteJwkUtils.java b/oidc-common-crypto-api/src/main/java/net/shibboleth/oidc/jwk/support/RemoteJwkUtils.java
new file mode 100644
index 0000000..6ab08cd
--- /dev/null
+++ b/oidc-common-crypto-api/src/main/java/net/shibboleth/oidc/jwk/support/RemoteJwkUtils.java
@@ -0,0 +1,107 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID 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 net.shibboleth.oidc.jwk.support;
+
+import java.io.IOException;
+import java.net.URI;
+
+import org.apache.http.HttpResponse;
+import org.apache.http.ParseException;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.client.methods.RequestBuilder;
+import org.apache.http.client.protocol.HttpClientContext;
+import org.apache.http.util.EntityUtils;
+import org.opensaml.security.httpclient.HttpClientSecurityParameters;
+import org.opensaml.security.httpclient.HttpClientSecuritySupport;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.nimbusds.jose.jwk.JWKSet;
+import com.nimbusds.jose.util.JSONObjectUtils;
+
+import net.minidev.json.JSONObject;
+
+/**
+ * Generic utility methods related to remote JWK sets.
+ */
+public final class RemoteJwkUtils {
+    
+    /**
+     * Constructor.
+     */
+    private RemoteJwkUtils() {
+        // prevented
+    }
+    
+    /**
+     * Fetches the JWK set from the given URI using the given client and security parameters.
+     * 
+     * @param logPrefix log prefix
+     * @param uri the endpoint for the JWK set.
+     * @param httpClient HTTP client
+     * @param httpClientSecurityParameters security parameters
+     * 
+     * @return The JWK set fetched from the endpoint, or null if it couldn't be fetched.
+     */
+    public static JWKSet fetchRemoteJwkSet(final String logPrefix, final URI uri, final HttpClient httpClient, 
+            final HttpClientSecurityParameters httpClientSecurityParameters) {
+        final Logger log = LoggerFactory.getLogger(RemoteJwkUtils.class);
+        final HttpResponse response;
+        try {
+            final HttpUriRequest get = RequestBuilder.get().setUri(uri).build();
+            final HttpClientContext clientContext = HttpClientContext.create();
+            HttpClientSecuritySupport.marshalSecurityParameters(clientContext, httpClientSecurityParameters, true);
+            HttpClientSecuritySupport.addDefaultTLSTrustEngineCriteria(clientContext, get);
+            response = httpClient.execute(get, clientContext);
+            HttpClientSecuritySupport.checkTLSCredentialEvaluated(clientContext, get.getURI().getScheme());
+        } catch (final IOException e) {
+            log.error("{} Could not get the JWK contents from {}", logPrefix, uri, e);
+            return null;
+        }
+        if (response == null) {
+            log.error("{} Could not get the JWK contents from {}", logPrefix, uri);
+            return null;
+        }
+        final String output;
+        try {
+            output = EntityUtils.toString(response.getEntity(), "UTF-8");
+        } catch (final ParseException | IOException e) {
+            log.error("{} Could not parse the JWK contents from {}", logPrefix, uri);
+            return null;
+        } finally {
+            EntityUtils.consumeQuietly(response.getEntity());
+        }
+        log.trace("{} Fetched the following response body: {}", logPrefix, output);
+        final JWKSet jwkSet;
+        try {
+            final JSONObject json = JSONObjectUtils.parse(output);
+            // The following check is needed to avoid NPE from Nimbus if keys claim not found
+            if (JSONObjectUtils.getJSONArray(json, "keys") == null) {
+                log.error("{} Could not find 'keys' array from the JSON from {}", logPrefix, uri);
+                return null;
+            }
+            jwkSet = JWKSet.parse(json);
+        } catch (final java.text.ParseException e) {
+            log.error("{} Could not parse the contents from {}", logPrefix, uri, e);
+            return null;
+        }
+        return jwkSet;
+    }
+
+}
diff --git a/oidc-common-crypto-api/src/main/java/net/shibboleth/oidc/jwk/support/package-info.java b/oidc-common-crypto-api/src/main/java/net/shibboleth/oidc/jwk/support/package-info.java
new file mode 100644
index 0000000..f8d8e44
--- /dev/null
+++ b/oidc-common-crypto-api/src/main/java/net/shibboleth/oidc/jwk/support/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID 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.
+ */
+
+/** Utility classes related to JSON Web Key (JWK) handling. */
+package net.shibboleth.oidc.jwk.support;
\ No newline at end of file
diff --git a/oidc-common-crypto-api/src/test/java/net/shibboleth/oidc/jwk/RemoteJwkSetCacheTest.java b/oidc-common-crypto-api/src/test/java/net/shibboleth/oidc/jwk/RemoteJwkSetCacheTest.java
new file mode 100644
index 0000000..e50acfd
--- /dev/null
+++ b/oidc-common-crypto-api/src/test/java/net/shibboleth/oidc/jwk/RemoteJwkSetCacheTest.java
@@ -0,0 +1,139 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID 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 net.shibboleth.oidc.jwk;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.time.Instant;
+
+import org.apache.http.HttpResponse;
+import org.apache.http.client.ClientProtocolException;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.protocol.HttpContext;
+import org.mockito.Mockito;
+import org.opensaml.core.config.InitializationException;
+import org.opensaml.storage.StorageService;
+import org.opensaml.storage.impl.MemoryStorageService;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.nimbusds.jose.jwk.JWKSet;
+
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+
+/**
+ * Unit tests for {@link RemoteJwkSetCache}.
+ */
+public class RemoteJwkSetCacheTest {
+    
+    RemoteJwkSetCache jwkSetCache;
+    StorageService storageService;
+    HttpClient httpClient;
+    
+    
+    @BeforeMethod
+    public void setup() throws InitializationException, ComponentInitializationException {
+        jwkSetCache = new RemoteJwkSetCache();
+        storageService = buildStorageService();
+    }
+    
+    protected StorageService buildStorageService() throws ComponentInitializationException {
+        MemoryStorageService storageService = new MemoryStorageService();
+        storageService.setId("mockId");
+        storageService.initialize();
+        return storageService;
+    }
+    
+    @Test(expectedExceptions = ComponentInitializationException.class)
+    public void testNoHttpClient() throws ComponentInitializationException {
+        jwkSetCache.setStorage(storageService);
+        jwkSetCache.initialize();
+    }
+
+    @Test(expectedExceptions = ComponentInitializationException.class)
+    public void testNoStorageService() throws ComponentInitializationException {
+        jwkSetCache.setHttpClient(HttpClientBuilder.create().build());
+        jwkSetCache.initialize();
+    }
+    
+    @Test
+    public void testSuccessNoSecurity() throws ClientProtocolException, IOException, ComponentInitializationException,
+            URISyntaxException, InterruptedException {
+        jwkSetCache.setStorage(storageService);
+        jwkSetCache.setHttpClient(createMockHttpClient(validJwkSet()));
+        jwkSetCache.setHttpClientSecurityParameters(null);
+        jwkSetCache.initialize();
+        final String uri = "http://example.org";
+        System.out.println(new URI(uri).toString());
+        JWKSet jwkSet = jwkSetCache.fetch(new URI(uri), Instant.now().plusSeconds(5));
+        Assert.assertNotNull(jwkSet);
+        Assert.assertNotNull(jwkSetCache.getStorage().read(RemoteJwkSetCache.CONTEXT_NAME, uri));
+        jwkSet = jwkSetCache.fetch(new URI(uri), Instant.now().plusSeconds(5));
+        Assert.assertNotNull(jwkSet);
+        Thread.sleep(5001);
+        Assert.assertNull(storageService.read(RemoteJwkSetCache.CONTEXT_NAME, uri));
+    }
+
+    @Test
+    public void testInvalidJwk() throws ClientProtocolException, IOException, ComponentInitializationException,
+            URISyntaxException {
+        jwkSetCache.setStorage(storageService);
+        jwkSetCache.setHttpClient(createMockHttpClient("not_jwk_set"));
+        jwkSetCache.initialize();
+        final JWKSet jwkSet = jwkSetCache.fetch(new URI("http://example.org"), Instant.now().plusSeconds(5));
+        Assert.assertNull(jwkSet);
+    }
+
+    protected HttpClient createMockHttpClient(String output) throws ClientProtocolException, IOException {
+        final HttpClient httpClient = Mockito.mock(HttpClient.class);
+        final HttpResponse httpResponse = Mockito.mock(HttpResponse.class);
+        Mockito.when(httpResponse.getEntity()).thenReturn(new StringEntity(output));
+        Mockito.when(httpClient.execute((HttpUriRequest) Mockito.any(), 
+                (HttpContext) Mockito.any())).thenReturn(httpResponse);
+        return httpClient;
+    }
+    
+    protected String validJwkSet() {
+        return "{\n" + 
+                "  \"keys\": [\n" + 
+                "    {\n" + 
+                "      \"kid\": \"08d3245c62f86b6362afcbbffe1d069826dd1dc1\",\n" + 
+                "      \"e\": \"AQAB\",\n" + 
+                "      \"kty\": \"RSA\",\n" + 
+                "      \"alg\": \"RS256\",\n" + 
+                "      \"n\": \"mSLCSG1hK28xrzcSfgbvRinkIRjecBlwsQggynHppHiiT6I80waivIqTJBSFYyVuRCAHXi6apSsL5FUWKd42GOhVUayIyzvuz1CqTuh5a9ACXaJjEVLUFO39QfXxWrxhpSJCTN9aMkdtoV1QJqfAd3IF9MYwfojsoEn3d5XX5TX4RxqZ9-HGbgSLsRuAzFIg9NxxfTYhbECBskhhR4RIcam-1T52FafmK2LMiuIEDPiVg6LvAqWi8gdMRd8WhiP_ZIRJTCH4C0NFKmw1PZyKadVxvwg97vwPTF8qkFdwJ_kjQAMmq77PxankluAkfWjFqbD4JepO4HH3aJvU8Sl_Ow\",\n" + 
+                "      \"use\": \"sig\"\n" + 
+                "    },\n" + 
+                "    {\n" + 
+                "      \"alg\": \"RS256\",\n" + 
+                "      \"n\": \"uS9Iep_r83oLpfnMXLnB5a8IVUP7ZRreM1rxNWYnaqEQr1NfRisyIi4cYG7KbWiuLCmRQOD7ybhpdHCcN9ty5evz4irWT5hIa98Jr3a2BISTskBbPmBgUR3_TuQ_fvxeQYCCETJUcho5gXK-yeDWJwcD2iwqpVzIZHz8BBe5AYFUlJMzwgzYMe9aqoOEWVv__Gd7Z_kaz5pa0lOsWUUPNFmeW4e4rtNvosx7ItyyyghIyG2KX-0phOgbfzG6Ub6qA9upBYK9KBtjcoe1ciV-Yn_3HaS5PlugYTo1zYnng1mW7UP5A_QT_HgDqD1clcz0WIEL6usVMRay87ECEmOhrw\",\n" + 
+                "      \"use\": \"sig\",\n" + 
+                "      \"kid\": \"b15a2b8f7a6b3f6bc08bc1c56a88410e146d01fd\",\n" + 
+                "      \"e\": \"AQAB\",\n" + 
+                "      \"kty\": \"RSA\"\n" + 
+                "    }\n" + 
+                "  ]\n" + 
+                "}";
+    }
+
+}

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.


More information about the commits mailing list