/* * validate "iat" claim in JWT */ apr_byte_t oidc_proto_validate_iat(request_rec *r, oidc_provider_t *provider, apr_jwt_t *jwt) { if (jwt->payload.iat == APR_JWT_CLAIM_TIME_EMPTY) { oidc_error(r, "id_token JSON payload did not contain an \"iat\" number value"); return FALSE; } /* check if this id_token has been issued just now +- slack (default 10 minutes) */ if ((apr_time_now() - apr_time_from_sec(provider->idtoken_iat_slack)) > jwt->payload.iat) { oidc_error(r, "\"iat\" validation failure (%" APR_TIME_T_FMT "): JWT was issued more than %d seconds ago", jwt->payload.iat, provider->idtoken_iat_slack); return FALSE; } if ((apr_time_now() + apr_time_from_sec(provider->idtoken_iat_slack)) < jwt->payload.iat) { oidc_error(r, "\"iat\" validation failure (%" APR_TIME_T_FMT "): JWT was issued more than %d seconds in the future", jwt->payload.iat, provider->idtoken_iat_slack); return FALSE; } return TRUE; }
/* * read a JSON metadata file from disk */ static apr_byte_t oidc_metadata_file_read_json(request_rec *r, const char *path, json_t **result) { char *buf = NULL; /* read the file contents */ if (oidc_util_file_read(r, path, &buf) == FALSE) return FALSE; /* decode the JSON contents of the buffer */ json_error_t json_error; *result = json_loads(buf, 0, &json_error); if (*result == NULL) { /* something went wrong */ oidc_error(r, "JSON parsing (%s) returned an error: %s", path, json_error.text); return FALSE; } if (!json_is_object(*result)) { /* oops, no JSON */ oidc_error(r, "parsed JSON from (%s) did not contain a JSON object", path); json_decref(*result); return FALSE; } return TRUE; }
/* * check if the specified entry in metadata is a valid URI */ static apr_byte_t oidc_metadata_is_valid_uri(request_rec *r, const char *type, const char *issuer, const json_t *json, const char *key, apr_byte_t is_mandatory) { apr_uri_t uri; json_t *entry = NULL; entry = json_object_get(json, key); if (entry == NULL) { if (is_mandatory) { oidc_error(r, "%s (%s) JSON metadata does not contain the mandatory \"%s\" entry", type, issuer, key); } return (!is_mandatory); } if (!json_is_string(entry)) { oidc_error(r, "%s (%s) JSON metadata contains a \"%s\" entry, but it is not a string value", type, issuer, key); return FALSE; } if (apr_uri_parse(r->pool, json_string_value(entry), &uri) != APR_SUCCESS) { oidc_error(r, "%s (%s) JSON metadata contains a \"%s\" entry, but it is not a valid URI", type, issuer, key); return FALSE; } return TRUE; }
/* * write JSON metadata to a file */ static apr_byte_t oidc_metadata_file_write(request_rec *r, const char *path, const char *data) { // TODO: completely erase the contents of the file if it already exists.... apr_file_t *fd = NULL; apr_status_t rc = APR_SUCCESS; apr_size_t bytes_written = 0; char s_err[128]; /* try to open the metadata file for writing, creating it if it does not exist */ if ((rc = apr_file_open(&fd, path, (APR_FOPEN_WRITE | APR_FOPEN_CREATE), APR_OS_DEFAULT, r->pool)) != APR_SUCCESS) { oidc_error(r, "file \"%s\" could not be opened (%s)", path, apr_strerror(rc, s_err, sizeof(s_err))); return FALSE; } /* lock the file and move the write pointer to the start of it */ apr_file_lock(fd, APR_FLOCK_EXCLUSIVE); apr_off_t begin = 0; apr_file_seek(fd, APR_SET, &begin); /* calculate the length of the data, which is a string length */ apr_size_t len = strlen(data); /* (blocking) write the number of bytes in the buffer */ rc = apr_file_write_full(fd, data, len, &bytes_written); /* check for a system error */ if (rc != APR_SUCCESS) { oidc_error(r, "could not write to: \"%s\" (%s)", path, apr_strerror(rc, s_err, sizeof(s_err))); return FALSE; } /* check that all bytes from the header were written */ if (bytes_written != len) { oidc_error(r, "could not write enough bytes to: \"%s\", bytes_written (%" APR_SIZE_T_FMT ") != len (%" APR_SIZE_T_FMT ")", path, bytes_written, len); return FALSE; } /* unlock and close the written file */ apr_file_unlock(fd); apr_file_close(fd); oidc_debug(r, "file \"%s\" written; number of bytes (%" APR_SIZE_T_FMT ")", path, len); return TRUE; }
/* * check whether the provided JWT is a valid id_token for the specified "provider" */ static apr_byte_t oidc_proto_validate_idtoken(request_rec *r, oidc_provider_t *provider, apr_jwt_t *jwt, const char *nonce) { oidc_cfg *cfg = ap_get_module_config(r->server->module_config, &auth_openidc_module); oidc_debug(r, "enter, jwt.header=\"%s\", jwt.payload=\%s\", nonce=%s", jwt->header.value.str, jwt->payload.value.str, nonce); /* if a nonce is not passed, we're doing a ("code") flow where the nonce is optional */ if (nonce != NULL) { /* if present, verify the nonce */ if (oidc_proto_validate_nonce(r, cfg, provider, nonce, jwt) == FALSE) return FALSE; } /* issuer is mandatory in id_token */ if (jwt->payload.iss == NULL) { oidc_error(r, "response JSON object did not contain an \"iss\" string"); return FALSE; } /* check if the issuer matches the requested value */ if (oidc_util_issuer_match(provider->issuer, jwt->payload.iss) == FALSE) { oidc_error(r, "configured issuer (%s) does not match received \"iss\" value in id_token (%s)", provider->issuer, jwt->payload.iss); return FALSE; } /* check exp */ if (oidc_proto_validate_exp(r, jwt) == FALSE) return FALSE; /* check iat */ if (oidc_proto_validate_iat(r, provider, jwt) == FALSE) return FALSE; /* check if the required-by-spec "sub" claim is present */ if (jwt->payload.sub == NULL) { oidc_error(r, "id_token JSON payload did not contain the required-by-spec \"sub\" string value"); return FALSE; } /* verify the "aud" and "azp" values */ if (oidc_proto_validate_aud_and_azp(r, cfg, provider, &jwt->payload) == FALSE) return FALSE; return TRUE; }
/* * generate a random value (nonce) to correlate request/response through browser state */ apr_byte_t oidc_proto_generate_nonce(request_rec *r, char **nonce) { unsigned char *nonce_bytes = apr_pcalloc(r->pool, OIDC_PROTO_NONCE_LENGTH); if (apr_generate_random_bytes(nonce_bytes, OIDC_PROTO_NONCE_LENGTH) != APR_SUCCESS) { oidc_error(r, "apr_generate_random_bytes returned an error"); return FALSE; } if (oidc_base64url_encode(r, nonce, (const char *) nonce_bytes, OIDC_PROTO_NONCE_LENGTH, TRUE) <= 0) { oidc_error(r, "oidc_base64url_encode returned an error"); return FALSE; } return TRUE; }
/* * validate a JWT access token (locally) * * TODO: document that we're reusing the following settings from the OIDC config section: * - JWKs URI refresh interval * - encryption key material (OIDCPrivateKeyFiles) * - iat slack (OIDCIDTokenIatSlack) * * OIDCOAuthRemoteUserClaim client_id * # 32x 61 hex * OIDCOAuthVerifySharedKeys aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa */ static apr_byte_t oidc_oauth_validate_jwt_access_token(request_rec *r, oidc_cfg *c, const char *access_token, json_t **token, char **response) { apr_jwt_error_t err; apr_jwt_t *jwt = NULL; if (apr_jwt_parse(r->pool, access_token, &jwt, oidc_util_merge_symmetric_key(r->pool, c->private_keys, c->oauth.client_secret, NULL), &err) == FALSE) { oidc_error(r, "could not parse JWT from access_token: %s", apr_jwt_e2s(r->pool, err)); return FALSE; } oidc_debug(r, "successfully parsed JWT with header: %s", jwt->header.value.str); /* validate the access token JWT, validating optional exp + iat */ if (oidc_proto_validate_jwt(r, jwt, NULL, FALSE, FALSE, c->provider.idtoken_iat_slack) == FALSE) { apr_jwt_destroy(jwt); return FALSE; } oidc_debug(r, "verify JWT against %d statically configured public keys and %d shared keys, with JWKs URI set to %s", c->oauth.verify_public_keys ? apr_hash_count(c->oauth.verify_public_keys) : 0, c->oauth.verify_shared_keys ? apr_hash_count(c->oauth.verify_shared_keys) : 0, c->oauth.verify_jwks_uri); oidc_jwks_uri_t jwks_uri = { c->oauth.verify_jwks_uri, c->provider.jwks_refresh_interval, c->oauth.ssl_validate_server }; if (oidc_proto_jwt_verify(r, c, jwt, &jwks_uri, oidc_util_merge_key_sets(r->pool, c->oauth.verify_public_keys, c->oauth.verify_shared_keys)) == FALSE) { oidc_error(r, "JWT access token signature could not be validated, aborting"); apr_jwt_destroy(jwt); return FALSE; } oidc_debug(r, "successfully verified JWT access token: %s", jwt->payload.value.str); *token = jwt->payload.value.json; *response = jwt->payload.value.str; return TRUE; }
/* * if a nonce was passed in the authorization request (and stored in the browser state), * check that it matches the nonce value in the id_token payload */ static apr_byte_t oidc_proto_validate_nonce(request_rec *r, oidc_cfg *cfg, oidc_provider_t *provider, const char *nonce, apr_jwt_t *jwt) { /* see if we have this nonce cached already */ const char *replay = NULL; cfg->cache->get(r, OIDC_CACHE_SECTION_NONCE, nonce, &replay); if (replay != NULL) { oidc_error(r, "the nonce value (%s) passed in the browser state was found in the cache already; possible replay attack!?", nonce); return FALSE; } /* get the "nonce" value in the id_token payload */ char *j_nonce = NULL; apr_jwt_get_string(r->pool, &jwt->payload.value, "nonce", &j_nonce); if (j_nonce == NULL) { oidc_error(r, "id_token JSON payload did not contain a \"nonce\" string"); return FALSE; } /* see if the nonce in the id_token matches the one that we sent in the authorization request */ if (apr_strnatcmp(nonce, j_nonce) != 0) { oidc_error(r, "the nonce value (%s) in the id_token did not match the one stored in the browser session (%s)", j_nonce, nonce); return FALSE; } /* * nonce cache duration (replay prevention window) is the 2x the configured * slack on the timestamp (+-) for token issuance plus 10 seconds for safety */ apr_time_t nonce_cache_duration = apr_time_from_sec( provider->idtoken_iat_slack * 2 + 10); /* store it in the cache for the calculated duration */ cfg->cache->set(r, OIDC_CACHE_SECTION_NONCE, nonce, nonce, apr_time_now() + nonce_cache_duration); oidc_debug(r, "nonce \"%s\" validated successfully and is now cached for %" APR_TIME_T_FMT " seconds", nonce, apr_time_sec(nonce_cache_duration)); return TRUE; }
/* * 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; }
/* * 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; }
/* * 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 the authorization header */ const char *auth_line; auth_line = apr_table_get(r->headers_in, "Authorization"); if (!auth_line) { oidc_debug(r, "no authorization header found"); return FALSE; } /* look for the Bearer keyword */ if (apr_strnatcasecmp(ap_getword(r->pool, &auth_line, ' '), "Bearer")) { oidc_error(r, "client used unsupported authentication scheme: %s", r->uri); return FALSE; } /* 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); /* log some stuff */ oidc_debug(r, "bearer token: %s", *access_token); return TRUE; }
/* * connect to Redis server */ static redisContext * oidc_cache_redis_connect(request_rec *r, oidc_cache_cfg_redis_t *context) { /* see if we already have a connection by looking it up in the process context */ redisContext *ctx = NULL; apr_pool_userdata_get((void **) &ctx, OIDC_CACHE_REDIS_CONTEXT, r->server->process->pool); if (ctx == NULL) { /* no connection, connect to the configured Redis server */ ctx = redisConnect(context->host_str, context->port); /* check for errors */ if ((ctx == NULL) || (ctx->err != 0)) { oidc_error(r, "failed to connect to Redis server (%s:%d): '%s'", context->host_str, context->port, ctx->errstr); return NULL; } /* store the connection in the process context */ apr_pool_userdata_set(ctx, OIDC_CACHE_REDIS_CONTEXT, (apr_status_t (*)(void *)) redisFree, r->server->process->pool); /* log the connection */ oidc_debug(r, "successfully connected to Redis server (%s:%d)", context->host_str, context->port); } return ctx; }
/* * use OpenID Connect Discovery to get metadata for the specified issuer */ apr_byte_t oidc_metadata_provider_retrieve(request_rec *r, oidc_cfg *cfg, const char *issuer, const char *url, json_t **j_metadata, const char **response) { /* get a handle to the directory config */ oidc_dir_cfg *dir_cfg = ap_get_module_config(r->per_dir_config, &auth_openidc_module); /* get provider metadata from the specified URL with the specified parameters */ if (oidc_util_http_get(r, url, NULL, NULL, NULL, cfg->provider.ssl_validate_server, response, cfg->http_timeout_short, cfg->outgoing_proxy, dir_cfg->pass_cookies) == FALSE) return FALSE; /* decode and see if it is not an error response somehow */ if (oidc_util_decode_json_and_check_error(r, *response, j_metadata) == FALSE) { oidc_error(r, "JSON parsing of retrieved Discovery document failed"); return FALSE; } /* check to see if it is valid metadata */ if (oidc_metadata_provider_is_valid(r, *j_metadata, issuer) == FALSE) return FALSE; /* all OK */ return TRUE; }
/* * return JWKs for the specified issuer */ apr_byte_t oidc_metadata_jwks_get(request_rec *r, oidc_cfg *cfg, const oidc_jwks_uri_t *jwks_uri, json_t **j_jwks, apr_byte_t *refresh) { oidc_debug(r, "enter, jwks_uri=%s, refresh=%d", jwks_uri->url, *refresh); /* see if we need to do a forced refresh */ if (*refresh == TRUE) { oidc_debug(r, "doing a forced refresh of the JWKs from URI \"%s\"", jwks_uri->url); if (oidc_metadata_jwks_retrieve_and_cache(r, cfg, jwks_uri, j_jwks) == TRUE) return TRUE; // else: fallback on any cached JWKs } /* see if the JWKs is cached */ const char *value = NULL; cfg->cache->get(r, OIDC_CACHE_SECTION_JWKS, oidc_metadata_jwks_cache_key(r, jwks_uri->url), &value); if (value == NULL) { /* it is non-existing or expired: do a forced refresh */ *refresh = TRUE; return oidc_metadata_jwks_retrieve_and_cache(r, cfg, jwks_uri, j_jwks); } /* decode and see if it is not an error response somehow */ if (oidc_util_decode_json_and_check_error(r, value, j_jwks) == FALSE) { oidc_error(r, "JSON parsing of cached JWKs data failed"); return FALSE; } return TRUE; }
/* * helper function to get the JWKs for the specified issuer */ static apr_byte_t oidc_metadata_jwks_retrieve_and_cache(request_rec *r, oidc_cfg *cfg, const oidc_jwks_uri_t *jwks_uri, json_t **j_jwks) { const char *response = NULL; /* get a handle to the directory config */ oidc_dir_cfg *dir_cfg = ap_get_module_config(r->per_dir_config, &auth_openidc_module); /* no valid provider metadata, get it at the specified URL with the specified parameters */ if (oidc_util_http_get(r, jwks_uri->url, NULL, NULL, NULL, jwks_uri->ssl_validate_server, &response, cfg->http_timeout_long, cfg->outgoing_proxy, dir_cfg->pass_cookies) == FALSE) return FALSE; /* decode and see if it is not an error response somehow */ if (oidc_util_decode_json_and_check_error(r, response, j_jwks) == FALSE) { oidc_error(r, "JSON parsing of JWKs published at the jwks_uri failed"); return FALSE; } /* check to see if it is valid metadata */ if (oidc_metadata_jwks_is_valid(r, jwks_uri, *j_jwks) == FALSE) return FALSE; /* store the JWKs in the cache */ cfg->cache->set(r, OIDC_CACHE_SECTION_JWKS, oidc_metadata_jwks_cache_key(r, jwks_uri->url), response, apr_time_now() + apr_time_from_sec(jwks_uri->refresh_interval)); return TRUE; }
/* * save a session to the cache */ static apr_status_t oidc_session_save_22(request_rec *r, session_rec *z) { oidc_cfg *c = ap_get_module_config(r->server->module_config, &auth_openidc_module); /* encode the actual state in to the encoded string */ oidc_session_identity_encode(r, z); /* store this session in the request context, so it is available to sub-requests as a quicker-than-file-backend cache */ oidc_request_state_set(r, "session", (const char *) z); apr_status_t rc = APR_SUCCESS; if (c->session_type == OIDC_SESSION_TYPE_22_SERVER_CACHE) { /* store the session in the cache */ rc = oidc_session_save_cache(r, z); } else if (c->session_type == OIDC_SESSION_TYPE_22_CLIENT_COOKIE) { /* store the session in a self-contained cookie */ rc = oidc_session_save_cookie(r, z); } else { oidc_error(r, "unknown session type: %d", c->session_type); rc = APR_EGENERAL; } return rc; }
/* * check a provided hash value (at_hash|c_hash) against a corresponding hash calculated for a specified value and algorithm */ static apr_byte_t oidc_proto_validate_hash(request_rec *r, const char *alg, const char *hash, const char *value, const char *type) { /* hash the provided access_token */ char *calc = NULL; unsigned int hash_len = 0; apr_jws_hash_string(r->pool, alg, value, &calc, &hash_len); /* calculate the base64url-encoded value of the hash */ char *encoded = NULL; oidc_base64url_encode(r, &encoded, calc, apr_jws_hash_length(alg) / 2, 1); /* compare the calculated hash against the provided hash */ if ((apr_strnatcmp(encoded, hash) != 0)) { oidc_error(r, "provided \"%s\" hash value (%s) does not match the calculated value (%s)", type, hash, encoded); return FALSE; } oidc_debug(r, "successfully validated the provided \"%s\" hash value (%s) against the calculated value (%s)", type, hash, encoded); return TRUE; }
/* * send an OpenID Connect authorization request to the specified provider preserving POST parameters using HTML5 storage */ int oidc_proto_authorization_request_post_preserve(request_rec *r, const char *authorization_request) { /* read the parameters that are POST-ed to us */ apr_table_t *params = apr_table_make(r->pool, 8); if (oidc_util_read_post(r, params) == FALSE) { oidc_error(r, "something went wrong when reading the POST parameters"); return HTTP_INTERNAL_SERVER_ERROR; } const apr_array_header_t *arr = apr_table_elts(params); const apr_table_entry_t *elts = (const apr_table_entry_t*) arr->elts; int i; char *json = ""; for (i = 0; i < arr->nelts; i++) { json = apr_psprintf(r->pool, "%s'%s': '%s'%s", json, oidc_util_html_escape(r->pool, elts[i].key), oidc_util_html_escape(r->pool, elts[i].val), i < arr->nelts - 1 ? "," : ""); } json = apr_psprintf(r->pool, "{ %s }", json); char *java_script = apr_psprintf(r->pool, " <script type=\"text/javascript\">\n" " function preserveOnLoad() {\n" " localStorage.setItem('mod_auth_openidc_preserve_post_params', JSON.stringify(%s));\n" " window.location='%s';\n" " }\n" " </script>\n", json, authorization_request); return oidc_util_html_send(r, "Preserving...", java_script, "preserveOnLoad", "<p>Preserving...</p>", DONE); }
/* * set the unique user identifier that will be propagated in the Apache r->user and REMOTE_USER variables */ static apr_byte_t oidc_proto_set_remote_user(request_rec *r, oidc_cfg *c, oidc_provider_t *provider, apr_jwt_t *jwt, char **user) { char *issuer = provider->issuer; char *claim_name = apr_pstrdup(r->pool, c->remote_user_claim); int n = strlen(claim_name); int post_fix_with_issuer = (claim_name[n - 1] == '@'); if (post_fix_with_issuer) { claim_name[n - 1] = '\0'; issuer = (strstr(issuer, "https://") == NULL) ? apr_pstrdup(r->pool, issuer) : apr_pstrdup(r->pool, issuer + strlen("https://")); } /* extract the username claim (default: "sub") from the id_token payload */ char *username = NULL; apr_jwt_get_string(r->pool, &jwt->payload.value, claim_name, &username); if (username == NULL) { oidc_error(r, "OIDCRemoteUserClaim is set to \"%s\", but the id_token JSON payload did not contain a \"%s\" string", c->remote_user_claim, claim_name); return FALSE; } /* set the unique username in the session (will propagate to r->user/REMOTE_USER) */ *user = post_fix_with_issuer ? apr_psprintf(r->pool, "%s@%s", username, issuer) : apr_pstrdup(r->pool, username); oidc_debug(r, "set remote_user to \"%s\"", *user); return TRUE; }
/* * 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; }
/* * printout readable error messages about memcache failures */ static void oidc_cache_memcache_log_status_error(request_rec *r, const char *s, apr_status_t rv) { char s_err[OIDC_CACHE_MEMCACHE_STATUS_ERR_SIZE]; apr_strerror(rv, s_err, OIDC_CACHE_MEMCACHE_STATUS_ERR_SIZE); oidc_error(r, "%s returned an error: [%s]; check your that your memcache server is available/accessible.", s, s_err); }
/* * 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; }
/* * store a name/value pair in memcache */ static apr_byte_t oidc_cache_memcache_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\"", section, key); oidc_cfg *cfg = ap_get_module_config(r->server->module_config, &auth_openidc_module); oidc_cache_cfg_memcache_t *context = (oidc_cache_cfg_memcache_t *) cfg->cache_cfg; apr_status_t rv = APR_SUCCESS; /* see if we should be clearing this entry */ if (value == NULL) { rv = apr_memcache_delete(context->cache_memcache, oidc_cache_memcache_get_key(r->pool, section, key), 0); if (rv == APR_NOTFOUND) { oidc_debug(r, "apr_memcache_delete: key %s not found in cache", oidc_cache_memcache_get_key(r->pool, section, key)); } else if (rv != APR_SUCCESS) { // TODO: error strings ? oidc_error(r, "apr_memcache_delete returned an error; perhaps your memcache server is not available?"); } } else { /* calculate the timeout from now */ apr_uint32_t timeout = apr_time_sec(expiry - apr_time_now()); /* store it */ rv = apr_memcache_set(context->cache_memcache, oidc_cache_memcache_get_key(r->pool, section, key), (char *) value, strlen(value), timeout, 0); // TODO: error strings ? if (rv != APR_SUCCESS) { oidc_error(r, "apr_memcache_set returned an error; perhaps your memcache server is not available?"); } } return (rv == APR_SUCCESS); }
/* * validate "exp" claim in JWT */ apr_byte_t oidc_proto_validate_exp(request_rec *r, apr_jwt_t *jwt) { if (apr_time_now() > jwt->payload.exp) { oidc_error(r, "\"exp\" validation failure (%" APR_TIME_T_FMT "): JWT expired", jwt->payload.exp); return FALSE; } 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; }
/* * checks if a parsed JWKs file is a valid one, cq. contains "keys" */ static apr_byte_t oidc_metadata_jwks_is_valid(request_rec *r, const oidc_jwks_uri_t *jwks_uri, json_t *j_jwks) { json_t *keys = json_object_get(j_jwks, "keys"); if ((keys == NULL) || (!json_is_array(keys))) { oidc_error(r, "JWKs JSON metadata obtained from URL \"%s\" did not contain a \"keys\" array", jwks_uri->url); return FALSE; } return TRUE; }
/* * checks if a parsed JWKs file is a valid one, cq. contains "keys" */ static apr_byte_t oidc_metadata_jwks_is_valid(request_rec *r, json_t *j_jwks, const char *issuer) { json_t *keys = json_object_get(j_jwks, "keys"); if ((keys == NULL) || (!json_is_array(keys))) { oidc_error(r, "provider (%s) JWKS JSON metadata did not contain a \"keys\" array", issuer); return FALSE; } return TRUE; }
/* * 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 */ if ((token_type != NULL) && (apr_strnatcasecmp(token_type, "Bearer") != 0) && (provider->userinfo_endpoint_url != NULL)) { oidc_error(r, "token_type is \"%s\" and UserInfo endpoint (%s) for issuer \"%s\" is set: can only deal with Bearer authentication against a UserInfo endpoint!", token_type, provider->userinfo_endpoint_url, provider->issuer); return FALSE; } return TRUE; }
/* * check is a specified JOSE feature is supported */ static apr_byte_t oidc_metadata_conf_jose_is_supported(request_rec *r, json_t *j_conf, const char *issuer, const char *key, apr_jose_is_supported_function_t jose_is_supported_function) { json_t *value = json_object_get(j_conf, key); if (value != NULL) { if (!json_is_string(value)) { oidc_error(r, "(%s) JSON conf data has \"%s\" entry but it is not a string", issuer, key); return FALSE; } if (jose_is_supported_function(r->pool, json_string_value(value)) == FALSE) { oidc_error(r, "(%s) JSON conf data has \"%s\" entry but it contains an unsupported algorithm or encryption type: \"%s\"", issuer, key, json_string_value(value)); return FALSE; } } return TRUE; }
static apr_byte_t oidc_authz_match_expression(request_rec *r, const char *spec_c, json_t *val) { const char *errorptr; int erroffset; pcre *preg; int i = 0; /* setup the regex; spec_c points to the NULL-terminated value pattern */ preg = pcre_compile(spec_c, 0, &errorptr, &erroffset, NULL); if (preg == NULL) { oidc_error(r, "pattern [%s] is not a valid regular expression", spec_c); pcre_free(preg); return FALSE; } /* see if the claim is a literal string */ if (json_is_string(val)) { /* PCRE-compare the string value against the expression */ if (pcre_exec(preg, NULL, json_string_value(val), (int) strlen(json_string_value(val)), 0, 0, NULL, 0) == 0) { pcre_free(preg); return TRUE; } /* see if the claim value is an array */ } else if (json_is_array(val)) { /* compare the claim values in the array against the expression */ for (i = 0; i < json_array_size(val); i++) { json_t *elem = json_array_get(val, i); if (json_is_string(elem)) { /* PCRE-compare the string value against the expression */ if (pcre_exec(preg, NULL, json_string_value(elem), (int) strlen(json_string_value(elem)), 0, 0, NULL, 0) == 0) { pcre_free(preg); return TRUE; } } } } pcre_free(preg); return FALSE; }