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.
- crossauth_backend/__init__.py +59 -0
- crossauth_backend/auth.py +132 -0
- crossauth_backend/common/__init__.py +2 -0
- crossauth_backend/common/error.py +498 -0
- crossauth_backend/common/interfaces.py +287 -0
- crossauth_backend/common/jwt.py +41 -0
- crossauth_backend/common/logger.py +202 -0
- crossauth_backend/cookieauth.py +575 -0
- crossauth_backend/crypto.py +457 -0
- crossauth_backend/oauth/__init__.py +2 -0
- crossauth_backend/oauth/client.py +1063 -0
- crossauth_backend/oauth/clientmanager.py +157 -0
- crossauth_backend/oauth/resserver.py +34 -0
- crossauth_backend/oauth/tokenconsumer.py +412 -0
- crossauth_backend/oauth/wellknown.py +108 -0
- crossauth_backend/session.py +450 -0
- crossauth_backend/storage.py +626 -0
- crossauth_backend/storageimpl/__init__.py +2 -0
- crossauth_backend/storageimpl/inmemorystorage.py +132 -0
- crossauth_backend/storageimpl/sqlalchemystorage.py +294 -0
- crossauth_backend/utils.py +150 -0
- crossauth_fastapi/__init__.py +23 -0
- crossauth_fastapi/fastapioauthclient.py +2185 -0
- crossauth_fastapi/fastapiresserver.py +300 -0
- crossauth_fastapi/fastapiserver.py +437 -0
- crossauth_fastapi/fastapiserverbase.py +62 -0
- crossauth_fastapi/fastapisession.py +654 -0
- crossauth_fastapi/fastapisessionadapter.py +94 -0
- pycrossauth-0.0.2.dist-info/LICENSE +203 -0
- pycrossauth-0.0.2.dist-info/METADATA +257 -0
- pycrossauth-0.0.2.dist-info/NOTICE +5 -0
- pycrossauth-0.0.2.dist-info/RECORD +34 -0
- pycrossauth-0.0.2.dist-info/WHEEL +5 -0
- 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,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']
|