1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
3 * soup-auth-manager.c: SoupAuth manager for SoupSession
5 * Copyright (C) 2007 Red Hat, Inc.
14 #include "soup-auth-manager.h"
16 #include "soup-connection-auth.h"
17 #include "soup-message-private.h"
18 #include "soup-message-queue.h"
19 #include "soup-path-map.h"
20 #include "soup-session-private.h"
23 * SECTION:soup-auth-manager
24 * @short_description: HTTP client-side authentication handler
25 * @see_also: #SoupSession, #SoupAuth
27 * #SoupAuthManager is the #SoupSessionFeature that handles HTTP
28 * authentication for a #SoupSession.
30 * A #SoupAuthManager is added to the session by default, and normally
31 * you don't need to worry about it at all. However, if you want to
32 * disable HTTP authentication, you can remove the feature from the
33 * session with soup_session_remove_feature_by_type(), or disable it on
34 * individual requests with soup_message_disable_feature().
40 * SOUP_TYPE_AUTH_MANAGER:
42 * The #GType of #SoupAuthManager; you can use this with
43 * soup_session_remove_feature_by_type() or
44 * soup_message_disable_feature().
46 * (Although this type has only been publicly visible since libsoup
47 * 2.42, it has always existed in the background, and you can use
48 * <literal><code>g_type_from_name ("SoupAuthManager")</code></literal>
49 * to get its #GType in earlier releases.)
53 static void soup_auth_manager_session_feature_init (SoupSessionFeatureInterface *feature_interface, gpointer interface_data);
54 static SoupSessionFeatureInterface *soup_session_feature_default_interface;
61 static guint signals[LAST_SIGNAL] = { 0 };
64 struct SoupAuthManagerPrivate {
66 GPtrArray *auth_types;
71 GHashTable *auth_hosts;
76 SoupPathMap *auth_realms; /* path -> scheme:realm */
77 GHashTable *auths; /* scheme:realm -> SoupAuth */
80 G_DEFINE_TYPE_WITH_CODE (SoupAuthManager, soup_auth_manager, G_TYPE_OBJECT,
81 G_ADD_PRIVATE (SoupAuthManager)
82 G_IMPLEMENT_INTERFACE (SOUP_TYPE_SESSION_FEATURE,
83 soup_auth_manager_session_feature_init))
85 static void soup_auth_host_free (SoupAuthHost *host);
86 static SoupAuth *record_auth_for_uri (SoupAuthManagerPrivate *priv,
87 SoupURI *uri, SoupAuth *auth,
88 gboolean prior_auth_failed);
91 soup_auth_manager_init (SoupAuthManager *manager)
93 SoupAuthManagerPrivate *priv;
95 priv = manager->priv = soup_auth_manager_get_instance_private (manager);
97 priv->auth_types = g_ptr_array_new_with_free_func ((GDestroyNotify)g_type_class_unref);
98 priv->auth_hosts = g_hash_table_new_full (soup_uri_host_hash,
101 (GDestroyNotify)soup_auth_host_free);
102 g_mutex_init (&priv->lock);
106 soup_auth_manager_finalize (GObject *object)
108 SoupAuthManagerPrivate *priv = SOUP_AUTH_MANAGER (object)->priv;
110 g_ptr_array_free (priv->auth_types, TRUE);
112 g_hash_table_destroy (priv->auth_hosts);
114 g_clear_object (&priv->proxy_auth);
116 g_mutex_clear (&priv->lock);
118 G_OBJECT_CLASS (soup_auth_manager_parent_class)->finalize (object);
122 soup_auth_manager_class_init (SoupAuthManagerClass *auth_manager_class)
124 GObjectClass *object_class = G_OBJECT_CLASS (auth_manager_class);
126 object_class->finalize = soup_auth_manager_finalize;
129 * SoupAuthManager::authenticate:
130 * @manager: the #SoupAuthManager
131 * @msg: the #SoupMessage being sent
132 * @auth: the #SoupAuth to authenticate
133 * @retrying: %TRUE if this is the second (or later) attempt
135 * Emitted when the manager requires the application to
136 * provide authentication credentials.
138 * #SoupSession connects to this signal and emits its own
139 * #SoupSession::authenticate signal when it is emitted, so
140 * you shouldn't need to use this signal directly.
142 signals[AUTHENTICATE] =
143 g_signal_new ("authenticate",
144 G_OBJECT_CLASS_TYPE (object_class),
146 G_STRUCT_OFFSET (SoupAuthManagerClass, authenticate),
157 auth_type_compare_func (gconstpointer a, gconstpointer b)
159 SoupAuthClass **auth1 = (SoupAuthClass **)a;
160 SoupAuthClass **auth2 = (SoupAuthClass **)b;
162 return (*auth1)->strength - (*auth2)->strength;
166 soup_auth_manager_add_feature (SoupSessionFeature *feature, GType type)
168 SoupAuthManagerPrivate *priv = SOUP_AUTH_MANAGER (feature)->priv;
169 SoupAuthClass *auth_class;
171 if (!g_type_is_a (type, SOUP_TYPE_AUTH))
174 auth_class = g_type_class_ref (type);
175 g_ptr_array_add (priv->auth_types, auth_class);
176 g_ptr_array_sort (priv->auth_types, auth_type_compare_func);
178 /* Plain SoupSession does not get the backward-compat
179 * auto-NTLM behavior; SoupSession subclasses do.
181 if (type == SOUP_TYPE_AUTH_NTLM &&
182 G_TYPE_FROM_INSTANCE (priv->session) != SOUP_TYPE_SESSION)
183 priv->auto_ntlm = TRUE;
189 soup_auth_manager_remove_feature (SoupSessionFeature *feature, GType type)
191 SoupAuthManagerPrivate *priv = SOUP_AUTH_MANAGER (feature)->priv;
192 SoupAuthClass *auth_class;
195 if (!g_type_is_a (type, SOUP_TYPE_AUTH))
198 auth_class = g_type_class_peek (type);
200 for (i = 0; i < priv->auth_types->len; i++) {
201 if (priv->auth_types->pdata[i] == (gpointer)auth_class) {
202 if (type == SOUP_TYPE_AUTH_NTLM)
203 priv->auto_ntlm = FALSE;
205 g_ptr_array_remove_index (priv->auth_types, i);
214 soup_auth_manager_has_feature (SoupSessionFeature *feature, GType type)
216 SoupAuthManagerPrivate *priv = SOUP_AUTH_MANAGER (feature)->priv;
217 SoupAuthClass *auth_class;
220 if (!g_type_is_a (type, SOUP_TYPE_AUTH))
223 auth_class = g_type_class_peek (type);
224 for (i = 0; i < priv->auth_types->len; i++) {
225 if (priv->auth_types->pdata[i] == (gpointer)auth_class)
232 soup_auth_manager_attach (SoupSessionFeature *feature, SoupSession *session)
234 SoupAuthManagerPrivate *priv = SOUP_AUTH_MANAGER (feature)->priv;
236 /* FIXME: should support multiple sessions */
237 priv->session = session;
239 soup_session_feature_default_interface->attach (feature, session);
242 static inline const char *
243 auth_header_for_message (SoupMessage *msg)
245 if (msg->status_code == SOUP_STATUS_PROXY_UNAUTHORIZED) {
246 return soup_message_headers_get_list (msg->response_headers,
247 "Proxy-Authenticate");
249 return soup_message_headers_get_list (msg->response_headers,
255 next_challenge_start (GSList *items)
257 /* The relevant grammar (from httpbis):
259 * WWW-Authenticate = 1#challenge
260 * Proxy-Authenticate = 1#challenge
261 * challenge = auth-scheme [ 1*SP ( b64token / #auth-param ) ]
262 * auth-scheme = token
263 * auth-param = token BWS "=" BWS ( token / quoted-string )
264 * b64token = 1*( ALPHA / DIGIT /
265 * "-" / "." / "_" / "~" / "+" / "/" ) *"="
267 * The fact that quoted-strings can contain commas, equals
268 * signs, and auth scheme names makes it tricky to "cheat" on
269 * the parsing. So soup_auth_manager_extract_challenge() will
270 * have used soup_header_parse_list() to split the header into
271 * items. Given the grammar above, the possible items are:
274 * auth-scheme 1*SP b64token
275 * auth-scheme 1*SP auth-param
278 * where the first three represent the start of a new challenge and
279 * the last one does not.
282 for (; items; items = items->next) {
283 const char *item = items->data;
284 const char *sp = strpbrk (item, "\t\r\n ");
285 const char *eq = strchr (item, '=');
288 /* No "=", so it can't be an auth-param */
291 if (!sp || sp > eq) {
292 /* No space, or first space appears after the "=",
293 * so it must be an auth-param.
297 while (g_ascii_isspace (*++sp))
300 /* First "=" appears immediately after the first
301 * space, so this must be an auth-param with
302 * space around the "=".
307 /* "auth-scheme auth-param" or "auth-scheme b64token" */
315 soup_auth_manager_extract_challenge (const char *challenges, const char *scheme)
317 GSList *items, *i, *next;
318 int schemelen = strlen (scheme);
322 items = soup_header_parse_list (challenges);
324 /* First item will start with the scheme name, followed by
325 * either nothing, or else a space and then the first
328 for (i = items; i; i = next_challenge_start (i->next)) {
330 if (!g_ascii_strncasecmp (item, scheme, schemelen) &&
331 (!item[schemelen] || g_ascii_isspace (item[schemelen])))
335 soup_header_free_list (items);
339 next = next_challenge_start (i->next);
340 challenge = g_string_new (item);
341 for (i = i->next; i != next; i = i->next) {
343 g_string_append (challenge, ", ");
344 g_string_append (challenge, item);
347 soup_header_free_list (items);
348 return g_string_free (challenge, FALSE);
352 create_auth (SoupAuthManagerPrivate *priv, SoupMessage *msg)
355 SoupAuthClass *auth_class;
356 char *challenge = NULL;
357 SoupAuth *auth = NULL;
360 header = auth_header_for_message (msg);
364 for (i = priv->auth_types->len - 1; i >= 0; i--) {
365 auth_class = priv->auth_types->pdata[i];
366 challenge = soup_auth_manager_extract_challenge (header, auth_class->scheme_name);
369 auth = soup_auth_new (G_TYPE_FROM_CLASS (auth_class), msg, challenge);
379 check_auth (SoupMessage *msg, SoupAuth *auth)
381 const char *header, *scheme;
382 char *challenge = NULL;
385 scheme = soup_auth_get_scheme_name (auth);
387 header = auth_header_for_message (msg);
389 challenge = soup_auth_manager_extract_challenge (header, scheme);
392 challenge = g_strdup (scheme);
395 if (!soup_auth_update (auth, msg, challenge))
401 static SoupAuthHost *
402 get_auth_host_for_uri (SoupAuthManagerPrivate *priv, SoupURI *uri)
406 host = g_hash_table_lookup (priv->auth_hosts, uri);
410 host = g_slice_new0 (SoupAuthHost);
411 host->uri = soup_uri_copy_host (uri);
412 g_hash_table_insert (priv->auth_hosts, host->uri, host);
418 soup_auth_host_free (SoupAuthHost *host)
420 g_clear_pointer (&host->auth_realms, soup_path_map_free);
421 g_clear_pointer (&host->auths, g_hash_table_destroy);
423 soup_uri_free (host->uri);
424 g_slice_free (SoupAuthHost, host);
428 make_auto_ntlm_auth (SoupAuthManagerPrivate *priv, SoupAuthHost *host)
432 if (!priv->auto_ntlm)
435 auth = g_object_new (SOUP_TYPE_AUTH_NTLM,
436 SOUP_AUTH_HOST, host->uri->host,
438 record_auth_for_uri (priv, host->uri, auth, FALSE);
439 g_object_unref (auth);
444 update_authorization_header (SoupMessage *msg, SoupAuth *auth, gboolean is_proxy)
446 const char *authorization_header = is_proxy ? "Proxy-Authorization" : "Authorization";
449 if (soup_message_get_auth (msg))
450 soup_message_headers_remove (msg->request_headers, authorization_header);
455 token = soup_auth_get_authorization (auth, msg);
459 soup_message_headers_replace (msg->request_headers, authorization_header, token);
464 lookup_auth (SoupAuthManagerPrivate *priv, SoupMessage *msg)
467 const char *path, *realm;
470 /* If the message already has a ready auth, use that instead */
471 auth = soup_message_get_auth (msg);
472 if (auth && soup_auth_is_ready (auth, msg))
475 if (soup_message_get_flags (msg) & SOUP_MESSAGE_DO_NOT_USE_AUTH_CACHE)
478 host = get_auth_host_for_uri (priv, soup_message_get_uri (msg));
479 if (!host->auth_realms && !make_auto_ntlm_auth (priv, host))
482 /* Cannot change the above '&&' into '||', because make_auto_ntlm_auth() is used
483 * to populate host->auth_realms when it's not set yet. Even the make_auto_ntlm_auth()
484 * returns TRUE only if it also populates the host->auth_realms, this extra test
485 * is required to mute a FORWARD_NULL Coverity Scan warning, which is a false-positive
487 if (!host->auth_realms)
490 path = soup_message_get_uri (msg)->path;
493 realm = soup_path_map_lookup (host->auth_realms, path);
495 return g_hash_table_lookup (host->auths, realm);
501 lookup_proxy_auth (SoupAuthManagerPrivate *priv, SoupMessage *msg)
505 /* If the message already has a ready auth, use that instead */
506 auth = soup_message_get_proxy_auth (msg);
507 if (auth && soup_auth_is_ready (auth, msg))
510 if (soup_message_get_flags (msg) & SOUP_MESSAGE_DO_NOT_USE_AUTH_CACHE)
513 return priv->proxy_auth;
517 authenticate_auth (SoupAuthManager *manager, SoupAuth *auth,
518 SoupMessage *msg, gboolean prior_auth_failed,
519 gboolean proxy, gboolean can_interact)
521 SoupAuthManagerPrivate *priv = manager->priv;
524 if (!soup_auth_can_authenticate (auth))
528 SoupMessageQueue *queue;
529 SoupMessageQueueItem *item;
531 queue = soup_session_get_queue (priv->session);
532 item = soup_message_queue_lookup (queue, msg);
536 /* When loaded from the disk cache, the connection is NULL. */
537 uri = item->conn ? soup_connection_get_proxy_uri (item->conn) : NULL;
538 soup_message_queue_item_unref (item);
542 uri = soup_message_get_uri (msg);
544 /* If a password is specified explicitly in the URI, use it
545 * even if the auth had previously already been authenticated.
547 if (uri->password && uri->user) {
548 soup_auth_authenticate (auth, uri->user, uri->password);
549 soup_uri_set_password (uri, NULL);
550 soup_uri_set_user (uri, NULL);
551 } else if (!soup_auth_is_authenticated (auth) && can_interact) {
552 g_signal_emit (manager, signals[AUTHENTICATE], 0,
553 msg, auth, prior_auth_failed);
558 record_auth_for_uri (SoupAuthManagerPrivate *priv, SoupURI *uri,
559 SoupAuth *auth, gboolean prior_auth_failed)
564 char *auth_info, *old_auth_info;
567 host = get_auth_host_for_uri (priv, uri);
568 auth_info = soup_auth_get_info (auth);
570 if (!host->auth_realms) {
571 host->auth_realms = soup_path_map_new (g_free);
572 host->auths = g_hash_table_new_full (g_str_hash, g_str_equal,
573 g_free, g_object_unref);
576 /* Record where this auth realm is used. */
577 pspace = soup_auth_get_protection_space (auth, uri);
578 for (p = pspace; p; p = p->next) {
580 old_auth_info = soup_path_map_lookup (host->auth_realms, path);
582 if (!strcmp (old_auth_info, auth_info))
584 soup_path_map_remove (host->auth_realms, path);
587 soup_path_map_add (host->auth_realms, path,
588 g_strdup (auth_info));
590 soup_auth_free_protection_space (auth, pspace);
592 /* Now, make sure the auth is recorded. (If there's a
593 * pre-existing good auth, we keep that rather than the new one,
594 * since the old one might already be authenticated.)
596 old_auth = g_hash_table_lookup (host->auths, auth_info);
597 if (old_auth && (old_auth != auth || !prior_auth_failed)) {
601 g_hash_table_insert (host->auths, auth_info,
602 g_object_ref (auth));
608 auth_got_headers (SoupMessage *msg, gpointer manager)
610 SoupAuthManagerPrivate *priv = SOUP_AUTH_MANAGER (manager)->priv;
611 SoupAuth *auth, *prior_auth;
612 gboolean prior_auth_failed = FALSE;
614 g_mutex_lock (&priv->lock);
616 /* See if we used auth last time */
617 prior_auth = soup_message_get_auth (msg);
618 if (prior_auth && check_auth (msg, prior_auth)) {
619 auth = g_object_ref (prior_auth);
620 if (!soup_auth_is_ready (auth, msg))
621 prior_auth_failed = TRUE;
623 auth = create_auth (priv, msg);
625 g_mutex_unlock (&priv->lock);
630 if (!(soup_message_get_flags (msg) & SOUP_MESSAGE_DO_NOT_USE_AUTH_CACHE)) {
633 new_auth = record_auth_for_uri (priv, soup_message_get_uri (msg),
634 auth, prior_auth_failed);
635 g_object_unref (auth);
636 auth = g_object_ref (new_auth);
639 /* If we need to authenticate, try to do it. */
640 authenticate_auth (manager, auth, msg,
641 prior_auth_failed, FALSE, TRUE);
642 soup_message_set_auth (msg, auth);
643 g_object_unref (auth);
644 g_mutex_unlock (&priv->lock);
648 auth_got_body (SoupMessage *msg, gpointer manager)
650 SoupAuthManagerPrivate *priv = SOUP_AUTH_MANAGER (manager)->priv;
653 g_mutex_lock (&priv->lock);
654 auth = lookup_auth (priv, msg);
655 if (auth && soup_auth_is_ready (auth, msg)) {
656 if (SOUP_IS_CONNECTION_AUTH (auth)) {
657 SoupMessageFlags flags;
659 flags = soup_message_get_flags (msg);
660 soup_message_set_flags (msg, flags & ~SOUP_MESSAGE_NEW_CONNECTION);
663 /* When not using cached credentials, update the Authorization header
664 * right before requeuing the message.
666 if (soup_message_get_flags (msg) & SOUP_MESSAGE_DO_NOT_USE_AUTH_CACHE)
667 update_authorization_header (msg, auth, FALSE);
669 soup_session_requeue_message (priv->session, msg);
671 g_mutex_unlock (&priv->lock);
675 proxy_auth_got_headers (SoupMessage *msg, gpointer manager)
677 SoupAuthManagerPrivate *priv = SOUP_AUTH_MANAGER (manager)->priv;
678 SoupAuth *auth = NULL, *prior_auth;
679 gboolean prior_auth_failed = FALSE;
681 g_mutex_lock (&priv->lock);
683 /* See if we used auth last time */
684 prior_auth = soup_message_get_proxy_auth (msg);
685 if (prior_auth && check_auth (msg, prior_auth)) {
686 if (!soup_auth_is_ready (prior_auth, msg))
687 prior_auth_failed = TRUE;
690 if (!(soup_message_get_flags (msg) & SOUP_MESSAGE_DO_NOT_USE_AUTH_CACHE))
691 auth = priv->proxy_auth ? g_object_ref (priv->proxy_auth) : NULL;
694 auth = create_auth (priv, msg);
696 g_mutex_unlock (&priv->lock);
699 if (!(soup_message_get_flags (msg) & SOUP_MESSAGE_DO_NOT_USE_AUTH_CACHE))
700 priv->proxy_auth = g_object_ref (auth);
703 /* If we need to authenticate, try to do it. */
704 authenticate_auth (manager, auth, msg,
705 prior_auth_failed, TRUE, TRUE);
706 soup_message_set_proxy_auth (msg, auth);
707 g_object_unref (auth);
708 g_mutex_unlock (&priv->lock);
712 proxy_auth_got_body (SoupMessage *msg, gpointer manager)
714 SoupAuthManagerPrivate *priv = SOUP_AUTH_MANAGER (manager)->priv;
717 g_mutex_lock (&priv->lock);
719 auth = lookup_proxy_auth (priv, msg);
720 if (auth && soup_auth_is_ready (auth, msg)) {
721 /* When not using cached credentials, update the Authorization header
722 * right before requeuing the message.
724 if (soup_message_get_flags (msg) & SOUP_MESSAGE_DO_NOT_USE_AUTH_CACHE)
725 update_authorization_header (msg, auth, TRUE);
726 soup_session_requeue_message (priv->session, msg);
729 g_mutex_unlock (&priv->lock);
733 auth_msg_starting (SoupMessage *msg, gpointer manager)
735 SoupAuthManagerPrivate *priv = SOUP_AUTH_MANAGER (manager)->priv;
738 if (soup_message_get_flags (msg) & SOUP_MESSAGE_DO_NOT_USE_AUTH_CACHE)
741 g_mutex_lock (&priv->lock);
743 if (msg->method != SOUP_METHOD_CONNECT) {
744 auth = lookup_auth (priv, msg);
746 authenticate_auth (manager, auth, msg, FALSE, FALSE, FALSE);
747 if (!soup_auth_is_ready (auth, msg))
750 soup_message_set_auth (msg, auth);
751 update_authorization_header (msg, auth, FALSE);
754 auth = lookup_proxy_auth (priv, msg);
756 authenticate_auth (manager, auth, msg, FALSE, TRUE, FALSE);
757 if (!soup_auth_is_ready (auth, msg))
760 soup_message_set_proxy_auth (msg, auth);
761 update_authorization_header (msg, auth, TRUE);
763 g_mutex_unlock (&priv->lock);
767 soup_auth_manager_request_queued (SoupSessionFeature *manager,
768 SoupSession *session,
771 g_signal_connect (msg, "starting",
772 G_CALLBACK (auth_msg_starting), manager);
774 soup_message_add_status_code_handler (
775 msg, "got_headers", SOUP_STATUS_UNAUTHORIZED,
776 G_CALLBACK (auth_got_headers), manager);
777 soup_message_add_status_code_handler (
778 msg, "got_body", SOUP_STATUS_UNAUTHORIZED,
779 G_CALLBACK (auth_got_body), manager);
781 soup_message_add_status_code_handler (
782 msg, "got_headers", SOUP_STATUS_PROXY_UNAUTHORIZED,
783 G_CALLBACK (proxy_auth_got_headers), manager);
784 soup_message_add_status_code_handler (
785 msg, "got_body", SOUP_STATUS_PROXY_UNAUTHORIZED,
786 G_CALLBACK (proxy_auth_got_body), manager);
790 soup_auth_manager_request_unqueued (SoupSessionFeature *manager,
791 SoupSession *session,
794 g_signal_handlers_disconnect_matched (msg, G_SIGNAL_MATCH_DATA,
795 0, 0, NULL, NULL, manager);
799 * soup_auth_manager_use_auth:
800 * @manager: a #SoupAuthManager
801 * @uri: the #SoupURI under which @auth is to be used
802 * @auth: the #SoupAuth to use
804 * Records that @auth is to be used under @uri, as though a
805 * WWW-Authenticate header had been received at that URI. This can be
806 * used to "preload" @manager's auth cache, to avoid an extra HTTP
807 * round trip in the case where you know ahead of time that a 401
808 * response will be returned.
810 * This is only useful for authentication types where the initial
811 * Authorization header does not depend on any additional information
812 * from the server. (Eg, Basic or NTLM, but not Digest.)
817 soup_auth_manager_use_auth (SoupAuthManager *manager,
821 SoupAuthManagerPrivate *priv = manager->priv;
823 g_mutex_lock (&priv->lock);
824 record_auth_for_uri (priv, uri, auth, FALSE);
825 g_mutex_unlock (&priv->lock);
829 * soup_auth_manager_clear_cached_credentials:
830 * @manager: a #SoupAuthManager
832 * Clear all credentials cached by @manager
837 soup_auth_manager_clear_cached_credentials (SoupAuthManager *manager)
839 SoupAuthManagerPrivate *priv;
841 g_return_if_fail (SOUP_IS_AUTH_MANAGER (manager));
843 priv = manager->priv;
844 g_mutex_lock (&priv->lock);
845 g_hash_table_remove_all (priv->auth_hosts);
846 g_mutex_unlock (&priv->lock);
850 soup_auth_manager_session_feature_init (SoupSessionFeatureInterface *feature_interface,
851 gpointer interface_data)
853 soup_session_feature_default_interface =
854 g_type_default_interface_peek (SOUP_TYPE_SESSION_FEATURE);
856 feature_interface->attach = soup_auth_manager_attach;
857 feature_interface->request_queued = soup_auth_manager_request_queued;
858 feature_interface->request_unqueued = soup_auth_manager_request_unqueued;
859 feature_interface->add_feature = soup_auth_manager_add_feature;
860 feature_interface->remove_feature = soup_auth_manager_remove_feature;
861 feature_interface->has_feature = soup_auth_manager_has_feature;