Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 6 additions & 15 deletions db/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -1271,12 +1271,10 @@ ALTER SEQUENCE api_umbrella.published_config_id_seq OWNED BY api_umbrella.publis
--

CREATE TABLE api_umbrella.sessions (
id_hash character varying(64) NOT NULL,
data_encrypted bytea NOT NULL,
data_encrypted_iv character varying(12) NOT NULL,
expires_at timestamp with time zone NOT NULL,
created_at timestamp with time zone DEFAULT transaction_timestamp() NOT NULL,
updated_at timestamp with time zone DEFAULT transaction_timestamp() NOT NULL
sid text NOT NULL,
name text,
data text,
exp timestamp with time zone
);


Expand Down Expand Up @@ -1842,7 +1840,7 @@ ALTER TABLE ONLY api_umbrella.rate_limits
--

ALTER TABLE ONLY api_umbrella.sessions
ADD CONSTRAINT sessions_pkey PRIMARY KEY (id_hash);
ADD CONSTRAINT sessions_pkey PRIMARY KEY (sid);


--
Expand Down Expand Up @@ -2034,7 +2032,7 @@ CREATE UNIQUE INDEX distributed_rate_limit_counters_version_idx ON api_umbrella.
-- Name: sessions_expires_at_idx; Type: INDEX; Schema: api_umbrella; Owner: -
--

CREATE INDEX sessions_expires_at_idx ON api_umbrella.sessions USING btree (expires_at);
CREATE INDEX sessions_exp_idx ON api_umbrella.sessions USING btree (exp);


--
Expand Down Expand Up @@ -2618,13 +2616,6 @@ CREATE TRIGGER published_config_stamp_record BEFORE INSERT OR DELETE OR UPDATE O
CREATE TRIGGER rate_limits_stamp_record BEFORE INSERT OR DELETE OR UPDATE ON api_umbrella.rate_limits FOR EACH ROW EXECUTE FUNCTION api_umbrella.stamp_record('[{"table_name":"api_backend_settings","primary_key":"id","foreign_key":"api_backend_settings_id"},{"table_name":"api_user_settings","primary_key":"id","foreign_key":"api_user_settings_id"}]');


--
-- Name: sessions sessions_stamp_record; Type: TRIGGER; Schema: api_umbrella; Owner: -
--

CREATE TRIGGER sessions_stamp_record BEFORE UPDATE ON api_umbrella.sessions FOR EACH ROW EXECUTE FUNCTION api_umbrella.update_timestamp();


--
-- Name: website_backends website_backends_stamp_record; Type: TRIGGER; Schema: api_umbrella; Owner: -
--
Expand Down
1 change: 1 addition & 0 deletions docker/dev/docker-entrypoint
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ mkdir -p /etc/api-umbrella
echo " password: dev_password"
} > /etc/api-umbrella/api-umbrella.yml

/app/configure
mkdir -p /build/.task
ln -snf /build/.task /app/.task

Expand Down
4 changes: 2 additions & 2 deletions src/api-umbrella-git-1.rockspec
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ dependencies = {
"lua-resty-mail ~> 1.2.0",
"lua-resty-mlcache ~> 2.7.0",
"lua-resty-nettle ~> 2.1",
"lua-resty-openidc ~> 1.7.6",
"lua-resty-session ~> 3.10",
"lua-resty-openidc ~> 1.8.0",
"lua-resty-session ~> 4.0",
"lua-resty-txid ~> 1.0.0",
"lua-resty-uuid ~> 1.1",
"lua-resty-validation ~> 2.7",
Expand Down
2 changes: 1 addition & 1 deletion src/api-umbrella/proxy/jobs/db_expirations.lua
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ local function do_run()
"DELETE FROM analytics_cache WHERE expires_at IS NOT NULL AND expires_at < now()",
"DELETE FROM cache WHERE expires_at IS NOT NULL AND expires_at < now()",
"DELETE FROM distributed_rate_limit_counters WHERE expires_at < now()",
"DELETE FROM sessions WHERE expires_at < now()",
"DELETE FROM sessions WHERE exp < now()",
}
for _, query in ipairs(queries) do
local result, err = pg_utils.query(query, nil, { quiet = true })
Expand Down
6 changes: 5 additions & 1 deletion src/api-umbrella/utils/pg_utils.lua
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,11 @@ function _M.connect()
return nil, err
end

_M.setup_connection(pg, "api-umbrella")
-- Always force session variable setup on every connection, even for reused
-- keepalive sockets. Other libraries (e.g., lua-resty-session's postgres
-- storage) may share the same connection pool and return connections without
-- the expected search_path set.
_M.setup_connection(pg, "api-umbrella", true)

return pg
end
Expand Down
13 changes: 8 additions & 5 deletions src/api-umbrella/web-app/actions/admin/sessions.lua
Original file line number Diff line number Diff line change
Expand Up @@ -139,12 +139,12 @@ end

function _M.destroy(self)
self:init_session_db()
local _, _, open_err = self.session_db:start()
if open_err then
local ok, open_err = self.session_db:open()
if not ok and open_err and open_err ~= "missing session cookie" then
ngx.log(ngx.ERR, "session open error: ", open_err)
end

