/* * 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) return FALSE; 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; }
/* * 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; }
/* * 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; }
/* * 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; }
/* * 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; }
/* * 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; }
/* * 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; }
/* * 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; }
int oidc_proto_javascript_implicit(request_rec *r, oidc_cfg *c) { oidc_debug(r, "enter"); const char *java_script = " <script type=\"text/javascript\">\n" " function postOnLoad() {\n" " encoded = location.hash.substring(1).split('&');\n" " for (i = 0; i < encoded.length; i++) {\n" " encoded[i].replace(/\\+/g, ' ');\n" " var n = encoded[i].indexOf('=');\n" " var input = document.createElement('input');\n" " input.type = 'hidden';\n" " input.name = decodeURIComponent(encoded[i].substring(0, n));\n" " input.value = decodeURIComponent(encoded[i].substring(n+1));\n" " document.forms[0].appendChild(input);\n" " }\n" " document.forms[0].action = window.location.href.substr(0, window.location.href.indexOf('#'));\n" " document.forms[0].submit();\n" " }\n" " </script>\n"; const char *html_body = " <p>Submitting...</p>\n" " <form method=\"post\" action=\"\">\n" " <p>\n" " <input type=\"hidden\" name=\"response_mode\" value=\"fragment\">\n" " </p>\n" " </form>\n"; return oidc_util_html_send(r, "Submitting...", java_script, "postOnLoad", html_body, DONE); }
/* * save a session to cache/cookie */ apr_byte_t oidc_session_save(request_rec *r, oidc_session_t *z, apr_byte_t first_time) { oidc_cfg *c = ap_get_module_config(r->server->module_config, &auth_openidc_module); apr_byte_t rc = FALSE; const char *p_tb_id = oidc_util_get_provided_token_binding_id(r); if (z->state != NULL) { oidc_session_set(r, z, OIDC_SESSION_REMOTE_USER_KEY, z->remote_user); json_object_set_new(z->state, OIDC_SESSION_EXPIRY_KEY, json_integer(apr_time_sec(z->expiry))); if ((first_time) && (p_tb_id != NULL)) { oidc_debug(r, "Provided Token Binding ID environment variable found; adding its value to the session state"); oidc_session_set(r, z, OIDC_SESSION_PROVIDED_TOKEN_BINDING_KEY, p_tb_id); } } if (c->session_type == OIDC_SESSION_TYPE_SERVER_CACHE) /* store the session in the cache */ rc = oidc_session_save_cache(r, z, first_time); /* if we get here we configured client-cookie or saving in the cache failed */ if ((c->session_type == OIDC_SESSION_TYPE_CLIENT_COOKIE) || ((rc == FALSE) && oidc_cfg_session_cache_fallback_to_cookie(r))) /* store the session in a self-contained cookie */ rc = oidc_session_save_cookie(r, z, first_time); return rc; }
void oidc_session_set_filtered_claims(request_rec *r, oidc_session_t *z, const char *session_key, const char *claims) { oidc_cfg *c = ap_get_module_config(r->server->module_config, &auth_openidc_module); const char *name; json_t *src = NULL, *dst = NULL, *value = NULL; void *iter = NULL; apr_byte_t is_allowed; if (oidc_util_decode_json_object(r, claims, &src) == FALSE) return; dst = json_object(); iter = json_object_iter(src); while (iter) { is_allowed = TRUE; name = json_object_iter_key(iter); value = json_object_iter_value(iter); if ((c->black_listed_claims != NULL) && (apr_hash_get(c->black_listed_claims, name, APR_HASH_KEY_STRING) != NULL)) { oidc_debug(r, "removing blacklisted claim [%s]: '%s'", session_key, name); is_allowed = FALSE; } if ((is_allowed == TRUE) && (c->white_listed_claims != NULL) && (apr_hash_get(c->white_listed_claims, name, APR_HASH_KEY_STRING) == NULL)) { oidc_debug(r, "removing non-whitelisted claim [%s]: '%s'", session_key, name); is_allowed = FALSE; } if (is_allowed == TRUE) json_object_set(dst, name, value); iter = json_object_iter_next(src, iter); } char *filtered_claims = oidc_util_encode_json_object(r, dst, JSON_COMPACT); json_decref(dst); json_decref(src); oidc_session_set(r, z, session_key, filtered_claims); }
apr_status_t oidc_session_save(request_rec *r, session_rec *z) { oidc_session_set(r, z, OIDC_SESSION_REMOTE_USER_KEY, z->remote_user); char key[APR_UUID_FORMATTED_LENGTH + 1]; apr_uuid_format((char *) &key, z->uuid); oidc_debug(r, "%s", key); oidc_session_set(r, z, OIDC_SESSION_UUID_KEY, key); return ap_session_save_fn(r, z); }
/* * 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); }
apr_status_t oidc_session_load(request_rec *r, session_rec **zz) { apr_status_t rc = ap_session_load_fn(r, zz); (*zz)->remote_user = apr_table_get((*zz)->entries, OIDC_SESSION_REMOTE_USER_KEY); const char *uuid = apr_table_get((*zz)->entries, OIDC_SESSION_UUID_KEY); oidc_debug(r, "%s", uuid ? uuid : "<null>"); if (uuid != NULL) apr_uuid_parse((*zz)->uuid, uuid); return rc; }
/* * 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; }
/* * get a value from the shared memory cache */ static apr_byte_t oidc_cache_shm_get(request_rec *r, const char *section, const char *key, const char **value) { 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_shm_t *context = (oidc_cache_cfg_shm_t *) cfg->cache_cfg; int i; const char *section_key = oidc_cache_shm_get_key(r->pool, section, key); *value = NULL; /* grab the global lock */ if (oidc_cache_mutex_lock(r, context->mutex) == FALSE) return FALSE; /* get the pointer to the start of the shared memory block */ oidc_cache_shm_entry_t *t = apr_shm_baseaddr_get(context->shm); /* loop over the block, looking for the key */ for (i = 0; i < cfg->cache_shm_size_max; i++, OIDC_CACHE_SHM_ADD_OFFSET(t, cfg->cache_shm_entry_size_max)) { const char *tablekey = t->section_key; if ( (tablekey != NULL) && (apr_strnatcmp(tablekey, section_key) == 0) ) { /* found a match, check if it has expired */ if (t->expires > apr_time_now()) { /* update access timestamp */ t->access = apr_time_now(); *value = t->value; } else { /* clear the expired entry */ t->section_key[0] = '\0'; t->access = 0; } /* we safely can break now since we would not have found an expired match twice */ break; } } /* release the global lock */ oidc_cache_mutex_unlock(r, context->mutex); return (*value == NULL) ? FALSE : TRUE; }
/* * see if a the Require value matches with a set of provided claims */ static apr_byte_t oidc_authz_match_claim(request_rec *r, const char * const attr_spec, const json_t * const claims) { const char *key; json_t *val; /* if we don't have any claims, they can never match any Require claim primitive */ if (claims == NULL) return FALSE; /* loop over all of the user claims */ void *iter = json_object_iter((json_t*) claims); while (iter) { key = json_object_iter_key(iter); val = json_object_iter_value(iter); oidc_debug(r, "evaluating key \"%s\"", (const char * ) key); const char *attr_c = (const char *) key; const char *spec_c = attr_spec; /* walk both strings until we get to the end of either or we find a differing character */ while ((*attr_c) && (*spec_c) && (*attr_c) == (*spec_c)) { attr_c++; spec_c++; } /* The match is a success if we walked the whole claim name and the attr_spec is at a colon. */ if (!(*attr_c) && (*spec_c) == ':') { /* skip the colon */ spec_c++; if (oidc_authz_match_value(r, spec_c, val, key) == TRUE) return TRUE; /* a tilde denotes a string PCRE match */ } else if (!(*attr_c) && (*spec_c) == '~') { /* skip the tilde */ spec_c++; if (oidc_authz_match_expression(r, spec_c, val) == TRUE) return TRUE; } iter = json_object_iter_next((json_t *) claims, iter); } return FALSE; }
/* * store a name/value pair in Redis */ static apr_byte_t oidc_cache_redis_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_redis_t *context = (oidc_cache_cfg_redis_t *) cfg->cache_cfg; redisReply *reply = NULL; /* grab the global lock */ if (oidc_cache_mutex_lock(r, context->mutex) == FALSE) return FALSE; /* see if we should be clearing this entry */ if (value == NULL) { /* delete it */ reply = oidc_cache_redis_command(r, context, "DEL %s", oidc_cache_redis_get_key(r->pool, section, key)); if (reply == NULL) { oidc_cache_mutex_unlock(r, context->mutex); return FALSE; } freeReplyObject(reply); } else { /* calculate the timeout from now */ apr_uint32_t timeout = apr_time_sec(expiry - apr_time_now()); /* store it */ reply = oidc_cache_redis_command(r, context, "SETEX %s %d %s", oidc_cache_redis_get_key(r->pool, section, key), timeout, value); if (reply == NULL) { oidc_cache_mutex_unlock(r, context->mutex); return FALSE; } freeReplyObject(reply); } /* release the global lock */ oidc_cache_mutex_unlock(r, context->mutex); return TRUE; }
/* * get a name/value pair from memcache */ static apr_byte_t oidc_cache_memcache_get(request_rec *r, const char *section, const char *key, const char **value) { 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_size_t len = 0; /* get it */ apr_status_t rv = apr_memcache_getp(context->cache_memcache, r->pool, oidc_cache_memcache_get_key(r->pool, section, key), (char **) value, &len, NULL); if (rv == APR_NOTFOUND) { oidc_debug(r, "apr_memcache_getp: key %s not found in cache", oidc_cache_memcache_get_key(r->pool, section, key)); return FALSE; } else if (rv != APR_SUCCESS) { // TODO: error strings ? oidc_error(r, "apr_memcache_getp returned an error; perhaps your memcache server is not available?"); return FALSE; } /* do sanity checking on the string value */ if ((*value) && (strlen(*value) != len)) { oidc_error(r, "apr_memcache_getp returned less bytes than expected: strlen(value) [%zu] != len [%" APR_SIZE_T_FMT "]", strlen(*value), len); return FALSE; } return TRUE; }
/* * get a name/value pair from memcache */ static apr_byte_t oidc_cache_memcache_get(request_rec *r, const char *section, const char *key, const char **value) { 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_size_t len = 0; /* get it */ apr_status_t rv = apr_memcache_getp(context->cache_memcache, r->pool, oidc_cache_memcache_get_key(r->pool, section, key), (char **) value, &len, NULL); if (rv == APR_NOTFOUND) { /* * NB: workaround the fact that the apr_memcache returns APR_NOTFOUND if a server has been marked dead */ if (oidc_cache_memcache_status(r, context) == FALSE) { oidc_cache_memcache_log_status_error(r, "apr_memcache_getp", rv); return FALSE; } oidc_debug(r, "apr_memcache_getp: key %s not found in cache", oidc_cache_memcache_get_key(r->pool, section, key)); return TRUE; } else if (rv != APR_SUCCESS) { oidc_cache_memcache_log_status_error(r, "apr_memcache_getp", rv); return FALSE; } /* do sanity checking on the string value */ if ((*value) && (strlen(*value) != len)) { oidc_error(r, "apr_memcache_getp returned less bytes than expected: strlen(value) [%zu] != len [%" APR_SIZE_T_FMT "]", strlen(*value), len); return FALSE; } return TRUE; }
/* * get a list of configured OIDC providers based on the entries in the provider metadata directory */ apr_byte_t oidc_metadata_list(request_rec *r, oidc_cfg *cfg, apr_array_header_t **list) { apr_status_t rc; apr_dir_t *dir; apr_finfo_t fi; char s_err[128]; oidc_debug(r, "enter"); /* open the metadata directory */ if ((rc = apr_dir_open(&dir, cfg->metadata_dir, r->pool)) != APR_SUCCESS) { oidc_error(r, "error opening metadata directory '%s' (%s)", cfg->metadata_dir, apr_strerror(rc, s_err, sizeof(s_err))); return FALSE; } /* allocate some space in the array that will hold the list of providers */ *list = apr_array_make(r->pool, 5, sizeof(sizeof(const char*))); /* BTW: we could estimate the number in the array based on # directory entries... */ /* loop over the entries in the provider metadata directory */ while (apr_dir_read(&fi, APR_FINFO_NAME, dir) == APR_SUCCESS) { /* skip "." and ".." entries */ if (fi.name[0] == '.') continue; /* skip other non-provider entries */ char *ext = strrchr(fi.name, '.'); if ((ext == NULL) || (strcmp(++ext, OIDC_METADATA_SUFFIX_PROVIDER) != 0)) continue; /* get the issuer from the filename */ const char *issuer = oidc_metadata_filename_to_issuer(r, fi.name); /* get the provider and client metadata, do all checks and registration if possible */ oidc_provider_t *provider = NULL; if (oidc_metadata_get(r, cfg, issuer, &provider) == TRUE) { /* push the decoded issuer filename in to the array */ *(const char**) apr_array_push(*list) = provider->issuer; } } /* we're done, cleanup now */ apr_dir_close(dir); return TRUE; }
/* * get a name/value pair from Redis */ static apr_byte_t oidc_cache_redis_get(request_rec *r, const char *section, const char *key, const char **value) { 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_redis_t *context = (oidc_cache_cfg_redis_t *) cfg->cache_cfg; redisReply *reply = NULL; /* grab the global lock */ if (oidc_cache_mutex_lock(r, context->mutex) == FALSE) return FALSE; /* get */ reply = oidc_cache_redis_command(r, context, "GET %s", oidc_cache_redis_get_key(r->pool, section, key)); if (reply == NULL) { oidc_cache_mutex_unlock(r, context->mutex); return FALSE; } /* check that we got a string back */ if (reply->type != REDIS_REPLY_STRING) { freeReplyObject(reply); /* this is a normal cache miss, so we'll return OK */ oidc_cache_mutex_unlock(r, context->mutex); return TRUE; } /* do a sanity check on the returned value */ if (reply->len != strlen(reply->str)) { oidc_error(r, "redisCommand reply->len != strlen(reply->str): '%s'", reply->str); freeReplyObject(reply); oidc_cache_mutex_unlock(r, context->mutex); return FALSE; } /* copy it in to the request memory pool */ *value = apr_pstrdup(r->pool, reply->str); freeReplyObject(reply); /* release the global lock */ oidc_cache_mutex_unlock(r, context->mutex); 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; }
/* * get claims from the OP UserInfo endpoint using the provided access_token */ apr_byte_t oidc_proto_resolve_userinfo(request_rec *r, oidc_cfg *cfg, oidc_provider_t *provider, const char *access_token, const char **response, json_t **claims) { oidc_debug(r, "enter, endpoint=%s, access_token=%s", provider->userinfo_endpoint_url, access_token); /* get the JSON response */ if (oidc_util_http_get(r, provider->userinfo_endpoint_url, NULL, NULL, access_token, provider->ssl_validate_server, response, cfg->http_timeout_long, cfg->outgoing_proxy) == FALSE) return FALSE; /* decode and check for an "error" response */ return oidc_util_decode_json_and_check_error(r, *response, claims); }
/* * refreshes the access_token/id_token /refresh_token received from the OP using the refresh_token */ apr_byte_t oidc_proto_refresh_request(request_rec *r, oidc_cfg *cfg, oidc_provider_t *provider, const char *rtoken, char **id_token, char **access_token, char **token_type, int *expires_in, char **refresh_token) { oidc_debug(r, "enter"); /* assemble the parameters for a call to the token endpoint */ apr_table_t *params = apr_table_make(r->pool, 5); apr_table_addn(params, "grant_type", "refresh_token"); apr_table_addn(params, "refresh_token", rtoken); apr_table_addn(params, "scope", provider->scope); return oidc_proto_token_endpoint_request(r, cfg, provider, params, id_token, access_token, token_type, expires_in, refresh_token); }
/* * resolves the code received from the OP in to an id_token, access_token and refresh_token */ apr_byte_t oidc_proto_resolve_code(request_rec *r, oidc_cfg *cfg, oidc_provider_t *provider, const char *code, char **id_token, char **access_token, char **token_type, int *expires_in, char **refresh_token) { oidc_debug(r, "enter"); /* assemble the parameters for a call to the token endpoint */ apr_table_t *params = apr_table_make(r->pool, 5); apr_table_addn(params, "grant_type", "authorization_code"); apr_table_addn(params, "code", code); apr_table_addn(params, "redirect_uri", cfg->redirect_uri); return oidc_proto_token_endpoint_request(r, cfg, provider, params, id_token, access_token, token_type, expires_in, refresh_token); }
/* * 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; }
/* * 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; }