Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Invalid login since 11/01/2021 ? #37

Open
qdel opened this issue Feb 11, 2021 · 78 comments
Open

Invalid login since 11/01/2021 ? #37

qdel opened this issue Feb 11, 2021 · 78 comments

Comments

@qdel
Copy link

qdel commented Feb 11, 2021

Hi,

Since the 11/01/2021 morning, the login webservice return a 500 error with the html page: error preview.

The dyson link application seems to work. Credential are correct.

Note also that cloudfare seems to limit the number of request we can do on this webservice. After a couple of 500, we take an error 429 with a Retry-After header set to 3600. (if you want, i made a fix for this and can make a PR).

@qdel
Copy link
Author

qdel commented Feb 11, 2021

Update:

Since around 12:00 Paris time i now meet a 401/{"Message":"Unable to authenticate user."}.

But i can still connect with the same credential using dyson link application and dyson website.

Sadly i don't know how to trace trafic between my phone and their webservices.

@googanhiem
Copy link
Contributor

Last time we had login issues with libpurecool they were caused by a lack of header info in the request, causing us to add a user agent (same as the app). It was assumed at the time this would eventually be blocked by dyson.

@Tloram
Copy link

Tloram commented Feb 11, 2021

Same issue here, as of today all 3 of my Dyson fans are unable to auth. Any ideas?

@qdel
Copy link
Author

qdel commented Feb 11, 2021

Maybe if someone can track how the dyson app connect to the server, we will have all infos.

I can try making a custom made webserver and rerouting the dns call of my phone. But can't it right now.

@bfayers
Copy link

bfayers commented Feb 11, 2021

All I can work out MITM-ing my phone is that it's using the linkapp-api.dyson.com domain

@googanhiem
Copy link
Contributor

googanhiem commented Feb 11, 2021

Also an FYI @shenxn is looking to build a new local control component to link Dyson to Home Assistant.

Not sure if they'd be able to shed any light on any changes to the Dyson API.

Another FYI, @etheralm has said he doesn't really have the time to work on this anymore, so its unlikely we'll be able to easily PR whatever change is needed for this issue.. probably needs a fork at this point.

@bfayers
Copy link

bfayers commented Feb 11, 2021

Header needs to be changed to android client and then it works again. Source: lukasroegner/homebridge-dyson-pure-cool#153

I tested this via postman and got the account/password things in response.

@googanhiem
Copy link
Contributor

Header needs to be changed to android client and then it works again.

Just tested this change to libpurecool in home assistant (by editing current header in the dyson.py) and it didn't work for me.

@bfayers
Copy link

bfayers commented Feb 11, 2021

Just tested this change to libpurecool in home assistant (by editing current header in the dyson.py) and it didn't work for me.

The same request I tried earlier in Postman now doesn't work. seems to be hit or miss as to whether it's going to work or not 🤔

Once this auth is worked out, it may be worth considering saving the credentials the API gives in the hass integration - depending on how long they last ofc.

@bfayers
Copy link

bfayers commented Feb 11, 2021

@googanhiem Ah, interesting the android client header works but only if you log out of the app, then log back in and the endpoint will work with that header for.... an amount of time or number of requests that I don't know yet.

I guess this could be related to the app talking to linkapp-api.dyson.com at some point during it's auth process.

EDIT: after double checking it looks like re-authing the app then using the header already in the library also works.

Something has to be screwy though, I can't get the library to auth even if Postman can.

@googanhiem
Copy link
Contributor

Yeah, I can't replicate the behaviour you're talking about, too bad it would be a decent temp fix.

Maybe some of the auth requests are making it through cloudflare at the moment... so if you reboot and it works.. hold off rebooting for a while if you can.

@bfayers
Copy link

bfayers commented Feb 11, 2021

Yeah, something is really odd - I've got the Account and Password auths from Postman luckily - I can manually make the library work with testing like this:

dc = DysonAccount("","","")
dc._logged = True
dc._auth = HTTPBasicAuth("accountresponsefrompostman","passwordresponsefrompostman")

devs = dc.devices()
connected = devs[0].connect("ipaddress")
devs[0].turn_on()

which does result in the fan turning on.

