Skip to content

MFA support: Token request failed: Unauthorized #2

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

Open
shtrom opened this issue Nov 18, 2023 · 7 comments · May be fixed by #3
Open

MFA support: Token request failed: Unauthorized #2

shtrom opened this issue Nov 18, 2023 · 7 comments · May be fixed by #3

Comments

@shtrom
Copy link

shtrom commented Nov 18, 2023

With Aurora+'s recent push to use MFA, this lib is no longer able to authenticate.

>>> import AuroraPlus
>>> api = AuroraPlus.api(username, password)
>>> api.gettoken(username, password)
>>> api.Error
'Token request failed: Unauthorized'

The most obvious impact is the HA integration LeighCurran/AuroraPlusHA#10

@shtrom
Copy link
Author

shtrom commented Nov 18, 2023

Some quick inspection of the flow from the browser. It looks like it's an OAuth bearer flow. I'm not fully familiar with it, but that looks pretty standard.

  1. The login pages https://customers.auroraenergy.com.au/auroracustomers1p.onmicrosoft.com/b2c_1a_sign_in/oauth2/v2.0/authorize?client_id=<CLIENT_UUID>&scope=openid%20profile%20offline_access&redirect_uri=https%3A%2F%2Fmy.auroraenergy.com.au%2Flogin%2Fredirect&client-request-id=<REQUEST_ID>&response_mode=fragment&response_type=code&x-client-SKU=msal.js.browser&x-client-VER=2.38.2&client_info=1&code_challenge=... sets cookies ASLBSA, ASLBSACORS (same hex value), and a few x-ms-* (base64 encoded)
  2. Login and password are POSTed to https://customers.auroraenergy.com.au/auroracustomers1p.onmicrosoft.com/B2C_1A_SIGN_IN/SelfAsserted?tx=StateProperties=<BASE64_JSON>&p=B2C_1A_SIGN_IN
  • The <BASE64_JSON> contains a single TID attribute, which looks like a UUID v4
  • The application/x-www-form request has an X-CSRF-TOKEN as and contain three fields: request_type: "RESPONSE", signinName and password.
  • The JSON response contains a status attribute, set to 200 on success, and sets 4 cookies
  1. MFA data is POSTed to https://customers.auroraenergy.com.au/auroracustomers1p.onmicrosoft.com/B2C_1A_SIGN_IN/SelfAsserted?tx=StateProperties=<BASE64_JSON>&p=B2C_1A_SIGN_IN
  • The <BASE64_JSON> is similar (maybe same?) as before
  • The application/x-www-form request has an X-CSRF-TOKEN as and contain three fields: request_type: "RESPONSE", otpCode.
  • The JSON response is similar to before
  1. It hits https://customers.auroraenergy.com.au/auroracustomers1p.onmicrosoft.com/B2C_1A_SIGN_IN/api/SelfAsserted/confirmed?csrf_token=<ANOTHER_CSRF_TOKEN>&tx=StateProperties=&p=B2C_1A_SIGN_IN&diags=%7B%22pageViewId[...]URL%3Ahttps%3A%2F%2Fcustomers.auroraenergy.com.au%2Fmy-cdn%2Fcomponents%2Fapi.selfasserted.totp%2Findex.html[...]
  • This returns a 302 redirection to the redirection URL given in step 1, with a `#state=<BASE64_JSON>&client_info=<BASE64_CLIENT_INFO_JSON>&code=
  • The JWT seems to be used as a refresh_token
  1. It hits https://customers.auroraenergy.com.au/auroracustomers1p.onmicrosoft.com/b2c_1a_sign_in/oauth2/v2.0/token
  • Request:
{
	"client_id": "<CLIENT_UUID>",
	"redirect_uri": "https://my.auroraenergy.com.au/login/redirect",
	"scope": "openid profile offline_access",
	"code": "<JWT>",
	"x-client-SKU": "msal.js.browser",
	"x-client-VER": "2.38.2",
	"x-ms-lib-capability": "retry-after, h429",
	"x-client-current-telemetry": "5|865,0,,,|@azure/msal-react,1.5.11",
	"x-client-last-telemetry": "5|0|||0,0",
	"code_verifier": "<SOME_STRING>",
	"grant_type": "authorization_code",
	"client_info": "1",
	"client-request-id": "<UUID>",
	"claims": "{\"access_token\":{\"xms_cc\":{\"values\":[\"CP1\"]}}}",
	"X-AnchorMailbox": "Oid:<UUID>-b2c_1a_sign_in@<UUID>"
}
  • Response:
{
        "id_token": "<OAUTH_TOKEN>",                                                                                                                                    
        "token_type": "Bearer",
        "not_before": 1700293825,
        "client_info": "<BASE64_CLIENT_INFO_JSON>",
        "scope": "",
        "refresh_token": "<JWT>",
        "refresh_token_expires_in": 86400
}
  1. Hit https://api.auroraenergy.com.au/api/identity/LoginToken with a JSON payload with the <OAUTH_TOKEN> as token and receive a JSON payload with "tokenType": "bearer" and an "accessToken": "bearer <ACCESS_TOKEN>"
  2. The accessToken can be put in an authorization HTTP header on subsequent requests to, e.g., https://api.auroraenergy.com.au/api/customers/current or https://api.auroraenergy.com.au/api/usage/day?serviceAgreementID=&customerId=&index=-1.
  • Step 3 is a way to get a (hopefully long-lived) refresh token
  • Step 4 (and 5) seems to be usable for renew a token after expiration.
  • Step 7 is what this lib needs.

@shtrom
Copy link
Author

shtrom commented Nov 19, 2023

I've started trying to play around with https://gist.github.com/shtrom/e13b667f8181b05a53761e34a473525e

Findings so far:

  1. Looks like pretty standard OAuth Web Application Flow https://requests-oauthlib.readthedocs.io/en/latest/oauth2_workflow.html#web-application-flow
  2. Can't change the client_id, or the redirect_uri, so we need no pretend we're the website (or, maybe, the Android app), and will probably need some manual handling to pass the redirect URL back to where we can parse it.
  3. I can't find the client_secret, which is needed to get a token. This makes sense, as it's a secret, but I suspect it's got to be somewhere so the browser-side code can create the needed code_verifier

@shtrom
Copy link
Author

shtrom commented Nov 19, 2023

Ok, the client_secret has nothing to do with it. The approach is to create a code_verifier, hash and base64urlsafe encode it as the code_challenge https://www.valentinog.com/blog/challenge/

code_verifier = ''.join([random.choice(
    string.ascii_letters
    + string.digits
    + '-_')
    for i in range(43)])
code_challenge = base64.urlsafe_b64encode(
    hashlib.sha256(code_verifier.encode()).digest()
).strip(b'=')

This now works, and I have updated the Gist, but we're facing the next problem of a oauthlib.oauth2.rfc6749.errors.MissingTokenError: (missing_token) Missing access token parameter.

Stay tuned.

@shtrom
Copy link
Author

shtrom commented Nov 19, 2023

The response from /token looks good, but we then need to grab the id_token, and pass it to https://api.auroraenergy.com.au/api/identity/LoginToken as

{ "token": <id_token> }

to get the bearer token to use.

@shtrom
Copy link
Author

shtrom commented Nov 19, 2023

More progress: I'm now getting an access_token... but it's then getting rejected... Gist updated.

@shtrom
Copy link
Author

shtrom commented Nov 20, 2023

Ok, I got it!

I have a Web application flow that allows a Python script to get a token from a user interaction with MFA to authorise it. For some reason, it seems that there is some user-agent sniffing, and python-requests is unwelcome, but curl, or what seems to be anything else, can go through.

I haven't try refreshing the token yet, but the gist is updated and should be functional. This still needs to be properly integrated into the lib, but this is looking good.

@shtrom
Copy link
Author

shtrom commented Nov 22, 2023

It also looks like the Login token doesn't expire (at least not within 24h, so far)

@shtrom shtrom linked a pull request Nov 22, 2023 that will close this issue
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

Successfully merging a pull request may close this issue.

1 participant