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"
25 // Digest authentication is specified in RFC 2617.
26 // The expanded derivations are listed in the tables below.
28 //==========+==========+==========================================+
29 // qop |algorithm | response |
30 //==========+==========+==========================================+
31 // ? | ?, md5, | MD5(MD5(A1):nonce:MD5(A2)) |
33 //--------- +----------+------------------------------------------+
34 // auth, | ?, md5, | MD5(MD5(A1):nonce:nc:cnonce:qop:MD5(A2)) |
35 // auth-int | md5-sess | |
36 //==========+==========+==========================================+
37 // qop |algorithm | A1 |
38 //==========+==========+==========================================+
39 // | ?, md5 | user:realm:password |
40 //----------+----------+------------------------------------------+
41 // | md5-sess | MD5(user:realm:password):nonce:cnonce |
42 //==========+==========+==========================================+
43 // qop |algorithm | A2 |
44 //==========+==========+==========================================+
45 // ?, auth | | req-method:req-uri |
46 //----------+----------+------------------------------------------+
47 // auth-int | | req-method:req-uri:MD5(req-entity-body) |
48 //=====================+==========================================+
50 HttpAuthHandlerDigest::NonceGenerator::NonceGenerator() {
53 HttpAuthHandlerDigest::NonceGenerator::~NonceGenerator() {
56 HttpAuthHandlerDigest::DynamicNonceGenerator::DynamicNonceGenerator() {
59 std::string
HttpAuthHandlerDigest::DynamicNonceGenerator::GenerateNonce()
61 // This is how mozilla generates their cnonce -- a 16 digit hex string.
62 static const char domain
[] = "0123456789abcdef";
65 for (int i
= 0; i
< 16; ++i
)
66 cnonce
.push_back(domain
[base::RandInt(0, 15)]);
70 HttpAuthHandlerDigest::FixedNonceGenerator::FixedNonceGenerator(
71 const std::string
& nonce
)
75 std::string
HttpAuthHandlerDigest::FixedNonceGenerator::GenerateNonce() const {
79 HttpAuthHandlerDigest::Factory::Factory()
80 : nonce_generator_(new DynamicNonceGenerator()) {
83 HttpAuthHandlerDigest::Factory::~Factory() {
86 void HttpAuthHandlerDigest::Factory::set_nonce_generator(
87 const NonceGenerator
* nonce_generator
) {
88 nonce_generator_
.reset(nonce_generator
);
91 int HttpAuthHandlerDigest::Factory::CreateAuthHandler(
92 HttpAuth::ChallengeTokenizer
* challenge
,
93 HttpAuth::Target target
,
96 int digest_nonce_count
,
97 const BoundNetLog
& net_log
,
98 scoped_ptr
<HttpAuthHandler
>* handler
) {
99 // TODO(cbentzel): Move towards model of parsing in the factory
100 // method and only constructing when valid.
101 scoped_ptr
<HttpAuthHandler
> tmp_handler(
102 new HttpAuthHandlerDigest(digest_nonce_count
, nonce_generator_
.get()));
103 if (!tmp_handler
->InitFromChallenge(challenge
, target
, origin
, net_log
))
104 return ERR_INVALID_RESPONSE
;
105 handler
->swap(tmp_handler
);
109 HttpAuth::AuthorizationResult
HttpAuthHandlerDigest::HandleAnotherChallenge(
110 HttpAuth::ChallengeTokenizer
* challenge
) {
111 // Even though Digest is not connection based, a "second round" is parsed
112 // to differentiate between stale and rejected responses.
113 // Note that the state of the current handler is not mutated - this way if
114 // there is a rejection the realm hasn't changed.
115 if (!LowerCaseEqualsASCII(challenge
->scheme(), "digest"))
116 return HttpAuth::AUTHORIZATION_RESULT_INVALID
;
118 HttpUtil::NameValuePairsIterator parameters
= challenge
->param_pairs();
120 // Try to find the "stale" value, and also keep track of the realm
121 // for the new challenge.
122 std::string original_realm
;
123 while (parameters
.GetNext()) {
124 if (LowerCaseEqualsASCII(parameters
.name(), "stale")) {
125 if (LowerCaseEqualsASCII(parameters
.value(), "true"))
126 return HttpAuth::AUTHORIZATION_RESULT_STALE
;
127 } else if (LowerCaseEqualsASCII(parameters
.name(), "realm")) {
128 original_realm
= parameters
.value();
131 return (original_realm_
!= original_realm
) ?
132 HttpAuth::AUTHORIZATION_RESULT_DIFFERENT_REALM
:
133 HttpAuth::AUTHORIZATION_RESULT_REJECT
;
136 bool HttpAuthHandlerDigest::Init(HttpAuth::ChallengeTokenizer
* challenge
) {
137 return ParseChallenge(challenge
);
140 int HttpAuthHandlerDigest::GenerateAuthTokenImpl(
141 const AuthCredentials
* credentials
, const HttpRequestInfo
* request
,
142 const CompletionCallback
& callback
, std::string
* auth_token
) {
143 // Generate a random client nonce.
144 std::string cnonce
= nonce_generator_
->GenerateNonce();
146 // Extract the request method and path -- the meaning of 'path' is overloaded
147 // in certain cases, to be a hostname.
150 GetRequestMethodAndPath(request
, &method
, &path
);
152 *auth_token
= AssembleCredentials(method
, path
, *credentials
,
153 cnonce
, nonce_count_
);
157 HttpAuthHandlerDigest::HttpAuthHandlerDigest(
158 int nonce_count
, const NonceGenerator
* nonce_generator
)
160 algorithm_(ALGORITHM_UNSPECIFIED
),
161 qop_(QOP_UNSPECIFIED
),
162 nonce_count_(nonce_count
),
163 nonce_generator_(nonce_generator
) {
164 DCHECK(nonce_generator_
);
167 HttpAuthHandlerDigest::~HttpAuthHandlerDigest() {
170 // The digest challenge header looks like:
171 // WWW-Authenticate: Digest
172 // [realm="<realm-value>"]
173 // nonce="<nonce-value>"
174 // [domain="<list-of-URIs>"]
175 // [opaque="<opaque-token-value>"]
176 // [stale="<true-or-false>"]
177 // [algorithm="<digest-algorithm>"]
178 // [qop="<list-of-qop-values>"]
179 // [<extension-directive>]
181 // Note that according to RFC 2617 (section 1.2) the realm is required.
182 // However we allow it to be omitted, in which case it will default to the
185 // This allowance is for better compatibility with webservers that fail to
186 // send the realm (See http://crbug.com/20984 for an instance where a
187 // webserver was not sending the realm with a BASIC challenge).
188 bool HttpAuthHandlerDigest::ParseChallenge(
189 HttpAuth::ChallengeTokenizer
* challenge
) {
190 auth_scheme_
= HttpAuth::AUTH_SCHEME_DIGEST
;
192 properties_
= ENCRYPTS_IDENTITY
;
194 // Initialize to defaults.
196 algorithm_
= ALGORITHM_UNSPECIFIED
;
197 qop_
= QOP_UNSPECIFIED
;
198 realm_
= original_realm_
= nonce_
= domain_
= opaque_
= std::string();
200 // FAIL -- Couldn't match auth-scheme.
201 if (!LowerCaseEqualsASCII(challenge
->scheme(), "digest"))
204 HttpUtil::NameValuePairsIterator parameters
= challenge
->param_pairs();
206 // Loop through all the properties.
207 while (parameters
.GetNext()) {
208 // FAIL -- couldn't parse a property.
209 if (!ParseChallengeProperty(parameters
.name(),
214 // Check if tokenizer failed.
215 if (!parameters
.valid())
218 // Check that a minimum set of properties were provided.
225 bool HttpAuthHandlerDigest::ParseChallengeProperty(const std::string
& name
,
226 const std::string
& value
) {
227 if (LowerCaseEqualsASCII(name
, "realm")) {
229 if (!base::ConvertToUtf8AndNormalize(value
, base::kCodepageLatin1
, &realm
))
232 original_realm_
= value
;
233 } else if (LowerCaseEqualsASCII(name
, "nonce")) {
235 } else if (LowerCaseEqualsASCII(name
, "domain")) {
237 } else if (LowerCaseEqualsASCII(name
, "opaque")) {
239 } else if (LowerCaseEqualsASCII(name
, "stale")) {
240 // Parse the stale boolean.
241 stale_
= LowerCaseEqualsASCII(value
, "true");
242 } else if (LowerCaseEqualsASCII(name
, "algorithm")) {
243 // Parse the algorithm.
244 if (LowerCaseEqualsASCII(value
, "md5")) {
245 algorithm_
= ALGORITHM_MD5
;
246 } else if (LowerCaseEqualsASCII(value
, "md5-sess")) {
247 algorithm_
= ALGORITHM_MD5_SESS
;
249 DVLOG(1) << "Unknown value of algorithm";
250 return false; // FAIL -- unsupported value of algorithm.
252 } else if (LowerCaseEqualsASCII(name
, "qop")) {
253 // Parse the comma separated list of qops.
254 // auth is the only supported qop, and all other values are ignored.
255 HttpUtil::ValuesIterator
qop_values(value
.begin(), value
.end(), ',');
256 qop_
= QOP_UNSPECIFIED
;
257 while (qop_values
.GetNext()) {
258 if (LowerCaseEqualsASCII(qop_values
.value(), "auth")) {
264 DVLOG(1) << "Skipping unrecognized digest property";
265 // TODO(eroman): perhaps we should fail instead of silently skipping?
271 std::string
HttpAuthHandlerDigest::QopToString(QualityOfProtection qop
) {
273 case QOP_UNSPECIFIED
:
274 return std::string();
279 return std::string();
284 std::string
HttpAuthHandlerDigest::AlgorithmToString(
285 DigestAlgorithm algorithm
) {
287 case ALGORITHM_UNSPECIFIED
:
288 return std::string();
291 case ALGORITHM_MD5_SESS
:
295 return std::string();
299 void HttpAuthHandlerDigest::GetRequestMethodAndPath(
300 const HttpRequestInfo
* request
,
302 std::string
* path
) const {
305 const GURL
& url
= request
->url
;
307 if (target_
== HttpAuth::AUTH_PROXY
&&
308 (url
.SchemeIs("https") || url
.SchemeIsWSOrWSS())) {
310 *path
= GetHostAndPort(url
);
312 *method
= request
->method
;
313 *path
= HttpUtil::PathForRequest(url
);
317 std::string
HttpAuthHandlerDigest::AssembleResponseDigest(
318 const std::string
& method
,
319 const std::string
& path
,
320 const AuthCredentials
& credentials
,
321 const std::string
& cnonce
,
322 const std::string
& nc
) const {
324 // TODO(eroman): is this the right encoding?
325 std::string ha1
= base::MD5String(base::UTF16ToUTF8(credentials
.username()) +
326 ":" + original_realm_
+ ":" +
327 base::UTF16ToUTF8(credentials
.password()));
328 if (algorithm_
== HttpAuthHandlerDigest::ALGORITHM_MD5_SESS
)
329 ha1
= base::MD5String(ha1
+ ":" + nonce_
+ ":" + cnonce
);
332 // TODO(eroman): need to add MD5(req-entity-body) for qop=auth-int.
333 std::string ha2
= base::MD5String(method
+ ":" + path
);
336 if (qop_
!= HttpAuthHandlerDigest::QOP_UNSPECIFIED
) {
337 nc_part
= nc
+ ":" + cnonce
+ ":" + QopToString(qop_
) + ":";
340 return base::MD5String(ha1
+ ":" + nonce_
+ ":" + nc_part
+ ha2
);
343 std::string
HttpAuthHandlerDigest::AssembleCredentials(
344 const std::string
& method
,
345 const std::string
& path
,
346 const AuthCredentials
& credentials
,
347 const std::string
& cnonce
,
348 int nonce_count
) const {
349 // the nonce-count is an 8 digit hex string.
350 std::string nc
= base::StringPrintf("%08x", nonce_count
);
352 // TODO(eroman): is this the right encoding?
353 std::string authorization
= (std::string("Digest username=") +
355 base::UTF16ToUTF8(credentials
.username())));
356 authorization
+= ", realm=" + HttpUtil::Quote(original_realm_
);
357 authorization
+= ", nonce=" + HttpUtil::Quote(nonce_
);
358 authorization
+= ", uri=" + HttpUtil::Quote(path
);
360 if (algorithm_
!= ALGORITHM_UNSPECIFIED
) {
361 authorization
+= ", algorithm=" + AlgorithmToString(algorithm_
);
363 std::string response
= AssembleResponseDigest(method
, path
, credentials
,
365 // No need to call HttpUtil::Quote() as the response digest cannot contain
366 // any characters needing to be escaped.
367 authorization
+= ", response=\"" + response
+ "\"";
369 if (!opaque_
.empty()) {
370 authorization
+= ", opaque=" + HttpUtil::Quote(opaque_
);
372 if (qop_
!= QOP_UNSPECIFIED
) {
373 // TODO(eroman): Supposedly IIS server requires quotes surrounding qop.
374 authorization
+= ", qop=" + QopToString(qop_
);
375 authorization
+= ", nc=" + nc
;
376 authorization
+= ", cnonce=" + HttpUtil::Quote(cnonce
);
379 return authorization
;