Skip to content

Commit 56005db

Browse files
authored
Merge pull request #5248 from StackStorm/samesite_cookie_support
Add support for setting SameSite and Secure attribute for auth cookie we set
2 parents a80fa2b + f5c0c0f commit 56005db

File tree

11 files changed

+225
-7
lines changed

11 files changed

+225
-7
lines changed

CHANGELOG.rst

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,21 @@ Added
312312

313313
Contributed by @Kami.
314314

315+
* Add new ``api.auth_cookie_secure`` and ``api.auth_cookie_same_site`` config options which
316+
specify values which are set for ``secure`` and ``SameSite`` attribute for the auth cookie
317+
we set when authenticating via token / api key in query parameter value (e.g. via st2web).
318+
319+
For security reasons, ``api.auth_cookie_secure`` defaults to ``True``. This should only be
320+
changed to ``False`` if you have a valid reason to not run StackStorm behind HTTPs proxy.
321+
322+
Default value for ``api.auth_cookie_same_site`` is ``lax``. If you want to disable this
323+
functionality so it behaves the same as in the previous releases, you can set that option
324+
to ``None``.
325+
326+
#5248
327+
328+
Contributed by @Kami.
329+
315330
* Mask secrets in output of an action execution in the API if the action has an output schema
316331
defined and one or more output parameters are marked as secret. #5250
317332

conf/st2.conf.sample

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ workflows_pool_size = 40
3838
[api]
3939
# List of origins allowed for api, auth and stream
4040
allow_origin = http://127.0.0.1:3000 # comma separated list allowed here.
41+
# SameSite attribute value for the auth-token cookie we set on successful authentication from st2web. If you don't have a specific reason (e.g. supporting old browsers) we recommend you set this value to strict. Setting it to "unset" will default to the behavior in previous releases and not set this SameSite header value.
42+
# Valid values: strict, lax, none, unset
43+
auth_cookie_same_site = lax
44+
# True if secure flag should be set for "auth-token" cookie which is set on successful authentication via st2web. You should only set this to False if you have a good reason to not run and access StackStorm behind https proxy.
45+
auth_cookie_secure = True
4146
# None
4247
debug = False
4348
# StackStorm API server host
@@ -142,6 +147,7 @@ ssl = False
142147
# ca_certs file contains a set of concatenated CA certificates, which are used to validate certificates passed from MongoDB.
143148
ssl_ca_certs = None
144149
# Specifies whether a certificate is required from the other side of the connection, and whether it will be validated if provided
150+
# Valid values: none, optional, required
145151
ssl_cert_reqs = None
146152
# Certificate file used to identify the localconnection
147153
ssl_certfile = None
@@ -151,7 +157,7 @@ ssl_keyfile = None
151157
ssl_match_hostname = True
152158
# username for db login
153159
username = None
154-
# Compression level when compressors is set to zlib. Valid calues are -1 to 9. Defaults to 6.
160+
# Compression level when compressors is set to zlib. Valid values are -1 to 9. Defaults to 6.
155161
zlib_compression_level =
156162

157163
[exporter]
@@ -195,7 +201,8 @@ redirect_stderr = False
195201
[messaging]
196202
# URL of all the nodes in a messaging service cluster.
197203
cluster_urls = # comma separated list allowed here.
198-
# Compression algorithm to use for compressing the payloads which are sent over the message bus. Valid values include: zstd, lzma, bz2, gzip. Defaults to no compression.
204+
# Compression algorithm to use for compressing the payloads which are sent over the message bus. Defaults to no compression.
205+
# Valid values: zstd, lzma, bz2, gzip, None
199206
compression = None
200207
# How many times should we retry connection before failing.
201208
connection_retries = 10
@@ -208,6 +215,7 @@ ssl = False
208215
# ca_certs file contains a set of concatenated CA certificates, which are used to validate certificates passed from RabbitMQ.
209216
ssl_ca_certs = None
210217
# Specifies whether a certificate is required from the other side of the connection, and whether it will be validated if provided.
218+
# Valid values: none, optional, required
211219
ssl_cert_reqs = None
212220
# Certificate file used to identify the local connection (client).
213221
ssl_certfile = None

