1 /** 2 Implements HTTP Digest Authentication. 3 4 This is a minimal implementation based on RFC 2069. 5 6 Copyright: © 2015 RejectedSoftware e.K. 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.http.server; 13 import vibe.core.log; 14 15 import std.base64; 16 import std.datetime; 17 import std.digest.md; 18 import std.exception; 19 import std.string; 20 import std.uuid; 21 22 @safe: 23 24 enum NonceState { Valid, Expired, Invalid } 25 26 class DigestAuthInfo 27 { 28 @safe: 29 30 string realm; 31 ubyte[] secret; 32 ulong timeout; 33 34 this() 35 { 36 secret = randomUUID().data.dup; 37 timeout = 300; 38 } 39 40 string createNonce(in HTTPServerRequest req) 41 { 42 auto now = Clock.currTime(UTC()).stdTime(); 43 auto time = () @trusted { return *cast(ubyte[now.sizeof]*)&now; } (); 44 MD5 md5; 45 md5.put(time); 46 md5.put(secret); 47 auto data = md5.finish(); 48 return Base64.encode(time ~ data); 49 } 50 51 NonceState checkNonce(in string nonce, in HTTPServerRequest req) 52 { 53 auto now = Clock.currTime(UTC()).stdTime(); 54 ubyte[] decoded = Base64.decode(nonce); 55 if (decoded.length != now.sizeof + secret.length) return NonceState.Invalid; 56 auto timebytes = decoded[0 .. now.sizeof]; 57 auto time = () @trusted { return (cast(typeof(now)[])timebytes)[0]; } (); 58 if (timeout + time > now) return NonceState.Expired; 59 MD5 md5; 60 md5.put(timebytes); 61 md5.put(secret); 62 auto data = md5.finish(); 63 if (data[] != decoded[now.sizeof .. $]) return NonceState.Invalid; 64 return NonceState.Valid; 65 } 66 } 67 68 private bool checkDigest(scope HTTPServerRequest req, DigestAuthInfo info, scope DigestHashCallback pwhash, out bool stale, out string username) 69 { 70 stale = false; 71 username = ""; 72 auto pauth = "Authorization" in req.headers; 73 74 if (pauth && (*pauth).startsWith("Digest ")) { 75 string realm, nonce, response, uri, algorithm; 76 foreach (param; split((*pauth)[7 .. $], ",")) { 77 auto kv = split(param, "="); 78 switch (kv[0].strip().toLower()) { 79 default: break; 80 case "realm": realm = kv[1][1..$-1]; break; 81 case "username": username = kv[1][1..$-1]; break; 82 case "nonce": nonce = kv[1][1..$-1]; break; 83 case "uri": uri = kv[1][1..$-1]; break; 84 case "response": response = kv[1][1..$-1]; break; 85 case "algorithm": algorithm = kv[1][1..$-1]; break; 86 } 87 } 88 89 if (realm != info.realm) 90 return false; 91 if (algorithm !is null && algorithm != "MD5") 92 return false; 93 94 auto nonceState = info.checkNonce(nonce, req); 95 if (nonceState != NonceState.Valid) { 96 stale = nonceState == NonceState.Expired; 97 return false; 98 } 99 100 auto ha1 = pwhash(realm, username); 101 auto ha2 = toHexString!(LetterCase.lower)(md5Of(httpMethodString(req.method) ~ ":" ~ uri)); 102 auto calcresponse = toHexString!(LetterCase.lower)(md5Of(ha1 ~ ":" ~ nonce ~ ":" ~ ha2 )); 103 if (response[] == calcresponse[]) 104 return true; 105 } 106 return false; 107 } 108 109 /** 110 Returns a request handler that enforces request to be authenticated using HTTP Digest Auth. 111 */ 112 HTTPServerRequestDelegate performDigestAuth(DigestAuthInfo info, scope DigestHashCallback pwhash) 113 { 114 void handleRequest(HTTPServerRequest req, HTTPServerResponse res) 115 @safe { 116 bool stale; 117 string username; 118 if (checkDigest(req, info, pwhash, stale, username)) { 119 req.username = username; 120 return ; 121 } 122 123 // else output an error page 124 res.statusCode = HTTPStatus.unauthorized; 125 res.contentType = "text/plain"; 126 res.headers["WWW-Authenticate"] = "Digest realm=\""~info.realm~"\", nonce=\""~info.createNonce(req)~"\", stale="~(stale?"true":"false"); 127 res.bodyWriter.write("Authorization required"); 128 } 129 return &handleRequest; 130 } 131 /// Scheduled for deprecation - use a `@safe` callback instead. 132 HTTPServerRequestDelegate performDigestAuth(DigestAuthInfo info, scope string delegate(string, string) @system pwhash) 133 @system { 134 return performDigestAuth(info, (r, u) @trusted => pwhash(r, u)); 135 } 136 137 /** 138 Enforces HTTP Digest Auth authentication on the given req/res pair. 139 140 Params: 141 req = Request object that is to be checked 142 res = Response object that will be used for authentication errors 143 info = Digest authentication info object 144 pwhash = A delegate queried for returning the digest password 145 146 Returns: Returns the name of the authenticated user. 147 148 Throws: Throws a HTTPStatusExeption in case of an authentication failure. 149 */ 150 string performDigestAuth(scope HTTPServerRequest req, scope HTTPServerResponse res, DigestAuthInfo info, scope DigestHashCallback pwhash) 151 { 152 bool stale; 153 string username; 154 if (checkDigest(req, info, pwhash, stale, username)) 155 return username; 156 157 res.headers["WWW-Authenticate"] = "Digest realm=\""~info.realm~"\", nonce=\""~info.createNonce(req)~"\", stale="~(stale?"true":"false"); 158 throw new HTTPStatusException(HTTPStatus.unauthorized); 159 } 160 /// Scheduled for deprecation - use a `@safe` callback instead. 161 string performDigestAuth(scope HTTPServerRequest req, scope HTTPServerResponse res, DigestAuthInfo info, scope string delegate(string, string) @system pwhash) 162 @system { 163 return performDigestAuth(req, res, info, (r, u) @trusted => pwhash(r, u)); 164 } 165 166 /** 167 Creates the digest password from the user name, realm and password. 168 169 Params: 170 realm = The realm 171 user = The user name 172 password = The plain text password 173 174 Returns: Returns the digest password 175 */ 176 string createDigestPassword(string realm, string user, string password) 177 { 178 return toHexString!(LetterCase.lower)(md5Of(user ~ ":" ~ realm ~ ":" ~ password)).dup; 179 } 180 181 alias DigestHashCallback = string delegate(string realm, string user);