From fb46342a4650481ba8057c785d8b7554bbc9ea66 Mon Sep 17 00:00:00 2001 From: Andres Javier Lopez Date: Fri, 18 Apr 2025 17:30:35 -0600 Subject: [PATCH 1/7] Add cli command to generate app token --- taskmaster/__main__.py | 1 + taskmaster/auth/cli.py | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/taskmaster/__main__.py b/taskmaster/__main__.py index d96b165..75d1294 100644 --- a/taskmaster/__main__.py +++ b/taskmaster/__main__.py @@ -20,6 +20,7 @@ def cli(): cli.add_command(auth_cli.add_user) cli.add_command(auth_cli.change_user_password) +cli.add_command(auth_cli.generate_app_token) if __name__ == "__main__": diff --git a/taskmaster/auth/cli.py b/taskmaster/auth/cli.py index e432f9c..0c1230c 100644 --- a/taskmaster/auth/cli.py +++ b/taskmaster/auth/cli.py @@ -1,8 +1,10 @@ import asyncio +from datetime import timedelta from uuid import UUID import click +from taskmaster.auth.token import Token, create_refresh_token from taskmaster.database.managers import UserManager from taskmaster.schemas.users import User @@ -34,3 +36,14 @@ def change_user_password(user_id): password = click.prompt("Password", hide_input=True) asyncio.run(_change_password(user_id, password)) click.echo(f"Password for user {user_id} changed") + + +@click.command() +@click.option("--app-name", prompt="App name") +def generate_app_token(app_name): + token = Token.create_with_username(app_name) + refresh_token = create_refresh_token( + token, + expires_delta=timedelta(days=60), + ) + click.echo(f"App token for {app_name}: {refresh_token}") From fdf6f6d5c3c20ea395914a46bcc033871e209a80 Mon Sep 17 00:00:00 2001 From: Andres Javier Lopez Date: Fri, 18 Apr 2025 18:11:16 -0600 Subject: [PATCH 2/7] Add endpoints to generate access token for user --- taskmaster/auth/cli.py | 1 + taskmaster/auth/endpoints.py | 76 ++++++++++++++++++++++++++++++++++++ taskmaster/auth/token.py | 5 +++ 3 files changed, 82 insertions(+) diff --git a/taskmaster/auth/cli.py b/taskmaster/auth/cli.py index 0c1230c..8d4cd6a 100644 --- a/taskmaster/auth/cli.py +++ b/taskmaster/auth/cli.py @@ -42,6 +42,7 @@ def change_user_password(user_id): @click.option("--app-name", prompt="App name") def generate_app_token(app_name): token = Token.create_with_username(app_name) + token.scopes = ["app-token"] refresh_token = create_refresh_token( token, expires_delta=timedelta(days=60), diff --git a/taskmaster/auth/endpoints.py b/taskmaster/auth/endpoints.py index e554daa..039db1c 100644 --- a/taskmaster/auth/endpoints.py +++ b/taskmaster/auth/endpoints.py @@ -15,6 +15,7 @@ from .fb import get_authorization_url, get_fb_info, get_fb_info_from_token from .token import ( AccessTokenInput, + PhoneTokenInput, RefreshTokenInput, Token, TokenResponse, @@ -101,6 +102,81 @@ async def refresh_authentication_token( ) +@router.post("/app-token/refresh", response_model=TokenResponse) +async def get_app_token(refresh_token_input: RefreshTokenInput): + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + + refresh_token = refresh_token_input.refresh_token + + try: + token = Token.decode_token(refresh_token) + except InvalidTokenError: + raise credentials_exception + + if "app-token" not in token.scopes: + raise credentials_exception + + if "refresh-token" not in token.scopes: + raise credentials_exception + + token_data = Token.create_with_username(token.username) + token_data.scopes = ["app-token"] + access_token = create_access_token(token_data, fresh=False) + refresh_token = create_refresh_token(token_data) + + return TokenResponse( + access_token=access_token, + refresh_token=refresh_token, + token_type="bearer", + ) + + +@router.post("/app-token/user_phone") +async def get_user_token_with_app_token_and_phone_number( + session: DBSession, + phone_token_input: PhoneTokenInput, +): + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + + try: + app_token = Token.decode_token(phone_token_input.app_token) + except InvalidTokenError: + raise credentials_exception + + if "app-token" not in app_token.scopes: + raise credentials_exception + + stmt = select(UserModel).where( + UserModel.phone_number == phone_token_input.phone_number + ) + results = await session.execute(stmt) + user = results.scalar_one_or_none() + + if not user: + raise credentials_exception + + if user.disabled: + raise credentials_exception + + token = Token.create_with_username(user.uuid) + token.scopes = [app_token.username] + access_token = create_access_token(token, fresh=False) + + return TokenResponse( + access_token=access_token, + refresh_token=None, + token_type="bearer", + ) + + @router.get("/auth/fb") async def facebook_login(): authorization_url = await get_authorization_url() diff --git a/taskmaster/auth/token.py b/taskmaster/auth/token.py index 20ed96f..7498123 100644 --- a/taskmaster/auth/token.py +++ b/taskmaster/auth/token.py @@ -43,6 +43,11 @@ class RefreshTokenInput(BaseModel): refresh_token: str = Field(alias="refreshToken") +class PhoneTokenInput(BaseModel): + app_token: str = Field(alias="appToken") + phone_number: str = Field(alias="phoneNumber") + + class TokenResponse(BaseModel): access_token: str refresh_token: str | None From 51d9b4b1fb44dc1640107b5b7fa27255f3f4c811 Mon Sep 17 00:00:00 2001 From: Andres Javier Lopez Date: Fri, 18 Apr 2025 18:20:50 -0600 Subject: [PATCH 3/7] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e59d28..4b85e87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # CHANGELOG ## CURRENT +* Add app token validation ## v1.1.1 * Update dependencies From 2b9a61e180910460ba22e2fa9c1cf34248e94dbb Mon Sep 17 00:00:00 2001 From: Andres Javier Lopez Date: Sat, 19 Apr 2025 10:17:10 -0600 Subject: [PATCH 4/7] Create users using phone number with app endpoint --- taskmaster/auth/endpoints.py | 9 ++++++++- taskmaster/auth/token.py | 1 + 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/taskmaster/auth/endpoints.py b/taskmaster/auth/endpoints.py index 039db1c..62e66cc 100644 --- a/taskmaster/auth/endpoints.py +++ b/taskmaster/auth/endpoints.py @@ -161,7 +161,14 @@ async def get_user_token_with_app_token_and_phone_number( user = results.scalar_one_or_none() if not user: - raise credentials_exception + # If user does not exists, let's create one with this phone number + user = UserModel( + name=phone_token_input.username, + phone_number=phone_token_input.phone_number, + ) + session.add(user) + await session.commit() + await session.refresh(user) if user.disabled: raise credentials_exception diff --git a/taskmaster/auth/token.py b/taskmaster/auth/token.py index 7498123..c1ab8af 100644 --- a/taskmaster/auth/token.py +++ b/taskmaster/auth/token.py @@ -46,6 +46,7 @@ class RefreshTokenInput(BaseModel): class PhoneTokenInput(BaseModel): app_token: str = Field(alias="appToken") phone_number: str = Field(alias="phoneNumber") + username: str class TokenResponse(BaseModel): From a67337e647ef222724c07d6337c421f5dc7218ea Mon Sep 17 00:00:00 2001 From: Andres Javier Lopez Date: Sat, 19 Apr 2025 10:28:50 -0600 Subject: [PATCH 5/7] Change expiration of app tokens --- taskmaster/auth/cli.py | 6 ++---- taskmaster/auth/endpoints.py | 7 +++++-- taskmaster/auth/token.py | 11 +++++++++++ 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/taskmaster/auth/cli.py b/taskmaster/auth/cli.py index 8d4cd6a..654d89d 100644 --- a/taskmaster/auth/cli.py +++ b/taskmaster/auth/cli.py @@ -4,7 +4,7 @@ import click -from taskmaster.auth.token import Token, create_refresh_token +from taskmaster.auth.token import Token, create_app_token from taskmaster.database.managers import UserManager from taskmaster.schemas.users import User @@ -42,9 +42,7 @@ def change_user_password(user_id): @click.option("--app-name", prompt="App name") def generate_app_token(app_name): token = Token.create_with_username(app_name) - token.scopes = ["app-token"] - refresh_token = create_refresh_token( + refresh_token = create_app_token( token, - expires_delta=timedelta(days=60), ) click.echo(f"App token for {app_name}: {refresh_token}") diff --git a/taskmaster/auth/endpoints.py b/taskmaster/auth/endpoints.py index 62e66cc..31f6a5b 100644 --- a/taskmaster/auth/endpoints.py +++ b/taskmaster/auth/endpoints.py @@ -126,11 +126,10 @@ async def get_app_token(refresh_token_input: RefreshTokenInput): token_data = Token.create_with_username(token.username) token_data.scopes = ["app-token"] access_token = create_access_token(token_data, fresh=False) - refresh_token = create_refresh_token(token_data) return TokenResponse( access_token=access_token, - refresh_token=refresh_token, + refresh_token=None, token_type="bearer", ) @@ -154,6 +153,10 @@ async def get_user_token_with_app_token_and_phone_number( if "app-token" not in app_token.scopes: raise credentials_exception + if "refresh-token" in app_token.scopes: + # Don't allow refresh token to be used + raise credentials_exception + stmt = select(UserModel).where( UserModel.phone_number == phone_token_input.phone_number ) diff --git a/taskmaster/auth/token.py b/taskmaster/auth/token.py index c1ab8af..74c0cf2 100644 --- a/taskmaster/auth/token.py +++ b/taskmaster/auth/token.py @@ -85,3 +85,14 @@ def create_refresh_token(data: Token, expires_delta: timedelta | None = None): to_encode.update({"exp": expire, "scope": scope}) encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) return encoded_jwt + + +def create_app_token(data: Token): + """Create an app token that doesn't expire""" + to_encode = data.model_dump(exclude={"scopes"}) + scopes = data.scopes.copy() + scopes.extend(["refresh-token", "app-token"]) + scope = " ".join(scopes) + to_encode.update({"scope": scope}) + encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + return encoded_jwt From bf622d1afe3c35248956136e043fd316e6320362 Mon Sep 17 00:00:00 2001 From: Andres Javier Lopez Date: Sat, 19 Apr 2025 15:06:17 -0600 Subject: [PATCH 6/7] Improve code readability --- taskmaster/auth/cli.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/taskmaster/auth/cli.py b/taskmaster/auth/cli.py index 654d89d..535ccc3 100644 --- a/taskmaster/auth/cli.py +++ b/taskmaster/auth/cli.py @@ -42,7 +42,5 @@ def change_user_password(user_id): @click.option("--app-name", prompt="App name") def generate_app_token(app_name): token = Token.create_with_username(app_name) - refresh_token = create_app_token( - token, - ) - click.echo(f"App token for {app_name}: {refresh_token}") + app_token = create_app_token(token) + click.echo(f"App token for {app_name}: {app_token}") From ad2cede1a917a8c9ebac26388db16f23539d6b5e Mon Sep 17 00:00:00 2001 From: Andres Javier Lopez Date: Sat, 19 Apr 2025 15:47:51 -0600 Subject: [PATCH 7/7] Bump to version 1.2.0 --- CHANGELOG.md | 2 ++ pyproject.toml | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b85e87..b6ac6c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # CHANGELOG ## CURRENT + +## v1.2.0 * Add app token validation ## v1.1.1 diff --git a/pyproject.toml b/pyproject.toml index 6a66da1..3700d3a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "taskmaster" -version = "1.1.1" +version = "1.2.0" description = "" authors = ["Andres Javier Lopez "] readme = "README.md"