pycrossauth 0.0.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. crossauth_backend/__init__.py +59 -0
  2. crossauth_backend/auth.py +132 -0
  3. crossauth_backend/common/__init__.py +2 -0
  4. crossauth_backend/common/error.py +498 -0
  5. crossauth_backend/common/interfaces.py +287 -0
  6. crossauth_backend/common/jwt.py +41 -0
  7. crossauth_backend/common/logger.py +202 -0
  8. crossauth_backend/cookieauth.py +575 -0
  9. crossauth_backend/crypto.py +457 -0
  10. crossauth_backend/oauth/__init__.py +2 -0
  11. crossauth_backend/oauth/client.py +1063 -0
  12. crossauth_backend/oauth/clientmanager.py +157 -0
  13. crossauth_backend/oauth/resserver.py +34 -0
  14. crossauth_backend/oauth/tokenconsumer.py +412 -0
  15. crossauth_backend/oauth/wellknown.py +108 -0
  16. crossauth_backend/session.py +450 -0
  17. crossauth_backend/storage.py +626 -0
  18. crossauth_backend/storageimpl/__init__.py +2 -0
  19. crossauth_backend/storageimpl/inmemorystorage.py +132 -0
  20. crossauth_backend/storageimpl/sqlalchemystorage.py +294 -0
  21. crossauth_backend/utils.py +150 -0
  22. crossauth_fastapi/__init__.py +23 -0
  23. crossauth_fastapi/fastapioauthclient.py +2185 -0
  24. crossauth_fastapi/fastapiresserver.py +300 -0
  25. crossauth_fastapi/fastapiserver.py +437 -0
  26. crossauth_fastapi/fastapiserverbase.py +62 -0
  27. crossauth_fastapi/fastapisession.py +654 -0
  28. crossauth_fastapi/fastapisessionadapter.py +94 -0
  29. pycrossauth-0.0.2.dist-info/LICENSE +203 -0
  30. pycrossauth-0.0.2.dist-info/METADATA +257 -0
  31. pycrossauth-0.0.2.dist-info/NOTICE +5 -0
  32. pycrossauth-0.0.2.dist-info/RECORD +34 -0
  33. pycrossauth-0.0.2.dist-info/WHEEL +5 -0
  34. pycrossauth-0.0.2.dist-info/top_level.txt +2 -0