st2api/st2api/app.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
from st2common.constants.system import VERSION_STRING
2929
from st2common.service_setup import setup as common_setup
3030
from st2common.util import spec_loader
31+
from st2api.validation import validate_auth_cookie_is_correctly_configured
3132
from st2api.validation import validate_rbac_is_correctly_configured
3233

3334
LOG = logging.getLogger(__name__)
@@ -66,6 +67,7 @@ def setup_app(config=None):
6667
)
6768

6869
# Additional pre-run time checks
70+
validate_auth_cookie_is_correctly_configured()
6971
validate_rbac_is_correctly_configured()
7072

7173
router = Router(

st2api/st2api/cmd/api.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
config.register_opts(ignore_errors=True)
3838

3939
from st2api import app
40+
from st2api.validation import validate_auth_cookie_is_correctly_configured
4041
from st2api.validation import validate_rbac_is_correctly_configured
4142

4243
__all__ = ["main"]
@@ -68,6 +69,7 @@ def _setup():
6869
)
6970

7071
# Additional pre-run time checks
72+
validate_auth_cookie_is_correctly_configured()
7173
validate_rbac_is_correctly_configured()
7274

7375

st2api/st2api/validation.py

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,58 @@
1515

1616
from oslo_config import cfg
1717

18-
__all__ = ["validate_rbac_is_correctly_configured"]
18+
from webob import cookies
1919

20+
__all__ = [
21+
"validate_auth_cookie_is_correctly_configured",
22+
"validate_rbac_is_correctly_configured",
23+
]
2024

21-
def validate_rbac_is_correctly_configured():
25+
26+
def validate_auth_cookie_is_correctly_configured() -> bool:
27+
"""
28+
Function which verifies that SameCookie config option value is correctly configured.
29+
30+
This method should be called in the api init phase so we catch any misconfiguration issues
31+
before startup.
32+
"""
33+
if cfg.CONF.api.auth_cookie_same_site not in ["strict", "lax", "none", "unset"]:
34+
raise ValueError(
35+
'Got invalid value "%s" (type %s) for cfg.CONF.api.auth_cookie_same_site config '
36+
"option. Valid values are: strict, lax, none, unset."
37+
% (
38+
cfg.CONF.api.auth_cookie_same_site,
39+
type(cfg.CONF.api.auth_cookie_same_site),
40+
)
41+
)
42+
43+
# Now we try to make a dummy cookie to verify all the options are configured correctly. Some
44+
# Options are mutually exclusive - e.g. SameSite none and Secure false.
45+
try:
46+
# NOTE: none and unset don't mean the same thing - unset implies not setting this attribute
47+
# (backward compatibility) and none implies setting this attribute value to none
48+
same_site = cfg.CONF.api.auth_cookie_same_site
49+
50+
kwargs = {}
51+
if same_site != "unset":
52+
kwargs["samesite"] = same_site
53+
54+
cookies.make_cookie(
55+
"test_cookie",
56+
"dummyvalue",
57+
httponly=True,
58+
secure=cfg.CONF.api.auth_cookie_secure,
59+
**kwargs,
60+
)
61+
except Exception as e:
62+
raise ValueError(
63+
"Failed to validate api.auth_cookie config options: %s" % (str(e))
64+
)
65+
66+
return True
67+
68+
69+
def validate_rbac_is_correctly_configured() -> bool:
2270
"""
2371
Function which verifies that RBAC is correctly set up and configured.
2472
"""

st2api/tests/unit/controllers/v1/test_auth.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import bson
2020
import mock
21+
from oslo_config import cfg
2122

2223
from st2tests.api import FunctionalTest
2324
from st2common.util import date as date_utils
@@ -69,6 +70,63 @@ def test_token_validation_token_in_query_params(self):
6970
self.assertIn("application/json", response.headers["content-type"])
7071
self.assertEqual(response.status_int, 200)
7172

