Skip to content

Conversation

@aleksrozman
Copy link
Contributor

This is a draft pull request as I need time to test this out further to make sure that it still works well after a few hours/days. The logic here assumes the cookies set by ASP (which I verified were the minimum subset) are enough to login without needing to trigger the MFA again.

Some more testing remain:

  1. How long until this expires? A few minutes back to back seems to work, and new tokens are generated each time
  2. How does it retrigger MFA when it is fully expired?

aleksrozman and others added 7 commits September 13, 2025 12:10
Code that enables SMS and Email verification, tested with one of Exelon's subsidiaries
Merged some more similar pieces of code together and separated out Text vs Phone as Call will likely never be an option we will support.
Coincidentally site went down during testing and revealed that it raises a different kind of error than invalid auth. This handles that logic since we rely on the redirect.
Thanks to trickv's diagnoses we know there are two forms of MFA in Exelon's codebase. I was able to locate an account that does not have an associated phone number and verify the logic flow between what I am calling "forced" and "opted" MFA. Users who do not provide a phone are forced into MFA (AFAIK there is no non-MFA version anymore). Users who add a phone, will forever be stuck as opted in. This logic adds email verification through both flows.
First pass to try to store the ASP session and cookies which can be used to acquire a new token.
@aleksrozman
Copy link
Contributor Author

Switching it over to review. @tronikos with new login data that gets created during MFA, what's the recommendation for folks to repopulate it? Should they reconfigure the integration (delete and add it again) or can we catch the 401 and force a new MFA?

@tronikos
Copy link
Owner

Switching it over to review. @tronikos with new login data that gets created during MFA, what's the recommendation for folks to repopulate it? Should they reconfigure the integration (delete and add it again) or can we catch the 401 and force a new MFA?

The expectation is that the login function will raise invalid auth which will trigger reauth in HA. The expectation is that the login function on success always returns a valid opower access token. The simplest solution is likely to raise invalid auth from your function rather than changing other parts of the code base. You could either detect the problematic previous login data or maybe check whether the access token you are about to return hasn't expired.

Also in your browser > developer tools > application > cookies you should be able to see the expiration for these cookies. That will tell you how long before you will be asked for MFA again.

@tronikos
Copy link
Owner

You have to resolve some conflicts cause by #148 How is your testing going? Is MFA now working well? Any change you could resolve this soon so that I can merge and make a release to bump the version in HA before the release on Wednesday?

@tronikos
Copy link
Owner

Adding @PaulSD here in case you could work together to resolve this.

@PaulSD
Copy link
Contributor

PaulSD commented Sep 28, 2025

Yeah, I can test this and let you know how it goes.

@censay
Copy link

censay commented Sep 28, 2025

Happy to test Pepco (Exelon company) which still throws premise info errors. I had completely removed the integration, readded it, and logged back in succesfully with the MFA and then the PremiseInfo errors start:

KeyError: 'message'
2025-09-28 19:05:16.229 ERROR (MainThread) [homeassistant.components.opower.coordinator] Unexpected error fetching Opower data
Traceback (most recent call last):
File "/usr/src/homeassistant/homeassistant/helpers/update_coordinator.py", line 392, in _async_refresh
self.data = await self._async_update_data()
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/src/homeassistant/homeassistant/components/opower/coordinator.py", line 93, in _async_update_data
await self.api.async_login()
File "/usr/local/lib/python3.13/site-packages/opower/opower.py", line 205, in async_login
self.access_token = await self.utility.async_login(self.session, self.username, self.password, self.login_data)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.13/site-packages/opower/utilities/exelon.py", line 407, in async_login
state = account["PremiseInfo"][0]["mainAddress"]["townDetail"]["stateOrProvince"]
~~~~~~~^^^^^^^^^^^^^^^
KeyError: 'PremiseInfo'
2025-09-28 19:05:21.393 ERROR (MainThread) [homeassistant.components.opower.coordinator] Unexpected error fetching Opower data
Traceback (most recent call last):
File "/usr/src/homeassistant/homeassistant/helpers/update_coordinator.py", line 392, in _async_refresh
self.data = await self._async_update_data()
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/src/homeassistant/homeassistant/components/opower/coordinator.py", line 93, in _async_update_data
await self.api.async_login()
File "/usr/local/lib/python3.13/site-packages/opower/opower.py", line 205, in async_login
self.access_token = await self.utility.async_login(self.session, self.username, self.password, self.login_data)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.13/site-packages/opower/utilities/exelon.py", line 407, in async_login
state = account["PremiseInfo"][0]["mainAddress"]["townDetail"]["stateOrProvince"]
~~~~~~~^^^^^^^^^^^^^^^

