square-authentication 1.0.0__tar.gz

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.
@@ -0,0 +1,40 @@
1
+ Metadata-Version: 2.1
2
+ Name: square_authentication
3
+ Version: 1.0.0
4
+ Summary: authentication layer for my personal server.
5
+ Home-page: https://github.com/thepmsquare/square_authentication
6
+ Author: thePmSquare
7
+ Author-email: thepmsquare@gmail.com
8
+ License: UNKNOWN
9
+ Platform: UNKNOWN
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Description-Content-Type: text/markdown
16
+
17
+ # square_authentication
18
+
19
+ ## about
20
+
21
+ authentication layer for my personal server.
22
+
23
+ ## Installation
24
+
25
+ ```shell
26
+ pip install square_authentication
27
+ ```
28
+
29
+ ## env
30
+
31
+ - python>=3.12.0
32
+
33
+ ## changelog
34
+
35
+ ### v1.0.0
36
+
37
+ - initial implementation.
38
+
39
+ ## Feedback is appreciated. Thank you!
40
+
@@ -0,0 +1,23 @@
1
+ # square_authentication
2
+
3
+ ## about
4
+
5
+ authentication layer for my personal server.
6
+
7
+ ## Installation
8
+
9
+ ```shell
10
+ pip install square_authentication
11
+ ```
12
+
13
+ ## env
14
+
15
+ - python>=3.12.0
16
+
17
+ ## changelog
18
+
19
+ ### v1.0.0
20
+
21
+ - initial implementation.
22
+
23
+ ## Feedback is appreciated. Thank you!
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,39 @@
1
+ from setuptools import find_packages, setup
2
+
3
+ package_name = "square_authentication"
4
+
5
+ setup(
6
+ name=package_name,
7
+ version="1.0.0",
8
+ packages=find_packages(),
9
+ package_data={
10
+ package_name: ["data/*"],
11
+ },
12
+ install_requires=[
13
+ "uvicorn>=0.24.0.post1",
14
+ "fastapi>=0.104.1",
15
+ "pydantic>=2.5.3",
16
+ "bcrypt>=4.1.2",
17
+ "pyjwt>=2.8.0",
18
+ "requests>=2.32.3",
19
+ "cryptography>=42.0.7",
20
+ "square_commons>=0.0.1",
21
+ "square_logger>=1.0.0",
22
+ "square_database_helper>=0.0.5",
23
+ "square_database_structure>=0.0.11",
24
+ ],
25
+ extras_require={},
26
+ author="thePmSquare",
27
+ author_email="thepmsquare@gmail.com",
28
+ description="authentication layer for my personal server.",
29
+ long_description=open("README.md", "r").read(),
30
+ long_description_content_type="text/markdown",
31
+ url=f"https://github.com/thepmsquare/{package_name}",
32
+ classifiers=[
33
+ "Development Status :: 3 - Alpha",
34
+ "Intended Audience :: Developers",
35
+ "License :: OSI Approved :: MIT License",
36
+ "Programming Language :: Python :: 3",
37
+ "Programming Language :: Python :: 3.9",
38
+ ],
39
+ )
@@ -0,0 +1,82 @@
1
+ import os
2
+ import sys
3
+
4
+ from square_commons import ConfigReader
5
+ from square_logger.main import SquareLogger
6
+
7
+ try:
8
+ config_file_path = (
9
+ os.path.dirname(os.path.abspath(__file__))
10
+ + os.sep
11
+ + "data"
12
+ + os.sep
13
+ + "config.ini"
14
+ )
15
+ ldict_configuration = ConfigReader(config_file_path).read_configuration()
16
+
17
+ # get all vars and typecast
18
+ # ===========================================
19
+ # general
20
+ config_str_module_name = ldict_configuration["GENERAL"]["MODULE_NAME"]
21
+ # ===========================================
22
+
23
+ # ===========================================
24
+ # environment
25
+ config_str_host_ip = ldict_configuration["ENVIRONMENT"]["HOST_IP"]
26
+ config_int_host_port = int(ldict_configuration["ENVIRONMENT"]["HOST_PORT"])
27
+ config_str_log_file_name = ldict_configuration["ENVIRONMENT"]["LOG_FILE_NAME"]
28
+ config_str_secret_key_for_access_token = ldict_configuration["ENVIRONMENT"][
29
+ "SECRET_KEY_FOR_ACCESS_TOKEN"
30
+ ]
31
+ config_str_secret_key_for_refresh_token = ldict_configuration["ENVIRONMENT"][
32
+ "SECRET_KEY_FOR_REFRESH_TOKEN"
33
+ ]
34
+ config_int_access_token_valid_minutes = int(
35
+ ldict_configuration["ENVIRONMENT"]["ACCESS_TOKEN_VALID_MINUTES"]
36
+ )
37
+ config_int_refresh_token_valid_minutes = int(
38
+ ldict_configuration["ENVIRONMENT"]["REFRESH_TOKEN_VALID_MINUTES"]
39
+ )
40
+ config_str_ssl_crt_file_path = ldict_configuration["ENVIRONMENT"][
41
+ "SSL_CRT_FILE_PATH"
42
+ ]
43
+ config_str_ssl_key_file_path = ldict_configuration["ENVIRONMENT"][
44
+ "SSL_KEY_FILE_PATH"
45
+ ]
46
+ # ===========================================
47
+
48
+ # ===========================================
49
+ # square_logger
50
+ config_int_log_level = int(ldict_configuration["SQUARE_LOGGER"]["LOG_LEVEL"])
51
+ config_str_log_path = ldict_configuration["SQUARE_LOGGER"]["LOG_PATH"]
52
+ config_int_log_backup_count = int(
53
+ ldict_configuration["SQUARE_LOGGER"]["LOG_BACKUP_COUNT"]
54
+ )
55
+ # ===========================================
56
+
57
+ # ===========================================
58
+ # square_database_helper
59
+
60
+ config_str_square_database_protocol = ldict_configuration["SQUARE_DATABASE_HELPER"][
61
+ "SQUARE_DATABASE_PROTOCOL"
62
+ ]
63
+ config_str_square_database_ip = ldict_configuration["SQUARE_DATABASE_HELPER"][
64
+ "SQUARE_DATABASE_IP"
65
+ ]
66
+ config_int_square_database_port = int(
67
+ ldict_configuration["SQUARE_DATABASE_HELPER"]["SQUARE_DATABASE_PORT"]
68
+ )
69
+ # ===========================================
70
+ # Initialize logger
71
+ global_object_square_logger = SquareLogger(
72
+ pstr_log_file_name=config_str_log_file_name,
73
+ pint_log_level=config_int_log_level,
74
+ pstr_log_path=config_str_log_path,
75
+ pint_log_backup_count=config_int_log_backup_count,
76
+ )
77
+ except Exception as e:
78
+ print(
79
+ "\033[91mMissing or incorrect config.ini file.\n"
80
+ "Error details: " + str(e) + "\033[0m"
81
+ )
82
+ sys.exit()
@@ -0,0 +1,42 @@
1
+ [GENERAL]
2
+ MODULE_NAME = square_authentication
3
+
4
+ [ENVIRONMENT]
5
+ HOST_IP = 0.0.0.0
6
+ HOST_PORT = 10011
7
+
8
+ LOG_FILE_NAME = square_authentication
9
+
10
+ SECRET_KEY_FOR_ACCESS_TOKEN = dummy_access
11
+ SECRET_KEY_FOR_REFRESH_TOKEN = dummy_refresh
12
+
13
+ ACCESS_TOKEN_VALID_MINUTES = 1440
14
+ REFRESH_TOKEN_VALID_MINUTES = 10080
15
+
16
+ # absolute path (mandatory only for http)
17
+ SSL_CRT_FILE_PATH = ssl.crt
18
+ SSL_KEY_FILE_PATH = ssl.key
19
+
20
+ [SQUARE_LOGGER]
21
+
22
+ # | Log Level | Value |
23
+ # | --------- | ----- |
24
+ # | CRITICAL | 50 |
25
+ # | ERROR | 40 |
26
+ # | WARNING | 30 |
27
+ # | INFO | 20 |
28
+ # | DEBUG | 10 |
29
+ # | NOTSET | 0 |
30
+
31
+ LOG_LEVEL = 20
32
+ # absolute or relative path
33
+ LOG_PATH = logs
34
+ # number of backup log files to keep during rotation
35
+ # if backupCount is zero, rollover never occurs.
36
+ LOG_BACKUP_COUNT = 3
37
+
38
+ [SQUARE_DATABASE_HELPER]
39
+
40
+ SQUARE_DATABASE_PROTOCOL = http
41
+ SQUARE_DATABASE_IP = localhost
42
+ SQUARE_DATABASE_PORT = 10010
@@ -0,0 +1,59 @@
1
+ import os.path
2
+
3
+ from fastapi import FastAPI, status
4
+ from fastapi.middleware.cors import CORSMiddleware
5
+ from fastapi.responses import JSONResponse
6
+ from uvicorn import run
7
+
8
+ from square_authentication.configuration import (
9
+ config_int_host_port,
10
+ config_str_host_ip,
11
+ global_object_square_logger,
12
+ config_str_module_name,
13
+ config_str_ssl_key_file_path,
14
+ config_str_ssl_crt_file_path,
15
+ )
16
+ from square_authentication.routes import core, utility
17
+
18
+ app = FastAPI()
19
+
20
+ app.add_middleware(
21
+ CORSMiddleware,
22
+ allow_origins=["*"],
23
+ allow_methods=["*"],
24
+ allow_headers=["*"],
25
+ )
26
+
27
+ app.include_router(core.router)
28
+ app.include_router(utility.router)
29
+
30
+
31
+ @app.get("/")
32
+ @global_object_square_logger.async_auto_logger
33
+ async def root():
34
+ return JSONResponse(
35
+ status_code=status.HTTP_200_OK, content={"text": config_str_module_name}
36
+ )
37
+
38
+
39
+ if __name__ == "__main__":
40
+ try:
41
+ if os.path.exists(config_str_ssl_key_file_path) and os.path.exists(
42
+ config_str_ssl_crt_file_path
43
+ ):
44
+ run(
45
+ app,
46
+ host=config_str_host_ip,
47
+ port=config_int_host_port,
48
+ ssl_certfile=config_str_ssl_crt_file_path,
49
+ ssl_keyfile=config_str_ssl_key_file_path,
50
+ )
51
+ else:
52
+ run(
53
+ app,
54
+ host=config_str_host_ip,
55
+ port=config_int_host_port,
56
+ )
57
+
58
+ except Exception as exc:
59
+ global_object_square_logger.logger.critical(exc, exc_info=True)
@@ -0,0 +1,464 @@
1
+ from datetime import datetime, timedelta, timezone
2
+ from typing import Annotated, Union
3
+
4
+ import bcrypt
5
+ import jwt
6
+ from fastapi import APIRouter, status, Header
7
+ from fastapi.responses import JSONResponse
8
+ from requests.exceptions import HTTPError
9
+ from square_database_helper.main import SquareDatabaseHelper
10
+ from square_database_structure.square.authentication.enums import UserLogEventEnum
11
+ from square_database_structure.square.authentication.tables import (
12
+ local_string_database_name,
13
+ local_string_schema_name,
14
+ User,
15
+ UserLog,
16
+ UserCredential,
17
+ UserProfile,
18
+ UserSession,
19
+ )
20
+
21
+ from square_authentication.configuration import (
22
+ global_object_square_logger,
23
+ config_str_secret_key_for_access_token,
24
+ config_int_access_token_valid_minutes,
25
+ config_int_refresh_token_valid_minutes,
26
+ config_str_secret_key_for_refresh_token,
27
+ config_str_square_database_ip,
28
+ config_int_square_database_port,
29
+ config_str_square_database_protocol,
30
+ )
31
+ from square_authentication.utils.token import get_jwt_payload
32
+
33
+ router = APIRouter(
34
+ tags=["core"],
35
+ )
36
+
37
+ global_object_square_database_helper = SquareDatabaseHelper(
38
+ param_str_square_database_ip=config_str_square_database_ip,
39
+ param_int_square_database_port=config_int_square_database_port,
40
+ param_str_square_database_protocol=config_str_square_database_protocol,
41
+ )
42
+
43
+
44
+ @router.get("/register_username/")
45
+ @global_object_square_logger.async_auto_logger
46
+ async def register_username(username: str, password: str):
47
+ local_str_user_id = None
48
+ try:
49
+ # ======================================================================================
50
+ # entry in user table
51
+ local_list_response_user = global_object_square_database_helper.insert_rows(
52
+ data=[{}],
53
+ database_name=local_string_database_name,
54
+ schema_name=local_string_schema_name,
55
+ table_name=User.__tablename__,
56
+ )
57
+ local_str_user_id = local_list_response_user[0][User.user_id.name]
58
+ # ======================================================================================
59
+
60
+ # ======================================================================================
61
+ # entry in user log
62
+ local_list_response_user_log = global_object_square_database_helper.insert_rows(
63
+ data=[
64
+ {
65
+ UserLog.user_id.name: local_str_user_id,
66
+ UserLog.user_log_event.name: UserLogEventEnum.CREATED.value,
67
+ }
68
+ ],
69
+ database_name=local_string_database_name,
70
+ schema_name=local_string_schema_name,
71
+ table_name=UserLog.__tablename__,
72
+ )
73
+ # ======================================================================================
74
+
75
+ # ======================================================================================
76
+ # entry in user profile
77
+ local_list_response_user_profile = (
78
+ global_object_square_database_helper.insert_rows(
79
+ data=[{UserProfile.user_id.name: local_str_user_id}],
80
+ database_name=local_string_database_name,
81
+ schema_name=local_string_schema_name,
82
+ table_name=UserProfile.__tablename__,
83
+ )
84
+ )
85
+
86
+ # ======================================================================================
87
+
88
+ # ======================================================================================
89
+ # entry in credential table
90
+
91
+ # hash password
92
+ local_str_hashed_password = bcrypt.hashpw(
93
+ password.encode("utf-8"), bcrypt.gensalt()
94
+ ).decode("utf-8")
95
+
96
+ # create access token
97
+ local_dict_access_token_payload = {
98
+ "user_id": local_str_user_id,
99
+ "exp": datetime.now(timezone.utc)
100
+ + timedelta(minutes=config_int_access_token_valid_minutes),
101
+ }
102
+ local_str_access_token = jwt.encode(
103
+ local_dict_access_token_payload, config_str_secret_key_for_access_token
104
+ )
105
+
106
+ # create refresh token
107
+ local_object_refresh_token_expiry_time = datetime.now(timezone.utc) + timedelta(
108
+ minutes=config_int_refresh_token_valid_minutes
109
+ )
110
+
111
+ local_dict_refresh_token_payload = {
112
+ "user_id": local_str_user_id,
113
+ "exp": local_object_refresh_token_expiry_time,
114
+ }
115
+ local_str_refresh_token = jwt.encode(
116
+ local_dict_refresh_token_payload, config_str_secret_key_for_refresh_token
117
+ )
118
+ try:
119
+ local_list_response_authentication_username = global_object_square_database_helper.insert_rows(
120
+ data=[
121
+ {
122
+ UserCredential.user_id.name: local_str_user_id,
123
+ UserCredential.user_credential_username.name: username,
124
+ UserCredential.user_credential_hashed_password.name: local_str_hashed_password,
125
+ }
126
+ ],
127
+ database_name=local_string_database_name,
128
+ schema_name=local_string_schema_name,
129
+ table_name=UserCredential.__tablename__,
130
+ )
131
+ except HTTPError as http_error:
132
+ if http_error.response.status_code == 400:
133
+ return JSONResponse(
134
+ status_code=status.HTTP_409_CONFLICT,
135
+ content=f"an account with the username {username} already exists.",
136
+ )
137
+ else:
138
+ raise http_error
139
+ # ======================================================================================
140
+
141
+ # ======================================================================================
142
+ # entry in user session table
143
+ local_list_response_user_session = global_object_square_database_helper.insert_rows(
144
+ data=[
145
+ {
146
+ UserSession.user_id.name: local_str_user_id,
147
+ UserSession.user_session_refresh_token.name: local_str_refresh_token,
148
+ UserSession.user_session_expiry_time.name: local_object_refresh_token_expiry_time.strftime(
149
+ "%Y-%m-%d %H:%M:%S.%f+00"
150
+ ),
151
+ }
152
+ ],
153
+ database_name=local_string_database_name,
154
+ schema_name=local_string_schema_name,
155
+ table_name=UserSession.__tablename__,
156
+ )
157
+ # ======================================================================================
158
+ return JSONResponse(
159
+ status_code=status.HTTP_200_OK,
160
+ content={
161
+ "user_id": local_str_user_id,
162
+ "access_token": local_str_access_token,
163
+ "refresh_token": local_str_refresh_token,
164
+ },
165
+ )
166
+ except Exception as e:
167
+ global_object_square_logger.logger.error(e, exc_info=True)
168
+ if local_str_user_id:
169
+ global_object_square_database_helper.delete_rows(
170
+ database_name=local_string_database_name,
171
+ schema_name=local_string_schema_name,
172
+ table_name=User.__tablename__,
173
+ filters={User.user_id.name: local_str_user_id},
174
+ )
175
+ return JSONResponse(
176
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content=str(e)
177
+ )
178
+
179
+
180
+ @router.get("/login_username/")
181
+ @global_object_square_logger.async_auto_logger
182
+ async def login_username(username: str, password: str):
183
+ try:
184
+ # ======================================================================================
185
+ # get entry from authentication_username table
186
+ local_list_authentication_user_response = (
187
+ global_object_square_database_helper.get_rows(
188
+ database_name=local_string_database_name,
189
+ schema_name=local_string_schema_name,
190
+ table_name=UserCredential.__tablename__,
191
+ filters={UserCredential.user_credential_username.name: username},
192
+ )
193
+ )
194
+ # ======================================================================================
195
+
196
+ # ======================================================================================
197
+ # validate username
198
+ # ======================================================================================
199
+ if len(local_list_authentication_user_response) != 1:
200
+ return JSONResponse(
201
+ status_code=status.HTTP_400_BAD_REQUEST, content="incorrect username."
202
+ )
203
+ # ======================================================================================
204
+ # validate password
205
+ # ======================================================================================
206
+ else:
207
+ if not (
208
+ bcrypt.checkpw(
209
+ password.encode("utf-8"),
210
+ local_list_authentication_user_response[0][
211
+ UserCredential.user_credential_hashed_password.name
212
+ ].encode("utf-8"),
213
+ )
214
+ ):
215
+ return JSONResponse(
216
+ status_code=status.HTTP_400_BAD_REQUEST,
217
+ content="incorrect password.",
218
+ )
219
+
220
+ # ======================================================================================
221
+ # return new access token and refresh token
222
+ # ======================================================================================
223
+ else:
224
+ local_str_user_id = local_list_authentication_user_response[0][
225
+ UserCredential.user_id.name
226
+ ]
227
+ # create access token
228
+ local_dict_access_token_payload = {
229
+ "user_id": local_str_user_id,
230
+ "exp": datetime.now(timezone.utc)
231
+ + timedelta(minutes=config_int_access_token_valid_minutes),
232
+ }
233
+ local_str_access_token = jwt.encode(
234
+ local_dict_access_token_payload,
235
+ config_str_secret_key_for_access_token,
236
+ )
237
+
238
+ # create refresh token
239
+ local_object_refresh_token_expiry_time = datetime.now(
240
+ timezone.utc
241
+ ) + timedelta(minutes=config_int_refresh_token_valid_minutes)
242
+
243
+ local_dict_refresh_token_payload = {
244
+ "user_id": local_str_user_id,
245
+ "exp": local_object_refresh_token_expiry_time,
246
+ }
247
+ local_str_refresh_token = jwt.encode(
248
+ local_dict_refresh_token_payload,
249
+ config_str_secret_key_for_refresh_token,
250
+ )
251
+ # ======================================================================================
252
+ # entry in user session table
253
+ local_list_response_user_session = global_object_square_database_helper.insert_rows(
254
+ data=[
255
+ {
256
+ UserSession.user_id.name: local_str_user_id,
257
+ UserSession.user_session_refresh_token.name: local_str_refresh_token,
258
+ UserSession.user_session_expiry_time.name: local_object_refresh_token_expiry_time.strftime(
259
+ "%Y-%m-%d %H:%M:%S.%f+00"
260
+ ),
261
+ }
262
+ ],
263
+ database_name=local_string_database_name,
264
+ schema_name=local_string_schema_name,
265
+ table_name=UserSession.__tablename__,
266
+ )
267
+ # ======================================================================================
268
+ return JSONResponse(
269
+ status_code=status.HTTP_200_OK,
270
+ content={
271
+ "user_id": local_str_user_id,
272
+ "access_token": local_str_access_token,
273
+ "refresh_token": local_str_refresh_token,
274
+ },
275
+ )
276
+ # ======================================================================================
277
+
278
+ except Exception as e:
279
+ global_object_square_logger.logger.error(e, exc_info=True)
280
+ return JSONResponse(
281
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content=str(e)
282
+ )
283
+
284
+
285
+ @router.get("/generate_access_token/")
286
+ @global_object_square_logger.async_auto_logger
287
+ async def generate_access_token(
288
+ user_id: str, refresh_token: Annotated[Union[str, None], Header()]
289
+ ):
290
+ try:
291
+ # ======================================================================================
292
+ # validate user_id
293
+ local_list_user_response = global_object_square_database_helper.get_rows(
294
+ database_name=local_string_database_name,
295
+ schema_name=local_string_schema_name,
296
+ table_name=User.__tablename__,
297
+ filters={User.user_id.name: user_id},
298
+ )
299
+
300
+ if len(local_list_user_response) != 1:
301
+ return JSONResponse(
302
+ status_code=status.HTTP_400_BAD_REQUEST,
303
+ content=f"incorrect user_id: {user_id}.",
304
+ )
305
+ # ======================================================================================
306
+
307
+ # ======================================================================================
308
+ # validate refresh token
309
+
310
+ # validating if a session refresh token exists in the database.
311
+ local_list_user_session_response = (
312
+ global_object_square_database_helper.get_rows(
313
+ database_name=local_string_database_name,
314
+ schema_name=local_string_schema_name,
315
+ table_name=UserSession.__tablename__,
316
+ filters={
317
+ UserSession.user_id.name: user_id,
318
+ UserSession.user_session_refresh_token.name: refresh_token,
319
+ },
320
+ )
321
+ )
322
+
323
+ if len(local_list_user_session_response) != 1:
324
+ return JSONResponse(
325
+ status_code=status.HTTP_400_BAD_REQUEST,
326
+ content=f"incorrect refresh token: {refresh_token} for user_id: {user_id}."
327
+ f"for user_id: {user_id}.",
328
+ )
329
+ # validating if the refresh token is valid, active and of the same user.
330
+ try:
331
+ local_dict_refresh_token_payload = get_jwt_payload(
332
+ refresh_token, config_str_secret_key_for_refresh_token
333
+ )
334
+ except Exception as error:
335
+ return JSONResponse(
336
+ status_code=status.HTTP_400_BAD_REQUEST,
337
+ content=str(error),
338
+ )
339
+
340
+ if local_dict_refresh_token_payload["user_id"] != user_id:
341
+ return JSONResponse(
342
+ status_code=status.HTTP_400_BAD_REQUEST,
343
+ content=f"refresh token and user_id mismatch.",
344
+ )
345
+
346
+ # ======================================================================================
347
+ # ======================================================================================
348
+ # create and send access token
349
+ local_dict_access_token_payload = {
350
+ "user_id": user_id,
351
+ "exp": datetime.now(timezone.utc)
352
+ + timedelta(minutes=config_int_access_token_valid_minutes),
353
+ }
354
+ local_str_access_token = jwt.encode(
355
+ local_dict_access_token_payload, config_str_secret_key_for_access_token
356
+ )
357
+
358
+ return JSONResponse(
359
+ status_code=status.HTTP_200_OK,
360
+ content={"access_token": local_str_access_token},
361
+ )
362
+ # ======================================================================================
363
+
364
+ except Exception as e:
365
+ global_object_square_logger.logger.error(e, exc_info=True)
366
+ return JSONResponse(
367
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content=str(e)
368
+ )
369
+
370
+
371
+ @router.delete("/logout/")
372
+ @global_object_square_logger.async_auto_logger
373
+ async def logout(
374
+ user_id: str,
375
+ access_token: Annotated[Union[str, None], Header()],
376
+ refresh_token: Annotated[Union[str, None], Header()],
377
+ ):
378
+ try:
379
+ # ======================================================================================
380
+ # validate user_id
381
+ local_list_user_response = global_object_square_database_helper.get_rows(
382
+ database_name=local_string_database_name,
383
+ schema_name=local_string_schema_name,
384
+ table_name=User.__tablename__,
385
+ filters={User.user_id.name: user_id},
386
+ )
387
+
388
+ if len(local_list_user_response) != 1:
389
+ return JSONResponse(
390
+ status_code=status.HTTP_400_BAD_REQUEST,
391
+ content=f"incorrect user_id: {user_id}.",
392
+ )
393
+ # ======================================================================================
394
+
395
+ # ======================================================================================
396
+ # validate refresh token
397
+
398
+ # validating if a session refresh token exists in the database.
399
+ local_list_user_session_response = (
400
+ global_object_square_database_helper.get_rows(
401
+ database_name=local_string_database_name,
402
+ schema_name=local_string_schema_name,
403
+ table_name=UserSession.__tablename__,
404
+ filters={
405
+ UserSession.user_id.name: user_id,
406
+ UserSession.user_session_refresh_token.name: refresh_token,
407
+ },
408
+ )
409
+ )
410
+
411
+ if len(local_list_user_session_response) != 1:
412
+ return JSONResponse(
413
+ status_code=status.HTTP_400_BAD_REQUEST,
414
+ content=f"incorrect refresh token: {refresh_token} for user_id: {user_id}."
415
+ f"for user_id: {user_id}.",
416
+ )
417
+ # not validating if the refresh token is valid, active and of the same user.
418
+ # ======================================================================================
419
+
420
+ # ======================================================================================
421
+ # validate access token
422
+ # validating if the access token is valid, active and of the same user.
423
+ try:
424
+ local_dict_access_token_payload = get_jwt_payload(
425
+ access_token, config_str_secret_key_for_access_token
426
+ )
427
+ except Exception as error:
428
+ return JSONResponse(
429
+ status_code=status.HTTP_400_BAD_REQUEST,
430
+ content=str(error),
431
+ )
432
+ if local_dict_access_token_payload["user_id"] != user_id:
433
+ return JSONResponse(
434
+ status_code=status.HTTP_400_BAD_REQUEST,
435
+ content=f"access token and user_id mismatch.",
436
+ )
437
+
438
+ # ======================================================================================
439
+
440
+ # NOTE: if both access token and refresh token have expired for a user,
441
+ # it can be assumed that user session only needs to be removed from the front end.
442
+
443
+ # ======================================================================================
444
+ # delete session for user
445
+ global_object_square_database_helper.delete_rows(
446
+ database_name=local_string_database_name,
447
+ schema_name=local_string_schema_name,
448
+ table_name=UserSession.__tablename__,
449
+ filters={
450
+ UserSession.user_id.name: user_id,
451
+ UserSession.user_session_refresh_token.name: refresh_token,
452
+ },
453
+ )
454
+
455
+ return JSONResponse(
456
+ status_code=status.HTTP_200_OK, content="Log out successful."
457
+ )
458
+ # ======================================================================================
459
+
460
+ except Exception as e:
461
+ global_object_square_logger.logger.error(e, exc_info=True)
462
+ return JSONResponse(
463
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content=str(e)
464
+ )
@@ -0,0 +1,3 @@
1
+ from fastapi import APIRouter
2
+
3
+ router = APIRouter(tags=["utility"], )
@@ -0,0 +1,54 @@
1
+ import base64
2
+
3
+ from cryptography.hazmat.backends import default_backend
4
+ from cryptography.hazmat.primitives import padding
5
+ from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
6
+
7
+
8
+ def encrypt(key, plaintext):
9
+ # Ensure the key length is 16, 24, or 32 bytes for AES
10
+ key = key.ljust(32)[:32].encode('utf-8')
11
+
12
+ # IV should be random but static in this context for deterministic output
13
+ iv = b'1234567890123456'
14
+
15
+ # Create a Cipher object
16
+ cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())
17
+ encryptor = cipher.encryptor()
18
+
19
+ # Pad the plaintext to be a multiple of the block size
20
+ padder = padding.PKCS7(algorithms.AES.block_size).padder()
21
+ padded_plaintext = padder.update(plaintext.encode('utf-8')) + padder.finalize()
22
+
23
+ # Encrypt the padded plaintext
24
+ ciphertext = encryptor.update(padded_plaintext) + encryptor.finalize()
25
+
26
+ # Combine IV and ciphertext and encode in Base64
27
+ encoded_ciphertext = base64.b64encode(iv + ciphertext).decode('utf-8')
28
+
29
+ return encoded_ciphertext
30
+
31
+
32
+ def decrypt(key, encoded_ciphertext):
33
+ # Ensure the key length is 16, 24, or 32 bytes for AES
34
+ key = key.ljust(32)[:32].encode('utf-8')
35
+
36
+ # Decode the Base64 encoded ciphertext
37
+ ciphertext = base64.b64decode(encoded_ciphertext)
38
+
39
+ # Extract the IV (first 16 bytes)
40
+ iv = ciphertext[:16]
41
+ ciphertext = ciphertext[16:]
42
+
43
+ # Create a Cipher object
44
+ cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())
45
+ decryptor = cipher.decryptor()
46
+
47
+ # Decrypt the ciphertext
48
+ padded_plaintext = decryptor.update(ciphertext) + decryptor.finalize()
49
+
50
+ # Unpad the plaintext
51
+ unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder()
52
+ plaintext = unpadder.update(padded_plaintext) + unpadder.finalize()
53
+
54
+ return plaintext.decode('utf-8')
@@ -0,0 +1,17 @@
1
+ import jwt
2
+ from jwt.exceptions import ExpiredSignatureError, DecodeError, InvalidTokenError
3
+
4
+
5
+ def get_jwt_payload(token, secret_key):
6
+ try:
7
+ # Decode the token and verify the signature
8
+ payload = jwt.decode(token, secret_key, algorithms=["HS256"])
9
+ return payload
10
+ except ExpiredSignatureError:
11
+ raise Exception("The token has expired.")
12
+ except DecodeError:
13
+ raise Exception("The token is invalid.")
14
+ except InvalidTokenError:
15
+ raise Exception("The token is invalid.")
16
+ except Exception:
17
+ raise
@@ -0,0 +1,40 @@
1
+ Metadata-Version: 2.1
2
+ Name: square-authentication
3
+ Version: 1.0.0
4
+ Summary: authentication layer for my personal server.
5
+ Home-page: https://github.com/thepmsquare/square_authentication
6
+ Author: thePmSquare
7
+ Author-email: thepmsquare@gmail.com
8
+ License: UNKNOWN
9
+ Platform: UNKNOWN
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Description-Content-Type: text/markdown
16
+
17
+ # square_authentication
18
+
19
+ ## about
20
+
21
+ authentication layer for my personal server.
22
+
23
+ ## Installation
24
+
25
+ ```shell
26
+ pip install square_authentication
27
+ ```
28
+
29
+ ## env
30
+
31
+ - python>=3.12.0
32
+
33
+ ## changelog
34
+
35
+ ### v1.0.0
36
+
37
+ - initial implementation.
38
+
39
+ ## Feedback is appreciated. Thank you!
40
+
@@ -0,0 +1,17 @@
1
+ README.md
2
+ setup.py
3
+ square_authentication/__init__.py
4
+ square_authentication/configuration.py
5
+ square_authentication/main.py
6
+ square_authentication.egg-info/PKG-INFO
7
+ square_authentication.egg-info/SOURCES.txt
8
+ square_authentication.egg-info/dependency_links.txt
9
+ square_authentication.egg-info/requires.txt
10
+ square_authentication.egg-info/top_level.txt
11
+ square_authentication/data/config.ini
12
+ square_authentication/routes/__init__.py
13
+ square_authentication/routes/core.py
14
+ square_authentication/routes/utility.py
15
+ square_authentication/utils/__init__.py
16
+ square_authentication/utils/encryption.py
17
+ square_authentication/utils/token.py
@@ -0,0 +1,11 @@
1
+ uvicorn>=0.24.0.post1
2
+ fastapi>=0.104.1
3
+ pydantic>=2.5.3
4
+ bcrypt>=4.1.2
5
+ pyjwt>=2.8.0
6
+ requests>=2.32.3
7
+ cryptography>=42.0.7
8
+ square_commons>=0.0.1
9
+ square_logger>=1.0.0
10
+ square_database_helper>=0.0.5
11
+ square_database_structure>=0.0.11
@@ -0,0 +1 @@
1
+ square_authentication