Skip to content

WIP: Add multi-factor authentication plug-able modules#15335

Closed
awarecan wants to merge 101 commits intohome-assistant:devfrom
awarecan:auth-2fa-v2
Closed

WIP: Add multi-factor authentication plug-able modules#15335
awarecan wants to merge 101 commits intohome-assistant:devfrom
awarecan:auth-2fa-v2

Conversation

@awarecan
Copy link
Copy Markdown
Contributor

@awarecan awarecan commented Jul 6, 2018

Description:

Add multi-factor authentication support for new auth system.

Auth module

Multi-factor auth modules has been added to auth/modules folder.

totp is Time-based One Time Password module which compatible with Google Authenticator and Authy (not test yet).

insecure_example is a Example designed for demo and unit testing purpose, should not be used in any production system.

Multi-facot auth module can be used mixed-match with auth providers. After normal auth provider validate, if there are auth modules enabled for the specific user, the login flow will direct to auth module input form. Auth module use separated storage, can be hardening it in future.

Related issue (if applicable): fixes #

Pull request in developers.home-assistant.io with documentation (if applicable): home-assistant/developers.home-assistant#52

Example entry for configuration.yaml (if applicable):

# configuration.yaml
homeassistant:
  # Auth Provider
  auth_providers:
    - type: homeassistant
  auth_mfa_modules:
    - type: totp

auth:

Checklist:

  • The code change is tested and works locally.
  • Local tests pass with tox. Your PR cannot be merged unless tests pass

If user exposed functionality or configuration variables are added/changed:

If the code communicates with devices, web services, or third-party tools:

  • New dependencies have been added to the REQUIREMENTS variable (example).
  • New dependencies are only imported inside functions that use them (example).
  • New or updated dependencies have been added to requirements_all.txt by running script/gen_requirements_all.py.

If the code does not interact with devices:

  • Tests have been added to verify that the new code works.

@awarecan awarecan requested a review from a team as a code owner July 6, 2018 23:57
@ghost ghost added the in progress label Jul 6, 2018
@awarecan
Copy link
Copy Markdown
Contributor Author

awarecan commented Jul 7, 2018

For other auth provider, such as legacy_api_password, you need to manually edit ~/.homeassistant/.storage/auth_module.totp file (the username for legacy_api_password is homeassistant)

To manually generate ota_secret, you can execute following python script

import pyotp
print(pyotp.random_base32())

@awarecan awarecan mentioned this pull request Jul 7, 2018
7 tasks
"""Return the name of the auth provider."""
return self.config.get(CONF_NAME, self.DEFAULT_TITLE)

async def load_modules(self, moudle_configs):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo

'users': [],
}

if 'users' not in data:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When can this happen?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Data file was corrupted? Cannot recall why I added this statement, can be removed if you feel redundant.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's redundant. If storage doesn't exist, we get None back and set the default above. If we save, the default was the foundation.

@balloob
Copy link
Copy Markdown
Member

balloob commented Jul 7, 2018

It's usually wise to discuss these things before creating big PRs like this. That way there is no work wasted if we have different views on the architecture.

await self.hass.async_add_executor_job(
data.validate_login, username, password)
finally:
await data.async_save()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to always save?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

legacy code, should be removed. It was used in v1 implementation when auth module save data in auth provider storage.

@awarecan
Copy link
Copy Markdown
Contributor Author

awarecan commented Jul 7, 2018

I thought we had the discussion around plugable auth module in #15225

@balloob
Copy link
Copy Markdown
Member

balloob commented Jul 9, 2018

That's true, but that's only 1 part of this. Starting code review now

"""Initialize the login flow."""
self._auth_provider = auth_provider
# self._auth_modules is mutable, we need a copy
self._auth_modules = auth_provider.modules.copy()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

auth modules should not change anymore after startup ?

"""Raised when we encounter invalid authentication."""


class SessionStore:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see any need for the session store. This should just be stored on the instance of the login flow handler. It will then be removed with that.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it was inherited from previous version of implement. I agreed that is over-design, we should not have session at all, flow_id can be used as a session.

@@ -0,0 +1,66 @@
"""Example auth module."""
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should not live under auth provider as it has nothing to do with it?

I suggest we restructure our auth into

  • homeassistant/auth/__init__.py
  • homeassistant/auth/providers/homeassistant.py
  • homeassistant/auth/modules/totp.py

I also not sure if I like modules, it's too generic. What about two_factor ?

}, extra=vol.PREVENT_EXTRA)

STORAGE_VERSION = 1
STORAGE_KEY = 'auth_module.insecure_example'
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that we should store these on the user objects in a data object that is stored/restored when user is saved/restored.

return {}


class LoginFlow(data_entry_flow.FlowHandler):
Copy link
Copy Markdown
Member

@balloob balloob Jul 9, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like this architecture.

I think that we should implement it as a FlowHandler that is not extended by the login flows, but instead wraps one.

It should take the login flow it needs to wrap. Once that flow returns a create entry, it should hold that value, look up the user, see if any two factor has been registered, instantiate those and let them handle the next step.

You can use __getattr__ to intercept the methods on the LoginFlow and decide where to redirect the call.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm , maybe I do like this architecture. I just don't like how it iterates through the modules and mutates them.

if not errors and result:
return await self.async_finish(result)
else:
session_id = await auth_module.\
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't create sessions, just instantiate and store on this instance.


async def async_finish(self, username):
"""Handle the pass of login flow."""
if self._auth_modules:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use a guard clause

if not X:
    return Y

