diff options
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | Makefile.am | 22 | ||||
-rw-r--r-- | include/webauth/webkdc.h | 126 | ||||
-rw-r--r-- | lib/internal.h | 3 | ||||
-rw-r--r-- | lib/libwebauth.map | 2 | ||||
-rw-r--r-- | lib/libwebauth.sym | 2 | ||||
-rw-r--r-- | lib/webkdc-config.c | 46 | ||||
-rw-r--r-- | lib/webkdc-login.c | 879 | ||||
-rw-r--r-- | modules/webkdc/config.c | 49 | ||||
-rw-r--r-- | modules/webkdc/mod_webkdc.c | 676 | ||||
-rw-r--r-- | modules/webkdc/mod_webkdc.h | 2 | ||||
-rw-r--r-- | tests/TESTS | 1 | ||||
-rw-r--r-- | tests/data/xml/info/normal.xml | 15 | ||||
-rw-r--r-- | tests/data/xml/info/random.xml | 11 | ||||
-rw-r--r-- | tests/lib/webkdc-login-t.c | 593 |
15 files changed, 2054 insertions, 374 deletions
@@ -51,6 +51,7 @@ /tests/lib/token-encode-t /tests/lib/token-t /tests/lib/webkdc-info-t +/tests/lib/webkdc-login-t /tests/portable/asprintf-t /tests/portable/snprintf-t /tests/portable/mkstemp-t diff --git a/Makefile.am b/Makefile.am index 85c1267f..4ec8305a 100644 --- a/Makefile.am +++ b/Makefile.am @@ -77,10 +77,11 @@ webauthincludedir = $(includedir)/webauth webauthinclude_HEADERS = include/webauth/basic.h include/webauth/keys.h \ include/webauth/tokens.h include/webauth/util.h nodist_webauthinclude_HEADERS = include/webauth/defines.h -lib_libwebauth_la_SOURCES = lib/apr-buffer.c lib/attrs.c lib/base64.c \ - lib/context.c lib/factors.c lib/key.c lib/keys.c lib/krb5.c \ - lib/internal.h lib/misc.c lib/random.c lib/token-decode.c \ - lib/token-encode.c lib/token.c lib/userinfo.c lib/util.c lib/xml.c +lib_libwebauth_la_SOURCES = lib/apr-buffer.c lib/attrs.c lib/base64.c \ + lib/context.c lib/factors.c lib/key.c lib/keys.c lib/krb5.c \ + lib/internal.h lib/misc.c lib/random.c lib/token-decode.c \ + lib/token-encode.c lib/token.c lib/userinfo.c lib/util.c \ + lib/webkdc-config.c lib/webkdc-login.c lib/xml.c EXTRA_lib_libwebauth_la_SOURCES = lib/krb5-heimdal.c lib/krb5-mit.c lib_libwebauth_la_CPPFLAGS = $(AM_CPPFLAGS) $(APR_CPPFLAGS) \ $(APRUTIL_CPPFLAGS) $(REMCTL_CPPFLAGS) $(KRB5_CPPFLAGS) \ @@ -257,10 +258,10 @@ check_PROGRAMS = tests/runtests tests/lib/attrs-t tests/lib/base64-t \ tests/lib/key-t tests/lib/keys-t tests/lib/krb5-t \ tests/lib/krb5-tgt-t tests/lib/random-t tests/lib/token-decode-t \ tests/lib/token-encode-t tests/lib/token-t tests/lib/webkdc-info-t \ - tests/portable/asprintf-t tests/portable/mkstemp-t \ - tests/portable/snprintf-t tests/portable/strlcat-t \ - tests/portable/strlcpy-t tests/util/concat-t tests/util/messages-t \ - tests/util/xmalloc + tests/lib/webkdc-login-t tests/portable/asprintf-t \ + tests/portable/mkstemp-t tests/portable/snprintf-t \ + tests/portable/strlcat-t tests/portable/strlcpy-t \ + tests/util/concat-t tests/util/messages-t tests/util/xmalloc tests_runtests_CPPFLAGS = -DSOURCE='"$(abs_top_srcdir)/tests"' \ -DBUILD='"$(abs_top_builddir)/tests"' check_LIBRARIES = tests/tap/libtap.a @@ -294,6 +295,11 @@ tests_lib_webkdc_info_t_CPPFLAGS = -DPATH_REMCTLD='"$(PATH_REMCTLD)"' \ tests_lib_webkdc_info_t_LDFLAGS = $(APR_LDFLAGS) $(KRB5_LDFLAGS) tests_lib_webkdc_info_t_LDADD = tests/tap/libtap.a lib/libwebauth.la \ util/libutil.a portable/libportable.la $(APR_LIBS) $(KRB5_LIBS) +tests_lib_webkdc_login_t_CPPFLAGS = -DPATH_REMCTLD='"$(PATH_REMCTLD)"' \ + $(APR_CPPFLAGS) $(AM_CPPFLAGS) +tests_lib_webkdc_login_t_LDFLAGS = $(APR_LDFLAGS) $(KRB5_LDFLAGS) +tests_lib_webkdc_login_t_LDADD = tests/tap/libtap.a lib/libwebauth.la \ + util/libutil.a portable/libportable.la $(APR_LIBS) $(KRB5_LIBS) tests_portable_asprintf_t_SOURCES = tests/portable/asprintf-t.c \ tests/portable/asprintf.c tests_portable_asprintf_t_LDADD = tests/tap/libtap.a portable/libportable.la diff --git a/include/webauth/webkdc.h b/include/webauth/webkdc.h index 1de86c6a..3bb81588 100644 --- a/include/webauth/webkdc.h +++ b/include/webauth/webkdc.h @@ -35,9 +35,74 @@ #include <sys/types.h> +#include <webauth.h> + struct webauth_context; /* + * General configuration information for the WebKDC functions. The WebKDC + * Apache module gets this information from the Apache configuration and then + * passes it into the library via webauth_webkdc_config. + */ +struct webauth_webkdc_config { + const char *keytab_path; /* Path to WebKDC's Kerberos keytab. */ + const char *principal; /* WebKDC's Kerberos principal. */ + time_t proxy_lifetime; /* Maximum webkdc-proxy token lifetime (s). */ + WA_APR_ARRAY_HEADER_T *permitted_realms; /* Array of char * realms. */ + WA_APR_ARRAY_HEADER_T *local_realms; /* Array of char * realms. */ +}; + +/* + * Holds an encoded webkdc-proxy token along with some additional metadata + * about it that may be needed by consumers who can't decode the token (or + * don't want to). + */ +struct webauth_webkdc_proxy_data { + const char *type; + const char *token; +}; + +/* + * Input for a <requestTokenRequest>, which is sent from the WebLogin server + * to the WebKDC and represents a request by a user to authenticate to a WAS. + * This request may contain webkdc-proxy tokens, representing existing single + * sign-on credentials, and a login token, representing a username and + * authentication credential provided by the user in this session. + */ +struct webauth_webkdc_login_request { + struct webauth_token_webkdc_service *service; + WA_APR_ARRAY_HEADER_T *creds; /* Array of webauth_token pointers. */ + struct webauth_token_request *request; + const char *remote_user; + const char *local_ip; + const char *local_port; + const char *remote_ip; + const char *remote_port; +}; + +/* + * Result from a <requestTokenResponse>, which is sent by the WebKDC back to + * the WebLogin server containing the results of an authentication request. + * It was successful if the login_error is 0. + */ +struct webauth_webkdc_login_response { + int login_error; + const char *login_message; + WA_APR_ARRAY_HEADER_T *factors_wanted; /* Array of char * factors. */ + WA_APR_ARRAY_HEADER_T *factors_configured; /* Array of char * factors. */ + WA_APR_ARRAY_HEADER_T *proxies; /* Array of webkdc_proxy_data structs. */ + const char *return_url; + const char *requester; + const char *subject; + const char *result; /* Encrypted id or cred token. */ + const char *result_type; /* Type of result token as a string. */ + const char *login_cancel; /* Encrypted error token. */ + const void *app_state; + size_t app_state_len; + WA_APR_ARRAY_HEADER_T *logins; /* Array of struct webauth_login. */ +}; + +/* * Supported protocols for contacting the user metadata and multifactor * authentication services. Currently, only remctl is supported. */ @@ -64,6 +129,17 @@ struct webauth_user_config { }; /* + * Stores a single suspicious or questionable login, or a login that for some + * other reason the user should be notified about. Returned in an APR array + * in the webauth_userinfo struct. + */ +struct webauth_login { + const char *ip; + const char *hostname; + time_t timestamp; +}; + +/* * The webauth_userinfo struct and its supporting data structures stores * metadata about a user, returned from the site-local user management * middleware. @@ -76,17 +152,6 @@ struct webauth_user_info { WA_APR_ARRAY_HEADER_T *logins; /* Array of struct webauth_login. */ }; -/* - * Stores a single suspicious or questionable login, or a login that for some - * other reason the user should be notified about. Returned in an APR array - * in the webauth_userinfo struct. - */ -struct webauth_login { - const char *ip; - const char *hostname; - time_t timestamp; -}; - BEGIN_DECLS /* @@ -96,9 +161,8 @@ BEGIN_DECLS * Returns a status code, which will be WA_ERR_NONE unless invalid parameters * were passed. */ -int -webauth_user_config(struct webauth_context *ctx, struct webauth_user_config *) - __attribute__((__nonnull__(1, 2))); +int webauth_user_config(struct webauth_context *, struct webauth_user_config *) + __attribute__((__nonnull__)); /* * Obtain user information for a given user. The IP address of the user (as a @@ -115,9 +179,37 @@ webauth_user_config(struct webauth_context *ctx, struct webauth_user_config *) * allocated from pool memory and returns WA_ERR_NONE. On failure, returns an * error code and sets the info parameter to NULL. */ -int -webauth_user_info(struct webauth_context *, const char *user, const char *ip, - int, struct webauth_user_info **info) +int webauth_user_info(struct webauth_context *, const char *user, + const char *ip, int, struct webauth_user_info **info) + __attribute__((__nonnull__)); + +/* + * Configure the WebKDC services. Takes the context and the configuration + * information. The configuration information is stored in the WebAuth + * context and is used for all subsequent webauth_webkdc functions. Returns a + * status code, which will be WA_ERR_NONE unless invalid parameters were + * passed. + */ +int webauth_webkdc_config(struct webauth_context *, + struct webauth_webkdc_config *) + __attribute__((__nonnull__)); + +/* + * Given the data from a <requestTokenRequest> login attempt, process that + * attempted login and return the information for a <requestTokenResponse> in + * a newly-allocated struct from pool memory. All of the tokens included in + * the input and output are the unencrypted struct representations; the caller + * does the encryption or decryption and base64 conversion. + * + * Returns WA_ERR_NONE if the request was successfully processed, which + * doesn't mean it succeeded; see the login_code attribute of the struct for + * that. Returns an error code if we were unable to process the struct even + * to generate an error response. + */ +int webauth_webkdc_login(struct webauth_context *, + struct webauth_webkdc_login_request *, + struct webauth_webkdc_login_response **, + WEBAUTH_KEYRING *keyring) __attribute__((__nonnull__)); END_DECLS diff --git a/lib/internal.h b/lib/internal.h index 55d5a9a5..773e01fb 100644 --- a/lib/internal.h +++ b/lib/internal.h @@ -27,6 +27,9 @@ struct webauth_context { /* The below are used only for the WebKDC functions. */ + /* General WebKDC configuration. */ + struct webauth_webkdc_config *webkdc; + /* Configuration for contacting the user metadata service. */ struct webauth_user_config *user; }; diff --git a/lib/libwebauth.map b/lib/libwebauth.map index ec2d5146..4462d69b 100644 --- a/lib/libwebauth.map +++ b/lib/libwebauth.map @@ -83,6 +83,8 @@ WEBAUTH_3 { webauth_token_type_string; webauth_user_config; webauth_user_info; + webauth_webkdc_config; + webauth_webkdc_login; local: *; diff --git a/lib/libwebauth.sym b/lib/libwebauth.sym index 7ddb39c4..d68f3db1 100644 --- a/lib/libwebauth.sym +++ b/lib/libwebauth.sym @@ -81,3 +81,5 @@ webauth_token_type_code webauth_token_type_string webauth_user_config webauth_user_info +webauth_webkdc_config +webauth_webkdc_login diff --git a/lib/webkdc-config.c b/lib/webkdc-config.c new file mode 100644 index 00000000..75dba8b0 --- /dev/null +++ b/lib/webkdc-config.c @@ -0,0 +1,46 @@ +/* + * Interface for configuring the WebKDC portion of the library. + * + * Written by Russ Allbery <rra@stanford.edu> + * Copyright 2011 + * The Board of Trustees of the Leland Stanford Junior University + * + * See LICENSE for licensing terms. + */ + +#include <config.h> +#include <portable/system.h> + +#include <lib/internal.h> +#include <webauth/basic.h> +#include <webauth/webkdc.h> + + +/* + * Configure the WebKDC services. Takes the context and the configuration + * information. The configuration information is stored in the WebAuth + * context and is used for all subsequent webauth_webkdc functions. Returns a + * status code, which will be WA_ERR_NONE unless invalid parameters were + * passed. + */ +int +webauth_webkdc_config(struct webauth_context *ctx, + struct webauth_webkdc_config *config) +{ + int status; + + if (config->local_realms == NULL) { + status = WA_ERR_INVALID; + webauth_error_set(ctx, status, "local realms must be present"); + return status; + } + if (config->permitted_realms == NULL) { + status = WA_ERR_INVALID; + webauth_error_set(ctx, status, "permitted realms must be present"); + return status; + } + ctx->webkdc = config; + + /* FIXME: Add more error checking for consistency of configuration. */ + return WA_ERR_NONE; +} diff --git a/lib/webkdc-login.c b/lib/webkdc-login.c new file mode 100644 index 00000000..c687f681 --- /dev/null +++ b/lib/webkdc-login.c @@ -0,0 +1,879 @@ +/* + * WebKDC interface for processing a <requestTokenRequest>. + * + * These interfaces are used by the WebKDC implementation to process a + * <requestTokenRequest> from the WebLogin server, representing a user's + * attempt to authenticate to a WAS, either with proxy tokens or with a + * username and authentication credential, or both. + * + * Written by Russ Allbery <rra@stanford.edu> + * Copyright 2011 + * The Board of Trustees of the Leland Stanford Junior University + * + * See LICENSE for licensing terms. + */ + +#include <config.h> +#include <portable/system.h> + +#include <apr_pools.h> +#include <apr_strings.h> +#include <apr_tables.h> + +#include <lib/internal.h> +#include <webauth/basic.h> +#include <webauth/keys.h> +#include <webauth/tokens.h> +#include <webauth/webkdc.h> + + +/* + * Given a Kerberos principal for an authenticated user, derive the WebAuth + * authenticated subject based on the local_realms parameter of the WebKDC + * configuration. The subject may be identical to the Kerberos principal, but + * often means stripping off the realm or applying Kerberos local name + * conversion. Returns the subject in newly allocated pool memory. Returns a + * status code on failure. + * + * The local_realms array in the WebKDC configuration may either be a single + * keyword or may be a list of realms. If it is a keyword, it's one of + * "local" or "none". "local" means to apply Kerberos local name conversion. + * "none" means to use the principal name without modification. Otherwise, + * it's taken to be a list of realms, and any of those realms are stripped + * from the principal. Any principal not in one of those realms is retained + * as a fully-qualified principal name. If local_realms is not set, assume + * "local", which is the default. + */ +static int +canonicalize_user(struct webauth_context *ctx, WEBAUTH_KRB5_CTXT *kctx, + const char **result) +{ + char *subject; + int status, i; + enum webauth_krb5_canon canonicalize = WA_KRB5_CANON_LOCAL; + + *result = NULL; + if (ctx->webkdc->local_realms->nelts > 0) { + const char *local; + char *realm; + + local = APR_ARRAY_IDX(ctx->webkdc->local_realms, 0, const char *); + if (strcmp(local, "none") == 0) + canonicalize = WA_KRB5_CANON_NONE; + else if (strcmp(local, "local") == 0) + canonicalize = WA_KRB5_CANON_LOCAL; + else { + canonicalize = WA_KRB5_CANON_NONE; + status = webauth_krb5_get_realm(kctx, &realm); + if (status != WA_ERR_NONE) + return status; + for (i = 0; i < ctx->webkdc->local_realms->nelts; i++) { + local = APR_ARRAY_IDX(ctx->webkdc->local_realms, i, + const char *); + if (strcmp(local, realm) == 0) + canonicalize = WA_KRB5_CANON_STRIP; + } + free(realm); + } + } + + /* + * We now know the canonicalization method we're using, so we can retrieve + * the principal from the context. + */ + status = webauth_krb5_get_principal(kctx, &subject, canonicalize); + if (status != WA_ERR_NONE) + return status; + *result = apr_pstrdup(ctx->pool, subject); + free(subject); + return WA_ERR_NONE; +} + + +/* + * Check that the realm of the authenticated principal is in the list of + * permitted realms, or that the list of realms is empty. Returns MWK_OK if + * the realm is permitted, MWK_ERROR otherwise. Sets the error on a failure, + * so the caller doesn't need to do so. + */ +static int +realm_permitted(struct webauth_context *ctx, WEBAUTH_KRB5_CTXT *kctx, + struct webauth_webkdc_login_response *response) +{ + int status, i; + char *realm; + const char *allowed; + bool okay = false; + + /* If we aren't restricting the realms, always return true. */ + if (ctx->webkdc->permitted_realms->nelts == 0) + return WA_ERR_NONE; + + /* Get the realm. */ + status = webauth_krb5_get_realm(kctx, &realm); + if (status != WA_ERR_NONE) + goto done; + + /* + * We assume that all realms listed in the configuration are already + * escaped, as is the realm parameter. + */ + for (i = 0; i < ctx->webkdc->permitted_realms->nelts; i++) { + allowed = APR_ARRAY_IDX(ctx->webkdc->permitted_realms, i, const char *); + if (strcmp(allowed, realm) == 0) { + okay = true; + break; + } + } + if (!okay) { + response->login_error = WA_PEC_USER_REJECTED; + response->login_message + = apr_psprintf(ctx->pool, "realm %s is not permitted", realm); + } + status = WA_ERR_NONE; + +done: + free(realm); + return status; +} + + +/* + * Attempt a username and password login. On success, generate a new + * webkdc-proxy token based on that information and store it in the token + * argument. On login failure, store the error code and message in the + * response. On a more fundamental failure, return an error code. + */ +static int +do_login(struct webauth_context *ctx, + struct webauth_webkdc_login_response *response, + struct webauth_token_login *login, + struct webauth_token **wkproxy) +{ + int status; + WEBAUTH_KRB5_CTXT *kctx; + const char *subject; + char *webkdc, *tmp; + char *tgt, *tmp_tgt; + size_t tgt_len; + time_t expires; + struct webauth_token_webkdc_proxy *pt; + + status = webauth_krb5_new(&kctx); + if (status != WA_ERR_NONE) { + if (status == WA_ERR_KRB5) + webauth_krb5_free(kctx); + return status; + } + status = webauth_krb5_init_via_password(kctx, + login->username, + login->password, + NULL, + ctx->webkdc->keytab_path, + ctx->webkdc->principal, + NULL, + &webkdc); + switch (status) { + case 0: + break; + case WA_ERR_LOGIN_FAILED: + response->login_error = WA_PEC_LOGIN_FAILED; + response->login_message = webauth_error_message(ctx, status); + status = WA_ERR_NONE; + goto cleanup; + case WA_ERR_CREDS_EXPIRED: + response->login_error = WA_PEC_CREDS_EXPIRED; + response->login_message = webauth_error_message(ctx, status); + status = WA_ERR_NONE; + goto cleanup; + case WA_ERR_USER_REJECTED: + response->login_error = WA_PEC_USER_REJECTED; + response->login_message = webauth_error_message(ctx, status); + status = WA_ERR_NONE; + goto cleanup; + case WA_ERR_KRB5: + webauth_error_set(ctx, status, "%s", webauth_krb5_error_message(kctx)); + /* fall through */ + default: + return status; + } + + /* + * webauth_krb5_init_via_password determined the principal of the WebKDC + * service to which we just authenticated and stored that information in + * webkdc, but it's not yet poolified, so make a copy in our memory pool + * so that we can free it. + */ + tmp = apr_pstrcat(ctx->pool, "krb5:", webkdc, NULL); + free(webkdc); + webkdc = tmp; + + /* + * Check if the realm of the authenticated principal is permitted and + * then canonicalize the user's identity. + */ + status = realm_permitted(ctx, kctx, response); + if (status != WA_ERR_NONE || response->login_error != 0) + goto cleanup; + status = canonicalize_user(ctx, kctx, &subject); + if (status != WA_ERR_NONE) + goto cleanup; + + /* Export the ticket-granting ticket for the webkdc-proxy token. */ + status = webauth_krb5_export_tgt(kctx, &tgt, &tgt_len, &expires); + if (status != WA_ERR_NONE) + goto cleanup; + tmp_tgt = apr_palloc(ctx->pool, tgt_len); + memcpy(tmp_tgt, tgt, tgt_len); + free(tgt); + tgt = tmp_tgt; + + /* + * We now have everything we need to create the webkdc-proxy token. We've + * already copied all this stuff into a pool, so there is no need to copy + * again. + */ + *wkproxy = apr_pcalloc(ctx->pool, sizeof(struct webauth_token)); + (*wkproxy)->type = WA_TOKEN_WEBKDC_PROXY; + pt = &(*wkproxy)->token.webkdc_proxy; + pt->subject = subject; + pt->proxy_type = "krb5"; + pt->proxy_subject = apr_pstrcat(ctx->pool, "WEBKDC:", webkdc, NULL); + pt->data = tgt; + pt->data_len = tgt_len; + pt->initial_factors = WA_FA_PASSWORD; + if (ctx->webkdc->proxy_lifetime == 0) + pt->expiration = expires; + else { + pt->expiration = time(NULL) + ctx->webkdc->proxy_lifetime; + if (pt->expiration > expires) + pt->expiration = expires; + } + +cleanup: + webauth_krb5_free(kctx); + return status; +} + + +/* + * Merge an array of webkdc-proxy tokens into a single token, which we'll then + * use for subsequent operations. Takes the context, the array of + * credentials, and a place to store the newly created webkdc-proxy token (or + * return a pointer to one of the ones passed in if there is only one. + * + * We use the following logic to merge webkdc-proxy tokens: + * + * 1. Expired tokens are discarded. + * 2. Tokens whose initial factors are a subset of the accumulated factors + * and which do not add krb5 capability are discarded. + * 3. The krb5 data is added if not already present, and the expiration is + * set to the token with the krb5 data. + * 4. Initial factors are merged between all webkdc-proxy tokens, with the + * expiration set to the nearest expiration of all contributing tokens. + * 5. Creation time is set to the current time if we pull from multiple + * tokens. + */ +static int +merge_webkdc_proxy(struct webauth_context *ctx, apr_array_header_t *creds, + struct webauth_token **result) +{ + bool created = false; + struct webauth_token *token, *tmp; + struct webauth_token *genbest = NULL; + struct webauth_token_webkdc_proxy *wkproxy; + struct webauth_token_webkdc_proxy *best = NULL; + struct webauth_factors *current; + struct webauth_factors *factors = NULL; + time_t now; + int i, status; + + *result = NULL; + if (creds->nelts == 0) + return WA_ERR_NONE; + now = time(NULL); + for (i = 0; i < creds->nelts; i++) { + token = APR_ARRAY_IDX(creds, i, struct webauth_token *); + if (token->type != WA_TOKEN_WEBKDC_PROXY) + continue; + wkproxy = &token->token.webkdc_proxy; + if (wkproxy->expiration <= now) + continue; + if (best == NULL) { + best = wkproxy; + genbest = token; + continue; + } + if (factors == NULL) { + status = webauth_factors_parse(ctx, best->initial_factors, + &factors); + if (status != WA_ERR_NONE) + return status; + } + current = NULL; + status = webauth_factors_parse(ctx, wkproxy->initial_factors, + ¤t); + if (status != WA_ERR_NONE) + return status; + if (webauth_factors_subset(ctx, current, factors) + && (strcmp(best->proxy_type, "krb5") == 0 + || strcmp(wkproxy->proxy_type, "krb5") != 0)) + continue; + if (!created) { + tmp = apr_palloc(ctx->pool, sizeof(struct webauth_token)); + *tmp = *genbest; + genbest = tmp; + best = &tmp->token.webkdc_proxy; + created = true; + } + if (strcmp(best->proxy_type, "krb5") != 0 + && strcmp(wkproxy->proxy_type, "krb5") == 0) { + best->data = wkproxy->data; + best->data_len = wkproxy->data_len; + } + status = webauth_factors_parse(ctx, wkproxy->initial_factors, + &factors); + if (status != WA_ERR_NONE) + return status; + if (wkproxy->expiration < best->expiration) + best->expiration = wkproxy->expiration; + if (wkproxy->loa > best->loa) + best->loa = wkproxy->loa; + } + if (created) { + best->initial_factors = webauth_factors_string(ctx, factors); + best->creation = now; + } + *result = genbest; + return WA_ERR_NONE; +} + + +/* + * Given the desired factors expressed by the WebAuth Application Server, the + * current set of factors the user has authenticated with, and the user + * metadata information (which may be NULL if there's no metadata configured), + * check whether multifactor authentication is already satisfied or + * unnecessary, required, or impossible. + * + * Returns WA_ERR_NONE and leaves request->login_error unchanged if any + * multifactor requirements are satisfied. Sets request->login_error if + * multifactor is required or unavailable. Returns an error code on errors in + * processing. + */ +static int +check_multifactor(struct webauth_context *ctx, + struct webauth_webkdc_login_request *request, + struct webauth_webkdc_login_response *response, + struct webauth_user_info *info) +{ + int i, status; + apr_array_header_t *factors = NULL; + struct webauth_factors *wanted; + struct webauth_factors configured; + struct webauth_factors *have = NULL; + struct webauth_token *cred; + struct webauth_token_request *req; + struct webauth_token_webkdc_proxy *wkproxy; + + /* If the WAS doesn't care, neither do we. */ + req = request->request; + + /* Figure out what factors we want and have. */ + if (req->initial_factors == NULL || req->initial_factors[0] == '\0') { + wanted = apr_pcalloc(ctx->pool, sizeof(struct webauth_factors)); + wanted->factors = apr_array_make(ctx->pool, 1, sizeof(char *)); + } else { + status = webauth_factors_parse(ctx, req->initial_factors, &wanted); + if (status != WA_ERR_NONE) + return status; + } + for (i = 0; i < request->creds->nelts; i++) { + cred = APR_ARRAY_IDX(request->creds, i, struct webauth_token *); + if (cred->type != WA_TOKEN_WEBKDC_PROXY) + continue; + wkproxy = &cred->token.webkdc_proxy; + status = webauth_factors_parse(ctx, wkproxy->initial_factors, &have); + if (status != WA_ERR_NONE) + return status; + } + + /* + * Check if multifactor is forced by user configuration. If so, and if + * the user has not already authenticated with multifactor, we need to + * return one or the other error code. Also set factors to the user's + * configured factors if they have any. + */ + if (info != NULL && info->factors != NULL && info->factors->nelts > 0) + factors = info->factors; + if (info != NULL && info->multifactor_required) { + status = webauth_factors_parse(ctx, WA_FA_MULTIFACTOR, &wanted); + if (status != WA_ERR_NONE) + return status; + } + + /* + * Second, see if the WAS-requested factors are already satisfied by the + * factors that we have. + */ + if (webauth_factors_subset(ctx, wanted, have)) + return WA_ERR_NONE; + + /* + * Finally, check if the WAS-requested factors can be satisfied by the + * factors configured by the user. We have to do a bit of work here to + * turn the user's configured factors into a webauth_factors struct. + * + * Assume we can always do password authentication. + */ + memset(&configured, 0, sizeof(configured)); + if (factors != NULL) { + configured.factors = apr_array_copy(ctx->pool, factors); + for (i = 0; i < factors->nelts; i++) + if (strcmp(APR_ARRAY_IDX(factors, i, const char *), "m") == 0) + configured.multifactor = true; + } else { + configured.factors = apr_array_make(ctx->pool, 1, sizeof(const char *)); + APR_ARRAY_PUSH(configured.factors, const char *) = "p"; + } + response->factors_wanted = wanted->factors; + response->factors_configured = configured.factors; + if (webauth_factors_subset(ctx, wanted, &configured)) { + response->login_error = WA_PEC_MULTIFACTOR_REQUIRED; + response->login_message = "multifactor login required"; + } else { + response->login_error = WA_PEC_MULTIFACTOR_UNAVAILABLE; + response->login_message = "multifactor required but not configured"; + } + return WA_ERR_NONE; +} + + +/* + * Given the identity of a WAS and a webkdc-proxy token identifying the user, + * obtain a Kerberos authenticator identifying that user to that WAS. Store + * it in the provided buffer. Returns either WA_ERR_NONE on success or a + * WebAuth error code. On error, also set the WebAuth error message. + */ +static int +get_krb5_authenticator(struct webauth_context *ctx, const char *server, + struct webauth_token_webkdc_proxy *wkproxy, + void **krb5_auth, size_t *krb5_auth_len) +{ + int status; + WEBAUTH_KRB5_CTXT *kctx; + char *tmp_auth; + + *krb5_auth = NULL; + status = webauth_krb5_new(&kctx); + if (status != WA_ERR_NONE) { + if (status == WA_ERR_KRB5) + webauth_krb5_free(kctx); + return status; + } + + /* + * FIXME: Probably need to examine errors a little more closely to + * determine if we should return a proxy-token error or a server-failure. + */ + status = webauth_krb5_init_via_cred(kctx, wkproxy->data, + wkproxy->data_len, NULL); + if (status != WA_ERR_NONE) { + if (status == WA_ERR_KRB5) + webauth_error_set(ctx, status, "%s", + webauth_krb5_error_message(kctx)); + webauth_krb5_free(kctx); + return status; + } + + /* + * Generate the Kerberos authenticator. + * + * FIXME: Probably need to examine errors a little more closely to + * determine if we should return a proxy-token error or a server-failure. + */ + if (strncmp(server, "krb5:", 5) == 0) + server += 5; + status = webauth_krb5_mk_req(kctx, server, &tmp_auth, krb5_auth_len); + if (status != WA_ERR_NONE) { + if (status == WA_ERR_KRB5) + webauth_error_set(ctx, status, "%s", + webauth_krb5_error_message(kctx)); + webauth_krb5_free(kctx); + return status; + } else { + *krb5_auth = apr_palloc(ctx->pool, *krb5_auth_len); + memcpy(*krb5_auth, tmp_auth, *krb5_auth_len); + free(tmp_auth); + } + webauth_krb5_free(kctx); + return WA_ERR_NONE; +} + + +/* + * Given a WebKDC proxy token and a request token, create the id token + * requested by the WAS and store it in the response. At this point, we've + * already done all required checks and ensured we have a WebKDC proxy token, + * so this just involves setting the correct fields. Returns a status code on + * any sort of internal WebAuth error. + */ +static int +create_id_token(struct webauth_context *ctx, + struct webauth_webkdc_login_request *request, + struct webauth_token_webkdc_proxy *wkproxy, + struct webauth_webkdc_login_response *response, + WEBAUTH_KEYRING *keyring) +{ + int status; + void *krb5_auth; + size_t krb5_auth_len; + struct webauth_token token; + struct webauth_token_id *id; + struct webauth_token_request *req; + + req = request->request; + memset(&token, 0, sizeof(token)); + token.type = WA_TOKEN_ID; + id = &token.token.id; + id->subject = wkproxy->subject; + id->auth = req->auth; + if (strcmp(req->auth, "krb5") == 0) { + status = get_krb5_authenticator(ctx, request->service->subject, + wkproxy, &krb5_auth, &krb5_auth_len); + if (status == WA_ERR_KRB5) { + response->login_error = WA_PEC_PROXY_TOKEN_INVALID; + response->login_message = webauth_error_message(ctx, status); + return WA_ERR_NONE; + } else if (status != WA_ERR_NONE) + return status; + id->auth_data = krb5_auth; + id->auth_data_len = krb5_auth_len; + } + id->expiration = wkproxy->expiration; + id->initial_factors = wkproxy->initial_factors; + id->loa = wkproxy->loa; + + /* FIXME: No idea what the session factors are. */ + id->session_factors = "u"; + + /* Encode the token and store the resulting string. */ + response->result_type = "id"; + return webauth_token_encode(ctx, &token, keyring, &response->result); +} + + +/* + * Given a WebKDC proxy token and a request token, create the proxy token + * requested by the WAS and store it in the response. At this point, we've + * already done all required checks and ensured we have a WebKDC proxy token, + * so this just involves setting the correct fields. Returns a status code on + * any sort of internal WebAuth error. + * + * This function needs the WebKDC keyring, since it has to encode the + * embedded webkdc-proxy token in the WebKDC's private key. The first keyring + * is the session keyring for the enclosing proxy token, and the second is the + * WebKDC's private keyring. + */ +static int +create_proxy_token(struct webauth_context *ctx, + struct webauth_webkdc_login_request *request, + struct webauth_token_webkdc_proxy *wkproxy, + struct webauth_webkdc_login_response *response, + WEBAUTH_KEYRING *session, WEBAUTH_KEYRING *keyring) +{ + int status; + struct webauth_token token, subtoken; + struct webauth_token_proxy *proxy; + struct webauth_token_request *req; + + /* Create the easy portions of the proxy token. */ + req = request->request; + memset(&token, 0, sizeof(token)); + token.type = WA_TOKEN_PROXY; + proxy = &token.token.proxy; + proxy->subject = wkproxy->subject; + proxy->type = req->proxy_type; + proxy->initial_factors = wkproxy->initial_factors; + proxy->loa = wkproxy->loa; + proxy->expiration = wkproxy->expiration; + + /* FIXME: No idea what the session factors are. */ + proxy->session_factors = "u"; + + /* Create the embedded webkdc-proxy token and limit its scope. */ + memset(&subtoken, 0, sizeof(subtoken)); + subtoken.type = WA_TOKEN_WEBKDC_PROXY; + subtoken.token.webkdc_proxy = *wkproxy; + subtoken.token.webkdc_proxy.proxy_subject = request->service->subject; + subtoken.token.webkdc_proxy.creation = 0; + status = webauth_token_encode_raw(ctx, &subtoken, keyring, + &proxy->webkdc_proxy, + &proxy->webkdc_proxy_len); + if (status != WA_ERR_NONE) + return status; + + /* Encode the token and store the resulting string. */ + response->result_type = "proxy"; + return webauth_token_encode(ctx, &token, session, &response->result); +} + + +/* + * Given the data from a <requestTokenRequest> login attempt, process that + * attempted login and return the information for a <requestTokenResponse> in + * a newly-allocated struct from pool memory. All of the tokens included in + * the input and output are the unencrypted struct representations; the caller + * does the encryption or decryption and base64 conversion. + * + * Returns WA_ERR_NONE if the request was successfully processed, which + * doesn't mean it succeeded; see the login_code attribute of the struct for + * that. Returns an error code if we were unable to process the struct even + * to generate an error response. + */ +int +webauth_webkdc_login(struct webauth_context *ctx, + struct webauth_webkdc_login_request *request, + struct webauth_webkdc_login_response **response, + WEBAUTH_KEYRING *keyring) +{ + struct webauth_token *cred, *newproxy; + struct webauth_token **token; + struct webauth_token cancel; + struct webauth_token_request *req; + struct webauth_token_webkdc_proxy *wkproxy = NULL; + int i, status; + struct webauth_user_info *info = NULL; + const char *ip; + const char *etoken; + bool did_login = false; + size_t size; + WEBAUTH_KEY key; + WEBAUTH_KEYRING *session; + + /* Basic sanity checking. */ + if (request->service == NULL || request->creds == NULL + || request->request == NULL) { + status = WA_ERR_CORRUPT; + webauth_error_set(ctx, status, "incomplete login request data"); + return status; + } + + /* Shorter names for things we'll be referring to often. */ + ip = request->remote_ip; + req = request->request; + + /* Fill in the basics of our response. */ + *response = apr_pcalloc(ctx->pool, sizeof(**response)); + (*response)->return_url = req->return_url; + (*response)->requester = request->service->subject; + (*response)->app_state = req->state; + (*response)->app_state_len = req->state_len; + + /* + * Several tokens, such as the login cancel token and the result token, + * have to be encrypted in the session key rather than in the WebKDC + * private key, since they're meant to be readable by the WAS. Create a + * keyring containing the session key we can use for those. + * + * FIXME: The conversion from the webkdc-service token to a key is an ugly + * hack. + */ + key.type = WA_AES_KEY; + key.length = request->service->session_key_len; + key.data = (void *) request->service->session_key; + status = webauth_keyring_from_key(ctx, &key, &session); + if (status != WA_ERR_NONE) + return status; + + /* + * If the WAS requested login cancel support, generate an error token + * representing a canceled login and store it in the response. We will + * return that token to WebLogin, which in turn will pass it (in the URL) + * back to the WAS if the user clicks on the cancel login link. + * + * FIXME: Use something less lame than strstr to see if the option is set. + */ + if (req->options != NULL && strstr(req->options, "lc") != NULL) { + cancel.type = WA_TOKEN_ERROR; + cancel.token.error.code = WA_PEC_LOGIN_CANCELED; + cancel.token.error.message = "user canceled login"; + status = webauth_token_encode(ctx, &cancel, session, &etoken); + if (status != WA_ERR_NONE) + return status; + (*response)->login_cancel = etoken; + } + + /* + * Check for a login token in the supplied creds. If there is one, use it + * to authenticate and transform it into a webkdc-proxy token. + * + * FIXME: Stop modifying the array in place. This is surprising to the + * caller and makes the test suite more annoying. + * + * FIXME: How do we get the LoA set in the resulting webkdc-proxy token? + */ + for (i = 0; i < request->creds->nelts; i++) { + cred = APR_ARRAY_IDX(request->creds, i, struct webauth_token *); + if (cred->type != WA_TOKEN_LOGIN) + continue; + token = apr_array_push(request->creds); + status = do_login(ctx, *response, &cred->token.login, token); + if (status != WA_ERR_NONE) + return status; + if (*token == NULL) + apr_array_pop(request->creds); + else + did_login = true; + + /* If the login failed, return what we have so far. */ + if ((*response)->login_error != 0) + return WA_ERR_NONE; + } + + /* + * If there was a login token, all webkdc-proxy tokens also supplied must + * be WEBKDC tokens (in other words, global single-sign-on tokens). A WAS + * can't send a WAS-scoped webkdc-proxy token from a proxy token combined + * with a login token. + */ + if (did_login) + for (i = 0; i < request->creds->nelts; i++) { + cred = APR_ARRAY_IDX(request->creds, i, struct webauth_token *); + if (cred->type != WA_TOKEN_WEBKDC_PROXY) + continue; + wkproxy = &cred->token.webkdc_proxy; + if (strncmp(wkproxy->proxy_subject, "WEBKDC:", 7) != 0) { + (*response)->login_error = WA_PEC_PROXY_TOKEN_INVALID; + (*response)->login_message + = apr_psprintf(ctx->pool, "proxy subject %s not allowed" + " with login token", wkproxy->proxy_subject); + return WA_ERR_NONE; + } + } + + /* + * We have condensed all the user authentication information at this point + * to a set of webkdc-proxy tokens. However, we want one and only one + * webkdc-proxy token that has our combined factor information. If we did + * a login (meaning that we generated new webkdc-proxy information), we + * want to copy that new webkdc-proxy token into our output. + */ + status = merge_webkdc_proxy(ctx, request->creds, &newproxy); + if (status != WA_ERR_NONE) + return status; + if (newproxy != NULL) { + struct webauth_webkdc_proxy_data *data; + + wkproxy = &newproxy->token.webkdc_proxy; + size = sizeof(struct webauth_webkdc_proxy_data); + (*response)->proxies = apr_array_make(ctx->pool, 1, size); + data = apr_array_push((*response)->proxies); + data->type = wkproxy->proxy_type; + status = webauth_token_encode(ctx, newproxy, keyring, &data->token); + if (status != WA_ERR_NONE) + return status; + } + + /* + * Determine the authenticated user. + * + * If we have configuration for a user metadata service, we now know as + * much as we're going to know about who the user is and should retrieve + * that information if possible. If we did a login, we should return + * login history if we have any. + */ + if (wkproxy != NULL) + (*response)->subject = wkproxy->subject; + if (ctx->user != NULL && wkproxy != NULL) { + status = webauth_user_info(ctx, wkproxy->subject, ip, 0, &info); + if (status != WA_ERR_NONE) + return status; + if (did_login) + (*response)->logins = info->logins; + if (wkproxy->loa > info->max_loa) + wkproxy->loa = info->max_loa; + } + + /* + * If we have no webkdc-proxy token, we're done; we can't authenticate the + * user, so bounce them back to the WebLogin screen with what information + * we do have. + */ + if (wkproxy == NULL) { + (*response)->login_error = WA_PEC_PROXY_TOKEN_REQUIRED; + (*response)->login_message = "need a proxy token"; + return WA_ERR_NONE; + } + + /* + * If forced login is set and we didn't just process a login token, error + * out with the error code for forced login, instructing WebLogin to put + * up the login screen. + * + * FIXME: strstr is still lame. + */ + if (!did_login) + if (req->options != NULL && strstr(req->options, "fa") != NULL) { + (*response)->login_error = WA_PEC_LOGIN_FORCED; + (*response)->login_message = "forced authentication, need to login"; + return WA_ERR_NONE; + } + + /* + * If the user metadata service says that multifactor is required, reject + * the login with either multifactor required or with multifactor + * unavailable, depending on whether the user has multifactor configured. + */ + status = check_multifactor(ctx, request, *response, info); + if (status != WA_ERR_NONE) + return status; + if ((*response)->login_error != 0) + return WA_ERR_NONE; + + /* + * We have to ensure that the webkdc-proxy token we have available is + * capable of satisfying the request from the WAS. This is always the + * case if the WAS just wants an id token of type webkdc (a simple + * identity assertion), but if the WAS asked for a krb5 id or proxy token, + * we have to have a krb5 webkdc-proxy token. + */ + if ((strcmp(req->type, "id") == 0 && strcmp(req->auth, "krb5") == 0) + || (strcmp(req->type, "proxy") == 0 + && strcmp(req->proxy_type, "krb5") == 0)) + if (strcmp(wkproxy->proxy_type, "krb5") != 0) { + (*response)->login_error = WA_PEC_PROXY_TOKEN_REQUIRED; + (*response)->login_message = "need a proxy token"; + return WA_ERR_NONE; + } + + /* + * Protect against an attacker using the WebLogin XML interface and + * sending, as the webkdc-proxy token, a webkdc-proxy token obtained by a + * WAS to use to get delegated credentials. That's only allowed to + * generate an id token if it's for the WAS that we're talking to. + */ + if (wkproxy != NULL + && strncmp(wkproxy->proxy_subject, "WEBKDC:", 7) != 0 + && strcmp(wkproxy->proxy_subject, request->service->subject) != 0) { + (*response)->login_error = WA_PEC_UNAUTHORIZED; + (*response)->login_message = "not authorized to use proxy token"; + return WA_ERR_NONE; + } + + /* + * We have a single (or no) webkdc-proxy token that contains everything we + * know about the user. Attempt to satisfy their request. + */ + if (strcmp(req->type, "id") == 0) + status = create_id_token(ctx, request, wkproxy, *response, session); + else if (strcmp(req->type, "proxy") == 0) + status = create_proxy_token(ctx, request, wkproxy, *response, session, + keyring); + else { + status = WA_ERR_CORRUPT; + webauth_error_set(ctx, status, "unsupported requested token type %s", + req->type); + } + return status; +} diff --git a/modules/webkdc/config.c b/modules/webkdc/config.c index c6ebfac5..10d28477 100644 --- a/modules/webkdc/config.c +++ b/modules/webkdc/config.c @@ -26,6 +26,7 @@ #include <modules/webkdc/mod_webkdc.h> #include <util/macros.h> #include <webauth/util.h> +#include <webauth/webkdc.h> /* * For each directive, we have the directive name (CD_), a usage string (CU_), @@ -57,6 +58,8 @@ DIRN(ProxyTokenLifetime, "lifetime of webkdc-proxy tokens") DIRN(ServiceTokenLifetime,"lifetime of webkdc-service tokens") DIRN(TokenAcl, "path to the token ACL file") DIRD(TokenMaxTTL, "max lifetime of recent tokens", int, 60 * 5) +DIRN(UserInfoURL, "URL to user metadata service") +DIRN(UserInfoPrincipal, "authentication identity of the metadata service") enum { E_Debug, @@ -70,6 +73,8 @@ enum { E_ServiceTokenLifetime, E_TokenAcl, E_TokenMaxTTL, + E_UserInfoURL, + E_UserInfoPrincipal, }; /* @@ -150,6 +155,8 @@ webkdc_config_merge(apr_pool_t *pool, void *basev, void *overv) MERGE_PTR(keytab_path); MERGE_PTR_OTHER(keytab_principal, keytab_path); MERGE_PTR(token_acl_path); + MERGE_PTR(userinfo_config); + MERGE_PTR(userinfo_principal); MERGE_SET(debug); MERGE_SET(keyring_auto_update); MERGE_SET(key_lifetime); @@ -245,6 +252,38 @@ parse_interval(cmd_parms *cmd, const char *arg, unsigned long *value) /* + * Utility function for parsing a user metadata service URL. This also does + * validation of the URL and the protocol to ensure that it represents a + * supported user metadata service. Returns an error string or NULL on + * success. The URL will be of the form: + * + * remctl://hostname.example.com:4373/oath + * + * where the path portion is the remctl command name. + */ +static const char * +parse_userinfo_url(cmd_parms *cmd, const char *arg, + struct webauth_user_config *config) +{ + apr_uri_t uri; + int status; + + status = apr_uri_parse(cmd->pool, arg, &uri); + if (status != APR_SUCCESS) + return apr_psprintf(cmd->pool, "Invalid user metadata service URL" + " \"%s\" for %s", arg, cmd->directive->directive); + if (strcmp(uri.scheme, "remctl") != 0) + return apr_psprintf(cmd->pool, "Unknown user metadata protocol \"%s\"" + " for %s", uri.scheme, cmd->directive->directive); + config->protocol = WA_PROTOCOL_REMCTL; + config->host = uri.hostname; + config->port = uri.port; + config->command = uri.path; + return NULL; +} + + +/* * Return the error message for an internal error parsing a configuration * directive. This happens when the wrong configuration handling routine is * called for a directive and indicates a coding error in the configuration @@ -310,6 +349,14 @@ cfg_str(cmd_parms *cmd, void *mconf UNUSED, const char *arg) if (err == NULL) sconf->token_max_ttl_set = true; break; + case E_UserInfoURL: + sconf->userinfo_config + = apr_palloc(cmd->pool, sizeof(struct webauth_user_config)); + err = parse_userinfo_url(cmd, arg, sconf->userinfo_config); + break; + case E_UserInfoPrincipal: + sconf->userinfo_principal = arg; + break; default: err = unknown_error(cmd, directive, "cfg_str"); break; @@ -396,5 +443,7 @@ const command_rec webkdc_cmds[] = { DIRECTIVE(AP_INIT_TAKE1, cfg_str, ServiceTokenLifetime), DIRECTIVE(AP_INIT_TAKE1, cfg_str, TokenAcl), DIRECTIVE(AP_INIT_TAKE1, cfg_str, TokenMaxTTL), + DIRECTIVE(AP_INIT_TAKE1, cfg_str, UserInfoURL), + DIRECTIVE(AP_INIT_TAKE1, cfg_str, UserInfoPrincipal), { NULL, { NULL }, NULL, OR_NONE, RAW_ARGS, NULL } }; diff --git a/modules/webkdc/mod_webkdc.c b/modules/webkdc/mod_webkdc.c index ef898442..1860e199 100644 --- a/modules/webkdc/mod_webkdc.c +++ b/modules/webkdc/mod_webkdc.c @@ -26,6 +26,8 @@ #include <util/macros.h> #include <webauth/basic.h> #include <webauth/keys.h> +#include <webauth/tokens.h> +#include <webauth/webkdc.h> /* @@ -701,30 +703,6 @@ get_krb5_sad(MWK_REQ_CTXT *rc, /* */ static enum mwk_status -create_error_token_from_req(MWK_REQ_CTXT *rc, - int error_code, - const char *error_message, - MWK_REQUESTER_CREDENTIAL *req_cred, - MWK_RETURNED_TOKEN *rtoken) -{ - static const char *mwk_func = "create_error_token_from_req"; - enum mwk_status ms; - struct webauth_token token; - - memset(&token, 0, sizeof(token)); - token.type = WA_TOKEN_ERROR; - token.token.error.code = error_code; - token.token.error.message = error_message; - - ms = make_token_with_key(rc, req_cred->u.st.session_key, - req_cred->u.st.session_key_len, &token, - &rtoken->token_data, mwk_func); - return ms; -} - -/* - */ -static enum mwk_status create_id_token_from_req(MWK_REQ_CTXT *rc, const char *auth_type, MWK_REQUESTER_CREDENTIAL *req_cred, @@ -889,7 +867,7 @@ create_proxy_token_from_req(MWK_REQ_CTXT *rc, mwk_func, true); } - /* make sure we are creating a proxy-tyoken that has + /* make sure we are creating a proxy-token that has the same type as the proxy-token we are using to create it */ sub_pt = find_proxy_token(rc, sub_cred, proxy_type, mwk_func, 1); if (sub_pt == NULL) @@ -1481,199 +1459,160 @@ get_subject(MWK_REQ_CTXT *rc, WEBAUTH_KRB5_CTXT *ctxt, return MWK_OK; } + /* - * attempt to login. If successful, fill in both sub_cred and rtokens and - * return MWK_OK. If unsuccessful, generate an errorResponse/log and return - * MWK_ERROR. - * - * This is the point at which different types of authentication could be - * plugged in, and the point at which we should create all the different types - * of proxy-tokens we'll be needing at login time. + * Parses a <requesterCredential> XML element containing a service token and + * stores it in the provided webauth_webkdc_login_request struct after + * decoding it and confirming that it's the right type. Use + * parse_requesterCredential to support either service or krb5 credentials. */ static enum mwk_status -mwk_do_login(MWK_REQ_CTXT *rc, - struct webauth_token_login *lt, - MWK_SUBJECT_CREDENTIAL *sub_cred, - MWK_RETURNED_PROXY_TOKEN rtokens[], - size_t *num_rtokens, - const char **subject_out) +parse_service_token(MWK_REQ_CTXT *rc, apr_xml_elem *e, + struct webauth_webkdc_login_request *request) { - static const char*mwk_func = "mwk_do_login"; - WEBAUTH_KRB5_CTXT *ctxt; - const char *subject; - char *server_principal, *temp; + static const char *mwk_func = "parse_service_token"; int status; - size_t tgt_len; - enum mwk_status ms; - time_t tgt_expiration; - char *tgt; - struct webauth_token_webkdc_proxy *pt; - struct webauth_token data; - - *subject_out = NULL; + struct webauth_token *data; + const char *at = get_attr_value(rc, e, "type", 1, mwk_func); + char *msg, *token; - ms = MWK_ERROR; + if (rc->sconf->ring == NULL) + return set_errorResponse(rc, WA_PEC_SERVER_FAILURE, "no keyring", + mwk_func, true); - ctxt = mwk_get_webauth_krb5_ctxt(rc->r, mwk_func); - if (ctxt == NULL) { - /* mwk_get_webauth_krb5_ctxt already logged error */ - return set_errorResponse(rc, WA_PEC_SERVER_FAILURE, - "server failure", mwk_func, false); + /* Make sure that the provided token type is service. */ + if (at == NULL) + return MWK_ERROR; + if (strcmp(at, "service") != 0) { + msg = apr_psprintf(rc->r->pool, "unknown <requesterCredential> type:" + " %s", at); + return set_errorResponse(rc, WA_PEC_INVALID_REQUEST, msg, mwk_func, + true); } - status = webauth_krb5_init_via_password(ctxt, - lt->username, - lt->password, - NULL, - rc->sconf->keytab_path, - rc->sconf->keytab_principal, - NULL, - &server_principal); - - if (status == WA_ERR_LOGIN_FAILED) { - char *msg = mwk_webauth_error_message(rc->r, status, ctxt, - "mwk_do_login", NULL); - set_errorResponse(rc, WA_PEC_LOGIN_FAILED, msg, mwk_func, true); - goto cleanup; - - } else if (status == WA_ERR_CREDS_EXPIRED) { - char *msg = mwk_webauth_error_message(rc->r, status, ctxt, - "mwk_do_login", NULL); - set_errorResponse(rc, WA_PEC_CREDS_EXPIRED, msg, mwk_func, true); - goto cleanup; - - } else if (status == WA_ERR_USER_REJECTED) { - char *msg = mwk_webauth_error_message(rc->r, status, ctxt, - "mwk_do_login", NULL); - set_errorResponse(rc, WA_PEC_USER_REJECTED, msg, mwk_func, true); - goto cleanup; - - } else if (status != WA_ERR_NONE) { - char *msg = mwk_webauth_error_message(rc->r, status, ctxt, - "webauth_krb5_init_via_password", - NULL); - set_errorResponse(rc, WA_PEC_SERVER_FAILURE, msg, mwk_func, true); - goto cleanup; + token = get_elem_text(rc, e, mwk_func); + if (token == NULL) + return MWK_ERROR; + status = webauth_token_decode(rc->ctx, WA_TOKEN_WEBKDC_SERVICE, token, + rc->sconf->ring, &data); + if (status != WA_ERR_NONE) { + mwk_log_webauth_error(rc->r->server, status, NULL, mwk_func, + "webauth_token_decode", NULL); + if (status == WA_ERR_TOKEN_EXPIRED) { + set_errorResponse(rc, WA_PEC_SERVICE_TOKEN_EXPIRED, + "service token was expired", + mwk_func, false); + } else if (status == WA_ERR_BAD_HMAC) { + set_errorResponse(rc, WA_PEC_SERVICE_TOKEN_INVALID, + "can't decrypt service token", mwk_func, + false); + } else { + set_errorResponse(rc, WA_PEC_SERVICE_TOKEN_INVALID, + "error parsing token", mwk_func, false); + } + return MWK_ERROR; } + request->service = &data->token.webkdc_service; + return MWK_OK; +} - /* copy server_principal to request pool */ - temp = apr_pstrcat(rc->r->pool, "krb5:", server_principal, NULL); - free(server_principal); - server_principal = temp; - - /* Check if the realm of the authenticated principal is permitted. */ - if (realm_permitted(rc, ctxt, mwk_func) != MWK_OK) - goto cleanup; - - /* get subject, attempt local-name conversion */ - if (get_subject(rc, ctxt, &subject, mwk_func) != MWK_OK) - goto cleanup; - *subject_out = subject; - /* export TGT for webkdc-proxy-token */ - status = webauth_krb5_export_tgt(ctxt, &tgt, &tgt_len, &tgt_expiration); - if (status != WA_ERR_NONE) { - char *msg = mwk_webauth_error_message(rc->r, status, ctxt, - "webauth_krb5_export_tgt", - NULL); - set_errorResponse(rc, WA_PEC_SERVER_FAILURE, msg, mwk_func, true); - goto cleanup; - } else { - char *new_tgt = apr_palloc(rc->r->pool, tgt_len); - memcpy(new_tgt, tgt, tgt_len); - free(tgt); - tgt = new_tgt; - } +/* + * Parse a <subjectCredential> XML element passed in to <requestTokenRequest> + * into a webauth_webkdc_login_request struct. Use parse_subjectCredential + * for <getTokens> for right now. + */ +static enum mwk_status +parse_subject_credentials(MWK_REQ_CTXT *rc, apr_xml_elem *e, + struct webauth_webkdc_login_request *request) +{ + static const char *mwk_func = "parse_subject_credentials"; + apr_xml_elem *child; + char *data; + struct webauth_token *token; + struct webauth_token_webkdc_proxy *wkproxy; - /* we now have everything we need to create the webkdc-proy-token - * lets load up data in the sub_cred proxy token and use it - * to create a token we'll return. - * - * we've already copied all this stuff into a pool, so there is no - * need to copy again... - */ - pt = &sub_cred->u.proxy.pt[0]; - pt->subject = subject; - pt->proxy_type = "krb5"; - pt->proxy_subject = apr_pstrcat(rc->r->pool, "WEBKDC:", - server_principal, NULL); - pt->data = tgt; - pt->data_len = tgt_len; + if (request->creds == NULL) + request->creds = apr_array_make(rc->r->pool, 2, + sizeof(struct webauth_token *)); /* - * FIXME: We only support password authentication right now, so we - * hard-code the factors. This will change once we add multifactor - * support. + * Just quietly ignore invalid webkdc-proxy tokens for right now. + * + * FIXME: Restore a facility for telling WebLogin to discard uninteresting + * webkdc-proxy tokens. + * + * FIXME: We're calling functions that get generic tokens and collapse + * them into the appropriate type, and then allocating a generic token and + * copying it back. This is silly. Restructure all this code. */ - pt->initial_factors = apr_pstrdup(rc->r->pool, "p"); - pt->loa = 0; - - /* if ProxyTokenLifetime is non-zero, use the min of it - and the tgt, else just use the tgt */ - if (rc->sconf->proxy_lifetime) { - time_t pmax = time(NULL) + rc->sconf->proxy_lifetime; - - pt->expiration = (tgt_expiration < pmax) ? tgt_expiration : pmax; - } else { - pt->expiration = tgt_expiration; - } - - /* Make the base64-encoded token. */ - memset(&data, 0, sizeof(data)); - data.type = WA_TOKEN_WEBKDC_PROXY; - data.token.webkdc_proxy = *pt; - ms = make_token(rc, &data, &rtokens[0].token_data, mwk_func); - if (ms == MWK_OK) { - rtokens[0].type = pt->proxy_type; - *num_rtokens = 1; - sub_cred->u.proxy.num_proxy_tokens = 1; - /* make sure we fill in type! */ - sub_cred->type = "proxy"; + for (child = e->first_child; child != NULL; child = child->next) { + if (strcmp(child->name, "proxyToken") == 0) { + data = get_elem_text(rc, child, mwk_func); + if (data == NULL) + return MWK_ERROR; + token = apr_pcalloc(rc->r->pool, sizeof(struct webauth_token)); + token->type = WA_TOKEN_WEBKDC_PROXY; + wkproxy = &token->token.webkdc_proxy; + if (!parse_webkdc_proxy_token(rc, data, wkproxy)) + continue; + APR_ARRAY_PUSH(request->creds, struct webauth_token *) = token; + } else if (strcmp(child->name, "loginToken") == 0) { + data = get_elem_text(rc, child, mwk_func); + if (data == NULL) + return MWK_ERROR; + token = apr_pcalloc(rc->r->pool, sizeof(struct webauth_token)); + token->type = WA_TOKEN_LOGIN; + if (!parse_login_token(rc, data, &token->token.login)) + return MWK_ERROR; + APR_ARRAY_PUSH(request->creds, struct webauth_token *) = token; + } else { + unknown_element(rc, mwk_func, e->name, child->name); + return MWK_ERROR; + } } - - cleanup: - webauth_krb5_free(ctxt); - return ms; + return MWK_OK; } + static enum mwk_status -parse_requestInfo(MWK_REQ_CTXT *rc, - apr_xml_elem *e, MWK_REQUEST_INFO *req_info) +parse_requestInfo(MWK_REQ_CTXT *rc, apr_xml_elem *e, + struct webauth_webkdc_login_request *request) { + static const char *mwk_func = "parse_requestInfo"; apr_xml_elem *ie; - static const char *mwk_func="parse_identInfo"; for (ie = e->first_child; ie; ie = ie->next) { if (strcmp(ie->name, "localIpAddr") == 0) { - req_info->local_addr = get_elem_text(rc, ie, mwk_func); - if (req_info->local_addr == NULL) + request->local_ip = get_elem_text(rc, ie, mwk_func); + if (request->local_ip == NULL) return MWK_ERROR; } else if (strcmp(ie->name, "localIpPort") == 0) { - req_info->local_port = get_elem_text(rc, ie, mwk_func); - if (req_info->local_port == NULL) + request->local_port = get_elem_text(rc, ie, mwk_func); + if (request->local_port == NULL) return MWK_ERROR; } else if (strcmp(ie->name, "remoteIpAddr") == 0) { - req_info->remote_addr = get_elem_text(rc, ie, mwk_func); - if (req_info->remote_addr == NULL) + request->remote_ip = get_elem_text(rc, ie, mwk_func); + if (request->remote_ip == NULL) return MWK_ERROR; } else if (strcmp(ie->name, "remoteIpPort") == 0) { - req_info->remote_port = get_elem_text(rc, ie, mwk_func); - if (req_info->remote_port == NULL) + request->remote_port = get_elem_text(rc, ie, mwk_func); + if (request->remote_port == NULL) return MWK_ERROR; } else if (strcmp(ie->name, "remoteUser") == 0) { - req_info->remote_user = get_elem_text(rc, ie, mwk_func); - if (req_info->remote_user == NULL) + request->remote_user = get_elem_text(rc, ie, mwk_func); + if (request->remote_user == NULL) return MWK_ERROR; } else { unknown_element(rc, mwk_func, e->name, ie->name); return MWK_ERROR; } } - if (req_info->remote_user == NULL - && (req_info->local_addr == NULL || - req_info->local_port == NULL || - req_info->remote_addr == NULL || - req_info->remote_port == NULL)) { + if (request->remote_user == NULL + && (request->local_ip == NULL || + request->local_port == NULL || + request->remote_ip == NULL || + request->remote_port == NULL)) { return set_errorResponse(rc, WA_PEC_INVALID_REQUEST, "<requestInfo> missing data", mwk_func, true); @@ -1681,6 +1620,29 @@ parse_requestInfo(MWK_REQ_CTXT *rc, return MWK_OK; } + +/* + * Given the request context, an XML tag, and an array of const char *, print + * out the contents of the array as a series of that XML element. If the + * array is NULL, prints out nothing. + */ +static void +print_xml_array(MWK_REQ_CTXT *rc, const char *tag, apr_array_header_t *array) +{ + int i; + const char *string; + + if (array == NULL) + return; + + for (i = 0; i < array->nelts; i++) { + string = APR_ARRAY_IDX(array, i, const char *); + string = apr_xml_quote_string(rc->r->pool, string, false); + ap_rvputs(rc->r, "<%s>%s</%s>", tag, string, tag, NULL); + } +} + + static enum mwk_status handle_requestTokenRequest(MWK_REQ_CTXT *rc, apr_xml_elem *e, const char **req_subject_out, @@ -1688,55 +1650,37 @@ handle_requestTokenRequest(MWK_REQ_CTXT *rc, apr_xml_elem *e, { apr_xml_elem *child; static const char *mwk_func="handle_requestTokenRequest"; - char *request_token; - MWK_REQUEST_INFO req_info; - MWK_REQUESTER_CREDENTIAL req_cred; - MWK_SUBJECT_CREDENTIAL parsed_sub_cred, login_sub_cred, *sub_cred; - enum mwk_status ms; - struct webauth_token_request *req_token; - int req_cred_parsed = 0; - int sub_cred_parsed = 0; - size_t i; - int did_login; - int login_ec; - const char *login_em = NULL; + char *request_token = NULL; + int i, status; const char *req_token_info; - MWK_RETURNED_TOKEN rtoken; - MWK_RETURNED_PROXY_TOKEN rptokens[MAX_PROXY_TOKENS_RETURNED]; - size_t num_proxy_tokens = 0; + struct webauth_user_info *info = NULL; + struct webauth_webkdc_login_request request; + struct webauth_webkdc_login_response *response; + /* + * FIXME: These should be set to NULL, not <unknown>. Chase down all the + * places we assume they aren't NULL. + */ *subject_out = "<unknown>"; *req_subject_out = "<unkknown>"; - did_login = 0; - num_proxy_tokens = 0; - login_ec = 0; - request_token = NULL; - sub_cred = NULL; req_token_info = ""; - - memset(&req_cred, 0, sizeof(req_cred)); - memset(&rtoken, 0, sizeof(rtoken)); - memset(&req_info, 0, sizeof(req_info)); + memset(&request, 0, sizeof(request)); /* walk through each child element in <requestTokenRequest> */ for (child = e->first_child; child; child = child->next) { if (strcmp(child->name, "requesterCredential") == 0) { - if (!parse_requesterCredential(rc, child, &req_cred, - req_subject_out)) + if (!parse_service_token(rc, child, &request)) return MWK_ERROR; - req_cred_parsed = 1; } else if (strcmp(child->name, "subjectCredential") == 0) { - if (!parse_subjectCredential(rc, child, &parsed_sub_cred, - &num_proxy_tokens, rptokens)) + if (!parse_subject_credentials(rc, child, &request)) return MWK_ERROR; - sub_cred_parsed = 1; } else if (strcmp(child->name, "requestToken") == 0) { request_token = get_elem_text(rc, child, mwk_func); if (request_token == NULL) return MWK_ERROR; } else if (strcmp(child->name, "requestInfo") == 0) { - if (!parse_requestInfo(rc, child, &req_info)) + if (!parse_requestInfo(rc, child, &request)) return MWK_ERROR; } else { unknown_element(rc, mwk_func, e->name, child->name); @@ -1745,26 +1689,23 @@ handle_requestTokenRequest(MWK_REQ_CTXT *rc, apr_xml_elem *e, } /* make sure we found requesterCredential */ - if (!req_cred_parsed) { + if (request.service == NULL) { return set_errorResponse(rc, WA_PEC_INVALID_REQUEST, "missing <requesterCredential>", mwk_func, true); } + if (request.service != NULL) + *req_subject_out = request.service->subject; - /* make sure we found subjectCredentials */ - if (!sub_cred_parsed) { + /* + * Make sure we found <subjectCredential>. Note that the array may be + * legitimately empty if the user has no proxy credentials and it's their + * first visit to WebLogin. + */ + if (request.creds == NULL) return set_errorResponse(rc, WA_PEC_INVALID_REQUEST, "missing <subjectCredential>", mwk_func, true); - } - - /* make sure req_cred is of type "service" */ - if (strcmp(req_cred.type, "service") != 0) { - return set_errorResponse(rc, WA_PEC_INVALID_REQUEST, - "must use <requesterCredential> " - "of type 'service'", - mwk_func, true); - } /* make sure we found requestToken */ if (request_token == NULL) { @@ -1772,197 +1713,212 @@ handle_requestTokenRequest(MWK_REQ_CTXT *rc, apr_xml_elem *e, "missing <requestToken>", mwk_func, true); } - - if (!parse_request_token(rc, request_token, &req_cred.u.st, &req_token)) { + if (!parse_request_token(rc, request_token, request.service, + &request.request)) return MWK_ERROR; + + /* + * Based on the type of token requested, check that the requesting WAS is + * permitted to get that type of token. + */ + if (strcmp(request.request->type, "id") == 0) { + if (!mwk_has_id_access(rc, request.service->subject)) { + return set_errorResponse(rc, WA_PEC_UNAUTHORIZED, + "not authorized to get an id token", + mwk_func, true); + } + } else if (strcmp(request.request->type, "proxy") == 0) { + if (!mwk_has_proxy_access(rc, request.service->subject, + request.request->proxy_type)) { + return set_errorResponse(rc, WA_PEC_UNAUTHORIZED, + "not authorized to get a proxy token", + mwk_func, true); + } } /* - * if we have a login-token, attempt to login with it, - * and if that succeeds, we'll get a new MWK_SUBJECT_CREDENTIAL - * to pass around, and new proxy-tokens to set. - * + * All of the supplied webkdc-proxy credentials, if any, must be for the + * same authenticated user (the same subject) and must be usable by the + * same entity (the same proxy_subject). We don't have to check login + * tokens here, since we'll do that later in webauth_webkdc_login when we + * turn those login tokens into webkdc-proxy tokens. */ - if (strcmp(parsed_sub_cred.type, "login") == 0) { - if (!mwk_do_login(rc, &parsed_sub_cred.u.lt, &login_sub_cred, - rptokens, &num_proxy_tokens, subject_out)) { - *subject_out = parsed_sub_cred.u.lt.username; - if (rc->error_code == WA_PEC_LOGIN_FAILED) { - login_ec = rc->error_code; - login_em = rc->error_message; - rc->error_code = 0; - rc->error_message = NULL; - goto send_response; + if (request.creds != NULL && request.creds->nelts > 0) { + struct webauth_token *token; + struct webauth_token_webkdc_proxy *wkproxy; + const char *subject = NULL; + const char *proxy_subject = NULL; + + for (i = 0; i < request.creds->nelts; i++) { + token = APR_ARRAY_IDX(request.creds, 0, struct webauth_token *); + if (token->type == WA_TOKEN_LOGIN) + continue; + wkproxy = &token->token.webkdc_proxy; + if (subject == NULL) { + subject = wkproxy->subject; + proxy_subject = wkproxy->proxy_subject; } else { - return MWK_ERROR; - } - } - sub_cred = &login_sub_cred; - did_login = 1; - } else { - sub_cred = &parsed_sub_cred; - /* grab first subject from passed in proxy tokens as subject_out */ - if (strcmp(parsed_sub_cred.type, "proxy") == 0) { - for (i=0; i < parsed_sub_cred.u.proxy.num_proxy_tokens; i++) { - if (parsed_sub_cred.u.proxy.pt[i].subject) { - *subject_out = parsed_sub_cred.u.proxy.pt[i].subject; - break; + if (strcmp(subject, wkproxy->subject) != 0 + || strcmp(proxy_subject, wkproxy->proxy_subject) != 0) { + return set_errorResponse(rc, WA_PEC_UNAUTHORIZED, + "not authorized to get a proxy" + " token", mwk_func, true); } } } } + + /* + * Call into libwebauth to process the login information. This will take + * the accumulated data in the request and attempt to fulfill it. On an + * internal error, this function will return a status other than + * WA_ERR_NONE. Otherwise, it may set the login_error and login_message + * in the response. We handle that below when we generate the XML + * response. + */ + status = webauth_webkdc_login(rc->ctx, &request, &response, + rc->sconf->ring); + if (status != WA_ERR_NONE) + return set_errorResponse(rc, WA_PEC_SERVER_FAILURE, + webauth_error_message(rc->ctx, status), + mwk_func, true); - /* lets see if they requested forced-authentication, if so - and we didn't just login, then we need - to return an error that will cause the web front-end to - prompt for a username/password */ - if (req_token->options != NULL - && ap_strstr(req_token->options, "fa") != 0 - && !did_login) { - login_ec = WA_PEC_LOGIN_FORCED; - login_em = "forced authentication, need to login"; - goto send_response; - } - - /* now examine req_token to see what they asked for */ - - if (strcmp(req_token->type, "id") == 0) { - ms = create_id_token_from_req(rc, req_token->auth, - &req_cred, sub_cred, &rtoken, - subject_out); - req_token_info = apr_pstrcat(rc->r->pool, - " sa=", - req_token->auth, + /* Accumulate logging information about what we were asked to do. */ + if (strcmp(request.request->type, "id") == 0) + req_token_info = apr_pstrcat(rc->r->pool, " sa=", request.request->auth, NULL); - } else if (strcmp(req_token->type, "proxy") == 0) { - ms = create_proxy_token_from_req(rc, req_token->proxy_type, - &req_cred, sub_cred, &rtoken); - req_token_info = apr_pstrcat(rc->r->pool, - " pt=", - req_token->proxy_type, - NULL); - } else { - char *msg = apr_psprintf(rc->r->pool, - "unsupported requested-token-type: %s", - req_token->type); - set_errorResponse(rc, WA_PEC_INVALID_REQUEST, msg, - mwk_func, true); - return MWK_ERROR; - } - - if (ms != MWK_OK) { - switch (rc->error_code) { - case WA_PEC_PROXY_TOKEN_REQUIRED: - login_ec = rc->error_code; - login_em = rc->error_message; - goto send_response; - default: - return MWK_ERROR; - } - } - - send_response: + else if (strcmp(request.request->type, "proxy") == 0) + req_token_info = apr_pstrcat(rc->r->pool, " pt=", + request.request->proxy_type, NULL); + if (response->subject != NULL) + *subject_out = response->subject; + /* + * If we saw an error other than proxy token required, abort and send the + * error message. + */ + if (response->login_error != 0 + && response->login_error != WA_PEC_PROXY_TOKEN_REQUIRED + && response->login_error != WA_PEC_MULTIFACTOR_REQUIRED + && response->login_error != WA_PEC_MULTIFACTOR_UNAVAILABLE + && response->login_error != WA_PEC_LOA_UNAVAILABLE) + return set_errorResponse(rc, response->login_error, + response->login_message, mwk_func, true); + + /* Send the XML response. */ ap_rvputs(rc->r, "<requestTokenResponse>", NULL); - if (login_ec) { - ap_rprintf(rc->r, "<loginErrorCode>%d</loginErrorCode>", login_ec); + if (response->login_error != 0) { + ap_rprintf(rc->r, "<loginErrorCode>%d</loginErrorCode>", + response->login_error); ap_rprintf(rc->r, "<loginErrorMessage>%s</loginErrorMessage>", - apr_xml_quote_string(rc->r->pool, login_em, 0)); + apr_xml_quote_string(rc->r->pool, response->login_message, + false)); } - if (num_proxy_tokens) { + if (response->login_error == WA_PEC_MULTIFACTOR_REQUIRED) { + ap_rvputs(rc->r, "<multifactorRequired>", NULL); + print_xml_array(rc, "factor", response->factors_wanted); + print_xml_array(rc, "configuredFactor", response->factors_configured); + ap_rvputs(rc->r, "</multifactorRequired>", NULL); + } + + if (response->proxies != NULL) { + struct webauth_webkdc_proxy_data *data; + ap_rvputs(rc->r, "<proxyTokens>", NULL); - for (i = 0; i < num_proxy_tokens; i++) { - ap_rvputs(rc->r, "<proxyToken type='", rptokens[i].type,"'>", - /* don't have to quote since base64'd data */ - rptokens[i].token_data, - "</proxyToken>", - NULL); + for (i = 0; i < response->proxies->nelts; i++) { + data = &APR_ARRAY_IDX(response->proxies, i, + struct webauth_webkdc_proxy_data); + ap_rvputs(rc->r, "<proxyToken type='", data->type, "'>", + data->token, "</proxyToken>", NULL); } ap_rvputs(rc->r, "</proxyTokens>", NULL); } /* put out return-url */ ap_rvputs(rc->r,"<returnUrl>", - apr_xml_quote_string(rc->r->pool, req_token->return_url, 1), + apr_xml_quote_string(rc->r->pool, response->return_url, 1), "</returnUrl>", NULL); /* requesterSubject */ ap_rvputs(rc->r, "<requesterSubject>", - apr_xml_quote_string(rc->r->pool, req_cred.subject, 1), + apr_xml_quote_string(rc->r->pool, response->requester, 1), "</requesterSubject>", NULL); /* subject */ - if (*subject_out != NULL) { + if (response->subject != NULL) { ap_rvputs(rc->r, "<subject>", - apr_xml_quote_string(rc->r->pool, *subject_out, 1), + apr_xml_quote_string(rc->r->pool, response->subject, 1), "</subject>", NULL); } /* requestedToken, don't need to quote */ - if (rtoken.token_data) { + if (response->result != NULL) { ap_rvputs(rc->r, "<requestedToken>", - rtoken.token_data, + response->result, "</requestedToken>", NULL); ap_rvputs(rc->r, "<requestedTokenType>", - apr_xml_quote_string(rc->r->pool, req_token->type, 1), + apr_xml_quote_string(rc->r->pool, response->result_type, 1), "</requestedTokenType>", NULL); } - if (req_token->options != NULL - && ap_strstr(req_token->options, "lc") != NULL) { - MWK_RETURNED_TOKEN lc_token; - - memset(&lc_token, 0, sizeof(lc_token)); - ms = create_error_token_from_req(rc, - WA_PEC_LOGIN_CANCELED, - "user canceled login", - &req_cred, - &lc_token); - if (ms == MWK_OK) - ap_rvputs(rc->r, - "<loginCanceledToken>", - lc_token.token_data, - "</loginCanceledToken>", - NULL); + if (response->login_cancel != NULL) { + ap_rvputs(rc->r, "<loginCanceledToken>", response->login_cancel, + "</loginCanceledToken>", NULL); } /* appState, need to base64-encode */ - if (req_token->state != NULL) { + if (request.request->state != NULL) { char *out_state = (char*) apr_palloc(rc->r->pool, - apr_base64_encode_len(req_token->state_len)); - apr_base64_encode(out_state, req_token->state, - req_token->state_len); + apr_base64_encode_len(request.request->state_len)); + apr_base64_encode(out_state, request.request->state, + request.request->state_len); /* don't need to quote */ ap_rvputs(rc->r, "<appState>", out_state , "</appState>", NULL); } + + /* loginHistory (if present) */ + if (info != NULL && info->logins != NULL) { + struct webauth_login *login; + + ap_rvputs(rc->r, "<loginHistory>", NULL); + for (i = 0; i < info->logins->nelts; i++) { + login = &APR_ARRAY_IDX(info->logins, i, struct webauth_login); + ap_rvputs(rc->r, "<loginLocation ip=\"", login->ip, "\"", NULL); + if (login->timestamp != 0) + ap_rprintf(rc->r, "time=\"%lu\"", + (unsigned long) login->timestamp); + ap_rvputs(rc->r, ">", login->hostname, "</loginLocation>", NULL); + } + ap_rvputs(rc->r, "</loginHistory>", NULL); + } + ap_rvputs(rc->r, "</requestTokenResponse>", NULL); ap_rflush(rc->r); - ap_log_error(APLOG_MARK, APLOG_NOTICE, 0, rc->r->server, "mod_webkdc: event=requestToken from=%s clientIp=%s " "server=%s user=%s rtt=%s%s%s%s%s", rc->r->connection->remote_ip, - (req_info.remote_addr == NULL ? "" : req_info.remote_addr), - *req_subject_out, - *subject_out, - req_token->type, + (request.remote_ip == NULL ? "" : request.remote_ip), + response->requester, + (response->subject == NULL ? "<unknown>" : response->subject), + request.request->type, req_token_info, - (req_token->options == NULL - || *req_token->options == '\0') ? "" : - apr_psprintf(rc->r->pool, " ro=%s", req_token->options), - (login_ec == 0 && !did_login) ? "" : - apr_psprintf(rc->r->pool, " lec=%d", login_ec), - login_em == NULL ? "" : - apr_psprintf(rc->r->pool, " lem=%s", log_escape(rc, login_em)) + (request.request->options == NULL + || *request.request->options == '\0') ? "" : + apr_psprintf(rc->r->pool, " ro=%s", request.request->options), + apr_psprintf(rc->r->pool, " lec=%d", response->login_error), + response->login_message == NULL ? "" : + apr_psprintf(rc->r->pool, " lem=%s", + log_escape(rc, response->login_message)) ); return MWK_OK; @@ -2431,6 +2387,7 @@ handler_hook(request_rec *r) MWK_REQ_CTXT rc; int status; const char *req_content_type; + struct webauth_webkdc_config config; memset(&rc, 0, sizeof(rc)); status = webauth_context_init_apr(&rc.ctx, r->pool); @@ -2443,6 +2400,27 @@ handler_hook(request_rec *r) rc.r = r; rc.sconf = ap_get_module_config(r->server->module_config, &webkdc_module); + config.keytab_path = rc.sconf->keytab_path; + config.principal = rc.sconf->keytab_principal; + config.proxy_lifetime = rc.sconf->proxy_lifetime; + config.permitted_realms = rc.sconf->permitted_realms; + config.local_realms = rc.sconf->local_realms; + status = webauth_webkdc_config(rc.ctx, &config); + if (status != WA_ERR_NONE) { + ap_log_error(APLOG_MARK, APLOG_CRIT, 0, r->server, + "mod_webkdc: webauth_webkdc_config failed: %s", + webauth_error_message(rc.ctx, status)); + return DECLINED; + } + if (rc.sconf->userinfo_config != NULL) { + status = webauth_user_config(rc.ctx, rc.sconf->userinfo_config); + if (status != WA_ERR_NONE) { + ap_log_error(APLOG_MARK, APLOG_CRIT, 0, r->server, + "mod_webkdc: webauth_user_config failed: %s", + webauth_error_message(rc.ctx, status)); + return DECLINED; + } + } if (strcmp(r->handler, "webkdc")) { return DECLINED; diff --git a/modules/webkdc/mod_webkdc.h b/modules/webkdc/mod_webkdc.h index 1c83e2ef..45572f17 100644 --- a/modules/webkdc/mod_webkdc.h +++ b/modules/webkdc/mod_webkdc.h @@ -60,6 +60,8 @@ struct config { const char *keytab_path; const char *keytab_principal; const char *token_acl_path; + struct webauth_user_config *userinfo_config; + const char *userinfo_principal; bool debug; bool keyring_auto_update; unsigned long key_lifetime; diff --git a/tests/TESTS b/tests/TESTS index c4b63c80..fa6caa19 100644 --- a/tests/TESTS +++ b/tests/TESTS @@ -11,6 +11,7 @@ lib/random lib/token lib/token-decode lib/token-encode +lib/webkdc-login portable/asprintf portable/snprintf portable/mkstemp diff --git a/tests/data/xml/info/normal.xml b/tests/data/xml/info/normal.xml new file mode 100644 index 00000000..5dfc3e49 --- /dev/null +++ b/tests/data/xml/info/normal.xml @@ -0,0 +1,15 @@ +<authdata user="normal"> + <multifactor> + <type>p</type> + <type>m</type> + <type>r</type> + <type>o</type> + <type>o2</type> + </multifactor> + <login-history> + <host ip="127.0.0.2">example.com</host> + <host ip="127.0.0.3">www.example.com</host> + </login-history> + <max-loa>3</max-loa> + <password-expires>1310675733</password-expires> +</authdata> diff --git a/tests/data/xml/info/random.xml b/tests/data/xml/info/random.xml new file mode 100644 index 00000000..f05cdee2 --- /dev/null +++ b/tests/data/xml/info/random.xml @@ -0,0 +1,11 @@ +<authdata user="random"> + <multifactor> + <type>p</type> + <type>m</type> + <type>r</type> + <type>o</type> + <type>o2</type> + <required /> + </multifactor> + <max-loa>2</max-loa> +</authdata> diff --git a/tests/lib/webkdc-login-t.c b/tests/lib/webkdc-login-t.c new file mode 100644 index 00000000..b82d5bdf --- /dev/null +++ b/tests/lib/webkdc-login-t.c @@ -0,0 +1,593 @@ +/* + * Test WebKDC login support. + * + * Written by Russ Allbery <rra@stanford.edu> + * Copyright 2011 + * The Board of Trustees of the Leland Stanford Junior University + * + * See LICENSE for licensing terms. + */ + +#include <config.h> +#include <portable/system.h> + +#include <apr_pools.h> +#include <apr_strings.h> +#include <apr_tables.h> +#include <time.h> + +#include <tests/tap/basic.h> +#include <tests/tap/kerberos.h> +#include <tests/tap/remctl.h> +#include <util/concat.h> +#include <webauth/basic.h> +#include <webauth/keys.h> +#include <webauth/tokens.h> +#include <webauth/webkdc.h> + + +int +main(void) +{ + apr_pool_t *pool = NULL; + WEBAUTH_KEYRING *ring, *session; + WEBAUTH_KEY *session_key; + char key_data[WA_AES_128], username[BUFSIZ], password[BUFSIZ]; + int status; + char *realm, *path, *keyring, *conf; + time_t now; + pid_t remctld; + FILE *file; + struct webauth_context *ctx; + struct webauth_webkdc_config config; + struct webauth_user_config user_config; + struct webauth_webkdc_login_request request; + struct webauth_webkdc_login_response *response; + struct webauth_token *token, login, wkproxy, wkproxy2; + struct webauth_token_request req; + struct webauth_token_webkdc_proxy *pt; + struct webauth_token_webkdc_service service; + struct webauth_webkdc_proxy_data *pd; + time_t expiration = 0; + + if (apr_initialize() != APR_SUCCESS) + bail("cannot initialize APR"); + if (apr_pool_create(&pool, NULL) != APR_SUCCESS) + bail("cannot create memory pool"); + if (webauth_context_init_apr(&ctx, pool) != WA_ERR_NONE) + bail("cannot initialize WebAuth context"); + + /* Load the precreated keyring that we'll use for token encryption. */ + keyring = test_file_path("data/keyring"); + status = webauth_keyring_read_file(keyring, &ring); + if (status != WA_ERR_NONE) + bail("cannot read %s: %s", keyring, + webauth_error_message(NULL, status)); + test_file_path_free(keyring); + + /* Ensure we have a basic configuration available. */ + memset(&config, 0, sizeof(config)); + config.local_realms = apr_array_make(pool, 1, sizeof(const char *)); + config.permitted_realms = apr_array_make(pool, 1, sizeof(const char *)); + memset(&user_config, 0, sizeof(user_config)); + if (chdir(getenv("SOURCE")) < 0) + bail("can't chdir to SOURCE"); + config.principal = kerberos_setup(); + if (config.principal == NULL) + skip_all("Kerberos tests not configured"); + realm = strchr(config.principal, '@'); + if (realm == NULL) + skip_all("Kerberos principal has no realm"); + realm++; + config.keytab_path = test_file_path("data/test.keytab"); + if (config.keytab_path == NULL) + skip_all("Kerberos tests not configured"); + + /* + * Ensure we have a username and password. + * + * FIXME: Ideally, we would skip the tests that don't require this and + * still do the rest. + */ + path = test_file_path("data/test.password"); + if (path == NULL) + skip_all("Kerberos tests not configured"); + file = fopen(path, "r"); + if (file == NULL) + sysbail("cannot open %s", path); + if (fgets(username, sizeof(username), file) == NULL) + bail("cannot read %s", path); + if (fgets(password, sizeof(password), file) == NULL) + bail("cannot read password from %s", path); + fclose(file); + if (username[strlen(username) - 1] != '\n') + bail("no newline in %s", path); + username[strlen(username) - 1] = '\0'; + if (password[strlen(password) - 1] != '\n') + bail("username or password too long in %s", path); + password[strlen(password) - 1] = '\0'; + test_file_path_free(path); + + /* + * FIXME: Similarly, if we don't have remctl, we should skip tests instead + * of skipping the whole thing. + */ +#ifndef PATH_REMCTLD + skip_all("remctld not found"); +#endif + if (chdir(getenv("SOURCE")) < 0) + bail("can't chdir to SOURCE"); + conf = concatpath(getenv("SOURCE"), "data/conf-webkdc"); + remctld = remctld_start(PATH_REMCTLD, config.principal, conf, NULL); + + plan(158); + + /* Provide basic configuration to the WebKDC code. */ + status = webauth_webkdc_config(ctx, &config); + is_int(WA_ERR_NONE, status, "WebKDC configuration succeeded"); + + /* Flesh out the absolute minimum required in the request. */ + now = time(NULL); + memset(&request, 0, sizeof(request)); + memset(&service, 0, sizeof(service)); + service.subject = "krb5:webauth/example.com@EXAMPLE.COM"; + if (webauth_random_key(key_data, sizeof(key_data)) != WA_ERR_NONE) + bail("cannot create random key"); + session_key = webauth_key_create(WA_AES_KEY, key_data, sizeof(key_data)); + status = webauth_keyring_from_key(ctx, session_key, &session); + if (status != WA_ERR_NONE) + bail("cannot create keyring from session key"); + service.session_key = session_key->data; + service.session_key_len = session_key->length; + service.creation = now; + service.expiration = now + 60; + request.service = &service; + memset(&req, 0, sizeof(req)); + req.type = "id"; + req.auth = "webkdc"; + req.return_url = "https://example.com/"; + req.creation = now; + request.request = &req; + request.creds = apr_array_make(pool, 1, sizeof(struct token *)); + + /* + * Attempted login with now proxy or login tokens. Should return an error + * indicating that a proxy token is required. + */ + status = webauth_webkdc_login(ctx, &request, &response, ring); + is_int(WA_ERR_NONE, status, "Minimal login returns success"); + is_int(WA_PEC_PROXY_TOKEN_REQUIRED, response->login_error, + "...with correct error code"); + is_string("need a proxy token", response->login_message, + "...and correct error message"); + ok(response->factors_wanted == NULL, "...no factors wanted"); + ok(response->factors_configured == NULL, "...no factors configured"); + ok(response->proxies == NULL, "...no new webkdc-proxy tokens"); + is_string("https://example.com/", response->return_url, + "...return URL is correct"); + is_string("krb5:webauth/example.com@EXAMPLE.COM", response->requester, + "...requester is correct"); + is_string(NULL, response->subject, "...no subject"); + is_string(NULL, response->result, "...no result token"); + is_string(NULL, response->result_type, "...no result type"); + is_string(NULL, response->login_cancel, "...no login cancel token"); + ok(response->app_state == NULL, "...no app state"); + is_int(0, response->app_state_len, "...no app state length"); + ok(response->logins == NULL, "...no login information"); + + /* Try again, but with a login cancel token requested. */ + req.options = "lc"; + status = webauth_webkdc_login(ctx, &request, &response, ring); + is_int(WA_ERR_NONE, status, "Login w/cancel returns success"); + is_int(WA_PEC_PROXY_TOKEN_REQUIRED, response->login_error, + "...with correct error code"); + is_string("need a proxy token", response->login_message, + "...and correct error message"); + ok(response->login_cancel != NULL, "...and now a cancel token"); + status = webauth_token_decode(ctx, WA_TOKEN_ERROR, response->login_cancel, + session, &token); + is_int(WA_ERR_NONE, status, "...which decodes properly"); + if (status != WA_ERR_NONE) + ok_block(3, 0, "...invalid error token"); + else { + is_int(WA_PEC_LOGIN_CANCELED, token->token.error.code, + "...with correct code"); + is_string("user canceled login", token->token.error.message, + "...and message"); + ok(token->token.error.creation - now < 3, "...and creation time"); + } + + /* Now add a login token and see if we can get an id token in response. */ + config.local_realms = apr_array_make(pool, 1, sizeof(const char *)); + APR_ARRAY_PUSH(config.local_realms, const char *) = "none"; + memset(&login, 0, sizeof(login)); + login.type = WA_TOKEN_LOGIN; + login.token.login.username = username; + login.token.login.password = password; + login.token.login.creation = now; + APR_ARRAY_PUSH(request.creds, struct webauth_token *) = &login; + status = webauth_webkdc_login(ctx, &request, &response, ring); + is_int(WA_ERR_NONE, status, "Login w/password returns success"); + is_int(0, response->login_error, "...with no error"); + is_string(NULL, response->login_message, "...and no message"); + ok(response->proxies != NULL, "...and now we have proxy tokens"); + pt = NULL; + if (response->proxies == NULL) + ok_block(11, 0, "...no proxy tokens"); + else { + is_int(1, response->proxies->nelts, "...one proxy token"); + pd = &APR_ARRAY_IDX(response->proxies, 0, + struct webauth_webkdc_proxy_data); + is_string("krb5", pd->type, "...of type krb5"); + status = webauth_token_decode(ctx, WA_TOKEN_WEBKDC_PROXY, pd->token, + ring, &token); + is_int(WA_ERR_NONE, status, "...which decodes properly"); + pt = &token->token.webkdc_proxy; + is_string(username, pt->subject, "...with correct subject"); + is_string("krb5", pt->proxy_type, "...and correct type"); + ok(strncmp("WEBKDC:krb5:", pt->proxy_subject, 12) == 0, + "...and correct proxy subject prefix"); + ok(strcmp(config.principal, pt->proxy_subject + 12) == 0, + "...and correct proxy subject identity"); + ok(pt->data != NULL, "...and data is not NULL"); + is_string("p", pt->initial_factors, "...and factors is password"); + ok(pt->creation - now < 3, "...and creation is okay"); + ok(pt->expiration > now, "...and expiration is sane"); + } + is_string(username, response->subject, "...subject is correct"); + ok(response->result != NULL, "...there is a result token"); + is_string("id", response->result_type, "...which is an id token"); + status = webauth_token_decode(ctx, WA_TOKEN_ID, response->result, + session, &token); + is_int(WA_ERR_NONE, status, "...result token decodes properly"); + if (status != WA_ERR_NONE) + ok_block(7, 0, "...no result token: %s", + webauth_error_message(ctx, status)); + else { + is_string(username, token->token.id.subject, + "...result subject is right"); + is_string("webkdc", token->token.id.auth, + "...result auth type is right"); + ok(token->token.id.auth_data == NULL, "...and there is no auth data"); + is_string("p", token->token.id.initial_factors, + "...result initial factors is right"); + is_int(0, token->token.id.loa, "...and no LoA"); + ok(token->token.id.creation - now < 3, "...and creation is sane"); + is_int(pt->expiration, token->token.id.expiration, + "...and expiration matches the expiration of the proxy token"); + } + + /* Get an id token with a Kerberos authenticator and test forced auth. */ + req.options = "lc,fa"; + request.creds = apr_array_make(pool, 2, sizeof(struct webauth_token *)); + APR_ARRAY_PUSH(request.creds, struct webauth_token *) = &login; + service.subject = apr_pstrcat(pool, "krb5:", config.principal, NULL); + req.auth = "krb5"; + status = webauth_webkdc_login(ctx, &request, &response, ring); + is_int(WA_ERR_NONE, status, "Login for krb5 auth returns success"); + is_int(0, response->login_error, "...with no error"); + is_string(NULL, response->login_message, "...and no message"); + ok(response->login_cancel != NULL, "...and there is a cancel token"); + ok(response->result != NULL, "...there is a result token"); + is_string("id", response->result_type, "...which is an id token"); + status = webauth_token_decode(ctx, WA_TOKEN_ID, response->result, + session, &token); + is_int(WA_ERR_NONE, status, "...result token decodes properly"); + if (status != WA_ERR_NONE) + ok_block(3, 0, "...no result token: %s", + webauth_error_message(ctx, status)); + else { + is_string(username, token->token.id.subject, + "...result subject is right"); + is_string("krb5", token->token.id.auth, + "...result auth type is right"); + ok(token->token.id.auth_data != NULL, "...and there is auth data"); + } + + /* Get a proxy token instead. */ + req.options = NULL; + request.creds = apr_array_make(pool, 2, sizeof(struct webauth_token *)); + APR_ARRAY_PUSH(request.creds, struct webauth_token *) = &login; + req.type = "proxy"; + req.auth = NULL; + req.proxy_type = "krb5"; + status = webauth_webkdc_login(ctx, &request, &response, ring); + is_int(WA_ERR_NONE, status, "Login for proxy token returns success"); + is_int(0, response->login_error, "...with no error"); + is_string(NULL, response->login_message, "...and no message"); + ok(response->login_cancel == NULL, "...and there is no cancel token"); + is_string(username, response->subject, "...subject is correct"); + pt = NULL; + if (response->proxies == NULL) + ok_block(3, 0, "...no proxy tokens"); + else { + is_int(1, response->proxies->nelts, "...one proxy token"); + pd = &APR_ARRAY_IDX(response->proxies, 0, + struct webauth_webkdc_proxy_data); + is_string("krb5", pd->type, "...of type krb5"); + status = webauth_token_decode(ctx, WA_TOKEN_WEBKDC_PROXY, pd->token, + ring, &token); + is_int(WA_ERR_NONE, status, "...which decodes properly"); + expiration = token->token.webkdc_proxy.expiration; + } + ok(response->result != NULL, "...there is a result token"); + is_string("proxy", response->result_type, "...which is a proxy token"); + status = webauth_token_decode(ctx, WA_TOKEN_PROXY, response->result, + session, &token); + is_int(WA_ERR_NONE, status, "...result token decodes properly"); + if (status != WA_ERR_NONE) + ok_block(6, 0, "...no result token: %s", + webauth_error_message(ctx, status)); + else { + is_string(username, token->token.proxy.subject, + "...result subject is right"); + is_string("krb5", token->token.proxy.type, + "...result proxy type is right"); + is_string("p", token->token.proxy.initial_factors, + "...result initial factors is right"); + is_int(0, token->token.proxy.loa, "...and no LoA"); + ok(token->token.proxy.creation - now < 3, "...and creation is sane"); + is_int(expiration, token->token.proxy.expiration, + "...and expiration matches the expiration of the proxy token"); + status = webauth_token_decode_raw(ctx, WA_TOKEN_WEBKDC_PROXY, + token->token.proxy.webkdc_proxy, + token->token.proxy.webkdc_proxy_len, + ring, &token); + is_int(WA_ERR_NONE, status, "...embedded webkdc-proxy token decodes"); + if (status != WA_ERR_NONE) + ok_block(7, 0, "...no webkdc-proxy token: %s", + webauth_error_message(ctx, status)); + else { + pt = &token->token.webkdc_proxy; + is_string(username, pt->subject, "...with correct subject"); + is_string("krb5", pt->proxy_type, "...and correct type"); + ok(strcmp(request.service->subject, pt->proxy_subject) == 0, + "...and correct proxy subject identity"); + ok(pt->data != NULL, "...and data is not NULL"); + is_string("p", pt->initial_factors, "...and factors is password"); + ok(pt->creation - now < 3, "...and creation is okay"); + ok(pt->expiration > now, "...and expiration is sane"); + } + } + + /* Get an id token with a single sign-on webkdc-proxy token. */ + memset(&wkproxy, 0, sizeof(wkproxy)); + wkproxy.type = WA_TOKEN_WEBKDC_PROXY; + wkproxy.token.webkdc_proxy.subject = "testuser"; + wkproxy.token.webkdc_proxy.proxy_type = "remuser"; + wkproxy.token.webkdc_proxy.proxy_subject = "WEBKDC:remuser"; + wkproxy.token.webkdc_proxy.data = "testuser"; + wkproxy.token.webkdc_proxy.data_len = strlen("testuser"); + wkproxy.token.webkdc_proxy.initial_factors = "x,x1"; + wkproxy.token.webkdc_proxy.loa = 3; + wkproxy.token.webkdc_proxy.creation = now; + wkproxy.token.webkdc_proxy.expiration = now + 60 * 60; + request.creds = apr_array_make(pool, 2, sizeof(struct webauth_token *)); + APR_ARRAY_PUSH(request.creds, struct webauth_token *) = &wkproxy; + req.type = "id"; + req.auth = "webkdc"; + req.proxy_type = NULL; + status = webauth_webkdc_login(ctx, &request, &response, ring); + if (status != WA_ERR_NONE) + diag("error status: %s", webauth_error_message(ctx, status)); + is_int(WA_ERR_NONE, status, "Proxy auth for webkdc returns success"); + is_int(0, response->login_error, "...with no error"); + is_string(NULL, response->login_message, "...and no message"); + ok(response->proxies != NULL, "...and we have proxy tokens"); + if (response->proxies == NULL) + ok_block(5, 0, "...no proxy tokens"); + else { + is_int(1, response->proxies->nelts, "...one proxy token"); + pd = &APR_ARRAY_IDX(response->proxies, 0, + struct webauth_webkdc_proxy_data); + is_string("remuser", pd->type, "...of type webkdc"); + status = webauth_token_decode(ctx, WA_TOKEN_WEBKDC_PROXY, pd->token, + ring, &token); + is_int(WA_ERR_NONE, status, "...which decodes properly"); + pt = &token->token.webkdc_proxy; + is_string("testuser", pt->subject, "...with correct subject"); + is_string("x,x1", pt->initial_factors, "...and initial factors"); + } + ok(response->result != NULL, "...there is a result token"); + is_string("id", response->result_type, "...which is an id token"); + status = webauth_token_decode(ctx, WA_TOKEN_ID, response->result, + session, &token); + is_int(WA_ERR_NONE, status, "...result token decodes properly"); + if (status != WA_ERR_NONE) + ok_block(6, 0, "...no result token: %s", + webauth_error_message(ctx, status)); + else { + is_string("testuser", token->token.id.subject, + "...result subject is right"); + is_string("webkdc", token->token.id.auth, + "...result auth type is right"); + is_string("x,x1", token->token.proxy.initial_factors, + "...result initial factors is right"); + is_int(3, token->token.id.loa, "...result LoA is right"); + ok(token->token.id.creation - now < 3, "...and creation is sane"); + is_int(now + 60 * 60, token->token.id.expiration, + "...and expiration matches the expiration of the proxy token"); + } + + /* Set forced authentication and try again. */ + req.options = "fa"; + status = webauth_webkdc_login(ctx, &request, &response, ring); + is_int(WA_ERR_NONE, status, "Proxy auth w/forced login returns success"); + is_int(WA_PEC_LOGIN_FORCED, response->login_error, + "...with the right error"); + is_string("forced authentication, need to login", response->login_message, + "...and the right message"); + is_string("testuser", response->subject, "...but we do know the subject"); + + /* Remove forced authentication but ask for a proxy token. */ + req.options = NULL; + req.type = "proxy"; + req.auth = NULL; + req.proxy_type = "krb5"; + status = webauth_webkdc_login(ctx, &request, &response, ring); + is_int(WA_ERR_NONE, status, "Proxy auth for proxy returns success"); + is_int(WA_PEC_PROXY_TOKEN_REQUIRED, response->login_error, + "...with the right error"); + is_string("need a proxy token", response->login_message, + "...and the right message"); + is_string("testuser", response->subject, "...but we do know the subject"); + + /* Now, add configuration for user information, and try this again. */ + user_config.protocol = WA_PROTOCOL_REMCTL; + user_config.host = "localhost"; + user_config.port = 14373; + user_config.identity = config.principal; + user_config.command = "test"; + status = webauth_user_config(ctx, &user_config); + is_int(WA_ERR_NONE, status, "User information config accepted"); + req.type = "id"; + req.auth = "webkdc"; + req.proxy_type = NULL; + wkproxy.token.webkdc_proxy.subject = "mini"; + wkproxy.token.webkdc_proxy.data = "mini"; + wkproxy.token.webkdc_proxy.data_len = strlen("mini"); + status = webauth_webkdc_login(ctx, &request, &response, ring); + if (status != WA_ERR_NONE) + diag("error status: %s", webauth_error_message(ctx, status)); + is_int(WA_ERR_NONE, status, "Proxy auth w/user config returns success"); + is_int(0, response->login_error, "...with no error"); + is_string(NULL, response->login_message, "...and no message"); + ok(response->result != NULL, "...there is a result token"); + is_string("id", response->result_type, "...which is an id token"); + status = webauth_token_decode(ctx, WA_TOKEN_ID, response->result, + session, &token); + is_int(WA_ERR_NONE, status, "...result token decodes properly"); + if (status != WA_ERR_NONE) + ok_block(4, 0, "...no result token: %s", + webauth_error_message(ctx, status)); + else { + is_string("mini", token->token.id.subject, + "...result subject is right"); + is_string("webkdc", token->token.id.auth, + "...result auth type is right"); + is_string("x,x1", token->token.proxy.initial_factors, + "...result initial factors is right"); + is_int(1, token->token.id.loa, "...result LoA is right"); + } + + /* + * Request an X.509 factor and try again. This should still work even + * though this user doesn't have password listed as a supported factor in + * the metadata. + */ + req.initial_factors = "x"; + status = webauth_webkdc_login(ctx, &request, &response, ring); + if (status != WA_ERR_NONE) + diag("error status: %s", webauth_error_message(ctx, status)); + is_int(WA_ERR_NONE, status, "Multifactor with proxy returns success"); + is_int(0, response->login_error, "...with no error"); + is_string(NULL, response->login_message, "...and no message"); + ok(response->result != NULL, "...there is a result token"); + is_string("id", response->result_type, "...which is an id token"); + + /* + * Change the WebKDC proxy token to assert just a password factor and ask + * for an OTP factor, and try again. This should be rejected with + * multifactor required. + */ + wkproxy.token.webkdc_proxy.initial_factors = "p"; + req.initial_factors = "o"; + status = webauth_webkdc_login(ctx, &request, &response, ring); + is_int(WA_ERR_NONE, status, "Multifactor without config returns success"); + is_int(WA_PEC_MULTIFACTOR_UNAVAILABLE, response->login_error, + "...with the right error"); + is_string("multifactor required but not configured", + response->login_message, "...and the right message"); + ok(response->result == NULL, "...and there is no result token"); + is_int(1, response->factors_wanted->nelts, "...and one factor is wanted"); + is_string("o", APR_ARRAY_IDX(response->factors_wanted, 0, const char *), + "...which is the OTP factor"); + is_int(1, response->factors_configured->nelts, + "...and one factor is configured"); + is_string("p", + APR_ARRAY_IDX(response->factors_configured, 0, const char *), + "...which is the password factor"); + + /* + * Try with a user who has multifactor configuration and forced + * multifactor. + */ + wkproxy.token.webkdc_proxy.subject = "full"; + wkproxy.token.webkdc_proxy.data = "full"; + wkproxy.token.webkdc_proxy.data_len = strlen("full"); + status = webauth_webkdc_login(ctx, &request, &response, ring); + is_int(WA_ERR_NONE, status, "Multifactor with config returns success"); + is_int(WA_PEC_MULTIFACTOR_REQUIRED, response->login_error, + "...with the right error"); + is_string("multifactor login required", response->login_message, + "...and the right message"); + ok(response->result == NULL, "...and there is no result token"); + is_int(2, response->factors_wanted->nelts, + "...and two factors are wanted"); + is_string("o", APR_ARRAY_IDX(response->factors_wanted, 0, const char *), + "...which is the OTP factor"); + is_int(5, response->factors_configured->nelts, + "...and five factors are configured"); + is_string("p", + APR_ARRAY_IDX(response->factors_configured, 0, const char *), + "...which is the password factor"); + is_string("r", + APR_ARRAY_IDX(response->factors_configured, 1, const char *), + "...the random factor"); + is_string("m", + APR_ARRAY_IDX(response->factors_configured, 2, const char *), + "...the generic multifactor factor"); + is_string("o", + APR_ARRAY_IDX(response->factors_configured, 3, const char *), + "...the OTP factor"); + is_string("o3", + APR_ARRAY_IDX(response->factors_configured, 4, const char *), + "...and the OTP-3 factor"); + + /* + * Add a second webkdc-proxy token that repesents an OTP login. This + * login should then work. + */ + wkproxy.token.webkdc_proxy.loa = 3; + wkproxy2.type = WA_TOKEN_WEBKDC_PROXY; + wkproxy2.token.webkdc_proxy.subject = "full"; + wkproxy2.token.webkdc_proxy.proxy_type = "remuser"; + wkproxy2.token.webkdc_proxy.proxy_subject = "WEBKDC:remuser"; + wkproxy2.token.webkdc_proxy.data = "full"; + wkproxy2.token.webkdc_proxy.data_len = strlen("full"); + wkproxy2.token.webkdc_proxy.initial_factors = "o,o3"; + wkproxy2.token.webkdc_proxy.loa = 2; + wkproxy2.token.webkdc_proxy.creation = now; + wkproxy2.token.webkdc_proxy.expiration = now + 30 * 60; + APR_ARRAY_PUSH(request.creds, struct webauth_token *) = &wkproxy2; + status = webauth_webkdc_login(ctx, &request, &response, ring); + is_int(WA_ERR_NONE, status, + "Multifactor with two proxies returns success"); + is_int(0, response->login_error, "...with no error"); + is_string(NULL, response->login_message, "...and no error message"); + ok(response->result != NULL, "...there is a result token"); + is_string("id", response->result_type, "...which is an id token"); + status = webauth_token_decode(ctx, WA_TOKEN_ID, response->result, + session, &token); + is_int(WA_ERR_NONE, status, "...result token decodes properly"); + if (status != WA_ERR_NONE) + ok_block(5, 0, "...no result token: %s", + webauth_error_message(ctx, status)); + else { + is_string("full", token->token.id.subject, + "...result subject is right"); + is_string("webkdc", token->token.id.auth, + "...result auth type is right"); + is_string("p,o,o3,m", token->token.proxy.initial_factors, + "...result initial factors is right"); + is_int(3, token->token.id.loa, "...result LoA is right"); + is_int(now + 30 * 60, token->token.id.expiration, + "...and expiration matches the shorter expiration"); + } + + /* Clean up. */ + remctld_stop(remctld); + kerberos_cleanup(); + apr_terminate(); + return 0; +} |