From 3dee59592927f03659ce16be9f618dd4f7ed36b1 Mon Sep 17 00:00:00 2001 From: Matin Tamizi Date: Wed, 11 Dec 2024 02:09:15 -0600 Subject: [PATCH 1/4] support return_on_mfa --- .markdownlint.json | 3 +- README.md | 35 +- garth/sso.py | 121 +- tests/cassettes/test_login_return_on_mfa.yaml | 1017 +++++++++++++++++ tests/conftest.py | 8 +- tests/test_sso.py | 27 + 6 files changed, 1178 insertions(+), 33 deletions(-) create mode 100644 tests/cassettes/test_login_return_on_mfa.yaml diff --git a/.markdownlint.json b/.markdownlint.json index 80b846a..5913753 100644 --- a/.markdownlint.json +++ b/.markdownlint.json @@ -1,5 +1,6 @@ { "MD033": { "allowed_elements": ["img"] - } + }, + "MD046": false } diff --git a/README.md b/README.md index cf1edd7..39d76dd 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,12 @@ # Garth -[![CI](https://github.com/matin/garth/workflows/CI/badge.svg?event=push)](https://github.com/matin/garth/actions/workflows/ci.yml?query=event%3Apush+branch%3Amain+workflow%3ACI) -[![codecov](https://codecov.io/gh/matin/garth/branch/main/graph/badge.svg?token=0EFFYJNFIL)](https://codecov.io/gh/matin/garth) -[![PyPI - Downloads](https://img.shields.io/pypi/dm/garth)](https://pypistats.org/packages/garth) +[![CI](https://github.com/matin/garth/workflows/CI/badge.svg?event=push)]( + https://github.com/matin/garth/actions/workflows/ci.yml?query=event%3Apush+branch%3Amain+workflow%3ACI) +[![codecov]( + https://codecov.io/gh/matin/garth/branch/main/graph/badge.svg?token=0EFFYJNFIL)]( + https://codecov.io/gh/matin/garth) +[![PyPI - Downloads](https://img.shields.io/pypi/dm/garth)]( + https://pypistats.org/packages/garth) Garmin SSO auth + Connect Python client @@ -16,7 +20,8 @@ individual days and the trend. The Colab retrieves up to three years of daily data. If there's less than three years of data, it retrieves whatever is available. -![Stress: Garph of 28-day rolling average](https://github.com/matin/garth/assets/98985/868ecf25-4644-4879-b28f-ed0706a9e7b9) +![Stress: Garph of 28-day rolling average]( + https://github.com/matin/garth/assets/98985/868ecf25-4644-4879-b28f-ed0706a9e7b9) ### [Sleep analysis over 90 days](https://colab.research.google.com/github/matin/garth/blob/main/colabs/sleep.ipynb) @@ -27,13 +32,15 @@ daily sleep quality in 28-day pages, but that doesn't show details. Using day with enough detail to product a stacked bar graph of the daily sleep stages. -![Sleep stages over 90 days](https://github.com/matin/garth/assets/98985/ba678baf-0c8a-4907-aa91-be43beec3090) +![Sleep stages over 90 days]( + https://github.com/matin/garth/assets/98985/ba678baf-0c8a-4907-aa91-be43beec3090) One specific graph that's useful but not available in the Connect app is sleep start and end times over an extended period. This provides context to the sleep hours and stages. -![Sleep times over 90 days](https://github.com/matin/garth/assets/98985/c5583b9e-ab8a-4b5c-bfe6-1cb0ca95d1de) +![Sleep times over 90 days]( + https://github.com/matin/garth/assets/98985/c5583b9e-ab8a-4b5c-bfe6-1cb0ca95d1de) ### [ChatGPT analysis of Garmin stats](https://colab.research.google.com/github/matin/garth/blob/main/colabs/chatgpt_analysis_of_stats.ipynb) @@ -123,14 +130,22 @@ garth.save("~/.garth") ### Custom MFA handler -There's already a default MFA handler that prompts for the code in the -terminal. You can provide your own handler. The handler should return the -MFA code through your custom prompt. +By default, MFA will prompt for the code in the terminal. You can provide your +own handler: ```python garth.login(email, password, prompt_mfa=lambda: input("Enter MFA code: ")) ``` +For advanced use cases (like async handling), MFA can be handled separately: + +```python +result = garth.login(email, password, return_on_mfa=True) +if isinstance(result, dict): # MFA is required + mfa_code = "123456" # Get this from your custom MFA flow + oauth1, oauth2 = garth.resume_login(result['client_state'], mfa_code) +``` + ### Configure #### Set domain for China @@ -188,7 +203,7 @@ list(sleep.keys()) ### Stats ```python -stress = garth.connectapi("/usersummary-service/stats/stress/weekly/2023-07-05/52") +stress = garth.connectapi("/usersummary-service/stats/stress/weekly/2023-07-05/52") ``` ```json diff --git a/garth/sso.py b/garth/sso.py index 0faf6bb..5ee932c 100644 --- a/garth/sso.py +++ b/garth/sso.py @@ -41,13 +41,52 @@ def __init__( self.verify = parent.verify +def _complete_login( + client: "http.Client", html: str +) -> Tuple[OAuth1Token, OAuth2Token]: + """Complete the login process after successful authentication. + + Args: + client: The HTTP client + html: The HTML response containing the ticket + + Returns: + Tuple[OAuth1Token, OAuth2Token]: The OAuth tokens + """ + # Parse ticket + m = re.search(r'embed\?ticket=([^"]+)"', html) + assert m, "Couldn't find ticket in response" + ticket = m.group(1) + + oauth1 = get_oauth1_token(ticket, client) + oauth2 = exchange(oauth1, client) + + return oauth1, oauth2 + + def login( email: str, password: str, /, client: "http.Client | None" = None, - prompt_mfa: Callable = lambda: input("MFA code: "), -) -> Tuple[OAuth1Token, OAuth2Token]: + prompt_mfa: Callable | None = lambda: input("MFA code: "), + return_on_mfa: bool = False, +) -> Tuple[OAuth1Token, OAuth2Token] | dict: + """Login to Garmin Connect. + + Args: + email: Garmin account email + password: Garmin account password + client: Optional HTTP client to use + prompt_mfa: Callable that prompts for MFA code. Returns on MFA if None. + return_on_mfa: If True, returns dict with MFA info instead of prompting + + Returns: + If return_on_mfa=False (default): + Tuple[OAuth1Token, OAuth2Token]: OAuth tokens after login + If return_on_mfa=True and MFA required: + dict: Contains needs_mfa and client_state for resume_login() + """ client = client or http.client # Define params based on domain @@ -98,28 +137,33 @@ def login( # Handle MFA if "MFA" in title: + if return_on_mfa or prompt_mfa is None: + return { + "needs_mfa": True, + "client_state": { + "csrf_token": csrf_token, + "signin_params": SIGNIN_PARAMS, + "client": client, + }, + } + handle_mfa(client, SIGNIN_PARAMS, prompt_mfa) title = get_title(client.last_resp.text) assert title == "Success", f"Unexpected title: {title}" - - # Parse ticket - m = re.search(r'embed\?ticket=([^"]+)"', client.last_resp.text) - assert m, "Couldn't find ticket in response" - ticket = m.group(1) - - oauth1 = get_oauth1_token(ticket, client) - oauth2 = exchange(oauth1, client) - - return oauth1, oauth2 + return _complete_login(client, client.last_resp.text) def get_oauth1_token(ticket: str, client: "http.Client") -> OAuth1Token: sess = GarminOAuth1Session(parent=client.sess) + base_url = f"https://connectapi.{client.domain}/oauth-service/oauth/" + login_url = f"https://sso.{client.domain}/sso/embed" + url = ( + f"{base_url}preauthorized?ticket={ticket}&login-url={login_url}" + "&accepts-mfa-tokens=true" + ) resp = sess.get( - f"https://connectapi.{client.domain}/oauth-service/oauth/" - f"preauthorized?ticket={ticket}&login-url=" - f"https://sso.{client.domain}/sso/embed&accepts-mfa-tokens=true", + url, headers=USER_AGENT, timeout=client.timeout, ) @@ -136,12 +180,15 @@ def exchange(oauth1: OAuth1Token, client: "http.Client") -> OAuth2Token: parent=client.sess, ) data = dict(mfa_token=oauth1.mfa_token) if oauth1.mfa_token else {} + base_url = f"https://connectapi.{client.domain}/oauth-service/oauth/" + url = f"{base_url}exchange/user/2.0" + headers = { + **USER_AGENT, + **{"Content-Type": "application/x-www-form-urlencoded"}, + } token = sess.post( - f"https://connectapi.{client.domain}/oauth-service/oauth/exchange/user/2.0", - headers={ - **USER_AGENT, - **{"Content-Type": "application/x-www-form-urlencoded"}, - }, + url, + headers=headers, data=data, timeout=client.timeout, ).json() @@ -190,3 +237,37 @@ def get_title(html: str) -> str: if not m: raise GarthException("Couldn't find title") return m.group(1) + + +def resume_login( + client_state: dict, mfa_code: str +) -> Tuple[OAuth1Token, OAuth2Token]: + """Complete login after MFA code is provided. + + Args: + client_state: The client state from login() when MFA was needed + mfa_code: The MFA code provided by the user + + Returns: + Tuple[OAuth1Token, OAuth2Token]: The OAuth tokens after login + """ + client = client_state["client"] + signin_params = client_state["signin_params"] + csrf_token = client_state["csrf_token"] + + client.post( + "sso", + "/sso/verifyMFA/loginEnterMfaCode", + params=signin_params, + referrer=True, + data={ + "mfa-code": mfa_code, + "embed": "true", + "_csrf": csrf_token, + "fromPage": "setupEnterMfaCode", + }, + ) + + title = get_title(client.last_resp.text) + assert title == "Success", f"Unexpected title: {title}" + return _complete_login(client, client.last_resp.text) diff --git a/tests/cassettes/test_login_return_on_mfa.yaml b/tests/cassettes/test_login_return_on_mfa.yaml new file mode 100644 index 0000000..d2e0f24 --- /dev/null +++ b/tests/cassettes/test_login_return_on_mfa.yaml @@ -0,0 +1,1017 @@ +interactions: +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - GCM-iOS-5.7.2.1 + method: GET + uri: https://sso.garmin.com/sso/embed?id=gauth-widget&embedWidget=true&gauthHost=https%3A%2F%2Fsso.garmin.com%2Fsso + response: + body: + string: "\n\t\n\t GAuth Embedded Version\n\t \n\t \n\t\n\t\n\t\t\n\n
\n\t
\n\tERROR:
+        clientId parameter must be specified!!!\n\n\tUsage: https://sso.garmin.com/sso/embed?clientId=<clientId>&locale=<locale>...\n\n\tRequest
+        parameter configuration options:\n\n\tNAME                           REQ  VALUES
+        \                                                  DESCRIPTION\n\t------------------
+        \            ---  -------------------------------------------------------
+        \ ---------------------------------------------------------------------------------------------------\n\tclientId
+        \                      Yes  \"MY_GARMIN\"/\"BUY_GARMIN\"/\"FLY_GARMIN\"/                   Client
+        identifier for your web application\n\t                                    \"RMA\"/\"GarminConnect\"/\"OpenCaching\"/etc\n\tlocale
+        \                        Yes  \"en\", \"bg\", \"cs\", \"da\", \"de\", \"es\",
+        \"el\", \"fr\", \"hr\",    User's current locale, to display the GAuth login
+        widget internationalized properly.\n\t                                    \"in\",
+        \"it\", \"iw\", \"hu\", \"ms\", \"nb\", \"nl\", \"no\", \"pl\",    (All the
+        currently supported locales are listed in the Values section.)\n\t                                    \"pt\",
+        \"pt_BR\", \"ru\", \"sk\", \"sl\", \"fi\", \"sv\", \"tr\",\n\t                                    \"uk\",
+        \"th\", \"ja\", \"ko\", \"zh_TW\", \"zh\", \"vi_VN\"\n\tcssUrl                          No
+        \ Absolute URL to custom CSS file.                         Use custom CSS
+        styling for the GAuth login widget.\n\treauth                          No
+        \ true/false (Default value is false)                      Specify true if
+        you want to ensure that the GAuth login widget shows up,\n\t                                                                                             even
+        if the SSO infrastructure remembers the user and would immediately log them
+        in.\n\t                                                                                             This
+        is useful if you know a user is logged on, but want a different user to be
+        allowed to logon.\n\tinitialFocus                    No  true/false (Default
+        value is true)                       If you don't want the GAuth login widget
+        to autofocus in it's \"Email or Username\" field upon initial loading,\n\t
+        \                                                                                            then
+        specify this option and set it to false.\n\trememberMeShown                 No
+        \ true/false (Default value is false)                      Whether the \"Remember
+        Me\" check box is shown in the GAuth login widget.\n\trememberMeChecked               No
+        \ true/false (Default value is false)                      Whether the \"Remember
+        Me\" check box feature is checked by default.\n\tcreateAccountShown              No
+        \ true/false (Default value is true)                       Whether the \"Don't
+        have an account? Create One\" link is shown in the GAuth login widget.\n\tsocialEnabled
+        \                  No  true/false (Default value is false)                       If
+        set to false, do not show any social sign in elements or allow social sign
+        ins.\n\tlockToEmailAddress              No  Email address to pre-load and
+        lock.                      If specified, the specified email address will
+        be pre-loaded in the main \"Email\" field in the SSO login form,\n\t                                                                                             as
+        well as in in the \"Email Address\" field in the \"Forgot Password?\" password
+        reset form,\n\t                                                                                             and
+        both fields will be disabled so they can't be changed.\n\t                                                                                             (If
+        for some reason you want to force re-authentications for a known customer
+        account, you can make use of this option.)\n\topenCreateAccount               No
+        \ true/false (Default value is false)                      If set to true,
+        immediately display the the account creation screen.\n\tdisplayNameShown                No
+        \ true/false (Default value is false)                      If set to true,
+        show the \"Display Name\" field on the account creation screen, to allow the
+        user\n\t                                                                                             to
+        set their central MyGarmin display name upon account creation.\n\tglobalOptInShown
+        \               No  true/false (Default value is false)                      Whether
+        the \"Global Opt-In\" check box is shown on the create account & create social
+        account screens.\n\t                                                                                             If
+        set to true these screens will show a \"Sign Up For Email\" check box with
+        accompanying text\n\t                                                                                             \"I
+        would also like to receive email about promotions and new products.\"\n\t
+        \                                                                                            If
+        checked, the Customer 2.0 account that is created will have it's global opt-in
+        flag set to true,\n\t                                                                                             and
+        Garmin email communications will be allowed.\n\tglobalOptInChecked              No
+        \ true/false (Default value is false)                      Whether the \"Global
+        Opt-In\" check box is checked by default.\n\tconsumeServiceTicket            No
+        \ true/false (Default value is true)                       IF you don't specify
+        a redirectAfterAccountLoginUrl AND you set this to false, the GAuth login
+        widget\n\t                                                                                             will
+        NOT consume the service ticket assigned and will not seamlessly log you into
+        your webapp.\n\t                                                                                             It
+        will send a SUCCESS JavaScript event with the service ticket and service url
+        you can take\n\t                                                                                             and
+        explicitly validate against the SSO infrastructure yourself.\n\t                                                                                             (By
+        using casClient's SingleSignOnUtils.authenticateServiceTicket() utility method,\n\t
+        \                                                                                            or
+        calling web service customerWebServices_v1.2 AccountManagementService.authenticateServiceTicket().)\n\tmobile
+        \                         No  true/false (Default value is false)                      Setting
+        to true will cause mobile friendly views to be shown instead of the tradition
+        screens.\n\ttermsOfUseUrl                   No  Absolute URL to your custom
+        terms of use URL.            If not specified, defaults to http://www.garmin.com/terms\n\tprivacyStatementUrl
+        \            No  Absolute URL to your custom privacy statement URL.       If
+        not specified, defaults to http://www.garmin.com/privacy\n\tproductSupportUrl
+        \              No  Absolute URL to your custom product support URL.         If
+        not specified, defaults to http://www.garmin.com/us/support/contact\n\tgenerateExtraServiceTicket
+        \     No  true/false (Default value is false)                      If set
+        to true, generate an extra unconsumed service ticket.\n\t\t                                                                                     (The
+        service ticket validation response will include the extra service ticket.)\n\tgenerateTwoExtraServiceTickets
+        \ No  true/false (Default value is false)                      If set to true,
+        generate two extra unconsumed service tickets.\n\t\t\t\t\t\t\t\t\t \t\t\t
+        \    (The service ticket validation response will include the extra service
+        tickets.)\n\tgenerateNoServiceTicket         No  true/false (Default value
+        is false)                      If you don't want SSO to generate a service
+        ticket at all when logging in to the GAuth login widget.\n                                                                                                     (Useful
+        when allowing logins to static sites that are not SSO enabled and can't consume
+        the service ticket.)\n\tconnectLegalTerms               No  true/false (Default
+        value is false)                      Whether to show the connectLegalTerms
+        on the create account page\n\tshowTermsOfUse                  No  true/false
+        (Default value is false)                      Whether to show the showTermsOfUse
+        on the create account page\n\tshowPrivacyPolicy               No  true/false
+        (Default value is false)                      Whether to show the showPrivacyPolicy
+        on the create account page\n\tshowConnectLegalAge             No  true/false
+        (Default value is false)                      Whether to show the showConnectLegalAge
+        on the create account page\n\tlocationPromptShown             No  true/false
+        (Default value is false)                      If set to true, ask the customer
+        during account creation to verify their country of residence.\n\tshowPassword
+        \                   No  true/false (Default value is true)                       If
+        set to false, mobile version for createAccount and login screens would hide
+        the password\n\tuseCustomHeader                 No  true/false (Default value
+        is false)                      If set to true, the \"Sign in\" text will be
+        replaced by custom text. Contact CDS team to set the i18n text for your client
+        id.\n\tmfaRequired                     No  true/false (Default value is false)
+        \                     Require multi factor authentication for all authenticating
+        users.\n\tperformMFACheck                 No  true/false (Default value is
+        false)                      If set to true, ask the logged in user to pass
+        a multi factor authentication check. (Only valid for an already logged in
+        user.)\n\trememberMyBrowserShown          No  true/false (Default value is
+        false)                      Whether the \"Remember My Browser\" check box
+        is shown in the GAuth login widget MFA verification screen.\n\trememberMyBrowserChecked
+        \       No  true/false (Default value is false)                      Whether
+        the \"Remember My Browser\" check box feature is checked by default.\n\tconsentTypeIds\t\t\t\t\tNo\tconsent_types
+        ids\t\t \t\t\t\t\t\t\t\t multiple consent types ids can be passed as consentTypeIds=type1&consentTypeIds=type2\n\t
\n
\n\n\n\t\n\n" + headers: + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Headers: + - Access-Control-Allow-Headers, Origin,Accept, X-Requested-With, Content-Type, + Access-Control-Request-Method, Access-Control-Request-Headers + Access-Control-Allow-Methods: + - GET,POST,OPTIONS + Access-Control-Allow-Origin: + - https://www.garmin.com + CF-Cache-Status: + - DYNAMIC + CF-RAY: + - 8f03e2cc5bdab6df-QRO + Connection: + - keep-alive + Content-Language: + - en + Content-Type: + - text/html;charset=UTF-8 + Date: + - Wed, 11 Dec 2024 07:50:49 GMT + NEL: + - '{"success_fraction":0.01,"report_to":"cf-nel","max_age":604800}' + Report-To: + - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=Puezu%2BVdH9JIb%2B2NvX57aGY2GEiBZuFrX2T0Std5%2FzkX8e0X%2BAqfhQ%2FsJ7C27cYXcK%2F6DNHv4SAwemmm0i7ZPcJLYrTj2vS2LGN91xWkq4%2BXMRsZfKGxomSM1gya2GGO"}],"group":"cf-nel","max_age":604800}' + Server: + - cloudflare + Set-Cookie: + - org.springframework.web.servlet.i18n.CookieLocaleResolver.LOCALE=SANITIZED; + Path=SANITIZED + - __cf_bm=SANITIZED; path=SANITIZED; expires=SANITIZED; domain=SANITIZED; HttpOnly; + Secure; SameSite=SANITIZED + - __cflb=SANITIZED; SameSite=SANITIZED; Secure; path=SANITIZED; expires=SANITIZED; + HttpOnly + - _cfuvid=SANITIZED; path=SANITIZED; domain=SANITIZED; HttpOnly; Secure; SameSite=SANITIZED + Transfer-Encoding: + - chunked + X-Application-Context: + - casServer:cloud,prod,prod-US_Olathe:2 + X-B3-Traceid: + - 707927fb58c44d44526f283f17ff87ef + X-Robots-Tag: + - noindex + X-Vcap-Request-Id: + - 707927fb-58c4-4d44-526f-283f17ff87ef + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Cookie: + - org.springframework.web.servlet.i18n.CookieLocaleResolver.LOCALE=SANITIZED; + __cflb=SANITIZED; __cf_bm=SANITIZED; _cfuvid=SANITIZED + User-Agent: + - GCM-iOS-5.7.2.1 + referer: + - https://sso.garmin.com/sso/embed?id=gauth-widget&embedWidget=true&gauthHost=https%3A%2F%2Fsso.garmin.com%2Fsso + method: GET + uri: https://sso.garmin.com/sso/signin?id=gauth-widget&embedWidget=true&gauthHost=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&service=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&source=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountLoginUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountCreationUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed + response: + body: + string: "\n\n \n \n \n \n GARMIN Authentication Application\n + \ \n\n\t \n\n \n + \ \n\t\t\n + \ \n + \ \n + \ \n + \ \n + \ \n + \ \n\t\t\n + \ \n + \ \n\n + \ \n \n \n + \ \n\n \n
\n + \ \n
\n \n\t \t \n + \ \n
\n + \

