/* * check to see if dynamically registered JSON client metadata is valid and has not expired */ static apr_byte_t oidc_metadata_client_is_valid(request_rec *r, json_t *j_client, const char *issuer) { /* get a handle to the client_id we need to use for this provider */ json_t *j_client_id = json_object_get(j_client, "client_id"); if ((j_client_id == NULL) || (!json_is_string(j_client_id))) { oidc_error(r, "client (%s) JSON metadata did not contain a \"client_id\" string", issuer); return FALSE; } /* get a handle to the client_secret we need to use for this provider */ json_t *j_client_secret = json_object_get(j_client, "client_secret"); if ((j_client_secret == NULL) || (!json_is_string(j_client_secret))) { oidc_warn(r, "client (%s) JSON metadata did not contain a \"client_secret\" string", issuer); //return FALSE; } /* the expiry timestamp from the JSON object */ json_t *expires_at = json_object_get(j_client, "client_secret_expires_at"); if ((expires_at == NULL) || (!json_is_integer(expires_at))) { oidc_debug(r, "client (%s) metadata did not contain a \"client_secret_expires_at\" setting", issuer); /* assume that it never expires */ return TRUE; } /* see if it is unrestricted */ if (json_integer_value(expires_at) == 0) { oidc_debug(r, "client (%s) metadata never expires (client_secret_expires_at=0)", issuer); return TRUE; } /* check if the value >= now */ if (apr_time_sec(apr_time_now()) > json_integer_value(expires_at)) { oidc_warn(r, "client (%s) secret expired", issuer); return FALSE; } oidc_debug(r, "client (%s) metadata is valid", issuer); return TRUE; }
/* * set the unique user identifier that will be propagated in the Apache r->user and REMOTE_USER variables */ static apr_byte_t oidc_oauth_set_remote_user(request_rec *r, oidc_cfg *c, json_t *token) { /* get the configured claim name to populate REMOTE_USER with (defaults to "Username") */ char *claim_name = apr_pstrdup(r->pool, c->oauth.remote_user_claim.claim_name); /* get the claim value from the resolved token JSON response to use as the REMOTE_USER key */ json_t *username = json_object_get(token, claim_name); if ((username == NULL) || (!json_is_string(username))) { oidc_warn(r, "response JSON object did not contain a \"%s\" string", claim_name); return FALSE; } r->user = apr_pstrdup(r->pool, json_string_value(username)); if (c->oauth.remote_user_claim.reg_exp != NULL) { char *error_str = NULL; if (oidc_util_regexp_first_match(r->pool, r->user, c->oauth.remote_user_claim.reg_exp, &r->user, &error_str) == FALSE) { oidc_error(r, "oidc_util_regexp_first_match failed: %s", error_str); r->user = NULL; return FALSE; } } oidc_debug(r, "set REMOTE_USER to claim %s=%s", claim_name, json_string_value(username)); return TRUE; }
/* * Apache >=2.4 authorization routine: match the claims from the authenticated user against the Require primitive */ authz_status oidc_authz_worker24(request_rec *r, const json_t * const claims, const char *require_args) { int count_oauth_claims = 0; const char *t, *w; /* needed for anonymous authentication */ if (r->user == NULL) return AUTHZ_DENIED_NO_USER; /* if no claims, impossible to satisfy */ if (!claims) return AUTHZ_DENIED; /* loop over the Required specifications */ t = require_args; while ((w = ap_getword_conf(r->pool, &t)) && w[0]) { count_oauth_claims++; oidc_debug(r, "evaluating claim specification: %s", w); /* see if we can match any of out input claims against this Require'd value */ if (oidc_authz_match_claim(r, w, claims) == TRUE) { oidc_debug(r, "require claim '%s' matched", w); return AUTHZ_GRANTED; } } /* if there wasn't anything after the Require claims directive... */ if (count_oauth_claims == 0) { oidc_warn(r, "'require claim' missing specification(s) in configuration, denying"); } return AUTHZ_DENIED; }
/* * load the session from the request context, create a new one if no luck */ static apr_status_t oidc_session_load_22(request_rec *r, session_rec **zz) { oidc_cfg *c = ap_get_module_config(r->server->module_config, &auth_openidc_module); /* first see if this is a sub-request and it was set already in the main request */ if (((*zz) = (session_rec *) oidc_request_state_get(r, "session")) != NULL) { oidc_debug(r, "loading session from request state"); return APR_SUCCESS; } /* allocate space for the session object and fill it */ session_rec *z = (*zz = apr_pcalloc(r->pool, sizeof(session_rec))); z->pool = r->pool; /* get a new uuid for this session */ z->uuid = (apr_uuid_t *) apr_pcalloc(z->pool, sizeof(apr_uuid_t)); apr_uuid_get(z->uuid); z->remote_user = NULL; z->encoded = NULL; z->entries = apr_table_make(z->pool, 10); apr_status_t rc = APR_SUCCESS; if (c->session_type == OIDC_SESSION_TYPE_22_SERVER_CACHE) { /* load the session from the cache */ rc = oidc_session_load_cache(r, z); } else if (c->session_type == OIDC_SESSION_TYPE_22_CLIENT_COOKIE) { /* load the session from a self-contained cookie */ rc = oidc_session_load_cookie(r, z); } else { oidc_error(r, "oidc_session_load_22: unknown session type: %d", c->session_type); rc = APR_EGENERAL; } /* see if it worked out */ if (rc != APR_SUCCESS) return rc; /* yup, now decode the info */ if (oidc_session_identity_decode(r, z) != APR_SUCCESS) return APR_EGENERAL; /* check whether it has expired */ if (apr_time_now() > z->expiry) { oidc_warn(r, "session restored from cache has expired"); apr_table_clear(z->entries); z->expiry = 0; z->encoded = NULL; return APR_EGENERAL; } /* store this session in the request context, so it is available to sub-requests */ oidc_request_state_set(r, "session", (const char *) z); return APR_SUCCESS; }
/* * parse (custom/configurable) token expiry claim in introspection result */ static apr_byte_t oidc_oauth_parse_and_cache_token_expiry(request_rec *r, oidc_cfg *c, json_t *introspection_response, const char *expiry_claim_name, int expiry_format_absolute, int expiry_claim_is_mandatory, apr_time_t *cache_until) { oidc_debug(r, "expiry_claim_name=%s, expiry_format_absolute=%d, expiry_claim_is_mandatory=%d", expiry_claim_name, expiry_format_absolute, expiry_claim_is_mandatory); json_t *expiry = json_object_get(introspection_response, expiry_claim_name); if (expiry == NULL) { if (expiry_claim_is_mandatory) { oidc_error(r, "introspection response JSON object did not contain an \"%s\" claim", expiry_claim_name); return FALSE; } return TRUE; } if (!json_is_integer(expiry)) { if (expiry_claim_is_mandatory) { oidc_error(r, "introspection response JSON object contains a \"%s\" claim but it is not a JSON integer", expiry_claim_name); return FALSE; } oidc_warn(r, "introspection response JSON object contains a \"%s\" claim that is not an (optional) JSON integer: the introspection result will NOT be cached", expiry_claim_name); return TRUE; } json_int_t value = json_integer_value(expiry); if (value <= 0) { oidc_warn(r, "introspection response JSON object integer number value <= 0 (%ld); introspection result will not be cached", (long)value); return TRUE; } *cache_until = apr_time_from_sec(value); if (expiry_format_absolute == FALSE) (*cache_until) += apr_time_now(); return TRUE; }
/* * load a session from the cache/cookie */ apr_byte_t oidc_session_load(request_rec *r, oidc_session_t **zz) { oidc_cfg *c = ap_get_module_config(r->server->module_config, &auth_openidc_module); apr_byte_t rc = FALSE; const char *ses_p_tb_id = NULL, *env_p_tb_id = NULL; /* allocate space for the session object and fill it */ oidc_session_t *z = (*zz = apr_pcalloc(r->pool, sizeof(oidc_session_t))); oidc_session_clear(r, z); if (c->session_type == OIDC_SESSION_TYPE_SERVER_CACHE) /* load the session from the cache */ rc = oidc_session_load_cache(r, z); /* if we get here we configured client-cookie or retrieving from the cache failed */ if ((c->session_type == OIDC_SESSION_TYPE_CLIENT_COOKIE) || ((rc == FALSE) && oidc_cfg_session_cache_fallback_to_cookie(r))) /* load the session from a self-contained cookie */ rc = oidc_session_load_cookie(r, c, z); if ((rc == TRUE) && (z->state != NULL)) { json_t *j_expires = json_object_get(z->state, OIDC_SESSION_EXPIRY_KEY); if (j_expires) z->expiry = apr_time_from_sec(json_integer_value(j_expires)); /* check whether it has expired */ if (apr_time_now() > z->expiry) { oidc_warn(r, "session restored from cache has expired"); oidc_session_clear(r, z); } else { oidc_session_get(r, z, OIDC_SESSION_PROVIDED_TOKEN_BINDING_KEY, &ses_p_tb_id); if (ses_p_tb_id != NULL) { env_p_tb_id = oidc_util_get_provided_token_binding_id(r); if ((env_p_tb_id == NULL) || (apr_strnatcmp(env_p_tb_id, ses_p_tb_id) != 0)) { oidc_error(r, "the Provided Token Binding ID stored in the session doesn't match the one presented by the user agent"); oidc_session_clear(r, z); } } oidc_session_get(r, z, OIDC_SESSION_REMOTE_USER_KEY, &z->remote_user); } } return rc; }
/* * parse the JSON client metadata in to a oidc_provider_t struct */ apr_byte_t oidc_metadata_client_parse(request_rec *r, oidc_cfg *cfg, json_t *j_client, oidc_provider_t *provider) { /* get a handle to the client_id we need to use for this provider */ oidc_json_object_get_string(r->pool, j_client, "client_id", &provider->client_id, NULL); /* get a handle to the client_secret we need to use for this provider */ oidc_json_object_get_string(r->pool, j_client, "client_secret", &provider->client_secret, NULL); /* see if the token endpoint auth method defined in the client metadata overrides the provider one */ char *token_endpoint_auth = NULL; oidc_json_object_get_string(r->pool, j_client, "token_endpoint_auth_method", &token_endpoint_auth, NULL); if (token_endpoint_auth != NULL) { if ((apr_strnatcmp(token_endpoint_auth, "client_secret_post") == 0) || (apr_strnatcmp(token_endpoint_auth, "client_secret_basic") == 0)) { provider->token_endpoint_auth = apr_pstrdup(r->pool, token_endpoint_auth); } else { oidc_warn(r, "unsupported client auth method \"%s\" in client metadata for entry \"token_endpoint_auth_method\"", token_endpoint_auth); } } /* determine the response type if not set by .conf */ if (provider->response_type == NULL) { provider->response_type = cfg->provider.response_type; /* "response_types" is an array in the client metadata as by spec */ json_t *j_response_types = json_object_get(j_client, "response_types"); if ((j_response_types != NULL) && (json_is_array(j_response_types))) { /* if there's an array we'll prefer the configured response_type if supported */ if (oidc_util_json_array_has_value(r, j_response_types, provider->response_type) == FALSE) { /* if the configured response_type is not supported, we'll fallback to the first one that is listed */ json_t *j_response_type = json_array_get(j_response_types, 0); if (json_is_string(j_response_type)) { provider->response_type = apr_pstrdup(r->pool, json_string_value(j_response_type)); } } } } return TRUE; }
static apr_status_t oidc_session_load_cookie(request_rec *r, session_rec *z) { oidc_dir_cfg *d = ap_get_module_config(r->per_dir_config, &auth_openidc_module); char *cookieValue = oidc_util_get_cookie(r, d->cookie); if (cookieValue != NULL) { if (oidc_base64url_decode_decrypt_string(r, (char **) &z->encoded, cookieValue) <= 0) { //oidc_util_set_cookie(r, d->cookie, ""); oidc_warn(r, "cookie value possibly corrupted"); return APR_EGENERAL; } } return APR_SUCCESS; }
/* * get the key from the (possibly cached) set of JWKs on the jwk_uri that corresponds with the key specified in the header */ static apr_jwk_t *oidc_proto_get_key_from_jwk_uri(request_rec *r, oidc_cfg *cfg, oidc_provider_t *provider, apr_jwt_header_t *jwt_hdr, const char *type, apr_byte_t *refresh) { json_t *j_jwks = NULL; apr_jwk_t *jwk = NULL; /* get the set of JSON Web Keys for this provider (possibly by downloading them from the specified provider->jwk_uri) */ oidc_metadata_jwks_get(r, cfg, provider, &j_jwks, refresh); if (j_jwks == NULL) { oidc_error(r, "could not resolve JSON Web Keys"); return NULL; } /* get the key corresponding to the kid from the header, referencing the key that was used to sign this message */ if (oidc_proto_get_key_from_jwks(r, jwt_hdr, j_jwks, type, &jwk) == FALSE) { json_decref(j_jwks); return NULL; } /* see what we've got back */ if ((jwk == NULL) && (refresh == FALSE)) { /* we did not get a key, but we have not refreshed the JWKs from the jwks_uri yet */ oidc_warn(r, "could not find a key in the cached JSON Web Keys, doing a forced refresh"); /* get the set of JSON Web Keys for this provider forcing a fresh download from the specified provider->jwk_uri) */ *refresh = TRUE; oidc_metadata_jwks_get(r, cfg, provider, &j_jwks, refresh); if (j_jwks == NULL) { oidc_error(r, "could not refresh JSON Web Keys"); return NULL; } /* get the key from the refreshed set of JWKs */ if (oidc_proto_get_key_from_jwks(r, jwt_hdr, j_jwks, type, &jwk) == FALSE) { json_decref(j_jwks); return NULL; } } json_decref(j_jwks); return jwk; }
/* * see if we have provider metadata and check its validity * if not, use OpenID Connect Discovery to get it, check it and store it */ static apr_byte_t oidc_metadata_provider_get(request_rec *r, oidc_cfg *cfg, const char *issuer, json_t **j_provider, apr_byte_t allow_discovery) { /* holds the response data/string/JSON from the OP */ const char *response = NULL; /* get the full file path to the provider metadata for this issuer */ const char *provider_path = oidc_metadata_provider_file_path(r, issuer); /* see if we have valid metadata already, if so, return it */ if (oidc_metadata_file_read_json(r, provider_path, j_provider) == TRUE) { /* return the validation result */ return oidc_metadata_provider_is_valid(r, *j_provider, issuer); } if (!allow_discovery) { oidc_warn(r, "no metadata found for the requested issuer (%s), and Discovery is not allowed", issuer); return FALSE; } // TODO: how to do validity/expiry checks on provider metadata /* assemble the URL to the .well-known OpenID metadata */ const char *url = apr_psprintf(r->pool, "%s", ((strstr(issuer, "http://") == issuer) || (strstr(issuer, "https://") == issuer)) ? issuer : apr_psprintf(r->pool, "https://%s", issuer)); url = apr_psprintf(r->pool, "%s%s.well-known/openid-configuration", url, url[strlen(url) - 1] != '/' ? "/" : ""); /* get the metadata for the issuer using OpenID Connect Discovery and validate it */ if (oidc_metadata_provider_retrieve(r, cfg, issuer, url, j_provider, &response) == FALSE) return FALSE; /* since it is valid, write the obtained provider metadata file */ if (oidc_metadata_file_write(r, provider_path, response) == FALSE) return FALSE; return TRUE; }
/* * check a hash value in the id_token against the corresponding hash calculated over a provided value */ static apr_byte_t oidc_proto_validate_hash_value(request_rec *r, oidc_provider_t *provider, apr_jwt_t *jwt, const char *response_type, const char *value, const char *key, apr_array_header_t *required_for_flows) { /* * get the hash value from the id_token */ char *hash = NULL; apr_jwt_get_string(r->pool, &jwt->payload.value, key, &hash); /* * check if the hash was present */ if (hash == NULL) { /* no hash..., now see if the flow required it */ int i; for (i = 0; i < required_for_flows->nelts; i++) { if (oidc_util_spaced_string_equals(r->pool, response_type, ((const char**) required_for_flows->elts)[i])) { oidc_warn(r, "flow is \"%s\", but no %s found in id_token", response_type, key); return FALSE; } } /* no hash but it was not required anyway */ return TRUE; } /* * we have a hash, validate it and return the result */ return oidc_proto_validate_hash(r, jwt->header.alg, hash, value, key); }
static apr_byte_t oidc_authz_match_value(request_rec *r, const char *spec_c, json_t *val, const char *key) { int i = 0; /* see if it is a string and it (case-insensitively) matches the Require'd value */ if (json_is_string(val)) { if (apr_strnatcmp(json_string_value(val), spec_c) == 0) return TRUE; /* see if it is a integer and it equals the Require'd value */ } else if (json_is_integer(val)) { if (json_integer_value(val) == atoi(spec_c)) return TRUE; /* see if it is a boolean and it (case-insensitively) matches the Require'd value */ } else if (json_is_boolean(val)) { if (apr_strnatcmp(json_is_true(val) ? "true" : "false", spec_c) == 0) return TRUE; /* if it is an array, we'll walk it */ } else if (json_is_array(val)) { /* compare the claim values */ for (i = 0; i < json_array_size(val); i++) { json_t *elem = json_array_get(val, i); if (json_is_string(elem)) { /* * approximately compare the claim value (ignoring * whitespace). At this point, spec_c points to the * NULL-terminated value pattern. */ if (apr_strnatcmp(json_string_value(elem), spec_c) == 0) return TRUE; } else if (json_is_boolean(elem)) { if (apr_strnatcmp( json_is_true(elem) ? "true" : "false", spec_c) == 0) return TRUE; } else if (json_is_integer(elem)) { if (json_integer_value(elem) == atoi(spec_c)) return TRUE; } else { oidc_warn(r, "unhandled in-array JSON object type [%d] for key \"%s\"", elem->type, (const char * ) key); } } } else { oidc_warn(r, "unhandled JSON object type [%d] for key \"%s\"", val->type, (const char * ) key); } return FALSE; }
/* * check to see if JSON provider metadata is valid */ static apr_byte_t oidc_metadata_provider_is_valid(request_rec *r, json_t *j_provider, const char *issuer) { /* get the "issuer" from the provider metadata and double-check that it matches what we looked for */ json_t *j_issuer = json_object_get(j_provider, "issuer"); if ((j_issuer == NULL) || (!json_is_string(j_issuer))) { oidc_error(r, "provider (%s) JSON metadata did not contain an \"issuer\" string", issuer); return FALSE; } /* check that the issuer matches */ if (issuer != NULL) { if (oidc_util_issuer_match(issuer, json_string_value(j_issuer)) == FALSE) { oidc_warn(r, "requested issuer (%s) does not match the \"issuer\" value in the provider metadata file: %s", issuer, json_string_value(j_issuer)); //return FALSE; } } /* verify that the provider supports the a flow that we implement */ json_t *j_response_types_supported = json_object_get(j_provider, "response_types_supported"); if ((j_response_types_supported != NULL) && (json_is_array(j_response_types_supported))) { int i = 0; for (i = 0; i < json_array_size(j_response_types_supported); i++) { json_t *elem = json_array_get(j_response_types_supported, i); if (!json_is_string(elem)) { oidc_error(r, "unhandled in-array JSON non-string object type [%d]", elem->type); continue; } if (oidc_proto_flow_is_supported(r->pool, json_string_value(elem))) break; } if (i == json_array_size(j_response_types_supported)) { oidc_warn(r, "could not find a supported response type in provider metadata (%s) for entry \"response_types_supported\"; assuming that \"code\" flow is supported...", issuer); //return FALSE; } } else { oidc_warn(r, "provider (%s) JSON metadata did not contain a \"response_types_supported\" array; assuming that \"code\" flow is supported...", issuer); // TODO: hey, this is required-by-spec stuff right? } /* verify that the provider supports a response_mode that we implement */ json_t *response_modes_supported = json_object_get(j_provider, "response_modes_supported"); if ((response_modes_supported != NULL) && (json_is_array(response_modes_supported))) { int i = 0; for (i = 0; i < json_array_size(response_modes_supported); i++) { json_t *elem = json_array_get(response_modes_supported, i); if (!json_is_string(elem)) { oidc_error(r, "unhandled in-array JSON non-string object type [%d]", elem->type); continue; } if ((apr_strnatcmp(json_string_value(elem), "fragment") == 0) || (apr_strnatcmp(json_string_value(elem), "query") == 0) || (apr_strnatcmp(json_string_value(elem), "form_post") == 0)) break; } if (i == json_array_size(response_modes_supported)) { oidc_warn(r, "could not find a supported response mode in provider metadata (%s) for entry \"response_modes_supported\"", issuer); return FALSE; } } else { oidc_debug(r, "provider (%s) JSON metadata did not contain a \"response_modes_supported\" array; assuming that \"fragment\" and \"query\" are supported", issuer); } /* check the required authorization endpoint */ if (oidc_metadata_is_valid_uri(r, "provider", issuer, j_provider, "authorization_endpoint", TRUE) == FALSE) return FALSE; /* check the optional token endpoint */ if (oidc_metadata_is_valid_uri(r, "provider", issuer, j_provider, "token_endpoint", FALSE) == FALSE) return FALSE; /* check the optional user info endpoint */ if (oidc_metadata_is_valid_uri(r, "provider", issuer, j_provider, "userinfo_endpoint", FALSE) == FALSE) return FALSE; /* check the optional JWKs URI */ if (oidc_metadata_is_valid_uri(r, "provider", issuer, j_provider, "jwks_uri", FALSE) == FALSE) return FALSE; /* find out what type of authentication the token endpoint supports (we only support post or basic) */ json_t *j_token_endpoint_auth_methods_supported = json_object_get( j_provider, "token_endpoint_auth_methods_supported"); if ((j_token_endpoint_auth_methods_supported == NULL) || (!json_is_array(j_token_endpoint_auth_methods_supported))) { oidc_debug(r, "provider (%s) JSON metadata did not contain a \"token_endpoint_auth_methods_supported\" array, assuming \"client_secret_basic\" is supported", issuer); } else { int i; for (i = 0; i < json_array_size(j_token_endpoint_auth_methods_supported); i++) { json_t *elem = json_array_get( j_token_endpoint_auth_methods_supported, i); if (!json_is_string(elem)) { oidc_warn(r, "unhandled in-array JSON object type [%d] in provider (%s) metadata for entry \"token_endpoint_auth_methods_supported\"", elem->type, issuer); continue; } if (strcmp(json_string_value(elem), "client_secret_post") == 0) { break; } if (strcmp(json_string_value(elem), "client_secret_basic") == 0) { break; } } if (i == json_array_size(j_token_endpoint_auth_methods_supported)) { oidc_error(r, "could not find a supported value [client_secret_post|client_secret_basic] in provider (%s) metadata for entry \"token_endpoint_auth_methods_supported\"", issuer); return FALSE; } } return TRUE; }
/* * send a code/refresh request to the token endpoint and return the parsed contents */ static apr_byte_t oidc_proto_token_endpoint_request(request_rec *r, oidc_cfg *cfg, oidc_provider_t *provider, apr_table_t *params, char **id_token, char **access_token, char **token_type, int *expires_in, char **refresh_token) { const char *response = NULL; /* see if we need to do basic auth or auth-through-post-params (both applied through the HTTP POST method though) */ const char *basic_auth = NULL; if ((provider->token_endpoint_auth == NULL) || (apr_strnatcmp(provider->token_endpoint_auth, "client_secret_basic") == 0)) { basic_auth = apr_psprintf(r->pool, "%s:%s", provider->client_id, provider->client_secret); } else { apr_table_addn(params, "client_id", provider->client_id); apr_table_addn(params, "client_secret", provider->client_secret); } /* add any configured extra static parameters to the token endpoint */ oidc_util_table_add_query_encoded_params(r->pool, params, provider->token_endpoint_params); /* send the refresh request to the token endpoint */ if (oidc_util_http_post_form(r, provider->token_endpoint_url, params, basic_auth, NULL, provider->ssl_validate_server, &response, cfg->http_timeout_long, cfg->outgoing_proxy) == FALSE) { oidc_warn(r, "error when calling the token endpoint (%s)", provider->token_endpoint_url); return FALSE; } /* check for errors, the response itself will have been logged already */ json_t *result = NULL; if (oidc_util_decode_json_and_check_error(r, response, &result) == FALSE) return FALSE; /* get the id_token from the parsed response */ oidc_json_object_get_string(r->pool, result, "id_token", id_token, NULL); /* get the access_token from the parsed response */ oidc_json_object_get_string(r->pool, result, "access_token", access_token, NULL); /* get the token type from the parsed response */ oidc_json_object_get_string(r->pool, result, "token_type", token_type, NULL); /* check the new token type */ if (token_type != NULL) { if (oidc_proto_validate_token_type(r, provider, *token_type) == FALSE) { oidc_warn(r, "access token type did not validate, dropping it"); *access_token = NULL; } } /* get the expires_in value */ oidc_json_object_get_int(r->pool, result, "expires_in", expires_in, -1); /* get the refresh_token from the parsed response */ oidc_json_object_get_string(r->pool, result, "refresh_token", refresh_token, NULL); json_decref(result); return TRUE; }
/* * verify the signature on an id_token */ apr_byte_t oidc_proto_idtoken_verify_signature(request_rec *r, oidc_cfg *cfg, oidc_provider_t *provider, apr_jwt_t *jwt, apr_byte_t *refresh) { apr_byte_t result = FALSE; if (apr_jws_signature_is_hmac(r->pool, jwt)) { oidc_debug(r, "verifying HMAC signature on id_token: header=%s, message=%s", jwt->header.value.str, jwt->message); result = apr_jws_verify_hmac(r->pool, jwt, provider->client_secret, strlen(provider->client_secret)); } else if (apr_jws_signature_is_rsa(r->pool, jwt) #if (OPENSSL_VERSION_NUMBER >= 0x01000000) || apr_jws_signature_is_ec(r->pool, jwt) #endif ) { /* get the key from the JWKs that corresponds with the key specified in the header */ apr_jwk_t *jwk = oidc_proto_get_key_from_jwk_uri(r, cfg, provider, &jwt->header, apr_jws_signature_is_rsa(r->pool, jwt) ? "RSA" : "EC", refresh); if (jwk != NULL) { oidc_debug(r, "verifying RSA/EC signature on id_token: header=%s, message=%s", jwt->header.value.str, jwt->message); result = apr_jws_signature_is_rsa(r->pool, jwt) ? apr_jws_verify_rsa(r->pool, jwt, jwk) : #if (OPENSSL_VERSION_NUMBER >= 0x01000000) apr_jws_verify_ec(r->pool, jwt, jwk); #else FALSE; #endif } else { oidc_warn(r, "could not find a key in the JSON Web Keys"); if (*refresh == FALSE) { oidc_debug(r, "force refresh of the JWKS"); /* do it again, forcing a JWKS refresh */ *refresh = TRUE; result = oidc_proto_idtoken_verify_signature(r, cfg, provider, jwt, refresh); } } } else { oidc_warn(r, "cannot verify id_token; unsupported algorithm \"%s\", must be RSA or HMAC", jwt->header.alg); } oidc_debug(r, "verification result of signature with algorithm \"%s\": %s", jwt->header.alg, (result == TRUE) ? "TRUE" : "FALSE"); return result; }
/* * get the key from the JWKs that corresponds with the key specified in the header */ static apr_byte_t oidc_proto_get_key_from_jwks(request_rec *r, apr_jwt_header_t *jwt_hdr, json_t *j_jwks, const char *type, apr_jwk_t **result) { char *x5t = NULL; apr_jwt_get_string(r->pool, &jwt_hdr->value, "x5t", &x5t); oidc_debug(r, "search for kid \"%s\" or thumbprint x5t \"%s\"", jwt_hdr->kid, x5t); /* get the "keys" JSON array from the JWKs object */ json_t *keys = json_object_get(j_jwks, "keys"); if ((keys == NULL) || !(json_is_array(keys))) { oidc_error(r, "\"keys\" array element is not a JSON array"); return FALSE; } int i; for (i = 0; i < json_array_size(keys); i++) { /* get the next element in the array */ json_t *elem = json_array_get(keys, i); /* check that it is a JSON object */ if (!json_is_object(elem)) { oidc_warn(r, "\"keys\" array element is not a JSON object, skipping"); continue; } /* get the key type and see if it is the RSA type that we are looking for */ json_t *kty = json_object_get(elem, "kty"); if ((!json_is_string(kty)) || (strcmp(json_string_value(kty), type) != 0)) continue; /* see if we were looking for a specific kid, if not we'll return the first one found */ if ((jwt_hdr->kid == NULL) && (x5t == NULL)) { oidc_debug(r, "no kid/x5t to match, return first key found"); apr_jwk_parse_json(r->pool, elem, NULL, result); break; } /* we are looking for a specific kid, get the kid from the current element */ json_t *ekid = json_object_get(elem, "kid"); if ((ekid != NULL) && json_is_string(ekid) && (jwt_hdr->kid != NULL)) { /* compare the requested kid against the current element */ if (apr_strnatcmp(jwt_hdr->kid, json_string_value(ekid)) == 0) { oidc_debug(r, "found matching kid: \"%s\"", jwt_hdr->kid); apr_jwk_parse_json(r->pool, elem, NULL, result); break; } } /* we are looking for a specific x5t, get the x5t from the current element */ json_t *ex5t = json_object_get(elem, "kid"); if ((ex5t != NULL) && json_is_string(ex5t) && (x5t != NULL)) { /* compare the requested kid against the current element */ if (apr_strnatcmp(x5t, json_string_value(ex5t)) == 0) { oidc_debug(r, "found matching x5t: \"%s\"", x5t); apr_jwk_parse_json(r->pool, elem, NULL, result); break; } } } return TRUE; }
/* * check the required parameters for the various flows after resolving the authorization code */ apr_byte_t oidc_proto_validate_code_response(request_rec *r, const char *response_type, char **id_token, char **access_token, char **token_type) { oidc_debug(r, "enter"); /* * check id_token parameter */ if (!oidc_util_spaced_string_contains(r->pool, response_type, "id_token")) { if (*id_token == NULL) { oidc_error(r, "requested flow is \"%s\" but no \"id_token\" parameter found in the code response", response_type); return FALSE; } } else { if (*id_token != NULL) { oidc_warn(r, "requested flow is \"%s\" but there is an \"id_token\" parameter in the code response that will be dropped", response_type); *id_token = NULL; } } /* * check access_token parameter */ if (!oidc_util_spaced_string_contains(r->pool, response_type, "token")) { if (*access_token == NULL) { oidc_error(r, "requested flow is \"%s\" but no \"access_token\" parameter found in the code response", response_type); return FALSE; } if (*token_type == NULL) { oidc_error(r, "requested flow is \"%s\" but no \"token_type\" parameter found in the code response", response_type); return FALSE; } } else { if (*access_token != NULL) { oidc_warn(r, "requested flow is \"%s\" but there is an \"access_token\" parameter in the code response that will be dropped", response_type); *access_token = NULL; } if (*token_type != NULL) { oidc_warn(r, "requested flow is \"%s\" but there is a \"token_type\" parameter in the code response that will be dropped", response_type); *token_type = NULL; } } return TRUE; }
/* * check the required parameters for the various flows on receipt of the authorization response */ apr_byte_t oidc_proto_validate_authorization_response(request_rec *r, const char *response_type, const char *requested_response_mode, char **code, char **id_token, char **access_token, char **token_type, const char *used_response_mode) { oidc_debug(r, "enter, response_type=%s, requested_response_mode=%s, code=%s, id_token=%s, access_token=%s, token_type=%s, used_response_mode=%s", response_type, requested_response_mode, *code, *id_token, *access_token, *token_type, used_response_mode); /* check the requested response mode against the one used by the OP */ if ((requested_response_mode != NULL) && (strcmp(requested_response_mode, used_response_mode)) != 0) { /* * only warn because I'm not sure that most OPs will respect a requested * response_mode and rather use the default for the flow */ oidc_warn(r, "requested response_mode is \"%s\" the provider used \"%s\" for the authorization response...", requested_response_mode, used_response_mode); } /* * check code parameter */ if (oidc_util_spaced_string_contains(r->pool, response_type, "code")) { if (*code == NULL) { oidc_error(r, "requested flow is \"%s\" but no \"code\" parameter found in the authorization response", response_type); return FALSE; } } else { if (*code != NULL) { oidc_warn(r, "requested flow is \"%s\" but there is a \"code\" parameter in the authorization response that will be dropped", response_type); *code = NULL; } } /* * check id_token parameter */ if (oidc_util_spaced_string_contains(r->pool, response_type, "id_token")) { if (*id_token == NULL) { oidc_error(r, "requested flow is \"%s\" but no \"id_token\" parameter found in the authorization response", response_type); return FALSE; } } else { if (*id_token != NULL) { oidc_warn(r, "requested flow is \"%s\" but there is an \"id_token\" parameter in the authorization response that will be dropped", response_type); *id_token = NULL; } } /* * check access_token parameter */ if (oidc_util_spaced_string_contains(r->pool, response_type, "token")) { if (*access_token == NULL) { oidc_error(r, "requested flow is \"%s\" but no \"access_token\" parameter found in the authorization response", response_type); return FALSE; } if (*token_type == NULL) { oidc_error(r, "requested flow is \"%s\" but no \"token_type\" parameter found in the authorization response", response_type); return FALSE; } } else { if (*access_token != NULL) { oidc_warn(r, "requested flow is \"%s\" but there is an \"access_token\" parameter in the authorization response that will be dropped", response_type); *access_token = NULL; } if (*token_type != NULL) { oidc_warn(r, "requested flow is \"%s\" but there is a \"token_type\" parameter in the authorization response that will be dropped", response_type); *token_type = NULL; } } return TRUE; }
/* * store a value in the shared memory cache */ static apr_byte_t oidc_cache_shm_set(request_rec *r, const char *section, const char *key, const char *value, apr_time_t expiry) { oidc_debug(r, "enter, section=\"%s\", key=\"%s\", value size=%llu", section, key, value ? (unsigned long long )strlen(value) : 0); oidc_cfg *cfg = ap_get_module_config(r->server->module_config, &auth_openidc_module); oidc_cache_cfg_shm_t *context = (oidc_cache_cfg_shm_t *) cfg->cache_cfg; oidc_cache_shm_entry_t *match, *free, *lru; oidc_cache_shm_entry_t *t; apr_time_t current_time; int i; apr_time_t age; const char *section_key = oidc_cache_shm_get_key(r->pool, section, key); /* check that the passed in key is valid */ if (strlen(section_key) > OIDC_CACHE_SHM_KEY_MAX) { oidc_error(r, "could not store value since key size is too large (%s)", section_key); return FALSE; } /* check that the passed in value is valid */ if ((value != NULL) && (strlen(value) > (cfg->cache_shm_entry_size_max - sizeof(oidc_cache_shm_entry_t)))) { oidc_error(r, "could not store value since value size is too large (%llu > %lu); consider increasing OIDCCacheShmEntrySizeMax", (unsigned long long)strlen(value), (unsigned long)(cfg->cache_shm_entry_size_max - sizeof(oidc_cache_shm_entry_t))); return FALSE; } /* grab the global lock */ if (oidc_cache_mutex_lock(r, context->mutex) == FALSE) return FALSE; /* get a pointer to the shared memory block */ t = apr_shm_baseaddr_get(context->shm); /* get the current time */ current_time = apr_time_now(); /* loop over the block, looking for the key */ match = NULL; free = NULL; lru = t; for (i = 0; i < cfg->cache_shm_size_max; i++, OIDC_CACHE_SHM_ADD_OFFSET(t, cfg->cache_shm_entry_size_max)) { /* see if this slot is free */ if (t->section_key[0] == '\0') { if (free == NULL) free = t; continue; } /* see if a value already exists for this key */ if (apr_strnatcmp(t->section_key, section_key) == 0) { match = t; break; } /* see if this slot has expired */ if (t->expires <= current_time) { if (free == NULL) free = t; continue; } /* see if this slot was less recently used than the current pointer */ if (t->access < lru->access) { lru = t; } } /* if we have no free slots, issue a warning about the LRU entry */ if (match == NULL && free == NULL) { age = (current_time - lru->access) / 1000000; if (age < 3600) { oidc_warn(r, "dropping LRU entry with age = %" APR_TIME_T_FMT "s, which is less than one hour; consider increasing the shared memory caching space (which is %d now) with the (global) OIDCCacheShmMax setting.", age, cfg->cache_shm_size_max); } } /* pick the best slot: choose one with a matching key over a free slot, over a least-recently-used one */ t = match ? match : (free ? free : lru); /* see if we need to clear or set the value */ if (value != NULL) { /* fill out the entry with the provided data */ strcpy(t->section_key, section_key); strcpy(t->value, value); t->expires = expiry; t->access = current_time; } else { t->section_key[0] = '\0'; t->access = 0; } /* release the global lock */ oidc_cache_mutex_unlock(r, context->mutex); return TRUE; }
/* * resolve and validate an access_token against the configured Authorization Server */ static apr_byte_t oidc_oauth_resolve_access_token(request_rec *r, oidc_cfg *c, const char *access_token, json_t **token, char **response) { json_t *result = NULL; const char *json = NULL; /* see if we've got the claims for this access_token cached already */ c->cache->get(r, OIDC_CACHE_SECTION_ACCESS_TOKEN, access_token, &json); if (json == NULL) { /* not cached, go out and validate the access_token against the Authorization server and get the JSON claims back */ if (oidc_oauth_validate_access_token(r, c, access_token, &json) == FALSE) { oidc_error(r, "could not get a validation response from the Authorization server"); return FALSE; } /* decode and see if it is not an error response somehow */ if (oidc_util_decode_json_and_check_error(r, json, &result) == FALSE) return FALSE; json_t *active = json_object_get(result, "active"); if (active != NULL) { if ((!json_is_boolean(active)) || (!json_is_true(active))) { oidc_debug(r, "no \"active\" boolean object with value \"true\" found in response JSON object"); json_decref(result); return FALSE; } json_t *exp = json_object_get(result, "exp"); if ((exp != NULL) && (json_is_number(exp))) { /* set it in the cache so subsequent request don't need to validate the access_token and get the claims anymore */ c->cache->set(r, OIDC_CACHE_SECTION_ACCESS_TOKEN, access_token, json, apr_time_from_sec(json_integer_value(exp))); } else if (json_integer_value(exp) <= 0) { oidc_debug(r, "response JSON object did not contain an \"exp\" integer number; introspection result will not be cached"); } } else { /* assume PingFederate validation: get and check the expiry timestamp */ json_t *expires_in = json_object_get(result, "expires_in"); if ((expires_in == NULL) || (!json_is_number(expires_in))) { oidc_error(r, "response JSON object did not contain an \"expires_in\" number"); json_decref(result); return FALSE; } if (json_integer_value(expires_in) <= 0) { oidc_warn(r, "\"expires_in\" number <= 0 (%" JSON_INTEGER_FORMAT "); token already expired...", json_integer_value(expires_in)); json_decref(result); return FALSE; } /* set it in the cache so subsequent request don't need to validate the access_token and get the claims anymore */ c->cache->set(r, OIDC_CACHE_SECTION_ACCESS_TOKEN, access_token, json, apr_time_now() + apr_time_from_sec(json_integer_value(expires_in))); } } else { /* we got the claims for this access_token in our cache, decode it in to a JSON structure */ json_error_t json_error; result = json_loads(json, 0, &json_error); if (result == NULL) { oidc_error(r, "cached JSON was corrupted: %s", json_error.text); return FALSE; } } /* return the access_token JSON object */ json_t *tkn = json_object_get(result, "access_token"); if ((tkn != NULL) && (json_is_object(tkn))) { /* * assume PingFederate validation: copy over those claims from the access_token * that are relevant for authorization purposes */ json_object_set(tkn, "client_id", json_object_get(result, "client_id")); json_object_set(tkn, "scope", json_object_get(result, "scope")); //oidc_oauth_spaced_string_to_array(r, result, "scope", tkn, "scopes"); /* return only the pimped access_token results */ *token = json_deep_copy(tkn); char *s_token = json_dumps(*token, 0); *response = apr_pstrdup(r->pool, s_token); free(s_token); json_decref(result); } else { //oidc_oauth_spaced_string_to_array(r, result, "scope", result, "scopes"); /* assume spec compliant introspection */ *token = result; *response = apr_pstrdup(r->pool, json); } return TRUE; }
/* * Apache <2.4 authorization routine: match the claims from the authenticated user against the Require primitive */ int oidc_authz_worker(request_rec *r, const json_t * const claims, const require_line * const reqs, int nelts) { const int m = r->method_number; const char *token; const char *requirement; int i; int have_oauthattr = 0; int count_oauth_claims = 0; /* go through applicable Require directives */ for (i = 0; i < nelts; ++i) { /* ignore this Require if it's in a <Limit> section that exclude this method */ if (!(reqs[i].method_mask & (AP_METHOD_BIT << m))) { continue; } /* ignore if it's not a "Require claim ..." */ requirement = reqs[i].requirement; token = ap_getword_white(r->pool, &requirement); if (apr_strnatcasecmp(token, OIDC_REQUIRE_NAME) != 0) { continue; } /* ok, we have a "Require claim" to satisfy */ have_oauthattr = 1; /* * If we have an applicable claim, but no claims were sent in the request, then we can * just stop looking here, because it's not satisfiable. The code after this loop will * give the appropriate response. */ if (!claims) { break; } /* * iterate over the claim specification strings in this require directive searching * for a specification that matches one of the claims. */ while (*requirement) { token = ap_getword_conf(r->pool, &requirement); count_oauth_claims++; oidc_debug(r, "evaluating claim specification: %s", token); if (oidc_authz_match_claim(r, token, claims) == TRUE) { /* if *any* claim matches, then authorization has succeeded and all of the others are ignored */ oidc_debug(r, "require claim '%s' matched", token); return OK; } } } /* if there weren't any "Require claim" directives, we're irrelevant */ if (!have_oauthattr) { oidc_debug(r, "no claim statements found, not performing authz"); return DECLINED; } /* if there was a "Require claim", but no actual claims, that's cause to warn the admin of an iffy configuration */ if (count_oauth_claims == 0) { oidc_warn(r, "'require claim' missing specification(s) in configuration, declining"); return DECLINED; } /* log the event, also in Apache speak */ oidc_debug(r, "authorization denied for client session"); ap_note_auth_failure(r); return HTTP_UNAUTHORIZED; }
/* * get the authorization header that should contain a bearer token */ static apr_byte_t oidc_oauth_get_bearer_token(request_rec *r, const char **access_token) { /* get a handle to the directory config */ oidc_dir_cfg *dir_cfg = ap_get_module_config(r->per_dir_config, &auth_openidc_module); *access_token = NULL; if ((dir_cfg->oauth_accept_token_in & OIDC_OAUTH_ACCEPT_TOKEN_IN_HEADER) || (dir_cfg->oauth_accept_token_in == OIDC_OAUTH_ACCEPT_TOKEN_IN_DEFAULT)) { /* get the authorization header */ const char *auth_line; auth_line = apr_table_get(r->headers_in, "Authorization"); if (auth_line) { oidc_debug(r, "authorization header found"); /* look for the Bearer keyword */ if (apr_strnatcasecmp(ap_getword(r->pool, &auth_line, ' '), "Bearer") == 0) { /* skip any spaces after the Bearer keyword */ while (apr_isspace(*auth_line)) { auth_line++; } /* copy the result in to the access_token */ *access_token = apr_pstrdup(r->pool, auth_line); } else { oidc_warn(r, "client used unsupported authentication scheme: %s", r->uri); } } } if ((*access_token == NULL) && (r->method_number == M_POST) && (dir_cfg->oauth_accept_token_in & OIDC_OAUTH_ACCEPT_TOKEN_IN_POST)) { apr_table_t *params = apr_table_make(r->pool, 8); if (oidc_util_read_post_params(r, params) == TRUE) { *access_token = apr_table_get(params, "access_token"); } } if ((*access_token == NULL) && (dir_cfg->oauth_accept_token_in & OIDC_OAUTH_ACCEPT_TOKEN_IN_QUERY)) { apr_table_t *params = apr_table_make(r->pool, 8); oidc_util_read_form_encoded_params(r, params, r->args); *access_token = apr_table_get(params, "access_token"); } if ((*access_token == NULL) && (dir_cfg->oauth_accept_token_in & OIDC_OAUTH_ACCEPT_TOKEN_IN_COOKIE)) { const char *cookie_name = apr_hash_get( dir_cfg->oauth_accept_token_options, "cookie-name", APR_HASH_KEY_STRING); const char *auth_line = oidc_util_get_cookie(r, cookie_name); if (auth_line != NULL) { /* copy the result in to the access_token */ *access_token = apr_pstrdup(r->pool, auth_line); } else { oidc_warn(r, "no cookie found with name: %s", cookie_name); } } if (*access_token == NULL) { oidc_debug(r, "no bearer token found in the allowed methods (authorization header, post, query parameter or cookie)"); return FALSE; } /* log some stuff */ oidc_debug(r, "bearer token: %s", *access_token); return TRUE; }