I was able to achieve HASS functionality again by editing /usr/src/homeassistant/homeassistant/components/dyson/__init__.py

adding an import of from requests.auth import HTTPBasicAuth and below the call for logged = dyson_account.login() adding in:

    logged = True
    dyson_account._logged = True
    dyson_account._auth = HTTPBasicAuth("account", "password")

Of course, I've no idea how long those values last.

And just to clarify, it seems to be repeatable that using Postman to POST https://appapi.cp.dyson.com/v1/userregistration/authenticate?country=GB with appropriate JSON data always results in the Account and Password values if done immediately after re-authenticating the official dyson app - even with a user agent of DysonLink/29019 CFNetwork/1188 Darwin/20.0.0

Exporting the postman request as code is this:

import requests

url = "https://appapi.cp.dyson.com/v1/userregistration/authenticate?country=GB"

payload="{\r\n    \"Email\": \"emailhere\",\r\n    \"Password\": \"passwordhere\"\r\n}"
headers = {
  'User-Agent': 'DysonLink/29019 CFNetwork/1188 Darwin/20.0.0',
  'Content-Type': 'application/json'
}

response = requests.request("POST", url, headers=headers, data=payload)

print(response.text)

And this does actually work - providing you've just re-authenticated the official app.

I also think it's worth mentioning that both those values have not changed during all my testing today.

@shenxn
Copy link

shenxn commented Feb 12, 2021

To properly send JSON data, you can use

response = requests.request("POST", url, headers=headers, json=payload)

instead of manually add Content-Type header. Note payload should be a dictionary instead of string.

@shenxn
Copy link

shenxn commented Feb 12, 2021

I think I found the problem. To make the authentication work, we should first check account status by making a GET request to /v1/userregistration/userstatus?country=GB&email=YOUR_EMAIL_ADDRESS and then do the normal login.

@Grizzelbee
Copy link

Grizzelbee commented Feb 12, 2021

@shenxn
Just tried your recommendation in my ioBroker-Adapter and can confirm: it's working.

@bfayers
Copy link

bfayers commented Feb 12, 2021

instead of manually add Content-Type header. Note payload should be a dictionary instead of string.

Ah yeah, my bad - that's just the code that postman generated.

@Alexwijn
Copy link

So in summary we only need to add this piece of code before we do the login.

        requests.get("https://{0}/v1/userregistration/userstatus?country={1}&email={2}".format(
            self._dyson_api_url, self._country, self._email
        ),
            headers=self._headers,
            verify=False
        )

@shenxn
Copy link

shenxn commented Feb 12, 2021

So in summary we only need to add this piece of code before we do the login.

        requests.get("https://{0}/v1/userregistration/userstatus?country={1}&email={2}".format(
            self._dyson_api_url, self._country, self._email
        ),
            headers=self._headers,
            verify=False
        )

You can use the params arg instead of string format so that requests can handle URL encode for you.

@Alexwijn
Copy link

You can use the params arg instead of string format so that requests can handle URL encode for you.

That is also an option, I just thought I would match the current code style.

@abshgrp
Copy link

abshgrp commented Feb 12, 2021

Hey @shenxn - apologies I am a tad confused. Are you suggesting the only change required is to add adding the following before login within the dyson.py file? The changes from @bfayers are not required?

requests.get("https://{0}/v1/userregistration/userstatus?country={1}&email={2}".format(
self._dyson_api_url, self._country, self._email
),
headers=self._headers,
verify=False
)

I have tried that but still not working so just curious if @bfayers changes are also requried and your suggestion gets around having to auth using the phone app before login from HA?

class DysonAccount:
"""Dyson account."""

def __init__(self, email, password, country):
    """Create a new Dyson account.

    :param email: User email
    :param password: User password
    :param country: 2 characters language code
    """
    self._email = email
    self._password = password
    self._country = country
    self._logged = False
    self._auth = None
    self._headers = {'User-Agent': DYSON_API_USER_AGENT}
    if country == "CN":
        self._dyson_api_url = DYSON_API_URL_CN
    else:
        self._dyson_api_url = DYSON_API_URL

