diff --git a/examples/kotti_velruse/CHANGES.txt b/examples/kotti_velruse/CHANGES.txt new file mode 100644 index 0000000..fa53044 --- /dev/null +++ b/examples/kotti_velruse/CHANGES.txt @@ -0,0 +1,4 @@ +0.1 +--- + +- Initial version diff --git a/examples/kotti_velruse/MANIFEST.in b/examples/kotti_velruse/MANIFEST.in new file mode 100644 index 0000000..7d7e671 --- /dev/null +++ b/examples/kotti_velruse/MANIFEST.in @@ -0,0 +1,2 @@ +include *.txt *.ini *.cfg *.rst +recursive-include kotti_velruse *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml diff --git a/examples/kotti_velruse/README.rst b/examples/kotti_velruse/README.rst new file mode 100644 index 0000000..67f843a --- /dev/null +++ b/examples/kotti_velruse/README.rst @@ -0,0 +1,43 @@ +For the impatient +----------------- + +1. Simply run script run-server.sh + +2. Navigate to page /login like the example below: + + $ firefox http://localhost:6543/login + + +Configuration +------------- + +1. Please adjust variable *realm* in development.ini. + +2. Several providers need to be configured according to your affiliation + keys with providers like Google OAuth2, Twitter, Facebook, etc. + +Several providers work out of the box, like Google Hybrid, Yahoo and most +of OpenID providers. + + +About this example +------------------ + +This example evolved to a proper plugin, which is available from PyPI at +https://pypi.python.org/pypi/kotti_velruse + + +Dependencies +------------ + +This example depends on a modified versions of velruse and openid-selector: + +* velruse: https://pypi.python.org/pypi/rgomes_velruse + +* openid-selector: https://pypi.python.org/pypi/openid-selector + +Sources for these changed sources are available at: + +* velruse: https://github.com/frgomes/velruse/tree/feature.kotti_auth + +* openid-selector: https://github.com/frgomes/velruse diff --git a/examples/kotti_velruse/development.ini b/examples/kotti_velruse/development.ini new file mode 100644 index 0000000..f407716 --- /dev/null +++ b/examples/kotti_velruse/development.ini @@ -0,0 +1,217 @@ +### +# wsgi server configuration +### + +[server:main] +use = egg:waitress#main +host = 0.0.0.0 +port = 6543 + + + +[app:kotti_velruse] +use = egg:kotti_velruse + +pyramid.reload_templates = true +pyramid.debug_authorization = false +pyramid.debug_notfound = false +pyramid.debug_routematch = false +pyramid.default_locale_name = en + +#pyramid.includes = +# pyramid_debugtoolbar + +# By default, the toolbar only appears for clients from IP addresses +# '127.0.0.1' and '::1'. + +# you should change this +session.secret=CHANGE-ME + + + + + +### -------------------------------------------------------------------------- +# [dummy] Kotti configuration +# These settings are necessary when testing plugins in isolation. +### + +[filter:fanstatic] +use = egg:fanstatic#fanstatic + +[pipeline:main] +pipeline = fanstatic + kotti + +[app:kotti] +use = egg:kotti + +pyramid.reload_templates = true +pyramid.debug_templates = true +pyramid.debug_authorization = false +pyramid.debug_notfound = false +pyramid.debug_routematch = false +pyramid.default_locale_name = en + +pyramid.includes = pyramid_debugtoolbar + pyramid_tm + +sqlalchemy.url = sqlite:///%(here)s/Kotti.db +#mail.default_sender = yourname@yourhost + +kotti.site_title = My Kotti site +kotti.secret = qwerty + +kotti.configurators = kotti_tinymce.kotti_configure + kotti_velruse.kotti_configure + +### -------------------------------------------------------------------------- + + + + +### -------------------------------------------------------------------------- +# velruse configuration +# +# Module velruse.app.includeme looks for entries named "provider." in order +# to discover which providers are configured. +# +# NOTE: these configurations must be inside [app:kotti] +# +### + +#--- +# Please adjust variable REALM +# +# Make sure that: +# +# 1. your browser is able to resolve the FQDN +# 2. your Kotti server is able to resolve the FQDN +# +#--- +realm=http://www.example.com + +endpoint = %(realm)s:6543/logged_in +store = memory +# store = redis +# store.host = localhost +# store.port = 6379 +# store.db = 0 +# store.key_prefix = velruse_ustore + +# OpenID +# Despite a single provide.openid is declared, you can specify multiple +# URLs that should be used for connecting to multiple OpenID endpoints. +# See: login.mako for an example of how this can be done +provider.openid.realm=%(realm)s +provider.openid.store=openid.store.memstore:MemoryStore + +# Google (this an alias to Google Hybrid, for backward compatibility) +provider.google.realm=%(realm)s +provider.google.consumer_key=CHANGE-ME +provider.google.consumer_secret=CHANGE-ME +provider.google.scope=CHANGE-ME + +# Google Hybrid +#provider.google_hybrid.realm=%(realm)s +#provider.google_hybrid.consumer_key=CHANGE-ME +#provider.google_hybrid.consumer_secret=CHANGE-ME +#provider.google_hybrid.scope=CHANGE-ME + +# Google OAuth2 +provider.google_oauth2.consumer_key=CHANGE-ME +provider.google_oauth2.consumer_secret=CHANGE-ME +provider.google_oauth2.scope=CHANGE-ME + +# Yahoo +provider.yahoo.realm=%(realm)s +provider.yahoo.consumer_key=CHANGE-ME +provider.yahoo.consumer_secret=CHANGE-ME + +# Live +provider.live.client_id=CHANGE-ME +provider.live.client_secret=CHANGE-ME +provider.live.consumer_key=CHANGE-ME +provider.live.consumer_secret=CHANGE-ME + +# Twitter +provider.twitter.consumer_key=CHANGE-ME +provider.twitter.consumer_secret=CHANGE-ME + +# Facebook +provider.facebook.app_id=CHANGE-ME +provider.facebook.app_secret=CHANGE-ME +provider.facebook.consumer_key=CHANGE-ME +provider.facebook.consumer_secret=CHANGE-ME +provider.facebook.scope=email,publish_stream,read_stream,create_event,offline_access + +# LinkedIn +provider.linkedin.consumer_key=CHANGE-ME +provider.linkedin.consumer_secret=CHANGE-ME + +# Github +provider.github.consumer_key=CHANGE-ME +provider.github.consumer_secret=CHANGE-ME +provider.github.scope=CHANGE-ME + +# BitBucket +provider.bitbucket.consumer_key=CHANGE-ME +provider.bitbucket.consumer_secret=CHANGE-ME + +# MailRU +provider.mailru.app_id=CHANGE-ME +provider.mailru.app_secret=CHANGE-ME +provider.mailru.consumer_key=CHANGE-ME +provider.mailru.consumer_secret=CHANGE-ME + +### -------------------------------------------------------------------------- + + + + +### -------------------------------------------------------------------------- +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html +### + +[loggers] +keys = root, kotti, velruse, sqlalchemy + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console + +[logger_kotti] +level = DEBUG +handlers = +qualname = kotti + +[logger_velruse] +level = DEBUG +handlers = +qualname = kotti_velruse + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine +# "level = INFO" logs SQL queries. +# "level = DEBUG" logs SQL queries and results. +# "level = WARN" logs neither. (Recommended for production systems.) + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s + +### -------------------------------------------------------------------------- diff --git a/examples/kotti_velruse/kotti_velruse/__init__.py b/examples/kotti_velruse/kotti_velruse/__init__.py new file mode 100644 index 0000000..f3286c5 --- /dev/null +++ b/examples/kotti_velruse/kotti_velruse/__init__.py @@ -0,0 +1,12 @@ +from pyramid.i18n import TranslationStringFactory + + +log = __import__('logging').getLogger(__name__) + + +_ = TranslationStringFactory('kotti_velruse') + + +def kotti_configure(settings): + settings['pyramid.includes'] += ' velruse.app' + settings['pyramid.includes'] += ' kotti_velruse.views' diff --git a/examples/kotti_velruse/kotti_velruse/templates/login.mako b/examples/kotti_velruse/kotti_velruse/templates/login.mako new file mode 100644 index 0000000..bf6eba8 --- /dev/null +++ b/examples/kotti_velruse/kotti_velruse/templates/login.mako @@ -0,0 +1,50 @@ + + + + + + ${project} :: Login + + + + + + + + + + + +