73+
@mock.patch.object(
74+
Token,
75+
"get",
76+
mock.Mock(
77+
return_value=TokenDB(id=OBJ_ID, user=USER, token=TOKEN, expiry=FUTURE)
78+
),
79+
)
80+
@mock.patch.object(User, "get_by_name", mock.Mock(return_value=USER_DB))
81+
def test_token_validation_token_in_query_params_auth_cookie_is_set(self):
82+
response = self.app.get(
83+
"/v1/actions?x-auth-token=%s" % (TOKEN), expect_errors=False
84+
)
85+
self.assertIn("application/json", response.headers["content-type"])
86+
self.assertEqual(response.status_int, 200)
87+
self.assertTrue("Set-Cookie" in response.headers)
88+
self.assertTrue("HttpOnly" in response.headers["Set-Cookie"])
89+
90+
# Also test same cookie values + secure
91+
valid_values = ["strict", "lax", "none", "unset"]
92+
93+
for value in valid_values:
94+
cfg.CONF.set_override(
95+
group="api", name="auth_cookie_same_site", override=value
96+
)
97+
cfg.CONF.set_override(group="api", name="auth_cookie_secure", override=True)
98+
99+
response = self.app.get(
100+
"/v1/actions?x-auth-token=%s" % (TOKEN), expect_errors=False
101+
)
102+
self.assertIn("application/json", response.headers["content-type"])
103+
self.assertEqual(response.status_int, 200)
104+
self.assertTrue("Set-Cookie" in response.headers)
105+
self.assertTrue("HttpOnly" in response.headers["Set-Cookie"])
106+
107+
if value == "unset":
108+
self.assertFalse("SameSite" in response.headers["Set-Cookie"])
109+
else:
110+
self.assertTrue(
111+
"SameSite=%s" % (value) in response.headers["Set-Cookie"]
112+
)
113+
114+
self.assertTrue("secure" in response.headers["Set-Cookie"])
115+
116+
# SameSite=Lax, Secure=False
117+
cfg.CONF.set_override(group="api", name="auth_cookie_same_site", override="lax")
118+
cfg.CONF.set_override(group="api", name="auth_cookie_secure", override=False)
119+
120+
response = self.app.get(
121+
"/v1/actions?x-auth-token=%s" % (TOKEN), expect_errors=False
122+
)
123+
self.assertIn("application/json", response.headers["content-type"])
124+
self.assertEqual(response.status_int, 200)
125+
self.assertTrue("Set-Cookie" in response.headers)
126+
self.assertTrue("HttpOnly" in response.headers["Set-Cookie"])
127+
self.assertTrue("SameSite=lax" in response.headers["Set-Cookie"])
128+
self.assertTrue("secure" not in response.headers["Set-Cookie"])
129+
72130
@mock.patch.object(
73131
Token,
74132
"get",

st2api/tests/unit/test_validation_utils.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import unittest2
1717
from oslo_config import cfg
1818

19+
from st2api.validation import validate_auth_cookie_is_correctly_configured
1920
from st2api.validation import validate_rbac_is_correctly_configured
2021
from st2tests import config as tests_config
2122

@@ -27,6 +28,49 @@ def setUp(self):
2728
super(ValidationUtilsTestCase, self).setUp()
2829
tests_config.parse_args()
2930

31+
def test_validate_auth_cookie_is_correctly_configured_success(self):
32+
valid_values = [
33+
"strict",
34+
"lax",
35+
"none",
36+
"unset",
37+
]
38+
39+
cfg.CONF.set_override(group="api", name="auth_cookie_secure", override=True)
40+
41+
for value in valid_values:
42+
cfg.CONF.set_override(
43+
group="api", name="auth_cookie_same_site", override=value
44+
)
45+
self.assertTrue(validate_auth_cookie_is_correctly_configured())
46+
47+
def test_validate_auth_cookie_is_correctly_configured_error(self):
48+
invalid_values = ["strictx", "laxx", "nonex", "invalid"]
49+
50+
for value in invalid_values:
51+
cfg.CONF.set_override(
52+
group="api", name="auth_cookie_same_site", override=value
53+
)
54+
55+
expected_msg = "Valid values are: strict, lax, none, unset"
56+
self.assertRaisesRegexp(
57+
ValueError, expected_msg, validate_auth_cookie_is_correctly_configured
58+
)
59+
60+
# SameSite=none + Secure=false is not compatible
61+
cfg.CONF.set_override(
62+
group="api", name="auth_cookie_same_site", override="none"
63+
)
64+
cfg.CONF.set_override(group="api", name="auth_cookie_secure", override=False)
65+
66+
expected_msg = (
67+
r"Failed to validate api.auth_cookie config options: Incompatible cookie attributes: "
68+
"when the samesite equals 'none', then the secure must be True"
69+
)
70+
self.assertRaisesRegexp(
71+
ValueError, expected_msg, validate_auth_cookie_is_correctly_configured
72+
)
73+
3074
def test_validate_rbac_is_correctly_configured_succcess(self):
3175
result = validate_rbac_is_correctly_configured()
3276
self.assertTrue(result)

st2common/st2common/config.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,7 @@ def register_opts(ignore_errors=False):
254254
cfg.IntOpt(
255255
"zlib_compression_level",
256256
default="",
257-
help="Compression level when compressors is set to zlib. Valid calues are -1 to 9. "
257+
help="Compression level when compressors is set to zlib. Valid values are -1 to 9. "
258258
"Defaults to 6.",
259259
),
260260
]
@@ -321,9 +321,9 @@ def register_opts(ignore_errors=False):
321321
cfg.StrOpt(
322322
"compression",
323323
default=None,
324+
choices=["zstd", "lzma", "bz2", "gzip", None],
324325
help="Compression algorithm to use for compressing the payloads which are sent over "
325-
"the message bus. Valid values include: zstd, lzma, bz2, gzip. Defaults to no "
326-
"compression.",
326+
"the message bus. Defaults to no compression.",
327327
),
328328
]
329329

@@ -373,6 +373,23 @@ def register_opts(ignore_errors=False):
373373
default=True,
374374
help="True to mask secrets in the API responses",
375375
),
376+
cfg.BoolOpt(
377+
"auth_cookie_secure",
378+
default=True,
379+
help='True if secure flag should be set for "auth-token" cookie which is set on '
380+
"successful authentication via st2web. You should only set this to False if you have "
381+
"a good reason to not run and access StackStorm behind https proxy.",
382+
),
383+
cfg.StrOpt(
384+
"auth_cookie_same_site",
385+
default="lax",
386+
choices=["strict", "lax", "none", "unset"],
387+
help="SameSite attribute value for the "
388+
"auth-token cookie we set on successful authentication from st2web. If you "
389+
"don't have a specific reason (e.g. supporting old browsers) we recommend you "
390+
'set this value to strict. Setting it to "unset" will default to the behavior '
391+
"in previous releases and not set this SameSite header value.",
392+
),
376393
]
377394

378395
do_register_opts(api_opts, "api", ignore_errors)

st2common/st2common/router.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,11 +383,22 @@ def __call__(self, req):
383383
max_age = (
384384
auth_resp.expiry - date_utils.get_datetime_utc_now()
385385
)
386+
# NOTE: unset and none don't mean the same thing - unset implies
387+
# not setting this attribute at all (backward compatibility) and
388+
# none implies setting this attribute value to none
389+
same_site = cfg.CONF.api.auth_cookie_same_site
390+
391+
kwargs = {}
392+
if same_site != "unset":
393+
kwargs["samesite"] = same_site
394+
386395
cookie_token = cookies.make_cookie(
387396
definition["x-set-cookie"],
388397
token,
389398
max_age=max_age,
390399
httponly=True,
400+
secure=cfg.CONF.api.auth_cookie_secure,
401+
**kwargs,
391402
)
392403

393404
break

st2tests/st2tests/config.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,11 @@ def _override_api_opts():
111111
override=["http://127.0.0.1:3000", "http://dev"],
112112
group="api",
113113
)
114+
CONF.set_override(
115+
name="auth_cookie_secure",
116+
override=False,
117+
group="api",
118+
)
114119

115120

116121
def _override_keyvalue_opts():

0 commit comments

Comments
 (0)