diff options
-rw-r--r-- | NEWS | 3 | ||||
-rw-r--r-- | README | 57 | ||||
-rw-r--r-- | auth_mellon.h | 2 | ||||
-rw-r--r-- | auth_mellon_config.c | 86 | ||||
-rw-r--r-- | auth_mellon_handler.c | 220 |
5 files changed, 282 insertions, 86 deletions
@@ -3,6 +3,9 @@ Version 0.3.1 * Allow MellonUser variable to be translated through MellonSetEnv +* A /mellon/probeDisco endpoint replaces the builtin:get-metadata + IdP dicovery URL scheme + Version 0.3.0 --------------------------------------------------------------------------- @@ -321,8 +321,17 @@ MellonPostCount 100 # The IdP discovery must redirect the user to the return URL, # with retueniDParam set to the selected IdP providerID. # - # Alternatively, a simple built-in IdP discovery can be used, - # by specifying "builtin:get-metadata?timeout=1" + # The builtin:get-metadata discovery URL is not supported anymore + # starting with 0.3.1. See MellonProbeDiscoveryTimeout for + # a replacement. + # + # Default: None set. + MellonDiscoveryURL "http://www.example.net/idp-discovery" + + # MellonProbeDiscoveryTimeout sets the timeout of the + # IdP probe discovery service, which is available on the + # probeDisco endoint. + # # This will cause the SP to send HTTP GET requests on the # configured IdP PorviderID URL. Theses URL should be used to # publish metadata, though this is not mandatory. If the IdP @@ -330,8 +339,16 @@ MellonPostCount 100 # If the PorviderID URL requires SSL, MellonIdPCAFile is used # as a trusted CA bundle. # - # Default: None set. - MellonDiscoveryURL "http://www.example.net/idp-discovery" + # Default: unset, which means the feature is disabled + # MellonProbeDiscoveryTimeout 1 + + # MellonProbeDiscoveryIdP can be used to restrict the + # list of IdP queried by the IdP probe discovery service. + # + # Default unset, which means that all configured IdP are + # queried. + # MellonProbeDiscoveryIdP http://idp1.example.com/saml/metadata + # MellonProbeDiscoveryIdP http://idp2.example.net/saml/metadata # This option will make the SAML authentication assertion # available in the MELLON_SAML_RESPONSE environement @@ -476,6 +493,38 @@ could contain a link element like the following: This will return the user to "https://www.example.org/logged_out.html" after the logout operation has completed. +=========================================================================== + Probe IdP discovery +=========================================================================== + +mod_auth_mellon has an IdP probe discovery service that sends HTTP GET +to IdP and picks the first that answers. This can be used as a poor +man's failover setup that redirects to your organisation internal IdP. +Here is a sample configuration: + + MellonEndpointPath "/saml" + (...) + MellonDiscoveryUrl "/saml/probeDisco" + MellonProbeDiscoveryTimeout 1 + +The SP will sends HTTP GET to each configured IdP providerId URL until +it gets an HTTP 200 response within the 1 second timeout. It will then +proceed with that IdP. + +If you are in a federation, then your IdP login page will need to provide +an IdP selection feature aimed at users from other institutions (after +such a choice, the user would be redirected to the SP's /saml/login +endpoint, with ReturnTo and IdP set appropriately). In such a setup, +you will want to configure external IdP in mod_auth_mellon, but not +use them for IdP probe discovery. The MellonProbeDiscoveryIdP +directive can be used to limit the usable IdP for probe discovery: + + MellonEndpointPath "/saml" + (...) + MellonDiscoveryUrl "/saml/probeDisco" + MellonProbeDiscoveryTimeout 1 + MellonProbeDiscoveryIdP "https://idp1.example.net/saml/metadata" + MellonProbeDiscoveryIdP "https://idp2.example.net/saml/metadata" =========================================================================== Contributors diff --git a/auth_mellon.h b/auth_mellon.h index 835ccd1..247b6a6 100644 --- a/auth_mellon.h +++ b/auth_mellon.h @@ -174,6 +174,8 @@ typedef struct am_dir_cfg_rec { /* IdP discovery service */ const char *discovery_url; + int probe_discovery_timeout; + apr_hash_t *probe_discovery_idp; /* Mutex to prevent us from creating several lasso server objects. */ apr_thread_mutex_t *server_mutex; diff --git a/auth_mellon_config.c b/auth_mellon_config.c index 228240b..eba1fa4 100644 --- a/auth_mellon_config.c +++ b/auth_mellon_config.c @@ -76,6 +76,47 @@ static const apr_size_t post_size = 1024 * 1024 * 1024; */ static const int post_count = 100; +/* This function handles configuration directives which set a + * multivalued string slot in the module configuration (the destination + * strucure is a hash). + * + * 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. + * This value isn't used by this function. + * const char *key The string argument following this configuration + * directive in the configuraion file. + * const char *value Optional value to be stored in the hash. + * + * Returns: + * NULL on success or an error string on failure. + */ +static const char *am_set_hash_string_slot(cmd_parms *cmd, + void *struct_ptr, + const char *key, + const char *value) +{ + server_rec *s = cmd->server; + apr_pool_t *pconf = s->process->pconf; + am_dir_cfg_rec *cfg = (am_dir_cfg_rec *)struct_ptr; + int offset; + apr_hash_t **hash; + + /* + * If no value is given, we just store the key in the hash. + */ + if (value == NULL || *value == '\0') + value = key; + + offset = (int)(long)cmd->info; + hash = (apr_hash_t **)((char *)cfg + offset); + apr_hash_set(*hash, apr_pstrdup(pconf, key), APR_HASH_KEY_STRING, value); + + return NULL; +} + /* This function handles configuration directives which set a file * slot in the module configuration. If lasso is recent enough, it * attempts to read the file immediatly. @@ -133,10 +174,10 @@ static const char *am_set_filestring_slot(cmd_parms *cmd, * 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) +static const char *am_get_provider_id(apr_pool_t *p, + server_rec *s, + const char *file, + const char **provider) { char *data; apr_xml_parser *xp; @@ -195,7 +236,7 @@ static const char *am_get_proovider_id(apr_pool_t *p, * Returns: * NULL on success or an error string on failure. */ -static const char *ap_set_idp_string_slot(cmd_parms *cmd, +static const char *am_set_idp_string_slot(cmd_parms *cmd, void *struct_ptr, const char *arg) { @@ -205,8 +246,8 @@ static const char *ap_set_idp_string_slot(cmd_parms *cmd, const char *error; const char *provider_id; - if ((error = am_get_proovider_id(cmd->pool, s, - arg, &provider_id)) != NULL) + if ((error = am_get_provider_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, @@ -649,8 +690,8 @@ const command_rec auth_mellon_commands[] = { ), AP_INIT_TAKE1( "MellonIdPMetadataFile", - ap_set_idp_string_slot, - NULL, + am_set_idp_string_slot, + NULL, OR_AUTHCFG, "Full path to xml metadata file for the IdP." ), @@ -705,6 +746,21 @@ const command_rec auth_mellon_commands[] = { "The URL of IdP discovery service. Default is unset." ), AP_INIT_TAKE1( + "MellonProbeDiscoveryTimeout", + ap_set_int_slot, + (void *)APR_OFFSETOF(am_dir_cfg_rec, probe_discovery_timeout), + OR_AUTHCFG, + "The timeout of IdP probe discovery service. " + "Default is 1s" + ), + AP_INIT_TAKE12( + "MellonProbeDiscoveryIdP", + am_set_hash_string_slot, + (void *)APR_OFFSETOF(am_dir_cfg_rec, probe_discovery_idp), + OR_AUTHCFG, + "An IdP that can be used for IdP probe discovery." + ), + AP_INIT_TAKE1( "MellonEndpointPath", am_set_endpoint_path, NULL, @@ -760,6 +816,8 @@ void *auth_mellon_dir_config(apr_pool_t *p, char *d) dir->idp_ca_file = NULL; dir->login_path = default_login_path; dir->discovery_url = NULL; + dir->probe_discovery_timeout = -1; /* -1 means no probe discovery */ + dir->probe_discovery_idp = apr_hash_make(p); dir->sp_org_name = apr_hash_make(p); dir->sp_org_display_name = apr_hash_make(p); @@ -903,6 +961,16 @@ void *auth_mellon_dir_merge(apr_pool_t *p, void *base, void *add) add_cfg->discovery_url : base_cfg->discovery_url); + new_cfg->probe_discovery_timeout = + (add_cfg->probe_discovery_timeout != -1 ? + add_cfg->probe_discovery_timeout : + base_cfg->probe_discovery_timeout); + + new_cfg->probe_discovery_idp = apr_hash_copy(p, + (apr_hash_count(add_cfg->probe_discovery_idp) > 0) ? + add_cfg->probe_discovery_idp : + base_cfg->probe_discovery_idp); + apr_thread_mutex_create(&new_cfg->server_mutex, APR_THREAD_MUTEX_DEFAULT, p); new_cfg->server = NULL; diff --git a/auth_mellon_handler.c b/auth_mellon_handler.c index 02b996b..5590ca8 100644 --- a/auth_mellon_handler.c +++ b/auth_mellon_handler.c @@ -226,33 +226,6 @@ static const char *am_first_idp(request_rec *r) 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 * @@ -267,8 +240,6 @@ 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. @@ -308,47 +279,6 @@ static const char *am_get_idp(request_rec *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. @@ -2506,7 +2436,6 @@ static int am_auth_new_ticket(request_rec *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; @@ -2536,8 +2465,7 @@ static int am_auth_new_ticket(request_rec *r) /* 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 */ + if (cfg->discovery_url != NULL) { char *return_url; return_url = am_extract_query_parameter(r->pool, r->args, "ReturnTo"); @@ -2615,6 +2543,150 @@ static int am_handle_login(request_rec *r) return am_send_authn_request(r, idp, return_to, is_passive); } +/* 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); + const char *idp = NULL; + int timeout; + apr_hash_index_t *index; + char *return_to; + char *idp_param; + char *redirect_url; + int ret; + + /* + * 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. + * + * Send probes for all configured IdP to check availability. + * The first to answer is chosen, but the list of usable + * IdP can be restricted in configuration. + */ + for (index = apr_hash_first(r->pool, cfg->idp_metadata_files); + index; + index = apr_hash_next(index)) { + void *dontcare; + const char *ping_url; + apr_size_t len; + apr_ssize_t slen; + long status; + + apr_hash_this(index, (const void **)&idp, + &slen, (void *)&dontcare); + ping_url = idp; + + /* + * If a list of IdP was given for probe discovery, + * skip any IdP that does not match. + */ + if (apr_hash_count(cfg->probe_discovery_idp) != 0) { + char *value = apr_hash_get(cfg->probe_discovery_idp, + idp, APR_HASH_KEY_STRING); + + if (value == NULL) { + /* idp not in list, try the next one */ + idp = NULL; + continue; + } else { + /* idp in list, use the value as the ping url */ + ping_url = value; + } + } + + status = 0; + if (am_httpclient_get(r, ping_url, &dontcare, &len, + timeout, &status) != OK) + continue; + + if (status != HTTP_OK) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "Cannot probe %s: \"%s\" returned HTTP %ld", + idp, ping_url, status); + continue; + } + + /* We got some succes */ + ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, r, + "probeDiscovery using %s", idp); + break; + } + + /* + * On failure, try default + */ + if (idp == NULL) { + idp = am_first_idp(r); + if (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", 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, idp)); + + apr_table_setn(r->headers_out, "Location", redirect_url); + + return HTTP_SEE_OTHER; +} + /* This function takes a request for an endpoint and passes it on to the * correct handler function. @@ -2656,6 +2728,8 @@ static int am_endpoint_handler(request_rec *r) 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.", |