gagebenne / pydexcom

A simple Python API to interact with Dexcom Share service
https://gagebenne.github.io/pydexcom/
MIT License
140 stars 33 forks source link

Authenticating with Phone number username format #55

Open markhuber opened 9 months ago

markhuber commented 9 months ago

Thanks for the work on the library. I'm trying to authenticate with a G7 sensor through the Share API. I have active followers, but cannot authenticate with the same credentials that work on uam1.dexcom.com

I've reproduced the AuthenticatePublisherAccount call in Postman and am receiving :

 "Code": "AccountPasswordInvalid",
 "Message": "Publisher account password failed"

The only oddity that I can think of is that I signed up entirely through the mobile app and ended up with a username that is my phone number formatted as +10000000 . Are there known issues authenticating when the account has this format number? I have changed passwords and ensured that it is only alphanumeric.

gagebenne commented 9 months ago

Hi, I've poked around at this a bit, and just cant find a means of logging in via phone number. I'll investigate further how the Dexcom G6 / G7 application behaves to see about supporting this behavior.

I do need to update the README to reflect this, but for now I'd recommend just creating a new account using an email / username. Apologies!

markhuber commented 9 months ago

Thanks. I attempted to inspect http traffic from the phone with burpsuite and a different proxy android app. I can see it's still taking to share2.dexcom.com but I believe because of SSL pinning, every time I route through the proxy the app shows the Follow service shows an Internet outage.

If we can get some info on the basic auth procedure I'd be happy to send a pull.

markhuber commented 9 months ago

Just to follow-up. I confirmed with my next sensor that a completely new account signed up through the web interface using my email works as expected. I can authenticate against the API, get readings back from pydexcom and the thing that started my investigation was getting g-watch app working again.

It's worth noting that as a brand new dexcom user (switched from Abbott FS3), the wizard navigation in the app for a new users defaults you to the phone number path unless you explicitly switch over to email. That likely will increase the frequency of these phone number based usernames. Being new, I'm not sure how long that has been in effect.

I noticed #56 posted this weekend. It's interesting that the username format for the phone numbers also starts with +. Perhaps an expected encoding issue for this character?

findthebug commented 9 months ago

Maybe it is nothing. I did some testing on this phone number issue and at least on the dexcom website i was able get a json with some data on it. There is a property called: "usernameType": "PhoneNumber", which indicates what we are looking for. I did a quick test and passed that as an parameter in the json body but with no luck.

// swift code adapted
let jsonBody: [String: String] = [
        "accountName": user,
        "password": password,
        "usernameType": "PhoneNumber",
        "applicationId": dexcomApplicationlID
    ]

Image left: New account (phone-only) Image right: Old account (email)

tt
findthebug commented 8 months ago

What is also interesting: If you login with phone, dexcom is doing a nice job and displays the proper phone number format for each country selected. e.g. Swiss phone is like 000 000 00 00, which is the proper format. if you change countries you will see a lot of different formats like USA is (000) 000-0000. The Country code itself is not represented in that format. maybe we need so post username like 000 000 00 00 with spaces & - ( ) and the country code with other json prop.

Also i read about dexcom is doing the SMS login since may 2023 and all new user will have this option.

https://www.dexcom.com/en-us/creating-dexcom-account-using-mobile-number

gagebenne commented 7 months ago

After doing a bit of Charles Proxy work, I think I'll need to do some openid authentication. I've been able to authenticate using a phone number using Python, and I think there is a means of getting a session ID for the Share service. Probably won't get around to much the month with the holidays, but perhaps in the new year I'll have a moment to implement this feature.

bruderjakob12 commented 7 months ago

I'm very interested in getting that issue resolved. Let me know if you need any support.

gagebenne commented 5 months ago

So, had a moment to explore this further. Here's are some loose notes on the login process using oauth / openid:

First request

First it navigates to a sign in page, with a seemingly random UUID. The body contains username and password, and I have confirmed that this can be a phone number.

DEXCOM_IDENTITY_LOGIN_ENDPOINT = "identity/login"
requests.post(
  DEXCOM_BASE_URL + DEXCOM_IDENTITY_LOGIN_ENDPOINT,
  params = {
    "signin": "...", # random UUID
  }
  data = {
    "username": username,
    "password": password,
    "idsrv.xsrf": "...", # random bytes
  },
)

The response returns a location that is used in the next request, along with a lot of cookies. There is also a client_id that is returned and seems to be constant every time. A state is set, but this is likely an oauth thing?

Location:
https://uam1.dexcom.com/identity/connect/authorize
?client_id = 0b72... # known, static
?redirect_uri = https://uam1.dexcom.com/auth.html
?state = ... # some state

Set-Cookie:
Lots of things

Second request

Now using the main oauth endpoint, the client_id from the last request is used (this is different if logging in via Dexcom app, still static though ffda...).

DEXCOM_AUTHORIZE_ENDPOINT = "identity/connect/authorize"
requests.get(
  DEXCOM_BASE_URL + DEXCOM_AUTHORIZE_ENDPOINT,
  params = {
    "client_id": "0b72...", # from first response header
    "redirect_uri": "https://uam1.dexcom.com/auth.html", # from first response header
    "response_type": "token",
    "scope": "AccountManagement",
    "state": "...", # from first response header
  },
  cookies = {}, # from first response header
)

Seems like we get an access_token, but it's a wimpy 3600 expiration token.

Location: 
https://uam1.dexcom.com/auth.html
?access_token = # random bytes
?token_type = Bearer
?expires_in = 3600
?scope = AccountManagement # same as second request params
?state = ... # same as second request params

