cyberjunky / home-assistant-garmin_connect

The Garmin Connect integration allows you to expose data from Garmin Connect to Home Assistant.
MIT License
235 stars 33 forks source link

Feature: Implement 2FA functionality #117

Open cyberjunky opened 8 months ago

cyberjunky commented 8 months ago

Placeholder for multiple related tickets

Gawronnek commented 8 months ago

I use the Mi Scale Exporter application on Android, the author Łukasz Świderski also had a problem with 2FA, but he managed to solve it, a window appears where we enter the received code, maybe he could tell us how to solve it in your application, he is a very kind man

velaar commented 8 months ago

Unfortunately Garmin now refuses to disable 2FA even via support request for any ECG-enabled watches. This might become more of a problem over time

scar77 commented 7 months ago

According to Garmin, by law they are required to permanently enable 2FA on watches that have the ECG feature. Until this integration has 2FA capability those of us with newer Garmin watches sporting the ECG feature will not be able to integrate them in Home Assistant. Bummer.

cyberjunky commented 7 months ago

It's the same Garmin who keep their API closed sourced and thus didn't help when I asked them for access to their developers program, that's a Bummer too... But all kidding aside adding this functionality to the integration is still on the top of my list.

RAYs3T commented 7 months ago

Also just hit this issue. But it seems like the package used in the backgroud Garth already supports 2FA. But looks like it tries to read the code from the pipe/std

Exception when login is tried with enabled 2FA:


  File "/config/custom_components/garmin_connect/config_flow.py", line 47, in async_step_user
    await self.hass.async_add_executor_job(api.login)
  File "/usr/local/lib/python3.11/concurrent/futures/thread.py", line 58, in run
    result = self.fn(*self.args, **self.kwargs)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/garminconnect/__init__.py", line 179, in login
    self.garth.login(self.username, self.password)
  File "/usr/local/lib/python3.11/site-packages/garth/http.py", line 160, in login
    self.oauth1_token, self.oauth2_token = sso.login(*args, client=self)
                                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/garth/sso.py", line 95, in login
    handle_mfa(client, SIGNIN_PARAMS)
  File "/usr/local/lib/python3.11/site-packages/garth/sso.py", line 147, in handle_mfa
    mfa_code = input("Enter MFA code: ")
               ^^^^^^^^^^^^^^^^^^^^^^^^^
