Skip to content

Commit 679a532

Browse files
committed
move method to issue token inside CoreAuthHandler
1 parent 78b88c1 commit 679a532

File tree

4 files changed

+46
-51
lines changed

4 files changed

+46
-51
lines changed

core/cat/auth/headers.py

+6-4
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,8 @@ async def get_user_stray(user: AuthUserInfo):
113113
else:
114114
stray = StrayCat(
115115
ws=websocket,
116-
user_id=user.id,
117-
user_data=user.extra,
116+
user_id=user.name, # TODOV2: user_id should be the user.id
117+
user_data=user,
118118
main_loop=asyncio.get_running_loop(),
119119
)
120120
strays[user.id] = stray
@@ -167,7 +167,8 @@ async def get_user_stray(user: AuthUserInfo):
167167

168168
if user.id not in strays.keys():
169169
strays[user.id] = StrayCat(
170-
user_id=user.id, user_data=user.extra, main_loop=event_loop
170+
# TODOV2: user_id should be the user.id
171+
user_id=user.name, user_data=user, main_loop=event_loop
171172
)
172173
return strays[user.id]
173174

@@ -227,7 +228,8 @@ async def frontend_auth(request: Request) -> None | StrayCat:
227228

228229
if user.id not in strays.keys():
229230
strays[user.id] = StrayCat(
230-
user_id=user.id, user_data=user.extra, main_loop=event_loop
231+
# TODOV2: user_id should be the user.id
232+
user_id=user.name, user_data=user, main_loop=event_loop
231233
)
232234
return strays[user.id]
233235

core/cat/factory/custom_auth_handler.py

+32-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
from abc import ABC, abstractmethod
2+
from datetime import datetime, timedelta
3+
from pytz import utc
24
import jwt
35

