You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

HttpAuthMethod.java 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528
  1. /*
  2. * Copyright (C) 2010, 2013, Google Inc. and others
  3. *
  4. * This program and the accompanying materials are made available under the
  5. * terms of the Eclipse Distribution License v. 1.0 which is available at
  6. * https://www.eclipse.org/org/documents/edl-v10.php.
  7. *
  8. * SPDX-License-Identifier: BSD-3-Clause
  9. */
  10. package org.eclipse.jgit.transport;
  11. import static java.nio.charset.StandardCharsets.UTF_8;
  12. import static org.eclipse.jgit.util.HttpSupport.HDR_AUTHORIZATION;
  13. import static org.eclipse.jgit.util.HttpSupport.HDR_WWW_AUTHENTICATE;
  14. import java.io.IOException;
  15. import java.net.URL;
  16. import java.security.MessageDigest;
  17. import java.security.NoSuchAlgorithmException;
  18. import java.security.SecureRandom;
  19. import java.util.Collection;
  20. import java.util.Collections;
  21. import java.util.HashMap;
  22. import java.util.LinkedHashMap;
  23. import java.util.List;
  24. import java.util.Locale;
  25. import java.util.Map;
  26. import java.util.Map.Entry;
  27. import org.eclipse.jgit.transport.http.HttpConnection;
  28. import org.eclipse.jgit.util.Base64;
  29. import org.eclipse.jgit.util.GSSManagerFactory;
  30. import org.ietf.jgss.GSSContext;
  31. import org.ietf.jgss.GSSException;
  32. import org.ietf.jgss.GSSManager;
  33. import org.ietf.jgss.GSSName;
  34. import org.ietf.jgss.Oid;
  35. /**
  36. * Support class to populate user authentication data on a connection.
  37. * <p>
  38. * Instances of an HttpAuthMethod are not thread-safe, as some implementations
  39. * may need to maintain per-connection state information.
  40. */
  41. abstract class HttpAuthMethod {
  42. /**
  43. * Enum listing the http authentication method types supported by jgit. They
  44. * are sorted by priority order!!!
  45. */
  46. public enum Type {
  47. NONE {
  48. @Override
  49. public HttpAuthMethod method(String hdr) {
  50. return None.INSTANCE;
  51. }
  52. @Override
  53. public String getSchemeName() {
  54. return "None"; //$NON-NLS-1$
  55. }
  56. },
  57. BASIC {
  58. @Override
  59. public HttpAuthMethod method(String hdr) {
  60. return new Basic();
  61. }
  62. @Override
  63. public String getSchemeName() {
  64. return "Basic"; //$NON-NLS-1$
  65. }
  66. },
  67. DIGEST {
  68. @Override
  69. public HttpAuthMethod method(String hdr) {
  70. return new Digest(hdr);
  71. }
  72. @Override
  73. public String getSchemeName() {
  74. return "Digest"; //$NON-NLS-1$
  75. }
  76. },
  77. NEGOTIATE {
  78. @Override
  79. public HttpAuthMethod method(String hdr) {
  80. return new Negotiate(hdr);
  81. }
  82. @Override
  83. public String getSchemeName() {
  84. return "Negotiate"; //$NON-NLS-1$
  85. }
  86. };
  87. /**
  88. * Creates a HttpAuthMethod instance configured with the provided HTTP
  89. * WWW-Authenticate header.
  90. *
  91. * @param hdr the http header
  92. * @return a configured HttpAuthMethod instance
  93. */
  94. public abstract HttpAuthMethod method(String hdr);
  95. /**
  96. * @return the name of the authentication scheme in the form to be used
  97. * in HTTP authentication headers as specified in RFC2617 and
  98. * RFC4559
  99. */
  100. public abstract String getSchemeName();
  101. }
  102. static final String EMPTY_STRING = ""; //$NON-NLS-1$
  103. static final String SCHEMA_NAME_SEPARATOR = " "; //$NON-NLS-1$
  104. /**
  105. * Handle an authentication failure and possibly return a new response.
  106. *
  107. * @param conn
  108. * the connection that failed.
  109. * @param ignoreTypes
  110. * authentication types to be ignored.
  111. * @return new authentication method to try.
  112. */
  113. static HttpAuthMethod scanResponse(final HttpConnection conn,
  114. Collection<Type> ignoreTypes) {
  115. final Map<String, List<String>> headers = conn.getHeaderFields();
  116. HttpAuthMethod authentication = Type.NONE.method(EMPTY_STRING);
  117. for (Entry<String, List<String>> entry : headers.entrySet()) {
  118. if (HDR_WWW_AUTHENTICATE.equalsIgnoreCase(entry.getKey())) {
  119. if (entry.getValue() != null) {
  120. for (String value : entry.getValue()) {
  121. if (value != null && value.length() != 0) {
  122. final String[] valuePart = value.split(
  123. SCHEMA_NAME_SEPARATOR, 2);
  124. try {
  125. Type methodType = Type.valueOf(
  126. valuePart[0].toUpperCase(Locale.ROOT));
  127. if ((ignoreTypes != null)
  128. && (ignoreTypes.contains(methodType))) {
  129. continue;
  130. }
  131. if (authentication.getType().compareTo(methodType) >= 0) {
  132. continue;
  133. }
  134. final String param;
  135. if (valuePart.length == 1)
  136. param = EMPTY_STRING;
  137. else
  138. param = valuePart[1];
  139. authentication = methodType
  140. .method(param);
  141. } catch (IllegalArgumentException e) {
  142. // This auth method is not supported
  143. }
  144. }
  145. }
  146. }
  147. break;
  148. }
  149. }
  150. return authentication;
  151. }
  152. protected final Type type;
  153. /**
  154. * Constructor for HttpAuthMethod.
  155. *
  156. * @param type
  157. * authentication method type
  158. */
  159. protected HttpAuthMethod(Type type) {
  160. this.type = type;
  161. }
  162. /**
  163. * Update this method with the credentials from the URIish.
  164. *
  165. * @param uri
  166. * the URI used to create the connection.
  167. * @param credentialsProvider
  168. * the credentials provider, or null. If provided,
  169. * {@link URIish#getPass() credentials in the URI} are ignored.
  170. *
  171. * @return true if the authentication method is able to provide
  172. * authorization for the given URI
  173. */
  174. boolean authorize(URIish uri, CredentialsProvider credentialsProvider) {
  175. String username;
  176. String password;
  177. if (credentialsProvider != null) {
  178. CredentialItem.Username u = new CredentialItem.Username();
  179. CredentialItem.Password p = new CredentialItem.Password();
  180. if (credentialsProvider.supports(u, p)
  181. && credentialsProvider.get(uri, u, p)) {
  182. username = u.getValue();
  183. char[] v = p.getValue();
  184. password = (v == null) ? null : new String(p.getValue());
  185. p.clear();
  186. } else
  187. return false;
  188. } else {
  189. username = uri.getUser();
  190. password = uri.getPass();
  191. }
  192. if (username != null) {
  193. authorize(username, password);
  194. return true;
  195. }
  196. return false;
  197. }
  198. /**
  199. * Update this method with the given username and password pair.
  200. *
  201. * @param user
  202. * @param pass
  203. */
  204. abstract void authorize(String user, String pass);
  205. /**
  206. * Update connection properties based on this authentication method.
  207. *
  208. * @param conn
  209. * @throws IOException
  210. */
  211. abstract void configureRequest(HttpConnection conn) throws IOException;
  212. /**
  213. * Gives the method type associated to this http auth method
  214. *
  215. * @return the method type
  216. */
  217. public Type getType() {
  218. return type;
  219. }
  220. /** Performs no user authentication. */
  221. private static class None extends HttpAuthMethod {
  222. static final None INSTANCE = new None();
  223. public None() {
  224. super(Type.NONE);
  225. }
  226. @Override
  227. void authorize(String user, String pass) {
  228. // Do nothing when no authentication is enabled.
  229. }
  230. @Override
  231. void configureRequest(HttpConnection conn) throws IOException {
  232. // Do nothing when no authentication is enabled.
  233. }
  234. }
  235. /** Performs HTTP basic authentication (plaintext username/password). */
  236. private static class Basic extends HttpAuthMethod {
  237. private String user;
  238. private String pass;
  239. public Basic() {
  240. super(Type.BASIC);
  241. }
  242. @Override
  243. void authorize(String username, String password) {
  244. this.user = username;
  245. this.pass = password;
  246. }
  247. @Override
  248. void configureRequest(HttpConnection conn) throws IOException {
  249. String ident = user + ":" + pass; //$NON-NLS-1$
  250. String enc = Base64.encodeBytes(ident.getBytes(UTF_8));
  251. conn.setRequestProperty(HDR_AUTHORIZATION, type.getSchemeName()
  252. + " " + enc); //$NON-NLS-1$
  253. }
  254. }
  255. /** Performs HTTP digest authentication. */
  256. private static class Digest extends HttpAuthMethod {
  257. private static final SecureRandom PRNG = new SecureRandom();
  258. private final Map<String, String> params;
  259. private int requestCount;
  260. private String user;
  261. private String pass;
  262. Digest(String hdr) {
  263. super(Type.DIGEST);
  264. params = parse(hdr);
  265. final String qop = params.get("qop"); //$NON-NLS-1$
  266. if ("auth".equals(qop)) { //$NON-NLS-1$
  267. final byte[] bin = new byte[8];
  268. PRNG.nextBytes(bin);
  269. params.put("cnonce", Base64.encodeBytes(bin)); //$NON-NLS-1$
  270. }
  271. }
  272. @Override
  273. void authorize(String username, String password) {
  274. this.user = username;
  275. this.pass = password;
  276. }
  277. @SuppressWarnings("boxing")
  278. @Override
  279. void configureRequest(HttpConnection conn) throws IOException {
  280. final Map<String, String> r = new LinkedHashMap<>();
  281. final String realm = params.get("realm"); //$NON-NLS-1$
  282. final String nonce = params.get("nonce"); //$NON-NLS-1$
  283. final String cnonce = params.get("cnonce"); //$NON-NLS-1$
  284. final String uri = uri(conn.getURL());
  285. final String qop = params.get("qop"); //$NON-NLS-1$
  286. final String method = conn.getRequestMethod();
  287. final String A1 = user + ":" + realm + ":" + pass; //$NON-NLS-1$ //$NON-NLS-2$
  288. final String A2 = method + ":" + uri; //$NON-NLS-1$
  289. r.put("username", user); //$NON-NLS-1$
  290. r.put("realm", realm); //$NON-NLS-1$
  291. r.put("nonce", nonce); //$NON-NLS-1$
  292. r.put("uri", uri); //$NON-NLS-1$
  293. final String response, nc;
  294. if ("auth".equals(qop)) { //$NON-NLS-1$
  295. nc = String.format("%08x", ++requestCount); //$NON-NLS-1$
  296. response = KD(H(A1), nonce + ":" + nc + ":" + cnonce + ":" //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
  297. + qop + ":" //$NON-NLS-1$
  298. + H(A2));
  299. } else {
  300. nc = null;
  301. response = KD(H(A1), nonce + ":" + H(A2)); //$NON-NLS-1$
  302. }
  303. r.put("response", response); //$NON-NLS-1$
  304. if (params.containsKey("algorithm")) //$NON-NLS-1$
  305. r.put("algorithm", "MD5"); //$NON-NLS-1$ //$NON-NLS-2$
  306. if (cnonce != null && qop != null)
  307. r.put("cnonce", cnonce); //$NON-NLS-1$
  308. if (params.containsKey("opaque")) //$NON-NLS-1$
  309. r.put("opaque", params.get("opaque")); //$NON-NLS-1$ //$NON-NLS-2$
  310. if (qop != null)
  311. r.put("qop", qop); //$NON-NLS-1$
  312. if (nc != null)
  313. r.put("nc", nc); //$NON-NLS-1$
  314. StringBuilder v = new StringBuilder();
  315. for (Map.Entry<String, String> e : r.entrySet()) {
  316. if (v.length() > 0)
  317. v.append(", "); //$NON-NLS-1$
  318. v.append(e.getKey());
  319. v.append('=');
  320. v.append('"');
  321. v.append(e.getValue());
  322. v.append('"');
  323. }
  324. conn.setRequestProperty(HDR_AUTHORIZATION, type.getSchemeName()
  325. + " " + v); //$NON-NLS-1$
  326. }
  327. private static String uri(URL u) {
  328. StringBuilder r = new StringBuilder();
  329. r.append(u.getProtocol());
  330. r.append("://"); //$NON-NLS-1$
  331. r.append(u.getHost());
  332. if (0 < u.getPort()) {
  333. if (u.getPort() == 80 && "http".equals(u.getProtocol())) { //$NON-NLS-1$
  334. /* nothing */
  335. } else if (u.getPort() == 443
  336. && "https".equals(u.getProtocol())) { //$NON-NLS-1$
  337. /* nothing */
  338. } else {
  339. r.append(':').append(u.getPort());
  340. }
  341. }
  342. r.append(u.getPath());
  343. if (u.getQuery() != null)
  344. r.append('?').append(u.getQuery());
  345. return r.toString();
  346. }
  347. private static String H(String data) {
  348. MessageDigest md = newMD5();
  349. md.update(data.getBytes(UTF_8));
  350. return LHEX(md.digest());
  351. }
  352. private static String KD(String secret, String data) {
  353. MessageDigest md = newMD5();
  354. md.update(secret.getBytes(UTF_8));
  355. md.update((byte) ':');
  356. md.update(data.getBytes(UTF_8));
  357. return LHEX(md.digest());
  358. }
  359. private static MessageDigest newMD5() {
  360. try {
  361. return MessageDigest.getInstance("MD5"); //$NON-NLS-1$
  362. } catch (NoSuchAlgorithmException e) {
  363. throw new RuntimeException("No MD5 available", e); //$NON-NLS-1$
  364. }
  365. }
  366. private static final char[] LHEX = { '0', '1', '2', '3', '4', '5', '6',
  367. '7', '8', '9', //
  368. 'a', 'b', 'c', 'd', 'e', 'f' };
  369. private static String LHEX(byte[] bin) {
  370. StringBuilder r = new StringBuilder(bin.length * 2);
  371. for (byte b : bin) {
  372. r.append(LHEX[(b >>> 4) & 0x0f]);
  373. r.append(LHEX[b & 0x0f]);
  374. }
  375. return r.toString();
  376. }
  377. private static Map<String, String> parse(String auth) {
  378. Map<String, String> p = new HashMap<>();
  379. int next = 0;
  380. while (next < auth.length()) {
  381. if (next < auth.length() && auth.charAt(next) == ',') {
  382. next++;
  383. }
  384. while (next < auth.length()
  385. && Character.isWhitespace(auth.charAt(next))) {
  386. next++;
  387. }
  388. int eq = auth.indexOf('=', next);
  389. if (eq < 0 || eq + 1 == auth.length()) {
  390. return Collections.emptyMap();
  391. }
  392. final String name = auth.substring(next, eq);
  393. final String value;
  394. if (auth.charAt(eq + 1) == '"') {
  395. int dq = auth.indexOf('"', eq + 2);
  396. if (dq < 0) {
  397. return Collections.emptyMap();
  398. }
  399. value = auth.substring(eq + 2, dq);
  400. next = dq + 1;
  401. } else {
  402. int space = auth.indexOf(' ', eq + 1);
  403. int comma = auth.indexOf(',', eq + 1);
  404. if (space < 0)
  405. space = auth.length();
  406. if (comma < 0)
  407. comma = auth.length();
  408. final int e = Math.min(space, comma);
  409. value = auth.substring(eq + 1, e);
  410. next = e + 1;
  411. }
  412. p.put(name, value);
  413. }
  414. return p;
  415. }
  416. }
  417. private static class Negotiate extends HttpAuthMethod {
  418. private static final GSSManagerFactory GSS_MANAGER_FACTORY = GSSManagerFactory
  419. .detect();
  420. private static final Oid OID;
  421. static {
  422. try {
  423. // OID for SPNEGO
  424. OID = new Oid("1.3.6.1.5.5.2"); //$NON-NLS-1$
  425. } catch (GSSException e) {
  426. throw new Error("Cannot create NEGOTIATE oid.", e); //$NON-NLS-1$
  427. }
  428. }
  429. private final byte[] prevToken;
  430. public Negotiate(String hdr) {
  431. super(Type.NEGOTIATE);
  432. prevToken = Base64.decode(hdr);
  433. }
  434. @Override
  435. void authorize(String user, String pass) {
  436. // not used
  437. }
  438. @Override
  439. void configureRequest(HttpConnection conn) throws IOException {
  440. GSSManager gssManager = GSS_MANAGER_FACTORY.newInstance(conn
  441. .getURL());
  442. String host = conn.getURL().getHost();
  443. String peerName = "HTTP@" + host.toLowerCase(Locale.ROOT); //$NON-NLS-1$
  444. try {
  445. GSSName gssName = gssManager.createName(peerName,
  446. GSSName.NT_HOSTBASED_SERVICE);
  447. GSSContext context = gssManager.createContext(gssName, OID,
  448. null, GSSContext.DEFAULT_LIFETIME);
  449. // Respect delegation policy in HTTP/SPNEGO.
  450. context.requestCredDeleg(true);
  451. byte[] token = context.initSecContext(prevToken, 0,
  452. prevToken.length);
  453. conn.setRequestProperty(HDR_AUTHORIZATION, getType().getSchemeName()
  454. + " " + Base64.encodeBytes(token)); //$NON-NLS-1$
  455. } catch (GSSException e) {
  456. throw new IOException(e);
  457. }
  458. }
  459. }
  460. }