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

be.ugent.rml.target.HttpRequestTargetHelper Maven / Gradle / Ivy

Go to download

The RMLMapper executes RML rules to generate high quality Linked Data from multiple originally (semi-)structured data sources.

The newest version!
package be.ugent.rml.target;


import be.ugent.rml.NAMESPACES;
import org.apache.http.HttpStatus;
import org.jose4j.jwk.EcJwkGenerator;
import org.jose4j.jwk.EllipticCurveJsonWebKey;
import org.jose4j.jws.AlgorithmIdentifiers;
import org.jose4j.jws.JsonWebSignature;
import org.jose4j.jwt.JwtClaims;
import org.jose4j.keys.EllipticCurves;
import org.jose4j.lang.JoseException;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.List;
import java.util.Map;

public class HttpRequestTargetHelper {
    private static final Logger log = LoggerFactory.getLogger(HttpRequestTargetHelper.class);
    private final EllipticCurveJsonWebKey jwk;

    private final HttpClient httpClient;
    /**
     *
     * Constructs a new HttpRequestTargetHelper instance. A new private + public key pair
     * gets generated which is used for communication with the Solid server, more
     * specifically for Distributed Proof of Possession (DPoP).
     * @throws JoseException Generating the private/public key pair goes wrong.
     */
    public HttpRequestTargetHelper() throws JoseException {
        jwk = EcJwkGenerator.generateJwk(EllipticCurves.P256);
        // Initialize a HTTP client
        httpClient = HttpClient.newBuilder()
                .version(HttpClient.Version.HTTP_1_1)   // The Community Solid Server only accepts HTTP 1.1 and complains about upgrade headers if you don't specify this.
                .build();
    }