def login(self):
    **"""Check Dyson Account Status"""
    requests.get("https://{0}/v1/userregistration/userstatus?country={1}&email={2}".format(
        self._dyson_api_url, self._country, self._email
    ),
        headers=self._headers,
        verify=False
    )**

    """Login to dyson web services."""
    urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
    _LOGGER.debug("Disabling insecure request warnings since "
                  "dyson are using a self signed certificate.")

    request_body = {
        "Email": self._email,
        "Password": self._password
    }
    login = requests.post(
        "https://{0}/v1/userregistration/authenticate?country={1}".format(
            self._dyson_api_url, self._country),
        headers=self._headers,
        data=request_body,
        verify=False
    )

@bmorris591
Copy link

bmorris591 commented Feb 12, 2021

Based on this thread I've added exactly that to my Kotlin integration with Dyson and it seems to work.

The only difference is that I parse the result of the userstatus call to check the account is ACTIVE.

@abshgrp
Copy link

abshgrp commented Feb 12, 2021

I have tried just @bfayers changes but I keep getting the following error. What am I doing wrong here?

Traceback (most recent call last):
File "/usr/src/homeassistant/homeassistant/setup.py", line 213, in _async_setup_component
result = await task
File "/usr/local/lib/python3.8/concurrent/futures/thread.py", line 57, in run
result = self.fn(*self.args, **self.kwargs)
File "/usr/src/homeassistant/homeassistant/components/dyson/init.py", line 68, in setup
dyson_devices = dyson_account.devices()
File "/usr/local/lib/python3.8/site-packages/libpurecool/dyson.py", line 93, in devices
for device in device_response.json():
File "/usr/local/lib/python3.8/site-packages/requests/models.py", line 900, in json
return complexjson.loads(self.text, **kwargs)
File "/usr/local/lib/python3.8/site-packages/simplejson/init.py", line 525, in loads
return _default_decoder.decode(s)
File "/usr/local/lib/python3.8/site-packages/simplejson/decoder.py", line 370, in decode
obj, end = self.raw_decode(s)
File "/usr/local/lib/python3.8/site-packages/simplejson/decoder.py", line 400, in raw_decode
return self.scan_once(s, idx=_w(s, idx).end())
simplejson.errors.JSONDecodeError: Expecting value: line 1 column 1 (char 0)

image

@Alexwijn
Copy link

Hmm...I got it working now but I also needed to change the login logic to this:

        login = requests.post(
            "https://{0}/v1/userregistration/authenticate?country={1}".format(
                self._dyson_api_url, self._country),
            headers=self._headers,
            json=request_body,
            verify=False
        )

Notice I changed data to json.

@abshgrp
Copy link

abshgrp commented Feb 12, 2021

Hey @Alexwijn did you only change the dyson.py file not the init.py?

@googanhiem
Copy link
Contributor

@bfayers code works great. Thanks all

@abshgrp
Copy link

abshgrp commented Feb 12, 2021

Thanks @bfayers! Updated dyson.py as per your commit and restored my init.py back to original and Dyson integration is now working again.

For anyone else that is confused and possibly not as technically proficient like myself, you only need to update the dyson.py file located /usr/local/lib/python3.8/site-packages/libpurecool. Refer to merge request above. Cheers

@qdel
Copy link
Author

qdel commented Feb 12, 2021

@bfayers just saw your PR, made an answer on home-assistant/core#46400 (comment) but it was not the correct place to.

Find on my code that i also manage a http error 429.

@JanJaapKo
Copy link

Hi all,

I'm maintaining a (plugin for Domoticz ) for the Dyson air purifiers. Can someone tell me what's the best repo for looking at code to do the authentication?

As I also get the problem. Whenever Domoticz (or the plugin) restarts, I can't login to Dyson cloud account anymore. The workaround with logging in on the app works indeed for a limited amount of time. I too will copy the credentials locally as a backup method. If someone wants to add stuff to their account, they'll need to go through the workaround of logging into the app.

@bfayers
Copy link

bfayers commented Mar 5, 2021

@JanJaapKo I've managed to find the API calls the app uses to make it work @googanhiem you may also be interested (as might @shenxn )

After entering the email in the app it does these two calls:

User Agent for all calls is: Dalvik/2.1.0 (Linux; U; Android 8.1.0; Google Build/OPM6.171019.030.E1)

Call 1

