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