Closed rlippmann closed 3 months ago
Hi Jacky,
As promised, here is what I've discovered about the process to do 2fa. I've omitted all the https://portal.adtpulse/myhome/version from all the URLs for brevity. first, when an invalid fingerprint is received, the browser gets a redirect to /mfa/mfaSignIn.jsp?workflow=challenge
In that page, there is some js source as follows:
<script type="text/javascript">
const workflow = "challenge";
window.g.mfa = {
xToken: '',
xPreAuthToken: '7987E5DCAFC24CD01C6F9D7598D329C7636194118616B100574ACA358067E0C3D1D84145769F6EBC51FBB9E557CCB56DFECA4749EF0AAB616E7CCABF8B37C2178F12995C5312A30722439450A7F33749A29DA9AA478B17FAE213805912B0AA604BBD8978E42092B81BF2D52ABF5450FDC492368326088AAD3B122AE664B6B278',
xLogin: "rml.adtpulse@gmail.com",
userEmail: "rml.adtpulse@gmail.com",
xClientType: "web",
env: "prod",
locale: "en_US",
partner: "adt",
sat: '3ac1a3c2-e771-4d28-82ef-2725a4a270fa',
proxyServletUrl: "/nga/serv/RunRRAProxy",
rootAddress: "",
workflow: workflow,
shouldForceMfaSetup: null,
supportPhoneNumber: "1 (800) 251-9581",
mockApi: false,
testCase: 0,
returnToPortalCb: returnToPortalCb
};
if (workflow === 'challenge' || workflow === 'initialSetup') {
// callback only for mfa sign in
window.g.mfa.returnToSignInCb = returnToSignInCb;
} else {
// callbacks only for mfa settings hosted in dialog
window.g.mfa.isDataLoadedCb = resizeAndRecenterDialog;
window.g.mfa.resizeDialogCb = resizeDialog;
window.g.mfa.registerDismissCallback = window.parent.registerDismissCallback;
}
</script>
you'll need the variables defined in g.mfa to set http headers:
{
"name": "X-clientType",
"value": "web"
},
{
"name": "x-dtpc",
"value": "9$392570027_372h2vPKSEFMFGKUWAMVLCGWMCWRFOJDUMFROR-0e0"
},
{
"name": "X-format",
"value": "json"
},
{
"name": "X-locale",
"value": "en-US"
},
{
"name": "X-login",
"value": "rlippmann@hotmail.com"
},
{
"name": "X-preAuthToken",
"value": "D07734ACAC6C9F4139B16B4453ECCFAE9600B51FF1D74A932FDFFAFFF1727252BA986CC361FE20BFD29A01E9426011D19A0CA8A6A4DE96A8A969F2313DEBB1BC374C99D953986488DDC115729B64E49354B51F0705DA4E0337005A23C4288D73BDA7726B6E4DDF91727F5EE8C644D76C422786F9AC426405AF21501C64C9F8F4"
},
{
"name": "X-version",
"value": "7.0"
}
I don't think the x-dtpc is necessary (it's some tracing code on ADT's side), and I'm not sure about X-version then, when you make the following request, you'll get some json back
request: /nga/serv/RunRRAProxy?href=rest/icontrol/ui/client/multiFactorAuth&sat=3ac1a3c2-e771-4d28-82ef-2725a4a270fa
(where sat is the value of the sat variable above)
response:
"state": {
"mfaEnabled": true,
"label": "Enabled (2 verification methods)",
"mfaProperties": [
{
"id": "SMS",
"type": "SMS",
"label": "(***) ***-5549",
"caption": "Text message to <span class=\"ic_strong\">(***) ***-5549</span>"
},
{
"id": "EMAIL",
"type": "EMAIL",
"label": "r***@h***.com",
"caption": "Email to <span class=\"ic_strong\">r***@h***.com</span>"
}
]
},
"commands": {
"requestOtpForRegisteredProperty": {
"params": {
"id": {
"label": "Select a verification method",
"options": [
{
"value": "SMS",
"type": "SMS",
"label": "by text to <span class=\"ic_strong\">(***) ***-5549</span>",
"caption": "Verify with text message"
},
{
"value": "EMAIL",
"type": "EMAIL",
"label": "by email to <span class=\"ic_strong\">r***@h***.com</span>",
"caption": "Verify with email"
}
],
"type": "select"
}
},
"method": "POST",
"action": "rest/adt/ui/client/multiFactorAuth/requestOtpForRegisteredProperty",
"label": "Request a verification code"
},
"validateOtp": {
"params": {
"otp": {
"type": "textInput",
"label": "Enter verification code"
}
},
"method": "POST",
"action": "rest/adt/ui/client/multiFactorAuth/validateOtp",
"label": "Validate verification code"
}
}
}
the commands give you the http requests and the parameters, so to request the otp, you would do: POST /nga/serv/RunRRAProxy with parameter href=rest/adt/ui/client/multiFactorAuth/requestOtpForRegisteredProperty and sat={the sat from the script in the first step} id=EMAIL (or SMS, or whatever id's where in mfapropertries)
Note, the href parameters are relative URLs (i.e. don't need the https://portal.adtpulse.com/myhome/version
to validate: POST /nga/serv/RunRRAProxy href=rest/adt/ui/client/multiFactorAuth/validateOtp id={the one time password the user received)
to save it, do: GET /nga/serv/RunRRAProxy?only=client.multiFactorAuth&exclude=&sat={sat from script}&href=rest/adt/ui/updates&sat={sat from script}&
One thing I'm not sure of is when you set the fingerprint. I'm guessing the server somehow keeps a copy of it when you log in. I haven't seen it passed around in any of the session cookies.
Anyway, hope this helps. Let me know how it goes, I'm eager to implement it in HA.
Thanks for the insights! I also want to see if the function has some sort of regex checking since during development I tried passing in some random string and it wouldn't accept it.
Probably the first thing to check in case ADT decides to change the fingerprint.
And yes, the x-dtpc
header is Dynatrace tracking. Explanations are in the source files. It's not required, but I generate these tokens (random) just to weed the observability out of the way.
Also the fingerprint is set into the backend session once you hit the submit button, it just depends if you complete the 2FA process. If it does, the backend would be setting that fingerprint into the account list of approved 2FA keys.
I didn't see where it passed the fingerprint as a parameter. So let me know what you find.
The fingerprint looks a lot like a JWT, so they might do some validation. But if we can just make a random fingerprint, that would be great.
I can't remember exactly how, but I did generate the json. I either ran https://auth.pulse-api.io/v2/sso/US/devicefingerprint somehow, or used the javascript console in browser development tools.
I got this:
https://github.com/rlippmann/pyadtpulse/blob/master/pyadtpulse/fingerprint.json
I was going to take that and modify the ua items to reflect the actual backend OS, and change the browser name to Home Assistant or something, and do something with the uaString.
I was also thinking of changing the http user-agent header before submitting the otp to see if I could make it save it as something like "Home Assistant/Linux OS" or something to make it easier to see which fingerprint was generated.
Oh, I also found out that if you use the same fingerprint on 2 sessions (even 2 different machines) the 2nd session will log out the first. And this is regardless of whether you used different usernames or not. Just an FYI. Was driving me crazy trying to figure out why sessions were being logged out when HA was running. Turns out I was using the same browser I used to generate the fingerprint on another machine to check things.
I didn't see where it passed the fingerprint as a parameter. So let me know what you find.
The fingerprint looks a lot like a JWT, so they might do some validation. But if we can just make a random fingerprint, that would be great.
I can't remember exactly how, but I did generate the json. I either ran https://auth.pulse-api.io/v2/sso/US/devicefingerprint somehow, or used the javascript console in browser development tools.
I got this:
https://github.com/rlippmann/pyadtpulse/blob/master/pyadtpulse/fingerprint.json
I was going to take that and modify the ua items to reflect the actual backend OS, and change the browser name to Home Assistant or something, and do something with the uaString.
I was also thinking of changing the http user-agent header before submitting the otp to see if I could make it save it as something like "Home Assistant/Linux OS" or something to make it easier to see which fingerprint was generated.
Oh, I also found out that if you use the same fingerprint on 2 sessions (even 2 different machines) the 2nd session will log out the first. And this is regardless of whether you used different usernames or not. Just an FYI. Was driving me crazy trying to figure out why sessions were being logged out when HA was running. Turns out I was using the same browser I used to generate the fingerprint on another machine to check things.
@rlippmann The fingerprint is generated by the script when you click login. That's when the system possibly generates a session based of that, and if the fingerprint doesn't match your account, you get redirected to the 2FA page.
The fingerprint is essentially just base64 of the JSON object. It's like JWT, but it doesn't have the parameters surrounding it.
We definitely can make a random fingerprint, but we need to test it out because since the fingerprint is base64, it's not encrypted. The backend servers can decode it and go through necessary checks to see if the fingerprint is indeed valid + if they do any sort of profiling in the backend, then well you're sort of out of luck on that end.
However, we can still base it on a couple of samples for making the fingerprint seem valid, like in my plugin, I fake the headers to make it look like the latest version of Chrome, and if I can get a few versions of the fingerprint or a base on what the fingerprint generates from (operating systems, fonts, plugins and such), then we really don't even need the script from ADT.
I do want to note that it's probably not a good idea to have a static fingerprint since the source of this plugin is open, and well if the fingerprint matches, a wide-scale attack can happen by hackers simply guessing the username/password and using a static fingerprint part of this project as the attack vector.
And for the sessions logging out, I think it makes sense. They have to be doing some sort of decoding on the fingerprint to find out that really is the same user/browser being used. However, this does not affect the operations between let's say an iPhone logging into the same account vs a browser. Those two essentially have different fingerprints.
Good point about the security of the fingerprint.
I assume they just do a simple string comparison of the fingerprint, but I can see them decoding part of it as a sneaky way around the fact that you can change your user agent header.
Maybe generate a UUID and put it in some of the version fields?
Or, even simpler, just create a new field called UUID in the json.
I might need to re-write some of the default headers that I send to ADT to match the browsers. Maybe I might just allow the user to pick from a browser, and then I'll generate what I can generate from there.
I have a feeling it doesn't actually save the browser/os type until you submit the otp.
I have a feeling it doesn't actually save the browser/os type until you submit the otp.
Correct, but the fingerprint is passed into the session once the user clicks login. That's the only way actually, because without it, how can the user save their session vs. how can the portal know this is a saved fingerprint?
This looks interesting: https://github.com/homebridge/plugin-ui-utils
So basically, I can learn how the key is generated (or generate it using a few web browser profiles), then complete the 2FA process automatically, fill in the form, and voila!
Just updating ya'll on the progress for today
Did some testing just now. The backend actually parses your fingerprint when you save the device. If I use some random base64 string, it would have this error:
Got to generate fake fingerprints based on random data. Don't need the fingerprint script anymore unless things change in the future.
Got some progress on it, 90% complete! The old fingerprint tool will be phased out. The plugin will now help you self-generate fingerprints
The 2FA workflow is now complete and released in v3.3.5
! I wanted to finish the new settings panel, but unfortunately, the form parser doesn't do array of strings (or I couldn't find a way to make it work).
So v3.4.0
would be released with a breaking change notice (again).
Anyways, this was a fun 2 week project for me. I'm going to take a break from this before continuing.
The feature request is now 100% complete. v3.4.0
now utilizes a brand new settings panel (React-based) with the option to view the older settings panel (must manually click in). This version brings:
Due to inactivity, this issue will be locked and marked as resolved. If you have any further questions or inquiries, please feel free to open a new issue.
Pre-check confirmation
Your email address
robert.lippmann.development@gmail.com
Describe the new feature requested
Automatically generating the fingerprint for 2fa
Legal Agreements