diff --git a/src/api-umbrella/web-app/actions/admin/sessions.lua b/src/api-umbrella/web-app/actions/admin/sessions.lua index 7bf53ded0..e5ea05408 100644 --- a/src/api-umbrella/web-app/actions/admin/sessions.lua +++ b/src/api-umbrella/web-app/actions/admin/sessions.lua @@ -139,12 +139,8 @@ end function _M.destroy(self) self:init_session_db() - local _, _, open_err = self.session_db:start() - if open_err then - ngx.log(ngx.ERR, "session open error: ", open_err) - end - - local sign_in_provider = self.session_db.data["sign_in_provider"] + self.session_db:open() + local sign_in_provider = self.session_db:get("sign_in_provider") self.session_db:destroy() flash.session(self, "info", t("Signed out successfully.")) @@ -173,8 +169,8 @@ function _M.logout_callback(self) local state = ngx.var.arg_state if state then self:init_session_cookie() - self.session_cookie:start() - local session_state = self.session_cookie.data["openid_connect_state"] + self.session_cookie:open() + local session_state = self.session_cookie:get("openid_connect_state") if state ~= session_state then ngx.log(ngx.WARN, "state from argument: " .. (state or "nil") .. " does not match state restored from session: " .. (session_state or "nil")) diff --git a/src/api-umbrella/web-app/app.lua b/src/api-umbrella/web-app/app.lua index 8ea46093e..56a0a38c4 100644 --- a/src/api-umbrella/web-app/app.lua +++ b/src/api-umbrella/web-app/app.lua @@ -16,12 +16,6 @@ local resty_session = require "resty.session" local t = require("api-umbrella.web-app.utils.gettext").gettext local table_keys = require("pl.tablex").keys -require "resty.session.ciphers.api_umbrella" -require "resty.session.hmac.api_umbrella" -require "resty.session.identifiers.api_umbrella" -require "resty.session.storage.api_umbrella_db" -require "resty.session.serializers.api_umbrella" - local supported_languages = table_keys(LOCALE_DATA) -- Custom error handler so we only show the default lapis debug details in @@ -78,24 +72,15 @@ end -- server-side control on expiring sessions, and it can't be spoofed even with -- knowledge of the encryption secret key. local session_db_options = { - storage = "api_umbrella_db", - cipher = "api_umbrella", - hmac = "api_umbrella", - serializer = "api_umbrella", - identifier = "api_umbrella", - name = "_api_umbrella_session", + storage = "postgres", + postgres = pg_utils.db_config, secret = assert(config["secret_key"]), - random = { - length = 40, - }, - cookie = { - samesite = "Lax", - secure = true, - httponly = true, - idletime = 30 * 60, -- 30 minutes - lifetime = 12 * 60 * 60, -- 12 hours - renew = -1, -- Disable renew - }, + cookie_name = "_api_umbrella_session", + cookie_same_site = "Lax", + cookie_secure = true, + cookie_http_only = true, + idling_timeout = 30 * 60, -- 30 minutes + absolute_timeout = 12 * 60 * 60, -- 12 hours } local function init_session_db(self) if not self.session_db then @@ -113,22 +98,13 @@ end -- session records in the database for the CSRF token). local session_cookie_options = { storage = "cookie", - cipher = "api_umbrella", - hmac = "api_umbrella", - serializer = "api_umbrella", - identifier = "api_umbrella", - name = "_api_umbrella_session_client", secret = assert(config["secret_key"]), - random = { - length = 40, - }, - cookie = { - samesite = "Lax", - secure = true, - httponly = true, - lifetime = 48 * 60 * 60, -- 48 hours - renew = 1 * 60 * 60, -- 1 hour - }, + cookie_name = "_api_umbrella_session_client", + cookie_same_site = "Lax", + cookie_secure = true, + cookie_http_only = true, + rolling_timeout = 1 * 60 * 60, -- 1 hour + absolute_timeout = 48 * 60 * 60, -- 48 hours } local function init_session_cookie(self) if not self.session_cookie then @@ -139,17 +115,19 @@ end local function current_admin_from_session(self) local current_admin self:init_session_db() - local _, _, open_err = self.session_db:start() - if open_err then + local _, open_err = self.session_db:open() + if open_err and open_err ~= "missing session cookie" then if open_err == "session cookie idle time has passed" or open_err == "session cookie has expired" then flash.session(self, "info", t("Your session expired. Please sign in again to continue.")) else ngx.log(ngx.ERR, "session open error: ", open_err) end + + return nil end - if self.session_db and self.session_db.data and self.session_db.data["admin_id"] then - local admin_id = self.session_db.data["admin_id"] + local admin_id = self.session_db:get("admin_id") + if admin_id then local admin = Admin:find({ id = admin_id }) if admin and not admin:is_access_locked() then current_admin = admin diff --git a/src/api-umbrella/web-app/hooks/init_preload_modules.lua b/src/api-umbrella/web-app/hooks/init_preload_modules.lua index 1d4fdebbe..09e6273ac 100644 --- a/src/api-umbrella/web-app/hooks/init_preload_modules.lua +++ b/src/api-umbrella/web-app/hooks/init_preload_modules.lua @@ -178,11 +178,6 @@ require "resty.http" require "resty.mlcache" require "resty.openidc" require "resty.session" -require "resty.session.ciphers.api_umbrella" -require "resty.session.hmac.api_umbrella" -require "resty.session.identifiers.api_umbrella" -require "resty.session.serializers.api_umbrella" -require "resty.session.storage.api_umbrella_db" require "resty.uuid" require "resty.validation" require "resty.validation.ngx" diff --git a/src/api-umbrella/web-app/utils/auth_external_oauth2.lua b/src/api-umbrella/web-app/utils/auth_external_oauth2.lua index ef8c928c2..bb0ae706b 100644 --- a/src/api-umbrella/web-app/utils/auth_external_oauth2.lua +++ b/src/api-umbrella/web-app/utils/auth_external_oauth2.lua @@ -89,8 +89,8 @@ end function _M.authorize(self, strategy_name, url, params) local state = random_token(64) self:init_session_cookie() - self.session_cookie:start() - self.session_cookie.data["oauth2_state"] = state + self.session_cookie:open() + self.session_cookie:set("oauth2_state", state) self.session_cookie:save() local callback_url = build_url(auth_external_path(strategy_name, "/callback")) @@ -119,17 +119,14 @@ function _M.userinfo(self, strategy_name, options) end self:init_session_cookie() - local _, _, open_err = self.session_cookie:start() - if open_err then - ngx.log(ngx.ERR, "session open error: ", open_err) - end + self.session_cookie:open() + local stored_state = self.session_cookie:get("oauth2_state") - if not self.session_cookie or not self.session_cookie.data or not self.session_cookie.data["oauth2_state"] then + if not stored_state then ngx.log(ngx.ERR, "oauth2 state not available") return nil, t("Cross-site request forgery detected") end - local stored_state = self.session_cookie.data["oauth2_state"] local state = self.params["state"] if state ~= stored_state then ngx.log(ngx.ERR, "oauth2 state does not match") diff --git a/src/api-umbrella/web-app/utils/auth_external_openid_connect.lua b/src/api-umbrella/web-app/utils/auth_external_openid_connect.lua index fdc2d741f..b9ed2c074 100644 --- a/src/api-umbrella/web-app/utils/auth_external_openid_connect.lua +++ b/src/api-umbrella/web-app/utils/auth_external_openid_connect.lua @@ -43,8 +43,8 @@ function _M.authenticate(self, strategy_name, callback) -- Call the provider-specific callback logic, which should handle -- authorizing the API Umbrella session and redirecting as appropriate. callback({ - id_token = session["data"]["id_token"], - user = session["data"]["user"], + id_token = session:get("id_token"), + user = session:get("user"), }) -- This shouldn't get hit, since callback should perform it's own @@ -82,14 +82,15 @@ function _M.authenticate(self, strategy_name, callback) end if discovery and discovery["end_session_endpoint"] then -- Generate the state parameter to send. + local openid_connect_state = random_token(64) self:init_session_cookie() - self.session_cookie:start() - self.session_cookie.data["openid_connect_state"] = random_token(64) + self.session_cookie:open() + self.session_cookie:set("openid_connect_state", openid_connect_state) self.session_cookie:save() -- Add the "state" param to the logout URL. local extra_logout_args = { - state = self.session_cookie.data["openid_connect_state"] + state = openid_connect_state, } -- Add the "client_id" param to the logout URL if id_token_hint won't be diff --git a/src/api-umbrella/web-app/utils/csrf.lua b/src/api-umbrella/web-app/utils/csrf.lua index 32212eaae..e9d3b46a7 100644 --- a/src/api-umbrella/web-app/utils/csrf.lua +++ b/src/api-umbrella/web-app/utils/csrf.lua @@ -17,18 +17,20 @@ local _M = {} function _M.generate_token(self) self:init_session_cookie() - self.session_cookie:start() - local csrf_token_key = self.session_cookie.data["csrf_token_key"] - local csrf_token_iv = self.session_cookie.data["csrf_token_iv"] + self.session_cookie:open() + local csrf_token_key = self.session_cookie:get("csrf_token_key") + ngx.log(ngx.ERR, "-DEBUG- GET generate csrf_token_key: ", csrf_token_key) + local csrf_token_iv = self.session_cookie:get("csrf_token_iv") if not csrf_token_key or not csrf_token_iv then if not csrf_token_key then csrf_token_key = random_token(40) - self.session_cookie.data["csrf_token_key"] = csrf_token_key + ngx.log(ngx.ERR, "-DEBUG- SET generate csrf_token_key: ", csrf_token_key) + self.session_cookie:set("csrf_token_key", csrf_token_key) end if not csrf_token_iv then csrf_token_iv = random_token(12) - self.session_cookie.data["csrf_token_iv"] = csrf_token_iv + self.session_cookie:set("csrf_token_iv", csrf_token_iv) end self.session_cookie:save() @@ -41,12 +43,11 @@ end local function validate_token(self) self:init_session_cookie() - local _, _, open_err = self.session_cookie:start() - if open_err then - ngx.log(ngx.ERR, "session open error: ", open_err) - end - - local key = self.session_cookie.data["csrf_token_key"] + self.session_cookie:open() + local key = self.session_cookie:get("csrf_token_key") + ngx.log(ngx.ERR, "-DEBUG- GET csrf_token_key: ", key) + ngx.log(ngx.ERR, "-DEBUG- ngx.var.cookie__api_umbrella_session_client", ngx.var.cookie__api_umbrella_session_client) + ngx.log(ngx.ERR, "-DEBUG- ngx.var.cookie__api_umbrella_session", ngx.var.cookie__api_umbrella_session) if not key then return false, "Missing CSRF token key" end diff --git a/src/api-umbrella/web-app/utils/flash.lua b/src/api-umbrella/web-app/utils/flash.lua index 931d25c81..1ef69199f 100644 --- a/src/api-umbrella/web-app/utils/flash.lua +++ b/src/api-umbrella/web-app/utils/flash.lua @@ -14,11 +14,13 @@ function _M.session(self, flash_type, message, options) data["message"] = message self:init_session_cookie() - self.session_cookie:start() - if not self.session_cookie.data["flash"] then - self.session_cookie.data["flash"] = {} + self.session_cookie:open() + local flash = self.session_cookie:get("flash") + if not flash then + flash = {} end - self.session_cookie.data["flash"][flash_type] = data + flash[flash_type] = data + self.session_cookie:set("flash", flash) self.session_cookie:save() end @@ -27,17 +29,14 @@ function _M.setup(self) self.restore_flashes = function() self:init_session_cookie() - local _, _, open_err = self.session_cookie:start() - if open_err then - ngx.log(ngx.ERR, "session open error: ", open_err) - end - - if self.session_cookie.data and not is_empty(self.session_cookie.data["flash"]) then - for flash_type, data in pairs(self.session_cookie.data["flash"]) do + self.session_cookie:open() + local flash_value = self.session_cookie:get("flash") + if not is_empty(flash_value) then + for flash_type, data in pairs(flash_value) do _M.now(self, flash_type, data["message"], data) end - self.session_cookie.data["flash"] = nil + self.session_cookie:set("flash", nil) self.session_cookie:save() end diff --git a/src/api-umbrella/web-app/utils/login_admin.lua b/src/api-umbrella/web-app/utils/login_admin.lua index baa865221..4a3403ccc 100644 --- a/src/api-umbrella/web-app/utils/login_admin.lua +++ b/src/api-umbrella/web-app/utils/login_admin.lua @@ -17,9 +17,9 @@ return function(self, admin, provider) db.query("COMMIT") self:init_session_db() - self.session_db:start() - self.session_db.data["admin_id"] = admin_id - self.session_db.data["sign_in_provider"] = provider + self.session_db:open() + self.session_db:set("admin_id", admin_id) + self.session_db:set("sign_in_provider", provider) self.session_db:save() return build_url("/admin/#/login") diff --git a/src/resty/session/ciphers/api_umbrella.lua b/src/resty/session/ciphers/api_umbrella.lua deleted file mode 100644 index afc849a34..000000000 --- a/src/resty/session/ciphers/api_umbrella.lua +++ /dev/null @@ -1,27 +0,0 @@ -local encryptor = require "api-umbrella.utils.encryptor" - -local _M = {} -_M.__index = _M - -function _M.new() - return setmetatable({}, _M) -end - -function _M.encrypt(_, data, _, id, auth_data) - local iv = string.sub(id, 1, 12) - local encrypted, _ = encryptor.encrypt(data, auth_data, { - iv = iv, - base64 = false, - }) - - return encrypted -end - -function _M.decrypt(_, encrypted_data, _, id, auth_data) - local iv = string.sub(id, 1, 12) - return encryptor.decrypt(encrypted_data, iv, auth_data, { - base64 = false, - }) -end - -return _M diff --git a/src/resty/session/hmac/api_umbrella.lua b/src/resty/session/hmac/api_umbrella.lua deleted file mode 100644 index e23836805..000000000 --- a/src/resty/session/hmac/api_umbrella.lua +++ /dev/null @@ -1,13 +0,0 @@ --- Hash resty-session values using HMAC SHA-256. --- --- resty-session defaults to HMAC SHA1 (since it's built in to OpenResty), but --- we'll use sha256 in resty-session to better align with the rest of our --- default hmac usage throughout our app. - -local hmac = require "resty.nettle.hmac" - -return function(secret_key, value) - local hmac_sha256 = hmac.sha256.new(secret_key) - hmac_sha256:update(value) - return hmac_sha256:digest() -end diff --git a/src/resty/session/identifiers/api_umbrella.lua b/src/resty/session/identifiers/api_umbrella.lua deleted file mode 100644 index ba219ebf7..000000000 --- a/src/resty/session/identifiers/api_umbrella.lua +++ /dev/null @@ -1,11 +0,0 @@ -local random_token = require "api-umbrella.utils.random_token" - -local defaults = { - length = 40 -} - -return function(session) - local config = session.random or defaults - local length = tonumber(config.length, 10) or defaults.length - return random_token(length) -end diff --git a/src/resty/session/serializers/api_umbrella.lua b/src/resty/session/serializers/api_umbrella.lua deleted file mode 100644 index dfda11055..000000000 --- a/src/resty/session/serializers/api_umbrella.lua +++ /dev/null @@ -1,16 +0,0 @@ -local json_encode_sorted_keys = require "api-umbrella.utils.json_encode_sorted_keys" -local json_decode = require("cjson.safe").decode - --- Replace lua-resty-session's default JSON serializer with one that serializes --- in a stable, sorted manner. --- --- The default serializer otherwise may return the same table in a different --- order each time it is serialized, which can cause issues with the encryption --- signatures or tagging. Without sorting the output by the keys, the same --- underlying table may be output in different ways on each serialization call, --- which can cause invalid signature errors when the session is updated but not --- actually changed (eg, when the inactive time is touched). -return { - serialize = json_encode_sorted_keys, - deserialize = json_decode, -} diff --git a/src/resty/session/storage/api_umbrella_db.lua b/src/resty/session/storage/api_umbrella_db.lua deleted file mode 100644 index a1a1480c9..000000000 --- a/src/resty/session/storage/api_umbrella_db.lua +++ /dev/null @@ -1,50 +0,0 @@ -local db = require "lapis.db" - -local _M = {} -_M.__index = _M - -function _M.new(session) - local self = { - encode = session.encoder.encode, - decode = session.encoder.decode, - } - - return setmetatable(self, _M) -end - -function _M.open(_, id_encoded) - local data - local res = db.query("SELECT data_encrypted FROM sessions WHERE id_hash = ? AND expires_at >= now()", id_encoded) - if res and res[1] and res[1]["data_encrypted"] then - data = res[1]["data_encrypted"] - end - - return data -end - -function _M.start() - return true -end - -function _M:save(id_encoded, ttl, data) - local id = self.decode(id_encoded) - local iv = string.sub(id, 1, 12) - - db.query("INSERT INTO sessions(id_hash, expires_at, data_encrypted, data_encrypted_iv) VALUES(?, now() + interval ?, ?, ?) ON CONFLICT (id_hash) DO UPDATE SET expires_at = EXCLUDED.expires_at, data_encrypted = EXCLUDED.data_encrypted, data_encrypted_iv = EXCLUDED.data_encrypted_iv", id_encoded, ttl .. " seconds", db.raw(ngx.ctx.pgmoon:encode_bytea(data)), iv) - return true -end - -function _M.close() - return true -end - -function _M.destroy(_, id_encoded) - db.query("DELETE FROM sessions WHERE id_hash = ?", id_encoded) -end - -function _M.ttl(_, id_encoded, ttl) - db.query("UPDATE sessions SET expires_at = now() + interval ? WHERE id_hash = ?", ttl .. " seconds", id_encoded) - return true -end - -return _M diff --git a/test/support/api_umbrella_test_helpers/admin_auth.rb b/test/support/api_umbrella_test_helpers/admin_auth.rb index 69a94d523..996559899 100644 --- a/test/support/api_umbrella_test_helpers/admin_auth.rb +++ b/test/support/api_umbrella_test_helpers/admin_auth.rb @@ -224,7 +224,7 @@ def admin_session_data(admin) end def session_base64_encode(value) - Base64.urlsafe_encode64(value, :padding => false) + Base64.urlsafe_encode64(value, padding: false) end def session_base64_decode(value) @@ -236,7 +236,8 @@ def encrypt_session_cookie(data) id_encoded = session_base64_encode(id) iv = id[0, 12] expires = Time.now.to_i + 3600 - data_serialized = MultiJson.dump(data) + audience = "default" + data_serialized = MultiJson.dump([[data, audience]]) hmac_data_key = OpenSSL::HMAC.digest("sha256", $config["secret_key"], [ id, expires, @@ -294,41 +295,71 @@ def decrypt_session_cookie(cookie_value) MultiJson.load(data_serialized) end - def encrypt_session_client_cookie(data) - id = SecureRandom.hex(20) - iv = id[0, 12] - id_encoded = session_base64_encode(id) - expires = Time.now.to_i + 3600 - data_serialized = MultiJson.dump(data) - hmac_data_key = OpenSSL::HMAC.digest("sha256", $config["secret_key"], [ - id, - expires, - ].join("")) - hmac_data = OpenSSL::HMAC.digest("sha256", hmac_data_key, [ - id, - expires, - data_serialized, - STATIC_USER_AGENT, - "http", - ].join("")) - auth_data = [ - STATIC_USER_AGENT, - "http", + # Serialize an encrypted cookie using the lua-resty-session v4 spec: + # https://github.com/bungle/lua-resty-session/blob/v4.0.5/lib/resty/session.lua#L72-L95 + def encrypt_lua_resty_session_cookie(data) + # Assemble the beginning part of the header data: + # https://github.com/bungle/lua-resty-session/blob/v4.0.5/lib/resty/session.lua#L926-L933 + ikm = Digest::SHA256.digest($config.fetch("secret_key")) + cookie_type = 1 + flags = 0 + sid = SecureRandom.bytes(32) + creation_time = Time.now.to_i + rolling_offset = 0 + data_size = Base64.urlsafe_encode64(data, padding: false).length + idling_offset = 0 + + # Pack the values as binary according to: + # https://github.com/bungle/lua-resty-session/blob/v4.0.5/lib/resty/session/utils.lua#L52-L76 + cookie_header = [ + [cookie_type].pack("C").ljust(1, "\x00")[0, 1], + [flags].pack("S<").ljust(1, "\x00")[0, 2], + sid, + [creation_time].pack("L<").ljust(5, "\x00")[0, 5], + [rolling_offset].pack("I<").ljust(4, "\x00")[0, 4], + [data_size].pack("I<").ljust(3, "\x00")[0, 3], ].join("") - data_encrypted = Encryptor.encrypt({ - :value => data_serialized, - :iv => iv, - :key => Digest::SHA256.digest($config["secret_key"]), - :auth_data => auth_data, - }) + # Determine the encryption key and IV from the `sid`: + # https://github.com/bungle/lua-resty-session/blob/v4.0.5/lib/resty/session.lua#L936-L941 + sid_key = OpenSSL::KDF.hkdf(ikm, salt: "", info: "encryption:#{sid}", length: 44, hash: "SHA256") + aes_key = sid_key[0, 32] + iv = sid_key[32, 12] + + # Encrypt the cookie data, using the header as auth data: + # https://github.com/bungle/lua-resty-session/blob/v4.0.5/lib/resty/session.lua#L947-L950 + cipher = OpenSSL::Cipher.new("aes-256-gcm") + cipher.encrypt + cipher.key = aes_key + cipher.iv = iv + cipher.auth_data = cookie_header + ciphertext = cipher.update(data) + ciphertext << cipher.final + tag = cipher.auth_tag + + # Append the encryption tag and other pieces to the header: + # https://github.com/bungle/lua-resty-session/blob/v4.0.5/lib/resty/session.lua#L952 + cookie_header += [ + tag, + [idling_offset].pack("I<").ljust(3, "\x00")[0, 3], + ].join("") - [ - id_encoded, - expires, - session_base64_encode(data_encrypted), - session_base64_encode(hmac_data), - ].join("|") + # https://github.com/bungle/lua-resty-session/blob/v4.0.5/lib/resty/session.lua#L954-L959 + mac_key = OpenSSL::KDF.hkdf(ikm, salt: "", info: "authentication:#{sid}", length: 32, hash: "SHA256") + mac = OpenSSL::HMAC.digest("sha256", mac_key, cookie_header)[0, 16] + cookie_header += mac + + # Assemble the header and encrypted value together: + # https://github.com/bungle/lua-resty-session/blob/v4.0.5/lib/resty/session.lua#L960-L968 + # https://github.com/bungle/lua-resty-session/blob/v4.0.5/lib/resty/session.lua#L998-L1003 + session_base64_encode(cookie_header) + session_base64_encode(ciphertext) + + end + + def encrypt_session_client_cookie(data) + audience = "default" + data_serialized = MultiJson.dump([[data, audience]]) + encrypt_lua_resty_session_cookie(data_serialized) end def decrypt_session_client_cookie(cookie_value)