/* 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;
}
}