Method: POST
Content-Type: application/json
URL: https://appapi.cp.dyson.com/v3/userregistration/email/userstatus?country=GB

Body:

{"email":"emailaddress"}

Expected Response:

{
"accountStatus":"ACTIVE",
"authenticationMethod":"EMAIL_PWD_2FA"
}

Call 2

Method: POST
Content-Type: application/json
URL: https://appapi.cp.dyson.com/v3/userregistration/email/auth?country=GB&culture=en-US

Body:

{"email":"emailaddress"}

Expected Response:

{"challengeId":"challengeIdHere"}

After entering the password and code these calls are made:

Call 3

Method: POST
Content-Type: application/json
URL: https://appapi.cp.dyson.com/v3/userregistration/email/verify?country=GB

Body:

{
  "email": "emailaddress",
  "password": "password",
  "challengeId": "challengeIdFromEarlier",
  "otpCode": "codeFromEmail"
}

Expected Response:

{
  "account": "accountID",
  "token": "token",
  "tokenType": "Bearer"
}

Call 4

Method: GET
URL: https://appapi.cp.dyson.com/v2/provisioningservice/manifest

Headers:

Authorization: Bearer tokenFromEarlier

Expected Response: Same as it already is, just authenticating differently.

Hope all this info is helpful in implementing. I'm unsure how we can deal with the new OTP based auth in this library more than just the recommendation of logging out and back into the app then using the library to retrieve local credentials - there might be something to be said for changing it on HASS' end to allow direct entry of local credentials that a user could retrieve themselves with the help of a script I could write.

@JanJaapKo
Copy link

@bfayers: really good work! I tied to use it by just changing to v2 endpoint, that didn't help. v3...... Interesting would be to see if a login from something else than the app can also succeed out of the blue. Perhaps have a look into the APK......

I will update my Domitcz plugin and build a mechanism to store the machine's credentials in Domoticz DB (they have a feature called 'Config' for the plugins to store this kind of info). It will then use the Config stored credentials if none were returned from the cloud and I'll leave a note on my Wiki that users must initially first login with the app.

I haven't found out yet how long the loginfrom the app clears other login's, during my testing I run into this error: '429, Too Many Requests'......

@bfayers
Copy link

bfayers commented Mar 5, 2021

@bfayers: really good work! I tied to use it by just changing to v2 endpoint, that didn't help. v3...... Interesting would be to see if a login from something else than the app can also succeed out of the blue. Perhaps have a look into the APK......

Perhaps, thought I somewhat doubt it - in my digging into the APK there's not much info about endpoints other than the ones I found, or the ones already known about for the chinese servers.

I haven't found out yet how long the loginfrom the app clears other login's, during my testing I run into this error: '429, Too Many Requests'......

Yep, you'll probably hit that lol just have to wait an hour.

@qdel
Copy link
Author

qdel commented Mar 7, 2021

@bfayers : I tried what you pointed. But i meet a 400 at Call 2.

Code is simple:

import requests

uri = "https://{0}/v3/userregistration/email/auth?country={1}&culture={2}".format(
    'appapi.cp.dyson.com', 'FR', 'en_US')
login = requests.post(
    uri,
    headers={'User-Agent': 'Dalvik/2.1.0 (Linux; U; Android 8.1.0; Google Build/OPM6.171019.030.E1)'},
    json={ 'email': 'mymail' },
    verify=False
)
print(login)

Note that i tried with various country / culture:

  • FR fr_FR (should be, for me)
  • FR en_US
  • GB en_US

Note that Call 1 works perfectly.
Do you know if i need more headers ?

seanrees added a commit to seanrees/prometheus-dyson that referenced this issue Mar 7, 2021
Recently Dyson changed their API which broke libpurecool[1]'s integration.
This resulted in prometheus-dyson being unable to enumerate devices via Dyson,
and thus fail to restart successfully.

libdyson refactors libpurecool with a clearer separation between the online
Dyson API & the device-interaction logic. This allows us to perform a one-time
login to Dyson and cache device information locally, removing the need for
repeated logins to Dyson. libdyson also has a more consistent API between
different models.

This change starts the transition by introducing login component (account.py)
and an adapter (libpurecool_adapter) to use the cached information with
libpurecool. This also adds a flag (--create_device_cache) to perform the
login&OTP dance with Dyson and generate the needed configuration.

