- Support for multiple IdP

- IdP discovery service
- add a dimple built-in IdP discovery mechanism: send HTTP GET on ProviderID
  URL. If we get HTTP 200, the IdP is selected


git-svn-id: https://modmellon.googlecode.com/svn/trunk@44 a716ebb1-153a-0410-b759-cfb97c6a1b53
This commit is contained in:
manu@netbsd.org 2009-05-12 15:28:49 +00:00
parent 6b2fb648ff
commit 4c740a21c1
5 changed files with 473 additions and 73 deletions

View File

@ -49,6 +49,7 @@
#include "apr_md5.h"
#include "apr_file_info.h"
#include "apr_file_io.h"
#include "apr_xml.h"
#include "ap_config.h"
#include "httpd.h"
@ -116,7 +117,6 @@ typedef enum {
am_decoder_feide,
} am_decoder_t;
typedef struct am_dir_cfg_rec {
/* enable_mellon is used to enable auth_mellon for a location.
*/
@ -144,7 +144,7 @@ typedef struct am_dir_cfg_rec {
const char *sp_metadata_file;
const char *sp_private_key_file;
const char *sp_cert_file;
const char *idp_metadata_file;
apr_hash_t *idp_metadata_files;
const char *idp_public_key_file;
const char *idp_ca_file;
@ -157,6 +157,9 @@ typedef struct am_dir_cfg_rec {
/* Login path for IdP initiated logins */
const char *login_path;
/* IdP discovery service */
const char *discovery_url;
/* Mutex to prevent us from creating several lasso server objects. */
apr_thread_mutex_t *server_mutex;
/* Cached lasso server object. */
@ -242,8 +245,9 @@ int am_check_uid(request_rec *r);
int am_handle_metadata(request_rec *r);
int am_httpclient_get(request_rec *r, const char *uri,
void **buffer, apr_size_t *size);
int am_httpclient_get(request_rec *r, const char *uri,
void **buffer, apr_size_t *size,
apr_time_t timeout, long *status);
int am_httpclient_post(request_rec *r, const char *uri,
const void *post_data, apr_size_t post_length,
const char *content_type,

View File

@ -83,6 +83,103 @@ static const char *am_set_filestring_slot(cmd_parms *cmd,
}
/* This function extracts an IdP ProviderID from metadata
*
* Parameters:
* apr_pool_t *p Pool to allocate temporary items from.
* server_rec *s The server.
* const char *file File containing metadata.
* const char **provider The providerID
*
* Returns:
* NULL on success or an error string on failure.
*
*/
static const char *am_get_proovider_id(apr_pool_t *p,
server_rec *s,
const char *file,
const char **provider)
{
char *data;
apr_xml_parser *xp;
apr_xml_doc *xd;
apr_xml_attr *xa;
char error[1024];
*provider = NULL;
/*
* Get the data
*/
if ((data = am_getfile(p, s, file)) == NULL)
return apr_psprintf(p, "Cannot read file %s", file);
/*
* Parse
*/
xp = apr_xml_parser_create(p);
if (apr_xml_parser_feed(xp, data, strlen(data)) != 0)
return apr_psprintf(p, "Cannot parse %s: %s", file,
apr_xml_parser_geterror(xp, error, sizeof(error)));
if (apr_xml_parser_done(xp, &xd) != 0)
return apr_psprintf(p, "Parse error %s: %s", file,
apr_xml_parser_geterror(xp, error, sizeof(error)));
/*
* Extract /EntityDescriptor@EntityID
*/
if (strcasecmp(xd->root->name, "EntityDescriptor") != 0)
return apr_psprintf(p, "<EntityDescriptor> is not root in %s", file);
for (xa = xd->root->attr; xa; xa = xa->next)
if (strcasecmp(xa->name, "entityID") == 0)
break;
if (xa == NULL)
return apr_psprintf(p, "entityID not found in %s", file);
*provider = xa->value;
return NULL;
}
/* This function handles configuration directives which set an
* idp related slot in the module configuration.
*
* Parameters:
* cmd_parms *cmd The command structure for this configuration
* directive.
* void *struct_ptr Pointer to the current directory configuration.
* NULL if we are not in a directory configuration.
* const char *arg The string argument following this configuration
* directive in the configuraion file.
*
* Returns:
* NULL on success or an error string on failure.
*/
static const char *ap_set_idp_string_slot(cmd_parms *cmd,
void *struct_ptr,
const char *arg)
{
server_rec *s = cmd->server;
apr_pool_t *pconf = s->process->pconf;
am_dir_cfg_rec *cfg = (am_dir_cfg_rec *)struct_ptr;
const char *error;
const char *provider_id;
if ((error = am_get_proovider_id(cmd->pool, s,
arg, &provider_id)) != NULL)
return apr_psprintf(cmd->pool, "%s - %s", cmd->cmd->name, error);
apr_hash_set(cfg->idp_metadata_files,
apr_pstrdup(pconf, provider_id),
APR_HASH_KEY_STRING,
apr_pstrdup(pconf, arg));
return NULL;
}
/* This function handles configuration directives which set a string
* slot in the module configuration.
*
@ -431,8 +528,8 @@ const command_rec auth_mellon_commands[] = {
),
AP_INIT_TAKE1(
"MellonIdPMetadataFile",
ap_set_string_slot,
(void *)APR_OFFSETOF(am_dir_cfg_rec, idp_metadata_file),
ap_set_idp_string_slot,
NULL,
OR_AUTHCFG,
"Full path to xml metadata file for the IdP."
),
@ -458,6 +555,13 @@ const command_rec auth_mellon_commands[] = {
"The location where to redirect after IdP initiated login."
" Default value is \"/\"."
),
AP_INIT_TAKE1(
"MellonDiscoveryURL",
ap_set_string_slot,
(void *)APR_OFFSETOF(am_dir_cfg_rec, discovery_url),
OR_AUTHCFG,
"The URL of IdP discovery service. Default is unset."
),
AP_INIT_TAKE1(
"MellonEndpointPath",
am_set_endpoint_path,
@ -506,11 +610,11 @@ void *auth_mellon_dir_config(apr_pool_t *p, char *d)
dir->sp_metadata_file = NULL;
dir->sp_private_key_file = NULL;
dir->sp_cert_file = NULL;
dir->idp_metadata_file = NULL;
dir->idp_metadata_files = apr_hash_make(p);
dir->idp_public_key_file = NULL;
dir->idp_ca_file = NULL;
dir->login_path = default_login_path;
dir->discovery_url = NULL;
apr_thread_mutex_create(&dir->server_mutex, APR_THREAD_MUTEX_DEFAULT, p);
@ -602,9 +706,10 @@ void *auth_mellon_dir_merge(apr_pool_t *p, void *base, void *add)
add_cfg->sp_cert_file :
base_cfg->sp_cert_file);
new_cfg->idp_metadata_file = (add_cfg->idp_metadata_file ?
add_cfg->idp_metadata_file :
base_cfg->idp_metadata_file);
new_cfg->idp_metadata_files = apr_hash_copy(p,
(apr_hash_count(add_cfg->idp_metadata_files) > 0) ?
add_cfg->idp_metadata_files :
base_cfg->idp_metadata_files);
new_cfg->idp_public_key_file = (add_cfg->idp_public_key_file ?
add_cfg->idp_public_key_file :
@ -618,6 +723,10 @@ void *auth_mellon_dir_merge(apr_pool_t *p, void *base, void *add)
add_cfg->login_path :
base_cfg->login_path);
new_cfg->discovery_url = (add_cfg->discovery_url ?
add_cfg->discovery_url :
base_cfg->discovery_url);
apr_thread_mutex_create(&new_cfg->server_mutex,
APR_THREAD_MUTEX_DEFAULT, p);
new_cfg->server = NULL;

View File

@ -30,7 +30,6 @@
#endif /* HAVE_lasso_server_new_from_buffers */
#ifdef HAVE_lasso_server_new_from_buffers
/* This function produces the endpoint URL
*
* Parameters:
@ -69,6 +68,7 @@ static char *am_get_endpoint_url(request_rec *r)
port, cfg->endpoint_path);
}
#ifdef HAVE_lasso_server_new_from_buffers
/* This function generates metadata
*
* Parameters:
@ -132,12 +132,213 @@ static char *am_generate_metadata(apr_pool_t *p, request_rec *r)
}
#endif /* HAVE_lasso_server_new_from_buffers */
/* 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)
{
am_dir_cfg_rec *cfg = am_get_dir_cfg(r);
apr_hash_index_t *index;
const char *provider_id;
apr_ssize_t len;
void *idp_metadata_file;
index = apr_hash_first(r->pool, cfg->idp_metadata_files);
if (index == NULL)
return NULL;
apr_hash_this(index, (const void **)&provider_id,
&len, &idp_metadata_file);
return provider_id;
}
/* This returns built-in IdP discovery timeout
*
* Parameters:
* request_rec *r The request we received.
*
* Returns:
* the timeout, -1 if not enabled.
*/
static long am_builtin_discovery_timeout(request_rec *r)
{
am_dir_cfg_rec *cfg = am_get_dir_cfg(r);
const char *builtin = "builtin:get-metadata";
const char *timeout = "?timeout=";
const char *cp;
const long default_timeout = 1L;
if ((cfg->discovery_url == NULL) ||
(strncmp(cfg->discovery_url, builtin, strlen(builtin)) != 0))
return -1;
cp = cfg->discovery_url + strlen(builtin);
if (strncmp(cp, timeout, strlen(timeout)) != 0)
return default_timeout;
cp += strlen(timeout);
return atoi(cp);
}
/* 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)
{
am_dir_cfg_rec *cfg = am_get_dir_cfg(r);
const char *idp_provider_id;
const char *idp_metadata_file;
apr_hash_index_t *index;
long timeout;
/*
* If we have a single IdP, return that one.
*/
if (apr_hash_count(cfg->idp_metadata_files) == 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 {
idp_metadata_file = apr_hash_get(cfg->idp_metadata_files,
idp_provider_id,
APR_HASH_KEY_STRING);
if (idp_metadata_file == 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;
}
/*
* If built-in IdP discovery is not configured, return default.
*/
timeout = am_builtin_discovery_timeout(r);
if (timeout == -1)
return am_first_idp(r);
/*
* Otherwise, proceed with built-in IdP discovery:
* send probes for all configures IdP to check availability.
* The first to answer is chosen. On error, use default.
*/
for (index = apr_hash_first(r->pool, cfg->idp_metadata_files);
index;
index = apr_hash_next(index)) {
void *buffer;
apr_size_t len;
apr_ssize_t slen;
long status;
apr_hash_this(index,
(const void **)&idp_provider_id,
&slen,
(void *)&idp_metadata_file);
status = 0;
if (am_httpclient_get(r, idp_provider_id, &buffer, &len,
timeout, &status) != OK)
continue;
if (status != HTTP_OK) {
ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
"Cannot probe %s: IdP returned HTTP %ld",
idp_provider_id, status);
continue;
}
/* We got some succes */
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 loads all IdP metadata in a lasso server
*
* Parameters:
* request_rec *r The request we received.
*
* Returns:
* number of loaded providers
*/
static int am_server_add_providers(request_rec *r)
{
am_dir_cfg_rec *cfg = am_get_dir_cfg(r);
const char *idp_metadata_file;
const char *idp_public_key_file;
apr_hash_index_t *index;
int count = 0;
if (apr_hash_count(cfg->idp_metadata_files) == 1)
idp_public_key_file = cfg->idp_public_key_file;
else
idp_public_key_file = NULL;
for (index = apr_hash_first(r->pool, cfg->idp_metadata_files);
index;
index = apr_hash_next(index)) {
const char *idp_provider_id;
apr_ssize_t len;
int ret;
apr_hash_this(index, (const void **)&idp_provider_id,
&len, (void *)&idp_metadata_file);
ret = lasso_server_add_provider(cfg->server, LASSO_PROVIDER_ROLE_IDP,
idp_metadata_file,
idp_public_key_file,
cfg->idp_ca_file);
if (ret != 0) {
ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
"Error adding IdP \"%s\" to lasso server object.",
idp_provider_id);
} else {
count++;
}
}
return count;
}
static LassoServer *am_get_lasso_server(request_rec *r)
{
am_dir_cfg_rec *cfg;
gint ret;
cfg = am_get_dir_cfg(r);
am_dir_cfg_rec *cfg = am_get_dir_cfg(r);
apr_thread_mutex_lock(cfg->server_mutex);
if(cfg->server == NULL) {
@ -166,12 +367,7 @@ static LassoServer *am_get_lasso_server(request_rec *r)
return NULL;
}
ret = lasso_server_add_provider(cfg->server, LASSO_PROVIDER_ROLE_IDP,
cfg->idp_metadata_file,
cfg->idp_public_key_file,
cfg->idp_ca_file);
if(ret != 0) {
if (am_server_add_providers(r) == 0) {
ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
"Error adding IdP to lasso server object. Please"
" verify the following configuration directives:"
@ -1452,59 +1648,58 @@ int am_handle_metadata(request_rec *r)
}
/* This function takes a request for an endpoint and passes it on to the
* correct handler function.
*
* Parameters:
* request_rec *r The request we are currently handling.
*
* Returns:
* The return value of the endpoint handler function,
* or HTTP_NOT_FOUND if we don't have a handler for the requested
* endpoint.
*/
static int am_endpoint_handler(request_rec *r)
{
const char *endpoint;
am_dir_cfg_rec *dir = am_get_dir_cfg(r);
/* r->uri starts with cfg->endpoint_path, so we can find the endpoint
* by extracting the string following chf->endpoint_path.
*/
endpoint = &r->uri[strlen(dir->endpoint_path)];
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, "metadata")) {
return OK;
} 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 {
ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
"Endpoint \"%s\" not handled by mod_auth_mellon.",
endpoint);
return HTTP_NOT_FOUND;
}
}
static int am_auth_new_ticket(request_rec *r)
{
am_dir_cfg_rec *cfg = am_get_dir_cfg(r);
LassoServer *server;
LassoLogin *login;
LassoSamlp2AuthnRequest *request;
gint ret;
char *redirect_to;
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_builtin_discovery_timeout(r) == -1) && /* no built-in discovery */
(am_extract_query_parameter(r->pool, r->args, "IdP") == NULL)) {
char *discovery_url;
char *return_url;
char *endpoint = am_get_endpoint_url(r);
char *sep;
/* If discovery URL already has a ? we append a & */
sep = (strchr(cfg->discovery_url, '?')) ? "&" : "?";
return_url = apr_psprintf(r->pool, "%sauth?ReturnTo=%s",
endpoint,
am_urlencode(r->pool, relay_state));
ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r,
"return_url = %s", return_url);
discovery_url = apr_psprintf(r->pool, "%s%sentityID=%smetadata&"
"return=%s&returnIDParam=IdP",
cfg->discovery_url, sep,
am_urlencode(r->pool, endpoint),
am_urlencode(r->pool, return_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;
}
/* If IdP discovery is in use and we have an IdP selected,
* set the relay_state
*/
if ((cfg->discovery_url != NULL) &&
(am_builtin_discovery_timeout(r) == -1)) { /* no built-in discovery */
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;
}
/* Add cookie for cookie test. We know that we should have
* a valid cookie when we return from the IdP after SP-initiated
@ -1525,7 +1720,7 @@ static int am_auth_new_ticket(request_rec *r)
return HTTP_INTERNAL_SERVER_ERROR;
}
ret = lasso_login_init_authn_request(login, NULL,
ret = lasso_login_init_authn_request(login, am_get_idp(r),
LASSO_HTTP_METHOD_REDIRECT);
if(ret != 0) {
ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
@ -1552,7 +1747,7 @@ static int am_auth_new_ticket(request_rec *r)
LASSO_SAMLP2_REQUEST_ABSTRACT(request)->Consent
= g_strdup(LASSO_SAML2_CONSENT_IMPLICIT);
LASSO_PROFILE(login)->msg_relayState = g_strdup(am_reconstruct_url(r));
LASSO_PROFILE(login)->msg_relayState = g_strdup(relay_state);
ret = lasso_login_build_authn_request_msg(login);
if(ret != 0) {
@ -1588,6 +1783,53 @@ static int am_auth_new_ticket(request_rec *r)
return HTTP_SEE_OTHER;
}
/* This function takes a request for an endpoint and passes it on to the
* correct handler function.
*
* Parameters:
* request_rec *r The request we are currently handling.
*
* Returns:
* The return value of the endpoint handler function,
* or HTTP_NOT_FOUND if we don't have a handler for the requested
* endpoint.
*/
static int am_endpoint_handler(request_rec *r)
{
const char *endpoint;
am_dir_cfg_rec *dir = am_get_dir_cfg(r);
/* r->uri starts with cfg->endpoint_path, so we can find the endpoint
* by extracting the string following chf->endpoint_path.
*/
endpoint = &r->uri[strlen(dir->endpoint_path)];
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_auth_new_ticket(r);
} else if(!strcmp(endpoint, "metadata")) {
return OK;
} 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 {
ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
"Endpoint \"%s\" not handled by mod_auth_mellon.",
endpoint);
return HTTP_NOT_FOUND;
}
}
int am_auth_mellon_user(request_rec *r)
{
@ -1606,7 +1848,6 @@ int am_auth_mellon_user(request_rec *r)
return DECLINED;
}
/* Disable all caching within this location. */
am_set_nocache(r);

View File

@ -246,6 +246,7 @@ static CURL *am_httpclient_init_curl(request_rec *r, const char *uri,
am_hc_block_header_t *bh,
char *curl_error)
{
am_dir_cfg_rec *cfg = am_get_dir_cfg(r);
CURL *curl;
CURLcode res;
@ -311,6 +312,17 @@ static CURL *am_httpclient_init_curl(request_rec *r, const char *uri,
goto cleanup_fail;
}
/* If we have a CA configured, try to use it */
if (cfg->idp_ca_file != NULL) {
res = curl_easy_setopt(curl, CURLOPT_CAINFO, cfg->idp_ca_file);
if(res != CURLE_OK) {
ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
"Failed to set SSL CA info %s:"
" [%u] %s", cfg->idp_ca_file, res, curl_error);
goto cleanup_fail;
}
}
/* Enable fail on http error. */
res = curl_easy_setopt(curl, CURLOPT_FAILONERROR, 1L);
if(res != CURLE_OK) {
@ -359,7 +371,7 @@ static CURL *am_httpclient_init_curl(request_rec *r, const char *uri,
}
/* This function downloads data from a specified URI.
/* This function downloads data from a specified URI, with specified timeout
*
* Parameters:
* request_rec *r The apache request this download is associated
@ -371,13 +383,16 @@ static CURL *am_httpclient_init_curl(request_rec *r, const char *uri,
* apr_size_t *size This is a pointer to where we will store the length
* of the downloaded data, not including the
* null-terminator we add. This parameter can be NULL.
* apr_time_t timeout Timeout in seconds, 0 for no timeout.
* long *status Pointer to HTTP status code.
*
* Returns:
* OK on success, or HTTP_INTERNAL_SERVER_ERROR on failure. On failure we
* will write a log message describing the error.
*/
int am_httpclient_get(request_rec *r, const char *uri,
void **buffer, apr_size_t *size)
void **buffer, apr_size_t *size,
apr_time_t timeout, long *status)
{
am_hc_block_header_t bh;
CURL *curl;
@ -393,15 +408,45 @@ int am_httpclient_get(request_rec *r, const char *uri,
return HTTP_INTERNAL_SERVER_ERROR;
}
res = curl_easy_setopt(curl, CURLOPT_TIMEOUT, timeout);
if(res != CURLE_OK) {
ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
"Failed to download data from the uri \"%s\", "
"cannot set timeout to %ld: [%u] %s",
uri, (long)timeout, res, curl_error);
goto cleanup_fail;
}
res = curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, timeout);
if(res != CURLE_OK) {
ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
"Failed to download data from the uri \"%s\", "
"cannot set connect timeout to %ld: [%u] %s",
uri, (long)timeout, res, curl_error);
goto cleanup_fail;
}
/* Do the download. */
res = curl_easy_perform(curl);
if(res != CURLE_OK) {
ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
"Failed to download data from the uri \"%s\": [%u] %s",
"Failed to download data from the uri \"%s\", "
"transaction aborted: [%u] %s",
uri, res, curl_error);
goto cleanup_fail;
}
if (status != NULL) {
res = curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, status);
if(res != CURLE_OK) {
ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
"Failed to download data from the uri \"%s\", "
"no status report: [%u] %s",
uri, res, curl_error);
goto cleanup_fail;
}
}
/* Free the curl object. */
curl_easy_cleanup(curl);

View File

@ -47,6 +47,7 @@ static apr_status_t am_global_kill(void *p)
server_rec *s = (server_rec *) p;
am_mod_cfg_rec *m = am_get_mod_cfg(s);
if (m->cache) {
/* Destroy the shared memory for session data. */
apr_shm_destroy(m->cache);