EOFError: EOF when reading a line```
velaar commented 7 months ago

So all we are missing is an interface to enter the 2fa code? Looks like garth is already requesting it. I tried to hack my way around it but so far no luck.

cyberjunky commented 7 months ago

First we need to make sure we gather and store the oauth tokens in HA after login to account/reauth, and use that for future logins, so ditch the password storage. (there are two ways, store in a file, or in a string in HA storage, the last one is preferred) Then fiddle with new version of Garth (separate branch) where we can set the callback routine to handle MFA (if user has it enabled) See here https://github.com/matin/garth/issues/41 Then look into things like reauth flow when token has expired. I'm looking into how the standard way of HA handling with oauth works (and handling a few of the above items) And trying to free up some time to work on it

matin commented 7 months ago

there are two ways, store in a file, or in a string in HA storage, the last one is preferred

I recommend using Client.dumps() and Client.loads() if you're looking for a way to dump and load the tokens as a base64 encoded string. There are a few projects that use this method vs storing the tokens in files in environment variables.

Then look into things like reauth flow when token has expired.

fwiw, the tokens generated after MFA are valid for one year 😁

obhammer commented 5 months ago

You really need to fix this asap. Garmin is getting more and more aggressive to ‘demand’ 2FA these days. I just bought a dashcam that CANNOT be used without 2FA, rendering my watch integration in HA useless 🤷🏼‍♂️ And all newer watch models with ECG is having the same limitation - meaning that more and more HA users will drop out…

craigrouse commented 5 months ago

@obhammer this integration is kindly developed by @cyberjunky in his spare time. Contributions are welcome from anyone willing to help. Please be a little more considerate with your language - nobody "needs" to fix it, but if someone does take the time, then we will all be grateful.

obhammer commented 5 months ago

@obhammer this integration is kindly developed by @cyberjunky in his spare time. Contributions are welcome from anyone willing to help. Please be a little more considerate with your language - nobody "needs" to fix it, but if someone does take the time, then we will all be grateful.

Well, the fact is that Garmin seem to force ‘all’ users to use 2FA pretty soon - and in that regard, this integration will be rendered useless for ‘all’ users. The word ‘need’ is of course very harsh to use - what I meant was ‘’maybe, if integration should stay relevant, you should evaluate if you maybe need to do something about it - soon….maybe’’ 😉

jmadejek commented 4 months ago

@obhammer this integration is kindly developed by @cyberjunky in his spare time. Contributions are welcome from anyone willing to help. Please be a little more considerate with your language - nobody "needs" to fix it, but if someone does take the time, then we will all be grateful.

Well, the fact is that Garmin seem to force ‘all’ users to use 2FA pretty soon - and in that regard, this integration will be rendered useless for ‘all’ users. The word ‘need’ is of course very harsh to use - what I meant was ‘’maybe, if integration should stay relevant, you should evaluate if you maybe need to do something about it - soon….maybe’’ 😉

Any updates with 2FA?

matin commented 4 months ago

There was a delay due to waiting on a change in garth, an underlying library. I just published the change that should allow this to move forward.

Gawronnek commented 3 months ago

I wrote earlier about the Mi Scale Exporter application for Android, author Łukasz Świderski, the author has already managed to do it so well that you don't even have to enter the code every time, maybe it would help with our problem

matin commented 3 months ago

@Gawronnek the Garmin integration for mi-scale-exporter is based on Garth 😉

I left an earlier comment on how to avoid repeatedly entering the MFA code. The token generated during the login is valid for one year. It just needs to be stored and reused (instead of using username/password + MFA code).

nardev commented 3 months ago

@matin Good news.

LsVzqz commented 3 months ago

Does this mean the feature is coming soon? 😀

I'm currently on a 1476 day step streak and use an input boolean to manually track whether I hit my 10k step goal for the day, otherwise, HA bombards me with reminder messages every hour past 8pm to finish my steps for the day. It would be nice if HA knew how many steps I had without me manually setting the input boolean every day.

Cheers!

Bretos commented 1 week ago

Everybody, there is quite an easy workaround available and working.

If you take a look at underlying login function here you'll notice that it already uses garth library. This is great not only because garth allows saving tokens, but also this integration already tries to load them! So, in summary to use this integration if you have 2FA enabled on your Garmin Connect account you need to:

  1. save session tokens using garth. This is doable with some simple python code, for instance (install garth first, pip install garth:
    
    import garth
    from getpass import getpass

email = input("Enter email address: ") password = getpass("Enter password: ")

If there's MFA, you'll be prompted during the login

garth.login(email, password) garth.save("~/.garth")


this will generate and save your session tokens to `~/.garth` (a hidden directory directly in your home dir, tokens are valid for 1 year)

2. you need to make those tokens accessible by Home Assistant.

    2.1 if you're running Home Assistant in docker:
    * copy content of `~/.garth` to any place you desire
    * mount that directory in the container, eg `/srv/docks/hass/.garth:/config/.garth`
    * add an environment variable `GARMINTOKENS` to equal to the path inside container

    2.2 if you're running HASS anyhow else - add env variable `GARMINTOKENS` that points at the directory tokens are in
4. configure the integration as you would if you didn't have 2FA enabled - tokens will be used
5. PROFIT! it works

[Full explaination](https://blog.tyborek.pl/home-assistant-and-garmin-connect-2fa-workaround/)
nelbs commented 6 days ago

awesome! Got it working 😀

FYI I used this custom integration to add the environment variable to homeassistant in step 2.2

RAYs3T commented 6 days ago

I took a look at it and tried to add the MFA request to the config flow, but since the call from inside garth for the prompt_mfa is not async, this is getting tricky with the HA config flow.

I have no real experience with the HA config flow, but there seems to be no easy way to show a new form over a "step" that is not yet finished. If anybody knows how, let me know and I put what I have into a PR.

My current approch is to start another async call that is waiting for input to the MFA field (with timeout), if given, the login continues.

cyberjunky commented 6 days ago

I took a look at it and tried to add the MFA request to the config flow, but since the call from inside garth for the prompt_mfa is not async, this is getting tricky with the HA config flow.

I have no real experience with the HA config flow, but there seems to be no easy way to show a new form over a "step" that is not yet finished. If anybody knows how, let me know and I put what I have into a PR.

My current approch is to start another async call that is waiting for input to the MFA field (with timeout), if given, the login continues.

I also implemented this, and was stuck on same issue, I then asked Matin from garth to make the function async, and he did, but I didn't have time yet to proceed/test it, make sure you are on last version of garth (in manifest) I believe he implemented it in production version

cyberjunky commented 6 days ago

I'm not 100% sure, it is in main branch or a seperate one, he mentioned it in a closed issue on garths gh, with an unrelated subject, cant find it that quick

RAYs3T commented 6 days ago

Awesome found it in 0.4.46, I will try to implement it and give it another try now.

RAYs3T commented 6 days ago

Yeehh, nah, this is still going into a direction it should not. I asked matin if it would be possible to split the mfa request into a different method, as this would make our life way easier and would go hand in hand with the HA config flow.

https://github.com/matin/garth/issues/41#issuecomment-2346596817

cyberjunky commented 6 days ago

Following the ha rules for config flow is always good yes, but I don't think we ever get it through to be official integration though. Done that with FireServiceRota once and kind of promised myself not to attempt it with another integration, it costed me 5 years of my life. When you made one part follow the strict rules and lint, another part got deprecated...Almost impossible...

cyberjunky commented 6 days ago

But keep in mind you somehow need to know when to ask for MFA and when not, not everyone will enable it.

RAYs3T commented 6 days ago

Following the ha rules for config flow is always good yes, but I don't think we ever get it through to be official integration though. Done that with FireServiceRota once and kind of promised myself not to attempt it with another integration, it costed me 5 years of my life. When you made one part follow the strict rules and lint, another part got deprecated...Almost impossible...

I understand, but the idea I had to get this working with the promt_mfa() Callback would be super dirty and if interrupted could then force a restart of the HA instance to get it working again, and it would also depend on a timer function.

I think we should implement a clean solution or otherwise use the workaround with the GARMINTOKENS as mentioned by Bretos.

I would love to have this implemented in a proper and stable way, even if this never makes it officially into HA, since it uses a closed API and is against the guidelines.

cyberjunky commented 6 days ago

Yeah. I agree. And yes more stable and following latest code structure is also what my aim is... And there are many many more sensors available then implemented in HA integration, I would love to implement categories of sensors to be enable/disabled by user, or autodetected

jacobrian commented 6 days ago

Workaround by Bretos definitely works. I just set it up, also used the custom integration mentioned by nelbs to add the ENV variable.

kirkoff71 commented 2 days ago

awesome! Got it working 😀

FYI I used this custom integration to add the environment variable to homeassistant in step 2.2

You can make a guide step to step? Now I have install Environment Variable, and garminconnect Start garmin connect and make login> login error with mail from garmin with passcode. in configuration mail insert this:

default_config:
environment_variable:
  GARMINTOKENS: 123456 #(123456 is the passcode number received via mail)

restart the home assistant

re-loging garminconnect, but with error.

Thanks

nelbs commented 2 days ago

The value for GARMINTOKENS should be the path to the folder where the tokens are stored, not the passkey you received.

kirkoff71 commented 2 days ago

The value for GARMINTOKENS should be the path to the folder where the tokens are stored, not the passkey you received.

Ok thanks, Then, for example, in the configuration.yaml insert:

`default_config: environment_variable: GARMINTOKENS: /config/.passcode

