org.dcache.webdav.transfer.CredentialServiceClient.java Source code

Java tutorial

Introduction

Here is the source code for org.dcache.webdav.transfer.CredentialServiceClient.java

Source

/* dCache - http://www.dcache.org/
 *
 * Copyright (C) 2014 Deutsches Elektronen-Synchrotron
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.dcache.webdav.transfer;

import com.google.common.base.Charsets;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.collect.ImmutableMap;
import eu.emi.security.authn.x509.X509Credential;
import eu.emi.security.authn.x509.impl.KeyAndCertCredential;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.auth.AuthenticationException;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.auth.BasicScheme;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.protocol.BasicHttpContext;
import org.json.JSONException;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Required;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.security.KeyStoreException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import diskCacheV111.srm.CredentialServiceAnnouncement;
import diskCacheV111.srm.CredentialServiceRequest;
import diskCacheV111.srm.dcache.SrmRequestCredentialMessage;
import diskCacheV111.util.CacheException;

import dmg.cells.nucleus.CellAddressCore;
import dmg.cells.nucleus.CellLifeCycleAware;
import dmg.cells.nucleus.CellMessageReceiver;
import dmg.cells.nucleus.CellPath;
import dmg.cells.nucleus.NoRouteToCellException;

import org.dcache.auth.OpenIdClientSecret;
import org.dcache.auth.StaticOpenIdCredential;
import org.dcache.auth.StaticOpenIdCredential.Builder;
import org.dcache.cells.CellStub;

import static java.util.concurrent.TimeUnit.SECONDS;

/**
 * This class acts as a client to credential services.
 */
public class CredentialServiceClient implements CellMessageReceiver, CellLifeCycleAware {
    private static final Logger LOGGER = LoggerFactory.getLogger(CredentialServiceClient.class);

    private static final String GRANT_TYPE = "urn:ietf:params:oauth:grant-type:token-exchange";
    private static final String TOKEN_TYPE = "urn:ietf:params:oauth:token-type:access_token";
    private static final String SCOPE = "offline_access openid profile email";

    private CellStub topic;

    private Cache<CellAddressCore, URI> cache = CacheBuilder.newBuilder().expireAfterWrite(70, SECONDS).build();

    @Required
    public void setTopicStub(CellStub topic) {
        this.topic = topic;
    }

    @Override
    public void afterStart() {
        topic.notify(new CredentialServiceRequest());
    }

    public void messageArrived(CredentialServiceAnnouncement message) {
        cache.put(message.getCellAddress(), message.getDelegationEndpoint());
    }

    public Collection<URI> getDelegationEndpoints() {
        return cache.asMap().values();
    }

    public X509Credential getDelegatedCredential(String dn, String primaryFqan, int minimumValidity, TimeUnit units)
            throws InterruptedException, ErrorResponseException {
        long bestRemainingLifetime = 0;
        X509Credential bestCredential = null;

        for (CellAddressCore address : cache.asMap().keySet()) {
            CellPath path = new CellPath(address);
            SrmRequestCredentialMessage msg = new SrmRequestCredentialMessage(dn, primaryFqan);
            try {
                msg = topic.sendAndWait(path, msg);

                if (!msg.hasCredential()) {
                    continue;
                }

                X509Certificate[] certificates = msg.getCertificateChain();
                long lifetime = calculateRemainingLifetime(certificates);
                if (lifetime > bestRemainingLifetime) {
                    bestCredential = new KeyAndCertCredential(msg.getPrivateKey(), certificates);
                    bestRemainingLifetime = lifetime;
                }
            } catch (CacheException | NoRouteToCellException e) {
                LOGGER.debug("failed to contact {} querying for {}, {}: {}", path, dn, primaryFqan, e.getMessage());
            } catch (KeyStoreException e) {
                LOGGER.warn("Received invalid key pair from {} for {}, {}: {}", path, dn, primaryFqan,
                        e.getMessage());
            }
        }

        return bestRemainingLifetime < units.toMillis(minimumValidity) ? null : bestCredential;
    }

    public StaticOpenIdCredential getDelegatedCredential(String token,
            ImmutableMap<String, OpenIdClientSecret> clientSecrets)
            throws InterruptedException, ErrorResponseException {
        HttpClient client = HttpClientBuilder.create().build();
        for (Map.Entry<String, OpenIdClientSecret> entry : clientSecrets.entrySet()) {
            String host = entry.getKey();
            String id = entry.getValue().getId();
            String secret = entry.getValue().getSecret();

            try {
                JSONObject json = delegateOpenIdCredential(client, buildRequest(token, host, id, secret));

                return createOidcCredential(host, id, secret, json);
            } catch (AuthenticationException | IOException | JSONException e) {
                LOGGER.warn("Fail Token Delegation with Openid Provider {}", host);
            }
        }
        return null;
    }

    private HttpPost buildRequest(String token, String host, String clientId, String clientSecret)
            throws UnsupportedEncodingException, AuthenticationException {
        UsernamePasswordCredentials clientCreds = new UsernamePasswordCredentials(clientId, clientSecret);
        BasicScheme scheme = new BasicScheme(Charsets.UTF_8);

        HttpPost post = new HttpPost(tokenEndPoint(host));
        List<NameValuePair> params = new ArrayList<>();
        params.add(new BasicNameValuePair("grant_type", GRANT_TYPE));
        params.add(new BasicNameValuePair("audience", clientId));
        params.add(new BasicNameValuePair("subject_token", token));
        params.add(new BasicNameValuePair("subject_token_type", TOKEN_TYPE));
        params.add(new BasicNameValuePair("scope", SCOPE));

        post.setEntity(new UrlEncodedFormEntity(params));
        post.addHeader(scheme.authenticate(clientCreds, post, new BasicHttpContext()));
        return post;
    }

    private JSONObject delegateOpenIdCredential(HttpClient client, HttpPost post) throws IOException {
        HttpResponse response = client.execute(post);
        if (response.getStatusLine().getStatusCode() == 200) {
            ByteArrayOutputStream os = new ByteArrayOutputStream();
            response.getEntity().writeTo(os);
            return new JSONObject(new String(os.toByteArray(), Charsets.UTF_8));
        } else {
            throw new IOException("Http Request Error (" + response.getStatusLine().getStatusCode() + "): ["
                    + response.getStatusLine().getReasonPhrase() + "]");
        }
    }

    private StaticOpenIdCredential createOidcCredential(String host, String clientId, String clientSecret,
            JSONObject json) {
        return new Builder().accessToken(json.getString("access_token")).expiry(json.getLong("expires_in"))
                .refreshToken(json.getString("refresh_token")).issuedTokenType(json.getString("issued_token_type"))
                .scope(json.getString("scope")).tokenType(json.getString("token_type"))
                .clientCredential(new OpenIdClientSecret(clientId, clientSecret)).provider(tokenEndPoint(host))
                .build();
    }

    private String tokenEndPoint(String hostname) {
        return "https://" + hostname + "/token";
    }

    private static long calculateRemainingLifetime(X509Certificate[] certificates) {
        long earliestExpiry = Long.MAX_VALUE;

        for (X509Certificate certificate : certificates) {
            earliestExpiry = Math.min(earliestExpiry, certificate.getNotAfter().getTime());
        }

        long now = System.currentTimeMillis();

        return (earliestExpiry <= now) ? 0 : earliestExpiry - now;
    }
}