@@ -0,0 +1,575 @@
1
+ from crossauth_backend.crypto import Crypto
2
+ from crossauth_backend.common.error import CrossauthError, ErrorCode
3
+ from crossauth_backend.common.logger import CrossauthLogger, j
4
+ from crossauth_backend.common.interfaces import Key, PartialKey, KeyPrefix
5
+ from crossauth_backend.storage import KeyStorage, UserStorage, UserAndSecrets
6
+ from crossauth_backend.utils import set_parameter, ParamType
7
+ from crossauth_backend.storage import UserStorage, UserStorageGetOptions
8
+ from typing import Mapping, Any, TypedDict, Literal, NotRequired, Optional, Callable, NamedTuple
9
+ from datetime import datetime, timedelta
10
+ from nulltype import NullType, Null
11
+ CSRF_LENGTH = 16
12
+ SESSIONID_LENGTH = 16
13
+
14
+ class CookieOptions(TypedDict, total=False):
15
+ """
16
+ Optional parameters when setting cookies,
17
+
18
+ These match the HTTP cookie parameters of the same name.
19
+ """
20
+
21
+ domain : str
22
+ expires : datetime
23
+ maxAge : int
24
+ httpOnly : bool
25
+ path : str
26
+ secure : bool
27
+ sameSite : bool | Literal["lax", "strict", "none"]
28
+
29
+ def to_cookie_serialize_options(options: CookieOptions) -> Mapping[str, Any]:
30
+ return {
31
+ **vars(options),
32
+ 'path': options["path"] if "path" in options else "/"
33
+ }
34
+
35
+ class Cookie(TypedDict, total=True):
36
+ """ Object encapsulating a cookie name, value and options. """
37
+
38
+ name : str
39
+ value : str
40
+ options : CookieOptions
41
+
42
+ class DoubleSubmitCsrfTokenOptions(CookieOptions):
43
+ """ Options for double-submit csrf tokens """
44
+
45
+ cookie_name : NotRequired[str]
46
+ header_name : NotRequired[str]
47
+ secret: NotRequired[str]
48
+
49
+ class DoubleSubmitCsrfToken:
50
+ """
51
+ Class for creating and validating CSRF tokens according to the double-submit cookie pattern.
52
+
53
+ CSRF token is send as a cookie plus either a header or a hidden form field.
54
+ """
55
+
56
+ @property
57
+ def header_name(self):
58
+ return self._header_name
59
+
60
+ @property
61
+ def cookie_name(self):
62
+ return self._cookie_name
63
+
64
+ @property
65
+ def domain(self):
66
+ return self._domain
67
+
68
+ @property
69
+ def httpOnly(self):
70
+ return self._httpOnly
71
+
72
+ @property
73
+ def path(self):
74
+ return self._path
75
+
76
+ @property
77
+ def secure(self):
78
+ return self._secure
79
+
80
+ @property
81
+ def sameSite(self):
82
+ return self._sameSite
83
+
84
+ def __init__(self, options: DoubleSubmitCsrfTokenOptions = DoubleSubmitCsrfTokenOptions()):
85
+ """
86
+ Constructor
87
+
88
+ :param DoubleSubmitCsrfTokenOptions options: See :class:`DoubleSubmitCsrfTokenOptions`
89
+
90
+ """
91
+
92
+ self._header_name = "X-CROSSAUTH-CSRF"
93
+ self._cookie_name = options["cookie_name"] if "cookie_name" in options else "CSRFTOKEN"
94
+ self._domain = options["domain"] if "domain" in options else None
95
+ self._httpOnly = options["httpOnly"] if "httpOnly" in options else False
96
+ self._path = options["path"] if "path" in options else "/"
97
+ self._secure = options["secure"] if "secure" in options else True
98
+ self._sameSite = options["sameSite"] if "sameSite" in options else "lax"
99
+
100
+ self.__secret = ""
101
+
102
+ # header options
103
+ set_parameter("header_name", ParamType.String, self, options, "CSRF_HEADER_NAME", protected=True)
104
+
105
+ # cookie options
106
+ set_parameter("cookie_name", ParamType.String, self, options, "CSRF_COOKIE_NAME", protected=True)
107
+ set_parameter("domain", ParamType.String, self, options, "CSRF_COOKIE_DOMAIN", protected=True)
108
+ set_parameter("httpOnly", ParamType.Boolean, self, options, "CSRF_COOKIE_HTTPONLY", protected=True)
109
+ set_parameter("path", ParamType.String, self, options, "CSRF_COOKIE_PATH", protected=True)
110
+ set_parameter("secure", ParamType.Boolean, self, options, "CSRF_COOKIE_SECURE", protected=True)
111
+ set_parameter("sameSite", ParamType.String, self, options, "CSRF_COOKIE_SAMESITE", protected=True)
112
+
113
+ # hasher options
114
+ set_parameter("secret", ParamType.String, self, options, "SECRET", True)
115
+
116
+ def create_csrf_token(self) -> str:
117
+ """
118
+ Creates a session key and saves in storage
119
+
120
+ Date created is the current date/time on the server.
121
+
122
+ :return: a random CSRF token.
123
+ """
124
+ return Crypto.random_value(CSRF_LENGTH)
125
+
126
+ def make_csrf_cookie(self, token: str) -> Cookie:
127
+ """
128
+ Returns a :class:`Cookie` object with the given session key.
129
+
130
+ :param str token: the value of the csrf token, with signature
131
+
132
+ :return a :class:`Cookie` object,
133
+ """
134
+ cookie_value = Crypto.sign_secure_token(token, self.__secret)
135
+ options : CookieOptions = {
136
+ "path": self.path,
137
+ "secure": self.secure,
138
+ "httpOnly": self.httpOnly}
139
+ if (self.domain is not None): options["domain"] = self.domain
140
+ options["sameSite"] = self.sameSite
141
+
142
+ return Cookie(name=self.cookie_name, value=cookie_value, options=options)
143
+
144
+ def make_csrf_form_or_header_token(self, token: str) -> str:
145
+ return self.mask_csrf_token(token)
146
+
147
+ def unsign_cookie(self, cookie_value: str) -> str:
148
+ return Crypto.unsign_secure_token(cookie_value, self.__secret)
149
+
150
+ def make_csrf_cookie_string(self, cookie_value: str) -> str:
151
+ """
152
+ Takes a session ID and creates a string representation of the cookie (value of the HTTP `Cookie` header).
153
+
154
+ :param str cookie_value the value to put in the cookie
155
+
156
+ :return: a string representation of the cookie and options.
157
+ """
158
+
159
+ cookie = f"{self.cookie_name}={cookie_value}; SameSite={self.sameSite}"
160
+ if self.domain:
161
+ cookie += f"; {self.domain}"
162
+ if self.path:
163
+ cookie += f"; {self.path}"
164
+ if self.httpOnly:
165
+ cookie += "; httpOnly"
166
+ if self.secure:
167
+ cookie += "; secure"
168
+ return cookie
169
+
170
+ def mask_csrf_token(self, token: str) -> str:
171
+ mask = Crypto.random_value(CSRF_LENGTH)
172
+ masked_token = Crypto.xor(token, mask)
173
+ return f"{mask}.{masked_token}"
174
+
175
+ def unmask_csrf_token(self, mask_and_token: str) -> str:
176
+ parts = mask_and_token.split(".")
177
+ if len(parts) != 2:
178
+ raise CrossauthError(ErrorCode.InvalidCsrf, "CSRF token in header or form not in correct format")
179
+ mask = parts[0]
180
+ masked_token = parts[1]
181
+ return Crypto.xor(masked_token, mask)
182
+
183
+ def validate_double_submit_csrf_token(self, cookie_value: str, form_or_header_name: str) -> None:
184
+ """
185
+ Validates the passed CSRF token.
186
+
187
+ To be valid:
188
+ - The signature in the cookie must match the token in the cookie
189
+ - The token in the cookie must matched the value in the form or header after unmasking
190
+
191
+ :param str cookie_value: the CSRF cookie value to validate.
192
+ :param str form_or_header_name the value from the csrf_token form header or the X-CROSSAUTH-CSRF header.
193
+
194
+ :raises :class:`crossauth_backend.CrossauthError` with :class:`ErrorCode` of `InvalidKey`
195
+ """
196
+ form_or_header_token = self.unmask_csrf_token(form_or_header_name)
197
+ try:
198
+ cookie_token = Crypto.unsign_secure_token(cookie_value, self.__secret)
199
+ except Exception as e:
200
+ CrossauthLogger.logger().error(j({"err": str(e)}))
201
+ raise CrossauthError(ErrorCode.InvalidCsrf, "Invalid CSRF cookie")
202
+
203
+ if cookie_token != form_or_header_token:
204
+ CrossauthLogger.logger().warn(j({"msg": "Invalid CSRF token received - form/header value does not match",
205
+ "csrfCookieHash": Crypto.hash(cookie_value)}))
206
+ raise CrossauthError(ErrorCode.InvalidCsrf)
207
+
208
+ def validate_csrf_cookie(self, cookie_value: str) -> str:
209
+ """
210
+ Validates the passed CSRF cookie (doesn't check it matches the token, just that the cookie is valid).
211
+
212
+ To be valid:
213
+ - The signature in the cookie must match the token in the cookie
214
+ - The token in the cookie must matched the value in the form or header after unmasking
215
+
216
+ :param str cookie_value: the CSRF cookie value to validate.
217
+
218
+ :raises :class:`crossauth_backend.CrossauthError` with :class:`ErrorCode` of `InvalidKey`
219
+ """
220
+ try:
221
+ return Crypto.unsign_secure_token(cookie_value, self.__secret)
222
+ except Exception as e:
223
+ CrossauthLogger.logger().error(j({"err": str(e)}))
224
+ raise CrossauthError(ErrorCode.InvalidCsrf, "Invalid CSRF cookie")
225
+
226
+ class UserAndKey(NamedTuple):
227
+ user: UserAndSecrets|None
228
+ key: Key
229
+
230
+ class SessionCookieOptions(CookieOptions, total=False): # Also inherit from TokenEmailerOptions
231
+ """
232
+ Options for double-submit csrf tokens
233
+ """
234
+
235
+ user_storage: UserStorage
236
+ """
237
+ If user login is enabled, you must provide the user storage class
238
+ """
239
+
240
+ cookie_name: str
241
+ """Name of cookie. Defaults to "CSRFTOKEN" """
242
+
243
+ hash_session_id: bool
244
+ """If true, session IDs are stored in hashed form in the key storage. Default False."""
245
+
246
+ idle_timeout: int
247
+ """
248
+ If non zero, sessions will time out after self number of seconds have elapsed without activity.
249
+ Default 0 (no timeout)
250
+ """
251
+
252
+ persist: bool
253
+ """If true, sessions cookies will be persisted between browser sessions. Default True"""
254
+
255
+ secret: str
256
+ """App secret"""
257
+
258
+ filter_function: Callable[[Key], bool]
259
+ """
260
+ self will be called with the session key to filter sessions
261
+ before returning. Function should return true if the session is valid or false otherwise.
262
+ """
263
+
264
+ class CookieReturn(NamedTuple):
265
+ userid: str|int|None
266
+ value: str
267
+ created: datetime
268
+ expires: datetime | None
269
+
270
+ class SessionCookie:
271
+ """
272
+ Class for session management using a session id cookie.
273
+ """
274
+
275
+ @property
276
+ def idle_timeout(self):
277
+ return self._idle_timeout
278
+
279
+ @property
280
+ def cookie_name(self):
281
+ return self._cookie_name
282
+
283
+ @property
284
+ def maxAge(self):
285
+ return self._maxAge
286
+
287
+ @property
288
+ def domain(self):
289
+ return self._domain
290
+
291
+ @property
292
+ def httpOnly(self):
293
+ return self._httpOnly
294
+
295
+ @property
296
+ def path(self):
297
+ return self._path
298
+
299
+ @property
300
+ def secure(self):
301
+ return self._secure
302
+
303
+ @property
304
+ def sameSite(self):
305
+ return self._sameSite
306
+
307
+ def __init__(self, key_storage : KeyStorage, options: SessionCookieOptions = {}):
308
+ """
309
+ Constructor
310
+
311
+ :param crossauth_backend.KeyStorage key_storage: where to store session keys
312
+
313
+ """
314
+ self.__persist : bool = True
315
+ self._idle_timeout : int = 0
316
+ self.__filter_function : Callable[[Key], bool] | None = None
317
+
318
+ ## cookie settings
319
+ self._cookie_name : str = "SESSIONID"
320
+ self._maxAge : int = 60*60*24*4; # 4 weeks
321
+ self._domain : str | None = None
322
+ self._httpOnly : bool = False
323
+ self._path : str = "/"
324
+ self._secure : bool = True
325
+ self._sameSite : bool | Literal["lax", "strict", "none"] | None = "lax"
326
+
327
+ ## hasher settings
328
+ self.__secret : str = ""
329
+
330
+ self.__user_storage = options["user_storage"] if "user_storage" in options else None
331
+ self.key_storage = key_storage
332
+ set_parameter("idle_timeout", ParamType.Number, self, options, "SESSION_IDLE_TIMEOUT", protected=True)
333
+ set_parameter("persist", ParamType.Number, self, options, "PERSIST_SESSION_ID")
334
+ self.filter_function = options['filterFunction'] if 'filterFunction' in options else None
335
+
336
+ # cookie settings
337
+ set_parameter("cookie_name", ParamType.String, self, options, "SESSION_COOKIE_NAME", protected=True)
338
+ set_parameter("maxAge", ParamType.String, self, options, "SESSION_COOKIE_maxAge", protected=True)
339
+ set_parameter("domain", ParamType.String, self, options, "SESSION_COOKIE_DOMAIN", protected=True)
340
+ set_parameter("httpOnly", ParamType.Boolean, self, options, "SESSIONCOOKIE_HTTPONLY", protected=True)
341
+ set_parameter("path", ParamType.String, self, options, "SESSION_COOKIE_PATH", protected=True)
342
+ set_parameter("secure", ParamType.Boolean, self, options, "SESSION_COOKIE_SECURE", protected=True)
343
+ set_parameter("sameSite", ParamType.String, self, options, "SESSION_COOKIE_SAMESITE", protected=True)
344
+
345
+ # hasher settings
346
+ self.__secret = options["secret"] if "secret" in options else ""
347
+
348
+ def _expiry(self, date_created: datetime) -> datetime | None:
349
+ expires = None
350
+ if self.maxAge > 0:
351
+ expires = date_created + timedelta(0, self.maxAge)
352
+ return expires
353
+
354
+ @staticmethod
355
+ def hash_session_id(session_id: str) -> str:
356
+ """
357
+ Returns a hash of a session ID, with the session ID prefix for storing
358
+ in the storage table.
359
+ :param str session_id the session ID to hash
360
+
361
+ :return: a base64-url-encoded string that can go into the storage
362
+ """
363
+ return KeyPrefix.session + Crypto.hash(session_id)
364
+
365
+ async def create_session_key(self, userid: str | int | None, extra_fields: Mapping[str, Any] = {}) -> Key:
366
+ """
367
+ Creates a session key and saves in storage
368
+
369
+ Date created is the current date/time on the server.
370
+
371
+ In the unlikely event of the key already existing, it is retried up to 10 times before throwing
372
+ an error with ErrorCode.KeyExists
373
+
374
+ :param str | int | None userid: the user ID to store with the session key.
375
+ :param Dict[str, Any]|None extra_fields: Any fields in here will also be added to the session
376
+ record
377
+
378
+ :return: the new session key
379
+
380
+ :raises :class:`crossauth_backend.CrossauthError`: with
381
+ :class:`ErrorCode` `KeyExists` if maximum
382
+ attempts exceeded trying to create a unique session id
383
+ """
384
+ max_tries = 10
385
+ num_tries = 0
386
+ session_id = Crypto.random_value(SESSIONID_LENGTH)
387
+ date_created = datetime.now()
388
+ expires = self._expiry(date_created)
389
+ succeeded = False
390
+
391
+ extra_fields_copy = {**extra_fields}
392
+ while num_tries < max_tries and not succeeded:
393
+ hashed_session_id = self.hash_session_id(session_id)
394
+ try:
395
+ if self.idle_timeout > 0 and userid:
396
+ extra_fields_copy['lastActivity'] = datetime.now()
397
+ data : str|None = None
398
+ if ("data" in extra_fields):
399
+ data = extra_fields["data"]
400
+ extra_fields = {**extra_fields}
401
+ del extra_fields["data"]
402
+ await self.key_storage.save_key(userid, hashed_session_id, date_created, expires, data, extra_fields)
403
+ succeeded = True
404
+ except Exception as e:
405
+ ce = CrossauthError.as_crossauth_error(e)
406
+ if ce.code in (ErrorCode.KeyExists, ErrorCode.InvalidKey):
407
+ num_tries += 1
408
+ session_id = Crypto.random_value(SESSIONID_LENGTH)
409
+ if num_tries > max_tries:
410
+ CrossauthLogger.logger().error({"msg": "Max attempts exceeded trying to create session ID"})
411
+ raise CrossauthError(ErrorCode.KeyExists)
412
+ else:
413
+ CrossauthLogger.logger().debug(j({"err": ce}))
414
+ raise e
415
+
416
+ key : Key = {
417
+ "value": session_id,
418
+ "created": date_created,
419
+ "expires": expires or Null,
420
+ }
421
+ if (userid is not None): key["userid"] = userid
422
+ return key
423
+
424
+ def make_cookie(self, session_key: Key, persist: Optional[bool] = None) -> Cookie:
425
+ """
426
+ Returns a :class:`Cookie` object with the given session key.
427
+
428
+ This class is compatible, for example, with Express.
429
+
430
+ :param crossauth_backend.Key session_key: the value of the session key
431
+ :param bool|None persist: if passed, overrides the persistSessionId setting
432
+
433
+ :return: a :class:`Cookie` object,
434
+
435
+ """
436
+ signed_value = Crypto.sign_secure_token( session_key['value'], self.__secret)
437
+ options : CookieOptions = {}
438
+ if persist is None:
439
+ persist = self.__persist
440
+ if self.domain:
441
+ options['domain'] = self.domain
442
+ if 'expires' in session_key and type(session_key['expires']) != NullType and persist:
443
+ options['expires'] = session_key['expires'] # type: ignore
444
+ if self.path:
445
+ options['path'] = self.path
446
+ options['sameSite'] = self.sameSite
447
+ if self.httpOnly:
448
+ options['httpOnly'] = self.httpOnly
449
+ if self.secure:
450
+ options['secure'] = self.secure
451
+ return {
452
+ 'name': self.cookie_name,
453
+ 'value': signed_value,
454
+ 'options': options
455
+ }
456
+
457
+ def make_cookie_string(self, cookie: Cookie) -> str:
458
+ """
459
+ Takes a session ID and creates a string representation of the cookie
460
+ (value of the HTTP `Cookie` header).
461
+
462
+ :param Cookie cookie: the cookie vlaues to make a string from
463
+
464
+ :return: a string representation of the cookie and options.
465
+ """
466
+ cookie_string = f"{cookie['name']}={cookie['value']}"
467
+ if self.sameSite:
468
+ cookie_string += f"; SameSite={self.sameSite}"
469
+ if 'expires' in cookie['options']:
470
+ expires = cookie['options']['expires'].strftime('%a, %d %b %Y %H:%M:%S %Z')
471
+ cookie_string += f"; expires={expires}"
472
+ if self.domain:
473
+ cookie_string += f"; domain={self.domain}"
474
+ if self.path:
475
+ cookie_string += f"; path={self.path}"
476
+ if self.httpOnly:
477
+ cookie_string += "; httpOnly"
478
+ if self.secure:
479
+ cookie_string += "; secure"
480
+ return cookie_string
481
+
482
+ async def update_session_key(self, session_key: PartialKey) -> None:
483
+ """
484
+ Updates a session record in storage
485
+ :param crossauth_backend.PartialKey session_key: the fields to update. `value` must be set, and
486
+ will not be updated. All other defined fields will be updated.
487
+
488
+ :raises :class:`crossauth_backend.CrossauthError`: if the session does
489
+ not exist.
490
+ """
491
+ if 'value' not in session_key:
492
+ raise CrossauthError(ErrorCode.InvalidKey, "No session when updating activity")
493
+ session_key['value'] = self.hash_session_id(session_key['value'])
494
+ await self.key_storage.update_key(session_key)
495
+
496
+ def unsign_cookie(self, cookie_value: str) -> str:
497
+ """
498
+ Unsigns a cookie and returns the original value.
499
+ :param str cookie_value: the signed cookie value
500
+
501
+ :return: the unsigned value
502
+
503
+ :raises :class:`crossauth_backend.CrossauthError`: if the signature
504
+ is invalid.
505
+ """
506
+ return Crypto.unsign_secure_token(cookie_value, self.__secret)
507
+
508
+ async def get_user_for_session_id(self, session_id: str, options: UserStorageGetOptions = {}) -> UserAndKey:
509
+ """
510
+ Returns the user matching the given session key in session storage, or throws an exception.
511
+
512
+ Looks the user up in the :class:`crossauth_backend.UserStorage` instance passed to the constructor.
513
+
514
+ Undefined will also fail is CookieAuthOptions.filterFunction is defined and returns false,
515
+
516
+ :param str session_id: the value in the session cookie
517
+ :param crossauth_backend.UserStorageGetOptions options: See :class:`crossauth_backend.UserStorageGetOptions`
518
+
519
+ :return: a :class:`crossauth_backend.User` object, with the password hash removed, and the:class:`crossauth_backend.Key` with the unhashed
520
+ session_id
521
+
522
+ :raises :class:`crossauth_backend.CrossauthError`: with :class:`ErrorCode` set to `InvalidSessionId` or `Expired`.
523
+ """
524
+ key = await self.get_session_key(session_id)
525
+ if not self.__user_storage:
526
+ return UserAndKey(None, key)
527
+ if 'userid' in key and type(key['userid']) is not NullType:
528
+ user = await self.__user_storage.get_user_by_id(key['userid'], options) # type: ignore
529
+ return UserAndKey(user, key)
530
+ else:
531
+ return UserAndKey(None, key)
532
+
533
+ async def get_session_key(self, session_id: str) -> Key:
534
+ """
535
+ Returns the user matching the given session key in session storage, or throws an exception.
536
+
537
+ Looks the user up in the :class:`UserStorage` instance passed to the constructor.
538
+
539
+ Undefined will also fail is CookieAuthOptions.filterFunction is defined and returns false,
540
+
541
+ :param str session_id: the unsigned value of the session cookie
542
+
543
+ :return: a :class:`crossauth_backend.User` object, with the password hash removed.
544
+
545
+ :raises :class:`crossauth_backend.CrossauthError`: with
546
+ :class:`ErrorCode` set to `InvalidSessionId`,
547
+ `Expired` or `UserNotExist`.
548
+ """
549
+ now = datetime.now()
550
+ hashed_session_id = self.hash_session_id(session_id)
551
+ key = await self.key_storage.get_key(hashed_session_id)
552
+ key['value'] = session_id # storage only has hashed version
553
+ if 'expires' in key:
554
+ expires = key['expires']
555
+ if type(expires) is not NullType and now > expires: # type: ignore
556
+ CrossauthLogger.logger().warn(j({"msg": "Session id in cookie expired in key storage", "hashedSessionCookie": Crypto.hash(session_id)}))
557
+ raise CrossauthError(ErrorCode.Expired)
558
+ if key.get('userid') and self.idle_timeout > 0 and 'lastactive' in key and now > key['lastactive'] + timedelta(0, self.idle_timeout):
559
+ CrossauthLogger.logger().warn(j({"msg": "Session cookie with expired idle time received", "hashedSessionCookie": Crypto.hash(session_id)}))
560
+ raise CrossauthError(ErrorCode.Expired)
561
+ if self.filter_function and not self.filter_function(key):
562
+ CrossauthLogger.logger().warn(j({"msg": "Filter function on session id in cookie failed", "hashedSessionCookie": Crypto.hash(session_id)}))
563
+ raise CrossauthError(ErrorCode.InvalidKey)
564
+ return key
565
+
566
+ async def delete_all_for_user(self, userid: str | int, except_key: str|None = None) -> None:
567
+ """
568
+ Deletes all keys for the given user
569
+ :param str|int userid: the user to delete keys for
570
+ :param str|None except_key: if defined, don't delete this key
571
+
572
+ """
573
+ if except_key:
574
+ except_key = self.hash_session_id(except_key)
575
+ await self.key_storage.delete_all_for_user(userid, KeyPrefix.session, except_key)