def encode_template(fn, ctx, base_dir="/app/templates"): path = os.path.join(base_dir, fn) # ctx is nested which has `config` and `secret` keys data = {} for _, v in ctx.items(): data.update(v) with open(path) as f: return generate_base64_contents(safe_render(f.read(), data))
def merge_config_api_ctx(ctx): def transform_url(url): auth_server_url = os.environ.get("CN_AUTH_SERVER_URL", "") if not auth_server_url: return url parse_result = urlparse(url) if parse_result.path.startswith("/.well-known"): path = f"/jans-auth{parse_result.path}" else: path = parse_result.path url = f"http://{auth_server_url}{path}" return url def get_injected_urls(): auth_config = json.loads( base64.b64decode(ctx["auth_config_base64"]).decode()) urls = ( "issuer", "openIdConfigurationEndpoint", "introspectionEndpoint", "tokenEndpoint", "tokenRevocationEndpoint", ) return {url: transform_url(auth_config[url]) for url in urls} approved_issuer = [ctx["hostname"]] token_server_hostname = os.environ.get("CN_TOKEN_SERVER_BASE_HOSTNAME") if token_server_hostname and token_server_hostname not in approved_issuer: approved_issuer.append(token_server_hostname) local_ctx = { "apiApprovedIssuer": ",".join([f'"https://{issuer}"' for issuer in approved_issuer]), "apiProtectionType": "oauth2", "jca_client_id": ctx["jca_client_id"], "jca_client_encoded_pw": ctx["jca_client_encoded_pw"], "endpointInjectionEnabled": "true", "configOauthEnabled": str(os.environ.get("CN_CONFIG_API_OAUTH_ENABLED") or True).lower(), } local_ctx.update(get_injected_urls()) basedir = '/app/templates/jans-config-api' file_mappings = { "config_api_dynamic_conf_base64": "dynamic-conf.json", } for key, file_ in file_mappings.items(): file_path = os.path.join(basedir, file_) with open(file_path) as fp: ctx[key] = generate_base64_contents(fp.read() % local_ctx) return ctx
def merge_scim_ctx(ctx): basedir = '/app/templates/jans-scim' file_mappings = { 'scim_dynamic_conf_base64': 'dynamic-conf.json', 'scim_static_conf_base64': 'static-conf.json', } for key, file_ in file_mappings.items(): file_path = os.path.join(basedir, file_) with open(file_path) as fp: ctx[key] = generate_base64_contents(fp.read() % ctx) return ctx
def merge_auth_ctx(ctx): basedir = '/app/templates/jans-auth' file_mappings = { 'auth_static_conf_base64': 'static-conf.json', 'auth_error_base64': 'errors.json', } if os.environ.get("CN_DISTRIBUTION", "default") == "openbanking": file_mappings["auth_config_base64"] = "dynamic-conf.ob.json" else: file_mappings["auth_config_base64"] = "dynamic-conf.json" for key, file_ in file_mappings.items(): file_path = os.path.join(basedir, file_) with open(file_path) as fp: ctx[key] = generate_base64_contents(fp.read() % ctx) return ctx
def merge_extension_ctx(ctx): basedir = "/app/static/extension" if os.environ.get("CN_DISTRIBUTION", "default") == "openbanking": basedir = "/app/static/ob_extension" for ext_type in os.listdir(basedir): ext_type_dir = os.path.join(basedir, ext_type) for fname in os.listdir(ext_type_dir): filepath = os.path.join(ext_type_dir, fname) ext_name = "{}_{}".format(ext_type, os.path.splitext(fname)[0].lower()) with open(filepath) as fd: ctx[ext_name] = generate_base64_contents(fd.read()) return ctx
def prune(self): # avoid conflict with external JWKS URI ext_jwks_uri = os.environ.get("CN_OB_EXT_SIGNING_JWKS_URI", "") if ext_jwks_uri: logger.warning(f"Found external JWKS URI at {ext_jwks_uri}; " "skipping proccess to avoid conflict with " "builtin key rotation feature in jans-auth") return config = self.backend.get_auth_config() if not config: # search failed due to missing entry logger.warning("Unable to find jans-auth config") return try: conf_dynamic = json.loads(config["jansConfDyn"]) except TypeError: # not string/buffer conf_dynamic = config["jansConfDyn"] if conf_dynamic["keyRegenerationEnabled"]: logger.warning("keyRegenerationEnabled config was set to true; " "skipping proccess to avoid conflict with " "builtin key rotation feature in jans-auth") return jks_pass = self.manager.secret.get("auth_openid_jks_pass") conf_dynamic.update({ "keyRegenerationEnabled": False, # always set to False "webKeysStorage": "keystore", "keyStoreSecret": jks_pass, "keyAlgsAllowedForGeneration": self.allowed_key_algs, "keyStoreFile": self.manager.config.get("auth_openid_jks_fn"), "jwksUri": f"https://{self.manager.config.get('hostname')}/jans-auth/restv1/jwks", }) # get old JWKS from persistence try: web_keys = json.loads(config["jansConfWebKeys"]) except TypeError: web_keys = config["jansConfWebKeys"] logger.info("Cleaning up keys (if any)") jks_fn = "/etc/certs/auth-keys.jks" self.manager.secret.to_file("auth_jks_base64", jks_fn, decode=True, binary_mode=True) # non-pruned keys new_jwks = [] # make sure keys sorted by newer ``exp`` first, so the older one # won't be added to new JWKS old_jwks = web_keys.get("keys", []) old_jwks = sorted(old_jwks, key=lambda k: k["exp"], reverse=True) cnt = Counter(j["alg"] for j in new_jwks) for jwk in old_jwks: # exclude alg if it's not allowed if jwk["alg"] not in self.allowed_key_algs: keytool_delete_key(jks_fn, jwk["kid"], jks_pass) continue # cannot have more than 1 key for same algorithm in new JWKS if cnt[jwk["alg"]]: keytool_delete_key(jks_fn, jwk["kid"], jks_pass) continue # preserve the key new_jwks.append(jwk) cnt[jwk["alg"]] += 1 web_keys["keys"] = new_jwks jwks_fn = "/etc/certs/auth-keys.json" with open(jwks_fn, "w") as f: f.write(json.dumps(web_keys, indent=2)) if self.dry_run: return auth_containers = [] if self.push_keys: auth_containers = self.meta_client.get_containers( "APP_NAME=auth-server") if not auth_containers: logger.warning( "Unable to find any jans-auth container; make sure " "to deploy jans-auth and set APP_NAME=auth-server " "label on container level") # exit immediately to avoid persistence/secrets being modified return for container in auth_containers: name = self.meta_client.get_container_name(container) logger.info(f"creating backup of {name}:{jks_fn}") self.meta_client.exec_cmd(container, f"cp {jks_fn} {jks_fn}.backup") logger.info(f"creating new {name}:{jks_fn}") self.meta_client.copy_to_container(container, jks_fn) logger.info(f"creating backup of {name}:{jwks_fn}") self.meta_client.exec_cmd(container, f"cp {jwks_fn} {jwks_fn}.backup") logger.info(f"creating new {name}:{jwks_fn}") self.meta_client.copy_to_container(container, jwks_fn) try: with open(jwks_fn) as f: keys = json.loads(f.read()) logger.info("modifying jans-auth configuration") rev = int(config["jansRevision"]) modified = self.backend.modify_auth_config( config["id"], rev + 1, conf_dynamic, keys, ) if not modified: # restore jks and jwks logger.warning("failed to modify jans-auth configuration") for container in auth_containers: name = self.meta_client.get_container_name(container) logger.info(f"restoring backup of {name}:{jks_fn}") self.meta_client.exec_cmd(container, f"cp {jks_fn}.backup {jks_fn}") logger.info(f"restoring backup of {name}:{jwks_fn}") self.meta_client.exec_cmd( container, f"cp {jwks_fn}.backup {jwks_fn}") return self.manager.secret.set("auth_jks_base64", encode_jks(self.manager)) self.manager.config.set("auth_key_rotated_at", int(time.time())) self.manager.secret.set("auth_openid_jks_pass", jks_pass) self.manager.config.set("auth_sig_keys", self.sig_keys) self.manager.config.set("auth_enc_keys", self.enc_keys) # jwks self.manager.secret.set( "auth_openid_key_base64", generate_base64_contents(json.dumps(keys)), ) except ( TypeError, ValueError, ) as exc: logger.warning(f"Unable to get public keys; reason={exc}")
def patch(self): # avoid conflict with external JWKS URI ext_jwks_uri = os.environ.get("CN_OB_EXT_SIGNING_JWKS_URI", "") if ext_jwks_uri: logger.warning(f"Found external JWKS URI at {ext_jwks_uri}; " "skipping proccess to avoid conflict with " "builtin key rotation feature in jans-auth") return strategies = ", ".join(KEY_STRATEGIES) if self.key_strategy not in KEY_STRATEGIES: logger.error(f"Key strategy must be one of {strategies}") sys.exit(1) if self.privkey_push_strategy not in KEY_STRATEGIES: logger.error( f"Private key push strategy must be one of {strategies}") sys.exit(1) push_delay_invalid = False try: if int(self.privkey_push_delay) < 0: push_delay_invalid = True except ValueError: push_delay_invalid = True if push_delay_invalid: logger.error("Invalid integer value for private key push delay") sys.exit(1) config = self.backend.get_auth_config() if not config: # search failed due to missing entry logger.warning("Unable to find jans-auth config") return try: conf_dynamic = json.loads(config["jansConfDyn"]) except TypeError: # not string/buffer conf_dynamic = config["jansConfDyn"] if conf_dynamic["keyRegenerationEnabled"]: logger.warning("keyRegenerationEnabled config was set to true; " "skipping proccess to avoid conflict with " "builtin key rotation feature in jans-auth") return jks_pass = self.manager.secret.get("auth_openid_jks_pass") conf_dynamic.update({ "keyRegenerationEnabled": False, # always set to False "keyRegenerationInterval": int(self.rotation_interval), "webKeysStorage": "keystore", "keyStoreSecret": jks_pass, "keySelectionStrategy": self.key_strategy, "keyAlgsAllowedForGeneration": self.allowed_key_algs, "keyStoreFile": self.manager.config.get("auth_openid_jks_fn"), "jwksUri": f"https://{self.manager.config.get('hostname')}/jans-auth/restv1/jwks", }) # get old JWKS from persistence try: web_keys = json.loads(config["jansConfWebKeys"]) except TypeError: web_keys = config["jansConfWebKeys"] with open("/etc/certs/auth-keys.old.json", "w") as f: f.write(json.dumps(web_keys, indent=2)) exp_hours = int(self.rotation_interval) + int( conf_dynamic["idTokenLifetime"] / 3600) jwks_fn, jks_fn = self.get_merged_keys(exp_hours) if self.dry_run: return auth_containers = [] if self.push_keys: auth_containers = self.meta_client.get_containers( "APP_NAME=auth-server") if not auth_containers: logger.warning( "Unable to find any jans-auth container; make sure " "to deploy jans-auth and set APP_NAME=auth-server " "label on container level") # exit immediately to avoid persistence/secrets being modified return for container in auth_containers: name = self.meta_client.get_container_name(container) logger.info(f"creating backup of {name}:{jwks_fn}") self.meta_client.exec_cmd(container, f"cp {jwks_fn} {jwks_fn}.backup") logger.info(f"creating new {name}:{jwks_fn}") self.meta_client.copy_to_container(container, jwks_fn) if int(self.privkey_push_delay) > 0: # delayed jks push continue logger.info(f"creating backup of {name}:{jks_fn}") self.meta_client.exec_cmd(container, f"cp {jks_fn} {jks_fn}.backup") logger.info(f"creating new {name}:{jks_fn}") self.meta_client.copy_to_container(container, jks_fn) try: with open(jwks_fn) as f: keys = json.loads(f.read()) logger.info("modifying jans-auth configuration") logger.info(f"using keySelectionStrategy {self.key_strategy}") rev = int(config["jansRevision"]) + 1 modified = self.backend.modify_auth_config( config["id"], rev, conf_dynamic, keys, ) if not modified: # restore jks and jwks logger.warning("failed to modify jans-auth configuration") for container in auth_containers: logger.info(f"restoring backup of {name}:{jwks_fn}") self.meta_client.exec_cmd( container, f"cp {jwks_fn}.backup {jwks_fn}") if int(self.privkey_push_delay) > 0: # delayed jks revert continue name = self.meta_client.get_container_name(container) logger.info(f"restoring backup of {name}:{jks_fn}") self.meta_client.exec_cmd(container, f"cp {jks_fn}.backup {jks_fn}") return self.manager.secret.set("auth_jks_base64", encode_jks(self.manager)) self.manager.config.set("auth_key_rotated_at", int(time.time())) self.manager.secret.set("auth_openid_jks_pass", jks_pass) self.manager.config.set("auth_sig_keys", self.sig_keys) self.manager.config.set("auth_enc_keys", self.enc_keys) # jwks self.manager.secret.set( "auth_openid_key_base64", generate_base64_contents(json.dumps(keys)), ) # publish delayed jks if int(self.privkey_push_delay) > 0: logger.info( f"Waiting for private key push delay ({int(self.privkey_push_delay)} seconds) ..." ) time.sleep(int(self.privkey_push_delay)) for container in auth_containers: logger.info(f"creating new {name}:{jks_fn}") self.meta_client.copy_to_container(container, jks_fn) # key selection is changed if self.privkey_push_strategy != self.key_strategy: rev = rev + 1 conf_dynamic.update({ "keySelectionStrategy": self.privkey_push_strategy, }) logger.info( f"using keySelectionStrategy {self.privkey_push_strategy}" ) self.backend.modify_auth_config( config["id"], rev, conf_dynamic, keys, ) except ( TypeError, ValueError, ) as exc: logger.warning(f"Unable to get public keys; reason={exc}")
def test_generate_base64_contents(text, num_spaces, expected): from jans.pycloudlib.utils import generate_base64_contents assert generate_base64_contents(text, num_spaces) == expected