From 488dadf9c397369c371abe2851c8d5fc67a34e5e Mon Sep 17 00:00:00 2001 From: Hans Zandbelt Date: Tue, 4 Jun 2024 04:23:28 -0700 Subject: [PATCH] add DPoP and FAPI 2.0 support - add (client) support for RFC 9449 OAuth 2.0 Demonstrating Proof of Possession (DPoP) - replace multi-provider .conf "issuer_specific_redirect_uri" boolean with "response_require_iss" boolean - tighten up the "aud" claim validation in ID tokens - add support for the FAPI 2.0 Security Profile https://openid.net/specs/fapi-2_0-security-profile-ID2.html Signed-off-by: Hans Zandbelt --- ChangeLog | 7 ++ Makefile.am | 1 + README.md | 2 + src/cfg/provider.c | 17 ++-- src/cfg/provider.h | 2 +- src/handle/content.c | 17 ++++ src/handle/dpop.c | 138 +++++++++++++++++++++++++++ src/handle/handle.h | 3 + src/handle/logout.c | 14 +-- src/handle/request.c | 5 +- src/handle/session_management.c | 3 +- src/http.c | 70 +++++++------- src/http.h | 25 ++--- src/metadata.c | 17 ++-- src/metrics.c | 2 + src/metrics.h | 2 + src/mod_auth_openidc.c | 17 +++- src/mod_auth_openidc.h | 6 ++ src/oauth.c | 6 +- src/proto.c | 164 ++++++++++++++++++++++++++------ src/proto.h | 2 + src/util.c | 19 ---- src/util.h | 1 - 23 files changed, 413 insertions(+), 127 deletions(-) create mode 100644 src/handle/dpop.c diff --git a/ChangeLog b/ChangeLog index 16421db4..e02efe5e 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,10 @@ +06/04/2024 +- add (client) support for RFC 9449 OAuth 2.0 Demonstrating Proof of Possession (DPoP) + TODO: must support server provided nonce +- replace multi-provider .conf "issuer_specific_redirect_uri" boolean with "response_require_iss" boolean +- tighten up the "aud" claim validation in ID tokens +- add support for the FAPI 2.0 Security Profile https://openid.net/specs/fapi-2_0-security-profile-ID2.html + 05/30/2024 - add support for RFC 9126 OAuth 2.0 Pushed Authorization Requests diff --git a/Makefile.am b/Makefile.am index e4f3ccd8..2be91302 100644 --- a/Makefile.am +++ b/Makefile.am @@ -17,6 +17,7 @@ libauth_openidc_la_SOURCES = \ src/handle/authz.c \ src/handle/content.c \ src/handle/discovery.c \ + src/handle/dpop.c \ src/handle/info.c \ src/handle/jwks.c \ src/handle/logout.c \ diff --git a/README.md b/README.md index 1a840103..b74136d0 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,8 @@ Interoperability - [OpenID Connect Dynamic Client Registration 1.0](http://openid.net/specs/openid-connect-registration-1_0.html) - [RFC 7636 - Proof Key for Code Exchange by OAuth Public Clients](https://datatracker.ietf.org/doc/html/rfc7636) - [RFC 9126 - OAuth 2.0 Pushed Authorization Requests](https://datatracker.ietf.org/doc/html/rfc9126) +- [RFC 9449 - OAuth 2.0 Demonstrating Proof of Possession (DPoP)](https://tools.ietf.org/html/rfc9449) +- [FAPI 2.0 Security Profile](https://openid.net/specs/fapi-2_0-security-profile-ID2.html) - [OAuth 2.0 Form Post Response Mode 1.0](http://openid.net/specs/oauth-v2-form-post-response-mode-1_0.html) - [OAuth 2.0 Multiple Response Type Encoding Practices 1.0](http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html) - [OpenID Connect Session Management 1.0](http://openid.net/specs/openid-connect-session-1_0.html) *see the [Wiki](https://github.com/OpenIDC/mod_auth_openidc/wiki/OpenID-Connect-Session-Management) for information on how to configure it)* diff --git a/src/cfg/provider.c b/src/cfg/provider.c index d36bb74f..6b961085 100644 --- a/src/cfg/provider.c +++ b/src/cfg/provider.c @@ -94,7 +94,7 @@ struct oidc_provider_t { oidc_userinfo_token_method_t userinfo_token_method; char *request_object; oidc_auth_request_method_t auth_request_method; - int issuer_specific_redirect_uri; + int response_require_iss; }; #define OIDC_PROVIDER_MEMBER_FUNCS_TYPE_DEF(member, type, def_val) \ @@ -297,10 +297,10 @@ OIDC_PROVIDER_MEMBER_FUNCS_FLAG(ssl_validate_server, OIDC_DEFAULT_SSL_VALIDATE_S #define OIDC_DEFAULT_VALIDATE_ISSUER 1 OIDC_PROVIDER_MEMBER_FUNCS_FLAG(validate_issuer, OIDC_DEFAULT_VALIDATE_ISSUER) -// define whether the issuer will be added to the redirect uri by default to mitigate the IDP mixup attack -// only used from metadata in multi-provider setups -#define OIDC_DEFAULT_PROVIDER_ISSUER_SPECIFIC_REDIRECT_URI 0 -OIDC_PROVIDER_MEMBER_FUNCS_FLAG(issuer_specific_redirect_uri, OIDC_DEFAULT_PROVIDER_ISSUER_SPECIFIC_REDIRECT_URI) +// define whether the iss parameter will be required in the response to the redirect uri by default to mitigate the IDP +// mixup attack only used from metadata in multi-provider setups +#define OIDC_DEFAULT_PROVIDER_RESPONSE_REQUIRE_ISS 0 +OIDC_PROVIDER_MEMBER_FUNCS_FLAG(response_require_iss, OIDC_DEFAULT_PROVIDER_RESPONSE_REQUIRE_ISS) // only used from metadata in multi-provider setups OIDC_PROVIDER_MEMBER_FUNCS_STR(registration_token, NULL) @@ -635,7 +635,7 @@ static void oidc_cfg_provider_init(oidc_provider_t *provider) { provider->userinfo_refresh_interval = OIDC_CONFIG_POS_INT_UNSET; provider->request_object = NULL; - provider->issuer_specific_redirect_uri = OIDC_CONFIG_POS_INT_UNSET; + provider->response_require_iss = OIDC_CONFIG_POS_INT_UNSET; } void oidc_cfg_provider_merge(apr_pool_t *pool, oidc_provider_t *dst, const oidc_provider_t *base, @@ -744,9 +744,8 @@ void oidc_cfg_provider_merge(apr_pool_t *pool, oidc_provider_t *dst, const oidc_ : base->userinfo_refresh_interval; dst->request_object = add->request_object != NULL ? add->request_object : base->request_object; - dst->issuer_specific_redirect_uri = add->issuer_specific_redirect_uri != OIDC_CONFIG_POS_INT_UNSET - ? add->issuer_specific_redirect_uri - : base->issuer_specific_redirect_uri; + dst->response_require_iss = add->response_require_iss != OIDC_CONFIG_POS_INT_UNSET ? add->response_require_iss + : base->response_require_iss; } oidc_provider_t *oidc_cfg_provider_create(apr_pool_t *pool) { diff --git a/src/cfg/provider.h b/src/cfg/provider.h index 261e3288..18f1348b 100644 --- a/src/cfg/provider.h +++ b/src/cfg/provider.h @@ -215,7 +215,7 @@ OIDC_CFG_PROVIDER_MEMBER_FUNCS_INT_DECL(ssl_validate_server) OIDC_CFG_PROVIDER_MEMBER_FUNCS_INT_DECL(validate_issuer) OIDC_CFG_PROVIDER_MEMBER_FUNCS_INT_DECL(idtoken_iat_slack) OIDC_CFG_PROVIDER_MEMBER_FUNCS_INT_DECL(session_max_duration) -OIDC_CFG_PROVIDER_MEMBER_FUNCS_INT_DECL(issuer_specific_redirect_uri) +OIDC_CFG_PROVIDER_MEMBER_FUNCS_INT_DECL(response_require_iss) // ints with 2 args OIDC_CFG_PROVIDER_MEMBER_FUNCS_INT_DECL(userinfo_refresh_interval, const char *) diff --git a/src/handle/content.c b/src/handle/content.c index 0e03964f..fb0419b4 100644 --- a/src/handle/content.c +++ b/src/handle/content.c @@ -92,6 +92,23 @@ int oidc_content_handler(request_rec *r) { /* free resources allocated for the session */ oidc_session_free(r, session); + } else if (oidc_util_request_has_parameter(r, OIDC_REDIRECT_URI_REQUEST_DPOP)) { + + OIDC_METRICS_COUNTER_INC(r, c, OM_CONTENT_REQUEST_DPOP); + + /* see if a session was retained in the request state */ + apr_pool_userdata_get((void **)&session, OIDC_USERDATA_SESSION, r->pool); + + /* if no retained session was found, load it from the cache or create a new one*/ + if (session != NULL) + /* handle request to create a DPoP proof */ + rc = oidc_dpop_request(r, c, session); + else + rc = HTTP_UNAUTHORIZED; + + /* free resources allocated for the session */ + oidc_session_free(r, session); + } else if (oidc_util_request_has_parameter(r, OIDC_REDIRECT_URI_REQUEST_JWKS)) { OIDC_METRICS_COUNTER_INC(r, c, OM_CONTENT_REQUEST_JWKS); diff --git a/src/handle/dpop.c b/src/handle/dpop.c new file mode 100644 index 00000000..bed4cbd1 --- /dev/null +++ b/src/handle/dpop.c @@ -0,0 +1,138 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/*************************************************************************** + * Copyright (C) 2017-2024 ZmartZone Holding BV + * All rights reserved. + * + * DISCLAIMER OF WARRANTIES: + * + * THE SOFTWARE PROVIDED HEREUNDER IS PROVIDED ON AN "AS IS" BASIS, WITHOUT + * ANY WARRANTIES OR REPRESENTATIONS EXPRESS, IMPLIED OR STATUTORY; INCLUDING, + * WITHOUT LIMITATION, WARRANTIES OF QUALITY, PERFORMANCE, NONINFRINGEMENT, + * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. NOR ARE THERE ANY + * WARRANTIES CREATED BY A COURSE OR DEALING, COURSE OF PERFORMANCE OR TRADE + * USAGE. FURTHERMORE, THERE ARE NO WARRANTIES THAT THE SOFTWARE WILL MEET + * YOUR NEEDS OR BE FREE FROM ERRORS, OR THAT THE OPERATION OF THE SOFTWARE + * WILL BE UNINTERRUPTED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @Author: Hans Zandbelt - hans.zandbelt@openidc.com + */ + +#include "handle/handle.h" +#include "mod_auth_openidc.h" +#include "proto.h" +#include "util.h" + +#define OIDC_DPOP_PARAM_URL "url" +#define OIDC_DPOP_PARAM_METHOD "method" + +int oidc_dpop_request(request_rec *r, oidc_cfg_t *c, oidc_session_t *session) { + int rc = HTTP_BAD_REQUEST; + char *s_url = NULL; + char *s_access_token = NULL; + const char *session_access_token = NULL; + char *s_method = NULL; + char *s_dpop = NULL; + char *s_response = NULL; + json_t *json = NULL; + + /* try to make sure that the proof-of-possession semantics are preserved */ + if ((_oidc_strnatcasecmp(r->useragent_ip, r->connection->local_ip) != 0) && + (apr_table_get(r->subprocess_env, "OIDC_DPOP_API_INSECURE") == 0)) { + oidc_warn( + r, + "reject DPoP creation request from remote host: you should create a separate virtual (sub)host " + "that requires client certificate authentication to allow and proxy this request " + "(r->useragent_ip=%s, " + "r->connection->local_ip=%s)", + r->useragent_ip, r->connection->local_ip); + rc = HTTP_UNAUTHORIZED; + goto end; + } + + /* retrieve the access token parameter */ + oidc_util_request_parameter_get(r, OIDC_REDIRECT_URI_REQUEST_DPOP, &s_access_token); + if (s_access_token == NULL) { + oidc_error(r, "\"access_token\" value to the \"%s\" parameter is missing", + OIDC_REDIRECT_URI_REQUEST_DPOP); + goto end; + } + + /* retrieve the URL parameter */ + oidc_util_request_parameter_get(r, OIDC_DPOP_PARAM_URL, &s_url); + if (s_url == NULL) { + oidc_error(r, "\"url\" parameter is missing"); + goto end; + } + + /* parse the optional HTTP method parameter */ + oidc_util_request_parameter_get(r, OIDC_DPOP_PARAM_METHOD, &s_method); + if (_oidc_strnatcasecmp(s_method, "post") == 0) + s_method = "POST"; + else if ((_oidc_strnatcasecmp(s_method, "get") == 0) || (s_method == NULL)) + s_method = "GET"; + + /* check that we actually have a user session and this is someone calling with a proper session cookie */ + if (session->remote_user == NULL) { + oidc_warn(r, "no user session found"); + rc = HTTP_UNAUTHORIZED; + goto end; + } + + session_access_token = oidc_session_get_access_token(r, session); + if (session_access_token == NULL) { + oidc_error(r, "no \"access_token\" was found in the session"); + goto end; + } + + if (_oidc_strcmp(s_access_token, session_access_token) != 0) { + oidc_error(r, "the provided \"access_token\" parameter is not matching the current access token stored " + "in the user session"); + goto end; + } + + /* create the DPoP header value */ + s_dpop = oidc_proto_dpop(r, c, s_url, s_method, s_access_token); + if (s_dpop == NULL) { + oidc_error(r, "creating the DPoP proof value failed"); + rc = HTTP_INTERNAL_SERVER_ERROR; + goto end; + } + + /* assemble and serialize the JSON response object */ + json = json_object(); + json_object_set_new(json, OIDC_HTTP_HDR_DPOP, json_string(s_dpop)); + s_response = oidc_util_encode_json_object(r, json, JSON_COMPACT | JSON_PRESERVE_ORDER); + + /* return the serialized JSON response */ + rc = oidc_util_http_send(r, s_response, _oidc_strlen(s_response), OIDC_HTTP_CONTENT_TYPE_JSON, OK); + +end: + + if (json) + json_decref(json); + + return rc; +} diff --git a/src/handle/handle.h b/src/handle/handle.h index 733c0c93..fffe2e48 100644 --- a/src/handle/handle.h +++ b/src/handle/handle.h @@ -79,6 +79,9 @@ int oidc_discovery_request(request_rec *r, oidc_cfg_t *cfg); apr_byte_t oidc_is_discovery_response(request_rec *r, oidc_cfg_t *cfg); int oidc_discovery_response(request_rec *r, oidc_cfg_t *c); +// dpop.c +int oidc_dpop_request(request_rec *r, oidc_cfg_t *c, oidc_session_t *session); + // info.c int oidc_info_request(request_rec *r, oidc_cfg_t *c, oidc_session_t *session, apr_byte_t needs_save); diff --git a/src/handle/logout.c b/src/handle/logout.c index b886bf8a..0e2e1bbd 100644 --- a/src/handle/logout.c +++ b/src/handle/logout.c @@ -93,9 +93,10 @@ static void oidc_logout_revoke_tokens(request_rec *r, oidc_cfg_t *c, oidc_sessio apr_table_setn(params, OIDC_PROTO_TOKEN, token); if (oidc_http_post_form(r, oidc_cfg_provider_revocation_endpoint_url_get(provider), params, basic_auth, - bearer_auth, oidc_cfg_provider_ssl_validate_server_get(provider), &response, - NULL, oidc_cfg_http_timeout_long_get(c), oidc_cfg_outgoing_proxy_get(c), - oidc_cfg_dir_pass_cookies_get(r), NULL, NULL, NULL) == FALSE) { + bearer_auth, NULL, oidc_cfg_provider_ssl_validate_server_get(provider), + &response, NULL, oidc_cfg_http_timeout_long_get(c), + oidc_cfg_outgoing_proxy_get(c), oidc_cfg_dir_pass_cookies_get(r), NULL, NULL, + NULL) == FALSE) { oidc_warn(r, "revoking refresh token failed"); } apr_table_unset(params, OIDC_PROTO_TOKEN_TYPE_HINT); @@ -108,9 +109,10 @@ static void oidc_logout_revoke_tokens(request_rec *r, oidc_cfg_t *c, oidc_sessio apr_table_setn(params, OIDC_PROTO_TOKEN, token); if (oidc_http_post_form(r, oidc_cfg_provider_revocation_endpoint_url_get(provider), params, basic_auth, - bearer_auth, oidc_cfg_provider_ssl_validate_server_get(provider), &response, - NULL, oidc_cfg_http_timeout_long_get(c), oidc_cfg_outgoing_proxy_get(c), - oidc_cfg_dir_pass_cookies_get(r), NULL, NULL, NULL) == FALSE) { + bearer_auth, NULL, oidc_cfg_provider_ssl_validate_server_get(provider), + &response, NULL, oidc_cfg_http_timeout_long_get(c), + oidc_cfg_outgoing_proxy_get(c), oidc_cfg_dir_pass_cookies_get(r), NULL, NULL, + NULL) == FALSE) { oidc_warn(r, "revoking access token failed"); } } diff --git a/src/handle/request.c b/src/handle/request.c index ebbd4dfa..bc0be1ce 100644 --- a/src/handle/request.c +++ b/src/handle/request.c @@ -241,9 +241,8 @@ int oidc_request_authenticate_user(request_rec *r, oidc_cfg_t *c, oidc_provider_ /* send off to the OpenID Connect Provider */ // TODO: maybe show intermediate/progress screen "redirecting to" - rc = oidc_proto_authorization_request(r, provider, login_hint, oidc_util_redirect_uri_iss(r, c, provider), - state, proto_state, id_token_hint, code_challenge, auth_request_params, - path_scope); + rc = oidc_proto_authorization_request(r, provider, login_hint, oidc_util_redirect_uri(r, c), state, proto_state, + id_token_hint, code_challenge, auth_request_params, path_scope); OIDC_METRICS_TIMING_ADD(r, c, OM_AUTHN_REQUEST); diff --git a/src/handle/session_management.c b/src/handle/session_management.c index 4f90ef34..e7cce7b3 100644 --- a/src/handle/session_management.c +++ b/src/handle/session_management.c @@ -203,8 +203,7 @@ int oidc_session_management(request_rec *r, oidc_cfg_t *c, oidc_session_t *sessi * session now? */ return oidc_request_authenticate_user( - r, c, provider, - apr_psprintf(r->pool, "%s?session=iframe_rp", oidc_util_redirect_uri_iss(r, c, provider)), NULL, + r, c, provider, apr_psprintf(r->pool, "%s?session=iframe_rp", oidc_util_redirect_uri(r, c)), NULL, id_token_hint, "none", oidc_cfg_dir_path_auth_request_params_get(r), oidc_cfg_dir_path_scope_get(r)); } diff --git a/src/http.c b/src/http.c index 37abde3f..3f056344 100644 --- a/src/http.c +++ b/src/http.c @@ -600,12 +600,12 @@ static const char *oidc_http_user_agent(request_rec *r) { /* * execute a HTTP (GET or POST) request */ -static apr_byte_t oidc_http_call(request_rec *r, const char *url, const char *data, const char *content_type, - const char *basic_auth, const char *bearer_token, int ssl_validate_server, - char **response, long *response_code, oidc_http_timeout_t *http_timeout, - const oidc_http_outgoing_proxy_t *outgoing_proxy, - const apr_array_header_t *pass_cookies, const char *ssl_cert, const char *ssl_key, - const char *ssl_key_pwd) { +static apr_byte_t oidc_http_request(request_rec *r, const char *url, const char *data, const char *content_type, + const char *basic_auth, const char *access_token, const char *dpop, + int ssl_validate_server, char **response, long *response_code, + oidc_http_timeout_t *http_timeout, const oidc_http_outgoing_proxy_t *outgoing_proxy, + const apr_array_header_t *pass_cookies, const char *ssl_cert, const char *ssl_key, + const char *ssl_key_pwd) { char curlError[CURL_ERROR_SIZE]; oidc_curl_buffer curlBuffer; @@ -619,10 +619,10 @@ static apr_byte_t oidc_http_call(request_rec *r, const char *url, const char *da /* do some logging about the inputs */ oidc_debug(r, - "url=%s, data=%s, content_type=%s, basic_auth=%s, bearer_token=%s, ssl_validate_server=%d, " + "url=%s, data=%s, content_type=%s, basic_auth=%s, access_token=%s, dpop=%s, ssl_validate_server=%d, " "request_timeout=%d, connect_timeout=%d, retries=%d, retry_interval=%d, outgoing_proxy=%s:%s:%d, " "pass_cookies=%pp, ssl_cert=%s, ssl_key=%s, ssl_key_pwd=%s", - url, data, content_type, basic_auth ? "****" : "null", bearer_token, ssl_validate_server, + url, data, content_type, basic_auth ? "****" : "null", access_token, dpop, ssl_validate_server, http_timeout->request_timeout, http_timeout->connect_timeout, http_timeout->retries, http_timeout->retry_interval, outgoing_proxy->host_port, outgoing_proxy->username_password ? "****" : "(null)", (int)outgoing_proxy->auth_type, pass_cookies, @@ -706,9 +706,10 @@ static apr_byte_t oidc_http_call(request_rec *r, const char *url, const char *da curl_easy_setopt(curl, CURLOPT_PROXYAUTH, outgoing_proxy->auth_type); } - /* see if we need to add token in the Bearer Authorization header */ - if (bearer_token != NULL) { - h_list = curl_slist_append(h_list, apr_psprintf(r->pool, "Authorization: Bearer %s", bearer_token)); + /* see if we need to add token in the Bearer/DPoP Authorization header */ + if (access_token != NULL) { + h_list = curl_slist_append(h_list, apr_psprintf(r->pool, "%s: %s %s", OIDC_HTTP_HDR_AUTHORIZATION, + dpop ? "DPoP" : "Bearer", access_token)); } /* see if we need to perform HTTP basic authentication to the remote site */ @@ -744,6 +745,11 @@ static apr_byte_t oidc_http_call(request_rec *r, const char *url, const char *da curl_slist_append(h_list, apr_psprintf(r->pool, "%s: %s", OIDC_HTTP_HDR_TRACE_PARENT, traceparent)); } + if (dpop != NULL) { + oidc_debug(r, "appending DPoP header (len=%d)", (int)strlen(dpop)); + h_list = curl_slist_append(h_list, apr_psprintf(r->pool, "%s: %s", OIDC_HTTP_HDR_DPOP, dpop)); + } + /* see if we need to add any custom headers */ if (h_list != NULL) curl_easy_setopt(curl, CURLOPT_HTTPHEADER, h_list); @@ -824,42 +830,42 @@ static apr_byte_t oidc_http_call(request_rec *r, const char *url, const char *da * execute HTTP GET request */ apr_byte_t oidc_http_get(request_rec *r, const char *url, const apr_table_t *params, const char *basic_auth, - const char *bearer_token, int ssl_validate_server, char **response, long *response_code, - oidc_http_timeout_t *http_timeout, const oidc_http_outgoing_proxy_t *outgoing_proxy, - const apr_array_header_t *pass_cookies, const char *ssl_cert, const char *ssl_key, - const char *ssl_key_pwd) { + const char *access_token, const char *dpop, int ssl_validate_server, char **response, + long *response_code, oidc_http_timeout_t *http_timeout, + const oidc_http_outgoing_proxy_t *outgoing_proxy, const apr_array_header_t *pass_cookies, + const char *ssl_cert, const char *ssl_key, const char *ssl_key_pwd) { char *query_url = oidc_http_query_encoded_url(r, url, params); - return oidc_http_call(r, query_url, NULL, NULL, basic_auth, bearer_token, ssl_validate_server, response, - response_code, http_timeout, outgoing_proxy, pass_cookies, ssl_cert, ssl_key, - ssl_key_pwd); + return oidc_http_request(r, query_url, NULL, NULL, basic_auth, access_token, dpop, ssl_validate_server, + response, response_code, http_timeout, outgoing_proxy, pass_cookies, ssl_cert, ssl_key, + ssl_key_pwd); } /* * execute HTTP POST request with form-encoded data */ apr_byte_t oidc_http_post_form(request_rec *r, const char *url, const apr_table_t *params, const char *basic_auth, - const char *bearer_token, int ssl_validate_server, char **response, long *response_code, - oidc_http_timeout_t *http_timeout, const oidc_http_outgoing_proxy_t *outgoing_proxy, - const apr_array_header_t *pass_cookies, const char *ssl_cert, const char *ssl_key, - const char *ssl_key_pwd) { + const char *access_token, const char *dpop, int ssl_validate_server, char **response, + long *response_code, oidc_http_timeout_t *http_timeout, + const oidc_http_outgoing_proxy_t *outgoing_proxy, const apr_array_header_t *pass_cookies, + const char *ssl_cert, const char *ssl_key, const char *ssl_key_pwd) { char *data = oidc_http_form_encoded_data(r, params); - return oidc_http_call(r, url, data, OIDC_HTTP_CONTENT_TYPE_FORM_ENCODED, basic_auth, bearer_token, - ssl_validate_server, response, response_code, http_timeout, outgoing_proxy, pass_cookies, - ssl_cert, ssl_key, ssl_key_pwd); + return oidc_http_request(r, url, data, OIDC_HTTP_CONTENT_TYPE_FORM_ENCODED, basic_auth, access_token, dpop, + ssl_validate_server, response, response_code, http_timeout, outgoing_proxy, + pass_cookies, ssl_cert, ssl_key, ssl_key_pwd); } /* * execute HTTP POST request with JSON-encoded data */ apr_byte_t oidc_http_post_json(request_rec *r, const char *url, json_t *json, const char *basic_auth, - const char *bearer_token, int ssl_validate_server, char **response, long *response_code, - oidc_http_timeout_t *http_timeout, const oidc_http_outgoing_proxy_t *outgoing_proxy, - const apr_array_header_t *pass_cookies, const char *ssl_cert, const char *ssl_key, - const char *ssl_key_pwd) { + const char *access_token, const char *dpop, int ssl_validate_server, char **response, + long *response_code, oidc_http_timeout_t *http_timeout, + const oidc_http_outgoing_proxy_t *outgoing_proxy, const apr_array_header_t *pass_cookies, + const char *ssl_cert, const char *ssl_key, const char *ssl_key_pwd) { char *data = json != NULL ? oidc_util_encode_json_object(r, json, JSON_COMPACT) : NULL; - return oidc_http_call(r, url, data, OIDC_HTTP_CONTENT_TYPE_JSON, basic_auth, bearer_token, ssl_validate_server, - response, response_code, http_timeout, outgoing_proxy, pass_cookies, ssl_cert, ssl_key, - ssl_key_pwd); + return oidc_http_request(r, url, data, OIDC_HTTP_CONTENT_TYPE_JSON, basic_auth, access_token, dpop, + ssl_validate_server, response, response_code, http_timeout, outgoing_proxy, + pass_cookies, ssl_cert, ssl_key, ssl_key_pwd); } /* diff --git a/src/http.h b/src/http.h index faa63176..32b17fb4 100644 --- a/src/http.h +++ b/src/http.h @@ -86,6 +86,7 @@ #define OIDC_HTTP_HDR_X_FRAME_OPTIONS "X-Frame-Options" #define OIDC_HTTP_HDR_WWW_AUTHENTICATE "WWW-Authenticate" #define OIDC_HTTP_HDR_TRACE_PARENT "traceparent" +#define OIDC_HTTP_HDR_DPOP "DPoP" #define OIDC_HTTP_HDR_VAL_XML_HTTP_REQUEST "XMLHttpRequest" #define OIDC_HTTP_HDR_VAL_NAVIGATE "navigate" @@ -139,20 +140,20 @@ const char *oidc_http_hdr_forwarded_get(const request_rec *r, const char *elem); char *oidc_http_hdr_normalize_name(const request_rec *r, const char *str); apr_byte_t oidc_http_get(request_rec *r, const char *url, const apr_table_t *params, const char *basic_auth, - const char *bearer_token, int ssl_validate_server, char **response, long *response_code, - oidc_http_timeout_t *http_timeout, const oidc_http_outgoing_proxy_t *outgoing_proxy, - const apr_array_header_t *pass_cookies, const char *ssl_cert, const char *ssl_key, - const char *ssl_key_pwd); + const char *access_token, const char *dpop, int ssl_validate_server, char **response, + long *response_code, oidc_http_timeout_t *http_timeout, + const oidc_http_outgoing_proxy_t *outgoing_proxy, const apr_array_header_t *pass_cookies, + const char *ssl_cert, const char *ssl_key, const char *ssl_key_pwd); apr_byte_t oidc_http_post_form(request_rec *r, const char *url, const apr_table_t *params, const char *basic_auth, - const char *bearer_token, int ssl_validate_server, char **response, long *response_code, - oidc_http_timeout_t *http_timeout, const oidc_http_outgoing_proxy_t *outgoing_proxy, - const apr_array_header_t *pass_cookies, const char *ssl_cert, const char *ssl_key, - const char *ssl_key_pwd); + const char *access_token, const char *dpop, int ssl_validate_server, char **response, + long *response_code, oidc_http_timeout_t *http_timeout, + const oidc_http_outgoing_proxy_t *outgoing_proxy, const apr_array_header_t *pass_cookies, + const char *ssl_cert, const char *ssl_key, const char *ssl_key_pwd); apr_byte_t oidc_http_post_json(request_rec *r, const char *url, json_t *data, const char *basic_auth, - const char *bearer_token, int ssl_validate_server, char **response, long *response_code, - oidc_http_timeout_t *http_timeout, const oidc_http_outgoing_proxy_t *outgoing_proxy, - const apr_array_header_t *pass_cookies, const char *ssl_cert, const char *ssl_key, - const char *ssl_key_pwd); + const char *access_token, const char *dpop, int ssl_validate_server, char **response, + long *response_code, oidc_http_timeout_t *http_timeout, + const oidc_http_outgoing_proxy_t *outgoing_proxy, const apr_array_header_t *pass_cookies, + const char *ssl_cert, const char *ssl_key, const char *ssl_key_pwd); apr_byte_t oidc_util_request_has_parameter(request_rec *r, const char *param); apr_byte_t oidc_util_request_parameter_get(request_rec *r, char *name, char **value); int oidc_util_http_send(request_rec *r, const char *data, size_t data_len, const char *content_type, diff --git a/src/metadata.c b/src/metadata.c index 5c868262..a9e359d6 100644 --- a/src/metadata.c +++ b/src/metadata.c @@ -122,7 +122,7 @@ #define OIDC_METADATA_REQUEST_OBJECT "request_object" #define OIDC_METADATA_USERINFO_TOKEN_METHOD "userinfo_token_method" #define OIDC_METADATA_AUTH_REQUEST_METHOD "auth_request_method" -#define OIDC_METADATA_ISSUER_SPECIFIC_REDIRECT_URI "issuer_specific_redirect_uri" +#define OIDC_METADATA_RESPONSE_REQUIRE_ISS "response_require_iss" /* * get the metadata filename for a specified issuer (cq. urlencode it) @@ -493,8 +493,7 @@ static apr_byte_t oidc_metadata_client_register(request_rec *r, oidc_cfg_t *cfg, /* assemble the JSON registration request */ json_t *data = json_object(); json_object_set_new(data, OIDC_METADATA_CLIENT_NAME, json_string(oidc_cfg_provider_client_name_get(provider))); - json_object_set_new(data, OIDC_METADATA_REDIRECT_URIS, - json_pack("[s]", oidc_util_redirect_uri_iss(r, cfg, provider))); + json_object_set_new(data, OIDC_METADATA_REDIRECT_URIS, json_pack("[s]", oidc_util_redirect_uri(r, cfg))); json_t *response_types = json_array(); apr_array_header_t *flows = oidc_proto_supported_flows(r->pool); @@ -594,7 +593,7 @@ static apr_byte_t oidc_metadata_client_register(request_rec *r, oidc_cfg_t *cfg, /* dynamically register the client with the specified parameters */ if (oidc_http_post_json(r, oidc_cfg_provider_registration_endpoint_url_get(provider), data, NULL, - oidc_cfg_provider_registration_token_get(provider), + oidc_cfg_provider_registration_token_get(provider), NULL, oidc_cfg_provider_ssl_validate_server_get(provider), response, NULL, oidc_cfg_http_timeout_short_get(cfg), oidc_cfg_outgoing_proxy_get(cfg), oidc_cfg_dir_pass_cookies_get(r), NULL, NULL, NULL) == FALSE) { @@ -623,7 +622,7 @@ static apr_byte_t oidc_metadata_jwks_retrieve_and_cache(request_rec *r, oidc_cfg const char *url = (jwks_uri->signed_uri != NULL) ? jwks_uri->signed_uri : jwks_uri->uri; /* get the JWKs from the specified URL with the specified parameters */ - if (oidc_http_get(r, url, NULL, NULL, NULL, ssl_validate_server, &response, NULL, + if (oidc_http_get(r, url, NULL, NULL, NULL, NULL, ssl_validate_server, &response, NULL, oidc_cfg_http_timeout_long_get(cfg), oidc_cfg_outgoing_proxy_get(cfg), oidc_cfg_dir_pass_cookies_get(r), NULL, NULL, NULL) == FALSE) return FALSE; @@ -733,7 +732,7 @@ apr_byte_t oidc_metadata_provider_retrieve(request_rec *r, oidc_cfg_t *cfg, cons OIDC_METRICS_TIMING_START(r, cfg); /* get provider metadata from the specified URL with the specified parameters */ - if (oidc_http_get(r, url, NULL, NULL, NULL, + if (oidc_http_get(r, url, NULL, NULL, NULL, NULL, oidc_cfg_provider_ssl_validate_server_get(oidc_cfg_provider_get(cfg)), response, NULL, oidc_cfg_http_timeout_short_get(cfg), oidc_cfg_outgoing_proxy_get(cfg), oidc_cfg_dir_pass_cookies_get(r), NULL, NULL, NULL) == FALSE) { @@ -1421,9 +1420,9 @@ apr_byte_t oidc_metadata_conf_parse(request_rec *r, oidc_cfg_t *cfg, json_t *j_c } /* get the issuer specific redirect URI option */ - oidc_metadata_parse_boolean(r, j_conf, OIDC_METADATA_ISSUER_SPECIFIC_REDIRECT_URI, &ivalue, - oidc_cfg_provider_issuer_specific_redirect_uri_get(oidc_cfg_provider_get(cfg))); - OIDC_METADATA_PROVIDER_SET_INT(issuer_specific_redirect_uri, ivalue, rv) + oidc_metadata_parse_boolean(r, j_conf, OIDC_METADATA_RESPONSE_REQUIRE_ISS, &ivalue, + oidc_cfg_provider_response_require_iss_get(oidc_cfg_provider_get(cfg))); + OIDC_METADATA_PROVIDER_SET_INT(response_require_iss, ivalue, rv) return TRUE; } diff --git a/src/metrics.c b/src/metrics.c index 741ab795..00b7c0d5 100644 --- a/src/metrics.c +++ b/src/metrics.c @@ -145,11 +145,13 @@ const oidc_metrics_counter_info_t _oidc_metrics_counters_info[] = { { OM_CLASS_REDIRECT_URI, "request.remove_at_cache", "access token cache removal requests to the redirect URI", }, { OM_CLASS_REDIRECT_URI, "request.session", "revoke session requests to the redirect URI", }, { OM_CLASS_REDIRECT_URI, "request.info", "info hook requests to the redirect URI", }, + { OM_CLASS_REDIRECT_URI, "request.dpop", "DPoP requests to the redirect URI", }, { OM_CLASS_REDIRECT_URI, "error.provider", "provider authentication response errors received at the redirect URI", }, { OM_CLASS_REDIRECT_URI, "error.invalid", "invalid requests to the redirect URI", }, { OM_CLASS_CONTENT, "request.declined", "requests declined by the content handler" }, { OM_CLASS_CONTENT, "request.info", "info hook requests to the content handler" }, + { OM_CLASS_CONTENT, "request.dpop", "DPoP requests to the content handler" }, { OM_CLASS_CONTENT, "request.jwks", "JWKs requests to the content handler" }, { OM_CLASS_CONTENT, "request.discovery", "discovery requests to the content handler" }, { OM_CLASS_CONTENT, "request.post-preserve", "HTTP POST preservation requests to the content handler" }, diff --git a/src/metrics.h b/src/metrics.h index fffbb595..a8ccea6d 100644 --- a/src/metrics.h +++ b/src/metrics.h @@ -172,11 +172,13 @@ typedef enum { OM_REDIRECT_URI_REQUEST_REMOVE_AT_CACHE, OM_REDIRECT_URI_REQUEST_REVOKE_SESSION, OM_REDIRECT_URI_REQUEST_INFO, + OM_REDIRECT_URI_REQUEST_DPOP, OM_REDIRECT_URI_ERROR_PROVIDER, OM_REDIRECT_URI_ERROR_INVALID, OM_CONTENT_REQUEST_DECLINED, OM_CONTENT_REQUEST_INFO, + OM_CONTENT_REQUEST_DPOP, OM_CONTENT_REQUEST_JWKS, OM_CONTENT_REQUEST_DISCOVERY, OM_CONTENT_REQUEST_POST_PRESERVE, diff --git a/src/mod_auth_openidc.c b/src/mod_auth_openidc.c index 55fcfc66..b3d95051 100644 --- a/src/mod_auth_openidc.c +++ b/src/mod_auth_openidc.c @@ -1420,6 +1420,21 @@ int oidc_handle_redirect_uri_request(request_rec *r, oidc_cfg_t *c, oidc_session return rc; + } else if (oidc_util_request_has_parameter(r, OIDC_REDIRECT_URI_REQUEST_DPOP)) { + + if (session->remote_user == NULL) + return HTTP_UNAUTHORIZED; + + OIDC_METRICS_COUNTER_INC(r, c, OM_REDIRECT_URI_REQUEST_DPOP); + + r->user = session->remote_user; + + // retain this session across the authentication and content handler phases + // by storing it in the request state + apr_pool_userdata_set(session, OIDC_USERDATA_SESSION, NULL, r->pool); + + return OK; + } else if (oidc_util_request_has_parameter(r, OIDC_REDIRECT_URI_REQUEST_INFO)) { if (session->remote_user == NULL) @@ -1430,7 +1445,7 @@ int oidc_handle_redirect_uri_request(request_rec *r, oidc_cfg_t *c, oidc_session // need to establish user/claims for authorization purposes rc = oidc_handle_existing_session(r, c, session, &needs_save); - // retain this session across the authentication hand content handler phases + // retain this session across the authentication and content handler phases // by storing it in the request state apr_pool_userdata_set(session, OIDC_USERDATA_SESSION, NULL, r->pool); diff --git a/src/mod_auth_openidc.h b/src/mod_auth_openidc.h index 5455166f..a657dcae 100644 --- a/src/mod_auth_openidc.h +++ b/src/mod_auth_openidc.h @@ -88,6 +88,7 @@ #define OIDC_METHOD_FORM_POST "form_post" #define OIDC_REDIRECT_URI_REQUEST_INFO "info" +#define OIDC_REDIRECT_URI_REQUEST_DPOP "dpop" #define OIDC_REDIRECT_URI_REQUEST_LOGOUT "logout" #define OIDC_REDIRECT_URI_REQUEST_JWKS "jwks" #define OIDC_REDIRECT_URI_REQUEST_SESSION "session" @@ -112,6 +113,11 @@ #define OIDC_CLAIM_TARGET_LINK_URI "target_link_uri" #define OIDC_CLAIM_SID "sid" #define OIDC_CLAIM_EVENTS "events" +#define OIDC_CLAIM_TYP "typ" +#define OIDC_CLAIM_JWK "jwk" +#define OIDC_CLAIM_HTM "htm" +#define OIDC_CLAIM_HTU "htu" +#define OIDC_CLAIM_ATH "ath" #define OIDC_APP_INFO_REFRESH_TOKEN "refresh_token" #define OIDC_APP_INFO_ACCESS_TOKEN "access_token" diff --git a/src/oauth.c b/src/oauth.c index ab0ff00a..cf13446a 100644 --- a/src/oauth.c +++ b/src/oauth.c @@ -60,7 +60,7 @@ apr_byte_t oidc_oauth_metadata_provider_retrieve(request_rec *r, oidc_cfg_t *cfg json_t **j_metadata, char **response) { /* get provider metadata from the specified URL with the specified parameters */ - if (oidc_http_get(r, url, NULL, NULL, NULL, oidc_cfg_oauth_ssl_validate_server_get(cfg), response, NULL, + if (oidc_http_get(r, url, NULL, NULL, NULL, NULL, oidc_cfg_oauth_ssl_validate_server_get(cfg), response, NULL, oidc_cfg_http_timeout_short_get(cfg), oidc_cfg_outgoing_proxy_get(cfg), oidc_cfg_dir_pass_cookies_get(r), NULL, NULL, NULL) == FALSE) return FALSE; @@ -172,14 +172,14 @@ static apr_byte_t oidc_oauth_validate_access_token(request_rec *r, oidc_cfg_t *c /* call the endpoint with the constructed parameter set and return the resulting response */ return oidc_cfg_oauth_introspection_endpoint_method_get(c) == OIDC_INTROSPECTION_METHOD_GET ? oidc_http_get(r, oidc_cfg_oauth_introspection_endpoint_url_get(c), params, basic_auth, bearer_auth, - oidc_cfg_oauth_ssl_validate_server_get(c), response, NULL, + NULL, oidc_cfg_oauth_ssl_validate_server_get(c), response, NULL, oidc_cfg_http_timeout_long_get(c), oidc_cfg_outgoing_proxy_get(c), oidc_cfg_dir_pass_cookies_get(r), oidc_cfg_oauth_introspection_endpoint_tls_client_cert_get(c), oidc_cfg_oauth_introspection_endpoint_tls_client_key_get(c), oidc_cfg_oauth_introspection_endpoint_tls_client_key_pwd_get(c)) : oidc_http_post_form(r, oidc_cfg_oauth_introspection_endpoint_url_get(c), params, basic_auth, - bearer_auth, oidc_cfg_oauth_ssl_validate_server_get(c), response, NULL, + bearer_auth, NULL, oidc_cfg_oauth_ssl_validate_server_get(c), response, NULL, oidc_cfg_http_timeout_long_get(c), oidc_cfg_outgoing_proxy_get(c), oidc_cfg_dir_pass_cookies_get(r), oidc_cfg_oauth_introspection_endpoint_tls_client_cert_get(c), diff --git a/src/proto.c b/src/proto.c index 586f4ac5..e09cca5d 100644 --- a/src/proto.c +++ b/src/proto.c @@ -158,10 +158,10 @@ static int oidc_proto_pushed_authorization_request(request_rec *r, struct oidc_p if (oidc_proto_token_endpoint_auth( r, cfg, oidc_cfg_provider_token_endpoint_auth_get(provider), oidc_cfg_provider_client_id_get(provider), oidc_cfg_provider_client_secret_get(provider), oidc_cfg_provider_client_keys_get(provider), - oidc_cfg_provider_token_endpoint_url_get(provider), params, NULL, &basic_auth, &bearer_auth) == FALSE) + oidc_cfg_provider_issuer_get(provider), params, NULL, &basic_auth, &bearer_auth) == FALSE) goto out; - if (oidc_http_post_form(r, endpoint_url, params, basic_auth, bearer_auth, + if (oidc_http_post_form(r, endpoint_url, params, basic_auth, bearer_auth, NULL, oidc_cfg_provider_ssl_validate_server_get(provider), &response, NULL, oidc_cfg_http_timeout_long_get(cfg), oidc_cfg_outgoing_proxy_get(cfg), oidc_cfg_dir_pass_cookies_get(r), NULL, NULL, NULL) == FALSE) @@ -352,11 +352,10 @@ apr_byte_t oidc_proto_is_post_authorization_response(request_rec *r, oidc_cfg_t */ apr_byte_t oidc_proto_is_redirect_authorization_response(request_rec *r, oidc_cfg_t *cfg) { - /* prereq: this is a call to the configured redirect_uri; see if it is a GET with state and id_token or code + /* prereq: this is a call to the configured redirect_uri; see if it is a GET with id_token or code * parameters */ - return ((r->method_number == M_GET) && oidc_util_request_has_parameter(r, OIDC_PROTO_STATE) && - (oidc_util_request_has_parameter(r, OIDC_PROTO_ID_TOKEN) || - oidc_util_request_has_parameter(r, OIDC_PROTO_CODE))); + return ((r->method_number == M_GET) && (oidc_util_request_has_parameter(r, OIDC_PROTO_ID_TOKEN) || + oidc_util_request_has_parameter(r, OIDC_PROTO_CODE))); } /* @@ -750,10 +749,10 @@ apr_byte_t oidc_proto_validate_aud_and_azp(request_rec *r, oidc_cfg_t *cfg, oidc } else if (json_is_array(aud)) { if ((json_array_size(aud) > 1) && (azp == NULL)) { - oidc_debug(r, - "the \"%s\" claim value in the id_token is an array with more than 1 " - "element, but \"%s\" claim is not present (a SHOULD in the spec...)", - OIDC_CLAIM_AUD, OIDC_CLAIM_AZP); + oidc_warn(r, + "the \"%s\" claim value in the id_token is an array with more than 1 " + "element, but \"%s\" claim is not present (a SHOULD in the spec...)", + OIDC_CLAIM_AUD, OIDC_CLAIM_AZP); } if (oidc_util_json_array_has_value(r, aud, oidc_cfg_provider_client_id_get(provider)) == @@ -764,6 +763,16 @@ apr_byte_t oidc_proto_validate_aud_and_azp(request_rec *r, oidc_cfg_t *cfg, oidc oidc_cfg_provider_client_id_get(provider), OIDC_CLAIM_AUD); return FALSE; } + + if (json_array_size(aud) > 1) { + oidc_error( + r, + "our configured client_id (%s) was found in the array of values " + "for \"%s\" claim, but there are other unknown/untrusted values included as well", + oidc_cfg_provider_client_id_get(provider), OIDC_CLAIM_AUD); + return FALSE; + } + } else { oidc_error(r, "id_token JSON payload \"%s\" claim is not a string nor an array", OIDC_CLAIM_AUD); @@ -1236,14 +1245,15 @@ apr_byte_t oidc_proto_parse_idtoken(request_rec *r, oidc_cfg_t *cfg, oidc_provid * check that the access_token type is supported */ static apr_byte_t oidc_proto_validate_token_type(request_rec *r, oidc_provider_t *provider, const char *token_type) { - /* we only support bearer/Bearer */ + /* we only support bearer/Bearer and DPoP/dpop */ if ((token_type != NULL) && (_oidc_strnatcasecmp(token_type, OIDC_PROTO_BEARER) != 0) && + (_oidc_strnatcasecmp(token_type, OIDC_PROTO_DPOP) != 0) && (oidc_cfg_provider_userinfo_endpoint_url_get(provider) != NULL)) { oidc_error(r, "token_type is \"%s\" and UserInfo endpoint (%s) for issuer \"%s\" is set: can only deal " - "with \"%s\" authentication against a UserInfo endpoint!", + "with \"%s\" or \"%s\" authentication against a UserInfo endpoint!", token_type, oidc_cfg_provider_userinfo_endpoint_url_get(provider), - oidc_cfg_provider_issuer_get(provider), OIDC_PROTO_BEARER); + oidc_cfg_provider_issuer_get(provider), OIDC_PROTO_BEARER, OIDC_PROTO_DPOP); return FALSE; } return TRUE; @@ -1503,6 +1513,7 @@ static apr_byte_t oidc_proto_token_endpoint_request(request_rec *r, oidc_cfg_t * char *response = NULL; char *basic_auth = NULL; char *bearer_auth = NULL; + char *dpop = NULL; json_t *j_result = NULL, *j_expires_in = NULL; /* add the token endpoint authentication credentials */ @@ -1516,9 +1527,12 @@ static apr_byte_t oidc_proto_token_endpoint_request(request_rec *r, oidc_cfg_t * oidc_util_table_add_query_encoded_params(r->pool, params, oidc_cfg_provider_token_endpoint_params_get(provider)); - /* send the refresh request to the token endpoint */ + if (oidc_cfg_provider_response_require_iss_get(provider)) + dpop = oidc_proto_dpop(r, cfg, oidc_cfg_provider_token_endpoint_url_get(provider), "POST", NULL); + + /* send the request to the token endpoint */ if (oidc_http_post_form(r, oidc_cfg_provider_token_endpoint_url_get(provider), params, basic_auth, bearer_auth, - oidc_cfg_provider_ssl_validate_server_get(provider), &response, NULL, + dpop, oidc_cfg_provider_ssl_validate_server_get(provider), &response, NULL, oidc_cfg_http_timeout_long_get(cfg), oidc_cfg_outgoing_proxy_get(cfg), oidc_cfg_dir_pass_cookies_get(r), oidc_cfg_provider_token_endpoint_tls_client_cert_get(provider), @@ -1582,7 +1596,7 @@ static apr_byte_t oidc_proto_resolve_code(request_rec *r, oidc_cfg_t *cfg, oidc_ apr_table_t *params = apr_table_make(r->pool, 5); apr_table_setn(params, OIDC_PROTO_GRANT_TYPE, OIDC_PROTO_GRANT_TYPE_AUTHZ_CODE); apr_table_setn(params, OIDC_PROTO_CODE, code); - apr_table_set(params, OIDC_PROTO_REDIRECT_URI, oidc_util_redirect_uri_iss(r, cfg, provider)); + apr_table_set(params, OIDC_PROTO_REDIRECT_URI, oidc_util_redirect_uri(r, cfg)); if (code_verifier) apr_table_setn(params, OIDC_PROTO_CODE_VERIFIER, code_verifier); @@ -1750,7 +1764,7 @@ static apr_byte_t oidc_proto_userinfo_request_composite_claims(request_rec *r, o json_string_value(json_object_get(value, OIDC_COMPOSITE_CLAIM_ENDPOINT)); if ((access_token != NULL) && (endpoint != NULL)) { oidc_http_get( - r, endpoint, NULL, NULL, access_token, + r, endpoint, NULL, NULL, access_token, NULL, oidc_cfg_provider_ssl_validate_server_get(oidc_cfg_provider_get(cfg)), &s_json, NULL, oidc_cfg_http_timeout_long_get(cfg), oidc_cfg_outgoing_proxy_get(cfg), oidc_cfg_dir_pass_cookies_get(r), NULL, @@ -1810,6 +1824,7 @@ static apr_byte_t oidc_proto_userinfo_request_composite_claims(request_rec *r, o apr_byte_t oidc_proto_userinfo_request(request_rec *r, oidc_cfg_t *cfg, oidc_provider_t *provider, const char *id_token_sub, const char *access_token, char **response, char **userinfo_jwt, long *response_code) { + char *dpop = NULL; oidc_debug(r, "enter, endpoint=%s, access_token=%s", oidc_cfg_provider_userinfo_endpoint_url_get(provider), access_token); @@ -1818,8 +1833,11 @@ apr_byte_t oidc_proto_userinfo_request(request_rec *r, oidc_cfg_t *cfg, oidc_pro /* get the JSON response */ if (oidc_cfg_provider_userinfo_token_method_get(provider) == OIDC_USER_INFO_TOKEN_METHOD_HEADER) { + if (oidc_cfg_provider_response_require_iss_get(provider)) + dpop = oidc_proto_dpop(r, cfg, oidc_cfg_provider_userinfo_endpoint_url_get(provider), "GET", + access_token); if (oidc_http_get(r, oidc_cfg_provider_userinfo_endpoint_url_get(provider), NULL, NULL, access_token, - oidc_cfg_provider_ssl_validate_server_get(provider), response, response_code, + dpop, oidc_cfg_provider_ssl_validate_server_get(provider), response, response_code, oidc_cfg_http_timeout_long_get(cfg), oidc_cfg_outgoing_proxy_get(cfg), oidc_cfg_dir_pass_cookies_get(r), NULL, NULL, NULL) == FALSE) { OIDC_METRICS_COUNTER_INC(r, cfg, OM_PROVIDER_USERINFO_ERROR); @@ -1828,10 +1846,14 @@ apr_byte_t oidc_proto_userinfo_request(request_rec *r, oidc_cfg_t *cfg, oidc_pro } else if (oidc_cfg_provider_userinfo_token_method_get(provider) == OIDC_USER_INFO_TOKEN_METHOD_POST) { apr_table_t *params = apr_table_make(r->pool, 4); apr_table_setn(params, OIDC_PROTO_ACCESS_TOKEN, access_token); + if (oidc_cfg_provider_response_require_iss_get(provider)) + dpop = oidc_proto_dpop(r, cfg, oidc_cfg_provider_userinfo_endpoint_url_get(provider), "POST", + access_token); if (oidc_http_post_form(r, oidc_cfg_provider_userinfo_endpoint_url_get(provider), params, NULL, NULL, - oidc_cfg_provider_ssl_validate_server_get(provider), response, response_code, - oidc_cfg_http_timeout_long_get(cfg), oidc_cfg_outgoing_proxy_get(cfg), - oidc_cfg_dir_pass_cookies_get(r), NULL, NULL, NULL) == FALSE) { + dpop, oidc_cfg_provider_ssl_validate_server_get(provider), response, + response_code, oidc_cfg_http_timeout_long_get(cfg), + oidc_cfg_outgoing_proxy_get(cfg), oidc_cfg_dir_pass_cookies_get(r), NULL, NULL, + NULL) == FALSE) { OIDC_METRICS_COUNTER_INC(r, cfg, OM_PROVIDER_USERINFO_ERROR); return FALSE; } @@ -1894,7 +1916,7 @@ static apr_byte_t oidc_proto_webfinger_discovery(request_rec *r, oidc_cfg_t *cfg apr_table_setn(params, "rel", "http://openid.net/specs/connect/1.0/issuer"); char *response = NULL; - if (oidc_http_get(r, url, params, NULL, NULL, + if (oidc_http_get(r, url, params, NULL, NULL, NULL, oidc_cfg_provider_ssl_validate_server_get(oidc_cfg_provider_get(cfg)), &response, NULL, oidc_cfg_http_timeout_short_get(cfg), oidc_cfg_outgoing_proxy_get(cfg), oidc_cfg_dir_pass_cookies_get(r), NULL, NULL, NULL) == FALSE) { @@ -2299,7 +2321,8 @@ static apr_byte_t oidc_proto_validate_response_mode(request_rec *r, oidc_proto_s * was sent to */ static apr_byte_t oidc_proto_validate_issuer_client_id(request_rec *r, const char *configured_issuer, - const char *response_issuer, const char *configured_client_id, + const char *response_issuer, int require_issuer, + const char *configured_client_id, const char *response_client_id) { if (response_issuer != NULL) { @@ -2310,6 +2333,9 @@ static apr_byte_t oidc_proto_validate_issuer_client_id(request_rec *r, const cha configured_issuer, response_issuer); return FALSE; } + } else if (require_issuer) { + oidc_error(r, "no required \"iss\" parameter provided in the response by the OP"); + return FALSE; } if (response_client_id != NULL) { @@ -2335,7 +2361,7 @@ static apr_byte_t oidc_proto_validate_response_type_mode_issuer(request_rec *r, apr_table_t *params, oidc_proto_state_t *proto_state, const char *response_mode, const char *default_response_mode, const char *issuer, - const char *c_client_id) { + int require_issuer, const char *c_client_id) { const char *code = apr_table_get(params, OIDC_PROTO_CODE); const char *id_token = apr_table_get(params, OIDC_PROTO_ID_TOKEN); @@ -2343,7 +2369,7 @@ static apr_byte_t oidc_proto_validate_response_type_mode_issuer(request_rec *r, const char *iss = apr_table_get(params, OIDC_PROTO_ISS); const char *client_id = apr_table_get(params, OIDC_PROTO_CLIENT_ID); - if (oidc_proto_validate_issuer_client_id(r, issuer, iss, c_client_id, client_id) == FALSE) + if (oidc_proto_validate_issuer_client_id(r, issuer, iss, require_issuer, c_client_id, client_id) == FALSE) return FALSE; if (oidc_proto_validate_response_type(r, requested_response_type, code, id_token, access_token) == FALSE) @@ -2456,7 +2482,8 @@ apr_byte_t oidc_proto_authorization_response_code_idtoken(request_rec *r, oidc_c if (oidc_proto_validate_response_type_mode_issuer( r, response_type, params, proto_state, response_mode, OIDC_PROTO_RESPONSE_MODE_FRAGMENT, - oidc_cfg_provider_issuer_get(provider), oidc_cfg_provider_client_id_get(provider)) == FALSE) + oidc_cfg_provider_issuer_get(provider), oidc_cfg_provider_response_require_iss_get(provider), + oidc_cfg_provider_client_id_get(provider)) == FALSE) return FALSE; if (oidc_proto_parse_idtoken_and_validate_code(r, c, proto_state, provider, response_type, params, jwt, TRUE) == @@ -2489,7 +2516,8 @@ apr_byte_t oidc_proto_handle_authorization_response_code_token(request_rec *r, o if (oidc_proto_validate_response_type_mode_issuer( r, response_type, params, proto_state, response_mode, OIDC_PROTO_RESPONSE_MODE_FRAGMENT, - oidc_cfg_provider_issuer_get(provider), oidc_cfg_provider_client_id_get(provider)) == FALSE) + oidc_cfg_provider_issuer_get(provider), oidc_cfg_provider_response_require_iss_get(provider), + oidc_cfg_provider_client_id_get(provider)) == FALSE) return FALSE; /* clear parameters that should only be set from the token endpoint */ @@ -2519,7 +2547,8 @@ apr_byte_t oidc_proto_handle_authorization_response_code(request_rec *r, oidc_cf if (oidc_proto_validate_response_type_mode_issuer( r, response_type, params, proto_state, response_mode, OIDC_PROTO_RESPONSE_MODE_QUERY, - oidc_cfg_provider_issuer_get(provider), oidc_cfg_provider_client_id_get(provider)) == FALSE) + oidc_cfg_provider_issuer_get(provider), oidc_cfg_provider_response_require_iss_get(provider), + oidc_cfg_provider_client_id_get(provider)) == FALSE) return FALSE; /* clear parameters that should only be set from the token endpoint */ @@ -2562,7 +2591,8 @@ static apr_byte_t oidc_proto_handle_implicit_flow(request_rec *r, oidc_cfg_t *c, if (oidc_proto_validate_response_type_mode_issuer( r, response_type, params, proto_state, response_mode, OIDC_PROTO_RESPONSE_MODE_FRAGMENT, - oidc_cfg_provider_issuer_get(provider), oidc_cfg_provider_client_id_get(provider)) == FALSE) + oidc_cfg_provider_issuer_get(provider), oidc_cfg_provider_response_require_iss_get(provider), + oidc_cfg_provider_client_id_get(provider)) == FALSE) return FALSE; if (oidc_proto_parse_idtoken_and_validate_code(r, c, proto_state, provider, response_type, params, jwt, TRUE) == @@ -2673,3 +2703,79 @@ int oidc_proto_return_www_authenticate(request_rec *r, const char *error, const oidc_http_hdr_err_out_add(r, OIDC_HTTP_HDR_WWW_AUTHENTICATE, hdr); return HTTP_UNAUTHORIZED; } + +/* + * generate a DPoP proof for the specified URL/method/access_token + */ +char *oidc_proto_dpop(request_rec *r, oidc_cfg_t *cfg, const char *url, const char *method, const char *access_token) { + // TODO: share with create_userinfo_jwt + oidc_jwt_t *jwt = NULL; + oidc_jwk_t *jwk = NULL; + oidc_jose_error_t err; + char *jti = NULL; + cjose_err cjose_err; + char *s_jwk = NULL; + char *cser = NULL; + char *ath = NULL; + + oidc_debug(r, "enter"); + + jwk = oidc_util_key_list_first(oidc_cfg_private_keys_get(cfg), -1, OIDC_JOSE_JWK_SIG_STR); + if (jwk == NULL) { + // TODO: may become an error once DPoP is required by config + oidc_debug(r, "no RSA/EC private signing keys have been configured (in " OIDCPrivateKeyFiles ")"); + goto end; + } + + jwt = oidc_jwt_new(r->pool, TRUE, TRUE); + if (jwt == NULL) + goto end; + + jwt->header.kid = apr_pstrdup(r->pool, jwk->kid); + + if (jwk->kty == CJOSE_JWK_KTY_RSA) + jwt->header.alg = apr_pstrdup(r->pool, CJOSE_HDR_ALG_PS256); + else if (jwk->kty == CJOSE_JWK_KTY_EC) + jwt->header.alg = apr_pstrdup(r->pool, CJOSE_HDR_ALG_ES256); + else { + oidc_error(r, "no usable RSA/EC signing keys has been configured (in " OIDCPrivateKeyFiles ")"); + goto end; + } + + json_object_set_new(jwt->header.value.json, OIDC_CLAIM_TYP, json_string("dpop+jwt")); + s_jwk = cjose_jwk_to_json(jwk->cjose_jwk, 0, &cjose_err); + cjose_header_set_raw(jwt->header.value.json, OIDC_CLAIM_JWK, s_jwk, &cjose_err); + + oidc_util_generate_random_string(r, &jti, 16); + json_object_set_new(jwt->payload.value.json, OIDC_CLAIM_JTI, json_string(jti)); + json_object_set_new(jwt->payload.value.json, OIDC_CLAIM_HTM, json_string(method)); + json_object_set_new(jwt->payload.value.json, OIDC_CLAIM_HTU, json_string(url)); + json_object_set_new(jwt->payload.value.json, OIDC_CLAIM_IAT, json_integer(apr_time_sec(apr_time_now()))); + + if (access_token) { + if (oidc_jose_hash_and_base64url_encode(r->pool, OIDC_JOSE_ALG_SHA256, access_token, + strlen(access_token), &ath, &err) == FALSE) { + oidc_error(r, "oidc_jose_hash_and_base64url_encode failed: %s", oidc_jose_e2s(r->pool, err)); + goto end; + } + json_object_set_new(jwt->payload.value.json, OIDC_CLAIM_ATH, json_string(ath)); + } + + if (oidc_jwt_sign(r->pool, jwt, jwk, FALSE, &err) == FALSE) { + oidc_error(r, "oidc_jwt_sign failed: %s", oidc_jose_e2s(r->pool, err)); + goto end; + } + + cser = oidc_jwt_serialize(r->pool, jwt, &err); + if (cser == NULL) { + oidc_error(r, "oidc_jwt_serialize failed: %s", oidc_jose_e2s(r->pool, err)); + goto end; + } + +end: + + if (jwt) + oidc_jwt_destroy(jwt); + + return cser; +} diff --git a/src/proto.h b/src/proto.h index 2a3bb102..408e5919 100644 --- a/src/proto.h +++ b/src/proto.h @@ -113,6 +113,7 @@ #define OIDC_PROTO_BEARER "Bearer" #define OIDC_PROTO_BASIC "Basic" +#define OIDC_PROTO_DPOP "DPoP" #define OIDC_PKCE_METHOD_PLAIN "plain" #define OIDC_PKCE_METHOD_S256 "S256" @@ -227,5 +228,6 @@ apr_byte_t oidc_proto_validate_nonce(request_rec *r, oidc_cfg_t *cfg, oidc_provi oidc_jwt_t *jwt); int oidc_proto_return_www_authenticate(request_rec *r, const char *error, const char *error_description); +char *oidc_proto_dpop(request_rec *r, oidc_cfg_t *cfg, const char *url, const char *method, const char *access_token); #endif /* _MOD_AUTH_OPENIDC_PROTO_H_ */ diff --git a/src/util.c b/src/util.c index 05afd2ed..7349017f 100644 --- a/src/util.c +++ b/src/util.c @@ -874,25 +874,6 @@ const char *oidc_util_redirect_uri(request_rec *r, oidc_cfg_t *cfg) { return oidc_util_absolute_url(r, cfg, oidc_cfg_redirect_uri_get(cfg)); } -/* - * determine absolute redirect uri that is issuer specific - */ -const char *oidc_util_redirect_uri_iss(request_rec *r, oidc_cfg_t *cfg, oidc_provider_t *provider) { - const char *redirect_uri = oidc_util_redirect_uri(r, cfg); - if (redirect_uri == NULL) { - oidc_error(r, "redirect URI is NULL"); - return NULL; - } - if (oidc_cfg_provider_issuer_specific_redirect_uri_get(provider) != 0) { - redirect_uri = - apr_psprintf(r->pool, "%s%s%s=%s", redirect_uri, - strchr(redirect_uri, OIDC_CHAR_QUERY) != NULL ? OIDC_STR_AMP : OIDC_STR_QUERY, - OIDC_PROTO_ISS, oidc_http_url_encode(r, oidc_cfg_provider_issuer_get(provider))); - oidc_debug(r, "determined issuer specific redirect uri: %s", redirect_uri); - } - return redirect_uri; -} - /* * see if the currently accessed path matches a path from a defined URL */ diff --git a/src/util.h b/src/util.h index edcf5c13..c940d9ac 100644 --- a/src/util.h +++ b/src/util.h @@ -70,7 +70,6 @@ apr_byte_t oidc_util_request_matches_url(request_rec *r, const char *url); char *oidc_util_current_url(request_rec *r, oidc_hdr_x_forwarded_t x_forwarded_headers); const char *oidc_util_absolute_url(request_rec *r, oidc_cfg_t *cfg, const char *url); const char *oidc_util_redirect_uri(request_rec *r, oidc_cfg_t *c); -const char *oidc_util_redirect_uri_iss(request_rec *r, oidc_cfg_t *c, oidc_provider_t *provider); apr_byte_t oidc_util_request_is_secure(request_rec *r, oidc_cfg_t *c); char *oidc_util_openssl_version(apr_pool_t *pool); apr_byte_t oidc_util_request_matches_url(request_rec *r, const char *url);