    /**
     * Retrieves the Dpop Access Token for CSS authentication using client credentials
     * @param httpRequestInfo A map with all necessary data. The map should contain following keys:
     *                  oidcIssuer, webId, email, password
     * @return        The Dpop Access Token.
     * @throws Exception Something goes wrong.
     */
    private String getDpopAccessToken(Map httpRequestInfo) throws Exception {

        // See https://blog.stackademic.com/how-dpop-works-a-guide-to-proof-of-possession-for-web-tokens-cbeac2d4e43c
        // for a simple and clear description of how DPoP works.

        try {
            // TODO: this implementation is tailored to the Community Solid Server and thus uses one URL for authorization server and solid server.
            String oidcIssuer = httpRequestInfo.get("oidcIssuer");
            String webId = httpRequestInfo.get("webId");
            String email = httpRequestInfo.get("email");
            String password = httpRequestInfo.get("password");

            /* Get account controls and retrieve login URL */
            HttpRequest accountInfoRequest = HttpRequest.newBuilder(URI.create(oidcIssuer + ".account/"))
                    .GET().build();
            HttpResponse accountInfoResponse = httpClient.send(accountInfoRequest, HttpResponse.BodyHandlers.ofString());
            String accountInfoStr = accountInfoResponse.body();
            if (accountInfoResponse.statusCode() != HttpStatus.SC_OK) {
                log.error("Could not get account info: {}", accountInfoStr);
                throw new Exception("Could not get account info: " + accountInfoStr);
            }
            JSONObject accountInfo = new JSONObject(accountInfoStr);
            String passwordLoginURL = accountInfo.getJSONObject("controls").getJSONObject("password").getString("login");
            log.debug("Found login URL: {}", passwordLoginURL);


            /* Log in using e-mail and password and get authorization token */
            String loginMessage = "{\"email\": \"" + email + "\",\"password\":\"" + password + "\"}";
            HttpRequest loginRequest = HttpRequest.newBuilder(URI.create(passwordLoginURL))
                    .POST(HttpRequest.BodyPublishers.ofString(loginMessage, StandardCharsets.UTF_8))
                    .setHeader("Content-Type", "application/json")
                    .build();
            HttpResponse loginResponse = httpClient.send(loginRequest, HttpResponse.BodyHandlers.ofString());
            String loginInfoStr = loginResponse.body();
            if (loginResponse.statusCode() != HttpStatus.SC_OK) {
                log.error("Could not log in: {}", loginInfoStr);
                throw new Exception("Could not get log in: " + loginInfoStr);
            }
            JSONObject loginInfo = new JSONObject(loginInfoStr);
            String authorizationToken = loginInfo.getString("authorization");
            log.debug("Found authorization token.");


            /* Use authorization token to get client credentials URL, added to account info */
            HttpRequest authorizedAccountInfoRequest = HttpRequest.newBuilder(URI.create(oidcIssuer + ".account/"))
                    .GET()
                    .setHeader("Authorization", "CSS-Account-Token " + authorizationToken)
                    .build();
            HttpResponse authorizedAccountInfoResponse = httpClient.send(authorizedAccountInfoRequest, HttpResponse.BodyHandlers.ofString());
            String authorizedAccountInfoStr = authorizedAccountInfoResponse.body();
            if (authorizedAccountInfoResponse.statusCode() != HttpStatus.SC_OK) {
                log.error("Could not get account info: {}", authorizedAccountInfoStr);
                throw new Exception("Could not get account info: " + authorizedAccountInfoStr);
            }
            JSONObject authorizedAccountInfo = new JSONObject(authorizedAccountInfoStr);
            String clientCredentialsURL = authorizedAccountInfo.getJSONObject("controls").getJSONObject("account").getString("clientCredentials");
            log.debug("Found client credentials URL: {}", clientCredentialsURL);


            /* Post WebID and token prefix to client credentials URL to get client credentials, to be used at oidc endpoint later on */
            String webIdAndTokenMessage = "{\"name\": \"my-token\",\"webId\":\"" + webId + "\"}";
            HttpRequest getOIDCTokenRequest = HttpRequest.newBuilder(URI.create(clientCredentialsURL))
                    .POST(HttpRequest.BodyPublishers.ofString(webIdAndTokenMessage, StandardCharsets.UTF_8))
                    .setHeader("Authorization", "CSS-Account-Token " + authorizationToken)
                    .setHeader("Content-Type", "application/json")
                    .build();
            HttpResponse getOIDCTokenResponse = httpClient.send(getOIDCTokenRequest, HttpResponse.BodyHandlers.ofString());
            String clientCredentialsStr = getOIDCTokenResponse.body();
            if (getOIDCTokenResponse.statusCode() != HttpStatus.SC_OK) {
                log.error("Could not get OpenID Connect token info: {}", clientCredentialsStr);
                throw new Exception("Could not get OpenID Connect token info: " + clientCredentialsStr);
            }
            JSONObject clientCredentials = new JSONObject(clientCredentialsStr);
            String clientCredentialsId = clientCredentials.getString("id");
            log.debug("Found Client credentials. id: {}", clientCredentialsId);
            String clientCredentialsSecret = clientCredentials.getString("secret");


            /* Get oidc info, used to obtain oidc token endpoints */
            // GET /.well-known/openid-configuration HTTP/1.1
            HttpRequest oidcInfoRequest = HttpRequest.newBuilder(URI.create(oidcIssuer + ".well-known/openid-configuration"))
                    .GET().build();
            HttpResponse oidcInfoResponse = httpClient.send(oidcInfoRequest, HttpResponse.BodyHandlers.ofString());
            String oidcInfoStr = oidcInfoResponse.body();
            if (oidcInfoResponse.statusCode() != HttpStatus.SC_OK) {
                log.error("Could not get OpenID Connect info: {}", oidcInfoStr);
                throw new Exception("Could not get OpenID Connect info: " + oidcInfoStr);
            }
            JSONObject oidcInfo = new JSONObject(oidcInfoStr);
            String oidcTokenEndpoint = oidcInfo.getString("token_endpoint");
            log.debug("Found oidc token endpoint: {}", oidcTokenEndpoint);

            String dpopJWT = generateJWT(oidcTokenEndpoint, "POST");

            /* POST a request to oidc token endpoint with client credentials to obtain an oidc access token */

            // Generate base64 string of client credentials
            String clientCredentialsConcatenated = clientCredentialsId + ':' + clientCredentialsSecret;
            String base64clientCredentials = Base64.getEncoder().encodeToString(clientCredentialsConcatenated.getBytes(StandardCharsets.UTF_8));

            HttpRequest getOidcAccessTokenRequest = HttpRequest.newBuilder(URI.create(oidcTokenEndpoint))
                    .POST(HttpRequest.BodyPublishers.ofString("grant_type=client_credentials&scope=webid", StandardCharsets.UTF_8))
                    .setHeader("Authorization", "Basic " + base64clientCredentials)
                    .setHeader("Content-Type", "application/x-www-form-urlencoded")
                    .setHeader("DPoP", dpopJWT)
                    .build();
            HttpResponse oidcAccessTokenResponse = httpClient.send(getOidcAccessTokenRequest, HttpResponse.BodyHandlers.ofString());
            String oidcAccessTokenStr = oidcAccessTokenResponse.body();
            if (oidcAccessTokenResponse.statusCode() != HttpStatus.SC_OK) {
                log.error("Could not get OpenID Connect access token: {}", oidcAccessTokenStr);
                throw new Exception("Could not get OpenID Connect info: " + oidcAccessTokenStr);
            }
            JSONObject oidcAccessToken = new JSONObject(oidcAccessTokenStr);
            return oidcAccessToken.getString("access_token");
            // token_type should be 'DPoP'
            // We don't use 'expires' at the moment because we send the next request immediately
            // and don't know if the next request would go to the same server. This can be checked
            // for in future implementations.
        } catch (Throwable e) { // This is to catch runtime exceptions as well.
            throw new Exception(e);
        }

    }

