001/** 002 * Copyright (C) 2006-2025 Talend Inc. - www.talend.com 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.talend.sdk.component.runtime.manager.service.http.configurer; 017 018import static java.util.Collections.emptyMap; 019import static java.util.Locale.ROOT; 020import static java.util.Optional.ofNullable; 021import static java.util.stream.Collectors.joining; 022import static java.util.stream.Collectors.toMap; 023 024import java.io.UnsupportedEncodingException; 025import java.net.MalformedURLException; 026import java.net.URL; 027import java.net.URLEncoder; 028import java.nio.charset.StandardCharsets; 029import java.security.InvalidKeyException; 030import java.security.MessageDigest; 031import java.security.NoSuchAlgorithmException; 032import java.security.Signature; 033import java.security.SignatureException; 034import java.util.Base64; 035import java.util.Map; 036import java.util.Objects; 037import java.util.TreeMap; 038import java.util.UUID; 039import java.util.concurrent.TimeUnit; 040import java.util.stream.Stream; 041 042import javax.crypto.Mac; 043import javax.crypto.spec.SecretKeySpec; 044 045import org.talend.sdk.component.api.service.http.Configurer; 046import org.talend.sdk.component.api.service.http.configurer.oauth1.OAuth1; 047 048public class OAuth1ProviderImpl implements OAuth1.OAuth1Provider { 049 050 @Override 051 public Map<String, String> buildParameters(final String method, final String url, final byte[] payload, 052 final OAuth1.Configuration oauth1Config) { 053 final String algorithm = ofNullable(oauth1Config.getAlgorithm()).orElse("HMAC-SHA1"); 054 final Map<String, String> values = new TreeMap<>(); 055 values.put("oauth_consumer_key", oauth1Config.getConsumerKey()); 056 values.put("oauth_nonce", ofNullable(oauth1Config.getNonce()).orElseGet(this::newNonce)); 057 values.put("oauth_signature_method", algorithm); 058 values 059 .put("oauth_timestamp", ofNullable(oauth1Config.getTimestamp()) 060 .map(String::valueOf) 061 .orElseGet(() -> Long.toString(TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis())))); 062 values.put("oauth_version", "1.0"); 063 ofNullable(oauth1Config.getToken()).ifPresent(token -> values.put("oauth_token", token)); 064 ofNullable(oauth1Config.getPayloadHashAlgorithm()) 065 .ifPresent(algo -> values 066 .put("oauth_body_hash", hash(algo, ofNullable(payload).orElseGet(() -> new byte[0])))); 067 ofNullable(oauth1Config.getOauthParameters()).ifPresent(values::putAll); 068 values.entrySet().forEach(e -> e.setValue(encode(e.getValue()))); 069 values.putAll(extractQuery(url)); 070 071 final String signature = sign(algorithm, signingString(values, method, url), oauth1Config); 072 values.put("oauth_signature", signature); 073 return values; 074 } 075 076 private Map<String, String> extractQuery(final String url) { 077 if (url.contains("?")) { 078 final String query = url.substring(url.indexOf('?') + 1); 079 if (!query.isEmpty()) { 080 return Stream.of(query.split("&")).map(kv -> { 081 final int sep = kv.indexOf("="); 082 if (sep > 0) { 083 return new String[] { kv.substring(0, sep), kv.substring(sep + 1) }; 084 } 085 return new String[] { kv, "" }; 086 }).collect(toMap(pair -> pair[0], pair -> pair[1])); 087 } 088 } 089 return emptyMap(); 090 } 091 092 @Override 093 public Configurer newConfigurer() { 094 return (connection, configuration) -> { 095 096 final OAuth1.Configuration oauth1Config = Stream 097 .of(configuration.configuration()) 098 .filter(OAuth1.Configuration.class::isInstance) 099 .findFirst() 100 .map(OAuth1.Configuration.class::cast) 101 .orElseThrow(() -> new IllegalArgumentException("No OAuth1.Configuration @ConfigurerOption set")); 102 103 final Map<String, String> values = 104 buildParameters(connection.getMethod(), connection.getUrl(), connection.getPayload(), oauth1Config); 105 106 final String authorization = ofNullable(oauth1Config.getHeaderPrefix()).orElse("OAuth ") + values 107 .entrySet() 108 .stream() 109 .filter(e -> e.getKey().startsWith("oauth_")) 110 .map(e -> e.getKey() + "=\"" + e.getValue() + "\"") 111 .collect(joining(", ")); 112 connection.withHeader(ofNullable(oauth1Config.getHeader()).orElse("Authorization"), authorization); 113 }; 114 } 115 116 private String hash(final String algo, final byte[] payload) { 117 if ("plain".equalsIgnoreCase(algo)) { 118 return Base64.getEncoder().encodeToString(payload); 119 } 120 try { 121 final MessageDigest digest = MessageDigest.getInstance(algo); 122 return Base64.getEncoder().encodeToString(digest.digest(payload)); 123 } catch (final NoSuchAlgorithmException e) { 124 throw new IllegalArgumentException("Invalid hashing computation using algorithm: " + algo, e); 125 } 126 } 127 128 private String sign(final String algorithm, final String signingString, final OAuth1.Configuration configuration) { 129 if (algorithm.toLowerCase(ROOT).contains("hmac")) { 130 final byte[] signingKey = ofNullable(configuration.getSigningHmacKey()) 131 .orElseGet(() -> Stream 132 .of(configuration.getConsumerSecret(), configuration.getTokenSecret()) 133 .filter(Objects::nonNull) 134 .map(this::encode) 135 .collect(joining("&")) 136 .getBytes(StandardCharsets.UTF_8)); 137 try { 138 final SecretKeySpec key = new SecretKeySpec(signingKey, algorithm); 139 final Mac mac = Mac.getInstance(key.getAlgorithm().replace("-", "")); 140 mac.init(key); 141 return encode(Base64 142 .getEncoder() 143 .encodeToString(mac.doFinal(signingString.getBytes(StandardCharsets.UTF_8)))); 144 } catch (final InvalidKeyException | NoSuchAlgorithmException e) { 145 throw new IllegalStateException(e); 146 } 147 } else { 148 try { 149 final Signature signature = Signature.getInstance(algorithm.replace("-", "")); 150 signature.initSign(configuration.getSigningSignatureKey()); 151 signature.update(signingString.getBytes(StandardCharsets.UTF_8)); 152 return encode(Base64.getEncoder().encodeToString(signature.sign())); 153 } catch (final SignatureException | InvalidKeyException | NoSuchAlgorithmException e) { 154 throw new IllegalArgumentException(e); 155 } 156 } 157 } 158 159 private String signingString(final Map<String, String> values, final String method, final String url) { 160 return method.toUpperCase(ROOT) + "&" + encode(prepareUrl(url)) + "&" 161 + encode(values 162 .entrySet() 163 .stream() 164 .map(e -> String.format("%s=%s", e.getKey(), e.getValue())) 165 .collect(joining("&"))); 166 } 167 168 private String prepareUrl(final String url) { 169 try { 170 final URL parsed = new URL(url); 171 return parsed.getProtocol() + "://" + parsed.getHost() 172 + (shouldSkipPort(parsed) ? "" : ":" + parsed.getPort()) + stripQuery(parsed.getFile()); 173 } catch (final MalformedURLException e) { // very unlikely 174 return stripQuery(url); 175 } 176 } 177 178 private boolean shouldSkipPort(final URL parsed) { 179 return parsed.getPort() == -1 || (parsed.getPort() == 80 && "http".equals(parsed.getProtocol())) 180 || (parsed.getPort() == 443 && "https".equals(parsed.getProtocol())); 181 } 182 183 private String stripQuery(final String str) { 184 if (str == null) { 185 return ""; 186 } 187 if (str.contains("?")) { 188 return str.substring(0, str.indexOf('?')); 189 } 190 return str; 191 } 192 193 private String encode(final String value) { 194 try { 195 return URLEncoder.encode(value, "UTF-8").replace("+", "%20").replace("*", "%2A").replace("%7E", "~"); 196 } catch (final UnsupportedEncodingException uee) { 197 throw new IllegalStateException(uee.getMessage(), uee); 198 } 199 } 200 201 private String newNonce() { 202 return UUID.randomUUID().toString().replace("-", ""); 203 } 204}