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.
- square_authentication-1.0.0/PKG-INFO +40 -0
- square_authentication-1.0.0/README.md +23 -0
- square_authentication-1.0.0/setup.cfg +4 -0
- square_authentication-1.0.0/setup.py +39 -0
- square_authentication-1.0.0/square_authentication/__init__.py +0 -0
- square_authentication-1.0.0/square_authentication/configuration.py +82 -0
- square_authentication-1.0.0/square_authentication/data/config.ini +42 -0
- square_authentication-1.0.0/square_authentication/main.py +59 -0
- square_authentication-1.0.0/square_authentication/routes/__init__.py +0 -0
- square_authentication-1.0.0/square_authentication/routes/core.py +464 -0
- square_authentication-1.0.0/square_authentication/routes/utility.py +3 -0
- square_authentication-1.0.0/square_authentication/utils/__init__.py +1 -0
- square_authentication-1.0.0/square_authentication/utils/encryption.py +54 -0
- square_authentication-1.0.0/square_authentication/utils/token.py +17 -0
- square_authentication-1.0.0/square_authentication.egg-info/PKG-INFO +40 -0
- square_authentication-1.0.0/square_authentication.egg-info/SOURCES.txt +17 -0
- square_authentication-1.0.0/square_authentication.egg-info/dependency_links.txt +1 -0
- square_authentication-1.0.0/square_authentication.egg-info/requires.txt +11 -0
- square_authentication-1.0.0/square_authentication.egg-info/top_level.txt +1 -0
@@ -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,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
|
+
)
|
File without changes
|
@@ -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)
|
File without changes
|
@@ -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 @@
|
|
1
|
+
|
@@ -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 @@
|
|
1
|
+
|
@@ -0,0 +1 @@
|
|
1
|
+
square_authentication
|