diff --git a/.vscode/settings.json b/.vscode/settings.json index 1e4a1e0..ee29726 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,6 +2,8 @@ "cSpell.words": [ "altinstall", "awscliv", + "boto", + "cabletv", "certbot", "certifi", "devel", @@ -11,6 +13,8 @@ "gitea", "iptv", "nohup", + "passlib", + "starlette", "stefano", "uvicorn", "venv" diff --git a/app/cabletv/utils/auth.py b/app/cabletv/utils/auth.py new file mode 100644 index 0000000..10678bc --- /dev/null +++ b/app/cabletv/utils/auth.py @@ -0,0 +1,36 @@ +from fastapi import Depends, HTTPException, status +from fastapi.security import OAuth2AuthorizationCodeBearer +from fastapi.responses import RedirectResponse +from typing import Optional +import os +import boto3 + +REGION = "us-east-2" +USER_POOL_ID = os.getenv("COGNITO_USER_POOL_ID") +CLIENT_ID = os.getenv("COGNITO_CLIENT_ID") +DOMAIN = f"https://iptv-updater.auth.{REGION}.amazoncognito.com" + +oauth2_scheme = OAuth2AuthorizationCodeBearer( + authorizationUrl=f"{DOMAIN}/oauth2/authorize", + tokenUrl=f"{DOMAIN}/oauth2/token" +) + +async def get_current_user(token: str = Depends(oauth2_scheme)): + if not token: + return RedirectResponse( + f"{DOMAIN}/login?client_id={CLIENT_ID}" + f"&response_type=code" + f"&scope=openid" + f"&redirect_uri=http://localhost:8000/auth/callback" + ) + + try: + cognito = boto3.client('cognito-idp', region_name=REGION) + response = cognito.get_user(AccessToken=token) + return response + except Exception as e: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid authentication credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) \ No newline at end of file diff --git a/app/main.py b/app/main.py index 1102a0e..b4db058 100644 --- a/app/main.py +++ b/app/main.py @@ -1,46 +1,21 @@ -import os -import json -import boto3 -from jose import jwt from fastapi import FastAPI, Depends, HTTPException -from fastapi.security import OAuth2AuthorizationCodeBearer +from fastapi.responses import RedirectResponse +from app.cabletv.utils.auth import get_current_user, DOMAIN, CLIENT_ID app = FastAPI() -# Get Cognito info from environment (set by userdata.sh) -REGION = "us-east-2" -USER_POOL_ID = os.getenv("COGNITO_USER_POOL_ID") -CLIENT_ID = os.getenv("COGNITO_CLIENT_ID") - -# OAuth2 scheme for authorization code flow -oauth2_scheme = OAuth2AuthorizationCodeBearer( - authorizationUrl=f"https://cognito-idp.{REGION}.amazonaws.com/{USER_POOL_ID}/oauth2/authorize", - tokenUrl=f"https://cognito-idp.{REGION}.amazonaws.com/{USER_POOL_ID}/oauth2/token" -) - -async def get_current_user(token: str = Depends(oauth2_scheme)): - try: - # Verify the JWT token with Cognito - cognito_idp = boto3.client('cognito-idp', region_name=REGION) - response = cognito_idp.get_user( - AccessToken=token - ) - return response - except Exception as e: - raise HTTPException( - status_code=401, - detail="Invalid authentication credentials", - headers={"WWW-Authenticate": "Bearer"}, - ) - @app.get("/") async def root(): return {"message": "IPTV Updater API"} -@app.get("/health") -async def health(): - return {"status": "healthy"} - @app.get("/protected") async def protected_route(user = Depends(get_current_user)): - return {"message": "This is a protected route", "user": user['Username']} \ No newline at end of file + if isinstance(user, RedirectResponse): + return user + return {"message": "Protected content", "user": user['Username']} + +@app.get("/auth/callback") +async def auth_callback(code: str): + # Here you would exchange the code for tokens + # For now, just redirect to protected route + return {"auth_code": code} \ No newline at end of file diff --git a/infrastructure/stack.py b/infrastructure/stack.py index 57cea57..98c07cf 100644 --- a/infrastructure/stack.py +++ b/infrastructure/stack.py @@ -70,20 +70,13 @@ class IptvUpdaterStack(Stack): "AmazonSSMManagedInstanceCore" ) ) - - # Read the userdata script with proper path resolution - script_dir = os.path.dirname(os.path.abspath(__file__)) - userdata_path = os.path.join(script_dir, "userdata.sh") - userdata_file = open(userdata_path, "rb").read() - # Creates a userdata object for Linux hosts - userdata = ec2.UserData.for_linux() - # Adds one or more commands to the userdata object. - userdata.add_commands( - f'echo "COGNITO_USER_POOL_ID={user_pool.user_pool_id}" >> /etc/environment', - f'echo "COGNITO_CLIENT_ID={client.user_pool_client_id}" >> /etc/environment' + # Add Cognito permissions to instance role + role.add_managed_policy( + iam.ManagedPolicy.from_aws_managed_policy_name( + "AmazonCognitoReadOnly" + ) ) - userdata.add_commands(str(userdata_file, 'utf-8')) # EC2 Instance instance = ec2.Instance( @@ -98,8 +91,7 @@ class IptvUpdaterStack(Stack): ), security_group=security_group, key_pair=key_pair, - role=role, - user_data=userdata, + role=role ) # Create Elastic IP @@ -117,37 +109,55 @@ class IptvUpdaterStack(Stack): password_policy=cognito.PasswordPolicy( min_length=8, require_lowercase=True, - require_numbers=True, + require_digits=True, require_symbols=True, require_uppercase=True ), account_recovery=cognito.AccountRecovery.EMAIL_ONLY ) - # Add App Client + # Add App Client with the correct callback URL client = user_pool.add_client("IptvUpdaterClient", o_auth=cognito.OAuthSettings( flows=cognito.OAuthFlows( authorization_code_grant=True ), scopes=[cognito.OAuthScope.OPENID], - callback_urls=[f"https://{instance.instance_public_dns_name}/auth/callback"] + callback_urls=[ + "http://localhost:8000/auth/callback", # For local testing + "https://*.amazonaws.com/auth/callback" # Will match EC2 public DNS + ] ) ) - # Add Cognito permissions to instance role - role.add_managed_policy( - iam.ManagedPolicy.from_aws_managed_policy_name( - "AmazonCognitoReadOnly" + # Add domain for hosted UI + domain = user_pool.add_domain("IptvUpdaterDomain", + cognito_domain=cognito.CognitoDomainOptions( + domain_prefix="iptv-updater" ) ) + + # Read the userdata script with proper path resolution + script_dir = os.path.dirname(os.path.abspath(__file__)) + userdata_path = os.path.join(script_dir, "userdata.sh") + userdata_file = open(userdata_path, "rb").read() - # Output the public DNS name - CfnOutput( - self, "InstancePublicDNS", - value=instance.instance_public_dns_name, + # Creates a userdata object for Linux hosts + userdata = ec2.UserData.for_linux() + # Adds one or more commands to the userdata object. + userdata.add_commands( + f'echo "COGNITO_USER_POOL_ID={user_pool.user_pool_id}" >> /etc/environment', + f'echo "COGNITO_CLIENT_ID={client.user_pool_client_id}" >> /etc/environment' ) + userdata.add_commands(str(userdata_file, 'utf-8')) - # Output Cognito information + # Update instance with userdata + instance.add_user_data(userdata.render()) + + # Outputs + CfnOutput(self, "InstancePublicIP", value=eip.attr_public_ip) CfnOutput(self, "UserPoolId", value=user_pool.user_pool_id) - CfnOutput(self, "UserPoolClientId", value=client.user_pool_client_id) \ No newline at end of file + CfnOutput(self, "UserPoolClientId", value=client.user_pool_client_id) + CfnOutput(self, "CognitoDomainUrl", + value=f"https://{domain.domain_name}.auth.{self.region}.amazoncognito.com" + ) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index dddbf35..58cf331 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,5 +5,6 @@ dotenv==0.9.9 python-dotenv==0.21.1 uvicorn==0.22.0 requests==2.31.0 -python-jose[cryptography]==3.3.0 -boto3==1.28.0 \ No newline at end of file +passlib[bcrypt]==1.7.4 +boto3==1.28.0 +starlette>=0.27.0 \ No newline at end of file