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/i18n/icu_string_conversions.h"
10 #include "base/logging.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"
24 // Digest authentication is specified in RFC 2617.
25 // The expanded derivations are listed in the tables below.
27 //==========+==========+==========================================+
28 // qop |algorithm | response |
29 //==========+==========+==========================================+
30 // ? | ?, md5, | MD5(MD5(A1):nonce:MD5(A2)) |
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 //=====================+==========================================+
49 HttpAuthHandlerDigest::NonceGenerator::NonceGenerator() {
52 HttpAuthHandlerDigest::NonceGenerator::~NonceGenerator() {
55 HttpAuthHandlerDigest::DynamicNonceGenerator::DynamicNonceGenerator() {
58 std::string
HttpAuthHandlerDigest::DynamicNonceGenerator::GenerateNonce()
60 // This is how mozilla generates their cnonce -- a 16 digit hex string.
61 static const char domain
[] = "0123456789abcdef";
64 for (int i
= 0; i
< 16; ++i
)
65 cnonce
.push_back(domain
[base::RandInt(0, 15)]);
69 HttpAuthHandlerDigest::FixedNonceGenerator::FixedNonceGenerator(
70 const std::string
& nonce
)
74 std::string
HttpAuthHandlerDigest::FixedNonceGenerator::GenerateNonce() const {
78 HttpAuthHandlerDigest::Factory::Factory()
79 : nonce_generator_(new DynamicNonceGenerator()) {
82 HttpAuthHandlerDigest::Factory::~Factory() {
85 void HttpAuthHandlerDigest::Factory::set_nonce_generator(
86 const NonceGenerator
* nonce_generator
) {
87 nonce_generator_
.reset(nonce_generator
);
90 int HttpAuthHandlerDigest::Factory::CreateAuthHandler(
91 HttpAuth::ChallengeTokenizer
* challenge
,
92 HttpAuth::Target target
,
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
);
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
;
117 HttpUtil::NameValuePairsIterator parameters
= challenge
->param_pairs();
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();
130 return (original_realm_
!= original_realm
) ?
131 HttpAuth::AUTHORIZATION_RESULT_DIFFERENT_REALM
:
132 HttpAuth::AUTHORIZATION_RESULT_REJECT
;
135 bool HttpAuthHandlerDigest::Init(HttpAuth::ChallengeTokenizer
* challenge
) {
136 return ParseChallenge(challenge
);
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();
145 // Extract the request method and path -- the meaning of 'path' is overloaded
146 // in certain cases, to be a hostname.
149 GetRequestMethodAndPath(request
, &method
, &path
);
151 *auth_token
= AssembleCredentials(method
, path
, *credentials
,
152 cnonce
, nonce_count_
);
156 HttpAuthHandlerDigest::HttpAuthHandlerDigest(
157 int nonce_count
, const NonceGenerator
* nonce_generator
)
159 algorithm_(ALGORITHM_UNSPECIFIED
),
160 qop_(QOP_UNSPECIFIED
),
161 nonce_count_(nonce_count
),
162 nonce_generator_(nonce_generator
) {
163 DCHECK(nonce_generator_
);
166 HttpAuthHandlerDigest::~HttpAuthHandlerDigest() {
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>]
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
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
;
191 properties_
= ENCRYPTS_IDENTITY
;
193 // Initialize to defaults.
195 algorithm_
= ALGORITHM_UNSPECIFIED
;
196 qop_
= QOP_UNSPECIFIED
;
197 realm_
= original_realm_
= nonce_
= domain_
= opaque_
= std::string();
199 // FAIL -- Couldn't match auth-scheme.
200 if (!LowerCaseEqualsASCII(challenge
->scheme(), "digest"))
203 HttpUtil::NameValuePairsIterator parameters
= challenge
->param_pairs();
205 // Loop through all the properties.
206 while (parameters
.GetNext()) {
207 // FAIL -- couldn't parse a property.
208 if (!ParseChallengeProperty(parameters
.name(),
213 // Check if tokenizer failed.
214 if (!parameters
.valid())
217 // Check that a minimum set of properties were provided.
224 bool HttpAuthHandlerDigest::ParseChallengeProperty(const std::string
& name
,
225 const std::string
& value
) {
226 if (LowerCaseEqualsASCII(name
, "realm")) {
228 if (!base::ConvertToUtf8AndNormalize(value
, base::kCodepageLatin1
, &realm
))
231 original_realm_
= value
;
232 } else if (LowerCaseEqualsASCII(name
, "nonce")) {
234 } else if (LowerCaseEqualsASCII(name
, "domain")) {
236 } else if (LowerCaseEqualsASCII(name
, "opaque")) {
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
;
248 DVLOG(1) << "Unknown value of algorithm";
249 return false; // FAIL -- unsupported value of algorithm.
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")) {
263 DVLOG(1) << "Skipping unrecognized digest property";
264 // TODO(eroman): perhaps we should fail instead of silently skipping?
270 std::string
HttpAuthHandlerDigest::QopToString(QualityOfProtection qop
) {
272 case QOP_UNSPECIFIED
:
273 return std::string();
278 return std::string();
283 std::string
HttpAuthHandlerDigest::AlgorithmToString(
284 DigestAlgorithm algorithm
) {
286 case ALGORITHM_UNSPECIFIED
:
287 return std::string();
290 case ALGORITHM_MD5_SESS
:
294 return std::string();
298 void HttpAuthHandlerDigest::GetRequestMethodAndPath(
299 const HttpRequestInfo
* request
,
301 std::string
* path
) const {
304 const GURL
& url
= request
->url
;
306 if (target_
== HttpAuth::AUTH_PROXY
&&
307 (url
.SchemeIs("https") || url
.SchemeIs("ws") || url
.SchemeIs("wss"))) {
309 *path
= GetHostAndPort(url
);
311 *method
= request
->method
;
312 *path
= HttpUtil::PathForRequest(url
);
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 {
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
);
331 // TODO(eroman): need to add MD5(req-entity-body) for qop=auth-int.
332 std::string ha2
= base::MD5String(method
+ ":" + path
);
335 if (qop_
!= HttpAuthHandlerDigest::QOP_UNSPECIFIED
) {
336 nc_part
= nc
+ ":" + cnonce
+ ":" + QopToString(qop_
) + ":";
339 return base::MD5String(ha1
+ ":" + nonce_
+ ":" + nc_part
+ ha2
);
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
);
351 // TODO(eroman): is this the right encoding?
352 std::string authorization
= (std::string("Digest username=") +
354 UTF16ToUTF8(credentials
.username())));
355 authorization
+= ", realm=" + HttpUtil::Quote(original_realm_
);
356 authorization
+= ", nonce=" + HttpUtil::Quote(nonce_
);
357 authorization
+= ", uri=" + HttpUtil::Quote(path
);
359 if (algorithm_
!= ALGORITHM_UNSPECIFIED
) {
360 authorization
+= ", algorithm=" + AlgorithmToString(algorithm_
);
362 std::string response
= AssembleResponseDigest(method
, path
, credentials
,
364 // No need to call HttpUtil::Quote() as the response digest cannot contain
365 // any characters needing to be escaped.
366 authorization
+= ", response=\"" + response
+ "\"";
368 if (!opaque_
.empty()) {
369 authorization
+= ", opaque=" + HttpUtil::Quote(opaque_
);
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
);
378 return authorization
;