diff --git a/auth_admin/__init__.py b/auth_admin/__init__.py
new file mode 100755
index 00000000..e4f4917a
--- /dev/null
+++ b/auth_admin/__init__.py
@@ -0,0 +1,3 @@
+from . import controllers
+from . import models
+from . import wizard
diff --git a/auth_admin/__manifest__.py b/auth_admin/__manifest__.py
new file mode 100755
index 00000000..25cc444b
--- /dev/null
+++ b/auth_admin/__manifest__.py
@@ -0,0 +1,31 @@
+{
+ 'name': 'Auth Admin',
+ 'author': 'Hibou Corp.',
+ 'category': 'Hidden',
+ 'version': '17.0.1.0.0',
+ 'description':
+ """
+Login as other user
+===================
+
+Provides a way for an authenticated user, with certain permissions, to login as a different user.
+Can also create a URL that logs in as that user.
+
+Out of the box, only allows you to generate a login for an 'External User', e.g. portal users.
+
+*2017-11-15* New button to generate the login on the Portal User Wizard (Action on Contact)
+
+Added the option to copy the Force Login URL by simply clicking the Copy widget in the Portal User Wizard.
+
+ """,
+ 'depends': [
+ 'base',
+ 'website',
+ 'portal',
+ ],
+ 'auto_install': False,
+ 'data': [
+ 'views/res_users.xml',
+ 'wizard/portal_wizard_views.xml',
+ ],
+}
diff --git a/auth_admin/controllers/__init__.py b/auth_admin/controllers/__init__.py
new file mode 100755
index 00000000..757b12a1
--- /dev/null
+++ b/auth_admin/controllers/__init__.py
@@ -0,0 +1,2 @@
+# -*- coding: utf-8 -*-
+from . import main
diff --git a/auth_admin/controllers/main.py b/auth_admin/controllers/main.py
new file mode 100755
index 00000000..2453387c
--- /dev/null
+++ b/auth_admin/controllers/main.py
@@ -0,0 +1,42 @@
+from odoo import http, exceptions
+from ..models.res_users import check_admin_auth_login
+
+from logging import getLogger
+_logger = getLogger(__name__)
+
+
+class AuthAdmin(http.Controller):
+
+ @http.route(['/auth_admin'], type='http', auth='public', website=True)
+ def index(self, *args, **post):
+ u = post.get('u')
+ e = post.get('e')
+ o = post.get('o')
+ h = post.get('h')
+
+ if not all([u, e, o, h]):
+ exceptions.Warning('Invalid Request')
+
+ u = str(u)
+ e = str(e)
+ o = str(o)
+ h = str(h)
+
+ try:
+ user = check_admin_auth_login(http.request.env, u, e, o, h)
+
+ # this is mostly like session finalize() as we skip MFA
+ env = http.request.env(user=user)
+ user_context = dict(env['res.users'].context_get())
+
+ http.request.session.should_rotate = True
+ http.request.session.update({
+ 'login': user.login,
+ 'uid': user.id,
+ 'context': user_context,
+ 'session_token': env.user._compute_session_token(http.request.session.sid),
+ })
+
+ return http.request.redirect('/my/home')
+ except (exceptions.Warning, ) as e:
+ return http.Response(e.message, status=400)
diff --git a/auth_admin/i18n/es.po b/auth_admin/i18n/es.po
new file mode 100644
index 00000000..f9c8f1c1
--- /dev/null
+++ b/auth_admin/i18n/es.po
@@ -0,0 +1,52 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * auth_admin
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 15.0+e\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2021-10-11 22:01+0000\n"
+"PO-Revision-Date: 2021-10-11 22:01+0000\n"
+"Last-Translator: \n"
+"Language-Team: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: \n"
+
+#. module: auth_admin
+#: model:ir.model.fields,field_description:auth_admin.field_portal_wizard_user__force_login_url
+msgid "Force Login URL"
+msgstr "Forzar URL de Login"
+
+#. module: auth_admin
+#: model_terms:ir.ui.view,arch_db:auth_admin.auth_admin_view_users_tree
+msgid "Generate Login"
+msgstr "Generar Login"
+
+#. module: auth_admin
+#: model_terms:ir.ui.view,arch_db:auth_admin.portal_wizard
+msgid "Generate Login URL"
+msgstr "Generar URL de Login"
+
+#. module: auth_admin
+#: model:ir.model,name:auth_admin.model_portal_wizard
+msgid "Grant Portal Access"
+msgstr "Otorgar Acceso al Portal "
+
+#. module: auth_admin
+#: code:addons/auth_admin/wizard/portal_wizard.py:0
+#, python-format
+msgid "Portal Access Management"
+msgstr "Gestionar Acceso al Portal"
+
+#. module: auth_admin
+#: model:ir.model,name:auth_admin.model_portal_wizard_user
+msgid "Portal User Config"
+msgstr "Configuración del Usuario de Portal"
+
+#. module: auth_admin
+#: model:ir.model,name:auth_admin.model_res_users
+msgid "Users"
+msgstr "Usuarios"
diff --git a/auth_admin/models/__init__.py b/auth_admin/models/__init__.py
new file mode 100755
index 00000000..88351653
--- /dev/null
+++ b/auth_admin/models/__init__.py
@@ -0,0 +1 @@
+from . import res_users
diff --git a/auth_admin/models/res_users.py b/auth_admin/models/res_users.py
new file mode 100755
index 00000000..f1d1f48f
--- /dev/null
+++ b/auth_admin/models/res_users.py
@@ -0,0 +1,92 @@
+from odoo import models, api, exceptions
+from odoo.http import request
+from datetime import datetime
+from time import mktime
+import hmac
+from hashlib import sha256
+
+from logging import getLogger
+_logger = getLogger(__name__)
+
+
+def admin_auth_generate_login(env, user):
+ """
+ Generates a URL to allow the current user to login as the portal user.
+
+ :param env: Odoo environment
+ :param user: `res.users` in
+ :return:
+ """
+ if not env['res.partner'].check_access_rights('write'):
+ return None
+ u = str(user.id)
+ now = datetime.utcnow()
+ fifteen = int(mktime(now.timetuple())) + (15 * 60)
+ e = str(fifteen)
+ o = str(env.uid)
+
+ config = env['ir.config_parameter'].sudo()
+ key = str(config.search([('key', '=', 'database.secret')], limit=1).value)
+ h = hmac.new(key.encode(), (u + e + o).encode(), sha256)
+
+ base_url = str(config.search([('key', '=', 'web.base.url')], limit=1).value)
+
+ _logger.warning('login url for user id: ' + u + ' original user id: ' + o)
+
+ return base_url + '/auth_admin?u=' + u + '&e=' + e + '&o=' + o + '&h=' + h.hexdigest()
+
+
+def check_admin_auth_login(env, u_user_id, e_expires, o_org_user_id, hash_):
+ """
+ Checks that the parameters are valid and that the user exists.
+
+ :param env: Odoo environment
+ :param u_user_id: Desired user id to login as.
+ :param e_expires: Expiration timestamp
+ :param o_org_user_id: Original user id.
+ :param hash_: HMAC generated hash
+ :return: `res.users`
+ """
+
+ now = datetime.utcnow()
+ now = int(mktime(now.timetuple()))
+ fifteen = now + (15 * 60)
+
+ config = env['ir.config_parameter'].sudo()
+ key = str(config.search([('key', '=', 'database.secret')], limit=1).value)
+
+ myh = hmac.new(key.encode(), str(str(u_user_id) + str(e_expires) + str(o_org_user_id)).encode(), sha256)
+
+ if not hmac.compare_digest(hash_, myh.hexdigest()):
+ raise exceptions.AccessDenied('Invalid Request')
+
+ if not (now <= int(e_expires) <= fifteen):
+ raise exceptions.AccessDenied('Expired')
+
+ user = env['res.users'].sudo().search([('id', '=', int(u_user_id))], limit=1)
+ if not user.id:
+ raise exceptions.AccessDenied('Invalid User')
+ return user
+
+
+class ResUsers(models.Model):
+ _inherit = 'res.users'
+
+ def admin_auth_generate_login(self):
+ self.ensure_one()
+
+ login_url = admin_auth_generate_login(self.env, self)
+ if login_url:
+ raise exceptions.UserError(login_url)
+
+ return False
+
+ def _check_credentials(self, password, env):
+ try:
+ return super(ResUsers, self)._check_credentials(password, env)
+ except exceptions.AccessDenied:
+ if request and hasattr(request, 'session') and request.session.get('auth_admin'):
+ _logger.warning('_check_credentials for user id: ' + \
+ str(request.session.uid) + ' original user id: ' + str(request.session.auth_admin))
+ else:
+ raise
diff --git a/auth_admin/views/res_users.xml b/auth_admin/views/res_users.xml
new file mode 100755
index 00000000..e91821c7
--- /dev/null
+++ b/auth_admin/views/res_users.xml
@@ -0,0 +1,16 @@
+
+
+
+ auth_admin.res.users.tree
+ res.users
+
+
+
+
+
+
+
+
+
diff --git a/auth_admin/wizard/__init__.py b/auth_admin/wizard/__init__.py
new file mode 100755
index 00000000..8bff21fa
--- /dev/null
+++ b/auth_admin/wizard/__init__.py
@@ -0,0 +1 @@
+from . import portal_wizard
diff --git a/auth_admin/wizard/portal_wizard.py b/auth_admin/wizard/portal_wizard.py
new file mode 100755
index 00000000..ed0b5cd0
--- /dev/null
+++ b/auth_admin/wizard/portal_wizard.py
@@ -0,0 +1,39 @@
+from odoo import api, fields, models, _
+from ..models.res_users import admin_auth_generate_login
+
+
+class PortalWizard(models.TransientModel):
+ _inherit = 'portal.wizard'
+
+ def admin_auth_generate_login(self):
+ self.ensure_one()
+ self.user_ids.admin_auth_generate_login()
+ return {
+ "type": "ir.actions.act_window",
+ "res_model": self._name,
+ "views": [[False, "form"]],
+ "res_id": self.id,
+ "target": "new",
+ }
+
+
+class PortalWizardUser(models.TransientModel):
+ _inherit = 'portal.wizard.user'
+
+ force_login_url = fields.Char(string='Force Login URL')
+
+ def admin_auth_generate_login(self):
+ ir_model_access = self.env['ir.model.access']
+ for row in self.filtered(lambda r: r.is_portal):
+ user = row.partner_id.user_ids[0] if row.partner_id.user_ids else None
+ if ir_model_access.check('res.partner', mode='unlink') and user:
+ row.force_login_url = admin_auth_generate_login(self.env, user)
+ self.filtered(lambda r: not r.is_portal).update({'force_login_url': ''})
+ return {
+ 'name': _('Portal Access Management'),
+ 'type': 'ir.actions.act_window',
+ 'view_mode': 'form',
+ 'res_model': 'portal.wizard',
+ 'res_id': self.wizard_id.id,
+ 'target': 'new',
+ }
diff --git a/auth_admin/wizard/portal_wizard_views.xml b/auth_admin/wizard/portal_wizard_views.xml
new file mode 100755
index 00000000..8a401aa0
--- /dev/null
+++ b/auth_admin/wizard/portal_wizard_views.xml
@@ -0,0 +1,18 @@
+
+
+
+
+ Portal Access Management - Auth Admin
+ portal.wizard
+
+
+
+
+
+
+
+
+
+
+
+