\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;
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",
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;
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);
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;
}
/* Disable all caching within this location. */
am_set_nocache(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;
}