All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.eclipse.edc.api.auth.delegated.JwksPublicKeyResolver Maven / Gradle / Ivy

/*
 *  Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
 *
 *  This program and the accompanying materials are made available under the
 *  terms of the Apache License, Version 2.0 which is available at
 *  https://www.apache.org/licenses/LICENSE-2.0
 *
 *  SPDX-License-Identifier: Apache-2.0
 *
 *  Contributors:
 *       Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation
 *
 */

package org.eclipse.edc.api.auth.delegated;

import com.nimbusds.jose.KeySourceException;
import com.nimbusds.jose.jwk.JWK;
import com.nimbusds.jose.jwk.JWKMatcher;
import com.nimbusds.jose.jwk.JWKSelector;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.jwk.source.JWKSourceBuilder;
import com.nimbusds.jose.proc.SecurityContext;
import com.nimbusds.jose.proc.SimpleSecurityContext;
import org.eclipse.edc.keys.spi.KeyParserRegistry;
import org.eclipse.edc.keys.spi.PublicKeyResolver;
import org.eclipse.edc.spi.EdcException;
import org.eclipse.edc.spi.monitor.Monitor;
import org.eclipse.edc.spi.result.Result;
import org.jetbrains.annotations.Nullable;

import java.net.MalformedURLException;
import java.net.URI;
import java.security.PublicKey;
import java.util.List;
import java.util.Optional;

import static com.nimbusds.jose.jwk.source.JWKSourceBuilder.DEFAULT_CACHE_REFRESH_TIMEOUT;
import static com.nimbusds.jose.jwk.source.JWKSourceBuilder.DEFAULT_RATE_LIMIT_MIN_INTERVAL;
import static com.nimbusds.jose.jwk.source.JWKSourceBuilder.DEFAULT_REFRESH_AHEAD_TIME;

/**
 * A {@link PublicKeyResolver} that resolves a JSON Web Key Set from a URL and parses the JWK with the given ID
 */
public class JwksPublicKeyResolver implements PublicKeyResolver {
    private final Monitor monitor;
    private final KeyParserRegistry keyParserRegistry;
    private final JWKSource jwkSource;

    private JwksPublicKeyResolver(KeyParserRegistry keyParserRegistry, Monitor monitor, JWKSource jwkSource) {
        this.keyParserRegistry = keyParserRegistry;
        this.monitor = monitor;
        this.jwkSource = jwkSource;
    }

    /**
     * Creates a new resolver that does use any cache. That means, that every request hits the server.
     *
     * @param keyParserRegistry Should contain all relevant key parsers. The minimum recommendation is adding a {@code JwkParser}.
     * @param jwksUrl           The URL of the public key server, where a JWK Set can be obtained.
     * @param monitor           A monitor
     * @throws EdcException if the jwksUrl is malformed
     */
    public static JwksPublicKeyResolver create(KeyParserRegistry keyParserRegistry, String jwksUrl, Monitor monitor) {
        return create(keyParserRegistry, jwksUrl, monitor, 0);
    }

    /**
     * Creates a new resolver that does use any cache. That means, that every request hits the server.
     *
     * @param keyParserRegistry Should contain all relevant key parsers. The minimum recommendation is adding a {@code JwkParser}.
     * @param jwksUrl           The URL of the public key server, where a JWK Set can be obtained.
     * @param monitor           A monitor
     * @param cacheValidityMs   The time in milliseconds that public keys may be cached locally.
     * @throws EdcException if the jwksUrl is malformed
     */
    public static JwksPublicKeyResolver create(KeyParserRegistry keyParserRegistry, String jwksUrl, Monitor monitor, long cacheValidityMs) {

        try {
            var builder = JWKSourceBuilder.create(URI.create(jwksUrl).toURL()).retrying(false);
            if (cacheValidityMs > 0) {
                builder.cache(cacheValidityMs, DEFAULT_CACHE_REFRESH_TIMEOUT);

                // rate-limit must be < cache TTL, this would cause the cache to be refreshed more often than allowed
                if (cacheValidityMs < DEFAULT_RATE_LIMIT_MIN_INTERVAL) {
                    builder.rateLimited(cacheValidityMs - 1);
                }
                // cache TTL must be > refresh-ahead time plus refresh timeout
                if (cacheValidityMs < DEFAULT_REFRESH_AHEAD_TIME + DEFAULT_CACHE_REFRESH_TIMEOUT) {
                    builder.refreshAheadCache(false);
                }

            } else {
                // disable all optimizations
                builder.cache(false);
                builder.rateLimited(false);
                builder.refreshAheadCache(false);
            }
            var jwkSource = builder.build();
            return new JwksPublicKeyResolver(keyParserRegistry, monitor, jwkSource);

        } catch (MalformedURLException e) {
            monitor.warning("Malformed JWK URL: " + jwksUrl, e);
            throw new EdcException(e);
        }
    }

    @Override
    public Result resolveKey(@Nullable String keyId) {
        var matcher = Optional.ofNullable(keyId) // get matcher with optional keyID property
                .map(kid -> new JWKMatcher.Builder().keyID(kid).build())
                .orElseGet(() -> new JWKMatcher.Builder().build());
        var selector = new JWKSelector(matcher);
        List keys;
        try {
            keys = jwkSource.get(selector, new SimpleSecurityContext());
        } catch (KeySourceException e) {
            monitor.warning("Error while retrieving JWKSet", e);
            return Result.failure("Error while retrieving JWKSet: " + e.getMessage());
        }

        if (keys.isEmpty()) {
            var msg = "JWKSet did not contain a matching key (desired keyId: '%s')".formatted(keyId);
            monitor.warning(msg);
            return Result.failure(msg);
        }
        if (keys.size() > 1) {
            String msg = keyId == null ?
                    "JWKSet contained %d keys, but no keyId was specified. Please consider specifying a keyId.".formatted(keys.size()) :
                    "JWKSet contained %d matching keys (desired keyId: '%s'), where only 1 is expected. Will abort!".formatted(keys.size(), keyId);
            monitor.warning(msg);
            return Result.failure(msg);
        }

        var jwk = keys.get(0);

        return keyParserRegistry.parse(jwk.toJSONString()).map(k -> (PublicKey) k);
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy