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

com.dnastack.oauth.client.OAuthClientFactory Maven / Gradle / Ivy

package com.dnastack.oauth.client;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import feign.*;
import feign.Logger.Level;
import feign.form.FormEncoder;
import feign.jackson.JacksonDecoder;
import feign.jackson.JacksonEncoder;
import feign.slf4j.Slf4jLogger;
import lombok.extern.slf4j.Slf4j;

import java.time.Instant;
import java.util.Date;
import java.util.Optional;
import java.util.concurrent.TimeUnit;

import static feign.FeignException.errorStatus;

public class OAuthClientFactory {
    private final OAuthClientConfiguration defaultConfig;
    private final Client httpClient;

    private static final ObjectMapper DEFAULT_MAPPER = new ObjectMapper()
        .registerModule(new JavaTimeModule())
        .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

    public OAuthClientFactory(OAuthClientConfiguration defaultConfig, Client httpClient) {
        this.defaultConfig = defaultConfig;
        this.httpClient = httpClient;
    }

    public Feign.Builder builder(OAuthClientConfiguration clientConfig) {
        OAuthClientConfiguration actualConfig = defaultConfig.update(clientConfig);
        return new OAuthClientProxy(httpClient, actualConfig).getBuilder();
    }

    public Feign.Builder builderWithCommonSetup(OAuthClientConfiguration clientConfig) {
        return builder(clientConfig)
            .encoder(new JacksonEncoder(DEFAULT_MAPPER))
            .decoder(new JacksonDecoder(DEFAULT_MAPPER));
    }

    @Slf4j
    public static class OAuthClientProxy {
        private final OAuthClientConfiguration config;
        private final OAuthClient tokenExchangeClient;

        private static final ObjectMapper MAPPER_FOR_TOKEN_EXCHANGE = new ObjectMapper()
            .setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE)
            .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        private final Client httpClient;

        private Long tokenRetrievedAt = 0L;

        private AccessToken accessToken = null;

        public OAuthClientProxy(Client httpClient, OAuthClientConfiguration config) {
            this.config = config;
            this.httpClient = httpClient;

            tokenExchangeClient = Feign.builder()
                .client(httpClient)
                .logger(config.getName() == null ? new Slf4jLogger() : new Slf4jLogger(config.getName()))
                .logLevel(Level.valueOf(Optional.ofNullable(config.getFeignLoggerLevel()).orElse("BASIC").toUpperCase()))
                .encoder(new FormEncoder(new JacksonEncoder(MAPPER_FOR_TOKEN_EXCHANGE)))
                .decoder(new JacksonDecoder(MAPPER_FOR_TOKEN_EXCHANGE))
                .target(OAuthClient.class, config.getTokenUri());
        }

        private OAuthRequest getOAuthRequest() {
            final var builder = OAuthRequest.builder()
                .grantType("client_credentials")
                .clientId(config.getClientId())
                .clientSecret(config.getClientSecret())
                .scope(config.getScope());

            if (config.getResource() != null) {
                builder.resource(config.getResource());
            } else if (config.getAudience() != null) {
                log.warn("The use of 'audience' IS NOT recommended. Please use 'resource' instead.");
                builder.audience(config.getAudience());
            } else {
                throw new IllegalArgumentException("Either resource (preferable) or audience must be defined");
            }

            return builder.build();
        }

        public RequestInterceptor getRequestInterceptor() {
            return (template) -> {
                if (accessToken == null || accessToken.isValid(tokenRetrievedAt)) {
                    log.debug("accessToken is expired or null. Refreshing access token.");

                    final OAuthRequest request = getOAuthRequest();

                    log.info("{}: requesting a new access token", request.getShortName());

                    try {
                        accessToken = tokenExchangeClient.getToken(request);
                    } catch (FeignException.BadRequest e) {
                        log.error("{}: failed to obtain a new access token", request.getShortName(), e);
                        throw new TokenExchangeException(String.format("Failed to get access token for %s", request.getShortName()), e);
                    }

                    log.warn("{}: obtained a new access token", request.getShortName());

                    tokenRetrievedAt = Instant.now().getEpochSecond();
                } else {
                    log.debug("Using existing accessToken for connector as it should still be valid and un-expired.");
                }

                // Only pass authenticated requests for valid request URL
                if (config.getAllowAuthenticationForUrl() == null || template.url().matches(config.getAllowAuthenticationForUrl())) {
                    final String tokenBody = accessToken.getToken().split("\\.")[1];
                    log.debug("{}: Bearer Token (body part only): {}", getOAuthRequest().getShortName(), tokenBody);
                    template.removeHeader("Authorization");
                    template.header("Authorization", "Bearer " + accessToken.getToken());
                } else {
                    log.info("The connector request does not match {}. Sending request without authentication",
                        config.getAllowAuthenticationForUrl());
                }
            };
        }

        public Feign.Builder getBuilder() {
            return Feign.builder()
                .client(httpClient)
                .errorDecoder((methodKey, response) -> {
                    FeignException exception = errorStatus(methodKey, response);
                    if (response.status() == 401 || response.status() == 403) {
                        accessToken = null;
                        String wwwAuthenticateHeader = response.headers().containsKey("www-authenticate")
                            ? String.join("; ", response.headers().get("www-authenticate"))
                            : null;
                        return new RetryableException(
                            response.status(),
                            String.format("%s, WWW-Authenticate: [%s]", exception.getMessage(), wwwAuthenticateHeader),
                            response.request().httpMethod(),
                            exception,
                            Date.from(Instant.now()),
                            response.request());
                    }
                    return exception;
                })
                .retryer(new Retryer.Default(100L, TimeUnit.SECONDS.toMillis(1L), 2))
                .requestInterceptor(getRequestInterceptor());
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy