gagebenne/pydexcom

Authenticating with Phone number username format

markhuber opened this issue · 33 comments

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.

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!

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.

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?

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

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

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.

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

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.

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.

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

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.

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).

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

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 :)

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

"username": "USERNAME", # phone

with

"phone": "+1234567890",

@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

Might be advisable to add this issue to the readme until it's resolved -- I think a lot of people will be using phone numbers as commentators above said.

Good idea -- just merged that and is published to PyPI pydexcom=0.4.0.

I just got non-US account ID retrieval working, and will post that up tomorrow (@melwaraki that should've been my first question, sorry -- are you US-based or outside of the US?).

Sorry for the delay y'all!

I was trying with someone's US account and that's when I got that error

But the piece of info you added to the README file is great! I might just ask my users to do that
image

If it's useful to you and your debugging, I've added my code and console output below: (no pressure to debug for my sake btw as I'm happy sticking with your suggestion in the README)

import requests
import re
from base64 import b64decode
from oic import rndstr
from pydexcom import Dexcom
from pydexcom.const import DEXCOM_BASE_URL

USERNAME = "+1..." # phone number here
PASSWORD = "****" # password here

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,
)

print(r1.headers["location"])
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"]

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

print(r3.headers["location"])

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

print(r4.headers["location"])
# # print(r4.text)
# exit(0)

m = re.search(r"&access_token=(?P<access_token>.+?)&", r4.headers["location"])
# The regex pattern may not match the content of r4.headers["location"], thus returning None
if m is None:
    raise ValueError("access_token not found in the location header")
access_token = b64decode(m["access_token"])

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

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())

Console:

https://uam1.dexcom.com/identity/login?signin=d88...
https://uam1.dexcom.com/identity/login?signin=d88...
https://uam1.dexcom.com/multiaccount?loginOptions=eyIr...
Traceback (most recent call last):
  File "/home/runner/Dexcom-Phone-Number/main.py", line 70, in <module>
    print(r4.headers["location"])
  File "/home/runner/Dexcom-Phone-Number/.pythonlibs/lib/python3.10/site-packages/requests/structures.py", line 52, in __getitem__
    return self._store[key.lower()][1]
KeyError: 'location'

I hid some info cuz it's not my account but that of a separate user

import requests
import random
import string
from typing import Optional
import re
from base64 import b64decode

###
USERNAME: str = USERNAME
PASSWORD: str = PASSWORD
OUS: bool = False
###

DEXCOM_OAUTH_BASE_URL_US: str = "https://uam1.dexcom.com"
DEXCOM_OAUTH_BASE_URL_OUS: str = "https://uam2.dexcom.com"

DEXCOM_OAUTH_BASE_URL: list[str] = [DEXCOM_OAUTH_BASE_URL_US, DEXCOM_OAUTH_BASE_URL_OUS]

DEXCOM_OAUTH_CLIENT_ID_US: str = "1be494ad-4312-a8f7-b01b-0c3cf6b93a96"
DEXCOM_OAUTH_CLIENT_ID_OUS: str = "0b72c581-2e98-4644-989c-52c76e34842b"

DEXCOM_OAUTH_CLIENT_ID: list[str] = [
    DEXCOM_OAUTH_CLIENT_ID_US,
    DEXCOM_OAUTH_CLIENT_ID_OUS,
]

DEXCOM_OAUTH_AUTHORIZE_ENDPOINT: str = "identity/connect/authorize"

DEXCOM_OAUTH_REDIRECT_URI_US: str = "https://myaccount.dexcom.com/profile"
DEXCOM_OAUTH_REDIRECT_URI_OUS: str = "https://uam2.dexcom.com/auth.html"

DEXCOM_OAUTH_REDIRECT_URI: list[str] = [
    DEXCOM_OAUTH_REDIRECT_URI_US,
    DEXCOM_OAUTH_REDIRECT_URI_OUS,
]

DEXCOM_OAUTH_RESPONSE_TYPE_US: str = "id_token token"
DEXCOM_OAUTH_RESPONSE_TYPE_OUS: str = "token"

DEXCOM_OAUTH_RESPONSE_TYPE: list[str] = [
    DEXCOM_OAUTH_RESPONSE_TYPE_US,
    DEXCOM_OAUTH_RESPONSE_TYPE_OUS,
]

DEXCOM_OAUTH_SCOPE_US: str = "openid AccountManagement"
DEXCOM_OAUTH_SCOPE_OUS: str = "AccountManagement"

DEXCOM_OAUTH_SCOPE: list[str] = [DEXCOM_OAUTH_SCOPE_US, DEXCOM_OAUTH_SCOPE_OUS]

s = requests.Session()


def generate_nonce() -> str:
    return "".join(
        [random.choice(string.ascii_letters + string.digits) for _ in range(32)]
    )


authorization_response = s.get(
    f"{DEXCOM_OAUTH_BASE_URL[OUS]}/{DEXCOM_OAUTH_AUTHORIZE_ENDPOINT}",
    params={
        "client_id": DEXCOM_OAUTH_CLIENT_ID[OUS],
        "redirect_uri": DEXCOM_OAUTH_REDIRECT_URI[OUS],
        "response_type": DEXCOM_OAUTH_RESPONSE_TYPE[OUS],
        "scope": DEXCOM_OAUTH_SCOPE[OUS],
        "nonce": generate_nonce(),
        "ui_locales": "en-US",
    },
    headers={
        "Content-Type": "application/x-www-form-urlencoded",
        "Accept": "application/json",
    },
)

if "idsrv.xsrf" not in authorization_response.cookies:
    print("authorization response did not set xsrf cookie")


def extract_xsrf_token(text: str) -> Optional[str]:
    match = re.search(
        r"idsrv\.xsrf&quot;,&quot;value&quot;:&quot;(?P<xsfr>.+?)&quot;}", text
    )
    return match["xsfr"] if match else None


xsrf_token = extract_xsrf_token(authorization_response.text)
if not xsrf_token:
    print("authorization response could not find xsrf token")

login_response = s.post(
    authorization_response.url,
    data={
        "idsrv.xsrf": xsrf_token,
        "username": USERNAME,
        "password": PASSWORD,
    },
    headers={
        "Content-Type": "application/x-www-form-urlencoded",
        "Accept": "application/json",
    },
)


def extract_access_token(location) -> Optional[str]:
    match = re.search(r"[&#]access_token=.+?\.(?P<access_token>.+?)\..+?&", location)
    return match["access_token"] if match else None


access_token = extract_access_token(login_response.url)

if not access_token:
    print("login response could not find access token")


def decode_access_token(access_token) -> Optional[str]:
    padding_needed = 4 - len(access_token) % 4
    if padding_needed != 4:
        access_token += "=" * padding_needed
    decoded_token = b64decode(access_token)
    match = re.search(rb"\"sub\":\"(?P<sub>.+?)\"", decoded_token)
    return match["sub"].decode() if match else None


account_id = decode_access_token(access_token)

if not account_id:
    print("could not find subtoken")

print(account_id)

Do some folks want to try this? I tested it with a US username account, and a non-US email account. Anyone with a US and non-US phone account be able to try this out? It should print your account ID (a non-default UUID).

Edit: Never mind -- it works. I was making a silly mistake.

login response could not find access token

Tried with a US phone number, let me know if you want more info -- happy to help troubleshoot or provide you temporary credentials to give it a shot

That is in fact exactly my issue :) @melwaraki

Ah, dependent accounts. From my understanding those types of accounts can still have a username and password combo -- what happens if you attempt to login using the dependent account instead of the managerial account?

In my case, it pulls the dependent's data not the managerial data

Hmm -- that script seems to be intermittently working? At least I can't get it working at all right now with a US Phone number, getting:

login response could not find access token
Traceback (most recent call last):
File "c:\Users\rs\Desktop\test1.py", line 124, in
account_id = decode_access_token(access_token)
File "c:\Users\rs\Desktop\test1.py", line 116, in decode_access_token
padding_needed = 4 - len(access_token) % 4
TypeError: object of type 'NoneType' has no len()

Ah, okay. I have a US phone-based account and am experienced the same thing. I'll do some more work to reverse that process and check back in.

Something.... weirder for you -- I'm using the pydexcom library as is right now with a US phone number and it's working fine.

Also, I tried using the UUID for an account I have with a username / password, and it doesn't work to login just FYI.

Happy to hop on a call to show you if any of it helps. Thanks for the hard work!

Woah. Very, very weird -- good catch. Yeah, I just tried with the US phone-based account as well as an EU email-based account... They both work out of the box. I did notice subtle API differences when I popped back in to quick release 0.4.0. Seems the Dexcom developers are adjusting the General/LoginPublisherAccountById endpoint to accept phones and emails? If this is the case for EU phones as well, that would be amazing.

Would others mind giving this a shot?

I used the library with the UID on my Windows device, and it worked mostly; sometimes, when I re-run the script, it would error out. I could authenticate with @melwaraki's Sweet Dreams app with a phone number and no issues. When I later tried my phone number with my Windows machine, it wouldn't work when I changed the user ID to phone.

Interesting development. Using SweetDreams 1.17, I was just able to successfully log in with my non-US Dexcom account using my phone number.

Oh wow, I'd just written up a guide to people to use Sweet Dreams with a phone number (https://marwan.craft.me/5m4XRMiiNixv8a) I'm surprised to hear it's now working out of the box? 🤯