Set-Cookie:
Sets cookie idsvr.clients

Third request

Now for the long-lasting token. Another known, static client_id, but this time the oauth scope is openid AccountManagement with response type id_token. The nonce is another field that likely an oauth client could generate.

DEXCOM_AUTHORIZE_ENDPOINT = "identity/connect/authorize"
requests.get(
  DEXCOM_BASE_URL + DEXCOM_AUTHORIZE_ENDPOINT,
  params = {
    "client_id": "1be4...", # known, static
    "redirect_uri": "https://myaccount.dexcom.com/profile",
        "scope": "openid AccountManagement",
        "response_type": "id_token token",
        "nonce" = "...", # random UUID
    },
    cookies = {}, # from first response header
)

Gets back an id_token that is long lasting.

Location:
https://myaccount.dexcom.com/profile
?id_token = # random bytes
?expires_in = 3153600
?session_state = ... # random bytes
?scope = openid AccountManagement

Set-Cookie:
Sets cookie idsvr.clients

Now... I'm hoping I can use this newly found token to retrieve Dexcom Share blood glucose values (or authenticate with the Dexcom Share service), just not sure on how yet.

I have not been able to retrieve this token manually (I was able to perform the first post request as mentioned previously, but that access_token doesn't seem all that useful). I'm just sharing updates to see if there are any oauth folks that have more insight on some of these things.

The SugarPixel by @CustomTypeOne and Gluroo apps are able to authenticate using phone numbers and also utilize the Dexcom Share service, so it's possible.

dreksten commented 3 months ago

Just switched from FSL3 to G7 and can confirm that the default signup is with a phone number, at least in my region (Switzerland), and I'd expect that many (most?) new users of the G7 will end up with phone number IDs as well. It seems Sugarmate (edit: SugarPixel) is also able to connect using the phone number accounts. I'd be glad to help in any way I can.

pandaboy6621 commented 1 month ago

Are there any updates on this? I recently got a Dexcom G7 and can't login via a phone number with HA.

criterion67 commented 3 weeks ago

Came here to check and see if there'd been any updates on this as I specifically switched over from the Freestyle Libre 3 to the Dexcom G7 and my username is my cell number with plus plus and one in front of it. This is so frustrating and disappointing that it's not working as this is the specific reason that I chose the G7 due to the integration. Hopefully, This can get resolved quickly.

gagebenne commented 3 weeks ago
import requests
import re
from base64 import b64decode
from oic import rndstr

s = requests.Session()

r1 = s.get(
    "https://uam1.dexcom.com/identity/connect/authorize",
    params={
        "client_id": "1be494ad-4312-a8f7-b01b-0c3cf6b93a96", # US, Dexcom G7
        "redirect_uri": "https://myaccount.dexcom.com/profile",
        "response_type": "id_token token",
        "scope": "openid AccountManagement",
        "nonce": rndstr(32),
        "ui_locales": "en-US",
    },
    headers={
        "Content-Type": "application/x-www-form-urlencoded",
        "Accept": "application/json"
    },
    allow_redirects=False,
)

r2 = s.get(
    r1.headers["location"],
    headers={
        "Content-Type": "application/x-www-form-urlencoded",
        "Accept": "application/json"
    },
    allow_redirects=False, # ?
)

m = re.search(r"idsrv\.xsrf&quot;,&quot;value&quot;:&quot;(?P<xsfr>.+?)&quot;}", r2.text)
xsfr_html = m["xsfr"]

r3 = s.post(
    r2.url,
    data={
        "idsrv.xsrf": xsfr_html,
        "username": "USERNAME", # phone
        "password": "PASSWORD",
    },
    headers={
        "Content-Type": "application/x-www-form-urlencoded",
        "Accept": "application/json"
    },
    allow_redirects=False,
)

r4 = s.get(
    r3.headers["location"],
    headers={
        "Content-Type": "application/x-www-form-urlencoded",
        "Accept": "application/json"
    },
    allow_redirects=False,
)

m = re.search(r"&access_token=(?P<access_token>.+?)&", r4.headers["location"])
access_token = b64decode(m["access_token"])

m = re.search(rb"\"sub\":\"(?P<sub>.+?)\"", access_token)
sub = m["sub"].decode()

from pydexcom import Dexcom
from pydexcom.const import DEXCOM_BASE_URL

class _Dexcom(Dexcom):
    def __init__(self, username, password):
        super().__init__(username, password)
    def _get_account_id(self) -> str:
        return sub.decode()

d = _Dexcom(USERNAME, PASSWORD)

print(d.get_current_glucose_reading())

Apologies for the delay, this was a more intense reverse engineering exercise than I expected. I am able to retrieve share reading using this process via Oauth. A lot of work to get this productized for phone numbers, and the EU as well, but the framework is there. I'm unable to work on this for the next two weeks, but will get back to it afterwards. Feel free to play around with this validation, or productize it (if anyone is more familiar with Oauth2).

gagebenne commented 3 weeks ago

This will eventually resolve home-assistant/core#106279.

melwaraki commented 1 week ago

I'm really excited for this, thanks! r4.headers didn't contain any headers with the "location" key (US phone number), but I'm looking forward to your implementation :)

gagebenne commented 4 days ago

@melwaraki -- Are you able to get anything back if you replace the

"username": "USERNAME", # phone

with

"phone": "+1234567890",
melwaraki commented 3 days ago

@gagebenne unfortunately not :( When I keep username it fails because r4.headers doesn't have location. But when I switch it to phone it breaks a bit earlier (r3.headers is then the one without location)

It's not my login details unfortunately but some users from my app shared theirs with me, and I tried with 2 of them with no luck