[1] etheralm/libpurecool#37
seanrees added a commit to seanrees/prometheus-dyson that referenced this issue Mar 7, 2021
@JanJaapKo
Copy link

@qdel you may want to check out this repo: https://github.com/shenxn/ha-dyson

I'm trying to get that converted into something that will work with Domoticz.....

@qdel
Copy link
Author

qdel commented Mar 7, 2021

This was because of a typo: en_US instead of en-US

@qdel qdel closed this as completed Mar 7, 2021
@qdel qdel reopened this Mar 7, 2021
@Grizzelbee
Copy link

Grizzelbee commented Mar 8, 2021

@bfayers : Just trying to implement but struggeling with the "otpCode": "codeFromEmail".
Where does this code come from? Can't find any hint in what you wrote earlier. Is there a change in registration process? And if so - how do users get this code who registered earlier? Or have I simply overseen something?

@qdel
Copy link
Author

qdel commented Mar 8, 2021

@googanhiem : it is the point of 2fa: you try to login you get a code by mail and you enter code in app (like steam for example)

When trying to login, the Call 2 should trigger a mail from [email protected] to your account mail. Take code from mail and place it in 'codeFromEmail'.

@Grizzelbee
Copy link

@qdel Okay. I understand.
But ... this is nothing I like to implement as the usual way to login to any software running unattended in batch mode.
Additionally I am wondering why the dyson app itself doesn't work this way. I can open it without any 2fa.
Hence there must be a way in the middle to get our logins working again. Avoid this 2fa on one hand and extending the old login on the other.

@qdel
Copy link
Author

qdel commented Mar 8, 2021

Don't like also.

But it is now quite often to have such process (apple ios dev for example...).
The app keep the token in internal storage and can use it afterward without making 2fa verifications. (Does it need to be refreshed after some time? dunno...)

@Grizzelbee
Copy link

Grizzelbee commented Mar 8, 2021

The app keep the token in internal storage

I'm pretty sure it does. ;)
But the main question is: At least for me there hasn't been any 2fa-mailing or something like that after the major app update.
The app got updated, logged in as usual using the well known and stored credentials - and done. That's all. No 2fa. No mail. Nothing.
So: what has the app done? That's what I like to implement - since there seem to be still a way to login with email and pwd (some static data) as before.

@qdel
Copy link
Author

qdel commented Mar 8, 2021 via email

@Grizzelbee
Copy link

Grizzelbee commented Mar 8, 2021

Maybe I made myself not clear enough.
I can't imagine that dyson forces their users every time they open the app to control any dyson device in any way (set the fan speed, light on/off, ...) to run trough the 2fa process. This would be the maximum of inconvenience and would kick dyson right of of the smarthome market - even if this occurs on every reboot of the mobile device only - since there are ways on a always-on device to determine if the token becomes invalid and refresh it early enough.
And again: the dyson app still logs in with only the static credentials - even after a reboot of the mobile device. So how does the app achieve this? Since we get a 401 error when trying to login.
What does the app do diferent than we do?

@qdel
Copy link
Author

qdel commented Mar 8, 2021

The 2fa is for acquiring a token and / or the http authent we were using before.
(logging in the app using 2fa actually allows the old authent to work, maybe it is a glitch from their server or a fallback for the moment)

Since we have either auth mode, we don't need to re-identify, we keep acquiring device manifest with the token / http auth we have on memory.

