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

no.ks.fiks.maskinporten.Maskinportenklient Maven / Gradle / Ivy

package no.ks.fiks.maskinporten;

import com.nimbusds.jose.*;
import com.nimbusds.jose.crypto.RSASSASigner;
import com.nimbusds.jose.util.Base64;
import com.nimbusds.jose.util.JSONObjectUtils;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
import lombok.Builder;
import lombok.Data;
import lombok.NonNull;
import net.jodah.expiringmap.ExpirationPolicy;
import net.jodah.expiringmap.ExpiringEntryLoader;
import net.jodah.expiringmap.ExpiringMap;
import net.jodah.expiringmap.ExpiringValue;
import no.ks.fiks.maskinporten.error.MaskinportenClientTokenRequestException;
import no.ks.fiks.maskinporten.error.MaskinportenTokenRequestException;
import org.apache.commons.codec.Charsets;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
import org.apache.hc.core5.http.*;
import org.apache.hc.core5.http.io.HttpClientResponseHandler;
import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.text.ParseException;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import static java.util.Collections.singletonList;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.MINUTES;

public class Maskinportenklient {
    private static final Logger log = LoggerFactory.getLogger(Maskinportenklient.class);
    static final String GRANT_TYPE = "urn:ietf:params:oauth:grant-type:jwt-bearer";
    static final String CLAIM_SCOPE = "scope";
    static final String CLAIM_CONSUMER_ORG = "consumer_org";
    static final String MDC_JTIID = "jtiId";
    private final MaskinportenklientProperties properties;
    private final JWSHeader jwsHeader;
    private final JWSSigner signer;
    private final ExpiringMap map;

    public Maskinportenklient(@NonNull KeyStore keyStore, String privateKeyAlias, char[] privateKeyPassword, @NonNull MaskinportenklientProperties properties) throws KeyStoreException, CertificateEncodingException, UnrecoverableKeyException, NoSuchAlgorithmException {
        this((PrivateKey) keyStore.getKey(privateKeyAlias, privateKeyPassword), (X509Certificate) keyStore.getCertificate(privateKeyAlias), properties);
    }

    public Maskinportenklient(@NonNull PrivateKey privateKey, X509Certificate certificate, @NonNull MaskinportenklientProperties properties) throws CertificateEncodingException {
        this.properties = properties;
        jwsHeader = new JWSHeader.Builder(JWSAlgorithm.RS256)
                .x509CertChain(singletonList(Base64.encode(certificate.getEncoded())))
                .build();
        signer = new RSASSASigner(privateKey);

        map = ExpiringMap.builder()
                .variableExpiration()
                .expiringEntryLoader((ExpiringEntryLoader) tokenRequest -> {
                    final Map json = parse(doAcquireAccessToken(tokenRequest));
                    final Map accessToken = parseAccessToken(json);
                    final long expiresIn = getExpiresIn(json);
                    final long duration = expiresIn - properties.getNumberOfSecondsLeftBeforeExpire();
                    long exp = TimeUnit.MILLISECONDS.convert(getExp(accessToken), TimeUnit.SECONDS);
                    log.info("Adding access token to cache; access_token.scopes: '{}', access_token.exp: {}, expires_in: {} seconds. Expires from cache in {} seconds ({}).", json.get(CLAIM_SCOPE), new Date(exp), expiresIn, duration, new Date(System.currentTimeMillis() + (1000 * duration)));
                    return new ExpiringValue<>(json.get("access_token").toString(), ExpirationPolicy.CREATED, duration, TimeUnit.SECONDS);
                })
                .build();
    }

    private long getExpiresIn(Map json) {
        Object value = Objects.requireNonNull(json.get("expires_in"), "JSON response fra Maskinporten mangler felt 'expires_in'");
        return Long.parseLong(value.toString());
    }

    private long getExp(Map accessToken) {
        Object value = Objects.requireNonNull(accessToken.get("exp"), "Access token fra Maskinporten mangler felt 'exp'");
        return Long.parseLong(value.toString());
    }

    public String getAccessToken(@NonNull Collection scopes) {

        return getTokenForRequest(AccessTokenRequest.builder().scopes(new HashSet<>(scopes)).build());
    }

    public String getAccessToken(String... scopes) {
        return getAccessToken(scopesToCollection(scopes));
    }

    public String getDelegatedAccessToken(@NonNull String consumerOrg, @NonNull Collection scopes) {

        return getTokenForRequest(AccessTokenRequest.builder().scopes(new HashSet<>(scopes)).consumerOrg(consumerOrg).build());
    }

    public String getDelegatedAccessToken(@NonNull String consumerOrg, String... scopes) {
        return getDelegatedAccessToken(consumerOrg, scopesToCollection(scopes));
    }

    private String getTokenForRequest(@NonNull AccessTokenRequest accessTokenRequest) {
        if (accessTokenRequest.getScopes().isEmpty()) {
            throw new IllegalArgumentException("Minst ett scope må oppgies");
        }
        return map.get(accessTokenRequest);
    }

