/* * * auth_mellon_handler.c: an authentication apache module * Copyright © 2003-2007 UNINETT (http://www.uninett.no/) * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * */ #include "auth_mellon.h" #ifdef HAVE_lasso_server_new_from_buffers # define SERVER_NEW lasso_server_new_from_buffers #else /* HAVE_lasso_server_new_from_buffers */ # define SERVER_NEW lasso_server_new #endif /* HAVE_lasso_server_new_from_buffers */ #ifdef HAVE_lasso_server_new_from_buffers /* This function generates optional metadata for a given element * * Parameters: * apr_pool_t *p Pool to allocate memory from * apr_hash_t *t Hash of lang -> strings * const char *e Name of the element * * Returns: * the metadata, or NULL if an error occured */ static char *am_optional_metadata_element(apr_pool_t *p, apr_hash_t *h, const char *e) { apr_hash_index_t *index; char *data = ""; for (index = apr_hash_first(p, h); index; index = apr_hash_next(index)) { char *lang; char *value; apr_ssize_t slen; char *xmllang = ""; apr_hash_this(index, (const void **)&lang, &slen, (void *)&value); if (*lang != '\0') xmllang = apr_psprintf(p, " xml:lang=\"%s\"", lang); data = apr_psprintf(p, "%s<%s%s>%s", data, e, xmllang, value, e); } return data; } /* This function generates optinal metadata * * Parameters: * request_rec *r The request we received. * * Returns: * the metadata, or NULL if an error occured */ static char *am_optional_metadata(apr_pool_t *p, request_rec *r) { am_dir_cfg_rec *cfg = am_get_dir_cfg(r); int count = 0; char *org_data = NULL; char *org_name = NULL; char *org_display_name = NULL; char *org_url = NULL; count += apr_hash_count(cfg->sp_org_name); count += apr_hash_count(cfg->sp_org_display_name); count += apr_hash_count(cfg->sp_org_url); if (count == 0) return ""; org_name = am_optional_metadata_element(p, cfg->sp_org_name, "OrganizationName"); org_display_name = am_optional_metadata_element(p, cfg->sp_org_display_name, "OrganizationDisplayName"); org_url = am_optional_metadata_element(p, cfg->sp_org_url, "OrganizationURL"); org_data = apr_psprintf(p, "%s%s%s", org_name, org_display_name, org_url); return org_data; } /* This function generates metadata * * Parameters: * request_rec *r The request we received. * * Returns: * the metadata, or NULL if an error occured */ static char *am_generate_metadata(apr_pool_t *p, request_rec *r) { am_dir_cfg_rec *cfg = am_get_dir_cfg(r); char *url = am_get_endpoint_url(r); char *cert = ""; const char *sp_entity_id; sp_entity_id = cfg->sp_entity_id ? cfg->sp_entity_id : url; if (cfg->sp_cert_file) { char *sp_cert_file; char *cp; char *bp; const char *begin = "-----BEGIN CERTIFICATE-----"; const char *end = "-----END CERTIFICATE-----"; /* * Try to remove leading and trailing garbage, as it can * wreak havoc XML parser if it contains [<>&] */ sp_cert_file = apr_pstrdup(p, cfg->sp_cert_file); cp = strstr(sp_cert_file, begin); if (cp != NULL) sp_cert_file = cp + strlen(begin); cp = strstr(sp_cert_file, end); if (cp != NULL) *cp = '\0'; /* * And remove any non printing char (CR, spaces...) */ bp = sp_cert_file; for (cp = sp_cert_file; *cp; cp++) { if (apr_isgraph(*cp)) *bp++ = *cp; } *bp = '\0'; cert = apr_psprintf(p, "" "" "" "%s" "" "" "" "" "" "" "%s" "" "" "", sp_cert_file, sp_cert_file); } return apr_psprintf(p, "\n\ \n\ \n\ %s\ \n\ \n\ urn:oasis:names:tc:SAML:2.0:nameid-format:transient\n\ \n\ \n\ \n\ %s\n\ ", sp_entity_id, cfg->sp_entity_id ? "" : "metadata", cert, url, url, url, url, am_optional_metadata(p, r)); } #endif /* HAVE_lasso_server_new_from_buffers */ /* * This function loads all IdP metadata in a lasso server * * Parameters: * am_dir_cfg_rec *cfg The server configuration. * request_rec *r The request we received. * * Returns: * number of loaded providers */ static guint am_server_add_providers(am_dir_cfg_rec *cfg, request_rec *r) { apr_size_t index; #ifndef HAVE_lasso_server_load_metadata const char *idp_public_key_file; if (cfg->idp_metadata->nelts == 1) idp_public_key_file = cfg->idp_public_key_file; else idp_public_key_file = NULL; #endif /* ! HAVE_lasso_server_load_metadata */ for (index = 0; index < cfg->idp_metadata->nelts; index++) { const am_metadata_t *idp_metadata; int error; #ifdef HAVE_lasso_server_load_metadata GList *loaded_idp = NULL; #endif /* HAVE_lasso_server_load_metadata */ idp_metadata = &( ((const am_metadata_t*)cfg->idp_metadata->elts) [index] ); #ifdef HAVE_lasso_server_load_metadata error = lasso_server_load_metadata(cfg->server, LASSO_PROVIDER_ROLE_IDP, idp_metadata->file, idp_metadata->chain, cfg->idp_ignore, &loaded_idp, LASSO_SERVER_LOAD_METADATA_FLAG_DEFAULT); if (error == 0) { GList *idx; for (idx = loaded_idp; idx != NULL; idx = idx->next) { ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, "loaded IdP \"%s\" from \"%s\".", (char *)idx->data, idp_metadata->file); } } if (loaded_idp != NULL) { for (GList *idx = loaded_idp; idx != NULL; idx = idx->next) { g_free(idx->data); } g_list_free(loaded_idp); } #else /* HAVE_lasso_server_load_metadata */ error = lasso_server_add_provider(cfg->server, LASSO_PROVIDER_ROLE_IDP, idp_metadata->file, idp_public_key_file, cfg->idp_ca_file); #endif /* HAVE_lasso_server_load_metadata */ if (error != 0) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Error adding metadata \"%s\" to " "lasso server objects: %s.", idp_metadata->file, lasso_strerror(error)); } } return g_hash_table_size(cfg->server->providers); } static LassoServer *am_get_lasso_server(request_rec *r) { am_dir_cfg_rec *cfg = am_get_dir_cfg(r); cfg = cfg->inherit_server_from; apr_thread_mutex_lock(cfg->server_mutex); if(cfg->server == NULL) { if(cfg->sp_metadata_file == NULL) { #ifdef HAVE_lasso_server_new_from_buffers /* * Try to generate missing metadata */ apr_pool_t *pool = r->server->process->pconf; cfg->sp_metadata_file = am_generate_metadata(pool, r); #else ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Missing MellonSPMetadataFile option."); apr_thread_mutex_unlock(cfg->server_mutex); return NULL; #endif /* HAVE_lasso_server_new_from_buffers */ } cfg->server = SERVER_NEW(cfg->sp_metadata_file, cfg->sp_private_key_file, NULL, cfg->sp_cert_file); if(cfg->server == NULL) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Error initializing lasso server object. Please" " verify the following configuration directives:" " MellonSPMetadataFile and MellonSPPrivateKeyFile."); apr_thread_mutex_unlock(cfg->server_mutex); return NULL; } if (am_server_add_providers(cfg, r) == 0) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Error adding IdP to lasso server object. Please" " verify the following configuration directives:" " MellonIdPMetadataFile and" " MellonIdPPublicKeyFile."); lasso_server_destroy(cfg->server); cfg->server = NULL; apr_thread_mutex_unlock(cfg->server_mutex); return NULL; } } apr_thread_mutex_unlock(cfg->server_mutex); return cfg->server; } /* Redirect to discovery service. * * Parameters: * request_rec *r The request we received. * const char *return_to The URL the user should be returned to after login. * * Returns: * HTTP_SEE_OTHER on success, an error otherwise. */ static int am_start_disco(request_rec *r, const char *return_to) { am_dir_cfg_rec *cfg = am_get_dir_cfg(r); const char *endpoint = am_get_endpoint_url(r); LassoServer *server; const char *sp_entity_id; const char *sep; const char *login_url; const char *discovery_url; server = am_get_lasso_server(r); if(server == NULL) { return HTTP_INTERNAL_SERVER_ERROR; } sp_entity_id = LASSO_PROVIDER(server)->ProviderID; login_url = apr_psprintf(r->pool, "%slogin?ReturnTo=%s", endpoint, am_urlencode(r->pool, return_to)); ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, "login_url = %s", login_url); /* If discovery URL already has a ? we append a & */ sep = (strchr(cfg->discovery_url, '?')) ? "&" : "?"; discovery_url = apr_psprintf(r->pool, "%s%sentityID=%s&" "return=%s&returnIDParam=IdP", cfg->discovery_url, sep, am_urlencode(r->pool, sp_entity_id), am_urlencode(r->pool, login_url)); ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, "discovery_url = %s", discovery_url); apr_table_setn(r->headers_out, "Location", discovery_url); return HTTP_SEE_OTHER; } /* This function returns the first configured IdP * * Parameters: * request_rec *r The request we received. * * Returns: * the providerID, or NULL if an error occured */ static const char *am_first_idp(request_rec *r) { LassoServer *server; GList *idp_list; const char *idp_providerid; server = am_get_lasso_server(r); if (server == NULL) return NULL; idp_list = g_hash_table_get_keys(server->providers); if (idp_list == NULL) return NULL; idp_providerid = idp_list->data; g_list_free(idp_list); return idp_providerid; } /* This function selects an IdP and returns its provider_id * * Parameters: * request_rec *r The request we received. * * Returns: * the provider_id, or NULL if an error occured */ static const char *am_get_idp(request_rec *r) { LassoServer *server; const char *idp_provider_id; server = am_get_lasso_server(r); if (server == NULL) return NULL; /* * If we have a single IdP, return that one. */ if (g_hash_table_size(server->providers) == 1) return am_first_idp(r); /* * If IdP discovery handed us an IdP, try to use it. */ idp_provider_id = am_extract_query_parameter(r->pool, r->args, "IdP"); if (idp_provider_id != NULL) { int rc; rc = am_urldecode((char *)idp_provider_id); if (rc != OK) { ap_log_rerror(APLOG_MARK, APLOG_ERR, rc, r, "Could not urldecode IdP discovery value."); idp_provider_id = NULL; } else { if (g_hash_table_lookup(server->providers, idp_provider_id) == NULL) idp_provider_id = NULL; } /* * If we do not know about it, fall back to default. */ if (idp_provider_id == NULL) { ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, "IdP discovery returned unknown or inexistant IdP"); idp_provider_id = am_first_idp(r); } return idp_provider_id; } /* * No IdP answered, use default * Perhaps we should redirect to an error page instead. */ return am_first_idp(r); } /* This function stores dumps of the LassoIdentity and LassoSession objects * for the given LassoProfile object. The dumps are stored in the session * belonging to the current request. * * Parameters: * request_rec *r The current request. * am_cache_entry_t *session The session we are creating. * LassoProfile *profile The profile object. * char *saml_response The full SAML 2.0 response message. * * Returns: * OK on success or HTTP_INTERNAL_SERVER_ERROR on failure. */ static int am_save_lasso_profile_state(request_rec *r, am_cache_entry_t *session, LassoProfile *profile, char *saml_response) { LassoIdentity *lasso_identity; LassoSession *lasso_session; gchar *identity_dump; gchar *session_dump; int ret; lasso_identity = lasso_profile_get_identity(profile); if(lasso_identity == NULL) { ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, "The current LassoProfile object doesn't contain a" " LassoIdentity object."); identity_dump = NULL; } else { identity_dump = lasso_identity_dump(lasso_identity); if(identity_dump == NULL) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Could not create a identity dump from the" " LassoIdentity object."); return HTTP_INTERNAL_SERVER_ERROR; } } lasso_session = lasso_profile_get_session(profile); if(lasso_session == NULL) { ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, "The current LassoProfile object doesn't contain a" " LassoSession object."); session_dump = NULL; } else { session_dump = lasso_session_dump(lasso_session); if(session_dump == NULL) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Could not create a session dump from the" " LassoSession object."); if(identity_dump != NULL) { g_free(identity_dump); } return HTTP_INTERNAL_SERVER_ERROR; } } /* Save the profile state. */ ret = am_cache_set_lasso_state(session, identity_dump, session_dump, saml_response); if(identity_dump != NULL) { g_free(identity_dump); } if(session_dump != NULL) { g_free(session_dump); } return ret; } /* Returns a SAML response * * Parameters: * request_rec *r The current request. * LassoProfile *profile The profile object. * * Returns: * HTTP_INTERNAL_SERVER_ERROR if an error occurs, HTTP_SEE_OTHER for the * Redirect binding and OK for the SOAP binding. */ static int am_return_logout_response(request_rec *r, LassoProfile *profile) { if (profile->msg_url && profile->msg_body) { /* POST binding response */ ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Error building logout response message." " POST binding is unsupported."); return HTTP_INTERNAL_SERVER_ERROR; } else if (profile->msg_url) { /* HTTP-Redirect binding response */ apr_table_setn(r->headers_out, "Location", apr_pstrdup(r->pool, profile->msg_url)); return HTTP_SEE_OTHER; } else if (profile->msg_body) { /* SOAP binding response */ ap_set_content_type(r, "text/xml"); ap_rputs(profile->msg_body, r); return OK; } else { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Error building logout response message." " There is no content to return."); return HTTP_INTERNAL_SERVER_ERROR; } } /* This function restores dumps of a LassoIdentity object and a LassoSession * object. The dumps are fetched from the session belonging to the current * request and restored to the given LassoProfile object. * * Parameters: * request_rec *r The current request. * LassoProfile *profile The profile object. * am_cache_entry_t *am_session The session structure. * * Returns: * OK on success or HTTP_INTERNAL_SERVER_ERROR on failure. */ static void am_restore_lasso_profile_state(request_rec *r, LassoProfile *profile, am_cache_entry_t *am_session) { const char *identity_dump; const char *session_dump; int rc; if(am_session == NULL) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Could not get auth_mellon session while attempting" " to restore the lasso profile state."); return; } identity_dump = am_cache_get_lasso_identity(am_session); if(identity_dump != NULL) { rc = lasso_profile_set_identity_from_dump(profile, identity_dump); if(rc < 0) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Could not restore identity from dump." " Lasso error: [%i] %s", rc, lasso_strerror(rc)); am_release_request_session(r, am_session); } } session_dump = am_cache_get_lasso_session(am_session); if(session_dump != NULL) { rc = lasso_profile_set_session_from_dump(profile, session_dump); if(rc < 0) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Could not restore session from dump." " Lasso error: [%i] %s", rc, lasso_strerror(rc)); am_release_request_session(r, am_session); } } } /* This function handles an IdP initiated logout request. * * Parameters: * request_rec *r The logout request. * LassoLogout *logout A LassoLogout object initiated with * the current session. * * Returns: * OK on success, or an error if any of the steps fail. */ static int am_handle_logout_request(request_rec *r, LassoLogout *logout, char *msg) { gint res = 0, rc = HTTP_OK; am_cache_entry_t *session = NULL; am_dir_cfg_rec *cfg = am_get_dir_cfg(r); /* Process the logout message. Ignore missing signature. */ res = lasso_logout_process_request_msg(logout, msg); #ifdef HAVE_lasso_profile_set_signature_verify_hint if(res != 0 && res != LASSO_DS_ERROR_SIGNATURE_NOT_FOUND) { if (apr_hash_get(cfg->do_not_verify_logout_signature, logout->parent.remote_providerID, APR_HASH_KEY_STRING)) { lasso_profile_set_signature_verify_hint(&logout->parent, LASSO_PROFILE_SIGNATURE_VERIFY_HINT_IGNORE); res = lasso_logout_process_request_msg(logout, msg); } } #endif if(res != 0 && res != LASSO_DS_ERROR_SIGNATURE_NOT_FOUND) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Error processing logout request message." " Lasso error: [%i] %s", res, lasso_strerror(res)); rc = HTTP_BAD_REQUEST; goto exit; } /* Search session using NameID */ if (! LASSO_IS_SAML2_NAME_ID(logout->parent.nameIdentifier)) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Error processing logout request message." " No NameID found"); rc = HTTP_BAD_REQUEST; goto exit; } session = am_get_request_session_by_nameid(r, ((LassoSaml2NameID*)logout->parent.nameIdentifier)->content); if (session == NULL) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Error processing logout request message." " No session found for NameID %s", ((LassoSaml2NameID*)logout->parent.nameIdentifier)->content); } if (session == NULL) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Error processing logout request message." " No session found."); } else { am_restore_lasso_profile_state(r, &logout->parent, session); } /* Validate the logout message. Ignore missing signature. */ res = lasso_logout_validate_request(logout); if(res != 0 && res != LASSO_DS_ERROR_SIGNATURE_NOT_FOUND && res != LASSO_PROFILE_ERROR_SESSION_NOT_FOUND) { ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, "Error validating logout request." " Lasso error: [%i] %s", res, lasso_strerror(res)); rc = HTTP_INTERNAL_SERVER_ERROR; goto exit; } /* We continue with the logout despite those errors. They could be * caused by the IdP believing that we are logged in when we are not. */ if (session != NULL && res != LASSO_PROFILE_ERROR_SESSION_NOT_FOUND) { /* We found a matching session -- delete it. */ am_delete_request_session(r, session); session = NULL; } /* Create response message. */ res = lasso_logout_build_response_msg(logout); if(res != 0) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Error building logout response message." " Lasso error: [%i] %s", res, lasso_strerror(res)); rc = HTTP_INTERNAL_SERVER_ERROR; goto exit; } rc = am_return_logout_response(r, &logout->parent); exit: if (session != NULL) { am_release_request_session(r, session); } lasso_logout_destroy(logout); return rc; } /* This function handles a logout response message from the IdP. We get * this message after we have sent a logout request to the IdP. * * Parameters: * request_rec *r The logout response request. * LassoLogout *logout A LassoLogout object initiated with * the current session. * * Returns: * OK on success, or an error if any of the steps fail. */ static int am_handle_logout_response(request_rec *r, LassoLogout *logout) { gint res; int rc; am_cache_entry_t *session; char *return_to; am_dir_cfg_rec *cfg = am_get_dir_cfg(r); res = lasso_logout_process_response_msg(logout, r->args); #ifdef HAVE_lasso_profile_set_signature_verify_hint if(res != 0 && res != LASSO_DS_ERROR_SIGNATURE_NOT_FOUND) { if (apr_hash_get(cfg->do_not_verify_logout_signature, logout->parent.remote_providerID, APR_HASH_KEY_STRING)) { lasso_profile_set_signature_verify_hint(&logout->parent, LASSO_PROFILE_SIGNATURE_VERIFY_HINT_IGNORE); res = lasso_logout_process_response_msg(logout, r->args); } } #endif if(res != 0) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Unable to process logout response." " Lasso error: [%i] %s", res, lasso_strerror(res)); lasso_logout_destroy(logout); return HTTP_BAD_REQUEST; } lasso_logout_destroy(logout); /* Delete the session. */ session = am_get_request_session(r); if(session != NULL) { am_delete_request_session(r, session); } return_to = am_extract_query_parameter(r->pool, r->args, "RelayState"); if(return_to == NULL) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "No RelayState parameter to logout response handler." " It is possible that your IdP doesn't support the" " RelayState parameter."); return HTTP_BAD_REQUEST; } rc = am_urldecode(return_to); if(rc != OK) { ap_log_rerror(APLOG_MARK, APLOG_ERR, rc, r, "Could not urldecode RelayState value in logout" " response."); return HTTP_BAD_REQUEST; } /* Check for bad characters in RelayState. */ rc = am_check_url(r, return_to); if (rc != OK) { return rc; } apr_table_setn(r->headers_out, "Location", return_to); return HTTP_SEE_OTHER; } /* This function initiates a logout request and sends it to the IdP. * * Parameters: * request_rec *r The logout response request. * LassoLogout *logout A LassoLogout object initiated with * the current session. * * Returns: * OK on success, or an error if any of the steps fail. */ static int am_init_logout_request(request_rec *r, LassoLogout *logout) { char *return_to; int rc; am_cache_entry_t *mellon_session; gint res; char *redirect_to; LassoProfile *profile; LassoSession *session; GList *assertion_list; LassoNode *assertion_n; LassoSaml2Assertion *assertion; LassoSaml2AuthnStatement *authnStatement; LassoSamlp2LogoutRequest *request; return_to = am_extract_query_parameter(r->pool, r->args, "ReturnTo"); rc = am_urldecode(return_to); if (rc != OK) { ap_log_rerror(APLOG_MARK, APLOG_ERR, rc, r, "Could not urldecode ReturnTo value."); return HTTP_BAD_REQUEST; } /* Disable the the local session (in case the IdP doesn't respond). */ mellon_session = am_get_request_session(r); if(mellon_session != NULL) { am_restore_lasso_profile_state(r, &logout->parent, mellon_session); mellon_session->logged_in = 0; am_release_request_session(r, mellon_session); } /* Create the logout request message. */ res = lasso_logout_init_request(logout, NULL, LASSO_HTTP_METHOD_REDIRECT); /* Early non failing return. */ if (res != 0) { if(res == LASSO_PROFILE_ERROR_SESSION_NOT_FOUND) { ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, "User attempted to initiate logout without being" " loggged in."); } else if (res == LASSO_LOGOUT_ERROR_UNSUPPORTED_PROFILE || res == LASSO_PROFILE_ERROR_UNSUPPORTED_PROFILE) { ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, "Current identity provider " "does not support single logout. Destroying local session only."); } else if(res != 0) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Unable to create logout request." " Lasso error: [%i] %s", res, lasso_strerror(res)); lasso_logout_destroy(logout); return HTTP_INTERNAL_SERVER_ERROR; } lasso_logout_destroy(logout); /* Check for bad characters in ReturnTo. */ rc = am_check_url(r, return_to); if (rc != OK) { return rc; } /* Redirect to the page the user should be sent to after logout. */ apr_table_setn(r->headers_out, "Location", return_to); return HTTP_SEE_OTHER; } profile = LASSO_PROFILE(logout); /* We need to set the SessionIndex in the LogoutRequest to the SessionIndex * we received during the login operation. This is not needed since release * 2.3.0. */ if (lasso_check_version(2, 3, 0, LASSO_CHECK_VERSION_NUMERIC) == 0) { session = lasso_profile_get_session(profile); assertion_list = lasso_session_get_assertions( session, profile->remote_providerID); if(! assertion_list || LASSO_IS_SAML2_ASSERTION(assertion_list->data) == FALSE) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "No assertions found for the current session."); lasso_logout_destroy(logout); return HTTP_INTERNAL_SERVER_ERROR; } /* We currently only look at the first assertion in the list * lasso_session_get_assertions returns. */ assertion_n = assertion_list->data; assertion = LASSO_SAML2_ASSERTION(assertion_n); /* We assume that the first authnStatement contains the data we want. */ authnStatement = LASSO_SAML2_AUTHN_STATEMENT(assertion->AuthnStatement->data); if(!authnStatement) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "No AuthnStatement found in the current assertion."); lasso_logout_destroy(logout); return HTTP_INTERNAL_SERVER_ERROR; } if(authnStatement->SessionIndex) { request = LASSO_SAMLP2_LOGOUT_REQUEST(profile->request); request->SessionIndex = g_strdup(authnStatement->SessionIndex); } } /* Set the RelayState parameter to the return url (if we have one). */ if(return_to) { profile->msg_relayState = g_strdup(return_to); } /* Serialize the request message into a url which we can redirect to. */ res = lasso_logout_build_request_msg(logout); if(res != 0) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Unable to serialize lasso logout message." " Lasso error: [%i] %s", res, lasso_strerror(res)); lasso_logout_destroy(logout); return HTTP_INTERNAL_SERVER_ERROR; } /* Set the redirect url. */ redirect_to = apr_pstrdup(r->pool, LASSO_PROFILE(logout)->msg_url); /* Check if the lasso library added the RelayState. If lasso didn't add * a RelayState parameter, then we add one ourself. This should hopefully * be removed in the future. */ if(return_to != NULL && strstr(redirect_to, "&RelayState=") == NULL && strstr(redirect_to, "?RelayState=") == NULL) { /* The url didn't contain the relaystate parameter. */ redirect_to = apr_pstrcat( r->pool, redirect_to, "&RelayState=", am_urlencode(r->pool, return_to), NULL ); } apr_table_setn(r->headers_out, "Location", redirect_to); lasso_logout_destroy(logout); /* Redirect (without including POST data if this was a POST request. */ return HTTP_SEE_OTHER; } /* This function handles requests to the logout handler. * * Parameters: * request_rec *r The request. * * Returns: * OK on success, or an error if any of the steps fail. */ static int am_handle_logout(request_rec *r) { LassoServer *server; LassoLogout *logout; server = am_get_lasso_server(r); if(server == NULL) { return HTTP_INTERNAL_SERVER_ERROR; } logout = lasso_logout_new(server); if(logout == NULL) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Error creating lasso logout object."); return HTTP_INTERNAL_SERVER_ERROR; } /* Check which type of request to the logout handler this is. * We have three types: * - logout requests: The IdP sends a logout request to this service. * it can be either through HTTP-Redirect or SOAP. * - logout responses: We have sent a logout request to the IdP, and * are receiving a response. * - We want to initiate a logout request. */ /* First check for IdP-initiated SOAP logout request */ if ((r->args == NULL) && (r->method_number == M_POST)) { int rc; char *post_data; rc = am_read_post_data(r, &post_data, NULL); if (rc != OK) { ap_log_rerror(APLOG_MARK, APLOG_ERR, rc, r, "Error reading POST data."); return HTTP_INTERNAL_SERVER_ERROR; } return am_handle_logout_request(r, logout, post_data); } else if(am_extract_query_parameter(r->pool, r->args, "SAMLRequest") != NULL) { /* SAMLRequest - logout request from the IdP. */ return am_handle_logout_request(r, logout, r->args); } else if(am_extract_query_parameter(r->pool, r->args, "SAMLResponse") != NULL) { /* SAMLResponse - logout response from the IdP. */ return am_handle_logout_response(r, logout); } else if(am_extract_query_parameter(r->pool, r->args, "ReturnTo") != NULL) { /* RedirectTo - SP initiated logout. */ return am_init_logout_request(r, logout); } else { /* Unknown request to the logout handler. */ ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "No known parameters passed to the logout" " handler. Query string was \"%s\". To initiate" " a logout, you need to pass a \"ReturnTo\"" " parameter with a url to the web page the user should" " be redirected to after a successful logout.", r->args); return HTTP_BAD_REQUEST; } } /* This function parses a timestamp for a SAML 2.0 condition. * * Parameters: * request_rec *r The current request. Used for logging of errors. * const char *timestamp The timestamp we should parse. Must be on * the following format: "YYYY-MM-DDThh:mm:ssZ" * * Returns: * An apr_time_t value with the timestamp, or 0 on error. */ static apr_time_t am_parse_timestamp(request_rec *r, const char *timestamp) { size_t len; int i; char c; const char *expected; apr_time_exp_t time_exp; apr_time_t res; apr_status_t rc; len = strlen(timestamp); /* Verify length of timestamp. */ if(len < 20){ ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, "Invalid length of timestamp: \"%s\".", timestamp); } /* Verify components of timestamp. */ for(i = 0; i < len - 1; i++) { c = timestamp[i]; expected = NULL; switch(i) { case 4: case 7: /* Matches " - - " */ if(c != '-') { expected = "'-'"; } break; case 10: /* Matches " T " */ if(c != 'T') { expected = "'T'"; } break; case 13: case 16: /* Matches " : : " */ if(c != ':') { expected = "':'"; } break; case 19: /* Matches " ." */ if (c != '.') { expected = "'.'"; } break; default: /* Matches "YYYY MM DD hh mm ss uuuuuu" */ if(c < '0' || c > '9') { expected = "a digit"; } break; } if(expected != NULL) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Invalid character in timestamp at position %i." " Expected %s, got '%c'. Full timestamp: \"%s\"", i, expected, c, timestamp); return 0; } } if (timestamp[len - 1] != 'Z') { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Timestamp wasn't in UTC (did not end with 'Z')." " Full timestamp: \"%s\"", timestamp); return 0; } time_exp.tm_usec = 0; if (len > 20) { /* Subsecond precision. */ if (len > 27) { /* Timestamp has more than microsecond precision. Just clip it to * microseconds. */ len = 27; } len -= 1; /* Drop the 'Z' off the end. */ for (i = 20; i < len; i++) { time_exp.tm_usec = time_exp.tm_usec * 10 + timestamp[i] - '0'; } for (i = len; i < 26; i++) { time_exp.tm_usec *= 10; } } time_exp.tm_sec = (timestamp[17] - '0') * 10 + (timestamp[18] - '0'); time_exp.tm_min = (timestamp[14] - '0') * 10 + (timestamp[15] - '0'); time_exp.tm_hour = (timestamp[11] - '0') * 10 + (timestamp[12] - '0'); time_exp.tm_mday = (timestamp[8] - '0') * 10 + (timestamp[9] - '0'); time_exp.tm_mon = (timestamp[5] - '0') * 10 + (timestamp[6] - '0') - 1; time_exp.tm_year = (timestamp[0] - '0') * 1000 + (timestamp[1] - '0') * 100 + (timestamp[2] - '0') * 10 + (timestamp[3] - '0') - 1900; time_exp.tm_wday = 0; /* Unknown. */ time_exp.tm_yday = 0; /* Unknown. */ time_exp.tm_isdst = 0; /* UTC, no daylight savings time. */ time_exp.tm_gmtoff = 0; /* UTC, no offset from UTC. */ rc = apr_time_exp_gmt_get(&res, &time_exp); if(rc != APR_SUCCESS) { ap_log_rerror(APLOG_MARK, APLOG_ERR, rc, r, "Error converting timestamp \"%s\".", timestamp); return 0; } return res; } /* Validate the subject on an Assertion. * * request_rec *r The current request. Used to log * errors. * LassoSaml2Assertion *assertion The assertion we will validate. * const char *url The current URL. * * Returns: * OK on success, HTTP_BAD_REQUEST on failure. */ static int am_validate_subject(request_rec *r, LassoSaml2Assertion *assertion, const char *url) { apr_time_t now; apr_time_t t; LassoSaml2SubjectConfirmation *sc; LassoSaml2SubjectConfirmationData *scd; am_dir_cfg_rec *cfg = am_get_dir_cfg(r); if (assertion->Subject == NULL) { /* No Subject to validate. */ return OK; } else if (!LASSO_IS_SAML2_SUBJECT(assertion->Subject)) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Wrong type of Subject node."); return HTTP_BAD_REQUEST; } if (assertion->Subject->SubjectConfirmation == NULL) { /* No SubjectConfirmation. */ return OK; } else if (!LASSO_IS_SAML2_SUBJECT_CONFIRMATION(assertion->Subject->SubjectConfirmation)) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Wrong type of SubjectConfirmation node."); return HTTP_BAD_REQUEST; } sc = assertion->Subject->SubjectConfirmation; if (sc->Method == NULL || strcmp(sc->Method, "urn:oasis:names:tc:SAML:2.0:cm:bearer")) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Invalid Method in SubjectConfirmation."); return HTTP_BAD_REQUEST; } scd = sc->SubjectConfirmationData; if (scd == NULL) { /* Nothing to verify. */ return OK; } else if (!LASSO_IS_SAML2_SUBJECT_CONFIRMATION_DATA(scd)) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Wrong type of SubjectConfirmationData node."); return HTTP_BAD_REQUEST; } now = apr_time_now(); if (scd->NotBefore) { t = am_parse_timestamp(r, scd->NotBefore); if (t == 0) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Invalid timestamp in NotBefore in SubjectConfirmationData."); return HTTP_BAD_REQUEST; } if (t - 60000000 > now) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "NotBefore in SubjectConfirmationData was in the future."); return HTTP_BAD_REQUEST; } } if (scd->NotOnOrAfter) { t = am_parse_timestamp(r, scd->NotOnOrAfter); if (t == 0) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Invalid timestamp in NotOnOrAfter in SubjectConfirmationData."); return HTTP_BAD_REQUEST; } if (now >= t + 60000000) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "NotOnOrAfter in SubjectConfirmationData was in the past."); return HTTP_BAD_REQUEST; } } if (scd->Recipient) { if (strcmp(scd->Recipient, url)) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Wrong Recipient in SubjectConfirmationData. Current URL is: %s, Recipient is %s", url, scd->Recipient); return HTTP_BAD_REQUEST; } } if (scd->Address && CFG_VALUE(cfg, subject_confirmation_data_address_check)) { if (strcasecmp(scd->Address, am_compat_request_ip(r))) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Wrong Address in SubjectConfirmationData." "Current address is \"%s\", but should have been \"%s\".", am_compat_request_ip(r), scd->Address); return HTTP_BAD_REQUEST; } } return OK; } /* Validate the conditions on an Assertion. * * Parameters: * request_rec *r The current request. Used to log * errors. * LassoSaml2Assertion *assertion The assertion we will validate. * const char *providerID The providerID of the SP. * * Returns: * OK on success, HTTP_BAD_REQUEST on failure. */ static int am_validate_conditions(request_rec *r, LassoSaml2Assertion *assertion, const char *providerID) { LassoSaml2Conditions *conditions; apr_time_t now; apr_time_t t; GList *i; LassoSaml2AudienceRestriction *ar; conditions = assertion->Conditions; if (!LASSO_IS_SAML2_CONDITIONS(conditions)) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Wrong type of Conditions node."); return HTTP_BAD_REQUEST; } if (conditions->Condition != NULL) { /* This is a list of LassoSaml2ConditionAbstract - if it * isn't empty, we have an unsupported condition. */ ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Unsupported condition in Assertion."); return HTTP_BAD_REQUEST; } now = apr_time_now(); if (conditions->NotBefore) { t = am_parse_timestamp(r, conditions->NotBefore); if (t == 0) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Invalid timestamp in NotBefore in Condition."); return HTTP_BAD_REQUEST; } if (t - 60000000 > now) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "NotBefore in Condition was in the future."); return HTTP_BAD_REQUEST; } } if (conditions->NotOnOrAfter) { t = am_parse_timestamp(r, conditions->NotOnOrAfter); if (t == 0) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Invalid timestamp in NotOnOrAfter in Condition."); return HTTP_BAD_REQUEST; } if (now >= t + 60000000) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "NotOnOrAfter in Condition was in the past."); return HTTP_BAD_REQUEST; } } for (i = g_list_first(conditions->AudienceRestriction); i != NULL; i = g_list_next(i)) { ar = i->data; if (!LASSO_IS_SAML2_AUDIENCE_RESTRICTION(ar)) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Wrong type of AudienceRestriction node."); return HTTP_BAD_REQUEST; } if (ar->Audience == NULL || strcmp(ar->Audience, providerID)) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Invalid Audience in Conditions. Should be: %s", providerID); return HTTP_BAD_REQUEST; } } return OK; } /* This function sets the session expire timestamp based on NotOnOrAfter * attribute of a condition element. * * Parameters: * request_rec *r The current request. Used to log * errors. * am_cache_entry_t *session The current session. * LassoSaml2Assertion *assertion The assertion which we will extract * the conditions from. * * Returns: * Nothing. */ static void am_handle_session_expire(request_rec *r, am_cache_entry_t *session, LassoSaml2Assertion *assertion) { GList *authn_itr; LassoSaml2AuthnStatement *authn; const char *not_on_or_after; apr_time_t t; for(authn_itr = g_list_first(assertion->AuthnStatement); authn_itr != NULL; authn_itr = g_list_next(authn_itr)) { authn = authn_itr->data; if (!LASSO_IS_SAML2_AUTHN_STATEMENT(authn)) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Wrong type of AuthnStatement node."); continue; } /* Find timestamp. */ not_on_or_after = authn->SessionNotOnOrAfter; if(not_on_or_after == NULL) { continue; } /* Parse timestamp. */ t = am_parse_timestamp(r, not_on_or_after); if(t == 0) { continue; } /* Updates the expires timestamp if this one is earlier than the * previous timestamp. */ am_cache_update_expires(session, t); } } /* This function is for decoding and storing attributes with the feide * encoding. It takes in an attribute name and a value. The value is split * into multiple values. We base64 decode these values, and store them in the * session data. * * Parameters: * request_rec *r The current request. * am_cache_entry_t *session The current session. * const char *name Name of the attribute. * const char *value The value(s) of the attribute. * * Returns: * OK on success or an error from am_cache_env_append(...) if it was unable * to store the attribute. */ static int am_store_attribute_feide(request_rec *r, am_cache_entry_t *session, const char *name, const char *value) { char *edit_value; char *start; char *next; int len; int ret; /* We need to be able to change the value. */ edit_value = apr_pstrdup(r->pool, value); for(start = edit_value; start != NULL; start = next) { /* The values are separated by '_'. */ next = strchr(start, '_'); if(next != NULL) { /* Insert null-terminator after current value. */ *next = '\0'; /* The next value begins at next+1. */ next++; } /* Now start points to the current value, which we have * null-terminated. next points to the next value, or NULL if * this is the last value. */ /* base64-decode current value. * From looking at the source of apr_base64_decode_binary, it * appears to be safe to use in-place. */ len = apr_base64_decode_binary((unsigned char *)start, start); /* Add null-terminator at end of string. */ start[len] = '\0'; /* Store current name-value-pair. */ ret = am_cache_env_append(session, name, start); if(ret != OK) { return ret; } } return OK; } /* This function is for storing attributes without any encoding. We just store * the attribute as it is. * * Parameters: * request_rec *r The current request. * am_cache_entry_t *session The current session. * const char *name The name of the attribute. * const char *value The value of the attribute. * * Returns: * OK on success or an error from am_cache_env_append(...) if it failed. */ static int am_store_attribute_none(request_rec *r, am_cache_entry_t *session, const char *name, const char *value) { /* Store current name-value-pair. */ return am_cache_env_append(session, name, value); } /* This function passes a name-value pair to the decoder selected by the * MellonDecoder configuration option. The decoder will decode the value * and store it in the session data. * * Parameters: * request_rec *r The current request. * am_cache_entry_t *session The current session. * const char *name The name of the attribute. * const char *value The value of the attribute. * * Returns: * OK on success or an error from the attribute decoder if it failed. */ static int am_store_attribute(request_rec *r, am_cache_entry_t *session, const char *name, const char *value) { am_dir_cfg_rec *dir_cfg; dir_cfg = am_get_dir_cfg(r); switch(dir_cfg->decoder) { case am_decoder_none: return am_store_attribute_none(r, session, name, value); case am_decoder_feide: return am_store_attribute_feide(r, session, name, value); default: return am_store_attribute_none(r, session, name, value); } } /* Add all the attributes from an assertion to the session data for the * current user. * * Parameters: * am_cache_entry_t *s The current session. * request_rec *r The current request. * const char *name_id The name identifier we received from * the IdP. * LassoSaml2Assertion *assertion The assertion. * * Returns: * HTTP_BAD_REQUEST if we couldn't find the session id of the user, or * OK if no error occured. */ static int add_attributes(am_cache_entry_t *session, request_rec *r, const char *name_id, LassoSaml2Assertion *assertion) { am_dir_cfg_rec *dir_cfg; GList *atr_stmt_itr; LassoSaml2AttributeStatement *atr_stmt; GList *atr_itr; LassoSaml2Attribute *attribute; GList *value_itr; LassoSaml2AttributeValue *value; LassoMiscTextNode *value_text; int ret; dir_cfg = am_get_dir_cfg(r); /* Set expires to whatever is set by MellonSessionLength. */ if(dir_cfg->session_length == -1) { /* -1 means "use default. The current default is 86400 seconds. */ am_cache_update_expires(session, apr_time_now() + apr_time_make(86400, 0)); } else { am_cache_update_expires(session, apr_time_now() + apr_time_make(dir_cfg->session_length, 0)); } /* Save session information. */ ret = am_cache_env_append(session, "NAME_ID", name_id); if(ret != OK) { return ret; } /* Update expires timestamp of session. */ am_handle_session_expire(r, session, assertion); /* assertion->AttributeStatement is a list of * LassoSaml2AttributeStatement objects. */ for(atr_stmt_itr = g_list_first(assertion->AttributeStatement); atr_stmt_itr != NULL; atr_stmt_itr = g_list_next(atr_stmt_itr)) { atr_stmt = atr_stmt_itr->data; if (!LASSO_IS_SAML2_ATTRIBUTE_STATEMENT(atr_stmt)) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Wrong type of AttributeStatement node."); continue; } /* atr_stmt->Attribute is list of LassoSaml2Attribute objects. */ for(atr_itr = g_list_first(atr_stmt->Attribute); atr_itr != NULL; atr_itr = g_list_next(atr_itr)) { attribute = atr_itr->data; if (!LASSO_IS_SAML2_ATTRIBUTE(attribute)) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Wrong type of Attribute node."); continue; } /* attribute->AttributeValue is a list of * LassoSaml2AttributeValue objects. */ for(value_itr = g_list_first(attribute->AttributeValue); value_itr != NULL; value_itr = g_list_next(value_itr)) { value = value_itr->data; if (!LASSO_IS_SAML2_ATTRIBUTE_VALUE(value)) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Wrong type of AttributeValue node."); continue; } /* value->any is a list with the child nodes of the * AttributeValue element. * * We assume that the list contains a single text node. */ if(value->any == NULL) { ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, "AttributeValue element was empty."); continue; } /* Verify that this is a LassoMiscTextNode object. */ if(!LASSO_IS_MISC_TEXT_NODE(value->any->data)) { ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, "AttributeValue element contained an " " element which wasn't a text node."); continue; } value_text = LASSO_MISC_TEXT_NODE(value->any->data); /* Decode and save the attribute. */ ret = am_store_attribute(r, session, attribute->Name, value_text->content); if(ret != OK) { return ret; } } } } return OK; } /* This function validates that the received assertion verify the security level configured by * MellonAuthnContextClassRef directives */ static int am_validate_authn_context_class_ref(request_rec *r, LassoSaml2Assertion *assertion) { int i = 0; LassoSaml2AuthnStatement *authn_statement = NULL; LassoSaml2AuthnContext *authn_context = NULL; am_dir_cfg_rec *dir_cfg; apr_array_header_t *refs; dir_cfg = am_get_dir_cfg(r); refs = dir_cfg->authn_context_class_ref; if (! refs->nelts) return OK; if (! assertion->AuthnStatement) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Missing AuthnStatement in assertion, returning BadRequest."); return HTTP_BAD_REQUEST; } /* we only consider the first AuthnStatement, I do not know of any idp * sending more than one. */ authn_statement = g_list_first(assertion->AuthnStatement)->data; if (! authn_statement->AuthnContext) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Missing AuthnContext in assertion, returning BadRequest."); return HTTP_BAD_REQUEST; } authn_context = authn_statement->AuthnContext; if (! authn_context->AuthnContextClassRef) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Missing AuthnContextClassRef in assertion, returning Forbidden."); return HTTP_FORBIDDEN; } for (i = 0; i < refs->nelts; i++) { const char *ref = ((char **)refs->elts)[i]; if (strcmp(ref, authn_context->AuthnContextClassRef) == 0) { ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, "AuthnContextClassRef (%s) matches the " "MellonAuthnContextClassRef directive, " "access can be granted.", authn_context->AuthnContextClassRef); return OK; } } ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "AuthnContextClassRef (%s) does not match the " "MellonAuthnContextClassRef directive, returning " "Forbidden.", authn_context->AuthnContextClassRef); return HTTP_FORBIDDEN; } /* This function finishes handling of a login response after it has been parsed * by the HTTP-POST or HTTP-Artifact handler. * * Parameters: * request_rec *r The current request. * LassoLogin *login The login object which has been initialized with the * data we have received from the IdP. * char *relay_state The RelayState parameter from the POST data or from * the request url. This parameter is urlencoded, and * this function will urldecode it in-place. Therefore it * must be possible to overwrite the data. * * Returns: * A HTTP status code which should be returned to the client. */ static int am_handle_reply_common(request_rec *r, LassoLogin *login, char *relay_state, char *saml_response) { char *url; char *chr; const char *name_id; LassoSamlp2Response *response; LassoSaml2Assertion *assertion; const char *in_response_to; am_dir_cfg_rec *dir_cfg; am_cache_entry_t *session; int rc; const char *idp; url = am_reconstruct_url(r); chr = strchr(url, '?'); if (! chr) { chr = strchr(url, ';'); } if (chr) { *chr = '\0'; } dir_cfg = am_get_dir_cfg(r); if(LASSO_PROFILE(login)->nameIdentifier == NULL) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "No acceptable name identifier found in" " SAML 2.0 response."); lasso_login_destroy(login); return HTTP_BAD_REQUEST; } name_id = LASSO_SAML2_NAME_ID(LASSO_PROFILE(login)->nameIdentifier) ->content; response = LASSO_SAMLP2_RESPONSE(LASSO_PROFILE(login)->response); if (response->parent.Destination) { if (strcmp(response->parent.Destination, url)) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Invalid Destination on Response. Should be: %s", url); lasso_login_destroy(login); return HTTP_BAD_REQUEST; } } if (g_list_length(response->Assertion) == 0) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "No Assertion in response."); lasso_login_destroy(login); return HTTP_BAD_REQUEST; } if (g_list_length(response->Assertion) > 1) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "More than one Assertion in response."); lasso_login_destroy(login); return HTTP_BAD_REQUEST; } assertion = g_list_first(response->Assertion)->data; if (!LASSO_IS_SAML2_ASSERTION(assertion)) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Wrong type of Assertion node."); lasso_login_destroy(login); return HTTP_BAD_REQUEST; } rc = am_validate_subject(r, assertion, url); if (rc != OK) { lasso_login_destroy(login); return rc; } rc = am_validate_conditions(r, assertion, LASSO_PROVIDER(LASSO_PROFILE(login)->server)->ProviderID); if (rc != OK) { lasso_login_destroy(login); return rc; } in_response_to = response->parent.InResponseTo; if(in_response_to != NULL) { /* This is SP-initiated login. Check that we have a cookie. */ if(am_cookie_get(r) == NULL) { /* Missing cookie. */ ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, "User has disabled cookies, or has lost" " the cookie before returning from the SAML2" " login server."); if(dir_cfg->no_cookie_error_page != NULL) { apr_table_setn(r->headers_out, "Location", dir_cfg->no_cookie_error_page); lasso_login_destroy(login); return HTTP_SEE_OTHER; } else { /* Return 400 Bad Request when the user hasn't set a * no-cookie error page. */ lasso_login_destroy(login); return HTTP_BAD_REQUEST; } } } /* Check AuthnContextClassRef */ rc = am_validate_authn_context_class_ref(r, assertion); if (rc != OK) { lasso_login_destroy(login); return rc; } /* Create a new session. */ session = am_new_request_session(r); if(session == NULL) { ap_log_error(APLOG_MARK, APLOG_ERR, 0, NULL, "am_new_request_session() failed"); return HTTP_INTERNAL_SERVER_ERROR; } rc = add_attributes(session, r, name_id, assertion); if(rc != OK) { am_release_request_session(r, session); lasso_login_destroy(login); return rc; } /* If requested, save the IdP ProviderId */ if(dir_cfg->idpattr != NULL) { idp = LASSO_PROFILE(login)->remote_providerID; if(idp != NULL) { rc = am_cache_env_append(session, dir_cfg->idpattr, idp); if(rc != OK) { am_release_request_session(r, session); lasso_login_destroy(login); return rc; } } } rc = lasso_login_accept_sso(login); if(rc < 0) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Unable to accept SSO message." " Lasso error: [%i] %s", rc, lasso_strerror(rc)); am_release_request_session(r, session); lasso_login_destroy(login); return HTTP_INTERNAL_SERVER_ERROR; } /* Save the profile state. */ rc = am_save_lasso_profile_state(r, session, LASSO_PROFILE(login), saml_response); if(rc != OK) { am_release_request_session(r, session); lasso_login_destroy(login); return rc; } /* Mark user as logged in. */ session->logged_in = 1; am_release_request_session(r, session); lasso_login_destroy(login); /* No RelayState - we don't know what to do. Use default login path. */ if(relay_state == NULL || strlen(relay_state) == 0) { dir_cfg = am_get_dir_cfg(r); apr_table_setn(r->headers_out, "Location", dir_cfg->login_path); return HTTP_SEE_OTHER; } rc = am_urldecode(relay_state); if (rc != OK) { ap_log_rerror(APLOG_MARK, APLOG_ERR, rc, r, "Could not urldecode RelayState value."); return HTTP_BAD_REQUEST; } /* Check for bad characters in RelayState. */ rc = am_check_url(r, relay_state); if (rc != OK) { return rc; } apr_table_setn(r->headers_out, "Location", relay_state); /* HTTP_SEE_OTHER should be a redirect where the browser doesn't repeat * the POST data to the new page. */ return HTTP_SEE_OTHER; } /* This function handles responses to login requests received with the * HTTP-POST binding. * * Parameters: * request_rec *r The request we received. * * Returns: * HTTP_SEE_OTHER on success, or an error on failure. */ static int am_handle_post_reply(request_rec *r) { int rc; char *post_data; char *saml_response; LassoServer *server; LassoLogin *login; char *relay_state; am_dir_cfg_rec *dir_cfg = am_get_dir_cfg(r); int i, err; /* Make sure that this is a POST request. */ if(r->method_number != M_POST) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Expected POST request for HTTP-POST endpoint." " Got a %s request instead.", r->method); /* According to the documentation for request_rec, a handler which * doesn't handle a request method, should set r->allowed to the * methods it handles, and return DECLINED. * However, the default handler handles GET-requests, so for GET * requests the handler should return HTTP_METHOD_NOT_ALLOWED. */ r->allowed = M_POST; if(r->method_number == M_GET) { return HTTP_METHOD_NOT_ALLOWED; } else { return DECLINED; } } /* Read POST-data. */ rc = am_read_post_data(r, &post_data, NULL); if (rc != OK) { ap_log_rerror(APLOG_MARK, APLOG_ERR, rc, r, "Error reading POST data."); return rc; } /* Extract the SAMLResponse-field from the data. */ saml_response = am_extract_query_parameter(r->pool, post_data, "SAMLResponse"); if (saml_response == NULL) { ap_log_rerror(APLOG_MARK, APLOG_ERR, rc, r, "Could not find SAMLResponse field in POST data."); return HTTP_BAD_REQUEST; } rc = am_urldecode(saml_response); if (rc != OK) { ap_log_rerror(APLOG_MARK, APLOG_ERR, rc, r, "Could not urldecode SAMLResponse value."); return rc; } server = am_get_lasso_server(r); if(server == NULL) { return HTTP_INTERNAL_SERVER_ERROR; } login = lasso_login_new(server); if (login == NULL) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Failed to initialize LassoLogin object."); return HTTP_INTERNAL_SERVER_ERROR; } /* Process login responce. */ rc = lasso_login_process_authn_response_msg(login, saml_response); if (rc != 0) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Error processing authn response." " Lasso error: [%i] %s", rc, lasso_strerror(rc)); lasso_login_destroy(login); err = HTTP_BAD_REQUEST; for (i = 0; auth_mellon_errormap[i].lasso_error != 0; i++) { if (auth_mellon_errormap[i].lasso_error == rc) { err = auth_mellon_errormap[i].http_error; break; } } if (err == HTTP_UNAUTHORIZED) { if (dir_cfg->no_success_error_page != NULL) { apr_table_setn(r->headers_out, "Location", dir_cfg->no_success_error_page); return HTTP_SEE_OTHER; } } return err; } /* Extract RelayState parameter. */ relay_state = am_extract_query_parameter(r->pool, post_data, "RelayState"); /* Finish handling the reply with the common handler. */ return am_handle_reply_common(r, login, relay_state, saml_response); } /* This function handles responses to login requests which use the * HTTP-Artifact binding. * * Parameters: * request_rec *r The request we received. * * Returns: * HTTP_SEE_OTHER on success, or an error on failure. */ static int am_handle_artifact_reply(request_rec *r) { int rc; LassoServer *server; LassoLogin *login; char *response; char *relay_state; char *saml_art; char *post_data; /* Make sure that this is a GET request. */ if(r->method_number != M_GET && r->method_number != M_POST) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Expected GET or POST request for the HTTP-Artifact endpoint." " Got a %s request instead.", r->method); /* According to the documentation for request_rec, a handler which * doesn't handle a request method, should set r->allowed to the * methods it handles, and return DECLINED. * However, the default handler handles GET-requests, so for GET * requests the handler should return HTTP_METHOD_NOT_ALLOWED. * This endpoints handles GET requests, so it isn't necessary to * check for method_number == M_GET. */ r->allowed = M_GET; return DECLINED; } server = am_get_lasso_server(r); if(server == NULL) { return HTTP_INTERNAL_SERVER_ERROR; } login = lasso_login_new(server); if (login == NULL) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Failed to initialize LassoLogin object."); return HTTP_INTERNAL_SERVER_ERROR; } /* Parse artifact url. */ if (r->method_number == M_GET) { rc = lasso_login_init_request(login, r->args, LASSO_HTTP_METHOD_ARTIFACT_GET); if(rc < 0) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Failed to handle login response." " Lasso error: [%i] %s", rc, lasso_strerror(rc)); lasso_login_destroy(login); return HTTP_BAD_REQUEST; } } else { rc = am_read_post_data(r, &post_data, NULL); if (rc != OK) { ap_log_rerror(APLOG_MARK, APLOG_ERR, rc, r, "Error reading POST data."); return HTTP_BAD_REQUEST; } saml_art = am_extract_query_parameter(r->pool, post_data, "SAMLart"); if (saml_art == NULL) { ap_log_rerror(APLOG_MARK, APLOG_ERR, rc, r, "Error reading POST data missing SAMLart form parameter."); return HTTP_BAD_REQUEST; } ap_unescape_url(saml_art); rc = lasso_login_init_request(login, saml_art, LASSO_HTTP_METHOD_ARTIFACT_POST); if(rc < 0) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Failed to handle login response." " Lasso error: [%i] %s", rc, lasso_strerror(rc)); lasso_login_destroy(login); return HTTP_BAD_REQUEST; } } /* Prepare SOAP request. */ rc = lasso_login_build_request_msg(login); if(rc < 0) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Failed to prepare SOAP message for HTTP-Artifact" " resolution." " Lasso error: [%i] %s", rc, lasso_strerror(rc)); lasso_login_destroy(login); return HTTP_INTERNAL_SERVER_ERROR; } /* Do the SOAP request. */ rc = am_httpclient_post_str( r, LASSO_PROFILE(login)->msg_url, LASSO_PROFILE(login)->msg_body, "text/xml", (void**)&response, NULL ); if(rc != OK) { lasso_login_destroy(login); return HTTP_INTERNAL_SERVER_ERROR; } rc = lasso_login_process_response_msg(login, response); if(rc != 0) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Failed to handle HTTP-Artifact response data." " Lasso error: [%i] %s", rc, lasso_strerror(rc)); lasso_login_destroy(login); return HTTP_INTERNAL_SERVER_ERROR; } /* Extract the RelayState parameter. */ if (r->method_number == M_GET) { relay_state = am_extract_query_parameter(r->pool, r->args, "RelayState"); } else { relay_state = am_extract_query_parameter(r->pool, post_data, "RelayState"); } /* Finish handling the reply with the common handler. */ return am_handle_reply_common(r, login, relay_state, ""); } /* This function builds web form inputs for a saved POST request, * in multipart/form-data format. * * Parameters: * request_rec *r The request * const char *post_data The savec POST request * * Returns: * The web form fragment, or NULL on failure. */ const char *am_post_mkform_multipart(request_rec *r, const char *post_data) { const char *mime_part; const char *boundary; char *l1; char *post_form = ""; /* Replace CRLF by LF */ post_data = am_strip_cr(r, post_data); if ((boundary = am_xstrtok(r, post_data, "\n", &l1)) == NULL) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Cannot figure initial boundary"); return NULL; } for (mime_part = am_xstrtok(r, post_data, boundary, &l1); mime_part; mime_part = am_xstrtok(r, NULL, boundary, &l1)) { const char *hdr; const char *name = NULL; const char *value = NULL; const char *input_item; /* End of MIME data */ if (strcmp(mime_part, "--\n") == 0) break; /* Remove leading CRLF */ if (strstr(mime_part, "\n") == mime_part) mime_part += 1; /* Empty part */ if (*mime_part == '\0') continue; /* Find Content-Disposition header * Looking for * Content-Disposition: form-data; name="the_name"\n */ hdr = am_get_mime_header(r, mime_part, "Content-Disposition"); if (hdr == NULL) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "No Content-Disposition header in MIME section,"); continue; } name = am_get_header_attr(r, hdr, "form-data", "name"); if (name == NULL) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Unexpected Content-Disposition header: \"%s\"", hdr); continue; } if ((value = am_get_mime_body(r, mime_part)) == NULL) value = ""; input_item = apr_psprintf(r->pool, " \n", am_htmlencode(r, name), am_htmlencode(r, value)); post_form = apr_pstrcat(r->pool, post_form, input_item, NULL); } return post_form; } /* This function builds web form inputs for a saved POST request, * in application/x-www-form-urlencoded format * * Parameters: * request_rec *r The request * const char *post_data The savec POST request * * Returns: * The web form fragment, or NULL on failure. */ const char *am_post_mkform_urlencoded(request_rec *r, const char *post_data) { const char *item; char *last; char *post_form = ""; for (item = am_xstrtok(r, post_data, "&", &last); item; item = am_xstrtok(r, NULL, "&", &last)) { char *l1; char *name; char *value; const char *input_item; name = (char *)am_xstrtok(r, item, "=", &l1); value = (char *)am_xstrtok(r, NULL, "=", &l1); if (name == NULL) continue; if (value == NULL) value = (char *)""; if (am_urldecode(name) != OK) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "urldecode(\"%s\") failed", name); return NULL; } if (am_urldecode(value) != OK) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "urldecode(\"%s\") failed", value); return NULL; } input_item = apr_psprintf(r->pool, " \n", am_htmlencode(r, name), am_htmlencode(r, value)); post_form = apr_pstrcat(r->pool, post_form, input_item, NULL); } return post_form; } /* This function handles responses to repost request * * Parameters: * request_rec *r The request we received. * * Returns: * OK on success, or an error on failure. */ static int am_handle_repost(request_rec *r) { am_mod_cfg_rec *mod_cfg; const char *query; const char *enctype; char *charset; char *psf_id; char *cp; char *psf_filename; char *post_data; const char *post_form; char *output; char *return_url; const char *(*post_mkform)(request_rec *, const char *); if (am_cookie_get(r) == NULL) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Repost query without a session"); return HTTP_FORBIDDEN; } mod_cfg = am_get_mod_cfg(r->server); if (!mod_cfg->post_dir) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Repost query without MellonPostDirectory."); return HTTP_NOT_FOUND; } query = r->parsed_uri.query; enctype = am_extract_query_parameter(r->pool, query, "enctype"); if (enctype == NULL) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Bad repost query: missing enctype"); return HTTP_BAD_REQUEST; } if (strcmp(enctype, "urlencoded") == 0) { enctype = "application/x-www-form-urlencoded"; post_mkform = am_post_mkform_urlencoded; } else if (strcmp(enctype, "multipart") == 0) { enctype = "multipart/form-data"; post_mkform = am_post_mkform_multipart; } else { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Bad repost query: invalid enctype \"%s\".", enctype); return HTTP_BAD_REQUEST; } charset = am_extract_query_parameter(r->pool, query, "charset"); if (charset != NULL) { if (am_urldecode(charset) != OK) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Bad repost query: invalid charset \"%s\"", charset); return HTTP_BAD_REQUEST; } /* Check that charset is sane */ for (cp = charset; *cp; cp++) { if (!apr_isalnum(*cp) && (*cp != '-') && (*cp != '_')) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Bad repost query: invalid charset \"%s\"", charset); return HTTP_BAD_REQUEST; } } } psf_id = am_extract_query_parameter(r->pool, query, "id"); if (psf_id == NULL) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Bad repost query: missing id"); return HTTP_BAD_REQUEST; } /* Check that Id is sane */ for (cp = psf_id; *cp; cp++) { if (!apr_isalnum(*cp)) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Bad repost query: invalid id \"%s\"", psf_id); return HTTP_BAD_REQUEST; } } return_url = am_extract_query_parameter(r->pool, query, "ReturnTo"); if (return_url == NULL) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Invalid or missing query ReturnTo parameter."); return HTTP_BAD_REQUEST; } if (am_urldecode(return_url) != OK) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Bad repost query: return"); return HTTP_BAD_REQUEST; } psf_filename = apr_psprintf(r->pool, "%s/%s", mod_cfg->post_dir, psf_id); post_data = am_getfile(r->pool, r->server, psf_filename); if (post_data == NULL) { /* Unable to load repost data. Just redirect us instead. */ ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, "Bad repost query: cannot find \"%s\"", psf_filename); apr_table_setn(r->headers_out, "Location", return_url); return HTTP_SEE_OTHER; } if ((post_form = (*post_mkform)(r, post_data)) == NULL) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "am_post_mkform() failed"); return HTTP_INTERNAL_SERVER_ERROR; } if (charset != NULL) { ap_set_content_type(r, apr_psprintf(r->pool, "text/html; charset=\"%s\"", charset)); charset = apr_psprintf(r->pool, " accept-charset=\"%s\"", charset); } else { ap_set_content_type(r, "text/html"); charset = (char *)""; } output = apr_psprintf(r->pool, "\n" "\n" " \n" " SAML rePOST request\n" " \n" " \n" " \n" "
\n%s" " \n" "
\n" " \n" "\n", am_htmlencode(r, return_url), enctype, charset, post_form); ap_rputs(output, r); return OK; } /* This function handles responses to metadata request * * Parameters: * request_rec *r The request we received. * * Returns: * OK on success, or an error on failure. */ static int am_handle_metadata(request_rec *r) { #ifdef HAVE_lasso_server_new_from_buffers am_dir_cfg_rec *cfg = am_get_dir_cfg(r); LassoServer *server; const char *data; server = am_get_lasso_server(r); if(server == NULL) return HTTP_INTERNAL_SERVER_ERROR; cfg = cfg->inherit_server_from; data = cfg->sp_metadata_file; if (data == NULL) return HTTP_INTERNAL_SERVER_ERROR; ap_set_content_type(r, "application/samlmetadata+xml"); ap_rputs(data, r); return OK; #else /* ! HAVE_lasso_server_new_from_buffers */ ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "metadata publishing require lasso 2.2.2 or higher"); return HTTP_NOT_FOUND; #endif } /* Send AuthnRequest using HTTP-Redirect binding. * * Note that this method frees the LassoLogin object. * * Parameters: * request_rec *r * LassoLogin *login * * Returns: * HTTP_SEE_OTHER on success, or an error on failure. */ static int am_send_authn_request_redirect(request_rec *r, LassoLogin *login) { char *redirect_to; /* The URL we should send the message to. */ redirect_to = apr_pstrdup(r->pool, LASSO_PROFILE(login)->msg_url); /* Check if the lasso library added the RelayState. If lasso didn't add * a RelayState parameter, then we add one ourself. This should hopefully * be removed in the future. */ if(strstr(redirect_to, "&RelayState=") == NULL && strstr(redirect_to, "?RelayState=") == NULL) { /* The url didn't contain the relaystate parameter. */ redirect_to = apr_pstrcat( r->pool, redirect_to, "&RelayState=", am_urlencode(r->pool, LASSO_PROFILE(login)->msg_relayState), NULL ); } apr_table_setn(r->headers_out, "Location", redirect_to); lasso_login_destroy(login); /* We don't want to include POST data (in case this was a POST request). */ return HTTP_SEE_OTHER; } /* Send AuthnRequest using HTTP-POST binding. * * Note that this method frees the LassoLogin object. * * Parameters: * request_rec *r The request we are processing. * LassoLogin *login The login message. * * Returns: * OK on success, or an error on failure. */ static int am_send_authn_request_post(request_rec *r, LassoLogin *login) { char *url; char *message; char *relay_state; char *output; url = am_htmlencode(r, LASSO_PROFILE(login)->msg_url); message = am_htmlencode(r, LASSO_PROFILE(login)->msg_body); relay_state = am_htmlencode(r, LASSO_PROFILE(login)->msg_relayState); lasso_login_destroy(login); output = apr_psprintf(r->pool, "\n" "\n" " \n" " \n" " POST data\n" " \n" " \n" " \n" "
\n" " \n" " \n" " \n" "
\n" " \n" "\n", url, message, relay_state); ap_set_content_type(r, "text/html"); ap_rputs(output, r); return OK; } /* Create and send an authentication request. * * Parameters: * request_rec *r The request we are processing. * const char *idp The entityID of the IdP. * const char *return_to The URL we should redirect to when receiving the request. * int is_passive Whether to send a passive request. * * Returns: * HTTP response code indicating success or failure. */ static int am_send_authn_request(request_rec *r, const char *idp, const char *return_to, int is_passive) { LassoServer *server; LassoProvider *provider; LassoLogin *login; LassoSamlp2AuthnRequest *request; LassoHttpMethod http_method; char *sso_url; gint ret; am_dir_cfg_rec *dir_cfg; char *acs_url; dir_cfg = am_get_dir_cfg(r); /* Add cookie for cookie test. We know that we should have * a valid cookie when we return from the IdP after SP-initiated * login. */ am_cookie_set(r, "cookietest"); server = am_get_lasso_server(r); if(server == NULL) { return HTTP_INTERNAL_SERVER_ERROR; } /* Find our IdP. */ provider = lasso_server_get_provider(server, idp); if (provider == NULL) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Could not find metadata for the IdP \"%s\".", idp); return HTTP_INTERNAL_SERVER_ERROR; } /* Determine what binding and endpoint we should use when * sending the request. */ http_method = LASSO_HTTP_METHOD_REDIRECT; sso_url = lasso_provider_get_metadata_one( provider, "SingleSignOnService HTTP-Redirect"); if (sso_url == NULL) { /* HTTP-Redirect unsupported - try HTTP-POST. */ http_method = LASSO_HTTP_METHOD_POST; sso_url = lasso_provider_get_metadata_one( provider, "SingleSignOnService HTTP-POST"); } if (sso_url == NULL) { /* Both HTTP-Redirect and HTTP-POST unsupported - give up. */ ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Could not find a supported SingleSignOnService endpoint" " for the IdP \"%s\".", idp); return HTTP_INTERNAL_SERVER_ERROR; } login = lasso_login_new(server); if(login == NULL) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Error creating LassoLogin object from LassoServer."); g_free(sso_url); return HTTP_INTERNAL_SERVER_ERROR; } ret = lasso_login_init_authn_request(login, idp, http_method); if(ret != 0) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Error creating login request." " Lasso error: [%i] %s", ret, lasso_strerror(ret)); g_free(sso_url); lasso_login_destroy(login); return HTTP_INTERNAL_SERVER_ERROR; } request = LASSO_SAMLP2_AUTHN_REQUEST(LASSO_PROFILE(login)->request); if(request->NameIDPolicy == NULL) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Error creating login request. Please verify the " "MellonSPMetadataFile directive."); g_free(sso_url); lasso_login_destroy(login); return HTTP_INTERNAL_SERVER_ERROR; } request->ForceAuthn = FALSE; request->IsPassive = is_passive; request->NameIDPolicy->AllowCreate = TRUE; LASSO_SAMLP2_REQUEST_ABSTRACT(request)->Consent = g_strdup(LASSO_SAML2_CONSENT_IMPLICIT); /* Add AuthnContextClassRef */ if (dir_cfg->authn_context_class_ref->nelts) { apr_array_header_t *refs = dir_cfg->authn_context_class_ref; int i = 0; LassoSamlp2RequestedAuthnContext *req_authn_context; req_authn_context = (LassoSamlp2RequestedAuthnContext*) lasso_samlp2_requested_authn_context_new(); request->RequestedAuthnContext = req_authn_context; for (i = 0; i < refs->nelts; i++) { const char *ref = ((char **)refs->elts)[i]; req_authn_context->AuthnContextClassRef = g_list_append(req_authn_context->AuthnContextClassRef, g_strdup(ref)); ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, "adding AuthnContextClassRef %s to the " "AuthnRequest", ref); } } /* * Make sure the Destination attribute is set to the IdP * SingleSignOnService endpoint. This is required for * Shibboleth 2 interoperability, and older versions of * lasso (at least up to 2.2.91) did not do it. */ if (LASSO_SAMLP2_REQUEST_ABSTRACT(request)->Destination == NULL) { LASSO_SAMLP2_REQUEST_ABSTRACT(request)->Destination = g_strdup(sso_url); } /* sso_url no longer needed. */ g_free(sso_url); /* Some IdPs insist they want to see an AttributeConsumerServiceURL * attribute in the authentication request, so try to add one if the * metadata contains one */ acs_url = lasso_provider_get_assertion_consumer_service_url( LASSO_PROVIDER(server), NULL); if (acs_url) { request->AssertionConsumerServiceURL = g_strdup(acs_url); /* Can't set request->ProtocolBinding (which is usually set along side * AssertionConsumerServiceURL) as there is no immediate function * like lasso_provider_get_assertion_consumer_service_url to get them. * So leave that empty for now, it is not strictly required */ } LASSO_PROFILE(login)->msg_relayState = g_strdup(return_to); ret = lasso_login_build_authn_request_msg(login); if(ret != 0) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Error building login request." " Lasso error: [%i] %s", ret, lasso_strerror(ret)); lasso_login_destroy(login); return HTTP_INTERNAL_SERVER_ERROR; } /* Time to actually send the authentication request. */ switch (http_method) { case LASSO_HTTP_METHOD_REDIRECT: return am_send_authn_request_redirect(r, login); case LASSO_HTTP_METHOD_POST: return am_send_authn_request_post(r, login); default: /* We should never get here. */ ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Unsupported http_method."); lasso_login_destroy(login); return HTTP_INTERNAL_SERVER_ERROR; } } /* Handle the "auth" endpoint. * * This endpoint is included for backwards-compatibility. * * Parameters: * request_rec *r The request we received. * * Returns: * OK or HTTP_SEE_OTHER on success, an error on failure. */ static int am_handle_auth(request_rec *r) { am_dir_cfg_rec *cfg = am_get_dir_cfg(r); const char *relay_state; relay_state = am_reconstruct_url(r); /* Check if IdP discovery is in use and no IdP was selected yet */ if ((cfg->discovery_url != NULL) && (am_extract_query_parameter(r->pool, r->args, "IdP") == NULL)) { return am_start_disco(r, relay_state); } /* If IdP discovery is in use and we have an IdP selected, * set the relay_state */ if (cfg->discovery_url != NULL) { char *return_url; return_url = am_extract_query_parameter(r->pool, r->args, "ReturnTo"); if ((return_url != NULL) && am_urldecode((char *)return_url) == 0) relay_state = return_url; } return am_send_authn_request(r, am_get_idp(r), relay_state, FALSE); } /* This function handles requests to the login handler. * * Parameters: * request_rec *r The request. * * Returns: * OK on success, or an error if any of the steps fail. */ static int am_handle_login(request_rec *r) { am_dir_cfg_rec *cfg = am_get_dir_cfg(r); char *idp_param; const char *idp; char *return_to; char *is_passive_str; int is_passive; int ret; return_to = am_extract_query_parameter(r->pool, r->args, "ReturnTo"); if(return_to == NULL) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Missing required ReturnTo parameter."); return HTTP_BAD_REQUEST; } ret = am_urldecode(return_to); if(ret != OK) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Error urldecoding ReturnTo parameter."); return ret; } idp_param = am_extract_query_parameter(r->pool, r->args, "IdP"); if(idp_param != NULL) { ret = am_urldecode(idp_param); if(ret != OK) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Error urldecoding IdP parameter."); return ret; } } is_passive_str = am_extract_query_parameter(r->pool, r->args, "IsPassive"); if(is_passive_str != NULL) { ret = am_urldecode((char*)is_passive_str); if(ret != OK) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Error urldecoding IsPassive parameter."); return ret; } if(!strcmp(is_passive_str, "true")) { is_passive = TRUE; } else if(!strcmp(is_passive_str, "false")) { is_passive = FALSE; } else { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Invalid value for IsPassive parameter - must be \"true\" or \"false\"."); return HTTP_BAD_REQUEST; } } else { is_passive = FALSE; } if(idp_param != NULL) { idp = idp_param; } else if(cfg->discovery_url) { if(is_passive) { /* We cannot currently do discovery with passive authentication requests. */ ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Discovery service with passive authentication request unsupported."); return HTTP_INTERNAL_SERVER_ERROR; } return am_start_disco(r, return_to); } else { /* No discovery service -- just use the default IdP. */ idp = am_get_idp(r); } return am_send_authn_request(r, idp, return_to, is_passive); } /* This function probes an URL (HTTP GET) * * Parameters: * request_rec *r The request. * const char *url The URL * int timeout Timeout in seconds * * Returns: * OK on success, or an error if any of the steps fail. */ static int am_probe_url(request_rec *r, const char *url, int timeout) { void *dontcare; apr_size_t len; long status; int error; status = 0; if ((error = am_httpclient_get(r, url, &dontcare, &len, timeout, &status)) != OK) return error; if (status != HTTP_OK) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Probe on \"%s\" returned HTTP %ld", url, status); return status; } return OK; } /* This function handles requests to the probe discovery handler * * Parameters: * request_rec *r The request. * * Returns: * OK on success, or an error if any of the steps fail. */ static int am_handle_probe_discovery(request_rec *r) { am_dir_cfg_rec *cfg = am_get_dir_cfg(r); LassoServer *server; const char *disco_idp = NULL; int timeout; char *return_to; char *idp_param; char *redirect_url; int ret; server = am_get_lasso_server(r); if(server == NULL) { return HTTP_INTERNAL_SERVER_ERROR; } /* * If built-in IdP discovery is not configured, return error. * For now we only have the get-metadata metadata method, so this * information is not saved in configuration nor it is checked here. */ timeout = cfg->probe_discovery_timeout; if (timeout == -1) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "probe discovery handler invoked but not " "configured. Plase set MellonProbeDiscoveryTimeout."); return HTTP_INTERNAL_SERVER_ERROR; } /* * Check for mandatory arguments early to avoid sending * probles for nothing. */ return_to = am_extract_query_parameter(r->pool, r->args, "return"); if(return_to == NULL) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Missing required return parameter."); return HTTP_BAD_REQUEST; } ret = am_urldecode(return_to); if (ret != OK) { ap_log_rerror(APLOG_MARK, APLOG_ERR, ret, r, "Could not urldecode return value."); return HTTP_BAD_REQUEST; } idp_param = am_extract_query_parameter(r->pool, r->args, "returnIDParam"); if(idp_param == NULL) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Missing required returnIDParam parameter."); return HTTP_BAD_REQUEST; } ret = am_urldecode(idp_param); if (ret != OK) { ap_log_rerror(APLOG_MARK, APLOG_ERR, ret, r, "Could not urldecode returnIDParam value."); return HTTP_BAD_REQUEST; } /* * Proceed with built-in IdP discovery. * * First try sending probes to IdP configured for discovery. * Second send probes for all configured IdP * The first to answer is chosen. * If none answer, use the first configured IdP */ if (!apr_is_empty_table(cfg->probe_discovery_idp)) { const apr_array_header_t *header; apr_table_entry_t *elts; const char *url; const char *idp; int i; header = apr_table_elts(cfg->probe_discovery_idp); elts = (apr_table_entry_t *)header->elts; for (i = 0; i < header->nelts; i++) { idp = elts[i].key; url = elts[i].val; if (am_probe_url(r, url, timeout) == OK) { disco_idp = idp; break; } } } else { GList *iter; GList *idp_list; const char *idp; idp_list = g_hash_table_get_keys(server->providers); for (iter = idp_list; iter != NULL; iter = iter->next) { idp = iter->data; if (am_probe_url(r, idp, timeout) == OK) { disco_idp = idp; break; } } g_list_free(idp_list); } /* * On failure, try default */ if (disco_idp == NULL) { disco_idp = am_first_idp(r); if (disco_idp == NULL) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "probeDiscovery found no usable IdP."); return HTTP_INTERNAL_SERVER_ERROR; } else { ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, "probeDiscovery " "failed, trying default IdP %s", disco_idp); } } else { ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, r, "probeDiscovery using %s", disco_idp); } redirect_url = apr_psprintf(r->pool, "%s%s%s=%s", return_to, strchr(return_to, '?') ? "&" : "?", am_urlencode(r->pool, idp_param), am_urlencode(r->pool, disco_idp)); apr_table_setn(r->headers_out, "Location", redirect_url); return HTTP_SEE_OTHER; } /* This function handles responses to request on our endpoint * * Parameters: * request_rec *r The request we received. * * Returns: * OK on success, or an error on failure. */ int am_handler(request_rec *r) { am_dir_cfg_rec *cfg = am_get_dir_cfg(r); const char *endpoint; /* Check if this is a request for one of our endpoints. We check if * the uri starts with the path set with the MellonEndpointPath * configuration directive. */ if(strstr(r->uri, cfg->endpoint_path) != r->uri) return DECLINED; endpoint = &r->uri[strlen(cfg->endpoint_path)]; if (!strcmp(endpoint, "metadata")) { return am_handle_metadata(r); } else if (!strcmp(endpoint, "repost")) { return am_handle_repost(r); } else if(!strcmp(endpoint, "postResponse")) { return am_handle_post_reply(r); } else if(!strcmp(endpoint, "artifactResponse")) { return am_handle_artifact_reply(r); } else if(!strcmp(endpoint, "auth")) { return am_handle_auth(r); } else if(!strcmp(endpoint, "logout") || !strcmp(endpoint, "logoutRequest")) { /* logoutRequest is included for backwards-compatibility * with version 0.0.6 and older. */ return am_handle_logout(r); } else if(!strcmp(endpoint, "login")) { return am_handle_login(r); } else if(!strcmp(endpoint, "probeDisco")) { return am_handle_probe_discovery(r); } else { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Endpoint \"%s\" not handled by mod_auth_mellon.", endpoint); return HTTP_NOT_FOUND; } } /** * Trigger a login operation from a "normal" request. * * Parameters: * request_rec *r The request we received. * * Returns: * HTTP_SEE_OTHER on success, or an error on failure. */ static int am_start_auth(request_rec *r) { am_dir_cfg_rec *cfg = am_get_dir_cfg(r); const char *endpoint = am_get_endpoint_url(r); const char *return_to; const char *idp; const char *login_url; return_to = am_reconstruct_url(r); /* If this is a POST request, attempt to save it */ if (r->method_number == M_POST) { if (CFG_VALUE(cfg, post_replay)) { if (am_save_post(r, &return_to) != OK) return HTTP_INTERNAL_SERVER_ERROR; } else { ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, "POST data dropped because we do not have a" " MellonPostReplay is not enabled."); } } /* Check if IdP discovery is in use. */ if (cfg->discovery_url) { return am_start_disco(r, return_to); } idp = am_get_idp(r); login_url = apr_psprintf(r->pool, "%slogin?ReturnTo=%s&IdP=%s", endpoint, am_urlencode(r->pool, return_to), am_urlencode(r->pool, idp)); ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, "Redirecting to login URL: %s", login_url); apr_table_setn(r->headers_out, "Location", login_url); return HTTP_SEE_OTHER; } int am_auth_mellon_user(request_rec *r) { am_dir_cfg_rec *dir = am_get_dir_cfg(r); int return_code = HTTP_UNAUTHORIZED; am_cache_entry_t *session; /* check if we are a subrequest. if we are, then just return OK * without any checking since these cannot be injected (heh). */ if (r->main) return OK; /* Check that the user has enabled authentication for this directory. */ if(dir->enable_mellon == am_enable_off || dir->enable_mellon == am_enable_default) { return DECLINED; } /* Set defaut Cache-Control headers within this location */ am_set_cache_control_headers(r); /* Check if this is a request for one of our endpoints. We check if * the uri starts with the path set with the MellonEndpointPath * configuration directive. */ if(strstr(r->uri, dir->endpoint_path) == r->uri) { /* No access control on our internal endpoints. */ return OK; } /* Get the session of this request. */ session = am_get_request_session(r); if(dir->enable_mellon == am_enable_auth) { /* This page requires the user to be authenticated and authorized. */ if(session == NULL || !session->logged_in) { /* We don't have a valid session. */ if(session) { /* Release the session. */ am_release_request_session(r, session); } /* Send the user to the authentication page on the IdP. */ return am_start_auth(r); } /* Verify that the user has access to this resource. */ return_code = am_check_permissions(r, session); if(return_code != OK) { am_release_request_session(r, session); return return_code; } /* The user has been authenticated, and we can now populate r->user * and the r->subprocess_env with values from the session store. */ am_cache_env_populate(r, session); /* Release the session. */ am_release_request_session(r, session); return OK; } else { /* dir->enable_mellon == am_enable_info: * We should pass information about the user to the web application * if the user is authorized to access this resource. * However, we shouldn't attempt to do any access control. */ if(session != NULL && session->logged_in && am_check_permissions(r, session) == OK) { /* The user is authenticated and has access to the resource. * Now we populate the environment with information about * the user. */ am_cache_env_populate(r, session); } if(session != NULL) { /* Release the session. */ am_release_request_session(r, session); } /* We shouldn't really do any access control, so we always return * DECLINED. */ return DECLINED; } } int am_check_uid(request_rec *r) { am_dir_cfg_rec *dir = am_get_dir_cfg(r); am_cache_entry_t *session; int return_code = HTTP_UNAUTHORIZED; /* check if we are a subrequest. if we are, then just return OK * without any checking since these cannot be injected (heh). */ if (r->main) return OK; /* Check if this is a request for one of our endpoints. We check if * the uri starts with the path set with the MellonEndpointPath * configuration directive. */ if(strstr(r->uri, dir->endpoint_path) == r->uri) { /* No access control on our internal endpoints. */ return OK; } /* Get the session of this request. */ session = am_get_request_session(r); /* If we don't have a session, then we can't authorize the user. */ if(session == NULL) { return HTTP_UNAUTHORIZED; } /* If the user isn't logged in, then we can't authorize the user. */ if(!session->logged_in) { am_release_request_session(r, session); return HTTP_UNAUTHORIZED; } /* Verify that the user has access to this resource. */ return_code = am_check_permissions(r, session); if(return_code != OK) { am_release_request_session(r, session); return HTTP_UNAUTHORIZED; } /* The user has been authenticated, and we can now populate r->user * and the r->subprocess_env with values from the session store. */ am_cache_env_populate(r, session); /* Release the session. */ am_release_request_session(r, session); return OK; }