These information can be stored persistently in your phone, thus persist after reboot (validity for days, years? i don't know and i would like to)...

It is why some implementation split the process of dyson cloud / dyson device:
https://github.com/shenxn/ha-dyson use https://github.com/shenxn/libdyson/tree/main/libdyson.

@bfayers
Copy link

bfayers commented Mar 9, 2021

@qdel Okay. I understand.
But ... this is nothing I like to implement as the usual way to login to any software running unattended in batch mode.
Additionally I am wondering why the dyson app itself doesn't work this way. I can open it without any 2fa.
Hence there must be a way in the middle to get our logins working again. Avoid this 2fa on one hand and extending the old login on the other.

You'll have to find a way around it - I recommend doing the login process once and then storing the localcredentials somewhere... even if you can technically use older versions of the api/app to login without 2FA those endpoints will eventually disappear.

@alkcxy
Copy link

alkcxy commented Mar 14, 2021

Following the indication from @bfayers and @JanJaapKo for retrieving the local credentials for the dyson devices, this is a very trivial workaround in dyson.py.
Change as needed, obviously.
This can't be a definitive solution for the library, of course.

    def login(self):
        """Return always true"""
        self._logged = True
        return self._logged

    def devices(self):
        """Return all devices linked to the account."""
        if self._logged:
            device_v2_response = """
                [
                    {
                        "Serial": SERIAL,
                        "Name": NAME,
                        "Version": VERSION,
                        "LocalCredentials": LOCAL_CREDENTIALS,
                        "AutoUpdate": true
                        "NewVersionAvailable": true,
                        "ProductType": PRODUCT_TYPE,
                        "ConnectionType": "wss"
                    }
                ]
            """
            devices = []

            for device_v2 in json.loads(device_v2_response):
                if is_dyson_pure_cool_device(device_v2):
                    devices.append(DysonPureCool(device_v2))
                elif is_heating_device_v2(device_v2):
                    devices.append(DysonPureHotCool(device_v2))

            return devices

        _LOGGER.warning("Not logged to Dyson Web Services.")
        raise DysonNotLoggedException()

@qdel
Copy link
Author

qdel commented Mar 14, 2021

Hi,

I commited there:
https://github.com/qdel/libpurecool

This version i use, it includes:

  • modification from branch pure_humidify_cool branch (the only device i have)
  • 2fa authent with interactive mode
  • cache, using cache location provided by package appdirs

Note:

  • Code quality is not... quality, made on the border of the table
  • never completly tested: my token never expired, never tested against other account, or any other devices
  • use nukeDeviceCache() from account class to force cloud connection to get new devices linked to account
  • cache is stored as CLEAR

Technical part:

  • the code was splitted in multiple calls: pre_auth, authent (ask for dyson to send mail), verify code, device query.
  • the system store in cache at each steps, so i call them only once (after a first login, i do not need to connect anymore to dyson cloud)
  • if i meet an unauthorized to a call, i nuke the cache.

Maybe I will try to commit my modifications if i need more.

@Tloram
Copy link

Tloram commented Jun 7, 2021

So as of today, this has stopped working again. Even the workaround of launching the Dyson mobile app before starting HA is now failing, meaning that there is no way to currently get the Dyson integration working in HA that I can see. Sadtimes.

@bmorris591
Copy link

@Tloram the best option is to use HACS with ha-dyson in offline mode. It works very well and now supports WiFi credentials to determine local credentials for the devices.

Given the changes Dyson have made it seems unlikely that a cloud API based solution will be workable long term. Although ha-dyson does support cloud discovery too.

@Tloram
Copy link

Tloram commented Jun 7, 2021

@bmorris591 thanks. I just set that up and working great. Guess I'll abandon the official integration for now then. Hopefully ha-dyson and ha-dyson-cloud can be integrated as the official integration replacement at some point soon.

@jamemalame
Copy link

@Tloram the best option is to use HACS with ha-dyson in offline mode. It works very well and now supports WiFi credentials to determine local credentials for the devices.

Given the changes Dyson have made it seems unlikely that a cloud API based solution will be workable long term. Although ha-dyson does support cloud discovery too.

How do you get WiFi credentials to determine local credentials? Since dyson cloud login is not working anymore I cannot use this method to get the WiFi credentials. I have a TP04 which does not have a sticker with WiFi SSID/pass.

@mig8447
Copy link

mig8447 commented Nov 20, 2021

@Tloram the best option is to use HACS with ha-dyson in offline mode. It works very well and now supports WiFi credentials to determine local credentials for the devices.

Given the changes Dyson have made it seems unlikely that a cloud API based solution will be workable long term. Although ha-dyson does support cloud discovery too.

How do you get WiFi credentials to determine local credentials? Since dyson cloud login is not working anymore I cannot use this method to get the WiFi credentials. I have a TP04 which does not have a sticker with WiFi SSID/pass.

Same Here, TP04 does not have a sticker

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests