homebridge-adt-pulse icon indicating copy to clipboard operation
homebridge-adt-pulse copied to clipboard

[Feature]: Add 2FA workflow

Open rlippmann opened this issue 1 year ago • 11 comments

Pre-check confirmation

  • [X] I have provided a descriptive title for my feature request
  • [X] I have kept the original title prefix, "[Feature]: "
  • [X] I understand that requests for Z-Wave related accessories will NOT be considered
  • [X] I have confirmed that this feature is NOT already present or planned

Your email address

[email protected]

Describe the new feature requested

Automatically generating the fingerprint for 2fa

Legal Agreements

  • [X] I agree to the Terms of Use and Privacy Policy

rlippmann avatar Jan 25 '24 08:01 rlippmann

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: "[email protected]",
    userEmail: "[email protected]",
    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": "[email protected]"
			},
			{
				"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.

rlippmann avatar Jan 25 '24 09:01 rlippmann

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.

mrjackyliang avatar Jan 25 '24 12:01 mrjackyliang

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.

mrjackyliang avatar Jan 25 '24 12:01 mrjackyliang

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.

mrjackyliang avatar Jan 25 '24 12:01 mrjackyliang

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 avatar Jan 25 '24 16:01 rlippmann

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.

mrjackyliang avatar Jan 25 '24 17:01 mrjackyliang

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.

rlippmann avatar Jan 25 '24 19:01 rlippmann

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.

mrjackyliang avatar Jan 25 '24 20:01 mrjackyliang

I have a feeling it doesn't actually save the browser/os type until you submit the otp.

rlippmann avatar Jan 25 '24 20:01 rlippmann

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?

mrjackyliang avatar Jan 25 '24 20:01 mrjackyliang

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!

mrjackyliang avatar Feb 01 '24 03:02 mrjackyliang

Just updating ya'll on the progress for today

Screenshot 2024-07-23 at 11 30 35 AM

mrjackyliang avatar Jul 23 '24 15:07 mrjackyliang

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:

Screenshot 2024-07-25 at 8 37 08 PM

mrjackyliang avatar Jul 26 '24 00:07 mrjackyliang

Got to generate fake fingerprints based on random data. Don't need the fingerprint script anymore unless things change in the future.

mrjackyliang avatar Jul 26 '24 20:07 mrjackyliang

Got some progress on it, 90% complete! The old fingerprint tool will be phased out. The plugin will now help you self-generate fingerprints

mrjackyliang avatar Aug 09 '24 16:08 mrjackyliang

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.

mrjackyliang avatar Aug 12 '24 13:08 mrjackyliang

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:

  • An automatic fingerprint generator (the old fingerprint tool no longer works).
  • An automatic sensor data pre-filler (no need to manually add sensors).
  • A plugin page to automatically show Homebridge information (makes it easy to screenshot in case of issues).

mrjackyliang avatar Aug 19 '24 21:08 mrjackyliang