Sign In

\n\n
\n\n
\n\t\t\t\t\t\t\t\n + \ \n \n \n + \ \n \n \n\n + \
\n + \
\n + \
\n
\n\t\t\t\t\t\t\t\n \t\t\n\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t + \ \n\n + \
\n\n
\n + \ \n (Forgot?)\n + \ \n Caps lock is on.\n\t\t\t\t\t
\n \n \n \n \n\n\n + \ \n \n\n
\n + \
\n \n\n \n\t
\n\t + \ \n\t
\n\t \n\n\t \n\t
\n \n\n\t\t\t\t\t\n\t
\n\t + \ \n
+ \n + \
\n \n\n\t\t
\n\t\t\n\n \n \n\n" + headers: + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Headers: + - Access-Control-Allow-Headers, Origin,Accept, X-Requested-With, Content-Type, + Access-Control-Request-Method, Access-Control-Request-Headers + Access-Control-Allow-Methods: + - GET,POST,OPTIONS + Access-Control-Allow-Origin: + - https://www.garmin.com + CF-Cache-Status: + - DYNAMIC + CF-Ray: + - 8f03e2cf4ba0b6e7-QRO + Connection: + - keep-alive + Content-Language: + - en + Content-Type: + - text/html;charset=UTF-8 + Date: + - Wed, 11 Dec 2024 07:50:49 GMT + NEL: + - '{"success_fraction":0.01,"report_to":"cf-nel","max_age":604800}' + Report-To: + - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=T46elug27MoZ2iqKBAAKcijwtJBfnKoKVn%2BWnJh5asgBRlmDbuFudS8NqTJHsJQDzq8opwhlDBSxe%2BbuSN2L3oNidiAVmHdDNZh8SHp9%2Bsx7ELg1%2BJX1MIVus2SspNJC"}],"group":"cf-nel","max_age":604800}' + Server: + - cloudflare + Set-Cookie: + - org.springframework.web.servlet.i18n.CookieLocaleResolver.LOCALE=SANITIZED; + Path=SANITIZED + - SESSION=SANITIZED; Path=SANITIZED; Secure; HttpOnly + - __VCAP_ID__=SANITIZED; Path=SANITIZED; HttpOnly; Secure + Transfer-Encoding: + - chunked + Vary: + - Accept-Encoding + X-Application-Context: + - casServer:cloud,prod,prod-US_Olathe:5 + X-B3-Traceid: + - 0ec83ff3eaac47f14c830dfeb5dcefd4 + X-Robots-Tag: + - noindex + X-Vcap-Request-Id: + - 0ec83ff3-eaac-47f1-4c83-0dfeb5dcefd4 + status: + code: 200 + message: OK +- request: + body: username=SANITIZED&password=SANITIZED&embed=true&_csrf=DD6F770AC6B4F2FE813E32CBDCFFE6C6E5A7B73A832E25F1EB2998BA80990A269BD50E938C2062E8EEFED2FCE2228680F058 + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '177' + Content-Type: + - application/x-www-form-urlencoded + Cookie: + - SESSION=SANITIZED; org.springframework.web.servlet.i18n.CookieLocaleResolver.LOCALE=SANITIZED; + __cflb=SANITIZED; __VCAP_ID__=SANITIZED; __cf_bm=SANITIZED; _cfuvid=SANITIZED + User-Agent: + - GCM-iOS-5.7.2.1 + referer: + - https://sso.garmin.com/sso/signin?id=gauth-widget&embedWidget=true&gauthHost=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&service=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&source=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountLoginUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountCreationUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed + method: POST + uri: https://sso.garmin.com/sso/signin?id=gauth-widget&embedWidget=true&gauthHost=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&service=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&source=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountLoginUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountCreationUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed + response: + body: + string: '' + headers: + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Headers: + - Access-Control-Allow-Headers, Origin,Accept, X-Requested-With, Content-Type, + Access-Control-Request-Method, Access-Control-Request-Headers + Access-Control-Allow-Methods: + - GET,POST,OPTIONS + Access-Control-Allow-Origin: + - https://www.garmin.com + CF-Cache-Status: + - DYNAMIC + CF-Ray: + - 8f03e2d268c86b11-DFW + Connection: + - keep-alive + Content-Language: + - en + Content-Length: + - '0' + Date: + - Wed, 11 Dec 2024 07:50:51 GMT + Location: + - https://sso.garmin.com/sso/verifyMFA/loginEnterMfaCode?id=gauth-widget&embedWidget=true&gauthHost=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&service=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&source=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountLoginUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountCreationUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed + NEL: + - '{"success_fraction":0.01,"report_to":"cf-nel","max_age":604800}' + Report-To: + - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=LK7lsEzUKTbHnWj9qIE1R8fQ%2BkjgwAVfczJ5bWWimx8nMVZ8pKDj3SOB6yyENkB9hUZK0ABqjvtENDvY1eqYOil82MQMrm0C7tpDYy%2BgEij99ZQjYhqrkD1sRXcbSS8K"}],"group":"cf-nel","max_age":604800}' + Server: + - cloudflare + Set-Cookie: + - org.springframework.web.servlet.i18n.CookieLocaleResolver.LOCALE=SANITIZED; + Path=SANITIZED + - __cfruid=SANITIZED; path=SANITIZED; domain=SANITIZED; HttpOnly; Secure; SameSite=SANITIZED + Vary: + - Accept-Encoding + X-Application-Context: + - casServer:cloud,prod,prod-US_Olathe:5 + X-B3-Traceid: + - 7268da1f0b35422b6b5e01f2891186e6 + X-Robots-Tag: + - noindex + X-Vcap-Request-Id: + - 7268da1f-0b35-422b-6b5e-01f2891186e6 + status: + code: 302 + message: Found +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Cookie: + - SESSION=SANITIZED; org.springframework.web.servlet.i18n.CookieLocaleResolver.LOCALE=SANITIZED; + __cflb=SANITIZED; __VCAP_ID__=SANITIZED; __cf_bm=SANITIZED; _cfuvid=SANITIZED; + __cfruid=SANITIZED + User-Agent: + - GCM-iOS-5.7.2.1 + referer: + - https://sso.garmin.com/sso/signin?id=gauth-widget&embedWidget=true&gauthHost=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&service=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&source=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountLoginUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountCreationUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed + method: GET + uri: https://sso.garmin.com/sso/verifyMFA/loginEnterMfaCode?id=gauth-widget&embedWidget=true&gauthHost=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&service=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&source=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountLoginUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountCreationUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed + response: + body: + string: "\n\n\n\n \n + \ \n \n + \ \n + \ \n + \ \n + \ \n + \ \n + \ \n + \ \n + \ \n \n Enter + MFA code for login\n \n + \ \n + \ \n \n \n \n\n\n\n
\n

