Home | History | Annotate | Download | only in http
      1 // Copyright (c) 2011 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 #include "net/http/http_auth_handler_digest.h"
      6 
      7 #include <string>
      8 
      9 #include "base/logging.h"
     10 #include "base/md5.h"
     11 #include "base/rand_util.h"
     12 #include "base/strings/string_util.h"
     13 #include "base/strings/stringprintf.h"
     14 #include "base/strings/utf_string_conversions.h"
     15 #include "net/base/net_errors.h"
     16 #include "net/base/net_string_util.h"
     17 #include "net/base/net_util.h"
     18 #include "net/http/http_auth.h"
     19 #include "net/http/http_auth_challenge_tokenizer.h"
     20 #include "net/http/http_request_info.h"
     21 #include "net/http/http_util.h"
     22 #include "url/gurl.h"
     23 
     24 namespace net {
     25 
     26 // Digest authentication is specified in RFC 2617.
     27 // The expanded derivations are listed in the tables below.
     28 
     29 //==========+==========+==========================================+
     30 //    qop   |algorithm |               response                   |
     31 //==========+==========+==========================================+
     32 //    ?     |  ?, md5, | MD5(MD5(A1):nonce:MD5(A2))               |
     33 //          | md5-sess |                                          |
     34 //--------- +----------+------------------------------------------+
     35 //   auth,  |  ?, md5, | MD5(MD5(A1):nonce:nc:cnonce:qop:MD5(A2)) |
     36 // auth-int | md5-sess |                                          |
     37 //==========+==========+==========================================+
     38 //    qop   |algorithm |                  A1                      |
     39 //==========+==========+==========================================+
     40 //          | ?, md5   | user:realm:password                      |
     41 //----------+----------+------------------------------------------+
     42 //          | md5-sess | MD5(user:realm:password):nonce:cnonce    |
     43 //==========+==========+==========================================+
     44 //    qop   |algorithm |                  A2                      |
     45 //==========+==========+==========================================+
     46 //  ?, auth |          | req-method:req-uri                       |
     47 //----------+----------+------------------------------------------+
     48 // auth-int |          | req-method:req-uri:MD5(req-entity-body)  |
     49 //=====================+==========================================+
     50 
     51 HttpAuthHandlerDigest::NonceGenerator::NonceGenerator() {
     52 }
     53 
     54 HttpAuthHandlerDigest::NonceGenerator::~NonceGenerator() {
     55 }
     56 
     57 HttpAuthHandlerDigest::DynamicNonceGenerator::DynamicNonceGenerator() {
     58 }
     59 
     60 std::string HttpAuthHandlerDigest::DynamicNonceGenerator::GenerateNonce()
     61     const {
     62   // This is how mozilla generates their cnonce -- a 16 digit hex string.
     63   static const char domain[] = "0123456789abcdef";
     64   std::string cnonce;
     65   cnonce.reserve(16);
     66   for (int i = 0; i < 16; ++i)
     67     cnonce.push_back(domain[base::RandInt(0, 15)]);
     68   return cnonce;
     69 }
     70 
     71 HttpAuthHandlerDigest::FixedNonceGenerator::FixedNonceGenerator(
     72     const std::string& nonce)
     73     : nonce_(nonce) {
     74 }
     75 
     76 std::string HttpAuthHandlerDigest::FixedNonceGenerator::GenerateNonce() const {
     77   return nonce_;
     78 }
     79 
     80 HttpAuthHandlerDigest::Factory::Factory()
     81     : nonce_generator_(new DynamicNonceGenerator()) {
     82 }
     83 
     84 HttpAuthHandlerDigest::Factory::~Factory() {
     85 }
     86 
     87 void HttpAuthHandlerDigest::Factory::set_nonce_generator(
     88     const NonceGenerator* nonce_generator) {
     89   nonce_generator_.reset(nonce_generator);
     90 }
     91 
     92 int HttpAuthHandlerDigest::Factory::CreateAuthHandler(
     93     HttpAuthChallengeTokenizer* challenge,
     94     HttpAuth::Target target,
     95     const GURL& origin,
     96     CreateReason reason,
     97     int digest_nonce_count,
     98     const BoundNetLog& net_log,
     99     scoped_ptr<HttpAuthHandler>* handler) {
    100   // TODO(cbentzel): Move towards model of parsing in the factory
    101   //                 method and only constructing when valid.
    102   scoped_ptr<HttpAuthHandler> tmp_handler(
    103       new HttpAuthHandlerDigest(digest_nonce_count, nonce_generator_.get()));
    104   if (!tmp_handler->InitFromChallenge(challenge, target, origin, net_log))
    105     return ERR_INVALID_RESPONSE;
    106   handler->swap(tmp_handler);
    107   return OK;
    108 }
    109 
    110 HttpAuth::AuthorizationResult HttpAuthHandlerDigest::HandleAnotherChallenge(
    111     HttpAuthChallengeTokenizer* challenge) {
    112   // Even though Digest is not connection based, a "second round" is parsed
    113   // to differentiate between stale and rejected responses.
    114   // Note that the state of the current handler is not mutated - this way if
    115   // there is a rejection the realm hasn't changed.
    116   if (!LowerCaseEqualsASCII(challenge->scheme(), "digest"))
    117     return HttpAuth::AUTHORIZATION_RESULT_INVALID;
    118 
    119   HttpUtil::NameValuePairsIterator parameters = challenge->param_pairs();
    120 
    121   // Try to find the "stale" value, and also keep track of the realm
    122   // for the new challenge.
    123   std::string original_realm;
    124   while (parameters.GetNext()) {
    125     if (LowerCaseEqualsASCII(parameters.name(), "stale")) {
    126       if (LowerCaseEqualsASCII(parameters.value(), "true"))
    127         return HttpAuth::AUTHORIZATION_RESULT_STALE;
    128     } else if (LowerCaseEqualsASCII(parameters.name(), "realm")) {
    129       original_realm = parameters.value();
    130     }
    131   }
    132   return (original_realm_ != original_realm) ?
    133       HttpAuth::AUTHORIZATION_RESULT_DIFFERENT_REALM :
    134       HttpAuth::AUTHORIZATION_RESULT_REJECT;
    135 }
    136 
    137 bool HttpAuthHandlerDigest::Init(HttpAuthChallengeTokenizer* challenge) {
    138   return ParseChallenge(challenge);
    139 }
    140 
    141 int HttpAuthHandlerDigest::GenerateAuthTokenImpl(
    142     const AuthCredentials* credentials, const HttpRequestInfo* request,
    143     const CompletionCallback& callback, std::string* auth_token) {
    144   // Generate a random client nonce.
    145   std::string cnonce = nonce_generator_->GenerateNonce();
    146 
    147   // Extract the request method and path -- the meaning of 'path' is overloaded
    148   // in certain cases, to be a hostname.
    149   std::string method;
    150   std::string path;
    151   GetRequestMethodAndPath(request, &method, &path);
    152 
    153   *auth_token = AssembleCredentials(method, path, *credentials,
    154                                     cnonce, nonce_count_);
    155   return OK;
    156 }
    157 
    158 HttpAuthHandlerDigest::HttpAuthHandlerDigest(
    159     int nonce_count, const NonceGenerator* nonce_generator)
    160     : stale_(false),
    161       algorithm_(ALGORITHM_UNSPECIFIED),
    162       qop_(QOP_UNSPECIFIED),
    163       nonce_count_(nonce_count),
    164       nonce_generator_(nonce_generator) {
    165   DCHECK(nonce_generator_);
    166 }
    167 
    168 HttpAuthHandlerDigest::~HttpAuthHandlerDigest() {
    169 }
    170 
    171 // The digest challenge header looks like:
    172 //   WWW-Authenticate: Digest
    173 //     [realm="<realm-value>"]
    174 //     nonce="<nonce-value>"
    175 //     [domain="<list-of-URIs>"]
    176 //     [opaque="<opaque-token-value>"]
    177 //     [stale="<true-or-false>"]
    178 //     [algorithm="<digest-algorithm>"]
    179 //     [qop="<list-of-qop-values>"]
    180 //     [<extension-directive>]
    181 //
    182 // Note that according to RFC 2617 (section 1.2) the realm is required.
    183 // However we allow it to be omitted, in which case it will default to the
    184 // empty string.
    185 //
    186 // This allowance is for better compatibility with webservers that fail to
    187 // send the realm (See http://crbug.com/20984 for an instance where a
    188 // webserver was not sending the realm with a BASIC challenge).
    189 bool HttpAuthHandlerDigest::ParseChallenge(
    190     HttpAuthChallengeTokenizer* challenge) {
    191   auth_scheme_ = HttpAuth::AUTH_SCHEME_DIGEST;
    192   score_ = 2;
    193   properties_ = ENCRYPTS_IDENTITY;
    194 
    195   // Initialize to defaults.
    196   stale_ = false;
    197   algorithm_ = ALGORITHM_UNSPECIFIED;
    198   qop_ = QOP_UNSPECIFIED;
    199   realm_ = original_realm_ = nonce_ = domain_ = opaque_ = std::string();
    200 
    201   // FAIL -- Couldn't match auth-scheme.
    202   if (!LowerCaseEqualsASCII(challenge->scheme(), "digest"))
    203     return false;
    204 
    205   HttpUtil::NameValuePairsIterator parameters = challenge->param_pairs();
    206 
    207   // Loop through all the properties.
    208   while (parameters.GetNext()) {
    209     // FAIL -- couldn't parse a property.
    210     if (!ParseChallengeProperty(parameters.name(),
    211                                 parameters.value()))
    212       return false;
    213   }
    214 
    215   // Check if tokenizer failed.
    216   if (!parameters.valid())
    217     return false;
    218 
    219   // Check that a minimum set of properties were provided.
    220   if (nonce_.empty())
    221     return false;
    222 
    223   return true;
    224 }
    225 
    226 bool HttpAuthHandlerDigest::ParseChallengeProperty(const std::string& name,
    227                                                    const std::string& value) {
    228   if (LowerCaseEqualsASCII(name, "realm")) {
    229     std::string realm;
    230     if (!net::ConvertToUtf8AndNormalize(value, kCharsetLatin1, &realm))
    231       return false;
    232     realm_ = realm;
    233     original_realm_ = value;
    234   } else if (LowerCaseEqualsASCII(name, "nonce")) {
    235     nonce_ = value;
    236   } else if (LowerCaseEqualsASCII(name, "domain")) {
    237     domain_ = value;
    238   } else if (LowerCaseEqualsASCII(name, "opaque")) {
    239     opaque_ = value;
    240   } else if (LowerCaseEqualsASCII(name, "stale")) {
    241     // Parse the stale boolean.
    242     stale_ = LowerCaseEqualsASCII(value, "true");
    243   } else if (LowerCaseEqualsASCII(name, "algorithm")) {
    244     // Parse the algorithm.
    245     if (LowerCaseEqualsASCII(value, "md5")) {
    246       algorithm_ = ALGORITHM_MD5;
    247     } else if (LowerCaseEqualsASCII(value, "md5-sess")) {
    248       algorithm_ = ALGORITHM_MD5_SESS;
    249     } else {
    250       DVLOG(1) << "Unknown value of algorithm";
    251       return false;  // FAIL -- unsupported value of algorithm.
    252     }
    253   } else if (LowerCaseEqualsASCII(name, "qop")) {
    254     // Parse the comma separated list of qops.
    255     // auth is the only supported qop, and all other values are ignored.
    256     HttpUtil::ValuesIterator qop_values(value.begin(), value.end(), ',');
    257     qop_ = QOP_UNSPECIFIED;
    258     while (qop_values.GetNext()) {
    259       if (LowerCaseEqualsASCII(qop_values.value(), "auth")) {
    260         qop_ = QOP_AUTH;
    261         break;
    262       }
    263     }
    264   } else {
    265     DVLOG(1) << "Skipping unrecognized digest property";
    266     // TODO(eroman): perhaps we should fail instead of silently skipping?
    267   }
    268   return true;
    269 }
    270 
    271 // static
    272 std::string HttpAuthHandlerDigest::QopToString(QualityOfProtection qop) {
    273   switch (qop) {
    274     case QOP_UNSPECIFIED:
    275       return std::string();
    276     case QOP_AUTH:
    277       return "auth";
    278     default:
    279       NOTREACHED();
    280       return std::string();
    281   }
    282 }
    283 
    284 // static
    285 std::string HttpAuthHandlerDigest::AlgorithmToString(
    286     DigestAlgorithm algorithm) {
    287   switch (algorithm) {
    288     case ALGORITHM_UNSPECIFIED:
    289       return std::string();
    290     case ALGORITHM_MD5:
    291       return "MD5";
    292     case ALGORITHM_MD5_SESS:
    293       return "MD5-sess";
    294     default:
    295       NOTREACHED();
    296       return std::string();
    297   }
    298 }
    299 
    300 void HttpAuthHandlerDigest::GetRequestMethodAndPath(
    301     const HttpRequestInfo* request,
    302     std::string* method,
    303     std::string* path) const {
    304   DCHECK(request);
    305 
    306   const GURL& url = request->url;
    307 
    308   if (target_ == HttpAuth::AUTH_PROXY &&
    309       (url.SchemeIs("https") || url.SchemeIsWSOrWSS())) {
    310     *method = "CONNECT";
    311     *path = GetHostAndPort(url);
    312   } else {
    313     *method = request->method;
    314     *path = HttpUtil::PathForRequest(url);
    315   }
    316 }
    317 
    318 std::string HttpAuthHandlerDigest::AssembleResponseDigest(
    319     const std::string& method,
    320     const std::string& path,
    321     const AuthCredentials& credentials,
    322     const std::string& cnonce,
    323     const std::string& nc) const {
    324   // ha1 = MD5(A1)
    325   // TODO(eroman): is this the right encoding?
    326   std::string ha1 = base::MD5String(base::UTF16ToUTF8(credentials.username()) +
    327                                     ":" + original_realm_ + ":" +
    328                                     base::UTF16ToUTF8(credentials.password()));
    329   if (algorithm_ == HttpAuthHandlerDigest::ALGORITHM_MD5_SESS)
    330     ha1 = base::MD5String(ha1 + ":" + nonce_ + ":" + cnonce);
    331 
    332   // ha2 = MD5(A2)
    333   // TODO(eroman): need to add MD5(req-entity-body) for qop=auth-int.
    334   std::string ha2 = base::MD5String(method + ":" + path);
    335 
    336   std::string nc_part;
    337   if (qop_ != HttpAuthHandlerDigest::QOP_UNSPECIFIED) {
    338     nc_part = nc + ":" + cnonce + ":" + QopToString(qop_) + ":";
    339   }
    340 
    341   return base::MD5String(ha1 + ":" + nonce_ + ":" + nc_part + ha2);
    342 }
    343 
    344 std::string HttpAuthHandlerDigest::AssembleCredentials(
    345     const std::string& method,
    346     const std::string& path,
    347     const AuthCredentials& credentials,
    348     const std::string& cnonce,
    349     int nonce_count) const {
    350   // the nonce-count is an 8 digit hex string.
    351   std::string nc = base::StringPrintf("%08x", nonce_count);
    352 
    353   // TODO(eroman): is this the right encoding?
    354   std::string authorization = (std::string("Digest username=") +
    355                                HttpUtil::Quote(
    356                                    base::UTF16ToUTF8(credentials.username())));
    357   authorization += ", realm=" + HttpUtil::Quote(original_realm_);
    358   authorization += ", nonce=" + HttpUtil::Quote(nonce_);
    359   authorization += ", uri=" + HttpUtil::Quote(path);
    360 
    361   if (algorithm_ != ALGORITHM_UNSPECIFIED) {
    362     authorization += ", algorithm=" + AlgorithmToString(algorithm_);
    363   }
    364   std::string response = AssembleResponseDigest(method, path, credentials,
    365                                                 cnonce, nc);
    366   // No need to call HttpUtil::Quote() as the response digest cannot contain
    367   // any characters needing to be escaped.
    368   authorization += ", response=\"" + response + "\"";
    369 
    370   if (!opaque_.empty()) {
    371     authorization += ", opaque=" + HttpUtil::Quote(opaque_);
    372   }
    373   if (qop_ != QOP_UNSPECIFIED) {
    374     // TODO(eroman): Supposedly IIS server requires quotes surrounding qop.
    375     authorization += ", qop=" + QopToString(qop_);
    376     authorization += ", nc=" + nc;
    377     authorization += ", cnonce=" + HttpUtil::Quote(cnonce);
    378   }
    379 
    380   return authorization;
    381 }
    382 
    383 }  // namespace net
    384