HttpAuthMethod.java

  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. }