Login to ${project}

+ +
+ + +
+ Sign-in +
+

Please click your account provider:

+
+
+
+ + +
+
+
+ +

OpenID allows you to log-on to many different websites using a single identity.
+ Find out more about OpenID and + how to get an OpenID enabled account.

+ + diff --git a/examples/kotti_velruse/kotti_velruse/templates/result.mako b/examples/kotti_velruse/kotti_velruse/templates/result.mako new file mode 100644 index 0000000..088a690 --- /dev/null +++ b/examples/kotti_velruse/kotti_velruse/templates/result.mako @@ -0,0 +1,14 @@ + + + + + Result Page + + + +
+${ result|n }
+
+ + + diff --git a/examples/kotti_velruse/kotti_velruse/tests.py b/examples/kotti_velruse/kotti_velruse/tests.py new file mode 100644 index 0000000..785bd45 --- /dev/null +++ b/examples/kotti_velruse/kotti_velruse/tests.py @@ -0,0 +1,20 @@ +import unittest + +from pyramid import testing + +import velruse.app + + +class ViewTests(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + testing.tearDown() + + def test_login_view(self): + from .views import login_view + request = testing.DummyRequest() + providers = velruse.app.find_providers(request.registry.settings) + info = login_view(request) + self.assertEqual(info['providers'], providers) diff --git a/examples/kotti_velruse/kotti_velruse/views.py b/examples/kotti_velruse/kotti_velruse/views.py new file mode 100644 index 0000000..fcba91d --- /dev/null +++ b/examples/kotti_velruse/kotti_velruse/views.py @@ -0,0 +1,121 @@ +from pyramid.httpexceptions import HTTPFound, HTTPNotFound +from pyramid.request import Request + +from velruse.api import login_url +from velruse.app import find_providers + +from kotti_velruse import log, _ + + +def includeme(config): + config.add_view(login, + route_name='login', + request_method='GET', + renderer='kotti_velruse:templates/login.mako') + config.add_view(login_, + route_name='login_', + renderer='json') + config.add_view(logged_in, + route_name='logged_in', + renderer='json') + config.add_view(logout, + route_name='logout', + permission='view') + + config.add_route('login', '/login') + config.add_route('login_', '/login_') + config.add_route('logged_in', '/logged_in') + config.add_route('logout', '/logout') + + try: + import openid_selector + log.info('openid_selector loaded successfully') + config.add_static_view(name='js', path='openid_selector:/js') + config.add_static_view(name='css', path='openid_selector:/css') + config.add_static_view(name='images', path='openid_selector:/images') + except Exception as e: + log.error(e) + raise e + log.info('kotti_velruse views are configured.') + + +def login(request): + settings = request.registry.settings + try: + #TODO:: before_kotti_velruse_loggedin(request) + return { + 'project' : settings['kotti.site_title'], + 'login_url': request.route_url('login_'), + } + except Exception as e: + log.error(e.message) + raise HTTPNotFound(e.message).exception + + +def login_(request): + ###################################################################################### + # # + # Let's clarify the difference between "provider" and "method" in this function: # + # # + # * Conceptually, [authentication] methods can be understood pretty much like # + # protocols or transports. So, methods would be for example: OpenID, OAuth2 and # + # other authentication protocols supported by Velruse. # + # # + # * A provider is simply an entity, like Google, Yahoo, Twitter, Facebook, Verisign, # + # Github, Launchpad and hundreds of other entities which employ authentication # + # methods like OpenID, OAuth2 and others supported by Velruse. # + # # + # * In particular, certain entities implement their own authentication methods or # + # they eventually offer several authentication methods. For this reason, there are # + # specific methods for "yahoo", "tweeter", "google_hybrid", "google_oauth2", etc. # + # # + ###################################################################################### + + provider = request.params['provider'] + method = request.params['method'] + + settings = request.registry.settings + if not method in find_providers(settings): + raise HTTPNotFound('Provider/method {}/{} is not configured'.format(provider, method)).exception + + velruse_url = login_url(request, method) + + payload = dict(request.params) + if 'yahoo' == method: payload['oauth'] = 'true' + if 'openid' == method: payload['use_popup'] = 'false' + payload['format'] = 'json' + del payload['provider'] + del payload['method'] + + redirect = Request.blank(velruse_url, POST=payload) + try: + response = request.invoke_subrequest( redirect ) + return response + except Exception as e: + log.error(e.message) + message = _(u'Provider/method: {}/{} :: {}').format(provider, method, e.message) + raise HTTPNotFound(message).exception + + + +def logged_in(request): + token = request.params['token'] + storage = request.registry.velruse_store + try: + json = storage.retrieve(token) + return json + except Exception as e: + log.error(e.message) + raise HTTPNotFound(e.message).exception + + +def logout(request): + from pyramid.security import forget + try: + request.session.invalidate() + request.session.flash( _(u'Session logged out.') ) + headers = forget(request) + return HTTPFound(location=request.application_url, headers=headers) + except Exception as e: + log.error(e.message) + raise HTTPNotFound(e.message).exception diff --git a/examples/kotti_velruse/run-server.sh b/examples/kotti_velruse/run-server.sh new file mode 100755 index 0000000..f6bb8b6 --- /dev/null +++ b/examples/kotti_velruse/run-server.sh @@ -0,0 +1,28 @@ +#/bin/bash + + +# give a little push to Kotti installation +pip install -r https://raw.github.com/Kotti/Kotti/0.9.2/requirements.txt $PIP_OPTIONS + +# installs Kotti and kotti_velruse +python setup.py develop + +# uninstall velruse cos rgomes-velruse replaces it for the time being +pip uninstall velruse << EOF +y +EOF + +# make sure proxy settings are ignored +`env | fgrep -i _proxy | cut -d= -f1 | xargs echo unset` + +# start server +echo . +echo . +echo '*************************************************' +echo '* *' +echo '* Starting the server... *' +echo '* *' +echo '* Please visit context /login when it is ready. *' +echo '* *' +echo '*************************************************' +pserve development.ini --reload diff --git a/examples/kotti_velruse/setup.cfg b/examples/kotti_velruse/setup.cfg new file mode 100644 index 0000000..0b9799b --- /dev/null +++ b/examples/kotti_velruse/setup.cfg @@ -0,0 +1,27 @@ +[nosetests] +match = ^test +nocapture = 1 +cover-package = kotti_velruse +with-coverage = 1 +cover-erase = 1 + +[compile_catalog] +directory = kotti_velruse/locale +domain = kotti_velruse +statistics = true + +[extract_messages] +add_comments = TRANSLATORS: +output_file = kotti_velruse/locale/kotti_velruse.pot +width = 80 + +[init_catalog] +domain = kotti_velruse +input_file = kotti_velruse/locale/kotti_velruse.pot +output_dir = kotti_velruse/locale + +[update_catalog] +domain = kotti_velruse +input_file = kotti_velruse/locale/kotti_velruse.pot +output_dir = kotti_velruse/locale +previous = true diff --git a/examples/kotti_velruse/setup.py b/examples/kotti_velruse/setup.py new file mode 100644 index 0000000..1e07e96 --- /dev/null +++ b/examples/kotti_velruse/setup.py @@ -0,0 +1,33 @@ +from setuptools import setup, find_packages +import os + +here = os.path.abspath(os.path.dirname(__file__)) +README = open(os.path.join(here, 'README.rst')).read() + +version = '0.1' + +requires = [ + 'Kotti', + 'kotti_velruse', + ] + +setup(name='kotti_velruse', + version=version, + description="Kotti authentication with Velruse: OpenID, OAuth2, Google, Yahoo, Live, Facebook, Twitter and others", + long_description=README, + classifiers=[ + "Programming Language :: Python", + "Framework :: Pyramid", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", + ], + keywords='kotti authentication velruse openid oauth2 google yahoo live facebook twitter', + author='Richard Gomes', + author_email='rgomes.info@gmail.com', + url='http://kotti_velruse.readthedocs.org', + license='BSD', + packages=find_packages(exclude=['ez_setup', 'examples', 'tests']), + include_package_data=True, + zip_safe=False, + install_requires=requires, + ) diff --git a/setup.py b/setup.py index 2bf6aec..4e14a99 100644 --- a/setup.py +++ b/setup.py @@ -38,7 +38,7 @@ README = CHANGES = '' setup(name='velruse', - version='1.1.1', + version='1.1.2', description=( 'Simplifying third-party authentication for web applications.'), long_description=README + '\n\n' + CHANGES, diff --git a/velruse/app/__init__.py b/velruse/app/__init__.py index 51ec761..0b02deb 100644 --- a/velruse/app/__init__.py +++ b/velruse/app/__init__.py @@ -118,6 +118,7 @@ def register_velruse_store(config, storage): 'github': 'add_github_login_from_settings', 'google': 'add_google_login_from_settings', 'google_oauth2': 'add_google_oauth2_login_from_settings', + 'google_hybrid': 'add_google_hybrid_login_from_settings', 'lastfm': 'add_lastfm_login_from_settings', 'linkedin': 'add_linkedin_login_from_settings', 'live': 'add_live_login_from_settings', @@ -126,6 +127,11 @@ def register_velruse_store(config, storage): 'taobao': 'add_taobao_login_from_settings', 'twitter': 'add_twitter_login_from_settings', 'weibo': 'add_weibo_login_from_settings', + 'openid': 'add_openid_login_from_settings', + 'yahoo': 'add_yahoo_login_from_settings', + 'mailru': 'add_mailru_login_from_settings', + 'yandex': 'add_yandex_login_from_settings', + 'vk': 'add_vk_login_from_settings', } diff --git a/velruse/providers/google.py b/velruse/providers/google.py index 3ec003f..cd14c8e 100644 --- a/velruse/providers/google.py +++ b/velruse/providers/google.py @@ -1,8 +1,44 @@ """This module exists as a bw-compat shim for google_hybrid.""" from .google_hybrid import ( - add_google_login, - GoogleAuthenticationComplete, + add_google_hybrid_login, ) +from ..settings import ProviderSettings + + def includeme(config): config.add_directive('add_google_login', add_google_login) + config.add_directive('add_google_login_from_settings', + add_google_login_from_settings) + + +def add_google_login_from_settings(config, prefix='velruse.google.'): + settings = config.registry.settings + p = ProviderSettings(settings, prefix) + p.update('consumer_key', required=True) + p.update('consumer_secret', required=True) + p.update('login_path') + p.update('callback_path') + config.add_google_login(**p.kwargs) + + +def add_google_login(config, + attrs=None, + realm=None, + storage=None, + consumer_key=None, + consumer_secret=None, + scope=None, + login_path='/login/google', + callback_path='/login/google/callback', + name='google'): + add_google_hybrid_login(config, + attrs, + realm, + storage, + consumer_key, + consumer_secret, + scope, + login_path, + callback_path, + name) diff --git a/velruse/providers/google_hybrid.py b/velruse/providers/google_hybrid.py index 70b43ad..2146754 100644 --- a/velruse/providers/google_hybrid.py +++ b/velruse/providers/google_hybrid.py @@ -18,6 +18,7 @@ OpenIDConsumer, ) +from ..settings import ProviderSettings log = __import__('logging').getLogger(__name__) @@ -38,18 +39,31 @@ def includeme(config): for the supported options. """ - config.add_directive('add_google_hybrid_login', add_google_login) - -def add_google_login(config, - attrs=None, - realm=None, - storage=None, - consumer_key=None, - consumer_secret=None, - scope=None, - login_path='/login/google', - callback_path='/login/google/callback', - name='google'): + config.add_directive('add_google_hybrid_login', add_google_hybrid_login) + config.add_directive('add_google_hybrid_login_from_settings', + add_google_hybrid_login_from_settings) + + +def add_google_hybrid_login_from_settings(config, prefix='velruse.google_hybrid.'): + settings = config.registry.settings + p = ProviderSettings(settings, prefix) + p.update('consumer_key', required=True) + p.update('consumer_secret', required=True) + p.update('login_path') + p.update('callback_path') + config.add_google_hybrid_login(**p.kwargs) + + +def add_google_hybrid_login(config, + attrs=None, + realm=None, + storage=None, + consumer_key=None, + consumer_secret=None, + scope=None, + login_path='/login/google_hybrid', + callback_path='/login/google_hybrid/callback', + name='google_hybrid'): """ Add a Google login provider to the application using the OpenID+OAuth hybrid protocol. This protocol can be configured for purely diff --git a/velruse/providers/google_oauth2.py b/velruse/providers/google_oauth2.py index 06f59e1..f1c7ce4 100644 --- a/velruse/providers/google_oauth2.py +++ b/velruse/providers/google_oauth2.py @@ -22,6 +22,7 @@ class GoogleAuthenticationComplete(AuthenticationComplete): """Google OAuth 2.0 auth complete""" + def includeme(config): """Activate the ``google_oauth2`` Pyramid plugin via ``config.include('velruse.providers.google_oauth2')``. After included, @@ -34,11 +35,12 @@ def includeme(config): ``config.add_google_oauth2_login_from_settings()`` """ - config.add_directive('add_google_oauth2_login', add_google_login) + config.add_directive('add_google_oauth2_login', add_google_oauth2_login) config.add_directive('add_google_oauth2_login_from_settings', - add_google_login_from_settings) + add_google_oauth2_login_from_settings) + -def add_google_login_from_settings(config, prefix='velruse.google.'): +def add_google_oauth2_login_from_settings(config, prefix='velruse.google_oauth2.'): settings = config.registry.settings p = ProviderSettings(settings, prefix) p.update('consumer_key', required=True) @@ -48,13 +50,14 @@ def add_google_login_from_settings(config, prefix='velruse.google.'): p.update('callback_path') config.add_google_oauth2_login(**p.kwargs) -def add_google_login(config, - consumer_key=None, - consumer_secret=None, - scope=None, - login_path='/login/google', - callback_path='/login/google/callback', - name='google'): + +def add_google_oauth2_login(config, + consumer_key=None, + consumer_secret=None, + scope=None, + login_path='/login/google_oauth2', + callback_path='/login/google_oauth2/callback', + name='google_oauth2'): """ Add a Google login provider to the application supporting the new OAuth2 protocol. diff --git a/velruse/providers/openid.py b/velruse/providers/openid.py index ae35bfa..fffe8d4 100644 --- a/velruse/providers/openid.py +++ b/velruse/providers/openid.py @@ -21,6 +21,8 @@ ThirdPartyFailure, ) +from ..settings import ProviderSettings + log = __import__('logging').getLogger(__name__) # Setup our attribute objects that we'll be requesting @@ -78,6 +80,16 @@ class OpenIDAuthenticationComplete(AuthenticationComplete): def includeme(config): config.add_directive('add_openid_login', add_openid_login) + config.add_directive('add_openid_login_from_settings', + add_openid_login_from_settings) + + +def add_openid_login_from_settings(config, prefix='velruse.openid.'): + settings = config.registry.settings + p = ProviderSettings(settings, prefix) + p.update('login_path') + p.update('callback_path') + config.add_openid_login(**p.kwargs) def add_openid_login(config, diff --git a/velruse/providers/yahoo.py b/velruse/providers/yahoo.py index a260953..8747765 100644 --- a/velruse/providers/yahoo.py +++ b/velruse/providers/yahoo.py @@ -16,6 +16,7 @@ OpenIDConsumer, ) +from ..settings import ProviderSettings log = __import__('logging').getLogger(__name__) @@ -29,6 +30,18 @@ class YahooAuthenticationComplete(OpenIDAuthenticationComplete): def includeme(config): config.add_directive('add_yahoo_login', add_yahoo_login) + config.add_directive('add_yahoo_login_from_settings', + add_yahoo_login_from_settings) + + +def add_yahoo_login_from_settings(config, prefix='velruse.yahoo.'): + settings = config.registry.settings + p = ProviderSettings(settings, prefix) + p.update('consumer_key', required=True) + p.update('consumer_secret', required=True) + p.update('login_path') + p.update('callback_path') + config.add_yahoo_login(**p.kwargs) def add_yahoo_login(config,