Module methods are mod_foo
[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 mod_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 mod_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         if (!krb5_is_thread_safe()) {
86 /*
87  *      rlm_krb5 was built as threadsafe
88  */
89 #ifdef KRB5_IS_THREAD_SAFE
90                 ERROR("Build time libkrb5 was threadsafe, but run time library claims not to be");
91                 ERROR("Modify runtime linker path (LD_LIBRARY_PATH on most systems), to prefer threadsafe libkrb5");
92                 return -1;
93 /*
94  *      rlm_krb5 was not built as threadsafe
95  */
96 #else
97                 WDEBUG("libkrb5 is not threadsafe, recompile it with thread support enabled ("
98 #  ifdef HEIMDAL_KRB5
99                        "--enable-pthread-support"
100 #  else
101                        "--disable-thread-support=no"
102 #  endif
103                        ")");
104                 WDEBUG("rlm_krb5 will run in single threaded mode, performance may be degraded");
105         } else {
106                 WDEBUG("Build time libkrb5 was not threadsafe, but run time library claims to be");
107                 WDEBUG("Reconfigure and recompile rlm_krb5 to enable thread support");
108 #endif
109         }
110
111         inst->xlat_name = cf_section_name2(conf);
112         if (!inst->xlat_name) {
113                 inst->xlat_name = cf_section_name1(conf);
114         }
115
116         ret = krb5_init_context(&inst->context);
117         if (ret) {
118                 ERROR("rlm_krb5 (%s): context initialisation failed: %s", inst->xlat_name,
119                       rlm_krb5_error(NULL, ret));
120
121                 return -1;
122         }
123
124         /*
125          *      Split service principal into service and host components
126          *      they're needed to build the server principal in MIT,
127          *      and to set the validation service in Heimdal.
128          */
129         if (inst->service_princ) {
130                 size_t len;
131                 /* Service principal appears to contain a host component */
132                 inst->hostname = strchr(inst->service_princ, '/');
133                 if (inst->hostname) {
134                         len = (inst->hostname - inst->service_princ);
135                         inst->hostname++;
136                 } else {
137                         len = strlen(inst->service_princ);
138                 }
139
140                 if (len) {
141                         inst->service = talloc_array(inst, char, (len + 1));
142                         strlcpy(inst->service, inst->service_princ, len + 1);
143                 }
144         }
145
146 #ifdef HEIMDAL_KRB5
147         if (inst->hostname) {
148                 DEBUG("rlm_krb5 (%s): Ignoring hostname component of service principal \"%s\", not "
149                       "needed/supported by Heimdal", inst->xlat_name, inst->hostname);
150         }
151 #else
152
153         /*
154          *      Convert the service principal string to a krb5 principal.
155          */
156         ret = krb5_sname_to_principal(inst->context, inst->hostname, inst->service, KRB5_NT_SRV_HST, &(inst->server));
157         if (ret) {
158                 ERROR("rlm_krb5 (%s): Failed parsing service principal: %s", inst->xlat_name,
159                       rlm_krb5_error(inst->context, ret));
160
161                 return -1;
162         }
163
164         ret = krb5_unparse_name(inst->context, inst->server, &princ_name);
165         if (ret) {
166                 /* Uh? */
167                 ERROR("rlm_krb5 (%s): Failed constructing service principal string: %s", inst->xlat_name,
168                       rlm_krb5_error(inst->context, ret));
169
170                 return -1;
171         }
172
173         /*
174          *      Not necessarily the same as the config item
175          */
176         DEBUG("rlm_krb5 (%s): Using service principal \"%s\"", inst->xlat_name, princ_name);
177
178         krb5_free_unparsed_name(inst->context, princ_name);
179
180         /*
181          *      Setup options for getting credentials and verifying them
182          */
183
184         /* For some reason the 'init' version of this function is deprecated */
185         ret = krb5_get_init_creds_opt_alloc(inst->context, &(inst->gic_options));
186         if (ret) {
187                 ERROR("rlm_krb5 (%s): Couldn't allocated inital credential options: %s", inst->xlat_name,
188                       rlm_krb5_error(inst->context, ret));
189
190                 return -1;
191         }
192
193         /*
194          *      Perform basic checks on the keytab
195          */
196         ret = inst->keytabname ?
197                 krb5_kt_resolve(inst->context, inst->keytabname, &keytab) :
198                 krb5_kt_default(inst->context, &keytab);
199         if (ret) {
200                 ERROR("rlm_krb5 (%s): Resolving keytab failed: %s", inst->xlat_name,
201                       rlm_krb5_error(inst->context, ret));
202
203                 return -1;
204         }
205
206         ret = krb5_kt_get_name(inst->context, keytab, keytab_name, sizeof(keytab_name));
207         krb5_kt_close(inst->context, keytab);
208         if (ret) {
209                 ERROR("rlm_krb5 (%s): Can't retrieve keytab name: %s", inst->xlat_name,
210                       rlm_krb5_error(inst->context, ret));
211
212                 return -1;
213         }
214
215         DEBUG("rlm_krb5 (%s): Using keytab \"%s\"", inst->xlat_name, keytab_name);
216
217         MEM(inst->vic_options = talloc_zero(inst, krb5_verify_init_creds_opt));
218         krb5_verify_init_creds_opt_init(inst->vic_options);
219 #endif
220
221 #ifdef KRB5_IS_THREAD_SAFE
222         /*
223          *      Initialize the socket pool.
224          */
225         inst->pool = fr_connection_pool_init(conf, inst, mod_conn_create, NULL, mod_conn_delete, NULL);
226         if (!inst->pool) {
227                 return -1;
228         }
229 #else
230         inst->conn = mod_conn_create(inst);
231         if (!inst->conn) {
232                 return -1;
233         }
234 #endif
235         return 0;
236 }
237
238 /** Common function for transforming a User-Name string into a principal.
239  *
240  * @param[out] client Where to write the client principal.
241  * @param[in] request Current request.
242  * @param[in] context Kerberos context.
243  */
244 static rlm_rcode_t krb5_parse_user(krb5_principal *client, REQUEST *request, krb5_context context)
245 {
246         krb5_error_code ret;
247         char *princ_name;
248
249         /*
250          *      We can only authenticate user requests which HAVE
251          *      a User-Name attribute.
252          */
253         if (!request->username) {
254                 REDEBUG("Attribute \"User-Name\" is required for authentication");
255
256                 return RLM_MODULE_INVALID;
257         }
258
259         /*
260          *      We can only authenticate user requests which HAVE
261          *      a User-Password attribute.
262          */
263         if (!request->password) {
264                 REDEBUG("Attribute \"User-Password\" is required for authentication");
265
266                 return RLM_MODULE_INVALID;
267         }
268
269         /*
270          *      Ensure that we're being passed a plain-text password,
271          *      and not anything else.
272          */
273         if (request->password->da->attr != PW_USER_PASSWORD) {
274                 REDEBUG("Attribute \"User-Password\" is required for authentication.  Cannot use \"%s\".",
275                         request->password->da->name);
276
277                 return RLM_MODULE_INVALID;
278         }
279
280         ret = krb5_parse_name(context, request->username->vp_strvalue, client);
281         if (ret) {
282                 REDEBUG("Failed parsing username as principal: %s", rlm_krb5_error(context, ret));
283
284                 return RLM_MODULE_FAIL;
285         }
286
287         krb5_unparse_name(context, *client, &princ_name);
288         RDEBUG("Using client principal \"%s\"", princ_name);
289 #ifdef HEIMDAL_KRB5
290         free(princ_name);
291 #else
292         krb5_free_unparsed_name(context, princ_name);
293 #endif
294         return RLM_MODULE_OK;
295 }
296
297 /** Log error message and return appropriate rcode
298  *
299  * Translate kerberos error codes into return codes.
300  * @param request Current request.
301  * @param ret code from kerberos.
302  * @param conn used in the last operation.
303  */
304 static rlm_rcode_t krb5_process_error(REQUEST *request, rlm_krb5_handle_t *conn, int ret)
305 {
306         rad_assert(ret != 0);
307         rad_assert(conn);       /* Silences warnings */
308
309         switch (ret) {
310         case KRB5_LIBOS_BADPWDMATCH:
311         case KRB5KRB_AP_ERR_BAD_INTEGRITY:
312                 REDEBUG("Provided password was incorrect (%i): %s", ret, rlm_krb5_error(conn->context, ret));
313                 return RLM_MODULE_REJECT;
314
315         case KRB5KDC_ERR_KEY_EXP:
316         case KRB5KDC_ERR_CLIENT_REVOKED:
317         case KRB5KDC_ERR_SERVICE_REVOKED:
318                 REDEBUG("Account has been locked out (%i): %s", ret, rlm_krb5_error(conn->context, ret));
319                 return RLM_MODULE_USERLOCK;
320
321         case KRB5KDC_ERR_C_PRINCIPAL_UNKNOWN:
322                 RDEBUG("User not found (%i): %s", ret, rlm_krb5_error(conn->context, ret));
323                 return RLM_MODULE_NOTFOUND;
324
325         default:
326                 REDEBUG("Error verifying credentials (%i): %s", ret, rlm_krb5_error(conn->context, ret));
327                 return RLM_MODULE_FAIL;
328         }
329 }
330
331 #ifdef HEIMDAL_KRB5
332
333 /*
334  *      Validate user/pass (Heimdal)
335  */
336 static rlm_rcode_t mod_authenticate(void *instance, REQUEST *request)
337 {
338         rlm_krb5_t *inst = instance;
339         rlm_rcode_t rcode;
340         krb5_error_code ret;
341
342         rlm_krb5_handle_t *conn;
343
344         krb5_principal client;
345
346 #ifdef KRB5_IS_THREAD_SAFE
347         conn = fr_connection_get(inst->pool);
348         if (!conn) {
349                 REDEBUG("All krb5 contexts are in use");
350
351                 return RLM_MODULE_FAIL;
352         }
353 #else
354         conn = inst->conn;
355 #endif
356
357         /*
358          *      Zero out local storage
359          */
360         memset(&client, 0, sizeof(client));
361
362         rcode = krb5_parse_user(&client, request, conn->context);
363         if (rcode != RLM_MODULE_OK) goto cleanup;
364
365         /*
366          *      Verify the user, using the options we set in instantiate
367          */
368         ret = krb5_verify_user_opt(conn->context, client, request->password->vp_strvalue, &conn->options);
369         if (ret) {
370                 rcode = krb5_process_error(request, conn, ret);
371                 goto cleanup;
372         }
373
374         /*
375          *      krb5_verify_user_opt adds the credentials to the ccache
376          *      we specified with krb5_verify_opt_set_ccache.
377          *
378          *      To make sure we don't accumulate thousands of sets of
379          *      credentials, remove them again here.
380          *
381          * @todo This should definitely be optional, which means writing code for the MIT
382          *       variant as well.
383          */
384         {
385                 krb5_cc_cursor cursor;
386                 krb5_creds cred;
387
388                 krb5_cc_start_seq_get(conn->context, conn->ccache, &cursor);
389                 for ((ret = krb5_cc_next_cred(conn->context, conn->ccache, &cursor, &cred));
390                      ret == 0;
391                      (ret = krb5_cc_next_cred(conn->context, conn->ccache, &cursor, &cred))) {
392                      krb5_cc_remove_cred(conn->context, conn->ccache, 0, &cred);
393                 }
394                 krb5_cc_end_seq_get(conn->context, conn->ccache, &cursor);
395         }
396
397 cleanup:
398         if (client) {
399                 krb5_free_principal(conn->context, client);
400         }
401
402 #ifdef KRB5_IS_THREAD_SAFE
403         fr_connection_release(inst->pool, conn);
404 #endif
405         return rcode;
406 }
407
408 #else  /* HEIMDAL_KRB5 */
409
410 /*
411  *  Validate userid/passwd (MIT)
412  */
413 static rlm_rcode_t mod_authenticate(void *instance, REQUEST *request)
414 {
415         rlm_krb5_t *inst = instance;
416         rlm_rcode_t rcode;
417         krb5_error_code ret;
418
419         rlm_krb5_handle_t *conn;
420
421         krb5_principal client;
422         krb5_creds init_creds;
423         char *password;         /* compiler warnings */
424
425         rad_assert(inst->context);
426
427 #ifdef KRB5_IS_THREAD_SAFE
428         conn = fr_connection_get(inst->pool);
429         if (!conn) {
430                 REDEBUG("All krb5 contexts are in use");
431
432                 return RLM_MODULE_FAIL;
433         }
434 #else
435         conn = inst->conn;
436 #endif
437
438         /*
439          *      Zero out local storage
440          */
441         memset(&client, 0, sizeof(client));
442         memset(&init_creds, 0, sizeof(init_creds));
443
444         /*
445          *      Check we have all the required VPs, and convert the username
446          *      into a principal.
447          */
448         rcode = krb5_parse_user(&client, request, conn->context);
449         if (rcode != RLM_MODULE_OK) goto cleanup;
450
451         /*
452          *      Retrieve the TGT from the TGS/KDC and check we can decrypt it.
453          */
454         memcpy(&password, &request->password->vp_strvalue, sizeof(password));
455         RDEBUG("Retrieving and decrypting TGT");
456         ret = krb5_get_init_creds_password(conn->context, &init_creds, client, password,
457                                            NULL, NULL, 0, NULL, inst->gic_options);
458         if (ret) {
459                 rcode = krb5_process_error(request, conn, ret);
460         }
461
462         RDEBUG("Attempting to authenticate against service principal");
463         ret = krb5_verify_init_creds(conn->context, &init_creds, inst->server, conn->keytab, NULL, inst->vic_options);
464         if (ret) {
465                 rcode = krb5_process_error(request, conn, ret);
466         }
467
468 cleanup:
469         if (client) {
470                 krb5_free_principal(conn->context, client);
471         }
472         krb5_free_cred_contents(conn->context, &init_creds);
473
474 #ifdef KRB5_IS_THREAD_SAFE
475         fr_connection_release(inst->pool, conn);
476 #endif
477         return rcode;
478 }
479
480 #endif /* MIT_KRB5 */
481
482 module_t rlm_krb5 = {
483         RLM_MODULE_INIT,
484         "krb5",
485         RLM_TYPE_HUP_SAFE
486 #ifdef KRB5_IS_THREAD_SAFE
487         | RLM_TYPE_THREAD_SAFE
488 #endif
489         ,
490         sizeof(rlm_krb5_t),
491         module_config,
492         mod_instantiate,                /* instantiation */
493         mod_detach,                     /* detach */
494         {
495                 mod_authenticate,       /* authenticate */
496                 NULL,                   /* authorize */
497                 NULL,                   /* pre-accounting */
498                 NULL,                   /* accounting */
499                 NULL,                   /* checksimul */
500                 NULL,                   /* pre-proxy */
501                 NULL,                   /* post-proxy */
502                 NULL                    /* post-auth */
503         },
504 };