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,59 @@
1
+ # Copyright (c) 2024 Matthew Baker. All rights reserved. Licenced under the Apache Licence 2.0. See LICENSE file
2
+ from .common.error import ErrorCode, CrossauthError
3
+ from .common.logger import CrossauthLogger, j
4
+ from .common.jwt import JWT
5
+
6
+ from .common.interfaces import Key, PartialKey, \
7
+ UserInputFields, User, \
8
+ UserSecretsInputFields, UserSecrets, UserState, KeyPrefix, ApiKey, \
9
+ PartialUserInputFields, PartialUser, PartialUserSecrets
10
+
11
+ from .oauth.wellknown import TokenEndpointAuthMethod, ResponseMode, \
12
+ GrantType, SubjectType, ClaimType, \
13
+ OpenIdConfiguration, Jwks, DEFAULT_OIDCCONFIG, \
14
+ AuthorizeQueryType, TokenBodyType
15
+
16
+ from .oauth.tokenconsumer import EncryptionKey, \
17
+ OAuthTokenConsumerOptions, OAuthTokenConsumer
18
+
19
+ from .utils import set_parameter, ParamType, MapGetter
20
+
21
+ from .crypto import Crypto
22
+
23
+ from .storage import UserStorageGetOptions, UserStorageOptions, UserStorage, \
24
+ KeyStorage, KeyDataEntry, \
25
+ OAuthClientStorageOptions, OAuthClientStorage, \
26
+ OAuthAuthorizationStorageOptions, OAuthAuthorizationStorage, UserAndSecrets
27
+
28
+ from .storageimpl.inmemorystorage import InMemoryKeyStorage
29
+ from .storageimpl.sqlalchemystorage import SqlAlchemyKeyStorage, SqlAlchemyKeyStorageOptions
30
+
31
+ from .cookieauth import DoubleSubmitCsrfToken, DoubleSubmitCsrfTokenOptions, SessionCookie, SessionCookieOptions
32
+ from .session import SessionManager, SessionManagerOptions
33
+
34
+ # Version of realpython-reader package
35
+ __version__ = "0.0.2"
36
+
37
+ __all__ = (
38
+ "ErrorCode", "CrossauthError",
39
+ "CrossauthLogger", "j",
40
+ "JWT",
41
+ "Key", "PartialKey", "KeyDataEntry", \
42
+ "UserInputFields", "User", "UserSecretsInputFields", "UserSecrets", "UserState", "KeyPrefix", "ApiKey",
43
+ "PartialUserInputFields", "PartialUser", "PartialUserSecrets",
44
+ "TokenEndpointAuthMethod", "ResponseMode", "GrantType", "SubjectType", "ClaimType",
45
+ "OpenIdConfiguration", "Jwks", "DEFAULT_OIDCCONFIG", "AuthorizeQueryType", "TokenBodyType",
46
+ "EncryptionKey", "OAuthTokenConsumerOptions", "OAuthTokenConsumer",
47
+ "set_parameter", "ParamType", "MapGetter",
48
+ "Crypto",
49
+ "UserStorageGetOptions", "UserStorageOptions", "UserStorage",
50
+ "KeyStorage",
51
+ "OAuthClientStorageOptions", "OAuthClientStorage",
52
+ "OAuthAuthorizationStorageOptions", "OAuthAuthorizationStorage",
53
+ "InMemoryKeyStorage",
54
+ "SqlAlchemyKeyStorage", "SqlAlchemyKeyStorageOptions",
55
+ "DoubleSubmitCsrfToken", "DoubleSubmitCsrfTokenOptions",
56
+ "SessionCookie", "SessionCookieOptions",
57
+ "SessionManager", "SessionManagerOptions",
58
+ "UserAndSecrets"
59
+ )
@@ -0,0 +1,132 @@
1
+ # Copyright (c) 2024 Matthew Baker. All rights reserved. Licenced under the Apache Licence 2.0. See LICENSE file
2
+ from abc import ABC, abstractmethod
3
+ from typing import Dict, List, Optional, TypedDict, Any
4
+ from crossauth_backend.common.error import CrossauthError, ErrorCode
5
+ from crossauth_backend.common.interfaces import Key, User, UserSecretsInputFields, UserInputFields
6
+
7
+ class AuthenticationParameters(UserSecretsInputFields, total=False):
8
+ """ Parameters needed for this this class to authenticator a user (besides username)
9
+ An example is `password`
10
+ """
11
+
12
+ otp: str
13
+
14
+ class AuthenticationOptions(TypedDict, total=False):
15
+ """
16
+ Options to pass to the constructor.
17
+ """
18
+
19
+ friendly_name: str
20
+ """ If passed, this is what will be displayed to the user when selecting
21
+ an authentication method.
22
+ """
23
+
24
+ class AuthenticatorCapabilities(TypedDict, total=True):
25
+ can_create_user: bool
26
+ can_update_user: bool
27
+ can_update_secrets: bool
28
+
29
+ class Authenticator(ABC):
30
+ """
31
+ Base class for username/password authentication.
32
+
33
+ Subclass this if you want something other than PBKDF2 password hashing.
34
+ """
35
+
36
+ friendly_name: str
37
+ factor_name: str = ""
38
+
39
+ def __init__(self, options: AuthenticationOptions = {}):
40
+ """
41
+ Constructor.
42
+ :param AuthenticationOptions options: see :class:`AuthenticationOptions`
43
+
44
+ """
45
+
46
+ if "friendly_name" not in options:
47
+ raise CrossauthError(ErrorCode.Configuration, "Authenticator must have a friendly name")
48
+ self.friendly_name = options["friendly_name"]
49
+
50
+ @abstractmethod
51
+ def skip_email_verification_on_signup(self) -> bool:
52
+ pass
53
+
54
+ @abstractmethod
55
+ async def prepare_configuration(self, user: UserInputFields) -> Optional[Dict[str, Dict[str, Any]]]:
56
+ pass
57
+
58
+ @abstractmethod
59
+ async def reprepare_configuration(self, username: str, session_key: Key) -> Optional[Dict[str, Dict[str, Any] | Optional[Dict[str, Any]]]]:
60
+ pass
61
+
62
+ @abstractmethod
63
+ def mfa_type(self) -> str:
64
+ pass
65
+
66
+ @abstractmethod
67
+ def mfa_channel(self) -> str:
68
+ pass
69
+
70
+ @abstractmethod
71
+ async def authenticate_user(self, user: UserInputFields|None, secrets: UserSecretsInputFields, params: AuthenticationParameters) -> None:
72
+ pass
73
+
74
+ @abstractmethod
75
+ async def create_persistent_secrets(self,
76
+ username: str,
77
+ params: AuthenticationParameters,
78
+ repeat_params: AuthenticationParameters|None = None) -> Dict[str, Any]:
79
+ pass
80
+
81
+ @abstractmethod
82
+ async def create_one_time_secrets(self, user: User) -> Dict[str, Any]:
83
+ pass
84
+
85
+ @abstractmethod
86
+ def can_create_user(self) -> bool:
87
+ pass
88
+
89
+ @abstractmethod
90
+ def can_update_secrets(self) -> bool:
91
+ pass
92
+
93
+ @abstractmethod
94
+ def can_update_user(self) -> bool:
95
+ pass
96
+
97
+ @abstractmethod
98
+ def secret_names(self) -> List[str]:
99
+ pass
100
+
101
+ @abstractmethod
102
+ def transient_secret_names(self) -> List[str]:
103
+ pass
104
+
105
+ @abstractmethod
106
+ def validate_secrets(self, params: AuthenticationParameters) -> List[str]:
107
+ pass
108
+
109
+ def capabilities(self) -> AuthenticatorCapabilities:
110
+ return AuthenticatorCapabilities(
111
+ can_create_user=self.can_create_user(),
112
+ can_update_user=self.can_update_user(),
113
+ can_update_secrets=self.can_update_secrets()
114
+ )
115
+
116
+ class PasswordAuthenticator(Authenticator):
117
+ """
118
+ base class for authenticators that validate passwords
119
+ """
120
+
121
+ def secret_names(self) -> List[str]:
122
+ return ["password"]
123
+
124
+ def transient_secret_names(self) -> List[str]:
125
+ return []
126
+
127
+ def mfa_type(self) -> str:
128
+ return "none"
129
+
130
+ def mfa_channel(self) -> str:
131
+ return "none"
132
+
@@ -0,0 +1,2 @@
1
+ # Copyright (c) 2024 Matthew Baker. All rights reserved. Licenced under the Apache Licence 2.0. See LICENSE file
2
+
@@ -0,0 +1,498 @@
1
+ # Copyright (c) 2024 Matthew Baker. All rights reserved. Licenced under the Apache Licence 2.0. See LICENSE file
2
+ from __future__ import annotations
3
+ from enum import Enum, auto
4
+ from typing import Any
5
+
6
+ class ErrorCode(Enum):
7
+ """
8
+ Indicates the type of error reported by :class:`crossauth_backend.CrossauthError`
9
+ """
10
+
11
+ UserNotExist = auto()
12
+ """ Thrown when a given username does not exist, eg during login """
13
+
14
+ PasswordInvalid = auto()
15
+ """ Thrown when a password does not match, eg during login or signup """
16
+
17
+ EmailNotExist = auto()
18
+ """ Thrown when a a password reset is requested and the email does not
19
+ exist """
20
+
21
+ UsernameOrPasswordInvalid = auto()
22
+ """ For endpoints provided by servers in this package, this is returned
23
+ instead of UserNotExist or PasswordNotMatch, for security reasons """
24
+
25
+ InvalidClientId = auto()
26
+ """ This is returned if an OAuth2 client id is invalid """
27
+
28
+ ClientExists = auto()
29
+ """ This is returned if attempting to make a client which already exists
30
+ (client_id or name/userid) """
31
+
32
+ InvalidClientSecret = auto()
33
+ """ This is returned if an OAuth2 client secret is invalid """
34
+
35
+ InvalidClientIdOrSecret = auto()
36
+ """ Server endpoints in this package will return this instead of
37
+ InvalidClientId or InvalidClientSecret for security purposes """
38
+
39
+ InvalidRedirectUri = auto()
40
+ """ This is returned a request is made with a redirect Uri that is not
41
+ registered """
42
+
43
+ InvalidOAuthFlow = auto()
44
+ """ This is returned a request is made with a an oauth flow name that is
45
+ not recognized """
46
+
47
+ UserNotActive = auto()
48
+ """ Thrown on login attempt with a user account marked inactive """
49
+
50
+ EmailNotVerified = auto()
51
+ """ Thrown on login attempt with a user account marked not having had the
52
+ email address validated """
53
+
54
+ TwoFactorIncomplete = auto()
55
+ """ Thrown on login attempt with a user account marked not having
56
+ completed 2FA setup """
57
+
58
+ Unauthorized = auto()
59
+ """ Thrown when a resource expecting user authorization was access and
60
+ authorization not provided or wrong """
61
+
62
+ UnauthorizedClient = auto()
63
+ """ Thrown for the OAuth unauthorized_client error (when the client is
64
+ unauthorized as opposed to the user) """
65
+
66
+ InvalidScope = auto()
67
+ """ Thrown for the OAuth invalid_scope error """
68
+
69
+ InsufficientScope = auto()
70
+ """ Thrown for the OAuth insufficient_scope error """
71
+
72
+ InsufficientPriviledges = auto()
73
+ """ Returned if user is valid but doesn't have permission to access
74
+ resource """
75
+
76
+ Forbidden = auto()
77
+ """ Returned with an HTTP 403 response """
78
+
79
+ InvalidKey = auto()
80
+ """
81
+ Thrown when a session or API key was provided that is not in the key
82
+ table.
83
+ For CSRF and sesison key, an InvalidCsrf or InvalidSession will be thrown
84
+ instead
85
+ """
86
+
87
+ InvalidCsrf = auto()
88
+ """ Thrown if the CSRF token is invalid """
89
+
90
+ InvalidSession = auto()
91
+ """ Thrown if the session cookie is invalid """
92
+
93
+ Expired = auto()
94
+ """ Thrown when a session or API key has expired """
95
+
96
+ Connection = auto()
97
+ """ Thrown when there is a connection error, eg to a database """
98
+
99
+ InvalidHash = auto()
100
+ """ Thrown when a hash, eg password, is not in the given format """
101
+
102
+ UnsupportedAlgorithm = auto()
103
+ """ Thrown when an algorithm is requested but not supported, eg hashing
104
+ algorithm """
105
+
106
+ KeyExists = auto()
107
+ """ Thrown if you try to create a key which already exists in key
108
+ storage """
109
+
110
+ PasswordChangeNeeded = auto()
111
+ """ Thrown if the user needs to reset his or her password """
112
+
113
+ PasswordResetNeeded = auto()
114
+ """ Thrown if the user needs to reset his or her password """
115
+
116
+ Factor2ResetNeeded = auto()
117
+ """ Thrown if the user needs to reset factor2 before logging in """
118
+
119
+ Configuration = auto()
120
+ """ Thrown when something is missing or inconsistent in configuration """
121
+
122
+ InvalidEmail = auto()
123
+ """ Thrown if an email address in invalid """
124
+
125
+ InvalidPhoneNumber = auto()
126
+ """ Thrown if a phone number in invalid """
127
+
128
+ InvalidUsername = auto()
129
+ """ Thrown if an email address in invalid """
130
+
131
+ PasswordMatch = auto()
132
+ """ Thrown when two passwords do not match each other (eg signup) """
133
+
134
+ InvalidToken = auto()
135
+ """ Thrown when a token (eg TOTP or OTP) is invalid """
136
+
137
+ MfaRequired = auto()
138
+ """ Thrown during OAuth password flow if an MFA step is needed """
139
+
140
+ PasswordFormat = auto()
141
+ """ Thrown when a password does not match rules (length,
142
+ uppercase/lowercase/digits) """
143
+
144
+ DataFormat = auto()
145
+ """ Thrown when a the data field of key storage is not valid json """
146
+
147
+ FetchError = auto()
148
+ """ Thrown if a fetch failed """
149
+
150
+ UserExists = auto()
151
+ """ Thrown when attempting to create a user that already exists """
152
+
153
+ FormEntry = auto()
154
+ """ Thrown by user-supplied validation functions if a user details form
155
+ was incorrectly filled out """
156
+
157
+ BadRequest = auto()
158
+ """ Thrown when an invalid request is made, eg configure 2FA when 2FA is
159
+ switched off for user """
160
+
161
+ AuthorizationPending = auto()
162
+ """ Thrown in the OAuth device code flow """
163
+
164
+ SlowDown = auto()
165
+ """ Thrown in the OAuth device code flow """
166
+
167
+ ExpiredToken = auto()
168
+ """ Thrown in the OAuth device code flow """
169
+
170
+ ConstraintViolation = auto()
171
+ """ Thrown in database handlers where an insert causes a constraint
172
+ violation """
173
+
174
+ NotImplemented = auto()
175
+ """ Thrown if a method is unimplemented, typically when a feature
176
+ is not yet supported in this language. """
177
+
178
+ ValueError = auto()
179
+ """ Thrown a dict field is unexpectedly missing or wrong type """
180
+
181
+ UnknownError = auto()
182
+ """ Thrown for an condition not convered above. """
183
+
184
+ _FRIENDLY_HTTP_STATUS : dict[str, str] = {
185
+ '200': 'OK',
186
+ '201': 'Created',
187
+ '202': 'Accepted',
188
+ '203': 'Non-Authoritative Information',
189
+ '204': 'No Content',
190
+ '205': 'Reset Content',
191
+ '206': 'Partial Content',
192
+ '300': 'Multiple Choices',
193
+ '301': 'Moved Permanently',
194
+ '302': 'Found',
195
+ '303': 'See Other',
196
+ '304': 'Not Modified',
197
+ '305': 'Use Proxy',
198
+ '306': 'Unused',
199
+ '307': 'Temporary Redirect',
200
+ '400': 'Bad Request',
201
+ '401': 'Unauthorized',
202
+ '402': 'Payment Required',
203
+ '403': 'Forbidden',
204
+ '404': 'Not Found',
205
+ '405': 'Method Not Allowed',
206
+ '406': 'Not Acceptable',
207
+ '407': 'Proxy Authentication Required',
208
+ '408': 'Request Timeout',
209
+ '409': 'Conflict',
210
+ '410': 'Gone',
211
+ '411': 'Length Required',
212
+ '412': 'Precondition Required',
213
+ '413': 'Request Entry Too Large',
214
+ '414': 'Request-URI Too Long',
215
+ '415': 'Unsupported Media Type',
216
+ '416': 'Requested Range Not Satisfiable',
217
+ '417': 'Expectation Failed',
218
+ '418': 'I\'m a teapot',
219
+ '429': 'Too Many Requests',
220
+ '500': 'Internal Server Error',
221
+ '501': 'Not Implemented',
222
+ '502': 'Bad Gateway',
223
+ '503': 'Service Unavailable',
224
+ '504': 'Gateway Timeout',
225
+ '505': 'HTTP Version Not Supported',
226
+
227
+ }
228
+
229
+ class CrossauthError(Exception):
230
+ """
231
+ Thrown by Crossauth functions whenever it encounters an error.
232
+ """
233
+
234
+ def __init__(self, code : ErrorCode, message : str | list[str] | None = None):
235
+ """
236
+ Construct a CrossauthError object.
237
+
238
+ ## Arguments
239
+ - :param ErrorCode code: Return this type of error
240
+ - :param str message: Return this method. If omitted, a default
241
+ will be returned. You can also return an array of messages
242
+ """
243
+
244
+ _message : str | None = None
245
+ _http_status : int = 500
246
+
247
+ if (code == ErrorCode.UserNotExist):
248
+ _message = "User does not exist"
249
+ _http_status = 401
250
+ elif (code == ErrorCode.PasswordInvalid):
251
+ _message = "Password doesn't match"
252
+ _http_status = 401
253
+ elif (code == ErrorCode.UsernameOrPasswordInvalid):
254
+ _message = "Username or password incorrect"
255
+ _http_status = 401
256
+ elif (code == ErrorCode.InvalidClientId):
257
+ _message = "Client id is invalid"
258
+ _http_status = 401
259
+ elif (code == ErrorCode.ClientExists):
260
+ _message = "Client ID or name already exists"
261
+ _http_status = 500
262
+ elif (code == ErrorCode.InvalidClientSecret):
263
+ _message = "Client secret is invalid"
264
+ _http_status = 401
265
+ elif (code == ErrorCode.InvalidClientIdOrSecret):
266
+ _message = "Client id or secret is invalid"
267
+ _http_status = 401
268
+ elif (code == ErrorCode.InvalidRedirectUri):
269
+ _message = "Redirect Uri is not registered"
270
+ _http_status = 401
271
+ elif (code == ErrorCode.InvalidOAuthFlow):
272
+ _message = "Invalid OAuth flow type"
273
+ _http_status = 500
274
+ elif (code == ErrorCode.EmailNotExist):
275
+ _message = "No user exists with that email address"
276
+ _http_status = 401
277
+ elif (code == ErrorCode.UserNotActive):
278
+ _message = "Account is not active"
279
+ _http_status = 403
280
+ elif (code == ErrorCode.InvalidUsername):
281
+ _message = "Username is not in an allowed format"
282
+ _http_status = 400
283
+ elif (code == ErrorCode.InvalidEmail):
284
+ _message = "Email is not in an allowed format"
285
+ _http_status = 400
286
+ elif (code == ErrorCode.InvalidPhoneNumber):
287
+ _message = "Phone number is not in an allowed format"
288
+ _http_status = 400
289
+ elif (code == ErrorCode.EmailNotVerified):
290
+ _message = "Email address has not been verified"
291
+ _http_status = 403
292
+ elif (code == ErrorCode.TwoFactorIncomplete):
293
+ _message = "Two-factor setup is not complete"
294
+ _http_status = 403
295
+ elif (code == ErrorCode.Unauthorized):
296
+ _message = "Not authorized"
297
+ _http_status = 401
298
+ elif (code == ErrorCode.UnauthorizedClient):
299
+ _message = "Client not authorized"
300
+ _http_status = 401
301
+ elif (code == ErrorCode.InvalidScope):
302
+ _message = "Invalid scope"
303
+ _http_status = 403
304
+ elif (code == ErrorCode.InsufficientScope):
305
+ _message = "Insufficient scope"
306
+ _http_status = 403
307
+ elif (code == ErrorCode.Connection):
308
+ _message = "Connection failure"
309
+ elif (code == ErrorCode.Expired):
310
+ _message = "Token has expired"
311
+ _http_status = 401
312
+ elif (code == ErrorCode.InvalidHash):
313
+ _message = "Hash is not in a valid format"
314
+ elif (code == ErrorCode.InvalidKey):
315
+ _message = "Key is invalid"
316
+ _http_status = 401
317
+ elif (code == ErrorCode.Forbidden):
318
+ _message = "You do not have permission to access this resource"
319
+ _http_status = 403
320
+ elif (code == ErrorCode.InsufficientPriviledges):
321
+ _message = "You do not have the right privileges to access this "\
322
+ + "resource"
323
+ _http_status = 401
324
+ elif (code == ErrorCode.InvalidCsrf):
325
+ _message = "CSRF token is invalid"
326
+ _http_status = 401
327
+ elif (code == ErrorCode.InvalidSession):
328
+ _message = "Session cookie is invalid"
329
+ _http_status = 401
330
+ elif (code == ErrorCode.UnsupportedAlgorithm):
331
+ _message = "Algorithm not supported"
332
+ elif (code == ErrorCode.KeyExists):
333
+ _message = "Attempt to create a key that already exists"
334
+ elif (code == ErrorCode.PasswordChangeNeeded):
335
+ _message = "User must change password"
336
+ _http_status = 403
337
+ elif (code == ErrorCode.PasswordResetNeeded):
338
+ _message = "User must reset password"
339
+ _http_status = 403
340
+ elif (code == ErrorCode.Factor2ResetNeeded):
341
+ _message = "User must reset 2FA"
342
+ _http_status = 403
343
+ elif (code == ErrorCode.Configuration):
344
+ _message = "There was an error in the configuration"
345
+ elif (code == ErrorCode.PasswordMatch):
346
+ _message = "Passwords do not match"
347
+ _http_status = 401
348
+ elif (code == ErrorCode.InvalidToken):
349
+ _message = "Token is not valid"
350
+ _http_status = 401
351
+ elif (code == ErrorCode.MfaRequired):
352
+ _message = "MFA is required"
353
+ _http_status = 401
354
+ elif (code == ErrorCode.PasswordFormat):
355
+ _message = "Password format was incorrect"
356
+ _http_status = 401
357
+ elif (code == ErrorCode.UserExists):
358
+ _message = "User already exists"
359
+ _http_status = 400
360
+ elif (code == ErrorCode.BadRequest):
361
+ _message = "The request is invalid"
362
+ _http_status = 400
363
+ elif (code == ErrorCode.DataFormat):
364
+ _message = "Session data has unexpected format"
365
+ _http_status = 500
366
+ elif (code == ErrorCode.FetchError):
367
+ _message = "Couldn't execute a fetch"
368
+ _http_status = 500
369
+ elif (code == ErrorCode.AuthorizationPending):
370
+ _message = "Waiting for authorization"
371
+ _http_status = 200
372
+ elif (code == ErrorCode.SlowDown):
373
+ _message = "Slow polling down by 5 seconds"
374
+ _http_status = 200
375
+ elif (code == ErrorCode.ExpiredToken):
376
+ _message = "Token has expired"
377
+ _http_status = 401
378
+ elif (code == ErrorCode.ConstraintViolation):
379
+ _message = "Database update/insert caused a constraint violation"
380
+ _http_status = 500
381
+ elif (code == ErrorCode.NotImplemented):
382
+ _message = "This method has not been implemented"
383
+ _http_status = 500
384
+ elif (code == ErrorCode.ValueError):
385
+ _message = "Field is missing or wrong type"
386
+ _http_status = 500
387
+ else:
388
+ _message = "Unknown error"
389
+ _http_status = 500
390
+
391
+ self.messages : list[str] | None = None
392
+ if (message != None and type(message) is str):
393
+ _message = message
394
+ self.messages = [message]
395
+ elif (type(message) is list[str]):
396
+ _message = ".".join(message)
397
+ self.messages = message
398
+
399
+ # Call the base class constructor with the parameters it needs
400
+ super(CrossauthError, self).__init__(_message)
401
+ self.message : str = _message
402
+ self.http_status : int = _http_status
403
+ self.code : ErrorCode = code
404
+
405
+ @staticmethod
406
+ def from_oauth_error(error : str, error_description: str | None = None) -> CrossauthError:
407
+ """
408
+ OAuth defines certain error types. To convert the error in an OAuth
409
+ response into a CrossauthError object, call this function.
410
+
411
+ :param str error: as returned by an OAuth call (converted to an
412
+ :class:`ErrorCode`).
413
+ :param str error_description as returned by an OAuth call (put in the
414
+ `message`)
415
+
416
+ :return a `crossauth_backend.CrossauthError` instance.
417
+ """
418
+ code = ErrorCode.UnknownError
419
+
420
+ match error:
421
+ case "invalid_request": code = ErrorCode.BadRequest
422
+ case "unauthorized_client": code = ErrorCode.UnauthorizedClient
423
+ case "access_denied": code = ErrorCode.Unauthorized
424
+ case "unsupported_response_type": code = ErrorCode.BadRequest
425
+ case "invalid_scope": code = ErrorCode.InvalidScope
426
+ case "server_error": code = ErrorCode.UnknownError
427
+ case "temporarily_unavailable": code = ErrorCode.Connection
428
+ case "invalid_token": code = ErrorCode.InvalidToken
429
+ case "expired_token": code = ErrorCode.ExpiredToken
430
+ case "insufficient_scope": code = ErrorCode.InvalidToken
431
+ case "mfa_required": code = ErrorCode.MfaRequired
432
+ case "authorization_pending": code = ErrorCode.AuthorizationPending
433
+ case "slow_down": code = ErrorCode.SlowDown
434
+ case _: code = ErrorCode.UnknownError
435
+ return CrossauthError(code, error_description)
436
+
437
+ @property
438
+ def code_name(self):
439
+ """ Return the name of the error code """
440
+ return self.code.name
441
+
442
+ @property
443
+ def oauthErrorCode(self) -> str:
444
+ """ Return the OAuth name of an error code (eg "server_error")"""
445
+ match (self.code):
446
+ case ErrorCode.BadRequest: return "invalid_request"
447
+ case ErrorCode.UnauthorizedClient: return "unauthorized_client"
448
+ case ErrorCode.Unauthorized: return "access_denied"
449
+ case ErrorCode.InvalidScope: return "invalid_scope"
450
+ case ErrorCode.Connection: return "temporarily_unavailable"
451
+ case ErrorCode.InvalidToken: return "invalid_token"
452
+ case ErrorCode.MfaRequired: return "mfa_required"
453
+ case ErrorCode.AuthorizationPending: return "authorization_pending"
454
+ case ErrorCode.SlowDown: return "slow_down"
455
+ case ErrorCode.ExpiredToken: return "expired_token"
456
+ case ErrorCode.Expired: return "expired_token"
457
+ case _: return "server_error"
458
+
459
+ @staticmethod
460
+ def as_crossauth_error(e : Any, default_message : str | None= None) -> CrossauthError:
461
+ """
462
+ If the passed object is a `crossauth_backend.CrossauthError` instance, simply returns
463
+ it.
464
+ If not and it is an object with `errorCode` in it, creates a
465
+ CrossauthError from that and `errorMessage`, if present.
466
+ Otherwise creates a `crossauth_backend.CrossauthError` object with :class:`ErrorCode`
467
+ of `Unknown` from it, setting the `message` if possible.
468
+
469
+ :param Any e: the error to convert.
470
+ :param str|None default_message: message to use if there was none in the original exception.
471
+
472
+ :return: a :class:`crossauth_backend.CrossauthError` instance.
473
+ """
474
+ if isinstance(e, CrossauthError):
475
+ return e
476
+ elif (isinstance(e, Exception)):
477
+ return CrossauthError(ErrorCode.UnknownError, str(e))
478
+
479
+ error_message = default_message if default_message is not None else ErrorCode.UnknownError.name
480
+ if 'message' in e:
481
+ error_message = e["message"]
482
+ return CrossauthError(ErrorCode.UnknownError, error_message)
483
+
484
+ @staticmethod
485
+ def http_status_name(status : str|int) -> str:
486
+ """
487
+ Returns the friendly name for an HTTP response code.
488
+
489
+ If it is not a recognized one, returns the friendly name for 500.
490
+ @param status the HTTP response code, which, while being numeric,
491
+ can be in a string or number.
492
+ @returns the string version of the response code.
493
+ """
494
+ if (type(status) == int):
495
+ status = str(status)
496
+ if (status in _FRIENDLY_HTTP_STATUS):
497
+ return _FRIENDLY_HTTP_STATUS[status]
498
+ return _FRIENDLY_HTTP_STATUS['500']