46
from cat.db.crud import get_users
57
from cat.auth.utils import (
6-
AuthPermission, AuthResource, AuthUserInfo, get_base_permissions, get_full_permissions, is_jwt
8+
AuthPermission, AuthResource, AuthUserInfo, get_base_permissions, get_full_permissions, is_jwt, check_password
79
)
810
from cat.env import get_env
911
from cat.log import log
@@ -56,6 +58,7 @@ async def authorize_user_from_key(
5658

5759
# Core auth handler, verify token on local idp
5860
class CoreAuthHandler(BaseAuthHandler):
61+
5962
async def authorize_user_from_jwt(
6063
self, token: str, auth_resource: AuthResource, auth_permission: AuthPermission
6164
) -> AuthUserInfo | None:
@@ -118,6 +121,34 @@ async def authorize_user_from_key(
118121

119122
# do not pass
120123
return None
124+
125+
async def issue_jwt(self, username: str, password: str) -> str | None:
126+
# authenticate local user credentials and return a JWT token
127+
128+
# brutal search over users, which are stored in a simple dictionary.
129+
# waiting to have graph in core to store them properly
130+
# TODOAUTH: get rid of this shameful loop
131+
users = get_users()
132+
for user_id, user in users.items():
133+
if user["username"] == username and check_password(password, user["password"]):
134+
# TODOAUTH: expiration with timezone needs to be tested
135+
# using seconds for easier testing
136+
expire_delta_in_seconds = float(get_env("CCAT_JWT_EXPIRE_MINUTES")) * 60
137+
expires = datetime.now(utc) + timedelta(seconds=expire_delta_in_seconds)
138+
# TODOAUTH: add issuer and redirect_uri (and verify them when a token is validated)
139+
140+
jwt_content = {
141+
"sub": user_id, # Subject (the user ID)
142+
"username": username, # Username
143+
"permissions": user["permissions"], # User permissions
144+
"exp": expires # Expiry date as a Unix timestamp
145+
}
146+
return jwt.encode(
147+
jwt_content,
148+
get_env("CCAT_JWT_SECRET"),
149+
algorithm=get_env("CCAT_JWT_ALGORITHM"),
150+
)
151+
return None
121152

122153

123154
# Default Auth, always deny auth by default (only core auth decides).

core/cat/routes/auth.py

+6-40
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,15 @@
1-
from pytz import utc
21
import asyncio
32
from typing import Dict, List
43
from urllib.parse import urlencode
5-
from datetime import datetime, timedelta
6-
import jwt
74
from pydantic import BaseModel
85

96
from fastapi import APIRouter, Request, HTTPException, Response, status, Query
107
from fastapi.responses import RedirectResponse
118

129

13-
from cat.auth.utils import AuthPermission, AuthResource, get_full_permissions, check_password
14-
from cat.db.crud import get_users
15-
from cat.env import get_env
10+
from cat.auth.utils import AuthPermission, AuthResource, get_full_permissions
1611
from cat.routes.static.templates import get_jinja_templates
1712

18-
1913
router = APIRouter()
2014

2115
class UserCredentials(BaseModel):
@@ -34,7 +28,8 @@ async def core_login_token(request: Request, response: Response):
3428
form_data = await request.form()
3529

3630
# use username and password to authenticate user from local identity provider and get token
37-
access_token = await authenticate_local_user(
31+
auth_handler = request.app.state.ccat.core_auth_handler
32+
access_token = await auth_handler.issue_jwt(
3833
form_data["username"], form_data["password"]
3934
)
4035

@@ -86,13 +81,14 @@ async def get_available_permissions():
8681
return get_full_permissions()
8782

8883
@router.post("/token", response_model=JWTResponse)
89-
async def auth_token(credentials: UserCredentials):
84+
async def auth_token(request: Request, credentials: UserCredentials):
9085
"""Endpoint called from client to get a JWT from local identity provider.
9186
This endpoint receives username and password as form-data, validates credentials and issues a JWT.
9287
"""
9388

9489
# use username and password to authenticate user from local identity provider and get token
95-
access_token = await authenticate_local_user(
90+
auth_handler = request.app.state.ccat.core_auth_handler
91+
access_token = await auth_handler.issue_jwt(
9692
credentials.username, credentials.password
9793
)
9894

@@ -103,33 +99,3 @@ async def auth_token(credentials: UserCredentials):
10399
# wait a little to avoid brute force attacks
104100
await asyncio.sleep(1)
105101
raise HTTPException(status_code=403, detail={"error": "Invalid Credentials"})
106-
107-
108-
async def authenticate_local_user(username: str, password: str) -> str | None:
109-
# authenticate local user credentials and return a JWT token
110-
111-
# brutal search over users, which are stored in a simple dictionary.
112-
# waiting to have graph in core to store them properly
113-
# TODOAUTH: get rid of this shameful loop
114-
users = get_users()
115-
for user_id, user in users.items():
116-
if user["username"] == username and check_password(password, user["password"]):
117-
# TODOAUTH: expiration with timezone needs to be tested
118-
# using seconds for easier testing
119-
expire_delta_in_seconds = float(get_env("CCAT_JWT_EXPIRE_MINUTES")) * 60
120-
expires = datetime.now(utc) + timedelta(seconds=expire_delta_in_seconds)
121-
# TODOAUTH: add issuer and redirect_uri (and verify them when a token is validated)
122-
# TODOAUTH: move this method in auth.utils
123-
124-
jwt_content = {
125-
"sub": user_id, # Subject (the user ID)
126-
"username": username, # Username
127-
"permissions": user["permissions"], # User permissions
128-
"exp": expires # Expiry date as a Unix timestamp
129-
}
130-
return jwt.encode(
131-
jwt_content,
132-
get_env("CCAT_JWT_SECRET"),
133-
algorithm=get_env("CCAT_JWT_ALGORITHM"),
134-
)
135-
return None

core/tests/routes/auth/test_jwt.py

+2-6
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ async def test_issue_jwt(client):
3939
"password": "admin"
4040
}
4141
res = client.post("/auth/token", json=creds)
42-
4342
assert res.status_code == 200
4443

4544
# did we obtain a JWT?
@@ -52,11 +51,8 @@ async def test_issue_jwt(client):
5251
user_info = await auth_handler.authorize_user_from_jwt(
5352
received_token, AuthResource.ADMIN, AuthPermission.WRITE
5453
)
55-
assert user_info.user_id == "admin"
56-
assert user_info.user_data["username"] == "admin"
57-
assert (
58-
user_info.user_data["exp"] - time.time() < 60 * 60 * 24
59-
) # expires in less than 24 hours
54+
assert len(user_info.id) == 36 and len(user_info.id.split("-")) == 5 # uuid4
55+
assert user_info.name == "admin"
6056

6157
# manual JWT verification
6258
try:

0 commit comments

Comments
 (0)