Add connection pool support to rlm_krb5
[freeradius.git] / src / modules / rlm_krb5 / rlm_krb5.c
1 /*
2  *   This program is free software; you can redistribute it and/or modify
3  *   it under the terms of the GNU General Public License as published by
4  *   the Free Software Foundation; either version 2 of the License, or
5  *   (at your option) any later version.
6  *
7  *   This program is distributed in the hope that it will be useful,
8  *   but WITHOUT ANY WARRANTY; without even the implied warranty of
9  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
10  *   GNU General Public License for more details.
11  *
12  *   You should have received a copy of the GNU General Public License
13  *   along with this program; if not, write to the Free Software
14  *   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
15  */
16
17 /**
18  * $Id$
19  * @file rlm_krb5.c
20  * @brief Authenticate users, retrieving their TGT from a Kerberos V5 TDC.
21  *
22  * @copyright 2000,2006,2012-2013  The FreeRADIUS server project
23  * @copyright 2013  Arran Cudbard-Bell <a.cudbardb@freeradius.org>
24  * @copyright 2000  Nathan Neulinger <nneul@umr.edu>
25  * @copyright 2000  Alan DeKok <aland@ox.org>
26  */
27 RCSID("$Id$")
28
29 #include <freeradius-devel/radiusd.h>
30 #include <freeradius-devel/modules.h>
31 #include <freeradius-devel/rad_assert.h>
32 #include "krb5.h"
33
34 static const CONF_PARSER module_config[] = {
35         { "keytab", PW_TYPE_STRING_PTR, offsetof(rlm_krb5_t, keytabname), NULL, NULL },
36         { "service_principal", PW_TYPE_STRING_PTR, offsetof(rlm_krb5_t,service_princ), NULL, NULL },
37         { NULL, -1, 0, NULL, NULL }
38 };
39
40 static int krb5_detach(void *instance)
41 {
42         rlm_krb5_t *inst = instance;
43
44 #ifndef HEIMDAL_KRB5
45         talloc_free(inst->vic_options);
46
47         if (inst->gic_options) {
48                 krb5_get_init_creds_opt_free(inst->context, inst->gic_options);
49         }
50
51         if (inst->server) {
52                 krb5_free_principal(inst->context, inst->server);
53         }
54 #endif
55
56         /* Don't free hostname, it's just a pointer into service_princ */
57         talloc_free(inst->service);
58
59         if (inst->context) {
60                 krb5_free_context(inst->context);
61         }
62 #ifdef KRB5_IS_THREAD_SAFE
63         fr_connection_pool_delete(inst->pool);
64 #endif
65
66         return 0;
67 }
68
69 static int krb5_instantiate(CONF_SECTION *conf, void *instance)
70 {
71         rlm_krb5_t *inst = instance;
72         krb5_error_code ret;
73 #ifndef HEIMDAL_KRB5
74         krb5_keytab keytab;
75         char keytab_name[200];
76         char *princ_name;
77 #endif
78
79 #ifdef HEIMDAL_KRB5
80         DEBUG("Using Heimdal Kerberos library");
81 #else
82         DEBUG("Using MIT Kerberos library");
83 #endif
84
85 #ifndef KRB5_IS_THREAD_SAFE
86         if (!krb5_is_thread_safe()) {
87                 DEBUGI("libkrb5 is not threadsafe, recompile it, and the server with thread support enabled");
88                 WDEBUG("rlm_krb5 will run in single threaded mode, performance may be degraded");
89         } else {
90                 WDEBUG("Build time libkrb5 was not threadsafe, but run time library claims to be");
91                 WDEBUG("Reconfigure and recompile rlm_krb5 to enable thread support");
92         }
93 #endif
94         inst->xlat_name = cf_section_name2(conf);
95         if (!inst->xlat_name) {
96                 inst->xlat_name = cf_section_name1(conf);
97         }
98
99         ret = krb5_init_context(&inst->context);
100         if (ret) {
101                 ERROR("rlm_krb5 (%s): context initialisation failed: %s", inst->xlat_name,
102                       rlm_krb5_error(NULL, ret));
103
104                 return -1;
105         }
106
107         /*
108          *      Split service principal into service and host components
109          *      they're needed to build the server principal in MIT,
110          *      and to set the validation service in Heimdal.
111          */
112         if (inst->service_princ) {
113                 size_t len;
114                 /* Service principal appears to contain a host component */
115                 inst->hostname = strchr(inst->service_princ, '/');
116                 if (inst->hostname) {
117                         len = (inst->hostname - inst->service_princ);
118                         inst->hostname++;
119                 } else {
120                         len = strlen(inst->service_princ);
121                 }
122
123                 if (len) {
124                         inst->service = talloc_array(inst, char, (len + 1));
125                         strlcpy(inst->service, inst->service_princ, len + 1);
126                 }
127         }
128
129 #ifdef HEIMDAL_KRB5
130         if (inst->hostname) {
131                 DEBUG("rlm_krb5 (%s): Ignoring hostname component of service principal \"%s\", not "
132                       "needed/supported by Heimdal", inst->xlat_name, inst->hostname);
133         }
134 #else
135
136         /*
137          *      Convert the service principal string to a krb5 principal.
138          */
139         ret = krb5_sname_to_principal(inst->context, inst->hostname, inst->service, KRB5_NT_SRV_HST, &(inst->server));
140         if (ret) {
141                 ERROR("rlm_krb5 (%s): Failed parsing service principal: %s", inst->xlat_name,
142                       rlm_krb5_error(inst->context, ret));
143
144                 return -1;
145         }
146
147         ret = krb5_unparse_name(inst->context, inst->server, &princ_name);
148         if (ret) {
149                 /* Uh? */
150                 ERROR("rlm_krb5 (%s): Failed constructing service principal string: %s", inst->xlat_name,
151                       rlm_krb5_error(inst->context, ret));
152
153                 return -1;
154         }
155
156         /*
157          *      Not necessarily the same as the config item
158          */
159         DEBUG("rlm_krb5 (%s): Using service principal \"%s\"", inst->xlat_name, princ_name);
160
161         krb5_free_unparsed_name(inst->context, princ_name);
162
163         /*
164          *      Setup options for getting credentials and verifying them
165          */
166
167         /* For some reason the 'init' version of this function is deprecated */
168         ret = krb5_get_init_creds_opt_alloc(inst->context, &(inst->gic_options));
169         if (ret) {
170                 ERROR("rlm_krb5 (%s): Couldn't allocated inital credential options: %s", inst->xlat_name,
171                       rlm_krb5_error(inst->context, ret));
172
173                 return -1;
174         }
175
176         /*
177          *      Perform basic checks on the keytab
178          */
179         ret = inst->keytabname ?
180                 krb5_kt_resolve(inst->context, inst->keytabname, &keytab) :
181                 krb5_kt_default(inst->context, &keytab);
182         if (ret) {
183                 ERROR("rlm_krb5 (%s): Resolving keytab failed: %s", inst->xlat_name,
184                       rlm_krb5_error(inst->context, ret));
185
186                 return -1;
187         }
188
189         ret = krb5_kt_get_name(inst->context, keytab, keytab_name, sizeof(keytab_name));
190         krb5_kt_close(inst->context, keytab);
191         if (ret) {
192                 ERROR("rlm_krb5 (%s): Can't retrieve keytab name: %s", inst->xlat_name,
193                       rlm_krb5_error(inst->context, ret));
194
195                 return -1;
196         }
197
198         DEBUG("rlm_krb5 (%s): Using keytab \"%s\"", inst->xlat_name, keytab_name);
199
200         MEM(inst->vic_options = talloc_zero(inst, krb5_verify_init_creds_opt));
201         krb5_verify_init_creds_opt_init(inst->vic_options);
202 #endif
203
204 #ifdef KRB5_IS_THREAD_SAFE
205         /*
206          *      Initialize the socket pool.
207          */
208         inst->pool = fr_connection_pool_init(conf, inst, mod_conn_create, NULL, mod_conn_delete, NULL);
209         if (!inst->pool) {
210                 return -1;
211         }
212 #else
213         inst->conn = mod_conn_create(inst);
214         if (!inst->conn) {
215                 return -1;
216         }
217 #endif
218         return 0;
219 }
220
221 /** Common function for transforming a User-Name string into a principal.
222  *
223  * @param[out] client Where to write the client principal.
224  * @param[in] request Current request.
225  * @param[in] context Kerberos context.
226  */
227 static rlm_rcode_t krb5_parse_user(krb5_principal *client, REQUEST *request, krb5_context context)
228 {
229         krb5_error_code ret;
230         char *princ_name;
231
232         /*
233          *      We can only authenticate user requests which HAVE
234          *      a User-Name attribute.
235          */
236         if (!request->username) {
237                 REDEBUG("Attribute \"User-Name\" is required for authentication");
238
239                 return RLM_MODULE_INVALID;
240         }
241
242         /*
243          *      We can only authenticate user requests which HAVE
244          *      a User-Password attribute.
245          */
246         if (!request->password) {
247                 REDEBUG("Attribute \"User-Password\" is required for authentication");
248
249                 return RLM_MODULE_INVALID;
250         }
251
252         /*
253          *      Ensure that we're being passed a plain-text password,
254          *      and not anything else.
255          */
256         if (request->password->da->attr != PW_USER_PASSWORD) {
257                 REDEBUG("Attribute \"User-Password\" is required for authentication.  Cannot use \"%s\".",
258                         request->password->da->name);
259
260                 return RLM_MODULE_INVALID;
261         }
262
263         ret = krb5_parse_name(context, request->username->vp_strvalue, client);
264         if (ret) {
265                 REDEBUG("Failed parsing username as principal: %s", rlm_krb5_error(context, ret));
266
267                 return RLM_MODULE_FAIL;
268         }
269
270         krb5_unparse_name(context, *client, &princ_name);
271         RDEBUG("Using client principal \"%s\"", princ_name);
272 #ifdef HEIMDAL_KRB5
273         free(princ_name);
274 #else
275         krb5_free_unparsed_name(context, princ_name);
276 #endif
277         return RLM_MODULE_OK;
278 }
279
280 #ifdef HEIMDAL_KRB5
281
282 /*
283  *      Validate user/pass (Heimdal)
284  */
285 static rlm_rcode_t krb5_auth(void *instance, REQUEST *request)
286 {
287         rlm_krb5_t *inst = instance;
288         rlm_rcode_t rcode;
289         krb5_error_code ret;
290
291         rlm_krb5_handle_t *conn;
292
293         krb5_principal client;
294
295 #ifdef KRB5_IS_THREAD_SAFE
296         conn = fr_connection_get(inst->pool);
297         if (!conn) {
298                 REDEBUG("All krb5 contexts are in use");
299
300                 return RLM_MODULE_FAIL;
301         }
302 #else
303         conn = inst->conn;
304 #endif
305
306         /*
307          *      Zero out local storage
308          */
309         memset(&client, 0, sizeof(client));
310
311         rcode = krb5_parse_user(&client, request, conn->context);
312         if (rcode != RLM_MODULE_OK) goto cleanup;
313
314         /*
315          *      Verify the user, using the options we set in instantiate
316          */
317         ret = krb5_verify_user_opt(conn->context, client, request->password->vp_strvalue, &conn->options);
318         if (ret) {
319                 switch (ret) {
320                 case KRB5_LIBOS_BADPWDMATCH:
321                 case KRB5KRB_AP_ERR_BAD_INTEGRITY:
322                         REDEBUG("Provided password was incorrect (%i): %s", ret, rlm_krb5_error(conn->context, ret));
323                         rcode = RLM_MODULE_REJECT;
324                         break;
325
326                 case KRB5KDC_ERR_KEY_EXP:
327                 case KRB5KDC_ERR_CLIENT_REVOKED:
328                 case KRB5KDC_ERR_SERVICE_REVOKED:
329                         REDEBUG("Account has been locked out (%i): %s", ret, rlm_krb5_error(conn->context, ret));
330                         rcode = RLM_MODULE_USERLOCK;
331                         break;
332
333                 case KRB5KDC_ERR_C_PRINCIPAL_UNKNOWN:
334                         RDEBUG("User not found: %s (%i)", ret, rlm_krb5_error(conn->context, ret));
335                         rcode = RLM_MODULE_NOTFOUND;
336
337                 default:
338                         REDEBUG("Error verifying credentials (%i): %s", ret, rlm_krb5_error(conn->context, ret));
339                         rcode = RLM_MODULE_FAIL;
340                         break;
341                 }
342
343                 goto cleanup;
344         }
345
346         cleanup:
347         if (client) {
348                 krb5_free_principal(conn->context, client);
349         }
350
351 #ifdef KRB5_IS_THREAD_SAFE
352         fr_connection_release(inst->pool, conn);
353 #endif
354         return rcode;
355 }
356
357 #else  /* HEIMDAL_KRB5 */
358
359 /*
360  *  Validate userid/passwd (MIT)
361  */
362 static rlm_rcode_t krb5_auth(void *instance, REQUEST *request)
363 {
364         rlm_krb5_t *inst = instance;
365         rlm_rcode_t rcode;
366         krb5_error_code ret;
367
368         rlm_krb5_handle_t *conn;
369
370         krb5_principal client;
371         krb5_creds init_creds;
372         char *password;         /* compiler warnings */
373
374         rad_assert(inst->context);
375
376 #ifdef KRB5_IS_THREAD_SAFE
377         conn = fr_connection_get(inst->pool);
378         if (!conn) {
379                 REDEBUG("All krb5 contexts are in use");
380
381                 return RLM_MODULE_FAIL;
382         }
383 #else
384         conn = inst->conn;
385 #endif
386
387         /*
388          *      Zero out local storage
389          */
390         memset(&client, 0, sizeof(client));
391         memset(&init_creds, 0, sizeof(init_creds));
392
393         /*
394          *      Check we have all the required VPs, and convert the username
395          *      into a principal.
396          */
397         rcode = krb5_parse_user(&client, request, conn->context);
398         if (rcode != RLM_MODULE_OK) goto cleanup;
399
400         /*
401          *      Retrieve the TGT from the TGS/KDC and check we can decrypt it.
402          */
403         memcpy(&password, &request->password->vp_strvalue, sizeof(password));
404         ret = krb5_get_init_creds_password(conn->context, &init_creds, client, password,
405                                            NULL, NULL, 0, NULL, inst->gic_options);
406         if (ret) {
407                 error:
408                 switch (ret) {
409                 case KRB5_LIBOS_BADPWDMATCH:
410                 case KRB5KRB_AP_ERR_BAD_INTEGRITY:
411                         REDEBUG("Provided password was incorrect (%i): %s", ret, rlm_krb5_error(conn->context, ret));
412                         rcode = RLM_MODULE_REJECT;
413                         break;
414
415                 case KRB5KDC_ERR_KEY_EXP:
416                 case KRB5KDC_ERR_CLIENT_REVOKED:
417                 case KRB5KDC_ERR_SERVICE_REVOKED:
418                         REDEBUG("Account has been locked out (%i): %s", ret, rlm_krb5_error(conn->context, ret));
419                         rcode = RLM_MODULE_USERLOCK;
420                         break;
421
422                 case KRB5KDC_ERR_C_PRINCIPAL_UNKNOWN:
423                         REDEBUG("User not found (%i): %s", ret,  rlm_krb5_error(conn->context, ret));
424                         rcode = RLM_MODULE_NOTFOUND;
425                         break;
426
427                 default:
428                         REDEBUG("Error retrieving or verifying credentials (%i): %s", ret,
429                                 rlm_krb5_error(conn->context, ret));
430                         rcode = RLM_MODULE_FAIL;
431                         break;
432                 }
433
434                 goto cleanup;
435         }
436
437         RDEBUG("Successfully retrieved and decrypted TGT");
438
439         ret = krb5_verify_init_creds(conn->context, &init_creds, inst->server, conn->keytab, NULL, inst->vic_options);
440         if (ret) goto error;
441
442         cleanup:
443         if (client) {
444                 krb5_free_principal(conn->context, client);
445         }
446         krb5_free_cred_contents(conn->context, &init_creds);
447
448 #ifdef KRB5_IS_THREAD_SAFE
449         fr_connection_release(inst->pool, conn);
450 #endif
451         return rcode;
452 }
453
454 #endif /* MIT_KRB5 */
455
456 module_t rlm_krb5 = {
457         RLM_MODULE_INIT,
458         "krb5",
459         RLM_TYPE_CHECK_CONFIG_SAFE | RLM_TYPE_HUP_SAFE
460 #ifdef KRB5_IS_THREAD_SAFE
461         | RLM_TYPE_THREAD_SAFE
462 #endif
463         ,
464         sizeof(rlm_krb5_t),
465         module_config,
466         krb5_instantiate,               /* instantiation */
467         krb5_detach,                    /* detach */
468         {
469                 krb5_auth,              /* authenticate */
470                 NULL,                   /* authorize */
471                 NULL,                   /* pre-accounting */
472                 NULL,                   /* accounting */
473                 NULL,                   /* checksimul */
474                 NULL,                   /* pre-proxy */
475                 NULL,                   /* post-proxy */
476                 NULL                    /* post-auth */
477         },
478 };