diff options
Diffstat (limited to 'lib/webkdc-login.c')
-rw-r--r-- | lib/webkdc-login.c | 879 |
1 files changed, 879 insertions, 0 deletions
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; +} |