    protected String createJwtRequestForAccessToken(AccessTokenRequest accessTokenRequest, String jtiId) throws JOSEException {
        final long issuedTimeInMillis = System.currentTimeMillis();
        final long expirationTimeInMillis = issuedTimeInMillis + MILLISECONDS.convert(2, MINUTES);

        final String audience = properties.getAudience();
        final String issuer = properties.getIssuer();
        final String claimScopes = String.join(" ", accessTokenRequest.getScopes());
        final String consumerOrg = Optional.ofNullable(accessTokenRequest.consumerOrg).orElse(properties.getConsumerOrg());
        log.debug("Signing JWTRequest with audience='{}',issuer='{}',scopes='{}',consumerOrg='{}', jtiId='{}'", audience, issuer, claimScopes, consumerOrg, jtiId);
        final JWTClaimsSet.Builder claimBuilder = new JWTClaimsSet.Builder()
                .audience(audience)
                .issuer(issuer)
                .claim(CLAIM_SCOPE, claimScopes)
                .jwtID(jtiId)
                .issueTime(new Date(issuedTimeInMillis))
                .expirationTime(new Date(expirationTimeInMillis));

        if (consumerOrg != null) {
            claimBuilder.claim(CLAIM_CONSUMER_ORG, consumerOrg);
        }
        final SignedJWT signedJWT = new SignedJWT(jwsHeader, claimBuilder
                .build());
        signedJWT.sign(signer);
        return signedJWT.serialize();
    }

    private String doAcquireAccessToken(AccessTokenRequest accessTokenRequest) {
        try {
            return acquireAccessToken(accessTokenRequest);
        } catch (JOSEException | IOException e) {
            log.error("Could not acquire access token due to an exception", e);
            throw new RuntimeException(e);
        }
    }

    private String acquireAccessToken(AccessTokenRequest accessTokenRequest) throws JOSEException, IOException {
        final String jtiId = UUID.randomUUID().toString();
        try(MDC.MDCCloseable ignore = MDC.putCloseable(MDC_JTIID, jtiId)) {
            final byte[] postData = "grant_type={grant_type}&assertion={assertion}"
                    .replace("{grant_type}", GRANT_TYPE)
                    .replace("{assertion}", createJwtRequestForAccessToken(accessTokenRequest, jtiId))
                    .getBytes(StandardCharsets.UTF_8);

            final String tokenEndpointUrlString = properties.getTokenEndpoint();
            log.debug("Acquiring access token from \"{}\"", tokenEndpointUrlString);
            long startTime = System.currentTimeMillis();
            try (final CloseableHttpClient httpClient = HttpClientBuilder.create()
                    .disableAutomaticRetries()
                    .disableRedirectHandling()
                    .disableAuthCaching()
                    .build()) {
                return httpClient.execute(createHttpRequest(postData), new HttpClientResponseHandler() {
                    @Override
                    public String handleResponse(final ClassicHttpResponse classicHttpResponse) throws IOException {
                        int responseCode = classicHttpResponse.getCode();
                        log.debug("Access token response received in {} ms with status {}", System.currentTimeMillis() - startTime, responseCode);

                        if (HttpStatus.SC_OK == responseCode) {
                            try (final InputStream contentStream = classicHttpResponse.getEntity().getContent()) {
                                return toString(contentStream);
                            }
                        } else {
                            final String errorFromMaskinporten;
                            try (final InputStream errorContentStream = classicHttpResponse.getEntity().getContent()) {
                                errorFromMaskinporten = toString(errorContentStream);
                            }
                            final String exceptionMessage = String.format("Http response code: %s, url: '%s', scopes: '%s', message: '%s'", responseCode,
                                    tokenEndpointUrlString, accessTokenRequest, errorFromMaskinporten);
                            log.warn("Failed to get token: {}", errorFromMaskinporten);
                            if (responseCode >= HttpStatus.SC_BAD_REQUEST && responseCode < HttpStatus.SC_INTERNAL_SERVER_ERROR) {
                                throw new MaskinportenClientTokenRequestException(exceptionMessage, responseCode, errorFromMaskinporten);
                            } else {
                                throw new MaskinportenTokenRequestException(exceptionMessage, responseCode, errorFromMaskinporten);
                            }
                        }
                    }

                    private String toString(InputStream inputStream) throws IOException {
                        if (inputStream == null) {
                            return null;
                        }

                        try (InputStreamReader isr = new InputStreamReader(inputStream);
                             BufferedReader br = new BufferedReader(isr)) {
                            return br.lines().collect(Collectors.joining("\n"));
                        }
                    }

                });
            }
        }
    }

    private ClassicHttpRequest createHttpRequest(byte[] entityBuffer) {
        return ClassicRequestBuilder.post(properties.getTokenEndpoint())
                .setCharset(Charsets.UTF_8)
                .addHeader("Charset", Charsets.UTF_8.name())
                .addHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_FORM_URLENCODED.getMimeType())
                .setEntity(entityBuffer, ContentType.APPLICATION_FORM_URLENCODED)
                .build();
    }


    private Map parse(String value) {
        try {
            return JSONObjectUtils.parse(value);
        } catch (ParseException e) {
            throw new RuntimeException(e);
        }
    }

    private Map parseAccessToken(Map json) {
        try {
            Object accessToken = Objects.requireNonNull(json.get("access_token"), "JSON response fra Maskinporten mangler felt 'access_token'");
            return JWSObject.parse(accessToken.toString())
                    .getPayload()
                    .toJSONObject();
        } catch (ParseException e) {
            throw new RuntimeException(e);
        }
    }

    private static Collection scopesToCollection(String... scopes) {
        return Arrays.asList(String.join(" ", scopes).split("\\s"));
    }

    @Data
    @Builder
    private static final class AccessTokenRequest {
        @NonNull
        private final Set scopes;
        private final String consumerOrg;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy