Skip to content

Commit 8a544cd

Browse files
author
Liyi Meng
committed
Support bcryt password
1 parent 2ec64c0 commit 8a544cd

File tree

4 files changed

+73
-16
lines changed

4 files changed

+73
-16
lines changed

.env.example

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,12 @@ API_URL=http://localhost:5055
8888

8989
# SECURITY
9090
# Set this to protect your Open Notebook instance with a password (for public hosting)
91+
# You may supply either a plaintext password or a bcrypt hash.
92+
# Examples:
93+
# Plaintext:
94+
# OPEN_NOTEBOOK_PASSWORD=your_secure_password
95+
# bcrypt hash (server must have bcrypt installed):
96+
# OPEN_NOTEBOOK_PASSWORD='$2b$12$K1...'
9197
# OPEN_NOTEBOOK_PASSWORD=
9298

9399
# OPENAI
@@ -250,8 +256,6 @@ SURREAL_COMMANDS_RETRY_WAIT_MAX=30
250256
# backoff ensures operations complete successfully even at high concurrency.
251257
SURREAL_COMMANDS_MAX_TASKS=5
252258

253-
# OPEN_NOTEBOOK_PASSWORD=
254-
255259
# FIRECRAWL - Get a key at https://firecrawl.dev/
256260
FIRECRAWL_API_KEY=
257261

api/auth.py

Lines changed: 43 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,44 @@
66
from starlette.middleware.base import BaseHTTPMiddleware
77
from starlette.responses import JSONResponse
88

9+
import bcrypt
10+
from loguru import logger
11+
12+
13+
def verify_password(provided: str, stored: str) -> bool:
14+
"""
15+
Verify a provided plaintext password against the stored value.
16+
17+
- If `stored` looks like a bcrypt hash (starts with "$2"), use bcrypt.checkpw.
18+
- Otherwise treat `stored` as a plaintext secret and compare directly.
19+
20+
Returns True if the password is valid, False otherwise.
21+
"""
22+
if not stored:
23+
return False
24+
25+
# bcrypt-style hashes begin with "$2b$", "$2a$", "$2y$", etc.
26+
if isinstance(stored, str) and stored.startswith("$2"):
27+
try:
28+
return bcrypt.checkpw(provided.encode("utf-8"), stored.encode("utf-8"))
29+
except Exception as e:
30+
# Any error in bcrypt verification should be treated as an invalid password
31+
logger.error(f"bcrypt verification failed: {e}")
32+
return False
33+
34+
# Plaintext comparison
35+
return secrets.compare_digest(provided, stored)
36+
937

1038
class PasswordAuthMiddleware(BaseHTTPMiddleware):
1139
"""
1240
Middleware to check password authentication for all API requests.
13-
Only active when OPEN_NOTEBOOK_PASSWORD environment variable is set.
41+
Active when OPEN_NOTEBOOK_PASSWORD environment variable is set.
42+
43+
Behavior:
44+
- If OPEN_NOTEBOOK_PASSWORD starts with "$2" it's treated as a bcrypt hash and
45+
incoming Bearer tokens are verified using verify_password().
46+
- Otherwise the value is treated as a plaintext secret and compared directly.
1447
"""
1548

1649
def __init__(self, app, excluded_paths: Optional[list] = None):
@@ -25,7 +58,7 @@ def __init__(self, app, excluded_paths: Optional[list] = None):
2558
]
2659

2760
async def dispatch(self, request: Request, call_next):
28-
# Skip authentication if no password is set
61+
# No auth configured
2962
if not self.password:
3063
return await call_next(request)
3164

@@ -51,16 +84,16 @@ async def dispatch(self, request: Request, call_next):
5184
try:
5285
scheme, credentials = auth_header.split(" ", 1)
5386
if scheme.lower() != "bearer":
54-
raise ValueError("Invalid authentication scheme")
87+
raise ValueError()
5588
except ValueError:
5689
return JSONResponse(
5790
status_code=401,
5891
content={"detail": "Invalid authorization header format"},
5992
headers={"WWW-Authenticate": "Bearer"},
6093
)
6194

62-
# Check password
63-
if credentials != self.password:
95+
# Verify password via helper
96+
if not verify_password(credentials, self.password):
6497
return JSONResponse(
6598
status_code=401,
6699
content={"detail": "Invalid password"},
@@ -80,25 +113,21 @@ def check_api_password(
80113
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security),
81114
) -> bool:
82115
"""
83-
Utility function to check API password.
84-
Can be used as a dependency in individual routes if needed.
116+
Dependency utility to verify the API password for individual routes.
117+
Uses verify_password() for the actual check.
85118
"""
86-
password = os.environ.get("OPEN_NOTEBOOK_PASSWORD")
87-
88-
# No password set, allow access
89-
if not password:
119+
password_env = os.environ.get("OPEN_NOTEBOOK_PASSWORD")
120+
if not password_env:
90121
return True
91122

92-
# No credentials provided
93123
if not credentials:
94124
raise HTTPException(
95125
status_code=401,
96126
detail="Missing authorization",
97127
headers={"WWW-Authenticate": "Bearer"},
98128
)
99129

100-
# Check password
101-
if credentials.credentials != password:
130+
if not verify_password(credentials.credentials, password_env):
102131
raise HTTPException(
103132
status_code=401,
104133
detail="Invalid password",

docs/5-CONFIGURATION/security.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,29 @@ OPEN_NOTEBOOK_PASSWORD=Notebook$Dev$2024$Strong!
6464
OPEN_NOTEBOOK_PASSWORD=$(openssl rand -base64 24)
6565
```
6666

67+
### Hashed password (optional)
68+
You can store a bcrypt hash in the OPEN_NOTEBOOK_PASSWORD environment variable instead of the plaintext secret. The server will detect a bcrypt-style hash (strings beginning with `$2`) and verify incoming Bearer tokens against that hash.
69+
70+
To generate a bcrypt hash locally (example using Python and the bcrypt package):
71+
72+
```bash
73+
# Install bcrypt locally
74+
pip install bcrypt
75+
76+
# Generate bcrypt hash (prints the hash)
77+
python -c "import bcrypt,sys;print(bcrypt.hashpw(sys.argv[1].encode(),bcrypt.gensalt()).decode())" yourpassword
78+
```
79+
80+
Then set the environment variable to the printed hash:
81+
82+
```bash
83+
OPEN_NOTEBOOK_PASSWORD='$2b$12$K1...' (paste the generated hash)
84+
```
85+
86+
Notes:
87+
- The frontend and API clients still send the plaintext password in `Authorization: Bearer <password>`.
88+
- The server compares that plaintext password against the stored bcrypt hash.
89+
6790
### Bad Passwords
6891

6992
```bash

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ dependencies = [
4040
"podcast-creator>=0.7.0,<1",
4141
"surreal-commands>=1.3.0,<2",
4242
"numpy>=2.4.1",
43+
"bcrypt>=4.0.0",
4344
]
4445

4546
[tool.setuptools]

0 commit comments

Comments
 (0)