1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- */
2 /*
3  * soup-auth-domain-digest.c: HTTP Digest Authentication (server-side)
4  *
5  * Copyright (C) 2007 Novell, Inc.
6  */
7 
8 #ifdef HAVE_CONFIG_H
9 #include <config.h>
10 #endif
11 
12 #include <string.h>
13 #include <stdlib.h>
14 
15 #include "soup-auth-domain-digest.h"
16 #include "soup-uri-utils-private.h"
17 #include "soup-message-headers-private.h"
18 #include "soup.h"
19 #include "auth/soup-auth-digest-private.h"
20 
21 /**
22  * SECTION:soup-auth-domain-digest
23  * @short_description: Server-side "Digest" authentication
24  *
25  * #SoupAuthDomainDigest handles the server side of HTTP "Digest"
26  * authentication.
27  **/
28 
29 /**
30  * SoupAuthDomainDigest:
31  *
32  * Subclass of #SoupAuthDomain for Digest authentication.
33  */
34 
35 enum {
36 	PROP_0,
37 
38 	PROP_AUTH_CALLBACK,
39 	PROP_AUTH_DATA,
40 
41 	LAST_PROPERTY
42 };
43 
44 static GParamSpec *properties[LAST_PROPERTY] = { NULL, };
45 
46 struct _SoupAuthDomainDigest {
47 	SoupAuthDomain parent;
48 };
49 
50 typedef struct {
51 	SoupAuthDomainDigestAuthCallback auth_callback;
52 	gpointer auth_data;
53 	GDestroyNotify auth_dnotify;
54 
55 } SoupAuthDomainDigestPrivate;
56 
G_DEFINE_FINAL_TYPE_WITH_PRIVATE(SoupAuthDomainDigest,soup_auth_domain_digest,SOUP_TYPE_AUTH_DOMAIN)57 G_DEFINE_FINAL_TYPE_WITH_PRIVATE (SoupAuthDomainDigest, soup_auth_domain_digest, SOUP_TYPE_AUTH_DOMAIN)
58 
59 static void
60 soup_auth_domain_digest_init (SoupAuthDomainDigest *digest)
61 {
62 }
63 
64 static void
soup_auth_domain_digest_finalize(GObject * object)65 soup_auth_domain_digest_finalize (GObject *object)
66 {
67 	SoupAuthDomainDigestPrivate *priv =
68 		soup_auth_domain_digest_get_instance_private (SOUP_AUTH_DOMAIN_DIGEST (object));
69 
70 	if (priv->auth_dnotify)
71 		priv->auth_dnotify (priv->auth_data);
72 
73 	G_OBJECT_CLASS (soup_auth_domain_digest_parent_class)->finalize (object);
74 }
75 
76 static void
soup_auth_domain_digest_set_property(GObject * object,guint prop_id,const GValue * value,GParamSpec * pspec)77 soup_auth_domain_digest_set_property (GObject *object, guint prop_id,
78 				      const GValue *value, GParamSpec *pspec)
79 {
80 	SoupAuthDomainDigestPrivate *priv =
81 		soup_auth_domain_digest_get_instance_private (SOUP_AUTH_DOMAIN_DIGEST (object));
82 
83 	switch (prop_id) {
84 	case PROP_AUTH_CALLBACK:
85 		priv->auth_callback = g_value_get_pointer (value);
86 		break;
87 	case PROP_AUTH_DATA:
88 		if (priv->auth_dnotify) {
89 			priv->auth_dnotify (priv->auth_data);
90 			priv->auth_dnotify = NULL;
91 		}
92 		priv->auth_data = g_value_get_pointer (value);
93 		break;
94 	default:
95 		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
96 		break;
97 	}
98 }
99 
100 static void
soup_auth_domain_digest_get_property(GObject * object,guint prop_id,GValue * value,GParamSpec * pspec)101 soup_auth_domain_digest_get_property (GObject *object, guint prop_id,
102 				      GValue *value, GParamSpec *pspec)
103 {
104 	SoupAuthDomainDigestPrivate *priv =
105 		soup_auth_domain_digest_get_instance_private (SOUP_AUTH_DOMAIN_DIGEST (object));
106 
107 	switch (prop_id) {
108 	case PROP_AUTH_CALLBACK:
109 		g_value_set_pointer (value, priv->auth_callback);
110 		break;
111 	case PROP_AUTH_DATA:
112 		g_value_set_pointer (value, priv->auth_data);
113 		break;
114 	default:
115 		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
116 		break;
117 	}
118 }
119 
120 /**
121  * soup_auth_domain_digest_new: (constructor)
122  * @optname1: name of first option, or %NULL
123  * @...: option name/value pairs
124  *
125  * Creates a #SoupAuthDomainDigest. You must set the
126  * SoupAuthDomain:realm property, to indicate the realm name to be
127  * returned with the authentication challenge to the client. Other
128  * parameters are optional.
129  *
130  * Returns: the new #SoupAuthDomain
131  **/
132 SoupAuthDomain *
soup_auth_domain_digest_new(const char * optname1,...)133 soup_auth_domain_digest_new (const char *optname1, ...)
134 {
135 	SoupAuthDomain *domain;
136 	va_list ap;
137 
138 	va_start (ap, optname1);
139 	domain = (SoupAuthDomain *)g_object_new_valist (SOUP_TYPE_AUTH_DOMAIN_DIGEST,
140 							optname1, ap);
141 	va_end (ap);
142 
143 	g_return_val_if_fail (soup_auth_domain_get_realm (domain) != NULL, NULL);
144 
145 	return domain;
146 }
147 
148 /**
149  * SoupAuthDomainDigestAuthCallback:
150  * @domain: (type SoupAuthDomainDigest): the domain
151  * @msg: the message being authenticated
152  * @username: the username provided by the client
153  * @user_data: the data passed to soup_auth_domain_digest_set_auth_callback()
154  *
155  * Callback used by #SoupAuthDomainDigest for authentication purposes.
156  * The application should look up @username in its password database,
157  * and return the corresponding encoded password (see
158  * soup_auth_domain_digest_encode_password()).
159  *
160  * Returns: (nullable): the encoded password, or %NULL if
161  * @username is not a valid user. @domain will free the password when
162  * it is done with it.
163  **/
164 
165 /**
166  * soup_auth_domain_digest_set_auth_callback:
167  * @domain: (type SoupAuthDomainDigest): the domain
168  * @callback: the callback
169  * @user_data: data to pass to @auth_callback
170  * @dnotify: destroy notifier to free @user_data when @domain
171  * is destroyed
172  *
173  * Sets the callback that @domain will use to authenticate incoming
174  * requests. For each request containing authorization, @domain will
175  * invoke the callback, and then either accept or reject the request
176  * based on @callback's return value.
177  *
178  * You can also set the auth callback by setting the
179  * SoupAuthDomainDigest:auth-callback and
180  * SoupAuthDomainDigest:auth-data properties, which can also be
181  * used to set the callback at construct time.
182  **/
183 void
soup_auth_domain_digest_set_auth_callback(SoupAuthDomain * domain,SoupAuthDomainDigestAuthCallback callback,gpointer user_data,GDestroyNotify dnotify)184 soup_auth_domain_digest_set_auth_callback (SoupAuthDomain *domain,
185 					   SoupAuthDomainDigestAuthCallback callback,
186 					   gpointer        user_data,
187 					   GDestroyNotify  dnotify)
188 {
189 	SoupAuthDomainDigestPrivate *priv =
190 		soup_auth_domain_digest_get_instance_private (SOUP_AUTH_DOMAIN_DIGEST (domain));
191 
192 	if (priv->auth_dnotify)
193 		priv->auth_dnotify (priv->auth_data);
194 
195 	priv->auth_callback = callback;
196 	priv->auth_data = user_data;
197 	priv->auth_dnotify = dnotify;
198 
199 	g_object_notify_by_pspec (G_OBJECT (domain), properties[PROP_AUTH_CALLBACK]);
200 	g_object_notify_by_pspec (G_OBJECT (domain), properties[PROP_AUTH_DATA]);
201 }
202 
203 static gboolean
check_hex_urp(SoupAuthDomain * domain,SoupServerMessage * msg,GHashTable * params,const char * username,const char * hex_urp)204 check_hex_urp (SoupAuthDomain    *domain,
205 	       SoupServerMessage *msg,
206 	       GHashTable        *params,
207 	       const char        *username,
208 	       const char        *hex_urp)
209 {
210 	const char *uri, *qop, *realm, *msg_username;
211 	const char *nonce, *nc, *cnonce, *response;
212 	char hex_a1[33], computed_response[33];
213 	int nonce_count;
214 	GUri *dig_uri, *req_uri;
215 
216 	msg_username = g_hash_table_lookup (params, "username");
217 	if (!msg_username || strcmp (msg_username, username) != 0)
218 		return FALSE;
219 
220 	/* Check uri */
221 	uri = g_hash_table_lookup (params, "uri");
222 	if (!uri)
223 		return FALSE;
224 
225 	req_uri = soup_server_message_get_uri (msg);
226 	dig_uri = g_uri_parse (uri, SOUP_HTTP_URI_FLAGS, NULL);
227 	if (dig_uri) {
228 		if (!soup_uri_equal (dig_uri, req_uri)) {
229 			g_uri_unref (dig_uri);
230 			return FALSE;
231 		}
232 		g_uri_unref (dig_uri);
233 	} else {
234 		char *req_path;
235 		char *dig_path;
236 
237 		req_path = soup_uri_get_path_and_query (req_uri);
238 		dig_path = g_uri_unescape_string (uri, NULL);
239 
240 		if (strcmp (dig_path, req_path) != 0) {
241 			g_free (req_path);
242 			g_free (dig_path);
243 			return FALSE;
244 		}
245 		g_free (req_path);
246 		g_free (dig_path);
247 	}
248 
249 	/* Check qop; we only support "auth" for now */
250 	qop = g_hash_table_lookup (params, "qop");
251 	if (!qop || strcmp (qop, "auth") != 0)
252 		return FALSE;
253 
254 	/* Check realm */
255 	realm = g_hash_table_lookup (params, "realm");
256 	if (!realm || strcmp (realm, soup_auth_domain_get_realm (domain)) != 0)
257 		return FALSE;
258 
259 	nonce = g_hash_table_lookup (params, "nonce");
260 	if (!nonce)
261 		return FALSE;
262 	nc = g_hash_table_lookup (params, "nc");
263 	if (!nc)
264 		return FALSE;
265 	nonce_count = strtoul (nc, NULL, 16);
266 	if (nonce_count <= 0)
267 		return FALSE;
268 	cnonce = g_hash_table_lookup (params, "cnonce");
269 	if (!cnonce)
270 		return FALSE;
271 	response = g_hash_table_lookup (params, "response");
272 	if (!response)
273 		return FALSE;
274 
275 	soup_auth_digest_compute_hex_a1 (hex_urp,
276 					 SOUP_AUTH_DIGEST_ALGORITHM_MD5,
277 					 nonce, cnonce, hex_a1);
278 	soup_auth_digest_compute_response (soup_server_message_get_method (msg),
279 					   uri,
280 					   hex_a1,
281 					   SOUP_AUTH_DIGEST_QOP_AUTH,
282 					   nonce, cnonce, nonce_count,
283 					   computed_response);
284 	return strcmp (response, computed_response) == 0;
285 }
286 
287 static char *
soup_auth_domain_digest_accepts(SoupAuthDomain * domain,SoupServerMessage * msg,const char * header)288 soup_auth_domain_digest_accepts (SoupAuthDomain    *domain,
289 				 SoupServerMessage *msg,
290 				 const char        *header)
291 {
292 	SoupAuthDomainDigestPrivate *priv =
293 		soup_auth_domain_digest_get_instance_private (SOUP_AUTH_DOMAIN_DIGEST (domain));
294 	GHashTable *params;
295 	const char *username;
296 	gboolean accept = FALSE;
297 	char *ret_user;
298 
299 	if (strncmp (header, "Digest ", 7) != 0)
300 		return NULL;
301 
302 	params = soup_header_parse_param_list (header + 7);
303 	if (!params)
304 		return NULL;
305 
306 	username = g_hash_table_lookup (params, "username");
307 	if (!username) {
308 		soup_header_free_param_list (params);
309 		return NULL;
310 	}
311 
312 	if (priv->auth_callback) {
313 		char *hex_urp;
314 
315 		hex_urp = priv->auth_callback (domain, msg, username,
316 					       priv->auth_data);
317 		if (hex_urp) {
318 			accept = check_hex_urp (domain, msg, params,
319 						username, hex_urp);
320 			g_free (hex_urp);
321 		} else
322 			accept = FALSE;
323 	} else {
324 		accept = soup_auth_domain_try_generic_auth_callback (
325 			domain, msg, username);
326 	}
327 
328 	ret_user = accept ? g_strdup (username) : NULL;
329 	soup_header_free_param_list (params);
330 	return ret_user;
331 }
332 
333 static char *
soup_auth_domain_digest_challenge(SoupAuthDomain * domain,SoupServerMessage * msg)334 soup_auth_domain_digest_challenge (SoupAuthDomain    *domain,
335 				   SoupServerMessage *msg)
336 {
337 	GString *str;
338 
339 	str = g_string_new ("Digest ");
340 	soup_header_g_string_append_param_quoted (str, "realm", soup_auth_domain_get_realm (domain));
341 	g_string_append_printf (str, ", nonce=\"%lu%lu\"",
342 				(unsigned long) msg,
343 				(unsigned long) time (0));
344 	g_string_append_printf (str, ", qop=\"auth\"");
345 	g_string_append_printf (str, ", algorithm=MD5");
346 
347 	return g_string_free (str, FALSE);
348 }
349 
350 /**
351  * soup_auth_domain_digest_encode_password:
352  * @username: a username
353  * @realm: an auth realm name
354  * @password: the password for @username in @realm
355  *
356  * Encodes the username/realm/password triplet for Digest
357  * authentication. (That is, it returns a stringified MD5 hash of
358  * @username, @realm, and @password concatenated together). This is
359  * the form that is needed as the return value of
360  * #SoupAuthDomainDigest's auth handler.
361  *
362  * For security reasons, you should store the encoded hash, rather
363  * than storing the cleartext password itself and calling this method
364  * only when you need to verify it. This way, if your server is
365  * compromised, the attackers will not gain access to cleartext
366  * passwords which might also be usable at other sites. (Note also
367  * that the encoded password returned by this method is identical to
368  * the encoded password stored in an Apache .htdigest file.)
369  *
370  * Returns: the encoded password
371  **/
372 char *
soup_auth_domain_digest_encode_password(const char * username,const char * realm,const char * password)373 soup_auth_domain_digest_encode_password (const char *username,
374 					 const char *realm,
375 					 const char *password)
376 {
377 	char hex_urp[33];
378 
379 	soup_auth_digest_compute_hex_urp (username, realm, password, hex_urp);
380 	return g_strdup (hex_urp);
381 }
382 
383 static gboolean
soup_auth_domain_digest_check_password(SoupAuthDomain * domain,SoupServerMessage * msg,const char * username,const char * password)384 soup_auth_domain_digest_check_password (SoupAuthDomain    *domain,
385 					SoupServerMessage *msg,
386 					const char        *username,
387 					const char        *password)
388 {
389 	const char *header;
390 	GHashTable *params;
391 	const char *msg_username;
392 	char hex_urp[33];
393 	gboolean accept;
394 
395 	header = soup_message_headers_get_one_common (soup_server_message_get_request_headers (msg),
396                                                       SOUP_HEADER_AUTHORIZATION);
397 	if (!header || (strncmp (header, "Digest ", 7) != 0))
398 		return FALSE;
399 
400 	params = soup_header_parse_param_list (header + 7);
401 	if (!params)
402 		return FALSE;
403 
404 	msg_username = g_hash_table_lookup (params, "username");
405 	if (!msg_username || strcmp (msg_username, username) != 0) {
406 		soup_header_free_param_list (params);
407 		return FALSE;
408 	}
409 
410 	soup_auth_digest_compute_hex_urp (username,
411 					  soup_auth_domain_get_realm (domain),
412 					  password, hex_urp);
413 	accept = check_hex_urp (domain, msg, params, username, hex_urp);
414 	soup_header_free_param_list (params);
415 	return accept;
416 }
417 
418 static void
soup_auth_domain_digest_class_init(SoupAuthDomainDigestClass * digest_class)419 soup_auth_domain_digest_class_init (SoupAuthDomainDigestClass *digest_class)
420 {
421 	SoupAuthDomainClass *auth_domain_class =
422 		SOUP_AUTH_DOMAIN_CLASS (digest_class);
423 	GObjectClass *object_class = G_OBJECT_CLASS (digest_class);
424 
425 	auth_domain_class->accepts        = soup_auth_domain_digest_accepts;
426 	auth_domain_class->challenge      = soup_auth_domain_digest_challenge;
427 	auth_domain_class->check_password = soup_auth_domain_digest_check_password;
428 
429 	object_class->finalize     = soup_auth_domain_digest_finalize;
430 	object_class->set_property = soup_auth_domain_digest_set_property;
431 	object_class->get_property = soup_auth_domain_digest_get_property;
432 
433 	/**
434 	 * SoupAuthDomainDigest:auth-callback: (type SoupAuthDomainDigestAuthCallback)
435 	 *
436 	 * The #SoupAuthDomainDigestAuthCallback
437 	 */
438         properties[PROP_AUTH_CALLBACK] =
439 		g_param_spec_pointer ("auth-callback",
440 				      "Authentication callback",
441 				      "Password-finding callback",
442 				      G_PARAM_READWRITE |
443 				      G_PARAM_STATIC_STRINGS);
444 	/**
445 	 * SoupAuthDomainDigest:auth-data:
446 	 *
447 	 * The data to pass to the #SoupAuthDomainDigestAuthCallback
448 	 */
449         properties[PROP_AUTH_DATA] =
450 		g_param_spec_pointer ("auth-data",
451 				      "Authentication callback data",
452 				      "Data to pass to authentication callback",
453 				      G_PARAM_READWRITE |
454 				      G_PARAM_STATIC_STRINGS);
455 
456         g_object_class_install_properties (object_class, LAST_PROPERTY, properties);
457 }
458