1 /** 2 Implements HTTP Digest Authentication. 3 4 This is a minimal implementation based on RFC 2069. 5 6 Copyright: © 2015 Sönke Ludwig 7 License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. 8 Authors: Kai Nacke 9 */ 10 module vibe.http.auth.digest_auth; 11 12 import vibe.core.log; 13 import vibe.crypto.cryptorand; 14 import vibe.http.server; 15 import vibe.inet.url; 16 17 import std.base64; 18 import std.datetime; 19 import std.digest.md; 20 import std.exception; 21 import std..string; 22 23 @safe: 24 25 enum NonceState { Valid, Expired, Invalid } 26 27 class DigestAuthInfo 28 { 29 @safe: 30 31 string realm; 32 ubyte[32] secret; 33 ulong timeout; 34 35 this() 36 { 37 secureRNG.read(secret[]); 38 timeout = 300; 39 } 40 41 string createNonce(in HTTPServerRequest req) 42 { 43 auto now = Clock.currTime(UTC()).stdTime(); 44 auto time = () @trusted { return *cast(ubyte[now.sizeof]*)&now; } (); 45 MD5 md5; 46 md5.put(time); 47 md5.put(secret); 48 auto data = md5.finish(); 49 return Base64.encode(time ~ data); 50 } 51 52 NonceState checkNonce(in string nonce, in HTTPServerRequest req) 53 { 54 auto now = Clock.currTime(UTC()).stdTime(); 55 ubyte[] decoded = Base64.decode(nonce); 56 if (decoded.length != now.sizeof + secret.length) return NonceState.Invalid; 57 auto timebytes = decoded[0 .. now.sizeof]; 58 auto time = () @trusted { return (cast(typeof(now)[])timebytes)[0]; } (); 59 if (timeout + time > now) return NonceState.Expired; 60 MD5 md5; 61 md5.put(timebytes); 62 md5.put(secret); 63 auto data = md5.finish(); 64 if (data[] != decoded[now.sizeof .. $]) return NonceState.Invalid; 65 return NonceState.Valid; 66 } 67 } 68 69 private bool checkDigest(scope HTTPServerRequest req, DigestAuthInfo info, scope DigestHashCallback pwhash, out bool stale, out string username) 70 { 71 stale = false; 72 username = ""; 73 auto pauth = "Authorization" in req.headers; 74 75 if (pauth && (*pauth).startsWith("Digest ")) { 76 string realm, nonce, response, uri, algorithm; 77 foreach (param; split((*pauth)[7 .. $], ",")) { 78 auto kv = split(param, "="); 79 switch (kv[0].strip().toLower()) { 80 default: break; 81 case "realm": realm = param.stripLeft()[7..$-1]; break; 82 case "username": username = param.stripLeft()[10..$-1]; break; 83 case "nonce": nonce = kv[1][1..$-1]; break; 84 case "uri": uri = param.stripLeft()[5..$-1]; break; 85 case "response": response = kv[1][1..$-1]; break; 86 case "algorithm": algorithm = kv[1][1..$-1]; break; 87 } 88 } 89 90 if (realm != info.realm) 91 return false; 92 if (algorithm !is null && algorithm != "MD5") 93 return false; 94 95 auto nonceState = info.checkNonce(nonce, req); 96 if (nonceState != NonceState.Valid) { 97 stale = nonceState == NonceState.Expired; 98 return false; 99 } 100 101 auto ha1 = pwhash(realm, username); 102 auto ha2 = toHexString!(LetterCase.lower)(md5Of(httpMethodString(req.method) ~ ":" ~ uri)); 103 auto calcresponse = toHexString!(LetterCase.lower)(md5Of(ha1 ~ ":" ~ nonce ~ ":" ~ ha2 )); 104 if (response[] == calcresponse[]) 105 return true; 106 } 107 return false; 108 } 109 110 /** 111 Returns a request handler that enforces request to be authenticated using HTTP Digest Auth. 112 */ 113 HTTPServerRequestDelegate performDigestAuth(DigestAuthInfo info, scope DigestHashCallback pwhash) 114 { 115 void handleRequest(HTTPServerRequest req, HTTPServerResponse res) 116 @safe { 117 bool stale; 118 string username; 119 if (checkDigest(req, info, pwhash, stale, username)) { 120 req.username = username; 121 return ; 122 } 123 124 // else output an error page 125 res.statusCode = HTTPStatus.unauthorized; 126 res.contentType = "text/plain"; 127 res.headers["WWW-Authenticate"] = "Digest realm=\""~info.realm~"\", nonce=\""~info.createNonce(req)~"\", stale="~(stale?"true":"false"); 128 res.bodyWriter.write("Authorization required"); 129 } 130 return &handleRequest; 131 } 132 /// Scheduled for deprecation - use a `@safe` callback instead. 133 HTTPServerRequestDelegate performDigestAuth(DigestAuthInfo info, scope string delegate(string, string) @system pwhash) 134 @system { 135 return performDigestAuth(info, (r, u) @trusted => pwhash(r, u)); 136 } 137 138 /** 139 Enforces HTTP Digest Auth authentication on the given req/res pair. 140 141 Params: 142 req = Request object that is to be checked 143 res = Response object that will be used for authentication errors 144 info = Digest authentication info object 145 pwhash = A delegate queried for returning the digest password 146 147 Returns: Returns the name of the authenticated user. 148 149 Throws: Throws a HTTPStatusExeption in case of an authentication failure. 150 */ 151 string performDigestAuth(scope HTTPServerRequest req, scope HTTPServerResponse res, DigestAuthInfo info, scope DigestHashCallback pwhash) 152 { 153 bool stale; 154 string username; 155 if (checkDigest(req, info, pwhash, stale, username)) 156 return username; 157 158 res.headers["WWW-Authenticate"] = "Digest realm=\""~info.realm~"\", nonce=\""~info.createNonce(req)~"\", stale="~(stale?"true":"false"); 159 throw new HTTPStatusException(HTTPStatus.unauthorized); 160 } 161 /// Scheduled for deprecation - use a `@safe` callback instead. 162 string performDigestAuth(scope HTTPServerRequest req, scope HTTPServerResponse res, DigestAuthInfo info, scope string delegate(string, string) @system pwhash) 163 @system { 164 return performDigestAuth(req, res, info, (r, u) @trusted => pwhash(r, u)); 165 } 166 167 /** 168 Creates the digest password from the user name, realm and password. 169 170 Params: 171 realm = The realm 172 user = The user name 173 password = The plain text password 174 175 Returns: Returns the digest password 176 */ 177 string createDigestPassword(string realm, string user, string password) 178 { 179 return toHexString!(LetterCase.lower)(md5Of(user ~ ":" ~ realm ~ ":" ~ password)).dup; 180 } 181 182 alias DigestHashCallback = string delegate(string realm, string user); 183 184 /// Structure which describes requirements of the digest authentication - see https://tools.ietf.org/html/rfc2617 185 struct DigestAuthParams { 186 enum Qop { none = 0, auth = 1, auth_int = 2 } 187 enum Algorithm { none = 0, md5 = 1, md5_sess = 2 } 188 189 string realm, domain, nonce, opaque; 190 Algorithm algorithm = Algorithm.md5; 191 bool stale; 192 Qop qop; 193 194 /// Parses WWW-Authenticate header value with the digest parameters 195 this(string auth) { 196 import std.algorithm : splitter; 197 198 assert(auth.startsWith("Digest "), "Correct Digest authentication request not provided"); 199 200 foreach (param; auth["Digest ".length..$].splitter(',')) 201 { 202 auto idx = param.indexOf("="); 203 if (idx <= 0) { 204 logError("Invalid parameter in auth header: %s (%s)", param, auth); 205 continue; 206 } 207 auto k = param[0..idx]; 208 auto v = param[idx+1..$]; 209 switch (k.strip().toLower()) { 210 default: break; 211 case "realm": realm = v[1..$-1]; break; 212 case "domain": domain = v[1..$-1]; break; 213 case "nonce": nonce = v[1..$-1]; break; 214 case "opaque": opaque = v[1..$-1]; break; 215 case "stale": stale = v.toLower() == "true"; break; 216 case "algorithm": 217 switch (v) { 218 default: break; 219 case "MD5": algorithm = Algorithm.md5; break; 220 case "MD5-sess": algorithm = Algorithm.md5_sess; break; 221 } 222 break; 223 case "qop": 224 foreach (q; v[1..$-1].splitter(',')) { 225 switch (q) { 226 default: break; 227 case "auth": qop |= Qop.auth; break; 228 case "auth-int": qop |= Qop.auth_int; break; 229 } 230 } 231 break; 232 } 233 } 234 } 235 } 236 237 /** 238 Creates the digest authorization request header. 239 240 Params: 241 method = HTTP method (required only when some qop is requested) 242 username = user name 243 password = user password 244 url = requested url 245 auth = value from the WWW-Authenticate response header 246 cnonce = client generated unique data string (required only when some qop is requested) 247 nc = the count of requests sent by the client (required only when some qop is requested) 248 entityBody = request entity body required only if qop==auth-int 249 */ 250 auto createDigestAuthHeader(U)(HTTPMethod method, U url, string username, string password, DigestAuthParams auth, 251 string cnonce = null, int nc = 0, in ubyte[] entityBody = null) 252 if (is(U == string) || is(U == URL)) { 253 254 import std.array : appender; 255 import std.format : formattedWrite; 256 257 auto getHA1(string username, string password, string realm, string nonce = null, string cnonce = null) { 258 259 assert((nonce is null && cnonce is null) || (nonce !is null && cnonce !is null)); 260 261 auto ha1 = toHexString!(LetterCase.lower)(md5Of(format("%s:%s:%s", username, realm, password))).dup; 262 if (nonce !is null) ha1 = toHexString!(LetterCase.lower)(md5Of(format("%s:%s:%s", ha1, nonce, cnonce))).dup; 263 return ha1; 264 } 265 266 auto getHA2(HTTPMethod method, string uri, in ubyte[] ebody = null) { 267 return ebody is null 268 ? toHexString!(LetterCase.lower)(md5Of(format("%s:%s", method, uri))).dup 269 : toHexString!(LetterCase.lower)(md5Of(format("%s:%s:%s", method, uri, toHexString!(LetterCase.lower)(md5Of(ebody)).dup))).dup; 270 } 271 272 static if (is(U == string)) auto uri = URL(url).pathString; 273 else auto uri = url.pathString; 274 275 auto dig = appender!string(); 276 dig ~= "Digest "; 277 dig ~= `username="`; dig ~= username; dig ~= `", `; 278 dig ~= `realm="`; dig ~= auth.realm; dig ~= `", `; 279 dig ~= `nonce="`; dig ~= auth.nonce; dig ~= `", `; 280 dig ~= `uri="`; dig ~= uri; dig ~= `", `; 281 if (auth.opaque.length) { dig ~= `opaque="`; dig ~= auth.opaque; dig ~= `", `; } 282 283 //choose one of provided qop 284 DigestAuthParams.Qop qop; 285 if ((auth.qop & DigestAuthParams.Qop.auth) == DigestAuthParams.Qop.auth) qop = DigestAuthParams.Qop.auth; 286 else if ((auth.qop & DigestAuthParams.Qop.auth_int) == DigestAuthParams.Qop.auth_int) qop = DigestAuthParams.Qop.auth_int; 287 288 if (qop != DigestAuthParams.Qop.none) { 289 assert(cnonce !is null, "cnonce is required"); 290 assert(nc != 0, "nc is required"); 291 292 dig ~= `qop="`; dig ~= qop == DigestAuthParams.Qop.auth ? "auth" : "auth-int"; dig ~= `", `; 293 dig ~= `cnonce="`; dig ~= cnonce; dig ~= `", `; 294 dig ~= `nc="`; dig.formattedWrite("%08x", nc); dig ~= `", `; 295 } 296 297 auto ha1 = auth.algorithm == DigestAuthParams.Algorithm.md5_sess 298 ? getHA1(username, password, auth.realm, auth.nonce, cnonce) 299 : getHA1(username, password, auth.realm); 300 301 auto ha2 = qop != DigestAuthParams.Qop.auth_int 302 ? getHA2(method, uri) 303 : getHA2(method, uri, entityBody); 304 305 auto resp = qop == DigestAuthParams.Qop.none 306 ? toHexString!(LetterCase.lower)(md5Of(format("%s:%s:%s", ha1, auth.nonce, ha2))).dup 307 : toHexString!(LetterCase.lower)(md5Of(format("%s:%s:%08x:%s:%s:%s", ha1, auth.nonce, nc, cnonce, qop == DigestAuthParams.Qop.auth ? "auth" : "auth-int" , ha2))).dup; 308 309 dig ~= `response="`; dig ~= resp; dig ~= `"`; 310 311 return dig.data; 312 }