Enter + security code

\n \n \n\n + \
\n
\n \n \n + \ Code sent to mt*****@gmail.com\n + \ \n
\n + \
\n
\n \n \n \n + \ \n \n \n
\n + \
\n
\n \n
\n + \
\n

\n
\n + \ Get help
\n
\n + \
\n + \ Request a new code\n + \
\n \n \n + \
\n \n
\n + \ \n
\n \n \n \n + \
\n
\n
\n
\n
\n
\n \n\n" + headers: + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Headers: + - Access-Control-Allow-Headers, Origin,Accept, X-Requested-With, Content-Type, + Access-Control-Request-Method, Access-Control-Request-Headers + Access-Control-Allow-Methods: + - GET,POST,OPTIONS + Access-Control-Allow-Origin: + - https://www.garmin.com + CF-Cache-Status: + - DYNAMIC + CF-RAY: + - 8f03e2db3e836c32-DFW + Connection: + - keep-alive + Content-Language: + - en + Content-Type: + - text/html;charset=UTF-8 + Date: + - Wed, 11 Dec 2024 07:50:51 GMT + NEL: + - '{"success_fraction":0.01,"report_to":"cf-nel","max_age":604800}' + Report-To: + - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=69a5yve42wg%2Fug%2BUJKJZGapiftAp0V3ssRMcsTBGBjRd71dOeXiN%2B84LrE%2BmBpsqE0qCzO%2BcSui9Mj5sJCmZl4mNf2Y%2B80evi5y8aQL47J3zOjAs1zBGhHDW8F7t%2F6Q4"}],"group":"cf-nel","max_age":604800}' + Server: + - cloudflare + Set-Cookie: + - org.springframework.web.servlet.i18n.CookieLocaleResolver.LOCALE=SANITIZED; + Path=SANITIZED + Transfer-Encoding: + - chunked + X-Application-Context: + - casServer:cloud,prod,prod-US_Olathe:5 + X-B3-Traceid: + - ba933ac91e6f46c2773ab14761f2dbb8 + X-Robots-Tag: + - noindex + X-Vcap-Request-Id: + - ba933ac9-1e6f-46c2-773a-b14761f2dbb8 + status: + code: 200 + message: OK +- request: + body: mfa-code=243715&embed=true&_csrf=DD6F770AC6B4F2FE813E32CBDCFFE6C6E5A7B73A832E25F1EB2998BA80990A269BD50E938C2062E8EEFED2FCE2228680F058&fromPage=setupEnterMfaCode + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '160' + Content-Type: + - application/x-www-form-urlencoded + Cookie: + - SESSION=SANITIZED; org.springframework.web.servlet.i18n.CookieLocaleResolver.LOCALE=SANITIZED; + __cflb=SANITIZED; __VCAP_ID__=SANITIZED; __cf_bm=SANITIZED; _cfuvid=SANITIZED; + __cfruid=SANITIZED + User-Agent: + - GCM-iOS-5.7.2.1 + referer: + - https://sso.garmin.com/sso/verifyMFA/loginEnterMfaCode?id=gauth-widget&embedWidget=true&gauthHost=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&service=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&source=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountLoginUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountCreationUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed + method: POST + uri: https://sso.garmin.com/sso/verifyMFA/loginEnterMfaCode?id=gauth-widget&embedWidget=true&gauthHost=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&service=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&source=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountLoginUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountCreationUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed + response: + body: + string: '' + headers: + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Headers: + - Access-Control-Allow-Headers, Origin,Accept, X-Requested-With, Content-Type, + Access-Control-Request-Method, Access-Control-Request-Headers + Access-Control-Allow-Methods: + - GET,POST,OPTIONS + Access-Control-Allow-Origin: + - https://www.garmin.com + CF-Cache-Status: + - DYNAMIC + CF-RAY: + - 8f03e32168126bcc-DFW + Connection: + - keep-alive + Content-Language: + - en + Content-Length: + - '0' + Date: + - Wed, 11 Dec 2024 07:51:03 GMT + Location: + - https://sso.garmin.com/sso/login?logintoken=4vE3601wYS&service=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&locale=en&embed=true&source=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountLoginUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&embedWidget=true + NEL: + - '{"success_fraction":0.01,"report_to":"cf-nel","max_age":604800}' + Report-To: + - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=S8A1ygrdQh1s71kF8O2MaekU525HIH5aWxRgRRD3d7gXIDO6CWr8w1%2Bv%2BJbnkUQYMn6pHjhmENYFayncUWl07bzzshYdNsqrkSXheXCiM1rkv4SVcaFpJGiDVZ2NFQxZ"}],"group":"cf-nel","max_age":604800}' + Server: + - cloudflare + Set-Cookie: + - org.springframework.web.servlet.i18n.CookieLocaleResolver.LOCALE=SANITIZED; + Path=SANITIZED + X-Application-Context: + - casServer:cloud,prod,prod-US_Olathe:5 + X-B3-Traceid: + - 78e9ad92788a45e2707e19040336bcfe + X-Robots-Tag: + - noindex + X-Vcap-Request-Id: + - 78e9ad92-788a-45e2-707e-19040336bcfe + status: + code: 302 + message: Found +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Cookie: + - SESSION=SANITIZED; org.springframework.web.servlet.i18n.CookieLocaleResolver.LOCALE=SANITIZED; + __cflb=SANITIZED; __VCAP_ID__=SANITIZED; __cf_bm=SANITIZED; _cfuvid=SANITIZED; + __cfruid=SANITIZED + User-Agent: + - GCM-iOS-5.7.2.1 + referer: + - https://sso.garmin.com/sso/verifyMFA/loginEnterMfaCode?id=gauth-widget&embedWidget=true&gauthHost=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&service=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&source=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountLoginUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountCreationUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed + method: GET + uri: https://sso.garmin.com/sso/login?logintoken=4vE3601wYS&service=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&locale=en&embed=true&source=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&redirectAfterAccountLoginUrl=https%3A%2F%2Fsso.garmin.com%2Fsso%2Fembed&embedWidget=true + response: + body: + string: "\n\n\t\n\t\tSuccess\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\n\t\n\t\t
\n\t\t\t\n\t\t
\n\t\t\n\t\n\n" + headers: + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Headers: + - Access-Control-Allow-Headers, Origin,Accept, X-Requested-With, Content-Type, + Access-Control-Request-Method, Access-Control-Request-Headers + Access-Control-Allow-Methods: + - GET,POST,OPTIONS + Access-Control-Allow-Origin: + - https://www.garmin.com + CF-Cache-Status: + - DYNAMIC + CF-Ray: + - 8f03e328aaf26c2e-DFW + Connection: + - keep-alive + Content-Language: + - en + Content-Type: + - text/html;charset=UTF-8 + Date: + - Wed, 11 Dec 2024 07:51:04 GMT + NEL: + - '{"success_fraction":0.01,"report_to":"cf-nel","max_age":604800}' + Report-To: + - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=Itl9hSd%2FmfO1zgpDz%2BzEwc3AQfKRrK%2BQgJoJlO7wP%2BcwF0CVBbnf1dDxL55MMXRHdfkBJL56X%2B6RezMkac9imgafMJZQi5%2Fk4wOFY6YA5eiF25Ip0Vqb1kFXDYH%2F%2BNFu"}],"group":"cf-nel","max_age":604800}' + Server: + - cloudflare + Set-Cookie: + - org.springframework.web.servlet.i18n.CookieLocaleResolver.LOCALE=SANITIZED; + Path=SANITIZED + - CASTGC=SANITIZED; Path=SANITIZED; Secure; HttpOnly + - CASTGC=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Path=SANITIZED; Secure; + HttpOnly + - GARMIN-SSO=SANITIZED; Domain=SANITIZED; Path=SANITIZED; Secure + - GARMIN-SSO=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Domain=SANITIZED; + Path=SANITIZED; Secure + - GarminNoCache=SANITIZED; Domain=SANITIZED; Path=SANITIZED; Secure + - GarminNoCache=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Domain=SANITIZED; + Path=SANITIZED; Secure + - GarminBuyCacheKey=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Domain=SANITIZED; + Path=SANITIZED + - GarminBuyCacheKey=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Domain=SANITIZED; + Path=SANITIZED + - GARMIN-SSO-GUID=SANITIZED; Domain=SANITIZED; Path=SANITIZED; Secure + - GARMIN-SSO-GUID=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Domain=SANITIZED; + Path=SANITIZED; Secure + - GARMIN-SSO-CUST-GUID=SANITIZED; Domain=SANITIZED; Path=SANITIZED; Secure + - GARMIN-SSO-CUST-GUID=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Domain=SANITIZED; + Path=SANITIZED; Secure + - CASTGC=SANITIZED; Path=SANITIZED; Secure; HttpOnly + - CASTGC=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Path=SANITIZED; Secure; + HttpOnly + Transfer-Encoding: + - chunked + Vary: + - Accept-Encoding + X-Application-Context: + - casServer:cloud,prod,prod-US_Olathe:5 + X-B3-Traceid: + - ee9e2564bcce49624d5254589b64b0f7 + X-Robots-Tag: + - noindex + X-Vcap-Request-Id: + - ee9e2564-bcce-4962-4d52-54589b64b0f7 + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - python-requests/2.32.3 + method: GET + uri: https://thegarth.s3.amazonaws.com/oauth_consumer.json + response: + body: + string: '{"consumer_key": "SANITIZED", "consumer_secret": "SANITIZED"}' + headers: + Accept-Ranges: + - bytes + Content-Length: + - '124' + Content-Type: + - application/json + Date: + - Wed, 11 Dec 2024 07:51:05 GMT + ETag: + - '"20240b1013cb35419bb5b2cff1407a4e"' + Last-Modified: + - Thu, 03 Aug 2023 00:16:11 GMT + Server: + - AmazonS3 + x-amz-id-2: + - s3pVFN2F1v75yAgvx1/ZvXKtn4CFgJU2hxDUZP6INxj8pGlMW+A4ms3jzJERG5obLkougEbN+QcN1Ko8dtopNrPu7Vw0XewgzWSVJN7PsUU= + x-amz-request-id: + - W7VC86WPXD70NJA0 + x-amz-server-side-encryption: + - AES256 + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - !!binary | + Ki8q + Accept-Encoding: + - !!binary | + Z3ppcCwgZGVmbGF0ZQ== + Authorization: + - Bearer SANITIZED + Connection: + - !!binary | + a2VlcC1hbGl2ZQ== + User-Agent: + - !!binary | + Y29tLmdhcm1pbi5hbmRyb2lkLmFwcHMuY29ubmVjdG1vYmlsZQ== + method: GET + uri: https://connectapi.garmin.com/oauth-service/oauth/preauthorized?ticket=ST-01343161-FXXpWgghlgF1mqZqlwsM-cas&login-url=https://sso.garmin.com/sso/embed&accepts-mfa-tokens=true + response: + body: + string: oauth_token=SANITIZED&oauth_token_secret=SANITIZED&mfa_token=SANITIZED&mfa_expiration_timestamp=2025-12-11 + 07:51:04.000 + headers: + CF-Cache-Status: + - DYNAMIC + CF-RAY: + - 8f03e32f3cc36b59-DFW + Connection: + - keep-alive + Content-Type: + - text/plain;charset=utf-8 + Date: + - Wed, 11 Dec 2024 07:51:05 GMT + NEL: + - '{"success_fraction":0.01,"report_to":"cf-nel","max_age":604800}' + Report-To: + - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=h%2Bq3jCaVrmy6KAj9ICaLlbooW0SAd9HkgD5NMy6KzVnuDWr10Hwg4et36M4nnfSQdkq2HndhQtRlrajTPbamwQJm53xei1BVmXeYVdDLBQF9wcopqMtU%2FDYRwcRkNjP0fVq44pA4ig%3D%3D"}],"group":"cf-nel","max_age":604800}' + Server: + - cloudflare + Set-Cookie: + - _cfuvid=SANITIZED; path=SANITIZED; domain=SANITIZED; HttpOnly; Secure; SameSite=SANITIZED + Transfer-Encoding: + - chunked + alt-svc: + - h3=":443"; ma=86400 + status: + code: 200 + message: OK +- request: + body: mfa_token=MFA-14124-SX2AMCGswvbn0nvztdszPL0XaUSCHLtfPldUKlADPb1MhoPSKq-cas + headers: + Accept: + - !!binary | + Ki8q + Accept-Encoding: + - !!binary | + Z3ppcCwgZGVmbGF0ZQ== + Authorization: + - Bearer SANITIZED + Connection: + - !!binary | + a2VlcC1hbGl2ZQ== + Content-Length: + - '74' + Content-Type: + - !!binary | + YXBwbGljYXRpb24veC13d3ctZm9ybS11cmxlbmNvZGVk + User-Agent: + - !!binary | + Y29tLmdhcm1pbi5hbmRyb2lkLmFwcHMuY29ubmVjdG1vYmlsZQ== + method: POST + uri: https://connectapi.garmin.com/oauth-service/oauth/exchange/user/2.0 + response: + body: + string: '{"scope": "COMMUNITY_COURSE_READ GARMINPAY_WRITE GOLF_API_READ ATP_READ + GHS_SAMD GHS_UPLOAD INSIGHTS_READ COMMUNITY_COURSE_WRITE CONNECT_WRITE GCOFFER_WRITE + GARMINPAY_READ DT_CLIENT_ANALYTICS_WRITE GOLF_API_WRITE INSIGHTS_WRITE PRODUCT_SEARCH_READ + OMT_CAMPAIGN_READ OMT_SUBSCRIPTION_READ GCOFFER_READ CONNECT_READ ATP_WRITE", + "jti": "SANITIZED", "access_token": "SANITIZED", "token_type": "Bearer", "refresh_token": + "SANITIZED", "expires_in": 106068, "refresh_token_expires_in": 2591999}' + headers: + CF-Cache-Status: + - DYNAMIC + CF-RAY: + - 8f03e332baa56c79-DFW + Cache-Control: + - no-cache, no-store, private + Connection: + - keep-alive + Content-Type: + - application/json + Date: + - Wed, 11 Dec 2024 07:51:06 GMT + NEL: + - '{"success_fraction":0.01,"report_to":"cf-nel","max_age":604800}' + Report-To: + - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=eeRTFcqJTduIbdcZwq8YbTTRUGqmdqt1Oq%2BJLMCdjzo5UVT4aAp1T%2FCLD94NtAViOXKshlPxo7ZsJ6LTOUvIsEh3q7H2e59WyXd3spqGw8kift8aJ0SAdSKCcl0J87Yu8sfrQ4pLkA%3D%3D"}],"group":"cf-nel","max_age":604800}' + Server: + - cloudflare + Set-Cookie: + - _cfuvid=SANITIZED; path=SANITIZED; domain=SANITIZED; HttpOnly; Secure; SameSite=SANITIZED + Transfer-Encoding: + - chunked + alt-svc: + - h3=":443"; ma=86400 + pragma: + - no-cache + status: + code: 200 + message: OK +version: 1 diff --git a/tests/conftest.py b/tests/conftest.py index 69c6502..d9201e9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -113,8 +113,12 @@ def sanitize_response(response): if encoding[0] == "gzip": body = response["body"]["string"] buffer = io.BytesIO(body) - body = gzip.GzipFile(fileobj=buffer).read() - response["body"]["string"] = body + try: + body = gzip.GzipFile(fileobj=buffer).read() + except gzip.BadGzipFile: + ... + else: + response["body"]["string"] = body for key in ["set-cookie", "Set-Cookie"]: if key in response["headers"]: diff --git a/tests/test_sso.py b/tests/test_sso.py index daa80d9..523fd9c 100644 --- a/tests/test_sso.py +++ b/tests/test_sso.py @@ -64,6 +64,33 @@ async def prompt_mfa(): assert isinstance(oauth2, OAuth2Token) +@pytest.mark.vcr +def test_login_return_on_mfa(client: Client): + result = sso.login( + "user@example.com", + "correct_password", + client=client, + return_on_mfa=True, + ) + + assert isinstance(result, dict) + assert result["needs_mfa"] is True + assert "client_state" in result + assert "csrf_token" in result["client_state"] + assert "signin_params" in result["client_state"] + assert "client" in result["client_state"] + + code = "123456" # obtain from custom login + + # test resuming the login + oauth1, oauth2 = sso.resume_login(result["client_state"], code) + + assert oauth1 + assert isinstance(oauth1, OAuth1Token) + assert oauth2 + assert isinstance(oauth2, OAuth2Token) + + def test_set_expirations(oauth2_token_dict: dict): token = sso.set_expirations(oauth2_token_dict) assert ( From f7bea8afe863de6abcc0f2b6fe5097c565fc65e9 Mon Sep 17 00:00:00 2001 From: Matin Tamizi Date: Wed, 11 Dec 2024 02:11:46 -0600 Subject: [PATCH 2/4] bump version --- garth/version.py | 2 +- uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/garth/version.py b/garth/version.py index 3d18726..dd9b22c 100644 --- a/garth/version.py +++ b/garth/version.py @@ -1 +1 @@ -__version__ = "0.5.0" +__version__ = "0.5.1" diff --git a/uv.lock b/uv.lock index 2c7139f..21a6a82 100644 --- a/uv.lock +++ b/uv.lock @@ -401,7 +401,7 @@ wheels = [ [[package]] name = "garth" -version = "0.5.0" +version = "0.5.1" source = { editable = "." } dependencies = [ { name = "pydantic" }, From 08d620dc7deb6b0cfba7d785159149e27585c6e6 Mon Sep 17 00:00:00 2001 From: Matin Tamizi Date: Wed, 11 Dec 2024 02:14:44 -0600 Subject: [PATCH 3/4] ignore this exception in coverage --- tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index d9201e9..fe7e310 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -115,7 +115,7 @@ def sanitize_response(response): buffer = io.BytesIO(body) try: body = gzip.GzipFile(fileobj=buffer).read() - except gzip.BadGzipFile: + except gzip.BadGzipFile: # pragma: no cover ... else: response["body"]["string"] = body From 89e2b9ad42da97f7c6ad7306e5a33457e5084fd1 Mon Sep 17 00:00:00 2001 From: Matin Tamizi Date: Wed, 11 Dec 2024 02:22:01 -0600 Subject: [PATCH 4/4] use fewer asserts --- garth/sso.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/garth/sso.py b/garth/sso.py index 5ee932c..4fe3448 100644 --- a/garth/sso.py +++ b/garth/sso.py @@ -55,7 +55,10 @@ def _complete_login( """ # Parse ticket m = re.search(r'embed\?ticket=([^"]+)"', html) - assert m, "Couldn't find ticket in response" + if not m: + raise GarthException( + "Couldn't find ticket in response" + ) # pragma: no cover ticket = m.group(1) oauth1 = get_oauth1_token(ticket, client) @@ -150,7 +153,8 @@ def login( handle_mfa(client, SIGNIN_PARAMS, prompt_mfa) title = get_title(client.last_resp.text) - assert title == "Success", f"Unexpected title: {title}" + if title != "Success": + raise GarthException(f"Unexpected title: {title}") # pragma: no cover return _complete_login(client, client.last_resp.text) @@ -269,5 +273,6 @@ def resume_login( ) title = get_title(client.last_resp.text) - assert title == "Success", f"Unexpected title: {title}" + if title != "Success": + raise GarthException(f"Unexpected title: {title}") # pragma: no cover return _complete_login(client, client.last_resp.text)