""" Demo using pyotp and auth0 to generate a bearer token using a one-time-password. To run: # Update the .env. values cp .env.shadow .env # run the command ./env/bin/python3 main.py --email="pdptest+mfa@theorchard.com" \ --password="" \ --otp-secret-key="otp-secret-key" """ import argparse import requests import pyotp from environs import Env def read_env(): """Read environment variables from .env file.""" env = Env() env.read_env() return { "AUTH_AUDIENCE": env.str("AUTH_AUDIENCE"), "AUTH_URL": env.str("AUTH_URL"), "CLIENT_ID": env.str("CLIENT_ID"), "CLIENT_SECRET": env.str("CLIENT_SECRET"), } # Load environment variables _env_vars = read_env() AUTH_AUDIENCE = _env_vars["AUTH_AUDIENCE"] AUTH_URL = _env_vars["AUTH_URL"] CLIENT_ID = _env_vars["CLIENT_ID"] CLIENT_SECRET = _env_vars["CLIENT_SECRET"] class MfaTokenLookupError(Exception): """ Raised when multifactor authentication fails due to missing credentials or a 400 error returned by the `/oauth/token` Auth0 endpoint. """ pass def generate_bearer_jwt_token_mfa( otp_secret_key: str, mfa_token: str, ) -> str: """Generate an auth token for the given UserCreds, Auth0Creds and mfa_token.""" print(f"otp_secret_key: {otp_secret_key[:5]}...") print(f"mfa_token: {mfa_token[:10]}...") # Raise an error when `otp_secret_key` is falsy or is the string "null". if (not otp_secret_key) or (otp_secret_key == "null"): raise MfaTokenLookupError("UserCreds.otp_secret_key is empty") if not mfa_token: raise MfaTokenLookupError("mfa_token is empty") # Generate one-time-passcode one_time_password = pyotp.TOTP(otp_secret_key).now() print(f"one_time_password: {one_time_password}") data = { "grant_type": "http://auth0.com/oauth/grant-type/mfa-otp", "client_id": CLIENT_ID, "client_secret": CLIENT_SECRET, "mfa_token": mfa_token, "otp": one_time_password, } r = requests.post(AUTH_URL, data=data) if r.status_code != 200: raise MfaTokenLookupError( f"Auth0 error occurred (status {r.status_code}): {r.text}" ) resp = r.json() return str(resp["access_token"]) def parse_args(): """Parse command line arguments.""" parser = argparse.ArgumentParser(description="Generate bearer JWT token with MFA support") parser.add_argument("--email", required=True, help="User email address") parser.add_argument("--password", required=True, help="User password") parser.add_argument("--otp-secret-key", required=True, help="OTP secret key for MFA") return parser.parse_args() def generate_bearer_jwt_token(email: str, password: str, otp_secret_key: str) -> str: """Generate an auth token for the given UserCreds and Auth0Creds.""" data = { "grant_type": "password", "username": email, "password": password, "audience": AUTH_AUDIENCE, "scope": "", "client_id": CLIENT_ID, "client_secret": CLIENT_SECRET, } r = requests.post(AUTH_URL, data=data) resp = r.json() if "error" in resp and resp["error"] == "mfa_required": return generate_bearer_jwt_token_mfa( otp_secret_key=otp_secret_key, mfa_token=resp["mfa_token"] ) elif "error" in resp: raise ValueError(f"Auth0 error occurred: {resp}") return str(resp["access_token"]) def main(): """Main function to parse arguments and generate bearer token.""" args = parse_args() token = generate_bearer_jwt_token( email=args.email, password=args.password, otp_secret_key=args.otp_secret_key ) print(f"Bearer token: {token[:10]}...") if __name__ == "__main__": main()