@PaulSD
Copy link
Contributor

PaulSD commented Sep 29, 2025

@censay
The PremiseInfo code has been merged but not released yet, so it isn't surprising you are still running into issues.
What kind of HA install do you have? (I may be able to show you how to apply the changes before they are released...)

@censay
Copy link

censay commented Sep 29, 2025

@PaulSD fairly vanilla integration of HA on a proxmox mini PC. So theoretically can change anything. Would probably take an image before adjusting anything.

aleksrozman and others added 3 commits September 30, 2025 17:07
If token returns nothing, a reauthentication is needed because the original authentication flow broke. We have to raise an error to support this flow.
@aleksrozman
Copy link
Contributor Author

The cookies are set to expire at the end of the session (the OpenID ones expire in the past). I was hoping with this change I could keep the session alive for more than the 30 minutes, but alas it did not work a week or even a few hours later of not touching the backend. Unfortunately if the backend can choose to expire the tokens anytime (as we even see in the frontend), that means we need to support re-authentication frequently. Right now I delete/add the integration and get a backfill, so there is an annoying path forward. I need to spend more time to find a way to keep the session alive, which means some more reverse engineering. This patch at least should support the reauth process.

@censay
Copy link

censay commented Oct 2, 2025

@censay The PremiseInfo code has been merged but not released yet, so it isn't surprising you are still running into issues. What kind of HA install do you have? (I may be able to show you how to apply the changes before they are released...)

Thanks for the quick reply. Maybe I'm unclear on the workflow but it looked like the PremiseInfo fix was pulled into main meaning it should work right? I get a successful provision after MFA but then premise info starts.

@PaulSD
Copy link
Contributor

PaulSD commented Oct 8, 2025

@censay

To apply changes before they are released:

  • Settings -> Add-ons -> Add-on Store -> "Advanced SSH & Web Terminal"
  • (In Terminal Add-on) "Info" tab -> Protection mode: N -> RESTART -> OPEN WEB UI
  • Run:
docker container exec -it homeassistant bash
cd /usr/local/lib/python3.13/site-packages/opower
apk add --no-cache patch
# For the PremiseInfo fix:
wget -O patch https://github.com/tronikos/opower/commit/4f0849e6327cdc1796cc013ed3108fd6c891090b.patch
patch -p 3 < patch
rm patch
# For the current changes in this PR:
wget -O patch https://patch-diff.githubusercontent.com/raw/tronikos/opower/pull/145.patch
patch -p 3 < patch
rm patch
apk del patch
exit  # From docker container
ha core restart
exit  # From terminal
  • (In Terminal Add-on) "Info" tab -> Protection mode: Y -> RESTART

@aleksrozman
Copy link
Contributor Author

So far I haven't found a way to create a longer lived token without a forced MFA. This remains to be the case with the website as well, it will timeout and then start the authentication flow at the MFA part. This pull request adds one main feature, it will detect the lack of token and issue an invalid auth. It further adds cookie storage, so when we do uncover how to make it last longer the logic is there (current cookie logic lasts a session which is somewhere around 30 minutes). It may be worth pulling this in, and then opening an issue to find a way to create longer lived ASP cookies.

@tronikos
Copy link
Owner

Is this ready to be merged? Does the title need to be updated? Could you also update the HA docs page?

@aleksrozman
Copy link
Contributor Author

