def _refresh_oauth2(self) -> None: """Refresh an oauth2 token. Raises: AuthenticationError: If refresh_token is missing or if there is an error when trying to refresh a token. """ if self.refresh_token is None: raise AuthenticationError( "Cannot refresh login with unset refresh token") relogin_payload = { "grant_type": "refresh_token", "refresh_token": self.refresh_token, "scope": "internal", "client_id": CLIENT_ID, "expires_in": EXPIRATION_TIME, } self.session.headers.pop("Authorization", None) res = self.post(OAUTH_TOKEN_URL, data=relogin_payload, auto_login=False, raise_errors=False) if "error" in res: raise AuthenticationError("Failed to refresh token") self._process_auth_body(res)
def _refresh_oauth2(self) -> None: """Refresh an oauth2 token. Raises: AuthenticationError: If refresh_token is missing or if there is an error when trying to refresh a token. """ if not self.oauth.is_valid: raise AuthenticationError( "Cannot refresh login with unset refresh token") relogin_payload = { "grant_type": "refresh_token", "refresh_token": self.oauth.refresh_token, "scope": "internal", "client_id": CLIENT_ID, "expires_in": EXPIRATION_TIME, } self.session.headers.pop("Authorization", None) try: oauth = self.post( urls.OAUTH, data=relogin_payload, auto_login=False, schema=OAuthSchema(), ) except HTTPError: raise AuthenticationError("Failed to refresh token") self._configure_manager(oauth)
def _challenge_oauth2(self, oauth: OAuth, oauth_payload: JSON) -> OAuth: """Process the ouath challenge flow. Args: oauth: An oauth response model from a login request. oauth_payload: The payload to use once the challenge has been processed. Returns: An OAuth response model from the login request. Raises: AuthenticationError: If there is an error in the initial challenge response. .. # noqa: DAR202 .. https://github.com/terrencepreilly/darglint/issues/81 """ # login challenge challenge_url = urls.build_challenge(oauth.challenge.id) print( f"Input challenge code from {oauth.challenge.type.capitalize()} " f"({oauth.challenge.remaining_attempts}/" f"{oauth.challenge.remaining_retries}):" ) challenge_code = input() challenge_payload = {"response": str(challenge_code)} challenge_header = CaseInsensitiveDict( {"X-ROBINHOOD-CHALLENGE-RESPONSE-ID": str(oauth.challenge.id)} ) oauth_inner, res = self.post( challenge_url, data=challenge_payload, raise_errors=False, headers=challenge_header, auto_login=False, return_response=True, schema=OAuthSchema(), ) if res.status_code == requests.codes.ok: try: # the cast is required for mypy return cast( OAuth, self.post( urls.OAUTH, data=oauth_payload, headers=challenge_header, auto_login=False, schema=OAuthSchema(), ), ) except HTTPError: raise AuthenticationError("Error in finalizing auth token") elif oauth_inner.is_challenge and oauth_inner.challenge.can_retry: print("Invalid code entered") return self._challenge_oauth2(oauth, oauth_payload) else: raise AuthenticationError("Exceeded available attempts or code expired")
def _login_oauth2(self) -> None: """Create a new oauth2 token. Raises: AuthenticationError: If the login credentials are not set, if a challenge wasn't accepted, or if an mfa code is not accepted. """ self.session.headers.pop("Authorization", None) oauth_payload = { "password": self.password, "username": self.username, "grant_type": "password", "client_id": CLIENT_ID, "expires_in": EXPIRATION_TIME, "scope": "internal", "device_token": "4cf700d5-933b-4aff-86b5-33ddd0e93cb4" } if self.challenge_type != "none": oauth_payload = { "password": self.password, "username": self.username, "grant_type": "password", "client_id": CLIENT_ID, "expires_in": EXPIRATION_TIME, "scope": "internal", "device_token": self.device_token, "challenge_type": self.challenge_type } oauth = self.post( urls.OAUTH, data=oauth_payload, raise_errors=False, auto_login=False, schema=OAuthSchema(), ) if oauth.is_challenge: oauth = self._challenge_oauth2(oauth, oauth_payload) elif oauth.is_mfa: oauth = self._mfa_oauth2(oauth_payload) if not oauth.is_valid: if hasattr(oauth, "error"): msg = f"{oauth.error}" elif hasattr(oauth, "detail"): msg = f"{oauth.detail}" else: msg = "Unknown login error" raise AuthenticationError(msg) else: self._configure_manager(oauth)
def logout(self) -> None: """Logout from the session. Raises: AuthenticationError: If there is an error when logging out. """ logout_payload = {"client_id": CLIENT_ID, "token": self.oauth.refresh_token} try: self.post(urls.OAUTH_REVOKE, data=logout_payload, auto_login=False) self.oauth = OAuth() self.session.headers.pop("Authorization", None) except HTTPError: raise AuthenticationError("Could not log out")
def logout(self) -> None: """Logout from the session. Raises: AuthenticationError: If there is an error when logging out. """ logout_payload = { "client_id": CLIENT_ID, "token": self.refresh_token, } res = self.post(OAUTH_REVOKE_URL, data=logout_payload, raise_errors=False, auto_login=False) if len(res) == 0: self.access_token = None self.refresh_token = None else: raise AuthenticationError("Could not log out")
def _process_auth_body(self, res: Dict) -> None: """Process an authentication response dictionary. This method updates the internal state of the session based on a login or token refresh request. Args: res: A response dictionary from a login request. Raises: AuthenticationError: If the input dictionary is malformed. """ try: self.access_token = res["access_token"] self.refresh_token = res["refresh_token"] self.expires_at = datetime.now() + timedelta( seconds=EXPIRATION_TIME) self.session.headers.update( {"Authorization": f"Bearer {self.access_token}"}) except KeyError: raise AuthenticationError( "Authorization result body missing required responses.")
def _mfa_oauth2(self, oauth_payload: JSON, attempts: int = 3) -> OAuth: """Mfa auth flow. For people with 2fa. Args: oauth_payload: JSON payload to send on mfa approval. attempts: The number of attempts to allow for mfa approval. Returns: An OAuth response model object. Raises: AuthenticationError: If the mfa code is incorrect more than specified \ number of attempts. """ print(f"Input mfa code:") mfa_code = input() oauth_payload["mfa_code"] = mfa_code oauth, res = self.post( urls.OAUTH, data=oauth_payload, raise_errors=False, auto_login=False, return_response=True, schema=OAuthSchema(), ) attempts -= 1 if (res.status_code != requests.codes.ok) and (attempts > 0): print("Invalid mfa code") return self._mfa_oauth2(oauth_payload, attempts) elif res.status_code == requests.codes.ok: # TODO: Write mypy issue on why this needs to be casted? return cast(OAuth, oauth) else: raise AuthenticationError("Too many incorrect mfa attempts")
def _login_oauth2(self) -> None: """Create a new oauth2 token. Raises: AuthenticationError: If the login credentials are not set, if a challenge wasn't accepted, or if an mfa code is not accepted. """ if not self.login_set: raise AuthenticationError( "Username and password must be passed to constructor or must be loaded " "from json") self.session.headers.pop("Authorization", None) oauth_payload = { "password": self.password, "username": self.username, "grant_type": "password", "client_id": CLIENT_ID, "expires_in": EXPIRATION_TIME, "scope": "internal", "device_token": self.device_token, "challenge_type": self.challenge_type, } res = self.post(OAUTH_TOKEN_URL, data=oauth_payload, raise_errors=False, auto_login=False) if res is None or "error" in res: raise AuthenticationError("Unknown login error") elif "detail" in res and any(k in res["detail"] for k in ["Invalid", "Unable"]): raise AuthenticationError(f"{res['detail']}") elif "challenge" in res: challenge_id = res["challenge"]["id"] # TODO: use api module challenge_url = ( f"https://api.robinhood.com/challenge/{challenge_id}/respond/") print( f"Input challenge code from {self.challenge_type.capitalize()}:" ) challenge_code = input() challenge_payload = {"response": str(challenge_code)} challenge_header = CaseInsensitiveDict( {"X-ROBINHOOD-CHALLENGE-RESPONSE-ID": challenge_id}) challenge = self.post( challenge_url, data=challenge_payload, raise_errors=False, headers=challenge_header, auto_login=False, ) if challenge is None or challenge.get("status", "") != "validated": raise AuthenticationError( "Challenge response was not accepted") res = self.post( OAUTH_TOKEN_URL, data=oauth_payload, headers=challenge_header, auto_login=False, ) elif "mfa_required" in res: print(f"Input mfa code:") mfa_code = input() oauth_payload["mfa_code"] = mfa_code res = self.post( OAUTH_TOKEN_URL, data=oauth_payload, raise_errors=False, auto_login=False, ) if res is None or (res.get("detail", "") == "Please enter a valid code."): raise AuthenticationError("Mfa code was not accepted") self._process_auth_body(res)