import functools import os from urllib.parse import urlparse, urlunparse import jwt from authing import AuthenticationClient from fastapi import Request, Response from fastapi.responses import RedirectResponse from auth import BaseAuthPlugin class PingCAPAuthenticationClient: def __init__(self, auth_app_id, auth_app_host, auth_app_secret, redirect_uri=None): self.client = AuthenticationClient( auth_app_id, auth_app_host, auth_app_secret, redirect_uri=redirect_uri, ) def build_authorize_url(self, scope: str, state: str) -> str: return self.client.build_authorize_url(scope=scope, state=state) def get_token_by_code(self, code: str) -> str: r = self.client.get_access_token_by_code(code) if "access_token" not in r: raise ValueError(f"invalid code: {code}") return jwt.encode({"access_token": r["access_token"]}, "") def get_token_by_credentials( self, scope: str, access_key: str, secret_key: str ) -> str: r = self.client.get_access_token_by_client_credentials( scope, access_key, secret_key ) if "access_token" not in r: raise ValueError(f"invalid credentials: {access_key}") # the access_token is not useful in this case return jwt.encode( { "access_key": access_key, }, "", ) def get_user_identity_by_token(self, token: str) -> str: payload = jwt.decode(token, options={"verify_signature": False}) if "access_key" in payload: return payload["access_key"] if "access_token" in payload: user_info = self.client.get_user_info_by_access_token( payload["access_token"] ) return user_info["profile"] raise ValueError(f"invalid token: {token}") class PingCAPAuth(BaseAuthPlugin): def __init__(self): self.auth_app_id = os.environ["AUTH_APP_ID"] self.auth_app_host = os.environ["AUTH_APP_HOST"] self.auth_app_secret = os.environ["AUTH_APP_SECRET"] def authorize_url(self, request: Request) -> str: referer = request.headers.get("referer") if not referer: return None r = urlparse(referer) redirect_uri = urlunparse( (r.scheme, r.netloc, "/linguflow-api/login", None, None, None) ) auth_client = PingCAPAuthenticationClient( self.auth_app_id, self.auth_app_host, self.auth_app_secret, redirect_uri=redirect_uri, ) return auth_client.build_authorize_url(scope="profile openid", state=referer) def login(self, request: Request) -> Response: state = request.query_params["state"] r = urlparse(state) auth_client = PingCAPAuthenticationClient( self.auth_app_id, self.auth_app_host, self.auth_app_secret, redirect_uri=urlunparse( (r.scheme, r.netloc, "/linguflow-api/login", None, None, None) ), ) code = request.query_params.get("code") if not code: return {"status": "error", "message": "no code in query params"}, 400 token = auth_client.get_token_by_code(code) user = auth_client.get_user_identity_by_token(token) response = RedirectResponse(url=state) response.set_cookie( key="token", value=jwt.encode({"user": user}, self.auth_app_secret, algorithm="HS256"), ) return response def user_identity(self, request: Request) -> str: user = request.headers.get("x-linguflow-user") access_key = request.headers.get("x-linguflow-access-key") access_secret = request.headers.get("x-linguflow-access-secret") if user and self.check_credentials(access_key, access_secret): return user token = request.cookies.get("token") if not token: return None try: credentials = jwt.decode(token, self.auth_app_secret, algorithms=["HS256"]) return credentials["user"] except: return None def message(self, request: Request) -> str: return ( "please provide valid x-linguflow-access-key, " "x-linguflow-access-secret and x-linguflow-user headers" ) @functools.lru_cache def check_credentials(self, access_key: str, access_secret: str) -> bool: auth_client = PingCAPAuthenticationClient( self.auth_app_id, self.auth_app_host, self.auth_app_secret, ) try: auth_client.get_token_by_credentials( "profile openid", access_key, access_secret ) return True except Exception as e: return False