diff --git a/AUTHORS b/AUTHORS index e2fd56d..d9b02f8 100644 --- a/AUTHORS +++ b/AUTHORS @@ -36,3 +36,4 @@ reporting bugs, providing fixes, suggesting useful features or other: Eduardo Gonçalves Thorsten Fleischmann Tilmann Hars + Chris Frodo diff --git a/README.md b/README.md index eb1efef..95cd047 100644 --- a/README.md +++ b/README.md @@ -676,6 +676,115 @@ http { } ``` +## Sample Configuration for Keycloak OpenID Connect authentication with encrypted tokens + +Sample `nginx.conf` configuration to authenticate using OpenID Connect against a Keycloak 18.0 Authorization Server. +With this configuration, the authorization server returns encrypted tokens for each OIDC tokens. + +```nginx +events { + worker_connections 128; +} + +http { + + lua_package_path '~/lua/?.lua;;'; + + resolver 8.8.8.8; + + lua_ssl_trusted_certificate /opt/local/etc/openssl/cert.pem; + lua_ssl_verify_depth 5; + + # cache for validation results + lua_shared_dict introspection 10m; + + lua_package_cpath "/usr/local/include/lua/?.so;;"; + lua_package_path "/usr/local/openresty/luajit/share/lua/?.lua;/usr/local/lib/lua/?.lua;/usr/local/share/lua/5.1/?.lua;"; + lua_shared_dict discovery 1m; + lua_shared_dict jwks 1m; + + server { + # General settings + listen 443 ssl; + root /var/www/html; + resolver 127.0.0.1:5353; + + # Logs settings + access_log /etc/nginx/log/app-access.log; + error_log /etc/nginx/log/app-error.log; + + # SSL Settings + ssl_certificate /etc/nginx/keys/tls.crt; + ssl_certificate_key /etc/nginx/keys/tls.key; + ssl_verify_depth 2; + ssl_trusted_certificate /etc/nginx/ca/caBundle.crt; + + location = /favicon.ico { + log_not_found off; + } + + # Publish a statically generated JWKS to be used by the OP + location /public/jwks.json { + alias /etc/nginx/public/jwks.json; + + } + + location /secure { + access_by_lua_block { + local opts = { + redirect_uri_path = "/secure/redirect_uri", + discovery = "https:///realms//.well-known/openid-configuration", + client_id = "", + -- Set the client to use JWS as an authentication method + token_endpoint_auth_method = "private_key_jwt", + client_rsa_private_key =[[ +MIIEogIBAAKCAQEAiThmpvXBYdur716D2q7fYKirKxzZIU5QrkBGDvUOwg5izcTv +[...] +h2JHukolz9xf6qN61QMLSd83+kwoBr2drp6xg3eGDLIkQCQLrkY= + ]], + client_rsa_private_key_id = "265tDmmRsigvKPz8oygR0GcNdGX_naMP2cEGXR9Ueo0", + + + -- Encryption settings + -- client_rsa_private_enc_key is the RSA private key to be used to decrypt the JWE access_token / id_token generated by OP to authenticate the lua-resty-openidc RP. + -- client_rsa_private_enc_key_id is the key id to be set in the JWE header to identify which public key the OP has use to encrypt the access_token / id_token. + client_rsa_private_enc_key = [[ +-----BEGIN PRIVATE KEY----- +MIIJKgIBAAKCAgEA1jhYqRlY7WiW36fzdFo4dxkwQXQhouhDlqJSu5MRiaPpwVLn +[...] +5zgUDtKKOXrDePay6pcaqjLKRc2nB8ljeNpYGsrQHAiK20EckOjHJZoH+1dy0Q== +-----END PRIVATE KEY----- + ]], + client_rsa_private_enc_key_id = "AAAtDmmRsigvKPz8oygR0GcNdGX_naMP2cEGXR9Ueo0", + -- end encryption settings + + + redirect_uri_scheme = "https", + session_contents = {id_token=true, user=true, enc_id_token=true, access_token=true}, + token_signing_alg_values_expected = {"RS256"}, + ssl_verify = "yes", + scope = "openid email profile", + + logout_path = "/secure/logout", + redirect_after_logout_uri = "https:///realms//protocol/openid-connect/logout", + redirect_after_logout_with_id_token_hint = false, + post_logout_redirect_uri = "https:///" + + } + local openidc = require("resty.openidc") + local res, err = openidc.authenticate(opts) + if err then + ngx.status = 403 + ngx.say(err) + ngx.exit(ngx.HTTP_FORBIDDEN) + end + ngx.req.set_header('REMOTE_USER', res.id_token.email) + } + } + } +} +``` + ## Logging Logging can be customized, including using custom logger and remapping OpenIDC's diff --git a/lib/resty/openidc.lua b/lib/resty/openidc.lua index cb78c05..3ebdd5f 100644 --- a/lib/resty/openidc.lua +++ b/lib/resty/openidc.lua @@ -405,6 +405,7 @@ local function openidc_parse_json_response(response, ignore_body_on_success) if not res then err = "JSON decoding failed" + end end @@ -637,7 +638,20 @@ function openidc.call_userinfo_endpoint(opts, access_token) log(DEBUG, "userinfo response: ", res.body) -- parse the response from the user info endpoint - return openidc_parse_json_response(res) + local json + json, err = openidc_parse_json_response(res) + + -- If err, try to decode as jwt + if not json and err then + local r_jwt = require("resty.jwt") + local jwt_obj = r_jwt:load_jwt(res.body, nil) + if jwt_obj.valid then + json = jwt_obj.payload + err = nil + end + end + + return json, err end local function can_use_token_auth_method(method, opts) @@ -959,9 +973,28 @@ end local function openidc_load_jwt_and_verify_crypto(opts, jwt_string, asymmetric_secret, symmetric_secret, expected_algs, ...) local r_jwt = require("resty.jwt") - local enc_hdr, enc_payload, enc_sign = string.match(jwt_string, '^(.+)%.(.+)%.(.*)$') - if enc_payload and (not enc_sign or enc_sign == "") then - local jwt = openidc_load_jwt_none_alg(enc_hdr, enc_payload) + local jwt_obj + + -- Expect a JWT encoded as a JWE, extracting parts + local part1, part2, part3, part4, part5 = string.match(jwt_string, '^(.+)%.(.+)%.(.+)%.(.+)%.(.*)$') + + -- No parts extracted, try to extract parts of a JWS encoded JWT + if not part1 and not part2 then + part1, part2, part3= string.match(jwt_string, '^(.+)%.(.+)%.(.*)$') + part4, part5 = nil + end + + log(DEBUG, "part 1 : ",part1, ", part 2 : ",part2, ", part 3 : ",part3, ", part 4 : ",part4, ", part 5 : ",part5) + -- Determine type of JWT (simple JWT, JWS, JWE) : + + -- Case : is a simple JWT + if part1 and not part2 and not part3 and not part4 and not part5 then + return nil, "token is not secured, it's a simple JWT." + + -- Case : is an unsigned JWS + elseif part1 and part2 and (not part3 or part3 == "") and not part4 and not part5 then + -- part1 = JOSE Header, part2 = Payload, part3 = Signature, others are unused + local jwt = openidc_load_jwt_none_alg(part1, part2) if jwt then if opts.accept_none_alg then log(DEBUG, "accept JWT with alg \"none\" and no signature") @@ -969,10 +1002,59 @@ symmetric_secret, expected_algs, ...) else return jwt, "token uses \"none\" alg but accept_none_alg is not enabled" end - end -- otherwise the JWT is invalid and load_jwt produces an error + else + -- Return error when token look like a JWT or the token is unsigned but shouldn't (alg other than \"none\"") + return nil, "invalid unsigned jwt" + end + + -- Case : is a signed JWS + elseif part1 and part2 and part3 and not part4 and not part5 then + -- part1 = JOSE Header, part2 = Payload, part3 = Signature, others are unused + jwt_obj = r_jwt:load_jwt(jwt_string, nil) + + -- Case : is a JWE, without or with preshared key + elseif (part1 and part2 and part3 and part4 and not part5) or + (part1 and part2 and part3 and part4 and part5) then + -- part1 = JOSE Header, part2 = Initialization Vector, part3 = Cyphertext, part4 = Authentication Tag , others are unused + -- or + -- part1 = JOSE Header, part2 = Pre-shared key, part3 = Initialization Vector, part4 = Cyphertext, part5 = Authentication Tag + local jwe_header = cjson.decode(unb64(part1)) + local jwe_obj = nil + + + -- Limiration imposed by lua-resty-jwt v0.2.3 : + -- the "alg" must be either "RSA-OAEP-256" or "DIR" (function parse_jwe in lib jwt.lua, line 256 ) + if not jwe_header.alg then + return nil, "jwe_header is missing the \"alg\" parameter" + elseif jwe_header.alg == "RSA-OAEP-256" then + if not opts.client_rsa_private_enc_key then + return nil, "OIDC config is missing a private RSA key" + elseif not opts.client_rsa_private_enc_key_id then + return nil, "OIDC config is missing a private RSA kid" + end + + if jwe_header.kid == opts.client_rsa_private_enc_key_id then + jwe_obj = r_jwt:load_jwt(jwt_string, opts.client_rsa_private_enc_key) + -- Test if JWE payload exist or not + if jwe_obj.payload == nil and jwe_obj.internal ~= nil then + jwt_obj = r_jwt:load_jwt(jwe_obj.internal.json_payload, nil) + elseif type(jwe_obj.payload) == 'string' then + jwt_obj = r_jwt:load_jwt(jwe_obj.payload, nil) + elseif type(jwe_obj.payload) == 'table' then + return nil, "jwe_payload must be signed before beeing encrypted" + else + return nil, "jwe token cannot be decrypted" + end + else + return nil, "jwe_header.kid not matching client_rsa_private_enc_key_id" + end + else + return nil, "jwe_header.alg not supported by the jwt.lua library" + end + else + return nil, "invalid jwt" end - local jwt_obj = r_jwt:load_jwt(jwt_string, nil) if not jwt_obj.valid then local reason = "invalid jwt" if jwt_obj.reason then @@ -1168,6 +1250,7 @@ local function openidc_authorization_response(opts, session) log(ERROR, "error calling userinfo endpoint: " .. err) elseif user then if id_token.sub ~= user.sub then + err = "\"sub\" claim in id_token (\"" .. (id_token.sub or "null") .. "\") is not equal to the \"sub\" claim returned from the userinfo endpoint (\"" .. (user.sub or "null") .. "\")" log(ERROR, err) else diff --git a/tests/spec/id_token_validation_spec.lua b/tests/spec/id_token_validation_spec.lua index 1f49d94..151f5c2 100644 --- a/tests/spec/id_token_validation_spec.lua +++ b/tests/spec/id_token_validation_spec.lua @@ -384,3 +384,165 @@ describe("when the id token is signed by an algorithm not announced by discovery end) end) +describe("when the id_token is encrypted with a pre-shared key", function() + describe("and the key managment algorithm is \"RSA-OAEP-256\"", function() + test_support.start_server({ + jwe_enc_rsa_key = test_support.load("/spec/public_rsa_key.pem"), + oidc_opts = { + client_rsa_private_enc_key = test_support.load("/spec/private_rsa_key.pem"), + client_rsa_private_enc_key_id = "RSAencKID" + } + }) + teardown(test_support.stop_server) + local _, status = test_support.login() + it("login succeeds", function() + assert.are.equals(302, status) + end) + + end) + +end) + +describe("when the id_token cannot be decrypted", function() + describe("because the wrong RSA key is used", function() + test_support.start_server({ + jwe_enc_rsa_key = test_support.load("/spec/public_rsa_key.pem"), + oidc_opts = { + client_rsa_private_enc_key = test_support.load("/spec/private_longer_rsa_key.pem"), + client_rsa_private_enc_key_id = "RSAencKID" + } + }) + teardown(test_support.stop_server) + local _, status = test_support.login() + it("authenticate returns an error", function() + assert.error_log_contains("jwe token cannot be decrypted") + end) + end) + describe("because the wrong RSA kid is used", function() + test_support.start_server({ + jwe_enc_rsa_key = test_support.load("/spec/public_rsa_key.pem"), + oidc_opts = { + client_rsa_private_enc_key = test_support.load("/spec/private_rsa_key.pem"), + client_rsa_private_enc_key_id = "RSAWrongencKID" + } + }) + teardown(test_support.stop_server) + local _, status = test_support.login() + it("authenticate returns an error", function() + assert.error_log_contains("jwe_header.kid not matching client_rsa_private_enc_key_id") + end) + end) + describe("because the wrong RSA kid is used", function() + test_support.start_server({ + jwe_enc_rsa_key = test_support.load("/spec/public_rsa_key.pem"), + oidc_opts = { + client_rsa_private_enc_key = test_support.load("/spec/private_rsa_key.pem"), + client_rsa_private_enc_key_id = "RSAWrongencKID" + } + }) + teardown(test_support.stop_server) + local _, status = test_support.login() + it("authenticate returns an error", function() + assert.error_log_contains("jwe_header.kid not matching client_rsa_private_enc_key_id") + end) + end) + describe("because an unsupported key managment algorithm is used", function() + test_support.start_server({ + jwe_enc_rsa_key = test_support.load("/spec/public_rsa_key.pem"), + jwe_fake_alg = "true", + oidc_opts = { + client_rsa_private_enc_key = test_support.load("/spec/private_rsa_key.pem"), + client_rsa_private_enc_key_id = "RSAencKID" + } + }) + teardown(test_support.stop_server) + local _, status = test_support.login() + it("authenticate returns an error", function() + assert.error_log_contains("jwe_header.alg not supported by the jwt.lua library") + end) + end) + describe("because an unsupported encryption algorithm is used", function() + test_support.start_server({ + jwe_enc_rsa_key = test_support.load("/spec/public_rsa_key.pem"), + jwe_fake_enc = "true", + oidc_opts = { + client_rsa_private_enc_key = test_support.load("/spec/private_rsa_key.pem"), + client_rsa_private_enc_key_id = "RSAencKID" + } + }) + teardown(test_support.stop_server) + local _, status = test_support.login() + it("authenticate returns an error", function() + assert.error_log_contains("jwe token cannot be decrypted") + end) + end) + describe("because the token is faked", function() + test_support.start_server({ + jwe_enc_rsa_key = test_support.load("/spec/public_rsa_key.pem"), + jwe_fake_jwe = "true", + oidc_opts = { + client_rsa_private_enc_key = test_support.load("/spec/private_rsa_key.pem"), + client_rsa_private_enc_key_id = "RSAencKID" + } + }) + teardown(test_support.stop_server) + local _, status = test_support.login() + it("authenticate returns an error", function() + assert.error_log_contains("jwe token cannot be decrypted") + end) + end) +end) + +describe("when the id_token is encrypted and use a RSA \"alg\" and the config", function() + describe("is missing a private RSA key", function() + test_support.start_server({ + jwe_enc_rsa_key = test_support.load("/spec/public_rsa_key.pem"), + oidc_opts = { + client_rsa_private_enc_key_id = "RSAencKID" + } + }) + teardown(test_support.stop_server) + local _, status = test_support.login() + it("authenticate returns an error", function() + assert.error_log_contains("OIDC config is missing a private RSA key") + end) + end) + describe("is missing a private RSA kid", function() + test_support.start_server({ + jwe_enc_rsa_key = test_support.load("/spec/public_rsa_key.pem"), + oidc_opts = { + client_rsa_private_enc_key = test_support.load("/spec/private_rsa_key.pem") + } + }) + teardown(test_support.stop_server) + local _, status = test_support.login() + it("authenticate returns an error", function() + assert.error_log_contains("OIDC config is missing a private RSA kid") + end) + end) +end) + +describe("when the id_token is encrypted but not signed", function() + test_support.start_server({ + jwe_enc_rsa_key = test_support.load("/spec/public_rsa_key.pem"), + jwe_signed_payload = "false", + oidc_opts = { + client_rsa_private_enc_key = test_support.load("/spec/private_rsa_key.pem"), + client_rsa_private_enc_key_id = "RSAencKID" + } + }) + teardown(test_support.stop_server) + local _, status = test_support.login() + it("authenticate returns an error", function() + assert.error_log_contains("jwe_payload must be signed before beeing encrypted") + end) +end) + +-- TODO : Test valid 4 part JWE + -- Not implemented yet in openidc.lua : linked to direct symetric encryption (AES) + +-- TODO : Test no configured AES key + -- Not implemented yet in openidc.lua : linked to symetric encryption (AES) + +-- TODO : Test invalid JWE with AES => load_jwt fails + -- Not implemented yet in openidc.lua : linked to symetric encryption (AES) \ No newline at end of file diff --git a/tests/spec/test_support.lua b/tests/spec/test_support.lua index fb1d229..e2d4a57 100644 --- a/tests/spec/test_support.lua +++ b/tests/spec/test_support.lua @@ -64,6 +64,22 @@ function test_support.self_signed_jwt(payload, alg, signature) end local DEFAULT_JWT_SIGN_SECRET = test_support.load("/spec/private_rsa_key.pem") +local DEFAULT_JWE_ENC_RSA_KEY = "" +local DEFAULT_JWE_ENC_RSA_KID = "RSAencKID" +local DEFAULT_JWE_DEC_RSA_KEY = test_support.load("/spec/private_rsa_key.pem") +local DEFAULT_JWE_ENC_AES_KEY = "" +local DEFAULT_JWE_ENC_AES_KID = "AESencKID" +local DEFAULT_JWE_TOKEN_HEADER = { + typ = "JWE", + alg = "RSA-OAEP-256", + enc = "A128CBC-HS256", + kid = DEFAULT_JWE_ENC_RSA_KID +} +local DEFAULT_JWE_FAKE_ALG = "false" +local DEFAULT_JWE_FAKE_ENC = "false" +local DEFAULT_JWE_FAKE_JWE = "false" + +local DEFAULT_JWE_SIGNED_PAYLOAD = "true" local DEFAULT_JWK = test_support.load("/spec/rsa_key_jwk_with_x5c.json") @@ -95,19 +111,31 @@ local test_globals = {} local sign_secret = [=[ JWT_SIGN_SECRET]=] +local jwe_enc_rsa_key = [=[JWE_ENC_RSA_KEY]=] +local jwe_enc_aes_key = [=[JWE_ENC_AES_KEY]=] + -- ground work for future implementation of JWE using AES 'alg' + + if os.getenv('coverage') then require("luacov.runner")("/spec/luacov/settings.luacov") end test_globals.oidc = require "resty.openidc" test_globals.cjson = require "cjson" + + +test_globals.jwks = [=[JWK]=] +test_globals.use_jwe = jwe_enc_rsa_key ~= "" or jwe_enc_aes_key ~= "" + test_globals.delay = function(delay_response) if delay_response > 0 then ngx.sleep(delay_response / 1000) end end + test_globals.b64url = function(s) return ngx.encode_base64(test_globals.cjson.encode(s)):gsub('+','-'):gsub('/','_') end + test_globals.create_jwt = function(payload, fake_signature) if not fake_signature then local jwt_content = { @@ -124,17 +152,50 @@ test_globals.create_jwt = function(payload, fake_signature) return header .. "." .. test_globals.b64url(payload) .. ".NOT_A_VALID_SIGNATURE" end end + +test_globals.create_jwe = function(payload, fake_alg, fake_enc, fake_jwe) + if jwe_enc_rsa_key ~= "" then + local jwe_header = JWE_TOKEN_HEADER + if fake_alg then + jwe_header.alg = "WRONG_ALG" + end + if fake_enc then + jwe_header.enc = "WRONG_ENC" + end + + if fake_alg or fake_enc or fake_jwe then + return test_globals.b64url(jwe_header) .. ".NOT_A_VALID_PRESHARED_KEY.NOT_A_VALID_IV.NOT_A_VALID_CIPHERTEXT.NOT_A_VALID_MAC" + else + local jwt_content = { + header = jwe_header, + payload = payload + } + local jwt = require "resty.jwt" + return jwt:sign(jwe_enc_rsa_key, jwt_content) + end + elseif jwe_enc_aes_key ~= "" then + ngx.log(ngx.ERR, "JWE w/ AES test not implemented yet") + return nil + else + ngx.log(ngx.ERR, "Something went wrong while creating the JWE") + return nil + end +end + test_globals.query_decorator = function(req) req.query = "foo=bar" return req end + test_globals.body_decorator = function(req) local body = ngx.decode_args(req.body) body.foo = "bar" req.body = ngx.encode_args(body) return req end + test_globals.jwks = [=[JWK]=] + return test_globals ]] @@ -266,9 +327,17 @@ http { }) jwt_token = header .. "." .. test_globals.b64url(id_token) .. "." else - jwt_token = test_globals.create_jwt(id_token, FAKE_ID_TOKEN_SIGNATURE) - if BREAK_ID_TOKEN_SIGNATURE then - jwt_token = jwt_token:sub(1, -6) .. "XXXXX" + if not test_globals.use_jwe then + jwt_token = test_globals.create_jwt(id_token, FAKE_ID_TOKEN_SIGNATURE) + if BREAK_ID_TOKEN_SIGNATURE then + jwt_token = jwt_token:sub(1, -6) .. "XXXXX" + end + else + if JWE_SIGNED_PAYLOAD then + jwt_token = test_globals.create_jwe(test_globals.create_jwt(id_token), JWE_FAKE_ALG, JWE_FAKE_ENC, JWE_FAKE_JWE) + else + jwt_token = test_globals.create_jwe(id_token, JWE_FAKE_ALG, JWE_FAKE_ENC, JWE_fake_JWE) + end end end local token_response = { @@ -439,6 +508,8 @@ local function write_template(out, template, custom_config) local verify_opts = merge(merge({}, DEFAULT_VERIFY_OPTS), custom_config["verify_opts"] or {}) local access_token = merge(merge({}, DEFAULT_ACCESS_TOKEN), custom_config["access_token"] or {}) local token_header = merge(merge({}, DEFAULT_TOKEN_HEADER), custom_config["token_header"] or {}) + local jwe_token_header = merge(merge({}, DEFAULT_JWE_TOKEN_HEADER), custom_config["jwe_token_header"] or {}) + local userinfo = merge(merge({}, DEFAULT_ID_TOKEN), custom_config["userinfo"] or {}) local introspection_response = merge(merge({}, DEFAULT_INTROSPECTION_RESPONSE), custom_config["introspection_response"] or {}) @@ -450,6 +521,7 @@ local function write_template(out, template, custom_config) local refreshing_token_fails = custom_config["refreshing_token_fails"] or DEFAULT_REFRESHING_TOKEN_FAILS local refresh_response_contains_id_token = custom_config["refresh_response_contains_id_token"] or DEFAULT_REFRESH_RESPONSE_CONTAINS_ID_TOKEN local access_token_opts = merge(merge({}, DEFAULT_OIDC_CONFIG), custom_config["access_token_opts"] or {}) + for _, k in ipairs(custom_config["remove_id_token_claims"] or {}) do id_token[k] = nil end @@ -473,6 +545,7 @@ local function write_template(out, template, custom_config) end local content = template :gsub("OIDC_CONFIG", serpent.block(oidc_config, {comment = false })) + :gsub("JWE_TOKEN_HEADER", serpent.block(jwe_token_header, {comment = false })) :gsub("TOKEN_HEADER", serpent.block(token_header, {comment = false })) :gsub("JWT_SIGN_SECRET", custom_config["jwt_sign_secret"] or DEFAULT_JWT_SIGN_SECRET) :gsub("VERIFY_OPTS", serpent.block(verify_opts, {comment = false })) @@ -499,6 +572,17 @@ local function write_template(out, template, custom_config) :gsub("ID_TOKEN", serpent.block(id_token, {comment = false })) :gsub("ACCESS_TOKEN", serpent.block(access_token, {comment = false })) :gsub("UNAUTH_ACTION", custom_config["unauth_action"] and ('"' .. custom_config["unauth_action"] .. '"') or DEFAULT_UNAUTH_ACTION) + :gsub("JWE_ENC_RSA_KEY", custom_config["jwe_enc_rsa_key"] or DEFAULT_JWE_ENC_RSA_KEY) + :gsub("JWE_ENC_RSA_KID", custom_config["jwe_enc_rsa_kid"] or DEFAULT_JWE_ENC_RSA_KID) + :gsub("JWE_DEC_RSA_KEY", custom_config["jwe_dec_rsa_key"] or DEFAULT_JWE_DEC_RSA_KEY) + :gsub("JWE_DEC_RSA_KID", custom_config["jwe_enc_rsa_kid"] or DEFAULT_JWE_ENC_RSA_KID) + :gsub("JWE_ENC_AES_KEY", custom_config["jwe_enc_aes_key"] or DEFAULT_JWE_ENC_AES_KEY) + :gsub("JWE_ENC_AES_KID", custom_config["jwe_enc_aes_kid"] or DEFAULT_JWE_ENC_AES_KID) + :gsub("JWE_SIGNED_PAYLOAD", custom_config["jwe_signed_payload"] or DEFAULT_JWE_SIGNED_PAYLOAD) + :gsub("JWE_FAKE_ALG", custom_config["jwe_fake_alg"] or DEFAULT_JWE_FAKE_ALG) + :gsub("JWE_FAKE_ENC", custom_config["jwe_fake_enc"] or DEFAULT_JWE_FAKE_ENC) + :gsub("JWE_FAKE_JWE", custom_config["jwe_fake_jwe"] or DEFAULT_JWE_FAKE_JWE) + out:write(content) end