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

io.gravitee.policy.jwt.jwk.source.JWKSUrlJWKSourceResolver Maven / Gradle / Ivy

Go to download

Validate the token signature and expiration date before sending the API call to the target backend

The newest version!
/*
 * Copyright © 2015 The Gravitee team (http://gravitee.io)
 *
 * Licensed 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 io.gravitee.policy.jwt.jwk.source;

import com.nimbusds.jose.KeySourceException;
import com.nimbusds.jose.jwk.JWK;
import com.nimbusds.jose.jwk.JWKSelector;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import com.nimbusds.jose.util.Resource;
import io.reactivex.rxjava3.core.Completable;
import io.reactivex.rxjava3.core.Maybe;
import io.reactivex.rxjava3.schedulers.Schedulers;
import java.text.ParseException;
import java.time.Duration;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Specific implementation of {@link JWKSource} allowing retrieve the JWKS from an url and triggering refresh every {@link #refreshInterval} in background.
 * If an error occurred during background refresh, previous version is kept to limit negative impacts.
 *
 * @author Jeoffrey HAEYAERT (jeoffrey.haeyaert at graviteesource.com)
 * @author GraviteeSource Team
 */
public class JWKSUrlJWKSourceResolver implements JWKSource {

    private static final Logger log = LoggerFactory.getLogger(JWKSUrlJWKSourceResolver.class);

    private final String jwksUrl;
    private final Duration refreshInterval;
    private final AtomicBoolean refreshing;
    private final ResourceRetriever resourceRetriever;

    /**
     * Global cache allowing to share jwk source across the whole platform and avoid re-loading the same JWKS url multiple times.
     */
    static final ConcurrentHashMap> cache = new ConcurrentHashMap<>();

    public JWKSUrlJWKSourceResolver(String jwksUrl, ResourceRetriever resourceRetriever, Duration refreshInterval) {
        this.jwksUrl = jwksUrl;
        this.refreshInterval = refreshInterval;
        this.refreshing = new AtomicBoolean(false);
        this.resourceRetriever = resourceRetriever;
    }

    @Override
    public List get(JWKSelector jwkSelector, C context) throws KeySourceException {
        CachedJWKSource cachedJWKSource = cache.get(jwksUrl);

        if (cachedJWKSource != null) {
            if (cachedJWKSource.isCacheExpired(refreshInterval)) {
                backgroundRefresh();
            }

            return cachedJWKSource.get(jwkSelector, context);
        }

        return Collections.emptyList();
    }

    public Completable initialize() {
        CachedJWKSource cachedJWKSource = cache.get(jwksUrl);
        if (cachedJWKSource == null) {
            return load();
        }

        return Completable.complete();
    }

    private Completable load() {
        if (refreshing.compareAndSet(false, true)) {
            return resourceRetriever
                .retrieve(jwksUrl)
                .flatMapMaybe(this::readJwkSourceFromResource)
                .map(CachedJWKSource::new)
                .doOnSuccess(jwkSource -> cache.put(jwksUrl, jwkSource))
                .doFinally(() -> refreshing.set(false))
                .ignoreElement();
        }

        return Completable.complete();
    }

    private void backgroundRefresh() {
        load()
            .subscribeOn(Schedulers.io())
            .subscribe(
                () -> log.debug("JWKS from url {} has been refreshed", jwksUrl),
                throwable ->
                    log.error(
                        "An error occurred when trying to background refresh the JWK from url {}. Previous JWKS kept untouched.",
                        jwksUrl,
                        throwable
                    )
            );
    }

    private Maybe> readJwkSourceFromResource(Resource resource) {
        try {
            JWKSet parsedJWKSet = JWKSet.parse(resource.getContent());
            ImmutableJWKSet jwkSet = new ImmutableJWKSet<>(parsedJWKSet);
            return Maybe.just(jwkSet);
        } catch (ParseException e) {
            return Maybe.error(e);
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy