Skip to content
This repository was archived by the owner on Jun 12, 2021. It is now read-only.

Commit c218e01

Browse files
committed
Added support for SameSite in cookies.
1 parent 93bbd93 commit c218e01

File tree

2 files changed

+132
-49
lines changed

2 files changed

+132
-49
lines changed

src/oidcendpoint/cookie.py

Lines changed: 98 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import json
44
import logging
55
import os
6+
import sys
67
import time
78
from http.cookies import SimpleCookie
89
from urllib.parse import urlparse
@@ -12,12 +13,13 @@
1213
from cryptojwt.jwe.aes import AES_GCMEncrypter
1314
from cryptojwt.jwe.utils import split_ctx_and_tag
1415
from cryptojwt.jwk.hmac import SYMKey
15-
from cryptojwt.jwk.jwk import key_from_jwk_dict
1616
from cryptojwt.jws.hmac import HMACSigner
17-
from cryptojwt.key_bundle import init_key, import_jwk
17+
from cryptojwt.key_bundle import import_jwk
18+
from cryptojwt.key_bundle import init_key
1819
from cryptojwt.utils import as_bytes
1920
from cryptojwt.utils import as_unicode
2021
from cryptojwt.utils import b64e
22+
from oidcmsg import time_util
2123
from oidcmsg.time_util import in_a_while
2224

2325
from oidcendpoint.util import lv_pack
@@ -34,6 +36,21 @@
3436
]
3537

3638

