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.
5 #include "net/http/http_auth_handler_digest.h"
9 #include "base/logging.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"
26 // Digest authentication is specified in RFC 2617.
27 // The expanded derivations are listed in the tables below.
29 //==========+==========+==========================================+
30 // qop |algorithm | response |
31 //==========+==========+==========================================+
32 // ? | ?, md5, | MD5(MD5(A1):nonce:MD5(A2)) |
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 //=====================+==========================================+
51 HttpAuthHandlerDigest::NonceGenerator::NonceGenerator() {
54 HttpAuthHandlerDigest::NonceGenerator::~NonceGenerator() {
57 HttpAuthHandlerDigest::DynamicNonceGenerator::DynamicNonceGenerator() {
60 std::string
HttpAuthHandlerDigest::DynamicNonceGenerator::GenerateNonce()
62 // This is how mozilla generates their cnonce -- a 16 digit hex string.
63 static const char domain
[] = "0123456789abcdef";
66 for (int i
= 0; i
< 16; ++i
)
67 cnonce
.push_back(domain
[base::RandInt(0, 15)]);
71 HttpAuthHandlerDigest::FixedNonceGenerator::FixedNonceGenerator(
72 const std::string
& nonce
)
76 std::string
HttpAuthHandlerDigest::FixedNonceGenerator::GenerateNonce() const {
80 HttpAuthHandlerDigest::Factory::Factory()
81 : nonce_generator_(new DynamicNonceGenerator()) {
84 HttpAuthHandlerDigest::Factory::~Factory() {
87 void HttpAuthHandlerDigest::Factory::set_nonce_generator(
88 const NonceGenerator
* nonce_generator
) {
89 nonce_generator_
.reset(nonce_generator
);
92 int HttpAuthHandlerDigest::Factory::CreateAuthHandler(
93 HttpAuthChallengeTokenizer
* challenge
,
94 HttpAuth::Target target
,
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
);
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 (!base::LowerCaseEqualsASCII(challenge
->scheme(), "digest"))
117 return HttpAuth::AUTHORIZATION_RESULT_INVALID
;
119 HttpUtil::NameValuePairsIterator parameters
= challenge
->param_pairs();
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 (base::LowerCaseEqualsASCII(parameters
.name(), "stale")) {
126 if (base::LowerCaseEqualsASCII(parameters
.value(), "true"))
127 return HttpAuth::AUTHORIZATION_RESULT_STALE
;
128 } else if (base::LowerCaseEqualsASCII(parameters
.name(), "realm")) {
129 original_realm
= parameters
.value();
132 return (original_realm_
!= original_realm
) ?
133 HttpAuth::AUTHORIZATION_RESULT_DIFFERENT_REALM
:
134 HttpAuth::AUTHORIZATION_RESULT_REJECT
;
137 bool HttpAuthHandlerDigest::Init(HttpAuthChallengeTokenizer
* challenge
) {
138 return ParseChallenge(challenge
);
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();
147 // Extract the request method and path -- the meaning of 'path' is overloaded
148 // in certain cases, to be a hostname.
151 GetRequestMethodAndPath(request
, &method
, &path
);
153 *auth_token
= AssembleCredentials(method
, path
, *credentials
,
154 cnonce
, nonce_count_
);
158 HttpAuthHandlerDigest::HttpAuthHandlerDigest(
159 int nonce_count
, const NonceGenerator
* nonce_generator
)
161 algorithm_(ALGORITHM_UNSPECIFIED
),
162 qop_(QOP_UNSPECIFIED
),
163 nonce_count_(nonce_count
),
164 nonce_generator_(nonce_generator
) {
165 DCHECK(nonce_generator_
);
168 HttpAuthHandlerDigest::~HttpAuthHandlerDigest() {
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>]
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
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
;
193 properties_
= ENCRYPTS_IDENTITY
;
195 // Initialize to defaults.
197 algorithm_
= ALGORITHM_UNSPECIFIED
;
198 qop_
= QOP_UNSPECIFIED
;
199 realm_
= original_realm_
= nonce_
= domain_
= opaque_
= std::string();
201 // FAIL -- Couldn't match auth-scheme.
202 if (!base::LowerCaseEqualsASCII(challenge
->scheme(), "digest"))
205 HttpUtil::NameValuePairsIterator parameters
= challenge
->param_pairs();
207 // Loop through all the properties.
208 while (parameters
.GetNext()) {
209 // FAIL -- couldn't parse a property.
210 if (!ParseChallengeProperty(parameters
.name(),
215 // Check if tokenizer failed.
216 if (!parameters
.valid())
219 // Check that a minimum set of properties were provided.
226 bool HttpAuthHandlerDigest::ParseChallengeProperty(const std::string
& name
,
227 const std::string
& value
) {
228 if (base::LowerCaseEqualsASCII(name
, "realm")) {
230 if (!ConvertToUtf8AndNormalize(value
, kCharsetLatin1
, &realm
))
233 original_realm_
= value
;
234 } else if (base::LowerCaseEqualsASCII(name
, "nonce")) {
236 } else if (base::LowerCaseEqualsASCII(name
, "domain")) {
238 } else if (base::LowerCaseEqualsASCII(name
, "opaque")) {
240 } else if (base::LowerCaseEqualsASCII(name
, "stale")) {
241 // Parse the stale boolean.
242 stale_
= base::LowerCaseEqualsASCII(value
, "true");
243 } else if (base::LowerCaseEqualsASCII(name
, "algorithm")) {
244 // Parse the algorithm.
245 if (base::LowerCaseEqualsASCII(value
, "md5")) {
246 algorithm_
= ALGORITHM_MD5
;
247 } else if (base::LowerCaseEqualsASCII(value
, "md5-sess")) {
248 algorithm_
= ALGORITHM_MD5_SESS
;
250 DVLOG(1) << "Unknown value of algorithm";
251 return false; // FAIL -- unsupported value of algorithm.
253 } else if (base::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 (base::LowerCaseEqualsASCII(qop_values
.value(), "auth")) {
265 DVLOG(1) << "Skipping unrecognized digest property";
266 // TODO(eroman): perhaps we should fail instead of silently skipping?
272 std::string
HttpAuthHandlerDigest::QopToString(QualityOfProtection qop
) {
274 case QOP_UNSPECIFIED
:
275 return std::string();
280 return std::string();
285 std::string
HttpAuthHandlerDigest::AlgorithmToString(
286 DigestAlgorithm algorithm
) {
288 case ALGORITHM_UNSPECIFIED
:
289 return std::string();
292 case ALGORITHM_MD5_SESS
:
296 return std::string();
300 void HttpAuthHandlerDigest::GetRequestMethodAndPath(
301 const HttpRequestInfo
* request
,
303 std::string
* path
) const {
306 const GURL
& url
= request
->url
;
308 if (target_
== HttpAuth::AUTH_PROXY
&&
309 (url
.SchemeIs("https") || url
.SchemeIsWSOrWSS())) {
311 *path
= GetHostAndPort(url
);
313 *method
= request
->method
;
314 *path
= url
.PathForRequest();
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 {
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
);
333 // TODO(eroman): need to add MD5(req-entity-body) for qop=auth-int.
334 std::string ha2
= base::MD5String(method
+ ":" + path
);
337 if (qop_
!= HttpAuthHandlerDigest::QOP_UNSPECIFIED
) {
338 nc_part
= nc
+ ":" + cnonce
+ ":" + QopToString(qop_
) + ":";
341 return base::MD5String(ha1
+ ":" + nonce_
+ ":" + nc_part
+ ha2
);
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
);
353 // TODO(eroman): is this the right encoding?
354 std::string authorization
= (std::string("Digest username=") +
356 base::UTF16ToUTF8(credentials
.username())));
357 authorization
+= ", realm=" + HttpUtil::Quote(original_realm_
);
358 authorization
+= ", nonce=" + HttpUtil::Quote(nonce_
);
359 authorization
+= ", uri=" + HttpUtil::Quote(path
);
361 if (algorithm_
!= ALGORITHM_UNSPECIFIED
) {
362 authorization
+= ", algorithm=" + AlgorithmToString(algorithm_
);
364 std::string response
= AssembleResponseDigest(method
, path
, credentials
,
366 // No need to call HttpUtil::Quote() as the response digest cannot contain
367 // any characters needing to be escaped.
368 authorization
+= ", response=\"" + response
+ "\"";
370 if (!opaque_
.empty()) {
371 authorization
+= ", opaque=" + HttpUtil::Quote(opaque_
);
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
);
380 return authorization
;