local sign_in_provider = self.session_db.data["sign_in_provider"]
local sign_in_provider = self.session_db:get("sign_in_provider")
self.session_db:destroy()

flash.session(self, "info", t("Signed out successfully."))
Expand Down Expand Up @@ -173,8 +173,11 @@ 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"]
local ok, open_err = self.session_cookie:open()
if not ok and open_err and open_err ~= "missing session cookie" then
ngx.log(ngx.ERR, "session open error: ", open_err)
end
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"))

Expand Down
83 changes: 37 additions & 46 deletions src/api-umbrella/web-app/app.lua
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,12 @@ local http_headers = require "api-umbrella.utils.http_headers"
local is_empty = require "api-umbrella.utils.is_empty"
local lapis = require "lapis"
local lapis_config = require("lapis.config").get()
local pg_utils = require "api-umbrella.utils.pg_utils"
local refresh_local_active_config_cache = require("api-umbrella.web-app.stores.active_config_store").refresh_local_cache
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
Expand Down Expand Up @@ -77,24 +72,26 @@ 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",
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
storage = "postgres",
postgres = {
host = pg_utils.db_config.host,
port = pg_utils.db_config.port,
database = pg_utils.db_config.database,
username = pg_utils.db_config.user,
password = pg_utils.db_config.password,
ssl = pg_utils.db_config.ssl,
ssl_verify = pg_utils.db_config.ssl_verify,
ssl_required = pg_utils.db_config.ssl_required,
table = "api_umbrella.sessions",
},
secret = assert(config["secret_key"]),
cookie_name = "_api_umbrella_session",
cookie_same_site = "Lax",
cookie_secure = true,
cookie_http_only = true,
idling_timeout = 30 * 60, -- 30 minutes
rolling_timeout = 0, -- disabled, matches v3 renew=-1
absolute_timeout = 12 * 60 * 60, -- 12 hours
}
local function init_session_db(self)
if not self.session_db then
Expand All @@ -112,22 +109,14 @@ 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,
idling_timeout = 0, -- disabled for cookie-only sessions
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
Expand All @@ -138,17 +127,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
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)
local ok, open_err = self.session_db:open()
if not ok then
if open_err and open_err ~= "missing session cookie" then
if open_err == "session idling timeout exceeded" or open_err == "session absolute timeout exceeded" 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
end
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
Expand Down
5 changes: 0 additions & 5 deletions src/api-umbrella/web-app/hooks/init_preload_modules.lua
Original file line number Diff line number Diff line change
Expand Up @@ -182,11 +182,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"
13 changes: 6 additions & 7 deletions src/api-umbrella/web-app/utils/auth_external_oauth2.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand Down Expand Up @@ -119,17 +119,16 @@ function _M.userinfo(self, strategy_name, options)
end

self:init_session_cookie()
local _, _, open_err = self.session_cookie:start()
if open_err then
local ok, open_err = self.session_cookie:open()
if not ok and open_err and open_err ~= "missing session cookie" then
ngx.log(ngx.ERR, "session open error: ", open_err)
end

if not self.session_cookie or not self.session_cookie.data or not self.session_cookie.data["oauth2_state"] then
local stored_state = self.session_cookie:get("oauth2_state")
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")
Expand Down
13 changes: 7 additions & 6 deletions src/api-umbrella/web-app/utils/auth_external_openid_connect.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -83,13 +83,14 @@ function _M.authenticate(self, strategy_name, callback)
if discovery and discovery["end_session_endpoint"] then
-- Generate the state parameter to send.
self:init_session_cookie()
self.session_cookie:start()
self.session_cookie.data["openid_connect_state"] = random_token(64)
self.session_cookie:open()
local oidc_state = random_token(64)
self.session_cookie:set("openid_connect_state", oidc_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 = oidc_state
}

-- Add the "client_id" param to the logout URL if id_token_hint won't be
Expand Down Expand Up @@ -121,7 +122,7 @@ function _M.authenticate(self, strategy_name, callback)
-- Create a separate session for lua-resty-openidc's storage so it doesn't
-- conflict with any of our sessions.
local session_options = deepcopy(self.session_db_options)
session_options["name"] = "_api_umbrella_openidc"
session_options["cookie_name"] = "_api_umbrella_openidc"

-- In the test environment allow mocking the login process.
if config["app_env"] == "test" and ngx.var.cookie_test_mock_userinfo then
Expand Down
16 changes: 8 additions & 8 deletions src/api-umbrella/web-app/utils/csrf.lua
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,18 @@ 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")
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
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()
Expand All @@ -41,12 +41,12 @@ end

local function validate_token(self)
self:init_session_cookie()
local _, _, open_err = self.session_cookie:start()
if open_err then
local ok, open_err = self.session_cookie:open()
if not ok and open_err and open_err ~= "missing session cookie" then
ngx.log(ngx.ERR, "session open error: ", open_err)
end

local key = self.session_cookie.data["csrf_token_key"]
local key = self.session_cookie:get("csrf_token_key")
if not key then
return false, "Missing CSRF token key"
end
Expand Down
Loading
Loading