The Samba team recommends to use their "ntlm_auth" command line helper
for "NTLM" and "GSS-SPNEGO" authentication. This helper interacts with
the Samba's winbind daemon, and this way can authenticate users against
NT or Active Directory windows domain.
Currently Dovecot can do "NTLM" authentication too, but just "locally"
(against a local or sql database etc.).
I've made a patch (attached), which adds "ntlm_auth" (or "winbind")
support for Dovecot.
The idea is to add two new authentication mechanisms:
"mech_winbind_ntlm" and "mech_winbind_spnego". Both are coded in one
additional file, "mech-winbind.c". An option "auth_ntlm_use_winbind"
specifies whether to use the current implementation of ntlm, or do it by
the "ntlm_auth" helper. "GSS-SPNEGO" always performed by the helper.
Normally, "ntlm_auth" is invoked once, for all further requests.
Such a way, "ntlm_auth helper from the Samba package, interacting with
the Samba's winbind daemon", is used now by Squid, Apache and AFAIK some
other applications. It is "strongly recommended" by the Samba team, and
was already proposed even in this maillist 3 year ago (see f.e.
http://www.dovecot.org/list/dovecot/2004-September/004775.html ).
I hope there are no any serious performance issues for such a "complex
way" -- f.e. with our web proxy, using this way, ~200 users do not feel
any actual delays etc.
I've successfully tested this patch with NTLM against AD domain.
This patch can considerably improve the situation of "Email client on
Windows desktop under Windows domain, but imap/pop at UNIX server".
Currently, windows users have to specify their "login/password" for
email accounts manually. There is an "SPA" (Secure Password
Authentication) alternative for them, where just the desktop's login is
used transparently, but it cannot be used now, because Dovecot cannot
perform NTLM against, say, Active Directory domain.
Since "dovecot-auth" daemon can be utilized by MTA as well (Postfix and
other), the support of "ntlm_auth" in Dovecot can satisfy both SMTP and
IMAP servers at UNIX side, and solve the issue completely.
Questions and requests:
- I try to code things most close to used style, i.e. using Dovecot's
memory-management and io-pipe routines etc., but could someone look at
it and check whether I've missed something or not?
- Perhaps some names (of routines, modules) could be chosen better?
- Maybe some other options should be implemented, i.e.
"auth_winbind_helper_ntlm" and "auth_winbind_helper_spnego" to specify
the helper's cmdline exactly ?
- Currently I strip domain part of the username returned, i.e. from
"DOMAIN\user" just to "user". Maybe better add some option
"auth_winbind_strip_domain" for this?
Certainly, it will be fine if someone else check it more, especially for
"GSS-SPNEGO" which I cannot test for a while.
Regards,
Dmitry Butskoy
http://www.fedoraproject.org/wiki/DmitryButskoy
diff -Nru dovecot-1.0.1/src/auth/Makefile.am dovecot-1.0.1-OK/src/auth/Makefile.am
--- dovecot-1.0.1/src/auth/Makefile.am 2007-05-19 15:14:04.000000000 +0400
+++ dovecot-1.0.1-OK/src/auth/Makefile.am 2007-06-29 17:34:33.000000000 +0400
@@ -56,6 +56,7 @@
mech-cram-md5.c \
mech-digest-md5.c \
mech-ntlm.c \
+ mech-winbind.c \
mech-gssapi.c \
mech-rpa.c \
mech-apop.c \
diff -Nru dovecot-1.0.1/src/auth/Makefile.in dovecot-1.0.1-OK/src/auth/Makefile.in
--- dovecot-1.0.1/src/auth/Makefile.in 2007-06-14 16:02:13.000000000 +0400
+++ dovecot-1.0.1-OK/src/auth/Makefile.in 2007-06-29 17:35:06.000000000 +0400
@@ -78,6 +78,7 @@
mech.$(OBJEXT) mech-anonymous.$(OBJEXT) mech-plain.$(OBJEXT) \
mech-login.$(OBJEXT) mech-cram-md5.$(OBJEXT) \
mech-digest-md5.$(OBJEXT) mech-ntlm.$(OBJEXT) \
+ mech-winbind.$(OBJEXT) \
mech-gssapi.$(OBJEXT) mech-rpa.$(OBJEXT) mech-apop.$(OBJEXT) \
passdb.$(OBJEXT) passdb-blocking.$(OBJEXT) \
passdb-bsdauth.$(OBJEXT) passdb-cache.$(OBJEXT) \
@@ -325,6 +326,7 @@
mech-cram-md5.c \
mech-digest-md5.c \
mech-ntlm.c \
+ mech-winbind.c \
mech-gssapi.c \
mech-rpa.c \
mech-apop.c \
@@ -494,6 +496,7 @@
@AMDEP_TRUE@@am__include@ @[EMAIL PROTECTED]/$(DEPDIR)/[EMAIL PROTECTED]@
@AMDEP_TRUE@@am__include@ @[EMAIL PROTECTED]/$(DEPDIR)/[EMAIL PROTECTED]@
@AMDEP_TRUE@@am__include@ @[EMAIL PROTECTED]/$(DEPDIR)/[EMAIL PROTECTED]@
[EMAIL PROTECTED]@@am__include@ @[EMAIL PROTECTED]/$(DEPDIR)/[EMAIL PROTECTED]@
@AMDEP_TRUE@@am__include@ @[EMAIL PROTECTED]/$(DEPDIR)/[EMAIL PROTECTED]@
@AMDEP_TRUE@@am__include@ @[EMAIL PROTECTED]/$(DEPDIR)/[EMAIL PROTECTED]@
@AMDEP_TRUE@@am__include@ @[EMAIL PROTECTED]/$(DEPDIR)/[EMAIL PROTECTED]@
diff -Nru dovecot-1.0.1/src/auth/mech-winbind.c dovecot-1.0.1-OK/src/auth/mech-winbind.c
--- dovecot-1.0.1/src/auth/mech-winbind.c 1970-01-01 03:00:00.000000000 +0300
+++ dovecot-1.0.1-OK/src/auth/mech-winbind.c 2007-06-29 17:34:33.000000000 +0400
@@ -0,0 +1,269 @@
+/*
+ * NTLM and Negotiate authentication mechanisms,
+ * using Samba winbind daemon
+ *
+ * Copyright (c) 2007 Dmitry Butskoy <[EMAIL PROTECTED]>
+ *
+ * This software is released under the MIT license.
+ */
+
+#include "common.h"
+#include "mech.h"
+#include "str.h"
+#include "buffer.h"
+#include "safe-memset.h"
+#include "base64.h"
+#include "istream.h"
+#include "ostream.h"
+
+#include <unistd.h>
+
+
+struct winbind_auth_request {
+ struct auth_request auth_request;
+
+ pool_t pool;
+
+ int continued;
+
+ int negotiate; /* 1 -- GSS-SPNEGO, 0 -- NTLM */
+
+};
+
+
+static struct istream *in_pipe = NULL;
+static struct ostream *out_pipe = NULL;
+
+
+static void init_helper (int negotiate) {
+ int infd[2], outfd[2];
+
+ if (in_pipe) i_stream_destroy (&in_pipe);
+ if (out_pipe) o_stream_destroy (&out_pipe);
+
+
+ if (pipe (infd) < 0 || pipe (outfd) < 0)
+ i_fatal ("pipe creation failed");
+
+ if (fork () == 0) { /* child */
+ char *args[3];
+
+ close (infd[0]);
+ close (outfd[1]);
+
+ dup2 (outfd[0], 0);
+ dup2 (infd[1], 1);
+
+ args[0] = "/usr/bin/ntlm_auth";
+ if (negotiate)
+ args[1] = "--helper-protocol=gss-spnego";
+ else
+ args[1] = "--helper-protocol=squid-2.5-ntlmssp";
+ args[2] = NULL;
+
+ execv (args[0], args);
+ i_fatal ("execv of %s failed", args[0]);
+ _exit (127); /* paranoia */
+ }
+
+ /* parent */
+
+ in_pipe = i_stream_create_file (infd[0], default_pool,
+ AUTH_CLIENT_MAX_LINE_LENGTH, FALSE);
+ out_pipe = o_stream_create_file (outfd[1], default_pool,
+ (size_t) -1, FALSE);
+
+ return;
+}
+
+
+static void mech_winbind_auth_continue(struct auth_request *auth_request,
+ const unsigned char *data, size_t data_size) {
+ struct winbind_auth_request *request =
+ (struct winbind_auth_request *) auth_request;
+ string_t *str;
+ char *answer;
+ const char **token;
+
+ str = t_str_new (MAX_BASE64_ENCODED_SIZE (data_size + 1) + 4);
+
+ str_printfa (str, "%s ", request->continued ? "KK" : "YR");
+ base64_encode (data, data_size, str);
+ str_append_c (str, '\n');
+
+ if (o_stream_send_str (out_pipe, str_c (str)) < 0 ||
+ o_stream_flush (out_pipe) < 0
+ ) {
+ auth_request_log_info (auth_request, "winbind",
+ "cannot write to helper pipe");
+ goto restart;
+ }
+
+ request->continued = 0;
+
+
+ answer = i_stream_read_next_line (in_pipe);
+ if (!answer) {
+ auth_request_log_info (auth_request, "winbind",
+ "cannot read from helper pipe");
+ goto restart;
+ }
+
+ token = t_strsplit_spaces (answer, " \n");
+ if (!token || !token[0] ||
+ (!token[1] && strcmp (token[0], "BH") != 0) ||
+ (request->negotiate && !token[2])
+ ) {
+ auth_request_log_info (auth_request, "winbind",
+ "could not parse `%s' helper callback", answer);
+ goto restart;
+ }
+
+ /*
+ * NTLM:
+ * The child's reply contains 2 parts:
+ * - The code: TT, AF or NA
+ * - The argument:
+ * For TT it's the blob to send to the client, coded in base64
+ * For AF it's user or DOMAIN\user
+ * For NA it's the NT error code
+ *
+ * GSS-SPNEGO:
+ * The child's reply contains 3 parts:
+ * - The code: TT, AF or NA
+ * - The blob to send to the client, coded in base64
+ * - The argument:
+ * For TT it's a dummy '*'
+ * For AF it's DOMAIN\user
+ * For NA it's the NT error code
+ */
+
+ if (!strcmp (token[0], "TT")) {
+ buffer_t *buf;
+ size_t len = strlen (token[1]);
+
+ buf = buffer_create_dynamic (pool_datastack_create(),
+ MAX_BASE64_DECODED_SIZE (len));
+ base64_decode (token[1], len, NULL, buf);
+
+ auth_request->callback (auth_request,
+ AUTH_CLIENT_RESULT_CONTINUE,
+ buf->data, buf->used);
+ request->continued = 1;
+ return;
+ }
+ else if (!strcmp (token[0], "NA")) {
+ const char *error = request->negotiate ? token[2] : token[1];
+
+ auth_request_log_info (auth_request, "winbind",
+ "user not authenticated: %s", error);
+
+ auth_request_fail (auth_request);
+ return;
+ }
+ else if (!strcmp (token[0], "AF")) {
+ const char *user, *p;
+
+ user = request->negotiate ? token[2] : token[1];
+ p = strchr (user, '\\'); /* Hmmm... */
+ if (p) user = ++p;
+
+ request->auth_request.user =
+ p_strdup (request->auth_request.pool, user);
+
+ if (request->negotiate && strcmp (token[1], "*") != 0) {
+ buffer_t *buf;
+ size_t len = strlen (token[1]);
+
+ buf = buffer_create_dynamic (pool_datastack_create(),
+ MAX_BASE64_DECODED_SIZE (len));
+ base64_decode (token[1], len, NULL, buf);
+
+ auth_request_success (&request->auth_request,
+ buf->data, buf->used);
+ } else
+ auth_request_success (&request->auth_request, NULL, 0);
+
+ return;
+ }
+ else if (!strcmp (token[0], "BH")) {
+ auth_request_log_info (auth_request, "winbind",
+ "ntlm_auth reports broken helper: %s",
+ token[1] ? token[1] : "");
+ auth_request_fail (auth_request);
+ return;
+ }
+ else {
+ auth_request_log_info (auth_request, "winbind",
+ "could not parse `%s' helper callback", answer);
+ auth_request_fail (auth_request);
+ return;
+ }
+
+
+restart:
+ init_helper (request->negotiate); /* try to restart */
+
+ auth_request_fail (auth_request);
+ return;
+}
+
+
+static struct auth_request *do_auth_new (int negotiate) {
+ struct winbind_auth_request *request;
+ pool_t pool;
+
+ pool = pool_alloconly_create("winbind_auth_request", 1024);
+ request = p_new(pool, struct winbind_auth_request, 1);
+ request->pool = pool;
+
+ request->negotiate = negotiate;
+
+ request->continued = 0;
+
+ if (!in_pipe || !out_pipe)
+ init_helper (negotiate);
+
+ request->auth_request.pool = pool;
+ return &request->auth_request;
+}
+
+static struct auth_request *mech_winbind_ntlm_auth_new (void) {
+
+ return do_auth_new (0);
+}
+
+static struct auth_request *mech_winbind_spnego_auth_new (void) {
+
+ return do_auth_new (1);
+}
+
+
+const struct mech_module mech_winbind_ntlm = {
+ "NTLM",
+
+ MEMBER(flags) MECH_SEC_DICTIONARY | MECH_SEC_ACTIVE,
+
+ MEMBER(passdb_need_plain) FALSE,
+ MEMBER(passdb_need_credentials) FALSE,
+
+ mech_winbind_ntlm_auth_new,
+ mech_generic_auth_initial,
+ mech_winbind_auth_continue,
+ mech_generic_auth_free
+};
+
+const struct mech_module mech_winbind_spnego = {
+ "GSS-SPNEGO",
+
+ MEMBER(flags) 0,
+
+ MEMBER(passdb_need_plain) FALSE,
+ MEMBER(passdb_need_credentials) FALSE,
+
+ mech_winbind_spnego_auth_new,
+ mech_generic_auth_initial,
+ mech_winbind_auth_continue,
+ mech_generic_auth_free
+};
+
diff -Nru dovecot-1.0.1/src/auth/mech.c dovecot-1.0.1-OK/src/auth/mech.c
--- dovecot-1.0.1/src/auth/mech.c 2007-05-19 15:14:04.000000000 +0400
+++ dovecot-1.0.1-OK/src/auth/mech.c 2007-06-29 17:34:33.000000000 +0400
@@ -73,6 +73,8 @@
#ifdef HAVE_GSSAPI
extern struct mech_module mech_gssapi;
#endif
+extern struct mech_module mech_winbind_ntlm;
+extern struct mech_module mech_winbind_spnego;
void mech_init(void)
{
@@ -81,12 +83,16 @@
mech_register_module(&mech_apop);
mech_register_module(&mech_cram_md5);
mech_register_module(&mech_digest_md5);
+ if (getenv("NTLM_USE_WINBIND") != NULL)
+ mech_register_module(&mech_winbind_ntlm);
+ else
mech_register_module(&mech_ntlm);
mech_register_module(&mech_rpa);
mech_register_module(&mech_anonymous);
#ifdef HAVE_GSSAPI
mech_register_module(&mech_gssapi);
#endif
+ mech_register_module(&mech_winbind_spnego);
}
void mech_deinit(void)
@@ -96,10 +102,14 @@
mech_unregister_module(&mech_apop);
mech_unregister_module(&mech_cram_md5);
mech_unregister_module(&mech_digest_md5);
+ if (getenv("NTLM_USE_WINBIND") != NULL)
+ mech_unregister_module(&mech_winbind_ntlm);
+ else
mech_unregister_module(&mech_ntlm);
mech_unregister_module(&mech_rpa);
mech_unregister_module(&mech_anonymous);
#ifdef HAVE_GSSAPI
mech_unregister_module(&mech_gssapi);
#endif
+ mech_unregister_module(&mech_winbind_spnego);
}
diff -Nru dovecot-1.0.1/src/master/auth-process.c dovecot-1.0.1-OK/src/master/auth-process.c
--- dovecot-1.0.1/src/master/auth-process.c 2007-06-12 20:43:46.000000000 +0400
+++ dovecot-1.0.1-OK/src/master/auth-process.c 2007-06-29 17:34:33.000000000 +0400
@@ -474,6 +474,8 @@
env_put("SSL_REQUIRE_CLIENT_CERT=1");
if (set->ssl_username_from_cert)
env_put("SSL_USERNAME_FROM_CERT=1");
+ if (set->ntlm_use_winbind)
+ env_put("NTLM_USE_WINBIND=1");
if (*set->krb5_keytab != '\0') {
/* Environment used by Kerberos 5 library directly */
env_put(t_strconcat("KRB5_KTNAME=", set->krb5_keytab, NULL));
diff -Nru dovecot-1.0.1/src/master/master-settings.c dovecot-1.0.1-OK/src/master/master-settings.c
--- dovecot-1.0.1/src/master/master-settings.c 2007-06-12 20:43:06.000000000 +0400
+++ dovecot-1.0.1-OK/src/master/master-settings.c 2007-06-29 17:34:33.000000000 +0400
@@ -78,6 +78,7 @@
DEF(SET_BOOL, debug_passwords),
DEF(SET_BOOL, ssl_require_client_cert),
DEF(SET_BOOL, ssl_username_from_cert),
+ DEF(SET_BOOL, ntlm_use_winbind),
DEF(SET_INT, count),
DEF(SET_INT, worker_max_count),
@@ -297,6 +298,7 @@
MEMBER(debug_passwords) FALSE,
MEMBER(ssl_require_client_cert) FALSE,
MEMBER(ssl_username_from_cert) FALSE,
+ MEMBER(ntlm_use_winbind) FALSE,
MEMBER(count) 1,
MEMBER(worker_max_count) 30,
diff -Nru dovecot-1.0.1/src/master/master-settings.h dovecot-1.0.1-OK/src/master/master-settings.h
--- dovecot-1.0.1/src/master/master-settings.h 2007-06-12 20:42:52.000000000 +0400
+++ dovecot-1.0.1-OK/src/master/master-settings.h 2007-06-29 17:34:33.000000000 +0400
@@ -195,6 +195,7 @@
bool verbose, debug, debug_passwords;
bool ssl_require_client_cert;
bool ssl_username_from_cert;
+ bool ntlm_use_winbind;
unsigned int count;
unsigned int worker_max_count;