From a1948d1a69034417251f10eb9cf6484ed91762e5 Mon Sep 17 00:00:00 2001 From: Mark Hammell Date: Sun, 3 Jul 2022 16:42:05 -0400 Subject: [PATCH 1/4] Bugfixes to handle changes to Personal Capital CSRF token parsing --- personalcapital/personalcapital.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/personalcapital/personalcapital.py b/personalcapital/personalcapital.py index 7c79795..7a210b5 100644 --- a/personalcapital/personalcapital.py +++ b/personalcapital/personalcapital.py @@ -1,8 +1,10 @@ import requests import re -csrf_regexp = re.compile(r"globals.csrf='([a-f0-9-]+)'") +csrf_regexp = re.compile(r"window.csrf ='([a-f0-9-]+)'") +user_agent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36' base_url = 'https://home.personalcapital.com' +ident_endpoint = base_url + '/page/login/goHome' api_endpoint = base_url + '/api' SP_HEADER_KEY = "spHeader" @@ -39,11 +41,16 @@ class LoginFailedException(Exception): class PersonalCapital(object): def __init__(self): self.__session = requests.Session() + self.__session.headers.update({'user-agent': user_agent}) self.__csrf = "" def login(self, username, password): - initial_csrf = self.__get_csrf_from_home_page(base_url) + initial_csrf = self.__get_csrf_from_home_page(ident_endpoint) + if initial_csrf is None: + LoginFailedException("Unable to extract initial CSRF token") csrf, auth_level = self.__identify_user(username, initial_csrf) + if csrf is None or auth_level is None: + LoginFailedException("Unable to extract CSRF token and/or user auth level") if csrf and auth_level: self.__csrf = csrf @@ -175,7 +182,7 @@ def __authenticate_sms(self, code): def __authenticate_password(self, passwd): data = { "bindDevice": "true", - "deviceName": "", + "deviceName": "Personal Capital Python API", "redirectTo": "", "skipFirstUse": "", "skipLinkAccount": "false", From 50fc8400b5f47f60f2123c2bc5ec063fa39f5667 Mon Sep 17 00:00:00 2001 From: hammem Date: Sun, 10 Jul 2022 10:20:51 -0400 Subject: [PATCH 2/4] Add support for saving and loading sessions Adds methods for saving and loading a session, to avoid having to authenticate each time the API is used in a new execution, kernel instance, etc. Both the session cookies and CSRF token are stored to a file via Python's bulit-in pickling. Both methods take a file name param, to define where the data should be written or read. --- personalcapital/personalcapital.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/personalcapital/personalcapital.py b/personalcapital/personalcapital.py index 7a210b5..52f66df 100644 --- a/personalcapital/personalcapital.py +++ b/personalcapital/personalcapital.py @@ -1,3 +1,4 @@ +import pickle import requests import re @@ -50,7 +51,7 @@ def login(self, username, password): LoginFailedException("Unable to extract initial CSRF token") csrf, auth_level = self.__identify_user(username, initial_csrf) if csrf is None or auth_level is None: - LoginFailedException("Unable to extract CSRF token and/or user auth level") + LoginFailedException("Unable to extract CSRF token and user auth level") if csrf and auth_level: self.__csrf = csrf @@ -107,9 +108,23 @@ def set_session(self, cookies): """ self.__session.cookies = requests.utils.cookiejar_from_dict(cookies) - # private methods - + def save_session(self, filename): + session_data = { + "csrf": self.__csrf, + "cookies": self.__session.cookies._cookies, + } + with open(filename, 'wb') as fh: + pickle.dump(session_data, fh) + + def load_session(self, filename): + with open(filename, 'rb') as fh: + data = pickle.load(fh) + jar = requests.cookies.RequestsCookieJar() + jar._cookies = data["cookies"] + self.__session.cookies = jar + self.__csrf = data["csrf"] + # private methods def __get_csrf_from_home_page(self, url): r = self.__session.get(url) found_csrf = csrf_regexp.search(r.text) @@ -192,3 +207,4 @@ def __authenticate_password(self, passwd): "csrf": self.__csrf } return self.post("/credential/authenticatePassword", data) + \ No newline at end of file From 1683fdb18f2eb0c0bdaa65738b9879eb105a8bdb Mon Sep 17 00:00:00 2001 From: KL4RKS <2390741+KL4RKS@users.noreply.github.com> Date: Tue, 28 Nov 2023 17:51:21 +0000 Subject: [PATCH 3/4] Add support for Multi-Factor Authentication Every Sign In setting --- personalcapital/personalcapital.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/personalcapital/personalcapital.py b/personalcapital/personalcapital.py index 52f66df..3388994 100644 --- a/personalcapital/personalcapital.py +++ b/personalcapital/personalcapital.py @@ -27,6 +27,7 @@ def getErrorValue(result): class AuthLevelEnum(object): USER_REMEMBERED = "USER_REMEMBERED" + MFA_REQUIRED = "MFA_REQUIRED" class TwoFactorVerificationModeEnum(object): SMS = 0 @@ -60,6 +61,8 @@ def login(self, username, password): result = self.__authenticate_password(password).json() if getSpHeaderValue(result, SUCCESS_KEY) == False: raise LoginFailedException(getErrorValue(result)) + elif getSpHeaderValue(result, AUTH_LEVEL_KEY) == AuthLevelEnum.MFA_REQUIRED: + raise RequireTwoFactorException() else: raise LoginFailedException() @@ -207,4 +210,4 @@ def __authenticate_password(self, passwd): "csrf": self.__csrf } return self.post("/credential/authenticatePassword", data) - \ No newline at end of file + From 41a530c137acdac80f2226a57ba3d43e3618f76b Mon Sep 17 00:00:00 2001 From: traviscook Date: Mon, 29 Dec 2025 15:06:36 -0800 Subject: [PATCH 4/4] Update base-url using new ref in issue: https://github.com/haochi/personalcapital/issues/28 --- personalcapital/personalcapital.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/personalcapital/personalcapital.py b/personalcapital/personalcapital.py index 3388994..912b659 100644 --- a/personalcapital/personalcapital.py +++ b/personalcapital/personalcapital.py @@ -4,7 +4,7 @@ csrf_regexp = re.compile(r"window.csrf ='([a-f0-9-]+)'") user_agent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36' -base_url = 'https://home.personalcapital.com' +base_url = 'https://pc-api.empower-retirement.com' ident_endpoint = base_url + '/page/login/goHome' api_endpoint = base_url + '/api'