And in config folder create a file ".passcode" with inside:

123456 In this case the code received from garmin? Thanks

Bretos commented 2 days ago

@kirkoff71 nope, inside directory .passcode put the tokens (two files) which were created by the python script I pasted in my comment

kirkoff71 commented 2 days ago

@kirkoff71 nope, inside directory .passcode put the tokens (two files) which were created by the python script I pasted in my comment

Solve thanks at all... Write for another.

kirkoff71 commented 2 days ago

Solve thanks at all... Write for another peaple if have problem.

1) Enter in home assistant with terminal, if heven't, install through add component " I have install "Advanced SSH & Web Terminal", I have virtual machine on Qnap NAS

2) Install through HACS the 2Environment Variable"

3) Edit "config.yaml" ed insert this code:

default_config:
environment_variable:
  GARMINTOKENS: /config/.passcode/.garth

4) Restart home assistent

5) Create in in /config/.passcode the file "garmin.py" and copy this in the file and copy the file in the "/config/.passcode" (script create from @Bretos, thanks)

import garth
from getpass import getpass

email = input("Enter email address: ")
password = getpass("Enter password: ")
# If there's MFA, you'll be prompted during the login
garth.login(email, password)
garth.save("~/.garth")

6) open the terminal and install "garth" with this command pip install garth

7) In terminal change directory with this command: cd /config/.passcode

8) Lunch the script: python garmin.py and insert, the mail, the password and the passcode send via mail from Garmin

9) Change directory with this command: cd ~ (the ~ create press alt key and digit 126 code on number key)

10) Check if file is present with ls -a

11) If is present now move the ".garth" direcory with this command mv .garth/ /config/.passcode/

12) Install the garmin add on via hacs and make the login with mail and password.

13) Enjoy :)

LsVzqz commented 2 days ago
6. open the terminal and install "garth" with this command `pip install garth`

7. In terminal change directory with this command: `cd /config/.passcode`

8. Lunch the script: `python garmin.py` and insert, the mail, the password and the passcode send via mail from Garmin

Hello! In my terminal, I don't have the pip command, nor python. How would I get that installed? I'm using HAOS..