\n"
" SAML rePOST request\n"
" \n"
" \n"
" \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;
am_diag_printf(r, "enter function %s\n", __func__);
server = am_get_lasso_server(r);
if(server == NULL)
return HTTP_INTERNAL_SERVER_ERROR;
cfg = cfg->inherit_server_from;
data = cfg->sp_metadata_file ? cfg->sp_metadata_file->contents : NULL;
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 */
AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r,
"metadata publishing require lasso 2.2.2 or higher");
return HTTP_NOT_FOUND;
#endif
}
/* Use Lasso Login to set the HTTP content & headers for HTTP-Redirect binding.
*
* Parameters:
* request_rec *r
* LassoLogin *login
*
* Returns:
* HTTP_SEE_OTHER on success, or an error on failure.
*/
static int am_set_authn_request_redirect_content(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);
/* We don't want to include POST data (in case this was a POST request). */
return HTTP_SEE_OTHER;
}
/* Use Lasso Login to set the HTTP content & headers for HTTP-POST binding.
*
* 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_set_authn_request_post_content(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);
output = apr_psprintf(r->pool,
"\n"
"\n"
" \n"
" \n"
" POST data\n"
" \n"
" \n"
" \n"
" \n"
" \n"
"\n",
url, message, relay_state);
ap_set_content_type(r, "text/html");
ap_rputs(output, r);
return OK;
}
/* Use Lasso Login to set the HTTP content & headers for PAOS binding.
*
* 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_set_authn_request_paos_content(request_rec *r, LassoLogin *login)
{
ap_set_content_type(r, MEDIA_TYPE_PAOS);
ap_rputs(LASSO_PROFILE(login)->msg_body, r);
return OK;
}
/*
* Create and initialize LassoLogin object
*
* This function creates a LassoLogin object and initializes it to the
* greatest extent possible to allow it to be shared by multiple
* callers. There are two return values. The function return is an
* error code, the login_return parameter is a pointer in which to
* receive the LassoLogin object. The caller MUST free the returned
* login object using lasso_login_destroy() in all cases (even when
* this function returns an error), the only execption is if the
* returned LassoLogin is NULL.
*
* Parameters:
* r The request we are processing.
* login_return The returned LassoLogin object (caller must free)
* idp The provider id of remote Idp
* [optional, may be NULL]
* http_method Specifies the SAML profile to use
* destination_url If the idp parameter is non-NULL this should be
* the URL of the IdP endpoint the message is being sent to
* [optional, may be NULL]
* assertion_consumer_service_url
* The URL of this SP's endpoint which will receive the
* SAML assertion
* return_to_url Used to initialize the RelayState value
* is_passive The SAML IsPassive flag
*
* Returns:
* OK on success, HTTP error code otherwise
*
*/
static int am_init_authn_request_common(request_rec *r,
LassoLogin **login_return,
const char *idp,
LassoHttpMethod http_method,
const char *destination_url,
const char *assertion_consumer_service_url,
const char *return_to_url,
int is_passive)
{
gint ret;
am_dir_cfg_rec *dir_cfg;
LassoServer *server;
LassoLogin *login;
LassoSamlp2AuthnRequest *request;
const char *sp_name;
*login_return = NULL;
dir_cfg = am_get_dir_cfg(r);
server = am_get_lasso_server(r);
if (server == NULL) {
return HTTP_INTERNAL_SERVER_ERROR;
}
login = lasso_login_new(server);
if(login == NULL) {
AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r,
"Error creating LassoLogin object from LassoServer.");
return HTTP_INTERNAL_SERVER_ERROR;
}
*login_return = login;
ret = lasso_login_init_authn_request(login, idp, http_method);
if(ret != 0) {
AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r,
"Error creating login request."
" Lasso error: [%i] %s", ret, lasso_strerror(ret));
return HTTP_INTERNAL_SERVER_ERROR;
}
request = LASSO_SAMLP2_AUTHN_REQUEST(LASSO_PROFILE(login)->request);
if (request->NameIDPolicy == NULL) {
AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r,
"Error creating login request. Please verify the "
"MellonSPMetadataFile directive.");
return HTTP_INTERNAL_SERVER_ERROR;
}
/*
* 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 (destination_url &&
LASSO_SAMLP2_REQUEST_ABSTRACT(request)->Destination == NULL) {
lasso_assign_string(LASSO_SAMLP2_REQUEST_ABSTRACT(request)->Destination,
destination_url);
}
if (assertion_consumer_service_url) {
lasso_assign_string(request->AssertionConsumerServiceURL,
assertion_consumer_service_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 */
}
request->ForceAuthn = FALSE;
request->IsPassive = is_passive;
request->NameIDPolicy->AllowCreate = TRUE;
sp_name = am_get_config_langstring(dir_cfg->sp_org_display_name, NULL);
if (sp_name) {
lasso_assign_string(request->ProviderName, sp_name);
}
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));
AM_LOG_RERROR(APLOG_MARK, APLOG_DEBUG, 0, r,
"adding AuthnContextClassRef %s to the "
"AuthnRequest", ref);
}
if (dir_cfg->authn_context_comparison_type != NULL) {
lasso_assign_string(request->RequestedAuthnContext->Comparison,
dir_cfg->authn_context_comparison_type);
}
}
LASSO_PROFILE(login)->msg_relayState = g_strdup(return_to_url);
#ifdef HAVE_ECP
{
am_req_cfg_rec *req_cfg;
ECPServiceOptions unsupported_ecp_options;
req_cfg = am_get_req_cfg(r);
/*
* Currently we only support the WANT_AUTHN_SIGNED ECP option,
* if a client sends us anything else let them know it's not
* implemented.
*
* We do test for CHANNEL_BINDING below but that's because if
* and when we support it we don't want to forget channel
* bindings require the authn request to be signed.
*/
unsupported_ecp_options =
req_cfg->ecp_service_options &
~ECP_SERVICE_OPTION_WANT_AUTHN_SIGNED;
if (unsupported_ecp_options) {
AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r,
"Unsupported ECP service options [%s]",
am_ecp_service_options_str(r->pool,
unsupported_ecp_options));
return HTTP_NOT_IMPLEMENTED;
}
/*
* The signature hint must be set prior to calling
* lasso_login_build_authn_request_msg
*/
if (req_cfg->ecp_service_options &
(ECP_SERVICE_OPTION_WANT_AUTHN_SIGNED |
ECP_SERVICE_OPTION_CHANNEL_BINDING)) {
/*
* authnRequest should be signed if the client requested it
* or if channel bindings are enabled.
*/
lasso_profile_set_signature_hint(LASSO_PROFILE(login),
LASSO_PROFILE_SIGNATURE_HINT_FORCE);
}
}
#endif
ret = lasso_login_build_authn_request_msg(login);
if (ret != 0) {
AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r,
"Error building login request."
" Lasso error: [%i] %s", ret, lasso_strerror(ret));
return HTTP_INTERNAL_SERVER_ERROR;
}
return OK;
}
/* Use Lasso Login to set the HTTP content & headers for selected binding.
*
* Parameters:
* request_rec *r The request we are processing.
* LassoLogin *login The login message.
*
* Returns:
* HTTP response code
*/
static int am_set_authn_request_content(request_rec *r, LassoLogin *login)
{
am_diag_log_lasso_node(r, 0, LASSO_PROFILE(login)->request,
"SAML AuthnRequest: http_method=%s URL=%s",
am_diag_lasso_http_method_str(login->http_method),
LASSO_PROFILE(login)->msg_url);
switch (login->http_method) {
case LASSO_HTTP_METHOD_REDIRECT:
return am_set_authn_request_redirect_content(r, login);
case LASSO_HTTP_METHOD_POST:
return am_set_authn_request_post_content(r, login);
case LASSO_HTTP_METHOD_PAOS:
return am_set_authn_request_paos_content(r, login);
default:
/* We should never get here. */
AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r,
"Unsupported http_method.");
return HTTP_INTERNAL_SERVER_ERROR;
}
}
#ifdef HAVE_ECP
/* Build an IDPList whose members have an endpoint supporing
* the protocol_type and http_method.
*/
static LassoNode *
am_get_idp_list(const LassoServer *server, LassoMdProtocolType protocol_type, LassoHttpMethod http_method)
{
GList *idp_entity_ids = NULL;
GList *entity_id = NULL;
GList *idp_entries = NULL;
LassoSamlp2IDPList *idp_list;
LassoSamlp2IDPEntry *idp_entry;
idp_list = LASSO_SAMLP2_IDP_LIST(lasso_samlp2_idp_list_new());
idp_entity_ids =
lasso_server_get_filtered_provider_list(server,
LASSO_PROVIDER_ROLE_IDP,
protocol_type, http_method);
for (entity_id = g_list_first(idp_entity_ids); entity_id != NULL;
entity_id = g_list_next(entity_id)) {
idp_entry = LASSO_SAMLP2_IDP_ENTRY(lasso_samlp2_idp_entry_new());
idp_entry->ProviderID = g_strdup(entity_id->data);
/* RFE: we should have a mechanism to obtain these values */
idp_entry->Name = NULL;
idp_entry->Loc = NULL;
idp_entries = g_list_append(idp_entries, idp_entry);
}
lasso_release_list_of_strings(idp_entity_ids);
idp_list->IDPEntry = idp_entries;
return LASSO_NODE(idp_list);
}
/* Send AuthnRequest using PAOS binding.
*
* Parameters:
* request_rec *r
*
* Returns:
* OK on success, or an error on failure.
*/
static int am_send_paos_authn_request(request_rec *r)
{
gint ret;
am_dir_cfg_rec *dir_cfg;
LassoServer *server;
LassoLogin *login;
const char *relay_state = NULL;
char *assertion_consumer_service_url;
int is_passive = FALSE;
dir_cfg = am_get_dir_cfg(r);
server = am_get_lasso_server(r);
if(server == NULL) {
return HTTP_INTERNAL_SERVER_ERROR;
}
relay_state = am_reconstruct_url(r);
assertion_consumer_service_url =
am_get_assertion_consumer_service_by_binding(LASSO_PROVIDER(server),
"PAOS");
ret = am_init_authn_request_common(r, &login,
NULL, LASSO_HTTP_METHOD_PAOS, NULL,
assertion_consumer_service_url,
relay_state, is_passive);
g_free(assertion_consumer_service_url);
if (ret != OK) {
if (login) {
lasso_login_destroy(login);
}
return ret;
}
if (CFG_VALUE(dir_cfg, ecp_send_idplist)) {
lasso_profile_set_idp_list(LASSO_PROFILE(login),
am_get_idp_list(LASSO_PROFILE(login)->server,
LASSO_MD_PROTOCOL_TYPE_SINGLE_SIGN_ON,
LASSO_HTTP_METHOD_SOAP));
}
ret = am_set_authn_request_content(r, login);
lasso_login_destroy(login);
return ret;
}
#endif /* HAVE_ECP */
/* 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 The value of the IsPassive flag in
*
* Returns:
* HTTP response code indicating success or failure.
*/
static int am_send_login_authn_request(request_rec *r, const char *idp,
const char *return_to_url,
int is_passive)
{
int ret;
LassoServer *server;
LassoProvider *provider;
LassoHttpMethod http_method;
char *destination_url;
char *assertion_consumer_service_url;
LassoLogin *login;
/* 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) {
AM_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;
destination_url = lasso_provider_get_metadata_one(
provider, "SingleSignOnService HTTP-Redirect");
if (destination_url == NULL) {
/* HTTP-Redirect unsupported - try HTTP-POST. */
http_method = LASSO_HTTP_METHOD_POST;
destination_url = lasso_provider_get_metadata_one(
provider, "SingleSignOnService HTTP-POST");
}
if (destination_url == NULL) {
/* Both HTTP-Redirect and HTTP-POST unsupported - give up. */
AM_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;
}
assertion_consumer_service_url =
lasso_provider_get_assertion_consumer_service_url(
LASSO_PROVIDER(server), NULL);
ret = am_init_authn_request_common(r, &login, idp, http_method,
destination_url,
assertion_consumer_service_url,
return_to_url, is_passive);
g_free(destination_url);
g_free(assertion_consumer_service_url);
if (ret != OK) {
if (login) {
lasso_login_destroy(login);
}
return ret;
}
ret = am_set_authn_request_content(r, login);
lasso_login_destroy(login);
return ret;
}
/* 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;
am_diag_printf(r, "enter function %s\n", __func__);
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_login_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;
int is_passive;
int ret;
am_diag_printf(r, "enter function %s\n", __func__);
return_to = am_extract_query_parameter(r->pool, r->args, "ReturnTo");
if(return_to == NULL) {
AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r,
"Missing required ReturnTo parameter.");
return HTTP_BAD_REQUEST;
}
ret = am_urldecode(return_to);
if(ret != OK) {
AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r,
"Error urldecoding ReturnTo parameter.");
return ret;
}
ret = am_validate_redirect_url(r, return_to);
if(ret != OK) {
AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r,
"Invalid target domain in login request 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) {
AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r,
"Error urldecoding IdP parameter.");
return ret;
}
}
ret = am_get_boolean_query_parameter(r, "IsPassive", &is_passive, FALSE);
if (ret != OK) {
return ret;
}
if(idp_param != NULL) {
idp = idp_param;
} else if(cfg->discovery_url) {
if(is_passive) {
/* We cannot currently do discovery with passive authentication requests. */
AM_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_login_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) {
AM_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;
am_diag_printf(r, "enter function %s\n", __func__);
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) {
AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r,
"probe discovery handler invoked but not "
"configured. Please 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) {
AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r,
"Missing required return parameter.");
return HTTP_BAD_REQUEST;
}
ret = am_urldecode(return_to);
if (ret != OK) {
AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, ret, r,
"Could not urldecode return value.");
return HTTP_BAD_REQUEST;
}
ret = am_validate_redirect_url(r, return_to);
if (ret != OK) {
AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r,
"Invalid target domain in probe discovery return parameter.");
return ret;
}
idp_param = am_extract_query_parameter(r->pool, r->args, "returnIDParam");
if(idp_param == NULL) {
AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r,
"Missing required returnIDParam parameter.");
return HTTP_BAD_REQUEST;
}
ret = am_urldecode(idp_param);
if (ret != OK) {
AM_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, fail if a MellonProbeDiscoveryIdP
* list was provided, otherwise try first IdP.
*/
if (disco_idp == NULL) {
if (!apr_is_empty_table(cfg->probe_discovery_idp)) {
AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r,
"probeDiscovery failed and non empty "
"MellonProbeDiscoveryIdP was provided.");
return HTTP_INTERNAL_SERVER_ERROR;
}
disco_idp = am_first_idp(r);
if (disco_idp == NULL) {
AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r,
"probeDiscovery found no usable IdP.");
return HTTP_INTERNAL_SERVER_ERROR;
} else {
AM_LOG_RERROR(APLOG_MARK, APLOG_WARNING, 0, r, "probeDiscovery "
"failed, trying default IdP %s", disco_idp);
}
} else {
AM_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);
#ifdef HAVE_ECP
am_req_cfg_rec *req_cfg = am_get_req_cfg(r);
#endif /* HAVE_ECP */
const char *endpoint;
/*
* Normally this content handler is used to dispatch to the SAML
* endpoints implmented by mod_auth_mellon. SAML endpoint dispatch
* occurs when the URI begins with the SAML endpoint path.
*
* However, this handler is also responsible for generating ECP
* authn requests, in this case the URL will be a protected
* resource we're doing authtentication for. Early in the request
* processing pipeline we detected we were doing ECP authn and set
* a flag on the request. Here we test for that flag and if true
* respond with the ECP PAOS authn request.
*
* If the request is neither for a SAML endpoint nor one that
* requires generating an ECP authn we decline handling the request.
*/
#ifdef HAVE_ECP
if (req_cfg->ecp_authn_req) { /* Are we doing ECP? */
return am_send_paos_authn_request(r);
}
#endif /* HAVE_ECP */
/* 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, "paosResponse")) {
return am_handle_paos_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 {
AM_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;
am_diag_printf(r, "enter function %s\n", __func__);
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 {
AM_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));
AM_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;
const char *ajax_header;
if (r->main) {
/* We are a subrequest. Trust the main request to have
* performed the authentication.
*/
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;
}
am_diag_printf(r, "enter function %s\n", __func__);
/* Set defaut Cache-Control headers within this location */
if (CFG_VALUE(dir, send_cache_control_header)) {
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. */
am_diag_printf(r, "%s am_enable_auth, no valid session\n",
__func__);
if(session) {
/* Release the session. */
am_release_request_session(r, session);
}
/*
* If this is an AJAX request, we cannot proceed to the IdP,
* Just fail early to save our resources
*/
ajax_header = apr_table_get(r->headers_in, "X-Requested-With");
if (ajax_header != NULL &&
strcmp(ajax_header, "XMLHttpRequest") == 0) {
AM_LOG_RERROR(APLOG_MARK, APLOG_INFO, 0, r,
"Deny unauthenticated X-Requested-With XMLHttpRequest "
"(AJAX) request");
return HTTP_FORBIDDEN;
}
#ifdef HAVE_ECP
/*
* If PAOS set a flag on the request indicating we're
* doing ECP and allow the request to proceed through the
* request handlers until we reach am_handler which then
* checks the flag and if True initiates an ECP transaction.
* See am_check_uid for detailed explanation.
*/
bool is_paos;
int error_code;
is_paos = am_is_paos_request(r, &error_code);
if (error_code) return HTTP_BAD_REQUEST;
if (is_paos) {
am_req_cfg_rec *req_cfg;
req_cfg = am_get_req_cfg(r);
req_cfg->ecp_authn_req = true;
return OK;
} else {
/* Send the user to the authentication page on the IdP. */
return am_start_auth(r);
}
#else /* HAVE_ECP */
/* Send the user to the authentication page on the IdP. */
return am_start_auth(r);
#endif /* HAVE_ECP */
}
am_diag_printf(r, "%s am_enable_auth, have valid session\n",
__func__);
/* Verify that the user has access to this resource. */
return_code = am_check_permissions(r, session);
if(return_code != OK) {
am_diag_printf(r, "%s failed am_check_permissions, status=%d\n",
__func__, return_code);
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) {
am_diag_printf(r, "%s am_enable_info, have valid session\n",
__func__);
/* 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);
} else {
am_diag_printf(r, "%s am_enable_info, no valid session\n",
__func__);
}
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;
if (r->main) {
/* We are a subrequest. Trust the main request to have
* performed the authentication.
*/
if (r->main->user) {
/* Make sure that the username from the main request is
* available in the subrequest.
*/
r->user = apr_pstrdup(r->pool, r->main->user);
}
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;
}
am_diag_printf(r, "enter function %s\n", __func__);
#ifdef HAVE_ECP
am_req_cfg_rec *req_cfg = am_get_req_cfg(r);
if (req_cfg->ecp_authn_req) {
AM_LOG_RERROR(APLOG_MARK, APLOG_DEBUG, 0, r,
"am_check_uid is performing ECP authn request flow");
/*
* Normally when a protected resource requires authentication
* the request processing pipeline is exited early by
* responding with either a 401 or a redirect. But the flow
* for ECP is different, there will be a successful response
* (200) but instead of the response body containing the
* protected resource it will contain a SAML AuthnRequest
* with the Content-Type indicating it's PAOS ECP.
*
* In order to return a 200 Success with a PAOS body we have
* to reach the handler stage of the request processing
* pipeline. But this is a protected resource and we won't
* reach the handler stage unless authn and authz are
* satisfied. Therefore we lie and return results which
* indicate authn and authz are satisfied. This is OK because
* we're not actually going to respond with the protected
* resource, instead we'll be responsing with a SAML request.
*
* Apache's internal request logic
* (ap_process_request_internal) requires that after a
* successful return from the check_user_id authentication
* hook the r->user value be non-NULL. This makes sense
* because authentication establishes who the authenticated
* principal is. But with ECP flow there is no authenticated
* user at this point, we're just faking successful
* authentication in order to reach the handler stage. To get
* around this problem we set r-user to the empty string to
* keep Apache happy, otherwise it would throw an
* error. mod_shibboleth does the same thing.
*/
r->user = "";
return OK;
}
#endif /* HAVE_ECP */
/* 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. */
r->user = ""; /* see above explanation */
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) {
am_diag_printf(r, "%s no session, return HTTP_UNAUTHORIZED\n",
__func__);
return HTTP_UNAUTHORIZED;
}
/* If the user isn't logged in, then we can't authorize the user. */
if(!session->logged_in) {
am_diag_printf(r, "%s session not logged in,"
" return HTTP_UNAUTHORIZED\n", __func__);
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_diag_printf(r, "%s failed am_check_permissions, status=%d\n",
__func__, return_code);
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;
}