digest_auth.d 10 KB

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