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);