Title and docs are accurate. The docs claim periodically, in this case it happens to be a little too periodic.

I would say we can merge it (PR was for ASP cookies and that is what is here), because it will at least run the InvalidAuth part and instead of add/removing the integration you just reauth. Though I am using the ASP cookies, it still will not last beyond a session. I would propose we merge this and open (assigning me) an issue that the token does not last longer than a session. I am not sure if or when I can fix it, but I will keep an eye out and periodically try to improve it.

@tronikos tronikos merged commit 39ffd48 into tronikos:main Oct 13, 2025
3 checks passed
@PaulSD
Copy link
Contributor

PaulSD commented Oct 14, 2025

@aleksrozman

I've done some review and testing here. Unfortunately I don't see any easy solutions. However, I can at least provide some more details on the behavior and potential solutions for further exploration.

At a high level:

  • secure.pepco.com is a server-side .NET web app, developed/managed by Pepco/Exelon and hosted in Azure.
  • secure.exeloncorp.com is an instance of Microsoft Entra External ID running within the Microsoft Identity Platform (also known as Microsoft Entra ID). (This is also hosted in Azure, but is developed and managed by Microsoft, and is only configured by Pepco/Exelon.)
  • The secure.pepco.com web app uses secure.exeloncorp.com for authentication via standard Microsoft libraries that use the OAuth 2.0 Code Flow protocol for integration.
  • pep.opower.com is an instance of Opower Integration Hub hosted by Oracle, using Account-Scope Access Tokens for per-user authentication/authorization.
  • Most details about the authentication implementation for pep.opower.com are hidden from users, but we can deduce that it probably works like this:
    • At initial login time, secure.pepco.com requests both an Access Token and a Refresh Token (via the "offline_access" scope) from secure.exeloncorp.com. After login, the Access Token is used to complete the initial authentication to secure.pepco.com, and the Refresh Token is stored server-side in the secure.pepco.com app (in the user's session object).
    • Calling secure.pepco.com/api/Services/OpowerService.svc/GetOpowerToken will use the stored Refresh Token to request another Access Token from secure.exeloncorp.com for use with pep.opower.com, then return the new Access Token to the caller.
    • pep.opower.com is configured to validate the Access Token with secure.exeloncorp.com (instead of with an Oracle Identity and Access Management instance as suggested in the Opower docs).

At a lower level:

  • Be aware that the word "session" is used to refer to related but independent concepts in browsers vs server-side apps. Based on some of the comments above it sounds like you may not be aware of the relevant differences.
    • Browser cookies are defined as either "session" cookies or "persistent" cookies. "session" cookies have no configurable expiration date/time, and they are stored for the duration of the "browser session", which generally means they are stored in memory until the browser process is exited (until all windows/tabs are closed, which could potentially be a really long time). "persistent" cookies have a configurable expiration date/time, and they are stored on-disk until that date/time (regardless of whether the browser is exited or remains open).
    • Server-side apps typically have something called "session", which is typically an object that stores user-specific state between HTTP requests. For example, the first name, last name, and unique user identifier for a particular logged-in user would typically be stored in the session object so that the user only needs to be authenticated once for a series of HTTP requests (as opposed to having to re-authenticate on every request).
    • Browser cookies are generally required to make server-side sessions work. For example, the server can generate a large random number for each user, set a browser cookie containing that random number, then use the random number as an index in some lookup table or database which stores the actual user session data on the server. Or, alternatively, the server can encrypt the user session data, store the encrypted data directly in a browser cookie, then decrypt the cookie data on each subsequent HTTP request. In general, most server-side apps use browser "session" cookies to store these cookies, although browser "persistent" cookies would also work (with some security and usability trade-offs).
    • The lifespan of a server-side session object is generally not tied to the lifespan of the associated browser cookie. Server-side session objects are typically deleted or invalidated after a period of inactivity. If the server-side session is deleted/invalidated before the browser cookie is dropped then on a subsequent request the server will typically just ignore the outdated cookie and make the user start over (re-authenticate, etc) with a new server-side session. If the browser cookie is dropped before the server-side session object is deleted/invalidated then the server-side session object remains (but simply isn't used) until the inactivity time limit is reached.
    • In some cases, server-side session objects that are used continuously (to avoid hitting the inactivity time limit) may be deleted/invalidated (to force re-authentication) when their total age reaches a certain limit. It is also possible that a regularly scheduled server-side maintenance process could periodically invalidate all existing server-side sessions. I haven't tested whether either of those is implemented here.
  • The Microsoft Entra docs say that each Access Token used for pep.opower.com should remain valid for a lifespan that is randomly selected between 60 and 90 minutes, although this is configurable (by Pepco/Exelon). I haven't tested the lifespan of these, but it sounds like you determined they are valid for 30 minutes?
  • As described above, the secure.pepco.com server-side session appears to store a Refresh Token that can be used to generate additional Access Tokens. The Refresh Token likely has a long lifespan (the docs say it defaults to 90 days, although this is configurable by Pepco/Exelon). However, the Refresh Token can only be used as long as the secure.pepco.com server-side session remains valid.
  • For secure.pepco.com, it appears that the server-side session inactivity time limit is 30 minutes. However, periodically hitting secure.pepco.com with the relevant cookie(s) will keep the session alive longer than 30 minutes. I haven't tested to see if there is a limit on how long you can keep it alive with activity.
  • secure.exeloncorp.com also has a server-side session that can be used to establish a new secure.pepco.com session without re-authenticating. (Note that this is contrary to some statements in comments above.) After signing in, if you wipe all secure.pepco.com cookies, refresh the page, and click the "Sign In" button, then secure.exeloncorp.com will recognize that you have already signed in to it, and will authenticate you to secure.pepco.com again without requiring another manual sign-in or MFA.
  • For secure.exeloncorp.com, the server-side session inactivity time limit also appears to be 30 minutes, but can also be kept alive by periodically hitting it with the relevant cookie(s).
  • Clicking the "Sign Out" button invalidates the server-side sessions in both secure.pepco.com and secure.exeloncorp.com, ensuring that you must log in again after clicking "Sign Out".

Potential solutions:

  • Looking only at the functionality that Pepco/Exelon use themselves (as described above), I think the only way to avoid reauthenticating is to make periodic web calls to secure.pepco.com (and/or secure.exeloncorp.com) no less than once every 30 minutes (in order to keep the server-side session alive). We don't actually have to retrieve data this often; We simply have to hit something on the site with the necessary cookie(s) to keep the session active. Maybe we could run a background thread in the opower library to handle this independent of the normal data retrieval code paths? It isn't clear exactly how long this will avoid reauthentication; It could potentially last up to 90 days when the Refresh Token expires, or it could potentially last only something like 24 hours if other time limits or session cleanup processes are implemented.
  • To make this more robust, we may need to think outside of Pepco/Exelon's box. A potential solution is to find a way to get direct access to a Refresh Token (that we can use to generate Access Tokens independent of the secure.pepco.com server-side session). As an added bonus, having a Refresh Token may allow us to generate a new Refresh Token (using the OAuth 2.0 Token Exchange Flow) periodically to work around the Refresh Token lifespan as well. To get a Refresh Token, we would probably need to implement a separate custom web app which also uses secure.exeloncorp.com for authentication. While Pepco/Exelon can block third-party apps from using secure.exeloncorp.com, by default Microsoft allows it. If someone wants to try this, docs are here.
  • If that doesn't work, we may need to think outside of Microsoft's box as well. A potential solution is to set up automated email processing to handle email-based MFA without user interaction. For example, have the opower library run a simple mail server that can accept MFA emails, automatically parse out the validation code, and submit the validation code via the website to complete MFA login. Users could then be instructed to add an email rule to their mailbox to forward the MFA emails to the opower library's mail server.

@aleksrozman aleksrozman deleted the aleksrozman-patch-1 branch October 18, 2025 06:17
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 this pull request may close these issues.

4 participants