39+
def _expiration(timeout, time_format=None):
40+
"""
41+
Return an expiration time
42+
43+
:param timeout: When
44+
:param time_format: The format of the returned value
45+
:return: A timeout date
46+
"""
47+
if timeout == "now":
48+
return time_util.instant(time_format)
49+
else:
50+
# validity time should match lifetime of assertions
51+
return time_util.in_a_while(minutes=timeout, time_format=time_format)
52+
53+
3754
def sign_enc_payload(load, timestamp=0, sign_key=None, enc_key=None, sign_alg="SHA256"):
3855
"""
3956
@@ -106,7 +123,7 @@ def ver_dec_content(parts, sign_key=None, enc_key=None, sign_alg="SHA256"):
106123
mac = base64.b64decode(b64_mac)
107124
verifier = HMACSigner(algorithm=sign_alg)
108125
if verifier.verify(
109-
load.encode("utf-8") + timestamp.encode("utf-8"), mac, sign_key.key
126+
load.encode("utf-8") + timestamp.encode("utf-8"), mac, sign_key.key
110127
):
111128
return load, timestamp
112129
else:
@@ -125,9 +142,9 @@ def ver_dec_content(parts, sign_key=None, enc_key=None, sign_alg="SHA256"):
125142
if len(p) == 3:
126143
verifier = HMACSigner(algorithm=sign_alg)
127144
if verifier.verify(
128-
load.encode("utf-8") + timestamp.encode("utf-8"),
129-
base64.b64decode(p[2]),
130-
sign_key.key,
145+
load.encode("utf-8") + timestamp.encode("utf-8"),
146+
base64.b64decode(p[2]),
147+
sign_key.key,
131148
):
132149
return load, timestamp
133150
else:
@@ -136,15 +153,19 @@ def ver_dec_content(parts, sign_key=None, enc_key=None, sign_alg="SHA256"):
136153

137154

138155
def make_cookie_content(
139-
name,
140-
load,
141-
sign_key,
142-
domain=None,
143-
path=None,
144-
timestamp="",
145-
enc_key=None,
146-
max_age=0,
147-
sign_alg="SHA256",
156+
name,
157+
load,
158+
sign_key,
159+
domain=None,
160+
path=None,
161+
expire=0,
162+
timestamp="",
163+
enc_key=None,
164+
max_age=0,
165+
sign_alg="SHA256",
166+
secure=True,
167+
http_only=True,
168+
same_site=""
148169
):
149170
"""
150171
Create and return a cookies content
@@ -167,12 +188,21 @@ def make_cookie_content(
167188
:type sign_key: A :py:class:`cryptojwt.jwk.hmac.SYMKey` instance
168189
:param domain: The domain of the cookie
169190
:param path: The path specification for the cookie
191+
:param expire: Number of minutes before this cookie goes stale
192+
:type expire: int
170193
:param timestamp: A time stamp
171194
:type timestamp: text
172195
:param enc_key: The key to use for payload encryption.
173196
:type enc_key: A :py:class:`cryptojwt.jwk.hmac.SYMKey` instance
174197
:param max_age: The time in seconds for when a cookie will be deleted
175198
:type max_age: int
199+
:param secure: A secure cookie is only sent to the server with an encrypted request over the
200+
HTTPS protocol.
201+
:type secure: boolean
202+
:param http_only: HttpOnly cookies are inaccessible to JavaScript's Document.cookie API
203+
:type http_only: boolean
204+
:param same_site: Whether SameSite (None,Strict or Lax) should be added to the cookie
205+
:type same_site: byte string
176206
:return: A SimpleCookie instance
177207
"""
178208
if not timestamp:
@@ -183,45 +213,70 @@ def make_cookie_content(
183213
)
184214

185215
content = {name: {"value": _cookie_value}}
216+
186217
if path is not None:
187218
content[name]["path"] = path
188219
if domain is not None:
189220
content[name]["domain"] = domain
190-
191-
content[name]["httponly"] = True
192-
193221
if max_age:
194222
content[name]["expires"] = in_a_while(seconds=max_age)
223+
if path:
224+
content[name]["path"] = path
225+
if domain:
226+
content[name]["domain"] = domain
227+
if expire:
228+
content[name]["expires"] = _expiration(expire, "%a, %d-%b-%Y %H:%M:%S GMT")
229+
if same_site:
230+
content[name]["SameSite"] = same_site
231+
232+
# these are booleans so just set them.
233+
content[name]["Secure"] = secure
234+
content[name]["httponly"] = http_only
235+
195236

196237
return content
197238

198239

199240
def make_cookie(
200-
name,
201-
payload,
202-
sign_key,
203-
domain=None,
204-
path=None,
205-
timestamp="",
206-
enc_key=None,
207-
max_age=0,
208-
sign_alg="SHA256",
241+
name,
242+
payload,
243+
sign_key,
244+
domain=None,
245+
path=None,
246+
expire=0,
247+
timestamp="",
248+
enc_key=None,
249+
max_age=0,
250+
sign_alg="SHA256",
251+
secure=True,
252+
http_only=True,
253+
same_site=""
254+
209255
):
210256
content = make_cookie_content(
211257
name,
212258
payload,
213259
sign_key,
214260
domain=domain,
215261
path=path,
262+
expire=expire,
216263
timestamp=timestamp,
217264
enc_key=enc_key,
218265
max_age=max_age,
219266
sign_alg=sign_alg,
267+
secure=secure,
268+
http_only=http_only,
269+
same_site=same_site
220270
)
221271

222272
cookie = SimpleCookie()
273+
223274
for name, args in content.items():
224275
cookie[name] = args["value"]
276+
# Necessary if Python version < 3.8
277+
if sys.version_info[:2] <= (3, 8):
278+
cookie[name]._reserved[str("samesite")] = str("SameSite")
279+
225280
for key, value in args.items():
226281
if key == "value":
227282
continue
@@ -273,26 +328,20 @@ def parse_cookie(name, sign_key, kaka, enc_key=None, sign_alg="SHA256"):
273328
return ver_dec_content(parts, sign_key, enc_key, sign_alg)
274329

275330

276-
def import_jwk(filename):
277-
with open(filename) as jwk_file:
278-
jwk_dict = json.loads(jwk_file.read())
279-
return key_from_jwk_dict(jwk_dict)
280-
281-
282331
class CookieDealer(object):
283332
"""
284333
Functionality that an entity that deals with cookies need to have
285334
access to.
286335
"""
287336

288337
def __init__(
289-
self,
290-
sign_key="",
291-
enc_key="",
292-
sign_alg="SHA256",
293-
default_values=None,
294-
sign_jwk=None,
295-
enc_jwk=None,
338+
self,
339+
sign_key="",
340+
enc_key="",
341+
sign_alg="SHA256",
342+
default_values=None,
343+
sign_jwk=None,
344+
enc_jwk=None,
296345
):
297346

298347
if sign_key:
@@ -418,15 +467,15 @@ def get_cookie_value(self, cookie=None, cookie_name=None):
418467
return None
419468

420469
def append_cookie(
421-
self,
422-
cookie,
423-
name,
424-
payload,
425-
typ,
426-
domain=None,
427-
path=None,
428-
timestamp="",
429-
max_age=0,
470+
self,
471+
cookie,
472+
name,
473+
payload,
474+
typ,
475+
domain=None,
476+
path=None,
477+
timestamp="",
478+
max_age=0,
430479
):
431480
"""
432481
Adds a cookie to a SimpleCookie instance

tests/test_09_cookie_dealer.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from oidcendpoint.cookie import compute_session_state
99
from oidcendpoint.cookie import cookie_value
1010
from oidcendpoint.cookie import create_session_cookie
11+
from oidcendpoint.cookie import make_cookie
1112
from oidcendpoint.cookie import new_cookie
1213
from oidcendpoint.endpoint_context import EndpointContext
1314
from oidcendpoint.oidc.token import AccessToken
@@ -252,3 +253,36 @@ def test_new_cookie():
252253
b64val, ts, typ = val
253254
info = cookie_value(b64val)
254255
assert set(info.keys()) == {"client_id", "sid"}
256+
257+
258+
def test_cookie_default():
259+
_key = SYMKey(k="ghsNKDDLshZTPn974nOsIGhedULrsqnsGoBFBLwUKuJhE2ch")
260+
kaka = make_cookie('test', "data", sign_key=_key)
261+
assert kaka['test']["secure"] is True
262+
assert kaka["test"]["httponly"] is True
263+
assert kaka["test"]["samesite"] is ""
264+
265+
266+
def test_cookie_http_only_false():
267+
_key = SYMKey(k="ghsNKDDLshZTPn974nOsIGhedULrsqnsGoBFBLwUKuJhE2ch")
268+
kaka = make_cookie('test', "data", sign_key=_key, http_only=False)
269+
assert kaka['test']["secure"] is True
270+
assert kaka["test"]["httponly"] is False
271+
assert kaka["test"]["samesite"] is ""
272+
273+
274+
def test_cookie_not_secure():
275+
_key = SYMKey(k="ghsNKDDLshZTPn974nOsIGhedULrsqnsGoBFBLwUKuJhE2ch")
276+
kaka = make_cookie('test', "data", _key, secure=False)
277+
assert kaka['test']["secure"] is False
278+
assert kaka["test"]["httponly"] is True
279+
assert kaka["test"]["samesite"] is ""
280+
281+
282+
def test_cookie_same_site_none():
283+
_key = SYMKey(k="ghsNKDDLshZTPn974nOsIGhedULrsqnsGoBFBLwUKuJhE2ch")
284+
kaka = make_cookie('test', "data", sign_key=_key, same_site="None")
285+
assert kaka['test']["secure"] is True
286+
assert kaka["test"]["httponly"] is True
287+
assert kaka["test"]["samesite"] is "None"
288+

0 commit comments

Comments
 (0)