async def async_finish(self, username):
"""Handle the pass of login flow."""
if self._auth_modules:
_, auth_module = self._auth_modules.popitem(False)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is weird, instead, once finish called, we should check if user has any 2fa configured (stored on user object?) and then create a local list of 2fa to ask which we can mutate.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A user can have multiple 2FA active, it only needs to pass 1 to log in.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see your point, so the flow should like

admin create user -> user login -> user enable/setup one of many 2fa from a list
user select auth_provider -> auth_provider validate -> user select which 2fa he/she want to use -> ask 2fa challenge -> 2fa module validate -> user logged in

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, and we can do it all within a login flow without needed a frontend change

if self._auth_modules:
_, auth_module = self._auth_modules.popitem(False)

step_id = 'auth_module_' + auth_module.type
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

string concatenation should be done using .format

await self.async_save()
return data

async def async_cleanup_session(self):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Session expiration is up to the LoginFlow to enforce.

vol.Optional(CONF_NAME): str,
# Specify ID if you have two auth providers for same type.
vol.Optional(CONF_ID): str,
vol.Optional(CONF_MODULES):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Modules should not be nested under an auth provider. Instead, users should be able to enable 2fa for their account.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the configuration should like

# configuration.yml
homeassistant:
  auth_providers:
    - type: a_provder
    - type: b_provider
  auth_2fa_modules:
    - type: totp
    - type: hotp_sms

user should be able to enable one of many module in his setting page
user should be able to select use which module after auth_provider validate

That is the sequence used in my Cisco VPN client configured with okta integration.

@homeassistant
Copy link
Copy Markdown
Contributor

Hello @awarecan,

When attempting to inspect the commits of your pull request for CLA signature status among all authors we encountered commit(s) which were not linked to a GitHub account, thus not allowing us to determine their status(es).

The commits that are missing a linked GitHub account are the following:

Unfortunately, we are unable to accept this pull request until this situation is corrected.

Here are your options:

  1. If you had an email address set for the commit that simply wasn't linked to your GitHub account you can link that email now and it will retroactively apply to your commits. The simplest way to do this is to click the link to one of the above commits and look for a blue question mark in a blue circle in the top left. Hovering over that bubble will show you what email address you used. Clicking on that button will take you to your email address settings on GitHub. Just add the email address on that page and you're all set. GitHub has more information about this option in their help center.

  2. If you didn't use an email address at all, it was an invalid email, or it's one you can't link to your GitHub, you will need to change the authorship information of the commit and your global Git settings so this doesn't happen again going forward. GitHub provides some great instructions on how to change your authorship information in their help center.

    • If you only made a single commit you should be able to run
      git commit --amend --author="Author Name <email@address.com>"
      
      (substituting Author Name and email@address.com for your actual information) to set the authorship information.
    • If you made more than one commit and the commit with the missing authorship information is not the most recent one you have two options:
      1. You can re-create all commits missing authorship information. This is going to be the easiest solution for developers that aren't extremely confident in their Git and command line skills.
      2. You can use this script that GitHub provides to rewrite history. Please note: this should be used only if you are very confident in your abilities and understand its impacts.
    • Whichever method you choose, I will come by to re-check the pull request once you push the fixes to this branch.

We apologize for this inconvenience, especially since it usually bites new contributors to Home Assistant. We hope you understand the need for us to protect ourselves and the great community we all have built legally. The best thing to come out of this is that you only need to fix this once and it benefits the entire Home Assistant and GitHub community.

Thanks, I look forward to checking this PR again soon! ❤️

tchellomello and others added 16 commits July 16, 2018 05:28
* support for tuya platform

* support tuya platform

* lint fix

* change dependency

* add tuya platform support

* remove tuya platform except switch. fix code as required

* fix the code as review required

* fix as required

* fix a mistake
* Update __init__.py

* Update requirements_all.txt
There are some devices that speak HomeKit that we shouldn't expose. Some
bridges (such as the Hue) provide reduced functionality over HomeKit and
have a functional native API, so should be ignored. We also shouldn't
offer to configure the built-in Home Assistant HomeKit bridge.
* Add HomematicIP security zone

* Update access point tests

* Fix state if not armed and coments

* Add comment for the empty state_attributes

* Fix comment

* Fix spelling
* Add Python 3.8-dev tox tests.

* Allow failures on 3.8-dev

* Allow failures on 3.8-dev take2

* Only run on pushes to dev
## Description:

Make typing checks more strict: add `--strict-optional` flag that forbids implicit None return type. This flag will become default in the next version of mypy (0.600)

Add `homeassistant/util/` to checked dirs.

## Checklist:
  - [x] The code change is tested and works locally.
  - [x] Local tests pass with `tox`. **Your PR cannot be merged unless tests pass**
* Upgrade mypy to 0.600

* Upgrade mypy to 0.610

* Typing improvements

* remove unneeded or

* remove merge artifact

* Update loader.py
* User management

* Lint

* Fix dict

* Reuse data instance

* OrderedDict all the way
Only include base class and insecure_example
@awarecan awarecan requested a review from a team July 16, 2018 13:23
@awarecan awarecan changed the title Add multi-factor authentication plug-able modules WIP: Add multi-factor authentication plug-able modules Jul 16, 2018
@awarecan
Copy link
Copy Markdown
Contributor Author

Rebase seems mess up, close this PR for now

@awarecan awarecan closed this Jul 16, 2018
@ghost ghost removed the in progress label Jul 16, 2018
@home-assistant home-assistant locked and limited conversation to collaborators Dec 10, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.