    /**
     * Generates a JSON Web Token for a given URL and HTTP method.
     * @param url     The endpoint of the request using this token.
     * @param method  The HTTP method of the request using this token.
     * @return        The signed and serialized generated JWT.
     * @throws JoseException    Olé José: something goes wrong generating the token.
     */
    private String generateJWT(final String url, final String method) throws JoseException {
        // set claims
        JwtClaims claims = new JwtClaims();
        claims.setGeneratedJwtId(); // jti claim
        claims.setClaim("htm", method);
        claims.setClaim("htu", url);
        claims.setIssuedAtToNow(); // iat claim

        // create jws
        JsonWebSignature jws = new JsonWebSignature();
        jws.setPayload(claims.toJson());
        jws.setKey(jwk.getPrivateKey());
        jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.ECDSA_USING_P256_CURVE_AND_SHA256);  // alg header
        jws.setHeader("typ", "dpop+jwt");
        jws.setJwkHeader(jwk);
        jws.sign();
        return jws.getCompactSerialization();
    }

    /** Executes a HTTP request to a web resource, optionally using authentication with CSS client credentials.
     * @param httpRequestInfo A map with all necessary data. The map should contain following keys: absoluteURI, methodName.
     *                        The map may contain following keys: contentType, accept, data,
     *                        authenticationType, email, password, oidcIssuer, webId
     *                        absoluteURI, methodName, contentType, data
     * @return response body
     * @throws Exception Something goes wrong.
     */
    public String executeHttpRequest(Map httpRequestInfo) throws Exception{
        try {
            String absoluteURI = httpRequestInfo.get("absoluteURI");
            String methodName = httpRequestInfo.get("methodName");

            HttpRequest.Builder RequestBuilder = HttpRequest.newBuilder(URI.create(absoluteURI));

            if (httpRequestInfo.containsKey("data")){
                RequestBuilder.method(methodName,
                        HttpRequest.BodyPublishers.ofString(httpRequestInfo.get("data"), StandardCharsets.UTF_8));
            } else {
                RequestBuilder.method(methodName, HttpRequest.BodyPublishers.noBody());
            }
            if (httpRequestInfo.containsKey("contentType")){
                RequestBuilder.setHeader("Content-Type", httpRequestInfo.get("contentType"));
            }
            if (httpRequestInfo.containsKey("accept")){
                RequestBuilder.setHeader("Accept", httpRequestInfo.get("accept"));
            }
            addAuthentication(RequestBuilder, httpRequestInfo);
            HttpRequest httpRequest = RequestBuilder.build();

            HttpResponse httpResponse = httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofString());
            if (isNotSuccessful(httpResponse.statusCode())) {
                log.error("Could not successfully execute HTTP request for URI {} and method {}: {}", absoluteURI, methodName, httpResponse.statusCode());
                throw new Exception("Could not successfully execute HTTP request for URI " + absoluteURI + " and method " + methodName+ ": " + httpResponse.statusCode());
            }
            return httpResponse.body();
        } catch (Throwable e) { // This is to catch runtime exceptions as well.
            throw new Exception(e);
        }
    }

    /** Executes a HTTP request to a linked web resource, optionally using authentication with CSS client credentials.
     * @param httpRequestInfo A map with all necessary data. The map should contain following keys:
     *                        linkingAbsoluteURI, linkRelation, methodName.
     *                        The map may contain following keys: contentType, accept, data,
     *                        authenticationType, email, password, oidcIssuer, webId
     * @return response body
     * @throws Exception Something goes wrong.
     */
    public String executeLinkedHttpRequest(Map httpRequestInfo) throws Exception{
        try {
            String linkingAbsoluteURI = httpRequestInfo.get("linkingAbsoluteURI");
            String linkRelation = httpRequestInfo.get("linkRelation");
            /* HEAD request to retrieve the linked absolute URI via a link relation */
            HttpRequest.Builder RequestBuilder = HttpRequest.newBuilder(URI.create(linkingAbsoluteURI))
                    .method(HttpMethod.HEAD.name(), HttpRequest.BodyPublishers.noBody());
            addAuthentication(RequestBuilder, httpRequestInfo);
            HttpRequest httpRequest = RequestBuilder.build();

            HttpResponse headResponse = httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofString());

            List links = headResponse.headers().map().get("link");
            boolean foundLink = false;
            int index = 0;
            String absoluteURI = null;
            String responseBody = null;
            while (!foundLink && index < links.size() ){
                String link = links.get(index);
                // a better method to parse the link header in Java would be welcome ...
                if (link.contains("rel=\"" + linkRelation + "\"")) {
                    absoluteURI = link.substring(link.indexOf("<") + 1, link.indexOf(">"));
                    httpRequestInfo.put("absoluteURI", absoluteURI);
                    responseBody = executeHttpRequest(httpRequestInfo);
                    foundLink = true;
                }
                index += 1;
            }

            if (!foundLink) {
                String message = "Could not get linked absolute URI for link relation " + linkRelation
                        + " and linking absolute URI " + linkingAbsoluteURI;
                log.error(message);
                throw new Exception(message);
            } else {
                return responseBody;
            }

        } catch (Throwable e) { // This is to catch runtime exceptions as well.
            throw new Exception(e);
        }
    }

    private void addAuthentication(HttpRequest.Builder RequestBuilder, Map httpRequestInfo) throws Exception {
        if (httpRequestInfo.containsKey("authenticationType") &&
                // only authentication with CSS Client Credentials implemented until now
                httpRequestInfo.get("authenticationType").equals(NAMESPACES.RMLE + "CssClientCredentialsAuthentication")) {
            // Get dpop access token
            String dpopAccessToken = getDpopAccessToken(httpRequestInfo);
            RequestBuilder.setHeader("Authorization", "DPoP " + dpopAccessToken);
            // Generate new JWT token for this request
            String dataJWT = generateJWT(httpRequestInfo.get("absoluteURI"), httpRequestInfo.get("methodName"));
            RequestBuilder.setHeader("DPoP", dataJWT);
        }
    }


    private boolean isNotSuccessful(int statusCode){
        return (statusCode < 200) || (statusCode > 299) ;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy