basyx commited on
Commit
baccf4e
·
verified ·
1 Parent(s): a1d29d8

Update auth/security.py

Browse files
Files changed (1) hide show
  1. auth/security.py +172 -18
auth/security.py CHANGED
@@ -1,32 +1,186 @@
1
- from datetime import datetime, timedelta
2
- from jose import jwt, JWTError
 
 
 
 
 
 
 
 
3
  from passlib.context import CryptContext
4
 
5
- SECRET_KEY = "CHANGE_THIS_SECRET"
6
- ALGORITHM = "HS256"
7
- TOKEN_EXPIRE_DAYS = 30
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
 
9
- pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
10
 
 
 
 
11
 
12
- def hash_password(password: str):
13
- return pwd_context.hash(password)
14
 
 
 
 
 
 
 
 
15
 
16
- def verify_password(password, hashed):
17
- return pwd_context.verify(password, hashed)
18
 
 
 
 
19
 
20
- def create_token(user_id: int):
21
- payload = {
22
- "sub": str(user_id),
23
- "exp": datetime.utcnow() + timedelta(days=TOKEN_EXPIRE_DAYS)
 
24
  }
25
- return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)
26
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
 
28
- def decode_token(token: str):
29
  try:
30
- return jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
 
 
 
 
 
 
 
 
 
 
 
31
  except JWTError:
32
- return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ auth/security.py
3
+ Enterprise Authentication Security Layer
4
+ Basyx Whisper V10.1
5
+ """
6
+
7
+ from datetime import datetime, timedelta, timezone
8
+ from typing import Optional, Dict, Any
9
+
10
+ from jose import JWTError, jwt
11
  from passlib.context import CryptContext
12
 
13
+ from utils.logger import logger
14
+
15
+ # ==========================================================
16
+ # SECURITY CONSTANTS
17
+ # ==========================================================
18
+
19
+ # IMPORTANT:
20
+ # Replace ONLY via environment variable in production.
21
+ # No fallback assumptions allowed.
22
+ import os
23
+
24
+ JWT_SECRET_KEY = os.environ.get("JWT_SECRET_KEY")
25
+ if not JWT_SECRET_KEY:
26
+ raise RuntimeError("JWT_SECRET_KEY environment variable is required")
27
+
28
+ JWT_ALGORITHM = "HS256"
29
+ ACCESS_TOKEN_EXPIRE_MINUTES = int(
30
+ os.environ.get("ACCESS_TOKEN_EXPIRE_MINUTES", "60")
31
+ )
32
+
33
+ # bcrypt safe configuration
34
+ pwd_context = CryptContext(
35
+ schemes=["bcrypt"],
36
+ deprecated="auto",
37
+ )
38
+
39
+ # ==========================================================
40
+ # PASSWORD UTILITIES
41
+ # ==========================================================
42
+
43
+
44
+ def _sanitize_password(password: str) -> bytes:
45
+ """
46
+ bcrypt only supports 72 bytes.
47
+
48
+ Enterprise rule:
49
+ - Never silently fail
50
+ - Deterministically truncate
51
+ """
52
+
53
+ if not isinstance(password, str):
54
+ raise TypeError("Password must be string")
55
+
56
+ encoded = password.encode("utf-8")
57
+
58
+ if len(encoded) > 72:
59
+ logger.warning("Password exceeded bcrypt limit — truncated safely")
60
+ encoded = encoded[:72]
61
+
62
+ return encoded
63
+
64
+
65
+ def hash_password(password: str) -> str:
66
+ """
67
+ Secure password hashing
68
+ """
69
+
70
+ try:
71
+ safe_password = _sanitize_password(password)
72
+ return pwd_context.hash(safe_password)
73
+ except Exception as e:
74
+ logger.exception("Password hashing failed")
75
+ raise RuntimeError("Password hashing failure") from e
76
+
77
+
78
+ def verify_password(plain_password: str, hashed_password: str) -> bool:
79
+ """
80
+ Verify password against stored hash
81
+ """
82
+
83
+ try:
84
+ safe_password = _sanitize_password(plain_password)
85
+ return pwd_context.verify(safe_password, hashed_password)
86
+ except Exception:
87
+ logger.warning("Password verification failed")
88
+ return False
89
 
 
90
 
91
+ # ==========================================================
92
+ # JWT TOKEN MANAGEMENT
93
+ # ==========================================================
94
 
 
 
95
 
96
+ def create_access_token(
97
+ subject: str,
98
+ additional_claims: Optional[Dict[str, Any]] = None,
99
+ ) -> str:
100
+ """
101
+ Create signed JWT access token
102
+ """
103
 
104
+ if not subject:
105
+ raise ValueError("Token subject required")
106
 
107
+ expire = datetime.now(timezone.utc) + timedelta(
108
+ minutes=ACCESS_TOKEN_EXPIRE_MINUTES
109
+ )
110
 
111
+ payload: Dict[str, Any] = {
112
+ "sub": subject,
113
+ "exp": expire,
114
+ "iat": datetime.now(timezone.utc),
115
+ "type": "access",
116
  }
 
117
 
118
+ if additional_claims:
119
+ payload.update(additional_claims)
120
+
121
+ token = jwt.encode(
122
+ payload,
123
+ JWT_SECRET_KEY,
124
+ algorithm=JWT_ALGORITHM,
125
+ )
126
+
127
+ return token
128
+
129
+
130
+ def decode_access_token(token: str) -> Optional[Dict[str, Any]]:
131
+ """
132
+ Decode and validate JWT token
133
+ """
134
 
 
135
  try:
136
+ payload = jwt.decode(
137
+ token,
138
+ JWT_SECRET_KEY,
139
+ algorithms=[JWT_ALGORITHM],
140
+ )
141
+
142
+ if payload.get("type") != "access":
143
+ logger.warning("Invalid token type")
144
+ return None
145
+
146
+ return payload
147
+
148
  except JWTError:
149
+ logger.warning("JWT decode failed")
150
+ return None
151
+
152
+
153
+ # ==========================================================
154
+ # AUTH HELPER FUNCTIONS
155
+ # ==========================================================
156
+
157
+
158
+ def get_subject_from_token(token: str) -> Optional[str]:
159
+ """
160
+ Extract user identity from token
161
+ """
162
+
163
+ payload = decode_access_token(token)
164
+
165
+ if not payload:
166
+ return None
167
+
168
+ return payload.get("sub")
169
+
170
+
171
+ # ==========================================================
172
+ # HEALTH CHECK (DEBUG SAFE)
173
+ # ==========================================================
174
+
175
+
176
+ def security_status() -> dict:
177
+ """
178
+ Internal verification helper.
179
+ Safe for health diagnostics.
180
+ """
181
+
182
+ return {
183
+ "jwt_algorithm": JWT_ALGORITHM,
184
+ "token_expire_minutes": ACCESS_TOKEN_EXPIRE_MINUTES,
185
+ "bcrypt_scheme": pwd_context.schemes(),
186
+ }